| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- 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.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.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()
|