feat(types): add TypedDict documentation and BaseCalculator helpers

Phase 1.1 - TypedDict Documentation System:
- Created sensor/types.py with 14 TypedDict classes documenting sensor attributes
- Created binary_sensor/types.py with 3 TypedDict classes for binary sensors
- Added Literal types (PriceLevel, PriceRating, VolatilityLevel, DataCompleteness)
- Updated imports in sensor/attributes/__init__.py and binary_sensor/attributes.py
- Changed function signatures to use dict[str, Any] for runtime flexibility
- TypedDicts serve as IDE documentation, not runtime validation

Phase 1.2 - BaseCalculator Improvements:
- Added 8 smart data access methods to BaseCalculator:
  * get_intervals(day) - day-specific intervals with None-safety
  * intervals_today/tomorrow/yesterday - convenience properties
  * get_all_intervals() - combined yesterday+today+tomorrow
  * find_interval_at_offset(offset) - interval lookup with bounds checking
  * safe_get_from_interval(interval, key, default) - safe dict access
  * has_data() / has_price_info() - existence checks
  * get_day_intervals(day) - alias for consistency
- Refactored 5 calculator files to use new helper methods:
  * daily_stat.py: -11 lines (coordinator_data checks, get_intervals usage)
  * interval.py: -18 lines (eliminated find_price_data_for_interval duplication)
  * rolling_hour.py: -3 lines (simplified interval collection)
  * volatility.py: -4 lines (eliminated price_info local variable)
  * window_24h.py: -2 lines (replaced coordinator_data check)
  * Total: -38 lines of duplicate code eliminated
- Added noqa comment for lazy import (circular import avoidance)

Type Duplication Resolution:
- Identified duplication: Literal types in types.py vs string constants in const.py
- Attempted solution: Derive constants from Literal types using typing.get_args()
- Result: Circular import failure (const.py → sensor/types.py → sensor/__init__.py → const.py)
- Final solution: Keep string constants as single source of truth
- Added SYNC comments in all 3 files (const.py, sensor/types.py, binary_sensor/types.py)
- Accept manual synchronization to avoid circular dependencies
- Platform separation maintained (no cross-imports between sensor/ and binary_sensor/)

Impact: Developers get IDE autocomplete and type hints for attribute dictionaries.
Calculator code is more readable with fewer None-checks and clearer data access patterns.
Type/constant duplication documented with sync requirements.
This commit is contained in:
Julian Pawlowski 2025-11-22 14:32:24 +00:00
parent 32857c0cc0
commit 3b11c6721e
11 changed files with 621 additions and 40 deletions

View file

