mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 16:27:08 +00:00
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
@ -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.fan import DOMAIN as FAN_DOMAIN
|
||||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||||
from homeassistant.components.lock import DOMAIN as LOCK_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.scene import DOMAIN as SCENE_DOMAIN
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN
|
from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN
|
||||||
@ -40,6 +41,7 @@ PLATFORMS = [
|
|||||||
FAN_DOMAIN,
|
FAN_DOMAIN,
|
||||||
LIGHT_DOMAIN,
|
LIGHT_DOMAIN,
|
||||||
LOCK_DOMAIN,
|
LOCK_DOMAIN,
|
||||||
|
NUMBER_DOMAIN,
|
||||||
SCENE_DOMAIN,
|
SCENE_DOMAIN,
|
||||||
SENSOR_DOMAIN,
|
SENSOR_DOMAIN,
|
||||||
SIREN_DOMAIN,
|
SIREN_DOMAIN,
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/deconz",
|
"documentation": "https://www.home-assistant.io/integrations/deconz",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"pydeconz==84"
|
"pydeconz==85"
|
||||||
],
|
],
|
||||||
"ssdp": [
|
"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
|
pydanfossair==0.1.0
|
||||||
|
|
||||||
# homeassistant.components.deconz
|
# homeassistant.components.deconz
|
||||||
pydeconz==84
|
pydeconz==85
|
||||||
|
|
||||||
# homeassistant.components.delijn
|
# homeassistant.components.delijn
|
||||||
pydelijn==0.6.1
|
pydelijn==0.6.1
|
||||||
|
@ -838,7 +838,7 @@ pycoolmasternet-async==0.1.2
|
|||||||
pydaikin==2.6.0
|
pydaikin==2.6.0
|
||||||
|
|
||||||
# homeassistant.components.deconz
|
# homeassistant.components.deconz
|
||||||
pydeconz==84
|
pydeconz==85
|
||||||
|
|
||||||
# homeassistant.components.dexcom
|
# homeassistant.components.dexcom
|
||||||
pydexcom==0.2.0
|
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.fan import DOMAIN as FAN_DOMAIN
|
||||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||||
from homeassistant.components.lock import DOMAIN as LOCK_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.scene import DOMAIN as SCENE_DOMAIN
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
from homeassistant.components.siren import DOMAIN as SIREN_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[4][1] == (config_entry, FAN_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[5][1] == (config_entry, LIGHT_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[6][1] == (config_entry, LOCK_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[7][1] == (config_entry, SCENE_DOMAIN)
|
assert forward_entry_setup.mock_calls[7][1] == (config_entry, NUMBER_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[8][1] == (config_entry, SENSOR_DOMAIN)
|
assert forward_entry_setup.mock_calls[8][1] == (config_entry, SCENE_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[9][1] == (config_entry, SIREN_DOMAIN)
|
assert forward_entry_setup.mock_calls[9][1] == (config_entry, SENSOR_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[10][1] == (config_entry, SWITCH_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):
|
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…
x
Reference in New Issue
Block a user