update dev environment

This commit is contained in:
Julian Pawlowski 2025-11-03 15:54:01 +00:00
parent 5ee780da87
commit 6040a19136
32 changed files with 479 additions and 173 deletions

View file

@ -2,6 +2,7 @@
"name": "jpawlowski/hass.tibber_prices",
"image": "mcr.microsoft.com/devcontainers/python:3.13",
"postCreateCommand": "scripts/setup",
"postStartCommand": "scripts/motd",
"containerEnv": {
"PYTHONASYNCIODEBUG": "1"
},
@ -90,4 +91,4 @@
]
}
}
}
}

View file

@ -1,16 +1,31 @@
---
name: "Bug report"
description: "Report a bug with the integration"
description: "Report a bug with the custom integration"
labels: ["bug"]
body:
- type: markdown
attributes:
value: Before you open a new issue, search through the existing issues to see if others have had the same problem.
- type: input
attributes:
label: "Home Assistant version"
description: "The version of Home Assistant you are using"
placeholder: "2025.1.0"
validations:
required: true
- type: input
attributes:
label: "Integration version"
description: "The version of this custom integration you are using"
placeholder: "1.0.0"
validations:
required: false
- type: textarea
attributes:
label: "System Health details"
description: "Paste the data from the System Health card in Home Assistant (https://www.home-assistant.io/more-info/system-health#github-issues)"
validations:
required: true
required: false
- type: checkboxes
attributes:
label: Checklist
@ -52,3 +67,5 @@ body:
attributes:
label: "Diagnostics dump"
description: "Drag the diagnostics dump file here. (see https://www.home-assistant.io/integrations/diagnostics/ for info)"
validations:
required: false

View file

@ -1,6 +1,7 @@
---
name: "Feature request"
description: "Suggest an idea for this project"
description: "Suggest an idea for this custom integration"
labels: ["Feature request"]
body:
- type: markdown
attributes:

View file

@ -0,0 +1,34 @@
## Description
<!-- Describe your changes in detail -->
## Related Issue
<!-- If this PR fixes an issue, please link to the issue here -->
Fixes #(issue)
## Type of Change
<!-- Please delete options that are not relevant -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update
- [ ] Code quality improvements
## Checklist
- [ ] I have read the [CONTRIBUTING](CONTRIBUTING.md) document
- [ ] My code follows the code style of this project (run `scripts/lint`)
- [ ] I have updated the documentation accordingly
- [ ] I have updated the translations if needed
## Testing
<!-- Please describe how you tested your changes -->
## Screenshots (if applicable)
<!-- Add screenshots to help explain your changes -->

View file

@ -22,13 +22,14 @@ jobs:
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.13"
cache: "pip"
- name: Install uv
uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0
with:
version: "0.9.3"
- name: Install requirements
run: python3 -m pip install -r requirements.txt
run: scripts/bootstrap
- name: Lint
run: python3 -m ruff check .
- name: Format
run: python3 -m ruff format . --check
- name: Lint check
run: scripts/lint-check

View file

@ -3,7 +3,7 @@ name: Validate
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
- cron: "0 0 * * *"
push:
branches:
- main
@ -32,5 +32,5 @@ jobs:
uses: hacs/action@d556e736723344f83838d08488c983a15381059a # 22.5.0
with:
category: integration
# Remove this 'ignore' key when you have added brand images for your integration to https://github.com/home-assistant/brands
# Remove this 'ignore' key when you have added brand images for your custom integration to https://github.com/home-assistant/brands
ignore: brands

18
.gitignore vendored
View file

