mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
fix(services): use injected now in resolve_search_range day offset
_resolve_time_with_day_offset() was calling dt_util.now() internally instead of using the injected now parameter. This caused incorrect date calculations in tests and any caller that passes a specific reference time. Also add missing price_rank_* sensor keys to TIME_SENSITIVE_ENTITY_KEYS in coordinator/constants.py so quarter-hour refresh is registered for all 11 price rank sensors (current/next/previous interval and hour variants). Rename dt as dt_utils → dt as dt_util (ICN001) across 11 files to follow the project-wide import alias convention. Apply ruff auto-fixes for import ordering and collapsing single-item imports throughout the codebase. Released-Bug: no
This commit is contained in:
parent
07788a57ea
commit
1d065b11cd
83 changed files with 250 additions and 503 deletions
|
|
@ -4,16 +4,16 @@ from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
from homeassistant.util import dt as dt_utils
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
TibberPricesApiClientAuthenticationError,
|
TibberPricesApiClientAuthenticationError,
|
||||||
|
|
@ -21,12 +21,7 @@ from .exceptions import (
|
||||||
TibberPricesApiClientError,
|
TibberPricesApiClientError,
|
||||||
TibberPricesApiClientPermissionError,
|
TibberPricesApiClientPermissionError,
|
||||||
)
|
)
|
||||||
from .helpers import (
|
from .helpers import flatten_price_info, prepare_headers, verify_graphql_response, verify_response_or_raise
|
||||||
flatten_price_info,
|
|
||||||
prepare_headers,
|
|
||||||
verify_graphql_response,
|
|
||||||
verify_response_or_raise,
|
|
||||||
)
|
|
||||||
from .queries import TibberPricesQueryType
|
from .queries import TibberPricesQueryType
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -163,9 +158,7 @@ class TibberPricesApiClient:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Import here to avoid circular dependency (interval_pool imports TibberPricesApiClient)
|
# Import here to avoid circular dependency (interval_pool imports TibberPricesApiClient)
|
||||||
from custom_components.tibber_prices.interval_pool import ( # noqa: PLC0415
|
from custom_components.tibber_prices.interval_pool import get_price_intervals_for_range # noqa: PLC0415
|
||||||
get_price_intervals_for_range,
|
|
||||||
)
|
|
||||||
|
|
||||||
price_info = await get_price_intervals_for_range(
|
price_info = await get_price_intervals_for_range(
|
||||||
api_client=self,
|
api_client=self,
|
||||||
|
|
@ -581,7 +574,7 @@ class TibberPricesApiClient:
|
||||||
"""
|
"""
|
||||||
Calculate day before yesterday midnight in home's timezone.
|
Calculate day before yesterday midnight in home's timezone.
|
||||||
|
|
||||||
CRITICAL: Uses REAL TIME (dt_utils.now()), NOT TimeService.now().
|
CRITICAL: Uses REAL TIME (dt_util.now()), NOT TimeService.now().
|
||||||
This ensures API boundary calculations are based on actual current time,
|
This ensures API boundary calculations are based on actual current time,
|
||||||
not simulated time from TimeService.
|
not simulated time from TimeService.
|
||||||
|
|
||||||
|
|
@ -594,7 +587,7 @@ class TibberPricesApiClient:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Get current REAL time (not TimeService)
|
# Get current REAL time (not TimeService)
|
||||||
now = dt_utils.now()
|
now = dt_util.now()
|
||||||
|
|
||||||
# Convert to home's timezone or fallback to HA system timezone
|
# Convert to home's timezone or fallback to HA system timezone
|
||||||
if home_timezone:
|
if home_timezone:
|
||||||
|
|
@ -607,10 +600,10 @@ class TibberPricesApiClient:
|
||||||
home_timezone,
|
home_timezone,
|
||||||
error,
|
error,
|
||||||
)
|
)
|
||||||
now_in_home_tz = dt_utils.as_local(now)
|
now_in_home_tz = dt_util.as_local(now)
|
||||||
else:
|
else:
|
||||||
# Fallback to HA system timezone
|
# Fallback to HA system timezone
|
||||||
now_in_home_tz = dt_utils.as_local(now)
|
now_in_home_tz = dt_util.as_local(now)
|
||||||
|
|
||||||
# Calculate day before yesterday midnight
|
# Calculate day before yesterday midnight
|
||||||
return (now_in_home_tz - timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)
|
return (now_in_home_tz - timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
|
@ -640,7 +633,7 @@ class TibberPricesApiClient:
|
||||||
Timezone-aware datetime object.
|
Timezone-aware datetime object.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return dt_utils.parse_datetime(timestamp_str) or dt_utils.now()
|
return dt_util.parse_datetime(timestamp_str) or dt_util.now()
|
||||||
|
|
||||||
def _calculate_cursor_for_home(self, home_timezone: str | None) -> str:
|
def _calculate_cursor_for_home(self, home_timezone: str | None) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -492,7 +492,7 @@ def build_final_attributes_simple(
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
async def build_async_extra_state_attributes( # noqa: PLR0913
|
async def build_async_extra_state_attributes(
|
||||||
entity_key: str,
|
entity_key: str,
|
||||||
translation_key: str | None,
|
translation_key: str | None,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|
@ -555,7 +555,7 @@ async def build_async_extra_state_attributes( # noqa: PLR0913
|
||||||
return attributes or None
|
return attributes or None
|
||||||
|
|
||||||
|
|
||||||
def build_sync_extra_state_attributes( # noqa: PLR0913
|
def build_sync_extra_state_attributes(
|
||||||
entity_key: str,
|
entity_key: str,
|
||||||
translation_key: str | None,
|
translation_key: str | None,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,7 @@ from custom_components.tibber_prices.coordinator.core import get_connection_stat
|
||||||
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
||||||
from custom_components.tibber_prices.entity import TibberPricesEntity
|
from custom_components.tibber_prices.entity import TibberPricesEntity
|
||||||
from custom_components.tibber_prices.entity_utils import get_binary_sensor_icon
|
from custom_components.tibber_prices.entity_utils import get_binary_sensor_icon
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorEntityDescription
|
||||||
BinarySensorEntity,
|
|
||||||
BinarySensorEntityDescription,
|
|
||||||
)
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
|
@ -27,9 +24,7 @@ from .attributes import (
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
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 TibberPricesTimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import BinarySensorDeviceClass, BinarySensorEntityDescription
|
||||||
BinarySensorDeviceClass,
|
|
||||||
BinarySensorEntityDescription,
|
|
||||||
)
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
|
|
||||||
# Period lookahead removed - icons show "waiting" state if ANY future periods exist
|
# Period lookahead removed - icons show "waiting" state if ANY future periods exist
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@ The actual implementation is in the config_flow_handlers package.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from .config_flow_handlers.options_flow import (
|
from .config_flow_handlers.options_flow import TibberPricesOptionsFlowHandler as OptionsFlowHandler
|
||||||
TibberPricesOptionsFlowHandler as OptionsFlowHandler,
|
|
||||||
)
|
|
||||||
from .config_flow_handlers.schemas import (
|
from .config_flow_handlers.schemas import (
|
||||||
get_best_price_schema,
|
get_best_price_schema,
|
||||||
get_options_init_schema,
|
get_options_init_schema,
|
||||||
|
|
@ -23,9 +21,7 @@ from .config_flow_handlers.schemas import (
|
||||||
get_user_schema,
|
get_user_schema,
|
||||||
get_volatility_schema,
|
get_volatility_schema,
|
||||||
)
|
)
|
||||||
from .config_flow_handlers.subentry_flow import (
|
from .config_flow_handlers.subentry_flow import TibberPricesSubentryFlowHandler as SubentryFlowHandler
|
||||||
TibberPricesSubentryFlowHandler as SubentryFlowHandler,
|
|
||||||
)
|
|
||||||
from .config_flow_handlers.user_flow import TibberPricesConfigFlowHandler as ConfigFlow
|
from .config_flow_handlers.user_flow import TibberPricesConfigFlowHandler as ConfigFlow
|
||||||
from .config_flow_handlers.validators import (
|
from .config_flow_handlers.validators import (
|
||||||
TibberPricesCannotConnectError,
|
TibberPricesCannotConnectError,
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,7 @@ Supporting modules:
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
# Phase 3: Import flow handlers from their new modular structure
|
# Phase 3: Import flow handlers from their new modular structure
|
||||||
from custom_components.tibber_prices.config_flow_handlers.options_flow import (
|
from custom_components.tibber_prices.config_flow_handlers.options_flow import TibberPricesOptionsFlowHandler
|
||||||
TibberPricesOptionsFlowHandler,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.config_flow_handlers.schemas import (
|
from custom_components.tibber_prices.config_flow_handlers.schemas import (
|
||||||
get_best_price_schema,
|
get_best_price_schema,
|
||||||
get_options_init_schema,
|
get_options_init_schema,
|
||||||
|
|
@ -36,12 +34,8 @@ from custom_components.tibber_prices.config_flow_handlers.schemas import (
|
||||||
get_user_schema,
|
get_user_schema,
|
||||||
get_volatility_schema,
|
get_volatility_schema,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.config_flow_handlers.subentry_flow import (
|
from custom_components.tibber_prices.config_flow_handlers.subentry_flow import TibberPricesSubentryFlowHandler
|
||||||
TibberPricesSubentryFlowHandler,
|
from custom_components.tibber_prices.config_flow_handlers.user_flow import TibberPricesConfigFlowHandler
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.config_flow_handlers.user_flow import (
|
|
||||||
TibberPricesConfigFlowHandler,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.config_flow_handlers.validators import (
|
from custom_components.tibber_prices.config_flow_handlers.validators import (
|
||||||
TibberPricesCannotConnectError,
|
TibberPricesCannotConnectError,
|
||||||
TibberPricesInvalidAuthError,
|
TibberPricesInvalidAuthError,
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ ConfigOverrides = dict[str, dict[str, Any]]
|
||||||
|
|
||||||
def is_field_overridden(
|
def is_field_overridden(
|
||||||
config_key: str,
|
config_key: str,
|
||||||
config_section: str, # noqa: ARG001 - kept for API compatibility
|
config_section: str,
|
||||||
overrides: ConfigOverrides | None,
|
overrides: ConfigOverrides | None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@ from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from custom_components.tibber_prices.config_flow_handlers.options_flow import (
|
from custom_components.tibber_prices.config_flow_handlers.options_flow import TibberPricesOptionsFlowHandler
|
||||||
TibberPricesOptionsFlowHandler,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.config_flow_handlers.schemas import (
|
from custom_components.tibber_prices.config_flow_handlers.schemas import (
|
||||||
get_reauth_confirm_schema,
|
get_reauth_confirm_schema,
|
||||||
get_select_home_schema,
|
get_select_home_schema,
|
||||||
|
|
@ -20,26 +18,11 @@ from custom_components.tibber_prices.config_flow_handlers.validators import (
|
||||||
TibberPricesInvalidAuthError,
|
TibberPricesInvalidAuthError,
|
||||||
validate_api_token,
|
validate_api_token,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import DOMAIN, LOGGER, get_default_options, get_translation
|
||||||
DOMAIN,
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow
|
||||||
LOGGER,
|
|
||||||
get_default_options,
|
|
||||||
get_translation,
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import (
|
|
||||||
ConfigEntry,
|
|
||||||
ConfigFlow,
|
|
||||||
ConfigFlowResult,
|
|
||||||
OptionsFlow,
|
|
||||||
)
|
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.selector import (
|
from homeassistant.helpers.selector import SelectOptionDict, SelectSelector, SelectSelectorConfig, SelectSelectorMode
|
||||||
SelectOptionDict,
|
|
||||||
SelectSelector,
|
|
||||||
SelectSelectorConfig,
|
|
||||||
SelectSelectorMode,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from homeassistant.config_entries import ConfigSubentryFlow
|
from homeassistant.config_entries import ConfigSubentryFlow
|
||||||
|
|
@ -65,7 +48,7 @@ class TibberPricesConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
@callback
|
@callback
|
||||||
def async_get_supported_subentry_types(
|
def async_get_supported_subentry_types(
|
||||||
cls,
|
cls,
|
||||||
config_entry: ConfigEntry, # noqa: ARG003
|
config_entry: ConfigEntry,
|
||||||
) -> dict[str, type[ConfigSubentryFlow]]:
|
) -> dict[str, type[ConfigSubentryFlow]]:
|
||||||
"""Return subentries supported by this integration."""
|
"""Return subentries supported by this integration."""
|
||||||
# Temporarily disabled: Time-travel feature not yet fully implemented
|
# Temporarily disabled: Time-travel feature not yet fully implemented
|
||||||
|
|
@ -85,7 +68,7 @@ class TibberPricesConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
"""Return True if match_dict matches this flow."""
|
"""Return True if match_dict matches this flow."""
|
||||||
return bool(other_flow.get("domain") == DOMAIN)
|
return bool(other_flow.get("domain") == DOMAIN)
|
||||||
|
|
||||||
async def async_step_reauth(self, entry_data: dict[str, Any]) -> ConfigFlowResult: # noqa: ARG002
|
async def async_step_reauth(self, entry_data: dict[str, Any]) -> ConfigFlowResult:
|
||||||
"""Handle reauth flow when access token becomes invalid."""
|
"""Handle reauth flow when access token becomes invalid."""
|
||||||
entry_id = self.context.get("entry_id")
|
entry_id = self.context.get("entry_id")
|
||||||
if entry_id:
|
if entry_id:
|
||||||
|
|
@ -295,7 +278,7 @@ class TibberPricesConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
description_placeholders={"tibber_url": "https://developer.tibber.com"},
|
description_placeholders={"tibber_url": "https://developer.tibber.com"},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_select_home(self, user_input: dict | None = None) -> ConfigFlowResult: # noqa: PLR0911
|
async def async_step_select_home(self, user_input: dict | None = None) -> ConfigFlowResult:
|
||||||
"""Handle home selection during initial setup."""
|
"""Handle home selection during initial setup."""
|
||||||
homes = self._viewer.get("homes", []) if self._viewer else []
|
homes = self._viewer.get("homes", []) if self._viewer else []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,7 @@ Main components:
|
||||||
- period_handlers/: Period calculation sub-package
|
- period_handlers/: Period calculation sub-package
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .constants import (
|
from .constants import MINUTE_UPDATE_ENTITY_KEYS, STORAGE_VERSION, TIME_SENSITIVE_ENTITY_KEYS
|
||||||
MINUTE_UPDATE_ENTITY_KEYS,
|
|
||||||
STORAGE_VERSION,
|
|
||||||
TIME_SENSITIVE_ENTITY_KEYS,
|
|
||||||
)
|
|
||||||
from .core import TibberPricesDataUpdateCoordinator
|
from .core import TibberPricesDataUpdateCoordinator
|
||||||
from .time_service import TibberPricesTimeService
|
from .time_service import TibberPricesTimeService
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,18 @@ TIME_SENSITIVE_ENTITY_KEYS = frozenset(
|
||||||
"best_price_next_start_time",
|
"best_price_next_start_time",
|
||||||
"peak_price_end_time",
|
"peak_price_end_time",
|
||||||
"peak_price_next_start_time",
|
"peak_price_next_start_time",
|
||||||
|
# Price rank sensors (rank of current/next/previous interval within a day scope)
|
||||||
|
"current_interval_price_rank_today",
|
||||||
|
"current_interval_price_rank_tomorrow",
|
||||||
|
"current_interval_price_rank_today_tomorrow",
|
||||||
|
"current_hour_price_rank_today",
|
||||||
|
"current_hour_price_rank_today_tomorrow",
|
||||||
|
"next_interval_price_rank_today",
|
||||||
|
"next_interval_price_rank_today_tomorrow",
|
||||||
|
"next_hour_price_rank_today",
|
||||||
|
"next_hour_price_rank_today_tomorrow",
|
||||||
|
"previous_interval_price_rank_today",
|
||||||
|
"previous_interval_price_rank_today_tomorrow",
|
||||||
# Lifecycle sensor needs quarter-hour precision for state transitions:
|
# Lifecycle sensor needs quarter-hour precision for state transitions:
|
||||||
# - 23:45: turnover_pending (last interval before midnight)
|
# - 23:45: turnover_pending (last interval before midnight)
|
||||||
# - 00:00: turnover complete (after midnight API update)
|
# - 00:00: turnover complete (after midnight API update)
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
|
|
@ -24,16 +24,11 @@ from custom_components.tibber_prices.api import (
|
||||||
TibberPricesApiClientError,
|
TibberPricesApiClientError,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.const import DOMAIN
|
from custom_components.tibber_prices.const import DOMAIN
|
||||||
from custom_components.tibber_prices.utils.price import (
|
from custom_components.tibber_prices.utils.price import find_price_data_for_interval
|
||||||
find_price_data_for_interval,
|
|
||||||
)
|
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
|
|
||||||
from . import helpers
|
from . import helpers
|
||||||
from .constants import (
|
from .constants import STORAGE_VERSION, UPDATE_INTERVAL
|
||||||
STORAGE_VERSION,
|
|
||||||
UPDATE_INTERVAL,
|
|
||||||
)
|
|
||||||
from .data_transformation import TibberPricesDataTransformer
|
from .data_transformation import TibberPricesDataTransformer
|
||||||
from .listeners import TibberPricesListenerManager
|
from .listeners import TibberPricesListenerManager
|
||||||
from .midnight_handler import TibberPricesMidnightHandler
|
from .midnight_handler import TibberPricesMidnightHandler
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from .types import TibberPricesPeriodConfig
|
from .types import TibberPricesPeriodConfig
|
||||||
|
|
||||||
from .outlier_filtering import (
|
from .outlier_filtering import filter_price_outliers
|
||||||
filter_price_outliers,
|
|
||||||
)
|
|
||||||
from .period_building import (
|
from .period_building import (
|
||||||
add_interval_ends,
|
add_interval_ends,
|
||||||
build_periods,
|
build_periods,
|
||||||
|
|
@ -24,9 +22,7 @@ from .period_building import (
|
||||||
filter_superseded_periods,
|
filter_superseded_periods,
|
||||||
split_intervals_by_day,
|
split_intervals_by_day,
|
||||||
)
|
)
|
||||||
from .period_statistics import (
|
from .period_statistics import extract_period_summaries
|
||||||
extract_period_summaries,
|
|
||||||
)
|
|
||||||
from .shape_extension import extend_periods_for_shape
|
from .shape_extension import extend_periods_for_shape
|
||||||
from .types import TibberPricesThresholdConfig
|
from .types import TibberPricesThresholdConfig
|
||||||
|
|
||||||
|
|
@ -81,7 +77,7 @@ def calculate_periods(
|
||||||
|
|
||||||
from .types import INDENT_L0 # noqa: PLC0415
|
from .types import INDENT_L0 # noqa: PLC0415
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__) # noqa: N806
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Extract config values
|
# Extract config values
|
||||||
reverse_sort = config.reverse_sort
|
reverse_sort = config.reverse_sort
|
||||||
|
|
@ -141,7 +137,7 @@ def calculate_periods(
|
||||||
# User's flex setting still applies to period criteria (in_flex check).
|
# User's flex setting still applies to period criteria (in_flex check).
|
||||||
|
|
||||||
# Import details logger locally (core.py imports logger locally in function)
|
# Import details logger locally (core.py imports logger locally in function)
|
||||||
_LOGGER_DETAILS = logging.getLogger(__name__ + ".details") # noqa: N806
|
_LOGGER_DETAILS = logging.getLogger(__name__ + ".details")
|
||||||
|
|
||||||
outlier_flex = min(abs(flex) * 100, MAX_OUTLIER_FLEX * 100)
|
outlier_flex = min(abs(flex) * 100, MAX_OUTLIER_FLEX * 100)
|
||||||
if abs(flex) * 100 > MAX_OUTLIER_FLEX * 100:
|
if abs(flex) * 100 > MAX_OUTLIER_FLEX * 100:
|
||||||
|
|
@ -298,7 +294,7 @@ def calculate_periods(
|
||||||
def _period_belongs_to_side(
|
def _period_belongs_to_side(
|
||||||
period: list[dict],
|
period: list[dict],
|
||||||
side_times: set,
|
side_times: set,
|
||||||
time: "TibberPricesTimeService",
|
time: TibberPricesTimeService,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Return True if the majority of a period's intervals are in side_times."""
|
"""Return True if the majority of a period's intervals are in side_times."""
|
||||||
if not period:
|
if not period:
|
||||||
|
|
@ -307,14 +303,14 @@ def _period_belongs_to_side(
|
||||||
return in_side * 2 >= len(period)
|
return in_side * 2 >= len(period)
|
||||||
|
|
||||||
|
|
||||||
def _apply_segment_forcing( # noqa: PLR0913
|
def _apply_segment_forcing(
|
||||||
all_prices_smoothed: list[dict],
|
all_prices_smoothed: list[dict],
|
||||||
periods: list[list[dict]],
|
periods: list[list[dict]],
|
||||||
price_context: dict[str, Any],
|
price_context: dict[str, Any],
|
||||||
config: "TibberPricesPeriodConfig",
|
config: TibberPricesPeriodConfig,
|
||||||
*,
|
*,
|
||||||
day_patterns_by_date: dict,
|
day_patterns_by_date: dict,
|
||||||
time: "TibberPricesTimeService",
|
time: TibberPricesTimeService,
|
||||||
) -> list[list[dict]]:
|
) -> list[list[dict]]:
|
||||||
"""
|
"""
|
||||||
Force at least segment_min_periods periods per segment for W/M-shaped days.
|
Force at least segment_min_periods periods per segment for W/M-shaped days.
|
||||||
|
|
@ -341,7 +337,7 @@ def _apply_segment_forcing( # noqa: PLR0913
|
||||||
from .period_building import build_periods # noqa: PLC0415
|
from .period_building import build_periods # noqa: PLC0415
|
||||||
from .types import DAY_PATTERN_DOUBLE_PEAK, DAY_PATTERN_DOUBLE_VALLEY, INDENT_L1, INDENT_L2 # noqa: PLC0415
|
from .types import DAY_PATTERN_DOUBLE_PEAK, DAY_PATTERN_DOUBLE_VALLEY, INDENT_L1, INDENT_L2 # noqa: PLC0415
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__) # noqa: N806
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
reverse_sort = config.reverse_sort
|
reverse_sort = config.reverse_sort
|
||||||
target_pattern = DAY_PATTERN_DOUBLE_PEAK if reverse_sort else DAY_PATTERN_DOUBLE_VALLEY
|
target_pattern = DAY_PATTERN_DOUBLE_PEAK if reverse_sort else DAY_PATTERN_DOUBLE_VALLEY
|
||||||
|
|
|
||||||
|
|
@ -344,7 +344,7 @@ def _deduplicate_extrema(extrema: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
# ─── pattern classification ───────────────────────────────────────────────────
|
# ─── pattern classification ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def _classify_pattern( # noqa: PLR0911, PLR0912
|
def _classify_pattern(
|
||||||
extrema: list[dict[str, Any]],
|
extrema: list[dict[str, Any]],
|
||||||
cv_pct: float,
|
cv_pct: float,
|
||||||
times: list[datetime],
|
times: list[datetime],
|
||||||
|
|
@ -399,7 +399,7 @@ def _classify_pattern( # noqa: PLR0911, PLR0912
|
||||||
return DAY_PATTERN_PEAK, confidence
|
return DAY_PATTERN_PEAK, confidence
|
||||||
|
|
||||||
# ── two extrema ─────────────────────────────────────────────────────────────
|
# ── two extrema ─────────────────────────────────────────────────────────────
|
||||||
if n_extrema == 2: # noqa: PLR2004
|
if n_extrema == 2:
|
||||||
if types == ["max", "min"]:
|
if types == ["max", "min"]:
|
||||||
return DAY_PATTERN_FALLING, 0.7
|
return DAY_PATTERN_FALLING, 0.7
|
||||||
if types == ["min", "max"]:
|
if types == ["min", "max"]:
|
||||||
|
|
@ -410,7 +410,7 @@ def _classify_pattern( # noqa: PLR0911, PLR0912
|
||||||
return DAY_PATTERN_DOUBLE_PEAK, 0.65
|
return DAY_PATTERN_DOUBLE_PEAK, 0.65
|
||||||
|
|
||||||
# ── three extrema ────────────────────────────────────────────────────────────
|
# ── three extrema ────────────────────────────────────────────────────────────
|
||||||
if n_extrema == 3: # noqa: PLR2004
|
if n_extrema == 3:
|
||||||
# min-max-min → W-shape
|
# min-max-min → W-shape
|
||||||
if types == ["min", "max", "min"]:
|
if types == ["min", "max", "min"]:
|
||||||
return DAY_PATTERN_DOUBLE_VALLEY, 0.75
|
return DAY_PATTERN_DOUBLE_VALLEY, 0.75
|
||||||
|
|
@ -498,7 +498,7 @@ def _find_knee_on_flank(
|
||||||
# Normalise so that start=(0,0) and end=(1,1)
|
# Normalise so that start=(0,0) and end=(1,1)
|
||||||
px_range = float(length)
|
px_range = float(length)
|
||||||
py_range = p_end - p_start
|
py_range = p_end - p_start
|
||||||
if abs(py_range) < 1e-9: # noqa: PLR2004
|
if abs(py_range) < 1e-9:
|
||||||
return None # Flat flank - no knee
|
return None # Flat flank - no knee
|
||||||
|
|
||||||
max_dist = 0.0
|
max_dist = 0.0
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ def check_interval_criteria(
|
||||||
if scale_factor < SCALE_FACTOR_WARNING_THRESHOLD:
|
if scale_factor < SCALE_FACTOR_WARNING_THRESHOLD:
|
||||||
import logging # noqa: PLC0415
|
import logging # noqa: PLC0415
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(f"{__name__}.details") # noqa: N806
|
_LOGGER = logging.getLogger(f"{__name__}.details")
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"High flex %.1f%% detected: Reducing min_distance %.1f%% → %.1f%% (scale %.2f)",
|
"High flex %.1f%% detected: Reducing min_distance %.1f%% → %.1f%% (scale %.2f)",
|
||||||
flex_abs * 100,
|
flex_abs * 100,
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ Uses statistical methods:
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
from custom_components.tibber_prices.utils.price import calculate_coefficient_of_variation
|
from custom_components.tibber_prices.utils.price import calculate_coefficient_of_variation
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import PRICE_LEVEL_MAPPING
|
from custom_components.tibber_prices.const import PRICE_LEVEL_MAPPING
|
||||||
|
|
@ -11,11 +11,7 @@ from custom_components.tibber_prices.const import PRICE_LEVEL_MAPPING
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
from .level_filtering import (
|
from .level_filtering import apply_level_filter, check_interval_criteria, compute_geometric_flex_bonus
|
||||||
apply_level_filter,
|
|
||||||
check_interval_criteria,
|
|
||||||
compute_geometric_flex_bonus,
|
|
||||||
)
|
|
||||||
from .types import TibberPricesIntervalCriteria
|
from .types import TibberPricesIntervalCriteria
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
@ -54,7 +50,7 @@ def calculate_reference_prices(intervals_by_day: dict[date, list[dict]], *, reve
|
||||||
return ref_prices
|
return ref_prices
|
||||||
|
|
||||||
|
|
||||||
def build_periods( # noqa: PLR0913, PLR0915, PLR0912 - Complex period building logic requires many arguments, statements, and branches
|
def build_periods(
|
||||||
all_prices: list[dict],
|
all_prices: list[dict],
|
||||||
price_context: dict[str, Any],
|
price_context: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
from .types import (
|
from .types import TibberPricesPeriodData, TibberPricesPeriodStatistics, TibberPricesThresholdConfig
|
||||||
TibberPricesPeriodData,
|
|
||||||
TibberPricesPeriodStatistics,
|
|
||||||
TibberPricesThresholdConfig,
|
|
||||||
)
|
|
||||||
|
|
||||||
from custom_components.tibber_prices.utils.average import calculate_median
|
from custom_components.tibber_prices.utils.average import calculate_median
|
||||||
from custom_components.tibber_prices.utils.price import (
|
from custom_components.tibber_prices.utils.price import (
|
||||||
|
|
@ -272,7 +268,7 @@ def _add_interval_flag_counts(summary: dict, period: list[dict], *, geo_extensio
|
||||||
summary["segment_forced"] = True
|
summary["segment_forced"] = True
|
||||||
|
|
||||||
|
|
||||||
def extract_period_summaries( # noqa: PLR0912, PLR0915 - CV pre-check for geo-extension adds necessary branches/statements
|
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],
|
||||||
|
|
@ -302,10 +298,7 @@ def extract_period_summaries( # noqa: PLR0912, PLR0915 - CV pre-check for geo-e
|
||||||
time: TibberPricesTimeService instance (required).
|
time: TibberPricesTimeService instance (required).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from .types import ( # noqa: PLC0415 - Avoid circular import
|
from .types import TibberPricesPeriodData, TibberPricesPeriodStatistics # noqa: PLC0415 - Avoid circular import
|
||||||
TibberPricesPeriodData,
|
|
||||||
TibberPricesPeriodStatistics,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Build lookup dictionary for full price data by timestamp
|
# Build lookup dictionary for full price data by timestamp
|
||||||
price_lookup: dict[str, dict] = {}
|
price_lookup: dict[str, dict] = {}
|
||||||
|
|
@ -344,7 +337,7 @@ def extract_period_summaries( # noqa: PLR0912, PLR0915 - CV pre-check for geo-e
|
||||||
if cv_fails:
|
if cv_fails:
|
||||||
base_period = _strip_geo_from_edges(period)
|
base_period = _strip_geo_from_edges(period)
|
||||||
if base_period:
|
if base_period:
|
||||||
period = base_period # noqa: PLW2901 - intentional period replacement
|
period = base_period
|
||||||
geo_extension_status = "attempted"
|
geo_extension_status = "attempted"
|
||||||
else:
|
else:
|
||||||
geo_extension_status = "active"
|
geo_extension_status = "active"
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -14,10 +14,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from custom_components.tibber_prices.utils.price import calculate_coefficient_of_variation, calculate_iqr_stats
|
from custom_components.tibber_prices.utils.price import calculate_coefficient_of_variation, calculate_iqr_stats
|
||||||
|
|
||||||
from .period_overlap import (
|
from .period_overlap import recalculate_period_metadata, resolve_period_overlaps
|
||||||
recalculate_period_metadata,
|
|
||||||
resolve_period_overlaps,
|
|
||||||
)
|
|
||||||
from .types import (
|
from .types import (
|
||||||
INDENT_L0,
|
INDENT_L0,
|
||||||
INDENT_L1,
|
INDENT_L1,
|
||||||
|
|
@ -486,7 +483,7 @@ def _compute_day_effective_min(
|
||||||
|
|
||||||
price_values = [float(p["total"]) for p in day_prices if p.get("total") is not None]
|
price_values = [float(p["total"]) for p in day_prices if p.get("total") is not None]
|
||||||
|
|
||||||
if len(price_values) < 2: # noqa: PLR2004 - need at least 2 prices for any metric
|
if len(price_values) < 2:
|
||||||
day_effective_min[day] = min_periods
|
day_effective_min[day] = min_periods
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -532,7 +529,7 @@ def _compute_day_effective_min(
|
||||||
return day_effective_min, flat_day_count
|
return day_effective_min, flat_day_count
|
||||||
|
|
||||||
|
|
||||||
def calculate_periods_with_relaxation( # noqa: PLR0912, PLR0913, PLR0915 - Per-day relaxation requires many parameters and branches
|
def calculate_periods_with_relaxation(
|
||||||
all_prices: list[dict],
|
all_prices: list[dict],
|
||||||
*,
|
*,
|
||||||
config: TibberPricesPeriodConfig,
|
config: TibberPricesPeriodConfig,
|
||||||
|
|
@ -584,12 +581,8 @@ def calculate_periods_with_relaxation( # noqa: PLR0912, PLR0913, PLR0915 - Per-
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Import here to avoid circular dependency
|
# Import here to avoid circular dependency
|
||||||
from .core import ( # noqa: PLC0415
|
from .core import calculate_periods # noqa: PLC0415
|
||||||
calculate_periods,
|
from .period_building import filter_superseded_periods # noqa: PLC0415
|
||||||
)
|
|
||||||
from .period_building import ( # noqa: PLC0415
|
|
||||||
filter_superseded_periods,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Compact INFO-level summary
|
# Compact INFO-level summary
|
||||||
period_type = "PEAK PRICE" if config.reverse_sort else "BEST PRICE"
|
period_type = "PEAK PRICE" if config.reverse_sort else "BEST PRICE"
|
||||||
|
|
@ -691,7 +684,7 @@ def calculate_periods_with_relaxation( # noqa: PLR0912, PLR0913, PLR0915 - Per-
|
||||||
any_normal_day = False
|
any_normal_day = False
|
||||||
for day_prices in prices_by_day.values():
|
for day_prices in prices_by_day.values():
|
||||||
prices = [float(p["total"]) for p in day_prices if p.get("total") is not None]
|
prices = [float(p["total"]) for p in day_prices if p.get("total") is not None]
|
||||||
if len(prices) >= 2: # noqa: PLR2004
|
if len(prices) >= 2:
|
||||||
day_min = min(prices)
|
day_min = min(prices)
|
||||||
day_avg = sum(prices) / len(prices)
|
day_avg = sum(prices) / len(prices)
|
||||||
span = abs(day_avg - day_min)
|
span = abs(day_avg - day_min)
|
||||||
|
|
@ -877,7 +870,7 @@ def calculate_periods_with_relaxation( # noqa: PLR0912, PLR0913, PLR0915 - Per-
|
||||||
return final_result
|
return final_result
|
||||||
|
|
||||||
|
|
||||||
def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation requires many parameters and statements
|
def relax_all_prices(
|
||||||
all_prices: list[dict],
|
all_prices: list[dict],
|
||||||
config: TibberPricesPeriodConfig,
|
config: TibberPricesPeriodConfig,
|
||||||
min_periods: int,
|
min_periods: int,
|
||||||
|
|
@ -914,9 +907,7 @@ def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation require
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Import here to avoid circular dependency
|
# Import here to avoid circular dependency
|
||||||
from .core import ( # noqa: PLC0415
|
from .core import calculate_periods # noqa: PLC0415
|
||||||
calculate_periods,
|
|
||||||
)
|
|
||||||
|
|
||||||
flex_increment = 0.03 # 3% per step (hard-coded for reliability)
|
flex_increment = 0.03 # 3% per step (hard-coded for reliability)
|
||||||
base_flex = abs(config.flex)
|
base_flex = abs(config.flex)
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ created by this step.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import statistics
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import statistics
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import (
|
||||||
|
|
@ -30,10 +30,7 @@ from custom_components.tibber_prices.const import (
|
||||||
PRICE_LEVEL_VERY_CHEAP,
|
PRICE_LEVEL_VERY_CHEAP,
|
||||||
PRICE_LEVEL_VERY_EXPENSIVE,
|
PRICE_LEVEL_VERY_EXPENSIVE,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.utils.price import (
|
from custom_components.tibber_prices.utils.price import aggregate_period_levels, aggregate_period_ratings
|
||||||
aggregate_period_levels,
|
|
||||||
aggregate_period_ratings,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .period_statistics import (
|
from .period_statistics import (
|
||||||
calculate_aggregated_rating_difference,
|
calculate_aggregated_rating_difference,
|
||||||
|
|
@ -51,7 +48,7 @@ if TYPE_CHECKING:
|
||||||
_INTERVAL_DURATION = timedelta(minutes=15)
|
_INTERVAL_DURATION = timedelta(minutes=15)
|
||||||
|
|
||||||
|
|
||||||
def extend_periods_for_shape( # noqa: PLR0913 - Extension requires all context params
|
def extend_periods_for_shape(
|
||||||
periods: list[dict[str, Any]],
|
periods: list[dict[str, Any]],
|
||||||
all_prices: list[dict[str, Any]],
|
all_prices: list[dict[str, Any]],
|
||||||
price_context: dict[str, Any],
|
price_context: dict[str, Any],
|
||||||
|
|
@ -164,7 +161,7 @@ def _walk_contiguous(
|
||||||
return additions
|
return additions
|
||||||
|
|
||||||
|
|
||||||
def _extend_period_edges( # noqa: PLR0913 - Period edge extension requires many args
|
def _extend_period_edges(
|
||||||
period: dict[str, Any],
|
period: dict[str, Any],
|
||||||
interval_index: dict[datetime, dict[str, Any]],
|
interval_index: dict[datetime, dict[str, Any]],
|
||||||
*,
|
*,
|
||||||
|
|
@ -253,7 +250,7 @@ def _extend_period_edges( # noqa: PLR0913 - Period edge extension requires many
|
||||||
# ── recalculate volatility (coefficient of variation) ────────────────────
|
# ── recalculate volatility (coefficient of variation) ────────────────────
|
||||||
prices_for_vol = [float(p["total"]) for p in all_period_intervals if "total" in p]
|
prices_for_vol = [float(p["total"]) for p in all_period_intervals if "total" in p]
|
||||||
cv_pct: float | None = None
|
cv_pct: float | None = None
|
||||||
if len(prices_for_vol) >= 2: # noqa: PLR2004
|
if len(prices_for_vol) >= 2:
|
||||||
mean_p = statistics.mean(prices_for_vol)
|
mean_p = statistics.mean(prices_for_vol)
|
||||||
if mean_p > 0:
|
if mean_p > 0:
|
||||||
cv_pct = round(statistics.stdev(prices_for_vol) / mean_p * 100, 1)
|
cv_pct = round(statistics.stdev(prices_for_vol) / mean_p * 100, 1)
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ gap tolerance, and coordination of the period_handlers calculation functions.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from custom_components.tibber_prices import const as _const
|
from custom_components.tibber_prices import const as _const
|
||||||
|
|
@ -20,10 +20,7 @@ if TYPE_CHECKING:
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
|
||||||
from .helpers import get_intervals_for_day_offsets
|
from .helpers import get_intervals_for_day_offsets
|
||||||
from .period_handlers import (
|
from .period_handlers import TibberPricesPeriodConfig, calculate_periods_with_relaxation
|
||||||
TibberPricesPeriodConfig,
|
|
||||||
calculate_periods_with_relaxation,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ source of truth. This module only caches user_data for daily refresh cycle.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from custom_components.tibber_prices.api import (
|
from custom_components.tibber_prices.api import (
|
||||||
|
|
@ -71,7 +71,7 @@ class TibberPricesPriceDataManager:
|
||||||
This class orchestrates WHEN to fetch and processes the results.
|
This class orchestrates WHEN to fetch and processes the results.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__( # noqa: PLR0913
|
def __init__(
|
||||||
self,
|
self,
|
||||||
api: TibberPricesApiClient,
|
api: TibberPricesApiClient,
|
||||||
store: Any,
|
store: Any,
|
||||||
|
|
@ -178,7 +178,7 @@ class TibberPricesPriceDataManager:
|
||||||
)
|
)
|
||||||
await cache.save_cache(self._store, cache_data, self._log_prefix)
|
await cache.save_cache(self._store, cache_data, self._log_prefix)
|
||||||
|
|
||||||
def _validate_user_data(self, user_data: dict, home_id: str) -> bool: # noqa: PLR0911
|
def _validate_user_data(self, user_data: dict, home_id: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Validate user data completeness.
|
Validate user data completeness.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ class TibberPricesRepairManager:
|
||||||
|
|
||||||
async def check_tomorrow_data_availability(
|
async def check_tomorrow_data_availability(
|
||||||
self,
|
self,
|
||||||
has_tomorrow_data: bool, # noqa: FBT001 - Clear meaning in context
|
has_tomorrow_data: bool,
|
||||||
current_time: datetime,
|
current_time: datetime,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,8 @@ scheduling delays. It is NOT used for Timer #1's offset tracking.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import math
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
hass: HomeAssistant, # noqa: ARG001
|
hass: HomeAssistant,
|
||||||
entry: TibberPricesConfigEntry,
|
entry: TibberPricesConfigEntry,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return diagnostics for a config entry."""
|
"""Return diagnostics for a config entry."""
|
||||||
|
|
|
||||||
|
|
@ -18,15 +18,9 @@ For pure data transformation (no HA dependencies), see utils/ package.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from .attributes import (
|
from .attributes import add_description_attributes, async_add_description_attributes
|
||||||
add_description_attributes,
|
|
||||||
async_add_description_attributes,
|
|
||||||
)
|
|
||||||
from .colors import add_icon_color_attribute, get_icon_color
|
from .colors import add_icon_color_attribute, get_icon_color
|
||||||
from .helpers import (
|
from .helpers import find_rolling_hour_center_index, get_price_value
|
||||||
find_rolling_hour_center_index,
|
|
||||||
get_price_value,
|
|
||||||
)
|
|
||||||
from .icons import (
|
from .icons import (
|
||||||
get_binary_sensor_icon,
|
get_binary_sensor_icon,
|
||||||
get_dynamic_icon,
|
get_dynamic_icon,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ if TYPE_CHECKING:
|
||||||
from ..data import TibberPricesConfigEntry # noqa: TID252
|
from ..data import TibberPricesConfigEntry # noqa: TID252
|
||||||
|
|
||||||
|
|
||||||
def add_description_attributes( # noqa: PLR0913, PLR0912
|
def add_description_attributes(
|
||||||
attributes: dict,
|
attributes: dict,
|
||||||
platform: str,
|
platform: str,
|
||||||
translation_key: str | None,
|
translation_key: str | None,
|
||||||
|
|
@ -113,7 +113,7 @@ def add_description_attributes( # noqa: PLR0913, PLR0912
|
||||||
attributes[key] = value
|
attributes[key] = value
|
||||||
|
|
||||||
|
|
||||||
async def async_add_description_attributes( # noqa: PLR0913, PLR0912
|
async def async_add_description_attributes(
|
||||||
attributes: dict,
|
attributes: dict,
|
||||||
platform: str,
|
platform: str,
|
||||||
translation_key: str | None,
|
translation_key: str | None,
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,14 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from homeassistant.util import dt as dt_utils
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator.time_service import (
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
TibberPricesTimeService,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
_LOGGER_DETAILS = logging.getLogger(__name__ + ".details")
|
_LOGGER_DETAILS = logging.getLogger(__name__ + ".details")
|
||||||
|
|
@ -114,7 +112,7 @@ class TibberPricesIntervalPoolFetchGroupCache:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Use TimeService if available (Time Machine support), else real time
|
# Use TimeService if available (Time Machine support), else real time
|
||||||
now = self._time_service.now() if self._time_service else dt_utils.now()
|
now = self._time_service.now() if self._time_service else dt_util.now()
|
||||||
today_date_str = now.date().isoformat()
|
today_date_str = now.date().isoformat()
|
||||||
|
|
||||||
# Check cache validity (invalidate daily)
|
# Check cache validity (invalidate daily)
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from homeassistant.util import dt as dt_utils
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
@ -287,11 +287,9 @@ class TibberPricesIntervalPoolFetcher:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Import here to avoid circular dependency
|
# Import here to avoid circular dependency
|
||||||
from custom_components.tibber_prices.interval_pool.routing import ( # noqa: PLC0415
|
from custom_components.tibber_prices.interval_pool.routing import get_price_intervals_for_range # noqa: PLC0415
|
||||||
get_price_intervals_for_range,
|
|
||||||
)
|
|
||||||
|
|
||||||
fetch_time_iso = dt_utils.now().isoformat()
|
fetch_time_iso = dt_util.now().isoformat()
|
||||||
all_fetched_intervals = []
|
all_fetched_intervals = []
|
||||||
|
|
||||||
for idx, (missing_start_iso, missing_end_iso) in enumerate(missing_ranges, start=1):
|
for idx, (missing_start_iso, missing_end_iso) in enumerate(missing_ranges, start=1):
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
import logging
|
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ from custom_components.tibber_prices.api.exceptions import (
|
||||||
TibberPricesApiClientCommunicationError,
|
TibberPricesApiClientCommunicationError,
|
||||||
TibberPricesApiClientError,
|
TibberPricesApiClientError,
|
||||||
)
|
)
|
||||||
from homeassistant.util import dt as dt_utils
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .cache import TibberPricesIntervalPoolFetchGroupCache
|
from .cache import TibberPricesIntervalPoolFetchGroupCache
|
||||||
from .fetcher import TibberPricesIntervalPoolFetcher
|
from .fetcher import TibberPricesIntervalPoolFetcher
|
||||||
|
|
@ -23,9 +23,7 @@ from .storage import async_save_pool_state
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.api.client import TibberPricesApiClient
|
from custom_components.tibber_prices.api.client import TibberPricesApiClient
|
||||||
from custom_components.tibber_prices.coordinator.time_service import (
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
TibberPricesTimeService,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
_LOGGER_DETAILS = logging.getLogger(__name__ + ".details")
|
_LOGGER_DETAILS = logging.getLogger(__name__ + ".details")
|
||||||
|
|
@ -101,7 +99,7 @@ class TibberPricesIntervalPool:
|
||||||
hass: HomeAssistant instance for auto-save (optional).
|
hass: HomeAssistant instance for auto-save (optional).
|
||||||
entry_id: Config entry ID for auto-save (optional).
|
entry_id: Config entry ID for auto-save (optional).
|
||||||
time_service: TimeService for time-travel support (optional).
|
time_service: TimeService for time-travel support (optional).
|
||||||
If None, uses real time (dt_utils.now()).
|
If None, uses real time (dt_util.now()).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self._home_id = home_id
|
self._home_id = home_id
|
||||||
|
|
@ -206,7 +204,7 @@ class TibberPricesIntervalPool:
|
||||||
# Fetch missing ranges from API
|
# Fetch missing ranges from API
|
||||||
api_fetch_failed = False
|
api_fetch_failed = False
|
||||||
if missing_ranges:
|
if missing_ranges:
|
||||||
fetch_time_iso = dt_utils.now().isoformat()
|
fetch_time_iso = dt_util.now().isoformat()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Fetch with callback for immediate caching
|
# Fetch with callback for immediate caching
|
||||||
|
|
@ -301,7 +299,7 @@ class TibberPricesIntervalPool:
|
||||||
|
|
||||||
# Calculate range in home's timezone
|
# Calculate range in home's timezone
|
||||||
tz = ZoneInfo(tz_str) if tz_str else None
|
tz = ZoneInfo(tz_str) if tz_str else None
|
||||||
now = self._time_service.now() if self._time_service else dt_utils.now()
|
now = self._time_service.now() if self._time_service else dt_util.now()
|
||||||
now_local = now.astimezone(tz) if tz else now
|
now_local = now.astimezone(tz) if tz else now
|
||||||
|
|
||||||
# Day before yesterday 00:00 (start) - same for both fetch and return
|
# Day before yesterday 00:00 (start) - same for both fetch and return
|
||||||
|
|
@ -598,7 +596,7 @@ class TibberPricesIntervalPool:
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
# Determine interval step (15 min post-2025-10-01, 60 min pre)
|
# Determine interval step (15 min post-2025-10-01, 60 min pre)
|
||||||
resolution_change_naive = datetime(2025, 10, 1) # noqa: DTZ001
|
resolution_change_naive = datetime(2025, 10, 1)
|
||||||
interval_minutes = INTERVAL_QUARTER_HOURLY if current_naive >= resolution_change_naive else INTERVAL_HOURLY
|
interval_minutes = INTERVAL_QUARTER_HOURLY if current_naive >= resolution_change_naive else INTERVAL_HOURLY
|
||||||
|
|
||||||
fetch_groups = self._cache.get_fetch_groups()
|
fetch_groups = self._cache.get_fetch_groups()
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ This module handles intelligent routing between different Tibber API endpoints:
|
||||||
- PRICE_INFO_RANGE: Historical data (before "day before yesterday midnight")
|
- PRICE_INFO_RANGE: Historical data (before "day before yesterday midnight")
|
||||||
- Automatic splitting and merging when range spans the boundary
|
- Automatic splitting and merging when range spans the boundary
|
||||||
|
|
||||||
CRITICAL: Uses REAL TIME (dt_utils.now()) for API boundary calculation,
|
CRITICAL: Uses REAL TIME (dt_util.now()) for API boundary calculation,
|
||||||
NOT TimeService.now() which may be shifted for internal simulation.
|
NOT TimeService.now() which may be shifted for internal simulation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from custom_components.tibber_prices.api.exceptions import TibberPricesApiClientError
|
from custom_components.tibber_prices.api.exceptions import TibberPricesApiClientError
|
||||||
from homeassistant.util import dt as dt_utils
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
@ -43,7 +43,7 @@ async def get_price_intervals_for_range(
|
||||||
- PRICE_INFO: For intervals from "day before yesterday midnight" onwards
|
- PRICE_INFO: For intervals from "day before yesterday midnight" onwards
|
||||||
- Both: If range spans across the boundary, splits the request
|
- Both: If range spans across the boundary, splits the request
|
||||||
|
|
||||||
CRITICAL: Uses REAL TIME (dt_utils.now()) for API boundary calculation,
|
CRITICAL: Uses REAL TIME (dt_util.now()) for API boundary calculation,
|
||||||
NOT TimeService.now() which may be shifted for internal simulation.
|
NOT TimeService.now() which may be shifted for internal simulation.
|
||||||
This ensures predictable API responses.
|
This ensures predictable API responses.
|
||||||
|
|
||||||
|
|
@ -173,7 +173,7 @@ def _parse_timestamp(timestamp_str: str) -> datetime:
|
||||||
ValueError: If timestamp string cannot be parsed.
|
ValueError: If timestamp string cannot be parsed.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
result = dt_utils.parse_datetime(timestamp_str)
|
result = dt_util.parse_datetime(timestamp_str)
|
||||||
if result is None:
|
if result is None:
|
||||||
msg = f"Failed to parse timestamp: {timestamp_str}"
|
msg = f"Failed to parse timestamp: {timestamp_str}"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,7 @@ import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||||
from homeassistant.helpers import issue_registry as ir
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,19 +11,13 @@ from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import DOMAIN, get_home_type_translation, get_translation
|
||||||
DOMAIN,
|
|
||||||
get_home_type_translation,
|
|
||||||
get_translation,
|
|
||||||
)
|
|
||||||
from homeassistant.components.number import NumberEntity, RestoreNumber
|
from homeassistant.components.number import NumberEntity, RestoreNumber
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator import (
|
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
|
||||||
TibberPricesDataUpdateCoordinator,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .definitions import TibberPricesNumberEntityDescription
|
from .definitions import TibberPricesNumberEntityDescription
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from homeassistant.components.number import (
|
from homeassistant.components.number import NumberEntityDescription, NumberMode
|
||||||
NumberEntityDescription,
|
|
||||||
NumberMode,
|
|
||||||
)
|
|
||||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import CONF_CURRENCY_DISPLAY_MODE, DISPLAY_MODE_BASE
|
||||||
CONF_CURRENCY_DISPLAY_MODE,
|
|
||||||
DISPLAY_MODE_BASE,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .core import TibberPricesSensor
|
from .core import TibberPricesSensor
|
||||||
from .definitions import ENTITY_DESCRIPTIONS
|
from .definitions import ENTITY_DESCRIPTIONS
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from custom_components.tibber_prices.entity_utils import (
|
from custom_components.tibber_prices.entity_utils import add_description_attributes, add_icon_color_attribute
|
||||||
add_description_attributes,
|
|
||||||
add_icon_color_attribute,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.sensor.types import (
|
from custom_components.tibber_prices.sensor.types import (
|
||||||
DailyStatPriceAttributes,
|
DailyStatPriceAttributes,
|
||||||
DailyStatRatingAttributes,
|
DailyStatRatingAttributes,
|
||||||
|
|
@ -32,9 +29,7 @@ from custom_components.tibber_prices.sensor.types import (
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
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 TibberPricesTimeService
|
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
|
||||||
|
|
@ -74,7 +69,7 @@ __all__ = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def build_sensor_attributes( # noqa: PLR0912
|
def build_sensor_attributes(
|
||||||
key: str,
|
key: str,
|
||||||
coordinator: TibberPricesDataUpdateCoordinator,
|
coordinator: TibberPricesDataUpdateCoordinator,
|
||||||
native_value: Any,
|
native_value: Any,
|
||||||
|
|
@ -228,7 +223,7 @@ def build_sensor_attributes( # noqa: PLR0912
|
||||||
return attributes or None
|
return attributes or None
|
||||||
|
|
||||||
|
|
||||||
def build_extra_state_attributes( # noqa: PLR0913
|
def build_extra_state_attributes(
|
||||||
entity_key: str,
|
entity_key: str,
|
||||||
translation_key: str | None,
|
translation_key: str | None,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ def add_alternate_average_attribute(
|
||||||
cached_data: dict,
|
cached_data: dict,
|
||||||
base_key: str,
|
base_key: str,
|
||||||
*,
|
*,
|
||||||
config_entry: TibberPricesConfigEntry, # noqa: ARG001
|
config_entry: TibberPricesConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Add both average values (mean and median) as attributes.
|
Add both average values (mean and median) as attributes.
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,8 @@ 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.core import (
|
from custom_components.tibber_prices.coordinator.core import TibberPricesDataUpdateCoordinator
|
||||||
TibberPricesDataUpdateCoordinator,
|
from custom_components.tibber_prices.sensor.calculators.lifecycle import TibberPricesLifecycleCalculator
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.sensor.calculators.lifecycle import (
|
|
||||||
TibberPricesLifecycleCalculator,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_lifecycle_attributes(
|
def build_lifecycle_attributes(
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@ from typing import TYPE_CHECKING, Any
|
||||||
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
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
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 TibberPricesTimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ def add_volatility_attributes(
|
||||||
attributes: dict,
|
attributes: dict,
|
||||||
cached_data: dict,
|
cached_data: dict,
|
||||||
*,
|
*,
|
||||||
time: TibberPricesTimeService, # noqa: ARG001
|
time: TibberPricesTimeService,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Add attributes for volatility sensors.
|
Add attributes for volatility sensors.
|
||||||
|
|
@ -197,9 +197,7 @@ def add_percentile_rank_attributes(
|
||||||
coordinator_data = cached_data.get("coordinator_data")
|
coordinator_data = cached_data.get("coordinator_data")
|
||||||
|
|
||||||
if coordinator_data:
|
if coordinator_data:
|
||||||
from custom_components.tibber_prices.coordinator.helpers import ( # noqa: PLC0415
|
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets # noqa: PLC0415
|
||||||
get_intervals_for_day_offsets,
|
|
||||||
)
|
|
||||||
|
|
||||||
all_intervals = get_intervals_for_day_offsets(coordinator_data, [-1, 0, 1])
|
all_intervals = get_intervals_for_day_offsets(coordinator_data, [-1, 0, 1])
|
||||||
now = time.now()
|
now = time.now()
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@ from typing import TYPE_CHECKING
|
||||||
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
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 TibberPricesTimeService
|
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
|
||||||
|
|
||||||
|
|
@ -43,7 +41,7 @@ def _update_extreme_interval(extreme_interval: dict | None, price_data: dict, ke
|
||||||
return price_data if is_new_extreme else extreme_interval
|
return price_data if is_new_extreme else extreme_interval
|
||||||
|
|
||||||
|
|
||||||
def add_average_price_attributes( # noqa: PLR0913
|
def add_average_price_attributes(
|
||||||
attributes: dict,
|
attributes: dict,
|
||||||
key: str,
|
key: str,
|
||||||
coordinator: TibberPricesDataUpdateCoordinator,
|
coordinator: TibberPricesDataUpdateCoordinator,
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,10 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.helpers import (
|
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
||||||
get_intervals_for_day_offsets,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator import (
|
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
|
||||||
TibberPricesDataUpdateCoordinator,
|
|
||||||
)
|
|
||||||
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,8 @@ Organization by calculation pattern:
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription, SensorStateClass
|
||||||
SensorDeviceClass,
|
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfArea, UnitOfElectricCurrent, UnitOfEnergy, UnitOfTime
|
||||||
SensorEntityDescription,
|
|
||||||
SensorStateClass,
|
|
||||||
)
|
|
||||||
from homeassistant.const import (
|
|
||||||
PERCENTAGE,
|
|
||||||
EntityCategory,
|
|
||||||
UnitOfArea,
|
|
||||||
UnitOfElectricCurrent,
|
|
||||||
UnitOfEnergy,
|
|
||||||
UnitOfTime,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# SENSOR DEFINITIONS - Grouped by calculation method
|
# SENSOR DEFINITIONS - Grouped by calculation method
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.sensor.calculators.window_24h import TibberPricesWindow24hCalculator
|
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(
|
||||||
interval_calculator: TibberPricesIntervalCalculator,
|
interval_calculator: TibberPricesIntervalCalculator,
|
||||||
rolling_hour_calculator: TibberPricesRollingHourCalculator,
|
rolling_hour_calculator: TibberPricesRollingHourCalculator,
|
||||||
daily_stat_calculator: TibberPricesDailyStatCalculator,
|
daily_stat_calculator: TibberPricesDailyStatCalculator,
|
||||||
|
|
|
||||||
|
|
@ -50,11 +50,7 @@ from .find_most_expensive_hours import (
|
||||||
FIND_MOST_EXPENSIVE_HOURS_SERVICE_SCHEMA,
|
FIND_MOST_EXPENSIVE_HOURS_SERVICE_SCHEMA,
|
||||||
handle_find_most_expensive_hours,
|
handle_find_most_expensive_hours,
|
||||||
)
|
)
|
||||||
from .get_apexcharts_yaml import (
|
from .get_apexcharts_yaml import APEXCHARTS_SERVICE_SCHEMA, APEXCHARTS_YAML_SERVICE_NAME, handle_apexcharts_yaml
|
||||||
APEXCHARTS_SERVICE_SCHEMA,
|
|
||||||
APEXCHARTS_YAML_SERVICE_NAME,
|
|
||||||
handle_apexcharts_yaml,
|
|
||||||
)
|
|
||||||
from .get_chartdata import CHARTDATA_SERVICE_NAME, CHARTDATA_SERVICE_SCHEMA, handle_chartdata
|
from .get_chartdata import CHARTDATA_SERVICE_NAME, CHARTDATA_SERVICE_SCHEMA, handle_chartdata
|
||||||
from .get_price import GET_PRICE_SERVICE_NAME, GET_PRICE_SERVICE_SCHEMA, handle_get_price
|
from .get_price import GET_PRICE_SERVICE_NAME, GET_PRICE_SERVICE_SCHEMA, handle_get_price
|
||||||
from .refresh_user_data import (
|
from .refresh_user_data import (
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ After calling this service:
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
|
||||||
|
|
@ -8,25 +8,21 @@ machine, dryer).
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import DOMAIN, get_display_unit_factor, get_display_unit_string
|
||||||
DOMAIN,
|
|
||||||
get_display_unit_factor,
|
|
||||||
get_display_unit_string,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.utils.price_window import (
|
from custom_components.tibber_prices.utils.price_window import (
|
||||||
calculate_window_statistics,
|
calculate_window_statistics,
|
||||||
find_cheapest_contiguous_window,
|
find_cheapest_contiguous_window,
|
||||||
)
|
)
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.util import dt as dt_utils
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
INTERVAL_MINUTES,
|
INTERVAL_MINUTES,
|
||||||
|
|
@ -143,7 +139,7 @@ def _determine_no_window_reason(
|
||||||
return "insufficient_contiguous_window"
|
return "insufficient_contiguous_window"
|
||||||
|
|
||||||
|
|
||||||
async def _handle_find_block( # noqa: PLR0915
|
async def _handle_find_block(
|
||||||
call: ServiceCall,
|
call: ServiceCall,
|
||||||
*,
|
*,
|
||||||
reverse: bool = False,
|
reverse: bool = False,
|
||||||
|
|
@ -187,7 +183,7 @@ async def _handle_find_block( # noqa: PLR0915
|
||||||
home_tz = ZoneInfo(home_timezone)
|
home_tz = ZoneInfo(home_timezone)
|
||||||
|
|
||||||
# Resolve search range (priority: explicit datetime > time+offset > minutes offset > default)
|
# Resolve search range (priority: explicit datetime > time+offset > minutes offset > default)
|
||||||
now = dt_utils.now().astimezone(home_tz)
|
now = dt_util.now().astimezone(home_tz)
|
||||||
search_start, search_end = resolve_search_range(call.data, now, home_tz)
|
search_start, search_end = resolve_search_range(call.data, now, home_tz)
|
||||||
|
|
||||||
duration_intervals = duration_minutes // INTERVAL_MINUTES
|
duration_intervals = duration_minutes // INTERVAL_MINUTES
|
||||||
|
|
|
||||||
|
|
@ -8,25 +8,18 @@ Intervals need not be contiguous — designed for flexible loads
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import DOMAIN, get_display_unit_factor, get_display_unit_string
|
||||||
DOMAIN,
|
from custom_components.tibber_prices.utils.price_window import calculate_window_statistics, find_cheapest_n_intervals
|
||||||
get_display_unit_factor,
|
|
||||||
get_display_unit_string,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.utils.price_window import (
|
|
||||||
calculate_window_statistics,
|
|
||||||
find_cheapest_n_intervals,
|
|
||||||
)
|
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.util import dt as dt_utils
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
INTERVAL_MINUTES,
|
INTERVAL_MINUTES,
|
||||||
|
|
@ -101,7 +94,7 @@ def _determine_no_intervals_reason(
|
||||||
return "insufficient_intervals_for_constraints"
|
return "insufficient_intervals_for_constraints"
|
||||||
|
|
||||||
|
|
||||||
def _build_found_response( # noqa: PLR0913
|
def _build_found_response(
|
||||||
*,
|
*,
|
||||||
result: dict,
|
result: dict,
|
||||||
comparison_result: dict | None,
|
comparison_result: dict | None,
|
||||||
|
|
@ -202,7 +195,7 @@ def _build_found_response( # noqa: PLR0913
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def _handle_find_hours( # noqa: PLR0915
|
async def _handle_find_hours(
|
||||||
call: ServiceCall,
|
call: ServiceCall,
|
||||||
*,
|
*,
|
||||||
reverse: bool = False,
|
reverse: bool = False,
|
||||||
|
|
@ -251,7 +244,7 @@ async def _handle_find_hours( # noqa: PLR0915
|
||||||
home_tz = ZoneInfo(home_timezone)
|
home_tz = ZoneInfo(home_timezone)
|
||||||
|
|
||||||
# Resolve search range (priority: explicit datetime > time+offset > minutes offset > default)
|
# Resolve search range (priority: explicit datetime > time+offset > minutes offset > default)
|
||||||
now = dt_utils.now().astimezone(home_tz)
|
now = dt_util.now().astimezone(home_tz)
|
||||||
search_start, search_end = resolve_search_range(call.data, now, home_tz)
|
search_start, search_end = resolve_search_range(call.data, now, home_tz)
|
||||||
|
|
||||||
total_intervals = total_minutes // INTERVAL_MINUTES
|
total_intervals = total_minutes // INTERVAL_MINUTES
|
||||||
|
|
|
||||||
|
|
@ -8,25 +8,21 @@ each task claims the cheapest available contiguous window in the remaining pool.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import DOMAIN, get_display_unit_factor, get_display_unit_string
|
||||||
DOMAIN,
|
|
||||||
get_display_unit_factor,
|
|
||||||
get_display_unit_string,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.utils.price_window import (
|
from custom_components.tibber_prices.utils.price_window import (
|
||||||
calculate_window_statistics,
|
calculate_window_statistics,
|
||||||
find_cheapest_contiguous_window,
|
find_cheapest_contiguous_window,
|
||||||
)
|
)
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.util import dt as dt_utils
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
INTERVAL_MINUTES,
|
INTERVAL_MINUTES,
|
||||||
|
|
@ -213,7 +209,7 @@ def _find_cheapest_window_in_pool(
|
||||||
return (best_start, best_start + duration_intervals)
|
return (best_start, best_start + duration_intervals)
|
||||||
|
|
||||||
|
|
||||||
async def handle_find_cheapest_schedule(call: ServiceCall) -> ServiceResponse: # noqa: PLR0915
|
async def handle_find_cheapest_schedule(call: ServiceCall) -> ServiceResponse:
|
||||||
"""Handle find_cheapest_schedule service call."""
|
"""Handle find_cheapest_schedule service call."""
|
||||||
service_label = "find_cheapest_schedule"
|
service_label = "find_cheapest_schedule"
|
||||||
hass: HomeAssistant = call.hass
|
hass: HomeAssistant = call.hass
|
||||||
|
|
@ -255,7 +251,7 @@ async def handle_find_cheapest_schedule(call: ServiceCall) -> ServiceResponse:
|
||||||
|
|
||||||
home_tz = ZoneInfo(home_timezone)
|
home_tz = ZoneInfo(home_timezone)
|
||||||
|
|
||||||
now = dt_utils.now().astimezone(home_tz)
|
now = dt_util.now().astimezone(home_tz)
|
||||||
search_start, search_end = resolve_search_range(call.data, now, home_tz)
|
search_start, search_end = resolve_search_range(call.data, now, home_tz)
|
||||||
|
|
||||||
# Resolve task durations (round up to intervals)
|
# Resolve task durations (round up to intervals)
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,7 @@ from custom_components.tibber_prices.const import (
|
||||||
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
||||||
get_translation,
|
get_translation,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.coordinator.helpers import (
|
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
||||||
get_intervals_for_day_offsets,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.sensor.helpers import aggregate_level_data, aggregate_rating_data
|
from custom_components.tibber_prices.sensor.helpers import aggregate_level_data, aggregate_rating_data
|
||||||
from custom_components.tibber_prices.utils.average import calculate_mean, calculate_median
|
from custom_components.tibber_prices.utils.average import calculate_mean, calculate_median
|
||||||
|
|
||||||
|
|
@ -51,7 +49,7 @@ def normalize_rating_level_filter(value: list[str] | None) -> list[str] | None:
|
||||||
return [v.upper() for v in value]
|
return [v.upper() for v in value]
|
||||||
|
|
||||||
|
|
||||||
def aggregate_to_hourly( # noqa: PLR0912
|
def aggregate_to_hourly(
|
||||||
intervals: list[dict],
|
intervals: list[dict],
|
||||||
coordinator: Any,
|
coordinator: Any,
|
||||||
threshold_low: float = DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
threshold_low: float = DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
||||||
|
|
@ -166,7 +164,7 @@ def aggregate_to_hourly( # noqa: PLR0912
|
||||||
return hourly_data
|
return hourly_data
|
||||||
|
|
||||||
|
|
||||||
def aggregate_hourly_exact( # noqa: PLR0913, PLR0912, PLR0915
|
def aggregate_hourly_exact(
|
||||||
intervals: list[dict],
|
intervals: list[dict],
|
||||||
start_time_field: str,
|
start_time_field: str,
|
||||||
price_field: str,
|
price_field: str,
|
||||||
|
|
@ -316,7 +314,7 @@ def aggregate_hourly_exact( # noqa: PLR0913, PLR0912, PLR0915
|
||||||
return hourly_data
|
return hourly_data
|
||||||
|
|
||||||
|
|
||||||
def get_period_data( # noqa: PLR0913, PLR0912, PLR0915
|
def get_period_data(
|
||||||
*,
|
*,
|
||||||
coordinator: Any,
|
coordinator: Any,
|
||||||
period_filter: str,
|
period_filter: str,
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,7 @@ from custom_components.tibber_prices.const import (
|
||||||
get_translation,
|
get_translation,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.entity_registry import (
|
from homeassistant.helpers.entity_registry import EntityRegistry, async_get as async_get_entity_registry
|
||||||
EntityRegistry,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.entity_registry import (
|
|
||||||
async_get as async_get_entity_registry,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .formatters import get_level_translation
|
from .formatters import get_level_translation
|
||||||
from .helpers import get_entry_and_data
|
from .helpers import get_entry_and_data
|
||||||
|
|
@ -265,7 +260,7 @@ def _get_missing_cards_notification(language: str, missing_cards: list[str]) ->
|
||||||
return {"title": title, "message": message}
|
return {"title": title, "message": message}
|
||||||
|
|
||||||
|
|
||||||
async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa: PLR0912, PLR0915, C901
|
async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa: C901
|
||||||
"""
|
"""
|
||||||
Return YAML snippet for ApexCharts card.
|
Return YAML snippet for ApexCharts card.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,9 @@ Response: JSON with chart-ready data
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import TYPE_CHECKING, Any, Final
|
from typing import TYPE_CHECKING, Any, Final
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
@ -49,17 +49,10 @@ from custom_components.tibber_prices.const import (
|
||||||
get_currency_info,
|
get_currency_info,
|
||||||
get_currency_name,
|
get_currency_name,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.coordinator.helpers import (
|
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
||||||
get_intervals_for_day_offsets,
|
|
||||||
)
|
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
|
||||||
from .formatters import (
|
from .formatters import aggregate_to_hourly, get_period_data, normalize_level_filter, normalize_rating_level_filter
|
||||||
aggregate_to_hourly,
|
|
||||||
get_period_data,
|
|
||||||
normalize_level_filter,
|
|
||||||
normalize_rating_level_filter,
|
|
||||||
)
|
|
||||||
from .helpers import get_entry_and_data, has_tomorrow_data
|
from .helpers import get_entry_and_data, has_tomorrow_data
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -92,7 +85,7 @@ def _is_transition_to_more_expensive(
|
||||||
return next_rank > current_rank
|
return next_rank > current_rank
|
||||||
|
|
||||||
|
|
||||||
def _calculate_metadata( # noqa: PLR0912, PLR0913, PLR0915
|
def _calculate_metadata(
|
||||||
chart_data: list[dict[str, Any]],
|
chart_data: list[dict[str, Any]],
|
||||||
price_field: str,
|
price_field: str,
|
||||||
start_time_field: str,
|
start_time_field: str,
|
||||||
|
|
@ -320,7 +313,7 @@ CHARTDATA_SERVICE_SCHEMA: Final = vol.Schema(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR0912, PLR0915, C901
|
async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: C901
|
||||||
"""
|
"""
|
||||||
Return price data in chart-friendly format.
|
Return price data in chart-friendly format.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import voluptuous as vol
|
||||||
from custom_components.tibber_prices.const import DOMAIN
|
from custom_components.tibber_prices.const import DOMAIN
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.util import dt as dt_utils
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .helpers import get_entry_and_data
|
from .helpers import get_entry_and_data
|
||||||
|
|
||||||
|
|
@ -116,13 +116,13 @@ async def handle_get_price(call: ServiceCall) -> ServiceResponse:
|
||||||
|
|
||||||
if start_time.tzinfo is None:
|
if start_time.tzinfo is None:
|
||||||
# Step 1: Localize to HA server timezone
|
# Step 1: Localize to HA server timezone
|
||||||
start_time = dt_utils.as_local(start_time)
|
start_time = dt_util.as_local(start_time)
|
||||||
# Step 2: Convert to home timezone
|
# Step 2: Convert to home timezone
|
||||||
start_time = start_time.astimezone(home_tz)
|
start_time = start_time.astimezone(home_tz)
|
||||||
|
|
||||||
if end_time.tzinfo is None:
|
if end_time.tzinfo is None:
|
||||||
# Step 1: Localize to HA server timezone
|
# Step 1: Localize to HA server timezone
|
||||||
end_time = dt_utils.as_local(end_time)
|
end_time = dt_util.as_local(end_time)
|
||||||
# Step 2: Convert to home timezone
|
# Step 2: Convert to home timezone
|
||||||
end_time = end_time.astimezone(home_tz)
|
end_time = end_time.astimezone(home_tz)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,13 @@ Used by:
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, time as dt_time, timedelta
|
||||||
from datetime import time as dt_time
|
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import DOMAIN
|
from custom_components.tibber_prices.const import DOMAIN
|
||||||
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.util import dt as dt_utils
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
@ -266,13 +265,13 @@ def localize_to_home_tz(dt_value: datetime, home_tz: ZoneInfo) -> datetime:
|
||||||
2. Convert from HA timezone to home timezone
|
2. Convert from HA timezone to home timezone
|
||||||
"""
|
"""
|
||||||
if dt_value.tzinfo is None:
|
if dt_value.tzinfo is None:
|
||||||
dt_value = dt_utils.as_local(dt_value)
|
dt_value = dt_util.as_local(dt_value)
|
||||||
return dt_value.astimezone(home_tz)
|
return dt_value.astimezone(home_tz)
|
||||||
|
|
||||||
|
|
||||||
def calculate_end_of_tomorrow(home_tz: ZoneInfo) -> datetime:
|
def calculate_end_of_tomorrow(home_tz: ZoneInfo) -> datetime:
|
||||||
"""Calculate end of tomorrow in home timezone."""
|
"""Calculate end of tomorrow in home timezone."""
|
||||||
now_home = dt_utils.now().astimezone(home_tz)
|
now_home = dt_util.now().astimezone(home_tz)
|
||||||
tomorrow = (now_home + timedelta(days=1)).date()
|
tomorrow = (now_home + timedelta(days=1)).date()
|
||||||
# End of tomorrow = midnight at start of day after tomorrow
|
# End of tomorrow = midnight at start of day after tomorrow
|
||||||
return now_home.replace(
|
return now_home.replace(
|
||||||
|
|
@ -295,9 +294,10 @@ def _resolve_time_with_day_offset(
|
||||||
time_value: dt_time,
|
time_value: dt_time,
|
||||||
day_offset: int,
|
day_offset: int,
|
||||||
home_tz: ZoneInfo,
|
home_tz: ZoneInfo,
|
||||||
|
now: datetime,
|
||||||
) -> datetime:
|
) -> datetime:
|
||||||
"""Resolve a time-of-day + day offset to a full datetime in home timezone."""
|
"""Resolve a time-of-day + day offset to a full datetime in home timezone."""
|
||||||
now_home = dt_utils.now().astimezone(home_tz)
|
now_home = now.astimezone(home_tz)
|
||||||
target_date = (now_home + timedelta(days=day_offset)).date()
|
target_date = (now_home + timedelta(days=day_offset)).date()
|
||||||
return datetime(
|
return datetime(
|
||||||
year=target_date.year,
|
year=target_date.year,
|
||||||
|
|
@ -477,7 +477,7 @@ def resolve_search_range(
|
||||||
search_start = localize_to_home_tz(call_data["search_start"], home_tz)
|
search_start = localize_to_home_tz(call_data["search_start"], home_tz)
|
||||||
elif "search_start_time" in call_data:
|
elif "search_start_time" in call_data:
|
||||||
day_offset = call_data.get("search_start_day_offset", 0)
|
day_offset = call_data.get("search_start_day_offset", 0)
|
||||||
search_start = _resolve_time_with_day_offset(call_data["search_start_time"], day_offset, home_tz)
|
search_start = _resolve_time_with_day_offset(call_data["search_start_time"], day_offset, home_tz, now)
|
||||||
elif "search_start_offset_minutes" in call_data:
|
elif "search_start_offset_minutes" in call_data:
|
||||||
search_start = now + timedelta(minutes=call_data["search_start_offset_minutes"])
|
search_start = now + timedelta(minutes=call_data["search_start_offset_minutes"])
|
||||||
if include_current:
|
if include_current:
|
||||||
|
|
@ -490,7 +490,7 @@ def resolve_search_range(
|
||||||
search_end = localize_to_home_tz(call_data["search_end"], home_tz)
|
search_end = localize_to_home_tz(call_data["search_end"], home_tz)
|
||||||
elif "search_end_time" in call_data:
|
elif "search_end_time" in call_data:
|
||||||
day_offset = call_data.get("search_end_day_offset", 0)
|
day_offset = call_data.get("search_end_day_offset", 0)
|
||||||
search_end = _resolve_time_with_day_offset(call_data["search_end_time"], day_offset, home_tz)
|
search_end = _resolve_time_with_day_offset(call_data["search_end_time"], day_offset, home_tz, now)
|
||||||
elif "search_end_offset_minutes" in call_data:
|
elif "search_end_offset_minutes" in call_data:
|
||||||
search_end = now + timedelta(minutes=call_data["search_end_offset_minutes"])
|
search_end = now + timedelta(minutes=call_data["search_end_offset_minutes"])
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,14 @@ from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import DOMAIN, get_home_type_translation, get_translation
|
||||||
DOMAIN,
|
|
||||||
get_home_type_translation,
|
|
||||||
get_translation,
|
|
||||||
)
|
|
||||||
from homeassistant.components.switch import SwitchEntity
|
from homeassistant.components.switch import SwitchEntity
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator import (
|
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
|
||||||
TibberPricesDataUpdateCoordinator,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .definitions import TibberPricesSwitchEntityDescription
|
from .definitions import TibberPricesSwitchEntityDescription
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import bisect
|
import bisect
|
||||||
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
import statistics
|
import statistics
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -353,7 +353,7 @@ def calculate_difference_percentage(
|
||||||
return ((current_interval_price - trailing_average) / abs(trailing_average)) * 100
|
return ((current_interval_price - trailing_average) / abs(trailing_average)) * 100
|
||||||
|
|
||||||
|
|
||||||
def calculate_rating_level( # noqa: PLR0911 - Multiple returns justified by clear hysteresis state machine
|
def calculate_rating_level(
|
||||||
difference: float | None,
|
difference: float | None,
|
||||||
threshold_low: float,
|
threshold_low: float,
|
||||||
threshold_high: float,
|
threshold_high: float,
|
||||||
|
|
@ -435,7 +435,7 @@ def calculate_rating_level( # noqa: PLR0911 - Multiple returns justified by cle
|
||||||
return PRICE_RATING_NORMAL
|
return PRICE_RATING_NORMAL
|
||||||
|
|
||||||
|
|
||||||
def _process_price_interval( # noqa: PLR0913 - Extra params needed for hysteresis
|
def _process_price_interval(
|
||||||
price_interval: dict[str, Any],
|
price_interval: dict[str, Any],
|
||||||
all_prices: list[dict[str, Any]],
|
all_prices: list[dict[str, Any]],
|
||||||
threshold_low: float,
|
threshold_low: float,
|
||||||
|
|
@ -651,7 +651,7 @@ def _apply_rating_gap_tolerance(
|
||||||
if interval.get("rating_level") is not None
|
if interval.get("rating_level") is not None
|
||||||
]
|
]
|
||||||
|
|
||||||
if len(rated_intervals) < 3: # noqa: PLR2004 - Minimum 3 for before/gap/after pattern
|
if len(rated_intervals) < 3:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Iteratively merge small blocks until no more changes
|
# Iteratively merge small blocks until no more changes
|
||||||
|
|
@ -720,7 +720,7 @@ def _apply_level_gap_tolerance(
|
||||||
if interval.get("level") is not None
|
if interval.get("level") is not None
|
||||||
]
|
]
|
||||||
|
|
||||||
if len(level_intervals) < 3: # noqa: PLR2004 - Minimum 3 for before/gap/after pattern
|
if len(level_intervals) < 3:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Iteratively merge small blocks until no more changes
|
# Iteratively merge small blocks until no more changes
|
||||||
|
|
@ -859,7 +859,7 @@ def _merge_small_level_blocks(
|
||||||
return len(merge_decisions)
|
return len(merge_decisions)
|
||||||
|
|
||||||
|
|
||||||
def enrich_price_info_with_differences( # noqa: PLR0913 - Extra params for rating stabilization
|
def enrich_price_info_with_differences(
|
||||||
all_intervals: list[dict[str, Any]],
|
all_intervals: list[dict[str, Any]],
|
||||||
*,
|
*,
|
||||||
threshold_low: float | None = None,
|
threshold_low: float | None = None,
|
||||||
|
|
@ -867,7 +867,7 @@ def enrich_price_info_with_differences( # noqa: PLR0913 - Extra params for rati
|
||||||
hysteresis: float | None = None,
|
hysteresis: float | None = None,
|
||||||
gap_tolerance: int | None = None,
|
gap_tolerance: int | None = None,
|
||||||
level_gap_tolerance: int | None = None,
|
level_gap_tolerance: int | None = None,
|
||||||
time: TibberPricesTimeService | None = None, # noqa: ARG001 # Used in production (via coordinator), kept for compatibility
|
time: TibberPricesTimeService | None = None, # Used in production (via coordinator), kept for compatibility
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Enrich price intervals with calculated 'difference' and 'rating_level' values.
|
Enrich price intervals with calculated 'difference' and 'rating_level' values.
|
||||||
|
|
@ -1229,7 +1229,7 @@ def _calculate_lookahead_volatility_factor(
|
||||||
return factor
|
return factor
|
||||||
|
|
||||||
|
|
||||||
def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for volatility-adaptive calculation
|
def calculate_price_trend(
|
||||||
current_interval_price: float,
|
current_interval_price: float,
|
||||||
future_average: float,
|
future_average: float,
|
||||||
threshold_rising: float = 3.0,
|
threshold_rising: float = 3.0,
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ These are stateless pure functions with no Home Assistant dependencies.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import statistics
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import statistics
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,11 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.services import find_cheapest_block as block_module
|
from custom_components.tibber_prices.services import (
|
||||||
from custom_components.tibber_prices.services import find_cheapest_hours as hours_module
|
find_cheapest_block as block_module,
|
||||||
from custom_components.tibber_prices.services import find_cheapest_schedule as schedule_module
|
find_cheapest_hours as hours_module,
|
||||||
|
find_cheapest_schedule as schedule_module,
|
||||||
|
)
|
||||||
from custom_components.tibber_prices.services.find_cheapest_block import (
|
from custom_components.tibber_prices.services.find_cheapest_block import (
|
||||||
_determine_no_window_reason,
|
_determine_no_window_reason,
|
||||||
handle_find_cheapest_block,
|
handle_find_cheapest_block,
|
||||||
|
|
|
||||||
|
|
@ -11,22 +11,15 @@ Also validates schema boundaries for all 4 services.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, time as dt_time, timedelta
|
||||||
from datetime import time as dt_time
|
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from custom_components.tibber_prices.services.find_cheapest_block import (
|
from custom_components.tibber_prices.services.find_cheapest_block import _COMMON_BLOCK_SCHEMA
|
||||||
_COMMON_BLOCK_SCHEMA,
|
from custom_components.tibber_prices.services.find_cheapest_hours import _COMMON_HOURS_SCHEMA
|
||||||
)
|
from custom_components.tibber_prices.services.helpers import resolve_search_range
|
||||||
from custom_components.tibber_prices.services.find_cheapest_hours import (
|
|
||||||
_COMMON_HOURS_SCHEMA,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.services.helpers import (
|
|
||||||
resolve_search_range,
|
|
||||||
)
|
|
||||||
|
|
||||||
BERLIN = ZoneInfo("Europe/Berlin")
|
BERLIN = ZoneInfo("Europe/Berlin")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,8 @@ from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.time_service import (
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
TibberPricesTimeService,
|
from custom_components.tibber_prices.utils.average import calculate_leading_24h_mean, calculate_trailing_24h_mean
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.utils.average import (
|
|
||||||
calculate_leading_24h_mean,
|
|
||||||
calculate_trailing_24h_mean,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,7 @@ from custom_components.tibber_prices.coordinator.period_handlers import (
|
||||||
TibberPricesPeriodConfig,
|
TibberPricesPeriodConfig,
|
||||||
calculate_periods_with_relaxation,
|
calculate_periods_with_relaxation,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.coordinator.time_service import (
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
TibberPricesTimeService,
|
|
||||||
)
|
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ from unittest.mock import AsyncMock, MagicMock
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
# Import at module level to avoid PLC0415
|
# Import at module level to avoid PLC0415
|
||||||
from custom_components.tibber_prices.coordinator.core import (
|
from custom_components.tibber_prices.coordinator.core import TibberPricesDataUpdateCoordinator
|
||||||
TibberPricesDataUpdateCoordinator,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
|
||||||
|
|
@ -18,24 +18,16 @@ Architecture:
|
||||||
Tests access internal components directly for fine-grained verification.
|
Tests access internal components directly for fine-grained verification.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
import json
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.interval_pool.cache import (
|
from custom_components.tibber_prices.interval_pool.cache import TibberPricesIntervalPoolFetchGroupCache
|
||||||
TibberPricesIntervalPoolFetchGroupCache,
|
from custom_components.tibber_prices.interval_pool.garbage_collector import TibberPricesIntervalPoolGarbageCollector
|
||||||
)
|
from custom_components.tibber_prices.interval_pool.index import TibberPricesIntervalPoolTimestampIndex
|
||||||
from custom_components.tibber_prices.interval_pool.garbage_collector import (
|
from custom_components.tibber_prices.interval_pool.manager import TibberPricesIntervalPool
|
||||||
TibberPricesIntervalPoolGarbageCollector,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.interval_pool.index import (
|
|
||||||
TibberPricesIntervalPoolTimestampIndex,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.interval_pool.manager import (
|
|
||||||
TibberPricesIntervalPool,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ from unittest.mock import AsyncMock, MagicMock
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.interval_pool import TibberPricesIntervalPool
|
from custom_components.tibber_prices.interval_pool import TibberPricesIntervalPool
|
||||||
from homeassistant.util import dt as dt_utils
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
pytest_plugins = ("pytest_homeassistant_custom_component",)
|
pytest_plugins = ("pytest_homeassistant_custom_component",)
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ async def test_no_cache_single_api_call() -> None:
|
||||||
"_calculate_day_before_yesterday_midnight",
|
"_calculate_day_before_yesterday_midnight",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
start = dt_utils.now().replace(hour=10, minute=0, second=0, microsecond=0)
|
start = dt_util.now().replace(hour=10, minute=0, second=0, microsecond=0)
|
||||||
end = start + timedelta(hours=2) # 8 intervals
|
end = start + timedelta(hours=2) # 8 intervals
|
||||||
|
|
||||||
# Create mock response
|
# Create mock response
|
||||||
|
|
@ -71,7 +71,7 @@ async def test_no_cache_single_api_call() -> None:
|
||||||
api_client.async_get_price_info_for_range = AsyncMock(return_value=mock_intervals)
|
api_client.async_get_price_info_for_range = AsyncMock(return_value=mock_intervals)
|
||||||
api_client._extract_home_timezones = MagicMock(return_value={"home123": "Europe/Berlin"}) # noqa: SLF001
|
api_client._extract_home_timezones = MagicMock(return_value={"home123": "Europe/Berlin"}) # noqa: SLF001
|
||||||
# Mock boundary calculation (returns day before yesterday midnight)
|
# Mock boundary calculation (returns day before yesterday midnight)
|
||||||
dby_midnight = (dt_utils.now() - timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)
|
dby_midnight = (dt_util.now() - timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
api_client._calculate_day_before_yesterday_midnight = MagicMock(return_value=dby_midnight) # noqa: SLF001
|
api_client._calculate_day_before_yesterday_midnight = MagicMock(return_value=dby_midnight) # noqa: SLF001
|
||||||
# Mock the actual price info fetching methods (they call async_get_price_info_for_range internally)
|
# Mock the actual price info fetching methods (they call async_get_price_info_for_range internally)
|
||||||
api_client.async_get_price_info = AsyncMock(return_value={"priceInfo": mock_intervals})
|
api_client.async_get_price_info = AsyncMock(return_value={"priceInfo": mock_intervals})
|
||||||
|
|
@ -103,7 +103,7 @@ async def test_full_cache_zero_api_calls() -> None:
|
||||||
"_calculate_day_before_yesterday_midnight",
|
"_calculate_day_before_yesterday_midnight",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
start = dt_utils.now().replace(hour=10, minute=0, second=0, microsecond=0)
|
start = dt_util.now().replace(hour=10, minute=0, second=0, microsecond=0)
|
||||||
end = start + timedelta(hours=2) # 8 intervals
|
end = start + timedelta(hours=2) # 8 intervals
|
||||||
|
|
||||||
# Pre-populate cache
|
# Pre-populate cache
|
||||||
|
|
@ -111,7 +111,7 @@ async def test_full_cache_zero_api_calls() -> None:
|
||||||
api_client.async_get_price_info_for_range = AsyncMock(return_value=mock_intervals)
|
api_client.async_get_price_info_for_range = AsyncMock(return_value=mock_intervals)
|
||||||
api_client._extract_home_timezones = MagicMock(return_value={"home123": "Europe/Berlin"}) # noqa: SLF001
|
api_client._extract_home_timezones = MagicMock(return_value={"home123": "Europe/Berlin"}) # noqa: SLF001
|
||||||
# Mock boundary calculation (returns day before yesterday midnight)
|
# Mock boundary calculation (returns day before yesterday midnight)
|
||||||
dby_midnight = (dt_utils.now() - timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)
|
dby_midnight = (dt_util.now() - timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
api_client._calculate_day_before_yesterday_midnight = MagicMock(return_value=dby_midnight) # noqa: SLF001
|
api_client._calculate_day_before_yesterday_midnight = MagicMock(return_value=dby_midnight) # noqa: SLF001
|
||||||
# Mock the actual price info fetching methods (they call async_get_price_info_for_range internally)
|
# Mock the actual price info fetching methods (they call async_get_price_info_for_range internally)
|
||||||
api_client.async_get_price_info = AsyncMock(return_value={"priceInfo": mock_intervals})
|
api_client.async_get_price_info = AsyncMock(return_value={"priceInfo": mock_intervals})
|
||||||
|
|
@ -146,7 +146,7 @@ async def test_single_gap_single_api_call() -> None:
|
||||||
"_calculate_day_before_yesterday_midnight",
|
"_calculate_day_before_yesterday_midnight",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
start = dt_utils.now().replace(hour=10, minute=0, second=0, microsecond=0)
|
start = dt_util.now().replace(hour=10, minute=0, second=0, microsecond=0)
|
||||||
end = start + timedelta(hours=3) # 12 intervals total
|
end = start + timedelta(hours=3) # 12 intervals total
|
||||||
|
|
||||||
user_data = {"timeZone": "Europe/Berlin"}
|
user_data = {"timeZone": "Europe/Berlin"}
|
||||||
|
|
@ -198,7 +198,7 @@ async def test_multiple_gaps_multiple_api_calls() -> None:
|
||||||
"_calculate_day_before_yesterday_midnight",
|
"_calculate_day_before_yesterday_midnight",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
start = dt_utils.now().replace(hour=10, minute=0, second=0, microsecond=0)
|
start = dt_util.now().replace(hour=10, minute=0, second=0, microsecond=0)
|
||||||
end = start + timedelta(hours=4) # 16 intervals total
|
end = start + timedelta(hours=4) # 16 intervals total
|
||||||
|
|
||||||
user_data = {"timeZone": "Europe/Berlin"}
|
user_data = {"timeZone": "Europe/Berlin"}
|
||||||
|
|
@ -270,7 +270,7 @@ async def test_partial_overlap_minimal_fetch() -> None:
|
||||||
"_calculate_day_before_yesterday_midnight",
|
"_calculate_day_before_yesterday_midnight",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
start = dt_utils.now().replace(hour=10, minute=0, second=0, microsecond=0)
|
start = dt_util.now().replace(hour=10, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
user_data = {"timeZone": "Europe/Berlin"}
|
user_data = {"timeZone": "Europe/Berlin"}
|
||||||
|
|
||||||
|
|
@ -314,7 +314,7 @@ async def test_detect_missing_ranges_optimization() -> None:
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
start = dt_utils.now().replace(hour=10, minute=0, second=0, microsecond=0)
|
start = dt_util.now().replace(hour=10, minute=0, second=0, microsecond=0)
|
||||||
end = start + timedelta(hours=4)
|
end = start + timedelta(hours=4)
|
||||||
|
|
||||||
user_data = {"timeZone": "Europe/Berlin"}
|
user_data = {"timeZone": "Europe/Berlin"}
|
||||||
|
|
@ -337,7 +337,7 @@ async def test_detect_missing_ranges_optimization() -> None:
|
||||||
pool._fetch_groups = [ # noqa: SLF001
|
pool._fetch_groups = [ # noqa: SLF001
|
||||||
{
|
{
|
||||||
"intervals": cached,
|
"intervals": cached,
|
||||||
"fetch_time": dt_utils.now().isoformat(),
|
"fetch_time": dt_util.now().isoformat(),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
pool._timestamp_index = {interval["startsAt"]: idx for idx, interval in enumerate(cached)} # noqa: SLF001
|
pool._timestamp_index = {interval["startsAt"]: idx for idx, interval in enumerate(cached)} # noqa: SLF001
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.period_handlers.level_filtering import (
|
from custom_components.tibber_prices.coordinator.period_handlers.level_filtering import check_interval_criteria
|
||||||
check_interval_criteria,
|
from custom_components.tibber_prices.coordinator.period_handlers.types import TibberPricesIntervalCriteria
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.coordinator.period_handlers.types import (
|
|
||||||
TibberPricesIntervalCriteria,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,14 @@ This test verifies that:
|
||||||
4. Calculations that depend on averages use mean internally (not affected by display setting)
|
4. Calculations that depend on averages use mean internally (not affected by display setting)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import statistics
|
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
import statistics
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import CONF_AVERAGE_SENSOR_DISPLAY, DEFAULT_AVERAGE_SENSOR_DISPLAY
|
||||||
CONF_AVERAGE_SENSOR_DISPLAY,
|
from custom_components.tibber_prices.sensor.attributes.helpers import add_alternate_average_attribute
|
||||||
DEFAULT_AVERAGE_SENSOR_DISPLAY,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.sensor.attributes.helpers import (
|
|
||||||
add_alternate_average_attribute,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.utils.average import calculate_mean, calculate_median
|
from custom_components.tibber_prices.utils.average import calculate_mean, calculate_median
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@ from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.midnight_handler import (
|
from custom_components.tibber_prices.coordinator.midnight_handler import TibberPricesMidnightHandler
|
||||||
TibberPricesMidnightHandler,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@ from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.period_handlers.relaxation import (
|
from custom_components.tibber_prices.coordinator.period_handlers.relaxation import group_periods_by_day
|
||||||
group_periods_by_day,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,9 @@ from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.period_handlers.core import (
|
from custom_components.tibber_prices.coordinator.period_handlers.core import calculate_periods
|
||||||
calculate_periods,
|
from custom_components.tibber_prices.coordinator.period_handlers.types import TibberPricesPeriodConfig
|
||||||
)
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
from custom_components.tibber_prices.coordinator.period_handlers.types import (
|
|
||||||
TibberPricesPeriodConfig,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.coordinator.time_service import (
|
|
||||||
TibberPricesTimeService,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create_price_interval(dt: datetime, price: float) -> dict:
|
def create_price_interval(dt: datetime, price: float) -> dict:
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@ from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.time_service import (
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
TibberPricesTimeService,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.utils.average import (
|
from custom_components.tibber_prices.utils.average import (
|
||||||
calculate_leading_24h_max,
|
calculate_leading_24h_max,
|
||||||
calculate_leading_24h_min,
|
calculate_leading_24h_min,
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,7 @@ from zoneinfo import ZoneInfo
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.constants import UPDATE_INTERVAL
|
from custom_components.tibber_prices.coordinator.constants import UPDATE_INTERVAL
|
||||||
from custom_components.tibber_prices.sensor.calculators.lifecycle import (
|
from custom_components.tibber_prices.sensor.calculators.lifecycle import TibberPricesLifecycleCalculator
|
||||||
TibberPricesLifecycleCalculator,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,7 @@ from custom_components.tibber_prices.coordinator.period_handlers import (
|
||||||
TibberPricesPeriodConfig,
|
TibberPricesPeriodConfig,
|
||||||
calculate_periods_with_relaxation,
|
calculate_periods_with_relaxation,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.coordinator.time_service import (
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
TibberPricesTimeService,
|
|
||||||
)
|
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@ from datetime import UTC, datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.period_handlers.period_statistics import (
|
from custom_components.tibber_prices.coordinator.period_handlers.period_statistics import calculate_period_price_diff
|
||||||
calculate_period_price_diff,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.utils.price import calculate_price_trend
|
from custom_components.tibber_prices.utils.price import calculate_price_trend
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,7 @@ from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.time_service import (
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
TibberPricesTimeService,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.utils.price import (
|
from custom_components.tibber_prices.utils.price import (
|
||||||
aggregate_period_levels,
|
aggregate_period_levels,
|
||||||
aggregate_period_ratings,
|
aggregate_period_ratings,
|
||||||
|
|
@ -348,7 +346,7 @@ def test_rating_level_none_difference() -> None:
|
||||||
(0.1, 0.1, 0.1, 0.0, "NORMAL", "prices near zero: stable"),
|
(0.1, 0.1, 0.1, 0.0, "NORMAL", "prices near zero: stable"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_enrich_price_info_scenarios( # noqa: PLR0913 # Many parameters needed for comprehensive test scenarios
|
def test_enrich_price_info_scenarios( # Many parameters needed for comprehensive test scenarios
|
||||||
day_before_yesterday_price: float,
|
day_before_yesterday_price: float,
|
||||||
yesterday_price: float,
|
yesterday_price: float,
|
||||||
today_price: float,
|
today_price: float,
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,7 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.utils.price import (
|
from custom_components.tibber_prices.utils.price import _apply_rating_gap_tolerance, calculate_rating_level
|
||||||
_apply_rating_gap_tolerance,
|
|
||||||
calculate_rating_level,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from _pytest.logging import LogCaptureFixture
|
from _pytest.logging import LogCaptureFixture
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,9 @@ from unittest.mock import AsyncMock, MagicMock, Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.binary_sensor.core import (
|
from custom_components.tibber_prices.binary_sensor.core import TibberPricesBinarySensor
|
||||||
TibberPricesBinarySensor,
|
from custom_components.tibber_prices.coordinator.core import TibberPricesDataUpdateCoordinator
|
||||||
)
|
from custom_components.tibber_prices.coordinator.listeners import TibberPricesListenerManager
|
||||||
from custom_components.tibber_prices.coordinator.core import (
|
|
||||||
TibberPricesDataUpdateCoordinator,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.coordinator.listeners import (
|
|
||||||
TibberPricesListenerManager,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.sensor.core import TibberPricesSensor
|
from custom_components.tibber_prices.sensor.core import TibberPricesSensor
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -200,15 +194,9 @@ class TestConfigEntryCleanup:
|
||||||
from custom_components.tibber_prices.coordinator.data_transformation import ( # noqa: PLC0415
|
from custom_components.tibber_prices.coordinator.data_transformation import ( # noqa: PLC0415
|
||||||
TibberPricesDataTransformer,
|
TibberPricesDataTransformer,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.coordinator.listeners import ( # noqa: PLC0415
|
from custom_components.tibber_prices.coordinator.listeners import TibberPricesListenerManager # noqa: PLC0415
|
||||||
TibberPricesListenerManager,
|
from custom_components.tibber_prices.coordinator.periods import TibberPricesPeriodCalculator # noqa: PLC0415
|
||||||
)
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService # noqa: PLC0415
|
||||||
from custom_components.tibber_prices.coordinator.periods import ( # noqa: PLC0415
|
|
||||||
TibberPricesPeriodCalculator,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.coordinator.time_service import ( # noqa: PLC0415
|
|
||||||
TibberPricesTimeService,
|
|
||||||
)
|
|
||||||
|
|
||||||
coordinator.time = TibberPricesTimeService(hass)
|
coordinator.time = TibberPricesTimeService(hass)
|
||||||
coordinator._listener_manager = object.__new__(TibberPricesListenerManager) # noqa: SLF001
|
coordinator._listener_manager = object.__new__(TibberPricesListenerManager) # noqa: SLF001
|
||||||
|
|
@ -257,9 +245,7 @@ class TestCacheInvalidation:
|
||||||
|
|
||||||
def test_period_cache_invalidated_on_options_change(self) -> None:
|
def test_period_cache_invalidated_on_options_change(self) -> None:
|
||||||
"""Test that period calculation cache is cleared when options change."""
|
"""Test that period calculation cache is cleared when options change."""
|
||||||
from custom_components.tibber_prices.coordinator.periods import ( # noqa: PLC0415
|
from custom_components.tibber_prices.coordinator.periods import TibberPricesPeriodCalculator # noqa: PLC0415
|
||||||
TibberPricesPeriodCalculator,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create calculator with cached data
|
# Create calculator with cached data
|
||||||
calculator = object.__new__(TibberPricesPeriodCalculator)
|
calculator = object.__new__(TibberPricesPeriodCalculator)
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,9 @@ from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.binary_sensor.core import (
|
from custom_components.tibber_prices.binary_sensor.core import TibberPricesBinarySensor
|
||||||
TibberPricesBinarySensor,
|
from custom_components.tibber_prices.coordinator.core import TibberPricesDataUpdateCoordinator, get_connection_state
|
||||||
)
|
from custom_components.tibber_prices.sensor.calculators.lifecycle import TibberPricesLifecycleCalculator
|
||||||
from custom_components.tibber_prices.coordinator.core import (
|
|
||||||
TibberPricesDataUpdateCoordinator,
|
|
||||||
get_connection_state,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.sensor.calculators.lifecycle import (
|
|
||||||
TibberPricesLifecycleCalculator,
|
|
||||||
)
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorEntityDescription
|
from homeassistant.components.binary_sensor import BinarySensorEntityDescription
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers.update_coordinator import UpdateFailed
|
from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,7 @@ Ensures:
|
||||||
from custom_components.tibber_prices.binary_sensor.definitions import (
|
from custom_components.tibber_prices.binary_sensor.definitions import (
|
||||||
ENTITY_DESCRIPTIONS as BINARY_SENSOR_ENTITY_DESCRIPTIONS,
|
ENTITY_DESCRIPTIONS as BINARY_SENSOR_ENTITY_DESCRIPTIONS,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.coordinator.constants import (
|
from custom_components.tibber_prices.coordinator.constants import MINUTE_UPDATE_ENTITY_KEYS, TIME_SENSITIVE_ENTITY_KEYS
|
||||||
MINUTE_UPDATE_ENTITY_KEYS,
|
|
||||||
TIME_SENSITIVE_ENTITY_KEYS,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.sensor.definitions import ENTITY_DESCRIPTIONS
|
from custom_components.tibber_prices.sensor.definitions import ENTITY_DESCRIPTIONS
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,7 @@ from datetime import UTC, datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.time_service import (
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
TibberPricesTimeService,
|
|
||||||
)
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Quarter-Hour Rounding with Boundary Tolerance (CRITICAL)
|
# Quarter-Hour Rounding with Boundary Tolerance (CRITICAL)
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,8 @@ from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.constants import (
|
from custom_components.tibber_prices.coordinator.constants import QUARTER_HOUR_BOUNDARIES
|
||||||
QUARTER_HOUR_BOUNDARIES,
|
from custom_components.tibber_prices.coordinator.listeners import TibberPricesListenerManager
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.coordinator.listeners import (
|
|
||||||
TibberPricesListenerManager,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,8 @@ from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.data_transformation import (
|
from custom_components.tibber_prices.coordinator.data_transformation import TibberPricesDataTransformer
|
||||||
TibberPricesDataTransformer,
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.coordinator.time_service import (
|
|
||||||
TibberPricesTimeService,
|
|
||||||
)
|
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,7 @@ import pytest
|
||||||
|
|
||||||
from custom_components.tibber_prices.api.exceptions import TibberPricesApiClientError
|
from custom_components.tibber_prices.api.exceptions import TibberPricesApiClientError
|
||||||
from custom_components.tibber_prices.api.helpers import flatten_price_info
|
from custom_components.tibber_prices.api.helpers import flatten_price_info
|
||||||
from custom_components.tibber_prices.coordinator.price_data_manager import (
|
from custom_components.tibber_prices.coordinator.price_data_manager import TibberPricesPriceDataManager
|
||||||
TibberPricesPriceDataManager,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -59,7 +57,7 @@ def mock_interval_pool() -> Mock:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_validate_user_data_complete(mock_api_client, mock_time_service, mock_store, mock_interval_pool) -> None: # noqa: ANN001
|
def test_validate_user_data_complete(mock_api_client, mock_time_service, mock_store, mock_interval_pool) -> None:
|
||||||
"""Test that complete user data passes validation."""
|
"""Test that complete user data passes validation."""
|
||||||
price_data_manager = TibberPricesPriceDataManager(
|
price_data_manager = TibberPricesPriceDataManager(
|
||||||
api=mock_api_client,
|
api=mock_api_client,
|
||||||
|
|
@ -201,7 +199,7 @@ def test_validate_user_data_subscription_without_currency(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_validate_user_data_home_not_found(mock_api_client, mock_time_service, mock_store, mock_interval_pool) -> None: # noqa: ANN001
|
def test_validate_user_data_home_not_found(mock_api_client, mock_time_service, mock_store, mock_interval_pool) -> None:
|
||||||
"""Test that user data without the requested home fails validation."""
|
"""Test that user data without the requested home fails validation."""
|
||||||
price_data_manager = TibberPricesPriceDataManager(
|
price_data_manager = TibberPricesPriceDataManager(
|
||||||
api=mock_api_client,
|
api=mock_api_client,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue