test_sensor.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. """Tests for sensor entities: native_value, extra_state_attributes, device attachment."""
  2. import pytest
  3. from unittest.mock import patch
  4. from homeassistant.helpers import device_registry as dr, entity_registry as er
  5. from pytest_homeassistant_custom_component.common import MockConfigEntry
  6. from custom_components.zlock_slots.coordinator import ZLockCoordinator
  7. from custom_components.zlock_slots.sensor import (
  8. ZLockLastActionSensor,
  9. ZLockLastUserSensor,
  10. ZLockSlotsSensor,
  11. ZLockGroupLastActionSensor,
  12. ZLockGroupLastUserSensor,
  13. ZLockGroupSlotsSensor,
  14. )
  15. from custom_components.zlock_slots.const import (
  16. DOMAIN, CONF_GROUPS, CONF_GROUP_NAME, CONF_LOCK_ENTITY_IDS,
  17. )
  18. from tests.conftest import LOCK_ENTITY_ID, ALARM_TYPE_ENTITY, ALARM_LEVEL_ENTITY, FakeNode, FakeUserData
  19. LOCK_2 = "lock.back_door"
  20. _ZWAVE_HELPER = "homeassistant.components.zwave_js.helpers.async_get_node_from_entity_id"
  21. @pytest.fixture
  22. async def coord(hass, config_entry, mock_node):
  23. config_entry.add_to_hass(hass)
  24. with patch(_ZWAVE_HELPER, return_value=mock_node):
  25. c = ZLockCoordinator(hass, config_entry)
  26. await c.async_setup()
  27. await hass.async_block_till_done()
  28. yield c
  29. c.async_teardown()
  30. # ---------------------------------------------------------------------------
  31. # ZLockLastActionSensor
  32. # ---------------------------------------------------------------------------
  33. async def test_last_action_none_initially(coord, config_entry):
  34. sensor = ZLockLastActionSensor(coord, config_entry, LOCK_ENTITY_ID)
  35. assert sensor.native_value is None
  36. async def test_last_action_after_event(hass, coord, config_entry):
  37. hass.states.async_set(ALARM_LEVEL_ENTITY, "1")
  38. hass.states.async_set(ALARM_TYPE_ENTITY, "18")
  39. await hass.async_block_till_done()
  40. sensor = ZLockLastActionSensor(coord, config_entry, LOCK_ENTITY_ID)
  41. assert sensor.native_value == "Locked"
  42. # ---------------------------------------------------------------------------
  43. # ZLockLastUserSensor
  44. # ---------------------------------------------------------------------------
  45. async def test_last_user_none_initially(coord, config_entry):
  46. sensor = ZLockLastUserSensor(coord, config_entry, LOCK_ENTITY_ID)
  47. assert sensor.native_value is None
  48. async def test_last_user_slot_number_when_unnamed(hass, coord, config_entry):
  49. hass.states.async_set(ALARM_LEVEL_ENTITY, "2")
  50. hass.states.async_set(ALARM_TYPE_ENTITY, "19")
  51. await hass.async_block_till_done()
  52. sensor = ZLockLastUserSensor(coord, config_entry, LOCK_ENTITY_ID)
  53. assert sensor.native_value == "2"
  54. async def test_last_user_name_when_named(hass, coord, config_entry):
  55. await coord.async_set_name(LOCK_ENTITY_ID, 1, "Eve")
  56. hass.states.async_set(ALARM_LEVEL_ENTITY, "1")
  57. hass.states.async_set(ALARM_TYPE_ENTITY, "18")
  58. await hass.async_block_till_done()
  59. sensor = ZLockLastUserSensor(coord, config_entry, LOCK_ENTITY_ID)
  60. assert sensor.native_value == "Eve"
  61. async def test_last_user_manual(hass, coord, config_entry):
  62. hass.states.async_set(ALARM_TYPE_ENTITY, "21")
  63. await hass.async_block_till_done()
  64. sensor = ZLockLastUserSensor(coord, config_entry, LOCK_ENTITY_ID)
  65. assert sensor.native_value == "<Manual>"
  66. async def test_last_user_radio(hass, coord, config_entry):
  67. hass.states.async_set(ALARM_TYPE_ENTITY, "25")
  68. await hass.async_block_till_done()
  69. sensor = ZLockLastUserSensor(coord, config_entry, LOCK_ENTITY_ID)
  70. assert sensor.native_value == "<Radio>"
  71. # ---------------------------------------------------------------------------
  72. # ZLockSlotsSensor
  73. # ---------------------------------------------------------------------------
  74. async def test_slots_sensor_count(coord, config_entry):
  75. sensor = ZLockSlotsSensor(coord, config_entry, LOCK_ENTITY_ID)
  76. assert sensor.native_value == 2
  77. async def test_slots_sensor_zero_when_empty(hass, config_entry):
  78. from tests.conftest import FakeNode
  79. config_entry.add_to_hass(hass)
  80. with patch(_ZWAVE_HELPER, return_value=FakeNode(users=[])):
  81. c = ZLockCoordinator(hass, config_entry)
  82. await c.async_setup()
  83. sensor = ZLockSlotsSensor(c, config_entry, LOCK_ENTITY_ID)
  84. assert sensor.native_value == 0
  85. assert sensor.extra_state_attributes == {"slots": []}
  86. c.async_teardown()
  87. async def test_slots_sensor_attributes_structure(coord, config_entry):
  88. sensor = ZLockSlotsSensor(coord, config_entry, LOCK_ENTITY_ID)
  89. attrs = sensor.extra_state_attributes
  90. assert "slots" in attrs
  91. slots = attrs["slots"]
  92. assert len(slots) == 2
  93. assert slots[0] == {"slot_num": 1, "status": "enabled", "name": None}
  94. assert slots[1] == {"slot_num": 2, "status": "disabled", "name": None}
  95. async def test_slots_sensor_attributes_sorted(hass, config_entry):
  96. from tests.conftest import FakeNode, FakeUserData
  97. node = FakeNode(users=[FakeUserData(5), FakeUserData(1), FakeUserData(3)])
  98. config_entry.add_to_hass(hass)
  99. with patch(_ZWAVE_HELPER, return_value=node):
  100. c = ZLockCoordinator(hass, config_entry)
  101. await c.async_setup()
  102. sensor = ZLockSlotsSensor(c, config_entry, LOCK_ENTITY_ID)
  103. slot_nums = [s["slot_num"] for s in sensor.extra_state_attributes["slots"]]
  104. assert slot_nums == [1, 3, 5]
  105. c.async_teardown()
  106. async def test_slots_sensor_attributes_include_names(hass, config_entry, mock_node):
  107. config_entry.add_to_hass(hass)
  108. with patch(_ZWAVE_HELPER, return_value=mock_node):
  109. c = ZLockCoordinator(hass, config_entry)
  110. c._names[f"{LOCK_ENTITY_ID}:1"] = "Frank"
  111. await c.async_setup()
  112. sensor = ZLockSlotsSensor(c, config_entry, LOCK_ENTITY_ID)
  113. slot1 = sensor.extra_state_attributes["slots"][0]
  114. assert slot1["name"] == "Frank"
  115. c.async_teardown()
  116. # ---------------------------------------------------------------------------
  117. # Group sensors
  118. # ---------------------------------------------------------------------------
  119. @pytest.fixture
  120. def group_config_entry():
  121. from pytest_homeassistant_custom_component.common import MockConfigEntry
  122. from custom_components.zlock_slots.const import DOMAIN
  123. return MockConfigEntry(
  124. domain=DOMAIN,
  125. data={CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]},
  126. options={
  127. CONF_GROUPS: [{
  128. CONF_GROUP_NAME: "All Locks",
  129. CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID],
  130. }]
  131. },
  132. entry_id="test_group_entry",
  133. )
  134. async def test_group_last_action_initially_none(hass, group_config_entry, mock_node):
  135. group_config_entry.add_to_hass(hass)
  136. with patch(_ZWAVE_HELPER, return_value=mock_node):
  137. c = ZLockCoordinator(hass, group_config_entry)
  138. await c.async_setup()
  139. sensor = ZLockGroupLastActionSensor(c, group_config_entry, "All Locks")
  140. assert sensor.native_value is None
  141. c.async_teardown()
  142. async def test_group_last_action_after_alarm(hass, group_config_entry, mock_node):
  143. group_config_entry.add_to_hass(hass)
  144. with patch(_ZWAVE_HELPER, return_value=mock_node):
  145. c = ZLockCoordinator(hass, group_config_entry)
  146. await c.async_setup()
  147. hass.states.async_set(ALARM_TYPE_ENTITY, "24")
  148. await hass.async_block_till_done()
  149. sensor = ZLockGroupLastActionSensor(c, group_config_entry, "All Locks")
  150. assert sensor.native_value == "Locked"
  151. c.async_teardown()
  152. async def test_group_last_user_after_alarm(hass, group_config_entry, mock_node):
  153. group_config_entry.add_to_hass(hass)
  154. with patch(_ZWAVE_HELPER, return_value=mock_node):
  155. c = ZLockCoordinator(hass, group_config_entry)
  156. await c.async_setup()
  157. hass.states.async_set(ALARM_TYPE_ENTITY, "22")
  158. await hass.async_block_till_done()
  159. sensor = ZLockGroupLastUserSensor(c, group_config_entry, "All Locks")
  160. assert sensor.native_value == "<Manual>"
  161. c.async_teardown()
  162. async def test_group_slots_sensor_naive_union(hass):
  163. """Slot numbers from every member merge; a slot present on only one member still appears."""
  164. entry = MockConfigEntry(
  165. domain=DOMAIN,
  166. data={CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]},
  167. options={
  168. CONF_GROUPS: [{
  169. CONF_GROUP_NAME: "All Locks",
  170. CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2],
  171. }]
  172. },
  173. entry_id="test_group_union_entry",
  174. )
  175. entry.add_to_hass(hass)
  176. nodes = {
  177. LOCK_ENTITY_ID: FakeNode(users=[FakeUserData(1, active=True)]),
  178. LOCK_2: FakeNode(users=[FakeUserData(2, active=False)]),
  179. }
  180. with patch(_ZWAVE_HELPER, side_effect=lambda hass_, eid: nodes[eid]):
  181. c = ZLockCoordinator(hass, entry)
  182. await c.async_setup()
  183. sensor = ZLockGroupSlotsSensor(c, entry, "All Locks", [LOCK_ENTITY_ID, LOCK_2])
  184. assert sensor.native_value == 2
  185. slots = {s["slot_num"]: s["status"] for s in sensor.extra_state_attributes["slots"]}
  186. assert slots == {1: "enabled", 2: "disabled"}
  187. c.async_teardown()
  188. # ---------------------------------------------------------------------------
  189. # Device attachment
  190. # ---------------------------------------------------------------------------
  191. def _register_lock_device(hass) -> dr.DeviceEntry:
  192. """Create a fake zwave_js device with a registered lock.front_door entity."""
  193. zwave_entry = MockConfigEntry(domain="zwave_js", entry_id="fake_zwave_entry")
  194. zwave_entry.add_to_hass(hass)
  195. device = dr.async_get(hass).async_get_or_create(
  196. config_entry_id=zwave_entry.entry_id,
  197. identifiers={("zwave_js", "3245-89")},
  198. name="Front Door",
  199. )
  200. er.async_get(hass).async_get_or_create(
  201. "lock", "zwave_js", "front_door_lock_unique",
  202. device_id=device.id,
  203. suggested_object_id="front_door",
  204. )
  205. return device
  206. async def _setup_integration(hass, entry, mock_node):
  207. entry.add_to_hass(hass)
  208. with patch(_ZWAVE_HELPER, return_value=mock_node):
  209. assert await hass.config_entries.async_setup(entry.entry_id)
  210. await hass.async_block_till_done()
  211. async def test_lock_sensors_attach_to_lock_device(hass, config_entry, mock_node):
  212. device = _register_lock_device(hass)
  213. await _setup_integration(hass, config_entry, mock_node)
  214. ent_reg = er.async_get(hass)
  215. for suffix in ("last_action", "last_user", "slots"):
  216. entity_id = ent_reg.async_get_entity_id(
  217. "sensor", DOMAIN, f"{config_entry.entry_id}_{LOCK_ENTITY_ID}_{suffix}"
  218. )
  219. assert entity_id is not None
  220. assert ent_reg.async_get(entity_id).device_id == device.id
  221. async def test_lock_sensors_without_zwave_device(hass, config_entry, mock_node):
  222. """Lock not in the entity registry — sensors still created, just device-less."""
  223. await _setup_integration(hass, config_entry, mock_node)
  224. ent_reg = er.async_get(hass)
  225. entity_id = ent_reg.async_get_entity_id(
  226. "sensor", DOMAIN, f"{config_entry.entry_id}_{LOCK_ENTITY_ID}_last_action"
  227. )
  228. assert entity_id is not None
  229. assert ent_reg.async_get(entity_id).device_id is None
  230. async def test_group_sensors_get_service_device(hass, group_config_entry, mock_node):
  231. await _setup_integration(hass, group_config_entry, mock_node)
  232. device = dr.async_get(hass).async_get_device(identifiers={(DOMAIN, "group_All Locks")})
  233. assert device is not None
  234. assert device.entry_type is dr.DeviceEntryType.SERVICE
  235. assert device.name == "All Locks"
  236. ent_reg = er.async_get(hass)
  237. entity_id = ent_reg.async_get_entity_id(
  238. "sensor", DOMAIN, f"{group_config_entry.entry_id}_group_All Locks_last_action"
  239. )
  240. assert ent_reg.async_get(entity_id).device_id == device.id
  241. async def test_stale_group_device_and_entities_removed_on_reload(
  242. hass, group_config_entry, mock_node
  243. ):
  244. await _setup_integration(hass, group_config_entry, mock_node)
  245. dev_reg = dr.async_get(hass)
  246. assert dev_reg.async_get_device(identifiers={(DOMAIN, "group_All Locks")})
  247. with patch(_ZWAVE_HELPER, return_value=mock_node):
  248. # Update listener reloads the entry; cleanup runs during sensor setup.
  249. hass.config_entries.async_update_entry(group_config_entry, options={CONF_GROUPS: []})
  250. await hass.async_block_till_done()
  251. assert dev_reg.async_get_device(identifiers={(DOMAIN, "group_All Locks")}) is None
  252. ent_reg = er.async_get(hass)
  253. assert ent_reg.async_get_entity_id(
  254. "sensor", DOMAIN, f"{group_config_entry.entry_id}_group_All Locks_last_action"
  255. ) is None