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 expandpull/66122/head
parent
911e488d48
commit
b012b79167
|
@ -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
|
||||
|
|
|
@ -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,11 +26,9 @@ 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": {
|
||||
TEST_DATA = [
|
||||
( # Presence sensor - delay configuration
|
||||
{
|
||||
"name": "Presence sensor",
|
||||
"type": "ZHAPresence",
|
||||
"state": {"dark": False, "presence": False},
|
||||
|
@ -40,13 +40,65 @@ async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket):
|
|||
},
|
||||
"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)
|
||||
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue