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

View File

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

View File

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

View File

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

View File

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

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", "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

View File

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

View File

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

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

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