| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- """Tests for sensor entities: native_value, extra_state_attributes, device attachment."""
- import pytest
- from unittest.mock import patch
- from homeassistant.helpers import device_registry as dr, entity_registry as er
- from pytest_homeassistant_custom_component.common import MockConfigEntry
- from custom_components.zlock_slots.coordinator import ZLockCoordinator
- from custom_components.zlock_slots.sensor import (
- ZLockLastActionSensor,
- ZLockLastUserSensor,
- ZLockSlotsSensor,
- ZLockGroupLastActionSensor,
- ZLockGroupLastUserSensor,
- )
- from custom_components.zlock_slots.const import (
- DOMAIN, CONF_GROUPS, CONF_GROUP_NAME, CONF_LOCK_ENTITY_IDS,
- )
- from tests.conftest import LOCK_ENTITY_ID, ALARM_TYPE_ENTITY, ALARM_LEVEL_ENTITY
- _ZWAVE_HELPER = "homeassistant.components.zwave_js.helpers.async_get_node_from_entity_id"
- @pytest.fixture
- async def coord(hass, config_entry, mock_node):
- config_entry.add_to_hass(hass)
- with patch(_ZWAVE_HELPER, return_value=mock_node):
- c = ZLockCoordinator(hass, config_entry)
- await c.async_setup()
- await hass.async_block_till_done()
- yield c
- c.async_teardown()
- # ---------------------------------------------------------------------------
- # ZLockLastActionSensor
- # ---------------------------------------------------------------------------
- async def test_last_action_none_initially(coord, config_entry):
- sensor = ZLockLastActionSensor(coord, config_entry, LOCK_ENTITY_ID)
- assert sensor.native_value is None
- async def test_last_action_after_event(hass, coord, config_entry):
- hass.states.async_set(ALARM_LEVEL_ENTITY, "1")
- hass.states.async_set(ALARM_TYPE_ENTITY, "18")
- await hass.async_block_till_done()
- sensor = ZLockLastActionSensor(coord, config_entry, LOCK_ENTITY_ID)
- assert sensor.native_value == "Locked"
- # ---------------------------------------------------------------------------
- # ZLockLastUserSensor
- # ---------------------------------------------------------------------------
- async def test_last_user_none_initially(coord, config_entry):
- sensor = ZLockLastUserSensor(coord, config_entry, LOCK_ENTITY_ID)
- assert sensor.native_value is None
- async def test_last_user_slot_number_when_unnamed(hass, coord, config_entry):
- hass.states.async_set(ALARM_LEVEL_ENTITY, "2")
- hass.states.async_set(ALARM_TYPE_ENTITY, "19")
- await hass.async_block_till_done()
- sensor = ZLockLastUserSensor(coord, config_entry, LOCK_ENTITY_ID)
- assert sensor.native_value == "2"
- async def test_last_user_name_when_named(hass, coord, config_entry):
- await coord.async_set_name(LOCK_ENTITY_ID, 1, "Eve")
- hass.states.async_set(ALARM_LEVEL_ENTITY, "1")
- hass.states.async_set(ALARM_TYPE_ENTITY, "18")
- await hass.async_block_till_done()
- sensor = ZLockLastUserSensor(coord, config_entry, LOCK_ENTITY_ID)
- assert sensor.native_value == "Eve"
- async def test_last_user_manual(hass, coord, config_entry):
- hass.states.async_set(ALARM_TYPE_ENTITY, "21")
- await hass.async_block_till_done()
- sensor = ZLockLastUserSensor(coord, config_entry, LOCK_ENTITY_ID)
- assert sensor.native_value == "<Manual>"
- async def test_last_user_radio(hass, coord, config_entry):
- hass.states.async_set(ALARM_TYPE_ENTITY, "25")
- await hass.async_block_till_done()
- sensor = ZLockLastUserSensor(coord, config_entry, LOCK_ENTITY_ID)
- assert sensor.native_value == "<Radio>"
- # ---------------------------------------------------------------------------
- # ZLockSlotsSensor
- # ---------------------------------------------------------------------------
- async def test_slots_sensor_count(coord, config_entry):
- sensor = ZLockSlotsSensor(coord, config_entry, LOCK_ENTITY_ID)
- assert sensor.native_value == 2
- async def test_slots_sensor_zero_when_empty(hass, config_entry):
- from tests.conftest import FakeNode
- config_entry.add_to_hass(hass)
- with patch(_ZWAVE_HELPER, return_value=FakeNode(users=[])):
- c = ZLockCoordinator(hass, config_entry)
- await c.async_setup()
- sensor = ZLockSlotsSensor(c, config_entry, LOCK_ENTITY_ID)
- assert sensor.native_value == 0
- assert sensor.extra_state_attributes == {"slots": []}
- c.async_teardown()
- async def test_slots_sensor_attributes_structure(coord, config_entry):
- sensor = ZLockSlotsSensor(coord, config_entry, LOCK_ENTITY_ID)
- attrs = sensor.extra_state_attributes
- assert "slots" in attrs
- slots = attrs["slots"]
- assert len(slots) == 2
- assert slots[0] == {"slot_num": 1, "status": "enabled", "name": None}
- assert slots[1] == {"slot_num": 2, "status": "disabled", "name": None}
- async def test_slots_sensor_attributes_sorted(hass, config_entry):
- from tests.conftest import FakeNode, FakeUserData
- node = FakeNode(users=[FakeUserData(5), FakeUserData(1), FakeUserData(3)])
- config_entry.add_to_hass(hass)
- with patch(_ZWAVE_HELPER, return_value=node):
- c = ZLockCoordinator(hass, config_entry)
- await c.async_setup()
- sensor = ZLockSlotsSensor(c, config_entry, LOCK_ENTITY_ID)
- slot_nums = [s["slot_num"] for s in sensor.extra_state_attributes["slots"]]
- assert slot_nums == [1, 3, 5]
- c.async_teardown()
- async def test_slots_sensor_attributes_include_names(hass, config_entry, mock_node):
- config_entry.add_to_hass(hass)
- with patch(_ZWAVE_HELPER, return_value=mock_node):
- c = ZLockCoordinator(hass, config_entry)
- c._names[f"{LOCK_ENTITY_ID}:1"] = "Frank"
- await c.async_setup()
- sensor = ZLockSlotsSensor(c, config_entry, LOCK_ENTITY_ID)
- slot1 = sensor.extra_state_attributes["slots"][0]
- assert slot1["name"] == "Frank"
- c.async_teardown()
- # ---------------------------------------------------------------------------
- # Group sensors
- # ---------------------------------------------------------------------------
- @pytest.fixture
- def group_config_entry():
- from pytest_homeassistant_custom_component.common import MockConfigEntry
- from custom_components.zlock_slots.const import DOMAIN
- return MockConfigEntry(
- domain=DOMAIN,
- data={CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]},
- options={
- CONF_GROUPS: [{
- CONF_GROUP_NAME: "All Locks",
- CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID],
- }]
- },
- entry_id="test_group_entry",
- )
- async def test_group_last_action_initially_none(hass, group_config_entry, mock_node):
- group_config_entry.add_to_hass(hass)
- with patch(_ZWAVE_HELPER, return_value=mock_node):
- c = ZLockCoordinator(hass, group_config_entry)
- await c.async_setup()
- sensor = ZLockGroupLastActionSensor(c, group_config_entry, "All Locks")
- assert sensor.native_value is None
- c.async_teardown()
- async def test_group_last_action_after_alarm(hass, group_config_entry, mock_node):
- group_config_entry.add_to_hass(hass)
- with patch(_ZWAVE_HELPER, return_value=mock_node):
- c = ZLockCoordinator(hass, group_config_entry)
- await c.async_setup()
- hass.states.async_set(ALARM_TYPE_ENTITY, "24")
- await hass.async_block_till_done()
- sensor = ZLockGroupLastActionSensor(c, group_config_entry, "All Locks")
- assert sensor.native_value == "Locked"
- c.async_teardown()
- async def test_group_last_user_after_alarm(hass, group_config_entry, mock_node):
- group_config_entry.add_to_hass(hass)
- with patch(_ZWAVE_HELPER, return_value=mock_node):
- c = ZLockCoordinator(hass, group_config_entry)
- await c.async_setup()
- hass.states.async_set(ALARM_TYPE_ENTITY, "22")
- await hass.async_block_till_done()
- sensor = ZLockGroupLastUserSensor(c, group_config_entry, "All Locks")
- assert sensor.native_value == "<Manual>"
- c.async_teardown()
- # ---------------------------------------------------------------------------
- # Device attachment
- # ---------------------------------------------------------------------------
- def _register_lock_device(hass) -> dr.DeviceEntry:
- """Create a fake zwave_js device with a registered lock.front_door entity."""
- zwave_entry = MockConfigEntry(domain="zwave_js", entry_id="fake_zwave_entry")
- zwave_entry.add_to_hass(hass)
- device = dr.async_get(hass).async_get_or_create(
- config_entry_id=zwave_entry.entry_id,
- identifiers={("zwave_js", "3245-89")},
- name="Front Door",
- )
- er.async_get(hass).async_get_or_create(
- "lock", "zwave_js", "front_door_lock_unique",
- device_id=device.id,
- suggested_object_id="front_door",
- )
- return device
- async def _setup_integration(hass, entry, mock_node):
- entry.add_to_hass(hass)
- with patch(_ZWAVE_HELPER, return_value=mock_node):
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
- async def test_lock_sensors_attach_to_lock_device(hass, config_entry, mock_node):
- device = _register_lock_device(hass)
- await _setup_integration(hass, config_entry, mock_node)
- ent_reg = er.async_get(hass)
- for suffix in ("last_action", "last_user", "slots"):
- entity_id = ent_reg.async_get_entity_id(
- "sensor", DOMAIN, f"{config_entry.entry_id}_{LOCK_ENTITY_ID}_{suffix}"
- )
- assert entity_id is not None
- assert ent_reg.async_get(entity_id).device_id == device.id
- async def test_lock_sensors_without_zwave_device(hass, config_entry, mock_node):
- """Lock not in the entity registry — sensors still created, just device-less."""
- await _setup_integration(hass, config_entry, mock_node)
- ent_reg = er.async_get(hass)
- entity_id = ent_reg.async_get_entity_id(
- "sensor", DOMAIN, f"{config_entry.entry_id}_{LOCK_ENTITY_ID}_last_action"
- )
- assert entity_id is not None
- assert ent_reg.async_get(entity_id).device_id is None
- async def test_group_sensors_get_service_device(hass, group_config_entry, mock_node):
- await _setup_integration(hass, group_config_entry, mock_node)
- device = dr.async_get(hass).async_get_device(identifiers={(DOMAIN, "group_All Locks")})
- assert device is not None
- assert device.entry_type is dr.DeviceEntryType.SERVICE
- assert device.name == "All Locks"
- ent_reg = er.async_get(hass)
- entity_id = ent_reg.async_get_entity_id(
- "sensor", DOMAIN, f"{group_config_entry.entry_id}_group_All Locks_last_action"
- )
- assert ent_reg.async_get(entity_id).device_id == device.id
- async def test_stale_group_device_and_entities_removed_on_reload(
- hass, group_config_entry, mock_node
- ):
- await _setup_integration(hass, group_config_entry, mock_node)
- dev_reg = dr.async_get(hass)
- assert dev_reg.async_get_device(identifiers={(DOMAIN, "group_All Locks")})
- with patch(_ZWAVE_HELPER, return_value=mock_node):
- # Update listener reloads the entry; cleanup runs during sensor setup.
- hass.config_entries.async_update_entry(group_config_entry, options={CONF_GROUPS: []})
- await hass.async_block_till_done()
- assert dev_reg.async_get_device(identifiers={(DOMAIN, "group_All Locks")}) is None
- ent_reg = er.async_get(hass)
- assert ent_reg.async_get_entity_id(
- "sensor", DOMAIN, f"{group_config_entry.entry_id}_group_All Locks_last_action"
- ) is None
|