hass.tibber_prices/custom_components/tibber_prices/sensor/definitions.py
Julian Pawlowski 60e05e0815 refactor(currency)!: rename major/minor to base/subunit currency terminology
Complete terminology migration from confusing "major/minor" to clearer
"base/subunit" currency naming throughout entire codebase, translations,
documentation, tests, and services.

BREAKING CHANGES:

1. **Service API Parameters Renamed**:
   - `get_chartdata`: `minor_currency` → `subunit_currency`
   - `get_apexcharts_yaml`: Updated service_data references from
     `minor_currency: true` to `subunit_currency: true`
   - All automations/scripts using these parameters MUST be updated

2. **Configuration Option Key Changed**:
   - Config entry option: Display mode setting now uses new terminology
   - Internal key: `currency_display_mode` values remain "base"/"subunit"
   - User-facing labels updated in all 5 languages (de, en, nb, nl, sv)

3. **Sensor Entity Key Renamed**:
   - `current_interval_price_major` → `current_interval_price_base`
   - Entity ID changes: `sensor.tibber_home_current_interval_price_major`
     → `sensor.tibber_home_current_interval_price_base`
   - Energy Dashboard configurations MUST update entity references

4. **Function Signatures Changed**:
   - `format_price_unit_major()` → `format_price_unit_base()`
   - `format_price_unit_minor()` → `format_price_unit_subunit()`
   - `get_price_value()`: Parameter `in_euro` deprecated in favor of
     `config_entry` (backward compatible for now)

5. **Translation Keys Renamed**:
   - All language files: Sensor translation key
     `current_interval_price_major` → `current_interval_price_base`
   - Service parameter descriptions updated in all languages
   - Selector options updated: Display mode dropdown values

Changes by Category:

**Core Code (Python)**:
- const.py: Renamed all format_price_unit_*() functions, updated docstrings
- entity_utils/helpers.py: Updated get_price_value() with config-driven
  conversion and backward-compatible in_euro parameter
- sensor/__init__.py: Added display mode filtering for base currency sensor
- sensor/core.py:
  * Implemented suggested_display_precision property for dynamic decimal places
  * Updated native_unit_of_measurement to use get_display_unit_string()
  * Updated all price conversion calls to use config_entry parameter
- sensor/definitions.py: Renamed entity key and updated all
  suggested_display_precision values (2 decimals for most sensors)
