mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
feat(sensor): use dynamic precision for price rounding and display
Add get_display_precision() to const.py returning DISPLAY_PRECISION_SUBUNIT (2) or DISPLAY_PRECISION_BASE (4) based on config. Replace hardcoded round(..., 2) with get_display_precision() in all calculators and attribute builders. Add _update_suggested_precision() to sensor core; syncs entity registry suggested_display_precision on every coordinator update. Interval price sensors get full precision (2 or 4 dp); other MONETARY sensors get half precision (1 or 2 dp) as sensible default. Impact: Price sensor states and attributes now correctly use 4 decimal places in base-currency mode (was always 2). Display precision in dashboards updates automatically when currency mode changes.
This commit is contained in:
parent
6d22ea7151
commit
a4ad506e01
13 changed files with 208 additions and 207 deletions
|
|
@ -9,12 +9,7 @@ from typing import TYPE_CHECKING, Any
|
|||
|
||||
import aiofiles
|
||||
|
||||
from homeassistant.const import (
|
||||
CURRENCY_DOLLAR,
|
||||
CURRENCY_EURO,
|
||||
UnitOfPower,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.const import CURRENCY_DOLLAR, CURRENCY_EURO, UnitOfPower, UnitOfTime
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
|
@ -29,6 +24,11 @@ LOGGER = logging.getLogger(__package__)
|
|||
DATA_CHART_CONFIG = "chart_config" # Key for chart export config in hass.data
|
||||
DATA_CHART_METADATA_CONFIG = "chart_metadata_config" # Key for chart metadata config in hass.data
|
||||
|
||||
# Config entry data flag: set when user switches currency display mode.
|
||||
# Triggers a fresh (un-dismissed) repair issue on every setup/reload until
|
||||
# the user explicitly re-saves the currency settings to acknowledge.
|
||||
DATA_STATISTICS_REVIEW_REQUIRED = "statistics_review_required"
|
||||
|
||||
# Configuration keys
|
||||
CONF_EXTENDED_DESCRIPTIONS = "extended_descriptions"
|
||||
CONF_VIRTUAL_TIME_OFFSET_DAYS = (
|
||||
|
|
@ -462,14 +462,42 @@ def get_display_unit_factor(config_entry: ConfigEntry) -> int:
|
|||
Example:
|
||||
price_base = 0.2534 # Internal: 0.2534 €/kWh
|
||||
factor = get_display_unit_factor(config_entry)
|
||||
display_value = round(price_base * factor, 2)
|
||||
# → 25.34 ct/kWh (subunit) or 0.25 €/kWh (base)
|
||||
precision = get_display_precision(config_entry)
|
||||
display_value = round(price_base * factor, precision)
|
||||
# → 25.34 ct/kWh (subunit, 2 decimals) or 0.2534 €/kWh (base, 4 decimals)
|
||||
|
||||
"""
|
||||
display_mode = config_entry.options.get(CONF_CURRENCY_DISPLAY_MODE, DISPLAY_MODE_SUBUNIT)
|
||||
return 100 if display_mode == DISPLAY_MODE_SUBUNIT else 1
|
||||
|
||||
|
||||
# Rounding precision constants for display currency
|
||||
DISPLAY_PRECISION_SUBUNIT = 2 # Decimal places for subunit currency (ct, øre)
|
||||
DISPLAY_PRECISION_BASE = 4 # Decimal places for base currency (€, kr)
|
||||
|
||||
|
||||
def get_display_precision(config_entry: ConfigEntry) -> int:
|
||||
"""
|
||||
Get decimal precision for rounding prices in the configured display currency.
|
||||
|
||||
Subunit currencies (ct, øre) use 2 decimal places (e.g., 25.34 ct/kWh).
|
||||
Base currencies (€, kr) use 4 decimal places (e.g., 0.2534 €/kWh).
|
||||
|
||||
This ensures sufficient precision for all currency modes:
|
||||
- Subunit: 2 decimals (the sub-cent level is rarely meaningful)
|
||||
- Base: 4 decimals (preserves full API precision for EUR/NOK/SEK prices)
|
||||
|
||||
Args:
|
||||
config_entry: ConfigEntry with currency_display_mode option
|
||||
|
||||
Returns:
|
||||
2 for subunit currency, 4 for base currency
|
||||
|
||||
"""
|
||||
display_mode = config_entry.options.get(CONF_CURRENCY_DISPLAY_MODE, DISPLAY_MODE_SUBUNIT)
|
||||
return DISPLAY_PRECISION_SUBUNIT if display_mode == DISPLAY_MODE_SUBUNIT else DISPLAY_PRECISION_BASE
|
||||
|
||||
|
||||
def get_display_unit_string(config_entry: ConfigEntry, currency_code: str | None) -> str:
|
||||
"""
|
||||
Get unit string for display based on configuration.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ 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.const import get_display_precision, get_display_unit_factor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
|
@ -55,7 +55,8 @@ def get_price_value(
|
|||
# New mode: use config_entry
|
||||
if config_entry is not None:
|
||||
factor = get_display_unit_factor(config_entry)
|
||||
return round(price * factor, 2)
|
||||
precision = get_display_precision(config_entry)
|
||||
return round(price * factor, precision)
|
||||
|
||||
# Fallback: default to subunit currency (backward compatibility)
|
||||
return round(price * 100, 2)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ class TibberPricesIconContext:
|
|||
has_future_periods_callback: Callable[[], bool] | None = None
|
||||
period_is_active_callback: Callable[[], bool] | None = None
|
||||
time: TibberPricesTimeService | None = None
|
||||
trend_change_direction: str | None = None # For next_price_trend_change icon lookup
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -74,7 +75,7 @@ def get_dynamic_icon(
|
|||
|
||||
# Try various icon sources in order
|
||||
return (
|
||||
get_trend_icon(key, value)
|
||||
get_trend_icon(key, value, context=ctx)
|
||||
or get_timing_sensor_icon(key, value, period_is_active_callback=ctx.period_is_active_callback)
|
||||
or get_price_sensor_icon(key, ctx.coordinator_data, time=ctx.time)
|
||||
or get_level_sensor_icon(key, value)
|
||||
|
|
@ -84,12 +85,24 @@ def get_dynamic_icon(
|
|||
)
|
||||
|
||||
|
||||
def get_trend_icon(key: str, value: Any) -> str | None:
|
||||
# 5-level trend icons: strongly uses double arrows, normal uses single
|
||||
_TREND_ICONS = {
|
||||
"strongly_rising": "mdi:chevron-double-up",
|
||||
"rising": "mdi:trending-up",
|
||||
"stable": "mdi:trending-neutral",
|
||||
"falling": "mdi:trending-down",
|
||||
"strongly_falling": "mdi:chevron-double-down",
|
||||
}
|
||||
|
||||
|
||||
def get_trend_icon(key: str, value: Any, *, context: TibberPricesIconContext | None = None) -> str | None:
|
||||
"""Get icon for trend sensors using 5-level trend scale."""
|
||||
# Handle next_price_trend_change TIMESTAMP sensor differently
|
||||
# (icon based on attributes, not value which is a timestamp)
|
||||
# next_price_trend_change is a TIMESTAMP sensor — icon comes from direction attribute
|
||||
if key == "next_price_trend_change":
|
||||
return None # Will be handled by sensor's icon property using attributes
|
||||
direction = context.trend_change_direction if context else None
|
||||
if isinstance(direction, str):
|
||||
return _TREND_ICONS.get(direction, "mdi:help-circle-outline")
|
||||
return "mdi:help-circle-outline"
|
||||
|
||||
if not key.startswith(("price_trend_", "price_outlook_", "price_trajectory_")) and key != "current_price_trend":
|
||||
return None
|
||||
|
|
@ -97,15 +110,7 @@ def get_trend_icon(key: str, value: Any) -> str | None:
|
|||
if not isinstance(value, str):
|
||||
return None
|
||||
|
||||
# 5-level trend icons: strongly uses double arrows, normal uses single
|
||||
trend_icons = {
|
||||
"strongly_rising": "mdi:chevron-double-up", # Strong upward movement
|
||||
"rising": "mdi:trending-up", # Normal upward trend
|
||||
"stable": "mdi:trending-neutral", # No significant change
|
||||
"falling": "mdi:trending-down", # Normal downward trend
|
||||
"strongly_falling": "mdi:chevron-double-down", # Strong downward movement
|
||||
}
|
||||
return trend_icons.get(value)
|
||||
return _TREND_ICONS.get(value, "mdi:help-circle-outline")
|
||||
|
||||
|
||||
def get_timing_sensor_icon(
|
||||
|
|
|
|||
|
|
@ -4,13 +4,8 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from custom_components.tibber_prices.const import (
|
||||
PRICE_RATING_MAPPING,
|
||||
get_display_unit_factor,
|
||||
)
|
||||
from custom_components.tibber_prices.coordinator.helpers import (
|
||||
get_intervals_for_day_offsets,
|
||||
)
|
||||
from custom_components.tibber_prices.const import PRICE_RATING_MAPPING, get_display_precision, get_display_unit_factor
|
||||
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
||||
from homeassistant.const import PERCENTAGE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -30,12 +25,13 @@ def _add_energy_tax_from_interval(
|
|||
) -> None:
|
||||
"""Add energy_price and tax from a single interval dict."""
|
||||
factor = get_display_unit_factor(config_entry)
|
||||
precision = get_display_precision(config_entry)
|
||||
energy = interval_data.get("energy")
|
||||
if energy is not None:
|
||||
attributes["energy_price"] = round(float(energy) * factor, 2)
|
||||
attributes["energy_price"] = round(float(energy) * factor, precision)
|
||||
tax = interval_data.get("tax")
|
||||
if tax is not None:
|
||||
attributes["tax"] = round(float(tax) * factor, 2)
|
||||
attributes["tax"] = round(float(tax) * factor, precision)
|
||||
|
||||
|
||||
def _add_energy_tax_averages_from_cache(
|
||||
|
|
@ -49,14 +45,15 @@ def _add_energy_tax_averages_from_cache(
|
|||
"last_energy_tax_averages", (None, None, None, None)
|
||||
)
|
||||
factor = get_display_unit_factor(config_entry)
|
||||
precision = get_display_precision(config_entry)
|
||||
if energy_mean is not None:
|
||||
attributes["energy_price_mean"] = round(float(energy_mean) * factor, 2)
|
||||
attributes["energy_price_mean"] = round(float(energy_mean) * factor, precision)
|
||||
if energy_median is not None:
|
||||
attributes["energy_price_median"] = round(float(energy_median) * factor, 2)
|
||||
attributes["energy_price_median"] = round(float(energy_median) * factor, precision)
|
||||
if tax_mean is not None:
|
||||
attributes["tax_mean"] = round(float(tax_mean) * factor, 2)
|
||||
attributes["tax_mean"] = round(float(tax_mean) * factor, precision)
|
||||
if tax_median is not None:
|
||||
attributes["tax_median"] = round(float(tax_median) * factor, 2)
|
||||
attributes["tax_median"] = round(float(tax_median) * factor, precision)
|
||||
|
||||
|
||||
def _get_day_midnight_timestamp(key: str, *, time: TibberPricesTimeService) -> datetime:
|
||||
|
|
|
|||
|
|
@ -4,13 +4,11 @@ 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.const import get_display_precision, 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.core import TibberPricesDataUpdateCoordinator
|
||||
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||
from custom_components.tibber_prices.data import TibberPricesConfigEntry
|
||||
|
||||
|
|
@ -20,7 +18,7 @@ from .helpers import add_alternate_average_attribute
|
|||
MAX_FORECAST_INTERVALS = 8 # Show up to 8 future intervals (2 hours with 15-min intervals)
|
||||
|
||||
|
||||
def add_next_avg_attributes( # noqa: PLR0913
|
||||
def add_next_avg_attributes(
|
||||
attributes: dict,
|
||||
key: str,
|
||||
coordinator: TibberPricesDataUpdateCoordinator,
|
||||
|
|
@ -142,7 +140,8 @@ def get_future_prices(
|
|||
# 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)
|
||||
precision = get_display_precision(config_entry)
|
||||
price_display = round(price_major * factor, precision)
|
||||
|
||||
future_prices.append(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,15 +8,14 @@ from typing import TYPE_CHECKING, Any
|
|||
from custom_components.tibber_prices.const import (
|
||||
PRICE_LEVEL_MAPPING,
|
||||
PRICE_RATING_MAPPING,
|
||||
get_display_precision,
|
||||
get_display_unit_factor,
|
||||
)
|
||||
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute
|
||||
from custom_components.tibber_prices.utils.price import find_price_data_for_interval
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from custom_components.tibber_prices.coordinator.core import (
|
||||
TibberPricesDataUpdateCoordinator,
|
||||
)
|
||||
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
|
||||
|
||||
|
|
@ -112,17 +111,18 @@ def _add_energy_tax_attributes(
|
|||
return
|
||||
|
||||
factor = get_display_unit_factor(config_entry)
|
||||
precision = get_display_precision(config_entry)
|
||||
|
||||
energy = interval_data.get("energy")
|
||||
if energy is not None:
|
||||
attributes["energy_price"] = round(float(energy) * factor, 2)
|
||||
attributes["energy_price"] = round(float(energy) * factor, precision)
|
||||
|
||||
tax = interval_data.get("tax")
|
||||
if tax is not None:
|
||||
attributes["tax"] = round(float(tax) * factor, 2)
|
||||
attributes["tax"] = round(float(tax) * factor, precision)
|
||||
|
||||
|
||||
def add_current_interval_price_attributes( # noqa: PLR0913
|
||||
def add_current_interval_price_attributes(
|
||||
attributes: dict,
|
||||
key: str,
|
||||
coordinator: TibberPricesDataUpdateCoordinator,
|
||||
|
|
@ -198,7 +198,7 @@ def add_current_interval_price_attributes( # noqa: PLR0913
|
|||
)
|
||||
|
||||
|
||||
def add_level_attributes_for_sensor( # noqa: PLR0913
|
||||
def add_level_attributes_for_sensor(
|
||||
attributes: dict,
|
||||
key: str,
|
||||
interval_data: dict | None,
|
||||
|
|
@ -252,7 +252,7 @@ def add_price_level_attributes(attributes: dict, level: str) -> None:
|
|||
add_icon_color_attribute(attributes, key="price_level", state_value=level)
|
||||
|
||||
|
||||
def add_rating_attributes_for_sensor( # noqa: PLR0913
|
||||
def add_rating_attributes_for_sensor(
|
||||
attributes: dict,
|
||||
key: str,
|
||||
interval_data: dict | None,
|
||||
|
|
|
|||
|
|
@ -9,12 +9,10 @@ from custom_components.tibber_prices.const import (
|
|||
CONF_PRICE_RATING_THRESHOLD_LOW,
|
||||
DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
|
||||
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
||||
get_display_precision,
|
||||
)
|
||||
from custom_components.tibber_prices.entity_utils import get_price_value
|
||||
from custom_components.tibber_prices.sensor.helpers import (
|
||||
aggregate_level_data,
|
||||
aggregate_rating_data,
|
||||
)
|
||||
from custom_components.tibber_prices.sensor.helpers import aggregate_level_data, aggregate_rating_data
|
||||
from custom_components.tibber_prices.utils.average import calculate_median
|
||||
|
||||
from .base import TibberPricesBaseCalculator
|
||||
|
|
@ -22,9 +20,7 @@ from .base import TibberPricesBaseCalculator
|
|||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from custom_components.tibber_prices.coordinator import (
|
||||
TibberPricesDataUpdateCoordinator,
|
||||
)
|
||||
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
|
||||
|
||||
|
||||
class TibberPricesDailyStatCalculator(TibberPricesBaseCalculator):
|
||||
|
|
@ -115,9 +111,10 @@ class TibberPricesDailyStatCalculator(TibberPricesBaseCalculator):
|
|||
# Compute and cache energy/tax averages for attribute builders
|
||||
self._cache_energy_tax_averages(price_intervals)
|
||||
# Convert to display currency units based on config
|
||||
avg_result = round(get_price_value(value, config_entry=self.coordinator.config_entry), 2)
|
||||
precision = get_display_precision(self.coordinator.config_entry)
|
||||
avg_result = round(get_price_value(value, config_entry=self.coordinator.config_entry), precision)
|
||||
median_result = (
|
||||
round(get_price_value(median, config_entry=self.coordinator.config_entry), 2)
|
||||
round(get_price_value(median, config_entry=self.coordinator.config_entry), precision)
|
||||
if median is not None
|
||||
else None
|
||||
)
|
||||
|
|
@ -132,9 +129,10 @@ class TibberPricesDailyStatCalculator(TibberPricesBaseCalculator):
|
|||
self._last_extreme_interval = pi["interval"]
|
||||
break
|
||||
|
||||
# Return in configured display currency units with 2 decimals
|
||||
# Return in configured display currency units
|
||||
precision = get_display_precision(self.coordinator.config_entry)
|
||||
result = get_price_value(value, config_entry=self.coordinator.config_entry)
|
||||
return round(result, 2)
|
||||
return round(result, precision)
|
||||
|
||||
def get_daily_aggregated_value(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@ 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.const import get_display_precision, get_display_unit_factor
|
||||
|
||||
from .base import TibberPricesBaseCalculator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from custom_components.tibber_prices.coordinator import (
|
||||
TibberPricesDataUpdateCoordinator,
|
||||
)
|
||||
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
|
||||
|
||||
|
||||
class TibberPricesIntervalCalculator(TibberPricesBaseCalculator):
|
||||
|
|
@ -36,7 +34,7 @@ class TibberPricesIntervalCalculator(TibberPricesBaseCalculator):
|
|||
self._last_rating_level: str | None = None
|
||||
self._last_rating_difference: float | None = None
|
||||
|
||||
def get_interval_value( # noqa: PLR0911
|
||||
def get_interval_value(
|
||||
self,
|
||||
*,
|
||||
interval_offset: int,
|
||||
|
|
@ -74,7 +72,8 @@ class TibberPricesIntervalCalculator(TibberPricesBaseCalculator):
|
|||
if in_euro:
|
||||
return price
|
||||
factor = get_display_unit_factor(self.config_entry)
|
||||
return round(price * factor, 2)
|
||||
precision = get_display_precision(self.config_entry)
|
||||
return round(price * factor, precision)
|
||||
|
||||
if value_type == "level":
|
||||
level = self.safe_get_from_interval(interval_data, "level")
|
||||
|
|
|
|||
|
|
@ -15,22 +15,18 @@ Caching strategy:
|
|||
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
from custom_components.tibber_prices.const import get_display_unit_factor
|
||||
from custom_components.tibber_prices.const import get_display_precision, get_display_unit_factor
|
||||
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
||||
from custom_components.tibber_prices.entity_utils.colors import get_icon_color
|
||||
from custom_components.tibber_prices.utils.average import calculate_mean, calculate_next_n_hours_mean
|
||||
from custom_components.tibber_prices.utils.price import (
|
||||
calculate_price_trend,
|
||||
find_price_data_for_interval,
|
||||
)
|
||||
from custom_components.tibber_prices.utils.price import calculate_price_trend, find_price_data_for_interval
|
||||
|
||||
from .base import TibberPricesBaseCalculator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
||||
from custom_components.tibber_prices.coordinator import (
|
||||
TibberPricesDataUpdateCoordinator,
|
||||
)
|
||||
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
|
||||
|
||||
# Constants
|
||||
MIN_HOURS_FOR_LATER_HALF = 1 # Minimum hours needed to calculate half-window averages (activates at 2h+)
|
||||
|
|
@ -62,7 +58,7 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
"strongly_rising": "rising",
|
||||
}
|
||||
|
||||
def __init__(self, coordinator: "TibberPricesDataUpdateCoordinator") -> None:
|
||||
def __init__(self, coordinator: TibberPricesDataUpdateCoordinator) -> None:
|
||||
"""Initialize trend calculator with caching state."""
|
||||
super().__init__(coordinator)
|
||||
# Per-sensor caches (for price_outlook_Xh and price_trajectory_Xh sensors)
|
||||
|
|
@ -168,18 +164,12 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
volatility_threshold_high=volatility_threshold_high,
|
||||
)
|
||||
|
||||
# Determine icon color based on trend state (5-level scale)
|
||||
# Strongly rising/falling uses more intense colors
|
||||
icon_color = {
|
||||
"strongly_rising": "var(--error-color)", # Red for strongly rising (very expensive)
|
||||
"rising": "var(--warning-color)", # Orange/Yellow for rising prices
|
||||
"stable": "var(--state-icon-color)", # Default gray for stable prices
|
||||
"falling": "var(--success-color)", # Green for falling prices (cheaper)
|
||||
"strongly_falling": "var(--success-color)", # Green for strongly falling (great deal)
|
||||
}.get(trend_state, "var(--state-icon-color)")
|
||||
# Determine icon color via centralized mapping (same as colors.py)
|
||||
icon_color = get_icon_color(f"price_outlook_{hours}h", trend_state) or "var(--state-icon-color)"
|
||||
|
||||
# Convert prices to display currency unit based on configuration
|
||||
factor = get_display_unit_factor(self.config_entry)
|
||||
precision = get_display_precision(self.config_entry)
|
||||
|
||||
# Store attributes in sensor-specific dictionary AND cache the trend value
|
||||
# Show effective thresholds (after volatility adjustment) so users can understand
|
||||
|
|
@ -188,7 +178,7 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
"timestamp": next_interval_start,
|
||||
"trend_value": trend_value,
|
||||
f"trend_{hours}h_%": round(diff_pct, 1),
|
||||
f"next_{hours}h_avg": round(future_mean * factor, 2),
|
||||
f"next_{hours}h_avg": round(future_mean * factor, precision),
|
||||
"interval_count": lookahead_intervals,
|
||||
"threshold_rising_%": round(threshold_rising * vol_factor, 1),
|
||||
"threshold_rising_strongly_%": round(threshold_strongly_rising * vol_factor, 1),
|
||||
|
|
@ -203,7 +193,7 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
# Get second half average for longer periods
|
||||
later_half_avg = self._calculate_later_half_average(hours, next_interval_start)
|
||||
if later_half_avg is not None:
|
||||
self._trend_attributes[f"second_half_{hours}h_avg"] = round(later_half_avg * factor, 2)
|
||||
self._trend_attributes[f"second_half_{hours}h_avg"] = round(later_half_avg * factor, precision)
|
||||
|
||||
# Calculate incremental change: how much does the later half differ from current?
|
||||
# CRITICAL: Use abs() for negative prices and allow calculation for all non-zero prices
|
||||
|
|
@ -237,15 +227,17 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
# price direction BEFORE the current trend (binary: rising/falling),
|
||||
# not the trend classification. next_price_trend_change uses "from_direction"
|
||||
# for the current 5-level trend state.
|
||||
current_trend_state = trend_info["current_trend_state"]
|
||||
self._current_trend_attributes = {
|
||||
"previous_direction": trend_info["from_direction"],
|
||||
"price_direction_duration_minutes": trend_info["trend_duration_minutes"],
|
||||
"price_direction_since": (
|
||||
trend_info["trend_start_time"].isoformat() if trend_info["trend_start_time"] else None
|
||||
),
|
||||
"icon_color": get_icon_color("current_price_trend", current_trend_state),
|
||||
}
|
||||
|
||||
return trend_info["current_trend_state"]
|
||||
return current_trend_state
|
||||
|
||||
def get_next_trend_change_value(self) -> datetime | None:
|
||||
"""
|
||||
|
|
@ -308,7 +300,7 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
Trend state: "rising" | "falling" | "stable", or None if unavailable
|
||||
|
||||
"""
|
||||
if hours < 2: # noqa: PLR2004
|
||||
if hours < 2:
|
||||
return None
|
||||
|
||||
if not self.has_data():
|
||||
|
|
@ -378,6 +370,7 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
)
|
||||
|
||||
factor = get_display_unit_factor(self.config_entry)
|
||||
precision = get_display_precision(self.config_entry)
|
||||
time_obj = self.coordinator.time
|
||||
total_intervals = time_obj.minutes_to_intervals(hours * 60)
|
||||
first_half_count = total_intervals // 2
|
||||
|
|
@ -387,8 +380,8 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
"timestamp": next_interval_start,
|
||||
"trend_value": trend_value,
|
||||
f"trajectory_{hours}h_%": round(diff_pct, 1),
|
||||
f"first_half_{hours}h_avg": round(first_half_avg * factor, 2),
|
||||
f"second_half_{hours}h_avg": round(second_half_avg * factor, 2),
|
||||
f"first_half_{hours}h_avg": round(first_half_avg * factor, precision),
|
||||
f"second_half_{hours}h_avg": round(second_half_avg * factor, precision),
|
||||
f"first_half_{hours}h_diff_from_current_%": round(
|
||||
((first_half_avg - current_interval_price) / abs(current_interval_price)) * 100, 1
|
||||
)
|
||||
|
|
@ -402,6 +395,7 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
"first_half_interval_count": first_half_count,
|
||||
"second_half_interval_count": second_half_count,
|
||||
"volatility_factor": vol_factor,
|
||||
"icon_color": get_icon_color(f"price_trajectory_{hours}h", trajectory_state),
|
||||
}
|
||||
|
||||
return trajectory_state
|
||||
|
|
@ -910,16 +904,17 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
change_price = float(change_interval["total"])
|
||||
minutes_until = time.minutes_until_rounded(change_time)
|
||||
factor = get_display_unit_factor(self.config_entry)
|
||||
precision = get_display_precision(self.config_entry)
|
||||
vf = first_change["vol_factor"]
|
||||
|
||||
self._trend_change_attributes = {
|
||||
"direction": first_change["trend"],
|
||||
"from_direction": current_trend_state,
|
||||
"minutes_until_change": minutes_until,
|
||||
"price_now": round(float(current_interval["total"]) * factor, 2),
|
||||
"price_at_change": round(change_price * factor, 2),
|
||||
"price_now": round(float(current_interval["total"]) * factor, precision),
|
||||
"price_at_change": round(change_price * factor, precision),
|
||||
"price_avg_after_change": (
|
||||
round(first_change["mean"] * factor, 2) if first_change["mean"] else None
|
||||
round(first_change["mean"] * factor, precision) if first_change["mean"] else None
|
||||
),
|
||||
"trend_diff_%": round(first_change["diff"], 1),
|
||||
"threshold_rising_%": round(thresholds["rising"] * vf, 1),
|
||||
|
|
|
|||
|
|
@ -12,14 +12,12 @@ from custom_components.tibber_prices.const import (
|
|||
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
|
||||
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
|
||||
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
|
||||
get_display_precision,
|
||||
get_display_unit_factor,
|
||||
)
|
||||
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
||||
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute, find_rolling_hour_center_index
|
||||
from custom_components.tibber_prices.sensor.attributes import (
|
||||
add_volatility_type_attributes,
|
||||
get_prices_for_volatility,
|
||||
)
|
||||
from custom_components.tibber_prices.sensor.attributes import add_volatility_type_attributes, get_prices_for_volatility
|
||||
from custom_components.tibber_prices.utils.average import calculate_mean
|
||||
from custom_components.tibber_prices.utils.price import (
|
||||
calculate_iqr_stats,
|
||||
|
|
@ -103,6 +101,7 @@ class TibberPricesVolatilityCalculator(TibberPricesBaseCalculator):
|
|||
|
||||
# Convert to display currency unit based on configuration
|
||||
factor = get_display_unit_factor(self.config_entry)
|
||||
precision = get_display_precision(self.config_entry)
|
||||
spread_display = spread * factor
|
||||
|
||||
# Calculate volatility level AND coefficient of variation
|
||||
|
|
@ -116,18 +115,18 @@ class TibberPricesVolatilityCalculator(TibberPricesBaseCalculator):
|
|||
attrs: dict[str, Any] = {
|
||||
"price_volatility": volatility.lower(),
|
||||
"price_coefficient_variation_%": round(cv, 2) if cv is not None else None,
|
||||
"price_spread": round(spread_display, 2),
|
||||
"price_min": round(price_min * factor, 2),
|
||||
"price_max": round(price_max * factor, 2),
|
||||
"price_mean": round(price_mean * factor, 2),
|
||||
"price_spread": round(spread_display, precision),
|
||||
"price_min": round(price_min * factor, precision),
|
||||
"price_max": round(price_max * factor, precision),
|
||||
"price_mean": round(price_mean * factor, precision),
|
||||
}
|
||||
|
||||
# Add IQR attributes when enough data is available (stay in price_* group)
|
||||
if iqr_stats is not None:
|
||||
attrs["price_median"] = round(iqr_stats["median"] * factor, 2)
|
||||
attrs["price_q25"] = round(iqr_stats["q25"] * factor, 2)
|
||||
attrs["price_q75"] = round(iqr_stats["q75"] * factor, 2)
|
||||
attrs["price_typical_spread"] = round(iqr_stats["iqr"] * factor, 2)
|
||||
attrs["price_median"] = round(iqr_stats["median"] * factor, precision)
|
||||
attrs["price_q25"] = round(iqr_stats["q25"] * factor, precision)
|
||||
attrs["price_q75"] = round(iqr_stats["q75"] * factor, precision)
|
||||
attrs["price_typical_spread"] = round(iqr_stats["iqr"] * factor, precision)
|
||||
if iqr_stats["iqr_pct"] is not None:
|
||||
attrs["price_typical_spread_%"] = round(iqr_stats["iqr_pct"], 2)
|
||||
attrs["price_spike_count"] = iqr_stats["outlier_count"]
|
||||
|
|
@ -208,15 +207,16 @@ class TibberPricesVolatilityCalculator(TibberPricesBaseCalculator):
|
|||
|
||||
# Convert to display units for attribute storage
|
||||
factor = get_display_unit_factor(self.config_entry)
|
||||
precision = get_display_precision(self.config_entry)
|
||||
price_attr_key = self._get_subject_price_attr_key(subject)
|
||||
|
||||
self._last_percentile_rank_attributes = {
|
||||
price_attr_key: round(subject_price * factor, 2),
|
||||
price_attr_key: round(subject_price * factor, precision),
|
||||
"prices_below_count": bisect.bisect_left(sorted(reference_prices), subject_price),
|
||||
"interval_count": len(reference_prices),
|
||||
"reference_min": round(min(reference_prices) * factor, 2),
|
||||
"reference_max": round(max(reference_prices) * factor, 2),
|
||||
"reference_mean": round(calculate_mean(reference_prices) * factor, 2),
|
||||
"reference_min": round(min(reference_prices) * factor, precision),
|
||||
"reference_max": round(max(reference_prices) * factor, precision),
|
||||
"reference_mean": round(calculate_mean(reference_prices) * factor, precision),
|
||||
}
|
||||
|
||||
return rank
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from custom_components.tibber_prices.const import get_display_precision
|
||||
from custom_components.tibber_prices.entity_utils import get_price_value
|
||||
|
||||
from .base import TibberPricesBaseCalculator
|
||||
|
|
@ -52,9 +53,10 @@ class TibberPricesWindow24hCalculator(TibberPricesBaseCalculator):
|
|||
if value is None:
|
||||
return None
|
||||
# Convert to display currency units based on config
|
||||
mean_result = round(get_price_value(value, config_entry=self.coordinator.config_entry), 2)
|
||||
precision = get_display_precision(self.coordinator.config_entry)
|
||||
mean_result = round(get_price_value(value, config_entry=self.coordinator.config_entry), precision)
|
||||
median_result = (
|
||||
round(get_price_value(median, config_entry=self.coordinator.config_entry), 2)
|
||||
round(get_price_value(median, config_entry=self.coordinator.config_entry), precision)
|
||||
if median is not None
|
||||
else None
|
||||
)
|
||||
|
|
@ -65,6 +67,7 @@ class TibberPricesWindow24hCalculator(TibberPricesBaseCalculator):
|
|||
if value is None:
|
||||
return None
|
||||
|
||||
# Return in configured display currency units with 2 decimals
|
||||
# Return in configured display currency units
|
||||
precision = get_display_precision(self.coordinator.config_entry)
|
||||
result = get_price_value(value, config_entry=self.coordinator.config_entry)
|
||||
return round(result, 2)
|
||||
return round(result, precision)
|
||||
|
|
|
|||
|
|
@ -2,54 +2,35 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime # noqa: TC003 - Used at runtime for _get_data_timestamp()
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from custom_components.tibber_prices.binary_sensor.attributes import (
|
||||
get_price_intervals_attributes,
|
||||
)
|
||||
from custom_components.tibber_prices.binary_sensor.attributes import get_price_intervals_attributes
|
||||
from custom_components.tibber_prices.const import (
|
||||
CONF_AVERAGE_SENSOR_DISPLAY,
|
||||
CONF_CURRENCY_DISPLAY_MODE,
|
||||
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
||||
CONF_PRICE_RATING_THRESHOLD_LOW,
|
||||
DEFAULT_AVERAGE_SENSOR_DISPLAY,
|
||||
DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
|
||||
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
||||
DISPLAY_MODE_BASE,
|
||||
DOMAIN,
|
||||
format_price_unit_base,
|
||||
get_display_precision,
|
||||
get_display_unit_factor,
|
||||
get_display_unit_string,
|
||||
)
|
||||
from custom_components.tibber_prices.coordinator import (
|
||||
MINUTE_UPDATE_ENTITY_KEYS,
|
||||
TIME_SENSITIVE_ENTITY_KEYS,
|
||||
)
|
||||
from custom_components.tibber_prices.coordinator.helpers import (
|
||||
get_intervals_for_day_offsets,
|
||||
)
|
||||
from custom_components.tibber_prices.coordinator import MINUTE_UPDATE_ENTITY_KEYS, TIME_SENSITIVE_ENTITY_KEYS
|
||||
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
||||
from custom_components.tibber_prices.entity import TibberPricesEntity
|
||||
from custom_components.tibber_prices.entity_utils import (
|
||||
add_icon_color_attribute,
|
||||
find_rolling_hour_center_index,
|
||||
get_price_value,
|
||||
)
|
||||
from custom_components.tibber_prices.entity_utils.icons import (
|
||||
TibberPricesIconContext,
|
||||
get_dynamic_icon,
|
||||
)
|
||||
from custom_components.tibber_prices.utils.average import (
|
||||
calculate_next_n_hours_mean,
|
||||
)
|
||||
from custom_components.tibber_prices.utils.price import (
|
||||
calculate_volatility_level,
|
||||
)
|
||||
from homeassistant.components.sensor import (
|
||||
RestoreSensor,
|
||||
SensorDeviceClass,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from custom_components.tibber_prices.entity_utils.icons import TibberPricesIconContext, get_dynamic_icon
|
||||
from custom_components.tibber_prices.utils.average import calculate_next_n_hours_mean
|
||||
from custom_components.tibber_prices.utils.price import calculate_volatility_level
|
||||
from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass, SensorEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import callback
|
||||
|
||||
|
|
@ -70,11 +51,7 @@ from .calculators import (
|
|||
TibberPricesVolatilityCalculator,
|
||||
TibberPricesWindow24hCalculator,
|
||||
)
|
||||
from .chart_data import (
|
||||
build_chart_data_attributes,
|
||||
call_chartdata_service_async,
|
||||
get_chart_data_state,
|
||||
)
|
||||
from .chart_data import build_chart_data_attributes, call_chartdata_service_async, get_chart_data_state
|
||||
from .chart_metadata import (
|
||||
build_chart_metadata_attributes,
|
||||
call_chartdata_service_for_metadata_async,
|
||||
|
|
@ -86,9 +63,7 @@ from .value_getters import get_value_getter_mapping
|
|||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from custom_components.tibber_prices.coordinator import (
|
||||
TibberPricesDataUpdateCoordinator,
|
||||
)
|
||||
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
|
||||
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||
|
||||
HOURS_IN_DAY = 24
|
||||
|
|
@ -426,6 +401,15 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
# Coordinator updates bring new API data — always write to ensure fresh state.
|
||||
# Reset _last_written_value so timer-based handlers also write next cycle.
|
||||
self._last_written_value = _SENTINEL
|
||||
|
||||
# Sync suggested display precision with entity registry. HA only updates
|
||||
# this on entity add and registry entry changes, not on state writes. When
|
||||
# the user switches currency display mode via options flow, we must push
|
||||
# the new precision to the registry explicitly. The call is cheap (property
|
||||
# read + dict comparison) and returns early when values already match.
|
||||
if self.registry_entry:
|
||||
self._update_suggested_precision()
|
||||
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
def _get_value_getter(self) -> Callable | None:
|
||||
|
|
@ -581,9 +565,10 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
self._last_extreme_interval = pi["interval"]
|
||||
break
|
||||
|
||||
# Return in configured display currency units with 2 decimals
|
||||
# Return in configured display currency units
|
||||
precision = get_display_precision(self.coordinator.config_entry)
|
||||
result = get_price_value(value, config_entry=self.coordinator.config_entry)
|
||||
return round(result, 2)
|
||||
return round(result, precision)
|
||||
|
||||
def _get_daily_aggregated_value(
|
||||
self,
|
||||
|
|
@ -659,9 +644,10 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
if value is None:
|
||||
return None
|
||||
|
||||
# Return in configured display currency units with 2 decimals
|
||||
# Return in configured display currency units
|
||||
precision = get_display_precision(self.coordinator.config_entry)
|
||||
result = get_price_value(value, config_entry=self.coordinator.config_entry)
|
||||
return round(result, 2)
|
||||
return round(result, precision)
|
||||
|
||||
def _translate_rating_level(self, level: str) -> str:
|
||||
"""Translate the rating level using custom translations, falling back to English or the raw value."""
|
||||
|
|
@ -710,6 +696,7 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
|
||||
# Get display unit factor (100 for minor, 1 for major)
|
||||
factor = get_display_unit_factor(self.coordinator.config_entry)
|
||||
precision = get_display_precision(self.coordinator.config_entry)
|
||||
|
||||
# Get user preference for display (mean or median)
|
||||
display_pref = self.coordinator.config_entry.options.get(
|
||||
|
|
@ -717,14 +704,14 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
)
|
||||
|
||||
# Store both values for attributes
|
||||
self.cached_data[f"next_avg_{hours}h_mean"] = round(mean_price * factor, 2)
|
||||
self.cached_data[f"next_avg_{hours}h_mean"] = round(mean_price * factor, precision)
|
||||
if median_price is not None:
|
||||
self.cached_data[f"next_avg_{hours}h_median"] = round(median_price * factor, 2)
|
||||
self.cached_data[f"next_avg_{hours}h_median"] = round(median_price * factor, precision)
|
||||
|
||||
# Return the value chosen for state display
|
||||
if display_pref == "median" and median_price is not None:
|
||||
return round(median_price * factor, 2)
|
||||
return round(mean_price * factor, 2) # "mean"
|
||||
return round(median_price * factor, precision)
|
||||
return round(mean_price * factor, precision) # "mean"
|
||||
|
||||
def _get_data_timestamp(self) -> datetime | None:
|
||||
"""
|
||||
|
|
@ -916,7 +903,7 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
return True
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | str | datetime | None: # noqa: PLR0912
|
||||
def native_value(self) -> float | str | datetime | None:
|
||||
"""Return the native value of the sensor."""
|
||||
try:
|
||||
if not self.coordinator.data or not self._value_getter:
|
||||
|
|
@ -1043,28 +1030,6 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
key = self.entity_description.key
|
||||
value = self.native_value
|
||||
|
||||
# Icon mapping for trend directions (5-level scale)
|
||||
trend_icons = {
|
||||
"strongly_rising": "mdi:chevron-double-up",
|
||||
"rising": "mdi:trending-up",
|
||||
"stable": "mdi:trending-neutral",
|
||||
"falling": "mdi:trending-down",
|
||||
"strongly_falling": "mdi:chevron-double-down",
|
||||
}
|
||||
|
||||
# Special handling for next_price_trend_change: Icon based on direction attribute
|
||||
if key == "next_price_trend_change":
|
||||
trend_change_attrs = self._trend_calculator.get_trend_change_attributes()
|
||||
if trend_change_attrs:
|
||||
direction = trend_change_attrs.get("direction")
|
||||
if isinstance(direction, str):
|
||||
return trend_icons.get(direction, "mdi:help-circle-outline")
|
||||
return "mdi:help-circle-outline"
|
||||
|
||||
# Special handling for current_price_trend: Icon based on current state value
|
||||
if key == "current_price_trend" and isinstance(value, str):
|
||||
return trend_icons.get(value, "mdi:help-circle-outline")
|
||||
|
||||
# Create callback for period active state check (used by timing sensors)
|
||||
period_is_active_callback = None
|
||||
if key.startswith("best_price_"):
|
||||
|
|
@ -1072,6 +1037,13 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
elif key.startswith("peak_price_"):
|
||||
period_is_active_callback = self._is_peak_price_period_active
|
||||
|
||||
# For next_price_trend_change, pass direction from cached attributes via context
|
||||
trend_change_direction = None
|
||||
if key == "next_price_trend_change":
|
||||
trend_change_attrs = self._trend_calculator.get_trend_change_attributes()
|
||||
if trend_change_attrs:
|
||||
trend_change_direction = trend_change_attrs.get("direction")
|
||||
|
||||
# Use centralized icon logic with context
|
||||
icon = get_dynamic_icon(
|
||||
key=key,
|
||||
|
|
@ -1080,24 +1052,37 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
coordinator_data=self.coordinator.data,
|
||||
period_is_active_callback=period_is_active_callback,
|
||||
time=self.coordinator.time,
|
||||
trend_change_direction=trend_change_direction,
|
||||
),
|
||||
)
|
||||
|
||||
# Fall back to static icon from entity description
|
||||
return icon or self.entity_description.icon
|
||||
|
||||
# Interval price sensors that show individual quarter-hour prices.
|
||||
# These get full data precision (subunit→2, base→4) as display default
|
||||
# because users rely on exact values for automations and dashboards.
|
||||
_INTERVAL_PRICE_KEYS = frozenset(
|
||||
{
|
||||
"current_interval_price",
|
||||
"current_interval_price_base",
|
||||
"next_interval_price",
|
||||
"previous_interval_price",
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
"""
|
||||
Return suggested display precision based on currency display mode.
|
||||
|
||||
For MONETARY sensors:
|
||||
- Current/Next Interval Price: Show exact price with higher precision
|
||||
- Base currency (€/kr): 4 decimals (e.g., 0.1234 €)
|
||||
- Subunit currency (ct/øre): 2 decimals (e.g., 12.34 ct)
|
||||
- All other price sensors:
|
||||
- Base currency (€/kr): 2 decimals (e.g., 0.12 €)
|
||||
- Subunit currency (ct/øre): 1 decimal (e.g., 12.5 ct)
|
||||
- Interval price sensors: Full precision (subunit→2, base→4)
|
||||
- Energy Dashboard sensor (current_interval_price_base): Always 4
|
||||
- All other price sensors: Reduced precision (subunit→1, base→2)
|
||||
|
||||
The actual state value retains full rounded precision (2 or 4 decimals),
|
||||
so users can increase display precision in the HA UI to see more detail.
|
||||
|
||||
For non-MONETARY sensors, use static value from entity description.
|
||||
"""
|
||||
|
|
@ -1105,23 +1090,16 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
if self.entity_description.device_class != SensorDeviceClass.MONETARY:
|
||||
return self.entity_description.suggested_display_precision
|
||||
|
||||
# Check display mode configuration
|
||||
display_mode = self.coordinator.config_entry.options.get(CONF_CURRENCY_DISPLAY_MODE, DISPLAY_MODE_BASE)
|
||||
|
||||
# Special case: Energy Dashboard sensor always shows base currency with 4 decimals
|
||||
# regardless of display mode (it's always in base currency by design)
|
||||
# Energy Dashboard sensor: always base currency, always 4 decimals
|
||||
if self.entity_description.key == "current_interval_price_base":
|
||||
return 4
|
||||
|
||||
# Special case: Current and Next interval price sensors get higher precision
|
||||
# to show exact prices as received from API
|
||||
if self.entity_description.key in ("current_interval_price", "next_interval_price"):
|
||||
# Major: 4 decimals (0.1234 €), Minor: 2 decimals (12.34 ct)
|
||||
return 4 if display_mode == DISPLAY_MODE_BASE else 2
|
||||
# Interval price sensors: full data precision (subunit→2, base→4)
|
||||
if self.entity_description.key in self._INTERVAL_PRICE_KEYS:
|
||||
return get_display_precision(self.coordinator.config_entry)
|
||||
|
||||
# All other sensors: Standard precision
|
||||
# Major: 2 decimals (0.12 €), Minor: 1 decimal (12.5 ct)
|
||||
return 2 if display_mode == DISPLAY_MODE_BASE else 1
|
||||
# All other MONETARY sensors: reduced precision (subunit→1, base→2)
|
||||
return get_display_precision(self.coordinator.config_entry) // 2
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||
|
|
|
|||
|
|
@ -16,12 +16,9 @@ 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.const import get_display_precision, get_display_unit_factor
|
||||
from custom_components.tibber_prices.utils.average import calculate_mean, calculate_median
|
||||
from custom_components.tibber_prices.utils.price import (
|
||||
aggregate_price_levels,
|
||||
aggregate_price_rating,
|
||||
)
|
||||
from custom_components.tibber_prices.utils.price import aggregate_price_levels, aggregate_price_rating
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
|
@ -51,7 +48,8 @@ def aggregate_average_data(
|
|||
median = calculate_median(prices)
|
||||
# Convert to display currency unit based on configuration
|
||||
factor = get_display_unit_factor(config_entry)
|
||||
return round(mean * factor, 2), round(median * factor, 2) if median is not None else None
|
||||
precision = get_display_precision(config_entry)
|
||||
return round(mean * factor, precision), round(median * factor, precision) if median is not None else None
|
||||
|
||||
|
||||
def aggregate_level_data(window_data: list[dict]) -> str | None:
|
||||
|
|
|
|||
Loading…
Reference in a new issue