@ -4,6 +4,7 @@ __pycache__
*.egg-info
*/build/*
*/dist/*
.venv
# misc
@ -11,8 +12,23 @@ __pycache__
.vscode
coverage.xml
.ruff_cache
uv.lock
# Home Assistant configuration
config/*
!config/configuration.yaml
!config/configuration.yaml
# Home Assistant database and logs
*.db
*.db-shm
*.db-wal
*.log
home-assistant.log*
home-assistant_v2.db*
# Temporary files
*.tmp
*.temp
.DS_Store
Thumbs.db

View file

@ -1,31 +0,0 @@
# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml
target-version = "py313"
line-length = 120
[lint]
select = [
"ALL",
]
ignore = [
"ANN101", # Missing type annotation for `self` in method
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed
"D203", # no-blank-line-before-class (incompatible with formatter)
"D212", # multi-line-summary-first-line (incompatible with formatter)
"COM812", # incompatible with formatter
"ISC001", # incompatible with formatter
]
[lint.flake8-pytest-style]
fixture-parentheses = false
[lint.isort]
force-single-line = false
known-first-party = ["custom_components", "homeassistant"]
[lint.pyupgrade]
keep-runtime-typing = true
[lint.mccabe]
max-complexity = 25

View file

@ -1,61 +1,62 @@
# Contribution guidelines
Contributing to this project should be as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features
## Github is used for everything
Github is used to host code, to track issues and feature requests, as well as accept pull requests.
Pull requests are the best way to propose changes to the codebase.
1. Fork the repo and create your branch from `main`.
2. If you've changed something, update the documentation.
3. Make sure your code lints (using `scripts/lint`).
4. Test you contribution.
5. Issue that pull request!
## Any contributions you make will be under the MIT Software License
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
## Report bugs using Github's [issues](../../issues)
GitHub issues are used to track public bugs.
Report a bug by [opening a new issue](../../issues/new/choose); it's that easy!
## Write bug reports with detail, background, and sample code
**Great Bug Reports** tend to have:
- A quick summary and/or background
- Steps to reproduce
- Be specific!
- Give sample code if you can.
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
People *love* thorough bug reports. I'm not even kidding.
## Use a Consistent Coding Style
Use [black](https://github.com/ambv/black) to make sure the code follows the style.
## Test your code modification
This custom component is based on [integration_blueprint template](https://github.com/ludeeus/integration_blueprint).
It comes with development environment in a container, easy to launch
if you use Visual Studio Code. With this container you will have a stand alone
Home Assistant instance running and already configured with the included
[`configuration.yaml`](./config/configuration.yaml)
file.
## License
By contributing, you agree that your contributions will be licensed under its MIT License.
# Contribution guidelines
Contributing to this project should be as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features
## Github is used for everything
Github is used to host code, to track issues and feature requests, as well as accept pull requests.
Pull requests are the best way to propose changes to the codebase.
1. Fork the repo and create your branch from `main`.
2. Run `scripts/bootstrap` to install dependencies and pre-commit hooks.
3. If you've changed something, update the documentation.
4. Make sure your code lints (using `scripts/lint`).
5. Test your contribution.
6. Issue that pull request!
## Any contributions you make will be under the MIT Software License
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
## Report bugs using Github's [issues](../../issues)
GitHub issues are used to track public bugs.
Report a bug by [opening a new issue](../../issues/new/choose); it's that easy!
## Write bug reports with detail, background, and sample code
**Great Bug Reports** tend to have:
- A quick summary and/or background
- Steps to reproduce
- Be specific!
- Give sample code if you can.
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
People *love* thorough bug reports. I'm not even kidding.
## Use a Consistent Coding Style
Use [Ruff](https://github.com/astral-sh/ruff) for linting and formatting. Run `scripts/lint` to format your code before submitting.
## Test your code modification
This custom component is based on [integration_blueprint template](https://github.com/ludeeus/integration_blueprint).
It comes with development environment in a container, easy to launch
if you use Visual Studio Code. With this container you will have a stand alone
Home Assistant instance running and already configured with the included
[`configuration.yaml`](./config/configuration.yaml)
file.
## License
By contributing, you agree that your contributions will be licensed under its MIT License.

View file

@ -12,6 +12,7 @@ script:
scene:
# https://www.home-assistant.io/integrations/logger/
logger:
default: info
logs:

View file

@ -16,7 +16,12 @@ from homeassistant.helpers.storage import Store
from homeassistant.loader import async_get_loaded_integration
from .api import TibberPricesApiClient
from .const import DOMAIN, LOGGER, async_load_standard_translations, async_load_translations
from .const import (
DOMAIN,
LOGGER,
async_load_standard_translations,
async_load_translations,
)
from .coordinator import STORAGE_VERSION, TibberPricesDataUpdateCoordinator
from .data import TibberPricesData
from .services import async_setup_services
@ -88,7 +93,12 @@ async def async_unload_entry(
# Unregister services if this was the last config entry
if not hass.config_entries.async_entries(DOMAIN):
for service in ["get_price", "get_apexcharts_data", "get_apexcharts_yaml", "refresh_user_data"]:
for service in [
"get_price",
"get_apexcharts_data",
"get_apexcharts_yaml",
"refresh_user_data",
]:
if hass.services.has_service(DOMAIN, service):
hass.services.async_remove(DOMAIN, service)

View file

@ -114,7 +114,11 @@ async def _verify_graphql_response(response_json: dict, query_type: QueryType) -
if error_code in ["RATE_LIMITED", "TOO_MANY_REQUESTS"]:
# Some GraphQL APIs return rate limit info in extensions
retry_after = extensions.get("retryAfter", "unknown")
_LOGGER.warning("Tibber API rate limited via GraphQL: %s (retry after %s)", message, retry_after)
_LOGGER.warning(
"Tibber API rate limited via GraphQL: %s (retry after %s)",
message,
retry_after,
)
raise TibberPricesApiClientError(
TibberPricesApiClientError.RATE_LIMIT_ERROR.format(retry_after=retry_after)
)
@ -178,7 +182,10 @@ def _is_data_empty(data: dict, query_type: str) -> bool:
)
is_empty = not has_user_id or not has_homes
_LOGGER.debug(
"Viewer check - has_user_id: %s, has_homes: %s, is_empty: %s", has_user_id, has_homes, is_empty
"Viewer check - has_user_id: %s, has_homes: %s, is_empty: %s",
has_user_id,
has_homes,
is_empty,
)
elif query_type == "price_info":
@ -617,7 +624,8 @@ class TibberPricesApiClient:
except TimeoutError as error:
_LOGGER.exception(
"Request timeout after %d seconds - slow network or server overload", self._request_timeout
"Request timeout after %d seconds - slow network or server overload",
self._request_timeout,
)
raise TibberPricesApiClientCommunicationError(
TibberPricesApiClientCommunicationError.TIMEOUT_ERROR.format(exception=str(error))
@ -714,7 +722,13 @@ class TibberPricesApiClient:
return False, 0
# Non-retryable errors - authentication and permission issues
if isinstance(error, (TibberPricesApiClientAuthenticationError, TibberPricesApiClientPermissionError)):
if isinstance(
error,
(
TibberPricesApiClientAuthenticationError,
TibberPricesApiClientPermissionError,
),
):
return False, 0
# Handle API-specific errors

View file

@ -585,7 +585,11 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
# Add basic description
description = await async_get_entity_description(
self.hass, "binary_sensor", self.entity_description.translation_key, language, "description"
self.hass,
"binary_sensor",
self.entity_description.translation_key,
language,
"description",
)
if description:
attributes["description"] = description
@ -611,7 +615,11 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
# Add usage tips if available
usage_tips = await async_get_entity_description(
self.hass, "binary_sensor", self.entity_description.translation_key, language, "usage_tips"
self.hass,
"binary_sensor",
self.entity_description.translation_key,
language,
"usage_tips",
)
if usage_tips:
attributes["usage_tips"] = usage_tips
@ -649,7 +657,10 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
# Add basic description from cache
description = get_entity_description(
"binary_sensor", self.entity_description.translation_key, language, "description"
"binary_sensor",
self.entity_description.translation_key,
language,
"description",
)
if description:
attributes["description"] = description
@ -664,14 +675,20 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
if extended_descriptions:
# Add long description if available in cache
long_desc = get_entity_description(
"binary_sensor", self.entity_description.translation_key, language, "long_description"
"binary_sensor",
self.entity_description.translation_key,
language,
"long_description",
)
if long_desc:
attributes["long_description"] = long_desc
# Add usage tips if available in cache
usage_tips = get_entity_description(
"binary_sensor", self.entity_description.translation_key, language, "usage_tips"
"binary_sensor",
self.entity_description.translation_key,
language,
"usage_tips",
)
if usage_tips:
attributes["usage_tips"] = usage_tips

View file

@ -71,7 +71,10 @@ class TibberPricesFlowHandler(ConfigFlow, domain=DOMAIN):
@classmethod
@callback
def async_get_supported_subentry_types(cls, config_entry: ConfigEntry) -> dict[str, type[ConfigSubentryFlow]]: # noqa: ARG003
def async_get_supported_subentry_types(
cls,
config_entry: ConfigEntry, # noqa: ARG003
) -> dict[str, type[ConfigSubentryFlow]]:
"""Return subentries supported by this integration."""
return {"home": TibberPricesSubentryFlowHandler}

View file

@ -30,7 +30,10 @@ from .const import (
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
DOMAIN,
)
from .price_utils import enrich_price_info_with_differences, find_price_data_for_interval
from .price_utils import (
enrich_price_info_with_differences,
find_price_data_for_interval,
)
_LOGGER = logging.getLogger(__name__)
@ -377,7 +380,10 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._cached_user_data = user_data
self._last_user_update = current_time
_LOGGER.debug("User data updated successfully")
except (TibberPricesApiClientError, TibberPricesApiClientCommunicationError) as ex:
except (
TibberPricesApiClientError,
TibberPricesApiClientCommunicationError,
) as ex:
_LOGGER.warning("Failed to update user data: %s", ex)
@callback

View file

@ -1,38 +1,47 @@
"""Diagnostics support for tibber_prices."""
"""
Diagnostics support for tibber_prices.
Learn more about diagnostics:
https://developers.home-assistant.io/docs/core/integration_diagnostics
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from homeassistant.components.diagnostics import async_redact_data
from .const import DOMAIN
if TYPE_CHECKING:
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
TO_REDACT = {"access_token"}
from .data import TibberPricesConfigEntry
async def async_get_config_entry_diagnostics(hass: HomeAssistant, entry: ConfigEntry) -> dict[str, Any]:
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, # noqa: ARG001
entry: TibberPricesConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id].coordinator
coordinator = entry.runtime_data.coordinator
return {
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
"coordinator_data": coordinator.data,
"last_update_success": coordinator.last_update_success,
"update_timestamps": {
"price": coordinator.last_price_update.isoformat() if coordinator.last_price_update else None,
"hourly_rating": coordinator.last_rating_update_hourly.isoformat()
if coordinator.last_rating_update_hourly
else None,
"daily_rating": coordinator.last_rating_update_daily.isoformat()
if coordinator.last_rating_update_daily
else None,
"monthly_rating": coordinator.last_rating_update_monthly.isoformat()
if coordinator.last_rating_update_monthly
else None,
"entry": {
"entry_id": entry.entry_id,
"version": entry.version,
"minor_version": entry.minor_version,
"domain": entry.domain,
"title": entry.title,
"state": str(entry.state),
},
"coordinator": {
"last_update_success": coordinator.last_update_success,
"update_interval": str(coordinator.update_interval),
"is_main_entry": coordinator.is_main_entry(),
"data": coordinator.data,
"update_timestamps": {
"price": coordinator._last_price_update.isoformat() if coordinator._last_price_update else None, # noqa: SLF001
"user": coordinator._last_user_update.isoformat() if coordinator._last_user_update else None, # noqa: SLF001
},
},
"error": {
"last_exception": str(coordinator.last_exception) if coordinator.last_exception else None,
},
}

View file

@ -77,7 +77,12 @@ class TibberPricesEntity(CoordinatorEntity[TibberPricesDataUpdateCoordinator]):
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, coordinator.config_entry.unique_id or coordinator.config_entry.entry_id)},
identifiers={
(
DOMAIN,
coordinator.config_entry.unique_id or coordinator.config_entry.entry_id,
)
},
name=home_name,
manufacturer="Tibber",
model=translated_model,

View file

@ -10,7 +10,13 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import CURRENCY_EURO, PERCENTAGE, EntityCategory, UnitOfPower, UnitOfTime
from homeassistant.const import (
CURRENCY_EURO,
PERCENTAGE,
EntityCategory,
UnitOfPower,
UnitOfTime,
)
from homeassistant.util import dt as dt_util
from .const import (
@ -239,10 +245,14 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
"highest_price_today": lambda: self._get_statistics_value(stat_func=max, in_euro=False, decimals=2),
"highest_price_today_eur": lambda: self._get_statistics_value(stat_func=max, in_euro=True, decimals=4),
"average_price_today": lambda: self._get_statistics_value(
stat_func=lambda prices: sum(prices) / len(prices), in_euro=False, decimals=2
stat_func=lambda prices: sum(prices) / len(prices),
in_euro=False,
decimals=2,
),
"average_price_today_eur": lambda: self._get_statistics_value(
stat_func=lambda prices: sum(prices) / len(prices), in_euro=True, decimals=4
stat_func=lambda prices: sum(prices) / len(prices),
in_euro=True,
decimals=4,
),
# Rating sensors
"price_rating": lambda: self._get_rating_value(rating_type="current"),
@ -357,7 +367,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
return None
def _get_statistics_value(
self, *, stat_func: Callable[[list[float]], float], in_euro: bool, decimals: int | None = None
self,
*,
stat_func: Callable[[list[float]], float],
in_euro: bool,
decimals: int | None = None,
) -> float | None:
"""
Handle statistics sensor values using the provided statistical function.
@ -794,7 +808,10 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
if self.entity_description.key == "price_level" and current_interval_data and "level" in current_interval_data:
self._add_price_level_attributes(attributes, current_interval_data["level"])
if self.entity_description.key in ["next_interval_price", "next_interval_price_eur"]:
if self.entity_description.key in [
"next_interval_price",
"next_interval_price_eur",
]:
price_info = self.coordinator.data.get("priceInfo", {})
now = dt_util.now()
next_interval_time = now + timedelta(minutes=MINUTES_PER_INTERVAL)
@ -815,7 +832,12 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
attributes["level_id"] = level
def _find_price_timestamp(
self, attributes: dict, price_info: Any, day_key: str, target_hour: int, target_date: date
self,
attributes: dict,
price_info: Any,
day_key: str,
target_hour: int,
target_date: date,
) -> None:
"""Find a price timestamp for a specific hour and date."""
for price_data in price_info.get(day_key, []):
@ -842,7 +864,12 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
if hasattr(self, "_last_rating_level") and self._last_rating_level is not None:
attributes["level_id"] = self._last_rating_level
attributes["level_value"] = PRICE_RATING_MAPPING.get(self._last_rating_level, self._last_rating_level)
elif key in ["lowest_price_today", "lowest_price_today_eur", "highest_price_today", "highest_price_today_eur"]:
elif key in [
"lowest_price_today",
"lowest_price_today_eur",
"highest_price_today",
"highest_price_today_eur",
]:
# Use the timestamp from the interval that has the extreme price (already stored during value calculation)
if hasattr(self, "_last_extreme_interval") and self._last_extreme_interval:
attributes["timestamp"] = self._last_extreme_interval.get("startsAt")

View file

@ -336,7 +336,10 @@ def _get_entry_and_data(hass: HomeAssistant, entry_id: str) -> tuple[Any, Any, d
"""Validate entry and extract coordinator and data."""
if not entry_id:
raise ServiceValidationError(translation_domain=DOMAIN, translation_key="missing_entry_id")
entry = next((e for e in hass.config_entries.async_entries(DOMAIN) if e.entry_id == entry_id), None)
entry = next(
(e for e in hass.config_entries.async_entries(DOMAIN) if e.entry_id == entry_id),
None,
)
if not entry or not hasattr(entry, "runtime_data") or not entry.runtime_data:
raise ServiceValidationError(translation_domain=DOMAIN, translation_key="invalid_entry_id")
coordinator = entry.runtime_data.coordinator

View file

@ -1,10 +1,37 @@
# pyproject.toml
[build-system]
requires = ["setuptools==78.1.1"]
build-backend = "setuptools.build_meta"
[tool.black]
[project]
name = "tibber_prices"
version = "0.1.0"
requires-python = ">=3.13"
[tool.ruff]
# Based on https://github.com/home-assistant/core/blob/dev/pyproject.toml
target-version = "py313"
line-length = 120
target-version = ['py313']
skip-string-normalization = false
[tool.isort]
profile = "black"
line_length = 120
[tool.ruff.lint]
select = ["ALL"]
ignore = [
# "ANN101", # Missing type annotation for `self` in method
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed
"D203", # no-blank-line-before-class (incompatible with formatter)
"D212", # multi-line-summary-first-line (incompatible with formatter)
"COM812", # incompatible with formatter
"ISC001", # incompatible with formatter
]
[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
[tool.ruff.lint.pyupgrade]
keep-runtime-typing = true
[tool.ruff.lint.mccabe]
max-complexity = 25
[tool.ruff.lint.isort]
force-single-line = false
known-first-party = ["custom_components", "homeassistant"]

View file

@ -1,5 +1,6 @@
colorlog>=6.9.0,<7.0.0
homeassistant>=2025.10.0,<2025.11.0
colorlog>=6.10.1,<6.11.0
homeassistant>=2025.6.0,<2025.7.0
pytest-homeassistant-custom-component>=0.13.0,<0.14.0
pip>=21.3.1
ruff>=0.11.6,<0.15.0
pre-commit>=4.3.0,<4.4.0
ruff>=0.14.1,<0.15.0

25
scripts/bootstrap Executable file
View file

@ -0,0 +1,25 @@
#!/bin/sh
# script/bootstrap: Install/update all dependencies required to run the project
set -e
cd "$(dirname "$0")/.."
echo "==> Updating system packages..."
sudo apt-get update
sudo apt-get upgrade -y
echo "==> Checking for uv..."
if ! command -v uv >/dev/null 2>&1; then
echo "UV not found, installing..."
pipx install uv
fi
echo "==> Installing dependencies..."
python3 -m pip install --requirement requirements.txt
echo "==> Installing pre-commit hooks..."
pre-commit install
echo "==> Bootstrap completed!"

39
scripts/help Executable file
View file

@ -0,0 +1,39 @@
#!/usr/bin/env bash
# script/help: Display information about available scripts.
set -e
cd "$(dirname "$0")/.."
REPO_NAME=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || basename "$(pwd)")
printf "\033[1m%s\033[36m %s\033[32m %s\033[0m \n\n" "Development environment for" "$REPO_NAME" ""
echo "Available scripts:"
echo ""
find scripts -type f -perm -111 -print0 | sort -z | while IFS= read -r -d '' script; do
script_name=$(basename "$script")
description=$(awk -v prefix="# script/$script_name:" '
BEGIN {desc=""}
$0 ~ prefix {
line = $0
sub(prefix, "", line)
sub(/^# */, "", line)
desc = desc (desc ? " " : "") line
next
}
desc != "" {exit}
END {print desc}
' "$script")
if [ -z "$description" ]; then
description="No description available"
fi
if [ ${#description} -gt 60 ]; then
description=$(echo "$description" | cut -c1-57)...
fi
printf " \033[36m %-25s\033[0m %s\n" "scripts/$script_name" "$description"
done
echo ""

View file

@ -1,8 +1,17 @@
#!/usr/bin/env bash
#!/bin/sh
# script/lint: Run linting tools and apply formatting
set -e
cd "$(dirname "$0")/.."
ruff format .
ruff check . --fix
echo "==> Running linting tools..."
echo "==> Running Ruff format..."
uv run ruff format .
echo "==> Running Ruff check..."
uv run ruff check . --fix
echo "==> Linting completed!"

15
scripts/lint-check Executable file
View file

@ -0,0 +1,15 @@
#!/bin/sh
# script/lint-check: Check linting without making changes.
set -e
cd "$(dirname "$0")/.."
echo "==> Checking code format..."
uv run ruff format . --check
echo "==> Checking code with Ruff..."
uv run ruff check .
echo "==> Linting check completed!"

24
scripts/motd Executable file
View file

@ -0,0 +1,24 @@
#/bin/sh
# script/motd: Display Message of the Day for development environment
set -e
cd "$(dirname "$0")/.."
REPO_NAME=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || basename "$(pwd)")
echo ""
echo "🎉 Welcome to the $REPO_NAME development environment!"
echo ""
echo "📂 Project: $(pwd)"
echo "🌿 Git branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'Not a git repo')"
echo "🐍 Python: $(python3 --version 2>/dev/null || echo 'Not available')"
echo ""
scripts/help
echo ""
echo "💡 Tip: Run 'scripts/develop' to start Home Assistant with your custom integration."
echo " Access it at http://localhost:8123"
echo ""

View file

@ -4,4 +4,7 @@ set -e
cd "$(dirname "$0")/.."
python3 -m pip install --requirement requirements.txt
#python3 -m pip install --requirement requirements.txt
scripts/bootstrap
echo "==> Project is now ready to go!"

11
scripts/update Executable file
View file

@ -0,0 +1,11 @@
#!/bin/sh
# script/update: Update project after a fresh pull
set -e
cd "$(dirname "$0")/.."
scripts/bootstrap
echo "==> Update completed!"

View file

@ -4,7 +4,9 @@ from unittest.mock import AsyncMock, Mock, patch
import pytest
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
from custom_components.tibber_prices.coordinator import (
TibberPricesDataUpdateCoordinator,
)
class TestBasicCoordinator:

View file

@ -9,7 +9,9 @@ import pytest
from custom_components.tibber_prices.api import TibberPricesApiClientCommunicationError
from custom_components.tibber_prices.const import DOMAIN
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
from custom_components.tibber_prices.coordinator import (
TibberPricesDataUpdateCoordinator,
)
class TestEnhancedCoordinator:
@ -63,7 +65,10 @@ class TestEnhancedCoordinator:
"custom_components.tibber_prices.coordinator.aiohttp_client.async_get_clientsession",
return_value=mock_session,
),
patch("custom_components.tibber_prices.coordinator.Store", return_value=mock_store),
patch(
"custom_components.tibber_prices.coordinator.Store",
return_value=mock_store,
),
):
coordinator = TibberPricesDataUpdateCoordinator(
hass=mock_hass,
@ -88,7 +93,10 @@ class TestEnhancedCoordinator:
"custom_components.tibber_prices.coordinator.aiohttp_client.async_get_clientsession",
return_value=mock_session,
),
patch("custom_components.tibber_prices.coordinator.Store", return_value=mock_store),
patch(
"custom_components.tibber_prices.coordinator.Store",
return_value=mock_store,
),
):
main_coordinator = TibberPricesDataUpdateCoordinator(
hass=mock_hass,
@ -112,7 +120,10 @@ class TestEnhancedCoordinator:
"custom_components.tibber_prices.coordinator.aiohttp_client.async_get_clientsession",
return_value=mock_session,
),
patch("custom_components.tibber_prices.coordinator.Store", return_value=mock_store),
patch(
"custom_components.tibber_prices.coordinator.Store",
return_value=mock_store,
),
):
sub_coordinator = TibberPricesDataUpdateCoordinator(
hass=mock_hass,

View file

@ -5,7 +5,9 @@ from __future__ import annotations
from datetime import UTC, datetime, timedelta
from unittest.mock import Mock, patch
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
from custom_components.tibber_prices.coordinator import (
TibberPricesDataUpdateCoordinator,
)
# Constants for test validation
INTERVALS_PER_DAY = 96

View file

@ -2,7 +2,9 @@
from datetime import datetime, timedelta
from custom_components.tibber_prices.price_utils import enrich_price_info_with_differences
from custom_components.tibber_prices.price_utils import (
enrich_price_info_with_differences,
)
from homeassistant.util import dt as dt_util
# Constants for integration testing