hass.tibber_prices/custom_components/tibber_prices/sensor/definitions.py
Julian Pawlowski 5411a75b79 fix(sensor): set state_class=None on static diagnostic metadata sensors
Four non-MONETARY diagnostic sensors had state_class set, causing HA
Recorder to add them to long-term statistics tables unnecessarily:

- home_size (m²):                   SensorStateClass.MEASUREMENT
- main_fuse_size (A):               SensorStateClass.MEASUREMENT
- number_of_residents:              SensorStateClass.MEASUREMENT
- estimated_annual_consumption(kWh):SensorStateClass.TOTAL

All four are static user metadata retrieved from Tibber's user API
(cached for 24 h, rarely or never changes in practice).  They carry no
time-series value: home_size and main_fuse_size don't change, and
estimated_annual_consumption is a rough Tibber estimate, not an actual
accumulating energy counter.

Setting state_class=None removes them from long-term statistics while
keeping normal state-change recording intact.

The three intentional non-None state_class values are unchanged:
- current_interval_price (MONETARY, TOTAL): Energy Dashboard
- current_interval_price_base (MONETARY, TOTAL): Energy Dashboard
- average_price_today (MONETARY, TOTAL): useful weekly/monthly trend

Impact: Reduced Recorder database growth; no user-visible sensor
behaviour change.
2026-04-06 14:24:02 +00:00

959 lines
40 KiB
Python

"""
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_OPTIONS 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_OPTIONS 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_OPTIONS 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_OPTIONS 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_OPTIONS 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_OPTIONS 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,
),
# Price trend forecast sensors (will prices be higher/lower in X hours?)
# Default enabled: 1h-5h
SensorEntityDescription(
key="price_trend_1h",
translation_key="price_trend_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_trend_2h",
translation_key="price_trend_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_trend_3h",
translation_key="price_trend_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_trend_4h",
translation_key="price_trend_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_trend_5h",
translation_key="price_trend_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_trend_6h",
translation_key="price_trend_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_trend_8h",
translation_key="price_trend_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_trend_12h",
translation_key="price_trend_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_OPTIONS 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
),
)
# ----------------------------------------------------------------------------
# 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.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.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.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.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.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.HOURS,
state_class=None, # Next-start timers excluded from statistics
suggested_display_precision=2,
),
)
# 8. 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,
*VOLATILITY_SENSORS,
*BEST_PRICE_TIMING_SENSORS,
*PEAK_PRICE_TIMING_SENSORS,
*DIAGNOSTIC_SENSORS,
)