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.scene import Scene as PydeconzScene
|
||||
from pydeconz.models.sensor.presence import Presence
|
||||
|
||||
from homeassistant.components.button import (
|
||||
DOMAIN,
|
||||
ButtonDeviceClass,
|
||||
ButtonEntity,
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
|
@ -17,7 +19,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||
from homeassistant.helpers.entity import EntityCategory
|
||||
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
|
||||
|
||||
|
||||
|
@ -61,7 +63,7 @@ async def async_setup_entry(
|
|||
"""Add scene button from deCONZ."""
|
||||
scene = gateway.api.scenes[scene_id]
|
||||
async_add_entities(
|
||||
DeconzButton(scene, gateway, description)
|
||||
DeconzSceneButton(scene, gateway, description)
|
||||
for description in ENTITY_DESCRIPTIONS.get(PydeconzScene, [])
|
||||
)
|
||||
|
||||
|
@ -70,8 +72,20 @@ async def async_setup_entry(
|
|||
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."""
|
||||
|
||||
TYPE = DOMAIN
|
||||
|
@ -99,3 +113,22 @@ class DeconzButton(DeconzSceneMixin, ButtonEntity):
|
|||
def get_device_identifier(self) -> str:
|
||||
"""Return a unique identifier for this scene."""
|
||||
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.NUMBER,
|
||||
Platform.SCENE,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SIREN,
|
||||
Platform.SWITCH,
|
||||
|
|
|
@ -9,6 +9,7 @@ from pydeconz.models.sensor.ancillary_control import (
|
|||
AncillaryControl,
|
||||
AncillaryControlAction,
|
||||
)
|
||||
from pydeconz.models.sensor.presence import Presence, PresenceStatePresenceEvent
|
||||
from pydeconz.models.sensor.switch import Switch
|
||||
|
||||
from homeassistant.const import (
|
||||
|
@ -28,6 +29,7 @@ from .gateway import DeconzGateway
|
|||
|
||||
CONF_DECONZ_EVENT = "deconz_event"
|
||||
CONF_DECONZ_ALARM_EVENT = "deconz_alarm_event"
|
||||
CONF_DECONZ_PRESENCE_EVENT = "deconz_presence_event"
|
||||
|
||||
SUPPORTED_DECONZ_ALARM_EVENTS = {
|
||||
AncillaryControlAction.EMERGENCY,
|
||||
|
@ -35,6 +37,16 @@ SUPPORTED_DECONZ_ALARM_EVENTS = {
|
|||
AncillaryControlAction.INVALID_CODE,
|
||||
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:
|
||||
|
@ -43,7 +55,7 @@ async def async_setup_events(gateway: DeconzGateway) -> None:
|
|||
@callback
|
||||
def async_add_sensor(_: EventType, sensor_id: str) -> None:
|
||||
"""Create DeconzEvent."""
|
||||
new_event: DeconzAlarmEvent | DeconzEvent
|
||||
new_event: DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent
|
||||
sensor = gateway.api.sensors[sensor_id]
|
||||
|
||||
if isinstance(sensor, Switch):
|
||||
|
@ -52,6 +64,11 @@ async def async_setup_events(gateway: DeconzGateway) -> None:
|
|||
elif isinstance(sensor, AncillaryControl):
|
||||
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.events.append(new_event)
|
||||
|
||||
|
@ -63,6 +80,10 @@ async def async_setup_events(gateway: DeconzGateway) -> None:
|
|||
async_add_sensor,
|
||||
gateway.api.sensors.ancillary_control,
|
||||
)
|
||||
gateway.register_platform_add_device_callback(
|
||||
async_add_sensor,
|
||||
gateway.api.sensors.presence,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
|
@ -83,7 +104,7 @@ class DeconzEventBase(DeconzBase):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
device: AncillaryControl | Switch,
|
||||
device: AncillaryControl | Presence | Switch,
|
||||
gateway: DeconzGateway,
|
||||
) -> None:
|
||||
"""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)
|
||||
|
||||
|
||||
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 . 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
|
||||
|
||||
CONF_SUBTYPE = "subtype"
|
||||
|
@ -622,7 +628,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
|||
def _get_deconz_event_from_device(
|
||||
hass: HomeAssistant,
|
||||
device: dr.DeviceEntry,
|
||||
) -> DeconzAlarmEvent | DeconzEvent:
|
||||
) -> DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent:
|
||||
"""Resolve deconz event from device."""
|
||||
gateways: dict[str, DeconzGateway] = hass.data.get(DOMAIN, {})
|
||||
for gateway in gateways.values():
|
||||
|
|
|
@ -41,7 +41,7 @@ from .const import (
|
|||
from .errors import AuthenticationRequired, CannotConnect
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .deconz_event import DeconzAlarmEvent, DeconzEvent
|
||||
from .deconz_event import DeconzAlarmEvent, DeconzEvent, DeconzPresenceEvent
|
||||
|
||||
SENSORS = (
|
||||
sensors.SensorResourceManager,
|
||||
|
@ -93,7 +93,7 @@ class DeconzGateway:
|
|||
|
||||
self.deconz_ids: dict[str, 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.deconz_groups: 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",
|
||||
},
|
||||
"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"]},
|
||||
blocking=True,
|
||||
)
|
||||
assert aioclient_mock.mock_calls[1][2] == {}
|
||||
assert aioclient_mock.mock_calls[1][2] == expected["request_data"]
|
||||
|
||||
# Unload entry
|
||||
|
||||
|
|
|
@ -6,11 +6,13 @@ from pydeconz.models.sensor.ancillary_control import (
|
|||
AncillaryControlAction,
|
||||
AncillaryControlPanel,
|
||||
)
|
||||
from pydeconz.models.sensor.presence import PresenceStatePresenceEvent
|
||||
|
||||
from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
|
||||
from homeassistant.components.deconz.deconz_event import (
|
||||
CONF_DECONZ_ALARM_EVENT,
|
||||
CONF_DECONZ_EVENT,
|
||||
CONF_DECONZ_PRESENCE_EVENT,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"""Verify no devices are created if unique id is bad or missing."""
|
||||
data = {
|
||||
|
|
|
@ -57,6 +57,7 @@ async def test_entry_diagnostics(
|
|||
str(Platform.LOCK): [],
|
||||
str(Platform.NUMBER): [],
|
||||
str(Platform.SCENE): [],
|
||||
str(Platform.SELECT): [],
|
||||
str(Platform.SENSOR): [],
|
||||
str(Platform.SIREN): [],
|
||||
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.number import DOMAIN as NUMBER_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.siren import DOMAIN as SIREN_DOMAIN
|
||||
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[8][1] == (config_entry, NUMBER_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[11][1] == (config_entry, SIREN_DOMAIN)
|
||||
assert forward_entry_setup.mock_calls[12][1] == (config_entry, SWITCH_DOMAIN)
|
||||
assert forward_entry_setup.mock_calls[10][1] == (config_entry, SELECT_DOMAIN)
|
||||
assert forward_entry_setup.mock_calls[11][1] == (config_entry, SENSOR_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)
|
||||
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