|
|
@@ -23,7 +23,7 @@ from .const import (
|
|
|
SYNC_MODE_API,
|
|
|
SYNC_MODE_CALDAV,
|
|
|
)
|
|
|
-from .models import ApiStatusData, OverrideData, ReservationData
|
|
|
+from .models import ApiStatusData, OverrideData, ReservationData, reservation_diff
|
|
|
from dataclasses import fields as dataclass_fields
|
|
|
from .store import ReservationStore
|
|
|
|
|
|
@@ -103,10 +103,18 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
|
|
|
if backend_fetched:
|
|
|
# Calendar is authoritative: write its result including None (clears the slot).
|
|
|
+ old_current = self.store.get(STORE_CURRENT)
|
|
|
await self.store.async_set(STORE_CURRENT, backend_current)
|
|
|
await self.store.async_set(STORE_NEXT, backend_next)
|
|
|
self._current_last_refresh = now
|
|
|
self._next_last_refresh = now
|
|
|
+ if self._override is None:
|
|
|
+ now_naive = dt_util.now().replace(tzinfo=None)
|
|
|
+ diff = reservation_diff(old_current, backend_current, now_naive)
|
|
|
+ if diff.checkout_needed:
|
|
|
+ await self._async_run_check_out_actions(diff.departing)
|
|
|
+ if diff.checkin_needed or diff.wifi_key_changed or diff.wifi_ssid_changed:
|
|
|
+ await self._async_run_check_in_actions(backend_current or self._empty_reservation())
|
|
|
|
|
|
store_current = self._with_defaults(self.store.get(STORE_CURRENT))
|
|
|
store_next = self._with_defaults(self.store.get(STORE_NEXT))
|
|
|
@@ -291,7 +299,6 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
|
|
|
async def _async_do_check_in(self) -> None:
|
|
|
await self.async_promote_next_to_current()
|
|
|
- await self._async_run_check_in_actions(self.data.current)
|
|
|
|
|
|
@callback
|
|
|
def _async_handle_check_out(self, _now: datetime) -> None:
|
|
|
@@ -301,29 +308,37 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
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()
|
|
|
+ nxt = self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
self.async_set_updated_data(CoordinatorData(
|
|
|
current=self._empty_reservation(),
|
|
|
- next=self.store.get(STORE_NEXT) or self._empty_reservation(),
|
|
|
+ next=nxt,
|
|
|
current_last_refresh=self._current_last_refresh,
|
|
|
next_last_refresh=self._next_last_refresh,
|
|
|
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)
|
|
|
|
|
|
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()
|
|
|
+ nxt = self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
self.async_set_updated_data(CoordinatorData(
|
|
|
current=self._empty_reservation(),
|
|
|
- next=self.store.get(STORE_NEXT) or self._empty_reservation(),
|
|
|
+ next=nxt,
|
|
|
current_last_refresh=self._current_last_refresh,
|
|
|
next_last_refresh=self._next_last_refresh,
|
|
|
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)
|
|
|
|
|
|
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()
|
|
|
@@ -336,10 +351,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
))
|
|
|
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._reservation_window_state(next_data) == "active":
|
|
|
- await self._async_set_guest_mode(True)
|
|
|
+ await self._async_run_check_in_actions(next_data)
|
|
|
|
|
|
async def async_start_override(self, override: OverrideData) -> None:
|
|
|
"""Activate a temporary override. Runs the full check-in sequence (WiFi + guest mode)."""
|
|
|
@@ -361,6 +374,14 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
|
|
|
async def async_end_override(self) -> None:
|
|
|
"""Clear the active override and reconcile room state from the store."""
|
|
|
+ if self._override is not None:
|
|
|
+ now_naive = dt_util.now().replace(tzinfo=None)
|
|
|
+ diff = reservation_diff(self._override.current, self.store.get(STORE_CURRENT), now_naive)
|
|
|
+ if diff.checkout_needed:
|
|
|
+ await self._async_run_check_out_actions(diff.departing)
|
|
|
+ if diff.checkin_needed or diff.wifi_key_changed or diff.wifi_ssid_changed:
|
|
|
+ store_current = self.store.get(STORE_CURRENT)
|
|
|
+ await self._async_run_check_in_actions(store_current or self._empty_reservation())
|
|
|
self._override = None
|
|
|
await self.async_reconcile_from_store()
|
|
|
|
|
|
@@ -404,7 +425,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
return
|
|
|
|
|
|
stored = self.store.get(target) or self._empty_reservation()
|
|
|
- await self.store.async_set(target, stored.apply_partial(partial))
|
|
|
+ new_stored = stored.apply_partial(partial)
|
|
|
+ await self.store.async_set(target, new_stored)
|
|
|
if target == STORE_CURRENT:
|
|
|
self._current_last_refresh = now
|
|
|
else:
|
|
|
@@ -425,13 +447,23 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
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()
|
|
|
self.async_set_updated_data(CoordinatorData(
|
|
|
- current=self.store.get(STORE_CURRENT) or self._empty_reservation(),
|
|
|
- next=self.store.get(STORE_NEXT) or self._empty_reservation(),
|
|
|
+ current=store_current,
|
|
|
+ next=store_next,
|
|
|
current_last_refresh=self._current_last_refresh,
|
|
|
next_last_refresh=self._next_last_refresh,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
))
|
|
|
+ self._reschedule_mode_sync(store_current, store_next)
|
|
|
+ if target == STORE_CURRENT:
|
|
|
+ now_naive = dt_util.now().replace(tzinfo=None)
|
|
|
+ diff = reservation_diff(stored, new_stored, now_naive)
|
|
|
+ if diff.checkout_needed:
|
|
|
+ await self._async_run_check_out_actions(diff.departing)
|
|
|
+ if diff.checkin_needed or diff.wifi_key_changed or diff.wifi_ssid_changed:
|
|
|
+ await self._async_run_check_in_actions(new_stored)
|
|
|
|
|
|
def cancel_mode_sync(self) -> None:
|
|
|
for cancel in self._mode_sync_handles:
|