test_coordinator.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  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.backends import BackendAuthError, BackendConnectError
  8. from custom_components.room_cal_ha.const import (
  9. CONF_API_BASE_URL,
  10. CONF_CALDAV_URL,
  11. CONF_CALENDAR_NAME,
  12. CONF_SYNC_MODE,
  13. DOMAIN,
  14. STORE_CURRENT,
  15. STORE_NEXT,
  16. SYNC_MODE_API,
  17. SYNC_MODE_CALDAV,
  18. SYNC_MODE_MANUAL,
  19. )
  20. from custom_components.room_cal_ha.coordinator import RoomCalCoordinator
  21. from custom_components.room_cal_ha.models import OverrideData, ReservationData
  22. SAMPLE = ReservationData(
  23. guest_name="Alice",
  24. check_in_date=date(2026, 7, 1),
  25. check_in_time=time(14, 0),
  26. check_out_date=date(2026, 7, 5),
  27. check_out_time=time(12, 0),
  28. wifi_key="stone-angel-market",
  29. door_code="0472",
  30. )
  31. _BASE_DATA = {
  32. "room_state_name": "Room State",
  33. "current_name": "Current",
  34. "next_name": "Next",
  35. }
  36. def _make_entry(hass, options: dict) -> MockConfigEntry:
  37. entry = MockConfigEntry(domain=DOMAIN, data=_BASE_DATA, options=options)
  38. entry.add_to_hass(hass)
  39. return entry
  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. # ---------------------------------------------------------------------------
  47. # Manual mode
  48. # ---------------------------------------------------------------------------
  49. async def test_manual_mode_serves_empty(coordinator: RoomCalCoordinator):
  50. await coordinator.async_refresh()
  51. assert coordinator.data.current == ReservationData()
  52. assert coordinator.data.next == ReservationData()
  53. assert coordinator.data.current_last_refresh is None
  54. assert coordinator.data.next_last_refresh is None
  55. # ---------------------------------------------------------------------------
  56. # CalDAV mode
  57. # ---------------------------------------------------------------------------
  58. async def test_caldav_fetch_writes_store_and_timestamps(hass: HomeAssistant):
  59. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com", CONF_CALENDAR_NAME: "Room"})
  60. coord = RoomCalCoordinator(hass, entry)
  61. await coord.async_setup()
  62. with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as MockBackend:
  63. MockBackend.return_value.async_fetch = AsyncMock(return_value=(SAMPLE, None))
  64. await coord.async_refresh()
  65. assert coord.data.current.guest_name == "Alice"
  66. assert coord.data.current.door_code == "0472"
  67. assert coord.data.next == ReservationData()
  68. assert coord.data.current_last_refresh is not None
  69. assert coord.data.next_last_refresh is not None
  70. async def test_caldav_empty_result_clears_store(hass: HomeAssistant):
  71. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com", CONF_CALENDAR_NAME: "Room"})
  72. coord = RoomCalCoordinator(hass, entry)
  73. await coord.async_setup()
  74. await coord.store.async_set(STORE_CURRENT, SAMPLE)
  75. with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as MockBackend:
  76. MockBackend.return_value.async_fetch = AsyncMock(return_value=(None, None))
  77. await coord.async_refresh()
  78. assert coord.data.current == ReservationData()
  79. assert coord.store.get(STORE_CURRENT) is None
  80. async def test_caldav_connect_error_preserves_store(hass: HomeAssistant):
  81. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com", CONF_CALENDAR_NAME: "Room"})
  82. coord = RoomCalCoordinator(hass, entry)
  83. await coord.async_setup()
  84. await coord.store.async_set(STORE_CURRENT, SAMPLE)
  85. with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as MockBackend:
  86. MockBackend.return_value.async_fetch = AsyncMock(side_effect=BackendConnectError("timeout"))
  87. await coord.async_refresh()
  88. assert coord.data.current.guest_name == "Alice"
  89. assert coord.data.current_last_refresh is None
  90. async def test_caldav_auth_error_triggers_reauth(hass: HomeAssistant):
  91. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com", CONF_CALENDAR_NAME: "Room"})
  92. coord = RoomCalCoordinator(hass, entry)
  93. await coord.async_setup()
  94. with (
  95. patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as MockBackend,
  96. patch.object(entry, "async_start_reauth") as mock_reauth,
  97. ):
  98. MockBackend.return_value.async_fetch = AsyncMock(side_effect=BackendAuthError)
  99. await coord.async_refresh()
  100. mock_reauth.assert_called_once_with(hass)
  101. # ---------------------------------------------------------------------------
  102. # API mode
  103. # ---------------------------------------------------------------------------
  104. async def test_api_fetch_writes_store(hass: HomeAssistant):
  105. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_API, CONF_API_BASE_URL: "https://api.example.com"})
  106. coord = RoomCalCoordinator(hass, entry)
  107. await coord.async_setup()
  108. with patch("custom_components.room_cal_ha.coordinator.RoomCalApiBackend") as MockBackend:
  109. instance = MockBackend.return_value
  110. instance.async_fetch = AsyncMock(return_value=(SAMPLE, None))
  111. await coord.async_refresh()
  112. assert coord.data.current.guest_name == "Alice"
  113. assert coord.data.current_last_refresh is not None
  114. async def test_api_connect_error_preserves_store(hass: HomeAssistant):
  115. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_API, CONF_API_BASE_URL: "https://api.example.com"})
  116. coord = RoomCalCoordinator(hass, entry)
  117. await coord.async_setup()
  118. await coord.store.async_set(STORE_CURRENT, SAMPLE)
  119. with patch("custom_components.room_cal_ha.coordinator.RoomCalApiBackend") as MockBackend:
  120. instance = MockBackend.return_value
  121. instance.async_fetch = AsyncMock(side_effect=BackendConnectError("timeout"))
  122. await coord.async_refresh()
  123. assert coord.data.current.guest_name == "Alice"
  124. assert coord.data.current_last_refresh is None
  125. async def test_api_auth_error_triggers_reauth(hass: HomeAssistant):
  126. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_API, CONF_API_BASE_URL: "https://api.example.com"})
  127. coord = RoomCalCoordinator(hass, entry)
  128. await coord.async_setup()
  129. with (
  130. patch("custom_components.room_cal_ha.coordinator.RoomCalApiBackend") as MockBackend,
  131. patch.object(entry, "async_start_reauth") as mock_reauth,
  132. ):
  133. instance = MockBackend.return_value
  134. instance.async_fetch = AsyncMock(side_effect=BackendAuthError)
  135. await coord.async_refresh()
  136. mock_reauth.assert_called_once_with(hass)
  137. # ---------------------------------------------------------------------------
  138. # Thin updates
  139. # ---------------------------------------------------------------------------
  140. async def test_thin_update_overlays_fields(coordinator: RoomCalCoordinator):
  141. await coordinator.store.async_set(STORE_CURRENT, SAMPLE)
  142. await coordinator.async_refresh()
  143. await coordinator.async_apply_update(STORE_CURRENT, {"wifi_ssid": "GuestNet", "door_code": "9999"})
  144. assert coordinator.data.current.wifi_ssid == "GuestNet"
  145. assert coordinator.data.current.door_code == "9999"
  146. assert coordinator.data.current.guest_name == "Alice"
  147. assert coordinator.data.current.wifi_key == "stone-angel-market"
  148. async def test_thin_update_updates_last_refresh(coordinator: RoomCalCoordinator):
  149. await coordinator.store.async_set(STORE_CURRENT, SAMPLE)
  150. await coordinator.async_refresh()
  151. assert coordinator.data.current_last_refresh is None
  152. await coordinator.async_apply_update(STORE_CURRENT, {"door_code": "1234"})
  153. assert coordinator.data.current_last_refresh is not None
  154. # ---------------------------------------------------------------------------
  155. # Store persistence
  156. # ---------------------------------------------------------------------------
  157. async def test_store_persists_across_reload(hass: HomeAssistant, mock_entry):
  158. mock_entry.add_to_hass(hass)
  159. coord = RoomCalCoordinator(hass, mock_entry)
  160. await coord.async_setup()
  161. await coord.store.async_set(STORE_CURRENT, SAMPLE)
  162. coord2 = RoomCalCoordinator(hass, mock_entry)
  163. await coord2.async_setup()
  164. loaded = coord2.store.get(STORE_CURRENT)
  165. assert loaded is not None
  166. assert loaded.guest_name == "Alice"
  167. assert loaded.door_code == "0472"
  168. # ---------------------------------------------------------------------------
  169. # _reservation_window_state
  170. # ---------------------------------------------------------------------------
  171. DATED = ReservationData(
  172. check_in_date=date(2026, 7, 1),
  173. check_in_time=time(14, 0),
  174. check_out_date=date(2026, 7, 5),
  175. check_out_time=time(12, 0),
  176. )
  177. def _state_at(coordinator, res, dt_str):
  178. """Freeze dt_util.now() to a naive local datetime and call _reservation_window_state."""
  179. frozen = MagicMock()
  180. frozen.replace.return_value = datetime.fromisoformat(dt_str) # naive, no TZ
  181. with patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=frozen):
  182. return coordinator._reservation_window_state(res)
  183. async def test_window_state_active(coordinator):
  184. assert _state_at(coordinator, DATED, "2026-07-03T10:00:00") == "active"
  185. async def test_window_state_pending(coordinator):
  186. assert _state_at(coordinator, DATED, "2026-06-30T10:00:00") == "pending"
  187. async def test_window_state_elapsed(coordinator):
  188. assert _state_at(coordinator, DATED, "2026-07-10T12:00:00") == "elapsed"
  189. async def test_window_state_at_check_in_is_active(coordinator):
  190. assert _state_at(coordinator, DATED, "2026-07-01T14:00:00") == "active"
  191. async def test_window_state_at_check_out_is_elapsed(coordinator):
  192. assert _state_at(coordinator, DATED, "2026-07-05T12:00:00") == "elapsed"
  193. async def test_window_state_no_dates_returns_none(coordinator):
  194. assert coordinator._reservation_window_state(ReservationData()) is None
  195. # ---------------------------------------------------------------------------
  196. # Check-in WiFi key behavior
  197. # ---------------------------------------------------------------------------
  198. _WIFI_NEXT = ReservationData(
  199. guest_name="Bob",
  200. check_in_date=date(2026, 7, 1),
  201. check_in_time=time(14, 0),
  202. check_out_date=date(2026, 7, 5),
  203. check_out_time=time(12, 0),
  204. wifi_ssid="GuestNet",
  205. wifi_key="securepass",
  206. )
  207. async def test_check_in_calls_wifi_key(hass: HomeAssistant, coordinator: RoomCalCoordinator):
  208. await coordinator.store.async_set(STORE_NEXT, _WIFI_NEXT)
  209. mock_switch = MagicMock()
  210. mock_switch.async_turn_on = AsyncMock()
  211. coordinator._guest_mode_switch = mock_switch
  212. with patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi:
  213. await coordinator._async_do_check_in()
  214. mock_wifi.assert_awaited_once_with(hass, "GuestNet", "securepass")
  215. mock_switch.async_turn_on.assert_awaited_once()
  216. async def test_check_in_skips_wifi_when_ssid_none(coordinator: RoomCalCoordinator):
  217. await coordinator.store.async_set(STORE_NEXT, ReservationData(wifi_ssid=None, wifi_key="securepass"))
  218. coordinator._guest_mode_switch = None
  219. with patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi:
  220. await coordinator._async_do_check_in()
  221. mock_wifi.assert_not_awaited()
  222. async def test_check_in_skips_wifi_when_key_none(coordinator: RoomCalCoordinator):
  223. await coordinator.store.async_set(STORE_NEXT, ReservationData(wifi_ssid="GuestNet", wifi_key=None))
  224. coordinator._guest_mode_switch = None
  225. with patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi:
  226. await coordinator._async_do_check_in()
  227. mock_wifi.assert_not_awaited()
  228. async def test_check_in_wifi_error_still_activates_guest_mode(coordinator: RoomCalCoordinator):
  229. await coordinator.store.async_set(STORE_NEXT, _WIFI_NEXT)
  230. mock_switch = MagicMock()
  231. mock_switch.async_turn_on = AsyncMock()
  232. coordinator._guest_mode_switch = mock_switch
  233. with patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", side_effect=HomeAssistantError("hub down")):
  234. await coordinator._async_do_check_in()
  235. mock_switch.async_turn_on.assert_awaited_once()
  236. # ---------------------------------------------------------------------------
  237. # async_start_override / async_end_override
  238. # ---------------------------------------------------------------------------
  239. _OVERRIDE_RES = ReservationData(guest_name="Override Guest", door_code="1111")
  240. _WIFI_OVERRIDE = ReservationData(guest_name="WiFi Guest", wifi_ssid="GuestNet", wifi_key="secret")
  241. async def test_start_override_sets_current_in_data(coordinator: RoomCalCoordinator):
  242. coordinator._guest_mode_switch = None
  243. await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES))
  244. assert coordinator.data.current.guest_name == "Override Guest"
  245. async def test_start_override_sets_override_active_flag(coordinator: RoomCalCoordinator):
  246. coordinator._guest_mode_switch = None
  247. await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES))
  248. assert coordinator.data.override_active is True
  249. async def test_start_override_cancels_mode_sync(coordinator: RoomCalCoordinator):
  250. cancel1, cancel2 = MagicMock(), MagicMock()
  251. coordinator._mode_sync_handles = [cancel1, cancel2]
  252. coordinator._guest_mode_switch = None
  253. await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES))
  254. cancel1.assert_called_once()
  255. cancel2.assert_called_once()
  256. assert coordinator._mode_sync_handles == []
  257. async def test_start_override_turns_on_guest_mode(coordinator: RoomCalCoordinator):
  258. mock_switch = MagicMock()
  259. mock_switch.async_turn_on = AsyncMock()
  260. coordinator._guest_mode_switch = mock_switch
  261. await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES))
  262. mock_switch.async_turn_on.assert_awaited_once()
  263. async def test_start_override_calls_wifi_when_credentials_present(hass: HomeAssistant, coordinator: RoomCalCoordinator):
  264. coordinator._guest_mode_switch = MagicMock()
  265. coordinator._guest_mode_switch.async_turn_on = AsyncMock()
  266. with patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi:
  267. await coordinator.async_start_override(OverrideData(current=_WIFI_OVERRIDE))
  268. mock_wifi.assert_awaited_once_with(hass, "GuestNet", "secret")
  269. async def test_start_override_skips_wifi_when_ssid_missing(coordinator: RoomCalCoordinator):
  270. coordinator._guest_mode_switch = None
  271. no_ssid = ReservationData(wifi_ssid=None, wifi_key="secret")
  272. with patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi:
  273. await coordinator.async_start_override(OverrideData(current=no_ssid))
  274. mock_wifi.assert_not_awaited()
  275. async def test_start_override_uses_store_next_when_override_next_is_none(coordinator: RoomCalCoordinator):
  276. await coordinator.store.async_set(STORE_NEXT, SAMPLE)
  277. coordinator._guest_mode_switch = None
  278. await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES))
  279. assert coordinator.data.next.guest_name == "Alice"
  280. async def test_start_override_uses_override_next_when_provided(coordinator: RoomCalCoordinator):
  281. override_next = ReservationData(guest_name="Override Next")
  282. coordinator._guest_mode_switch = None
  283. await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES, next=override_next))
  284. assert coordinator.data.next.guest_name == "Override Next"
  285. async def test_end_override_clears_override_active(coordinator: RoomCalCoordinator):
  286. coordinator._guest_mode_switch = None
  287. coordinator._override = OverrideData(current=_OVERRIDE_RES)
  288. await coordinator.async_end_override()
  289. assert coordinator.data.override_active is False
  290. async def test_end_override_restores_store_current(coordinator: RoomCalCoordinator):
  291. await coordinator.store.async_set(STORE_CURRENT, SAMPLE)
  292. coordinator._guest_mode_switch = None
  293. coordinator._override = OverrideData(current=_OVERRIDE_RES)
  294. await coordinator.async_end_override()
  295. assert coordinator.data.current.guest_name == "Alice"
  296. # ---------------------------------------------------------------------------
  297. # Thin updates during override
  298. # ---------------------------------------------------------------------------
  299. async def test_thin_update_current_during_override_updates_override_not_store(coordinator: RoomCalCoordinator):
  300. await coordinator.store.async_set(STORE_CURRENT, SAMPLE)
  301. await coordinator.async_refresh()
  302. coordinator._override = OverrideData(current=_OVERRIDE_RES)
  303. await coordinator.async_apply_update(STORE_CURRENT, {"door_code": "9999"})
  304. assert coordinator.data.current.door_code == "9999"
  305. # Store is unchanged
  306. assert coordinator.store.get(STORE_CURRENT).door_code == "0472"
  307. async def test_thin_update_current_during_override_data_still_shows_override_active(coordinator: RoomCalCoordinator):
  308. await coordinator.async_refresh()
  309. coordinator._override = OverrideData(current=_OVERRIDE_RES)
  310. await coordinator.async_apply_update(STORE_CURRENT, {"door_code": "9999"})
  311. assert coordinator.data.override_active is True
  312. async def test_thin_update_next_during_override_falls_through_to_store(coordinator: RoomCalCoordinator):
  313. coordinator._override = OverrideData(current=_OVERRIDE_RES) # override.next is None
  314. await coordinator.async_refresh()
  315. await coordinator.async_apply_update(STORE_NEXT, {"guest_name": "Next Guest"})
  316. assert coordinator.store.get(STORE_NEXT).guest_name == "Next Guest"
  317. assert coordinator.data.override_active is True
  318. assert coordinator.data.current.guest_name == "Override Guest"
  319. # ---------------------------------------------------------------------------
  320. # Backend fetch during override preserves override display
  321. # ---------------------------------------------------------------------------
  322. async def test_backend_fetch_during_override_updates_store_but_preserves_override(hass: HomeAssistant):
  323. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com", CONF_CALENDAR_NAME: "Room"})
  324. coord = RoomCalCoordinator(hass, entry)
  325. await coord.async_setup()
  326. coord._override = OverrideData(current=_OVERRIDE_RES)
  327. backend_data = ReservationData(guest_name="Backend Alice")
  328. with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as MockBackend:
  329. MockBackend.return_value.async_fetch = AsyncMock(return_value=(backend_data, None))
  330. await coord.async_refresh()
  331. # Store was written by the backend
  332. assert coord.store.get(STORE_CURRENT).guest_name == "Backend Alice"
  333. # But displayed data still shows the override
  334. assert coord.data.current.guest_name == "Override Guest"
  335. assert coord.data.override_active is True
  336. # ---------------------------------------------------------------------------
  337. # async_reconcile_from_store (shared helper)
  338. # ---------------------------------------------------------------------------
  339. _ACTIVE = ReservationData(
  340. guest_name="Active Guest",
  341. check_in_date=date(2026, 6, 20),
  342. check_in_time=time(14, 0),
  343. check_out_date=date(2026, 6, 25),
  344. check_out_time=time(12, 0),
  345. )
  346. _ELAPSED = ReservationData(
  347. guest_name="Past Guest",
  348. check_in_date=date(2026, 6, 1),
  349. check_in_time=time(14, 0),
  350. check_out_date=date(2026, 6, 5),
  351. check_out_time=time(12, 0),
  352. )
  353. _FUTURE = ReservationData(
  354. guest_name="Future Guest",
  355. check_in_date=date(2026, 7, 1),
  356. check_in_time=time(14, 0),
  357. check_out_date=date(2026, 7, 5),
  358. check_out_time=time(12, 0),
  359. )
  360. # Freeze time at 2026-06-22 12:00 local (naive)
  361. _RECONCILE_NOW = MagicMock()
  362. _RECONCILE_NOW.replace.return_value = datetime(2026, 6, 22, 12, 0, 0)
  363. async def test_reconcile_active_current_turns_on_guest_mode(coordinator: RoomCalCoordinator):
  364. await coordinator.store.async_set(STORE_CURRENT, _ACTIVE)
  365. mock_switch = MagicMock()
  366. mock_switch.async_turn_on = AsyncMock()
  367. mock_switch.async_turn_off = AsyncMock()
  368. coordinator._guest_mode_switch = mock_switch
  369. with (
  370. patch.object(coordinator, "_is_mode_sync_on", return_value=True),
  371. patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_RECONCILE_NOW),
  372. ):
  373. await coordinator.async_reconcile_from_store()
  374. coordinator.cancel_mode_sync()
  375. mock_switch.async_turn_on.assert_awaited_once()
  376. mock_switch.async_turn_off.assert_not_awaited()
  377. async def test_reconcile_elapsed_current_clears_store_and_turns_off_guest_mode(coordinator: RoomCalCoordinator):
  378. await coordinator.store.async_set(STORE_CURRENT, _ELAPSED)
  379. mock_switch = MagicMock()
  380. mock_switch.async_turn_on = AsyncMock()
  381. mock_switch.async_turn_off = AsyncMock()
  382. coordinator._guest_mode_switch = mock_switch
  383. with (
  384. patch.object(coordinator, "_is_mode_sync_on", return_value=True),
  385. patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_RECONCILE_NOW),
  386. ):
  387. await coordinator.async_reconcile_from_store()
  388. assert coordinator.store.get(STORE_CURRENT) is None
  389. mock_switch.async_turn_off.assert_awaited_once()
  390. async def test_reconcile_promotes_next_when_check_in_elapsed(coordinator: RoomCalCoordinator):
  391. await coordinator.store.async_set(STORE_NEXT, _ACTIVE)
  392. with (
  393. patch.object(coordinator, "_is_mode_sync_on", return_value=True),
  394. patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_RECONCILE_NOW),
  395. ):
  396. await coordinator.async_reconcile_from_store()
  397. coordinator.cancel_mode_sync()
  398. assert coordinator.store.get(STORE_CURRENT).guest_name == "Active Guest"
  399. async def test_reconcile_does_not_touch_guest_mode_when_mode_sync_off(coordinator: RoomCalCoordinator):
  400. await coordinator.store.async_set(STORE_CURRENT, _ACTIVE)
  401. mock_switch = MagicMock()
  402. mock_switch.async_turn_on = AsyncMock()
  403. mock_switch.async_turn_off = AsyncMock()
  404. coordinator._guest_mode_switch = mock_switch
  405. with (
  406. patch.object(coordinator, "_is_mode_sync_on", return_value=False),
  407. patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_RECONCILE_NOW),
  408. ):
  409. await coordinator.async_reconcile_from_store()
  410. mock_switch.async_turn_on.assert_not_awaited()
  411. mock_switch.async_turn_off.assert_not_awaited()