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 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

View File

@ -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