mirror of
https://github.com/home-assistant/core.git
synced 2025-07-10 14:57:09 +00:00
Add presence duration number (#79498)
This commit is contained in:
parent
d85866d49c
commit
d75834cd1e
@ -1,11 +1,15 @@
|
|||||||
"""Support for configuring different deCONZ sensors."""
|
"""Support for configuring different deCONZ numbers."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable, Coroutine
|
||||||
from dataclasses import dataclass
|
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.event import EventType
|
||||||
|
from pydeconz.models.sensor import SensorBase as PydeconzSensorBase
|
||||||
from pydeconz.models.sensor.presence import Presence
|
from pydeconz.models.sensor.presence import Presence
|
||||||
|
|
||||||
from homeassistant.components.number import (
|
from homeassistant.components.number import (
|
||||||
@ -17,39 +21,76 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity import EntityCategory
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
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 .deconz_device import DeconzDevice
|
||||||
from .gateway import DeconzGateway, get_gateway_from_config_entry
|
from .gateway import DeconzGateway, get_gateway_from_config_entry
|
||||||
|
|
||||||
|
T = TypeVar("T", Presence, PydeconzSensorBase)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DeconzNumberDescriptionMixin:
|
class DeconzNumberDescriptionMixin(Generic[T]):
|
||||||
"""Required values when describing deCONZ number entities."""
|
"""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
|
update_key: str
|
||||||
value_fn: Callable[[Presence], float | None]
|
value_fn: Callable[[T], float | None]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DeconzNumberDescription(NumberEntityDescription, DeconzNumberDescriptionMixin):
|
class DeconzNumberDescription(NumberEntityDescription, DeconzNumberDescriptionMixin[T]):
|
||||||
"""Class describing deCONZ number entities."""
|
"""Class describing deCONZ number entities."""
|
||||||
|
|
||||||
|
|
||||||
ENTITY_DESCRIPTIONS = {
|
ENTITY_DESCRIPTIONS: tuple[DeconzNumberDescription, ...] = (
|
||||||
Presence: [
|
DeconzNumberDescription[Presence](
|
||||||
DeconzNumberDescription(
|
|
||||||
key="delay",
|
key="delay",
|
||||||
value_fn=lambda device: device.delay,
|
instance_check=Presence,
|
||||||
suffix="Delay",
|
name_suffix="Delay",
|
||||||
|
set_fn=lambda api, id, v: api.sensors.presence.set_config(id=id, delay=v),
|
||||||
update_key="delay",
|
update_key="delay",
|
||||||
|
value_fn=lambda device: device.delay,
|
||||||
native_max_value=65535,
|
native_max_value=65535,
|
||||||
native_min_value=0,
|
native_min_value=0,
|
||||||
native_step=1,
|
native_step=1,
|
||||||
entity_category=EntityCategory.CONFIG,
|
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(
|
async def async_setup_entry(
|
||||||
@ -66,12 +107,14 @@ async def async_setup_entry(
|
|||||||
"""Add sensor from deCONZ."""
|
"""Add sensor from deCONZ."""
|
||||||
sensor = gateway.api.sensors.presence[sensor_id]
|
sensor = gateway.api.sensors.presence[sensor_id]
|
||||||
|
|
||||||
for description in ENTITY_DESCRIPTIONS.get(type(sensor), []):
|
for description in ENTITY_DESCRIPTIONS:
|
||||||
if (
|
if (
|
||||||
not hasattr(sensor, description.key)
|
not isinstance(sensor, description.instance_check)
|
||||||
or description.value_fn(sensor) is None
|
or description.value_fn(sensor) is None
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
if description.key == "delay":
|
||||||
|
async_update_unique_id(hass, sensor.unique_id, description)
|
||||||
async_add_entities([DeconzNumber(sensor, gateway, description)])
|
async_add_entities([DeconzNumber(sensor, gateway, description)])
|
||||||
|
|
||||||
gateway.register_platform_add_device_callback(
|
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."""
|
"""Representation of a deCONZ number entity."""
|
||||||
|
|
||||||
TYPE = DOMAIN
|
TYPE = DOMAIN
|
||||||
|
entity_description: DeconzNumberDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
device: Presence,
|
device: SensorResources,
|
||||||
gateway: DeconzGateway,
|
gateway: DeconzGateway,
|
||||||
description: DeconzNumberDescription,
|
description: DeconzNumberDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize deCONZ number entity."""
|
"""Initialize deCONZ number entity."""
|
||||||
self.entity_description: DeconzNumberDescription = description
|
self.entity_description = description
|
||||||
self._update_key = self.entity_description.update_key
|
self.unique_id_suffix = description.key
|
||||||
self._name_suffix = description.suffix
|
self._name_suffix = description.name_suffix
|
||||||
|
self._update_key = description.update_key
|
||||||
super().__init__(device, gateway)
|
super().__init__(device, gateway)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -105,12 +150,8 @@ class DeconzNumber(DeconzDevice[Presence], NumberEntity):
|
|||||||
|
|
||||||
async def async_set_native_value(self, value: float) -> None:
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
"""Set sensor config."""
|
"""Set sensor config."""
|
||||||
await self.gateway.api.sensors.presence.set_config(
|
await self.entity_description.set_fn(
|
||||||
id=self._device.resource_id,
|
self.gateway.api,
|
||||||
delay=int(value),
|
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()}"
|
|
||||||
|
@ -4,6 +4,7 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
|
||||||
from homeassistant.components.number import (
|
from homeassistant.components.number import (
|
||||||
ATTR_VALUE,
|
ATTR_VALUE,
|
||||||
DOMAIN as NUMBER_DOMAIN,
|
DOMAIN as NUMBER_DOMAIN,
|
||||||
@ -44,7 +45,8 @@ TEST_DATA = [
|
|||||||
"entity_count": 3,
|
"entity_count": 3,
|
||||||
"device_count": 3,
|
"device_count": 3,
|
||||||
"entity_id": "number.presence_sensor_delay",
|
"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",
|
"state": "0",
|
||||||
"entity_category": EntityCategory.CONFIG,
|
"entity_category": EntityCategory.CONFIG,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
@ -62,7 +64,43 @@ TEST_DATA = [
|
|||||||
"unsupported_service_response": {"delay": 0},
|
"unsupported_service_response": {"delay": 0},
|
||||||
"out_of_range_service_value": 66666,
|
"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)
|
ent_reg = er.async_get(hass)
|
||||||
dev_reg = dr.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}}):
|
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)
|
||||||
|
|
||||||
@ -105,8 +152,7 @@ async def test_number_entities(
|
|||||||
"e": "changed",
|
"e": "changed",
|
||||||
"r": "sensors",
|
"r": "sensors",
|
||||||
"id": "0",
|
"id": "0",
|
||||||
"config": {"delay": 10},
|
} | expected["websocket_event"]
|
||||||
}
|
|
||||||
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(expected["entity_id"]).state == expected["next_state"]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user