Add Aqara FP1 support to deCONZ integration (#77568)

pull/77586/head
Robert Svensson 2022-08-31 05:33:05 +02:00 committed by GitHub
parent 5b3f4ec471
commit ff3d3088ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 610 additions and 13 deletions

View File

@ -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,
)

View File

@ -35,6 +35,7 @@ PLATFORMS = [
Platform.LOCK,
Platform.NUMBER,
Platform.SCENE,
Platform.SELECT,
Platform.SENSOR,
Platform.SIREN,
Platform.SWITCH,

View File

@ -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)

View File

@ -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():

View File

@ -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()

View File

@ -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),
)

View File

@ -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

View File

@ -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 = {

View File

@ -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): [],

View File

@ -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(

View File

@ -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