fix(binary_sensor): remove 6-hour lookahead limit for period icons

Simplified _has_future_periods() to check for ANY future periods instead
of limiting to 6-hour window. This ensures icons show 'waiting' state
whenever periods are scheduled, not just within artificial time limit.

Also added pragmatic fallback in timing calculator _find_next_period():
when skip_current=True but only one future period exists, return it
anyway instead of showing 'unknown'. This prevents timing sensors from
showing unknown during active periods.

Changes:
- binary_sensor/definitions.py: Removed PERIOD_LOOKAHEAD_HOURS constant
- binary_sensor/core.py: Simplified _has_future_periods() logic
- sensor/calculators/timing.py: Added pragmatic fallback for single period

Impact: Better user experience - icons always show future periods, timing
sensors show values even during edge cases.
This commit is contained in:
Julian Pawlowski 2025-11-22 13:04:17 +00:00
parent f373c01fbb
commit 2d0febdab3
3 changed files with 15 additions and 24 deletions

View file

@ -21,7 +21,6 @@ from .attributes import (
get_price_intervals_attributes, get_price_intervals_attributes,
get_tomorrow_data_available_attributes, get_tomorrow_data_available_attributes,
) )
from .definitions import PERIOD_LOOKAHEAD_HOURS
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable from collections.abc import Callable
@ -46,11 +45,6 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{entity_description.key}" self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{entity_description.key}"
self._state_getter: Callable | None = self._get_value_getter() self._state_getter: Callable | None = self._get_value_getter()
self._time_sensitive_remove_listener: Callable | None = None self._time_sensitive_remove_listener: Callable | None = None
self._lifecycle_remove_listener: Callable | None = None
# Register for lifecycle push updates if this sensor depends on connection state
if entity_description.key in ("connection", "tomorrow_data_available"):
self._lifecycle_remove_listener = coordinator.register_lifecycle_callback(self.async_write_ha_state)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""When entity is added to hass.""" """When entity is added to hass."""
@ -71,11 +65,6 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
self._time_sensitive_remove_listener() self._time_sensitive_remove_listener()
self._time_sensitive_remove_listener = None self._time_sensitive_remove_listener = None
# Remove lifecycle listener if registered
if self._lifecycle_remove_listener:
self._lifecycle_remove_listener()
self._lifecycle_remove_listener = None
@callback @callback
def _handle_time_sensitive_update(self, time_service: TibberPricesTimeService) -> None: def _handle_time_sensitive_update(self, time_service: TibberPricesTimeService) -> None:
""" """
@ -270,10 +259,10 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
def _has_future_periods(self) -> bool: def _has_future_periods(self) -> bool:
""" """
Check if there are periods starting within the next 6 hours. Check if there are any future periods.
Returns True if any period starts between now and PERIOD_LOOKAHEAD_HOURS from now. Returns True if any period starts in the future (no time limit).
This provides a practical planning horizon instead of hard midnight cutoff. This ensures icons show "waiting" state whenever periods are scheduled.
""" """
attrs = self._get_sensor_attributes() attrs = self._get_sensor_attributes()
if not attrs or "periods" not in attrs: if not attrs or "periods" not in attrs:
@ -282,15 +271,15 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
time = self.coordinator.time time = self.coordinator.time
periods = attrs.get("periods", []) periods = attrs.get("periods", [])
# Check if any period starts within the look-ahead window # Check if any period starts in the future (no time limit)
for period in periods: for period in periods:
start_str = period.get("start") start_str = period.get("start")
if start_str: if start_str:
# Already datetime object (periods come from coordinator.data) # Already datetime object (periods come from coordinator.data)
start_time = start_str if not isinstance(start_str, str) else time.parse_datetime(start_str) start_time = start_str if not isinstance(start_str, str) else time.parse_datetime(start_str)
# Period starts in the future but within our horizon # Period starts in the future
if start_time and time.is_time_within_horizon(start_time, hours=PERIOD_LOOKAHEAD_HOURS): if start_time and time.is_in_future(start_time):
return True return True
return False return False

View file

@ -8,9 +8,8 @@ from homeassistant.components.binary_sensor import (
) )
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
# Look-ahead window for future period detection (hours) # Period lookahead removed - icons show "waiting" state if ANY future periods exist
# Icons will show "waiting" state if a period starts within this window # No artificial time limit - show all periods until midnight
PERIOD_LOOKAHEAD_HOURS = 6
ENTITY_DESCRIPTIONS = ( ENTITY_DESCRIPTIONS = (
BinarySensorEntityDescription( BinarySensorEntityDescription(

View file

@ -158,7 +158,8 @@ class TibberPricesTimingCalculator(TibberPricesBaseCalculator):
Args: Args:
periods: List of period dictionaries periods: List of period dictionaries
skip_current: If True, skip the first future period (to get next-next) skip_current: If True, try to skip the first future period (to get next-next)
If only one future period exists, return it anyway (pragmatic fallback)
Returns: Returns:
Next period dict or None if no future periods Next period dict or None if no future periods
@ -173,11 +174,13 @@ class TibberPricesTimingCalculator(TibberPricesBaseCalculator):
# Sort by start time to ensure correct order # Sort by start time to ensure correct order
future_periods.sort(key=lambda p: p["start"]) future_periods.sort(key=lambda p: p["start"])
# Return second period if skip_current=True (next-next), otherwise first (next) # If skip_current requested and we have multiple periods, return second
# If only one period left, return it anyway (pragmatic: better than showing unknown)
if skip_current and len(future_periods) > 1: if skip_current and len(future_periods) > 1:
return future_periods[1] return future_periods[1]
if not skip_current and future_periods:
return future_periods[0] # Default: return first future period
return future_periods[0] if future_periods else None
return None return None