Add siren platform (#48309)
* Add siren platform * add more supported flags and an ability to set siren duration * tone can be int or string * fix typing * fix typehinting * fix typehints * implement a proposed approach based on discussion * Address comments * fix tests * Small fix * Update homeassistant/components/demo/siren.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/demo/siren.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/demo/siren.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/demo/siren.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/demo/siren.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * typing * use class attributes * fix naming * remove device from service description * Filter out params from turn on service * fix tests * fix bugs and tests * add test * Combine is_on test with turn on/off/toggle service tests * Update homeassistant/components/siren/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * fix filtering of turn_on attributes * none check * remove services and attributes for volume level, default duration, and default tone * Update homeassistant/components/siren/__init__.py Co-authored-by: Franck Nijhof <frenck@frenck.nl> * Update homeassistant/components/siren/__init__.py Co-authored-by: Franck Nijhof <frenck@frenck.nl> * Update homeassistant/components/siren/__init__.py Co-authored-by: Franck Nijhof <frenck@frenck.nl> * import final * Update homeassistant/components/siren/__init__.py Co-authored-by: Franck Nijhof <frenck@frenck.nl> * Fix typing and used TypedDict for service parameters * remove is_on function * remove class name redundancy * remove extra service descriptions * switch to positive_int * fix schema for tone Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
11d7efb785
commit
0f076610fd
11 changed files with 432 additions and 0 deletions
|
@ -442,6 +442,7 @@ homeassistant/components/sighthound/* @robmarkcole
|
|||
homeassistant/components/signal_messenger/* @bbernhard
|
||||
homeassistant/components/simplisafe/* @bachya
|
||||
homeassistant/components/sinch/* @bendikrb
|
||||
homeassistant/components/siren/* @home-assistant/core @raman325
|
||||
homeassistant/components/sisyphus/* @jkeljo
|
||||
homeassistant/components/sky_hub/* @rogerselwyn
|
||||
homeassistant/components/slack/* @bachya
|
||||
|
|
|
@ -22,6 +22,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [
|
|||
"number",
|
||||
"select",
|
||||
"sensor",
|
||||
"siren",
|
||||
"switch",
|
||||
"vacuum",
|
||||
"water_heater",
|
||||
|
|
83
homeassistant/components/demo/siren.py
Normal file
83
homeassistant/components/demo/siren.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
"""Demo platform that offers a fake siren device."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.siren import SirenEntity
|
||||
from homeassistant.components.siren.const import (
|
||||
SUPPORT_DURATION,
|
||||
SUPPORT_TONES,
|
||||
SUPPORT_TURN_OFF,
|
||||
SUPPORT_TURN_ON,
|
||||
SUPPORT_VOLUME_SET,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import Config, HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TURN_OFF | SUPPORT_TURN_ON
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: Config,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType = None,
|
||||
) -> None:
|
||||
"""Set up the Demo siren devices."""
|
||||
async_add_entities(
|
||||
[
|
||||
DemoSiren(name="Siren"),
|
||||
DemoSiren(
|
||||
name="Siren with all features",
|
||||
available_tones=["fire", "alarm"],
|
||||
support_volume_set=True,
|
||||
support_duration=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Demo siren devices config entry."""
|
||||
await async_setup_platform(hass, {}, async_add_entities)
|
||||
|
||||
|
||||
class DemoSiren(SirenEntity):
|
||||
"""Representation of a demo siren device."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
available_tones: str | None = None,
|
||||
support_volume_set: bool = False,
|
||||
support_duration: bool = False,
|
||||
is_on: bool = True,
|
||||
) -> None:
|
||||
"""Initialize the siren device."""
|
||||
self._attr_name = name
|
||||
self._attr_should_poll = False
|
||||
self._attr_supported_features = SUPPORT_FLAGS
|
||||
self._attr_is_on = is_on
|
||||
if available_tones is not None:
|
||||
self._attr_supported_features |= SUPPORT_TONES
|
||||
if support_volume_set:
|
||||
self._attr_supported_features |= SUPPORT_VOLUME_SET
|
||||
if support_duration:
|
||||
self._attr_supported_features |= SUPPORT_DURATION
|
||||
self._attr_available_tones = available_tones
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the siren on."""
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the siren off."""
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
135
homeassistant/components/siren/__init__.py
Normal file
135
homeassistant/components/siren/__init__.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
"""Component to interface with various sirens/chimes."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, TypedDict, cast, final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON
|
||||
from homeassistant.core import ServiceCall
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||
PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA_BASE,
|
||||
)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
|
||||
from .const import (
|
||||
ATTR_AVAILABLE_TONES,
|
||||
ATTR_DURATION,
|
||||
ATTR_TONE,
|
||||
ATTR_VOLUME_LEVEL,
|
||||
DOMAIN,
|
||||
SUPPORT_DURATION,
|
||||
SUPPORT_TONES,
|
||||
SUPPORT_TURN_OFF,
|
||||
SUPPORT_TURN_ON,
|
||||
SUPPORT_VOLUME_SET,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
TURN_ON_SCHEMA = {
|
||||
vol.Optional(ATTR_TONE): vol.Any(vol.Coerce(int), cv.string),
|
||||
vol.Optional(ATTR_DURATION): cv.positive_int,
|
||||
vol.Optional(ATTR_VOLUME_LEVEL): cv.small_float,
|
||||
}
|
||||
|
||||
|
||||
class SirenTurnOnServiceParameters(TypedDict, total=False):
|
||||
"""Represent possible parameters to siren.turn_on service data dict type."""
|
||||
|
||||
tone: int | str
|
||||
duration: int
|
||||
volume_level: float
|
||||
|
||||
|
||||
def filter_turn_on_params(
|
||||
siren: SirenEntity, params: SirenTurnOnServiceParameters
|
||||
) -> SirenTurnOnServiceParameters:
|
||||
"""Filter out params not supported by the siren."""
|
||||
supported_features = siren.supported_features or 0
|
||||
|
||||
if not supported_features & SUPPORT_TONES:
|
||||
params.pop(ATTR_TONE, None)
|
||||
if not supported_features & SUPPORT_DURATION:
|
||||
params.pop(ATTR_DURATION, None)
|
||||
if not supported_features & SUPPORT_VOLUME_SET:
|
||||
params.pop(ATTR_VOLUME_LEVEL, None)
|
||||
|
||||
return params
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||
"""Set up siren devices."""
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
||||
)
|
||||
await component.async_setup(config)
|
||||
|
||||
async def async_handle_turn_on_service(
|
||||
siren: SirenEntity, call: ServiceCall
|
||||
) -> None:
|
||||
"""Handle turning a siren on."""
|
||||
await siren.async_turn_on(
|
||||
**filter_turn_on_params(
|
||||
siren, cast(SirenTurnOnServiceParameters, dict(call.data))
|
||||
)
|
||||
)
|
||||
|
||||
component.async_register_entity_service(
|
||||
SERVICE_TURN_ON, TURN_ON_SCHEMA, async_handle_turn_on_service, [SUPPORT_TURN_ON]
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_TURN_OFF, {}, "async_turn_off", [SUPPORT_TURN_OFF]
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_TOGGLE, {}, "async_toggle", [SUPPORT_TURN_ON & SUPPORT_TURN_OFF]
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
component: EntityComponent = hass.data[DOMAIN]
|
||||
return await component.async_setup_entry(entry)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
component: EntityComponent = hass.data[DOMAIN]
|
||||
return await component.async_unload_entry(entry)
|
||||
|
||||
|
||||
class SirenEntity(ToggleEntity):
|
||||
"""Representation of a siren device."""
|
||||
|
||||
_attr_available_tones: list[int | str] | None = None
|
||||
|
||||
@final
|
||||
@property
|
||||
def capability_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return capability attributes."""
|
||||
supported_features = self.supported_features or 0
|
||||
|
||||
if supported_features & SUPPORT_TONES and self.available_tones is not None:
|
||||
return {ATTR_AVAILABLE_TONES: self.available_tones}
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def available_tones(self) -> list[int | str] | None:
|
||||
"""
|
||||
Return a list of available tones.
|
||||
|
||||
Requires SUPPORT_TONES.
|
||||
"""
|
||||
return self._attr_available_tones
|
17
homeassistant/components/siren/const.py
Normal file
17
homeassistant/components/siren/const.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
"""Constants for the siren component."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "siren"
|
||||
|
||||
ATTR_TONE: Final = "tone"
|
||||
|
||||
ATTR_AVAILABLE_TONES: Final = "available_tones"
|
||||
ATTR_DURATION: Final = "duration"
|
||||
ATTR_VOLUME_LEVEL: Final = "volume_level"
|
||||
|
||||
SUPPORT_TURN_ON: Final = 1
|
||||
SUPPORT_TURN_OFF: Final = 2
|
||||
SUPPORT_TONES: Final = 4
|
||||
SUPPORT_VOLUME_SET: Final = 8
|
||||
SUPPORT_DURATION: Final = 16
|
7
homeassistant/components/siren/manifest.json
Normal file
7
homeassistant/components/siren/manifest.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"domain": "siren",
|
||||
"name": "Siren",
|
||||
"documentation": "https://www.home-assistant.io/integrations/siren",
|
||||
"codeowners": ["@home-assistant/core", "@raman325"],
|
||||
"quality_scale": "internal"
|
||||
}
|
41
homeassistant/components/siren/services.yaml
Normal file
41
homeassistant/components/siren/services.yaml
Normal file
|
@ -0,0 +1,41 @@
|
|||
# Describes the format for available siren services
|
||||
|
||||
turn_on:
|
||||
description: Turn siren on.
|
||||
target:
|
||||
entity:
|
||||
domain: siren
|
||||
fields:
|
||||
tone:
|
||||
description: The tone to emit when turning the siren on. Must be supported by the integration.
|
||||
example: fire
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
volume_level:
|
||||
description: The volume level of the noise to emit when turning the siren on. Must be supported by the integration.
|
||||
example: 0.5
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 1
|
||||
step: 0.05
|
||||
duration:
|
||||
description: The duration in seconds of the noise to emit when turning the siren on. Must be supported by the integration.
|
||||
example: 15
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
|
||||
turn_off:
|
||||
description: Turn siren off.
|
||||
target:
|
||||
entity:
|
||||
domain: siren
|
||||
|
||||
toggle:
|
||||
description: Toggles a siren.
|
||||
target:
|
||||
entity:
|
||||
domain: siren
|
|
@ -93,6 +93,7 @@ NO_IOT_CLASS = [
|
|||
"search",
|
||||
"select",
|
||||
"sensor",
|
||||
"siren",
|
||||
"stt",
|
||||
"switch",
|
||||
"system_health",
|
||||
|
|
108
tests/components/demo/test_siren.py
Normal file
108
tests/components/demo/test_siren.py
Normal file
|
@ -0,0 +1,108 @@
|
|||
"""The tests for the demo siren component."""
|
||||
from unittest.mock import call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.siren.const import (
|
||||
ATTR_AVAILABLE_TONES,
|
||||
ATTR_VOLUME_LEVEL,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TOGGLE,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
ENTITY_SIREN = "siren.siren"
|
||||
ENTITY_SIREN_WITH_ALL_FEATURES = "siren.siren_with_all_features"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_demo_siren(hass):
|
||||
"""Initialize setup demo siren."""
|
||||
assert await async_setup_component(hass, DOMAIN, {"siren": {"platform": "demo"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
def test_setup_params(hass):
|
||||
"""Test the initial parameters."""
|
||||
state = hass.states.get(ENTITY_SIREN)
|
||||
assert state.state == STATE_ON
|
||||
assert ATTR_AVAILABLE_TONES not in state.attributes
|
||||
|
||||
|
||||
def test_all_setup_params(hass):
|
||||
"""Test the setup with all parameters."""
|
||||
state = hass.states.get(ENTITY_SIREN_WITH_ALL_FEATURES)
|
||||
assert state.attributes.get(ATTR_AVAILABLE_TONES) == ["fire", "alarm"]
|
||||
|
||||
|
||||
async def test_turn_on(hass):
|
||||
"""Test turn on device."""
|
||||
await hass.services.async_call(
|
||||
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_SIREN}, blocking=True
|
||||
)
|
||||
state = hass.states.get(ENTITY_SIREN)
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SIREN}, blocking=True
|
||||
)
|
||||
state = hass.states.get(ENTITY_SIREN)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_turn_off(hass):
|
||||
"""Test turn off device."""
|
||||
await hass.services.async_call(
|
||||
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SIREN}, blocking=True
|
||||
)
|
||||
state = hass.states.get(ENTITY_SIREN)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_SIREN}, blocking=True
|
||||
)
|
||||
state = hass.states.get(ENTITY_SIREN)
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
async def test_toggle(hass):
|
||||
"""Test toggle device."""
|
||||
await hass.services.async_call(
|
||||
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SIREN}, blocking=True
|
||||
)
|
||||
state = hass.states.get(ENTITY_SIREN)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_SIREN}, blocking=True
|
||||
)
|
||||
state = hass.states.get(ENTITY_SIREN)
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_SIREN}, blocking=True
|
||||
)
|
||||
state = hass.states.get(ENTITY_SIREN)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_turn_on_strip_attributes(hass):
|
||||
"""Test attributes are stripped from turn_on service call when not supported."""
|
||||
with patch(
|
||||
"homeassistant.components.demo.siren.DemoSiren.async_turn_on"
|
||||
) as svc_call:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: ENTITY_SIREN, ATTR_VOLUME_LEVEL: 1},
|
||||
blocking=True,
|
||||
)
|
||||
assert svc_call.called
|
||||
assert svc_call.call_args_list[0] == call(**{ATTR_ENTITY_ID: [ENTITY_SIREN]})
|
1
tests/components/siren/__init__.py
Normal file
1
tests/components/siren/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the siren component."""
|
37
tests/components/siren/test_init.py
Normal file
37
tests/components/siren/test_init.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
"""The tests for the siren component."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homeassistant.components.siren import SirenEntity
|
||||
|
||||
|
||||
class MockSirenEntity(SirenEntity):
|
||||
"""Mock siren device to use in tests."""
|
||||
|
||||
_attr_is_on = True
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return the list of supported features."""
|
||||
return 0
|
||||
|
||||
|
||||
async def test_sync_turn_on(hass):
|
||||
"""Test if async turn_on calls sync turn_on."""
|
||||
siren = MockSirenEntity()
|
||||
siren.hass = hass
|
||||
|
||||
siren.turn_on = MagicMock()
|
||||
await siren.async_turn_on()
|
||||
|
||||
assert siren.turn_on.called
|
||||
|
||||
|
||||
async def test_sync_turn_off(hass):
|
||||
"""Test if async turn_off calls sync turn_off."""
|
||||
siren = MockSirenEntity()
|
||||
siren.hass = hass
|
||||
|
||||
siren.turn_off = MagicMock()
|
||||
await siren.async_turn_off()
|
||||
|
||||
assert siren.turn_off.called
|
Loading…
Add table
Reference in a new issue