mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
fix(services): use injected now in resolve_search_range day offset
_resolve_time_with_day_offset() was calling dt_util.now() internally instead of using the injected now parameter. This caused incorrect date calculations in tests and any caller that passes a specific reference time. Also add missing price_rank_* sensor keys to TIME_SENSITIVE_ENTITY_KEYS in coordinator/constants.py so quarter-hour refresh is registered for all 11 price rank sensors (current/next/previous interval and hour variants). Rename dt as dt_utils → dt as dt_util (ICN001) across 11 files to follow the project-wide import alias convention. Apply ruff auto-fixes for import ordering and collapsing single-item imports throughout the codebase. Released-Bug: no
This commit is contained in:
parent
07788a57ea
commit
1d065b11cd
83 changed files with 250 additions and 503 deletions
|
|
@ -4,16 +4,16 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
import 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:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue