mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
feat(sensors): expose energy/tax breakdown as sensor attributes
Add energy_price and tax attributes to interval and daily stat sensors: - Interval sensors (current/next/previous): energy_price and tax from the specific 15-minute interval - Daily min/max sensors: energy_price and tax from the extreme interval - Daily average sensors: energy_price_mean, energy_price_median, tax_mean, tax_median — matching the existing mean/median pattern used for the main price attribute Calculator caches both mean and median for energy/tax using calculate_median() from utils/average. All new attributes are excluded from Recorder to prevent database bloat. Impact: Users can see price composition (spot price vs. taxes) on all major price sensors. Enables solar feed-in and net metering automations based on raw energy prices.
This commit is contained in:
parent
f5dcf04aab
commit
edabb49309
4 changed files with 130 additions and 4 deletions
|
|
@ -4,7 +4,10 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from custom_components.tibber_prices.const import PRICE_RATING_MAPPING
|
||||
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,
|
||||
)
|
||||
|
|
@ -19,6 +22,43 @@ if TYPE_CHECKING:
|
|||
from .helpers import add_alternate_average_attribute
|
||||
|
||||
|
||||
def _add_energy_tax_from_interval(
|
||||
attributes: dict,
|
||||
interval_data: dict,
|
||||
*,
|
||||
config_entry: TibberPricesConfigEntry,
|
||||
) -> None:
|
||||
"""Add energy_price and tax from a single interval dict."""
|
||||
factor = get_display_unit_factor(config_entry)
|
||||
energy = interval_data.get("energy")
|
||||
if energy is not None:
|
||||
attributes["energy_price"] = round(float(energy) * factor, 2)
|
||||
tax = interval_data.get("tax")
|
||||
if tax is not None:
|
||||
attributes["tax"] = round(float(tax) * factor, 2)
|
||||
|
||||
|
||||
def _add_energy_tax_averages_from_cache(
|
||||
attributes: dict,
|
||||
cached_data: dict,
|
||||
*,
|
||||
config_entry: TibberPricesConfigEntry,
|
||||
) -> None:
|
||||
"""Add cached mean/median energy_price and tax values."""
|
||||
energy_mean, energy_median, tax_mean, tax_median = cached_data.get(
|
||||
"last_energy_tax_averages", (None, None, None, None)
|
||||
)
|
||||
factor = get_display_unit_factor(config_entry)
|
||||
if energy_mean is not None:
|
||||
attributes["energy_price_mean"] = round(float(energy_mean) * factor, 2)
|
||||
if energy_median is not None:
|
||||
attributes["energy_price_median"] = round(float(energy_median) * factor, 2)
|
||||
if tax_mean is not None:
|
||||
attributes["tax_mean"] = round(float(tax_mean) * factor, 2)
|
||||
if tax_median is not None:
|
||||
attributes["tax_median"] = round(float(tax_median) * factor, 2)
|
||||
|
||||
|
||||
def _get_day_midnight_timestamp(key: str, *, time: TibberPricesTimeService) -> datetime:
|
||||
"""Get midnight timestamp for a given day sensor key (returns datetime object)."""
|
||||
# Determine which day based on sensor key
|
||||
|
|
@ -117,7 +157,7 @@ def add_statistics_attributes(
|
|||
)
|
||||
return
|
||||
|
||||
# Extreme value sensors - show when the extreme occurs
|
||||
# Extreme value sensors - show when the extreme occurs + energy/tax breakdown
|
||||
extreme_sensors = {
|
||||
"lowest_price_today",
|
||||
"highest_price_today",
|
||||
|
|
@ -125,10 +165,13 @@ def add_statistics_attributes(
|
|||
"highest_price_tomorrow",
|
||||
}
|
||||
if key in extreme_sensors:
|
||||
if cached_data.get("last_extreme_interval"):
|
||||
extreme_starts_at = cached_data["last_extreme_interval"].get("startsAt")
|
||||
extreme_interval = cached_data.get("last_extreme_interval")
|
||||
if extreme_interval:
|
||||
extreme_starts_at = extreme_interval.get("startsAt")
|
||||
if extreme_starts_at:
|
||||
attributes["timestamp"] = extreme_starts_at
|
||||
# Add energy_price and tax from the extreme interval
|
||||
_add_energy_tax_from_interval(attributes, extreme_interval, config_entry=config_entry)
|
||||
return
|
||||
|
||||
# Daily average sensors - show midnight to indicate whole day + add alternate value
|
||||
|
|
@ -142,6 +185,8 @@ def add_statistics_attributes(
|
|||
key, # base_key = key itself ("average_price_today" or "average_price_tomorrow")
|
||||
config_entry=config_entry,
|
||||
)
|
||||
# Add energy/tax averages from cached calculator data
|
||||
_add_energy_tax_averages_from_cache(attributes, cached_data, config_entry=config_entry)
|
||||
return
|
||||
|
||||
# Daily aggregated level/rating sensors - show midnight to indicate whole day
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any
|
|||
from custom_components.tibber_prices.const import (
|
||||
PRICE_LEVEL_MAPPING,
|
||||
PRICE_RATING_MAPPING,
|
||||
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
|
||||
|
|
@ -89,6 +90,38 @@ def _get_interval_data_for_attributes(
|
|||
return None
|
||||
|
||||
|
||||
def _add_energy_tax_attributes(
|
||||
attributes: dict,
|
||||
interval_data: dict | None,
|
||||
*,
|
||||
config_entry: TibberPricesConfigEntry,
|
||||
) -> None:
|
||||
"""
|
||||
Add energy_price and tax attributes from interval data.
|
||||
|
||||
The API provides `energy` (raw spot price) and `tax` (tax component) in base
|
||||
currency. These are converted to display units using the same factor as `total`.
|
||||
|
||||
Args:
|
||||
attributes: Dictionary to add attributes to
|
||||
interval_data: Price interval data dict (may contain energy/tax fields)
|
||||
config_entry: Config entry for display unit preference
|
||||
|
||||
"""
|
||||
if not interval_data:
|
||||
return
|
||||
|
||||
factor = get_display_unit_factor(config_entry)
|
||||
|
||||
energy = interval_data.get("energy")
|
||||
if energy is not None:
|
||||
attributes["energy_price"] = round(float(energy) * factor, 2)
|
||||
|
||||
tax = interval_data.get("tax")
|
||||
if tax is not None:
|
||||
attributes["tax"] = round(float(tax) * factor, 2)
|
||||
|
||||
|
||||
def add_current_interval_price_attributes( # noqa: PLR0913
|
||||
attributes: dict,
|
||||
key: str,
|
||||
|
|
@ -126,6 +159,9 @@ def add_current_interval_price_attributes( # noqa: PLR0913
|
|||
if interval_data and "level" in interval_data:
|
||||
level = interval_data["level"]
|
||||
add_icon_color_attribute(attributes, key="price_level", state_value=level)
|
||||
|
||||
# Add energy_price and tax attributes from raw API data
|
||||
_add_energy_tax_attributes(attributes, interval_data, config_entry=config_entry)
|
||||
elif key in ["current_hour_average_price", "next_hour_average_price"]:
|
||||
# For hour-based price sensors, get level from cached_data
|
||||
level = cached_data.get("rolling_hour_level")
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ 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
|
||||
|
||||
|
|
@ -44,6 +45,10 @@ class TibberPricesDailyStatCalculator(TibberPricesBaseCalculator):
|
|||
"""
|
||||
super().__init__(coordinator)
|
||||
self._last_extreme_interval: dict | None = None
|
||||
self._last_energy_mean: float | None = None
|
||||
self._last_energy_median: float | None = None
|
||||
self._last_tax_mean: float | None = None
|
||||
self._last_tax_median: float | None = None
|
||||
|
||||
def get_daily_stat_value(
|
||||
self,
|
||||
|
|
@ -107,6 +112,8 @@ class TibberPricesDailyStatCalculator(TibberPricesBaseCalculator):
|
|||
# Store the interval (for avg, use first interval as reference)
|
||||
if price_intervals:
|
||||
self._last_extreme_interval = price_intervals[0]["interval"]
|
||||
# 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)
|
||||
median_result = (
|
||||
|
|
@ -198,3 +205,34 @@ class TibberPricesDailyStatCalculator(TibberPricesBaseCalculator):
|
|||
|
||||
"""
|
||||
return self._last_extreme_interval
|
||||
|
||||
def get_last_energy_tax_averages(
|
||||
self,
|
||||
) -> tuple[float | None, float | None, float | None, float | None]:
|
||||
"""
|
||||
Get cached mean and median energy and tax values from last average calculation.
|
||||
|
||||
Returns:
|
||||
Tuple of (energy_mean, energy_median, tax_mean, tax_median) in base currency,
|
||||
or (None, None, None, None).
|
||||
|
||||
"""
|
||||
return self._last_energy_mean, self._last_energy_median, self._last_tax_mean, self._last_tax_median
|
||||
|
||||
def _cache_energy_tax_averages(self, price_intervals: list[dict]) -> None:
|
||||
"""Compute and cache energy/tax mean and median from price intervals."""
|
||||
energy_prices: list[float] = []
|
||||
tax_prices: list[float] = []
|
||||
for pi in price_intervals:
|
||||
interval = pi["interval"]
|
||||
energy = interval.get("energy")
|
||||
if energy is not None:
|
||||
energy_prices.append(float(energy))
|
||||
tax = interval.get("tax")
|
||||
if tax is not None:
|
||||
tax_prices.append(float(tax))
|
||||
|
||||
self._last_energy_mean = sum(energy_prices) / len(energy_prices) if energy_prices else None
|
||||
self._last_energy_median = calculate_median(energy_prices) if energy_prices else None
|
||||
self._last_tax_mean = sum(tax_prices) / len(tax_prices) if tax_prices else None
|
||||
self._last_tax_median = calculate_median(tax_prices) if tax_prices else None
|
||||
|
|
|
|||
|
|
@ -138,6 +138,12 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
"second_half_12h_diff_from_current_%",
|
||||
# Static/Rarely Changing
|
||||
"tomorrow_expected_after",
|
||||
"energy_price",
|
||||
"energy_price_mean",
|
||||
"energy_price_median",
|
||||
"tax",
|
||||
"tax_mean",
|
||||
"tax_median",
|
||||
"level_value",
|
||||
"rating_value",
|
||||
"level_id",
|
||||
|
|
@ -1145,6 +1151,7 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
"trend_change_attributes": self._trend_calculator.get_trend_change_attributes(),
|
||||
"volatility_attributes": self._volatility_calculator.get_volatility_attributes(),
|
||||
"last_extreme_interval": self._daily_stat_calculator.get_last_extreme_interval(),
|
||||
"last_energy_tax_averages": self._daily_stat_calculator.get_last_energy_tax_averages(),
|
||||
"last_price_level": self._interval_calculator.get_last_price_level(),
|
||||
"last_rating_difference": self._interval_calculator.get_last_rating_difference(),
|
||||
"last_rating_level": self._interval_calculator.get_last_rating_level(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue