mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
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
283 lines
16 KiB
Python
283 lines
16 KiB
Python
"""Value getter mapping for Tibber Prices sensors."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from custom_components.tibber_prices.utils.average import (
|
|
calculate_current_leading_avg,
|
|
calculate_current_leading_max,
|
|
calculate_current_leading_min,
|
|
calculate_current_trailing_avg,
|
|
calculate_current_trailing_max,
|
|
calculate_current_trailing_min,
|
|
calculate_median,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
from datetime import datetime
|
|
|
|
from custom_components.tibber_prices.sensor.calculators.daily_stat import TibberPricesDailyStatCalculator
|
|
from custom_components.tibber_prices.sensor.calculators.interval import TibberPricesIntervalCalculator
|
|
from custom_components.tibber_prices.sensor.calculators.lifecycle import TibberPricesLifecycleCalculator
|
|
from custom_components.tibber_prices.sensor.calculators.metadata import TibberPricesMetadataCalculator
|
|
from custom_components.tibber_prices.sensor.calculators.rolling_hour import TibberPricesRollingHourCalculator
|
|
from custom_components.tibber_prices.sensor.calculators.timing import TibberPricesTimingCalculator
|
|
from custom_components.tibber_prices.sensor.calculators.trend import TibberPricesTrendCalculator
|
|
from custom_components.tibber_prices.sensor.calculators.volatility import TibberPricesVolatilityCalculator
|
|
from custom_components.tibber_prices.sensor.calculators.window_24h import TibberPricesWindow24hCalculator
|
|
|
|
|
|
def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parameters
|
|
interval_calculator: TibberPricesIntervalCalculator,
|
|
rolling_hour_calculator: TibberPricesRollingHourCalculator,
|
|
daily_stat_calculator: TibberPricesDailyStatCalculator,
|
|
window_24h_calculator: TibberPricesWindow24hCalculator,
|
|
trend_calculator: TibberPricesTrendCalculator,
|
|
timing_calculator: TibberPricesTimingCalculator,
|
|
volatility_calculator: TibberPricesVolatilityCalculator,
|
|
metadata_calculator: TibberPricesMetadataCalculator,
|
|
lifecycle_calculator: TibberPricesLifecycleCalculator,
|
|
get_next_avg_n_hours_value: Callable[[int], float | None],
|
|
get_data_timestamp: Callable[[], datetime | None],
|
|
get_chart_data_export_value: Callable[[], str | None],
|
|
get_chart_metadata_value: Callable[[], str | None],
|
|
) -> dict[str, Callable]:
|
|
"""
|
|
Build mapping from entity key to value getter callable.
|
|
|
|
This function centralizes the handler mapping logic, making it easier to maintain
|
|
and understand the relationship between sensor types and their calculation methods.
|
|
|
|
Args:
|
|
interval_calculator: Calculator for current/next/previous interval values
|
|
rolling_hour_calculator: Calculator for 5-interval rolling windows
|
|
daily_stat_calculator: Calculator for daily min/max/avg statistics
|
|
window_24h_calculator: Calculator for trailing/leading 24h windows
|
|
trend_calculator: Calculator for price trend analysis
|
|
timing_calculator: Calculator for best/peak price period timing
|
|
volatility_calculator: Calculator for price volatility analysis
|
|
metadata_calculator: Calculator for home/metering metadata
|
|
lifecycle_calculator: Calculator for data lifecycle tracking
|
|
get_next_avg_n_hours_value: Method for next N-hour average forecasts
|
|
get_data_timestamp: Method for data timestamp sensor
|
|
get_chart_data_export_value: Method for chart data export sensor
|
|
get_chart_metadata_value: Method for chart metadata sensor
|
|
|
|
Returns:
|
|
Dictionary mapping entity keys to their value getter callables.
|
|
|
|
"""
|
|
return {
|
|
# ================================================================
|
|
# INTERVAL-BASED SENSORS - via IntervalCalculator
|
|
# ================================================================
|
|
# Price level sensors
|
|
"current_interval_price_level": interval_calculator.get_price_level_value,
|
|
"next_interval_price_level": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=1, value_type="level"
|
|
),
|
|
"previous_interval_price_level": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=-1, value_type="level"
|
|
),
|
|
# Price sensors (in cents)
|
|
"current_interval_price": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=0, value_type="price", in_euro=False
|
|
),
|
|
"current_interval_price_base": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=0, value_type="price", in_euro=True
|
|
),
|
|
"next_interval_price": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=1, value_type="price", in_euro=False
|
|
),
|
|
"previous_interval_price": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=-1, value_type="price", in_euro=False
|
|
),
|
|
# Rating sensors
|
|
"current_interval_price_rating": lambda: interval_calculator.get_rating_value(rating_type="current"),
|
|
"next_interval_price_rating": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=1, value_type="rating"
|
|
),
|
|
"previous_interval_price_rating": lambda: interval_calculator.get_interval_value(
|
|
interval_offset=-1, value_type="rating"
|
|
),
|
|
# ================================================================
|
|
# ROLLING HOUR SENSORS (5-interval windows) - via RollingHourCalculator
|
|
# ================================================================
|
|
"current_hour_price_level": lambda: rolling_hour_calculator.get_rolling_hour_value(
|
|
hour_offset=0, value_type="level"
|
|
),
|
|
"next_hour_price_level": lambda: rolling_hour_calculator.get_rolling_hour_value(
|
|
hour_offset=1, value_type="level"
|
|
),
|
|
# Rolling hour average (5 intervals: 2 before + current + 2 after)
|
|
"current_hour_average_price": lambda: rolling_hour_calculator.get_rolling_hour_value(
|
|
hour_offset=0, value_type="price"
|
|
),
|
|
"next_hour_average_price": lambda: rolling_hour_calculator.get_rolling_hour_value(
|
|
hour_offset=1, value_type="price"
|
|
),
|
|
"current_hour_price_rating": lambda: rolling_hour_calculator.get_rolling_hour_value(
|
|
hour_offset=0, value_type="rating"
|
|
),
|
|
"next_hour_price_rating": lambda: rolling_hour_calculator.get_rolling_hour_value(
|
|
hour_offset=1, value_type="rating"
|
|
),
|
|
# ================================================================
|
|
# DAILY STATISTICS SENSORS - via DailyStatCalculator
|
|
# ================================================================
|
|
"lowest_price_today": lambda: daily_stat_calculator.get_daily_stat_value(day="today", stat_func=min),
|
|
"highest_price_today": lambda: daily_stat_calculator.get_daily_stat_value(day="today", stat_func=max),
|
|
"average_price_today": lambda: daily_stat_calculator.get_daily_stat_value(
|
|
day="today",
|
|
stat_func=lambda prices: (sum(prices) / len(prices), calculate_median(prices)),
|
|
),
|
|
# Tomorrow statistics sensors
|
|
"lowest_price_tomorrow": lambda: daily_stat_calculator.get_daily_stat_value(day="tomorrow", stat_func=min),
|
|
"highest_price_tomorrow": lambda: daily_stat_calculator.get_daily_stat_value(day="tomorrow", stat_func=max),
|
|
"average_price_tomorrow": lambda: daily_stat_calculator.get_daily_stat_value(
|
|
day="tomorrow",
|
|
stat_func=lambda prices: (sum(prices) / len(prices), calculate_median(prices)),
|
|
),
|
|
# Daily aggregated level sensors
|
|
"yesterday_price_level": lambda: daily_stat_calculator.get_daily_aggregated_value(
|
|
day="yesterday", value_type="level"
|
|
),
|
|
"today_price_level": lambda: daily_stat_calculator.get_daily_aggregated_value(day="today", value_type="level"),
|
|
"tomorrow_price_level": lambda: daily_stat_calculator.get_daily_aggregated_value(
|
|
day="tomorrow", value_type="level"
|
|
),
|
|
# Daily aggregated rating sensors
|
|
"yesterday_price_rating": lambda: daily_stat_calculator.get_daily_aggregated_value(
|
|
day="yesterday", value_type="rating"
|
|
),
|
|
"today_price_rating": lambda: daily_stat_calculator.get_daily_aggregated_value(
|
|
day="today", value_type="rating"
|
|
),
|
|
"tomorrow_price_rating": lambda: daily_stat_calculator.get_daily_aggregated_value(
|
|
day="tomorrow", value_type="rating"
|
|
),
|
|
# ================================================================
|
|
# 24H WINDOW SENSORS (trailing/leading from current) - via TibberPricesWindow24hCalculator
|
|
# ================================================================
|
|
# Trailing and leading average sensors
|
|
"trailing_price_average": lambda: window_24h_calculator.get_24h_window_value(
|
|
stat_func=calculate_current_trailing_avg,
|
|
),
|
|
"leading_price_average": lambda: window_24h_calculator.get_24h_window_value(
|
|
stat_func=calculate_current_leading_avg,
|
|
),
|
|
# Trailing and leading min/max sensors
|
|
"trailing_price_min": lambda: window_24h_calculator.get_24h_window_value(
|
|
stat_func=calculate_current_trailing_min,
|
|
),
|
|
"trailing_price_max": lambda: window_24h_calculator.get_24h_window_value(
|
|
stat_func=calculate_current_trailing_max,
|
|
),
|
|
"leading_price_min": lambda: window_24h_calculator.get_24h_window_value(
|
|
stat_func=calculate_current_leading_min,
|
|
),
|
|
"leading_price_max": lambda: window_24h_calculator.get_24h_window_value(
|
|
stat_func=calculate_current_leading_max,
|
|
),
|
|
# ================================================================
|
|
# FUTURE FORECAST SENSORS
|
|
# ================================================================
|
|
# Future average sensors (next N hours from next interval)
|
|
"next_avg_1h": lambda: get_next_avg_n_hours_value(1),
|
|
"next_avg_2h": lambda: get_next_avg_n_hours_value(2),
|
|
"next_avg_3h": lambda: get_next_avg_n_hours_value(3),
|
|
"next_avg_4h": lambda: get_next_avg_n_hours_value(4),
|
|
"next_avg_5h": lambda: get_next_avg_n_hours_value(5),
|
|
"next_avg_6h": lambda: get_next_avg_n_hours_value(6),
|
|
"next_avg_8h": lambda: get_next_avg_n_hours_value(8),
|
|
"next_avg_12h": lambda: get_next_avg_n_hours_value(12),
|
|
# Current and next trend change sensors
|
|
"current_price_trend": trend_calculator.get_current_trend_value,
|
|
"next_price_trend_change": trend_calculator.get_next_trend_change_value,
|
|
# Price trend sensors
|
|
"price_trend_1h": lambda: trend_calculator.get_price_trend_value(hours=1),
|
|
"price_trend_2h": lambda: trend_calculator.get_price_trend_value(hours=2),
|
|
"price_trend_3h": lambda: trend_calculator.get_price_trend_value(hours=3),
|
|
"price_trend_4h": lambda: trend_calculator.get_price_trend_value(hours=4),
|
|
"price_trend_5h": lambda: trend_calculator.get_price_trend_value(hours=5),
|
|
"price_trend_6h": lambda: trend_calculator.get_price_trend_value(hours=6),
|
|
"price_trend_8h": lambda: trend_calculator.get_price_trend_value(hours=8),
|
|
"price_trend_12h": lambda: trend_calculator.get_price_trend_value(hours=12),
|
|
# Diagnostic sensors
|
|
"data_timestamp": get_data_timestamp,
|
|
# Data lifecycle status sensor
|
|
"data_lifecycle_status": lambda: lifecycle_calculator.get_lifecycle_state(),
|
|
# Home metadata sensors (via MetadataCalculator)
|
|
"home_type": lambda: metadata_calculator.get_home_metadata_value("type"),
|
|
"home_size": lambda: metadata_calculator.get_home_metadata_value("size"),
|
|
"main_fuse_size": lambda: metadata_calculator.get_home_metadata_value("mainFuseSize"),
|
|
"number_of_residents": lambda: metadata_calculator.get_home_metadata_value("numberOfResidents"),
|
|
"primary_heating_source": lambda: metadata_calculator.get_home_metadata_value("primaryHeatingSource"),
|
|
# Metering point sensors (via MetadataCalculator)
|
|
"grid_company": lambda: metadata_calculator.get_metering_point_value("gridCompany"),
|
|
"grid_area_code": lambda: metadata_calculator.get_metering_point_value("gridAreaCode"),
|
|
"price_area_code": lambda: metadata_calculator.get_metering_point_value("priceAreaCode"),
|
|
"consumption_ean": lambda: metadata_calculator.get_metering_point_value("consumptionEan"),
|
|
"production_ean": lambda: metadata_calculator.get_metering_point_value("productionEan"),
|
|
"energy_tax_type": lambda: metadata_calculator.get_metering_point_value("energyTaxType"),
|
|
"vat_type": lambda: metadata_calculator.get_metering_point_value("vatType"),
|
|
"estimated_annual_consumption": lambda: metadata_calculator.get_metering_point_value(
|
|
"estimatedAnnualConsumption"
|
|
),
|
|
# Subscription sensors (via MetadataCalculator)
|
|
"subscription_status": lambda: metadata_calculator.get_subscription_value("status"),
|
|
# Volatility sensors (via VolatilityCalculator)
|
|
"today_volatility": lambda: volatility_calculator.get_volatility_value(volatility_type="today"),
|
|
"tomorrow_volatility": lambda: volatility_calculator.get_volatility_value(volatility_type="tomorrow"),
|
|
"next_24h_volatility": lambda: volatility_calculator.get_volatility_value(volatility_type="next_24h"),
|
|
"today_tomorrow_volatility": lambda: volatility_calculator.get_volatility_value(
|
|
volatility_type="today_tomorrow"
|
|
),
|
|
# ================================================================
|
|
# BEST/PEAK PRICE TIMING SENSORS - via TimingCalculator
|
|
# ================================================================
|
|
# Best Price timing sensors
|
|
"best_price_end_time": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="best_price", value_type="end_time"
|
|
),
|
|
"best_price_period_duration": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="best_price", value_type="period_duration"
|
|
),
|
|
"best_price_remaining_minutes": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="best_price", value_type="remaining_minutes"
|
|
),
|
|
"best_price_progress": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="best_price", value_type="progress"
|
|
),
|
|
"best_price_next_start_time": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="best_price", value_type="next_start_time"
|
|
),
|
|
"best_price_next_in_minutes": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="best_price", value_type="next_in_minutes"
|
|
),
|
|
# Peak Price timing sensors
|
|
"peak_price_end_time": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="peak_price", value_type="end_time"
|
|
),
|
|
"peak_price_period_duration": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="peak_price", value_type="period_duration"
|
|
),
|
|
"peak_price_remaining_minutes": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="peak_price", value_type="remaining_minutes"
|
|
),
|
|
"peak_price_progress": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="peak_price", value_type="progress"
|
|
),
|
|
"peak_price_next_start_time": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="peak_price", value_type="next_start_time"
|
|
),
|
|
"peak_price_next_in_minutes": lambda: timing_calculator.get_period_timing_value(
|
|
period_type="peak_price", value_type="next_in_minutes"
|
|
),
|
|
# Chart data export sensor
|
|
"chart_data_export": get_chart_data_export_value,
|
|
# Chart metadata sensor
|
|
"chart_metadata": get_chart_metadata_value,
|
|
}
|