Explorar o código

Rename GuestModeSwitch to RoomStateSwitch; make it the primary device entity

_attr_name = None promotes it to the canonical Room State entity (named after
the device) rather than a hardcoded "Guest Mode" suffix. unique_id updated to
_room_state. Internal coordinator references renamed to match.
ZiB hai 4 meses
pai
achega
338798b0a7

+ 17 - 17
custom_components/room_cal_ha/coordinator.py

@@ -53,7 +53,7 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
     self.entry = entry
     self.store = ReservationStore(hass, entry.entry_id)
     self._mode_sync_handles: list[Any] = []
-    self._guest_mode_switch: Any = None  # injected by switch.py after platform setup
+    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._override: OverrideData | None = None
@@ -61,8 +61,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
   async def async_setup(self) -> None:
     await self.store.async_load()
 
-  def inject_guest_mode_switch(self, switch: Any) -> None:
-    self._guest_mode_switch = switch
+  def inject_room_state_switch(self, switch: Any) -> None:
+    self._room_state_switch = switch
 
   async def _async_update_data(self) -> CoordinatorData:
     backend_current: ReservationData | None = None
@@ -189,18 +189,18 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
       return "pending"
     return None
 
-  async def _async_set_guest_mode(self, on: bool, *, _startup: bool = False) -> None:
-    """Set guest mode on/off. No-op when mode_sync is off."""
+  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():
       return
-    if self._guest_mode_switch is None:
+    if self._room_state_switch is None:
       return
     if _startup:
-      self._guest_mode_switch._state = on
+      self._room_state_switch._state = on
     elif on:
-      await self._guest_mode_switch.async_turn_on()
+      await self._room_state_switch.async_turn_on()
     else:
-      await self._guest_mode_switch.async_turn_off()
+      await self._room_state_switch.async_turn_off()
 
   async def _async_run_check_in_actions(self, res: ReservationData) -> None:
     """Run WiFi updates and guest mode activation. No-op when mode_sync is off."""
@@ -217,7 +217,7 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
           await async_set_wifi_key(self.hass, res.wifi_ssid, res.wifi_key)
         except Exception as err:
           _LOGGER.warning("Failed to set WiFi key: %s", err)
-    await self._async_set_guest_mode(True)
+    await self._async_set_room_state(True)
 
   async def _async_run_check_out_actions(self, res: ReservationData) -> None:
     """Run WiFi SSID disable and guest mode deactivation. No-op when mode_sync is off."""
@@ -229,13 +229,13 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
         await async_set_ssid_enabled(self.hass, res.wifi_ssid, False)
       except Exception as err:
         _LOGGER.warning("Failed to disable SSID '%s': %s", res.wifi_ssid, err)
-    await self._async_set_guest_mode(False)
+    await self._async_set_room_state(False)
 
   async def async_reconcile_from_store(self, *, _startup: bool = False) -> None:
     """Load store state, apply elapsed transitions, set room state, reschedule mode sync.
 
     Called at startup (via async_post_setup_reconcile) and when leaving override mode.
-    _startup=True uses direct state mutation on the guest mode switch instead of
+    _startup=True uses direct state mutation on the room state switch instead of
     async_turn_on/off, because HA state writes are deferred until commit_state().
     """
     current = self.store.get(STORE_CURRENT) or self._empty_reservation()
@@ -258,7 +258,7 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
         guest_on = True
 
       if guest_on is not None:
-        await self._async_set_guest_mode(guest_on, _startup=_startup)
+        await self._async_set_room_state(guest_on, _startup=_startup)
 
     real_current = self.store.get(STORE_CURRENT) or self._empty_reservation()
     real_next = self.store.get(STORE_NEXT) or self._empty_reservation()
