Add Aqara FP1 support to deCONZ integration (#77568)
parent
5b3f4ec471
commit
ff3d3088ee
|
@ -6,9 +6,11 @@ from dataclasses import dataclass
|
||||||
|
|
||||||
from pydeconz.models.event import EventType
|
from pydeconz.models.event import EventType
|
||||||
from pydeconz.models.scene import Scene as PydeconzScene
|
from pydeconz.models.scene import Scene as PydeconzScene
|
||||||
|
from pydeconz.models.sensor.presence import Presence
|
||||||
|
|
||||||
from homeassistant.components.button import (
|
from homeassistant.components.button import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
ButtonDeviceClass,
|
||||||
ButtonEntity,
|
ButtonEntity,
|
||||||
ButtonEntityDescription,
|
ButtonEntityDescription,
|
||||||
)
|
)
|
||||||
|
@ -17,7 +19,7 @@ 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
|
||||||
|
|
||||||
from .deconz_device import DeconzSceneMixin
|
from .deconz_device import DeconzDevice, DeconzSceneMixin
|
||||||
from .gateway import DeconzGateway, get_gateway_from_config_entry
|
from .gateway import DeconzGateway, get_gateway_from_config_entry
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,7 +63,7 @@ async def async_setup_entry(
|
||||||
"""Add scene button from deCONZ."""
|
"""Add scene button from deCONZ."""
|
||||||
scene = gateway.api.scenes[scene_id]
|
scene = gateway.api.scenes[scene_id]
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
DeconzButton(scene, gateway, description)
|
DeconzSceneButton(scene, gateway, description)
|
||||||
for description in ENTITY_DESCRIPTIONS.get(PydeconzScene, [])
|
for description in ENTITY_DESCRIPTIONS.get(PydeconzScene, [])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -70,8 +72,20 @@ async def async_setup_entry(
|
||||||
gateway.api.scenes,
|
gateway.api.scenes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_add_presence_sensor(_: EventType, sensor_id: str) -> None:
|
||||||
|
"""Add presence sensor reset button from deCONZ."""
|
||||||
|
sensor = gateway.api.sensors.presence[sensor_id]
|
||||||
|
if sensor.presence_event is not None:
|
||||||
|
async_add_entities([DeconzPresenceResetButton(sensor, gateway)])
|
||||||
|
|
||||||
class DeconzButton(DeconzSceneMixin, ButtonEntity):
|
gateway.register_platform_add_device_callback(
|
||||||
|
async_add_presence_sensor,
|
||||||
|
gateway.api.sensors.presence,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeconzSceneButton(DeconzSceneMixin, ButtonEntity):
|
||||||
"""Representation of a deCONZ button entity."""
|
"""Representation of a deCONZ button entity."""
|
||||||
|
|
||||||
TYPE = DOMAIN
|
TYPE = DOMAIN
|
||||||
|
@ -99,3 +113,22 @@ class DeconzButton(DeconzSceneMixin, ButtonEntity):
|
||||||
def get_device_identifier(self) -> str:
|
def get_device_identifier(self) -> str:
|
||||||
"""Return a unique identifier for this scene."""
|
"""Return a unique identifier for this scene."""
|
||||||
return f"{super().get_device_identifier()}-{self.entity_description.key}"
|
return f"{super().get_device_identifier()}-{self.entity_description.key}"
|
||||||
|
|
||||||
|
|
||||||
|
class DeconzPresenceResetButton(DeconzDevice[Presence], ButtonEntity):
|
||||||
|
"""Representation of a deCONZ presence reset button entity."""
|
||||||
|
|
||||||
|
_name_suffix = "Reset Presence"
|
||||||
|
unique_id_suffix = "reset_presence"
|
||||||
|
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
_attr_device_class = ButtonDeviceClass.RESTART
|
||||||
|
|
||||||
|
TYPE = DOMAIN
|
||||||
|
|
||||||
|
async def async_press(self) -> None:
|
||||||
|
"""Store reset presence state."""
|
||||||
|
await self.gateway.api.sensors.presence.set_config(
|
||||||
|
id=self._device.resource_id,
|
||||||
|
reset_presence=True,
|
||||||
|
)
|
||||||
|
|
|
@ -35,6 +35,7 @@ PLATFORMS = [
|
||||||
Platform.LOCK,
|
Platform.LOCK,
|
||||||
Platform.NUMBER,
|
Platform.NUMBER,
|
||||||
Platform.SCENE,
|
Platform.SCENE,
|
||||||
|
Platform.SELECT,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
Platform.SIREN,
|
Platform.SIREN,
|
||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
|
|
|
@ -9,6 +9,7 @@ from pydeconz.models.sensor.ancillary_control import (
|
||||||
AncillaryControl,
|
AncillaryControl,
|
||||||
AncillaryControlAction,
|
AncillaryControlAction,
|
||||||
)
|
)
|
||||||
|
from pydeconz.models.sensor.presence import Presence, PresenceStatePresenceEvent
|
||||||
from pydeconz.models.sensor.switch import Switch
|
from pydeconz.models.sensor.switch import Switch
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
@ -28,6 +29,7 @@ from .gateway import DeconzGateway
|
||||||
|
|
||||||
CONF_DECONZ_EVENT = "deconz_event"
|
CONF_DECONZ_EVENT = "deconz_event"
|
||||||
CONF_DECONZ_ALARM_EVENT = "deconz_alarm_event"
|
CONF_DECONZ_ALARM_EVENT = "deconz_alarm_event"
|
||||||
|
CONF_DECONZ_PRESENCE_EVENT = "deconz_presence_event"
|
||||||
|
|
||||||
SUPPORTED_DECONZ_ALARM_EVENTS = {
|
SUPPORTED_DECONZ_ALARM_EVENTS = {
|
||||||
AncillaryControlAction.EMERGENCY,
|
AncillaryControlAction.EMERGENCY,
|
||||||
|
@ -35,6 +37,16 @@ SUPPORTED_DECONZ_ALARM_EVENTS = {
|
||||||
AncillaryControlAction.INVALID_CODE,
|
AncillaryControlAction.INVALID_CODE,
|
||||||
AncillaryControlAction.PANIC,
|
AncillaryControlAction.PANIC,
|
||||||
}
|
}
|
||||||
|
SUPPORTED_DECONZ_PRESENCE_EVENTS = {
|
||||||
|
PresenceStatePresenceEvent.ENTER,
|
||||||
|
PresenceStatePresenceEvent.LEAVE,
|
||||||
|
PresenceStatePresenceEvent.ENTER_LEFT,
|
||||||
|
PresenceStatePresenceEvent.RIGHT_LEAVE,
|
||||||
|
PresenceStatePresenceEvent.ENTER_RIGHT,
|
||||||
|
PresenceStatePresenceEvent.LEFT_LEAVE,
|
||||||
|
PresenceStatePresenceEvent.APPROACHING,
|
||||||
|
PresenceStatePresenceEvent.ABSENTING,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_events(gateway: DeconzGateway) -> None:
|
async def async_setup_events(gateway: DeconzGateway) -> None:
|
||||||
|
@ -43,7 +55,7 @@ async def async_setup_events(gateway: DeconzGateway) -> None:
|
||||||
@callback
|
@callback
|
||||||
def async_add_sensor(_: EventType, sensor_id: str) -> None:
|
def async_add_sensor(_: EventType, sensor_id: str) -> None:
|
||||||
"""Create DeconzEvent."""
|
"""Create DeconzEvent."""
|
||||||
new_event: DeconzAlarmEvent | DeconzEvent
|
new_event: DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent
|
||||||
sensor = gateway.api.sensors[sensor_id]
|
sensor = gateway.api.sensors[sensor_id]
|
||||||
|
|
||||||
if isinstance(sensor, Switch):
|
if isinstance(sensor, Switch):
|
||||||
|
@ -52,6 +64,11 @@ async def async_setup_events(gateway: DeconzGateway) -> None:
|
||||||
elif isinstance(sensor, AncillaryControl):
|
elif isinstance(sensor, AncillaryControl):
|
||||||
new_event = DeconzAlarmEvent(sensor, gateway)
|
new_event = DeconzAlarmEvent(sensor, gateway)
|
||||||
|
|
||||||
|
elif isinstance(sensor, Presence):
|
||||||
|
if sensor.presence_event is None:
|
||||||
|
return
|
||||||
|
new_event = DeconzPresenceEvent(sensor, gateway)
|
||||||
|
|
||||||
gateway.hass.async_create_task(new_event.async_update_device_registry())
|
gateway.hass.async_create_task(new_event.async_update_device_registry())
|
||||||
gateway.events.append(new_event)
|
gateway.events.append(new_event)
|
||||||
|
|
||||||
|
@ -63,6 +80,10 @@ async def async_setup_events(gateway: DeconzGateway) -> None:
|
||||||
async_add_sensor,
|
async_add_sensor,
|
||||||
gateway.api.sensors.ancillary_control,
|
gateway.api.sensors.ancillary_control,
|
||||||
)
|
)
|
||||||
|
gateway.register_platform_add_device_callback(
|
||||||
|
async_add_sensor,
|
||||||
|
gateway.api.sensors.presence,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -83,7 +104,7 @@ class DeconzEventBase(DeconzBase):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
device: AncillaryControl | Switch,
|
device: AncillaryControl | Presence | Switch,
|
||||||
gateway: DeconzGateway,
|
gateway: DeconzGateway,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register callback that will be used for signals."""
|
"""Register callback that will be used for signals."""
|
||||||
|
@ -181,3 +202,28 @@ class DeconzAlarmEvent(DeconzEventBase):
|
||||||
}
|
}
|
||||||
|
|
||||||
self.gateway.hass.bus.async_fire(CONF_DECONZ_ALARM_EVENT, data)
|
self.gateway.hass.bus.async_fire(CONF_DECONZ_ALARM_EVENT, data)
|
||||||
|
|
||||||
|
|
||||||
|
class DeconzPresenceEvent(DeconzEventBase):
|
||||||
|
"""Presence event."""
|
||||||
|
|
||||||
|
_device: Presence
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_update_callback(self) -> None:
|
||||||
|
"""Fire the event if reason is new action is updated."""
|
||||||
|
if (
|
||||||
|
self.gateway.ignore_state_updates
|
||||||
|
or "presenceevent" not in self._device.changed_keys
|
||||||
|
or self._device.presence_event not in SUPPORTED_DECONZ_PRESENCE_EVENTS
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
data = {
|
||||||
|
CONF_ID: self.event_id,
|
||||||
|
CONF_UNIQUE_ID: self.serial,
|
||||||
|
CONF_DEVICE_ID: self.device_id,
|
||||||
|
CONF_EVENT: self._device.presence_event.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.gateway.hass.bus.async_fire(CONF_DECONZ_PRESENCE_EVENT, data)
|
||||||
|
|
|
@ -22,7 +22,13 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from . import DOMAIN
|
from . import DOMAIN
|
||||||
from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE, DeconzAlarmEvent, DeconzEvent
|
from .deconz_event import (
|
||||||
|
CONF_DECONZ_EVENT,
|
||||||
|
CONF_GESTURE,
|
||||||
|
DeconzAlarmEvent,
|
||||||
|
DeconzEvent,
|
||||||
|
DeconzPresenceEvent,
|
||||||
|
)
|
||||||
from .gateway import DeconzGateway
|
from .gateway import DeconzGateway
|
||||||
|
|
||||||
CONF_SUBTYPE = "subtype"
|
CONF_SUBTYPE = "subtype"
|
||||||
|
@ -622,7 +628,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||||
def _get_deconz_event_from_device(
|
def _get_deconz_event_from_device(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
device: dr.DeviceEntry,
|
device: dr.DeviceEntry,
|
||||||
) -> DeconzAlarmEvent | DeconzEvent:
|
) -> DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent:
|
||||||
"""Resolve deconz event from device."""
|
"""Resolve deconz event from device."""
|
||||||
gateways: dict[str, DeconzGateway] = hass.data.get(DOMAIN, {})
|
gateways: dict[str, DeconzGateway] = hass.data.get(DOMAIN, {})
|
||||||
for gateway in gateways.values():
|
for gateway in gateways.values():
|
||||||
|
|
|
@ -41,7 +41,7 @@ from .const import (
|
||||||
from .errors import AuthenticationRequired, CannotConnect
|
from .errors import AuthenticationRequired, CannotConnect
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .deconz_event import DeconzAlarmEvent, DeconzEvent
|
from .deconz_event import DeconzAlarmEvent, DeconzEvent, DeconzPresenceEvent
|
||||||
|
|
||||||
SENSORS = (
|
SENSORS = (
|
||||||
sensors.SensorResourceManager,
|
sensors.SensorResourceManager,
|
||||||
|
@ -93,7 +93,7 @@ class DeconzGateway:
|
||||||
|
|
||||||
self.deconz_ids: dict[str, str] = {}
|
self.deconz_ids: dict[str, str] = {}
|
||||||
self.entities: dict[str, set[str]] = {}
|
self.entities: dict[str, set[str]] = {}
|
||||||
self.events: list[DeconzAlarmEvent | DeconzEvent] = []
|
self.events: list[DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent] = []
|
||||||
self.clip_sensors: set[tuple[Callable[[EventType, str], None], str]] = set()
|
self.clip_sensors: set[tuple[Callable[[EventType, str], None], str]] = set()
|
||||||
self.deconz_groups: set[tuple[Callable[[EventType, str], None], str]] = set()
|
self.deconz_groups: set[tuple[Callable[[EventType, str], None], str]] = set()
|
||||||
self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set()
|
self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set()
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
"""Support for deCONZ select entities."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pydeconz.models.event import EventType
|
||||||
|
from pydeconz.models.sensor.presence import (
|
||||||
|
Presence,
|
||||||
|
PresenceConfigDeviceMode,
|
||||||
|
PresenceConfigSensitivity,
|
||||||
|
PresenceConfigTriggerDistance,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.select import DOMAIN, SelectEntity
|
||||||
|
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
|
||||||
|
|
||||||
|
from .deconz_device import DeconzDevice
|
||||||
|
from .gateway import get_gateway_from_config_entry
|
||||||
|
|
||||||
|
SENSITIVITY_TO_DECONZ = {
|
||||||
|
"High": PresenceConfigSensitivity.HIGH.value,
|
||||||
|
"Medium": PresenceConfigSensitivity.MEDIUM.value,
|
||||||
|
"Low": PresenceConfigSensitivity.LOW.value,
|
||||||
|
}
|
||||||
|
DECONZ_TO_SENSITIVITY = {value: key for key, value in SENSITIVITY_TO_DECONZ.items()}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the deCONZ button entity."""
|
||||||
|
gateway = get_gateway_from_config_entry(hass, config_entry)
|
||||||
|
gateway.entities[DOMAIN] = set()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_add_presence_sensor(_: EventType, sensor_id: str) -> None:
|
||||||
|
"""Add presence select entity from deCONZ."""
|
||||||
|
sensor = gateway.api.sensors.presence[sensor_id]
|
||||||
|
if sensor.presence_event is not None:
|
||||||
|
async_add_entities(
|
||||||
|
[
|
||||||
|
DeconzPresenceDeviceModeSelect(sensor, gateway),
|
||||||
|
DeconzPresenceSensitivitySelect(sensor, gateway),
|
||||||
|
DeconzPresenceTriggerDistanceSelect(sensor, gateway),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
gateway.register_platform_add_device_callback(
|
||||||
|
async_add_presence_sensor,
|
||||||
|
gateway.api.sensors.presence,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeconzPresenceDeviceModeSelect(DeconzDevice[Presence], SelectEntity):
|
||||||
|
"""Representation of a deCONZ presence device mode entity."""
|
||||||
|
|
||||||
|
_name_suffix = "Device Mode"
|
||||||
|
unique_id_suffix = "device_mode"
|
||||||
|
_update_key = "devicemode"
|
||||||
|
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
_attr_options = [
|
||||||
|
PresenceConfigDeviceMode.LEFT_AND_RIGHT.value,
|
||||||
|
PresenceConfigDeviceMode.UNDIRECTED.value,
|
||||||
|
]
|
||||||
|
|
||||||
|
TYPE = DOMAIN
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option(self) -> str | None:
|
||||||
|
"""Return the selected entity option to represent the entity state."""
|
||||||
|
if self._device.device_mode is not None:
|
||||||
|
return self._device.device_mode.value
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
"""Change the selected option."""
|
||||||
|
await self.gateway.api.sensors.presence.set_config(
|
||||||
|
id=self._device.resource_id,
|
||||||
|
device_mode=PresenceConfigDeviceMode(option),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeconzPresenceSensitivitySelect(DeconzDevice[Presence], SelectEntity):
|
||||||
|
"""Representation of a deCONZ presence sensitivity entity."""
|
||||||
|
|
||||||
|
_name_suffix = "Sensitivity"
|
||||||
|
unique_id_suffix = "sensitivity"
|
||||||
|
_update_key = "sensitivity"
|
||||||
|
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
_attr_options = list(SENSITIVITY_TO_DECONZ)
|
||||||
|
|
||||||
|
TYPE = DOMAIN
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option(self) -> str | None:
|
||||||
|
"""Return the selected entity option to represent the entity state."""
|
||||||
|
if self._device.sensitivity is not None:
|
||||||
|
return DECONZ_TO_SENSITIVITY[self._device.sensitivity]
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
"""Change the selected option."""
|
||||||
|
await self.gateway.api.sensors.presence.set_config(
|
||||||
|
id=self._device.resource_id,
|
||||||
|
sensitivity=SENSITIVITY_TO_DECONZ[option],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeconzPresenceTriggerDistanceSelect(DeconzDevice[Presence], SelectEntity):
|
||||||
|
"""Representation of a deCONZ presence trigger distance entity."""
|
||||||
|
|
||||||
|
_name_suffix = "Trigger Distance"
|
||||||
|
unique_id_suffix = "trigger_distance"
|
||||||
|
_update_key = "triggerdistance"
|
||||||
|
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
_attr_options = [
|
||||||
|
PresenceConfigTriggerDistance.FAR.value,
|
||||||
|
PresenceConfigTriggerDistance.MEDIUM.value,
|
||||||
|
PresenceConfigTriggerDistance.NEAR.value,
|
||||||
|
]
|
||||||
|
|
||||||
|
TYPE = DOMAIN
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option(self) -> str | None:
|
||||||
|
"""Return the selected entity option to represent the entity state."""
|
||||||
|
if self._device.trigger_distance is not None:
|
||||||
|
return self._device.trigger_distance.value
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
"""Change the selected option."""
|
||||||
|
await self.gateway.api.sensors.presence.set_config(
|
||||||
|
id=self._device.resource_id,
|
||||||
|
trigger_distance=PresenceConfigTriggerDistance(option),
|
||||||
|
)
|
|
@ -48,6 +48,49 @@ TEST_DATA = [
|
||||||
"friendly_name": "Light group Scene Store Current Scene",
|
"friendly_name": "Light group Scene Store Current Scene",
|
||||||
},
|
},
|
||||||
"request": "/groups/1/scenes/1/store",
|
"request": "/groups/1/scenes/1/store",
|
||||||
|
"request_data": {},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
( # Presence reset button
|
||||||
|
{
|
||||||
|
"sensors": {
|
||||||
|
"1": {
|
||||||
|
"config": {
|
||||||
|
"devicemode": "undirected",
|
||||||
|
"on": True,
|
||||||
|
"reachable": True,
|
||||||
|
"sensitivity": 3,
|
||||||
|
"triggerdistance": "medium",
|
||||||
|
},
|
||||||
|
"etag": "13ff209f9401b317987d42506dd4cd79",
|
||||||
|
"lastannounced": None,
|
||||||
|
"lastseen": "2022-06-28T23:13Z",
|
||||||
|
"manufacturername": "aqara",
|
||||||
|
"modelid": "lumi.motion.ac01",
|
||||||
|
"name": "Aqara FP1",
|
||||||
|
"state": {
|
||||||
|
"lastupdated": "2022-06-28T23:13:38.577",
|
||||||
|
"presence": True,
|
||||||
|
"presenceevent": "leave",
|
||||||
|
},
|
||||||
|
"swversion": "20210121",
|
||||||
|
"type": "ZHAPresence",
|
||||||
|
"uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entity_count": 5,
|
||||||
|
"device_count": 3,
|
||||||
|
"entity_id": "button.aqara_fp1_reset_presence",
|
||||||
|
"unique_id": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406-reset_presence",
|
||||||
|
"entity_category": EntityCategory.CONFIG,
|
||||||
|
"attributes": {
|
||||||
|
"device_class": "restart",
|
||||||
|
"friendly_name": "Aqara FP1 Reset Presence",
|
||||||
|
},
|
||||||
|
"request": "/sensors/1/config",
|
||||||
|
"request_data": {"resetpresence": True},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -92,7 +135,7 @@ async def test_button(hass, aioclient_mock, raw_data, expected):
|
||||||
{ATTR_ENTITY_ID: expected["entity_id"]},
|
{ATTR_ENTITY_ID: expected["entity_id"]},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
assert aioclient_mock.mock_calls[1][2] == {}
|
assert aioclient_mock.mock_calls[1][2] == expected["request_data"]
|
||||||
|
|
||||||
# Unload entry
|
# Unload entry
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,13 @@ from pydeconz.models.sensor.ancillary_control import (
|
||||||
AncillaryControlAction,
|
AncillaryControlAction,
|
||||||
AncillaryControlPanel,
|
AncillaryControlPanel,
|
||||||
)
|
)
|
||||||
|
from pydeconz.models.sensor.presence import PresenceStatePresenceEvent
|
||||||
|
|
||||||
from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
|
from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
|
||||||
from homeassistant.components.deconz.deconz_event import (
|
from homeassistant.components.deconz.deconz_event import (
|
||||||
CONF_DECONZ_ALARM_EVENT,
|
CONF_DECONZ_ALARM_EVENT,
|
||||||
CONF_DECONZ_EVENT,
|
CONF_DECONZ_EVENT,
|
||||||
|
CONF_DECONZ_PRESENCE_EVENT,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_DEVICE_ID,
|
CONF_DEVICE_ID,
|
||||||
|
@ -412,6 +414,107 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket):
|
||||||
assert len(hass.states.async_all()) == 0
|
assert len(hass.states.async_all()) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_deconz_presence_events(hass, aioclient_mock, mock_deconz_websocket):
|
||||||
|
"""Test successful creation of deconz presence events."""
|
||||||
|
data = {
|
||||||
|
"sensors": {
|
||||||
|
"1": {
|
||||||
|
"config": {
|
||||||
|
"devicemode": "undirected",
|
||||||
|
"on": True,
|
||||||
|
"reachable": True,
|
||||||
|
"sensitivity": 3,
|
||||||
|
"triggerdistance": "medium",
|
||||||
|
},
|
||||||
|
"etag": "13ff209f9401b317987d42506dd4cd79",
|
||||||
|
"lastannounced": None,
|
||||||
|
"lastseen": "2022-06-28T23:13Z",
|
||||||
|
"manufacturername": "aqara",
|
||||||
|
"modelid": "lumi.motion.ac01",
|
||||||
|
"name": "Aqara FP1",
|
||||||
|
"state": {
|
||||||
|
"lastupdated": "2022-06-28T23:13:38.577",
|
||||||
|
"presence": True,
|
||||||
|
"presenceevent": "leave",
|
||||||
|
},
|
||||||
|
"swversion": "20210121",
|
||||||
|
"type": "ZHAPresence",
|
||||||
|
"uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with patch.dict(DECONZ_WEB_REQUEST, data):
|
||||||
|
config_entry = await setup_deconz_integration(hass, aioclient_mock)
|
||||||
|
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
|
||||||
|
assert len(hass.states.async_all()) == 5
|
||||||
|
assert (
|
||||||
|
len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id))
|
||||||
|
== 3
|
||||||
|
)
|
||||||
|
|
||||||
|
device = device_registry.async_get_device(
|
||||||
|
identifiers={(DECONZ_DOMAIN, "xx:xx:xx:xx:xx:xx:xx:xx")}
|
||||||
|
)
|
||||||
|
|
||||||
|
captured_events = async_capture_events(hass, CONF_DECONZ_PRESENCE_EVENT)
|
||||||
|
|
||||||
|
for presence_event in (
|
||||||
|
PresenceStatePresenceEvent.ABSENTING,
|
||||||
|
PresenceStatePresenceEvent.APPROACHING,
|
||||||
|
PresenceStatePresenceEvent.ENTER,
|
||||||
|
PresenceStatePresenceEvent.ENTER_LEFT,
|
||||||
|
PresenceStatePresenceEvent.ENTER_RIGHT,
|
||||||
|
PresenceStatePresenceEvent.LEAVE,
|
||||||
|
PresenceStatePresenceEvent.LEFT_LEAVE,
|
||||||
|
PresenceStatePresenceEvent.RIGHT_LEAVE,
|
||||||
|
):
|
||||||
|
event_changed_sensor = {
|
||||||
|
"t": "event",
|
||||||
|
"e": "changed",
|
||||||
|
"r": "sensors",
|
||||||
|
"id": "1",
|
||||||
|
"state": {"presenceevent": presence_event},
|
||||||
|
}
|
||||||
|
await mock_deconz_websocket(data=event_changed_sensor)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(captured_events) == 1
|
||||||
|
assert captured_events[0].data == {
|
||||||
|
CONF_ID: "aqara_fp1",
|
||||||
|
CONF_UNIQUE_ID: "xx:xx:xx:xx:xx:xx:xx:xx",
|
||||||
|
CONF_DEVICE_ID: device.id,
|
||||||
|
CONF_EVENT: presence_event.value,
|
||||||
|
}
|
||||||
|
captured_events.clear()
|
||||||
|
|
||||||
|
# Unsupported presence event
|
||||||
|
|
||||||
|
event_changed_sensor = {
|
||||||
|
"t": "event",
|
||||||
|
"e": "changed",
|
||||||
|
"r": "sensors",
|
||||||
|
"id": "1",
|
||||||
|
"state": {"presenceevent": PresenceStatePresenceEvent.NINE},
|
||||||
|
}
|
||||||
|
await mock_deconz_websocket(data=event_changed_sensor)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(captured_events) == 0
|
||||||
|
|
||||||
|
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||||
|
|
||||||
|
states = hass.states.async_all()
|
||||||
|
assert len(hass.states.async_all()) == 5
|
||||||
|
for state in states:
|
||||||
|
assert state.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
|
||||||
|
|
||||||
|
|
||||||
async def test_deconz_events_bad_unique_id(hass, aioclient_mock):
|
async def test_deconz_events_bad_unique_id(hass, aioclient_mock):
|
||||||
"""Verify no devices are created if unique id is bad or missing."""
|
"""Verify no devices are created if unique id is bad or missing."""
|
||||||
data = {
|
data = {
|
||||||
|
|
|
@ -57,6 +57,7 @@ async def test_entry_diagnostics(
|
||||||
str(Platform.LOCK): [],
|
str(Platform.LOCK): [],
|
||||||
str(Platform.NUMBER): [],
|
str(Platform.NUMBER): [],
|
||||||
str(Platform.SCENE): [],
|
str(Platform.SCENE): [],
|
||||||
|
str(Platform.SELECT): [],
|
||||||
str(Platform.SENSOR): [],
|
str(Platform.SENSOR): [],
|
||||||
str(Platform.SIREN): [],
|
str(Platform.SIREN): [],
|
||||||
str(Platform.SWITCH): [],
|
str(Platform.SWITCH): [],
|
||||||
|
|
|
@ -28,6 +28,7 @@ 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.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.select import DOMAIN as SELECT_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
|
||||||
from homeassistant.components.ssdp import (
|
from homeassistant.components.ssdp import (
|
||||||
|
@ -169,9 +170,10 @@ async def test_gateway_setup(hass, aioclient_mock):
|
||||||
assert forward_entry_setup.mock_calls[7][1] == (config_entry, LOCK_DOMAIN)
|
assert forward_entry_setup.mock_calls[7][1] == (config_entry, LOCK_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[8][1] == (config_entry, NUMBER_DOMAIN)
|
assert forward_entry_setup.mock_calls[8][1] == (config_entry, NUMBER_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[9][1] == (config_entry, SCENE_DOMAIN)
|
assert forward_entry_setup.mock_calls[9][1] == (config_entry, SCENE_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[10][1] == (config_entry, SENSOR_DOMAIN)
|
assert forward_entry_setup.mock_calls[10][1] == (config_entry, SELECT_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[11][1] == (config_entry, SIREN_DOMAIN)
|
assert forward_entry_setup.mock_calls[11][1] == (config_entry, SENSOR_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[12][1] == (config_entry, SWITCH_DOMAIN)
|
assert forward_entry_setup.mock_calls[12][1] == (config_entry, SIREN_DOMAIN)
|
||||||
|
assert forward_entry_setup.mock_calls[13][1] == (config_entry, SWITCH_DOMAIN)
|
||||||
|
|
||||||
device_registry = dr.async_get(hass)
|
device_registry = dr.async_get(hass)
|
||||||
gateway_entry = device_registry.async_get_device(
|
gateway_entry = device_registry.async_get_device(
|
||||||
|
|
|
@ -0,0 +1,219 @@
|
||||||
|
"""deCONZ select platform tests."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from pydeconz.models.sensor.presence import (
|
||||||
|
PresenceConfigDeviceMode,
|
||||||
|
PresenceConfigTriggerDistance,
|
||||||
|
)
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.select import (
|
||||||
|
ATTR_OPTION,
|
||||||
|
DOMAIN as SELECT_DOMAIN,
|
||||||
|
SERVICE_SELECT_OPTION,
|
||||||
|
)
|
||||||
|
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 (
|
||||||
|
DECONZ_WEB_REQUEST,
|
||||||
|
mock_deconz_put_request,
|
||||||
|
setup_deconz_integration,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_select_entities(hass, aioclient_mock):
|
||||||
|
"""Test that no sensors in deconz results in no sensor entities."""
|
||||||
|
await setup_deconz_integration(hass, aioclient_mock)
|
||||||
|
assert len(hass.states.async_all()) == 0
|
||||||
|
|
||||||
|
|
||||||
|
TEST_DATA = [
|
||||||
|
( # Presence Device Mode
|
||||||
|
{
|
||||||
|
"sensors": {
|
||||||
|
"1": {
|
||||||
|
"config": {
|
||||||
|
"devicemode": "undirected",
|
||||||
|
"on": True,
|
||||||
|
"reachable": True,
|
||||||
|
"sensitivity": 3,
|
||||||
|
"triggerdistance": "medium",
|
||||||
|
},
|
||||||
|
"etag": "13ff209f9401b317987d42506dd4cd79",
|
||||||
|
"lastannounced": None,
|
||||||
|
"lastseen": "2022-06-28T23:13Z",
|
||||||
|
"manufacturername": "aqara",
|
||||||
|
"modelid": "lumi.motion.ac01",
|
||||||
|
"name": "Aqara FP1",
|
||||||
|
"state": {
|
||||||
|
"lastupdated": "2022-06-28T23:13:38.577",
|
||||||
|
"presence": True,
|
||||||
|
"presenceevent": "leave",
|
||||||
|
},
|
||||||
|
"swversion": "20210121",
|
||||||
|
"type": "ZHAPresence",
|
||||||
|
"uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entity_count": 5,
|
||||||
|
"device_count": 3,
|
||||||
|
"entity_id": "select.aqara_fp1_device_mode",
|
||||||
|
"unique_id": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406-device_mode",
|
||||||
|
"entity_category": EntityCategory.CONFIG,
|
||||||
|
"attributes": {
|
||||||
|
"friendly_name": "Aqara FP1 Device Mode",
|
||||||
|
"options": ["leftright", "undirected"],
|
||||||
|
},
|
||||||
|
"option": PresenceConfigDeviceMode.LEFT_AND_RIGHT.value,
|
||||||
|
"request": "/sensors/1/config",
|
||||||
|
"request_data": {"devicemode": "leftright"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
( # Presence Sensitivity
|
||||||
|
{
|
||||||
|
"sensors": {
|
||||||
|
"1": {
|
||||||
|
"config": {
|
||||||
|
"devicemode": "undirected",
|
||||||
|
"on": True,
|
||||||
|
"reachable": True,
|
||||||
|
"sensitivity": 3,
|
||||||
|
"triggerdistance": "medium",
|
||||||
|
},
|
||||||
|
"etag": "13ff209f9401b317987d42506dd4cd79",
|
||||||
|
"lastannounced": None,
|
||||||
|
"lastseen": "2022-06-28T23:13Z",
|
||||||
|
"manufacturername": "aqara",
|
||||||
|
"modelid": "lumi.motion.ac01",
|
||||||
|
"name": "Aqara FP1",
|
||||||
|
"state": {
|
||||||
|
"lastupdated": "2022-06-28T23:13:38.577",
|
||||||
|
"presence": True,
|
||||||
|
"presenceevent": "leave",
|
||||||
|
},
|
||||||
|
"swversion": "20210121",
|
||||||
|
"type": "ZHAPresence",
|
||||||
|
"uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entity_count": 5,
|
||||||
|
"device_count": 3,
|
||||||
|
"entity_id": "select.aqara_fp1_sensitivity",
|
||||||
|
"unique_id": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406-sensitivity",
|
||||||
|
"entity_category": EntityCategory.CONFIG,
|
||||||
|
"attributes": {
|
||||||
|
"friendly_name": "Aqara FP1 Sensitivity",
|
||||||
|
"options": ["High", "Medium", "Low"],
|
||||||
|
},
|
||||||
|
"option": "Medium",
|
||||||
|
"request": "/sensors/1/config",
|
||||||
|
"request_data": {"sensitivity": 2},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
( # Presence Trigger Distance
|
||||||
|
{
|
||||||
|
"sensors": {
|
||||||
|
"1": {
|
||||||
|
"config": {
|
||||||
|
"devicemode": "undirected",
|
||||||
|
"on": True,
|
||||||
|
"reachable": True,
|
||||||
|
"sensitivity": 3,
|
||||||
|
"triggerdistance": "medium",
|
||||||
|
},
|
||||||
|
"etag": "13ff209f9401b317987d42506dd4cd79",
|
||||||
|
"lastannounced": None,
|
||||||
|
"lastseen": "2022-06-28T23:13Z",
|
||||||
|
"manufacturername": "aqara",
|
||||||
|
"modelid": "lumi.motion.ac01",
|
||||||
|
"name": "Aqara FP1",
|
||||||
|
"state": {
|
||||||
|
"lastupdated": "2022-06-28T23:13:38.577",
|
||||||
|
"presence": True,
|
||||||
|
"presenceevent": "leave",
|
||||||
|
},
|
||||||
|
"swversion": "20210121",
|
||||||
|
"type": "ZHAPresence",
|
||||||
|
"uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entity_count": 5,
|
||||||
|
"device_count": 3,
|
||||||
|
"entity_id": "select.aqara_fp1_trigger_distance",
|
||||||
|
"unique_id": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406-trigger_distance",
|
||||||
|
"entity_category": EntityCategory.CONFIG,
|
||||||
|
"attributes": {
|
||||||
|
"friendly_name": "Aqara FP1 Trigger Distance",
|
||||||
|
"options": ["far", "medium", "near"],
|
||||||
|
},
|
||||||
|
"option": PresenceConfigTriggerDistance.FAR.value,
|
||||||
|
"request": "/sensors/1/config",
|
||||||
|
"request_data": {"triggerdistance": "far"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("raw_data, expected", TEST_DATA)
|
||||||
|
async def test_select(hass, aioclient_mock, raw_data, expected):
|
||||||
|
"""Test successful creation of button entities."""
|
||||||
|
ent_reg = er.async_get(hass)
|
||||||
|
dev_reg = dr.async_get(hass)
|
||||||
|
|
||||||
|
with patch.dict(DECONZ_WEB_REQUEST, raw_data):
|
||||||
|
config_entry = await setup_deconz_integration(hass, aioclient_mock)
|
||||||
|
|
||||||
|
assert len(hass.states.async_all()) == expected["entity_count"]
|
||||||
|
|
||||||
|
# Verify state data
|
||||||
|
|
||||||
|
button = hass.states.get(expected["entity_id"])
|
||||||
|
assert button.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"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify selecting option
|
||||||
|
|
||||||
|
mock_deconz_put_request(aioclient_mock, config_entry.data, expected["request"])
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
SELECT_DOMAIN,
|
||||||
|
SERVICE_SELECT_OPTION,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: expected["entity_id"],
|
||||||
|
ATTR_OPTION: expected["option"],
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert aioclient_mock.mock_calls[1][2] == expected["request_data"]
|
||||||
|
|
||||||
|
# Unload entry
|
||||||
|
|
||||||
|
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.async_block_till_done()
|
||||||
|
assert len(hass.states.async_all()) == 0
|
Loading…
Reference in New Issue