feat(sensors): add trend_change_in_minutes countdown sensor

New duration sensor showing time until next price trend change as hours
(e.g., 2.25 h). Registered in MINUTE_UPDATE_ENTITY_KEYS for per-minute
updates. Shares cached attributes with next_price_trend_change timestamp
sensor.

Added trend attributes to _unrecorded_attributes (threshold/volatility/diff
attributes excluded from recorder). Updated timer group size test expectation
from 6 to 7.

Impact: Users can display a live countdown to the next trend change on
dashboards and use it in automations (e.g., "if < 0.25 h, prepare").
This commit is contained in:
Julian Pawlowski 2026-04-07 13:44:22 +00:00
parent 90e2c3c1dc
commit 91efeed90f
6 changed files with 39 additions and 2 deletions

View file

@ -108,5 +108,7 @@ MINUTE_UPDATE_ENTITY_KEYS = frozenset(
"peak_price_remaining_minutes",
"peak_price_progress",
"peak_price_next_in_minutes",
# Trend change countdown sensor (needs minute updates)
"trend_change_in_minutes",
}
)

View file

@ -37,3 +37,6 @@ def _add_cached_trend_attributes(attributes: dict, key: str, cached_data: dict)
# Add cached attributes (timestamp already set by platform)
# State contains the timestamp of the trend change itself
attributes.update(cached_data["trend_change_attributes"])
elif key == "trend_change_in_minutes" and cached_data.get("trend_change_attributes"):
# Duration sensor shares same cached attributes as the timestamp sensor
attributes.update(cached_data["trend_change_attributes"])

View file

@ -120,6 +120,22 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
"cache_validity",
"data_completeness",
"data_status",
"threshold_rising_%",
"threshold_rising_strongly_%",
"threshold_falling_%",
"threshold_falling_strongly_%",
"volatility_factor",
"interval_count",
"price_direction_since",
"price_now",
"trend_diff_%",
# Dynamic keys for second_half diff (all trend hour variants)
"second_half_3h_diff_from_current_%",
"second_half_4h_diff_from_current_%",
"second_half_5h_diff_from_current_%",
"second_half_6h_diff_from_current_%",
"second_half_8h_diff_from_current_%",
"second_half_12h_diff_from_current_%",
# Static/Rarely Changing
"tomorrow_expected_after",
"level_value",
@ -313,7 +329,11 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
if self.entity_description.key.startswith("price_trend_"):
self._trend_calculator.clear_trend_cache()
# Clear trend calculation cache for trend sensors
elif self.entity_description.key in ("current_price_trend", "next_price_trend_change"):
elif self.entity_description.key in (
"current_price_trend",
"next_price_trend_change",
"trend_change_in_minutes",
):
self._trend_calculator.clear_calculation_cache()
# For lifecycle sensor: Only write state if it actually changed (state-change filter)

View file

@ -517,6 +517,17 @@ FUTURE_TREND_SENSORS = (
state_class=None, # Timestamp: no statistics
entity_registry_enabled_default=True,
),
# Trend change countdown sensor (how long until trend changes?)
SensorEntityDescription(
key="trend_change_in_minutes",
translation_key="trend_change_in_minutes",
icon="mdi:timer-outline",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.HOURS,
state_class=None, # Countdown timer: no statistics
suggested_display_precision=2,
entity_registry_enabled_default=True,
),
# Price trend forecast sensors (will prices be higher/lower in X hours?)
# Default enabled: 1h-5h
SensorEntityDescription(

View file

@ -205,6 +205,7 @@ def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parame
# Current and next trend change sensors
"current_price_trend": trend_calculator.get_current_trend_value,
"next_price_trend_change": trend_calculator.get_next_trend_change_value,
"trend_change_in_minutes": lambda: _minutes_to_hours(trend_calculator.get_trend_change_in_minutes_value()),
# Price trend sensors
"price_trend_1h": lambda: trend_calculator.get_price_trend_value(hours=1),
"price_trend_2h": lambda: trend_calculator.get_price_trend_value(hours=2),

View file

@ -448,7 +448,7 @@ def test_timer_group_sizes() -> None:
"""
# As of Nov 2025
expected_time_sensitive_min = 40 # At least 40 sensors
expected_minute_update = 6 # Exactly 6 timing sensors
expected_minute_update = 7 # Exactly 7 timing sensors
assert len(TIME_SENSITIVE_ENTITY_KEYS) >= expected_time_sensitive_min, (
f"Expected at least {expected_time_sensitive_min} TIME_SENSITIVE sensors, got {len(TIME_SENSITIVE_ENTITY_KEYS)}"