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
164 lines
5.6 KiB
Python
164 lines
5.6 KiB
Python
"""Future price/trend attribute builders for Tibber Prices sensors."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from custom_components.tibber_prices.const import get_display_unit_factor
|
|
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
|
|
|
if TYPE_CHECKING:
|
|
from custom_components.tibber_prices.coordinator.core import (
|
|
TibberPricesDataUpdateCoordinator,
|
|
)
|
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
|
from custom_components.tibber_prices.data import TibberPricesConfigEntry
|
|
|
|
from .helpers import add_alternate_average_attribute
|
|
|
|
# Constants
|
|
MAX_FORECAST_INTERVALS = 8 # Show up to 8 future intervals (2 hours with 15-min intervals)
|
|
|
|
|
|
def add_next_avg_attributes( # noqa: PLR0913
|
|
attributes: dict,
|
|
key: str,
|
|
coordinator: TibberPricesDataUpdateCoordinator,
|
|
*,
|
|
time: TibberPricesTimeService,
|
|
cached_data: dict | None = None,
|
|
config_entry: TibberPricesConfigEntry | None = None,
|
|
) -> None:
|
|
"""
|
|
Add attributes for next N hours average price sensors.
|
|
|
|
Args:
|
|
attributes: Dictionary to add attributes to
|
|
key: The sensor entity key
|
|
coordinator: The data update coordinator
|
|
time: TibberPricesTimeService instance (required)
|
|
cached_data: Optional cached data dictionary for median values
|
|
config_entry: Optional config entry for user preferences
|
|
|
|
"""
|
|
# Extract hours from sensor key (e.g., "next_avg_3h" -> 3)
|
|
try:
|
|
hours = int(key.split("_")[-1].replace("h", ""))
|
|
except (ValueError, AttributeError):
|
|
return
|
|
|
|
# Use TimeService to get the N-hour window starting from next interval
|
|
next_interval_start, window_end = time.get_next_n_hours_window(hours)
|
|
|
|
# Get all intervals (yesterday, today, tomorrow) via helper
|
|
all_prices = get_intervals_for_day_offsets(coordinator.data, [-1, 0, 1])
|
|
|
|
if not all_prices:
|
|
return
|
|
# Find all intervals in the window
|
|
intervals_in_window = []
|
|
for price_data in all_prices:
|
|
starts_at = time.get_interval_time(price_data)
|
|
if starts_at is None:
|
|
continue
|
|
if next_interval_start <= starts_at < window_end:
|
|
intervals_in_window.append(price_data)
|
|
|
|
# Add timestamp attribute (start of next interval - where calculation begins)
|
|
if intervals_in_window:
|
|
attributes["timestamp"] = intervals_in_window[0].get("startsAt")
|
|
attributes["interval_count"] = len(intervals_in_window)
|
|
attributes["hours"] = hours
|
|
|
|
# Add alternate average attribute if available in cached_data
|
|
if cached_data and config_entry:
|
|
base_key = f"next_avg_{hours}h"
|
|
add_alternate_average_attribute(
|
|
attributes,
|
|
cached_data,
|
|
base_key,
|
|
config_entry=config_entry,
|
|
)
|
|
|
|
|
|
def get_future_prices(
|
|
coordinator: TibberPricesDataUpdateCoordinator,
|
|
max_intervals: int | None = None,
|
|
*,
|
|
time: TibberPricesTimeService,
|
|
config_entry: TibberPricesConfigEntry,
|
|
) -> list[dict] | None:
|
|
"""
|
|
Get future price data for multiple upcoming intervals.
|
|
|
|
Args:
|
|
coordinator: The data update coordinator.
|
|
max_intervals: Maximum number of future intervals to return.
|
|
time: TibberPricesTimeService instance (required).
|
|
config_entry: Config entry to get display unit configuration.
|
|
|
|
Returns:
|
|
List of upcoming price intervals with timestamps and prices.
|
|
|
|
"""
|
|
if not coordinator.data:
|
|
return None
|
|
|
|
# Get all intervals (yesterday, today, tomorrow) via helper
|
|
all_prices = get_intervals_for_day_offsets(coordinator.data, [-1, 0, 1])
|
|
|
|
if not all_prices:
|
|
return None
|
|
|
|
# Initialize the result list
|
|
future_prices = []
|
|
|
|
# Track the maximum intervals to return
|
|
intervals_to_return = MAX_FORECAST_INTERVALS if max_intervals is None else max_intervals
|
|
|
|
# Get current date for day key determination
|
|
now = time.now()
|
|
today_date = now.date()
|
|
tomorrow_date = time.get_local_date(offset_days=1)
|
|
|
|
for price_data in all_prices:
|
|
starts_at = time.get_interval_time(price_data)
|
|
if starts_at is None:
|
|
continue
|
|
|
|
interval_end = starts_at + time.get_interval_duration()
|
|
|
|
# Use TimeService to check if interval is in future
|
|
if time.is_in_future(starts_at):
|
|
# Determine which day this interval belongs to
|
|
interval_date = starts_at.date()
|
|
if interval_date == today_date:
|
|
day_key = "today"
|
|
elif interval_date == tomorrow_date:
|
|
day_key = "tomorrow"
|
|
else:
|
|
day_key = "unknown"
|
|
|
|
# Convert to display currency unit based on configuration
|
|
price_major = float(price_data["total"])
|
|
factor = get_display_unit_factor(config_entry)
|
|
price_display = round(price_major * factor, 2)
|
|
|
|
future_prices.append(
|
|
{
|
|
"interval_start": starts_at,
|
|
"interval_end": interval_end,
|
|
"price": price_major,
|
|
"price_minor": price_display,
|
|
"level": price_data.get("level", "NORMAL"),
|
|
"rating": price_data.get("difference", None),
|
|
"rating_level": price_data.get("rating_level"),
|
|
"day": day_key,
|
|
}
|
|
)
|
|
|
|
# Sort by start time
|
|
future_prices.sort(key=lambda x: x["interval_start"])
|
|
|
|
# Limit to the requested number of intervals
|
|
return future_prices[:intervals_to_return] if future_prices else None
|