Add deCONZ number config entity for Hue motion sensor delay (#58076)
* First working draft of number platform * Replace duration with delay for Hue motion sensors Improve tests * Bump dependency to v85 * Use constant for entity category * Use type rather than using __class__ * Fix unique ID
This commit is contained in:
parent
25f4f2d86e
commit
008b784fc5
7 changed files with 241 additions and 7 deletions
|
@ -10,6 +10,7 @@ from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
|
|||
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
|
||||
from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN
|
||||
|
@ -40,6 +41,7 @@ PLATFORMS = [
|
|||
FAN_DOMAIN,
|
||||
LIGHT_DOMAIN,
|
||||
LOCK_DOMAIN,
|
||||
NUMBER_DOMAIN,
|
||||
SCENE_DOMAIN,
|
||||
SENSOR_DOMAIN,
|
||||
SIREN_DOMAIN,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/deconz",
|
||||
"requirements": [
|
||||
"pydeconz==84"
|
||||
"pydeconz==85"
|
||||
],
|
||||
"ssdp": [
|
||||
{
|
||||
|
|
126
homeassistant/components/deconz/number.py
Normal file
126
homeassistant/components/deconz/number.py
Normal file
|
@ -0,0 +1,126 @@
|
|||
"""Support for configuring different deCONZ sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pydeconz.sensor import PRESENCE_DELAY, Presence
|
||||
|
||||
from homeassistant.components.number import (
|
||||
DOMAIN,
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
)
|
||||
from homeassistant.const import ENTITY_CATEGORY_CONFIG
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .deconz_device import DeconzDevice
|
||||
from .gateway import get_gateway_from_config_entry
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeconzNumberEntityDescription(NumberEntityDescription):
|
||||
"""Class describing deCONZ number entities."""
|
||||
|
||||
entity_category = ENTITY_CATEGORY_CONFIG
|
||||
device_property: str | None = None
|
||||
suffix: str | None = None
|
||||
update_key: str | None = None
|
||||
max_value: int | None = None
|
||||
min_value: int | None = None
|
||||
step: int | None = None
|
||||
|
||||
|
||||
ENTITY_DESCRIPTIONS = {
|
||||
Presence: [
|
||||
DeconzNumberEntityDescription(
|
||||
key="delay",
|
||||
device_property="delay",
|
||||
suffix="Delay",
|
||||
update_key=PRESENCE_DELAY,
|
||||
max_value=65535,
|
||||
min_value=0,
|
||||
step=1,
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the deCONZ number entity."""
|
||||
gateway = get_gateway_from_config_entry(hass, config_entry)
|
||||
gateway.entities[DOMAIN] = set()
|
||||
|
||||
@callback
|
||||
def async_add_sensor(sensors=gateway.api.sensors.values()):
|
||||
"""Add number config sensor from deCONZ."""
|
||||
entities = []
|
||||
|
||||
for sensor in sensors:
|
||||
|
||||
if sensor.type.startswith("CLIP"):
|
||||
continue
|
||||
|
||||
known_number_entities = set(gateway.entities[DOMAIN])
|
||||
for description in ENTITY_DESCRIPTIONS.get(type(sensor), []):
|
||||
|
||||
if getattr(sensor, description.device_property) 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)
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
gateway.signal_new_sensor,
|
||||
async_add_sensor,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_sensor(
|
||||
[gateway.api.sensors[key] for key in sorted(gateway.api.sensors, key=int)]
|
||||
)
|
||||
|
||||
|
||||
class DeconzNumber(DeconzDevice, NumberEntity):
|
||||
"""Representation of a deCONZ number entity."""
|
||||
|
||||
TYPE = DOMAIN
|
||||
|
||||
def __init__(self, device, gateway, description):
|
||||
"""Initialize deCONZ number entity."""
|
||||
self.entity_description = description
|
||||
super().__init__(device, gateway)
|
||||
|
||||
self._attr_name = f"{self._device.name} {description.suffix}"
|
||||
self._attr_max_value = description.max_value
|
||||
self._attr_min_value = description.min_value
|
||||
self._attr_step = description.step
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, force_update: bool = False) -> None:
|
||||
"""Update the number value."""
|
||||
keys = {self.entity_description.update_key, "reachable"}
|
||||
if force_update or self._device.changed_keys.intersection(keys):
|
||||
super().async_update_callback(force_update=force_update)
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
"""Return the value of the sensor property."""
|
||||
return getattr(self._device, self.entity_description.device_property)
|
||||
|
||||
async def async_set_value(self, value: float) -> None:
|
||||
"""Set sensor config."""
|
||||
data = {self.entity_description.device_property: int(value)}
|
||||
await self._device.set_config(**data)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique identifier for this entity."""
|
||||
return f"{self.serial}-{self.entity_description.suffix.lower()}"
|
|
@ -1417,7 +1417,7 @@ pydaikin==2.6.0
|
|||
pydanfossair==0.1.0
|
||||
|
||||
# homeassistant.components.deconz
|
||||
pydeconz==84
|
||||
pydeconz==85
|
||||
|
||||
# homeassistant.components.delijn
|
||||
pydelijn==0.6.1
|
||||
|
|
|
@ -838,7 +838,7 @@ pycoolmasternet-async==0.1.2
|
|||
pydaikin==2.6.0
|
||||
|
||||
# homeassistant.components.deconz
|
||||
pydeconz==84
|
||||
pydeconz==85
|
||||
|
||||
# homeassistant.components.dexcom
|
||||
pydexcom==0.2.0
|
||||
|
|
|
@ -23,6 +23,7 @@ from homeassistant.components.deconz.gateway import (
|
|||
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
|
||||
from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN
|
||||
|
@ -162,10 +163,11 @@ async def test_gateway_setup(hass, aioclient_mock):
|
|||
assert forward_entry_setup.mock_calls[4][1] == (config_entry, FAN_DOMAIN)
|
||||
assert forward_entry_setup.mock_calls[5][1] == (config_entry, LIGHT_DOMAIN)
|
||||
assert forward_entry_setup.mock_calls[6][1] == (config_entry, LOCK_DOMAIN)
|
||||
assert forward_entry_setup.mock_calls[7][1] == (config_entry, SCENE_DOMAIN)
|
||||
assert forward_entry_setup.mock_calls[8][1] == (config_entry, SENSOR_DOMAIN)
|
||||
assert forward_entry_setup.mock_calls[9][1] == (config_entry, SIREN_DOMAIN)
|
||||
assert forward_entry_setup.mock_calls[10][1] == (config_entry, SWITCH_DOMAIN)
|
||||
assert forward_entry_setup.mock_calls[7][1] == (config_entry, NUMBER_DOMAIN)
|
||||
assert forward_entry_setup.mock_calls[8][1] == (config_entry, SCENE_DOMAIN)
|
||||
assert forward_entry_setup.mock_calls[9][1] == (config_entry, SENSOR_DOMAIN)
|
||||
assert forward_entry_setup.mock_calls[10][1] == (config_entry, SIREN_DOMAIN)
|
||||
assert forward_entry_setup.mock_calls[11][1] == (config_entry, SWITCH_DOMAIN)
|
||||
|
||||
|
||||
async def test_gateway_retry(hass):
|
||||
|
|
104
tests/components/deconz/test_number.py
Normal file
104
tests/components/deconz/test_number.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
"""deCONZ number platform tests."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.number import (
|
||||
ATTR_VALUE,
|
||||
DOMAIN as NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE
|
||||
|
||||
from .test_gateway import (
|
||||
DECONZ_WEB_REQUEST,
|
||||
mock_deconz_put_request,
|
||||
setup_deconz_integration,
|
||||
)
|
||||
|
||||
|
||||
async def test_no_number_entities(hass, aioclient_mock):
|
||||
"""Test that no sensors in deconz results in no number entities."""
|
||||
await setup_deconz_integration(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",
|
||||
},
|
||||
}
|
||||
}
|
||||
with patch.dict(DECONZ_WEB_REQUEST, 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"
|
||||
|
||||
event_changed_sensor = {
|
||||
"t": "event",
|
||||
"e": "changed",
|
||||
"r": "sensors",
|
||||
"id": "0",
|
||||
"config": {"delay": 10},
|
||||
}
|
||||
await mock_deconz_websocket(data=event_changed_sensor)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("number.presence_sensor_delay").state == "10"
|
||||
|
||||
# Verify service calls
|
||||
|
||||
mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config")
|
||||
|
||||
# Service set supported value
|
||||
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: "number.presence_sensor_delay", ATTR_VALUE: 111},
|
||||
blocking=True,
|
||||
)
|
||||
assert aioclient_mock.mock_calls[1][2] == {"delay": 111}
|
||||
|
||||
# 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},
|
||||
blocking=True,
|
||||
)
|
||||
assert aioclient_mock.mock_calls[2][2] == {"delay": 0}
|
||||
|
||||
# Service set value beyond the supported range
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: "number.presence_sensor_delay", ATTR_VALUE: 66666},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
|
||||
assert hass.states.get("number.presence_sensor_delay").state == STATE_UNAVAILABLE
|
||||
|
||||
await hass.config_entries.async_remove(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 0
|
Loading…
Add table
Reference in a new issue