Răsfoiți Sursa

Revert "Guard mode sync as if off while current guest is departed"

This reverts commit f545ba24d4e9df0c7a736260e3d068e2907e923c. Freezing
mode sync while departed also cancelled the scheduled check-out
transition, so the reservation wouldn't clear until the next backend
poll instead of ending on time. Rolling back to plain operation while
a replacement (likely built on override-reservation) is designed.
ZiB 3 luni în urmă
părinte
comite
bd77bef0ec

+ 8 - 23
custom_components/room_cal_ha/coordinator.py

@@ -198,7 +198,7 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
       cancel()
     self._mode_sync_handles.clear()
 
-    if not self._is_mode_sync_effectively_on():
+    if not self._is_mode_sync_on():
       return
 
     now = dt_util.now().replace(tzinfo=None)
@@ -241,8 +241,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
     return 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 (or effectively off)."""
-    if not self._is_mode_sync_effectively_on():
+    """Set room active state on/off. No-op when mode_sync is off."""
+    if not self._is_mode_sync_on():
       return
     if self._room_state_switch is None:
       return
@@ -254,8 +254,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
       await self._room_state_switch.async_turn_off()
 
   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 (or effectively off)."""
-    if not self._is_mode_sync_effectively_on():
+    """Run WiFi + lock updates and guest mode activation. No-op when mode_sync is off."""
+    if not self._is_mode_sync_on():
       return
     _LOGGER.info("Check-in: guest=%s", res.guest_name)
     if self._is_use_unifi_on() and isinstance(res.wifi_ssid, str):
@@ -280,8 +280,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
     _LOGGER.info("Check-in complete: guest=%s", res.guest_name)
 
   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 (or effectively off)."""
-    if not self._is_mode_sync_effectively_on():
+    """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():
       return
     _LOGGER.info("Check-out: guest=%s", res.guest_name)
     if self._is_use_unifi_on() and isinstance(res.wifi_ssid, str):
@@ -317,7 +317,7 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
     current = old_current or self._empty_reservation()
     nxt = self.store.get(STORE_NEXT) or self._empty_reservation()
 
-    if self._is_mode_sync_effectively_on():
+    if self._is_mode_sync_on():
       if self._reservation_window_state(nxt) in ("active", "elapsed"):
         await self.store.async_set(STORE_CURRENT, nxt)
         await self._async_maybe_reset_arrival_flags(old_current, nxt)
@@ -366,17 +366,6 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
         return entity.is_on
     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:
     from .switch import UseUnifiSwitch  # avoid circular at module level
     for entity in self.hass.data.get(DOMAIN, {}).get(self.entry.entry_id, {}).get("switches", []):
@@ -680,10 +669,6 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
   async def async_set_departed(self, departed: bool) -> None:
     self._current_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:
       self.async_set_updated_data(dataclass_replace(self.data, current_departed=departed))
 

+ 0 - 62
tests/test_arrived_departed.py

@@ -253,65 +253,3 @@ async def test_flags_reset_on_checkout(coord: RoomCalCoordinator):
   assert coord.data.current_arrived is False
 
 
-# ---------------------------------------------------------------------------
-# Coordinator: mode sync guarded as-if-off while current is departed
-# ---------------------------------------------------------------------------
-
-async def test_departed_blocks_check_in_actions(coord: RoomCalCoordinator):
-  await coord.async_set_departed(True)
-  with patch.object(coord, "_is_mode_sync_on", return_value=True):
-    await coord._async_run_check_in_actions(_ALICE)
-  assert coord._room_state_switch is None or not coord._room_state_switch.async_turn_on.called
-
-
-async def test_departed_blocks_reschedule(coord: RoomCalCoordinator):
-  await coord.async_set_departed(True)
-  with patch.object(coord, "_is_mode_sync_on", return_value=True):
-    coord._reschedule_mode_sync(_ALICE, _BOB)
-  assert coord._mode_sync_handles == []
-
-
-async def test_departed_cancels_pending_handles_immediately(coord: RoomCalCoordinator):
-  cancel = MagicMock()
-  coord._mode_sync_handles.append(cancel)
-
-  await coord.async_set_departed(True)
-
-  cancel.assert_called_once()
-  assert coord._mode_sync_handles == []
-
-
-async def test_recover_via_clear_current(coord: RoomCalCoordinator):
-  await coord.store.async_set(STORE_CURRENT, _ALICE)
-  await coord.async_refresh()
-  await coord.async_set_departed(True)
-  assert coord._current_departed is True
-
-  await coord.async_clear_current()
-
-  assert coord._current_departed is False
-  assert coord.data.current_departed is False
-
-
-async def test_recover_via_force_refresh(hass: HomeAssistant):
-  entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, "caldav_url": "https://x", "calendar_name": "R"})
-  c = RoomCalCoordinator(hass, entry)
-  await c.async_setup()
-
-  with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as M:
-    M.return_value.async_fetch = AsyncMock(return_value=(_ALICE, None))
-    await c.async_refresh()
-
-  await c.async_set_departed(True)
-  assert c._current_departed is True
-
-  # Backend no longer reports Alice as current (e.g. calendar window ended) — a
-  # force refresh should observe the identity change and thaw the guard.
-  with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as M:
-    M.return_value.async_fetch = AsyncMock(return_value=(None, None))
-    await c.async_refresh()
-
-  assert c._current_departed is False
-  assert c.data.current_departed is False
-
-