From b012b7916728ce5aa42775157c8f4f02cb18264a Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 8 Feb 2022 23:03:37 +0100 Subject: [PATCH] Adapt deCONZ number platform to align with updated design of binary sensor and sensor platforms (#65248) * Adapt number to align with binary sensor and sensor platforms * Make number tests easier to expand --- homeassistant/components/deconz/number.py | 44 ++++---- tests/components/deconz/test_number.py | 122 ++++++++++++++++------ 2 files changed, 114 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index fff70b9f7b5e..bf138aaef639 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import ValuesView +from collections.abc import Callable, ValuesView from dataclasses import dataclass -from pydeconz.sensor import PRESENCE_DELAY, Presence +from pydeconz.sensor import PRESENCE_DELAY, DeconzSensor as PydeconzSensor, Presence from homeassistant.components.number import ( DOMAIN, @@ -23,33 +23,30 @@ from .gateway import DeconzGateway, get_gateway_from_config_entry @dataclass -class DeconzNumberEntityDescriptionBase: +class DeconzNumberDescriptionMixin: """Required values when describing deCONZ number entities.""" - device_property: str suffix: str update_key: str + value_fn: Callable[[PydeconzSensor], bool | None] @dataclass -class DeconzNumberEntityDescription( - NumberEntityDescription, DeconzNumberEntityDescriptionBase -): +class DeconzNumberDescription(NumberEntityDescription, DeconzNumberDescriptionMixin): """Class describing deCONZ number entities.""" - entity_category = EntityCategory.CONFIG - ENTITY_DESCRIPTIONS = { Presence: [ - DeconzNumberEntityDescription( + DeconzNumberDescription( key="delay", - device_property="delay", + value_fn=lambda device: device.delay, suffix="Delay", update_key=PRESENCE_DELAY, max_value=65535, min_value=0, step=1, + entity_category=EntityCategory.CONFIG, ) ] } @@ -76,15 +73,18 @@ async def async_setup_entry( if sensor.type.startswith("CLIP"): continue - known_number_entities = set(gateway.entities[DOMAIN]) + known_entities = set(gateway.entities[DOMAIN]) for description in ENTITY_DESCRIPTIONS.get(type(sensor), []): - if getattr(sensor, description.device_property) is None: + if ( + not hasattr(sensor, description.key) + or description.value_fn(sensor) is None + ): continue - new_number_entity = DeconzNumber(sensor, gateway, description) - if new_number_entity.unique_id not in known_number_entities: - entities.append(new_number_entity) + new_entity = DeconzNumber(sensor, gateway, description) + if new_entity.unique_id not in known_entities: + entities.append(new_entity) if entities: async_add_entities(entities) @@ -112,29 +112,29 @@ class DeconzNumber(DeconzDevice, NumberEntity): self, device: Presence, gateway: DeconzGateway, - description: DeconzNumberEntityDescription, + description: DeconzNumberDescription, ) -> None: """Initialize deCONZ number entity.""" - self.entity_description: DeconzNumberEntityDescription = description + self.entity_description: DeconzNumberDescription = description super().__init__(device, gateway) self._attr_name = f"{device.name} {description.suffix}" + self._update_keys = {self.entity_description.update_key, "reachable"} @callback def async_update_callback(self) -> None: """Update the number value.""" - keys = {self.entity_description.update_key, "reachable"} - if self._device.changed_keys.intersection(keys): + if self._device.changed_keys.intersection(self._update_keys): super().async_update_callback() @property def value(self) -> float: """Return the value of the sensor property.""" - return getattr(self._device, self.entity_description.device_property) # type: ignore[no-any-return] + return self.entity_description.value_fn(self._device) # type: ignore[no-any-return] async def async_set_value(self, value: float) -> None: """Set sensor config.""" - data = {self.entity_description.device_property: int(value)} + data = {self.entity_description.key: int(value)} await self._device.set_config(**data) @property diff --git a/tests/components/deconz/test_number.py b/tests/components/deconz/test_number.py index 0cf0650e3d12..e0c469a1ba2f 100644 --- a/tests/components/deconz/test_number.py +++ b/tests/components/deconz/test_number.py @@ -10,6 +10,8 @@ from homeassistant.components.number import ( SERVICE_SET_VALUE, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory from .test_gateway import ( DECONZ_WEB_REQUEST, @@ -24,29 +26,79 @@ async def test_no_number_entities(hass, aioclient_mock): assert len(hass.states.async_all()) == 0 -async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): - """Test successful creation of binary sensor entities.""" - data = { - "sensors": { - "0": { - "name": "Presence sensor", - "type": "ZHAPresence", - "state": {"dark": False, "presence": False}, - "config": { - "delay": 0, - "on": True, - "reachable": True, - "temperature": 10, - }, - "uniqueid": "00:00:00:00:00:00:00:00-00", +TEST_DATA = [ + ( # Presence sensor - delay configuration + { + "name": "Presence sensor", + "type": "ZHAPresence", + "state": {"dark": False, "presence": False}, + "config": { + "delay": 0, + "on": True, + "reachable": True, + "temperature": 10, }, - } - } - with patch.dict(DECONZ_WEB_REQUEST, data): + "uniqueid": "00:00:00:00:00:00:00:00-00", + }, + { + "entity_count": 3, + "device_count": 3, + "entity_id": "number.presence_sensor_delay", + "unique_id": "00:00:00:00:00:00:00:00-delay", + "state": "0", + "entity_category": EntityCategory.CONFIG, + "attributes": { + "min": 0, + "max": 65535, + "step": 1, + "mode": "auto", + "friendly_name": "Presence sensor Delay", + }, + "websocket_event": {"config": {"delay": 10}}, + "next_state": "10", + "supported_service_value": 111, + "supported_service_response": {"delay": 111}, + "unsupported_service_value": 0.1, + "unsupported_service_response": {"delay": 0}, + "out_of_range_service_value": 66666, + }, + ) +] + + +@pytest.mark.parametrize("sensor_data, expected", TEST_DATA) +async def test_number_entities( + hass, aioclient_mock, mock_deconz_websocket, sensor_data, expected +): + """Test successful creation of number entities.""" + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + + with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"0": sensor_data}}): config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 3 - assert hass.states.get("number.presence_sensor_delay").state == "0" + assert len(hass.states.async_all()) == expected["entity_count"] + + # Verify state data + + entity = hass.states.get(expected["entity_id"]) + assert entity.state == expected["state"] + assert entity.attributes == expected["attributes"] + + # Verify entity registry data + + ent_reg_entry = ent_reg.async_get(expected["entity_id"]) + assert ent_reg_entry.entity_category is expected["entity_category"] + assert ent_reg_entry.unique_id == expected["unique_id"] + + # Verify device registry data + + assert ( + len(dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id)) + == expected["device_count"] + ) + + # Change state event_changed_sensor = { "t": "event", @@ -57,8 +109,7 @@ async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() - - assert hass.states.get("number.presence_sensor_delay").state == "10" + assert hass.states.get(expected["entity_id"]).state == expected["next_state"] # Verify service calls @@ -69,20 +120,26 @@ async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "number.presence_sensor_delay", ATTR_VALUE: 111}, + { + ATTR_ENTITY_ID: expected["entity_id"], + ATTR_VALUE: expected["supported_service_value"], + }, blocking=True, ) - assert aioclient_mock.mock_calls[1][2] == {"delay": 111} + assert aioclient_mock.mock_calls[1][2] == expected["supported_service_response"] # Service set float value await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "number.presence_sensor_delay", ATTR_VALUE: 0.1}, + { + ATTR_ENTITY_ID: expected["entity_id"], + ATTR_VALUE: expected["unsupported_service_value"], + }, blocking=True, ) - assert aioclient_mock.mock_calls[2][2] == {"delay": 0} + assert aioclient_mock.mock_calls[2][2] == expected["unsupported_service_response"] # Service set value beyond the supported range @@ -90,15 +147,20 @@ async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "number.presence_sensor_delay", ATTR_VALUE: 66666}, + { + ATTR_ENTITY_ID: expected["entity_id"], + ATTR_VALUE: expected["out_of_range_service_value"], + }, blocking=True, ) - await hass.config_entries.async_unload(config_entry.entry_id) + # Unload entry - assert hass.states.get("number.presence_sensor_delay").state == STATE_UNAVAILABLE + await hass.config_entries.async_unload(config_entry.entry_id) + assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE + + # Remove entry await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0