- sensor/calculators/*.py: Updated all price conversion calls (8 calculators)
- sensor/helpers.py: Updated aggregate_price_data() signature with config_entry
- sensor/attributes/future.py: Updated future price attributes conversion

**Services**:
- services/chartdata.py: Renamed parameter minor_currency → subunit_currency
  throughout (53 occurrences), updated metadata calculation
- services/apexcharts.py: Updated service_data references in generated YAML
- services/formatters.py: Renamed parameter use_minor_currency →
  use_subunit_currency in aggregate_hourly_exact() and get_period_data()
- sensor/chart_metadata.py: Updated default parameter name

**Translations (5 Languages)**:
- All /translations/*.json:
  * Added new config step "display_settings" with comprehensive explanations
  * Renamed current_interval_price_major → current_interval_price_base
  * Updated service parameter descriptions (subunit_currency)
  * Added selector.currency_display_mode.options with translated labels
- All /custom_translations/*.json:
  * Renamed sensor description keys
  * Updated chart_metadata usage_tips references

**Documentation**:
- docs/user/docs/actions.md: Updated parameter table and feature list
- docs/user/versioned_docs/version-v0.21.0/actions.md: Backported changes

**Tests**:
- Updated 7 test files with renamed parameters and conversion logic:
  * test_connect_segments.py: Renamed minor/major to subunit/base
  * test_period_data_format.py: Updated period price conversion tests
  * test_avg_none_fallback.py: Fixed tuple unpacking for new return format
  * test_best_price_e2e.py: Added config_entry parameter to all calls
  * test_cache_validity.py: Fixed cache data structure (price_info key)
  * test_coordinator_shutdown.py: Added repair_manager mock
  * test_midnight_turnover.py: Added config_entry parameter
  * test_peak_price_e2e.py: Added config_entry parameter, fixed price_avg → price_mean
  * test_percentage_calculations.py: Added config_entry mock

**Coordinator/Period Calculation**:
- coordinator/periods.py: Added config_entry parameter to
  calculate_periods_with_relaxation() calls (2 locations)

Migration Guide:

1. **Update Service Calls in Automations/Scripts**:
   \`\`\`yaml
   # Before:
   service: tibber_prices.get_chartdata
   data:
     minor_currency: true

   # After:
   service: tibber_prices.get_chartdata
   data:
     subunit_currency: true
   \`\`\`

2. **Update Energy Dashboard Configuration**:
   - Settings → Dashboards → Energy
   - Replace sensor entity:
     `sensor.tibber_home_current_interval_price_major` →
     `sensor.tibber_home_current_interval_price_base`

3. **Review Integration Configuration**:
   - Settings → Devices & Services → Tibber Prices → Configure
   - New "Currency Display Settings" step added
   - Default mode depends on currency (EUR → subunit, Scandinavian → base)

Rationale:

The "major/minor" terminology was confusing and didn't clearly communicate:
- **Major** → Unclear if this means "primary" or "large value"
- **Minor** → Easily confused with "less important" rather than "smaller unit"

New terminology is precise and self-explanatory:
- **Base currency** → Standard ISO currency (€, kr, $, £)
- **Subunit currency** → Fractional unit (ct, øre, ¢, p)

This aligns with:
- International terminology (ISO 4217 standard)
- Banking/financial industry conventions
- User expectations from payment processing systems

Impact: Aligns currency terminology with international standards. Users must
update service calls, automations, and Energy Dashboard configuration after
upgrade.

Refs: User feedback session (December 2025) identified terminology confusion
2025-12-11 08:26:30 +00:00

1040 lines
42 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",
name="Current Electricity 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",
name="Current Electricity Price (Energy Dashboard)",
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",
name="Next 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="previous_interval_price",
translation_key="previous_interval_price",
name="Previous Electricity Price",
icon="mdi:cash-refund", # Static: arrow back indicates "past"
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
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",
name="Current 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",
name="Next 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",
name="Previous 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",
name="Current 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",
name="Next 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",
name="Previous 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",
name="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=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
suggested_display_precision=2,
),
SensorEntityDescription(
key="next_hour_average_price",
translation_key="next_hour_average_price",
name="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=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
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",
name="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",
name="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",
name="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",
name="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",
name="Today's Lowest Price",
icon="mdi:arrow-collapse-down",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
suggested_display_precision=2,
),
SensorEntityDescription(
key="highest_price_today",
translation_key="highest_price_today",
name="Today's Highest Price",
icon="mdi:arrow-collapse-up",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
suggested_display_precision=2,
),
SensorEntityDescription(
key="average_price_today",
translation_key="average_price_today",
name="Today's Average Price",
icon="mdi:chart-line",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
suggested_display_precision=2,
),
SensorEntityDescription(
key="lowest_price_tomorrow",
translation_key="lowest_price_tomorrow",
name="Tomorrow's Lowest Price",
icon="mdi:arrow-collapse-down",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
suggested_display_precision=2,
),
SensorEntityDescription(
key="highest_price_tomorrow",
translation_key="highest_price_tomorrow",
name="Tomorrow's Highest Price",
icon="mdi:arrow-collapse-up",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
suggested_display_precision=2,
),
SensorEntityDescription(
key="average_price_tomorrow",
translation_key="average_price_tomorrow",
name="Tomorrow's Average Price",
icon="mdi:chart-line",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
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",
name="Yesterday's 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",
name="Today's 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",
name="Tomorrow's 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",
name="Yesterday's 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",
name="Today's 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",
name="Tomorrow's 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",
name="Trailing 24h Average Price",
icon="mdi:chart-line",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SensorEntityDescription(
key="leading_price_average",
translation_key="leading_price_average",
name="Leading 24h Average Price",
icon="mdi:chart-line-variant",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
entity_registry_enabled_default=False, # Advanced use case
suggested_display_precision=2,
),
SensorEntityDescription(
key="trailing_price_min",
translation_key="trailing_price_min",
name="Trailing 24h Minimum Price",
icon="mdi:arrow-collapse-down",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SensorEntityDescription(
key="trailing_price_max",
translation_key="trailing_price_max",
name="Trailing 24h Maximum Price",
icon="mdi:arrow-collapse-up",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SensorEntityDescription(
key="leading_price_min",
translation_key="leading_price_min",
name="Leading 24h Minimum Price",
icon="mdi:arrow-collapse-down",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
entity_registry_enabled_default=False, # Advanced use case
suggested_display_precision=2,
),
SensorEntityDescription(
key="leading_price_max",
translation_key="leading_price_max",
name="Leading 24h Maximum Price",
icon="mdi:arrow-collapse-up",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
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_AVG_SENSORS = (
# Default enabled: 1h-5h
SensorEntityDescription(
key="next_avg_1h",
translation_key="next_avg_1h",
name="Next 1h Average Price",
icon="mdi:chart-line",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
suggested_display_precision=2,
entity_registry_enabled_default=True,
),
SensorEntityDescription(
key="next_avg_2h",
translation_key="next_avg_2h",
name="Next 2h Average Price",
icon="mdi:chart-line",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
suggested_display_precision=2,
entity_registry_enabled_default=True,
),
SensorEntityDescription(
key="next_avg_3h",
translation_key="next_avg_3h",
name="Next 3h Average Price",
icon="mdi:chart-line",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
suggested_display_precision=2,
entity_registry_enabled_default=True,
),
SensorEntityDescription(
key="next_avg_4h",
translation_key="next_avg_4h",
name="Next 4h Average Price",
icon="mdi:chart-line",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
suggested_display_precision=2,
entity_registry_enabled_default=True,
),
SensorEntityDescription(
key="next_avg_5h",
translation_key="next_avg_5h",
name="Next 5h Average Price",
icon="mdi:chart-line",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
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",
name="Next 6h Average Price",
icon="mdi:chart-line",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="next_avg_8h",
translation_key="next_avg_8h",
name="Next 8h Average Price",
icon="mdi:chart-line",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="next_avg_12h",
translation_key="next_avg_12h",
name="Next 12h Average Price",
icon="mdi:chart-line",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL, # MONETARY requires TOTAL or None
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",
name="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=["rising", "falling", "stable"],
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",
name="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",
name="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=["rising", "falling", "stable"],
entity_registry_enabled_default=True,
),
SensorEntityDescription(
key="price_trend_2h",
translation_key="price_trend_2h",
name="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=["rising", "falling", "stable"],
entity_registry_enabled_default=True,
),
SensorEntityDescription(
key="price_trend_3h",
translation_key="price_trend_3h",
name="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=["rising", "falling", "stable"],
entity_registry_enabled_default=True,
),
SensorEntityDescription(
key="price_trend_4h",
translation_key="price_trend_4h",
name="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=["rising", "falling", "stable"],
entity_registry_enabled_default=True,
),
SensorEntityDescription(
key="price_trend_5h",
translation_key="price_trend_5h",
name="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=["rising", "falling", "stable"],
entity_registry_enabled_default=True,
),
# Disabled by default: 6h, 8h, 12h
SensorEntityDescription(
key="price_trend_6h",
translation_key="price_trend_6h",
name="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=["rising", "falling", "stable"],
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="price_trend_8h",
translation_key="price_trend_8h",
name="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=["rising", "falling", "stable"],
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="price_trend_12h",
translation_key="price_trend_12h",
name="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=["rising", "falling", "stable"],
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",
name="Today's Price 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",
name="Tomorrow's Price 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",
name="Next 24h Price 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",
name="Today + Tomorrow Price 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",
name="Best Price Period End",
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",
name="Best Price Period Duration",
icon="mdi:timer",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
state_class=None, # Changes with each period: no statistics
suggested_display_precision=0,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="best_price_remaining_minutes",
translation_key="best_price_remaining_minutes",
name="Best Price Remaining Time",
icon="mdi:timer-sand",
native_unit_of_measurement=UnitOfTime.MINUTES,
state_class=None, # Countdown timer: no statistics
suggested_display_precision=0,
),
SensorEntityDescription(
key="best_price_progress",
translation_key="best_price_progress",
name="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",
name="Best Price Next Period Start",
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",
name="Best Price Starts In",
icon="mdi:timer-outline",
native_unit_of_measurement=UnitOfTime.MINUTES,
state_class=None, # Countdown timer: no statistics
suggested_display_precision=0,
),
)
PEAK_PRICE_TIMING_SENSORS = (
SensorEntityDescription(
key="peak_price_end_time",
translation_key="peak_price_end_time",
name="Peak Price Period End",
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",
name="Peak Price Period Duration",
icon="mdi:timer",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
state_class=None, # Changes with each period: no statistics
suggested_display_precision=0,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="peak_price_remaining_minutes",
translation_key="peak_price_remaining_minutes",
name="Peak Price Remaining Time",
icon="mdi:timer-sand",
native_unit_of_measurement=UnitOfTime.MINUTES,
state_class=None, # Countdown timer: no statistics
suggested_display_precision=0,
),
SensorEntityDescription(
key="peak_price_progress",
translation_key="peak_price_progress",
name="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",
name="Peak Price Next Period Start",
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",
name="Peak Price Starts In",
icon="mdi:timer-outline",
native_unit_of_measurement=UnitOfTime.MINUTES,
state_class=None, # Countdown timer: no statistics
suggested_display_precision=0,
),
)
# 8. DIAGNOSTIC SENSORS (data availability and metadata)
# ----------------------------------------------------------------------------
DIAGNOSTIC_SENSORS = (
SensorEntityDescription(
key="data_lifecycle_status",
translation_key="data_lifecycle_status",
name="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",
name="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",
name="Home Size",
icon="mdi:ruler-square",
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
suggested_display_precision=0,
),
SensorEntityDescription(
key="main_fuse_size",
translation_key="main_fuse_size",
name="Main Fuse Size",
icon="mdi:fuse",
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
suggested_display_precision=0,
),
SensorEntityDescription(
key="number_of_residents",
translation_key="number_of_residents",
name="Number of Residents",
icon="mdi:account-group",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
suggested_display_precision=0,
),
SensorEntityDescription(
key="primary_heating_source",
translation_key="primary_heating_source",
name="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",
name="Grid Company",
icon="mdi:transmission-tower",
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="grid_area_code",
translation_key="grid_area_code",
name="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",
name="Price Area Code",
icon="mdi:currency-eur",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="consumption_ean",
translation_key="consumption_ean",
name="Consumption EAN",
icon="mdi:barcode",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="production_ean",
translation_key="production_ean",
name="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",
name="Energy Tax Type",
icon="mdi:cash",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="vat_type",
translation_key="vat_type",
name="VAT Type",
icon="mdi:percent",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="estimated_annual_consumption",
translation_key="estimated_annual_consumption",
name="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=SensorStateClass.TOTAL,
suggested_display_precision=0,
),
# Subscription data
SensorEntityDescription(
key="subscription_status",
translation_key="subscription_status",
name="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",
name="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",
name="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_AVG_SENSORS,
*FUTURE_TREND_SENSORS,
*VOLATILITY_SENSORS,
*BEST_PRICE_TIMING_SENSORS,
*PEAK_PRICE_TIMING_SENSORS,
*DIAGNOSTIC_SENSORS,
)