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:
parent
911e488d48
commit
b012b79167
2 changed files with 114 additions and 52 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue