test_locks.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. from datetime import date, datetime, time
  2. from unittest.mock import AsyncMock, MagicMock, patch
  3. import pytest
  4. from homeassistant.core import HomeAssistant
  5. from homeassistant.exceptions import HomeAssistantError
  6. from pytest_homeassistant_custom_component.common import MockConfigEntry
  7. from custom_components.room_cal_ha.const import (
  8. CONF_ZLOCK_ENTRIES,
  9. CONF_ZWAVE_SLOT,
  10. DOMAIN,
  11. STORE_CURRENT,
  12. STORE_NEXT,
  13. )
  14. from custom_components.room_cal_ha.coordinator import RoomCalCoordinator
  15. from custom_components.room_cal_ha.models import ReservationData
  16. _BASE_DATA = {
  17. "room_state_name": "Room State",
  18. "current_name": "Current",
  19. "next_name": "Next",
  20. }
  21. # Freeze time at the check-in moment so the window registers as active during check-in tests.
  22. _AT_CHECK_IN = MagicMock()
  23. _AT_CHECK_IN.replace.return_value = datetime(2026, 7, 1, 14, 0, 0)
  24. _ZWAVE_RES = ReservationData(
  25. guest_name="Bob",
  26. check_in_date=date(2026, 7, 1),
  27. check_in_time=time(14, 0),
  28. check_out_date=date(2026, 7, 5),
  29. check_out_time=time(12, 0),
  30. door_code="7890",
  31. )
  32. def _make_entry(hass, options: dict) -> MockConfigEntry:
  33. entry = MockConfigEntry(domain=DOMAIN, data=_BASE_DATA, options=options)
  34. entry.add_to_hass(hass)
  35. return entry
  36. def _make_zlock_entry(hass, lock_entity_ids: list[str]) -> MockConfigEntry:
  37. zlock = MockConfigEntry(domain="zlock_slots", data={"lock_entity_ids": lock_entity_ids})
  38. zlock.add_to_hass(hass)
  39. return zlock
  40. @pytest.fixture
  41. async def coordinator(hass: HomeAssistant, mock_entry):
  42. mock_entry.add_to_hass(hass)
  43. coord = RoomCalCoordinator(hass, mock_entry)
  44. await coord.async_setup()
  45. return coord
  46. @pytest.fixture
  47. async def coordinator_with_locks(hass: HomeAssistant):
  48. zlock = _make_zlock_entry(hass, ["lock.front_door"])
  49. entry = _make_entry(hass, {CONF_ZLOCK_ENTRIES: [zlock.entry_id], CONF_ZWAVE_SLOT: 5})
  50. coord = RoomCalCoordinator(hass, entry)
  51. await coord.async_setup()
  52. return coord
  53. # ---------------------------------------------------------------------------
  54. # Check-in Z-Wave lock behavior
  55. # ---------------------------------------------------------------------------
  56. async def test_check_in_sets_lock_codes(hass: HomeAssistant, coordinator_with_locks: RoomCalCoordinator):
  57. await coordinator_with_locks.store.async_set(STORE_NEXT, _ZWAVE_RES)
  58. mock_switch = MagicMock()
  59. mock_switch.async_turn_on = AsyncMock()
  60. coordinator_with_locks._room_state_switch = mock_switch
  61. with (
  62. patch.object(coordinator_with_locks, "_is_mode_sync_on", return_value=True),
  63. patch.object(coordinator_with_locks, "_is_use_zwave_on", return_value=True),
  64. patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_AT_CHECK_IN),
  65. patch("custom_components.room_cal_ha.locks._async_call_service", new_callable=AsyncMock) as mock_call,
  66. ):
  67. await coordinator_with_locks._async_do_check_in()
  68. coordinator_with_locks.cancel_mode_sync()
  69. mock_call.assert_any_await(hass, "zlock_slots", "set_code", {"lock_entity_id": "lock.front_door", "slot": 5, "code": "7890"})
  70. mock_call.assert_any_await(hass, "zlock_slots", "set_slot_enabled", {"lock_entity_id": "lock.front_door", "slot": 5, "enabled": True})
  71. mock_switch.async_turn_on.assert_awaited_once()
  72. async def test_check_in_sets_codes_on_all_locks(hass: HomeAssistant):
  73. zlock = _make_zlock_entry(hass, ["lock.front", "lock.back"])
  74. entry = _make_entry(hass, {CONF_ZLOCK_ENTRIES: [zlock.entry_id], CONF_ZWAVE_SLOT: 5})
  75. coord = RoomCalCoordinator(hass, entry)
  76. await coord.async_setup()
  77. await coord.store.async_set(STORE_NEXT, _ZWAVE_RES)
  78. mock_switch = MagicMock()
  79. mock_switch.async_turn_on = AsyncMock()
  80. coord._room_state_switch = mock_switch
  81. with (
  82. patch.object(coord, "_is_mode_sync_on", return_value=True),
  83. patch.object(coord, "_is_use_zwave_on", return_value=True),
  84. patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_AT_CHECK_IN),
  85. patch("custom_components.room_cal_ha.locks._async_call_service", new_callable=AsyncMock) as mock_call,
  86. ):
  87. await coord._async_do_check_in()
  88. coord.cancel_mode_sync()
  89. mock_call.assert_any_await(hass, "zlock_slots", "set_code", {"lock_entity_id": "lock.front", "slot": 5, "code": "7890"})
  90. mock_call.assert_any_await(hass, "zlock_slots", "set_code", {"lock_entity_id": "lock.back", "slot": 5, "code": "7890"})
  91. mock_call.assert_any_await(hass, "zlock_slots", "set_slot_enabled", {"lock_entity_id": "lock.front", "slot": 5, "enabled": True})
  92. mock_call.assert_any_await(hass, "zlock_slots", "set_slot_enabled", {"lock_entity_id": "lock.back", "slot": 5, "enabled": True})
  93. async def test_check_in_skips_lock_codes_when_door_code_none(hass: HomeAssistant, coordinator_with_locks: RoomCalCoordinator):
  94. no_code = _ZWAVE_RES.__class__(
  95. guest_name="Bob",
  96. check_in_date=_ZWAVE_RES.check_in_date,
  97. check_in_time=_ZWAVE_RES.check_in_time,
  98. check_out_date=_ZWAVE_RES.check_out_date,
  99. check_out_time=_ZWAVE_RES.check_out_time,
  100. door_code=None,
  101. )
  102. await coordinator_with_locks.store.async_set(STORE_NEXT, no_code)
  103. coordinator_with_locks._room_state_switch = None
  104. with (
  105. patch.object(coordinator_with_locks, "_is_mode_sync_on", return_value=True),
  106. patch.object(coordinator_with_locks, "_is_use_zwave_on", return_value=True),
  107. patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_AT_CHECK_IN),
  108. patch("custom_components.room_cal_ha.locks._async_call_service", new_callable=AsyncMock) as mock_call,
  109. ):
  110. await coordinator_with_locks._async_do_check_in()
  111. coordinator_with_locks.cancel_mode_sync()
  112. assert not any(
  113. c.args[1:3] == ("zlock_slots", "set_code") for c in mock_call.await_args_list
  114. )
  115. async def test_check_in_skips_lock_codes_when_use_zwave_off(hass: HomeAssistant, coordinator_with_locks: RoomCalCoordinator):
  116. await coordinator_with_locks.store.async_set(STORE_NEXT, _ZWAVE_RES)
  117. coordinator_with_locks._room_state_switch = None
  118. with (
  119. patch.object(coordinator_with_locks, "_is_mode_sync_on", return_value=True),
  120. patch.object(coordinator_with_locks, "_is_use_zwave_on", return_value=False),
  121. patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_AT_CHECK_IN),
  122. patch("custom_components.room_cal_ha.locks._async_call_service", new_callable=AsyncMock) as mock_call,
  123. ):
  124. await coordinator_with_locks._async_do_check_in()
  125. coordinator_with_locks.cancel_mode_sync()
  126. mock_call.assert_not_awaited()
  127. async def test_check_in_skips_lock_codes_when_no_locks_configured(hass: HomeAssistant, coordinator: RoomCalCoordinator):
  128. # coordinator fixture has no CONF_ZLOCK_ENTRIES in options
  129. await coordinator.store.async_set(STORE_NEXT, _ZWAVE_RES)
  130. coordinator._room_state_switch = None
  131. with (
  132. patch.object(coordinator, "_is_mode_sync_on", return_value=True),
  133. patch.object(coordinator, "_is_use_zwave_on", return_value=True),
  134. patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_AT_CHECK_IN),
  135. patch("custom_components.room_cal_ha.locks._async_call_service", new_callable=AsyncMock) as mock_call,
  136. ):
  137. await coordinator._async_do_check_in()
  138. coordinator.cancel_mode_sync()
  139. mock_call.assert_not_awaited()
  140. async def test_check_in_lock_error_still_activates_guest_mode(hass: HomeAssistant, coordinator_with_locks: RoomCalCoordinator):
  141. await coordinator_with_locks.store.async_set(STORE_NEXT, _ZWAVE_RES)
  142. mock_switch = MagicMock()
  143. mock_switch.async_turn_on = AsyncMock()
  144. coordinator_with_locks._room_state_switch = mock_switch
  145. with (
  146. patch.object(coordinator_with_locks, "_is_mode_sync_on", return_value=True),
  147. patch.object(coordinator_with_locks, "_is_use_zwave_on", return_value=True),
  148. patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_AT_CHECK_IN),
  149. patch("custom_components.room_cal_ha.locks._async_call_service", side_effect=HomeAssistantError("lock offline")),
  150. ):
  151. await coordinator_with_locks._async_do_check_in()
  152. coordinator_with_locks.cancel_mode_sync()
  153. mock_switch.async_turn_on.assert_awaited_once()
  154. # ---------------------------------------------------------------------------
  155. # Check-out Z-Wave lock behavior
  156. # ---------------------------------------------------------------------------
  157. async def test_check_out_disables_lock_slot(hass: HomeAssistant, coordinator_with_locks: RoomCalCoordinator):
  158. await coordinator_with_locks.store.async_set(STORE_CURRENT, _ZWAVE_RES)
  159. mock_switch = MagicMock()
  160. mock_switch.async_turn_off = AsyncMock()
  161. coordinator_with_locks._room_state_switch = mock_switch
  162. with (
  163. patch.object(coordinator_with_locks, "_is_mode_sync_on", return_value=True),
  164. patch.object(coordinator_with_locks, "_is_use_zwave_on", return_value=True),
  165. patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_AT_CHECK_IN),
  166. patch("custom_components.room_cal_ha.locks._async_call_service", new_callable=AsyncMock) as mock_call,
  167. ):
  168. await coordinator_with_locks._async_do_check_out()
  169. mock_call.assert_awaited_once_with(hass, "zlock_slots", "set_slot_enabled", {"lock_entity_id": "lock.front_door", "slot": 5, "enabled": False})
  170. mock_switch.async_turn_off.assert_awaited_once()
  171. async def test_check_out_skips_lock_slot_when_use_zwave_off(hass: HomeAssistant, coordinator_with_locks: RoomCalCoordinator):
  172. await coordinator_with_locks.store.async_set(STORE_CURRENT, _ZWAVE_RES)
  173. coordinator_with_locks._room_state_switch = None
  174. with (
  175. patch.object(coordinator_with_locks, "_is_mode_sync_on", return_value=True),
  176. patch.object(coordinator_with_locks, "_is_use_zwave_on", return_value=False),
  177. patch("custom_components.room_cal_ha.locks._async_call_service", new_callable=AsyncMock) as mock_call,
  178. ):
  179. await coordinator_with_locks._async_do_check_out()
  180. mock_call.assert_not_awaited()
  181. async def test_check_out_lock_error_still_deactivates_guest_mode(hass: HomeAssistant, coordinator_with_locks: RoomCalCoordinator):
  182. await coordinator_with_locks.store.async_set(STORE_CURRENT, _ZWAVE_RES)
  183. mock_switch = MagicMock()
  184. mock_switch.async_turn_off = AsyncMock()
  185. coordinator_with_locks._room_state_switch = mock_switch
  186. with (
  187. patch.object(coordinator_with_locks, "_is_mode_sync_on", return_value=True),
  188. patch.object(coordinator_with_locks, "_is_use_zwave_on", return_value=True),
  189. patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_AT_CHECK_IN),
  190. patch("custom_components.room_cal_ha.locks._async_call_service", side_effect=HomeAssistantError("lock offline")),
  191. ):
  192. await coordinator_with_locks._async_do_check_out()
  193. mock_switch.async_turn_off.assert_awaited_once()