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:
Julian Pawlowski 2026-04-14 19:33:24 +00:00
parent 07788a57ea
commit 1d065b11cd
83 changed files with 250 additions and 503 deletions

View file

@ -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:
""" """

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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:
""" """

View file

@ -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 []

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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],
*, *,

View file

@ -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"

View file

@ -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)

View file

@ -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)

View file

@ -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__)

View file

@ -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.

View file

@ -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:
""" """

View file

@ -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

View file

@ -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."""

View file

@ -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,

View file

@ -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,

View file

@ -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)

View file

@ -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):

View file

@ -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:

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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.

View file

@ -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(

View file

@ -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

View file

@ -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()

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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 (

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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,

View file

@ -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.

View file

@ -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.

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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,