test_coordinator.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. """Tests for ZLockCoordinator: slot fetch, alarm event handling, name/event persistence."""
  2. import pytest
  3. from unittest.mock import patch, AsyncMock
  4. from custom_components.zlock_slots.coordinator import ZLockCoordinator
  5. from custom_components.zlock_slots.const import (
  6. DOMAIN, CONF_LOCK_ENTITY_IDS, CONF_GROUPS, CONF_GROUP_NAME,
  7. )
  8. from tests.conftest import (
  9. LOCK_ENTITY_ID, ALARM_TYPE_ENTITY, ALARM_LEVEL_ENTITY,
  10. FakeUserData, FakeNode,
  11. )
  12. _ZWAVE_HELPER = "homeassistant.components.zwave_js.helpers.async_get_node_from_entity_id"
  13. @pytest.fixture
  14. async def coord(hass, config_entry, mock_node):
  15. config_entry.add_to_hass(hass)
  16. with patch(_ZWAVE_HELPER, return_value=mock_node):
  17. c = ZLockCoordinator(hass, config_entry)
  18. await c.async_setup()
  19. await hass.async_block_till_done()
  20. yield c
  21. c.async_teardown()
  22. # ---------------------------------------------------------------------------
  23. # Slot fetch
  24. # ---------------------------------------------------------------------------
  25. async def test_slot_fetch_populates_data(coord):
  26. slots = coord.data[LOCK_ENTITY_ID].slots
  27. assert set(slots) == {1, 2}
  28. async def test_slot_enabled_status(coord):
  29. assert coord.data[LOCK_ENTITY_ID].slots[1].status == "enabled"
  30. assert coord.data[LOCK_ENTITY_ID].slots[2].status == "disabled"
  31. async def test_slot_0_excluded(hass, config_entry):
  32. node = FakeNode(users=[FakeUserData(0), FakeUserData(1)])
  33. config_entry.add_to_hass(hass)
  34. with patch(_ZWAVE_HELPER, return_value=node):
  35. c = ZLockCoordinator(hass, config_entry)
  36. await c.async_setup()
  37. assert 0 not in c.data[LOCK_ENTITY_ID].slots
  38. assert 1 in c.data[LOCK_ENTITY_ID].slots
  39. c.async_teardown()
  40. async def test_slot_name_applied_during_fetch(hass, config_entry, mock_node):
  41. config_entry.add_to_hass(hass)
  42. with patch(_ZWAVE_HELPER, return_value=mock_node):
  43. c = ZLockCoordinator(hass, config_entry)
  44. # Pre-seed a name before setup fetches slots.
  45. c._names[f"{LOCK_ENTITY_ID}:1"] = "Alice"
  46. await c.async_setup()
  47. assert c.data[LOCK_ENTITY_ID].slots[1].name == "Alice"
  48. assert c.data[LOCK_ENTITY_ID].slots[2].name is None
  49. c.async_teardown()
  50. async def test_fetch_failure_returns_empty(hass, config_entry):
  51. config_entry.add_to_hass(hass)
  52. with patch(_ZWAVE_HELPER, side_effect=Exception("zwave unavailable")):
  53. c = ZLockCoordinator(hass, config_entry)
  54. await c.async_setup()
  55. assert c.data[LOCK_ENTITY_ID].slots == {}
  56. c.async_teardown()
  57. # ---------------------------------------------------------------------------
  58. # Name persistence
  59. # ---------------------------------------------------------------------------
  60. async def test_set_name_stores_and_retrieves(coord):
  61. await coord.async_set_name(LOCK_ENTITY_ID, 1, "Alice")
  62. assert coord.get_name(LOCK_ENTITY_ID, 1) == "Alice"
  63. async def test_set_name_empty_clears_entry(coord):
  64. await coord.async_set_name(LOCK_ENTITY_ID, 1, "Alice")
  65. await coord.async_set_name(LOCK_ENTITY_ID, 1, "")
  66. assert coord.get_name(LOCK_ENTITY_ID, 1) is None
  67. async def test_set_name_updates_cached_slot(coord):
  68. await coord.async_set_name(LOCK_ENTITY_ID, 1, "Bob")
  69. assert coord.data[LOCK_ENTITY_ID].slots[1].name == "Bob"
  70. async def test_set_name_persists_to_store(hass, config_entry, mock_node):
  71. """Names survive a coordinator restart via the Store."""
  72. config_entry.add_to_hass(hass)
  73. with patch(_ZWAVE_HELPER, return_value=mock_node):
  74. c1 = ZLockCoordinator(hass, config_entry)
  75. await c1.async_setup()
  76. await c1.async_set_name(LOCK_ENTITY_ID, 1, "Persisted")
  77. c1.async_teardown()
  78. c2 = ZLockCoordinator(hass, config_entry)
  79. await c2.async_setup()
  80. assert c2.get_name(LOCK_ENTITY_ID, 1) == "Persisted"
  81. c2.async_teardown()
  82. # ---------------------------------------------------------------------------
  83. # Alarm event handling
  84. # ---------------------------------------------------------------------------
  85. async def _fire_alarm(hass, alarm_type: str, alarm_level: str = "0"):
  86. hass.states.async_set(ALARM_LEVEL_ENTITY, alarm_level)
  87. hass.states.async_set(ALARM_TYPE_ENTITY, alarm_type)
  88. await hass.async_block_till_done()
  89. async def test_keypad_lock_sets_action_and_user(hass, coord):
  90. hass.states.async_set(ALARM_LEVEL_ENTITY, "1")
  91. hass.states.async_set(ALARM_TYPE_ENTITY, "18")
  92. await hass.async_block_till_done()
  93. assert coord.data[LOCK_ENTITY_ID].last_action == "locked"
  94. assert coord.data[LOCK_ENTITY_ID].last_user == "1"
  95. async def test_keypad_lock_uses_slot_name(hass, coord):
  96. await coord.async_set_name(LOCK_ENTITY_ID, 1, "Alice")
  97. hass.states.async_set(ALARM_LEVEL_ENTITY, "1")
  98. hass.states.async_set(ALARM_TYPE_ENTITY, "18")
  99. await hass.async_block_till_done()
  100. assert coord.data[LOCK_ENTITY_ID].last_user == "Alice"
  101. async def test_keypad_unlock(hass, coord):
  102. await _fire_alarm(hass, "19", alarm_level="2")
  103. assert coord.data[LOCK_ENTITY_ID].last_action == "unlocked"
  104. assert coord.data[LOCK_ENTITY_ID].last_user == "2"
  105. async def test_manual_lock(hass, coord):
  106. await _fire_alarm(hass, "21")
  107. assert coord.data[LOCK_ENTITY_ID].last_action == "locked"
  108. assert coord.data[LOCK_ENTITY_ID].last_user == "<Manual>"
  109. async def test_manual_unlock(hass, coord):
  110. await _fire_alarm(hass, "22")
  111. assert coord.data[LOCK_ENTITY_ID].last_action == "unlocked"
  112. assert coord.data[LOCK_ENTITY_ID].last_user == "<Manual>"
  113. async def test_radio_lock(hass, coord):
  114. await _fire_alarm(hass, "24")
  115. assert coord.data[LOCK_ENTITY_ID].last_action == "locked"
  116. assert coord.data[LOCK_ENTITY_ID].last_user == "<Radio>"
  117. async def test_radio_unlock(hass, coord):
  118. await _fire_alarm(hass, "25")
  119. assert coord.data[LOCK_ENTITY_ID].last_action == "unlocked"
  120. assert coord.data[LOCK_ENTITY_ID].last_user == "<Radio>"
  121. async def test_unknown_alarm_type_ignored(hass, coord):
  122. await _fire_alarm(hass, "99")
  123. assert coord.data[LOCK_ENTITY_ID].last_action is None
  124. assert coord.data[LOCK_ENTITY_ID].last_user is None
  125. async def test_non_integer_alarm_state_ignored(hass, coord):
  126. hass.states.async_set(ALARM_TYPE_ENTITY, "unavailable")
  127. await hass.async_block_till_done()
  128. assert coord.data[LOCK_ENTITY_ID].last_action is None
  129. # ---------------------------------------------------------------------------
  130. # last_events persistence and restore
  131. # ---------------------------------------------------------------------------
  132. async def test_last_events_persisted_after_alarm(hass, config_entry, mock_node):
  133. config_entry.add_to_hass(hass)
  134. with patch(_ZWAVE_HELPER, return_value=mock_node):
  135. c = ZLockCoordinator(hass, config_entry)
  136. await c.async_setup()
  137. hass.states.async_set(ALARM_LEVEL_ENTITY, "1")
  138. hass.states.async_set(ALARM_TYPE_ENTITY, "18")
  139. await hass.async_block_till_done()
  140. stored = await c._store.async_load()
  141. assert stored["last_events"]["locks"][LOCK_ENTITY_ID]["last_action"] == "locked"
  142. assert stored["last_events"]["locks"][LOCK_ENTITY_ID]["last_user"] == "1"
  143. c.async_teardown()
  144. async def test_last_events_restored_on_startup(hass, config_entry, mock_node):
  145. """last_action/last_user are non-None immediately after a cold start."""
  146. config_entry.add_to_hass(hass)
  147. with patch(_ZWAVE_HELPER, return_value=mock_node):
  148. c1 = ZLockCoordinator(hass, config_entry)
  149. await c1.async_setup()
  150. hass.states.async_set(ALARM_LEVEL_ENTITY, "2")
  151. hass.states.async_set(ALARM_TYPE_ENTITY, "22")
  152. await hass.async_block_till_done()
  153. c1.async_teardown()
  154. c2 = ZLockCoordinator(hass, config_entry)
  155. await c2.async_setup()
  156. assert c2.data[LOCK_ENTITY_ID].last_action == "unlocked"
  157. assert c2.data[LOCK_ENTITY_ID].last_user == "<Manual>"
  158. c2.async_teardown()
  159. async def test_names_still_present_after_alarm_save(hass, config_entry, mock_node):
  160. """_async_save merges names + last_events; names must not be clobbered."""
  161. config_entry.add_to_hass(hass)
  162. with patch(_ZWAVE_HELPER, return_value=mock_node):
  163. c = ZLockCoordinator(hass, config_entry)
  164. await c.async_setup()
  165. await c.async_set_name(LOCK_ENTITY_ID, 1, "Alice")
  166. await _fire_alarm(hass, "18", "1")
  167. stored = await c._store.async_load()
  168. assert stored["names"][f"{LOCK_ENTITY_ID}:1"] == "Alice"
  169. assert stored["last_events"]["locks"][LOCK_ENTITY_ID]["last_action"] == "locked"
  170. c.async_teardown()
  171. # ---------------------------------------------------------------------------
  172. # Group state
  173. # ---------------------------------------------------------------------------
  174. @pytest.fixture
  175. def group_config_entry():
  176. from pytest_homeassistant_custom_component.common import MockConfigEntry
  177. return MockConfigEntry(
  178. domain=DOMAIN,
  179. data={CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]},
  180. options={
  181. CONF_GROUPS: [{
  182. CONF_GROUP_NAME: "All Locks",
  183. CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID],
  184. }]
  185. },
  186. entry_id="test_entry_group",
  187. )
  188. async def test_group_state_updated_on_alarm(hass, group_config_entry, mock_node):
  189. group_config_entry.add_to_hass(hass)
  190. with patch(_ZWAVE_HELPER, return_value=mock_node):
  191. c = ZLockCoordinator(hass, group_config_entry)
  192. await c.async_setup()
  193. await _fire_alarm(hass, "19", "1")
  194. assert c.group_data["All Locks"].last_action == "unlocked"
  195. assert c.group_data["All Locks"].last_user == "1"
  196. c.async_teardown()
  197. async def test_group_last_events_restored(hass, group_config_entry, mock_node):
  198. group_config_entry.add_to_hass(hass)
  199. with patch(_ZWAVE_HELPER, return_value=mock_node):
  200. c1 = ZLockCoordinator(hass, group_config_entry)
  201. await c1.async_setup()
  202. await _fire_alarm(hass, "24")
  203. c1.async_teardown()
  204. c2 = ZLockCoordinator(hass, group_config_entry)
  205. await c2.async_setup()
  206. assert c2.group_data["All Locks"].last_action == "locked"
  207. assert c2.group_data["All Locks"].last_user == "<Radio>"
  208. c2.async_teardown()