hass.tibber_prices/custom_components/tibber_prices/sensor/chart_metadata.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

142 lines
4.6 KiB
Python

"""Chart metadata export functionality for Tibber Prices sensors."""
from __future__ import annotations
from typing import TYPE_CHECKING
from custom_components.tibber_prices.const import DATA_CHART_METADATA_CONFIG, DOMAIN
if TYPE_CHECKING:
from datetime import datetime
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
from custom_components.tibber_prices.data import TibberPricesConfigEntry
from homeassistant.core import HomeAssistant
async def call_chartdata_service_for_metadata_async(
hass: HomeAssistant,
coordinator: TibberPricesDataUpdateCoordinator,
config_entry: TibberPricesConfigEntry,
) -> tuple[dict | None, str | None]:
"""
Call get_chartdata service with configuration from configuration.yaml for metadata (async).
Returns:
Tuple of (response, error_message).
If successful: (response_dict, None)
If failed: (None, error_string)
"""
# Get configuration from hass.data (loaded from configuration.yaml)
domain_data = hass.data.get(DOMAIN, {})
chart_metadata_config = domain_data.get(DATA_CHART_METADATA_CONFIG, {})
# Use chart_metadata_config directly (already a dict from async_setup)
service_params = dict(chart_metadata_config) if chart_metadata_config else {}
# Add required entry_id parameter
service_params["entry_id"] = config_entry.entry_id
# Force metadata to "only" - this sensor ONLY provides metadata
service_params["metadata"] = "only"
# Default to subunit_currency=True for ApexCharts compatibility (can be overridden in configuration.yaml)
if "subunit_currency" not in service_params:
service_params["subunit_currency"] = True
# Call get_chartdata service using official HA service system
try:
response = await hass.services.async_call(
DOMAIN,
"get_chartdata",
service_params,
blocking=True,
return_response=True,
)
except Exception as ex:
coordinator.logger.exception("Chart metadata service call failed")
return None, str(ex)
else:
return response, None
def get_chart_metadata_state(
chart_metadata_response: dict | None,
chart_metadata_error: str | None,
) -> str | None:
"""
Return state for chart_metadata sensor.
Args:
chart_metadata_response: Last service response (or None)
chart_metadata_error: Last error message (or None)
Returns:
"error" if error occurred
"ready" if metadata available
"pending" if no data yet
"""
if chart_metadata_error:
return "error"
if chart_metadata_response:
return "ready"
return "pending"
def build_chart_metadata_attributes(
chart_metadata_response: dict | None,
chart_metadata_last_update: datetime | None,
chart_metadata_error: str | None,
) -> dict[str, object] | None:
"""
Return chart metadata from last service call as attributes.
Attribute order: timestamp, error (if any), metadata fields (at the end).
Args:
chart_metadata_response: Last service response (should contain "metadata" key)
chart_metadata_last_update: Timestamp of last update
chart_metadata_error: Error message if service call failed
Returns:
Dict with timestamp, optional error, and metadata fields.
"""
# Build base attributes with timestamp FIRST
attributes: dict[str, object] = {
"timestamp": chart_metadata_last_update,
}
# Add error message if service call failed
if chart_metadata_error:
attributes["error"] = chart_metadata_error
if not chart_metadata_response:
# No data - only timestamp (and error if present)
return attributes
# Extract metadata from response (get_chartdata returns {"metadata": {...}})
metadata = chart_metadata_response.get("metadata", {})
# Extract the fields we care about for charts
# These are the universal chart metadata fields useful for any chart card
if metadata:
yaxis_suggested = metadata.get("yaxis_suggested", {})
# Add yaxis bounds (useful for all chart cards)
if "min" in yaxis_suggested:
attributes["yaxis_min"] = yaxis_suggested["min"]
if "max" in yaxis_suggested:
attributes["yaxis_max"] = yaxis_suggested["max"]
# Add currency info (useful for labeling)
if "currency" in metadata:
attributes["currency"] = metadata["currency"]
# Add resolution info (interval duration in minutes)
if "resolution" in metadata:
attributes["resolution"] = metadata["resolution"]
return attributes