|
@@ -200,7 +200,7 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
cancel()
|
|
cancel()
|
|
|
self._mode_sync_handles.clear()
|
|
self._mode_sync_handles.clear()
|
|
|
|
|
|
|
|
- if not self._is_mode_sync_on():
|
|
|
|
|
|
|
+ if not self._is_mode_sync_effectively_on():
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
now = dt_util.now().replace(tzinfo=None)
|
|
now = dt_util.now().replace(tzinfo=None)
|
|
@@ -243,8 +243,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
async def _async_set_room_state(self, on: bool, *, _startup: bool = False) -> None:
|
|
async def _async_set_room_state(self, on: bool, *, _startup: bool = False) -> None:
|
|
|
- """Set room active state on/off. No-op when mode_sync is off."""
|
|
|
|
|
- if not self._is_mode_sync_on():
|
|
|
|
|
|
|
+ """Set room active state on/off. No-op when mode_sync is off (or effectively off)."""
|
|
|
|
|
+ if not self._is_mode_sync_effectively_on():
|
|
|
return
|
|
return
|
|
|
if self._room_state_switch is None:
|
|
if self._room_state_switch is None:
|
|
|
return
|
|
return
|
|
@@ -256,8 +256,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
await self._room_state_switch.async_turn_off()
|
|
await self._room_state_switch.async_turn_off()
|
|
|
|
|
|
|
|
async def _async_run_check_in_actions(self, res: ReservationData) -> None:
|
|
async def _async_run_check_in_actions(self, res: ReservationData) -> None:
|
|
|
- """Run WiFi + lock updates and guest mode activation. No-op when mode_sync is off."""
|
|
|
|
|
- if not self._is_mode_sync_on():
|
|
|
|
|
|
|
+ """Run WiFi + lock updates and guest mode activation. No-op when mode_sync is off (or effectively off)."""
|
|
|
|
|
+ if not self._is_mode_sync_effectively_on():
|
|
|
return
|
|
return
|
|
|
_LOGGER.info("Check-in: guest=%s", res.guest_name)
|
|
_LOGGER.info("Check-in: guest=%s", res.guest_name)
|
|
|
if self._is_use_unifi_on() and isinstance(res.wifi_ssid, str):
|
|
if self._is_use_unifi_on() and isinstance(res.wifi_ssid, str):
|
|
@@ -281,8 +281,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
_LOGGER.info("Check-in complete: guest=%s", res.guest_name)
|
|
_LOGGER.info("Check-in complete: guest=%s", res.guest_name)
|
|
|
|
|
|
|
|
async def _async_run_check_out_actions(self, res: ReservationData) -> None:
|
|
async def _async_run_check_out_actions(self, res: ReservationData) -> None:
|
|
|
- """Run WiFi SSID disable, lock slot disable, and guest mode deactivation. No-op when mode_sync is off."""
|
|
|
|
|
- if not self._is_mode_sync_on():
|
|
|
|
|
|
|
+ """Run WiFi SSID disable, lock slot disable, and guest mode deactivation. No-op when mode_sync is off (or effectively off)."""
|
|
|
|
|
+ if not self._is_mode_sync_effectively_on():
|
|
|
return
|
|
return
|
|
|
_LOGGER.info("Check-out: guest=%s", res.guest_name)
|
|
_LOGGER.info("Check-out: guest=%s", res.guest_name)
|
|
|
if self._is_use_unifi_on() and isinstance(res.wifi_ssid, str):
|
|
if self._is_use_unifi_on() and isinstance(res.wifi_ssid, str):
|
|
@@ -367,7 +367,7 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
current = old_current or self._empty_reservation()
|
|
current = old_current or self._empty_reservation()
|
|
|
nxt = self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
nxt = self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
|
|
|
|
|
- if self._is_mode_sync_on():
|
|
|
|
|
|
|
+ if self._is_mode_sync_effectively_on():
|
|
|
if self._reservation_window_state(nxt) in ("active", "elapsed"):
|
|
if self._reservation_window_state(nxt) in ("active", "elapsed"):
|
|
|
await self.store.async_set(STORE_CURRENT, nxt)
|
|
await self.store.async_set(STORE_CURRENT, nxt)
|
|
|
await self._async_maybe_reset_arrival_flags(old_current, nxt)
|
|
await self._async_maybe_reset_arrival_flags(old_current, nxt)
|
|
@@ -416,6 +416,17 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
return entity.is_on
|
|
return entity.is_on
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
|
|
+ def _is_mode_sync_effectively_on(self) -> bool:
|
|
|
|
|
+ """Mode sync, but forced off while the current guest is marked departed.
|
|
|
|
|
+
|
|
|
|
|
+ Once GuestDepartedButton fires, automation freezes so that any changes made in
|
|
|
|
|
+ response to the departure (thin updates, manual actions) persist undisturbed until
|
|
|
|
|
+ the current identity actually changes — typically a backend fetch clearing Current
|
|
|
|
|
+ to null, or an explicit Clear Current. Both reset current_departed via
|
|
|
|
|
+ _async_maybe_reset_arrival_flags and thaw automation again.
|
|
|
|
|
+ """
|
|
|
|
|
+ return self._is_mode_sync_on() and not self._current_departed
|
|
|
|
|
+
|
|
|
def _is_use_unifi_on(self) -> bool:
|
|
def _is_use_unifi_on(self) -> bool:
|
|
|
from .switch import UseUnifiSwitch # avoid circular at module level
|
|
from .switch import UseUnifiSwitch # 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", []):
|
|
@@ -719,6 +730,10 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
async def async_set_departed(self, departed: bool) -> None:
|
|
async def async_set_departed(self, departed: bool) -> None:
|
|
|
self._current_departed = departed
|
|
self._current_departed = departed
|
|
|
await self.store.async_set_departed(departed)
|
|
await self.store.async_set_departed(departed)
|
|
|
|
|
+ if departed:
|
|
|
|
|
+ # Guard as if mode sync were off: cancel any pending scheduled transition so it
|
|
|
|
|
+ # can't fire and clobber changes made in response to the departure.
|
|
|
|
|
+ self.cancel_mode_sync()
|
|
|
if self.data is not None:
|
|
if self.data is not None:
|
|
|
self.async_set_updated_data(dataclass_replace(self.data, current_departed=departed))
|
|
self.async_set_updated_data(dataclass_replace(self.data, current_departed=departed))
|
|
|
|
|
|