mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
refactor(naming): complete class naming convention alignment
Renamed 25 public classes + 1 Enum to include TibberPrices prefix
following Home Assistant integration naming standards.
All classes now follow pattern: TibberPrices{SemanticPurpose}
No package hierarchy in names (import path is namespace).
Key changes:
- Coordinator module: DataFetcher, DataTransformer, ListenerManager,
PeriodCalculator, TimeService (203 usages), CacheData
- Config flow: CannotConnectError, InvalidAuthError
- Entity utils: IconContext
- Sensor calculators: BaseCalculator + 8 subclasses
- Period handlers: 5 NamedTuples (PeriodConfig, PeriodData,
PeriodStatistics, ThresholdConfig, IntervalCriteria)
- Period handlers: SpikeCandidateContext (dataclass → NamedTuple)
- API: QueryType Enum
Documentation updates:
- AGENTS.md: Added Pyright code generation guidelines
- planning/class-naming-refactoring-plan.md: Complete execution log
Quality metrics:
- 0 Pyright errors (strict type checking)
- 0 Ruff errors (linting + formatting)
- All hassfest checks passed
- 79 files validated
Impact: Aligns with HA Core standards (TibberDataCoordinator pattern).
No user-facing changes - internal refactor only.
This commit is contained in:
parent
07f5990e06
commit
c2b9908e69
55 changed files with 407 additions and 366 deletions
21
AGENTS.md
21
AGENTS.md
|
|
@ -1516,6 +1516,25 @@ venv = ".venv"
|
||||||
typeCheckingMode = "basic"
|
typeCheckingMode = "basic"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**CRITICAL: When generating code, always aim for Pyright `basic` mode compliance:**
|
||||||
|
|
||||||
|
✅ **DO:**
|
||||||
|
- Add type hints to all function signatures (parameters + return types)
|
||||||
|
- Use proper type annotations: `dict[str, Any]`, `list[dict]`, `str | None`
|
||||||
|
- Handle Optional types explicitly (None-checks before use)
|
||||||
|
- Use TYPE_CHECKING imports for type-only dependencies
|
||||||
|
- Prefer explicit returns over implicit `None`
|
||||||
|
|
||||||
|
❌ **DON'T:**
|
||||||
|
- Leave functions without return type hints
|
||||||
|
- Ignore potential `None` values in Optional types
|
||||||
|
- Use `Any` as escape hatch (only when truly needed)
|
||||||
|
- Create functions that could return different types based on runtime logic
|
||||||
|
|
||||||
|
**Goal:** Generated code should pass `./scripts/type-check` on first try, minimizing post-generation fixes.
|
||||||
|
|
||||||
|
**See also:** "Ruff Code Style Guidelines" section below for complementary code style rules that ensure `./scripts/lint` compliance.
|
||||||
|
|
||||||
### When Type Errors Are Acceptable
|
### When Type Errors Are Acceptable
|
||||||
|
|
||||||
**Use `type: ignore` comments sparingly and ONLY when:**
|
**Use `type: ignore` comments sparingly and ONLY when:**
|
||||||
|
|
@ -2066,6 +2085,8 @@ Understanding **how** good documentation emerges is as important as knowing what
|
||||||
|
|
||||||
These rules ensure generated code passes `./scripts/lint` on first try. Ruff enforces these automatically.
|
These rules ensure generated code passes `./scripts/lint` on first try. Ruff enforces these automatically.
|
||||||
|
|
||||||
|
**See also:** "Linting Best Practices" section above for Pyright type checking guidelines that ensure `./scripts/type-check` compliance.
|
||||||
|
|
||||||
**String Formatting:**
|
**String Formatting:**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,10 @@ from .helpers import (
|
||||||
verify_graphql_response,
|
verify_graphql_response,
|
||||||
verify_response_or_raise,
|
verify_response_or_raise,
|
||||||
)
|
)
|
||||||
from .queries import QueryType
|
from .queries import TibberPricesQueryType
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -46,7 +46,7 @@ class TibberPricesApiClient:
|
||||||
self._session = session
|
self._session = session
|
||||||
self._version = version
|
self._version = version
|
||||||
self._request_semaphore = asyncio.Semaphore(2) # Max 2 concurrent requests
|
self._request_semaphore = asyncio.Semaphore(2) # Max 2 concurrent requests
|
||||||
self.time: TimeService | None = None # Set externally by coordinator
|
self.time: TibberPricesTimeService # Set externally by coordinator (always initialized before use)
|
||||||
self._last_request_time = None # Set on first request
|
self._last_request_time = None # Set on first request
|
||||||
self._min_request_interval = timedelta(seconds=1) # Min 1 second between requests
|
self._min_request_interval = timedelta(seconds=1) # Min 1 second between requests
|
||||||
self._max_retries = 5
|
self._max_retries = 5
|
||||||
|
|
@ -130,7 +130,7 @@ class TibberPricesApiClient:
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
},
|
},
|
||||||
query_type=QueryType.USER,
|
query_type=TibberPricesQueryType.USER,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_get_price_info(self, home_ids: set[str]) -> dict:
|
async def async_get_price_info(self, home_ids: set[str]) -> dict:
|
||||||
|
|
@ -183,7 +183,7 @@ class TibberPricesApiClient:
|
||||||
|
|
||||||
data = await self._api_wrapper(
|
data = await self._api_wrapper(
|
||||||
data={"query": query},
|
data={"query": query},
|
||||||
query_type=QueryType.PRICE_INFO,
|
query_type=TibberPricesQueryType.PRICE_INFO,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Parse aliased response
|
# Parse aliased response
|
||||||
|
|
@ -234,7 +234,7 @@ class TibberPricesApiClient:
|
||||||
}
|
}
|
||||||
}}}}}"""
|
}}}}}"""
|
||||||
},
|
},
|
||||||
query_type=QueryType.DAILY_RATING,
|
query_type=TibberPricesQueryType.DAILY_RATING,
|
||||||
)
|
)
|
||||||
homes = data.get("viewer", {}).get("homes", [])
|
homes = data.get("viewer", {}).get("homes", [])
|
||||||
|
|
||||||
|
|
@ -266,7 +266,7 @@ class TibberPricesApiClient:
|
||||||
}
|
}
|
||||||
}}}}}"""
|
}}}}}"""
|
||||||
},
|
},
|
||||||
query_type=QueryType.HOURLY_RATING,
|
query_type=TibberPricesQueryType.HOURLY_RATING,
|
||||||
)
|
)
|
||||||
homes = data.get("viewer", {}).get("homes", [])
|
homes = data.get("viewer", {}).get("homes", [])
|
||||||
|
|
||||||
|
|
@ -298,7 +298,7 @@ class TibberPricesApiClient:
|
||||||
}
|
}
|
||||||
}}}}}"""
|
}}}}}"""
|
||||||
},
|
},
|
||||||
query_type=QueryType.MONTHLY_RATING,
|
query_type=TibberPricesQueryType.MONTHLY_RATING,
|
||||||
)
|
)
|
||||||
homes = data.get("viewer", {}).get("homes", [])
|
homes = data.get("viewer", {}).get("homes", [])
|
||||||
|
|
||||||
|
|
@ -322,7 +322,7 @@ class TibberPricesApiClient:
|
||||||
self,
|
self,
|
||||||
headers: dict[str, str],
|
headers: dict[str, str],
|
||||||
data: dict,
|
data: dict,
|
||||||
query_type: QueryType,
|
query_type: TibberPricesQueryType,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Make an API request with comprehensive error handling for network issues."""
|
"""Make an API request with comprehensive error handling for network issues."""
|
||||||
_LOGGER.debug("Making API request with data: %s", data)
|
_LOGGER.debug("Making API request with data: %s", data)
|
||||||
|
|
@ -443,7 +443,7 @@ class TibberPricesApiClient:
|
||||||
self,
|
self,
|
||||||
headers: dict[str, str],
|
headers: dict[str, str],
|
||||||
data: dict,
|
data: dict,
|
||||||
query_type: QueryType,
|
query_type: TibberPricesQueryType,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Handle a single API request with rate limiting."""
|
"""Handle a single API request with rate limiting."""
|
||||||
async with self._request_semaphore:
|
async with self._request_semaphore:
|
||||||
|
|
@ -547,7 +547,7 @@ class TibberPricesApiClient:
|
||||||
self,
|
self,
|
||||||
data: dict | None = None,
|
data: dict | None = None,
|
||||||
headers: dict | None = None,
|
headers: dict | None = None,
|
||||||
query_type: QueryType = QueryType.USER,
|
query_type: TibberPricesQueryType = TibberPricesQueryType.USER,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Get information from the API with rate limiting and retry logic."""
|
"""Get information from the API with rate limiting and retry logic."""
|
||||||
headers = headers or prepare_headers(self._access_token, self._version)
|
headers = headers or prepare_headers(self._access_token, self._version)
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ from homeassistant.const import __version__ as ha_version
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
from .queries import QueryType
|
from .queries import TibberPricesQueryType
|
||||||
|
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
TibberPricesApiClientAuthenticationError,
|
TibberPricesApiClientAuthenticationError,
|
||||||
|
|
@ -50,7 +50,7 @@ def verify_response_or_raise(response: aiohttp.ClientResponse) -> None:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
|
|
||||||
async def verify_graphql_response(response_json: dict, query_type: QueryType) -> None:
|
async def verify_graphql_response(response_json: dict, query_type: TibberPricesQueryType) -> None:
|
||||||
"""Verify the GraphQL response for errors and data completeness, including empty data."""
|
"""Verify the GraphQL response for errors and data completeness, including empty data."""
|
||||||
if "errors" in response_json:
|
if "errors" in response_json:
|
||||||
errors = response_json["errors"]
|
errors = response_json["errors"]
|
||||||
|
|
@ -252,7 +252,7 @@ def prepare_headers(access_token: str, version: str) -> dict[str, str]:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def flatten_price_info(subscription: dict, currency: str | None = None, *, time: TimeService) -> dict:
|
def flatten_price_info(subscription: dict, currency: str | None = None, *, time: TibberPricesTimeService) -> dict:
|
||||||
"""
|
"""
|
||||||
Transform and flatten priceInfo from full API data structure.
|
Transform and flatten priceInfo from full API data structure.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class QueryType(Enum):
|
class TibberPricesQueryType(Enum):
|
||||||
"""Types of queries that can be made to the API."""
|
"""Types of queries that can be made to the API."""
|
||||||
|
|
||||||
PRICE_INFO = "price_info"
|
PRICE_INFO = "price_info"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
||||||
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute
|
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
@ -19,14 +19,14 @@ if TYPE_CHECKING:
|
||||||
def get_tomorrow_data_available_attributes(
|
def get_tomorrow_data_available_attributes(
|
||||||
coordinator_data: dict,
|
coordinator_data: dict,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""
|
"""
|
||||||
Build attributes for tomorrow_data_available sensor.
|
Build attributes for tomorrow_data_available sensor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
coordinator_data: Coordinator data dict
|
coordinator_data: Coordinator data dict
|
||||||
time: TimeService instance
|
time: TibberPricesTimeService instance
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Attributes dict with intervals_available and data_status
|
Attributes dict with intervals_available and data_status
|
||||||
|
|
@ -59,7 +59,7 @@ def get_tomorrow_data_available_attributes(
|
||||||
def get_price_intervals_attributes(
|
def get_price_intervals_attributes(
|
||||||
coordinator_data: dict,
|
coordinator_data: dict,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
reverse_sort: bool,
|
reverse_sort: bool,
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -72,7 +72,7 @@ def get_price_intervals_attributes(
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
coordinator_data: Coordinator data dict
|
coordinator_data: Coordinator data dict
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
reverse_sort: True for peak_price (highest first), False for best_price (lowest first)
|
reverse_sort: True for peak_price (highest first), False for best_price (lowest first)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
@ -117,7 +117,7 @@ def get_price_intervals_attributes(
|
||||||
return build_final_attributes_simple(current_period, period_summaries, time=time)
|
return build_final_attributes_simple(current_period, period_summaries, time=time)
|
||||||
|
|
||||||
|
|
||||||
def build_no_periods_result(*, time: TimeService) -> dict:
|
def build_no_periods_result(*, time: TibberPricesTimeService) -> dict:
|
||||||
"""
|
"""
|
||||||
Build result when no periods exist (not filtered, just none available).
|
Build result when no periods exist (not filtered, just none available).
|
||||||
|
|
||||||
|
|
@ -214,7 +214,7 @@ def build_final_attributes_simple(
|
||||||
current_period: dict | None,
|
current_period: dict | None,
|
||||||
period_summaries: list[dict],
|
period_summaries: list[dict],
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Build the final attributes dictionary from coordinator's period summaries.
|
Build the final attributes dictionary from coordinator's period summaries.
|
||||||
|
|
@ -237,7 +237,7 @@ def build_final_attributes_simple(
|
||||||
Args:
|
Args:
|
||||||
current_period: The current or next period (already complete from coordinator)
|
current_period: The current or next period (already complete from coordinator)
|
||||||
period_summaries: All period summaries from coordinator
|
period_summaries: All period summaries from coordinator
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Complete attributes dict with all fields
|
Complete attributes dict with all fields
|
||||||
|
|
@ -286,7 +286,7 @@ async def build_async_extra_state_attributes( # noqa: PLR0913
|
||||||
translation_key: str | None,
|
translation_key: str | None,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
config_entry: TibberPricesConfigEntry,
|
config_entry: TibberPricesConfigEntry,
|
||||||
sensor_attrs: dict | None = None,
|
sensor_attrs: dict | None = None,
|
||||||
is_on: bool | None = None,
|
is_on: bool | None = None,
|
||||||
|
|
@ -300,7 +300,7 @@ async def build_async_extra_state_attributes( # noqa: PLR0913
|
||||||
entity_key: Entity key (e.g., "best_price_period")
|
entity_key: Entity key (e.g., "best_price_period")
|
||||||
translation_key: Translation key for entity
|
translation_key: Translation key for entity
|
||||||
hass: Home Assistant instance
|
hass: Home Assistant instance
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
config_entry: Config entry with options (keyword-only)
|
config_entry: Config entry with options (keyword-only)
|
||||||
sensor_attrs: Sensor-specific attributes (keyword-only)
|
sensor_attrs: Sensor-specific attributes (keyword-only)
|
||||||
is_on: Binary sensor state (keyword-only)
|
is_on: Binary sensor state (keyword-only)
|
||||||
|
|
@ -349,7 +349,7 @@ def build_sync_extra_state_attributes( # noqa: PLR0913
|
||||||
translation_key: str | None,
|
translation_key: str | None,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
config_entry: TibberPricesConfigEntry,
|
config_entry: TibberPricesConfigEntry,
|
||||||
sensor_attrs: dict | None = None,
|
sensor_attrs: dict | None = None,
|
||||||
is_on: bool | None = None,
|
is_on: bool | None = None,
|
||||||
|
|
@ -363,7 +363,7 @@ def build_sync_extra_state_attributes( # noqa: PLR0913
|
||||||
entity_key: Entity key (e.g., "best_price_period")
|
entity_key: Entity key (e.g., "best_price_period")
|
||||||
translation_key: Translation key for entity
|
translation_key: Translation key for entity
|
||||||
hass: Home Assistant instance
|
hass: Home Assistant instance
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
config_entry: Config entry with options (keyword-only)
|
config_entry: Config entry with options (keyword-only)
|
||||||
sensor_attrs: Sensor-specific attributes (keyword-only)
|
sensor_attrs: Sensor-specific attributes (keyword-only)
|
||||||
is_on: Binary sensor state (keyword-only)
|
is_on: Binary sensor state (keyword-only)
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator import (
|
from custom_components.tibber_prices.coordinator import (
|
||||||
TibberPricesDataUpdateCoordinator,
|
TibberPricesDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
|
|
||||||
class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
|
|
@ -65,12 +65,12 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
self._time_sensitive_remove_listener = None
|
self._time_sensitive_remove_listener = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_time_sensitive_update(self, time_service: TimeService) -> None:
|
def _handle_time_sensitive_update(self, time_service: TibberPricesTimeService) -> None:
|
||||||
"""
|
"""
|
||||||
Handle time-sensitive update from coordinator.
|
Handle time-sensitive update from coordinator.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
time_service: TimeService instance with reference time for this update cycle
|
time_service: TibberPricesTimeService instance with reference time for this update cycle
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Store TimeService from Timer #2 for calculations during this update cycle
|
# Store TimeService from Timer #2 for calculations during this update cycle
|
||||||
|
|
|
||||||
|
|
@ -27,17 +27,17 @@ from .config_flow_handlers.subentry_flow import (
|
||||||
)
|
)
|
||||||
from .config_flow_handlers.user_flow import TibberPricesFlowHandler as ConfigFlow
|
from .config_flow_handlers.user_flow import TibberPricesFlowHandler as ConfigFlow
|
||||||
from .config_flow_handlers.validators import (
|
from .config_flow_handlers.validators import (
|
||||||
CannotConnectError,
|
TibberPricesCannotConnectError,
|
||||||
InvalidAuthError,
|
TibberPricesInvalidAuthError,
|
||||||
validate_api_token,
|
validate_api_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CannotConnectError",
|
|
||||||
"ConfigFlow",
|
"ConfigFlow",
|
||||||
"InvalidAuthError",
|
|
||||||
"OptionsFlowHandler",
|
"OptionsFlowHandler",
|
||||||
"SubentryFlowHandler",
|
"SubentryFlowHandler",
|
||||||
|
"TibberPricesCannotConnectError",
|
||||||
|
"TibberPricesInvalidAuthError",
|
||||||
"get_best_price_schema",
|
"get_best_price_schema",
|
||||||
"get_options_init_schema",
|
"get_options_init_schema",
|
||||||
"get_peak_price_schema",
|
"get_peak_price_schema",
|
||||||
|
|
|
||||||
|
|
@ -42,15 +42,15 @@ from custom_components.tibber_prices.config_flow_handlers.user_flow import (
|
||||||
TibberPricesFlowHandler,
|
TibberPricesFlowHandler,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.config_flow_handlers.validators import (
|
from custom_components.tibber_prices.config_flow_handlers.validators import (
|
||||||
CannotConnectError,
|
TibberPricesCannotConnectError,
|
||||||
InvalidAuthError,
|
TibberPricesInvalidAuthError,
|
||||||
validate_api_token,
|
validate_api_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CannotConnectError",
|
"TibberPricesCannotConnectError",
|
||||||
"InvalidAuthError",
|
|
||||||
"TibberPricesFlowHandler",
|
"TibberPricesFlowHandler",
|
||||||
|
"TibberPricesInvalidAuthError",
|
||||||
"TibberPricesOptionsFlowHandler",
|
"TibberPricesOptionsFlowHandler",
|
||||||
"TibberPricesSubentryFlowHandler",
|
"TibberPricesSubentryFlowHandler",
|
||||||
"get_best_price_schema",
|
"get_best_price_schema",
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
||||||
errors["base"] = "invalid_yaml_syntax"
|
errors["base"] = "invalid_yaml_syntax"
|
||||||
|
|
||||||
# Test service call with parsed parameters
|
# Test service call with parsed parameters
|
||||||
if not errors and parsed:
|
if not errors and parsed and isinstance(parsed, dict):
|
||||||
try:
|
try:
|
||||||
# Add entry_id to service call data
|
# Add entry_id to service call data
|
||||||
service_data = {**parsed, "entry_id": self.config_entry.entry_id}
|
service_data = {**parsed, "entry_id": self.config_entry.entry_id}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ from custom_components.tibber_prices.config_flow_handlers.subentry_flow import (
|
||||||
TibberPricesSubentryFlowHandler,
|
TibberPricesSubentryFlowHandler,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.config_flow_handlers.validators import (
|
from custom_components.tibber_prices.config_flow_handlers.validators import (
|
||||||
CannotConnectError,
|
TibberPricesCannotConnectError,
|
||||||
InvalidAuthError,
|
TibberPricesInvalidAuthError,
|
||||||
validate_api_token,
|
validate_api_token,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.const import DOMAIN, LOGGER
|
from custom_components.tibber_prices.const import DOMAIN, LOGGER
|
||||||
|
|
@ -84,10 +84,10 @@ class TibberPricesFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
viewer = await validate_api_token(self.hass, user_input[CONF_ACCESS_TOKEN])
|
viewer = await validate_api_token(self.hass, user_input[CONF_ACCESS_TOKEN])
|
||||||
except InvalidAuthError as exception:
|
except TibberPricesInvalidAuthError as exception:
|
||||||
LOGGER.warning(exception)
|
LOGGER.warning(exception)
|
||||||
_errors["base"] = "auth"
|
_errors["base"] = "auth"
|
||||||
except CannotConnectError as exception:
|
except TibberPricesCannotConnectError as exception:
|
||||||
LOGGER.error(exception)
|
LOGGER.error(exception)
|
||||||
_errors["base"] = "connection"
|
_errors["base"] = "connection"
|
||||||
else:
|
else:
|
||||||
|
|
@ -137,10 +137,10 @@ class TibberPricesFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
viewer = await validate_api_token(self.hass, user_input[CONF_ACCESS_TOKEN])
|
viewer = await validate_api_token(self.hass, user_input[CONF_ACCESS_TOKEN])
|
||||||
except InvalidAuthError as exception:
|
except TibberPricesInvalidAuthError as exception:
|
||||||
LOGGER.warning(exception)
|
LOGGER.warning(exception)
|
||||||
_errors["base"] = "auth"
|
_errors["base"] = "auth"
|
||||||
except CannotConnectError as exception:
|
except TibberPricesCannotConnectError as exception:
|
||||||
LOGGER.error(exception)
|
LOGGER.error(exception)
|
||||||
_errors["base"] = "connection"
|
_errors["base"] = "connection"
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,11 @@ MAX_FLEX_PERCENTAGE = 100.0
|
||||||
MAX_MIN_PERIODS = 10 # Arbitrary upper limit for sanity
|
MAX_MIN_PERIODS = 10 # Arbitrary upper limit for sanity
|
||||||
|
|
||||||
|
|
||||||
class InvalidAuthError(HomeAssistantError):
|
class TibberPricesInvalidAuthError(HomeAssistantError):
|
||||||
"""Error to indicate invalid authentication."""
|
"""Error to indicate invalid authentication."""
|
||||||
|
|
||||||
|
|
||||||
class CannotConnectError(HomeAssistantError):
|
class TibberPricesCannotConnectError(HomeAssistantError):
|
||||||
"""Error to indicate we cannot connect."""
|
"""Error to indicate we cannot connect."""
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -43,8 +43,8 @@ async def validate_api_token(hass: HomeAssistant, token: str) -> dict:
|
||||||
dict with viewer data on success
|
dict with viewer data on success
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
InvalidAuthError: Invalid token
|
TibberPricesInvalidAuthError: Invalid token
|
||||||
CannotConnectError: API connection failed
|
TibberPricesCannotConnectError: API connection failed
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
|
@ -57,11 +57,11 @@ async def validate_api_token(hass: HomeAssistant, token: str) -> dict:
|
||||||
result = await client.async_get_viewer_details()
|
result = await client.async_get_viewer_details()
|
||||||
return result["viewer"]
|
return result["viewer"]
|
||||||
except TibberPricesApiClientAuthenticationError as exception:
|
except TibberPricesApiClientAuthenticationError as exception:
|
||||||
raise InvalidAuthError from exception
|
raise TibberPricesInvalidAuthError from exception
|
||||||
except TibberPricesApiClientCommunicationError as exception:
|
except TibberPricesApiClientCommunicationError as exception:
|
||||||
raise CannotConnectError from exception
|
raise TibberPricesCannotConnectError from exception
|
||||||
except TibberPricesApiClientError as exception:
|
except TibberPricesApiClientError as exception:
|
||||||
raise CannotConnectError from exception
|
raise TibberPricesCannotConnectError from exception
|
||||||
|
|
||||||
|
|
||||||
def validate_threshold_range(value: float, min_val: float, max_val: float) -> bool:
|
def validate_threshold_range(value: float, min_val: float, max_val: float) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,12 @@ from .constants import (
|
||||||
TIME_SENSITIVE_ENTITY_KEYS,
|
TIME_SENSITIVE_ENTITY_KEYS,
|
||||||
)
|
)
|
||||||
from .core import TibberPricesDataUpdateCoordinator
|
from .core import TibberPricesDataUpdateCoordinator
|
||||||
from .time_service import TimeService
|
from .time_service import TibberPricesTimeService
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"MINUTE_UPDATE_ENTITY_KEYS",
|
"MINUTE_UPDATE_ENTITY_KEYS",
|
||||||
"STORAGE_VERSION",
|
"STORAGE_VERSION",
|
||||||
"TIME_SENSITIVE_ENTITY_KEYS",
|
"TIME_SENSITIVE_ENTITY_KEYS",
|
||||||
"TibberPricesDataUpdateCoordinator",
|
"TibberPricesDataUpdateCoordinator",
|
||||||
"TimeService",
|
"TibberPricesTimeService",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
|
|
||||||
from .time_service import TimeService
|
from .time_service import TibberPricesTimeService
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CacheData(NamedTuple):
|
class TibberPricesCacheData(NamedTuple):
|
||||||
"""Cache data structure."""
|
"""Cache data structure."""
|
||||||
|
|
||||||
price_data: dict[str, Any] | None
|
price_data: dict[str, Any] | None
|
||||||
|
|
@ -29,8 +29,8 @@ async def load_cache(
|
||||||
store: Store,
|
store: Store,
|
||||||
log_prefix: str,
|
log_prefix: str,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> CacheData:
|
) -> TibberPricesCacheData:
|
||||||
"""Load cached data from storage."""
|
"""Load cached data from storage."""
|
||||||
try:
|
try:
|
||||||
stored = await store.async_load()
|
stored = await store.async_load()
|
||||||
|
|
@ -51,7 +51,7 @@ async def load_cache(
|
||||||
last_midnight_check = time.parse_datetime(last_midnight_check_str)
|
last_midnight_check = time.parse_datetime(last_midnight_check_str)
|
||||||
|
|
||||||
_LOGGER.debug("%s Cache loaded successfully", log_prefix)
|
_LOGGER.debug("%s Cache loaded successfully", log_prefix)
|
||||||
return CacheData(
|
return TibberPricesCacheData(
|
||||||
price_data=cached_price_data,
|
price_data=cached_price_data,
|
||||||
user_data=cached_user_data,
|
user_data=cached_user_data,
|
||||||
last_price_update=last_price_update,
|
last_price_update=last_price_update,
|
||||||
|
|
@ -63,7 +63,7 @@ async def load_cache(
|
||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
_LOGGER.warning("%s Failed to load cache: %s", log_prefix, ex)
|
_LOGGER.warning("%s Failed to load cache: %s", log_prefix, ex)
|
||||||
|
|
||||||
return CacheData(
|
return TibberPricesCacheData(
|
||||||
price_data=None,
|
price_data=None,
|
||||||
user_data=None,
|
user_data=None,
|
||||||
last_price_update=None,
|
last_price_update=None,
|
||||||
|
|
@ -72,9 +72,9 @@ async def load_cache(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def store_cache(
|
async def save_cache(
|
||||||
store: Store,
|
store: Store,
|
||||||
cache_data: CacheData,
|
cache_data: TibberPricesCacheData,
|
||||||
log_prefix: str,
|
log_prefix: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Store cache data."""
|
"""Store cache data."""
|
||||||
|
|
@ -94,10 +94,10 @@ async def store_cache(
|
||||||
|
|
||||||
|
|
||||||
def is_cache_valid(
|
def is_cache_valid(
|
||||||
cache_data: CacheData,
|
cache_data: TibberPricesCacheData,
|
||||||
log_prefix: str,
|
log_prefix: str,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Validate if cached price data is still current.
|
Validate if cached price data is still current.
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
|
||||||
|
from .listeners import TimeServiceCallback
|
||||||
|
|
||||||
from custom_components.tibber_prices import const as _const
|
from custom_components.tibber_prices import const as _const
|
||||||
from custom_components.tibber_prices.api import (
|
from custom_components.tibber_prices.api import (
|
||||||
TibberPricesApiClient,
|
TibberPricesApiClient,
|
||||||
|
|
@ -34,11 +36,11 @@ from .constants import (
|
||||||
STORAGE_VERSION,
|
STORAGE_VERSION,
|
||||||
UPDATE_INTERVAL,
|
UPDATE_INTERVAL,
|
||||||
)
|
)
|
||||||
from .data_fetching import DataFetcher
|
from .data_fetching import TibberPricesDataFetcher
|
||||||
from .data_transformation import DataTransformer
|
from .data_transformation import TibberPricesDataTransformer
|
||||||
from .listeners import ListenerManager
|
from .listeners import TibberPricesListenerManager
|
||||||
from .periods import PeriodCalculator
|
from .periods import TibberPricesPeriodCalculator
|
||||||
from .time_service import TimeService
|
from .time_service import TibberPricesTimeService
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -134,28 +136,28 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
# Track if this is the main entry (first one created)
|
# Track if this is the main entry (first one created)
|
||||||
self._is_main_entry = not self._has_existing_main_coordinator()
|
self._is_main_entry = not self._has_existing_main_coordinator()
|
||||||
|
|
||||||
# Initialize time service (single source of truth for datetime operations)
|
# Initialize time service (single source of truth for all time operations)
|
||||||
self.time = TimeService()
|
self.time = TibberPricesTimeService()
|
||||||
|
|
||||||
# Set time on API client (needed for rate limiting)
|
# Set time on API client (needed for rate limiting)
|
||||||
self.api.time = self.time
|
self.api.time = self.time
|
||||||
|
|
||||||
# Initialize helper modules
|
# Initialize helper modules
|
||||||
self._listener_manager = ListenerManager(hass, self._log_prefix)
|
self._listener_manager = TibberPricesListenerManager(hass, self._log_prefix)
|
||||||
self._data_fetcher = DataFetcher(
|
self._data_fetcher = TibberPricesDataFetcher(
|
||||||
api=self.api,
|
api=self.api,
|
||||||
store=self._store,
|
store=self._store,
|
||||||
log_prefix=self._log_prefix,
|
log_prefix=self._log_prefix,
|
||||||
user_update_interval=timedelta(days=1),
|
user_update_interval=timedelta(days=1),
|
||||||
time=self.time,
|
time=self.time,
|
||||||
)
|
)
|
||||||
self._data_transformer = DataTransformer(
|
self._data_transformer = TibberPricesDataTransformer(
|
||||||
config_entry=config_entry,
|
config_entry=config_entry,
|
||||||
log_prefix=self._log_prefix,
|
log_prefix=self._log_prefix,
|
||||||
perform_turnover_fn=self._perform_midnight_turnover,
|
perform_turnover_fn=self._perform_midnight_turnover,
|
||||||
time=self.time,
|
time=self.time,
|
||||||
)
|
)
|
||||||
self._period_calculator = PeriodCalculator(
|
self._period_calculator = TibberPricesPeriodCalculator(
|
||||||
config_entry=config_entry,
|
config_entry=config_entry,
|
||||||
log_prefix=self._log_prefix,
|
log_prefix=self._log_prefix,
|
||||||
)
|
)
|
||||||
|
|
@ -191,7 +193,7 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
await self.async_request_refresh()
|
await self.async_request_refresh()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_add_time_sensitive_listener(self, update_callback: CALLBACK_TYPE) -> CALLBACK_TYPE:
|
def async_add_time_sensitive_listener(self, update_callback: TimeServiceCallback) -> CALLBACK_TYPE:
|
||||||
"""
|
"""
|
||||||
Listen for time-sensitive updates that occur every quarter-hour.
|
Listen for time-sensitive updates that occur every quarter-hour.
|
||||||
|
|
||||||
|
|
@ -205,18 +207,18 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
return self._listener_manager.async_add_time_sensitive_listener(update_callback)
|
return self._listener_manager.async_add_time_sensitive_listener(update_callback)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_time_sensitive_listeners(self, time_service: TimeService) -> None:
|
def _async_update_time_sensitive_listeners(self, time_service: TibberPricesTimeService) -> None:
|
||||||
"""
|
"""
|
||||||
Update all time-sensitive entities without triggering a full coordinator update.
|
Update all time-sensitive entities without triggering a full coordinator update.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
time_service: TimeService instance with reference time for this update cycle
|
time_service: TibberPricesTimeService instance with reference time for this update cycle
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self._listener_manager.async_update_time_sensitive_listeners(time_service)
|
self._listener_manager.async_update_time_sensitive_listeners(time_service)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_add_minute_update_listener(self, update_callback: CALLBACK_TYPE) -> CALLBACK_TYPE:
|
def async_add_minute_update_listener(self, update_callback: TimeServiceCallback) -> CALLBACK_TYPE:
|
||||||
"""
|
"""
|
||||||
Listen for minute-by-minute updates for timing sensors.
|
Listen for minute-by-minute updates for timing sensors.
|
||||||
|
|
||||||
|
|
@ -230,12 +232,12 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
return self._listener_manager.async_add_minute_update_listener(update_callback)
|
return self._listener_manager.async_add_minute_update_listener(update_callback)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_minute_listeners(self, time_service: TimeService) -> None:
|
def _async_update_minute_listeners(self, time_service: TibberPricesTimeService) -> None:
|
||||||
"""
|
"""
|
||||||
Update all minute-update entities without triggering a full coordinator update.
|
Update all minute-update entities without triggering a full coordinator update.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
time_service: TimeService instance with reference time for this update cycle
|
time_service: TibberPricesTimeService instance with reference time for this update cycle
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self._listener_manager.async_update_minute_listeners(time_service)
|
self._listener_manager.async_update_minute_listeners(time_service)
|
||||||
|
|
@ -259,7 +261,7 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
# Each timer has its own TimeService instance - no shared state between timers
|
# Each timer has its own TimeService instance - no shared state between timers
|
||||||
# This timer updates 30+ time-sensitive entities at quarter-hour boundaries
|
# This timer updates 30+ time-sensitive entities at quarter-hour boundaries
|
||||||
# (Timer #3 handles timing entities separately - no overlap)
|
# (Timer #3 handles timing entities separately - no overlap)
|
||||||
time_service = TimeService()
|
time_service = TibberPricesTimeService()
|
||||||
now = time_service.now()
|
now = time_service.now()
|
||||||
|
|
||||||
# Update shared coordinator time (used by Timer #1 and other operations)
|
# Update shared coordinator time (used by Timer #1 and other operations)
|
||||||
|
|
@ -311,7 +313,7 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
# Timer #2 updates 30+ time-sensitive entities (prices, levels, timestamps)
|
# Timer #2 updates 30+ time-sensitive entities (prices, levels, timestamps)
|
||||||
# Timer #3 updates 6 timing entities (remaining_minutes, progress, next_in_minutes)
|
# Timer #3 updates 6 timing entities (remaining_minutes, progress, next_in_minutes)
|
||||||
# NO overlap - entities are registered with either Timer #2 OR Timer #3, never both
|
# NO overlap - entities are registered with either Timer #2 OR Timer #3, never both
|
||||||
time_service = TimeService()
|
time_service = TibberPricesTimeService()
|
||||||
|
|
||||||
# Only log at debug level to avoid log spam (this runs every 30 seconds)
|
# Only log at debug level to avoid log spam (this runs every 30 seconds)
|
||||||
self._log("debug", "[Timer #3] 30-second refresh for timing sensors")
|
self._log("debug", "[Timer #3] 30-second refresh for timing sensors")
|
||||||
|
|
@ -447,7 +449,7 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
self._log("debug", "[Timer #1] DataUpdateCoordinator check triggered")
|
self._log("debug", "[Timer #1] DataUpdateCoordinator check triggered")
|
||||||
|
|
||||||
# Create TimeService with fresh reference time for this update cycle
|
# Create TimeService with fresh reference time for this update cycle
|
||||||
self.time = TimeService()
|
self.time = TibberPricesTimeService()
|
||||||
current_time = self.time.now()
|
current_time = self.time.now()
|
||||||
|
|
||||||
# Update helper modules with fresh TimeService instance
|
# Update helper modules with fresh TimeService instance
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,12 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from custom_components.tibber_prices.api import TibberPricesApiClient
|
from custom_components.tibber_prices.api import TibberPricesApiClient
|
||||||
|
|
||||||
from .time_service import TimeService
|
from .time_service import TibberPricesTimeService
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DataFetcher:
|
class TibberPricesDataFetcher:
|
||||||
"""Handles data fetching, caching, and main/subentry coordination."""
|
"""Handles data fetching, caching, and main/subentry coordination."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -42,14 +42,14 @@ class DataFetcher:
|
||||||
store: Any,
|
store: Any,
|
||||||
log_prefix: str,
|
log_prefix: str,
|
||||||
user_update_interval: timedelta,
|
user_update_interval: timedelta,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the data fetcher."""
|
"""Initialize the data fetcher."""
|
||||||
self.api = api
|
self.api = api
|
||||||
self._store = store
|
self._store = store
|
||||||
self._log_prefix = log_prefix
|
self._log_prefix = log_prefix
|
||||||
self._user_update_interval = user_update_interval
|
self._user_update_interval = user_update_interval
|
||||||
self.time = time
|
self.time: TibberPricesTimeService = time
|
||||||
|
|
||||||
# Cached data
|
# Cached data
|
||||||
self._cached_price_data: dict[str, Any] | None = None
|
self._cached_price_data: dict[str, Any] | None = None
|
||||||
|
|
@ -84,14 +84,14 @@ class DataFetcher:
|
||||||
|
|
||||||
async def store_cache(self, last_midnight_check: datetime | None = None) -> None:
|
async def store_cache(self, last_midnight_check: datetime | None = None) -> None:
|
||||||
"""Store cache data."""
|
"""Store cache data."""
|
||||||
cache_data = cache.CacheData(
|
cache_data = cache.TibberPricesCacheData(
|
||||||
price_data=self._cached_price_data,
|
price_data=self._cached_price_data,
|
||||||
user_data=self._cached_user_data,
|
user_data=self._cached_user_data,
|
||||||
last_price_update=self._last_price_update,
|
last_price_update=self._last_price_update,
|
||||||
last_user_update=self._last_user_update,
|
last_user_update=self._last_user_update,
|
||||||
last_midnight_check=last_midnight_check,
|
last_midnight_check=last_midnight_check,
|
||||||
)
|
)
|
||||||
await cache.store_cache(self._store, cache_data, self._log_prefix)
|
await cache.save_cache(self._store, cache_data, self._log_prefix)
|
||||||
|
|
||||||
async def update_user_data_if_needed(self, current_time: datetime) -> None:
|
async def update_user_data_if_needed(self, current_time: datetime) -> None:
|
||||||
"""Update user data if needed (daily check)."""
|
"""Update user data if needed (daily check)."""
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,12 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
|
||||||
from .time_service import TimeService
|
from .time_service import TibberPricesTimeService
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DataTransformer:
|
class TibberPricesDataTransformer:
|
||||||
"""Handles data transformation, enrichment, and period calculations."""
|
"""Handles data transformation, enrichment, and period calculations."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -27,13 +27,13 @@ class DataTransformer:
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
log_prefix: str,
|
log_prefix: str,
|
||||||
perform_turnover_fn: Callable[[dict[str, Any]], dict[str, Any]],
|
perform_turnover_fn: Callable[[dict[str, Any]], dict[str, Any]],
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the data transformer."""
|
"""Initialize the data transformer."""
|
||||||
self.config_entry = config_entry
|
self.config_entry = config_entry
|
||||||
self._log_prefix = log_prefix
|
self._log_prefix = log_prefix
|
||||||
self._perform_turnover_fn = perform_turnover_fn
|
self._perform_turnover_fn = perform_turnover_fn
|
||||||
self.time = time
|
self.time: TibberPricesTimeService = time
|
||||||
|
|
||||||
# Transformation cache
|
# Transformation cache
|
||||||
self._cached_transformed_data: dict[str, Any] | None = None
|
self._cached_transformed_data: dict[str, Any] | None = None
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .time_service import TimeService
|
from .time_service import TibberPricesTimeService
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import DOMAIN
|
from custom_components.tibber_prices.const import DOMAIN
|
||||||
|
|
||||||
|
|
@ -57,7 +57,7 @@ def needs_tomorrow_data(
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def perform_midnight_turnover(price_info: dict[str, Any], *, time: TimeService) -> dict[str, Any]:
|
def perform_midnight_turnover(price_info: dict[str, Any], *, time: TibberPricesTimeService) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Perform midnight turnover on price data.
|
Perform midnight turnover on price data.
|
||||||
|
|
||||||
|
|
@ -69,7 +69,7 @@ def perform_midnight_turnover(price_info: dict[str, Any], *, time: TimeService)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
price_info: The price info dict with 'today', 'tomorrow', 'yesterday' keys
|
price_info: The price info dict with 'today', 'tomorrow', 'yesterday' keys
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Updated price_info with rotated day data
|
Updated price_info with rotated day data
|
||||||
|
|
@ -103,7 +103,7 @@ def perform_midnight_turnover(price_info: dict[str, Any], *, time: TimeService)
|
||||||
return price_info
|
return price_info
|
||||||
|
|
||||||
|
|
||||||
def parse_all_timestamps(price_data: dict[str, Any], *, time: TimeService) -> dict[str, Any]:
|
def parse_all_timestamps(price_data: dict[str, Any], *, time: TibberPricesTimeService) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Parse all API timestamp strings to datetime objects.
|
Parse all API timestamp strings to datetime objects.
|
||||||
|
|
||||||
|
|
@ -114,7 +114,7 @@ def parse_all_timestamps(price_data: dict[str, Any], *, time: TimeService) -> di
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
price_data: Raw API data with string timestamps
|
price_data: Raw API data with string timestamps
|
||||||
time: TimeService for parsing
|
time: TibberPricesTimeService for parsing
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Same structure but with datetime objects instead of strings
|
Same structure but with datetime objects instead of strings
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,20 @@ from homeassistant.helpers.event import async_track_utc_time_change
|
||||||
from .constants import QUARTER_HOUR_BOUNDARIES
|
from .constants import QUARTER_HOUR_BOUNDARIES
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .time_service import TimeService
|
from .time_service import TibberPricesTimeService
|
||||||
|
|
||||||
|
# Callback type that accepts TibberPricesTimeService parameter
|
||||||
|
TimeServiceCallback = Callable[[TibberPricesTimeService], None]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ListenerManager:
|
class TibberPricesListenerManager:
|
||||||
"""Manages listeners and scheduling for coordinator updates."""
|
"""Manages listeners and scheduling for coordinator updates."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, log_prefix: str) -> None:
|
def __init__(self, hass: HomeAssistant, log_prefix: str) -> None:
|
||||||
|
|
@ -29,8 +33,8 @@ class ListenerManager:
|
||||||
self._log_prefix = log_prefix
|
self._log_prefix = log_prefix
|
||||||
|
|
||||||
# Listener lists
|
# Listener lists
|
||||||
self._time_sensitive_listeners: list[CALLBACK_TYPE] = []
|
self._time_sensitive_listeners: list[TimeServiceCallback] = []
|
||||||
self._minute_update_listeners: list[CALLBACK_TYPE] = []
|
self._minute_update_listeners: list[TimeServiceCallback] = []
|
||||||
|
|
||||||
# Timer cancellation callbacks
|
# Timer cancellation callbacks
|
||||||
self._quarter_hour_timer_cancel: CALLBACK_TYPE | None = None
|
self._quarter_hour_timer_cancel: CALLBACK_TYPE | None = None
|
||||||
|
|
@ -45,7 +49,7 @@ class ListenerManager:
|
||||||
getattr(_LOGGER, level)(prefixed_message, *args, **kwargs)
|
getattr(_LOGGER, level)(prefixed_message, *args, **kwargs)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_add_time_sensitive_listener(self, update_callback: CALLBACK_TYPE) -> CALLBACK_TYPE:
|
def async_add_time_sensitive_listener(self, update_callback: TimeServiceCallback) -> CALLBACK_TYPE:
|
||||||
"""
|
"""
|
||||||
Listen for time-sensitive updates that occur every quarter-hour.
|
Listen for time-sensitive updates that occur every quarter-hour.
|
||||||
|
|
||||||
|
|
@ -66,12 +70,12 @@ class ListenerManager:
|
||||||
return remove_listener
|
return remove_listener
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_update_time_sensitive_listeners(self, time_service: TimeService) -> None:
|
def async_update_time_sensitive_listeners(self, time_service: TibberPricesTimeService) -> None:
|
||||||
"""
|
"""
|
||||||
Update all time-sensitive entities without triggering a full coordinator update.
|
Update all time-sensitive entities without triggering a full coordinator update.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
time_service: TimeService instance with reference time for this update cycle
|
time_service: TibberPricesTimeService instance with reference time for this update cycle
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for update_callback in self._time_sensitive_listeners:
|
for update_callback in self._time_sensitive_listeners:
|
||||||
|
|
@ -84,7 +88,7 @@ class ListenerManager:
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_add_minute_update_listener(self, update_callback: CALLBACK_TYPE) -> CALLBACK_TYPE:
|
def async_add_minute_update_listener(self, update_callback: TimeServiceCallback) -> CALLBACK_TYPE:
|
||||||
"""
|
"""
|
||||||
Listen for minute-by-minute updates for timing sensors.
|
Listen for minute-by-minute updates for timing sensors.
|
||||||
|
|
||||||
|
|
@ -105,12 +109,12 @@ class ListenerManager:
|
||||||
return remove_listener
|
return remove_listener
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_update_minute_listeners(self, time_service: TimeService) -> None:
|
def async_update_minute_listeners(self, time_service: TibberPricesTimeService) -> None:
|
||||||
"""
|
"""
|
||||||
Update all minute-update entities without triggering a full coordinator update.
|
Update all minute-update entities without triggering a full coordinator update.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
time_service: TimeService instance with reference time for this update cycle
|
time_service: TibberPricesTimeService instance with reference time for this update cycle
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for update_callback in self._minute_update_listeners:
|
for update_callback in self._minute_update_listeners:
|
||||||
|
|
@ -124,7 +128,7 @@ class ListenerManager:
|
||||||
|
|
||||||
def schedule_quarter_hour_refresh(
|
def schedule_quarter_hour_refresh(
|
||||||
self,
|
self,
|
||||||
handler_callback: CALLBACK_TYPE,
|
handler_callback: Callable[[datetime], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Schedule the next quarter-hour entity refresh using Home Assistant's time tracking."""
|
"""Schedule the next quarter-hour entity refresh using Home Assistant's time tracking."""
|
||||||
# Cancel any existing timer
|
# Cancel any existing timer
|
||||||
|
|
@ -151,7 +155,7 @@ class ListenerManager:
|
||||||
|
|
||||||
def schedule_minute_refresh(
|
def schedule_minute_refresh(
|
||||||
self,
|
self,
|
||||||
handler_callback: CALLBACK_TYPE,
|
handler_callback: Callable[[datetime], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Schedule 30-second entity refresh for timing sensors."""
|
"""Schedule 30-second entity refresh for timing sensors."""
|
||||||
# Cancel any existing timer
|
# Cancel any existing timer
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,11 @@ from .types import (
|
||||||
INDENT_L3,
|
INDENT_L3,
|
||||||
INDENT_L4,
|
INDENT_L4,
|
||||||
INDENT_L5,
|
INDENT_L5,
|
||||||
IntervalCriteria,
|
TibberPricesIntervalCriteria,
|
||||||
PeriodConfig,
|
TibberPricesPeriodConfig,
|
||||||
PeriodData,
|
TibberPricesPeriodData,
|
||||||
PeriodStatistics,
|
TibberPricesPeriodStatistics,
|
||||||
ThresholdConfig,
|
TibberPricesThresholdConfig,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|
@ -47,11 +47,11 @@ __all__ = [
|
||||||
"INDENT_L3",
|
"INDENT_L3",
|
||||||
"INDENT_L4",
|
"INDENT_L4",
|
||||||
"INDENT_L5",
|
"INDENT_L5",
|
||||||
"IntervalCriteria",
|
"TibberPricesIntervalCriteria",
|
||||||
"PeriodConfig",
|
"TibberPricesPeriodConfig",
|
||||||
"PeriodData",
|
"TibberPricesPeriodData",
|
||||||
"PeriodStatistics",
|
"TibberPricesPeriodStatistics",
|
||||||
"ThresholdConfig",
|
"TibberPricesThresholdConfig",
|
||||||
"calculate_periods",
|
"calculate_periods",
|
||||||
"calculate_periods_with_relaxation",
|
"calculate_periods_with_relaxation",
|
||||||
"filter_price_outliers",
|
"filter_price_outliers",
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
from .types import PeriodConfig
|
from .types import TibberPricesPeriodConfig
|
||||||
|
|
||||||
from .outlier_filtering import (
|
from .outlier_filtering import (
|
||||||
filter_price_outliers,
|
filter_price_outliers,
|
||||||
|
|
@ -23,7 +23,7 @@ from .period_building import (
|
||||||
from .period_statistics import (
|
from .period_statistics import (
|
||||||
extract_period_summaries,
|
extract_period_summaries,
|
||||||
)
|
)
|
||||||
from .types import ThresholdConfig
|
from .types import TibberPricesThresholdConfig
|
||||||
|
|
||||||
# Flex limits to prevent degenerate behavior (see docs/development/period-calculation-theory.md)
|
# Flex limits to prevent degenerate behavior (see docs/development/period-calculation-theory.md)
|
||||||
MAX_SAFE_FLEX = 0.50 # 50% - hard cap: above this, period detection becomes unreliable
|
MAX_SAFE_FLEX = 0.50 # 50% - hard cap: above this, period detection becomes unreliable
|
||||||
|
|
@ -33,8 +33,8 @@ MAX_OUTLIER_FLEX = 0.25 # 25% - cap for outlier filtering: above this, spike de
|
||||||
def calculate_periods(
|
def calculate_periods(
|
||||||
all_prices: list[dict],
|
all_prices: list[dict],
|
||||||
*,
|
*,
|
||||||
config: PeriodConfig,
|
config: TibberPricesPeriodConfig,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Calculate price periods (best or peak) from price data.
|
Calculate price periods (best or peak) from price data.
|
||||||
|
|
@ -55,7 +55,7 @@ def calculate_periods(
|
||||||
all_prices: All price data points from yesterday/today/tomorrow
|
all_prices: All price data points from yesterday/today/tomorrow
|
||||||
config: Period configuration containing reverse_sort, flex, min_distance_from_avg,
|
config: Period configuration containing reverse_sort, flex, min_distance_from_avg,
|
||||||
min_period_length, threshold_low, and threshold_high
|
min_period_length, threshold_low, and threshold_high
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with:
|
Dict with:
|
||||||
|
|
@ -183,7 +183,7 @@ def calculate_periods(
|
||||||
# Step 8: Extract lightweight period summaries (no full price data)
|
# Step 8: Extract lightweight period summaries (no full price data)
|
||||||
# Note: Filtering for current/future is done here based on end date,
|
# Note: Filtering for current/future is done here based on end date,
|
||||||
# not start date. This preserves periods that started yesterday but end today.
|
# not start date. This preserves periods that started yesterday but end today.
|
||||||
thresholds = ThresholdConfig(
|
thresholds = TibberPricesThresholdConfig(
|
||||||
threshold_low=threshold_low,
|
threshold_low=threshold_low,
|
||||||
threshold_high=threshold_high,
|
threshold_high=threshold_high,
|
||||||
threshold_volatility_moderate=config.threshold_volatility_moderate,
|
threshold_volatility_moderate=config.threshold_volatility_moderate,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .types import IntervalCriteria
|
from .types import TibberPricesIntervalCriteria
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import PRICE_LEVEL_MAPPING
|
from custom_components.tibber_prices.const import PRICE_LEVEL_MAPPING
|
||||||
|
|
||||||
|
|
@ -104,7 +104,7 @@ def apply_level_filter(
|
||||||
|
|
||||||
def check_interval_criteria(
|
def check_interval_criteria(
|
||||||
price: float,
|
price: float,
|
||||||
criteria: IntervalCriteria,
|
criteria: TibberPricesIntervalCriteria,
|
||||||
) -> tuple[bool, bool]:
|
) -> tuple[bool, bool]:
|
||||||
"""
|
"""
|
||||||
Check if interval meets flex and minimum distance criteria.
|
Check if interval meets flex and minimum distance criteria.
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ Uses statistical methods:
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from typing import NamedTuple
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -32,8 +32,7 @@ ZIGZAG_TAIL_WINDOW = 6 # Skip zigzag/cluster detection for last ~1.5h (6 interv
|
||||||
INDENT_L0 = "" # All logs in this module (no indentation needed)
|
INDENT_L0 = "" # All logs in this module (no indentation needed)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
class TibberPricesSpikeCandidateContext(NamedTuple):
|
||||||
class SpikeCandidateContext:
|
|
||||||
"""Container for spike validation parameters."""
|
"""Container for spike validation parameters."""
|
||||||
|
|
||||||
current: dict
|
current: dict
|
||||||
|
|
@ -183,7 +182,7 @@ def _detect_zigzag_pattern(window: list[dict], context_std_dev: float) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def _validate_spike_candidate(
|
def _validate_spike_candidate(
|
||||||
candidate: SpikeCandidateContext,
|
candidate: TibberPricesSpikeCandidateContext,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Run stability, symmetry, and zigzag checks before smoothing."""
|
"""Run stability, symmetry, and zigzag checks before smoothing."""
|
||||||
avg_before = sum(x["total"] for x in candidate.context_before) / len(candidate.context_before)
|
avg_before = sum(x["total"] for x in candidate.context_before) / len(candidate.context_before)
|
||||||
|
|
@ -308,7 +307,7 @@ def filter_price_outliers(
|
||||||
# SPIKE CANDIDATE DETECTED - Now validate
|
# SPIKE CANDIDATE DETECTED - Now validate
|
||||||
remaining_intervals = len(intervals) - (i + 1)
|
remaining_intervals = len(intervals) - (i + 1)
|
||||||
analysis_window = [*context_before[-2:], current, *context_after[:2]]
|
analysis_window = [*context_before[-2:], current, *context_after[:2]]
|
||||||
candidate_context = SpikeCandidateContext(
|
candidate_context = TibberPricesSpikeCandidateContext(
|
||||||
current=current,
|
current=current,
|
||||||
context_before=context_before,
|
context_before=context_before,
|
||||||
context_after=context_after,
|
context_after=context_after,
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,13 @@ from custom_components.tibber_prices.const import PRICE_LEVEL_MAPPING
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
from .level_filtering import (
|
from .level_filtering import (
|
||||||
apply_level_filter,
|
apply_level_filter,
|
||||||
check_interval_criteria,
|
check_interval_criteria,
|
||||||
)
|
)
|
||||||
from .types import IntervalCriteria
|
from .types import TibberPricesIntervalCriteria
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ INDENT_L0 = "" # Entry point / main function
|
||||||
|
|
||||||
|
|
||||||
def split_intervals_by_day(
|
def split_intervals_by_day(
|
||||||
all_prices: list[dict], *, time: TimeService
|
all_prices: list[dict], *, time: TibberPricesTimeService
|
||||||
) -> tuple[dict[date, list[dict]], dict[date, float]]:
|
) -> tuple[dict[date, list[dict]], dict[date, float]]:
|
||||||
"""Split intervals by day and calculate average price per day."""
|
"""Split intervals by day and calculate average price per day."""
|
||||||
intervals_by_day: dict[date, list[dict]] = {}
|
intervals_by_day: dict[date, list[dict]] = {}
|
||||||
|
|
@ -60,7 +60,7 @@ def build_periods( # noqa: PLR0913, PLR0915, PLR0912 - Complex period building
|
||||||
reverse_sort: bool,
|
reverse_sort: bool,
|
||||||
level_filter: str | None = None,
|
level_filter: str | None = None,
|
||||||
gap_count: int = 0,
|
gap_count: int = 0,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> list[list[dict]]:
|
) -> list[list[dict]]:
|
||||||
"""
|
"""
|
||||||
Build periods, allowing periods to cross midnight (day boundary).
|
Build periods, allowing periods to cross midnight (day boundary).
|
||||||
|
|
@ -75,7 +75,7 @@ def build_periods( # noqa: PLR0913, PLR0915, PLR0912 - Complex period building
|
||||||
reverse_sort: True for peak price (high prices), False for best price (low prices)
|
reverse_sort: True for peak price (high prices), False for best price (low prices)
|
||||||
level_filter: Level filter string ("cheap", "expensive", "any", None)
|
level_filter: Level filter string ("cheap", "expensive", "any", None)
|
||||||
gap_count: Number of allowed consecutive intervals deviating by exactly 1 level step
|
gap_count: Number of allowed consecutive intervals deviating by exactly 1 level step
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ref_prices = price_context["ref_prices"]
|
ref_prices = price_context["ref_prices"]
|
||||||
|
|
@ -130,7 +130,7 @@ def build_periods( # noqa: PLR0913, PLR0915, PLR0912 - Complex period building
|
||||||
ref_date = period_start_date if period_start_date is not None else date_key
|
ref_date = period_start_date if period_start_date is not None else date_key
|
||||||
|
|
||||||
# Check flex and minimum distance criteria (using smoothed price and period start date reference)
|
# Check flex and minimum distance criteria (using smoothed price and period start date reference)
|
||||||
criteria = IntervalCriteria(
|
criteria = TibberPricesIntervalCriteria(
|
||||||
ref_price=ref_prices[ref_date],
|
ref_price=ref_prices[ref_date],
|
||||||
avg_price=avg_prices[ref_date],
|
avg_price=avg_prices[ref_date],
|
||||||
flex=flex,
|
flex=flex,
|
||||||
|
|
@ -230,14 +230,14 @@ def build_periods( # noqa: PLR0913, PLR0915, PLR0912 - Complex period building
|
||||||
|
|
||||||
|
|
||||||
def filter_periods_by_min_length(
|
def filter_periods_by_min_length(
|
||||||
periods: list[list[dict]], min_period_length: int, *, time: TimeService
|
periods: list[list[dict]], min_period_length: int, *, time: TibberPricesTimeService
|
||||||
) -> list[list[dict]]:
|
) -> list[list[dict]]:
|
||||||
"""Filter periods to only include those meeting the minimum length requirement."""
|
"""Filter periods to only include those meeting the minimum length requirement."""
|
||||||
min_intervals = time.minutes_to_intervals(min_period_length)
|
min_intervals = time.minutes_to_intervals(min_period_length)
|
||||||
return [period for period in periods if len(period) >= min_intervals]
|
return [period for period in periods if len(period) >= min_intervals]
|
||||||
|
|
||||||
|
|
||||||
def add_interval_ends(periods: list[list[dict]], *, time: TimeService) -> None:
|
def add_interval_ends(periods: list[list[dict]], *, time: TibberPricesTimeService) -> None:
|
||||||
"""Add interval_end to each interval in-place."""
|
"""Add interval_end to each interval in-place."""
|
||||||
interval_duration = time.get_interval_duration()
|
interval_duration = time.get_interval_duration()
|
||||||
for period in periods:
|
for period in periods:
|
||||||
|
|
@ -247,7 +247,7 @@ def add_interval_ends(periods: list[list[dict]], *, time: TimeService) -> None:
|
||||||
interval["interval_end"] = start + interval_duration
|
interval["interval_end"] = start + interval_duration
|
||||||
|
|
||||||
|
|
||||||
def filter_periods_by_end_date(periods: list[list[dict]], *, time: TimeService) -> list[list[dict]]:
|
def filter_periods_by_end_date(periods: list[list[dict]], *, time: TibberPricesTimeService) -> list[list[dict]]:
|
||||||
"""
|
"""
|
||||||
Filter periods to keep only relevant ones for today and tomorrow.
|
Filter periods to keep only relevant ones for today and tomorrow.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -16,7 +16,7 @@ INDENT_L1 = " " # Nested logic / loop iterations
|
||||||
INDENT_L2 = " " # Deeper nesting
|
INDENT_L2 = " " # Deeper nesting
|
||||||
|
|
||||||
|
|
||||||
def recalculate_period_metadata(periods: list[dict], *, time: TimeService) -> None:
|
def recalculate_period_metadata(periods: list[dict], *, time: TibberPricesTimeService) -> None:
|
||||||
"""
|
"""
|
||||||
Recalculate period metadata after merging periods.
|
Recalculate period metadata after merging periods.
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@ def recalculate_period_metadata(periods: list[dict], *, time: TimeService) -> No
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
periods: List of period summary dicts (mutated in-place)
|
periods: List of period summary dicts (mutated in-place)
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not periods:
|
if not periods:
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@ from typing import TYPE_CHECKING, Any
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
from .types import (
|
from .types import (
|
||||||
PeriodData,
|
TibberPricesPeriodData,
|
||||||
PeriodStatistics,
|
TibberPricesPeriodStatistics,
|
||||||
ThresholdConfig,
|
TibberPricesThresholdConfig,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.utils.price import (
|
from custom_components.tibber_prices.utils.price import (
|
||||||
aggregate_period_levels,
|
aggregate_period_levels,
|
||||||
|
|
@ -115,8 +115,8 @@ def calculate_period_price_statistics(period_price_data: list[dict]) -> dict[str
|
||||||
|
|
||||||
|
|
||||||
def build_period_summary_dict(
|
def build_period_summary_dict(
|
||||||
period_data: PeriodData,
|
period_data: TibberPricesPeriodData,
|
||||||
stats: PeriodStatistics,
|
stats: TibberPricesPeriodStatistics,
|
||||||
*,
|
*,
|
||||||
reverse_sort: bool,
|
reverse_sort: bool,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
|
@ -176,9 +176,9 @@ def extract_period_summaries(
|
||||||
periods: list[list[dict]],
|
periods: list[list[dict]],
|
||||||
all_prices: list[dict],
|
all_prices: list[dict],
|
||||||
price_context: dict[str, Any],
|
price_context: dict[str, Any],
|
||||||
thresholds: ThresholdConfig,
|
thresholds: TibberPricesThresholdConfig,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Extract complete period summaries with all aggregated attributes.
|
Extract complete period summaries with all aggregated attributes.
|
||||||
|
|
@ -199,12 +199,12 @@ def extract_period_summaries(
|
||||||
all_prices: All price data from the API (enriched with level, difference, rating_level)
|
all_prices: All price data from the API (enriched with level, difference, rating_level)
|
||||||
price_context: Dictionary with ref_prices and avg_prices per day
|
price_context: Dictionary with ref_prices and avg_prices per day
|
||||||
thresholds: Threshold configuration for calculations
|
thresholds: Threshold configuration for calculations
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from .types import ( # noqa: PLC0415 - Avoid circular import
|
from .types import ( # noqa: PLC0415 - Avoid circular import
|
||||||
PeriodData,
|
TibberPricesPeriodData,
|
||||||
PeriodStatistics,
|
TibberPricesPeriodStatistics,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build lookup dictionary for full price data by timestamp
|
# Build lookup dictionary for full price data by timestamp
|
||||||
|
|
@ -284,7 +284,7 @@ def extract_period_summaries(
|
||||||
level_gap_count = sum(1 for interval in period if interval.get("is_level_gap", False))
|
level_gap_count = sum(1 for interval in period if interval.get("is_level_gap", False))
|
||||||
|
|
||||||
# Build period data and statistics objects
|
# Build period data and statistics objects
|
||||||
period_data = PeriodData(
|
period_data = TibberPricesPeriodData(
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
end_time=end_time,
|
end_time=end_time,
|
||||||
period_length=len(period),
|
period_length=len(period),
|
||||||
|
|
@ -292,7 +292,7 @@ def extract_period_summaries(
|
||||||
total_periods=total_periods,
|
total_periods=total_periods,
|
||||||
)
|
)
|
||||||
|
|
||||||
stats = PeriodStatistics(
|
stats = TibberPricesPeriodStatistics(
|
||||||
aggregated_level=aggregated_level,
|
aggregated_level=aggregated_level,
|
||||||
aggregated_rating=aggregated_rating,
|
aggregated_rating=aggregated_rating,
|
||||||
rating_difference_pct=rating_difference_pct,
|
rating_difference_pct=rating_difference_pct,
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
from .types import PeriodConfig
|
from .types import TibberPricesPeriodConfig
|
||||||
|
|
||||||
from .period_overlap import (
|
from .period_overlap import (
|
||||||
recalculate_period_metadata,
|
recalculate_period_metadata,
|
||||||
|
|
@ -60,13 +60,13 @@ def group_periods_by_day(periods: list[dict]) -> dict[date, list[dict]]:
|
||||||
return periods_by_day
|
return periods_by_day
|
||||||
|
|
||||||
|
|
||||||
def group_prices_by_day(all_prices: list[dict], *, time: TimeService) -> dict[date, list[dict]]:
|
def group_prices_by_day(all_prices: list[dict], *, time: TibberPricesTimeService) -> dict[date, list[dict]]:
|
||||||
"""
|
"""
|
||||||
Group price intervals by the day they belong to (today and future only).
|
Group price intervals by the day they belong to (today and future only).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
all_prices: List of price dicts with "startsAt" timestamp
|
all_prices: List of price dicts with "startsAt" timestamp
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict mapping date to list of price intervals for that day (only today and future)
|
Dict mapping date to list of price intervals for that day (only today and future)
|
||||||
|
|
@ -87,7 +87,7 @@ def group_prices_by_day(all_prices: list[dict], *, time: TimeService) -> dict[da
|
||||||
|
|
||||||
|
|
||||||
def check_min_periods_per_day(
|
def check_min_periods_per_day(
|
||||||
periods: list[dict], min_periods: int, all_prices: list[dict], *, time: TimeService
|
periods: list[dict], min_periods: int, all_prices: list[dict], *, time: TibberPricesTimeService
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if minimum periods requirement is met for each day individually.
|
Check if minimum periods requirement is met for each day individually.
|
||||||
|
|
@ -99,7 +99,7 @@ def check_min_periods_per_day(
|
||||||
periods: List of period summary dicts
|
periods: List of period summary dicts
|
||||||
min_periods: Minimum number of periods required per day
|
min_periods: Minimum number of periods required per day
|
||||||
all_prices: All available price intervals (used to determine which days have data)
|
all_prices: All available price intervals (used to determine which days have data)
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if every day with price data has at least min_periods, False otherwise
|
True if every day with price data has at least min_periods, False otherwise
|
||||||
|
|
@ -171,12 +171,12 @@ def mark_periods_with_relaxation(
|
||||||
def calculate_periods_with_relaxation( # noqa: PLR0913, PLR0915 - Per-day relaxation requires many parameters and statements
|
def calculate_periods_with_relaxation( # noqa: PLR0913, PLR0915 - Per-day relaxation requires many parameters and statements
|
||||||
all_prices: list[dict],
|
all_prices: list[dict],
|
||||||
*,
|
*,
|
||||||
config: PeriodConfig,
|
config: TibberPricesPeriodConfig,
|
||||||
enable_relaxation: bool,
|
enable_relaxation: bool,
|
||||||
min_periods: int,
|
min_periods: int,
|
||||||
max_relaxation_attempts: int,
|
max_relaxation_attempts: int,
|
||||||
should_show_callback: Callable[[str | None], bool],
|
should_show_callback: Callable[[str | None], bool],
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Calculate periods with optional per-day filter relaxation.
|
Calculate periods with optional per-day filter relaxation.
|
||||||
|
|
@ -201,7 +201,7 @@ def calculate_periods_with_relaxation( # noqa: PLR0913, PLR0915 - Per-day relax
|
||||||
should_show_callback: Callback function(level_override) -> bool
|
should_show_callback: Callback function(level_override) -> bool
|
||||||
Returns True if periods should be shown with given filter overrides. Pass None
|
Returns True if periods should be shown with given filter overrides. Pass None
|
||||||
to use original configured filter values.
|
to use original configured filter values.
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (periods_result, relaxation_metadata):
|
Tuple of (periods_result, relaxation_metadata):
|
||||||
|
|
@ -273,7 +273,11 @@ def calculate_periods_with_relaxation( # noqa: PLR0913, PLR0915 - Per-day relax
|
||||||
|
|
||||||
# Validate we have price data for today/future
|
# Validate we have price data for today/future
|
||||||
today = time.now().date()
|
today = time.now().date()
|
||||||
future_prices = [p for p in all_prices if time.get_interval_time(p).date() >= today]
|
future_prices = [
|
||||||
|
p
|
||||||
|
for p in all_prices
|
||||||
|
if (interval_time := time.get_interval_time(p)) is not None and interval_time.date() >= today
|
||||||
|
]
|
||||||
|
|
||||||
if not future_prices:
|
if not future_prices:
|
||||||
# No price data for today/future
|
# No price data for today/future
|
||||||
|
|
@ -394,13 +398,13 @@ def calculate_periods_with_relaxation( # noqa: PLR0913, PLR0915 - Per-day relax
|
||||||
|
|
||||||
def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation requires many parameters and statements
|
def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation requires many parameters and statements
|
||||||
all_prices: list[dict],
|
all_prices: list[dict],
|
||||||
config: PeriodConfig,
|
config: TibberPricesPeriodConfig,
|
||||||
min_periods: int,
|
min_periods: int,
|
||||||
max_relaxation_attempts: int,
|
max_relaxation_attempts: int,
|
||||||
should_show_callback: Callable[[str | None], bool],
|
should_show_callback: Callable[[str | None], bool],
|
||||||
baseline_periods: list[dict],
|
baseline_periods: list[dict],
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Relax filters for all prices until min_periods per day is reached.
|
Relax filters for all prices until min_periods per day is reached.
|
||||||
|
|
@ -416,7 +420,7 @@ def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation require
|
||||||
max_relaxation_attempts: Maximum flex levels to try
|
max_relaxation_attempts: Maximum flex levels to try
|
||||||
should_show_callback: Callback to check if a flex level should be shown
|
should_show_callback: Callback to check if a flex level should be shown
|
||||||
baseline_periods: Baseline periods (before relaxation)
|
baseline_periods: Baseline periods (before relaxation)
|
||||||
time: TimeService instance
|
time: TibberPricesTimeService instance
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (result_dict, metadata_dict)
|
Tuple of (result_dict, metadata_dict)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ INDENT_L4 = " " # Period-by-period analysis
|
||||||
INDENT_L5 = " " # Segment details
|
INDENT_L5 = " " # Segment details
|
||||||
|
|
||||||
|
|
||||||
class PeriodConfig(NamedTuple):
|
class TibberPricesPeriodConfig(NamedTuple):
|
||||||
"""Configuration for period calculation."""
|
"""Configuration for period calculation."""
|
||||||
|
|
||||||
reverse_sort: bool
|
reverse_sort: bool
|
||||||
|
|
@ -40,7 +40,7 @@ class PeriodConfig(NamedTuple):
|
||||||
gap_count: int = 0 # Number of allowed consecutive deviating intervals
|
gap_count: int = 0 # Number of allowed consecutive deviating intervals
|
||||||
|
|
||||||
|
|
||||||
class PeriodData(NamedTuple):
|
class TibberPricesPeriodData(NamedTuple):
|
||||||
"""Data for building a period summary."""
|
"""Data for building a period summary."""
|
||||||
|
|
||||||
start_time: datetime
|
start_time: datetime
|
||||||
|
|
@ -50,7 +50,7 @@ class PeriodData(NamedTuple):
|
||||||
total_periods: int
|
total_periods: int
|
||||||
|
|
||||||
|
|
||||||
class PeriodStatistics(NamedTuple):
|
class TibberPricesPeriodStatistics(NamedTuple):
|
||||||
"""Calculated statistics for a period."""
|
"""Calculated statistics for a period."""
|
||||||
|
|
||||||
aggregated_level: str | None
|
aggregated_level: str | None
|
||||||
|
|
@ -65,7 +65,7 @@ class PeriodStatistics(NamedTuple):
|
||||||
period_price_diff_pct: float | None
|
period_price_diff_pct: float | None
|
||||||
|
|
||||||
|
|
||||||
class ThresholdConfig(NamedTuple):
|
class TibberPricesThresholdConfig(NamedTuple):
|
||||||
"""Threshold configuration for period calculations."""
|
"""Threshold configuration for period calculations."""
|
||||||
|
|
||||||
threshold_low: float | None
|
threshold_low: float | None
|
||||||
|
|
@ -76,7 +76,7 @@ class ThresholdConfig(NamedTuple):
|
||||||
reverse_sort: bool
|
reverse_sort: bool
|
||||||
|
|
||||||
|
|
||||||
class IntervalCriteria(NamedTuple):
|
class TibberPricesIntervalCriteria(NamedTuple):
|
||||||
"""Criteria for checking if an interval qualifies for a period."""
|
"""Criteria for checking if an interval qualifies for a period."""
|
||||||
|
|
||||||
ref_price: float
|
ref_price: float
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,10 @@ from typing import TYPE_CHECKING, Any
|
||||||
from custom_components.tibber_prices import const as _const
|
from custom_components.tibber_prices import const as _const
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
from .period_handlers import (
|
from .period_handlers import (
|
||||||
PeriodConfig,
|
TibberPricesPeriodConfig,
|
||||||
calculate_periods_with_relaxation,
|
calculate_periods_with_relaxation,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ if TYPE_CHECKING:
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PeriodCalculator:
|
class TibberPricesPeriodCalculator:
|
||||||
"""Handles period calculations with level filtering and gap tolerance."""
|
"""Handles period calculations with level filtering and gap tolerance."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -37,7 +37,7 @@ class PeriodCalculator:
|
||||||
"""Initialize the period calculator."""
|
"""Initialize the period calculator."""
|
||||||
self.config_entry = config_entry
|
self.config_entry = config_entry
|
||||||
self._log_prefix = log_prefix
|
self._log_prefix = log_prefix
|
||||||
self.time: TimeService # Set by coordinator before first use
|
self.time: TibberPricesTimeService # Set by coordinator before first use
|
||||||
self._config_cache: dict[str, dict[str, Any]] | None = None
|
self._config_cache: dict[str, dict[str, Any]] | None = None
|
||||||
self._config_cache_valid = False
|
self._config_cache_valid = False
|
||||||
|
|
||||||
|
|
@ -602,7 +602,7 @@ class PeriodCalculator:
|
||||||
_const.CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
|
_const.CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||||
_const.DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
|
_const.DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||||
)
|
)
|
||||||
best_period_config = PeriodConfig(
|
best_period_config = TibberPricesPeriodConfig(
|
||||||
reverse_sort=False,
|
reverse_sort=False,
|
||||||
flex=best_config["flex"],
|
flex=best_config["flex"],
|
||||||
min_distance_from_avg=best_config["min_distance_from_avg"],
|
min_distance_from_avg=best_config["min_distance_from_avg"],
|
||||||
|
|
@ -670,7 +670,7 @@ class PeriodCalculator:
|
||||||
_const.CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
|
_const.CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||||
_const.DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
|
_const.DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||||
)
|
)
|
||||||
peak_period_config = PeriodConfig(
|
peak_period_config = TibberPricesPeriodConfig(
|
||||||
reverse_sort=True,
|
reverse_sort=True,
|
||||||
flex=peak_config["flex"],
|
flex=peak_config["flex"],
|
||||||
min_distance_from_avg=peak_config["min_distance_from_avg"],
|
min_distance_from_avg=peak_config["min_distance_from_avg"],
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ _INTERVALS_PER_DAY = 24 * _INTERVALS_PER_HOUR # 96
|
||||||
_BOUNDARY_TOLERANCE_SECONDS = 2
|
_BOUNDARY_TOLERANCE_SECONDS = 2
|
||||||
|
|
||||||
|
|
||||||
class TimeService:
|
class TibberPricesTimeService:
|
||||||
"""
|
"""
|
||||||
Centralized time service for Tibber Prices integration.
|
Centralized time service for Tibber Prices integration.
|
||||||
|
|
||||||
|
|
@ -669,6 +669,10 @@ class TimeService:
|
||||||
# - Normal day: 24 hours (96 intervals)
|
# - Normal day: 24 hours (96 intervals)
|
||||||
#
|
#
|
||||||
tz = self._reference_time.tzinfo # Get timezone from reference time
|
tz = self._reference_time.tzinfo # Get timezone from reference time
|
||||||
|
if tz is None:
|
||||||
|
# Should never happen - dt_util.now() always returns timezone-aware datetime
|
||||||
|
msg = "Reference time has no timezone information"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
# Create naive datetimes for midnight of target and next day
|
# Create naive datetimes for midnight of target and next day
|
||||||
start_naive = datetime.combine(target_date, datetime.min.time())
|
start_naive = datetime.combine(target_date, datetime.min.time())
|
||||||
|
|
@ -678,8 +682,9 @@ class TimeService:
|
||||||
# Localize to get correct DST offset for each date
|
# Localize to get correct DST offset for each date
|
||||||
if hasattr(tz, "localize"):
|
if hasattr(tz, "localize"):
|
||||||
# pytz timezone - use localize() to handle DST correctly
|
# pytz timezone - use localize() to handle DST correctly
|
||||||
start_midnight_local = tz.localize(start_naive)
|
# Type checker doesn't understand hasattr runtime check, but this is safe
|
||||||
end_midnight_local = tz.localize(end_naive)
|
start_midnight_local = tz.localize(start_naive) # type: ignore[attr-defined]
|
||||||
|
end_midnight_local = tz.localize(end_naive) # type: ignore[attr-defined]
|
||||||
else:
|
else:
|
||||||
# zoneinfo or other timezone - can use replace directly
|
# zoneinfo or other timezone - can use replace directly
|
||||||
start_midnight_local = start_naive.replace(tzinfo=tz)
|
start_midnight_local = start_naive.replace(tzinfo=tz)
|
||||||
|
|
@ -767,9 +772,9 @@ class TimeService:
|
||||||
# Time-Travel Support
|
# Time-Travel Support
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
def with_reference_time(self, new_time: datetime) -> TimeService:
|
def with_reference_time(self, new_time: datetime) -> TibberPricesTimeService:
|
||||||
"""
|
"""
|
||||||
Create new TimeService with different reference time.
|
Create new TibberPricesTimeService with different reference time.
|
||||||
|
|
||||||
Used for time-travel testing: inject simulated "now".
|
Used for time-travel testing: inject simulated "now".
|
||||||
|
|
||||||
|
|
@ -777,7 +782,7 @@ class TimeService:
|
||||||
new_time: New reference time.
|
new_time: New reference time.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
New TimeService instance with updated reference time.
|
New TibberPricesTimeService instance with updated reference time.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
# Simulate being at 14:30 on 2025-11-19
|
# Simulate being at 14:30 on 2025-11-19
|
||||||
|
|
@ -785,4 +790,4 @@ class TimeService:
|
||||||
future_service = time_service.with_reference_time(simulated_time)
|
future_service = time_service.with_reference_time(simulated_time)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return TimeService(reference_time=new_time)
|
return TibberPricesTimeService(reference_time=new_time)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ from custom_components.tibber_prices.const import get_price_level_translation
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -91,7 +91,7 @@ def find_rolling_hour_center_index(
|
||||||
current_time: datetime,
|
current_time: datetime,
|
||||||
hour_offset: int,
|
hour_offset: int,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> int | None:
|
) -> int | None:
|
||||||
"""
|
"""
|
||||||
Find the center index for the rolling hour window.
|
Find the center index for the rolling hour window.
|
||||||
|
|
@ -100,7 +100,7 @@ def find_rolling_hour_center_index(
|
||||||
all_prices: List of all price interval dictionaries with 'startsAt' key
|
all_prices: List of all price interval dictionaries with 'startsAt' key
|
||||||
current_time: Current datetime to find the current interval
|
current_time: Current datetime to find the current interval
|
||||||
hour_offset: Number of hours to offset from current interval (can be negative)
|
hour_offset: Number of hours to offset from current interval (can be negative)
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Index of the center interval for the rolling hour window, or None if not found
|
Index of the center interval for the rolling hour window, or None if not found
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from datetime import timedelta
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import (
|
||||||
BINARY_SENSOR_ICON_MAPPING,
|
BINARY_SENSOR_ICON_MAPPING,
|
||||||
|
|
@ -26,14 +26,14 @@ _INTERVAL_MINUTES = 15 # Tibber's 15-minute intervals
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class IconContext:
|
class TibberPricesIconContext:
|
||||||
"""Context data for dynamic icon selection."""
|
"""Context data for dynamic icon selection."""
|
||||||
|
|
||||||
is_on: bool | None = None
|
is_on: bool | None = None
|
||||||
coordinator_data: dict | None = None
|
coordinator_data: dict | None = None
|
||||||
has_future_periods_callback: Callable[[], bool] | None = None
|
has_future_periods_callback: Callable[[], bool] | None = None
|
||||||
period_is_active_callback: Callable[[], bool] | None = None
|
period_is_active_callback: Callable[[], bool] | None = None
|
||||||
time: TimeService | None = None
|
time: TibberPricesTimeService | None = None
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -53,7 +53,7 @@ def get_dynamic_icon(
|
||||||
key: str,
|
key: str,
|
||||||
value: Any,
|
value: Any,
|
||||||
*,
|
*,
|
||||||
context: IconContext | None = None,
|
context: TibberPricesIconContext | None = None,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""
|
"""
|
||||||
Get dynamic icon based on sensor state.
|
Get dynamic icon based on sensor state.
|
||||||
|
|
@ -69,7 +69,7 @@ def get_dynamic_icon(
|
||||||
Icon string or None if no dynamic icon applies
|
Icon string or None if no dynamic icon applies
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ctx = context or IconContext()
|
ctx = context or TibberPricesIconContext()
|
||||||
|
|
||||||
# Try various icon sources in order
|
# Try various icon sources in order
|
||||||
return (
|
return (
|
||||||
|
|
@ -173,7 +173,7 @@ def get_price_sensor_icon(
|
||||||
key: str,
|
key: str,
|
||||||
coordinator_data: dict | None,
|
coordinator_data: dict | None,
|
||||||
*,
|
*,
|
||||||
time: TimeService | None,
|
time: TibberPricesTimeService | None,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""
|
"""
|
||||||
Get icon for current price sensors (dynamic based on price level).
|
Get icon for current price sensors (dynamic based on price level).
|
||||||
|
|
@ -185,7 +185,7 @@ def get_price_sensor_icon(
|
||||||
Args:
|
Args:
|
||||||
key: Entity description key
|
key: Entity description key
|
||||||
coordinator_data: Coordinator data for price level lookups
|
coordinator_data: Coordinator data for price level lookups
|
||||||
time: TimeService instance (required for determining current interval)
|
time: TibberPricesTimeService instance (required for determining current interval)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Icon string or None if not a current price sensor
|
Icon string or None if not a current price sensor
|
||||||
|
|
@ -300,7 +300,7 @@ def get_price_level_for_icon(
|
||||||
coordinator_data: dict,
|
coordinator_data: dict,
|
||||||
*,
|
*,
|
||||||
interval_offset: int | None = None,
|
interval_offset: int | None = None,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""
|
"""
|
||||||
Get the price level for icon determination.
|
Get the price level for icon determination.
|
||||||
|
|
@ -310,7 +310,7 @@ def get_price_level_for_icon(
|
||||||
Args:
|
Args:
|
||||||
coordinator_data: Coordinator data
|
coordinator_data: Coordinator data
|
||||||
interval_offset: Interval offset (0=current, 1=next, -1=previous)
|
interval_offset: Interval offset (0=current, 1=next, -1=previous)
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Price level string or None if not found
|
Price level string or None if not found
|
||||||
|
|
@ -336,7 +336,7 @@ def get_rolling_hour_price_level_for_icon(
|
||||||
coordinator_data: dict,
|
coordinator_data: dict,
|
||||||
*,
|
*,
|
||||||
hour_offset: int = 0,
|
hour_offset: int = 0,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""
|
"""
|
||||||
Get the aggregated price level for rolling hour icon determination.
|
Get the aggregated price level for rolling hour icon determination.
|
||||||
|
|
@ -349,7 +349,7 @@ def get_rolling_hour_price_level_for_icon(
|
||||||
Args:
|
Args:
|
||||||
coordinator_data: Coordinator data
|
coordinator_data: Coordinator data
|
||||||
hour_offset: Hour offset (0=current hour, 1=next hour)
|
hour_offset: Hour offset (0=current hour, 1=next hour)
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Aggregated price level string or None if not found
|
Aggregated price level string or None if not found
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.core import (
|
from custom_components.tibber_prices.coordinator.core import (
|
||||||
TibberPricesDataUpdateCoordinator,
|
TibberPricesDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
from custom_components.tibber_prices.data import TibberPricesConfigEntry
|
from custom_components.tibber_prices.data import TibberPricesConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
|
@ -171,7 +171,7 @@ def build_extra_state_attributes( # noqa: PLR0913
|
||||||
config_entry: TibberPricesConfigEntry,
|
config_entry: TibberPricesConfigEntry,
|
||||||
coordinator_data: dict,
|
coordinator_data: dict,
|
||||||
sensor_attrs: dict | None = None,
|
sensor_attrs: dict | None = None,
|
||||||
time: TimeService | None = None,
|
time: TibberPricesTimeService,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""
|
"""
|
||||||
Build extra state attributes for sensors.
|
Build extra state attributes for sensors.
|
||||||
|
|
@ -189,7 +189,7 @@ def build_extra_state_attributes( # noqa: PLR0913
|
||||||
config_entry: Config entry with options (keyword-only)
|
config_entry: Config entry with options (keyword-only)
|
||||||
coordinator_data: Coordinator data dict (keyword-only)
|
coordinator_data: Coordinator data dict (keyword-only)
|
||||||
sensor_attrs: Sensor-specific attributes (keyword-only)
|
sensor_attrs: Sensor-specific attributes (keyword-only)
|
||||||
time: TimeService instance (optional, creates new if not provided)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Complete attributes dict or None if no data available
|
Complete attributes dict or None if no data available
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,13 @@ from custom_components.tibber_prices.const import PRICE_RATING_MAPPING
|
||||||
from homeassistant.const import PERCENTAGE
|
from homeassistant.const import PERCENTAGE
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from datetime import datetime
|
||||||
|
|
||||||
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
|
|
||||||
def _get_day_midnight_timestamp(key: str, *, time: TimeService) -> str:
|
def _get_day_midnight_timestamp(key: str, *, time: TibberPricesTimeService) -> datetime:
|
||||||
"""Get midnight timestamp for a given day sensor key."""
|
"""Get midnight timestamp for a given day sensor key (returns datetime object)."""
|
||||||
# Determine which day based on sensor key
|
# Determine which day based on sensor key
|
||||||
if key.startswith("yesterday") or key == "average_price_yesterday":
|
if key.startswith("yesterday") or key == "average_price_yesterday":
|
||||||
day = "yesterday"
|
day = "yesterday"
|
||||||
|
|
@ -65,7 +67,7 @@ def add_statistics_attributes(
|
||||||
key: str,
|
key: str,
|
||||||
cached_data: dict,
|
cached_data: dict,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Add attributes for statistics and rating sensors.
|
Add attributes for statistics and rating sensors.
|
||||||
|
|
@ -74,7 +76,7 @@ def add_statistics_attributes(
|
||||||
attributes: Dictionary to add attributes to
|
attributes: Dictionary to add attributes to
|
||||||
key: The sensor entity key
|
key: The sensor entity key
|
||||||
cached_data: Dictionary containing cached sensor data
|
cached_data: Dictionary containing cached sensor data
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Data timestamp sensor - shows API fetch time
|
# Data timestamp sensor - shows API fetch time
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.core import (
|
from custom_components.tibber_prices.coordinator.core import (
|
||||||
TibberPricesDataUpdateCoordinator,
|
TibberPricesDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
MAX_FORECAST_INTERVALS = 8 # Show up to 8 future intervals (2 hours with 15-min intervals)
|
MAX_FORECAST_INTERVALS = 8 # Show up to 8 future intervals (2 hours with 15-min intervals)
|
||||||
|
|
@ -20,7 +20,7 @@ def add_next_avg_attributes(
|
||||||
key: str,
|
key: str,
|
||||||
coordinator: TibberPricesDataUpdateCoordinator,
|
coordinator: TibberPricesDataUpdateCoordinator,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Add attributes for next N hours average price sensors.
|
Add attributes for next N hours average price sensors.
|
||||||
|
|
@ -29,7 +29,7 @@ def add_next_avg_attributes(
|
||||||
attributes: Dictionary to add attributes to
|
attributes: Dictionary to add attributes to
|
||||||
key: The sensor entity key
|
key: The sensor entity key
|
||||||
coordinator: The data update coordinator
|
coordinator: The data update coordinator
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Extract hours from sensor key (e.g., "next_avg_3h" -> 3)
|
# Extract hours from sensor key (e.g., "next_avg_3h" -> 3)
|
||||||
|
|
@ -70,7 +70,7 @@ def add_price_forecast_attributes(
|
||||||
attributes: dict,
|
attributes: dict,
|
||||||
coordinator: TibberPricesDataUpdateCoordinator,
|
coordinator: TibberPricesDataUpdateCoordinator,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Add forecast attributes for the price forecast sensor.
|
Add forecast attributes for the price forecast sensor.
|
||||||
|
|
@ -78,7 +78,7 @@ def add_price_forecast_attributes(
|
||||||
Args:
|
Args:
|
||||||
attributes: Dictionary to add attributes to
|
attributes: Dictionary to add attributes to
|
||||||
coordinator: The data update coordinator
|
coordinator: The data update coordinator
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
future_prices = get_future_prices(coordinator, max_intervals=MAX_FORECAST_INTERVALS, time=time)
|
future_prices = get_future_prices(coordinator, max_intervals=MAX_FORECAST_INTERVALS, time=time)
|
||||||
|
|
@ -164,7 +164,7 @@ def get_future_prices(
|
||||||
coordinator: TibberPricesDataUpdateCoordinator,
|
coordinator: TibberPricesDataUpdateCoordinator,
|
||||||
max_intervals: int | None = None,
|
max_intervals: int | None = None,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> list[dict] | None:
|
) -> list[dict] | None:
|
||||||
"""
|
"""
|
||||||
Get future price data for multiple upcoming intervals.
|
Get future price data for multiple upcoming intervals.
|
||||||
|
|
@ -172,7 +172,7 @@ def get_future_prices(
|
||||||
Args:
|
Args:
|
||||||
coordinator: The data update coordinator
|
coordinator: The data update coordinator
|
||||||
max_intervals: Maximum number of future intervals to return
|
max_intervals: Maximum number of future intervals to return
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of upcoming price intervals with timestamps and prices
|
List of upcoming price intervals with timestamps and prices
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.core import (
|
from custom_components.tibber_prices.coordinator.core import (
|
||||||
TibberPricesDataUpdateCoordinator,
|
TibberPricesDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
from .metadata import get_current_interval_data
|
from .metadata import get_current_interval_data
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@ def add_current_interval_price_attributes( # noqa: PLR0913
|
||||||
native_value: Any,
|
native_value: Any,
|
||||||
cached_data: dict,
|
cached_data: dict,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Add attributes for current interval price sensors.
|
Add attributes for current interval price sensors.
|
||||||
|
|
@ -39,7 +39,7 @@ def add_current_interval_price_attributes( # noqa: PLR0913
|
||||||
coordinator: The data update coordinator
|
coordinator: The data update coordinator
|
||||||
native_value: The current native value of the sensor
|
native_value: The current native value of the sensor
|
||||||
cached_data: Dictionary containing cached sensor data
|
cached_data: Dictionary containing cached sensor data
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
price_info = coordinator.data.get("priceInfo", {}) if coordinator.data else {}
|
price_info = coordinator.data.get("priceInfo", {}) if coordinator.data else {}
|
||||||
|
|
@ -137,7 +137,7 @@ def add_level_attributes_for_sensor( # noqa: PLR0913
|
||||||
coordinator: TibberPricesDataUpdateCoordinator,
|
coordinator: TibberPricesDataUpdateCoordinator,
|
||||||
native_value: Any,
|
native_value: Any,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Add price level attributes based on sensor type.
|
Add price level attributes based on sensor type.
|
||||||
|
|
@ -148,7 +148,7 @@ def add_level_attributes_for_sensor( # noqa: PLR0913
|
||||||
interval_data: Interval data for next/previous sensors
|
interval_data: Interval data for next/previous sensors
|
||||||
coordinator: The data update coordinator
|
coordinator: The data update coordinator
|
||||||
native_value: The current native value of the sensor
|
native_value: The current native value of the sensor
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# For interval-based level sensors (next/previous), use interval data
|
# For interval-based level sensors (next/previous), use interval data
|
||||||
|
|
@ -191,7 +191,7 @@ def add_rating_attributes_for_sensor( # noqa: PLR0913
|
||||||
coordinator: TibberPricesDataUpdateCoordinator,
|
coordinator: TibberPricesDataUpdateCoordinator,
|
||||||
native_value: Any,
|
native_value: Any,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Add price rating attributes based on sensor type.
|
Add price rating attributes based on sensor type.
|
||||||
|
|
@ -202,7 +202,7 @@ def add_rating_attributes_for_sensor( # noqa: PLR0913
|
||||||
interval_data: Interval data for next/previous sensors
|
interval_data: Interval data for next/previous sensors
|
||||||
coordinator: The data update coordinator
|
coordinator: The data update coordinator
|
||||||
native_value: The current native value of the sensor
|
native_value: The current native value of the sensor
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# For interval-based rating sensors (next/previous), use interval data
|
# For interval-based rating sensors (next/previous), use interval data
|
||||||
|
|
|
||||||
|
|
@ -10,20 +10,20 @@ if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.core import (
|
from custom_components.tibber_prices.coordinator.core import (
|
||||||
TibberPricesDataUpdateCoordinator,
|
TibberPricesDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
|
|
||||||
def get_current_interval_data(
|
def get_current_interval_data(
|
||||||
coordinator: TibberPricesDataUpdateCoordinator,
|
coordinator: TibberPricesDataUpdateCoordinator,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""
|
"""
|
||||||
Get current interval's price data.
|
Get current interval's price data.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
coordinator: The data update coordinator
|
coordinator: The data update coordinator
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Current interval data or None if not found
|
Current interval data or None if not found
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any
|
||||||
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute
|
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
# Timer #3 triggers every 30 seconds
|
# Timer #3 triggers every 30 seconds
|
||||||
TIMER_30_SEC_BOUNDARY = 30
|
TIMER_30_SEC_BOUNDARY = 30
|
||||||
|
|
@ -35,7 +35,7 @@ def add_period_timing_attributes(
|
||||||
key: str,
|
key: str,
|
||||||
state_value: Any = None,
|
state_value: Any = None,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Add timestamp and icon_color attributes for best_price/peak_price timing sensors.
|
Add timestamp and icon_color attributes for best_price/peak_price timing sensors.
|
||||||
|
|
@ -48,7 +48,7 @@ def add_period_timing_attributes(
|
||||||
attributes: Dictionary to add attributes to
|
attributes: Dictionary to add attributes to
|
||||||
key: The sensor entity key (e.g., "best_price_end_time")
|
key: The sensor entity key (e.g., "best_price_end_time")
|
||||||
state_value: Current sensor value for icon_color calculation
|
state_value: Current sensor value for icon_color calculation
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Determine if this is a quarter-hour or 30-second update sensor
|
# Determine if this is a quarter-hour or 30-second update sensor
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
from .timing import add_period_timing_attributes
|
from .timing import add_period_timing_attributes
|
||||||
from .volatility import add_volatility_attributes
|
from .volatility import add_volatility_attributes
|
||||||
|
|
@ -17,7 +17,7 @@ def _add_timing_or_volatility_attributes(
|
||||||
cached_data: dict,
|
cached_data: dict,
|
||||||
native_value: Any = None,
|
native_value: Any = None,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add attributes for timing or volatility sensors."""
|
"""Add attributes for timing or volatility sensors."""
|
||||||
if key.endswith("_volatility"):
|
if key.endswith("_volatility"):
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,14 @@ from typing import TYPE_CHECKING
|
||||||
from custom_components.tibber_prices.utils.price import calculate_volatility_level
|
from custom_components.tibber_prices.utils.price import calculate_volatility_level
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
|
|
||||||
def add_volatility_attributes(
|
def add_volatility_attributes(
|
||||||
attributes: dict,
|
attributes: dict,
|
||||||
cached_data: dict,
|
cached_data: dict,
|
||||||
*,
|
*,
|
||||||
time: TimeService, # noqa: ARG001
|
time: TibberPricesTimeService, # noqa: ARG001
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Add attributes for volatility sensors.
|
Add attributes for volatility sensors.
|
||||||
|
|
@ -23,7 +23,7 @@ def add_volatility_attributes(
|
||||||
Args:
|
Args:
|
||||||
attributes: Dictionary to add attributes to
|
attributes: Dictionary to add attributes to
|
||||||
cached_data: Dictionary containing cached sensor data
|
cached_data: Dictionary containing cached sensor data
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if cached_data.get("volatility_attributes"):
|
if cached_data.get("volatility_attributes"):
|
||||||
|
|
@ -34,7 +34,7 @@ def get_prices_for_volatility(
|
||||||
volatility_type: str,
|
volatility_type: str,
|
||||||
price_info: dict,
|
price_info: dict,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> list[float]:
|
) -> list[float]:
|
||||||
"""
|
"""
|
||||||
Get price list for volatility calculation based on type.
|
Get price list for volatility calculation based on type.
|
||||||
|
|
@ -42,7 +42,7 @@ def get_prices_for_volatility(
|
||||||
Args:
|
Args:
|
||||||
volatility_type: One of "today", "tomorrow", "next_24h", "today_tomorrow"
|
volatility_type: One of "today", "tomorrow", "next_24h", "today_tomorrow"
|
||||||
price_info: Price information dictionary from coordinator data
|
price_info: Price information dictionary from coordinator data
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of prices to analyze
|
List of prices to analyze
|
||||||
|
|
@ -88,7 +88,7 @@ def add_volatility_type_attributes(
|
||||||
price_info: dict,
|
price_info: dict,
|
||||||
thresholds: dict,
|
thresholds: dict,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Add type-specific attributes for volatility sensors.
|
Add type-specific attributes for volatility sensors.
|
||||||
|
|
@ -98,7 +98,7 @@ def add_volatility_type_attributes(
|
||||||
volatility_type: Type of volatility calculation
|
volatility_type: Type of volatility calculation
|
||||||
price_info: Price information dictionary from coordinator data
|
price_info: Price information dictionary from coordinator data
|
||||||
thresholds: Volatility thresholds configuration
|
thresholds: Volatility thresholds configuration
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Add timestamp for calendar day volatility sensors (midnight of the day)
|
# Add timestamp for calendar day volatility sensors (midnight of the day)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.core import (
|
from custom_components.tibber_prices.coordinator.core import (
|
||||||
TibberPricesDataUpdateCoordinator,
|
TibberPricesDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
|
|
||||||
def _update_extreme_interval(extreme_interval: dict | None, price_data: dict, key: str) -> dict:
|
def _update_extreme_interval(extreme_interval: dict | None, price_data: dict, key: str) -> dict:
|
||||||
|
|
@ -43,7 +43,7 @@ def add_average_price_attributes(
|
||||||
key: str,
|
key: str,
|
||||||
coordinator: TibberPricesDataUpdateCoordinator,
|
coordinator: TibberPricesDataUpdateCoordinator,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Add attributes for trailing and leading average/min/max price sensors.
|
Add attributes for trailing and leading average/min/max price sensors.
|
||||||
|
|
@ -52,7 +52,7 @@ def add_average_price_attributes(
|
||||||
attributes: Dictionary to add attributes to
|
attributes: Dictionary to add attributes to
|
||||||
key: The sensor entity key
|
key: The sensor entity key
|
||||||
coordinator: The data update coordinator
|
coordinator: The data update coordinator
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Determine if this is trailing or leading
|
# Determine if this is trailing or leading
|
||||||
|
|
|
||||||
|
|
@ -10,24 +10,24 @@ All calculators inherit from BaseCalculator and have access to coordinator data.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from .base import BaseCalculator
|
from .base import TibberPricesBaseCalculator
|
||||||
from .daily_stat import DailyStatCalculator
|
from .daily_stat import TibberPricesDailyStatCalculator
|
||||||
from .interval import IntervalCalculator
|
from .interval import TibberPricesIntervalCalculator
|
||||||
from .metadata import MetadataCalculator
|
from .metadata import TibberPricesMetadataCalculator
|
||||||
from .rolling_hour import RollingHourCalculator
|
from .rolling_hour import TibberPricesRollingHourCalculator
|
||||||
from .timing import TimingCalculator
|
from .timing import TibberPricesTimingCalculator
|
||||||
from .trend import TrendCalculator
|
from .trend import TibberPricesTrendCalculator
|
||||||
from .volatility import VolatilityCalculator
|
from .volatility import TibberPricesVolatilityCalculator
|
||||||
from .window_24h import Window24hCalculator
|
from .window_24h import TibberPricesWindow24hCalculator
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BaseCalculator",
|
"TibberPricesBaseCalculator",
|
||||||
"DailyStatCalculator",
|
"TibberPricesDailyStatCalculator",
|
||||||
"IntervalCalculator",
|
"TibberPricesIntervalCalculator",
|
||||||
"MetadataCalculator",
|
"TibberPricesMetadataCalculator",
|
||||||
"RollingHourCalculator",
|
"TibberPricesRollingHourCalculator",
|
||||||
"TimingCalculator",
|
"TibberPricesTimingCalculator",
|
||||||
"TrendCalculator",
|
"TibberPricesTrendCalculator",
|
||||||
"VolatilityCalculator",
|
"TibberPricesVolatilityCalculator",
|
||||||
"Window24hCalculator",
|
"TibberPricesWindow24hCalculator",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
|
||||||
class BaseCalculator:
|
class TibberPricesBaseCalculator:
|
||||||
"""
|
"""
|
||||||
Base class for all sensor value calculators.
|
Base class for all sensor value calculators.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ from custom_components.tibber_prices.sensor.helpers import (
|
||||||
aggregate_rating_data,
|
aggregate_rating_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .base import BaseCalculator
|
from .base import TibberPricesBaseCalculator
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
@ -26,7 +26,7 @@ if TYPE_CHECKING:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DailyStatCalculator(BaseCalculator):
|
class TibberPricesDailyStatCalculator(TibberPricesBaseCalculator):
|
||||||
"""
|
"""
|
||||||
Calculator for daily statistics.
|
Calculator for daily statistics.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from custom_components.tibber_prices.utils.price import find_price_data_for_interval
|
from custom_components.tibber_prices.utils.price import find_price_data_for_interval
|
||||||
|
|
||||||
from .base import BaseCalculator
|
from .base import TibberPricesBaseCalculator
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator import (
|
from custom_components.tibber_prices.coordinator import (
|
||||||
|
|
@ -14,7 +14,7 @@ if TYPE_CHECKING:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class IntervalCalculator(BaseCalculator):
|
class TibberPricesIntervalCalculator(TibberPricesBaseCalculator):
|
||||||
"""
|
"""
|
||||||
Calculator for interval-based sensors.
|
Calculator for interval-based sensors.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from .base import BaseCalculator
|
from .base import TibberPricesBaseCalculator
|
||||||
|
|
||||||
|
|
||||||
class MetadataCalculator(BaseCalculator):
|
class TibberPricesMetadataCalculator(TibberPricesBaseCalculator):
|
||||||
"""
|
"""
|
||||||
Calculator for home metadata, metering point, and subscription data.
|
Calculator for home metadata, metering point, and subscription data.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,10 @@ from custom_components.tibber_prices.sensor.helpers import (
|
||||||
aggregate_rating_data,
|
aggregate_rating_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .base import BaseCalculator
|
from .base import TibberPricesBaseCalculator
|
||||||
|
|
||||||
|
|
||||||
class RollingHourCalculator(BaseCalculator):
|
class TibberPricesRollingHourCalculator(TibberPricesBaseCalculator):
|
||||||
"""
|
"""
|
||||||
Calculator for rolling hour values (5-interval windows).
|
Calculator for rolling hour values (5-interval windows).
|
||||||
|
|
||||||
|
|
@ -75,9 +75,9 @@ class RollingHourCalculator(BaseCalculator):
|
||||||
if not window_data:
|
if not window_data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self._aggregate_window_data(window_data, value_type)
|
return self.aggregate_window_data(window_data, value_type)
|
||||||
|
|
||||||
def _aggregate_window_data(
|
def aggregate_window_data(
|
||||||
self,
|
self,
|
||||||
window_data: list[dict],
|
window_data: list[dict],
|
||||||
value_type: str,
|
value_type: str,
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,12 @@ The calculator provides smart defaults:
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from .base import BaseCalculator # Constants
|
from .base import TibberPricesBaseCalculator # Constants
|
||||||
|
|
||||||
PROGRESS_GRACE_PERIOD_SECONDS = 60 # Show 100% for 1 minute after period ends
|
PROGRESS_GRACE_PERIOD_SECONDS = 60 # Show 100% for 1 minute after period ends
|
||||||
|
|
||||||
|
|
||||||
class TimingCalculator(BaseCalculator):
|
class TibberPricesTimingCalculator(TibberPricesBaseCalculator):
|
||||||
"""
|
"""
|
||||||
Calculator for period timing sensors.
|
Calculator for period timing sensors.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ from custom_components.tibber_prices.utils.price import (
|
||||||
find_price_data_for_interval,
|
find_price_data_for_interval,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .base import BaseCalculator
|
from .base import TibberPricesBaseCalculator
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator import (
|
from custom_components.tibber_prices.coordinator import (
|
||||||
|
|
@ -32,7 +32,7 @@ if TYPE_CHECKING:
|
||||||
MIN_HOURS_FOR_LATER_HALF = 3 # Minimum hours needed to calculate later half average
|
MIN_HOURS_FOR_LATER_HALF = 3 # Minimum hours needed to calculate later half average
|
||||||
|
|
||||||
|
|
||||||
class TrendCalculator(BaseCalculator):
|
class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
||||||
"""
|
"""
|
||||||
Calculator for price trend sensors.
|
Calculator for price trend sensors.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,13 @@ from custom_components.tibber_prices.sensor.attributes import (
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.utils.price import calculate_volatility_level
|
from custom_components.tibber_prices.utils.price import calculate_volatility_level
|
||||||
|
|
||||||
from .base import BaseCalculator
|
from .base import TibberPricesBaseCalculator
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class VolatilityCalculator(BaseCalculator):
|
class TibberPricesVolatilityCalculator(TibberPricesBaseCalculator):
|
||||||
"""
|
"""
|
||||||
Calculator for price volatility analysis.
|
Calculator for price volatility analysis.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from custom_components.tibber_prices.entity_utils import get_price_value
|
from custom_components.tibber_prices.entity_utils import get_price_value
|
||||||
|
|
||||||
from .base import BaseCalculator
|
from .base import TibberPricesBaseCalculator
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
|
||||||
class Window24hCalculator(BaseCalculator):
|
class TibberPricesWindow24hCalculator(TibberPricesBaseCalculator):
|
||||||
"""
|
"""
|
||||||
Calculator for 24-hour sliding window statistics.
|
Calculator for 24-hour sliding window statistics.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,10 @@ from custom_components.tibber_prices.entity_utils import (
|
||||||
find_rolling_hour_center_index,
|
find_rolling_hour_center_index,
|
||||||
get_price_value,
|
get_price_value,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.entity_utils.icons import IconContext, get_dynamic_icon
|
from custom_components.tibber_prices.entity_utils.icons import (
|
||||||
|
TibberPricesIconContext,
|
||||||
|
get_dynamic_icon,
|
||||||
|
)
|
||||||
from custom_components.tibber_prices.utils.average import (
|
from custom_components.tibber_prices.utils.average import (
|
||||||
calculate_next_n_hours_avg,
|
calculate_next_n_hours_avg,
|
||||||
)
|
)
|
||||||
|
|
@ -50,14 +53,14 @@ from .attributes import (
|
||||||
get_prices_for_volatility,
|
get_prices_for_volatility,
|
||||||
)
|
)
|
||||||
from .calculators import (
|
from .calculators import (
|
||||||
DailyStatCalculator,
|
TibberPricesDailyStatCalculator,
|
||||||
IntervalCalculator,
|
TibberPricesIntervalCalculator,
|
||||||
MetadataCalculator,
|
TibberPricesMetadataCalculator,
|
||||||
RollingHourCalculator,
|
TibberPricesRollingHourCalculator,
|
||||||
TimingCalculator,
|
TibberPricesTimingCalculator,
|
||||||
TrendCalculator,
|
TibberPricesTrendCalculator,
|
||||||
VolatilityCalculator,
|
TibberPricesVolatilityCalculator,
|
||||||
Window24hCalculator,
|
TibberPricesWindow24hCalculator,
|
||||||
)
|
)
|
||||||
from .chart_data import (
|
from .chart_data import (
|
||||||
build_chart_data_attributes,
|
build_chart_data_attributes,
|
||||||
|
|
@ -73,7 +76,7 @@ if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator import (
|
from custom_components.tibber_prices.coordinator import (
|
||||||
TibberPricesDataUpdateCoordinator,
|
TibberPricesDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
HOURS_IN_DAY = 24
|
HOURS_IN_DAY = 24
|
||||||
LAST_HOUR_OF_DAY = 23
|
LAST_HOUR_OF_DAY = 23
|
||||||
|
|
@ -95,14 +98,14 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{entity_description.key}"
|
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{entity_description.key}"
|
||||||
self._attr_has_entity_name = True
|
self._attr_has_entity_name = True
|
||||||
# Instantiate calculators
|
# Instantiate calculators
|
||||||
self._metadata_calculator = MetadataCalculator(coordinator)
|
self._metadata_calculator = TibberPricesMetadataCalculator(coordinator)
|
||||||
self._volatility_calculator = VolatilityCalculator(coordinator)
|
self._volatility_calculator = TibberPricesVolatilityCalculator(coordinator)
|
||||||
self._window_24h_calculator = Window24hCalculator(coordinator)
|
self._window_24h_calculator = TibberPricesWindow24hCalculator(coordinator)
|
||||||
self._rolling_hour_calculator = RollingHourCalculator(coordinator)
|
self._rolling_hour_calculator = TibberPricesRollingHourCalculator(coordinator)
|
||||||
self._daily_stat_calculator = DailyStatCalculator(coordinator)
|
self._daily_stat_calculator = TibberPricesDailyStatCalculator(coordinator)
|
||||||
self._interval_calculator = IntervalCalculator(coordinator)
|
self._interval_calculator = TibberPricesIntervalCalculator(coordinator)
|
||||||
self._timing_calculator = TimingCalculator(coordinator)
|
self._timing_calculator = TibberPricesTimingCalculator(coordinator)
|
||||||
self._trend_calculator = TrendCalculator(coordinator)
|
self._trend_calculator = TibberPricesTrendCalculator(coordinator)
|
||||||
self._value_getter: Callable | None = self._get_value_getter()
|
self._value_getter: Callable | None = self._get_value_getter()
|
||||||
self._time_sensitive_remove_listener: Callable | None = None
|
self._time_sensitive_remove_listener: Callable | None = None
|
||||||
self._minute_update_remove_listener: Callable | None = None
|
self._minute_update_remove_listener: Callable | None = None
|
||||||
|
|
@ -146,12 +149,12 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
self._minute_update_remove_listener = None
|
self._minute_update_remove_listener = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_time_sensitive_update(self, time_service: TimeService) -> None:
|
def _handle_time_sensitive_update(self, time_service: TibberPricesTimeService) -> None:
|
||||||
"""
|
"""
|
||||||
Handle time-sensitive update from coordinator.
|
Handle time-sensitive update from coordinator.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
time_service: TimeService instance with reference time for this update cycle
|
time_service: TibberPricesTimeService instance with reference time for this update cycle
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Store TimeService from Timer #2 for calculations during this update cycle
|
# Store TimeService from Timer #2 for calculations during this update cycle
|
||||||
|
|
@ -166,12 +169,12 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_minute_update(self, time_service: TimeService) -> None:
|
def _handle_minute_update(self, time_service: TibberPricesTimeService) -> None:
|
||||||
"""
|
"""
|
||||||
Handle minute-by-minute update from coordinator.
|
Handle minute-by-minute update from coordinator.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
time_service: TimeService instance with reference time for this update cycle
|
time_service: TibberPricesTimeService instance with reference time for this update cycle
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Store TimeService from Timer #3 for calculations during this update cycle
|
# Store TimeService from Timer #3 for calculations during this update cycle
|
||||||
|
|
@ -267,7 +270,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
if not window_data:
|
if not window_data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self._aggregate_window_data(window_data, value_type)
|
return self._rolling_hour_calculator.aggregate_window_data(window_data, value_type)
|
||||||
|
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
# INTERVAL-BASED VALUE METHODS
|
# INTERVAL-BASED VALUE METHODS
|
||||||
|
|
@ -470,7 +473,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
return en_translations["sensor"]["current_interval_price_rating"]["price_levels"][level]
|
return en_translations["sensor"]["current_interval_price_rating"]["price_levels"][level]
|
||||||
return level
|
return level
|
||||||
|
|
||||||
def _get_next_avg_n_hours_value(self, *, hours: int) -> float | None:
|
def _get_next_avg_n_hours_value(self, hours: int) -> float | None:
|
||||||
"""
|
"""
|
||||||
Get average price for next N hours starting from next interval.
|
Get average price for next N hours starting from next interval.
|
||||||
|
|
||||||
|
|
@ -803,7 +806,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
icon = get_dynamic_icon(
|
icon = get_dynamic_icon(
|
||||||
key=key,
|
key=key,
|
||||||
value=value,
|
value=value,
|
||||||
context=IconContext(
|
context=TibberPricesIconContext(
|
||||||
coordinator_data=self.coordinator.data,
|
coordinator_data=self.coordinator.data,
|
||||||
period_is_active_callback=period_is_active_callback,
|
period_is_active_callback=period_is_active_callback,
|
||||||
time=self.coordinator.time,
|
time=self.coordinator.time,
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ from datetime import timedelta
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
from custom_components.tibber_prices.entity_utils.helpers import get_price_value
|
from custom_components.tibber_prices.entity_utils.helpers import get_price_value
|
||||||
from custom_components.tibber_prices.utils.price import (
|
from custom_components.tibber_prices.utils.price import (
|
||||||
|
|
@ -134,7 +134,7 @@ def get_hourly_price_value(
|
||||||
*,
|
*,
|
||||||
hour_offset: int,
|
hour_offset: int,
|
||||||
in_euro: bool,
|
in_euro: bool,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
"""
|
"""
|
||||||
Get price for current hour or with offset.
|
Get price for current hour or with offset.
|
||||||
|
|
@ -146,7 +146,7 @@ def get_hourly_price_value(
|
||||||
price_info: Price information dict with 'today' and 'tomorrow' keys
|
price_info: Price information dict with 'today' and 'tomorrow' keys
|
||||||
hour_offset: Hour offset from current time (positive=future, negative=past)
|
hour_offset: Hour offset from current time (positive=future, negative=past)
|
||||||
in_euro: If True, return price in major currency (EUR), else minor (cents/øre)
|
in_euro: If True, return price in major currency (EUR), else minor (cents/øre)
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Price value, or None if not found
|
Price value, or None if not found
|
||||||
|
|
|
||||||
|
|
@ -15,29 +15,30 @@ from custom_components.tibber_prices.utils.average import (
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from custom_components.tibber_prices.sensor.calculators.daily_stat import DailyStatCalculator
|
from custom_components.tibber_prices.sensor.calculators.daily_stat import TibberPricesDailyStatCalculator
|
||||||
from custom_components.tibber_prices.sensor.calculators.interval import IntervalCalculator
|
from custom_components.tibber_prices.sensor.calculators.interval import TibberPricesIntervalCalculator
|
||||||
from custom_components.tibber_prices.sensor.calculators.metadata import MetadataCalculator
|
from custom_components.tibber_prices.sensor.calculators.metadata import TibberPricesMetadataCalculator
|
||||||
from custom_components.tibber_prices.sensor.calculators.rolling_hour import RollingHourCalculator
|
from custom_components.tibber_prices.sensor.calculators.rolling_hour import TibberPricesRollingHourCalculator
|
||||||
from custom_components.tibber_prices.sensor.calculators.timing import TimingCalculator
|
from custom_components.tibber_prices.sensor.calculators.timing import TibberPricesTimingCalculator
|
||||||
from custom_components.tibber_prices.sensor.calculators.trend import TrendCalculator
|
from custom_components.tibber_prices.sensor.calculators.trend import TibberPricesTrendCalculator
|
||||||
from custom_components.tibber_prices.sensor.calculators.volatility import VolatilityCalculator
|
from custom_components.tibber_prices.sensor.calculators.volatility import TibberPricesVolatilityCalculator
|
||||||
from custom_components.tibber_prices.sensor.calculators.window_24h import Window24hCalculator
|
from custom_components.tibber_prices.sensor.calculators.window_24h import TibberPricesWindow24hCalculator
|
||||||
|
|
||||||
|
|
||||||
def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parameters
|
def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parameters
|
||||||
interval_calculator: IntervalCalculator,
|
interval_calculator: TibberPricesIntervalCalculator,
|
||||||
rolling_hour_calculator: RollingHourCalculator,
|
rolling_hour_calculator: TibberPricesRollingHourCalculator,
|
||||||
daily_stat_calculator: DailyStatCalculator,
|
daily_stat_calculator: TibberPricesDailyStatCalculator,
|
||||||
window_24h_calculator: Window24hCalculator,
|
window_24h_calculator: TibberPricesWindow24hCalculator,
|
||||||
trend_calculator: TrendCalculator,
|
trend_calculator: TibberPricesTrendCalculator,
|
||||||
timing_calculator: TimingCalculator,
|
timing_calculator: TibberPricesTimingCalculator,
|
||||||
volatility_calculator: VolatilityCalculator,
|
volatility_calculator: TibberPricesVolatilityCalculator,
|
||||||
metadata_calculator: MetadataCalculator,
|
metadata_calculator: TibberPricesMetadataCalculator,
|
||||||
get_next_avg_n_hours_value: Callable[[int], float | None],
|
get_next_avg_n_hours_value: Callable[[int], float | None],
|
||||||
get_price_forecast_value: Callable[[], str | None],
|
get_price_forecast_value: Callable[[], str | None],
|
||||||
get_data_timestamp: Callable[[], str | None],
|
get_data_timestamp: Callable[[], datetime | None],
|
||||||
get_chart_data_export_value: Callable[[], str | None],
|
get_chart_data_export_value: Callable[[], str | None],
|
||||||
) -> dict[str, Callable]:
|
) -> dict[str, Callable]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -154,7 +155,7 @@ def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parame
|
||||||
day="tomorrow", value_type="rating"
|
day="tomorrow", value_type="rating"
|
||||||
),
|
),
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# 24H WINDOW SENSORS (trailing/leading from current) - via Window24hCalculator
|
# 24H WINDOW SENSORS (trailing/leading from current) - via TibberPricesWindow24hCalculator
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# Trailing and leading average sensors
|
# Trailing and leading average sensors
|
||||||
"trailing_price_average": lambda: window_24h_calculator.get_24h_window_value(
|
"trailing_price_average": lambda: window_24h_calculator.get_24h_window_value(
|
||||||
|
|
@ -180,14 +181,14 @@ def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parame
|
||||||
# FUTURE FORECAST SENSORS
|
# FUTURE FORECAST SENSORS
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# Future average sensors (next N hours from next interval)
|
# Future average sensors (next N hours from next interval)
|
||||||
"next_avg_1h": lambda: get_next_avg_n_hours_value(hours=1),
|
"next_avg_1h": lambda: get_next_avg_n_hours_value(1),
|
||||||
"next_avg_2h": lambda: get_next_avg_n_hours_value(hours=2),
|
"next_avg_2h": lambda: get_next_avg_n_hours_value(2),
|
||||||
"next_avg_3h": lambda: get_next_avg_n_hours_value(hours=3),
|
"next_avg_3h": lambda: get_next_avg_n_hours_value(3),
|
||||||
"next_avg_4h": lambda: get_next_avg_n_hours_value(hours=4),
|
"next_avg_4h": lambda: get_next_avg_n_hours_value(4),
|
||||||
"next_avg_5h": lambda: get_next_avg_n_hours_value(hours=5),
|
"next_avg_5h": lambda: get_next_avg_n_hours_value(5),
|
||||||
"next_avg_6h": lambda: get_next_avg_n_hours_value(hours=6),
|
"next_avg_6h": lambda: get_next_avg_n_hours_value(6),
|
||||||
"next_avg_8h": lambda: get_next_avg_n_hours_value(hours=8),
|
"next_avg_8h": lambda: get_next_avg_n_hours_value(8),
|
||||||
"next_avg_12h": lambda: get_next_avg_n_hours_value(hours=12),
|
"next_avg_12h": lambda: get_next_avg_n_hours_value(12),
|
||||||
# Current and next trend change sensors
|
# Current and next trend change sensors
|
||||||
"current_price_trend": trend_calculator.get_current_trend_value,
|
"current_price_trend": trend_calculator.get_current_trend_value,
|
||||||
"next_price_trend_change": trend_calculator.get_next_trend_change_value,
|
"next_price_trend_change": trend_calculator.get_next_trend_change_value,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from datetime import datetime, timedelta
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
|
|
||||||
def calculate_trailing_24h_avg(all_prices: list[dict], interval_start: datetime) -> float:
|
def calculate_trailing_24h_avg(all_prices: list[dict], interval_start: datetime) -> float:
|
||||||
|
|
@ -16,7 +16,7 @@ def calculate_trailing_24h_avg(all_prices: list[dict], interval_start: datetime)
|
||||||
Args:
|
Args:
|
||||||
all_prices: List of all price data (yesterday, today, tomorrow combined)
|
all_prices: List of all price data (yesterday, today, tomorrow combined)
|
||||||
interval_start: Start time of the interval to calculate average for
|
interval_start: Start time of the interval to calculate average for
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Average price for the 24 hours preceding the interval (not including the interval itself)
|
Average price for the 24 hours preceding the interval (not including the interval itself)
|
||||||
|
|
@ -49,7 +49,7 @@ def calculate_leading_24h_avg(all_prices: list[dict], interval_start: datetime)
|
||||||
Args:
|
Args:
|
||||||
all_prices: List of all price data (yesterday, today, tomorrow combined)
|
all_prices: List of all price data (yesterday, today, tomorrow combined)
|
||||||
interval_start: Start time of the interval to calculate average for
|
interval_start: Start time of the interval to calculate average for
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Average price for up to 24 hours following the interval (including the interval itself)
|
Average price for up to 24 hours following the interval (including the interval itself)
|
||||||
|
|
@ -78,14 +78,14 @@ def calculate_leading_24h_avg(all_prices: list[dict], interval_start: datetime)
|
||||||
def calculate_current_trailing_avg(
|
def calculate_current_trailing_avg(
|
||||||
coordinator_data: dict,
|
coordinator_data: dict,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
"""
|
"""
|
||||||
Calculate the trailing 24-hour average for the current time.
|
Calculate the trailing 24-hour average for the current time.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
coordinator_data: The coordinator data containing priceInfo
|
coordinator_data: The coordinator data containing priceInfo
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Current trailing 24-hour average price, or None if unavailable
|
Current trailing 24-hour average price, or None if unavailable
|
||||||
|
|
@ -110,14 +110,14 @@ def calculate_current_trailing_avg(
|
||||||
def calculate_current_leading_avg(
|
def calculate_current_leading_avg(
|
||||||
coordinator_data: dict,
|
coordinator_data: dict,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
"""
|
"""
|
||||||
Calculate the leading 24-hour average for the current time.
|
Calculate the leading 24-hour average for the current time.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
coordinator_data: The coordinator data containing priceInfo
|
coordinator_data: The coordinator data containing priceInfo
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Current leading 24-hour average price, or None if unavailable
|
Current leading 24-hour average price, or None if unavailable
|
||||||
|
|
@ -143,7 +143,7 @@ def calculate_trailing_24h_min(
|
||||||
all_prices: list[dict],
|
all_prices: list[dict],
|
||||||
interval_start: datetime,
|
interval_start: datetime,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate trailing 24-hour minimum price for a given interval.
|
Calculate trailing 24-hour minimum price for a given interval.
|
||||||
|
|
@ -151,7 +151,7 @@ def calculate_trailing_24h_min(
|
||||||
Args:
|
Args:
|
||||||
all_prices: List of all price data (yesterday, today, tomorrow combined)
|
all_prices: List of all price data (yesterday, today, tomorrow combined)
|
||||||
interval_start: Start time of the interval to calculate minimum for
|
interval_start: Start time of the interval to calculate minimum for
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Minimum price for the 24 hours preceding the interval (not including the interval itself)
|
Minimum price for the 24 hours preceding the interval (not including the interval itself)
|
||||||
|
|
@ -181,7 +181,7 @@ def calculate_trailing_24h_max(
|
||||||
all_prices: list[dict],
|
all_prices: list[dict],
|
||||||
interval_start: datetime,
|
interval_start: datetime,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate trailing 24-hour maximum price for a given interval.
|
Calculate trailing 24-hour maximum price for a given interval.
|
||||||
|
|
@ -189,7 +189,7 @@ def calculate_trailing_24h_max(
|
||||||
Args:
|
Args:
|
||||||
all_prices: List of all price data (yesterday, today, tomorrow combined)
|
all_prices: List of all price data (yesterday, today, tomorrow combined)
|
||||||
interval_start: Start time of the interval to calculate maximum for
|
interval_start: Start time of the interval to calculate maximum for
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Maximum price for the 24 hours preceding the interval (not including the interval itself)
|
Maximum price for the 24 hours preceding the interval (not including the interval itself)
|
||||||
|
|
@ -219,7 +219,7 @@ def calculate_leading_24h_min(
|
||||||
all_prices: list[dict],
|
all_prices: list[dict],
|
||||||
interval_start: datetime,
|
interval_start: datetime,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate leading 24-hour minimum price for a given interval.
|
Calculate leading 24-hour minimum price for a given interval.
|
||||||
|
|
@ -227,7 +227,7 @@ def calculate_leading_24h_min(
|
||||||
Args:
|
Args:
|
||||||
all_prices: List of all price data (yesterday, today, tomorrow combined)
|
all_prices: List of all price data (yesterday, today, tomorrow combined)
|
||||||
interval_start: Start time of the interval to calculate minimum for
|
interval_start: Start time of the interval to calculate minimum for
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Minimum price for up to 24 hours following the interval (including the interval itself)
|
Minimum price for up to 24 hours following the interval (including the interval itself)
|
||||||
|
|
@ -257,7 +257,7 @@ def calculate_leading_24h_max(
|
||||||
all_prices: list[dict],
|
all_prices: list[dict],
|
||||||
interval_start: datetime,
|
interval_start: datetime,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate leading 24-hour maximum price for a given interval.
|
Calculate leading 24-hour maximum price for a given interval.
|
||||||
|
|
@ -265,7 +265,7 @@ def calculate_leading_24h_max(
|
||||||
Args:
|
Args:
|
||||||
all_prices: List of all price data (yesterday, today, tomorrow combined)
|
all_prices: List of all price data (yesterday, today, tomorrow combined)
|
||||||
interval_start: Start time of the interval to calculate maximum for
|
interval_start: Start time of the interval to calculate maximum for
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Maximum price for up to 24 hours following the interval (including the interval itself)
|
Maximum price for up to 24 hours following the interval (including the interval itself)
|
||||||
|
|
@ -294,14 +294,14 @@ def calculate_leading_24h_max(
|
||||||
def calculate_current_trailing_min(
|
def calculate_current_trailing_min(
|
||||||
coordinator_data: dict,
|
coordinator_data: dict,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
"""
|
"""
|
||||||
Calculate the trailing 24-hour minimum for the current time.
|
Calculate the trailing 24-hour minimum for the current time.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
coordinator_data: The coordinator data containing priceInfo
|
coordinator_data: The coordinator data containing priceInfo
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Current trailing 24-hour minimum price, or None if unavailable
|
Current trailing 24-hour minimum price, or None if unavailable
|
||||||
|
|
@ -326,14 +326,14 @@ def calculate_current_trailing_min(
|
||||||
def calculate_current_trailing_max(
|
def calculate_current_trailing_max(
|
||||||
coordinator_data: dict,
|
coordinator_data: dict,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
"""
|
"""
|
||||||
Calculate the trailing 24-hour maximum for the current time.
|
Calculate the trailing 24-hour maximum for the current time.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
coordinator_data: The coordinator data containing priceInfo
|
coordinator_data: The coordinator data containing priceInfo
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Current trailing 24-hour maximum price, or None if unavailable
|
Current trailing 24-hour maximum price, or None if unavailable
|
||||||
|
|
@ -358,14 +358,14 @@ def calculate_current_trailing_max(
|
||||||
def calculate_current_leading_min(
|
def calculate_current_leading_min(
|
||||||
coordinator_data: dict,
|
coordinator_data: dict,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
"""
|
"""
|
||||||
Calculate the leading 24-hour minimum for the current time.
|
Calculate the leading 24-hour minimum for the current time.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
coordinator_data: The coordinator data containing priceInfo
|
coordinator_data: The coordinator data containing priceInfo
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Current leading 24-hour minimum price, or None if unavailable
|
Current leading 24-hour minimum price, or None if unavailable
|
||||||
|
|
@ -390,14 +390,14 @@ def calculate_current_leading_min(
|
||||||
def calculate_current_leading_max(
|
def calculate_current_leading_max(
|
||||||
coordinator_data: dict,
|
coordinator_data: dict,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
"""
|
"""
|
||||||
Calculate the leading 24-hour maximum for the current time.
|
Calculate the leading 24-hour maximum for the current time.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
coordinator_data: The coordinator data containing priceInfo
|
coordinator_data: The coordinator data containing priceInfo
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Current leading 24-hour maximum price, or None if unavailable
|
Current leading 24-hour maximum price, or None if unavailable
|
||||||
|
|
@ -423,7 +423,7 @@ def calculate_next_n_hours_avg(
|
||||||
coordinator_data: dict,
|
coordinator_data: dict,
|
||||||
hours: int,
|
hours: int,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
"""
|
"""
|
||||||
Calculate average price for the next N hours starting from the next interval.
|
Calculate average price for the next N hours starting from the next interval.
|
||||||
|
|
@ -434,7 +434,7 @@ def calculate_next_n_hours_avg(
|
||||||
Args:
|
Args:
|
||||||
coordinator_data: The coordinator data containing priceInfo
|
coordinator_data: The coordinator data containing priceInfo
|
||||||
hours: Number of hours to look ahead (1, 2, 3, 4, 5, 6, 8, 12, etc.)
|
hours: Number of hours to look ahead (1, 2, 3, 4, 5, 6, 8, 12, etc.)
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Average price for the next N hours, or None if insufficient data
|
Average price for the next N hours, or None if insufficient data
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from datetime import datetime, timedelta
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import (
|
||||||
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
|
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
|
||||||
|
|
@ -281,7 +281,7 @@ def enrich_price_info_with_differences(
|
||||||
price_info: Dictionary with 'yesterday', 'today', 'tomorrow' keys
|
price_info: Dictionary with 'yesterday', 'today', 'tomorrow' keys
|
||||||
threshold_low: Low threshold percentage for rating_level (defaults to -10)
|
threshold_low: Low threshold percentage for rating_level (defaults to -10)
|
||||||
threshold_high: High threshold percentage for rating_level (defaults to 10)
|
threshold_high: High threshold percentage for rating_level (defaults to 10)
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Updated price_info dict with 'difference' and 'rating_level' added
|
Updated price_info dict with 'difference' and 'rating_level' added
|
||||||
|
|
@ -324,7 +324,7 @@ def find_price_data_for_interval(
|
||||||
price_info: Any,
|
price_info: Any,
|
||||||
target_time: datetime,
|
target_time: datetime,
|
||||||
*,
|
*,
|
||||||
time: TimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""
|
"""
|
||||||
Find the price data for a specific 15-minute interval timestamp.
|
Find the price data for a specific 15-minute interval timestamp.
|
||||||
|
|
@ -332,7 +332,7 @@ def find_price_data_for_interval(
|
||||||
Args:
|
Args:
|
||||||
price_info: The price info dictionary from Tibber API
|
price_info: The price info dictionary from Tibber API
|
||||||
target_time: The target timestamp to find price data for
|
target_time: The target timestamp to find price data for
|
||||||
time: TimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Price data dict if found, None otherwise
|
Price data dict if found, None otherwise
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue