Add presence duration number (#79498)

This commit is contained in:
Robert Svensson 2022-10-23 22:30:03 +02:00 committed by GitHub
parent d85866d49c
commit d75834cd1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 126 additions and 39 deletions

View File

@ -1,11 +1,15 @@
"""Support for configuring different deCONZ sensors."""
"""Support for configuring different deCONZ numbers."""
from __future__ import annotations
from collections.abc import Callable
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any, Generic, TypeVar
from pydeconz.gateway import DeconzSession
from pydeconz.interfaces.sensors import SensorResources
from pydeconz.models.event import EventType
from pydeconz.models.sensor import SensorBase as PydeconzSensorBase
from pydeconz.models.sensor.presence import Presence
from homeassistant.components.number import (
@ -17,39 +21,76 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.helpers.entity_registry as er
from .const import DOMAIN as DECONZ_DOMAIN
from .deconz_device import DeconzDevice
from .gateway import DeconzGateway, get_gateway_from_config_entry
T = TypeVar("T", Presence, PydeconzSensorBase)
@dataclass
class DeconzNumberDescriptionMixin:
class DeconzNumberDescriptionMixin(Generic[T]):
"""Required values when describing deCONZ number entities."""
suffix: str
instance_check: type[T]
name_suffix: str
set_fn: Callable[[DeconzSession, str, int], Coroutine[Any, Any, dict[str, Any]]]
update_key: str
value_fn: Callable[[Presence], float | None]
value_fn: Callable[[T], float | None]
@dataclass
class DeconzNumberDescription(NumberEntityDescription, DeconzNumberDescriptionMixin):
class DeconzNumberDescription(NumberEntityDescription, DeconzNumberDescriptionMixin[T]):
"""Class describing deCONZ number entities."""
ENTITY_DESCRIPTIONS = {
Presence: [
DeconzNumberDescription(
key="delay",
value_fn=lambda device: device.delay,
suffix="Delay",
update_key="delay",
native_max_value=65535,
native_min_value=0,
native_step=1,
entity_category=EntityCategory.CONFIG,
)
]
}
ENTITY_DESCRIPTIONS: tuple[DeconzNumberDescription, ...] = (
DeconzNumberDescription[Presence](
key="delay",
instance_check=Presence,
name_suffix="Delay",
set_fn=lambda api, id, v: api.sensors.presence.set_config(id=id, delay=v),
update_key="delay",
value_fn=lambda device: device.delay,
native_max_value=65535,
native_min_value=0,
native_step=1,
entity_category=EntityCategory.CONFIG,
),
DeconzNumberDescription[Presence](
key="duration",
instance_check=Presence,
name_suffix="Duration",
set_fn=lambda api, id, v: api.sensors.presence.set_config(id=id, duration=v),
update_key="duration",
value_fn=lambda device: device.duration,
native_max_value=65535,
native_min_value=0,
native_step=1,
entity_category=EntityCategory.CONFIG,
),
)
@callback
def async_update_unique_id(
hass: HomeAssistant, unique_id: str, description: DeconzNumberDescription
) -> None:
"""Update unique ID base to be on full unique ID rather than device serial.
Introduced with release 2022.11.
"""
ent_reg = er.async_get(hass)
new_unique_id = f"{unique_id}-{description.key}"
if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id):
return
unique_id = f'{unique_id.split("-", 1)[0]}-{description.key}'
if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id):
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
async def async_setup_entry(
@ -66,12 +107,14 @@ async def async_setup_entry(
"""Add sensor from deCONZ."""
sensor = gateway.api.sensors.presence[sensor_id]
for description in ENTITY_DESCRIPTIONS.get(type(sensor), []):
for description in ENTITY_DESCRIPTIONS:
if (
not hasattr(sensor, description.key)
not isinstance(sensor, description.instance_check)
or description.value_fn(sensor) is None
):
continue
if description.key == "delay":
async_update_unique_id(hass, sensor.unique_id, description)
async_add_entities([DeconzNumber(sensor, gateway, description)])
gateway.register_platform_add_device_callback(
@ -81,21 +124,23 @@ async def async_setup_entry(
)
class DeconzNumber(DeconzDevice[Presence], NumberEntity):
class DeconzNumber(DeconzDevice[SensorResources], NumberEntity):
"""Representation of a deCONZ number entity."""
TYPE = DOMAIN
entity_description: DeconzNumberDescription
def __init__(
self,
device: Presence,
device: SensorResources,
gateway: DeconzGateway,
description: DeconzNumberDescription,
) -> None:
"""Initialize deCONZ number entity."""
self.entity_description: DeconzNumberDescription = description
self._update_key = self.entity_description.update_key
self._name_suffix = description.suffix
self.entity_description = description
self.unique_id_suffix = description.key
self._name_suffix = description.name_suffix
self._update_key = description.update_key
super().__init__(device, gateway)
@property
@ -105,12 +150,8 @@ class DeconzNumber(DeconzDevice[Presence], NumberEntity):
async def async_set_native_value(self, value: float) -> None:
"""Set sensor config."""
await self.gateway.api.sensors.presence.set_config(
id=self._device.resource_id,
delay=int(value),
await self.entity_description.set_fn(
self.gateway.api,
self._device.resource_id,
int(value),
)
@property
def unique_id(self) -> str:
"""Return a unique identifier for this entity."""
return f"{self.serial}-{self.entity_description.suffix.lower()}"

View File

@ -4,6 +4,7 @@ from unittest.mock import patch
import pytest
from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
from homeassistant.components.number import (
ATTR_VALUE,
DOMAIN as NUMBER_DOMAIN,
@ -44,7 +45,8 @@ TEST_DATA = [
"entity_count": 3,
"device_count": 3,
"entity_id": "number.presence_sensor_delay",
"unique_id": "00:00:00:00:00:00:00:00-delay",
"unique_id": "00:00:00:00:00:00:00:00-00-delay",
"old_unique_id": "00:00:00:00:00:00:00:00-delay",
"state": "0",
"entity_category": EntityCategory.CONFIG,
"attributes": {
@ -62,7 +64,43 @@ TEST_DATA = [
"unsupported_service_response": {"delay": 0},
"out_of_range_service_value": 66666,
},
)
),
( # Presence sensor - duration configuration
{
"name": "Presence sensor",
"type": "ZHAPresence",
"state": {"dark": False, "presence": False},
"config": {
"duration": 0,
"on": True,
"reachable": True,
"temperature": 10,
},
"uniqueid": "00:00:00:00:00:00:00:00-00",
},
{
"entity_count": 3,
"device_count": 3,
"entity_id": "number.presence_sensor_duration",
"unique_id": "00:00:00:00:00:00:00:00-00-duration",
"state": "0",
"entity_category": EntityCategory.CONFIG,
"attributes": {
"min": 0,
"max": 65535,
"step": 1,
"mode": "auto",
"friendly_name": "Presence sensor Duration",
},
"websocket_event": {"config": {"duration": 10}},
"next_state": "10",
"supported_service_value": 111,
"supported_service_response": {"duration": 111},
"unsupported_service_value": 0.1,
"unsupported_service_response": {"duration": 0},
"out_of_range_service_value": 66666,
},
),
]
@ -74,6 +112,15 @@ async def test_number_entities(
ent_reg = er.async_get(hass)
dev_reg = dr.async_get(hass)
# Create entity entry to migrate to new unique ID
if "old_unique_id" in expected:
ent_reg.async_get_or_create(
NUMBER_DOMAIN,
DECONZ_DOMAIN,
expected["old_unique_id"],
suggested_object_id=expected["entity_id"].replace("number.", ""),
)
with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"0": sensor_data}}):
config_entry = await setup_deconz_integration(hass, aioclient_mock)
@ -105,8 +152,7 @@ async def test_number_entities(
"e": "changed",
"r": "sensors",
"id": "0",
"config": {"delay": 10},
}
} | expected["websocket_event"]
await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get(expected["entity_id"]).state == expected["next_state"]