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
This commit is contained in:
Robert Svensson 2022-02-08 23:03:37 +01:00 committed by GitHub
parent 911e488d48
commit b012b79167
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 114 additions and 52 deletions

View file

@ -2,10 +2,10 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import ValuesView from collections.abc import Callable, ValuesView
from dataclasses import dataclass 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 ( from homeassistant.components.number import (
DOMAIN, DOMAIN,
@ -23,33 +23,30 @@ from .gateway import DeconzGateway, get_gateway_from_config_entry
@dataclass @dataclass
class DeconzNumberEntityDescriptionBase: class DeconzNumberDescriptionMixin:
"""Required values when describing deCONZ number entities.""" """Required values when describing deCONZ number entities."""
device_property: str
suffix: str suffix: str
update_key: str update_key: str
value_fn: Callable[[PydeconzSensor], bool | None]
@dataclass @dataclass
class DeconzNumberEntityDescription( class DeconzNumberDescription(NumberEntityDescription, DeconzNumberDescriptionMixin):
NumberEntityDescription, DeconzNumberEntityDescriptionBase
):
"""Class describing deCONZ number entities.""" """Class describing deCONZ number entities."""
entity_category = EntityCategory.CONFIG
ENTITY_DESCRIPTIONS = { ENTITY_DESCRIPTIONS = {
Presence: [ Presence: [
DeconzNumberEntityDescription( DeconzNumberDescription(
key="delay", key="delay",
device_property="delay", value_fn=lambda device: device.delay,
suffix="Delay", suffix="Delay",
update_key=PRESENCE_DELAY, update_key=PRESENCE_DELAY,
max_value=65535, max_value=65535,
min_value=0, min_value=0,
step=1, step=1,
entity_category=EntityCategory.CONFIG,
) )
] ]
} }
@ -76,15 +73,18 @@ async def async_setup_entry(
if sensor.type.startswith("CLIP"): if sensor.type.startswith("CLIP"):
continue continue
known_number_entities = set(gateway.entities[DOMAIN]) known_entities = set(gateway.entities[DOMAIN])
for description in ENTITY_DESCRIPTIONS.get(type(sensor), []): 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 continue
new_number_entity = DeconzNumber(sensor, gateway, description) new_entity = DeconzNumber(sensor, gateway, description)
if new_number_entity.unique_id not in known_number_entities: if new_entity.unique_id not in known_entities:
entities.append(new_number_entity) entities.append(new_entity)
if entities: if entities:
async_add_entities(entities) async_add_entities(entities)
@ -112,29 +112,29 @@ class DeconzNumber(DeconzDevice, NumberEntity):
self, self,
device: Presence, device: Presence,
gateway: DeconzGateway, gateway: DeconzGateway,
description: DeconzNumberEntityDescription, description: DeconzNumberDescription,
) -> None: ) -> None:
"""Initialize deCONZ number entity.""" """Initialize deCONZ number entity."""
self.entity_description: DeconzNumberEntityDescription = description self.entity_description: DeconzNumberDescription = description
super().__init__(device, gateway) super().__init__(device, gateway)
self._attr_name = f"{device.name} {description.suffix}" self._attr_name = f"{device.name} {description.suffix}"
self._update_keys = {self.entity_description.update_key, "reachable"}
@callback @callback
def async_update_callback(self) -> None: def async_update_callback(self) -> None:
"""Update the number value.""" """Update the number value."""
keys = {self.entity_description.update_key, "reachable"} if self._device.changed_keys.intersection(self._update_keys):
if self._device.changed_keys.intersection(keys):
super().async_update_callback() super().async_update_callback()
@property @property
def value(self) -> float: def value(self) -> float:
"""Return the value of the sensor property.""" """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: async def async_set_value(self, value: float) -> None:
"""Set sensor config.""" """Set sensor config."""
data = {self.entity_description.device_property: int(value)} data = {self.entity_description.key: int(value)}
await self._device.set_config(**data) await self._device.set_config(**data)
@property @property

View file

@ -10,6 +10,8 @@ from homeassistant.components.number import (
SERVICE_SET_VALUE, SERVICE_SET_VALUE,
) )
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE 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 ( from .test_gateway import (
DECONZ_WEB_REQUEST, DECONZ_WEB_REQUEST,
@ -24,29 +26,79 @@ async def test_no_number_entities(hass, aioclient_mock):
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 0
async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): TEST_DATA = [
"""Test successful creation of binary sensor entities.""" ( # Presence sensor - delay configuration
data = { {
"sensors": { "name": "Presence sensor",
"0": { "type": "ZHAPresence",
"name": "Presence sensor", "state": {"dark": False, "presence": False},
"type": "ZHAPresence", "config": {
"state": {"dark": False, "presence": False}, "delay": 0,
"config": { "on": True,
"delay": 0, "reachable": True,
"on": True, "temperature": 10,
"reachable": True,
"temperature": 10,
},
"uniqueid": "00:00:00:00:00:00:00:00-00",
}, },
} "uniqueid": "00:00:00:00:00:00:00:00-00",
} },
with patch.dict(DECONZ_WEB_REQUEST, data): {
"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) config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 3 assert len(hass.states.async_all()) == expected["entity_count"]
assert hass.states.get("number.presence_sensor_delay").state == "0"
# 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 = { event_changed_sensor = {
"t": "event", "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 mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get(expected["entity_id"]).state == expected["next_state"]
assert hass.states.get("number.presence_sensor_delay").state == "10"
# Verify service calls # Verify service calls
@ -69,20 +120,26 @@ async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket):
await hass.services.async_call( await hass.services.async_call(
NUMBER_DOMAIN, NUMBER_DOMAIN,
SERVICE_SET_VALUE, 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, 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 # Service set float value
await hass.services.async_call( await hass.services.async_call(
NUMBER_DOMAIN, NUMBER_DOMAIN,
SERVICE_SET_VALUE, 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, 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 # 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( await hass.services.async_call(
NUMBER_DOMAIN, NUMBER_DOMAIN,
SERVICE_SET_VALUE, 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, 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.config_entries.async_remove(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 0