Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

deCONZ use siren platform #56397

Merged
merged 6 commits into from Sep 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions homeassistant/components/deconz/const.py
Expand Up @@ -12,6 +12,7 @@
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__)
Expand Down Expand Up @@ -41,6 +42,7 @@
LOCK_DOMAIN,
SCENE_DOMAIN,
SENSOR_DOMAIN,
SIREN_DOMAIN,
SWITCH_DOMAIN,
]

Expand Down Expand Up @@ -69,10 +71,11 @@
# 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"
11 changes: 7 additions & 4 deletions homeassistant/components/deconz/light.py
Expand Up @@ -34,30 +34,33 @@
LOCK_TYPES,
NEW_GROUP,
NEW_LIGHT,
SWITCH_TYPES,
POWER_PLUGS,
SIRENS,
)
from .deconz_device import DeconzDevice
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."""
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))
Expand Down
78 changes: 78 additions & 0 deletions homeassistant/components/deconz/siren.py
@@ -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
Kane610 marked this conversation as resolved.
Show resolved Hide resolved
)
)

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()
37 changes: 12 additions & 25 deletions homeassistant/components/deconz/switch.py
Expand Up @@ -3,7 +3,7 @@
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

Expand All @@ -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."""
Expand All @@ -29,11 +40,6 @@ def async_add_switch(lights=gateway.api.lights.values()):
):
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)

Expand Down Expand Up @@ -63,22 +69,3 @@ async def async_turn_on(self, **kwargs):
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()
4 changes: 3 additions & 1 deletion tests/components/deconz/test_gateway.py
Expand Up @@ -25,6 +25,7 @@
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,
Expand Down Expand Up @@ -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):
Expand Down
132 changes: 132 additions & 0 deletions tests/components/deconz/test_siren.py
@@ -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)