mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
update dev environment
This commit is contained in:
parent
5ee780da87
commit
6040a19136
32 changed files with 479 additions and 173 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "jpawlowski/hass.tibber_prices",
|
"name": "jpawlowski/hass.tibber_prices",
|
||||||
"image": "mcr.microsoft.com/devcontainers/python:3.13",
|
"image": "mcr.microsoft.com/devcontainers/python:3.13",
|
||||||
"postCreateCommand": "scripts/setup",
|
"postCreateCommand": "scripts/setup",
|
||||||
|
"postStartCommand": "scripts/motd",
|
||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
"PYTHONASYNCIODEBUG": "1"
|
"PYTHONASYNCIODEBUG": "1"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
21
.github/ISSUE_TEMPLATE/bug.yml
vendored
21
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
|
@ -1,16 +1,31 @@
|
||||||
---
|
---
|
||||||
name: "Bug report"
|
name: "Bug report"
|
||||||
description: "Report a bug with the integration"
|
description: "Report a bug with the custom integration"
|
||||||
|
labels: ["bug"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: Before you open a new issue, search through the existing issues to see if others have had the same problem.
|
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
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: "System Health details"
|
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)"
|
description: "Paste the data from the System Health card in Home Assistant (https://www.home-assistant.io/more-info/system-health#github-issues)"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: false
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Checklist
|
label: Checklist
|
||||||
|
|
@ -52,3 +67,5 @@ body:
|
||||||
attributes:
|
attributes:
|
||||||
label: "Diagnostics dump"
|
label: "Diagnostics dump"
|
||||||
description: "Drag the diagnostics dump file here. (see https://www.home-assistant.io/integrations/diagnostics/ for info)"
|
description: "Drag the diagnostics dump file here. (see https://www.home-assistant.io/integrations/diagnostics/ for info)"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
|
||||||
3
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
3
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
name: "Feature request"
|
name: "Feature request"
|
||||||
description: "Suggest an idea for this project"
|
description: "Suggest an idea for this custom integration"
|
||||||
|
labels: ["Feature request"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|
|
||||||
34
.github/ISSUE_TEMPLATE/pull_request_template.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/pull_request_template.md
vendored
Normal 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 -->
|
||||||
15
.github/workflows/lint.yml
vendored
15
.github/workflows/lint.yml
vendored
|
|
@ -22,13 +22,14 @@ jobs:
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.13"
|
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
|
- name: Install requirements
|
||||||
run: python3 -m pip install -r requirements.txt
|
run: scripts/bootstrap
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint check
|
||||||
run: python3 -m ruff check .
|
run: scripts/lint-check
|
||||||
|
|
||||||
- name: Format
|
|
||||||
run: python3 -m ruff format . --check
|
|
||||||
|
|
|
||||||
4
.github/workflows/validate.yml
vendored
4
.github/workflows/validate.yml
vendored
|
|
@ -3,7 +3,7 @@ name: Validate
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * *"
|
- cron: "0 0 * * *"
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
@ -32,5 +32,5 @@ jobs:
|
||||||
uses: hacs/action@d556e736723344f83838d08488c983a15381059a # 22.5.0
|
uses: hacs/action@d556e736723344f83838d08488c983a15381059a # 22.5.0
|
||||||
with:
|
with:
|
||||||
category: integration
|
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
|
ignore: brands
|
||||||
|
|
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
|
|
@ -4,6 +4,7 @@ __pycache__
|
||||||
*.egg-info
|
*.egg-info
|
||||||
*/build/*
|
*/build/*
|
||||||
*/dist/*
|
*/dist/*
|
||||||
|
.venv
|
||||||
|
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
|
|
@ -11,8 +12,23 @@ __pycache__
|
||||||
.vscode
|
.vscode
|
||||||
coverage.xml
|
coverage.xml
|
||||||
.ruff_cache
|
.ruff_cache
|
||||||
|
uv.lock
|
||||||
|
|
||||||
|
|
||||||
# Home Assistant configuration
|
# Home Assistant configuration
|
||||||
config/*
|
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
|
||||||
|
|
|
||||||
31
.ruff.toml
31
.ruff.toml
|
|
@ -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
|
|
||||||
|
|
@ -14,10 +14,11 @@ Github is used to host code, to track issues and feature requests, as well as ac
|
||||||
Pull requests are the best way to propose changes to the codebase.
|
Pull requests are the best way to propose changes to the codebase.
|
||||||
|
|
||||||
1. Fork the repo and create your branch from `main`.
|
1. Fork the repo and create your branch from `main`.
|
||||||
2. If you've changed something, update the documentation.
|
2. Run `scripts/bootstrap` to install dependencies and pre-commit hooks.
|
||||||
3. Make sure your code lints (using `scripts/lint`).
|
3. If you've changed something, update the documentation.
|
||||||
4. Test you contribution.
|
4. Make sure your code lints (using `scripts/lint`).
|
||||||
5. Issue that pull request!
|
5. Test your contribution.
|
||||||
|
6. Issue that pull request!
|
||||||
|
|
||||||
## Any contributions you make will be under the MIT Software License
|
## Any contributions you make will be under the MIT Software License
|
||||||
|
|
||||||
|
|
@ -44,7 +45,7 @@ People *love* thorough bug reports. I'm not even kidding.
|
||||||
|
|
||||||
## Use a Consistent Coding Style
|
## Use a Consistent Coding Style
|
||||||
|
|
||||||
Use [black](https://github.com/ambv/black) to make sure the code follows the 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
|
## Test your code modification
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ script:
|
||||||
|
|
||||||
scene:
|
scene:
|
||||||
|
|
||||||
|
# https://www.home-assistant.io/integrations/logger/
|
||||||
logger:
|
logger:
|
||||||
default: info
|
default: info
|
||||||
logs:
|
logs:
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,12 @@ from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.loader import async_get_loaded_integration
|
from homeassistant.loader import async_get_loaded_integration
|
||||||
|
|
||||||
from .api import TibberPricesApiClient
|
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 .coordinator import STORAGE_VERSION, TibberPricesDataUpdateCoordinator
|
||||||
from .data import TibberPricesData
|
from .data import TibberPricesData
|
||||||
from .services import async_setup_services
|
from .services import async_setup_services
|
||||||
|
|
@ -88,7 +93,12 @@ async def async_unload_entry(
|
||||||
|
|
||||||
# Unregister services if this was the last config entry
|
# Unregister services if this was the last config entry
|
||||||
if not hass.config_entries.async_entries(DOMAIN):
|
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):
|
if hass.services.has_service(DOMAIN, service):
|
||||||
hass.services.async_remove(DOMAIN, service)
|
hass.services.async_remove(DOMAIN, service)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,11 @@ async def _verify_graphql_response(response_json: dict, query_type: QueryType) -
|
||||||
if error_code in ["RATE_LIMITED", "TOO_MANY_REQUESTS"]:
|
if error_code in ["RATE_LIMITED", "TOO_MANY_REQUESTS"]:
|
||||||
# Some GraphQL APIs return rate limit info in extensions
|
# Some GraphQL APIs return rate limit info in extensions
|
||||||
retry_after = extensions.get("retryAfter", "unknown")
|
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(
|
raise TibberPricesApiClientError(
|
||||||
TibberPricesApiClientError.RATE_LIMIT_ERROR.format(retry_after=retry_after)
|
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
|
is_empty = not has_user_id or not has_homes
|
||||||
_LOGGER.debug(
|
_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":
|
elif query_type == "price_info":
|
||||||
|
|
@ -617,7 +624,8 @@ class TibberPricesApiClient:
|
||||||
|
|
||||||
except TimeoutError as error:
|
except TimeoutError as error:
|
||||||
_LOGGER.exception(
|
_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(
|
raise TibberPricesApiClientCommunicationError(
|
||||||
TibberPricesApiClientCommunicationError.TIMEOUT_ERROR.format(exception=str(error))
|
TibberPricesApiClientCommunicationError.TIMEOUT_ERROR.format(exception=str(error))
|
||||||
|
|
@ -714,7 +722,13 @@ class TibberPricesApiClient:
|
||||||
return False, 0
|
return False, 0
|
||||||
|
|
||||||
# Non-retryable errors - authentication and permission issues
|
# Non-retryable errors - authentication and permission issues
|
||||||
if isinstance(error, (TibberPricesApiClientAuthenticationError, TibberPricesApiClientPermissionError)):
|
if isinstance(
|
||||||
|
error,
|
||||||
|
(
|
||||||
|
TibberPricesApiClientAuthenticationError,
|
||||||
|
TibberPricesApiClientPermissionError,
|
||||||
|
),
|
||||||
|
):
|
||||||
return False, 0
|
return False, 0
|
||||||
|
|
||||||
# Handle API-specific errors
|
# Handle API-specific errors
|
||||||
|
|
|
||||||
|
|
@ -585,7 +585,11 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
|
|
||||||
# Add basic description
|
# Add basic description
|
||||||
description = await async_get_entity_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:
|
if description:
|
||||||
attributes["description"] = description
|
attributes["description"] = description
|
||||||
|
|
@ -611,7 +615,11 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
|
|
||||||
# Add usage tips if available
|
# Add usage tips if available
|
||||||
usage_tips = await async_get_entity_description(
|
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:
|
if usage_tips:
|
||||||
attributes["usage_tips"] = usage_tips
|
attributes["usage_tips"] = usage_tips
|
||||||
|
|
@ -649,7 +657,10 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
|
|
||||||
# Add basic description from cache
|
# Add basic description from cache
|
||||||
description = get_entity_description(
|
description = get_entity_description(
|
||||||
"binary_sensor", self.entity_description.translation_key, language, "description"
|
"binary_sensor",
|
||||||
|
self.entity_description.translation_key,
|
||||||
|
language,
|
||||||
|
"description",
|
||||||
)
|
)
|
||||||
if description:
|
if description:
|
||||||
attributes["description"] = description
|
attributes["description"] = description
|
||||||
|
|
@ -664,14 +675,20 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
if extended_descriptions:
|
if extended_descriptions:
|
||||||
# Add long description if available in cache
|
# Add long description if available in cache
|
||||||
long_desc = get_entity_description(
|
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:
|
if long_desc:
|
||||||
attributes["long_description"] = long_desc
|
attributes["long_description"] = long_desc
|
||||||
|
|
||||||
# Add usage tips if available in cache
|
# Add usage tips if available in cache
|
||||||
usage_tips = get_entity_description(
|
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:
|
if usage_tips:
|
||||||
attributes["usage_tips"] = usage_tips
|
attributes["usage_tips"] = usage_tips
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,10 @@ class TibberPricesFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@callback
|
@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 subentries supported by this integration."""
|
||||||
return {"home": TibberPricesSubentryFlowHandler}
|
return {"home": TibberPricesSubentryFlowHandler}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,10 @@ from .const import (
|
||||||
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
||||||
DOMAIN,
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -377,7 +380,10 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
self._cached_user_data = user_data
|
self._cached_user_data = user_data
|
||||||
self._last_user_update = current_time
|
self._last_user_update = current_time
|
||||||
_LOGGER.debug("User data updated successfully")
|
_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)
|
_LOGGER.warning("Failed to update user data: %s", ex)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
|
||||||
|
|
@ -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 __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from homeassistant.components.diagnostics import async_redact_data
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
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."""
|
"""Return diagnostics for a config entry."""
|
||||||
coordinator = hass.data[DOMAIN][entry.entry_id].coordinator
|
coordinator = entry.runtime_data.coordinator
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
"entry": {
|
||||||
"coordinator_data": coordinator.data,
|
"entry_id": entry.entry_id,
|
||||||
"last_update_success": coordinator.last_update_success,
|
"version": entry.version,
|
||||||
"update_timestamps": {
|
"minor_version": entry.minor_version,
|
||||||
"price": coordinator.last_price_update.isoformat() if coordinator.last_price_update else None,
|
"domain": entry.domain,
|
||||||
"hourly_rating": coordinator.last_rating_update_hourly.isoformat()
|
"title": entry.title,
|
||||||
if coordinator.last_rating_update_hourly
|
"state": str(entry.state),
|
||||||
else None,
|
},
|
||||||
"daily_rating": coordinator.last_rating_update_daily.isoformat()
|
"coordinator": {
|
||||||
if coordinator.last_rating_update_daily
|
"last_update_success": coordinator.last_update_success,
|
||||||
else None,
|
"update_interval": str(coordinator.update_interval),
|
||||||
"monthly_rating": coordinator.last_rating_update_monthly.isoformat()
|
"is_main_entry": coordinator.is_main_entry(),
|
||||||
if coordinator.last_rating_update_monthly
|
"data": coordinator.data,
|
||||||
else None,
|
"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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,12 @@ class TibberPricesEntity(CoordinatorEntity[TibberPricesDataUpdateCoordinator]):
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
entry_type=DeviceEntryType.SERVICE,
|
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,
|
name=home_name,
|
||||||
manufacturer="Tibber",
|
manufacturer="Tibber",
|
||||||
model=translated_model,
|
model=translated_model,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,13 @@ from homeassistant.components.sensor import (
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
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 homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import (
|
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": 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),
|
"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(
|
"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(
|
"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
|
# Rating sensors
|
||||||
"price_rating": lambda: self._get_rating_value(rating_type="current"),
|
"price_rating": lambda: self._get_rating_value(rating_type="current"),
|
||||||
|
|
@ -357,7 +367,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_statistics_value(
|
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:
|
) -> float | None:
|
||||||
"""
|
"""
|
||||||
Handle statistics sensor values using the provided statistical function.
|
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:
|
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"])
|
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", {})
|
price_info = self.coordinator.data.get("priceInfo", {})
|
||||||
now = dt_util.now()
|
now = dt_util.now()
|
||||||
next_interval_time = now + timedelta(minutes=MINUTES_PER_INTERVAL)
|
next_interval_time = now + timedelta(minutes=MINUTES_PER_INTERVAL)
|
||||||
|
|
@ -815,7 +832,12 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
attributes["level_id"] = level
|
attributes["level_id"] = level
|
||||||
|
|
||||||
def _find_price_timestamp(
|
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:
|
) -> None:
|
||||||
"""Find a price timestamp for a specific hour and date."""
|
"""Find a price timestamp for a specific hour and date."""
|
||||||
for price_data in price_info.get(day_key, []):
|
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:
|
if hasattr(self, "_last_rating_level") and self._last_rating_level is not None:
|
||||||
attributes["level_id"] = self._last_rating_level
|
attributes["level_id"] = self._last_rating_level
|
||||||
attributes["level_value"] = PRICE_RATING_MAPPING.get(self._last_rating_level, 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)
|
# 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:
|
if hasattr(self, "_last_extreme_interval") and self._last_extreme_interval:
|
||||||
attributes["timestamp"] = self._last_extreme_interval.get("startsAt")
|
attributes["timestamp"] = self._last_extreme_interval.get("startsAt")
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
"""Validate entry and extract coordinator and data."""
|
||||||
if not entry_id:
|
if not entry_id:
|
||||||
raise ServiceValidationError(translation_domain=DOMAIN, translation_key="missing_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:
|
if not entry or not hasattr(entry, "runtime_data") or not entry.runtime_data:
|
||||||
raise ServiceValidationError(translation_domain=DOMAIN, translation_key="invalid_entry_id")
|
raise ServiceValidationError(translation_domain=DOMAIN, translation_key="invalid_entry_id")
|
||||||
coordinator = entry.runtime_data.coordinator
|
coordinator = entry.runtime_data.coordinator
|
||||||
|
|
|
||||||
|
|
@ -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
|
line-length = 120
|
||||||
target-version = ['py313']
|
|
||||||
skip-string-normalization = false
|
|
||||||
|
|
||||||
[tool.isort]
|
[tool.ruff.lint]
|
||||||
profile = "black"
|
select = ["ALL"]
|
||||||
line_length = 120
|
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"]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
colorlog>=6.9.0,<7.0.0
|
colorlog>=6.10.1,<6.11.0
|
||||||
homeassistant>=2025.10.0,<2025.11.0
|
homeassistant>=2025.6.0,<2025.7.0
|
||||||
pytest-homeassistant-custom-component>=0.13.0,<0.14.0
|
pytest-homeassistant-custom-component>=0.13.0,<0.14.0
|
||||||
pip>=21.3.1
|
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
25
scripts/bootstrap
Executable 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
39
scripts/help
Executable 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 ""
|
||||||
15
scripts/lint
15
scripts/lint
|
|
@ -1,8 +1,17 @@
|
||||||
#!/usr/bin/env bash
|
#!/bin/sh
|
||||||
|
|
||||||
|
# script/lint: Run linting tools and apply formatting
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
ruff format .
|
echo "==> Running linting tools..."
|
||||||
ruff check . --fix
|
|
||||||
|
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
15
scripts/lint-check
Executable 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
24
scripts/motd
Executable 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 ""
|
||||||
|
|
@ -4,4 +4,7 @@ set -e
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
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
11
scripts/update
Executable 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!"
|
||||||
|
|
@ -4,7 +4,9 @@ from unittest.mock import AsyncMock, Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
|
from custom_components.tibber_prices.coordinator import (
|
||||||
|
TibberPricesDataUpdateCoordinator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestBasicCoordinator:
|
class TestBasicCoordinator:
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.api import TibberPricesApiClientCommunicationError
|
from custom_components.tibber_prices.api import TibberPricesApiClientCommunicationError
|
||||||
from custom_components.tibber_prices.const import DOMAIN
|
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:
|
class TestEnhancedCoordinator:
|
||||||
|
|
@ -63,7 +65,10 @@ class TestEnhancedCoordinator:
|
||||||
"custom_components.tibber_prices.coordinator.aiohttp_client.async_get_clientsession",
|
"custom_components.tibber_prices.coordinator.aiohttp_client.async_get_clientsession",
|
||||||
return_value=mock_session,
|
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(
|
coordinator = TibberPricesDataUpdateCoordinator(
|
||||||
hass=mock_hass,
|
hass=mock_hass,
|
||||||
|
|
@ -88,7 +93,10 @@ class TestEnhancedCoordinator:
|
||||||
"custom_components.tibber_prices.coordinator.aiohttp_client.async_get_clientsession",
|
"custom_components.tibber_prices.coordinator.aiohttp_client.async_get_clientsession",
|
||||||
return_value=mock_session,
|
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(
|
main_coordinator = TibberPricesDataUpdateCoordinator(
|
||||||
hass=mock_hass,
|
hass=mock_hass,
|
||||||
|
|
@ -112,7 +120,10 @@ class TestEnhancedCoordinator:
|
||||||
"custom_components.tibber_prices.coordinator.aiohttp_client.async_get_clientsession",
|
"custom_components.tibber_prices.coordinator.aiohttp_client.async_get_clientsession",
|
||||||
return_value=mock_session,
|
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(
|
sub_coordinator = TibberPricesDataUpdateCoordinator(
|
||||||
hass=mock_hass,
|
hass=mock_hass,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ from __future__ import annotations
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from unittest.mock import Mock, patch
|
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
|
# Constants for test validation
|
||||||
INTERVALS_PER_DAY = 96
|
INTERVALS_PER_DAY = 96
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
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
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
# Constants for integration testing
|
# Constants for integration testing
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue