From 91efeed90fc2de2f3621af7a4e5cb360e33fd2f1 Mon Sep 17 00:00:00 2001 From: Julian Pawlowski Date: Tue, 7 Apr 2026 13:44:22 +0000 Subject: [PATCH] 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"). --- .../tibber_prices/coordinator/constants.py | 2 ++ .../tibber_prices/sensor/attributes/trend.py | 3 +++ .../tibber_prices/sensor/core.py | 22 ++++++++++++++++++- .../tibber_prices/sensor/definitions.py | 11 ++++++++++ .../tibber_prices/sensor/value_getters.py | 1 + tests/test_sensor_timer_assignment.py | 2 +- 6 files changed, 39 insertions(+), 2 deletions(-) diff --git a/custom_components/tibber_prices/coordinator/constants.py b/custom_components/tibber_prices/coordinator/constants.py index b3b2f12..8fd6133 100644 --- a/custom_components/tibber_prices/coordinator/constants.py +++ b/custom_components/tibber_prices/coordinator/constants.py @@ -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", } ) diff --git a/custom_components/tibber_prices/sensor/attributes/trend.py b/custom_components/tibber_prices/sensor/attributes/trend.py index f02a107..d853606 100644 --- a/custom_components/tibber_prices/sensor/attributes/trend.py +++ b/custom_components/tibber_prices/sensor/attributes/trend.py @@ -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"]) diff --git a/custom_components/tibber_prices/sensor/core.py b/custom_components/tibber_prices/sensor/core.py index 030a7cd..d6f4256 100644 --- a/custom_components/tibber_prices/sensor/core.py +++ b/custom_components/tibber_prices/sensor/core.py @@ -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) diff --git a/custom_components/tibber_prices/sensor/definitions.py b/custom_components/tibber_prices/sensor/definitions.py index 8f437ec..f6ed421 100644 --- a/custom_components/tibber_prices/sensor/definitions.py +++ b/custom_components/tibber_prices/sensor/definitions.py @@ -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( diff --git a/custom_components/tibber_prices/sensor/value_getters.py b/custom_components/tibber_prices/sensor/value_getters.py index 4375bc8..d29d0f9 100644 --- a/custom_components/tibber_prices/sensor/value_getters.py +++ b/custom_components/tibber_prices/sensor/value_getters.py @@ -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), diff --git a/tests/test_sensor_timer_assignment.py b/tests/test_sensor_timer_assignment.py index 8215c9b..ffcf8f0 100644 --- a/tests/test_sensor_timer_assignment.py +++ b/tests/test_sensor_timer_assignment.py @@ -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)}"