@@ -273,11 +273,11 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
 
   async def async_post_setup_reconcile(self) -> None:
     """Called from async_setup_entry after all platforms are set up and switch states
-    are restored. Reconciles room state from the store, then commits the guest mode
-    switch state in a single HA write."""
+    are restored. Reconciles room state from the store, then commits the room state
+    switch in a single HA write."""
     await self.async_reconcile_from_store(_startup=True)
-    if self._guest_mode_switch is not None:
-      self._guest_mode_switch.commit_state()
+    if self._room_state_switch is not None:
+      self._room_state_switch.commit_state()
 
   def _is_mode_sync_on(self) -> bool:
     from .switch import ModeSyncSwitch  # avoid circular at module level

+ 8 - 8
custom_components/room_cal_ha/switch.py

@@ -20,19 +20,19 @@ async def async_setup_entry(
   coordinator: RoomCalCoordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
   device_name = f"{entry.data.get(CONF_ROOM_NAME, entry.title)} Room State"
 
-  guest_mode = GuestModeSwitch(coordinator, entry, device_name)
+  guest_mode = RoomStateSwitch(coordinator, entry, device_name)
   mode_sync = ModeSyncSwitch(coordinator, entry, device_name)
   override = OverrideReservationSwitch(coordinator, entry, device_name)
   use_unifi = UseUnifiSwitch(coordinator, entry, device_name)
 
-  coordinator.inject_guest_mode_switch(guest_mode)
+  coordinator.inject_room_state_switch(guest_mode)
 
   switches = [guest_mode, mode_sync, override, use_unifi]
   hass.data[DOMAIN][entry.entry_id]["switches"] = switches
   async_add_entities(switches)
 
 
-class _RoomStateSwitch(SwitchEntity, RestoreEntity):
+class _SwitchBase(SwitchEntity, RestoreEntity):
   _attr_has_entity_name = True
   _state: bool = False
 
@@ -68,13 +68,13 @@ class _RoomStateSwitch(SwitchEntity, RestoreEntity):
     self.async_write_ha_state()
 
 
-class GuestModeSwitch(_RoomStateSwitch):
-  _attr_name = "Guest Mode"
+class RoomStateSwitch(_SwitchBase):
+  _attr_name = None  # primary entity — named after the device
   _deferred: bool = True  # suppresses writes until coordinator reconcile calls commit_state()
 
   def __init__(self, coordinator: RoomCalCoordinator, entry: ConfigEntry, device_name: str) -> None:
     super().__init__(coordinator, entry, device_name)
-    self._attr_unique_id = f"{entry.entry_id}_guest_mode"
+    self._attr_unique_id = f"{entry.entry_id}_room_state"
 
   def async_write_ha_state(self) -> None:
     if self._deferred:
@@ -87,7 +87,7 @@ class GuestModeSwitch(_RoomStateSwitch):
     super().async_write_ha_state()
 
 
-class ModeSyncSwitch(_RoomStateSwitch):
+class ModeSyncSwitch(_SwitchBase):
   _attr_name = "Mode Sync"
 
   def __init__(self, coordinator: RoomCalCoordinator, entry: ConfigEntry, device_name: str) -> None:
@@ -99,7 +99,7 @@ class ModeSyncSwitch(_RoomStateSwitch):
     self._coordinator.cancel_mode_sync()
 
 
-class UseUnifiSwitch(_RoomStateSwitch):
+class UseUnifiSwitch(_SwitchBase):
   _attr_name = "Use UniFi"
   _attr_icon = "mdi:wifi-settings"
   _state: bool = True  # default on; base async_added_to_hass restores saved state

+ 25 - 25
tests/test_coordinator.py

@@ -290,7 +290,7 @@ async def test_check_in_calls_wifi_key(hass: HomeAssistant, coordinator: RoomCal
   await coordinator.store.async_set(STORE_NEXT, _WIFI_NEXT)
   mock_switch = MagicMock()
   mock_switch.async_turn_on = AsyncMock()
-  coordinator._guest_mode_switch = mock_switch
+  coordinator._room_state_switch = mock_switch
 
   with (
     patch.object(coordinator, "_is_mode_sync_on", return_value=True),
@@ -308,7 +308,7 @@ async def test_check_in_calls_wifi_key(hass: HomeAssistant, coordinator: RoomCal
 
 async def test_check_in_skips_wifi_when_ssid_none(coordinator: RoomCalCoordinator):
   await coordinator.store.async_set(STORE_NEXT, ReservationData(wifi_ssid=None, wifi_key="securepass"))
-  coordinator._guest_mode_switch = None
+  coordinator._room_state_switch = None
 
   with patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi:
     await coordinator._async_do_check_in()
@@ -318,7 +318,7 @@ async def test_check_in_skips_wifi_when_ssid_none(coordinator: RoomCalCoordinato
 
 async def test_check_in_skips_wifi_when_key_none(coordinator: RoomCalCoordinator):
   await coordinator.store.async_set(STORE_NEXT, ReservationData(wifi_ssid="GuestNet", wifi_key=None))
-  coordinator._guest_mode_switch = None
+  coordinator._room_state_switch = None
 
   with patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi:
     await coordinator._async_do_check_in()
@@ -330,7 +330,7 @@ async def test_check_in_skips_wifi_when_use_unifi_off(hass: HomeAssistant, coord
   await coordinator.store.async_set(STORE_NEXT, _WIFI_NEXT)
   mock_switch = MagicMock()
   mock_switch.async_turn_on = AsyncMock()
-  coordinator._guest_mode_switch = mock_switch
+  coordinator._room_state_switch = mock_switch
 
   with (
     patch.object(coordinator, "_is_mode_sync_on", return_value=True),
@@ -351,7 +351,7 @@ async def test_check_in_wifi_error_still_activates_guest_mode(coordinator: RoomC
   await coordinator.store.async_set(STORE_NEXT, _WIFI_NEXT)
   mock_switch = MagicMock()
   mock_switch.async_turn_on = AsyncMock()
-  coordinator._guest_mode_switch = mock_switch
+  coordinator._room_state_switch = mock_switch
 
   with (
     patch.object(coordinator, "_is_mode_sync_on", return_value=True),
@@ -373,7 +373,7 @@ async def test_check_out_disables_ssid(hass: HomeAssistant, coordinator: RoomCal
   await coordinator.store.async_set(STORE_CURRENT, _WIFI_NEXT)
   mock_switch = MagicMock()
   mock_switch.async_turn_off = AsyncMock()
-  coordinator._guest_mode_switch = mock_switch
+  coordinator._room_state_switch = mock_switch
 
   with (
     patch.object(coordinator, "_is_mode_sync_on", return_value=True),
@@ -387,7 +387,7 @@ async def test_check_out_disables_ssid(hass: HomeAssistant, coordinator: RoomCal
 
 async def test_check_out_skips_ssid_when_ssid_none(coordinator: RoomCalCoordinator):
   await coordinator.store.async_set(STORE_CURRENT, ReservationData(wifi_ssid=None))
-  coordinator._guest_mode_switch = None
+  coordinator._room_state_switch = None
 
   with patch("custom_components.room_cal_ha.wifi.async_set_ssid_enabled", new_callable=AsyncMock) as mock_ssid:
     await coordinator._async_do_check_out()
@@ -397,7 +397,7 @@ async def test_check_out_skips_ssid_when_ssid_none(coordinator: RoomCalCoordinat
 
 async def test_check_out_skips_ssid_when_use_unifi_off(coordinator: RoomCalCoordinator):
   await coordinator.store.async_set(STORE_CURRENT, _WIFI_NEXT)
-  coordinator._guest_mode_switch = None
+  coordinator._room_state_switch = None
 
   with (
     patch.object(coordinator, "_is_mode_sync_on", return_value=True),
@@ -413,7 +413,7 @@ async def test_check_out_ssid_error_still_deactivates_guest_mode(coordinator: Ro
   await coordinator.store.async_set(STORE_CURRENT, _WIFI_NEXT)
   mock_switch = MagicMock()
   mock_switch.async_turn_off = AsyncMock()
-  coordinator._guest_mode_switch = mock_switch
+  coordinator._room_state_switch = mock_switch
 
   with (
     patch.object(coordinator, "_is_mode_sync_on", return_value=True),
@@ -433,13 +433,13 @@ _WIFI_OVERRIDE = ReservationData(guest_name="WiFi Guest", wifi_ssid="GuestNet",
 
 
 async def test_start_override_sets_current_in_data(coordinator: RoomCalCoordinator):
-  coordinator._guest_mode_switch = None
+  coordinator._room_state_switch = None
   await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES))
   assert coordinator.data.current.guest_name == "Override Guest"
 
 
 async def test_start_override_sets_override_active_flag(coordinator: RoomCalCoordinator):
-  coordinator._guest_mode_switch = None
+  coordinator._room_state_switch = None
   await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES))
   assert coordinator.data.override_active is True
 
@@ -447,7 +447,7 @@ async def test_start_override_sets_override_active_flag(coordinator: RoomCalCoor
 async def test_start_override_cancels_mode_sync(coordinator: RoomCalCoordinator):
   cancel1, cancel2 = MagicMock(), MagicMock()
   coordinator._mode_sync_handles = [cancel1, cancel2]
-  coordinator._guest_mode_switch = None
+  coordinator._room_state_switch = None
   await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES))
   cancel1.assert_called_once()
   cancel2.assert_called_once()
@@ -457,7 +457,7 @@ async def test_start_override_cancels_mode_sync(coordinator: RoomCalCoordinator)
 async def test_start_override_turns_on_guest_mode(coordinator: RoomCalCoordinator):
   mock_switch = MagicMock()
   mock_switch.async_turn_on = AsyncMock()
-  coordinator._guest_mode_switch = mock_switch
+  coordinator._room_state_switch = mock_switch
   with patch.object(coordinator, "_is_mode_sync_on", return_value=True):
     await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES))
   mock_switch.async_turn_on.assert_awaited_once()
@@ -466,15 +466,15 @@ async def test_start_override_turns_on_guest_mode(coordinator: RoomCalCoordinato
 async def test_start_override_does_not_turn_on_guest_mode_when_mode_sync_off(coordinator: RoomCalCoordinator):
   mock_switch = MagicMock()
   mock_switch.async_turn_on = AsyncMock()
-  coordinator._guest_mode_switch = mock_switch
+  coordinator._room_state_switch = mock_switch
   with patch.object(coordinator, "_is_mode_sync_on", return_value=False):
     await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES))
   mock_switch.async_turn_on.assert_not_awaited()
 
 
 async def test_start_override_calls_wifi_when_credentials_present(hass: HomeAssistant, coordinator: RoomCalCoordinator):
-  coordinator._guest_mode_switch = MagicMock()
-  coordinator._guest_mode_switch.async_turn_on = AsyncMock()
+  coordinator._room_state_switch = MagicMock()
+  coordinator._room_state_switch.async_turn_on = AsyncMock()
   with (
     patch.object(coordinator, "_is_mode_sync_on", return_value=True),
     patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi,
@@ -484,7 +484,7 @@ async def test_start_override_calls_wifi_when_credentials_present(hass: HomeAssi
 
 
 async def test_start_override_skips_wifi_when_mode_sync_off(coordinator: RoomCalCoordinator):
-  coordinator._guest_mode_switch = None
+  coordinator._room_state_switch = None
   with (
     patch.object(coordinator, "_is_mode_sync_on", return_value=False),
     patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi,
@@ -494,7 +494,7 @@ async def test_start_override_skips_wifi_when_mode_sync_off(coordinator: RoomCal
 
 
 async def test_start_override_skips_wifi_when_ssid_missing(coordinator: RoomCalCoordinator):
-  coordinator._guest_mode_switch = None
+  coordinator._room_state_switch = None
   no_ssid = ReservationData(wifi_ssid=None, wifi_key="secret")
   with patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi:
     await coordinator.async_start_override(OverrideData(current=no_ssid))
@@ -503,14 +503,14 @@ async def test_start_override_skips_wifi_when_ssid_missing(coordinator: RoomCalC
 
 async def test_start_override_uses_store_next_when_override_next_is_none(coordinator: RoomCalCoordinator):
   await coordinator.store.async_set(STORE_NEXT, SAMPLE)
-  coordinator._guest_mode_switch = None
+  coordinator._room_state_switch = None
   await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES))
   assert coordinator.data.next.guest_name == "Alice"
 
 
 async def test_start_override_uses_override_next_when_provided(coordinator: RoomCalCoordinator):
   override_next = ReservationData(guest_name="Override Next")
-  coordinator._guest_mode_switch = None
+  coordinator._room_state_switch = None
   await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES, next=override_next))
   assert coordinator.data.next.guest_name == "Override Next"
 
@@ -601,7 +601,7 @@ async def test_start_override_applies_defaults_to_store_next(hass: HomeAssistant
 
 
 async def test_end_override_clears_override_active(coordinator: RoomCalCoordinator):
-  coordinator._guest_mode_switch = None
+  coordinator._room_state_switch = None
   coordinator._override = OverrideData(current=_OVERRIDE_RES)
   await coordinator.async_end_override()
   assert coordinator.data.override_active is False
@@ -609,7 +609,7 @@ async def test_end_override_clears_override_active(coordinator: RoomCalCoordinat
 
 async def test_end_override_restores_store_current(coordinator: RoomCalCoordinator):
   await coordinator.store.async_set(STORE_CURRENT, SAMPLE)
-  coordinator._guest_mode_switch = None
+  coordinator._room_state_switch = None
   coordinator._override = OverrideData(current=_OVERRIDE_RES)
   await coordinator.async_end_override()
   assert coordinator.data.current.guest_name == "Alice"
@@ -709,7 +709,7 @@ async def test_reconcile_active_current_turns_on_guest_mode(coordinator: RoomCal
   mock_switch = MagicMock()
   mock_switch.async_turn_on = AsyncMock()
   mock_switch.async_turn_off = AsyncMock()
-  coordinator._guest_mode_switch = mock_switch
+  coordinator._room_state_switch = mock_switch
 
   with (
     patch.object(coordinator, "_is_mode_sync_on", return_value=True),
@@ -727,7 +727,7 @@ async def test_reconcile_elapsed_current_clears_store_and_turns_off_guest_mode(c
   mock_switch = MagicMock()
   mock_switch.async_turn_on = AsyncMock()
   mock_switch.async_turn_off = AsyncMock()
-  coordinator._guest_mode_switch = mock_switch
+  coordinator._room_state_switch = mock_switch
 
   with (
     patch.object(coordinator, "_is_mode_sync_on", return_value=True),
@@ -757,7 +757,7 @@ async def test_reconcile_does_not_touch_guest_mode_when_mode_sync_off(coordinato
   mock_switch = MagicMock()
   mock_switch.async_turn_on = AsyncMock()
   mock_switch.async_turn_off = AsyncMock()
-  coordinator._guest_mode_switch = mock_switch
+  coordinator._room_state_switch = mock_switch
 
   with (
     patch.object(coordinator, "_is_mode_sync_on", return_value=False),

+ 17 - 17
tests/test_switches.py

@@ -1,4 +1,4 @@
-"""Unit tests for GuestModeSwitch, ModeSyncSwitch, and OverrideReservationSwitch.
+"""Unit tests for RoomStateSwitch, ModeSyncSwitch, and OverrideReservationSwitch.
 
 Uses object.__new__ to bypass HA entity setup. async_write_ha_state is
 replaced with a MagicMock to avoid needing a real HA instance.
@@ -7,11 +7,11 @@ from unittest.mock import AsyncMock, MagicMock, patch
 
 import pytest
 
-from custom_components.room_cal_ha.switch import GuestModeSwitch, ModeSyncSwitch, OverrideReservationSwitch, UseUnifiSwitch
+from custom_components.room_cal_ha.switch import RoomStateSwitch, ModeSyncSwitch, OverrideReservationSwitch, UseUnifiSwitch
 
 
-def _guest_mode():
-  s = object.__new__(GuestModeSwitch)
+def _room_state():
+  s = object.__new__(RoomStateSwitch)
   s._state = False
   s._deferred = True
   s._coordinator = MagicMock()
@@ -29,23 +29,23 @@ def _mode_sync():
 
 
 # ---------------------------------------------------------------------------
-# GuestModeSwitch — basic state
+# RoomStateSwitch — basic state
 # ---------------------------------------------------------------------------
 
-async def test_guest_mode_initial_state_is_false():
-  s = _guest_mode()
+async def test_room_state_initial_state_is_false():
+  s = _room_state()
   assert s.is_on is False
 
 
-async def test_guest_mode_turn_on_sets_state():
-  s = _guest_mode()
+async def test_room_state_turn_on_sets_state():
+  s = _room_state()
   s._deferred = False
   await s.async_turn_on()
   assert s.is_on is True
 
 
-async def test_guest_mode_turn_off_sets_state():
-  s = _guest_mode()
+async def test_room_state_turn_off_sets_state():
+  s = _room_state()
   s._state = True
   s._deferred = False
   await s.async_turn_off()
@@ -53,23 +53,23 @@ async def test_guest_mode_turn_off_sets_state():
 
 
 # ---------------------------------------------------------------------------
-# GuestModeSwitch — deferred write behavior
+# RoomStateSwitch — deferred write behavior
 # ---------------------------------------------------------------------------
 
-async def test_guest_mode_deferred_suppresses_write():
-  s = _guest_mode()
+async def test_room_state_deferred_suppresses_write():
+  s = _room_state()
   assert s._deferred is True
   s.async_write_ha_state()  # should be a no-op when deferred
   # The real async_write_ha_state is replaced; calling it when deferred returns early.
   # We verify by re-implementing the check:
   with patch.object(type(s).__bases__[0], "async_write_ha_state") as mock_parent:
-    s.async_write_ha_state = GuestModeSwitch.async_write_ha_state.__get__(s, type(s))
+    s.async_write_ha_state = RoomStateSwitch.async_write_ha_state.__get__(s, type(s))
     s.async_write_ha_state()
     mock_parent.assert_not_called()
 
 
-def test_guest_mode_commit_state_unblocks_and_writes():
-  s = _guest_mode()
+def test_room_state_commit_state_unblocks_and_writes():
+  s = _room_state()
   s._state = True
   real_write = MagicMock()