Răsfoiți Sursa

Delegate lock management to zlock_slots integration

Replace direct Z-Wave JS calls with zlock_slots service calls. Config flow
now selects zlock_slots config entries instead of lock entities directly.
Check-in calls set_code + set_slot_enabled(true); checkout disables the
slot without clearing the code. Switch renamed to "Use zLock Slots".
ZiB 4 luni în urmă
părinte
comite
4bad2ba21e

+ 6 - 6
custom_components/room_cal_ha/config_flow.py

@@ -10,8 +10,8 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResu
 from homeassistant.core import callback
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
 from homeassistant.helpers.selector import (
-  EntitySelector,
-  EntitySelectorConfig,
+  ConfigEntrySelector,
+  ConfigEntrySelectorConfig,
   NumberSelector,
   NumberSelectorConfig,
   NumberSelectorMode,
@@ -34,7 +34,7 @@ from .const import (
   CONF_ROOM_NAME,
   CONF_SYNC_MODE,
   CONF_USERNAME,
-  CONF_ZWAVE_LOCKS,
+  CONF_ZLOCK_ENTRIES,
   CONF_ZWAVE_SLOT,
   DEFAULT_REFRESH_INTERVAL,
   DOMAIN,
@@ -342,7 +342,7 @@ class RoomCalOptionsFlow(OptionsFlow):
     self, user_input: dict[str, Any] | None = None
   ) -> ConfigFlowResult:
     if user_input is not None:
-      self._pending_options[CONF_ZWAVE_LOCKS] = user_input.get(CONF_ZWAVE_LOCKS, [])
+      self._pending_options[CONF_ZLOCK_ENTRIES] = user_input.get(CONF_ZLOCK_ENTRIES, [])
       slot = user_input.get(CONF_ZWAVE_SLOT)
       self._pending_options[CONF_ZWAVE_SLOT] = int(slot) if slot is not None else None
       return self.async_create_entry(data=self._pending_options)
@@ -352,8 +352,8 @@ class RoomCalOptionsFlow(OptionsFlow):
     return self.async_show_form(
       step_id="locks",
       data_schema=vol.Schema({
-        vol.Optional(CONF_ZWAVE_LOCKS, default=opts.get(CONF_ZWAVE_LOCKS, [])): EntitySelector(
-          EntitySelectorConfig(domain="lock", multiple=True)
+        vol.Optional(CONF_ZLOCK_ENTRIES, default=opts.get(CONF_ZLOCK_ENTRIES, [])): ConfigEntrySelector(
+          ConfigEntrySelectorConfig(integration="zlock_slots")
         ),
         vol.Optional(CONF_ZWAVE_SLOT, default=opts.get(CONF_ZWAVE_SLOT, 1)): NumberSelector(
           NumberSelectorConfig(min=1, max=250, mode=NumberSelectorMode.BOX)

+ 3 - 3
custom_components/room_cal_ha/const.py

@@ -18,9 +18,9 @@ CONF_RESERVATION_DEFAULTS = "reservation_defaults"
 # entry.data keys (sensitive)
 CONF_API_TOKEN = "api_token"
 
-# Z-Wave lock options
-CONF_ZWAVE_LOCKS = "zwave_locks"   # list[str] of lock entity_ids
-CONF_ZWAVE_SLOT = "zwave_slot"     # int — shared code slot for all locks
+# zLock Slots options
+CONF_ZLOCK_ENTRIES = "zlock_entries"  # list[str] of zlock_slots config entry IDs
+CONF_ZWAVE_SLOT = "zwave_slot"        # int — shared code slot for all locks
 
 # Sync mode values
 SYNC_MODE_CALDAV = "CalDAV"

+ 35 - 15
custom_components/room_cal_ha/coordinator.py

@@ -16,7 +16,7 @@ from .const import (
   CONF_REFRESH_INTERVAL,
   CONF_RESERVATION_DEFAULTS,
   CONF_SYNC_MODE,
-  CONF_ZWAVE_LOCKS,
+  CONF_ZLOCK_ENTRIES,
   CONF_ZWAVE_SLOT,
   DEFAULT_REFRESH_INTERVAL,
   DOMAIN,
@@ -237,31 +237,51 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
       await self._async_clear_lock_codes()
     await self._async_set_room_state(False)
 
+  def _zlock_entity_ids(self) -> list[str]:
+    """Return all lock entity IDs from the configured zlock_slots entries."""
+    entity_ids: list[str] = []
+    for zlock_entry_id in self.entry.options.get(CONF_ZLOCK_ENTRIES, []):
+      zlock_entry = self.hass.config_entries.async_get_entry(zlock_entry_id)
+      if zlock_entry is not None:
+        entity_ids.extend(zlock_entry.data.get("lock_entity_ids", []))
+    return entity_ids
+
   async def _async_push_lock_codes(self, code: str) -> None:
-    from .zwave_lock import async_set_lock_usercode
-    locks: list[str] = self.entry.options.get(CONF_ZWAVE_LOCKS, [])
     slot = self.entry.options.get(CONF_ZWAVE_SLOT)
-    if not locks or slot is None:
+    lock_entity_ids = self._zlock_entity_ids()
+    if not lock_entity_ids or slot is None:
       return
     slot = int(slot)
-    for entity_id in locks:
+    for entity_id in lock_entity_ids:
       try:
-        await async_set_lock_usercode(self.hass, entity_id, slot, code)
+        await self.hass.services.async_call(
+          "zlock_slots", "set_code",
+          {"lock_entity_id": entity_id, "slot": slot, "code": code},
+          blocking=True,
+        )
+        await self.hass.services.async_call(
+          "zlock_slots", "set_slot_enabled",
+          {"lock_entity_id": entity_id, "slot": slot, "enabled": True},
+          blocking=True,
+        )
       except Exception as err:
-        _LOGGER.warning("Failed to set lock usercode on '%s': %s", entity_id, err)
+        _LOGGER.warning("Failed to set lock code on '%s': %s", entity_id, err)
 
   async def _async_clear_lock_codes(self) -> None:
-    from .zwave_lock import async_clear_lock_usercode
-    locks: list[str] = self.entry.options.get(CONF_ZWAVE_LOCKS, [])
     slot = self.entry.options.get(CONF_ZWAVE_SLOT)
-    if not locks or slot is None:
+    lock_entity_ids = self._zlock_entity_ids()
+    if not lock_entity_ids or slot is None:
       return
     slot = int(slot)
-    for entity_id in locks:
+    for entity_id in lock_entity_ids:
       try:
-        await async_clear_lock_usercode(self.hass, entity_id, slot)
+        await self.hass.services.async_call(
+          "zlock_slots", "set_slot_enabled",
+          {"lock_entity_id": entity_id, "slot": slot, "enabled": False},
+          blocking=True,
+        )
       except Exception as err:
-        _LOGGER.warning("Failed to clear lock usercode on '%s': %s", entity_id, err)
+        _LOGGER.warning("Failed to disable lock slot on '%s': %s", entity_id, err)
 
   async def async_reconcile_from_store(self, *, _startup: bool = False) -> None:
     """Load store state, apply elapsed transitions, set room state, reschedule mode sync.
@@ -321,9 +341,9 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
     return True  # safe default: don't silently skip WiFi if switch isn't found yet
 
   def _is_use_zwave_on(self) -> bool:
-    from .switch import UseZwaveLockSwitch  # avoid circular at module level
+    from .switch import UseZlockSlotsSwitch  # avoid circular at module level
     for entity in self.hass.data.get(DOMAIN, {}).get(self.entry.entry_id, {}).get("switches", []):
-      if isinstance(entity, UseZwaveLockSwitch):
+      if isinstance(entity, UseZlockSlotsSwitch):
         return entity.is_on
     return False  # safe default: don't push codes if switch isn't found yet
 

+ 4 - 4
custom_components/room_cal_ha/strings.json

@@ -38,7 +38,7 @@
         "menu_options": {
           "sync_mode": "Sync Mode & Connection",
           "defaults": "Reservation Defaults",
-          "locks": "Z-Wave Locks"
+          "locks": "zLock Slots"
         }
       },
       "sync_mode": {
@@ -78,10 +78,10 @@
         }
       },
       "locks": {
-        "title": "Z-Wave Locks",
-        "description": "Select lock entities and the code slot to use for guest door codes. All selected locks share the same slot number. Leave empty to skip Z-Wave lock management.",
+        "title": "zLock Slots",
+        "description": "Select zLock Slots integrations and the code slot to use for guest door codes. All selected locks share the same slot number. Leave empty to skip lock management.",
         "data": {
-          "zwave_locks": "Lock Entities",
+          "zlock_entries": "zLock Slots Integrations",
           "zwave_slot": "Code Slot"
         }
       }

+ 7 - 7
custom_components/room_cal_ha/switch.py

@@ -8,7 +8,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.restore_state import RestoreEntity
 from homeassistant.helpers.update_coordinator import CoordinatorEntity
 
-from .const import CONF_ROOM_NAME, DOMAIN, CONF_ZWAVE_LOCKS
+from .const import CONF_ROOM_NAME, DOMAIN, CONF_ZLOCK_ENTRIES
 from .coordinator import RoomCalCoordinator
 
 
@@ -24,7 +24,7 @@ async def async_setup_entry(
   mode_sync = ModeSyncSwitch(coordinator, entry, device_name)
   override = OverrideReservationSwitch(coordinator, entry, device_name)
   use_unifi = UseUnifiSwitch(coordinator, entry, device_name)
-  use_zwave = UseZwaveLockSwitch(coordinator, entry, device_name)
+  use_zwave = UseZlockSlotsSwitch(coordinator, entry, device_name)
 
   coordinator.inject_room_state_switch(guest_mode)
 
@@ -110,18 +110,18 @@ class UseUnifiSwitch(_SwitchBase):
     self._attr_unique_id = f"{entry.entry_id}_use_unifi"
 
 
-class UseZwaveLockSwitch(_SwitchBase):
-  _attr_name = "Use Z-Wave Locks"
+class UseZlockSlotsSwitch(_SwitchBase):
+  _attr_name = "Use zLock Slots"
   _attr_icon = "mdi:lock-smart"
-  _state: bool = False  # default off until locks are configured
+  _state: bool = False  # default off until zlock entries are configured
 
   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}_use_zwave_locks"
+    self._attr_unique_id = f"{entry.entry_id}_use_zlock_slots"
 
   @property
   def available(self) -> bool:
-    return bool(self._entry.options.get(CONF_ZWAVE_LOCKS))
+    return bool(self._entry.options.get(CONF_ZLOCK_ENTRIES))
 
 
 class OverrideReservationSwitch(CoordinatorEntity[RoomCalCoordinator], SwitchEntity):

+ 4 - 4
custom_components/room_cal_ha/translations/en.json

@@ -38,7 +38,7 @@
         "menu_options": {
           "sync_mode": "Sync Mode & Connection",
           "defaults": "Reservation Defaults",
-          "locks": "Z-Wave Locks"
+          "locks": "zLock Slots"
         }
       },
       "sync_mode": {
@@ -78,10 +78,10 @@
         }
       },
       "locks": {
-        "title": "Z-Wave Locks",
-        "description": "Select lock entities and the code slot to use for guest door codes. All selected locks share the same slot number. Leave empty to skip Z-Wave lock management.",
+        "title": "zLock Slots",
+        "description": "Select zLock Slots integrations and the code slot to use for guest door codes. All selected locks share the same slot number. Leave empty to skip lock management.",
         "data": {
-          "zwave_locks": "Lock Entities",
+          "zlock_entries": "zLock Slots Integrations",
           "zwave_slot": "Code Slot"
         }
       }

+ 0 - 41
custom_components/room_cal_ha/zwave_lock.py

@@ -1,41 +0,0 @@
-"""Z-Wave JS lock helpers — set and clear user codes by slot number."""
-from __future__ import annotations
-
-from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import HomeAssistantError
-
-ZWAVE_JS_DOMAIN = "zwave_js"
-
-
-async def async_set_lock_usercode(
-  hass: HomeAssistant, entity_id: str, slot: int, code: str
-) -> None:
-  """Set a user code on a Z-Wave lock entity."""
-  try:
-    await hass.services.async_call(
-      ZWAVE_JS_DOMAIN,
-      "set_lock_usercode",
-      {"entity_id": entity_id, "code_slot": slot, "usercode": code},
-      blocking=True,
-    )
-  except Exception as err:
-    raise HomeAssistantError(
-      f"Failed to set lock usercode on '{entity_id}' slot {slot}: {err}"
-    ) from err
-
-
-async def async_clear_lock_usercode(
-  hass: HomeAssistant, entity_id: str, slot: int
-) -> None:
-  """Clear a user code slot on a Z-Wave lock entity."""
-  try:
-    await hass.services.async_call(
-      ZWAVE_JS_DOMAIN,
-      "clear_lock_usercode",
-      {"entity_id": entity_id, "code_slot": slot},
-      blocking=True,
-    )
-  except Exception as err:
-    raise HomeAssistantError(
-      f"Failed to clear lock usercode on '{entity_id}' slot {slot}: {err}"
-    ) from err

+ 5 - 5
tests/test_config_flow.py

@@ -16,7 +16,7 @@ from custom_components.room_cal_ha.const import (
   CONF_ROOM_NAME,
   CONF_SYNC_MODE,
   CONF_USERNAME,
-  CONF_ZWAVE_LOCKS,
+  CONF_ZLOCK_ENTRIES,
   CONF_ZWAVE_SLOT,
   DEFAULT_REFRESH_INTERVAL,
   DOMAIN,
@@ -34,7 +34,7 @@ _EMPTY_DEFAULTS = {
   "door_code": "",
 }
 
-_EMPTY_LOCKS = {CONF_ZWAVE_LOCKS: [], CONF_ZWAVE_SLOT: 1}
+_EMPTY_LOCKS = {CONF_ZLOCK_ENTRIES: [], CONF_ZWAVE_SLOT: 1}
 
 
 async def _options_init(hass: HomeAssistant, mock_entry):
@@ -393,10 +393,10 @@ async def test_options_locks_saves_entities_and_slot(hass: HomeAssistant, mock_e
 
   result = await hass.config_entries.options.async_configure(
     result["flow_id"],
-    user_input={CONF_ZWAVE_LOCKS: ["lock.front_door", "lock.back_door"], CONF_ZWAVE_SLOT: 5},
+    user_input={CONF_ZLOCK_ENTRIES: ["entry_abc", "entry_def"], CONF_ZWAVE_SLOT: 5},
   )
   assert result["type"] == "create_entry"
-  assert result["data"][CONF_ZWAVE_LOCKS] == ["lock.front_door", "lock.back_door"]
+  assert result["data"][CONF_ZLOCK_ENTRIES] == ["entry_abc", "entry_def"]
   assert result["data"][CONF_ZWAVE_SLOT] == 5
 
 
@@ -405,7 +405,7 @@ async def test_options_locks_empty_saves_empty_list(hass: HomeAssistant, mock_en
 
   result = await _submit_locks(hass, result["flow_id"])
   assert result["type"] == "create_entry"
-  assert result["data"][CONF_ZWAVE_LOCKS] == []
+  assert result["data"][CONF_ZLOCK_ENTRIES] == []
 
 
 # ---------------------------------------------------------------------------

+ 44 - 32
tests/test_coordinator.py

@@ -13,7 +13,7 @@ from custom_components.room_cal_ha.const import (
   CONF_CALENDAR_NAME,
   CONF_RESERVATION_DEFAULTS,
   CONF_SYNC_MODE,
-  CONF_ZWAVE_LOCKS,
+  CONF_ZLOCK_ENTRIES,
   CONF_ZWAVE_SLOT,
   DOMAIN,
   STORE_CURRENT,
@@ -866,9 +866,16 @@ async def test_unifi_diff_key_check_skipped_when_actual_key_none(coordinator):
   assert not diff.has_changes
 
 
+def _make_zlock_entry(hass, lock_entity_ids: list[str]) -> MockConfigEntry:
+  zlock = MockConfigEntry(domain="zlock_slots", data={"lock_entity_ids": lock_entity_ids})
+  zlock.add_to_hass(hass)
+  return zlock
+
+
 @pytest.fixture
 async def coordinator_with_locks(hass: HomeAssistant):
-  entry = _make_entry(hass, {CONF_ZWAVE_LOCKS: ["lock.front_door"], CONF_ZWAVE_SLOT: 5})
+  zlock = _make_zlock_entry(hass, ["lock.front_door"])
+  entry = _make_entry(hass, {CONF_ZLOCK_ENTRIES: [zlock.entry_id], CONF_ZWAVE_SLOT: 5})
   coord = RoomCalCoordinator(hass, entry)
   await coord.async_setup()
   return coord
@@ -905,17 +912,19 @@ async def test_check_in_sets_lock_codes(hass: HomeAssistant, coordinator_with_lo
     patch.object(coordinator_with_locks, "_is_mode_sync_on", return_value=True),
     patch.object(coordinator_with_locks, "_is_use_zwave_on", return_value=True),
     patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_AT_CHECK_IN),
-    patch("custom_components.room_cal_ha.zwave_lock.async_set_lock_usercode", new_callable=AsyncMock) as mock_set,
+    patch.object(hass.services, "async_call", new_callable=AsyncMock) as mock_call,
   ):
     await coordinator_with_locks._async_do_check_in()
   coordinator_with_locks.cancel_mode_sync()
 
-  mock_set.assert_awaited_once_with(hass, "lock.front_door", 5, "7890")
+  mock_call.assert_any_await("zlock_slots", "set_code", {"lock_entity_id": "lock.front_door", "slot": 5, "code": "7890"}, blocking=True)
+  mock_call.assert_any_await("zlock_slots", "set_slot_enabled", {"lock_entity_id": "lock.front_door", "slot": 5, "enabled": True}, blocking=True)
   mock_switch.async_turn_on.assert_awaited_once()
 
 
 async def test_check_in_sets_codes_on_all_locks(hass: HomeAssistant):
-  entry = _make_entry(hass, {CONF_ZWAVE_LOCKS: ["lock.front", "lock.back"], CONF_ZWAVE_SLOT: 5})
+  zlock = _make_zlock_entry(hass, ["lock.front", "lock.back"])
+  entry = _make_entry(hass, {CONF_ZLOCK_ENTRIES: [zlock.entry_id], CONF_ZWAVE_SLOT: 5})
   coord = RoomCalCoordinator(hass, entry)
   await coord.async_setup()
   await coord.store.async_set(STORE_NEXT, _ZWAVE_RES)
@@ -927,14 +936,15 @@ async def test_check_in_sets_codes_on_all_locks(hass: HomeAssistant):
     patch.object(coord, "_is_mode_sync_on", return_value=True),
     patch.object(coord, "_is_use_zwave_on", return_value=True),
     patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_AT_CHECK_IN),
-    patch("custom_components.room_cal_ha.zwave_lock.async_set_lock_usercode", new_callable=AsyncMock) as mock_set,
+    patch.object(hass.services, "async_call", new_callable=AsyncMock) as mock_call,
   ):
     await coord._async_do_check_in()
   coord.cancel_mode_sync()
 
-  assert mock_set.await_count == 2
-  mock_set.assert_any_await(hass, "lock.front", 5, "7890")
-  mock_set.assert_any_await(hass, "lock.back", 5, "7890")
+  mock_call.assert_any_await("zlock_slots", "set_code", {"lock_entity_id": "lock.front", "slot": 5, "code": "7890"}, blocking=True)
+  mock_call.assert_any_await("zlock_slots", "set_code", {"lock_entity_id": "lock.back", "slot": 5, "code": "7890"}, blocking=True)
+  mock_call.assert_any_await("zlock_slots", "set_slot_enabled", {"lock_entity_id": "lock.front", "slot": 5, "enabled": True}, blocking=True)
+  mock_call.assert_any_await("zlock_slots", "set_slot_enabled", {"lock_entity_id": "lock.back", "slot": 5, "enabled": True}, blocking=True)
 
 
 async def test_check_in_skips_lock_codes_when_door_code_none(hass: HomeAssistant, coordinator_with_locks: RoomCalCoordinator):
@@ -953,12 +963,14 @@ async def test_check_in_skips_lock_codes_when_door_code_none(hass: HomeAssistant
     patch.object(coordinator_with_locks, "_is_mode_sync_on", return_value=True),
     patch.object(coordinator_with_locks, "_is_use_zwave_on", return_value=True),
     patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_AT_CHECK_IN),
-    patch("custom_components.room_cal_ha.zwave_lock.async_set_lock_usercode", new_callable=AsyncMock) as mock_set,
+    patch.object(hass.services, "async_call", new_callable=AsyncMock) as mock_call,
   ):
     await coordinator_with_locks._async_do_check_in()
   coordinator_with_locks.cancel_mode_sync()
 
-  mock_set.assert_not_awaited()
+  assert not any(
+    c.args[:2] == ("zlock_slots", "set_code") for c in mock_call.await_args_list
+  )
 
 
 async def test_check_in_skips_lock_codes_when_use_zwave_off(hass: HomeAssistant, coordinator_with_locks: RoomCalCoordinator):
@@ -969,16 +981,18 @@ async def test_check_in_skips_lock_codes_when_use_zwave_off(hass: HomeAssistant,
     patch.object(coordinator_with_locks, "_is_mode_sync_on", return_value=True),
     patch.object(coordinator_with_locks, "_is_use_zwave_on", return_value=False),
     patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_AT_CHECK_IN),
-    patch("custom_components.room_cal_ha.zwave_lock.async_set_lock_usercode", new_callable=AsyncMock) as mock_set,
+    patch.object(hass.services, "async_call", new_callable=AsyncMock) as mock_call,
   ):
     await coordinator_with_locks._async_do_check_in()
   coordinator_with_locks.cancel_mode_sync()
 
-  mock_set.assert_not_awaited()
+  assert not any(
+    c.args[:1] == ("zlock_slots",) for c in mock_call.await_args_list
+  )
 
 
 async def test_check_in_skips_lock_codes_when_no_locks_configured(hass: HomeAssistant, coordinator: RoomCalCoordinator):
-  # coordinator fixture has no CONF_ZWAVE_LOCKS in options
+  # coordinator fixture has no CONF_ZLOCK_ENTRIES in options
   await coordinator.store.async_set(STORE_NEXT, _ZWAVE_RES)
   coordinator._room_state_switch = None
 
@@ -986,15 +1000,17 @@ async def test_check_in_skips_lock_codes_when_no_locks_configured(hass: HomeAssi
     patch.object(coordinator, "_is_mode_sync_on", return_value=True),
     patch.object(coordinator, "_is_use_zwave_on", return_value=True),
     patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_AT_CHECK_IN),
-    patch("custom_components.room_cal_ha.zwave_lock.async_set_lock_usercode", new_callable=AsyncMock) as mock_set,
+    patch.object(hass.services, "async_call", new_callable=AsyncMock) as mock_call,
   ):
     await coordinator._async_do_check_in()
   coordinator.cancel_mode_sync()
 
-  mock_set.assert_not_awaited()
+  assert not any(
+    c.args[:1] == ("zlock_slots",) for c in mock_call.await_args_list
+  )
 
 
-async def test_check_in_lock_error_still_activates_guest_mode(coordinator_with_locks: RoomCalCoordinator):
+async def test_check_in_lock_error_still_activates_guest_mode(hass: HomeAssistant, coordinator_with_locks: RoomCalCoordinator):
   await coordinator_with_locks.store.async_set(STORE_NEXT, _ZWAVE_RES)
   mock_switch = MagicMock()
   mock_switch.async_turn_on = AsyncMock()
@@ -1004,10 +1020,7 @@ async def test_check_in_lock_error_still_activates_guest_mode(coordinator_with_l
     patch.object(coordinator_with_locks, "_is_mode_sync_on", return_value=True),
     patch.object(coordinator_with_locks, "_is_use_zwave_on", return_value=True),
     patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_AT_CHECK_IN),
-    patch(
-      "custom_components.room_cal_ha.zwave_lock.async_set_lock_usercode",
-      side_effect=HomeAssistantError("lock offline"),
-    ),
+    patch.object(hass.services, "async_call", side_effect=HomeAssistantError("lock offline")),
   ):
     await coordinator_with_locks._async_do_check_in()
   coordinator_with_locks.cancel_mode_sync()
@@ -1019,7 +1032,7 @@ async def test_check_in_lock_error_still_activates_guest_mode(coordinator_with_l
 # Check-out Z-Wave lock behavior
 # ---------------------------------------------------------------------------
 
-async def test_check_out_clears_lock_codes(hass: HomeAssistant, coordinator_with_locks: RoomCalCoordinator):
+async def test_check_out_disables_lock_slot(hass: HomeAssistant, coordinator_with_locks: RoomCalCoordinator):
   await coordinator_with_locks.store.async_set(STORE_CURRENT, _ZWAVE_RES)
   mock_switch = MagicMock()
   mock_switch.async_turn_off = AsyncMock()
@@ -1028,29 +1041,31 @@ async def test_check_out_clears_lock_codes(hass: HomeAssistant, coordinator_with
   with (
     patch.object(coordinator_with_locks, "_is_mode_sync_on", return_value=True),
     patch.object(coordinator_with_locks, "_is_use_zwave_on", return_value=True),
-    patch("custom_components.room_cal_ha.zwave_lock.async_clear_lock_usercode", new_callable=AsyncMock) as mock_clear,
+    patch.object(hass.services, "async_call", new_callable=AsyncMock) as mock_call,
   ):
     await coordinator_with_locks._async_do_check_out()
 
-  mock_clear.assert_awaited_once_with(hass, "lock.front_door", 5)
+  mock_call.assert_awaited_once_with("zlock_slots", "set_slot_enabled", {"lock_entity_id": "lock.front_door", "slot": 5, "enabled": False}, blocking=True)
   mock_switch.async_turn_off.assert_awaited_once()
 
 
-async def test_check_out_skips_lock_codes_when_use_zwave_off(coordinator_with_locks: RoomCalCoordinator):
+async def test_check_out_skips_lock_slot_when_use_zwave_off(hass: HomeAssistant, coordinator_with_locks: RoomCalCoordinator):
   await coordinator_with_locks.store.async_set(STORE_CURRENT, _ZWAVE_RES)
   coordinator_with_locks._room_state_switch = None
 
   with (
     patch.object(coordinator_with_locks, "_is_mode_sync_on", return_value=True),
     patch.object(coordinator_with_locks, "_is_use_zwave_on", return_value=False),
-    patch("custom_components.room_cal_ha.zwave_lock.async_clear_lock_usercode", new_callable=AsyncMock) as mock_clear,
+    patch.object(hass.services, "async_call", new_callable=AsyncMock) as mock_call,
   ):
     await coordinator_with_locks._async_do_check_out()
 
-  mock_clear.assert_not_awaited()
+  assert not any(
+    c.args[:1] == ("zlock_slots",) for c in mock_call.await_args_list
+  )
 
 
-async def test_check_out_lock_error_still_deactivates_guest_mode(coordinator_with_locks: RoomCalCoordinator):
+async def test_check_out_lock_error_still_deactivates_guest_mode(hass: HomeAssistant, coordinator_with_locks: RoomCalCoordinator):
   await coordinator_with_locks.store.async_set(STORE_CURRENT, _ZWAVE_RES)
   mock_switch = MagicMock()
   mock_switch.async_turn_off = AsyncMock()
@@ -1059,10 +1074,7 @@ async def test_check_out_lock_error_still_deactivates_guest_mode(coordinator_wit
   with (
     patch.object(coordinator_with_locks, "_is_mode_sync_on", return_value=True),
     patch.object(coordinator_with_locks, "_is_use_zwave_on", return_value=True),
-    patch(
-      "custom_components.room_cal_ha.zwave_lock.async_clear_lock_usercode",
-      side_effect=HomeAssistantError("lock offline"),
-    ),
+    patch.object(hass.services, "async_call", side_effect=HomeAssistantError("lock offline")),
   ):
     await coordinator_with_locks._async_do_check_out()
 

+ 14 - 14
tests/test_switches.py

@@ -7,8 +7,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
 
 import pytest
 
-from custom_components.room_cal_ha.const import CONF_ZWAVE_LOCKS
-from custom_components.room_cal_ha.switch import RoomStateSwitch, ModeSyncSwitch, OverrideReservationSwitch, UseUnifiSwitch, UseZwaveLockSwitch
+from custom_components.room_cal_ha.const import CONF_ZLOCK_ENTRIES
+from custom_components.room_cal_ha.switch import RoomStateSwitch, ModeSyncSwitch, OverrideReservationSwitch, UseUnifiSwitch, UseZlockSlotsSwitch
 
 
 def _room_state():
@@ -177,34 +177,34 @@ async def test_use_unifi_turn_on_restores_true():
 
 
 # ---------------------------------------------------------------------------
-# UseZwaveLockSwitch
+# UseZlockSlotsSwitch
 # ---------------------------------------------------------------------------
 
-def _use_zwave(locks: list | None = None):
-  s = object.__new__(UseZwaveLockSwitch)
+def _use_zlock(entries: list | None = None):
+  s = object.__new__(UseZlockSlotsSwitch)
   s._coordinator = MagicMock()
   s.async_write_ha_state = MagicMock()
   entry = MagicMock()
-  entry.options = {CONF_ZWAVE_LOCKS: locks} if locks is not None else {}
+  entry.options = {CONF_ZLOCK_ENTRIES: entries} if entries is not None else {}
   s._entry = entry
   return s
 
 
-def test_use_zwave_default_state_is_false():
-  s = _use_zwave()
+def test_use_zlock_default_state_is_false():
+  s = _use_zlock()
   assert s.is_on is False
 
 
-def test_use_zwave_available_false_when_no_locks_configured():
-  assert _use_zwave(locks=[]).available is False
+def test_use_zlock_available_false_when_no_entries_configured():
+  assert _use_zlock(entries=[]).available is False
 
 
-def test_use_zwave_available_false_when_options_missing():
-  assert _use_zwave().available is False
+def test_use_zlock_available_false_when_options_missing():
+  assert _use_zlock().available is False
 
 
-def test_use_zwave_available_true_when_locks_configured():
-  assert _use_zwave(locks=["lock.front_door"]).available is True
+def test_use_zlock_available_true_when_entries_configured():
+  assert _use_zlock(entries=["abc123"]).available is True
 
 
 async def test_use_zwave_turn_on_sets_true():