deCONZ use siren platform (#56397)

* Add siren.py

* Working siren platform with 100% test coverage

* Also add test file...

* Add test to verify that switch platform cleans up legacy entities now that sirens are their own platform

* Update homeassistant/components/deconz/siren.py

Co-authored-by: jjlawren <jjlawren@users.noreply.github.com>
This commit is contained in:
Robert Svensson 2021-09-18 21:59:04 +02:00 committed by GitHub
parent 6b6e26c96d
commit bf7c2753d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 237 additions and 105 deletions

View File

@ -12,6 +12,7 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
LOGGER = logging.getLogger(__package__)
@ -41,6 +42,7 @@ PLATFORMS = [
LOCK_DOMAIN,
SCENE_DOMAIN,
SENSOR_DOMAIN,
SIREN_DOMAIN,
SWITCH_DOMAIN,
]
@ -69,10 +71,11 @@ FANS = ["Fan"]
# Locks
LOCK_TYPES = ["Door Lock", "ZHADoorLock"]
# Sirens
SIRENS = ["Warning device"]
# Switches
POWER_PLUGS = ["On/Off light", "On/Off plug-in unit", "Smart plug"]
SIRENS = ["Warning device"]
SWITCH_TYPES = POWER_PLUGS + SIRENS
CONF_ANGLE = "angle"
CONF_GESTURE = "gesture"

View File

@ -34,7 +34,8 @@ from .const import (
LOCK_TYPES,
NEW_GROUP,
NEW_LIGHT,
SWITCH_TYPES,
POWER_PLUGS,
SIRENS,
)
from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
@ -42,14 +43,16 @@ from .gateway import get_gateway_from_config_entry
CONTROLLER = ["Configuration tool"]
DECONZ_GROUP = "is_deconz_group"
OTHER_LIGHT_RESOURCE_TYPES = (
CONTROLLER + COVER_TYPES + LOCK_TYPES + POWER_PLUGS + SIRENS
)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ lights and groups from a config entry."""
gateway = get_gateway_from_config_entry(hass, config_entry)
gateway.entities[DOMAIN] = set()
other_light_resource_types = CONTROLLER + COVER_TYPES + LOCK_TYPES + SWITCH_TYPES
@callback
def async_add_light(lights=gateway.api.lights.values()):
"""Add light from deCONZ."""
@ -57,7 +60,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for light in lights:
if (
light.type not in other_light_resource_types
light.type not in OTHER_LIGHT_RESOURCE_TYPES
and light.unique_id not in gateway.entities[DOMAIN]
):
entities.append(DeconzLight(light, gateway))

View File

@ -0,0 +1,78 @@
"""Support for deCONZ siren."""
from pydeconz.light import Siren
from homeassistant.components.siren import (
ATTR_DURATION,
DOMAIN,
SUPPORT_DURATION,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
SirenEntity,
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import NEW_LIGHT
from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up sirens for deCONZ component."""
gateway = get_gateway_from_config_entry(hass, config_entry)
gateway.entities[DOMAIN] = set()
@callback
def async_add_siren(lights=gateway.api.lights.values()):
"""Add siren from deCONZ."""
entities = []
for light in lights:
if (
isinstance(light, Siren)
and light.unique_id not in gateway.entities[DOMAIN]
):
entities.append(DeconzSiren(light, gateway))
if entities:
async_add_entities(entities)
config_entry.async_on_unload(
async_dispatcher_connect(
hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_siren
)
)
async_add_siren()
class DeconzSiren(DeconzDevice, SirenEntity):
"""Representation of a deCONZ siren."""
TYPE = DOMAIN
def __init__(self, device, gateway) -> None:
"""Set up siren."""
super().__init__(device, gateway)
self._attr_supported_features = (
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_DURATION
)
@property
def is_on(self):
"""Return true if siren is on."""
return self._device.is_on
async def async_turn_on(self, **kwargs):
"""Turn on siren."""
data = {}
if (duration := kwargs.get(ATTR_DURATION)) is not None:
data["duration"] = duration * 10
await self._device.turn_on(**data)
async def async_turn_off(self, **kwargs):
"""Turn off siren."""
await self._device.turn_off()

View File

@ -3,7 +3,7 @@ from homeassistant.components.switch import DOMAIN, SwitchEntity
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import NEW_LIGHT, POWER_PLUGS, SIRENS
from .const import DOMAIN as DECONZ_DOMAIN, NEW_LIGHT, POWER_PLUGS, SIRENS
from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
@ -16,6 +16,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway = get_gateway_from_config_entry(hass, config_entry)
gateway.entities[DOMAIN] = set()
entity_registry = await hass.helpers.entity_registry.async_get_registry()
# Siren platform replacing sirens in switch platform added in 2021.10
for light in gateway.api.lights.values():
if light.type not in SIRENS:
continue
if entity_id := entity_registry.async_get_entity_id(
DOMAIN, DECONZ_DOMAIN, light.unique_id
):
entity_registry.async_remove(entity_id)
@callback
def async_add_switch(lights=gateway.api.lights.values()):
"""Add switch from deCONZ."""
@ -29,11 +40,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
):
entities.append(DeconzPowerPlug(light, gateway))
elif (
light.type in SIRENS and light.unique_id not in gateway.entities[DOMAIN]
):
entities.append(DeconzSiren(light, gateway))
if entities:
async_add_entities(entities)
@ -63,22 +69,3 @@ class DeconzPowerPlug(DeconzDevice, SwitchEntity):
async def async_turn_off(self, **kwargs):
"""Turn off switch."""
await self._device.set_state(on=False)
class DeconzSiren(DeconzDevice, SwitchEntity):
"""Representation of a deCONZ siren."""
TYPE = DOMAIN
@property
def is_on(self):
"""Return true if switch is on."""
return self._device.is_on
async def async_turn_on(self, **kwargs):
"""Turn on switch."""
await self._device.turn_on()
async def async_turn_off(self, **kwargs):
"""Turn off switch."""
await self._device.turn_off()