@ -6,6 +6,8 @@ from typing import TYPE_CHECKING
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute
# Import TypedDict definitions for documentation (not used in signatures)
if TYPE_CHECKING:
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
@ -24,6 +26,8 @@ def get_tomorrow_data_available_attributes(
"""
Build attributes for tomorrow_data_available sensor.
Returns TomorrowDataAvailableAttributes structure.
Args:
coordinator_data: Coordinator data dict
time: TibberPricesTimeService instance
@ -65,6 +69,8 @@ def get_price_intervals_attributes(
"""
Build attributes for period-based sensors (best/peak price).
Returns PeriodAttributes structure.
All data is already calculated in the coordinator - we just need to:
1. Get period summaries from coordinator (already filtered and fully calculated)
2. Add the current timestamp

View file

@ -0,0 +1,171 @@
"""
Type definitions for Tibber Prices binary sensor attributes.
These TypedDict definitions serve as **documentation** of the attribute structure
for each binary sensor type. They enable IDE autocomplete and type checking when
working with attribute dictionaries.
NOTE: In function signatures, we still use dict[str, Any] for flexibility,
but these TypedDict definitions document what keys and types are expected.
IMPORTANT: PriceLevel and PriceRating types are duplicated here to avoid
cross-platform dependencies. Keep in sync with sensor/types.py.
"""
from __future__ import annotations
from typing import Literal, TypedDict
# ============================================================================
# Literal Type Definitions (Duplicated from sensor/types.py)
# ============================================================================
# SYNC: Keep these in sync with:
# 1. sensor/types.py (Literal type definitions)
# 2. const.py (runtime string constants - single source of truth)
#
# const.py defines:
# - PRICE_LEVEL_VERY_CHEAP, PRICE_LEVEL_CHEAP, etc.
# - PRICE_RATING_LOW, PRICE_RATING_NORMAL, etc.
#
# These types are intentionally duplicated here to avoid cross-platform imports.
# Binary sensor attributes need these types for type safety without importing
# from sensor/ package (maintains platform separation).
# Price level literals (shared with sensor platform - keep in sync!)
PriceLevel = Literal[
"VERY_CHEAP",
"CHEAP",
"NORMAL",
"EXPENSIVE",
"VERY_EXPENSIVE",
]
# Price rating literals (shared with sensor platform - keep in sync!)
PriceRating = Literal[
"LOW",
"NORMAL",
"HIGH",
]
class BaseAttributes(TypedDict, total=False):
"""
Base attributes common to all binary sensors.
All binary sensor attributes include at minimum:
- timestamp: ISO 8601 string indicating when the state/attributes are valid
- error: Optional error message if something went wrong
"""
timestamp: str
error: str
class TomorrowDataAvailableAttributes(BaseAttributes, total=False):
"""
Attributes for tomorrow_data_available binary sensor.
Indicates whether tomorrow's price data is available from Tibber API.
"""
intervals_available: int # Number of intervals available for tomorrow
data_status: Literal["none", "partial", "full"] # Data completeness status
class PeriodSummary(TypedDict, total=False):
"""
Structure for period summary nested in period attributes.
Each period summary contains all calculated information about one period.
"""
# Time information (priority 1)
start: str # ISO 8601 timestamp of period start
end: str # ISO 8601 timestamp of period end
duration_minutes: int # Duration in minutes
# Core decision attributes (priority 2)
level: PriceLevel # Price level classification
rating_level: PriceRating # Price rating classification
rating_difference_pct: float # Difference from daily average (%)
# Price statistics (priority 3)
price_avg: float # Average price in period (minor currency)
price_min: float # Minimum price in period (minor currency)
price_max: float # Maximum price in period (minor currency)
price_spread: float # Price spread (max - min)
volatility: float # Price volatility within period
# Price comparison (priority 4)
period_price_diff_from_daily_min: float # Difference from daily min (minor currency)
period_price_diff_from_daily_min_pct: float # Difference from daily min (%)
# Detail information (priority 5)
period_interval_count: int # Number of intervals in period
period_position: int # Period position (1-based)
periods_total: int # Total number of periods
periods_remaining: int # Remaining periods after this one
# Relaxation information (priority 6 - only if period was relaxed)
relaxation_active: bool # Whether this period was found via relaxation
relaxation_level: int # Relaxation level used (1-based)
relaxation_threshold_original_pct: float # Original flex threshold (%)
relaxation_threshold_applied_pct: float # Applied flex threshold after relaxation (%)
class PeriodAttributes(BaseAttributes, total=False):
"""
Attributes for period-based binary sensors (best_price_period, peak_price_period).
These sensors indicate whether the current/next cheap/expensive period is active.
Attributes follow priority ordering:
1. Time information (timestamp, start, end, duration_minutes)
2. Core decision attributes (level, rating_level, rating_difference_%)
3. Price statistics (price_avg, price_min, price_max, price_spread, volatility)
4. Price comparison (period_price_diff_from_daily_min, period_price_diff_from_daily_min_%)
5. Detail information (period_interval_count, period_position, periods_total, periods_remaining)
6. Relaxation information (only if period was relaxed)
7. Meta information (periods list)
"""
# Time information (priority 1) - start/end refer to current/next period
start: str | None # ISO 8601 timestamp of current/next period start
end: str | None # ISO 8601 timestamp of current/next period end
duration_minutes: int # Duration of current/next period in minutes
# Core decision attributes (priority 2)
level: PriceLevel # Price level of current/next period
rating_level: PriceRating # Price rating of current/next period
rating_difference_pct: float # Difference from daily average (%)
# Price statistics (priority 3)
price_avg: float # Average price in current/next period (minor currency)
price_min: float # Minimum price in current/next period (minor currency)
price_max: float # Maximum price in current/next period (minor currency)
price_spread: float # Price spread (max - min) in current/next period
volatility: float # Price volatility within current/next period
# Price comparison (priority 4)
period_price_diff_from_daily_min: float # Difference from daily min (minor currency)
period_price_diff_from_daily_min_pct: float # Difference from daily min (%)
# Detail information (priority 5)
period_interval_count: int # Number of intervals in current/next period
period_position: int # Period position (1-based)
periods_total: int # Total number of periods found
periods_remaining: int # Remaining periods after current/next one
# Relaxation information (priority 6 - only if period was relaxed)
relaxation_active: bool # Whether current/next period was found via relaxation
relaxation_level: int # Relaxation level used (1-based)
relaxation_threshold_original_pct: float # Original flex threshold (%)
relaxation_threshold_applied_pct: float # Applied flex threshold after relaxation (%)
# Meta information (priority 7)
periods: list[PeriodSummary] # All periods found (sorted by start time)
# Union type for all binary sensor attributes (for documentation purposes)
# In actual code, use dict[str, Any] for flexibility
BinarySensorAttributes = TomorrowDataAvailableAttributes | PeriodAttributes

View file

@ -211,7 +211,14 @@ def format_price_unit_minor(currency_code: str | None) -> str:
return f"{minor_symbol}/{UnitOfPower.KILO_WATT}{UnitOfTime.HOURS}"
# Price level constants from Tibber API
# ============================================================================
# Price Level, Rating, and Volatility Constants
# ============================================================================
# IMPORTANT: These string constants are the single source of truth for
# valid enum values. The Literal types in sensor/types.py and binary_sensor/types.py
# should be kept in sync with these values manually.
# Price level constants (from Tibber API)
PRICE_LEVEL_VERY_CHEAP = "VERY_CHEAP"
PRICE_LEVEL_CHEAP = "CHEAP"
PRICE_LEVEL_NORMAL = "NORMAL"
@ -223,7 +230,7 @@ PRICE_RATING_LOW = "LOW"
PRICE_RATING_NORMAL = "NORMAL"
PRICE_RATING_HIGH = "HIGH"
# Price volatility levels (based on coefficient of variation: std_dev / mean * 100%)
# Price volatility level constants
VOLATILITY_LOW = "LOW"
VOLATILITY_MODERATE = "MODERATE"
VOLATILITY_HIGH = "HIGH"

View file

@ -14,6 +14,22 @@ 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,
FutureAttributes,
IntervalLevelAttributes,
# Import all types for re-export
IntervalPriceAttributes,
IntervalRatingAttributes,
LifecycleAttributes,
MetadataAttributes,
SensorAttributes,
TimingAttributes,
TrendAttributes,
VolatilityAttributes,
Window24hAttributes,
)
if TYPE_CHECKING:
from custom_components.tibber_prices.coordinator.core import (
@ -34,6 +50,20 @@ from .volatility import add_volatility_type_attributes, get_prices_for_volatilit
from .window_24h import add_average_price_attributes
__all__ = [
"DailyStatPriceAttributes",
"DailyStatRatingAttributes",
"FutureAttributes",
"IntervalLevelAttributes",
"IntervalPriceAttributes",
"IntervalRatingAttributes",
"LifecycleAttributes",
"MetadataAttributes",
# Type exports
"SensorAttributes",
"TimingAttributes",
"TrendAttributes",
"VolatilityAttributes",
"Window24hAttributes",
"add_volatility_type_attributes",
"build_extra_state_attributes",
"build_sensor_attributes",
@ -47,7 +77,7 @@ def build_sensor_attributes(
coordinator: TibberPricesDataUpdateCoordinator,
native_value: Any,
cached_data: dict,
) -> dict | None:
) -> dict[str, Any] | None:
"""
Build attributes for a sensor based on its key.
@ -175,7 +205,7 @@ def build_extra_state_attributes( # noqa: PLR0913
*,
config_entry: TibberPricesConfigEntry,
coordinator_data: dict,
sensor_attrs: dict | None = None,
sensor_attrs: dict[str, Any] | None = None,
time: TibberPricesTimeService,
) -> dict[str, Any] | None:
"""

View file

@ -69,3 +69,126 @@ class TibberPricesBaseCalculator:
def currency(self) -> str:
"""Get currency code from price info."""
return self.price_info.get("currency", "EUR")
# Smart data access methods with built-in None-safety
def get_intervals(self, day: str) -> list[dict]:
"""
Get price intervals for a specific day with None-safety.
Args:
day: Day key ("yesterday", "today", "tomorrow").
Returns:
List of interval dictionaries, empty list if unavailable.
"""
if not self.coordinator_data:
return []
return self.price_info.get(day, [])
@property
def intervals_today(self) -> list[dict]:
"""Get today's intervals with None-safety."""
return self.get_intervals("today")
@property
def intervals_tomorrow(self) -> list[dict]:
"""Get tomorrow's intervals with None-safety."""
return self.get_intervals("tomorrow")
@property
def intervals_yesterday(self) -> list[dict]:
"""Get yesterday's intervals with None-safety."""
return self.get_intervals("yesterday")
def get_all_intervals(self) -> list[dict]:
"""
Get all available intervals (yesterday + today + tomorrow).
Returns:
Combined list of all interval dictionaries.
"""
return [
*self.intervals_yesterday,
*self.intervals_today,
*self.intervals_tomorrow,
]
def find_interval_at_offset(self, offset: int) -> dict | None:
"""
Find interval at given offset from current time with bounds checking.
Args:
offset: Offset from current interval (0=current, 1=next, -1=previous).
Returns:
Interval dictionary or None if out of bounds or unavailable.
"""
if not self.coordinator_data:
return None
from custom_components.tibber_prices.utils.price import ( # noqa: PLC0415 - avoid circular import
find_price_data_for_interval,
)
time = self.coordinator.time
target_time = time.get_interval_offset_time(offset)
return find_price_data_for_interval(self.price_info, target_time, time=time)
def safe_get_from_interval(
self,
interval: dict[str, Any],
key: str,
default: Any = None,
) -> Any:
"""
Safely get a value from an interval dictionary.
Args:
interval: Interval dictionary.
key: Key to retrieve.
default: Default value if key not found.
Returns:
Value from interval or default.
"""
return interval.get(key, default) if interval else default
def has_data(self) -> bool:
"""
Check if coordinator has any data available.
Returns:
True if data is available, False otherwise.
"""
return bool(self.coordinator_data)
def has_price_info(self) -> bool:
"""
Check if price info is available in coordinator data.
Returns:
True if price info exists, False otherwise.
"""
return bool(self.price_info)
def get_day_intervals(self, day: str) -> list[dict]:
"""
Get intervals for a specific day from coordinator data.
This is an alias for get_intervals() with consistent naming.
Args:
day: Day key ("yesterday", "today", "tomorrow").
Returns:
List of interval dictionaries, empty list if unavailable.
"""
return self.get_intervals(day)

View file

@ -65,11 +65,9 @@ class TibberPricesDailyStatCalculator(TibberPricesBaseCalculator):
Price value in minor currency units (cents/øre), or None if unavailable.
"""
if not self.coordinator_data:
if not self.has_data():
return None
price_info = self.price_info
# Get local midnight boundaries based on the requested day using TimeService
time = self.coordinator.time
local_midnight, local_midnight_next_day = time.get_day_boundaries(day)
@ -78,7 +76,7 @@ class TibberPricesDailyStatCalculator(TibberPricesBaseCalculator):
# that fall within the target day's local date boundaries
price_intervals = []
for day_key in ["today", "tomorrow"]:
for price_data in price_info.get(day_key, []):
for price_data in self.get_intervals(day_key):
starts_at = price_data.get("startsAt") # Already datetime in local timezone
if not starts_at:
continue
@ -131,11 +129,9 @@ class TibberPricesDailyStatCalculator(TibberPricesBaseCalculator):
Aggregated level/rating value (lowercase), or None if unavailable.
"""
if not self.coordinator_data:
if not self.has_data():
return None
price_info = self.price_info
# Get local midnight boundaries based on the requested day using TimeService
time = self.coordinator.time
local_midnight, local_midnight_next_day = time.get_day_boundaries(day)
@ -144,7 +140,7 @@ class TibberPricesDailyStatCalculator(TibberPricesBaseCalculator):
# that fall within the target day's local date boundaries
day_intervals = []
for day_key in ["yesterday", "today", "tomorrow"]:
for price_data in price_info.get(day_key, []):
for price_data in self.get_intervals(day_key):
starts_at = price_data.get("startsAt") # Already datetime in local timezone
if not starts_at:
continue

View file

@ -4,8 +4,6 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from custom_components.tibber_prices.utils.price import find_price_data_for_interval
from .base import TibberPricesBaseCalculator
if TYPE_CHECKING:
@ -57,32 +55,27 @@ class TibberPricesIntervalCalculator(TibberPricesBaseCalculator):
None if data unavailable.
"""
if not self.coordinator_data:
if not self.has_data():
return None
price_info = self.price_info
time = self.coordinator.time
# Use TimeService to get interval offset time
target_time = time.get_interval_offset_time(interval_offset)
interval_data = find_price_data_for_interval(price_info, target_time, time=time)
interval_data = self.find_interval_at_offset(interval_offset)
if not interval_data:
return None
# Extract value based on type
if value_type == "price":
price = interval_data.get("total")
price = self.safe_get_from_interval(interval_data, "total")
if price is None:
return None
price = float(price)
return price if in_euro else round(price * 100, 2)
if value_type == "level":
level = interval_data.get("level")
level = self.safe_get_from_interval(interval_data, "level")
return level.lower() if level else None
# For rating: extract rating_level
rating = interval_data.get("rating_level")
rating = self.safe_get_from_interval(interval_data, "rating_level")
return rating.lower() if rating else None
def get_price_level_value(self) -> str | None:
@ -117,19 +110,16 @@ class TibberPricesIntervalCalculator(TibberPricesBaseCalculator):
Rating level (lowercase), or None if unavailable.
"""
if not self.coordinator_data or rating_type != "current":
if not self.has_data() or rating_type != "current":
self._last_rating_difference = None
self._last_rating_level = None
return None
time = self.coordinator.time
now = time.now()
price_info = self.price_info
current_interval = find_price_data_for_interval(price_info, now, time=time)
current_interval = self.find_interval_at_offset(0)
if current_interval:
rating_level = current_interval.get("rating_level")
difference = current_interval.get("difference")
rating_level = self.safe_get_from_interval(current_interval, "rating_level")
difference = self.safe_get_from_interval(current_interval, "difference")
if rating_level is not None:
self._last_rating_difference = float(difference) if difference is not None else None
self._last_rating_level = rating_level

View file

@ -48,12 +48,11 @@ class TibberPricesRollingHourCalculator(TibberPricesBaseCalculator):
- "rating": str (aggregated rating: "low", "normal", "high")
"""
if not self.coordinator_data:
if not self.has_data():
return None
# Get all available price data
price_info = self.price_info
all_prices = price_info.get("yesterday", []) + price_info.get("today", []) + price_info.get("tomorrow", [])
all_prices = self.get_all_intervals()
if not all_prices:
return None

View file

@ -51,11 +51,9 @@ class TibberPricesVolatilityCalculator(TibberPricesBaseCalculator):
Volatility level: "low", "moderate", "high", "very_high", or None if unavailable.
"""
if not self.coordinator_data:
if not self.has_data():
return None
price_info = self.price_info
# Get volatility thresholds from config
thresholds = {
"threshold_moderate": self.config.get("volatility_threshold_moderate", 5.0),
@ -64,7 +62,7 @@ class TibberPricesVolatilityCalculator(TibberPricesBaseCalculator):
}
# Get prices based on volatility type
prices_to_analyze = get_prices_for_volatility(volatility_type, price_info, time=self.coordinator.time)
prices_to_analyze = get_prices_for_volatility(volatility_type, self.price_info, time=self.coordinator.time)
if not prices_to_analyze:
return None
@ -96,7 +94,7 @@ class TibberPricesVolatilityCalculator(TibberPricesBaseCalculator):
# Add type-specific attributes
add_volatility_type_attributes(
self._last_volatility_attributes, volatility_type, price_info, thresholds, time=self.coordinator.time
self._last_volatility_attributes, volatility_type, self.price_info, thresholds, time=self.coordinator.time
)
# Return lowercase for ENUM device class

View file

@ -39,7 +39,7 @@ class TibberPricesWindow24hCalculator(TibberPricesBaseCalculator):
Price value in minor currency units (cents/øre), or None if unavailable.
"""
if not self.coordinator_data:
if not self.has_data():
return None
value = stat_func(self.coordinator_data, time=self.coordinator.time)

View file

@ -0,0 +1,261 @@
"""
Type definitions for Tibber Prices sensor attributes.
These TypedDict definitions serve as **documentation** of the attribute structure
for each sensor type. They enable IDE autocomplete and type checking when working
with attribute dictionaries.
NOTE: In function signatures, we still use dict[str, Any] for flexibility,
but these TypedDict definitions document what keys and types are expected.
IMPORTANT: The Literal types defined here should be kept in sync with the
string constants in const.py, which are the single source of truth for runtime values.
"""
from __future__ import annotations
from typing import Literal, TypedDict
# ============================================================================
# Literal Type Definitions
# ============================================================================
# SYNC: Keep these in sync with constants in const.py
#
# const.py defines the runtime constants (single source of truth):
# - PRICE_LEVEL_VERY_CHEAP, PRICE_LEVEL_CHEAP, etc.
# - PRICE_RATING_LOW, PRICE_RATING_NORMAL, etc.
# - VOLATILITY_LOW, VOLATILITY_MODERATE, etc.
#
# These Literal types should mirror those constants for type safety.
# Price level literals (from Tibber API)
PriceLevel = Literal[
"VERY_CHEAP",
"CHEAP",
"NORMAL",
"EXPENSIVE",
"VERY_EXPENSIVE",
]
# Price rating literals (calculated values)
PriceRating = Literal[
"LOW",
"NORMAL",
"HIGH",
]
# Volatility level literals (based on coefficient of variation)
VolatilityLevel = Literal[
"LOW",
"MODERATE",
"HIGH",
"VERY_HIGH",
]
# Data completeness literals
DataCompleteness = Literal[
"complete",
"partial_yesterday",
"partial_today",
"partial_tomorrow",
"missing_yesterday",
"missing_today",
"missing_tomorrow",
]
# ============================================================================
# TypedDict Definitions
# ============================================================================
class BaseAttributes(TypedDict, total=False):
"""
Base attributes common to all sensors.
All sensor attributes include at minimum:
- timestamp: ISO 8601 string indicating when the state/attributes are valid
- error: Optional error message if something went wrong
"""
timestamp: str
error: str
class IntervalPriceAttributes(BaseAttributes, total=False):
"""
Attributes for interval price sensors (current/next/previous).
These sensors show price information for a specific 15-minute interval.
"""
level_value: int # Numeric value for price level (1-5)
level_id: PriceLevel # String identifier for price level
icon_color: str # Optional icon color based on level
class IntervalLevelAttributes(BaseAttributes, total=False):
"""
Attributes for interval level sensors.
These sensors show the price level classification for an interval.
"""
icon_color: str # Icon color based on level
class IntervalRatingAttributes(BaseAttributes, total=False):
"""
Attributes for interval rating sensors.
These sensors show the price rating (LOW/NORMAL/HIGH) for an interval.
"""
rating_value: int # Numeric value for price rating (1-3)
rating_id: PriceRating # String identifier for price rating
icon_color: str # Optional icon color based on rating
class RollingHourAttributes(BaseAttributes, total=False):
"""
Attributes for rolling hour sensors.
These sensors aggregate data across 5 intervals (2 before + current + 2 after).
"""
icon_color: str # Optional icon color based on aggregated level
class DailyStatPriceAttributes(BaseAttributes, total=False):
"""
Attributes for daily statistics price sensors (min/max/avg).
These sensors show price statistics for a full calendar day.
"""
# No additional attributes for daily price stats beyond base
class DailyStatRatingAttributes(BaseAttributes, total=False):
"""
Attributes for daily statistics rating sensors.
These sensors show rating statistics for a full calendar day.
"""
diff_percent: str # Key is actually "diff_%" - percentage difference
level_id: PriceRating # Rating level identifier
level_value: int # Numeric rating value (1-3)
class Window24hAttributes(BaseAttributes, total=False):
"""
Attributes for 24-hour window sensors (trailing/leading).
These sensors analyze price data across a 24-hour window from current time.
"""
interval_count: int # Number of intervals in the window
class VolatilityAttributes(BaseAttributes, total=False):
"""
Attributes for volatility sensors.
These sensors analyze price variation and spread across time periods.
"""
today_spread: float # Price range for today (max - min)
today_volatility: str # Volatility level for today
interval_count_today: int # Number of intervals analyzed today
tomorrow_spread: float # Price range for tomorrow (max - min)
tomorrow_volatility: str # Volatility level for tomorrow
interval_count_tomorrow: int # Number of intervals analyzed tomorrow
class TrendAttributes(BaseAttributes, total=False):
"""
Attributes for trend sensors.
These sensors analyze price trends and forecast future movements.
Trend attributes are complex and may vary based on trend type.
"""
# Trend attributes are dynamic and vary by sensor type
# Keep flexible with total=False
class TimingAttributes(BaseAttributes, total=False):
"""
Attributes for period timing sensors (best_price/peak_price timing).
These sensors track timing information for best/peak price periods.
"""
icon_color: str # Icon color based on timing status
class FutureAttributes(BaseAttributes, total=False):
"""
Attributes for future forecast sensors.
These sensors provide N-hour forecasts starting from next interval.
"""
interval_count: int # Number of intervals in forecast
hours: int # Number of hours in forecast window
class LifecycleAttributes(BaseAttributes, total=False):
"""
Attributes for lifecycle/diagnostic sensors.
These sensors provide system information and cache status.
"""
cache_age: str # Human-readable cache age
cache_age_minutes: int # Cache age in minutes
cache_validity: str # Cache validity status
last_api_fetch: str # ISO 8601 timestamp of last API fetch
last_cache_update: str # ISO 8601 timestamp of last cache update
data_completeness: DataCompleteness # Data completeness status
yesterday_available: bool # Whether yesterday data exists
today_available: bool # Whether today data exists
tomorrow_available: bool # Whether tomorrow data exists
tomorrow_expected_after: str # Time when tomorrow data expected
next_api_poll: str # ISO 8601 timestamp of next API poll
next_midnight_turnover: str # ISO 8601 timestamp of next midnight turnover
updates_today: int # Number of API updates today
last_turnover: str # ISO 8601 timestamp of last midnight turnover
last_error: str # Last error message if any
class MetadataAttributes(BaseAttributes, total=False):
"""
Attributes for metadata sensors (home info, metering point).
These sensors provide Tibber account and home metadata.
Metadata attributes vary by sensor type.
"""
# Metadata attributes are dynamic and vary by sensor type
# Keep flexible with total=False
# Union type for all sensor attributes (for documentation purposes)
# In actual code, use dict[str, Any] for flexibility
SensorAttributes = (
IntervalPriceAttributes
| IntervalLevelAttributes
| IntervalRatingAttributes
| RollingHourAttributes
| DailyStatPriceAttributes
| DailyStatRatingAttributes
| Window24hAttributes
| VolatilityAttributes
| TrendAttributes
| TimingAttributes
| FutureAttributes
| LifecycleAttributes
| MetadataAttributes
)