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 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 (
|
from custom_components.tibber_prices.coordinator.helpers import (
|
||||||
get_intervals_for_day_offsets,
|
get_intervals_for_day_offsets,
|
||||||
)
|
)
|
||||||
|
|
@ -19,6 +22,43 @@ if TYPE_CHECKING:
|
||||||
from .helpers import add_alternate_average_attribute
|
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:
|
def _get_day_midnight_timestamp(key: str, *, time: TibberPricesTimeService) -> datetime:
|
||||||
"""Get midnight timestamp for a given day sensor key (returns datetime object)."""
|
"""Get midnight timestamp for a given day sensor key (returns datetime object)."""
|
||||||
# Determine which day based on sensor key
|
# Determine which day based on sensor key
|
||||||
|
|
@ -117,7 +157,7 @@ def add_statistics_attributes(
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Extreme value sensors - show when the extreme occurs
|
# Extreme value sensors - show when the extreme occurs + energy/tax breakdown
|
||||||
extreme_sensors = {
|
extreme_sensors = {
|
||||||
"lowest_price_today",
|
"lowest_price_today",
|
||||||
"highest_price_today",
|
"highest_price_today",
|
||||||
|
|
@ -125,10 +165,13 @@ def add_statistics_attributes(
|
||||||
"highest_price_tomorrow",
|
"highest_price_tomorrow",
|
||||||
}
|
}
|
||||||
if key in extreme_sensors:
|
if key in extreme_sensors:
|
||||||
if cached_data.get("last_extreme_interval"):
|
extreme_interval = cached_data.get("last_extreme_interval")
|
||||||
extreme_starts_at = cached_data["last_extreme_interval"].get("startsAt")
|
if extreme_interval:
|
||||||
|
extreme_starts_at = extreme_interval.get("startsAt")
|
||||||
if extreme_starts_at:
|
if extreme_starts_at:
|
||||||
attributes["timestamp"] = 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
|
return
|
||||||
|
|
||||||
# Daily average sensors - show midnight to indicate whole day + add alternate value
|
# 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")
|
key, # base_key = key itself ("average_price_today" or "average_price_tomorrow")
|
||||||
config_entry=config_entry,
|
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
|
return
|
||||||
|
|
||||||
# Daily aggregated level/rating sensors - show midnight to indicate whole day
|
# 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 (
|
from custom_components.tibber_prices.const import (
|
||||||
PRICE_LEVEL_MAPPING,
|
PRICE_LEVEL_MAPPING,
|
||||||
PRICE_RATING_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.entity_utils import add_icon_color_attribute
|
||||||
from custom_components.tibber_prices.utils.price import find_price_data_for_interval
|
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
|
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
|
def add_current_interval_price_attributes( # noqa: PLR0913
|
||||||
attributes: dict,
|
attributes: dict,
|
||||||
key: str,
|
key: str,
|
||||||
|
|
@ -126,6 +159,9 @@ def add_current_interval_price_attributes( # noqa: PLR0913
|
||||||
if interval_data and "level" in interval_data:
|
if interval_data and "level" in interval_data:
|
||||||
level = interval_data["level"]
|
level = interval_data["level"]
|
||||||
add_icon_color_attribute(attributes, key="price_level", state_value=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"]:
|
elif key in ["current_hour_average_price", "next_hour_average_price"]:
|
||||||
# For hour-based price sensors, get level from cached_data
|
# For hour-based price sensors, get level from cached_data
|
||||||
level = cached_data.get("rolling_hour_level")
|
level = cached_data.get("rolling_hour_level")
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ from custom_components.tibber_prices.sensor.helpers import (
|
||||||
aggregate_level_data,
|
aggregate_level_data,
|
||||||
aggregate_rating_data,
|
aggregate_rating_data,
|
||||||
)
|
)
|
||||||
|
from custom_components.tibber_prices.utils.average import calculate_median
|
||||||
|
|
||||||
from .base import TibberPricesBaseCalculator
|
from .base import TibberPricesBaseCalculator
|
||||||
|
|
||||||
|
|
@ -44,6 +45,10 @@ class TibberPricesDailyStatCalculator(TibberPricesBaseCalculator):
|
||||||
"""
|
"""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._last_extreme_interval: dict | None = None
|
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(
|
def get_daily_stat_value(
|
||||||
self,
|
self,
|
||||||
|
|
@ -107,6 +112,8 @@ class TibberPricesDailyStatCalculator(TibberPricesBaseCalculator):
|
||||||
# Store the interval (for avg, use first interval as reference)
|
# Store the interval (for avg, use first interval as reference)
|
||||||
if price_intervals:
|
if price_intervals:
|
||||||
self._last_extreme_interval = price_intervals[0]["interval"]
|
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
|
# Convert to display currency units based on config
|
||||||
avg_result = round(get_price_value(value, config_entry=self.coordinator.config_entry), 2)
|
avg_result = round(get_price_value(value, config_entry=self.coordinator.config_entry), 2)
|
||||||
median_result = (
|
median_result = (
|
||||||
|
|
@ -198,3 +205,34 @@ class TibberPricesDailyStatCalculator(TibberPricesBaseCalculator):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self._last_extreme_interval
|
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_%",
|
"second_half_12h_diff_from_current_%",
|
||||||
# Static/Rarely Changing
|
# Static/Rarely Changing
|
||||||
"tomorrow_expected_after",
|
"tomorrow_expected_after",
|
||||||
|
"energy_price",
|
||||||
|
"energy_price_mean",
|
||||||
|
"energy_price_median",
|
||||||
|
"tax",
|
||||||
|
"tax_mean",
|
||||||
|
"tax_median",
|
||||||
"level_value",
|
"level_value",
|
||||||
"rating_value",
|
"rating_value",
|
||||||
"level_id",
|
"level_id",
|
||||||
|
|
@ -1145,6 +1151,7 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
||||||
"trend_change_attributes": self._trend_calculator.get_trend_change_attributes(),
|
"trend_change_attributes": self._trend_calculator.get_trend_change_attributes(),
|
||||||
"volatility_attributes": self._volatility_calculator.get_volatility_attributes(),
|
"volatility_attributes": self._volatility_calculator.get_volatility_attributes(),
|
||||||
"last_extreme_interval": self._daily_stat_calculator.get_last_extreme_interval(),
|
"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_price_level": self._interval_calculator.get_last_price_level(),
|
||||||
"last_rating_difference": self._interval_calculator.get_last_rating_difference(),
|
"last_rating_difference": self._interval_calculator.get_last_rating_difference(),
|
||||||
"last_rating_level": self._interval_calculator.get_last_rating_level(),
|
"last_rating_level": self._interval_calculator.get_last_rating_level(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue