diff --git a/custom_components/daily_schedule/binary_sensor.py b/custom_components/daily_schedule/binary_sensor.py index fa3bb0f..66c1417 100644 --- a/custom_components/daily_schedule/binary_sensor.py +++ b/custom_components/daily_schedule/binary_sensor.py @@ -14,11 +14,13 @@ from .const import ( ATTR_NEXT_TOGGLE, + ATTR_NEXT_TOGGLES, CONF_DISABLED, CONF_FROM, CONF_SCHEDULE, CONF_TO, CONF_UTC, + NEXT_TOGGLES_COUNT, SERVICE_SET, ) from .schedule import Schedule @@ -69,6 +71,9 @@ class DailyScheduleSensor(BinarySensorEntity): _attr_has_entity_name = True _attr_should_poll = False _attr_icon = "mdi:timetable" + _entity_component_unrecorded_attributes = frozenset( + {ATTR_NEXT_TOGGLE, ATTR_NEXT_TOGGLES} + ) def __init__(self, config_entry: ConfigEntry) -> None: """Initialize object with defaults.""" @@ -115,8 +120,10 @@ async def async_set(self, schedule: list[dict[str, Any]]) -> None: def _update_state(self, _: datetime.datetime | None = None) -> None: """Update the state and schedule next update.""" self._clean_up_listener() - next_update = self._schedule.next_update(self._now()) + next_toggles = self._schedule.next_updates(self._now(), NEXT_TOGGLES_COUNT) + next_update = next_toggles[0] if len(next_toggles) > 0 else None self._attr_extra_state_attributes[ATTR_NEXT_TOGGLE] = next_update + self._attr_extra_state_attributes[ATTR_NEXT_TOGGLES] = next_toggles self.async_write_ha_state() if next_update: self._unsub_update = event_helper.async_track_point_in_time( diff --git a/custom_components/daily_schedule/const.py b/custom_components/daily_schedule/const.py index 6bf1d4b..2f14e0b 100644 --- a/custom_components/daily_schedule/const.py +++ b/custom_components/daily_schedule/const.py @@ -15,5 +15,7 @@ CONF_UTC: Final = "utc" ATTR_NEXT_TOGGLE: Final = "next_toggle" +ATTR_NEXT_TOGGLES: Final = "next_toggles" +NEXT_TOGGLES_COUNT: Final = 4 SERVICE_SET: Final = "set" diff --git a/custom_components/daily_schedule/schedule.py b/custom_components/daily_schedule/schedule.py index d6765bc..cdd2274 100644 --- a/custom_components/daily_schedule/schedule.py +++ b/custom_components/daily_schedule/schedule.py @@ -143,3 +143,15 @@ def next_update(self, date: datetime.datetime) -> datetime.datetime | None: return datetime.datetime.combine( today + datetime.timedelta(days=1), timestamps[0], tzinfo=date.tzinfo ) + + def next_updates( + self, date: datetime.datetime, count: int + ) -> list[datetime.datetime]: + """Get list of future updates.""" + updates = [] + if (update := self.next_update(date)) is not None: + while len(updates) < count: + updates.append(update) + if (update := self.next_update(update)) is None: + break + return updates diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py index 18eadfb..db41174 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py @@ -16,6 +16,7 @@ from custom_components.daily_schedule.const import ( ATTR_NEXT_TOGGLE, + ATTR_NEXT_TOGGLES, CONF_FROM, CONF_SCHEDULE, CONF_TO, @@ -221,6 +222,12 @@ async def test_next_update( state = hass.states.get(f"{Platform.BINARY_SENSOR}.test1") assert state assert state.attributes[ATTR_NEXT_TOGGLE] == in_5_minutes + assert state.attributes[ATTR_NEXT_TOGGLES] == [ + in_5_minutes, + previous_5_minutes + datetime.timedelta(days=1), + in_5_minutes + datetime.timedelta(days=1), + previous_5_minutes + datetime.timedelta(days=2), + ] # After all ranges. await setup_entity( @@ -242,6 +249,12 @@ async def test_next_update( state = hass.states.get(f"{Platform.BINARY_SENSOR}.test2") assert state assert state.attributes[ATTR_NEXT_TOGGLE] == expected_next_update + assert state.attributes[ATTR_NEXT_TOGGLES] == [ + expected_next_update, + previous_5_minutes + datetime.timedelta(days=1), + expected_next_update + datetime.timedelta(days=1), + previous_5_minutes + datetime.timedelta(days=2), + ] # Before any range. await setup_entity( @@ -260,6 +273,12 @@ async def test_next_update( next_update = async_track_point_in_time.call_args[0][2] assert next_update == in_5_minutes assert state.attributes[ATTR_NEXT_TOGGLE] == in_5_minutes + assert state.attributes[ATTR_NEXT_TOGGLES] == [ + in_5_minutes, + in_10_minutes, + in_5_minutes + datetime.timedelta(days=1), + in_10_minutes + datetime.timedelta(days=1), + ] await async_cleanup(hass) @@ -340,8 +359,8 @@ async def test_utc( ) -> None: """Test utc schedule.""" utc_time = datetime.datetime(2023, 5, 30, 12, tzinfo=pytz.utc) # 12pm - local_time = utc_time.astimezone(pytz.timezone("US/Eastern")) # 7am - offset = utc_time.timestamp() - local_time.replace(tzinfo=None).timestamp() # 5h + local_time = utc_time.astimezone(pytz.timezone("US/Pacific")) # 4am + offset = utc_time.timestamp() - local_time.replace(tzinfo=None).timestamp() # 8h freezer.move_to(local_time) entity_id = f"{Platform.BINARY_SENSOR}.my_test" await setup_entity( @@ -359,5 +378,31 @@ async def test_utc( assert state assert state.state == STATE_ON if utc else STATE_OFF next_toggle_timestamp = state.attributes[ATTR_NEXT_TOGGLE].timestamp() - assert next_toggle_timestamp == utc_time.timestamp() + 1 if utc else offset + assert next_toggle_timestamp == local_time.timestamp() + (1 if utc else offset) + if utc: + assert state.attributes[ATTR_NEXT_TOGGLES][0].timestamp() == ( + local_time.timestamp() + 1 + ) + assert state.attributes[ATTR_NEXT_TOGGLES][1].timestamp() == ( + (local_time + datetime.timedelta(days=1)).timestamp() + ) + assert state.attributes[ATTR_NEXT_TOGGLES][2].timestamp() == ( + (local_time + datetime.timedelta(days=1)).timestamp() + 1 + ) + assert state.attributes[ATTR_NEXT_TOGGLES][3].timestamp() == ( + (local_time + datetime.timedelta(days=2)).timestamp() + ) + else: + assert state.attributes[ATTR_NEXT_TOGGLES][0].timestamp() == ( + local_time.timestamp() + offset + ) + assert state.attributes[ATTR_NEXT_TOGGLES][1].timestamp() == ( + local_time.timestamp() + offset + 1 + ) + assert state.attributes[ATTR_NEXT_TOGGLES][2].timestamp() == ( + (local_time + datetime.timedelta(days=1)).timestamp() + offset + ) + assert state.attributes[ATTR_NEXT_TOGGLES][3].timestamp() == ( + (local_time + datetime.timedelta(days=1)).timestamp() + offset + 1 + ) await async_cleanup(hass) diff --git a/tests/test_schedule.py b/tests/test_schedule.py index a82eff0..e81306e 100644 --- a/tests/test_schedule.py +++ b/tests/test_schedule.py @@ -249,3 +249,26 @@ def test_next_update( if next_update_sec_offset is not None else None ) + + +def test_next_updates() -> None: + """Test next updates.""" + now = datetime.datetime.fromisoformat("2000-01-01") + assert Schedule( + [ + { + CONF_FROM: "01:00", + CONF_TO: "02:00", + }, + { + CONF_FROM: "03:00", + CONF_TO: "04:00", + }, + ] + ).next_updates(now, 5) == [ + now + datetime.timedelta(hours=1), + now + datetime.timedelta(hours=2), + now + datetime.timedelta(hours=3), + now + datetime.timedelta(hours=4), + now + datetime.timedelta(days=1, hours=1), + ]