|
|
@@ -38,6 +38,8 @@ class CoordinatorData:
|
|
|
next: ReservationData
|
|
|
current_last_refresh: datetime | None
|
|
|
next_last_refresh: datetime | None
|
|
|
+ current_last_updated: datetime | None = None
|
|
|
+ next_last_updated: datetime | None = None
|
|
|
override_active: bool = False
|
|
|
api_status: ApiStatusData | None = None
|
|
|
|
|
|
@@ -58,14 +60,45 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
self._room_state_switch: Any = None # injected by switch.py after platform setup
|
|
|
self._current_last_refresh: datetime | None = None
|
|
|
self._next_last_refresh: datetime | None = None
|
|
|
+ self._current_last_updated: datetime | None = None
|
|
|
+ self._next_last_updated: datetime | None = None
|
|
|
self._override: OverrideData | None = None
|
|
|
|
|
|
async def async_setup(self) -> None:
|
|
|
await self.store.async_load()
|
|
|
+ self._current_last_updated = self.store.get_last_updated(STORE_CURRENT)
|
|
|
+ self._next_last_updated = self.store.get_last_updated(STORE_NEXT)
|
|
|
|
|
|
def inject_room_state_switch(self, switch: Any) -> None:
|
|
|
self._room_state_switch = switch
|
|
|
|
|
|
+ async def _async_note_displayed_change(
|
|
|
+ self,
|
|
|
+ new_current: ReservationData,
|
|
|
+ new_next: ReservationData,
|
|
|
+ now: datetime,
|
|
|
+ ) -> None:
|
|
|
+ """Update last_updated timestamps when the displayed (applied) state changes.
|
|
|
+
|
|
|
+ On first call (self.data is None), uses _empty_reservation() as the baseline
|
|
|
+ unless a persisted timestamp already exists — in that case the persisted value
|
|
|
+ is trusted and no update is issued, avoiding a false 'changed now' on startup.
|
|
|
+ """
|
|
|
+ empty = self._empty_reservation()
|
|
|
+ if self.data is not None:
|
|
|
+ old_current: ReservationData = self.data.current
|
|
|
+ old_next: ReservationData = self.data.next
|
|
|
+ else:
|
|
|
+ old_current = empty if self._current_last_updated is None else new_current
|
|
|
+ old_next = empty if self._next_last_updated is None else new_next
|
|
|
+
|
|
|
+ if old_current != new_current:
|
|
|
+ self._current_last_updated = now
|
|
|
+ await self.store.async_set_last_updated(STORE_CURRENT, now)
|
|
|
+ if old_next != new_next:
|
|
|
+ self._next_last_updated = now
|
|
|
+ await self.store.async_set_last_updated(STORE_NEXT, now)
|
|
|
+
|
|
|
async def _async_update_data(self) -> CoordinatorData:
|
|
|
backend_current: ReservationData | None = None
|
|
|
backend_next: ReservationData | None = None
|
|
|
@@ -122,18 +155,23 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
store_next = self._with_defaults(self.store.get(STORE_NEXT))
|
|
|
|
|
|
if self._override is not None:
|
|
|
+ new_current = self._with_defaults(self._override.current)
|
|
|
displayed_next = (
|
|
|
self._with_defaults(self._override.next) if self._override.next is not None else store_next
|
|
|
)
|
|
|
+ await self._async_note_displayed_change(new_current, displayed_next, now)
|
|
|
return CoordinatorData(
|
|
|
- current=self._with_defaults(self._override.current),
|
|
|
+ current=new_current,
|
|
|
next=displayed_next,
|
|
|
current_last_refresh=self._current_last_refresh,
|
|
|
next_last_refresh=self._next_last_refresh,
|
|
|
+ current_last_updated=self._current_last_updated,
|
|
|
+ next_last_updated=self._next_last_updated,
|
|
|
override_active=True,
|
|
|
api_status=api_status,
|
|
|
)
|
|
|
|
|
|
+ await self._async_note_displayed_change(store_current, store_next, now)
|
|
|
self._reschedule_mode_sync(store_current, store_next)
|
|
|
|
|
|
return CoordinatorData(
|
|
|
@@ -141,6 +179,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
next=store_next,
|
|
|
current_last_refresh=self._current_last_refresh,
|
|
|
next_last_refresh=self._next_last_refresh,
|
|
|
+ current_last_updated=self._current_last_updated,
|
|
|
+ next_last_updated=self._next_last_updated,
|
|
|
api_status=api_status,
|
|
|
)
|
|
|
|
|
|
@@ -323,12 +363,16 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
|
|
|
real_current = self.store.get(STORE_CURRENT) or self._empty_reservation()
|
|
|
real_next = self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
+ now = dt_util.utcnow()
|
|
|
+ await self._async_note_displayed_change(real_current, real_next, now)
|
|
|
self._reschedule_mode_sync(real_current, real_next)
|
|
|
self.async_set_updated_data(CoordinatorData(
|
|
|
current=real_current,
|
|
|
next=real_next,
|
|
|
current_last_refresh=self._current_last_refresh,
|
|
|
next_last_refresh=self._next_last_refresh,
|
|
|
+ current_last_updated=self._current_last_updated,
|
|
|
+ next_last_updated=self._next_last_updated,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
))
|
|
|
|
|
|
@@ -375,47 +419,61 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
async def _async_do_check_out(self) -> None:
|
|
|
departing = self.store.get(STORE_CURRENT) or self._empty_reservation()
|
|
|
await self.store.async_set(STORE_CURRENT, None)
|
|
|
- self._current_last_refresh = dt_util.utcnow()
|
|
|
+ now = dt_util.utcnow()
|
|
|
+ self._current_last_refresh = now
|
|
|
nxt = self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
+ empty = self._empty_reservation()
|
|
|
+ await self._async_note_displayed_change(empty, nxt, now)
|
|
|
self.async_set_updated_data(CoordinatorData(
|
|
|
- current=self._empty_reservation(),
|
|
|
+ current=empty,
|
|
|
next=nxt,
|
|
|
current_last_refresh=self._current_last_refresh,
|
|
|
next_last_refresh=self._next_last_refresh,
|
|
|
+ current_last_updated=self._current_last_updated,
|
|
|
+ next_last_updated=self._next_last_updated,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
))
|
|
|
await self._async_run_check_out_actions(departing)
|
|
|
- self._reschedule_mode_sync(self._empty_reservation(), nxt)
|
|
|
+ self._reschedule_mode_sync(empty, nxt)
|
|
|
|
|
|
async def async_clear_current(self) -> None:
|
|
|
"""Clear the Current slot in the store and notify listeners."""
|
|
|
departing = self.store.get(STORE_CURRENT)
|
|
|
await self.store.async_set(STORE_CURRENT, None)
|
|
|
- self._current_last_refresh = dt_util.utcnow()
|
|
|
+ now = dt_util.utcnow()
|
|
|
+ self._current_last_refresh = now
|
|
|
nxt = self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
+ empty = self._empty_reservation()
|
|
|
+ await self._async_note_displayed_change(empty, nxt, now)
|
|
|
self.async_set_updated_data(CoordinatorData(
|
|
|
- current=self._empty_reservation(),
|
|
|
+ current=empty,
|
|
|
next=nxt,
|
|
|
current_last_refresh=self._current_last_refresh,
|
|
|
next_last_refresh=self._next_last_refresh,
|
|
|
+ current_last_updated=self._current_last_updated,
|
|
|
+ next_last_updated=self._next_last_updated,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
))
|
|
|
if departing is not None:
|
|
|
await self._async_run_check_out_actions(departing)
|
|
|
- self._reschedule_mode_sync(self._empty_reservation(), nxt)
|
|
|
+ self._reschedule_mode_sync(empty, nxt)
|
|
|
|
|
|
async def async_promote_next_to_current(self) -> None:
|
|
|
"""Copy the Next slot into Current and notify listeners. Next is left unchanged."""
|
|
|
old_current = self.store.get(STORE_CURRENT)
|
|
|
next_data = self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
await self.store.async_set(STORE_CURRENT, next_data)
|
|
|
- self._current_last_refresh = dt_util.utcnow()
|
|
|
+ now = dt_util.utcnow()
|
|
|
+ self._current_last_refresh = now
|
|
|
new_next = self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
+ await self._async_note_displayed_change(next_data, new_next, now)
|
|
|
self.async_set_updated_data(CoordinatorData(
|
|
|
current=next_data,
|
|
|
next=new_next,
|
|
|
current_last_refresh=self._current_last_refresh,
|
|
|
next_last_refresh=self._next_last_refresh,
|
|
|
+ current_last_updated=self._current_last_updated,
|
|
|
+ next_last_updated=self._next_last_updated,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
))
|
|
|
self._reschedule_mode_sync(next_data, new_next)
|
|
|
@@ -426,15 +484,20 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
"""Activate a temporary override. Runs the full check-in sequence (WiFi + guest mode)."""
|
|
|
self._override = override
|
|
|
self.cancel_mode_sync()
|
|
|
+ new_current = self._with_defaults(override.current)
|
|
|
displayed_next = (
|
|
|
self._with_defaults(override.next) if override.next is not None
|
|
|
else self._with_defaults(self.store.get(STORE_NEXT) or self._empty_reservation())
|
|
|
)
|
|
|
+ now = dt_util.utcnow()
|
|
|
+ await self._async_note_displayed_change(new_current, displayed_next, now)
|
|
|
self.async_set_updated_data(CoordinatorData(
|
|
|
- current=self._with_defaults(override.current),
|
|
|
+ current=new_current,
|
|
|
next=displayed_next,
|
|
|
current_last_refresh=self._current_last_refresh,
|
|
|
next_last_refresh=self._next_last_refresh,
|
|
|
+ current_last_updated=self._current_last_updated,
|
|
|
+ next_last_updated=self._next_last_updated,
|
|
|
override_active=True,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
))
|
|
|
@@ -482,11 +545,14 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
self._override.next if self._override.next is not None
|
|
|
else self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
)
|
|
|
+ await self._async_note_displayed_change(self._override.current, displayed_next, now)
|
|
|
self.async_set_updated_data(CoordinatorData(
|
|
|
current=self._override.current,
|
|
|
next=displayed_next,
|
|
|
current_last_refresh=self._current_last_refresh,
|
|
|
next_last_refresh=self._next_last_refresh,
|
|
|
+ current_last_updated=self._current_last_updated,
|
|
|
+ next_last_updated=self._next_last_updated,
|
|
|
override_active=True,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
))
|
|
|
@@ -506,22 +572,28 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
self._override.next if self._override.next is not None
|
|
|
else self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
)
|
|
|
+ await self._async_note_displayed_change(self._override.current, displayed_next, now)
|
|
|
self.async_set_updated_data(CoordinatorData(
|
|
|
current=self._override.current,
|
|
|
next=displayed_next,
|
|
|
current_last_refresh=self._current_last_refresh,
|
|
|
next_last_refresh=self._next_last_refresh,
|
|
|
+ current_last_updated=self._current_last_updated,
|
|
|
+ next_last_updated=self._next_last_updated,
|
|
|
override_active=True,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
))
|
|
|
else:
|
|
|
store_current = self.store.get(STORE_CURRENT) or self._empty_reservation()
|
|
|
store_next = self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
+ await self._async_note_displayed_change(store_current, store_next, now)
|
|
|
self.async_set_updated_data(CoordinatorData(
|
|
|
current=store_current,
|
|
|
next=store_next,
|
|
|
current_last_refresh=self._current_last_refresh,
|
|
|
next_last_refresh=self._next_last_refresh,
|
|
|
+ current_last_updated=self._current_last_updated,
|
|
|
+ next_last_updated=self._next_last_updated,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
))
|
|
|
self._reschedule_mode_sync(store_current, store_next)
|