|
@@ -150,6 +150,73 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
async_call_later(self.hass, delay, self._async_handle_check_out)
|
|
async_call_later(self.hass, delay, self._async_handle_check_out)
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+ def _reservation_window_state(self, res: ReservationData) -> str | None:
|
|
|
|
|
+ """
|
|
|
|
|
+ Returns 'active' if now is within check_in..check_out, 'elapsed' if past check_out,
|
|
|
|
|
+ 'pending' if before check_in, or None if the reservation has no datetime info.
|
|
|
|
|
+ """
|
|
|
|
|
+ now = dt_util.now()
|
|
|
|
|
+ check_in = check_out = None
|
|
|
|
|
+ if res.check_in_date and res.check_in_time:
|
|
|
|
|
+ check_in = dt_util.as_local(datetime.combine(res.check_in_date, res.check_in_time))
|
|
|
|
|
+ if res.check_out_date and res.check_out_time:
|
|
|
|
|
+ check_out = dt_util.as_local(datetime.combine(res.check_out_date, res.check_out_time))
|
|
|
|
|
+ if check_out and now >= check_out:
|
|
|
|
|
+ return "elapsed"
|
|
|
|
|
+ if check_in and now >= check_in:
|
|
|
|
|
+ return "active"
|
|
|
|
|
+ if check_in:
|
|
|
|
|
+ return "pending"
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ async def async_post_setup_reconcile(self) -> None:
|
|
|
|
|
+ """
|
|
|
|
|
+ Called from async_setup_entry after all platforms are set up and switch states
|
|
|
|
|
+ are restored. Silently corrects guest_mode._state if mode_sync is on and the
|
|
|
|
|
+ stored reservation data implies a different state than what was restored, then
|
|
|
|
|
+ commits a single HA state write via GuestModeSwitch.commit_state().
|
|
|
|
|
+ """
|
|
|
|
|
+ current = self.store.get(STORE_CURRENT) or ReservationData()
|
|
|
|
|
+ nxt = self.store.get(STORE_NEXT) or ReservationData()
|
|
|
|
|
+ data_changed = False
|
|
|
|
|
+
|
|
|
|
|
+ if self._is_mode_sync_on():
|
|
|
|
|
+ # If next check-in elapsed while HA was down, promote it now.
|
|
|
|
|
+ if self._reservation_window_state(nxt) in ("active", "elapsed"):
|
|
|
|
|
+ await self.store.async_set(STORE_CURRENT, nxt)
|
|
|
|
|
+ self._current_last_refresh = dt_util.utcnow()
|
|
|
|
|
+ current = nxt
|
|
|
|
|
+ data_changed = True
|
|
|
|
|
+
|
|
|
|
|
+ state = self._reservation_window_state(current)
|
|
|
|
|
+
|
|
|
|
|
+ if state == "elapsed":
|
|
|
|
|
+ await self.store.async_set(STORE_CURRENT, None)
|
|
|
|
|
+ self._current_last_refresh = dt_util.utcnow()
|
|
|
|
|
+ current = ReservationData()
|
|
|
|
|
+ if self._guest_mode_switch is not None:
|
|
|
|
|
+ self._guest_mode_switch._state = False
|
|
|
|
|
+ data_changed = True
|
|
|
|
|
+ elif state == "active":
|
|
|
|
|
+ if self._guest_mode_switch is not None:
|
|
|
|
|
+ self._guest_mode_switch._state = True
|
|
|
|
|
+ data_changed = True
|
|
|
|
|
+
|
|
|
|
|
+ new_current = self.store.get(STORE_CURRENT) or ReservationData()
|
|
|
|
|
+ new_next = self.store.get(STORE_NEXT) or ReservationData()
|
|
|
|
|
+ self._reschedule_mode_sync(new_current, new_next)
|
|
|
|
|
+
|
|
|
|
|
+ if data_changed:
|
|
|
|
|
+ self.async_set_updated_data(CoordinatorData(
|
|
|
|
|
+ current=new_current,
|
|
|
|
|
+ next=new_next,
|
|
|
|
|
+ current_last_refresh=self._current_last_refresh,
|
|
|
|
|
+ next_last_refresh=self._next_last_refresh,
|
|
|
|
|
+ ))
|
|
|
|
|
+
|
|
|
|
|
+ if self._guest_mode_switch is not None:
|
|
|
|
|
+ self._guest_mode_switch.commit_state()
|
|
|
|
|
+
|
|
|
def _is_mode_sync_on(self) -> bool:
|
|
def _is_mode_sync_on(self) -> bool:
|
|
|
from .switch import ModeSyncSwitch # avoid circular at module level
|
|
from .switch import ModeSyncSwitch # avoid circular at module level
|
|
|
for entity in self.hass.data.get(DOMAIN, {}).get(self.entry.entry_id, {}).get("switches", []):
|
|
for entity in self.hass.data.get(DOMAIN, {}).get(self.entry.entry_id, {}).get("switches", []):
|
|
@@ -187,12 +254,19 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
next_data = self.store.get(STORE_NEXT) or ReservationData()
|
|
next_data = self.store.get(STORE_NEXT) or ReservationData()
|
|
|
await self.store.async_set(STORE_CURRENT, next_data)
|
|
await self.store.async_set(STORE_CURRENT, next_data)
|
|
|
self._current_last_refresh = dt_util.utcnow()
|
|
self._current_last_refresh = dt_util.utcnow()
|
|
|
|
|
+ new_next = self.store.get(STORE_NEXT) or ReservationData()
|
|
|
self.async_set_updated_data(CoordinatorData(
|
|
self.async_set_updated_data(CoordinatorData(
|
|
|
current=next_data,
|
|
current=next_data,
|
|
|
- next=self.store.get(STORE_NEXT) or ReservationData(),
|
|
|
|
|
|
|
+ next=new_next,
|
|
|
current_last_refresh=self._current_last_refresh,
|
|
current_last_refresh=self._current_last_refresh,
|
|
|
next_last_refresh=self._next_last_refresh,
|
|
next_last_refresh=self._next_last_refresh,
|
|
|
))
|
|
))
|
|
|
|
|
+ self._reschedule_mode_sync(next_data, new_next)
|
|
|
|
|
+ # If mode_sync is on and the newly imported record's check-in has already passed,
|
|
|
|
|
+ # activate guest mode immediately (the scheduled handle already fired or was missed).
|
|
|
|
|
+ if self._is_mode_sync_on() and self._reservation_window_state(next_data) == "active":
|
|
|
|
|
+ if self._guest_mode_switch is not None:
|
|
|
|
|
+ await self._guest_mode_switch.async_turn_on()
|
|
|
|
|
|
|
|
async def async_apply_update(self, target: str, partial: dict[str, Any]) -> None:
|
|
async def async_apply_update(self, target: str, partial: dict[str, Any]) -> None:
|
|
|
"""Apply a thin update (service call) to current or next."""
|
|
"""Apply a thin update (service call) to current or next."""
|