mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 09:47:13 +00:00
Add alarm control panel support to deCONZ integration (#48736)
* Infrastructure in place * Base implementation * Add alarm event * Add custom services to alarm control panel * Add service descriptions * Increase test coverage * Simplified to one entity service with an options selector * Remove everything but the essentials * Add library with proper support * Fix stale comments
This commit is contained in:
parent
fa05e5a8a0
commit
34245c3add
133
homeassistant/components/deconz/alarm_control_panel.py
Normal file
133
homeassistant/components/deconz/alarm_control_panel.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
"""Support for deCONZ alarm control panel devices."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pydeconz.sensor import (
|
||||||
|
ANCILLARY_CONTROL_ARMED_AWAY,
|
||||||
|
ANCILLARY_CONTROL_ARMED_NIGHT,
|
||||||
|
ANCILLARY_CONTROL_ARMED_STAY,
|
||||||
|
ANCILLARY_CONTROL_DISARMED,
|
||||||
|
AncillaryControl,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.alarm_control_panel import (
|
||||||
|
DOMAIN,
|
||||||
|
SUPPORT_ALARM_ARM_AWAY,
|
||||||
|
SUPPORT_ALARM_ARM_HOME,
|
||||||
|
SUPPORT_ALARM_ARM_NIGHT,
|
||||||
|
AlarmControlPanelEntity,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
|
from .const import NEW_SENSOR
|
||||||
|
from .deconz_device import DeconzDevice
|
||||||
|
from .gateway import get_gateway_from_config_entry
|
||||||
|
|
||||||
|
DECONZ_TO_ALARM_STATE = {
|
||||||
|
ANCILLARY_CONTROL_ARMED_AWAY: STATE_ALARM_ARMED_AWAY,
|
||||||
|
ANCILLARY_CONTROL_ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT,
|
||||||
|
ANCILLARY_CONTROL_ARMED_STAY: STATE_ALARM_ARMED_HOME,
|
||||||
|
ANCILLARY_CONTROL_DISARMED: STATE_ALARM_DISARMED,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities) -> None:
|
||||||
|
"""Set up the deCONZ alarm control panel devices.
|
||||||
|
|
||||||
|
Alarm control panels are based on the same device class as sensors in deCONZ.
|
||||||
|
"""
|
||||||
|
gateway = get_gateway_from_config_entry(hass, config_entry)
|
||||||
|
gateway.entities[DOMAIN] = set()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_add_alarm_control_panel(sensors=gateway.api.sensors.values()) -> None:
|
||||||
|
"""Add alarm control panel devices from deCONZ."""
|
||||||
|
entities = []
|
||||||
|
|
||||||
|
for sensor in sensors:
|
||||||
|
|
||||||
|
if (
|
||||||
|
sensor.type in AncillaryControl.ZHATYPE
|
||||||
|
and sensor.uniqueid not in gateway.entities[DOMAIN]
|
||||||
|
):
|
||||||
|
entities.append(DeconzAlarmControlPanel(sensor, gateway))
|
||||||
|
|
||||||
|
if entities:
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
gateway.listeners.append(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
hass,
|
||||||
|
gateway.async_signal_new_device(NEW_SENSOR),
|
||||||
|
async_add_alarm_control_panel,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async_add_alarm_control_panel()
|
||||||
|
|
||||||
|
|
||||||
|
class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity):
|
||||||
|
"""Representation of a deCONZ alarm control panel."""
|
||||||
|
|
||||||
|
TYPE = DOMAIN
|
||||||
|
|
||||||
|
def __init__(self, device, gateway) -> None:
|
||||||
|
"""Set up alarm control panel device."""
|
||||||
|
super().__init__(device, gateway)
|
||||||
|
|
||||||
|
self._features = SUPPORT_ALARM_ARM_AWAY
|
||||||
|
self._features |= SUPPORT_ALARM_ARM_HOME
|
||||||
|
self._features |= SUPPORT_ALARM_ARM_NIGHT
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self) -> int:
|
||||||
|
"""Return the list of supported features."""
|
||||||
|
return self._features
|
||||||
|
|
||||||
|
@property
|
||||||
|
def code_arm_required(self) -> bool:
|
||||||
|
"""Code is not required for arm actions."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def code_format(self) -> None:
|
||||||
|
"""Code is not supported."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_update_callback(self, force_update: bool = False) -> None:
|
||||||
|
"""Update the control panels state."""
|
||||||
|
keys = {"armed", "reachable"}
|
||||||
|
if force_update or (
|
||||||
|
self._device.changed_keys.intersection(keys)
|
||||||
|
and self._device.state in DECONZ_TO_ALARM_STATE
|
||||||
|
):
|
||||||
|
super().async_update_callback(force_update=force_update)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> str:
|
||||||
|
"""Return the state of the control panel."""
|
||||||
|
return DECONZ_TO_ALARM_STATE.get(self._device.state, STATE_UNKNOWN)
|
||||||
|
|
||||||
|
async def async_alarm_arm_away(self, code: None = None) -> None:
|
||||||
|
"""Send arm away command."""
|
||||||
|
await self._device.arm_away()
|
||||||
|
|
||||||
|
async def async_alarm_arm_home(self, code: None = None) -> None:
|
||||||
|
"""Send arm home command."""
|
||||||
|
await self._device.arm_stay()
|
||||||
|
|
||||||
|
async def async_alarm_arm_night(self, code: None = None) -> None:
|
||||||
|
"""Send arm night command."""
|
||||||
|
await self._device.arm_night()
|
||||||
|
|
||||||
|
async def async_alarm_disarm(self, code: None = None) -> None:
|
||||||
|
"""Send disarm command."""
|
||||||
|
await self._device.disarm()
|
@ -1,6 +1,9 @@
|
|||||||
"""Constants for the deCONZ component."""
|
"""Constants for the deCONZ component."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.alarm_control_panel import (
|
||||||
|
DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
|
||||||
|
)
|
||||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||||
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
|
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
|
||||||
@ -29,6 +32,7 @@ CONF_ALLOW_NEW_DEVICES = "allow_new_devices"
|
|||||||
CONF_MASTER_GATEWAY = "master"
|
CONF_MASTER_GATEWAY = "master"
|
||||||
|
|
||||||
PLATFORMS = [
|
PLATFORMS = [
|
||||||
|
ALARM_CONTROL_PANEL_DOMAIN,
|
||||||
BINARY_SENSOR_DOMAIN,
|
BINARY_SENSOR_DOMAIN,
|
||||||
CLIMATE_DOMAIN,
|
CLIMATE_DOMAIN,
|
||||||
COVER_DOMAIN,
|
COVER_DOMAIN,
|
||||||
|
@ -1,7 +1,26 @@
|
|||||||
"""Representation of a deCONZ remote."""
|
"""Representation of a deCONZ remote or keypad."""
|
||||||
from pydeconz.sensor import Switch
|
|
||||||
|
|
||||||
from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_UNIQUE_ID
|
from pydeconz.sensor import (
|
||||||
|
ANCILLARY_CONTROL_ARMED_AWAY,
|
||||||
|
ANCILLARY_CONTROL_ARMED_NIGHT,
|
||||||
|
ANCILLARY_CONTROL_ARMED_STAY,
|
||||||
|
ANCILLARY_CONTROL_DISARMED,
|
||||||
|
AncillaryControl,
|
||||||
|
Switch,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_CODE,
|
||||||
|
CONF_DEVICE_ID,
|
||||||
|
CONF_EVENT,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_UNIQUE_ID,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
@ -10,6 +29,14 @@ from .const import CONF_ANGLE, CONF_GESTURE, CONF_XY, LOGGER, NEW_SENSOR
|
|||||||
from .deconz_device import DeconzBase
|
from .deconz_device import DeconzBase
|
||||||
|
|
||||||
CONF_DECONZ_EVENT = "deconz_event"
|
CONF_DECONZ_EVENT = "deconz_event"
|
||||||
|
CONF_DECONZ_ALARM_EVENT = "deconz_alarm_event"
|
||||||
|
|
||||||
|
DECONZ_TO_ALARM_STATE = {
|
||||||
|
ANCILLARY_CONTROL_ARMED_AWAY: STATE_ALARM_ARMED_AWAY,
|
||||||
|
ANCILLARY_CONTROL_ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT,
|
||||||
|
ANCILLARY_CONTROL_ARMED_STAY: STATE_ALARM_ARMED_HOME,
|
||||||
|
ANCILLARY_CONTROL_DISARMED: STATE_ALARM_DISARMED,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_events(gateway) -> None:
|
async def async_setup_events(gateway) -> None:
|
||||||
@ -23,12 +50,18 @@ async def async_setup_events(gateway) -> None:
|
|||||||
if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"):
|
if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if sensor.type not in Switch.ZHATYPE or sensor.uniqueid in {
|
if (
|
||||||
event.unique_id for event in gateway.events
|
sensor.type not in Switch.ZHATYPE + AncillaryControl.ZHATYPE
|
||||||
}:
|
or sensor.uniqueid in {event.unique_id for event in gateway.events}
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
new_event = DeconzEvent(sensor, gateway)
|
if sensor.type in Switch.ZHATYPE:
|
||||||
|
new_event = DeconzEvent(sensor, gateway)
|
||||||
|
|
||||||
|
elif sensor.type in AncillaryControl.ZHATYPE:
|
||||||
|
new_event = DeconzAlarmEvent(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)
|
||||||
|
|
||||||
@ -119,3 +152,29 @@ class DeconzEvent(DeconzBase):
|
|||||||
config_entry_id=self.gateway.config_entry.entry_id, **self.device_info
|
config_entry_id=self.gateway.config_entry.entry_id, **self.device_info
|
||||||
)
|
)
|
||||||
self.device_id = entry.id
|
self.device_id = entry.id
|
||||||
|
|
||||||
|
|
||||||
|
class DeconzAlarmEvent(DeconzEvent):
|
||||||
|
"""Alarm control panel companion event when user inputs a code."""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_update_callback(self, force_update=False):
|
||||||
|
"""Fire the event if reason is that state is updated."""
|
||||||
|
if (
|
||||||
|
self.gateway.ignore_state_updates
|
||||||
|
or "action" not in self._device.changed_keys
|
||||||
|
or self._device.action == ""
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
state, code, _area = self._device.action.split(",")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
CONF_ID: self.event_id,
|
||||||
|
CONF_UNIQUE_ID: self.serial,
|
||||||
|
CONF_DEVICE_ID: self.device_id,
|
||||||
|
CONF_EVENT: DECONZ_TO_ALARM_STATE.get(state, STATE_UNKNOWN),
|
||||||
|
CONF_CODE: code,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.gateway.hass.bus.async_fire(CONF_DECONZ_ALARM_EVENT, data)
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "deCONZ",
|
"name": "deCONZ",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/deconz",
|
"documentation": "https://www.home-assistant.io/integrations/deconz",
|
||||||
"requirements": ["pydeconz==78"],
|
"requirements": ["pydeconz==79"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Royal Philips Electronics"
|
"manufacturer": "Royal Philips Electronics"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Support for deCONZ sensors."""
|
"""Support for deCONZ sensors."""
|
||||||
from pydeconz.sensor import (
|
from pydeconz.sensor import (
|
||||||
|
AncillaryControl,
|
||||||
Battery,
|
Battery,
|
||||||
Consumption,
|
Consumption,
|
||||||
Daylight,
|
Daylight,
|
||||||
@ -104,7 +105,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
if (
|
if (
|
||||||
not sensor.BINARY
|
not sensor.BINARY
|
||||||
and sensor.type
|
and sensor.type
|
||||||
not in Battery.ZHATYPE
|
not in AncillaryControl.ZHATYPE
|
||||||
|
+ Battery.ZHATYPE
|
||||||
+ DoorLock.ZHATYPE
|
+ DoorLock.ZHATYPE
|
||||||
+ Switch.ZHATYPE
|
+ Switch.ZHATYPE
|
||||||
+ Thermostat.ZHATYPE
|
+ Thermostat.ZHATYPE
|
||||||
|
@ -1343,7 +1343,7 @@ pydaikin==2.4.1
|
|||||||
pydanfossair==0.1.0
|
pydanfossair==0.1.0
|
||||||
|
|
||||||
# homeassistant.components.deconz
|
# homeassistant.components.deconz
|
||||||
pydeconz==78
|
pydeconz==79
|
||||||
|
|
||||||
# homeassistant.components.delijn
|
# homeassistant.components.delijn
|
||||||
pydelijn==0.6.1
|
pydelijn==0.6.1
|
||||||
|
@ -726,7 +726,7 @@ pycoolmasternet-async==0.1.2
|
|||||||
pydaikin==2.4.1
|
pydaikin==2.4.1
|
||||||
|
|
||||||
# homeassistant.components.deconz
|
# homeassistant.components.deconz
|
||||||
pydeconz==78
|
pydeconz==79
|
||||||
|
|
||||||
# homeassistant.components.dexcom
|
# homeassistant.components.dexcom
|
||||||
pydexcom==0.2.0
|
pydexcom==0.2.0
|
||||||
|
216
tests/components/deconz/test_alarm_control_panel.py
Normal file
216
tests/components/deconz/test_alarm_control_panel.py
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
"""deCONZ alarm control panel platform tests."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from pydeconz.sensor import (
|
||||||
|
ANCILLARY_CONTROL_ARMED_AWAY,
|
||||||
|
ANCILLARY_CONTROL_ARMED_NIGHT,
|
||||||
|
ANCILLARY_CONTROL_ARMED_STAY,
|
||||||
|
ANCILLARY_CONTROL_DISARMED,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.alarm_control_panel import (
|
||||||
|
DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
SERVICE_ALARM_ARM_AWAY,
|
||||||
|
SERVICE_ALARM_ARM_HOME,
|
||||||
|
SERVICE_ALARM_ARM_NIGHT,
|
||||||
|
SERVICE_ALARM_DISARM,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .test_gateway import (
|
||||||
|
DECONZ_WEB_REQUEST,
|
||||||
|
mock_deconz_put_request,
|
||||||
|
setup_deconz_integration,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_sensors(hass, aioclient_mock):
|
||||||
|
"""Test that no sensors in deconz results in no climate entities."""
|
||||||
|
await setup_deconz_integration(hass, aioclient_mock)
|
||||||
|
assert len(hass.states.async_all()) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_alarm_control_panel(hass, aioclient_mock, mock_deconz_websocket):
|
||||||
|
"""Test successful creation of alarm control panel entities."""
|
||||||
|
data = {
|
||||||
|
"sensors": {
|
||||||
|
"0": {
|
||||||
|
"config": {
|
||||||
|
"armed": "disarmed",
|
||||||
|
"enrolled": 0,
|
||||||
|
"on": True,
|
||||||
|
"panel": "disarmed",
|
||||||
|
"pending": [],
|
||||||
|
"reachable": True,
|
||||||
|
},
|
||||||
|
"ep": 1,
|
||||||
|
"etag": "3c4008d74035dfaa1f0bb30d24468b12",
|
||||||
|
"lastseen": "2021-04-02T13:07Z",
|
||||||
|
"manufacturername": "Universal Electronics Inc",
|
||||||
|
"modelid": "URC4450BC0-X-R",
|
||||||
|
"name": "Keypad",
|
||||||
|
"state": {
|
||||||
|
"action": "armed_away,1111,55",
|
||||||
|
"lastupdated": "2021-04-02T13:08:18.937",
|
||||||
|
"lowbattery": False,
|
||||||
|
"tampered": True,
|
||||||
|
},
|
||||||
|
"type": "ZHAAncillaryControl",
|
||||||
|
"uniqueid": "00:0d:6f:00:13:4f:61:39-01-0501",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with patch.dict(DECONZ_WEB_REQUEST, data):
|
||||||
|
config_entry = await setup_deconz_integration(hass, aioclient_mock)
|
||||||
|
|
||||||
|
assert len(hass.states.async_all()) == 1
|
||||||
|
assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_DISARMED
|
||||||
|
|
||||||
|
# Event signals alarm control panel armed away
|
||||||
|
|
||||||
|
event_changed_sensor = {
|
||||||
|
"t": "event",
|
||||||
|
"e": "changed",
|
||||||
|
"r": "sensors",
|
||||||
|
"id": "0",
|
||||||
|
"config": {"armed": ANCILLARY_CONTROL_ARMED_AWAY},
|
||||||
|
}
|
||||||
|
await mock_deconz_websocket(data=event_changed_sensor)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_AWAY
|
||||||
|
|
||||||
|
# Event signals alarm control panel armed night
|
||||||
|
|
||||||
|
event_changed_sensor = {
|
||||||
|
"t": "event",
|
||||||
|
"e": "changed",
|
||||||
|
"r": "sensors",
|
||||||
|
"id": "0",
|
||||||
|
"config": {"armed": ANCILLARY_CONTROL_ARMED_NIGHT},
|
||||||
|
}
|
||||||
|
await mock_deconz_websocket(data=event_changed_sensor)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_NIGHT
|
||||||
|
)
|
||||||
|
|
||||||
|
# Event signals alarm control panel armed home
|
||||||
|
|
||||||
|
event_changed_sensor = {
|
||||||
|
"t": "event",
|
||||||
|
"e": "changed",
|
||||||
|
"r": "sensors",
|
||||||
|
"id": "0",
|
||||||
|
"config": {"armed": ANCILLARY_CONTROL_ARMED_STAY},
|
||||||
|
}
|
||||||
|
await mock_deconz_websocket(data=event_changed_sensor)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_HOME
|
||||||
|
|
||||||
|
# Event signals alarm control panel armed night
|
||||||
|
|
||||||
|
event_changed_sensor = {
|
||||||
|
"t": "event",
|
||||||
|
"e": "changed",
|
||||||
|
"r": "sensors",
|
||||||
|
"id": "0",
|
||||||
|
"config": {"armed": ANCILLARY_CONTROL_ARMED_NIGHT},
|
||||||
|
}
|
||||||
|
await mock_deconz_websocket(data=event_changed_sensor)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_NIGHT
|
||||||
|
)
|
||||||
|
|
||||||
|
# Event signals alarm control panel disarmed
|
||||||
|
|
||||||
|
event_changed_sensor = {
|
||||||
|
"t": "event",
|
||||||
|
"e": "changed",
|
||||||
|
"r": "sensors",
|
||||||
|
"id": "0",
|
||||||
|
"config": {"armed": ANCILLARY_CONTROL_DISARMED},
|
||||||
|
}
|
||||||
|
await mock_deconz_websocket(data=event_changed_sensor)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_DISARMED
|
||||||
|
|
||||||
|
# Verify service calls
|
||||||
|
|
||||||
|
mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config")
|
||||||
|
|
||||||
|
# Service set alarm to away mode
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
ALARM_CONTROL_PANEL_DOMAIN,
|
||||||
|
SERVICE_ALARM_ARM_AWAY,
|
||||||
|
{ATTR_ENTITY_ID: "alarm_control_panel.keypad"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert aioclient_mock.mock_calls[1][2] == {
|
||||||
|
"armed": ANCILLARY_CONTROL_ARMED_AWAY,
|
||||||
|
"panel": ANCILLARY_CONTROL_ARMED_AWAY,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Service set alarm to home mode
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
ALARM_CONTROL_PANEL_DOMAIN,
|
||||||
|
SERVICE_ALARM_ARM_HOME,
|
||||||
|
{ATTR_ENTITY_ID: "alarm_control_panel.keypad"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert aioclient_mock.mock_calls[2][2] == {
|
||||||
|
"armed": ANCILLARY_CONTROL_ARMED_STAY,
|
||||||
|
"panel": ANCILLARY_CONTROL_ARMED_STAY,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Service set alarm to night mode
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
ALARM_CONTROL_PANEL_DOMAIN,
|
||||||
|
SERVICE_ALARM_ARM_NIGHT,
|
||||||
|
{ATTR_ENTITY_ID: "alarm_control_panel.keypad"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert aioclient_mock.mock_calls[3][2] == {
|
||||||
|
"armed": ANCILLARY_CONTROL_ARMED_NIGHT,
|
||||||
|
"panel": ANCILLARY_CONTROL_ARMED_NIGHT,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Service set alarm to disarmed
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
ALARM_CONTROL_PANEL_DOMAIN,
|
||||||
|
SERVICE_ALARM_DISARM,
|
||||||
|
{ATTR_ENTITY_ID: "alarm_control_panel.keypad"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert aioclient_mock.mock_calls[4][2] == {
|
||||||
|
"armed": ANCILLARY_CONTROL_DISARMED,
|
||||||
|
"panel": ANCILLARY_CONTROL_DISARMED,
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||||
|
|
||||||
|
states = hass.states.async_all()
|
||||||
|
assert len(states) == 1
|
||||||
|
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
|
@ -3,8 +3,18 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
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 CONF_DECONZ_EVENT
|
from homeassistant.components.deconz.deconz_event import (
|
||||||
from homeassistant.const import STATE_UNAVAILABLE
|
CONF_DECONZ_ALARM_EVENT,
|
||||||
|
CONF_DECONZ_EVENT,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_CODE,
|
||||||
|
CONF_DEVICE_ID,
|
||||||
|
CONF_EVENT,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_UNIQUE_ID,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
)
|
||||||
from homeassistant.helpers.device_registry import async_entries_for_config_entry
|
from homeassistant.helpers.device_registry import async_entries_for_config_entry
|
||||||
|
|
||||||
from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
|
from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
|
||||||
@ -161,6 +171,20 @@ async def test_deconz_events(hass, aioclient_mock, mock_deconz_websocket):
|
|||||||
"device_id": device.id,
|
"device_id": device.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Unsupported event
|
||||||
|
|
||||||
|
event_changed_sensor = {
|
||||||
|
"t": "event",
|
||||||
|
"e": "changed",
|
||||||
|
"r": "sensors",
|
||||||
|
"id": "1",
|
||||||
|
"name": "other name",
|
||||||
|
}
|
||||||
|
await mock_deconz_websocket(data=event_changed_sensor)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(captured_events) == 4
|
||||||
|
|
||||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||||
|
|
||||||
states = hass.states.async_all()
|
states = hass.states.async_all()
|
||||||
@ -173,6 +197,100 @@ async def test_deconz_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_alarm_events(hass, aioclient_mock, mock_deconz_websocket):
|
||||||
|
"""Test successful creation of deconz alarm events."""
|
||||||
|
data = {
|
||||||
|
"sensors": {
|
||||||
|
"1": {
|
||||||
|
"config": {
|
||||||
|
"armed": "disarmed",
|
||||||
|
"enrolled": 0,
|
||||||
|
"on": True,
|
||||||
|
"panel": "disarmed",
|
||||||
|
"pending": [],
|
||||||
|
"reachable": True,
|
||||||
|
},
|
||||||
|
"ep": 1,
|
||||||
|
"etag": "3c4008d74035dfaa1f0bb30d24468b12",
|
||||||
|
"lastseen": "2021-04-02T13:07Z",
|
||||||
|
"manufacturername": "Universal Electronics Inc",
|
||||||
|
"modelid": "URC4450BC0-X-R",
|
||||||
|
"name": "Keypad",
|
||||||
|
"state": {
|
||||||
|
"action": "armed_away,1111,55",
|
||||||
|
"lastupdated": "2021-04-02T13:08:18.937",
|
||||||
|
"lowbattery": False,
|
||||||
|
"tampered": True,
|
||||||
|
},
|
||||||
|
"type": "ZHAAncillaryControl",
|
||||||
|
"uniqueid": "00:00:00:00:00:00:00:01-01-0501",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with patch.dict(DECONZ_WEB_REQUEST, data):
|
||||||
|
config_entry = await setup_deconz_integration(hass, aioclient_mock)
|
||||||
|
|
||||||
|
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||||
|
|
||||||
|
assert len(hass.states.async_all()) == 1
|
||||||
|
# 1 alarm control device + 2 additional devices for deconz service and host
|
||||||
|
assert (
|
||||||
|
len(async_entries_for_config_entry(device_registry, config_entry.entry_id)) == 3
|
||||||
|
)
|
||||||
|
|
||||||
|
captured_events = async_capture_events(hass, CONF_DECONZ_ALARM_EVENT)
|
||||||
|
|
||||||
|
# Armed away event
|
||||||
|
|
||||||
|
event_changed_sensor = {
|
||||||
|
"t": "event",
|
||||||
|
"e": "changed",
|
||||||
|
"r": "sensors",
|
||||||
|
"id": "1",
|
||||||
|
"state": {"action": "armed_away,1234,1"},
|
||||||
|
}
|
||||||
|
await mock_deconz_websocket(data=event_changed_sensor)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
device = device_registry.async_get_device(
|
||||||
|
identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:01")}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(captured_events) == 1
|
||||||
|
assert captured_events[0].data == {
|
||||||
|
CONF_ID: "keypad",
|
||||||
|
CONF_UNIQUE_ID: "00:00:00:00:00:00:00:01",
|
||||||
|
CONF_DEVICE_ID: device.id,
|
||||||
|
CONF_EVENT: "armed_away",
|
||||||
|
CONF_CODE: "1234",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Unsupported event
|
||||||
|
|
||||||
|
event_changed_sensor = {
|
||||||
|
"t": "event",
|
||||||
|
"e": "changed",
|
||||||
|
"r": "sensors",
|
||||||
|
"id": "1",
|
||||||
|
"config": {"panel": "armed_away"},
|
||||||
|
}
|
||||||
|
await mock_deconz_websocket(data=event_changed_sensor)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(captured_events) == 1
|
||||||
|
|
||||||
|
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||||
|
|
||||||
|
states = hass.states.async_all()
|
||||||
|
assert len(hass.states.async_all()) == 1
|
||||||
|
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, mock_deconz_websocket):
|
async def test_deconz_events_bad_unique_id(hass, aioclient_mock, mock_deconz_websocket):
|
||||||
"""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 = {
|
||||||
|
@ -7,6 +7,9 @@ import pydeconz
|
|||||||
from pydeconz.websocket import STATE_RETRYING, STATE_RUNNING
|
from pydeconz.websocket import STATE_RETRYING, STATE_RUNNING
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.alarm_control_panel import (
|
||||||
|
DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
|
||||||
|
)
|
||||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||||
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
|
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
|
||||||
@ -147,17 +150,21 @@ async def test_gateway_setup(hass, aioclient_mock):
|
|||||||
assert len(hass.states.async_all()) == 0
|
assert len(hass.states.async_all()) == 0
|
||||||
|
|
||||||
assert forward_entry_setup.mock_calls[0][1] == (
|
assert forward_entry_setup.mock_calls[0][1] == (
|
||||||
|
config_entry,
|
||||||
|
ALARM_CONTROL_PANEL_DOMAIN,
|
||||||
|
)
|
||||||
|
assert forward_entry_setup.mock_calls[1][1] == (
|
||||||
config_entry,
|
config_entry,
|
||||||
BINARY_SENSOR_DOMAIN,
|
BINARY_SENSOR_DOMAIN,
|
||||||
)
|
)
|
||||||
assert forward_entry_setup.mock_calls[1][1] == (config_entry, CLIMATE_DOMAIN)
|
assert forward_entry_setup.mock_calls[2][1] == (config_entry, CLIMATE_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[2][1] == (config_entry, COVER_DOMAIN)
|
assert forward_entry_setup.mock_calls[3][1] == (config_entry, COVER_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[3][1] == (config_entry, FAN_DOMAIN)
|
assert forward_entry_setup.mock_calls[4][1] == (config_entry, FAN_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[4][1] == (config_entry, LIGHT_DOMAIN)
|
assert forward_entry_setup.mock_calls[5][1] == (config_entry, LIGHT_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[5][1] == (config_entry, LOCK_DOMAIN)
|
assert forward_entry_setup.mock_calls[6][1] == (config_entry, LOCK_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[6][1] == (config_entry, SCENE_DOMAIN)
|
assert forward_entry_setup.mock_calls[7][1] == (config_entry, SCENE_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[7][1] == (config_entry, SENSOR_DOMAIN)
|
assert forward_entry_setup.mock_calls[8][1] == (config_entry, SENSOR_DOMAIN)
|
||||||
assert forward_entry_setup.mock_calls[8][1] == (config_entry, SWITCH_DOMAIN)
|
assert forward_entry_setup.mock_calls[9][1] == (config_entry, SWITCH_DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
async def test_gateway_retry(hass):
|
async def test_gateway_retry(hass):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user