View File

@ -25,6 +25,7 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN
from homeassistant.components.ssdp import (
ATTR_SSDP_LOCATION,
ATTR_UPNP_MANUFACTURER_URL,
@ -163,7 +164,8 @@ async def test_gateway_setup(hass, aioclient_mock):
assert forward_entry_setup.mock_calls[6][1] == (config_entry, LOCK_DOMAIN)
assert forward_entry_setup.mock_calls[7][1] == (config_entry, SCENE_DOMAIN)
assert forward_entry_setup.mock_calls[8][1] == (config_entry, SENSOR_DOMAIN)
assert forward_entry_setup.mock_calls[9][1] == (config_entry, SWITCH_DOMAIN)
assert forward_entry_setup.mock_calls[9][1] == (config_entry, SIREN_DOMAIN)
assert forward_entry_setup.mock_calls[10][1] == (config_entry, SWITCH_DOMAIN)
async def test_gateway_retry(hass):

View File

@ -0,0 +1,132 @@
"""deCONZ switch platform tests."""
from unittest.mock import patch
from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
from homeassistant.components.siren import ATTR_DURATION, DOMAIN as SIREN_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
)
from homeassistant.helpers import entity_registry as er
from .test_gateway import (
DECONZ_WEB_REQUEST,
mock_deconz_put_request,
setup_deconz_integration,
)
async def test_sirens(hass, aioclient_mock, mock_deconz_websocket):
"""Test that siren entities are created."""
data = {
"lights": {
"1": {
"name": "Warning device",
"type": "Warning device",
"state": {"alert": "lselect", "reachable": True},
"uniqueid": "00:00:00:00:00:00:00:00-00",
},
"2": {
"name": "Unsupported siren",
"type": "Not a siren",
"state": {"reachable": True},
"uniqueid": "00:00:00:00:00:00:00:01-00",
},
}
}
with patch.dict(DECONZ_WEB_REQUEST, data):
config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 2
assert hass.states.get("siren.warning_device").state == STATE_ON
assert not hass.states.get("siren.unsupported_siren")
event_changed_light = {
"t": "event",
"e": "changed",
"r": "lights",
"id": "1",
"state": {"alert": None},
}
await mock_deconz_websocket(data=event_changed_light)
await hass.async_block_till_done()
assert hass.states.get("siren.warning_device").state == STATE_OFF
# Verify service calls
mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state")
# Service turn on siren
await hass.services.async_call(
SIREN_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "siren.warning_device"},
blocking=True,
)
assert aioclient_mock.mock_calls[1][2] == {"alert": "lselect"}
# Service turn off siren
await hass.services.async_call(
SIREN_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "siren.warning_device"},
blocking=True,
)
assert aioclient_mock.mock_calls[2][2] == {"alert": "none"}
# Service turn on siren with duration
await hass.services.async_call(
SIREN_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "siren.warning_device", ATTR_DURATION: 10},
blocking=True,
)
assert aioclient_mock.mock_calls[3][2] == {"alert": "lselect", "ontime": 100}
await hass.config_entries.async_unload(config_entry.entry_id)
states = hass.states.async_all()
assert len(states) == 2
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_remove_legacy_siren_switch(hass, aioclient_mock):
"""Test that switch platform cleans up legacy siren entities."""
unique_id = "00:00:00:00:00:00:00:00-00"
registry = er.async_get(hass)
switch_siren_entity = registry.async_get_or_create(
SWITCH_DOMAIN, DECONZ_DOMAIN, unique_id
)
assert switch_siren_entity
data = {
"lights": {
"1": {
"name": "Warning device",
"type": "Warning device",
"state": {"alert": "lselect", "reachable": True},
"uniqueid": unique_id,
},
}
}
with patch.dict(DECONZ_WEB_REQUEST, data):
await setup_deconz_integration(hass, aioclient_mock)
assert not registry.async_get(switch_siren_entity.entity_id)

View File

@ -107,76 +107,3 @@ async def test_power_plugs(hass, aioclient_mock, mock_deconz_websocket):
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_sirens(hass, aioclient_mock, mock_deconz_websocket):
"""Test that siren entities are created."""
data = {
"lights": {
"1": {
"name": "Warning device",
"type": "Warning device",
"state": {"alert": "lselect", "reachable": True},
"uniqueid": "00:00:00:00:00:00:00:00-00",
},
"2": {
"name": "Unsupported switch",
"type": "Not a switch",
"state": {"reachable": True},
"uniqueid": "00:00:00:00:00:00:00:01-00",
},
}
}
with patch.dict(DECONZ_WEB_REQUEST, data):
config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 2
assert hass.states.get("switch.warning_device").state == STATE_ON
assert not hass.states.get("switch.unsupported_switch")
event_changed_light = {
"t": "event",
"e": "changed",
"r": "lights",
"id": "1",
"state": {"alert": None},
}
await mock_deconz_websocket(data=event_changed_light)
await hass.async_block_till_done()
assert hass.states.get("switch.warning_device").state == STATE_OFF
# Verify service calls
mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state")
# Service turn on siren
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "switch.warning_device"},
blocking=True,
)
assert aioclient_mock.mock_calls[1][2] == {"alert": "lselect"}
# Service turn off siren
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.warning_device"},
blocking=True,
)
assert aioclient_mock.mock_calls[2][2] == {"alert": "none"}
await hass.config_entries.async_unload(config_entry.entry_id)
states = hass.states.async_all()
assert len(states) == 2
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