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 base64
from datetime import datetime, timedelta
import logging
import re
import socket
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any
from zoneinfo import ZoneInfo
import aiohttp
from homeassistant.util import dt as dt_utils
from homeassistant.util import dt as dt_util
from .exceptions import (
TibberPricesApiClientAuthenticationError,
@ -21,12 +21,7 @@ from .exceptions import (
TibberPricesApiClientError,
TibberPricesApiClientPermissionError,
)
from .helpers import (
flatten_price_info,
prepare_headers,
verify_graphql_response,
verify_response_or_raise,
)
from .helpers import flatten_price_info, prepare_headers, verify_graphql_response, verify_response_or_raise
from .queries import TibberPricesQueryType
if TYPE_CHECKING:
@ -163,9 +158,7 @@ class TibberPricesApiClient:
"""
# Import here to avoid circular dependency (interval_pool imports TibberPricesApiClient)
from custom_components.tibber_prices.interval_pool import ( # noqa: PLC0415
get_price_intervals_for_range,
)
from custom_components.tibber_prices.interval_pool import get_price_intervals_for_range # noqa: PLC0415
price_info = await get_price_intervals_for_range(
api_client=self,
@ -581,7 +574,7 @@ class TibberPricesApiClient:
"""
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,
not simulated time from TimeService.
@ -594,7 +587,7 @@ class TibberPricesApiClient:
"""
# 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
if home_timezone:
@ -607,10 +600,10 @@ class TibberPricesApiClient:
home_timezone,
error,
)
now_in_home_tz = dt_utils.as_local(now)
now_in_home_tz = dt_util.as_local(now)
else:
# 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
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.
"""
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:
"""

View file

@ -492,7 +492,7 @@ def build_final_attributes_simple(
return result
async def build_async_extra_state_attributes( # noqa: PLR0913
async def build_async_extra_state_attributes(
entity_key: str,
translation_key: str | None,
hass: HomeAssistant,
@ -555,7 +555,7 @@ async def build_async_extra_state_attributes( # noqa: PLR0913
return attributes or None
def build_sync_extra_state_attributes( # noqa: PLR0913
def build_sync_extra_state_attributes(
entity_key: str,
translation_key: str | None,
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.entity import TibberPricesEntity
from custom_components.tibber_prices.entity_utils import get_binary_sensor_icon
from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorEntityDescription
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.restore_state import RestoreEntity
@ -27,9 +24,7 @@ from .attributes import (
if TYPE_CHECKING:
from collections.abc import Callable
from custom_components.tibber_prices.coordinator import (
TibberPricesDataUpdateCoordinator,
)
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService

View file

@ -2,10 +2,7 @@
from __future__ import annotations
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntityDescription,
)
from homeassistant.components.binary_sensor import BinarySensorDeviceClass, BinarySensorEntityDescription
from homeassistant.const import EntityCategory
# 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 .config_flow_handlers.options_flow import (
TibberPricesOptionsFlowHandler as OptionsFlowHandler,
)
from .config_flow_handlers.options_flow import TibberPricesOptionsFlowHandler as OptionsFlowHandler
from .config_flow_handlers.schemas import (
get_best_price_schema,
get_options_init_schema,
@ -23,9 +21,7 @@ from .config_flow_handlers.schemas import (
get_user_schema,
get_volatility_schema,
)
from .config_flow_handlers.subentry_flow import (
TibberPricesSubentryFlowHandler as SubentryFlowHandler,
)
from .config_flow_handlers.subentry_flow import TibberPricesSubentryFlowHandler as SubentryFlowHandler
from .config_flow_handlers.user_flow import TibberPricesConfigFlowHandler as ConfigFlow
from .config_flow_handlers.validators import (
TibberPricesCannotConnectError,

View file

@ -20,9 +20,7 @@ Supporting modules:
from __future__ import annotations
# Phase 3: Import flow handlers from their new modular structure
from custom_components.tibber_prices.config_flow_handlers.options_flow import (
TibberPricesOptionsFlowHandler,
)
from custom_components.tibber_prices.config_flow_handlers.options_flow import TibberPricesOptionsFlowHandler
from custom_components.tibber_prices.config_flow_handlers.schemas import (
get_best_price_schema,
get_options_init_schema,
@ -36,12 +34,8 @@ from custom_components.tibber_prices.config_flow_handlers.schemas import (
get_user_schema,
get_volatility_schema,
)
from custom_components.tibber_prices.config_flow_handlers.subentry_flow import (
TibberPricesSubentryFlowHandler,
)
from custom_components.tibber_prices.config_flow_handlers.user_flow import (
TibberPricesConfigFlowHandler,
)
from custom_components.tibber_prices.config_flow_handlers.subentry_flow import TibberPricesSubentryFlowHandler
from custom_components.tibber_prices.config_flow_handlers.user_flow import TibberPricesConfigFlowHandler
from custom_components.tibber_prices.config_flow_handlers.validators import (
TibberPricesCannotConnectError,
TibberPricesInvalidAuthError,

View file

@ -174,7 +174,7 @@ ConfigOverrides = dict[str, dict[str, Any]]
def is_field_overridden(
config_key: str,
config_section: str, # noqa: ARG001 - kept for API compatibility
config_section: str,
overrides: ConfigOverrides | None,
) -> bool:
"""

View file

@ -7,9 +7,7 @@ from typing import TYPE_CHECKING, Any
import voluptuous as vol
from custom_components.tibber_prices.config_flow_handlers.options_flow import (
TibberPricesOptionsFlowHandler,
)
from custom_components.tibber_prices.config_flow_handlers.options_flow import TibberPricesOptionsFlowHandler
from custom_components.tibber_prices.config_flow_handlers.schemas import (
get_reauth_confirm_schema,
get_select_home_schema,
@ -20,26 +18,11 @@ from custom_components.tibber_prices.config_flow_handlers.validators import (
TibberPricesInvalidAuthError,
validate_api_token,
)
from custom_components.tibber_prices.const import (
DOMAIN,
LOGGER,
get_default_options,
get_translation,
)
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from custom_components.tibber_prices.const import DOMAIN, LOGGER, get_default_options, get_translation
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import callback
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from homeassistant.helpers.selector import SelectOptionDict, SelectSelector, SelectSelectorConfig, SelectSelectorMode
if TYPE_CHECKING:
from homeassistant.config_entries import ConfigSubentryFlow
@ -65,7 +48,7 @@ class TibberPricesConfigFlowHandler(ConfigFlow, domain=DOMAIN):
@callback
def async_get_supported_subentry_types(
cls,
config_entry: ConfigEntry, # noqa: ARG003
config_entry: ConfigEntry,
) -> dict[str, type[ConfigSubentryFlow]]:
"""Return subentries supported by this integration."""
# 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 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."""
entry_id = self.context.get("entry_id")
if entry_id:
@ -295,7 +278,7 @@ class TibberPricesConfigFlowHandler(ConfigFlow, domain=DOMAIN):
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."""
homes = self._viewer.get("homes", []) if self._viewer else []

View file

@ -16,11 +16,7 @@ Main components:
- period_handlers/: Period calculation sub-package
"""
from .constants import (
MINUTE_UPDATE_ENTITY_KEYS,
STORAGE_VERSION,
TIME_SENSITIVE_ENTITY_KEYS,
)
from .constants import MINUTE_UPDATE_ENTITY_KEYS, STORAGE_VERSION, TIME_SENSITIVE_ENTITY_KEYS
from .core import TibberPricesDataUpdateCoordinator
from .time_service import TibberPricesTimeService

View file

@ -93,6 +93,18 @@ TIME_SENSITIVE_ENTITY_KEYS = frozenset(
"best_price_next_start_time",
"peak_price_end_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:
# - 23:45: turnover_pending (last interval before midnight)
# - 00:00: turnover complete (after midnight API update)

View file

@ -2,8 +2,8 @@
from __future__ import annotations
import logging
from datetime import timedelta
import logging
from typing import TYPE_CHECKING, Any
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
@ -24,16 +24,11 @@ from custom_components.tibber_prices.api import (
TibberPricesApiClientError,
)
from custom_components.tibber_prices.const import DOMAIN
from custom_components.tibber_prices.utils.price import (
find_price_data_for_interval,
)
from custom_components.tibber_prices.utils.price import find_price_data_for_interval
from homeassistant.exceptions import ConfigEntryAuthFailed
from . import helpers
from .constants import (
STORAGE_VERSION,
UPDATE_INTERVAL,
)
from .constants import STORAGE_VERSION, UPDATE_INTERVAL
from .data_transformation import TibberPricesDataTransformer
from .listeners import TibberPricesListenerManager
from .midnight_handler import TibberPricesMidnightHandler

View file

@ -2,8 +2,8 @@
from __future__ import annotations
import logging
from datetime import timedelta
import logging
from typing import TYPE_CHECKING, Any
from homeassistant.util import dt as dt_util

View file

@ -11,9 +11,7 @@ if TYPE_CHECKING:
from .types import TibberPricesPeriodConfig
from .outlier_filtering import (
filter_price_outliers,
)
from .outlier_filtering import filter_price_outliers
from .period_building import (
add_interval_ends,
build_periods,
@ -24,9 +22,7 @@ from .period_building import (
filter_superseded_periods,
split_intervals_by_day,
)
from .period_statistics import (
extract_period_summaries,
)
from .period_statistics import extract_period_summaries
from .shape_extension import extend_periods_for_shape
from .types import TibberPricesThresholdConfig
@ -81,7 +77,7 @@ def calculate_periods(
from .types import INDENT_L0 # noqa: PLC0415
_LOGGER = logging.getLogger(__name__) # noqa: N806
_LOGGER = logging.getLogger(__name__)
# Extract config values
reverse_sort = config.reverse_sort
@ -141,7 +137,7 @@ def calculate_periods(
# User's flex setting still applies to period criteria (in_flex check).
# 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)
if abs(flex) * 100 > MAX_OUTLIER_FLEX * 100:
@ -298,7 +294,7 @@ def calculate_periods(
def _period_belongs_to_side(
period: list[dict],
side_times: set,
time: "TibberPricesTimeService",
time: TibberPricesTimeService,
) -> bool:
"""Return True if the majority of a period's intervals are in side_times."""
if not period:
@ -307,14 +303,14 @@ def _period_belongs_to_side(
return in_side * 2 >= len(period)
def _apply_segment_forcing( # noqa: PLR0913
def _apply_segment_forcing(
all_prices_smoothed: list[dict],
periods: list[list[dict]],
price_context: dict[str, Any],
config: "TibberPricesPeriodConfig",
config: TibberPricesPeriodConfig,
*,
day_patterns_by_date: dict,
time: "TibberPricesTimeService",
time: TibberPricesTimeService,
) -> list[list[dict]]:
"""
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 .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
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 ───────────────────────────────────────────────────
def _classify_pattern( # noqa: PLR0911, PLR0912
def _classify_pattern(
extrema: list[dict[str, Any]],
cv_pct: float,
times: list[datetime],
@ -399,7 +399,7 @@ def _classify_pattern( # noqa: PLR0911, PLR0912
return DAY_PATTERN_PEAK, confidence
# ── two extrema ─────────────────────────────────────────────────────────────
if n_extrema == 2: # noqa: PLR2004
if n_extrema == 2:
if types == ["max", "min"]:
return DAY_PATTERN_FALLING, 0.7
if types == ["min", "max"]:
@ -410,7 +410,7 @@ def _classify_pattern( # noqa: PLR0911, PLR0912
return DAY_PATTERN_DOUBLE_PEAK, 0.65
# ── three extrema ────────────────────────────────────────────────────────────
if n_extrema == 3: # noqa: PLR2004
if n_extrema == 3:
# min-max-min → W-shape
if types == ["min", "max", "min"]:
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)
px_range = float(length)
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
max_dist = 0.0

View file

@ -204,7 +204,7 @@ def check_interval_criteria(
if scale_factor < SCALE_FACTOR_WARNING_THRESHOLD:
import logging # noqa: PLC0415
_LOGGER = logging.getLogger(f"{__name__}.details") # noqa: N806
_LOGGER = logging.getLogger(f"{__name__}.details")
_LOGGER.debug(
"High flex %.1f%% detected: Reducing min_distance %.1f%%%.1f%% (scale %.2f)",
flex_abs * 100,

View file

@ -14,8 +14,8 @@ Uses statistical methods:
from __future__ import annotations
import logging
from datetime import datetime
import logging
from typing import NamedTuple
from custom_components.tibber_prices.utils.price import calculate_coefficient_of_variation

View file

@ -2,8 +2,8 @@
from __future__ import annotations
import logging
from datetime import date, datetime, timedelta
import logging
from typing import TYPE_CHECKING, Any
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:
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
from .level_filtering import (
apply_level_filter,
check_interval_criteria,
compute_geometric_flex_bonus,
)
from .level_filtering import apply_level_filter, check_interval_criteria, compute_geometric_flex_bonus
from .types import TibberPricesIntervalCriteria
_LOGGER = logging.getLogger(__name__)
@ -54,7 +50,7 @@ def calculate_reference_prices(intervals_by_day: dict[date, list[dict]], *, reve
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],
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 .types import (
TibberPricesPeriodData,
TibberPricesPeriodStatistics,
TibberPricesThresholdConfig,
)
from .types import TibberPricesPeriodData, TibberPricesPeriodStatistics, TibberPricesThresholdConfig
from custom_components.tibber_prices.utils.average import calculate_median
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
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]],
all_prices: list[dict],
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).
"""
from .types import ( # noqa: PLC0415 - Avoid circular import
TibberPricesPeriodData,
TibberPricesPeriodStatistics,
)
from .types import TibberPricesPeriodData, TibberPricesPeriodStatistics # noqa: PLC0415 - Avoid circular import
# Build lookup dictionary for full price data by timestamp
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:
base_period = _strip_geo_from_edges(period)
if base_period:
period = base_period # noqa: PLW2901 - intentional period replacement
period = base_period
geo_extension_status = "attempted"
else:
geo_extension_status = "active"

View file

@ -2,8 +2,8 @@
from __future__ import annotations
import logging
from datetime import timedelta
import logging
from typing import TYPE_CHECKING, Any
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 .period_overlap import (
recalculate_period_metadata,
resolve_period_overlaps,
)
from .period_overlap import recalculate_period_metadata, resolve_period_overlaps
from .types import (
INDENT_L0,
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]
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
continue
@ -532,7 +529,7 @@ def _compute_day_effective_min(
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],
*,
config: TibberPricesPeriodConfig,
@ -584,12 +581,8 @@ def calculate_periods_with_relaxation( # noqa: PLR0912, PLR0913, PLR0915 - Per-
"""
# Import here to avoid circular dependency
from .core import ( # noqa: PLC0415
calculate_periods,
)
from .period_building import ( # noqa: PLC0415
filter_superseded_periods,
)
from .core import calculate_periods # noqa: PLC0415
from .period_building import filter_superseded_periods # noqa: PLC0415
# Compact INFO-level summary
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
for day_prices in prices_by_day.values():
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_avg = sum(prices) / len(prices)
span = abs(day_avg - day_min)
@ -877,7 +870,7 @@ def calculate_periods_with_relaxation( # noqa: PLR0912, PLR0913, PLR0915 - Per-
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],
config: TibberPricesPeriodConfig,
min_periods: int,
@ -914,9 +907,7 @@ def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation require
"""
# Import here to avoid circular dependency
from .core import ( # noqa: PLC0415
calculate_periods,
)
from .core import calculate_periods # noqa: PLC0415
flex_increment = 0.03 # 3% per step (hard-coded for reliability)
base_flex = abs(config.flex)

View file

@ -20,8 +20,8 @@ created by this step.
from __future__ import annotations
import statistics
from datetime import timedelta
import statistics
from typing import TYPE_CHECKING, Any
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_EXPENSIVE,
)
from custom_components.tibber_prices.utils.price import (
aggregate_period_levels,
aggregate_period_ratings,
)
from custom_components.tibber_prices.utils.price import aggregate_period_levels, aggregate_period_ratings
from .period_statistics import (
calculate_aggregated_rating_difference,
@ -51,7 +48,7 @@ if TYPE_CHECKING:
_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]],
all_prices: list[dict[str, Any]],
price_context: dict[str, Any],
@ -164,7 +161,7 @@ def _walk_contiguous(
return additions
def _extend_period_edges( # noqa: PLR0913 - Period edge extension requires many args
def _extend_period_edges(
period: 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) ────────────────────
prices_for_vol = [float(p["total"]) for p in all_period_intervals if "total" in p]
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)
if mean_p > 0:
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
import logging
from datetime import date, timedelta
import logging
from typing import TYPE_CHECKING, Any
from custom_components.tibber_prices import const as _const
@ -20,10 +20,7 @@ if TYPE_CHECKING:
from homeassistant.config_entries import ConfigEntry
from .helpers import get_intervals_for_day_offsets
from .period_handlers import (
TibberPricesPeriodConfig,
calculate_periods_with_relaxation,
)
from .period_handlers import TibberPricesPeriodConfig, calculate_periods_with_relaxation
_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
import logging
from datetime import timedelta
import logging
from typing import TYPE_CHECKING, Any
from custom_components.tibber_prices.api import (
@ -71,7 +71,7 @@ class TibberPricesPriceDataManager:
This class orchestrates WHEN to fetch and processes the results.
"""
def __init__( # noqa: PLR0913
def __init__(
self,
api: TibberPricesApiClient,
store: Any,
@ -178,7 +178,7 @@ class TibberPricesPriceDataManager:
)
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.

View file

@ -57,7 +57,7 @@ class TibberPricesRepairManager:
async def check_tomorrow_data_availability(
self,
has_tomorrow_data: bool, # noqa: FBT001 - Clear meaning in context
has_tomorrow_data: bool,
current_time: datetime,
) -> None:
"""

View file

@ -43,8 +43,8 @@ scheduling delays. It is NOT used for Timer #1's offset tracking.
from __future__ import annotations
import math
from datetime import datetime, timedelta
import math
from typing import TYPE_CHECKING
from homeassistant.util import dt as dt_util

View file

@ -16,7 +16,7 @@ if TYPE_CHECKING:
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, # noqa: ARG001
hass: HomeAssistant,
entry: TibberPricesConfigEntry,
) -> dict[str, Any]:
"""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 .attributes import (
add_description_attributes,
async_add_description_attributes,
)
from .attributes import add_description_attributes, async_add_description_attributes
from .colors import add_icon_color_attribute, get_icon_color
from .helpers import (
find_rolling_hour_center_index,
get_price_value,
)
from .helpers import find_rolling_hour_center_index, get_price_value
from .icons import (
get_binary_sensor_icon,
get_dynamic_icon,

View file

@ -10,7 +10,7 @@ if TYPE_CHECKING:
from ..data import TibberPricesConfigEntry # noqa: TID252
def add_description_attributes( # noqa: PLR0913, PLR0912
def add_description_attributes(
attributes: dict,
platform: str,
translation_key: str | None,
@ -113,7 +113,7 @@ def add_description_attributes( # noqa: PLR0913, PLR0912
attributes[key] = value
async def async_add_description_attributes( # noqa: PLR0913, PLR0912
async def async_add_description_attributes(
attributes: dict,
platform: str,
translation_key: str | None,

View file

@ -2,16 +2,14 @@
from __future__ import annotations
import logging
from datetime import datetime, timedelta
import logging
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:
from custom_components.tibber_prices.coordinator.time_service import (
TibberPricesTimeService,
)
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
_LOGGER = logging.getLogger(__name__)
_LOGGER_DETAILS = logging.getLogger(__name__ + ".details")
@ -114,7 +112,7 @@ class TibberPricesIntervalPoolFetchGroupCache:
"""
# 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()
# Check cache validity (invalidate daily)

View file

@ -2,11 +2,11 @@
from __future__ import annotations
import logging
from datetime import UTC, datetime, timedelta
import logging
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:
from collections.abc import Callable
@ -287,11 +287,9 @@ class TibberPricesIntervalPoolFetcher:
"""
# Import here to avoid circular dependency
from custom_components.tibber_prices.interval_pool.routing import ( # noqa: PLC0415
get_price_intervals_for_range,
)
from custom_components.tibber_prices.interval_pool.routing import get_price_intervals_for_range # noqa: PLC0415
fetch_time_iso = dt_utils.now().isoformat()
fetch_time_iso = dt_util.now().isoformat()
all_fetched_intervals = []
for idx, (missing_start_iso, missing_end_iso) in enumerate(missing_ranges, start=1):

View file

@ -2,8 +2,8 @@
from __future__ import annotations
import logging
from datetime import datetime
import logging
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:

View file

@ -4,8 +4,8 @@ from __future__ import annotations
import asyncio
import contextlib
import logging
from datetime import UTC, datetime, timedelta
import logging
from typing import TYPE_CHECKING, Any
from zoneinfo import ZoneInfo
@ -13,7 +13,7 @@ from custom_components.tibber_prices.api.exceptions import (
TibberPricesApiClientCommunicationError,
TibberPricesApiClientError,
)
from homeassistant.util import dt as dt_utils
from homeassistant.util import dt as dt_util
from .cache import TibberPricesIntervalPoolFetchGroupCache
from .fetcher import TibberPricesIntervalPoolFetcher
@ -23,9 +23,7 @@ from .storage import async_save_pool_state
if TYPE_CHECKING:
from custom_components.tibber_prices.api.client import TibberPricesApiClient
from custom_components.tibber_prices.coordinator.time_service import (
TibberPricesTimeService,
)
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
_LOGGER = logging.getLogger(__name__)
_LOGGER_DETAILS = logging.getLogger(__name__ + ".details")
@ -101,7 +99,7 @@ class TibberPricesIntervalPool:
hass: HomeAssistant instance for auto-save (optional).
entry_id: Config entry ID for auto-save (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
@ -206,7 +204,7 @@ class TibberPricesIntervalPool:
# Fetch missing ranges from API
api_fetch_failed = False
if missing_ranges:
fetch_time_iso = dt_utils.now().isoformat()
fetch_time_iso = dt_util.now().isoformat()
try:
# Fetch with callback for immediate caching
@ -301,7 +299,7 @@ class TibberPricesIntervalPool:
# Calculate range in home's timezone
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
# Day before yesterday 00:00 (start) - same for both fetch and return
@ -598,7 +596,7 @@ class TibberPricesIntervalPool:
result = []
# 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
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")
- 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.
"""
@ -17,7 +17,7 @@ import logging
from typing import TYPE_CHECKING, Any
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:
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
- 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.
This ensures predictable API responses.
@ -173,7 +173,7 @@ def _parse_timestamp(timestamp_str: str) -> datetime:
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:
msg = f"Failed to parse timestamp: {timestamp_str}"
raise ValueError(msg)

View file

@ -17,8 +17,7 @@ import logging
from typing import TYPE_CHECKING
from homeassistant.core import callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from .const import DOMAIN

View file

@ -11,19 +11,13 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from custom_components.tibber_prices.const import (
DOMAIN,
get_home_type_translation,
get_translation,
)
from custom_components.tibber_prices.const import DOMAIN, get_home_type_translation, get_translation
from homeassistant.components.number import NumberEntity, RestoreNumber
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
if TYPE_CHECKING:
from custom_components.tibber_prices.coordinator import (
TibberPricesDataUpdateCoordinator,
)
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
from .definitions import TibberPricesNumberEntityDescription

View file

@ -13,10 +13,7 @@ from __future__ import annotations
from dataclasses import dataclass
from homeassistant.components.number import (
NumberEntityDescription,
NumberMode,
)
from homeassistant.components.number import NumberEntityDescription, NumberMode
from homeassistant.const import PERCENTAGE, EntityCategory

View file

@ -17,10 +17,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from custom_components.tibber_prices.const import (
CONF_CURRENCY_DISPLAY_MODE,
DISPLAY_MODE_BASE,
)
from custom_components.tibber_prices.const import CONF_CURRENCY_DISPLAY_MODE, DISPLAY_MODE_BASE
from .core import TibberPricesSensor
from .definitions import ENTITY_DESCRIPTIONS

View file

@ -10,10 +10,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
from custom_components.tibber_prices.entity_utils import (
add_description_attributes,
add_icon_color_attribute,
)
from custom_components.tibber_prices.entity_utils import add_description_attributes, add_icon_color_attribute
from custom_components.tibber_prices.sensor.types import (
DailyStatPriceAttributes,
DailyStatRatingAttributes,
@ -32,9 +29,7 @@ from custom_components.tibber_prices.sensor.types import (
)
if TYPE_CHECKING:
from custom_components.tibber_prices.coordinator.core import (
TibberPricesDataUpdateCoordinator,
)
from custom_components.tibber_prices.coordinator.core import TibberPricesDataUpdateCoordinator
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
from custom_components.tibber_prices.data import TibberPricesConfigEntry
from homeassistant.core import HomeAssistant
@ -74,7 +69,7 @@ __all__ = [
]
def build_sensor_attributes( # noqa: PLR0912
def build_sensor_attributes(
key: str,
coordinator: TibberPricesDataUpdateCoordinator,
native_value: Any,
@ -228,7 +223,7 @@ def build_sensor_attributes( # noqa: PLR0912
return attributes or None
def build_extra_state_attributes( # noqa: PLR0913
def build_extra_state_attributes(
entity_key: str,
translation_key: str | None,
hass: HomeAssistant,

View file

@ -13,7 +13,7 @@ def add_alternate_average_attribute(
cached_data: dict,
base_key: str,
*,
config_entry: TibberPricesConfigEntry, # noqa: ARG001
config_entry: TibberPricesConfigEntry,
) -> None:
"""
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
if TYPE_CHECKING:
from custom_components.tibber_prices.coordinator.core import (
TibberPricesDataUpdateCoordinator,
)
from custom_components.tibber_prices.sensor.calculators.lifecycle import (
TibberPricesLifecycleCalculator,
)
from custom_components.tibber_prices.coordinator.core import TibberPricesDataUpdateCoordinator
from custom_components.tibber_prices.sensor.calculators.lifecycle import TibberPricesLifecycleCalculator
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
if TYPE_CHECKING:
from custom_components.tibber_prices.coordinator.core import (
TibberPricesDataUpdateCoordinator,
)
from custom_components.tibber_prices.coordinator.core import TibberPricesDataUpdateCoordinator
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService

View file

@ -16,7 +16,7 @@ def add_volatility_attributes(
attributes: dict,
cached_data: dict,
*,
time: TibberPricesTimeService, # noqa: ARG001
time: TibberPricesTimeService,
) -> None:
"""
Add attributes for volatility sensors.
@ -197,9 +197,7 @@ def add_percentile_rank_attributes(
coordinator_data = cached_data.get("coordinator_data")
if coordinator_data:
from custom_components.tibber_prices.coordinator.helpers import ( # noqa: PLC0415
get_intervals_for_day_offsets,
)
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets # noqa: PLC0415
all_intervals = get_intervals_for_day_offsets(coordinator_data, [-1, 0, 1])
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
if TYPE_CHECKING:
from custom_components.tibber_prices.coordinator.core import (
TibberPricesDataUpdateCoordinator,
)
from custom_components.tibber_prices.coordinator.core import TibberPricesDataUpdateCoordinator
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
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
def add_average_price_attributes( # noqa: PLR0913
def add_average_price_attributes(
attributes: dict,
key: str,
coordinator: TibberPricesDataUpdateCoordinator,

View file

@ -4,14 +4,10 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
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:
from custom_components.tibber_prices.coordinator import (
TibberPricesDataUpdateCoordinator,
)
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
from custom_components.tibber_prices.data import TibberPricesConfigEntry
from homeassistant.core import HomeAssistant

View file

@ -18,19 +18,8 @@ Organization by calculation pattern:
from __future__ import annotations
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
UnitOfArea,
UnitOfElectricCurrent,
UnitOfEnergy,
UnitOfTime,
)
from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription, SensorStateClass
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfArea, UnitOfElectricCurrent, UnitOfEnergy, UnitOfTime
# ============================================================================
# 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
def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parameters
def get_value_getter_mapping(
interval_calculator: TibberPricesIntervalCalculator,
rolling_hour_calculator: TibberPricesRollingHourCalculator,
daily_stat_calculator: TibberPricesDailyStatCalculator,

View file

@ -50,11 +50,7 @@ from .find_most_expensive_hours import (
FIND_MOST_EXPENSIVE_HOURS_SERVICE_SCHEMA,
handle_find_most_expensive_hours,
)
from .get_apexcharts_yaml import (
APEXCHARTS_SERVICE_SCHEMA,
APEXCHARTS_YAML_SERVICE_NAME,
handle_apexcharts_yaml,
)
from .get_apexcharts_yaml import APEXCHARTS_SERVICE_SCHEMA, APEXCHARTS_YAML_SERVICE_NAME, handle_apexcharts_yaml
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 .refresh_user_data import (

View file

@ -20,8 +20,8 @@ After calling this service:
from __future__ import annotations
import logging
from datetime import datetime, timedelta
import logging
from typing import TYPE_CHECKING, Any
import voluptuous as vol

View file

@ -8,25 +8,21 @@ machine, dryer).
from __future__ import annotations
from datetime import datetime, timedelta
import logging
import math
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
import voluptuous as vol
from custom_components.tibber_prices.const import (
DOMAIN,
get_display_unit_factor,
get_display_unit_string,
)
from custom_components.tibber_prices.const import DOMAIN, get_display_unit_factor, get_display_unit_string
from custom_components.tibber_prices.utils.price_window import (
calculate_window_statistics,
find_cheapest_contiguous_window,
)
from homeassistant.exceptions import ServiceValidationError
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 (
INTERVAL_MINUTES,
@ -143,7 +139,7 @@ def _determine_no_window_reason(
return "insufficient_contiguous_window"
async def _handle_find_block( # noqa: PLR0915
async def _handle_find_block(
call: ServiceCall,
*,
reverse: bool = False,
@ -187,7 +183,7 @@ async def _handle_find_block( # noqa: PLR0915
home_tz = ZoneInfo(home_timezone)
# 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)
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 datetime import datetime, timedelta
import logging
import math
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
import voluptuous as vol
from custom_components.tibber_prices.const import (
DOMAIN,
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 custom_components.tibber_prices.const import DOMAIN, 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.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 (
INTERVAL_MINUTES,
@ -101,7 +94,7 @@ def _determine_no_intervals_reason(
return "insufficient_intervals_for_constraints"
def _build_found_response( # noqa: PLR0913
def _build_found_response(
*,
result: dict,
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,
*,
reverse: bool = False,
@ -251,7 +244,7 @@ async def _handle_find_hours( # noqa: PLR0915
home_tz = ZoneInfo(home_timezone)
# 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)
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 datetime import datetime, timedelta
import logging
import math
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any
import voluptuous as vol
from custom_components.tibber_prices.const import (
DOMAIN,
get_display_unit_factor,
get_display_unit_string,
)
from custom_components.tibber_prices.const import DOMAIN, get_display_unit_factor, get_display_unit_string
from custom_components.tibber_prices.utils.price_window import (
calculate_window_statistics,
find_cheapest_contiguous_window,
)
from homeassistant.exceptions import ServiceValidationError
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 (
INTERVAL_MINUTES,
@ -213,7 +209,7 @@ def _find_cheapest_window_in_pool(
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."""
service_label = "find_cheapest_schedule"
hass: HomeAssistant = call.hass
@ -255,7 +251,7 @@ async def handle_find_cheapest_schedule(call: ServiceCall) -> ServiceResponse:
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)
# 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,
get_translation,
)
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.sensor.helpers import aggregate_level_data, aggregate_rating_data
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]
def aggregate_to_hourly( # noqa: PLR0912
def aggregate_to_hourly(
intervals: list[dict],
coordinator: Any,
threshold_low: float = DEFAULT_PRICE_RATING_THRESHOLD_LOW,
@ -166,7 +164,7 @@ def aggregate_to_hourly( # noqa: PLR0912
return hourly_data
def aggregate_hourly_exact( # noqa: PLR0913, PLR0912, PLR0915
def aggregate_hourly_exact(
intervals: list[dict],
start_time_field: str,
price_field: str,
@ -316,7 +314,7 @@ def aggregate_hourly_exact( # noqa: PLR0913, PLR0912, PLR0915
return hourly_data
def get_period_data( # noqa: PLR0913, PLR0912, PLR0915
def get_period_data(
*,
coordinator: Any,
period_filter: str,

View file

@ -37,12 +37,7 @@ from custom_components.tibber_prices.const import (
get_translation,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_registry import (
EntityRegistry,
)
from homeassistant.helpers.entity_registry import (
async_get as async_get_entity_registry,
)
from homeassistant.helpers.entity_registry import EntityRegistry, async_get as async_get_entity_registry
from .formatters import get_level_translation
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}
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.

View file

@ -21,9 +21,9 @@ Response: JSON with chart-ready data
from __future__ import annotations
from datetime import datetime, timedelta
import math
import re
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any, Final
import voluptuous as vol
@ -49,17 +49,10 @@ from custom_components.tibber_prices.const import (
get_currency_info,
get_currency_name,
)
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 .formatters import (
aggregate_to_hourly,
get_period_data,
normalize_level_filter,
normalize_rating_level_filter,
)
from .formatters import aggregate_to_hourly, get_period_data, normalize_level_filter, normalize_rating_level_filter
from .helpers import get_entry_and_data, has_tomorrow_data
if TYPE_CHECKING:
@ -92,7 +85,7 @@ def _is_transition_to_more_expensive(
return next_rank > current_rank
def _calculate_metadata( # noqa: PLR0912, PLR0913, PLR0915
def _calculate_metadata(
chart_data: list[dict[str, Any]],
price_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.

View file

@ -21,7 +21,7 @@ import voluptuous as vol
from custom_components.tibber_prices.const import DOMAIN
from homeassistant.exceptions import ServiceValidationError
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
@ -116,13 +116,13 @@ async def handle_get_price(call: ServiceCall) -> ServiceResponse:
if start_time.tzinfo is None:
# 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
start_time = start_time.astimezone(home_tz)
if end_time.tzinfo is None:
# 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
end_time = end_time.astimezone(home_tz)

View file

@ -29,14 +29,13 @@ Used by:
from __future__ import annotations
from datetime import datetime, timedelta
from datetime import time as dt_time
from datetime import datetime, time as dt_time, timedelta
from typing import TYPE_CHECKING, Any
from custom_components.tibber_prices.const import DOMAIN
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
from homeassistant.exceptions import ServiceValidationError
from homeassistant.util import dt as dt_utils
from homeassistant.util import dt as dt_util
if TYPE_CHECKING:
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
"""
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)
def calculate_end_of_tomorrow(home_tz: ZoneInfo) -> datetime:
"""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()
# End of tomorrow = midnight at start of day after tomorrow
return now_home.replace(
@ -295,9 +294,10 @@ def _resolve_time_with_day_offset(
time_value: dt_time,
day_offset: int,
home_tz: ZoneInfo,
now: datetime,
) -> datetime:
"""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()
return datetime(
year=target_date.year,
@ -477,7 +477,7 @@ def resolve_search_range(
search_start = localize_to_home_tz(call_data["search_start"], home_tz)
elif "search_start_time" in call_data:
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:
search_start = now + timedelta(minutes=call_data["search_start_offset_minutes"])
if include_current:
@ -490,7 +490,7 @@ def resolve_search_range(
search_end = localize_to_home_tz(call_data["search_end"], home_tz)
elif "search_end_time" in call_data:
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:
search_end = now + timedelta(minutes=call_data["search_end_offset_minutes"])
else:

View file

@ -11,20 +11,14 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from custom_components.tibber_prices.const import (
DOMAIN,
get_home_type_translation,
get_translation,
)
from custom_components.tibber_prices.const import DOMAIN, get_home_type_translation, get_translation
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.restore_state import RestoreEntity
if TYPE_CHECKING:
from custom_components.tibber_prices.coordinator import (
TibberPricesDataUpdateCoordinator,
)
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
from .definitions import TibberPricesSwitchEntityDescription

View file

@ -3,9 +3,9 @@
from __future__ import annotations
import bisect
from datetime import datetime, timedelta
import logging
import statistics
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
@ -353,7 +353,7 @@ def calculate_difference_percentage(
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,
threshold_low: float,
threshold_high: float,
@ -435,7 +435,7 @@ def calculate_rating_level( # noqa: PLR0911 - Multiple returns justified by cle
return PRICE_RATING_NORMAL
def _process_price_interval( # noqa: PLR0913 - Extra params needed for hysteresis
def _process_price_interval(
price_interval: dict[str, Any],
all_prices: list[dict[str, Any]],
threshold_low: float,
@ -651,7 +651,7 @@ def _apply_rating_gap_tolerance(
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
# 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 len(level_intervals) < 3: # noqa: PLR2004 - Minimum 3 for before/gap/after pattern
if len(level_intervals) < 3:
return
# Iteratively merge small blocks until no more changes
@ -859,7 +859,7 @@ def _merge_small_level_blocks(
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]],
*,
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,
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]]:
"""
Enrich price intervals with calculated 'difference' and 'rating_level' values.
@ -1229,7 +1229,7 @@ def _calculate_lookahead_volatility_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,
future_average: float,
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
import statistics
from datetime import datetime, timedelta
import statistics
from typing import Any

View file

@ -11,9 +11,11 @@ if TYPE_CHECKING:
import pytest
from custom_components.tibber_prices.services import find_cheapest_block as block_module
from custom_components.tibber_prices.services import find_cheapest_hours as hours_module
from custom_components.tibber_prices.services import find_cheapest_schedule as schedule_module
from custom_components.tibber_prices.services import (
find_cheapest_block as block_module,
find_cheapest_hours as hours_module,
find_cheapest_schedule as schedule_module,
)
from custom_components.tibber_prices.services.find_cheapest_block import (
_determine_no_window_reason,
handle_find_cheapest_block,

View file

@ -11,22 +11,15 @@ Also validates schema boundaries for all 4 services.
from __future__ import annotations
from datetime import datetime, timedelta
from datetime import time as dt_time
from datetime import datetime, time as dt_time, timedelta
from zoneinfo import ZoneInfo
import pytest
import voluptuous as vol
from custom_components.tibber_prices.services.find_cheapest_block import (
_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_block import _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
BERLIN = ZoneInfo("Europe/Berlin")

View file

@ -4,13 +4,8 @@ from datetime import UTC, datetime, timedelta
import pytest
from custom_components.tibber_prices.coordinator.time_service import (
TibberPricesTimeService,
)
from custom_components.tibber_prices.utils.average import (
calculate_leading_24h_mean,
calculate_trailing_24h_mean,
)
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
from custom_components.tibber_prices.utils.average import calculate_leading_24h_mean, calculate_trailing_24h_mean
@pytest.fixture

View file

@ -21,9 +21,7 @@ from custom_components.tibber_prices.coordinator.period_handlers import (
TibberPricesPeriodConfig,
calculate_periods_with_relaxation,
)
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

View file

@ -5,9 +5,7 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
# Import at module level to avoid PLC0415
from custom_components.tibber_prices.coordinator.core import (
TibberPricesDataUpdateCoordinator,
)
from custom_components.tibber_prices.coordinator.core import TibberPricesDataUpdateCoordinator
@pytest.mark.asyncio

View file

@ -18,24 +18,16 @@ Architecture:
Tests access internal components directly for fine-grained verification.
"""
import json
from datetime import UTC, datetime
import json
from unittest.mock import MagicMock
import pytest
from custom_components.tibber_prices.interval_pool.cache import (
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.manager import (
TibberPricesIntervalPool,
)
from custom_components.tibber_prices.interval_pool.cache import 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.manager import TibberPricesIntervalPool
@pytest.fixture

View file

@ -23,7 +23,7 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
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",)
@ -63,7 +63,7 @@ async def test_no_cache_single_api_call() -> None:
"_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
# 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._extract_home_timezones = MagicMock(return_value={"home123": "Europe/Berlin"}) # noqa: SLF001
# 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
# 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})
@ -103,7 +103,7 @@ async def test_full_cache_zero_api_calls() -> None:
"_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
# 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._extract_home_timezones = MagicMock(return_value={"home123": "Europe/Berlin"}) # noqa: SLF001
# 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
# 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})
@ -146,7 +146,7 @@ async def test_single_gap_single_api_call() -> None:
"_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
user_data = {"timeZone": "Europe/Berlin"}
@ -198,7 +198,7 @@ async def test_multiple_gaps_multiple_api_calls() -> None:
"_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
user_data = {"timeZone": "Europe/Berlin"}
@ -270,7 +270,7 @@ async def test_partial_overlap_minimal_fetch() -> None:
"_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"}
@ -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)
user_data = {"timeZone": "Europe/Berlin"}
@ -337,7 +337,7 @@ async def test_detect_missing_ranges_optimization() -> None:
pool._fetch_groups = [ # noqa: SLF001
{
"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

View file

@ -16,12 +16,8 @@ from __future__ import annotations
import pytest
from custom_components.tibber_prices.coordinator.period_handlers.level_filtering import (
check_interval_criteria,
)
from custom_components.tibber_prices.coordinator.period_handlers.types import (
TibberPricesIntervalCriteria,
)
from custom_components.tibber_prices.coordinator.period_handlers.level_filtering import check_interval_criteria
from custom_components.tibber_prices.coordinator.period_handlers.types import TibberPricesIntervalCriteria
@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)
"""
import statistics
from datetime import UTC, datetime, timedelta
import statistics
from unittest.mock import Mock
import pytest
from custom_components.tibber_prices.const import (
CONF_AVERAGE_SENSOR_DISPLAY,
DEFAULT_AVERAGE_SENSOR_DISPLAY,
)
from custom_components.tibber_prices.sensor.attributes.helpers import (
add_alternate_average_attribute,
)
from custom_components.tibber_prices.const import CONF_AVERAGE_SENSOR_DISPLAY, 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

View file

@ -12,9 +12,7 @@ from zoneinfo import ZoneInfo
import pytest
from custom_components.tibber_prices.coordinator.midnight_handler import (
TibberPricesMidnightHandler,
)
from custom_components.tibber_prices.coordinator.midnight_handler import TibberPricesMidnightHandler
@pytest.mark.unit

View file

@ -7,9 +7,7 @@ from zoneinfo import ZoneInfo
import pytest
from custom_components.tibber_prices.coordinator.period_handlers.relaxation import (
group_periods_by_day,
)
from custom_components.tibber_prices.coordinator.period_handlers.relaxation import group_periods_by_day
@pytest.fixture

View file

@ -8,15 +8,9 @@ from zoneinfo import ZoneInfo
import pytest
from custom_components.tibber_prices.coordinator.period_handlers.core import (
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.core import calculate_periods
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:

View file

@ -4,9 +4,7 @@ from datetime import UTC, datetime, timedelta
import pytest
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.utils.average import (
calculate_leading_24h_max,
calculate_leading_24h_min,

View file

@ -14,9 +14,7 @@ from zoneinfo import ZoneInfo
import pytest
from custom_components.tibber_prices.coordinator.constants import UPDATE_INTERVAL
from custom_components.tibber_prices.sensor.calculators.lifecycle import (
TibberPricesLifecycleCalculator,
)
from custom_components.tibber_prices.sensor.calculators.lifecycle import TibberPricesLifecycleCalculator
@pytest.mark.unit

View file

@ -21,9 +21,7 @@ from custom_components.tibber_prices.coordinator.period_handlers import (
TibberPricesPeriodConfig,
calculate_periods_with_relaxation,
)
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

View file

@ -4,9 +4,7 @@ from datetime import UTC, datetime
import pytest
from custom_components.tibber_prices.coordinator.period_handlers.period_statistics import (
calculate_period_price_diff,
)
from custom_components.tibber_prices.coordinator.period_handlers.period_statistics import calculate_period_price_diff
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
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.utils.price import (
aggregate_period_levels,
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"),
],
)
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,
yesterday_price: float,
today_price: float,

View file

@ -5,10 +5,7 @@ from typing import TYPE_CHECKING
import pytest
from custom_components.tibber_prices.utils.price import (
_apply_rating_gap_tolerance,
calculate_rating_level,
)
from custom_components.tibber_prices.utils.price import _apply_rating_gap_tolerance, calculate_rating_level
if TYPE_CHECKING:
from _pytest.logging import LogCaptureFixture

View file

@ -4,15 +4,9 @@ from unittest.mock import AsyncMock, MagicMock, Mock
import pytest
from custom_components.tibber_prices.binary_sensor.core import (
TibberPricesBinarySensor,
)
from custom_components.tibber_prices.coordinator.core import (
TibberPricesDataUpdateCoordinator,
)
from custom_components.tibber_prices.coordinator.listeners import (
TibberPricesListenerManager,
)
from custom_components.tibber_prices.binary_sensor.core import TibberPricesBinarySensor
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
@ -200,15 +194,9 @@ class TestConfigEntryCleanup:
from custom_components.tibber_prices.coordinator.data_transformation import ( # noqa: PLC0415
TibberPricesDataTransformer,
)
from custom_components.tibber_prices.coordinator.listeners import ( # noqa: PLC0415
TibberPricesListenerManager,
)
from custom_components.tibber_prices.coordinator.periods import ( # noqa: PLC0415
TibberPricesPeriodCalculator,
)
from custom_components.tibber_prices.coordinator.time_service import ( # noqa: PLC0415
TibberPricesTimeService,
)
from custom_components.tibber_prices.coordinator.listeners import TibberPricesListenerManager # noqa: PLC0415
from custom_components.tibber_prices.coordinator.periods import TibberPricesPeriodCalculator # noqa: PLC0415
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService # noqa: PLC0415
coordinator.time = TibberPricesTimeService(hass)
coordinator._listener_manager = object.__new__(TibberPricesListenerManager) # noqa: SLF001
@ -257,9 +245,7 @@ class TestCacheInvalidation:
def test_period_cache_invalidated_on_options_change(self) -> None:
"""Test that period calculation cache is cleared when options change."""
from custom_components.tibber_prices.coordinator.periods import ( # noqa: PLC0415
TibberPricesPeriodCalculator,
)
from custom_components.tibber_prices.coordinator.periods import TibberPricesPeriodCalculator # noqa: PLC0415
# Create calculator with cached data
calculator = object.__new__(TibberPricesPeriodCalculator)

View file

@ -8,16 +8,9 @@ from unittest.mock import Mock
import pytest
from custom_components.tibber_prices.binary_sensor.core import (
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.binary_sensor.core import TibberPricesBinarySensor
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.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import UpdateFailed

View file

@ -15,10 +15,7 @@ Ensures:
from custom_components.tibber_prices.binary_sensor.definitions import (
ENTITY_DESCRIPTIONS as BINARY_SENSOR_ENTITY_DESCRIPTIONS,
)
from custom_components.tibber_prices.coordinator.constants import (
MINUTE_UPDATE_ENTITY_KEYS,
TIME_SENSITIVE_ENTITY_KEYS,
)
from custom_components.tibber_prices.coordinator.constants import MINUTE_UPDATE_ENTITY_KEYS, TIME_SENSITIVE_ENTITY_KEYS
from custom_components.tibber_prices.sensor.definitions import ENTITY_DESCRIPTIONS

View file

@ -6,9 +6,7 @@ from datetime import UTC, datetime
import pytest
from custom_components.tibber_prices.coordinator.time_service import (
TibberPricesTimeService,
)
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
# =============================================================================
# Quarter-Hour Rounding with Boundary Tolerance (CRITICAL)

View file

@ -15,12 +15,8 @@ from unittest.mock import MagicMock, patch
import pytest
from custom_components.tibber_prices.coordinator.constants import (
QUARTER_HOUR_BOUNDARIES,
)
from custom_components.tibber_prices.coordinator.listeners import (
TibberPricesListenerManager,
)
from custom_components.tibber_prices.coordinator.constants import QUARTER_HOUR_BOUNDARIES
from custom_components.tibber_prices.coordinator.listeners import TibberPricesListenerManager
from homeassistant.core import HomeAssistant

View file

@ -14,12 +14,8 @@ from zoneinfo import ZoneInfo
import pytest
from custom_components.tibber_prices.coordinator.data_transformation import (
TibberPricesDataTransformer,
)
from custom_components.tibber_prices.coordinator.time_service import (
TibberPricesTimeService,
)
from custom_components.tibber_prices.coordinator.data_transformation import TibberPricesDataTransformer
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
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.helpers import flatten_price_info
from custom_components.tibber_prices.coordinator.price_data_manager import (
TibberPricesPriceDataManager,
)
from custom_components.tibber_prices.coordinator.price_data_manager import TibberPricesPriceDataManager
@pytest.fixture
@ -59,7 +57,7 @@ def mock_interval_pool() -> Mock:
@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."""
price_data_manager = TibberPricesPriceDataManager(
api=mock_api_client,
@ -201,7 +199,7 @@ def test_validate_user_data_subscription_without_currency(
@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."""
price_data_manager = TibberPricesPriceDataManager(
api=mock_api_client,