Add siren platform to devolo Home Control (#53400)

* Rework mocking

* Add siren platform

* Rebase with dev

* React on change of default tone

* Fix linting error
This commit is contained in:
Guido Schmitz 2022-01-20 14:10:06 +01:00 committed by GitHub
parent 0fd003b21e
commit 144371d843
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 269 additions and 1 deletions

View File

@ -205,7 +205,6 @@ omit =
homeassistant/components/devolo_home_control/climate.py
homeassistant/components/devolo_home_control/const.py
homeassistant/components/devolo_home_control/cover.py
homeassistant/components/devolo_home_control/devolo_multi_level_switch.py
homeassistant/components/devolo_home_control/light.py
homeassistant/components/devolo_home_control/sensor.py
homeassistant/components/devolo_home_control/subscriber.py

View File

@ -11,6 +11,7 @@ PLATFORMS = [
Platform.COVER,
Platform.LIGHT,
Platform.SENSOR,
Platform.SIREN,
Platform.SWITCH,
]
CONF_MYDEVOLO = "mydevolo_url"

View File

@ -0,0 +1,86 @@
"""Platform for siren integration."""
from typing import Any
from devolo_home_control_api.devices.zwave import Zwave
from devolo_home_control_api.homecontrol import HomeControl
from homeassistant.components.siren import (
ATTR_TONE,
SUPPORT_TONES,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
SirenEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Get all binary sensor and multi level sensor devices and setup them via config entry."""
entities = []
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]:
for device in gateway.multi_level_switch_devices:
for multi_level_switch in device.multi_level_switch_property:
if multi_level_switch.startswith("devolo.SirenMultiLevelSwitch"):
entities.append(
DevoloSirenDeviceEntity(
homecontrol=gateway,
device_instance=device,
element_uid=multi_level_switch,
)
)
async_add_entities(entities, False)
class DevoloSirenDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, SirenEntity):
"""Representation of a cover device within devolo Home Control."""
def __init__(
self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str
) -> None:
"""Initialize a devolo multi level switch."""
super().__init__(
homecontrol=homecontrol,
device_instance=device_instance,
element_uid=element_uid,
)
self._attr_available_tones = [
*range(
self._multi_level_switch_property.min,
self._multi_level_switch_property.max + 1,
)
]
self._attr_supported_features = (
SUPPORT_TURN_OFF | SUPPORT_TURN_ON | SUPPORT_TONES
)
self._default_tone = device_instance.settings_property["tone"].tone
@property
def is_on(self) -> bool:
"""Whether the device is on or off."""
return self._value != 0
def turn_on(self, **kwargs: Any) -> None:
"""Turn the device off."""
tone = kwargs.get(ATTR_TONE) or self._default_tone
self._multi_level_switch_property.set(tone)
def turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
self._multi_level_switch_property.set(0)
def _generic_message(self, message: tuple) -> None:
"""Handle generic messages."""
if message[0].startswith("mss"):
# The default tone was changed
self._default_tone = message[1]
else:
super()._generic_message(message=message)

View File

@ -8,6 +8,9 @@ from devolo_home_control_api.homecontrol import HomeControl
from devolo_home_control_api.properties.binary_sensor_property import (
BinarySensorProperty,
)
from devolo_home_control_api.properties.multi_level_switch_property import (
MultiLevelSwitchProperty,
)
from devolo_home_control_api.properties.settings_property import SettingsProperty
from devolo_home_control_api.publisher.publisher import Publisher
@ -25,6 +28,19 @@ class BinarySensorPropertyMock(BinarySensorProperty):
self.state = False
class SirenPropertyMock(MultiLevelSwitchProperty):
"""devolo Home Control siren mock."""
def __init__(self, **kwargs: Any) -> None:
"""Initialize the mock."""
self.element_uid = "Test"
self.max = 0
self.min = 0
self.switch_type = "tone"
self._value = 0
self._logger = MagicMock()
class SettingsMock(SettingsProperty):
"""devolo Home Control settings mock."""
@ -33,6 +49,7 @@ class SettingsMock(SettingsProperty):
self._logger = MagicMock()
self.name = "Test"
self.zone = "Test"
self.tone = 1
class DeviceMock(Zwave):
@ -87,6 +104,19 @@ class DisabledBinarySensorMock(DeviceMock):
}
class SirenMock(DeviceMock):
"""devolo Home Control siren device mock."""
def __init__(self) -> None:
"""Initialize the mock."""
super().__init__()
self.device_model_uid = "devolo.model.Siren"
self.multi_level_switch_property = {
"devolo.SirenMultiLevelSwitch:Test": SirenPropertyMock()
}
self.settings_property["tone"] = SettingsMock()
class HomeControlMock(HomeControl):
"""devolo Home Control gateway mock."""
@ -131,3 +161,14 @@ class HomeControlMockDisabledBinarySensor(HomeControlMock):
"""Initialize the mock."""
super().__init__()
self.devices = {"Test": DisabledBinarySensorMock()}
class HomeControlMockSiren(HomeControlMock):
"""devolo Home Control gateway mock with siren device."""
def __init__(self, **kwargs: Any) -> None:
"""Initialize the mock."""
super().__init__()
self.devices = {"Test": SirenMock()}
self.publisher = Publisher(self.devices.keys())
self.publisher.unregister = MagicMock()

View File

@ -0,0 +1,141 @@
"""Tests for the devolo Home Control binary sensors."""
from unittest.mock import patch
import pytest
from homeassistant.components.siren import DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from . import configure_integration
from .mocks import HomeControlMock, HomeControlMockSiren
@pytest.mark.usefixtures("mock_zeroconf")
async def test_siren(hass: HomeAssistant):
"""Test setup and state change of a siren device."""
entry = configure_integration(hass)
test_gateway = HomeControlMockSiren()
test_gateway.devices["Test"].status = 0
with patch(
"homeassistant.components.devolo_home_control.HomeControl",
side_effect=[test_gateway, HomeControlMock()],
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(f"{DOMAIN}.test")
assert state is not None
assert state.state == STATE_OFF
# Emulate websocket message: sensor turned on
test_gateway.publisher.dispatch("Test", ("devolo.SirenMultiLevelSwitch:Test", 1))
await hass.async_block_till_done()
assert hass.states.get(f"{DOMAIN}.test").state == STATE_ON
# Emulate websocket message: device went offline
test_gateway.devices["Test"].status = 1
test_gateway.publisher.dispatch("Test", ("Status", False, "status"))
await hass.async_block_till_done()
assert hass.states.get(f"{DOMAIN}.test").state == STATE_UNAVAILABLE
@pytest.mark.usefixtures("mock_zeroconf")
async def test_siren_switching(hass: HomeAssistant):
"""Test setup and state change via switching of a siren device."""
entry = configure_integration(hass)
test_gateway = HomeControlMockSiren()
test_gateway.devices["Test"].status = 0
with patch(
"homeassistant.components.devolo_home_control.HomeControl",
side_effect=[test_gateway, HomeControlMock()],
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(f"{DOMAIN}.test")
assert state is not None
assert state.state == STATE_OFF
with patch(
"devolo_home_control_api.properties.multi_level_switch_property.MultiLevelSwitchProperty.set"
) as set:
await hass.services.async_call(
"siren",
"turn_on",
{"entity_id": f"{DOMAIN}.test"},
blocking=True,
)
# The real device state is changed by a websocket message
test_gateway.publisher.dispatch(
"Test", ("devolo.SirenMultiLevelSwitch:Test", 1)
)
await hass.async_block_till_done()
set.assert_called_once_with(1)
with patch(
"devolo_home_control_api.properties.multi_level_switch_property.MultiLevelSwitchProperty.set"
) as set:
await hass.services.async_call(
"siren",
"turn_off",
{"entity_id": f"{DOMAIN}.test"},
blocking=True,
)
# The real device state is changed by a websocket message
test_gateway.publisher.dispatch(
"Test", ("devolo.SirenMultiLevelSwitch:Test", 0)
)
await hass.async_block_till_done()
assert hass.states.get(f"{DOMAIN}.test").state == STATE_OFF
set.assert_called_once_with(0)
@pytest.mark.usefixtures("mock_zeroconf")
async def test_siren_change_default_tone(hass: HomeAssistant):
"""Test changing the default tone on message."""
entry = configure_integration(hass)
test_gateway = HomeControlMockSiren()
test_gateway.devices["Test"].status = 0
with patch(
"homeassistant.components.devolo_home_control.HomeControl",
side_effect=[test_gateway, HomeControlMock()],
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(f"{DOMAIN}.test")
assert state is not None
with patch(
"devolo_home_control_api.properties.multi_level_switch_property.MultiLevelSwitchProperty.set"
) as set:
test_gateway.publisher.dispatch("Test", ("mss:Test", 2))
await hass.services.async_call(
"siren",
"turn_on",
{"entity_id": f"{DOMAIN}.test"},
blocking=True,
)
set.assert_called_once_with(2)
@pytest.mark.usefixtures("mock_zeroconf")
async def test_remove_from_hass(hass: HomeAssistant):
"""Test removing entity."""
entry = configure_integration(hass)
test_gateway = HomeControlMockSiren()
with patch(
"homeassistant.components.devolo_home_control.HomeControl",
side_effect=[test_gateway, HomeControlMock()],
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(f"{DOMAIN}.test")
assert state is not None
await hass.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
test_gateway.publisher.unregister.assert_called_once()