""" Sensor entity definitions for Tibber Prices. This module contains all SensorEntityDescription definitions organized by calculation method. Sensor definitions are declarative and independent of the implementation logic. Organization by calculation pattern: 1. Interval-based: Time offset from current interval 2. Rolling hour: 5-interval aggregation windows 3. Daily statistics: Calendar day min/max/avg 4. 24h windows: Trailing/leading statistics 5. Future forecast: N-hour windows from next interval 6. Volatility: Price variation analysis 7. Best/Peak Price timing: Period-based time tracking 8. Diagnostic: System metadata """ from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription, SensorStateClass from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfArea, UnitOfElectricCurrent, UnitOfEnergy, UnitOfTime # ============================================================================ # SENSOR DEFINITIONS - Grouped by calculation method # ============================================================================ # # Sensors are organized by HOW they calculate values, not WHAT they display. # This groups sensors that share common logic and enables code reuse through # unified handler methods. # # Calculation patterns: # 1. Interval-based: Use time offset from current interval # 2. Rolling hour: Aggregate 5-interval window (2 before + center + 2 after) # 3. Daily statistics: Min/max/avg within calendar day boundaries # 4. 24h windows: Trailing/leading from current interval # 5. Future forecast: N-hour windows starting from next interval # 6. Volatility: Statistical analysis of price variation # 7. Best/Peak Price timing: Period-based time tracking (requires minute updates) # 8. Diagnostic: System information and metadata # ============================================================================ # ---------------------------------------------------------------------------- # 1. INTERVAL-BASED SENSORS (offset: -1, 0, +1 from current interval) # ---------------------------------------------------------------------------- # All use find_price_data_for_interval() with time offset # Shared handler: _get_interval_value(interval_offset, value_type) INTERVAL_PRICE_SENSORS = ( SensorEntityDescription( key="current_interval_price", translation_key="current_interval_price", icon="mdi:cash", # Dynamic: shows cash-multiple/plus/cash/minus/remove based on price level device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None suggested_display_precision=2, ), SensorEntityDescription( key="current_interval_price_base", translation_key="current_interval_price_base", icon="mdi:cash", # Dynamic: shows cash-multiple/plus/cash/minus/remove based on price level device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None for Energy Dashboard suggested_display_precision=4, # More precision for base currency (e.g., 0.2534 EUR/kWh) ), SensorEntityDescription( key="next_interval_price", translation_key="next_interval_price", icon="mdi:cash", # Dynamic: shows cash-multiple/plus/cash/minus/remove based on price level device_class=SensorDeviceClass.MONETARY, state_class=None, # Future value: historical chart not useful suggested_display_precision=2, ), SensorEntityDescription( key="previous_interval_price", translation_key="previous_interval_price", icon="mdi:cash-refund", # Static: arrow back indicates "past" device_class=SensorDeviceClass.MONETARY, state_class=None, # Past snapshot: historical chart not useful entity_registry_enabled_default=False, suggested_display_precision=2, ), ) # NOTE: Enum options are defined inline (not imported from const.py) to avoid # import timing issues with Home Assistant's entity platform initialization. # Keep in sync with PRICE_LEVEL_* constants in const.py! INTERVAL_LEVEL_SENSORS = ( SensorEntityDescription( key="current_interval_price_level", translation_key="current_interval_price_level", icon="mdi:gauge", # Dynamic: shows gauge/gauge-empty/gauge-low/gauge-full based on level value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["very_cheap", "cheap", "normal", "expensive", "very_expensive"], ), SensorEntityDescription( key="next_interval_price_level", translation_key="next_interval_price_level", icon="mdi:gauge", # Dynamic: shows gauge/gauge-empty/gauge-low/gauge-full based on level value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["very_cheap", "cheap", "normal", "expensive", "very_expensive"], ), SensorEntityDescription( key="previous_interval_price_level", translation_key="previous_interval_price_level", icon="mdi:gauge", # Dynamic: shows gauge/gauge-empty/gauge-low/gauge-full based on level value entity_registry_enabled_default=False, device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["very_cheap", "cheap", "normal", "expensive", "very_expensive"], ), ) # NOTE: Enum options are defined inline (not imported from const.py) to avoid # import timing issues with Home Assistant's entity platform initialization. # Keep in sync with PRICE_RATING_* constants in const.py! INTERVAL_RATING_SENSORS = ( SensorEntityDescription( key="current_interval_price_rating", translation_key="current_interval_price_rating", icon="mdi:thumbs-up-down", # Dynamic: shows thumbs-up/thumbs-up-down/thumbs-down based on rating value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["low", "normal", "high"], entity_registry_enabled_default=False, # Level is more commonly used ), SensorEntityDescription( key="next_interval_price_rating", translation_key="next_interval_price_rating", icon="mdi:thumbs-up-down", # Dynamic: shows thumbs-up/thumbs-up-down/thumbs-down based on rating value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["low", "normal", "high"], entity_registry_enabled_default=False, # Level is more commonly used ), SensorEntityDescription( key="previous_interval_price_rating", translation_key="previous_interval_price_rating", icon="mdi:thumbs-up-down", # Dynamic: shows thumbs-up/thumbs-up-down/thumbs-down based on rating value entity_registry_enabled_default=False, device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["low", "normal", "high"], ), ) # ---------------------------------------------------------------------------- # 2. ROLLING HOUR SENSORS (5-interval window: 2 before + center + 2 after) # ---------------------------------------------------------------------------- # All aggregate data from rolling 5-interval window around a specific hour # Shared handler: _get_rolling_hour_value(hour_offset, value_type) ROLLING_HOUR_PRICE_SENSORS = ( SensorEntityDescription( key="current_hour_average_price", translation_key="current_hour_average_price", icon="mdi:cash", # Dynamic: shows cash-multiple/plus/cash/minus/remove based on aggregated price level device_class=SensorDeviceClass.MONETARY, state_class=None, # Rolling derived value: historical chart not useful suggested_display_precision=2, ), SensorEntityDescription( key="next_hour_average_price", translation_key="next_hour_average_price", icon="mdi:cash-fast", # Dynamic: shows cash-multiple/plus/cash/minus/remove based on aggregated price level device_class=SensorDeviceClass.MONETARY, state_class=None, # Future derived value: historical chart not useful suggested_display_precision=2, ), ) # NOTE: Enum options are defined inline (not imported from const.py) to avoid # import timing issues with Home Assistant's entity platform initialization. # Keep in sync with PRICE_LEVEL_* constants in const.py! ROLLING_HOUR_LEVEL_SENSORS = ( SensorEntityDescription( key="current_hour_price_level", translation_key="current_hour_price_level", icon="mdi:gauge", # Dynamic: shows gauge/gauge-empty/gauge-low/gauge-full based on aggregated level value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["very_cheap", "cheap", "normal", "expensive", "very_expensive"], ), SensorEntityDescription( key="next_hour_price_level", translation_key="next_hour_price_level", icon="mdi:gauge", # Dynamic: shows gauge/gauge-empty/gauge-low/gauge-full based on aggregated level value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["very_cheap", "cheap", "normal", "expensive", "very_expensive"], ), ) # NOTE: Enum options are defined inline (not imported from const.py) to avoid # import timing issues with Home Assistant's entity platform initialization. # Keep in sync with PRICE_RATING_* constants in const.py! ROLLING_HOUR_RATING_SENSORS = ( SensorEntityDescription( key="current_hour_price_rating", translation_key="current_hour_price_rating", # Dynamic: shows thumbs-up/thumbs-up-down/thumbs-down based on aggregated rating value icon="mdi:thumbs-up-down", device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["low", "normal", "high"], entity_registry_enabled_default=False, # Level is more commonly used ), SensorEntityDescription( key="next_hour_price_rating", translation_key="next_hour_price_rating", # Dynamic: shows thumbs-up/thumbs-up-down/thumbs-down based on aggregated rating value icon="mdi:thumbs-up-down", device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["low", "normal", "high"], entity_registry_enabled_default=False, # Level is more commonly used ), ) # ---------------------------------------------------------------------------- # 3. DAILY STATISTICS SENSORS (min/max/avg for calendar day boundaries) # ---------------------------------------------------------------------------- # Calculate statistics for specific calendar days (today/tomorrow) DAILY_STAT_SENSORS = ( SensorEntityDescription( key="lowest_price_today", translation_key="lowest_price_today", icon="mdi:arrow-collapse-down", device_class=SensorDeviceClass.MONETARY, state_class=None, # Daily snapshot: stays constant most of the day suggested_display_precision=2, ), SensorEntityDescription( key="highest_price_today", translation_key="highest_price_today", icon="mdi:arrow-collapse-up", device_class=SensorDeviceClass.MONETARY, state_class=None, # Daily snapshot: stays constant most of the day suggested_display_precision=2, ), SensorEntityDescription( key="average_price_today", translation_key="average_price_today", icon="mdi:chart-line", device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL, # Keep TOTAL: useful to track daily avg over weeks/months suggested_display_precision=2, ), SensorEntityDescription( key="lowest_price_tomorrow", translation_key="lowest_price_tomorrow", icon="mdi:arrow-collapse-down", device_class=SensorDeviceClass.MONETARY, state_class=None, # Future data: historical chart not useful suggested_display_precision=2, ), SensorEntityDescription( key="highest_price_tomorrow", translation_key="highest_price_tomorrow", icon="mdi:arrow-collapse-up", device_class=SensorDeviceClass.MONETARY, state_class=None, # Future data: historical chart not useful suggested_display_precision=2, ), SensorEntityDescription( key="average_price_tomorrow", translation_key="average_price_tomorrow", icon="mdi:chart-line", device_class=SensorDeviceClass.MONETARY, state_class=None, # Future data: historical chart not useful suggested_display_precision=2, ), ) # NOTE: Enum options are defined inline (not imported from const.py) to avoid # import timing issues with Home Assistant's entity platform initialization. # Keep in sync with PRICE_LEVEL_* constants in const.py! DAILY_LEVEL_SENSORS = ( SensorEntityDescription( key="yesterday_price_level", translation_key="yesterday_price_level", icon="mdi:gauge", # Dynamic: shows gauge/gauge-empty/gauge-low/gauge-full based on daily level value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["very_cheap", "cheap", "normal", "expensive", "very_expensive"], entity_registry_enabled_default=False, ), SensorEntityDescription( key="today_price_level", translation_key="today_price_level", icon="mdi:gauge", # Dynamic: shows gauge/gauge-empty/gauge-low/gauge-full based on daily level value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["very_cheap", "cheap", "normal", "expensive", "very_expensive"], ), SensorEntityDescription( key="tomorrow_price_level", translation_key="tomorrow_price_level", icon="mdi:gauge", # Dynamic: shows gauge/gauge-empty/gauge-low/gauge-full based on daily level value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["very_cheap", "cheap", "normal", "expensive", "very_expensive"], ), ) # NOTE: Enum options are defined inline (not imported from const.py) to avoid # import timing issues with Home Assistant's entity platform initialization. # Keep in sync with PRICE_RATING_* constants in const.py! DAILY_RATING_SENSORS = ( SensorEntityDescription( key="yesterday_price_rating", translation_key="yesterday_price_rating", # Dynamic: shows thumbs-up/thumbs-up-down/thumbs-down based on daily rating value icon="mdi:thumbs-up-down", device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["low", "normal", "high"], entity_registry_enabled_default=False, ), SensorEntityDescription( key="today_price_rating", translation_key="today_price_rating", # Dynamic: shows thumbs-up/thumbs-up-down/thumbs-down based on daily rating value icon="mdi:thumbs-up-down", device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["low", "normal", "high"], entity_registry_enabled_default=False, # Level is more commonly used ), SensorEntityDescription( key="tomorrow_price_rating", translation_key="tomorrow_price_rating", # Dynamic: shows thumbs-up/thumbs-up-down/thumbs-down based on daily rating value icon="mdi:thumbs-up-down", device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["low", "normal", "high"], entity_registry_enabled_default=False, # Level is more commonly used ), ) # ---------------------------------------------------------------------------- # 4. 24H WINDOW SENSORS (trailing/leading from current interval) # ---------------------------------------------------------------------------- # Calculate statistics over sliding 24-hour windows WINDOW_24H_SENSORS = ( SensorEntityDescription( key="trailing_price_average", translation_key="trailing_price_average", icon="mdi:chart-line", device_class=SensorDeviceClass.MONETARY, state_class=None, # Rolling window: value shifts every 15min, history chart misleading entity_registry_enabled_default=False, suggested_display_precision=2, ), SensorEntityDescription( key="leading_price_average", translation_key="leading_price_average", icon="mdi:chart-line-variant", device_class=SensorDeviceClass.MONETARY, state_class=None, # Rolling window: value shifts every 15min, history chart misleading entity_registry_enabled_default=False, # Advanced use case suggested_display_precision=2, ), SensorEntityDescription( key="trailing_price_min", translation_key="trailing_price_min", icon="mdi:arrow-collapse-down", device_class=SensorDeviceClass.MONETARY, state_class=None, # Rolling window: value shifts every 15min, history chart misleading entity_registry_enabled_default=False, suggested_display_precision=2, ), SensorEntityDescription( key="trailing_price_max", translation_key="trailing_price_max", icon="mdi:arrow-collapse-up", device_class=SensorDeviceClass.MONETARY, state_class=None, # Rolling window: value shifts every 15min, history chart misleading entity_registry_enabled_default=False, suggested_display_precision=2, ), SensorEntityDescription( key="leading_price_min", translation_key="leading_price_min", icon="mdi:arrow-collapse-down", device_class=SensorDeviceClass.MONETARY, state_class=None, # Rolling window: value shifts every 15min, history chart misleading entity_registry_enabled_default=False, # Advanced use case suggested_display_precision=2, ), SensorEntityDescription( key="leading_price_max", translation_key="leading_price_max", icon="mdi:arrow-collapse-up", device_class=SensorDeviceClass.MONETARY, state_class=None, # Rolling window: value shifts every 15min, history chart misleading entity_registry_enabled_default=False, # Advanced use case suggested_display_precision=2, ), ) # ---------------------------------------------------------------------------- # 5. FUTURE FORECAST SENSORS (N-hour windows starting from next interval) # ---------------------------------------------------------------------------- # Calculate averages and trends for upcoming time windows FUTURE_MEAN_SENSORS = ( # Default enabled: 1h-5h SensorEntityDescription( key="next_avg_1h", translation_key="next_avg_1h", icon="mdi:chart-line", device_class=SensorDeviceClass.MONETARY, state_class=None, # Future forecast: historical chart not useful suggested_display_precision=2, entity_registry_enabled_default=True, ), SensorEntityDescription( key="next_avg_2h", translation_key="next_avg_2h", icon="mdi:chart-line", device_class=SensorDeviceClass.MONETARY, state_class=None, # Future forecast: historical chart not useful suggested_display_precision=2, entity_registry_enabled_default=True, ), SensorEntityDescription( key="next_avg_3h", translation_key="next_avg_3h", icon="mdi:chart-line", device_class=SensorDeviceClass.MONETARY, state_class=None, # Future forecast: historical chart not useful suggested_display_precision=2, entity_registry_enabled_default=True, ), SensorEntityDescription( key="next_avg_4h", translation_key="next_avg_4h", icon="mdi:chart-line", device_class=SensorDeviceClass.MONETARY, state_class=None, # Future forecast: historical chart not useful suggested_display_precision=2, entity_registry_enabled_default=True, ), SensorEntityDescription( key="next_avg_5h", translation_key="next_avg_5h", icon="mdi:chart-line", device_class=SensorDeviceClass.MONETARY, state_class=None, # Future forecast: historical chart not useful suggested_display_precision=2, entity_registry_enabled_default=True, ), # Disabled by default: 6h, 8h, 12h (advanced use cases) SensorEntityDescription( key="next_avg_6h", translation_key="next_avg_6h", icon="mdi:chart-line", device_class=SensorDeviceClass.MONETARY, state_class=None, # Future forecast: historical chart not useful suggested_display_precision=2, entity_registry_enabled_default=False, ), SensorEntityDescription( key="next_avg_8h", translation_key="next_avg_8h", icon="mdi:chart-line", device_class=SensorDeviceClass.MONETARY, state_class=None, # Future forecast: historical chart not useful suggested_display_precision=2, entity_registry_enabled_default=False, ), SensorEntityDescription( key="next_avg_12h", translation_key="next_avg_12h", icon="mdi:chart-line", device_class=SensorDeviceClass.MONETARY, state_class=None, # Future forecast: historical chart not useful suggested_display_precision=2, entity_registry_enabled_default=False, ), ) FUTURE_TREND_SENSORS = ( # Current trend sensor (what is the trend right now, valid until next change?) SensorEntityDescription( key="current_price_trend", translation_key="current_price_trend", icon="mdi:trending-up", # Dynamic: trending-up/trending-down/trending-neutral based on current trend device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), # Next trend change sensor (when will trend change?) SensorEntityDescription( key="next_price_trend_change", translation_key="next_price_trend_change", icon="mdi:clock-alert", # Dynamic: trending-up/trending-down/trending-neutral based on direction device_class=SensorDeviceClass.TIMESTAMP, state_class=None, # Timestamp: no statistics entity_registry_enabled_default=True, ), # Trend change countdown sensor (how long until trend changes?) SensorEntityDescription( key="next_price_trend_change_in", translation_key="next_price_trend_change_in", icon="mdi:timer-outline", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, suggested_unit_of_measurement=UnitOfTime.HOURS, state_class=None, # Countdown timer: no statistics suggested_display_precision=2, entity_registry_enabled_default=True, ), # Price outlook forecast sensors (is the average of the next Xh cheaper/more expensive than now?) # Default enabled: 1h-5h SensorEntityDescription( key="price_outlook_1h", translation_key="price_outlook_1h", icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), SensorEntityDescription( key="price_outlook_2h", translation_key="price_outlook_2h", icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), SensorEntityDescription( key="price_outlook_3h", translation_key="price_outlook_3h", icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), SensorEntityDescription( key="price_outlook_4h", translation_key="price_outlook_4h", icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), SensorEntityDescription( key="price_outlook_5h", translation_key="price_outlook_5h", icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), # Disabled by default: 6h, 8h, 12h SensorEntityDescription( key="price_outlook_6h", translation_key="price_outlook_6h", icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=False, ), SensorEntityDescription( key="price_outlook_8h", translation_key="price_outlook_8h", icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=False, ), SensorEntityDescription( key="price_outlook_12h", translation_key="price_outlook_12h", icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=False, ), ) # ---------------------------------------------------------------------------- # 5b. PRICE TRAJECTORY SENSORS (first-half vs second-half window comparison) # ---------------------------------------------------------------------------- # These sensors reveal turning points: is the price rising or falling WITHIN # the window? Complements price_outlook_Xh sensors. # # Example at a price minimum (12:00): # - price_outlook_4h: "strongly_falling" (Ø next 4h is below current high) # - price_trajectory_4h: "rising" (second half avg > first half avg) # → Combined: act now, reversal is coming within the window. # # Coverage starts at 2h (minimum for meaningful first/second half split). # Default enabled: 2h-5h PRICE_TRAJECTORY_SENSORS = ( SensorEntityDescription( key="price_trajectory_2h", translation_key="price_trajectory_2h", icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), SensorEntityDescription( key="price_trajectory_3h", translation_key="price_trajectory_3h", icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), SensorEntityDescription( key="price_trajectory_4h", translation_key="price_trajectory_4h", icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), SensorEntityDescription( key="price_trajectory_5h", translation_key="price_trajectory_5h", icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), # Disabled by default: 6h, 8h, 12h SensorEntityDescription( key="price_trajectory_6h", translation_key="price_trajectory_6h", icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=False, ), SensorEntityDescription( key="price_trajectory_8h", translation_key="price_trajectory_8h", icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=False, ), SensorEntityDescription( key="price_trajectory_12h", translation_key="price_trajectory_12h", icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=False, ), ) # ---------------------------------------------------------------------------- # 6. VOLATILITY SENSORS (coefficient of variation analysis) # ---------------------------------------------------------------------------- # NOTE: Enum options are defined inline (not imported from const.py) to avoid # import timing issues with Home Assistant's entity platform initialization. # Keep in sync with VOLATILITY_* constants in const.py! VOLATILITY_SENSORS = ( SensorEntityDescription( key="today_volatility", translation_key="today_volatility", # Dynamic: shows chart-bell-curve/chart-gantt/finance based on volatility level icon="mdi:chart-bell-curve-cumulative", device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["low", "moderate", "high", "very_high"], ), SensorEntityDescription( key="tomorrow_volatility", translation_key="tomorrow_volatility", # Dynamic: shows chart-bell-curve/chart-gantt/finance based on volatility level icon="mdi:chart-bell-curve-cumulative", device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["low", "moderate", "high", "very_high"], entity_registry_enabled_default=False, # Today's volatility is usually sufficient ), SensorEntityDescription( key="next_24h_volatility", translation_key="next_24h_volatility", # Dynamic: shows chart-bell-curve/chart-gantt/finance based on volatility level icon="mdi:chart-bell-curve-cumulative", device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["low", "moderate", "high", "very_high"], entity_registry_enabled_default=False, # Advanced use case ), SensorEntityDescription( key="today_tomorrow_volatility", translation_key="today_tomorrow_volatility", # Dynamic: shows chart-bell-curve/chart-gantt/finance based on volatility level icon="mdi:chart-bell-curve-cumulative", device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics options=["low", "moderate", "high", "very_high"], entity_registry_enabled_default=False, # Advanced use case ), ) # ---------------------------------------------------------------------------- # 6b. PRICE PERCENTILE RANK SENSORS # ---------------------------------------------------------------------------- # These sensors show where the current price ranks within a reference period. # The state (0-100%) answers: "What percentage of reference prices are cheaper # than the current price?" # # 0% = current price is the cheapest in the reference period # 50% = half the prices are cheaper (current price at median level) # ~99% = almost everything is cheaper (current price near the maximum) # # Reference periods: # - today: 96 intervals of today (local calendar day) # - tomorrow: 96 intervals of tomorrow (once data is available) # - today_tomorrow: 192 combined intervals when tomorrow is available # # Use case: "Is now the right time to run a large appliance?" # - current_interval_price_rank_today < 25 → bottom quartile, great time to use energy # - current_interval_price_rank_today > 75 → top quartile, consider delaying consumption PERCENTILE_RANK_SENSORS = ( # ---------------------------------------------------------------- # Current interval rank sensors # ---------------------------------------------------------------- SensorEntityDescription( key="current_interval_price_rank_today", translation_key="current_interval_price_rank_today", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, state_class=None, # Position metric: no statistics suggested_display_precision=0, ), SensorEntityDescription( key="current_interval_price_rank_tomorrow", translation_key="current_interval_price_rank_tomorrow", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, state_class=None, # Position metric: no statistics suggested_display_precision=0, entity_registry_enabled_default=False, # Available once tomorrow's data arrives ), SensorEntityDescription( key="current_interval_price_rank_today_tomorrow", translation_key="current_interval_price_rank_today_tomorrow", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, state_class=None, # Position metric: no statistics suggested_display_precision=0, entity_registry_enabled_default=False, # Advanced overview use case ), # ---------------------------------------------------------------- # Next interval rank sensors # ---------------------------------------------------------------- SensorEntityDescription( key="next_interval_price_rank_today", translation_key="next_interval_price_rank_today", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, state_class=None, suggested_display_precision=0, entity_registry_enabled_default=False, ), SensorEntityDescription( key="next_interval_price_rank_today_tomorrow", translation_key="next_interval_price_rank_today_tomorrow", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, state_class=None, suggested_display_precision=0, entity_registry_enabled_default=False, ), # ---------------------------------------------------------------- # Previous interval rank sensors # ---------------------------------------------------------------- SensorEntityDescription( key="previous_interval_price_rank_today", translation_key="previous_interval_price_rank_today", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, state_class=None, suggested_display_precision=0, entity_registry_enabled_default=False, ), SensorEntityDescription( key="previous_interval_price_rank_today_tomorrow", translation_key="previous_interval_price_rank_today_tomorrow", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, state_class=None, suggested_display_precision=0, entity_registry_enabled_default=False, ), # ---------------------------------------------------------------- # Rolling-hour rank sensors (rank of 1h rolling average) # ---------------------------------------------------------------- SensorEntityDescription( key="current_hour_price_rank_today", translation_key="current_hour_price_rank_today", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, state_class=None, suggested_display_precision=0, entity_registry_enabled_default=False, ), SensorEntityDescription( key="current_hour_price_rank_today_tomorrow", translation_key="current_hour_price_rank_today_tomorrow", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, state_class=None, suggested_display_precision=0, entity_registry_enabled_default=False, ), SensorEntityDescription( key="next_hour_price_rank_today", translation_key="next_hour_price_rank_today", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, state_class=None, suggested_display_precision=0, entity_registry_enabled_default=False, ), SensorEntityDescription( key="next_hour_price_rank_today_tomorrow", translation_key="next_hour_price_rank_today_tomorrow", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, state_class=None, suggested_display_precision=0, entity_registry_enabled_default=False, ), ) # ---------------------------------------------------------------------------- # 7. BEST/PEAK PRICE TIMING SENSORS (period-based time tracking) # ---------------------------------------------------------------------------- # These sensors track time relative to best_price/peak_price binary sensor periods. # They require minute-by-minute updates via async_track_time_interval. # # When period is active (binary_sensor ON): # - end_time: Timestamp when current period ends # - remaining_minutes: Minutes until period ends # - progress: Percentage of period completed (0-100%) # # When period is inactive (binary_sensor OFF): # - next_start_time: Timestamp when next period starts # - next_in_minutes: Minutes until next period starts # # All return None/Unknown when no period is active/scheduled. BEST_PRICE_TIMING_SENSORS = ( SensorEntityDescription( key="best_price_end_time", translation_key="best_price_end_time", icon="mdi:clock-end", device_class=SensorDeviceClass.TIMESTAMP, state_class=None, # Timestamps: no statistics ), SensorEntityDescription( key="best_price_period_duration", translation_key="best_price_period_duration", icon="mdi:timer", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, suggested_unit_of_measurement=UnitOfTime.HOURS, state_class=None, # Duration not needed in long-term statistics suggested_display_precision=2, entity_registry_enabled_default=False, ), SensorEntityDescription( key="best_price_remaining_minutes", translation_key="best_price_remaining_minutes", icon="mdi:timer-sand", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, suggested_unit_of_measurement=UnitOfTime.HOURS, state_class=None, # Countdown timers excluded from statistics suggested_display_precision=2, ), SensorEntityDescription( key="best_price_progress", translation_key="best_price_progress", icon="mdi:percent", # Dynamic: mdi:percent-0 to mdi:percent-100 native_unit_of_measurement=PERCENTAGE, state_class=None, # Progress counter: no statistics suggested_display_precision=0, ), SensorEntityDescription( key="best_price_next_start_time", translation_key="best_price_next_start_time", icon="mdi:clock-start", device_class=SensorDeviceClass.TIMESTAMP, state_class=None, # Timestamps: no statistics ), SensorEntityDescription( key="best_price_next_in_minutes", translation_key="best_price_next_in_minutes", icon="mdi:timer-outline", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, suggested_unit_of_measurement=UnitOfTime.HOURS, state_class=None, # Next-start timers excluded from statistics suggested_display_precision=2, ), ) PEAK_PRICE_TIMING_SENSORS = ( SensorEntityDescription( key="peak_price_end_time", translation_key="peak_price_end_time", icon="mdi:clock-end", device_class=SensorDeviceClass.TIMESTAMP, state_class=None, # Timestamps: no statistics ), SensorEntityDescription( key="peak_price_period_duration", translation_key="peak_price_period_duration", icon="mdi:timer", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, suggested_unit_of_measurement=UnitOfTime.HOURS, state_class=None, # Duration not needed in long-term statistics suggested_display_precision=2, entity_registry_enabled_default=False, ), SensorEntityDescription( key="peak_price_remaining_minutes", translation_key="peak_price_remaining_minutes", icon="mdi:timer-sand", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, suggested_unit_of_measurement=UnitOfTime.HOURS, state_class=None, # Countdown timers excluded from statistics suggested_display_precision=2, ), SensorEntityDescription( key="peak_price_progress", translation_key="peak_price_progress", icon="mdi:percent", # Dynamic: mdi:percent-0 to mdi:percent-100 native_unit_of_measurement=PERCENTAGE, state_class=None, # Progress counter: no statistics suggested_display_precision=0, ), SensorEntityDescription( key="peak_price_next_start_time", translation_key="peak_price_next_start_time", icon="mdi:clock-start", device_class=SensorDeviceClass.TIMESTAMP, state_class=None, # Timestamps: no statistics ), SensorEntityDescription( key="peak_price_next_in_minutes", translation_key="peak_price_next_in_minutes", icon="mdi:timer-outline", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, suggested_unit_of_measurement=UnitOfTime.HOURS, state_class=None, # Next-start timers excluded from statistics suggested_display_precision=2, ), ) # 8. DAY PATTERN SENSORS (price shape classification per calendar day) # ---------------------------------------------------------------------------- DAY_PATTERN_SENSORS = ( SensorEntityDescription( key="day_pattern_yesterday", translation_key="day_pattern_yesterday", icon="mdi:chart-bell-curve", device_class=SensorDeviceClass.ENUM, options=[ "valley", "peak", "double_dip", "duck_curve", "flat", "rising", "falling", "mixed", ], state_class=None, entity_registry_enabled_default=False, ), SensorEntityDescription( key="day_pattern_today", translation_key="day_pattern_today", icon="mdi:chart-bell-curve", device_class=SensorDeviceClass.ENUM, options=[ "valley", "peak", "double_dip", "duck_curve", "flat", "rising", "falling", "mixed", ], state_class=None, entity_registry_enabled_default=True, ), SensorEntityDescription( key="day_pattern_tomorrow", translation_key="day_pattern_tomorrow", icon="mdi:chart-bell-curve", device_class=SensorDeviceClass.ENUM, options=[ "valley", "peak", "double_dip", "duck_curve", "flat", "rising", "falling", "mixed", ], state_class=None, entity_registry_enabled_default=False, ), ) # 8b. PRICE PHASE SENSORS (current/next intra-day price phase classification) # ---------------------------------------------------------------------------- PRICE_PHASE_SENSORS = ( SensorEntityDescription( key="current_price_phase", translation_key="current_price_phase", icon="mdi:chart-timeline-variant", device_class=SensorDeviceClass.ENUM, options=["rising", "falling", "flat"], state_class=None, entity_registry_enabled_default=True, ), SensorEntityDescription( key="next_price_phase", translation_key="next_price_phase", icon="mdi:chart-timeline-variant", device_class=SensorDeviceClass.ENUM, options=["rising", "falling", "flat"], state_class=None, entity_registry_enabled_default=True, ), ) # 8c. PRICE PHASE TIMING SENSORS (current phase duration/progress + next-phase-by-type) # ---------------------------------------------------------------------------- # # When current phase is active: # - end_time: Timestamp when current phase ends # - remaining_minutes: Minutes until current phase ends # - duration: Total length of current phase (disabled by default) # - progress: Percentage of current phase completed (disabled by default) # # Next occurrence of a specific phase type (after current segment, today or tomorrow): # - next_*_phase_start_time: Timestamp when next rising/falling/flat phase starts # - next_*_phase_in_minutes: Minutes until that phase starts # # All return None/Unknown when no segment data is available. PRICE_PHASE_TIMING_SENSORS = ( SensorEntityDescription( key="current_price_phase_end_time", translation_key="current_price_phase_end_time", icon="mdi:clock-end", device_class=SensorDeviceClass.TIMESTAMP, state_class=None, # Timestamps: no statistics ), SensorEntityDescription( key="current_price_phase_remaining_minutes", translation_key="current_price_phase_remaining_minutes", icon="mdi:timer-sand", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, suggested_unit_of_measurement=UnitOfTime.HOURS, state_class=None, # Countdown timers excluded from statistics suggested_display_precision=2, ), SensorEntityDescription( key="current_price_phase_duration", translation_key="current_price_phase_duration", icon="mdi:timer", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, suggested_unit_of_measurement=UnitOfTime.HOURS, state_class=None, # Duration not needed in long-term statistics suggested_display_precision=2, entity_registry_enabled_default=False, ), SensorEntityDescription( key="current_price_phase_progress", translation_key="current_price_phase_progress", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, state_class=None, # Progress counter: no statistics suggested_display_precision=0, entity_registry_enabled_default=False, ), # Next occurrence of each phase type (across remaining today + tomorrow) SensorEntityDescription( key="next_rising_phase_start_time", translation_key="next_rising_phase_start_time", icon="mdi:trending-up", device_class=SensorDeviceClass.TIMESTAMP, state_class=None, entity_registry_enabled_default=False, ), SensorEntityDescription( key="next_falling_phase_start_time", translation_key="next_falling_phase_start_time", icon="mdi:trending-down", device_class=SensorDeviceClass.TIMESTAMP, state_class=None, entity_registry_enabled_default=False, ), SensorEntityDescription( key="next_flat_phase_start_time", translation_key="next_flat_phase_start_time", icon="mdi:trending-neutral", device_class=SensorDeviceClass.TIMESTAMP, state_class=None, entity_registry_enabled_default=False, ), SensorEntityDescription( key="next_rising_phase_in_minutes", translation_key="next_rising_phase_in_minutes", icon="mdi:timer-outline", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, suggested_unit_of_measurement=UnitOfTime.HOURS, state_class=None, suggested_display_precision=2, entity_registry_enabled_default=False, ), SensorEntityDescription( key="next_falling_phase_in_minutes", translation_key="next_falling_phase_in_minutes", icon="mdi:timer-outline", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, suggested_unit_of_measurement=UnitOfTime.HOURS, state_class=None, suggested_display_precision=2, entity_registry_enabled_default=False, ), SensorEntityDescription( key="next_flat_phase_in_minutes", translation_key="next_flat_phase_in_minutes", icon="mdi:timer-outline", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, suggested_unit_of_measurement=UnitOfTime.HOURS, state_class=None, suggested_display_precision=2, entity_registry_enabled_default=False, ), ) # 9. DIAGNOSTIC SENSORS (data availability and metadata) # ---------------------------------------------------------------------------- DIAGNOSTIC_SENSORS = ( SensorEntityDescription( key="data_lifecycle_status", translation_key="data_lifecycle_status", icon="mdi:database-sync", device_class=SensorDeviceClass.ENUM, options=["cached", "fresh", "refreshing", "searching_tomorrow", "turnover_pending", "error"], state_class=None, # Status value: no statistics entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, # Critical for debugging ), # Home metadata from user data SensorEntityDescription( key="home_type", translation_key="home_type", icon="mdi:home-variant", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, options=["apartment", "rowhouse", "house", "cottage"], entity_registry_enabled_default=False, ), SensorEntityDescription( key="home_size", translation_key="home_size", icon="mdi:ruler-square", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfArea.SQUARE_METERS, state_class=None, # Static user metadata: no statistics useful entity_registry_enabled_default=False, suggested_display_precision=0, ), SensorEntityDescription( key="main_fuse_size", translation_key="main_fuse_size", icon="mdi:fuse", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=None, # Static user metadata: no statistics useful entity_registry_enabled_default=False, suggested_display_precision=0, ), SensorEntityDescription( key="number_of_residents", translation_key="number_of_residents", icon="mdi:account-group", entity_category=EntityCategory.DIAGNOSTIC, state_class=None, # Static user metadata: no statistics useful entity_registry_enabled_default=False, suggested_display_precision=0, ), SensorEntityDescription( key="primary_heating_source", translation_key="primary_heating_source", icon="mdi:heating-coil", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, options=[ "air2air_heatpump", "air2water_heatpump", "boiler", "central_heating", "district_heating", "district_heating", "district", "electric_boiler", "electricity", "floor", "gas", "ground_heatpump", "ground", "oil", "other", "waste", ], entity_registry_enabled_default=False, ), # Metering point data SensorEntityDescription( key="grid_company", translation_key="grid_company", icon="mdi:transmission-tower", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="grid_area_code", translation_key="grid_area_code", icon="mdi:map-marker", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), SensorEntityDescription( key="price_area_code", translation_key="price_area_code", icon="mdi:currency-eur", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), SensorEntityDescription( key="consumption_ean", translation_key="consumption_ean", icon="mdi:barcode", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), SensorEntityDescription( key="production_ean", translation_key="production_ean", icon="mdi:solar-power", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), SensorEntityDescription( key="energy_tax_type", translation_key="energy_tax_type", icon="mdi:cash", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), SensorEntityDescription( key="vat_type", translation_key="vat_type", icon="mdi:percent", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), SensorEntityDescription( key="estimated_annual_consumption", translation_key="estimated_annual_consumption", icon="mdi:lightning-bolt", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=None, # Static Tibber estimate, not an actual accumulating counter suggested_display_precision=0, ), # Subscription data SensorEntityDescription( key="subscription_status", translation_key="subscription_status", icon="mdi:file-document-check", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, options=["running", "ended", "pending", "unknown"], entity_registry_enabled_default=False, ), SensorEntityDescription( key="chart_data_export", translation_key="chart_data_export", icon="mdi:database-export", device_class=SensorDeviceClass.ENUM, options=["pending", "ready", "error"], entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, # Opt-in ), SensorEntityDescription( key="chart_metadata", translation_key="chart_metadata", icon="mdi:chart-box-outline", device_class=SensorDeviceClass.ENUM, options=["pending", "ready", "error"], entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, # Critical for chart features ), ) # ---------------------------------------------------------------------------- # COMBINED SENSOR DEFINITIONS # ---------------------------------------------------------------------------- ENTITY_DESCRIPTIONS = ( *INTERVAL_PRICE_SENSORS, *INTERVAL_LEVEL_SENSORS, *INTERVAL_RATING_SENSORS, *ROLLING_HOUR_PRICE_SENSORS, *ROLLING_HOUR_LEVEL_SENSORS, *ROLLING_HOUR_RATING_SENSORS, *DAILY_STAT_SENSORS, *DAILY_LEVEL_SENSORS, *DAILY_RATING_SENSORS, *WINDOW_24H_SENSORS, *FUTURE_MEAN_SENSORS, *FUTURE_TREND_SENSORS, *PRICE_TRAJECTORY_SENSORS, *VOLATILITY_SENSORS, *PERCENTILE_RANK_SENSORS, *BEST_PRICE_TIMING_SENSORS, *PEAK_PRICE_TIMING_SENSORS, *DAY_PATTERN_SENSORS, *PRICE_PHASE_SENSORS, *PRICE_PHASE_TIMING_SENSORS, *DIAGNOSTIC_SENSORS, )