from datetime import date, datetime, time from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.room_cal_ha.const import ( CONF_ZLOCK_ENTRIES, CONF_ZWAVE_SLOT, DOMAIN, STORE_CURRENT, STORE_NEXT, ) from custom_components.room_cal_ha.coordinator import RoomCalCoordinator from custom_components.room_cal_ha.models import ReservationData _BASE_DATA = { "room_state_name": "Room State", "current_name": "Current", "next_name": "Next", } # Freeze time at the check-in moment so the window registers as active during check-in tests. _AT_CHECK_IN = MagicMock() _AT_CHECK_IN.replace.return_value = datetime(2026, 7, 1, 14, 0, 0) _ZWAVE_RES = ReservationData( guest_name="Bob", check_in_date=date(2026, 7, 1), check_in_time=time(14, 0), check_out_date=date(2026, 7, 5), check_out_time=time(12, 0), door_code="7890", ) def _make_entry(hass, options: dict) -> MockConfigEntry: entry = MockConfigEntry(domain=DOMAIN, data=_BASE_DATA, options=options) entry.add_to_hass(hass) return entry 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(hass: HomeAssistant, mock_entry): mock_entry.add_to_hass(hass) coord = RoomCalCoordinator(hass, mock_entry) await coord.async_setup() return coord @pytest.fixture async def coordinator_with_locks(hass: HomeAssistant): 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 # --------------------------------------------------------------------------- # Check-in Z-Wave lock behavior # --------------------------------------------------------------------------- async def test_check_in_sets_lock_codes(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() coordinator_with_locks._room_state_switch = mock_switch 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.coordinator.dt_util.now", return_value=_AT_CHECK_IN), patch("custom_components.room_cal_ha.locks._async_call_service", new_callable=AsyncMock) as mock_call, ): await coordinator_with_locks._async_do_check_in() coordinator_with_locks.cancel_mode_sync() mock_call.assert_any_await(hass, "zlock_slots", "set_code", {"lock_entity_id": "lock.front_door", "slot": 5, "code": "7890"}) mock_call.assert_any_await(hass, "zlock_slots", "set_slot_enabled", {"lock_entity_id": "lock.front_door", "slot": 5, "enabled": True}) mock_switch.async_turn_on.assert_awaited_once() async def test_check_in_sets_codes_on_all_locks(hass: HomeAssistant): 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) mock_switch = MagicMock() mock_switch.async_turn_on = AsyncMock() coord._room_state_switch = mock_switch with ( 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.locks._async_call_service", new_callable=AsyncMock) as mock_call, ): await coord._async_do_check_in() coord.cancel_mode_sync() mock_call.assert_any_await(hass, "zlock_slots", "set_code", {"lock_entity_id": "lock.front", "slot": 5, "code": "7890"}) mock_call.assert_any_await(hass, "zlock_slots", "set_code", {"lock_entity_id": "lock.back", "slot": 5, "code": "7890"}) mock_call.assert_any_await(hass, "zlock_slots", "set_slot_enabled", {"lock_entity_id": "lock.front", "slot": 5, "enabled": True}) mock_call.assert_any_await(hass, "zlock_slots", "set_slot_enabled", {"lock_entity_id": "lock.back", "slot": 5, "enabled": True}) async def test_check_in_skips_lock_codes_when_door_code_none(hass: HomeAssistant, coordinator_with_locks: RoomCalCoordinator): no_code = _ZWAVE_RES.__class__( guest_name="Bob", check_in_date=_ZWAVE_RES.check_in_date, check_in_time=_ZWAVE_RES.check_in_time, check_out_date=_ZWAVE_RES.check_out_date, check_out_time=_ZWAVE_RES.check_out_time, door_code=None, ) await coordinator_with_locks.store.async_set(STORE_NEXT, no_code) 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=True), patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_AT_CHECK_IN), patch("custom_components.room_cal_ha.locks._async_call_service", new_callable=AsyncMock) as mock_call, ): await coordinator_with_locks._async_do_check_in() coordinator_with_locks.cancel_mode_sync() assert not any( c.args[1:3] == ("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): await coordinator_with_locks.store.async_set(STORE_NEXT, _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.coordinator.dt_util.now", return_value=_AT_CHECK_IN), patch("custom_components.room_cal_ha.locks._async_call_service", new_callable=AsyncMock) as mock_call, ): await coordinator_with_locks._async_do_check_in() coordinator_with_locks.cancel_mode_sync() mock_call.assert_not_awaited() async def test_check_in_skips_lock_codes_when_no_locks_configured(hass: HomeAssistant, coordinator: RoomCalCoordinator): # coordinator fixture has no CONF_ZLOCK_ENTRIES in options await coordinator.store.async_set(STORE_NEXT, _ZWAVE_RES) coordinator._room_state_switch = None with ( 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.locks._async_call_service", new_callable=AsyncMock) as mock_call, ): await coordinator._async_do_check_in() coordinator.cancel_mode_sync() mock_call.assert_not_awaited() 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() coordinator_with_locks._room_state_switch = mock_switch 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.coordinator.dt_util.now", return_value=_AT_CHECK_IN), patch("custom_components.room_cal_ha.locks._async_call_service", side_effect=HomeAssistantError("lock offline")), ): await coordinator_with_locks._async_do_check_in() coordinator_with_locks.cancel_mode_sync() mock_switch.async_turn_on.assert_awaited_once() # --------------------------------------------------------------------------- # Check-out Z-Wave lock behavior # --------------------------------------------------------------------------- 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() coordinator_with_locks._room_state_switch = mock_switch 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.coordinator.dt_util.now", return_value=_AT_CHECK_IN), patch("custom_components.room_cal_ha.locks._async_call_service", new_callable=AsyncMock) as mock_call, ): await coordinator_with_locks._async_do_check_out() mock_call.assert_awaited_once_with(hass, "zlock_slots", "set_slot_enabled", {"lock_entity_id": "lock.front_door", "slot": 5, "enabled": False}) mock_switch.async_turn_off.assert_awaited_once() 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.locks._async_call_service", new_callable=AsyncMock) as mock_call, ): await coordinator_with_locks._async_do_check_out() mock_call.assert_not_awaited() 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() coordinator_with_locks._room_state_switch = mock_switch 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.coordinator.dt_util.now", return_value=_AT_CHECK_IN), patch("custom_components.room_cal_ha.locks._async_call_service", side_effect=HomeAssistantError("lock offline")), ): await coordinator_with_locks._async_do_check_out() mock_switch.async_turn_off.assert_awaited_once()