diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index 7728be8a6b2..f79000d0646 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -116,6 +116,7 @@ class ZigbeeChannel(LogMixin): self.value_attribute = attr self._status = ChannelStatus.CREATED self._cluster.add_listener(self) + self.data_cache = {} @property def id(self) -> str: diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 67a79d9dea7..3620a95b128 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -7,6 +7,7 @@ import logging import bellows.zigbee.application import voluptuous as vol from zigpy.config import CONF_DEVICE_PATH # noqa: F401 # pylint: disable=unused-import +import zigpy.types as t import zigpy_deconz.zigbee.application import zigpy_xbee.zigbee.application import zigpy_zigate.zigbee.application @@ -110,6 +111,7 @@ PLATFORMS = ( Platform.LIGHT, Platform.LOCK, Platform.NUMBER, + Platform.SELECT, Platform.SENSOR, Platform.SIREN, Platform.SWITCH, @@ -387,3 +389,10 @@ EFFECT_BREATHE = 0x01 EFFECT_OKAY = 0x02 EFFECT_DEFAULT_VARIANT = 0x00 + + +class Strobe(t.enum8): + """Strobe enum.""" + + No_Strobe = 0x00 + Strobe = 0x01 diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index dcc932a76a5..26323793e13 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -25,6 +25,7 @@ from .. import ( # noqa: F401 pylint: disable=unused-import, light, lock, number, + select, sensor, siren, switch, diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py new file mode 100644 index 00000000000..ff76023d96d --- /dev/null +++ b/homeassistant/components/zha/select.py @@ -0,0 +1,133 @@ +"""Support for ZHA controls using the select platform.""" +from __future__ import annotations + +from enum import Enum +import functools + +from zigpy.zcl.clusters.security import IasWd + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG, STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .core import discovery +from .core.const import CHANNEL_IAS_WD, DATA_ZHA, SIGNAL_ADD_ENTITIES, Strobe +from .core.registries import ZHA_ENTITIES +from .core.typing import ChannelType, ZhaDeviceType +from .entity import ZhaEntity + +MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.SELECT) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Zigbee Home Automation siren from config entry.""" + entities_to_create = hass.data[DATA_ZHA][Platform.SELECT] + + unsub = async_dispatcher_connect( + hass, + SIGNAL_ADD_ENTITIES, + functools.partial( + discovery.async_add_entities, + async_add_entities, + entities_to_create, + update_before_add=False, + ), + ) + config_entry.async_on_unload(unsub) + + +class ZHAEnumSelectEntity(ZhaEntity, SelectEntity): + """Representation of a ZHA select entity.""" + + _attr_entity_category = ENTITY_CATEGORY_CONFIG + _enum: Enum = None + + def __init__( + self, + unique_id: str, + zha_device: ZhaDeviceType, + channels: list[ChannelType], + **kwargs, + ) -> None: + """Init this select entity.""" + self._attr_name = self._enum.__name__ + self._attr_options = [entry.name.replace("_", " ") for entry in self._enum] + self._channel: ChannelType = channels[0] + super().__init__(unique_id, zha_device, channels, **kwargs) + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + option = self._channel.data_cache.get(self._attr_name) + if option is None: + return None + return option.name.replace("_", " ") + + async def async_select_option(self, option: str | int) -> None: + """Change the selected option.""" + self._channel.data_cache[self._attr_name] = self._enum[option.replace(" ", "_")] + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Run when about to be added to hass.""" + await super().async_added_to_hass() + if last_state := await self.async_get_last_state(): + self.async_restore_last_state(last_state) + + @callback + def async_restore_last_state(self, last_state) -> None: + """Restore previous state.""" + if last_state.state and last_state.state != STATE_UNKNOWN: + self._channel.data_cache[self._attr_name] = self._enum[ + last_state.state.replace(" ", "_") + ] + + +class ZHANonZCLSelectEntity(ZHAEnumSelectEntity): + """Representation of a ZHA select entity with no ZCL interaction.""" + + @property + def available(self) -> bool: + """Return entity availability.""" + return True + + +@MULTI_MATCH(channel_names=CHANNEL_IAS_WD) +class ZHADefaultToneSelectEntity( + ZHANonZCLSelectEntity, id_suffix=IasWd.Warning.WarningMode.__name__ +): + """Representation of a ZHA default siren tone select entity.""" + + _enum: Enum = IasWd.Warning.WarningMode + + +@MULTI_MATCH(channel_names=CHANNEL_IAS_WD) +class ZHADefaultSirenLevelSelectEntity( + ZHANonZCLSelectEntity, id_suffix=IasWd.Warning.SirenLevel.__name__ +): + """Representation of a ZHA default siren level select entity.""" + + _enum: Enum = IasWd.Warning.SirenLevel + + +@MULTI_MATCH(channel_names=CHANNEL_IAS_WD) +class ZHADefaultStrobeLevelSelectEntity( + ZHANonZCLSelectEntity, id_suffix=IasWd.StrobeLevel.__name__ +): + """Representation of a ZHA default siren strobe level select entity.""" + + _enum: Enum = IasWd.StrobeLevel + + +@MULTI_MATCH(channel_names=CHANNEL_IAS_WD) +class ZHADefaultStrobeSelectEntity(ZHANonZCLSelectEntity, id_suffix=Strobe.__name__): + """Representation of a ZHA default siren strobe select entity.""" + + _enum: Enum = Strobe diff --git a/homeassistant/components/zha/siren.py b/homeassistant/components/zha/siren.py index ce558e8f1a5..5ba83dbef12 100644 --- a/homeassistant/components/zha/siren.py +++ b/homeassistant/components/zha/siren.py @@ -5,6 +5,8 @@ from __future__ import annotations import functools from typing import Any +from zigpy.zcl.clusters.security import IasWd as WD + from homeassistant.components.siren import ( ATTR_DURATION, SUPPORT_DURATION, @@ -39,13 +41,15 @@ from .core.const import ( WARNING_DEVICE_MODE_POLICE_PANIC, WARNING_DEVICE_MODE_STOP, WARNING_DEVICE_SOUND_HIGH, + WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_NO, + Strobe, ) from .core.registries import ZHA_ENTITIES from .core.typing import ChannelType, ZhaDeviceType from .entity import ZhaEntity -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SIREN) +MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.SIREN) DEFAULT_DURATION = 5 # seconds @@ -70,7 +74,7 @@ async def async_setup_entry( config_entry.async_on_unload(unsub) -@STRICT_MATCH(channel_names=CHANNEL_IAS_WD) +@MULTI_MATCH(channel_names=CHANNEL_IAS_WD) class ZHASiren(ZhaEntity, SirenEntity): """Representation of a ZHA siren.""" @@ -107,9 +111,27 @@ class ZHASiren(ZhaEntity, SirenEntity): if self._off_listener: self._off_listener() self._off_listener = None - siren_tone = WARNING_DEVICE_MODE_EMERGENCY + tone_cache = self._channel.data_cache.get(WD.Warning.WarningMode.__name__) + siren_tone = ( + tone_cache.value + if tone_cache is not None + else WARNING_DEVICE_MODE_EMERGENCY + ) siren_duration = DEFAULT_DURATION - siren_level = WARNING_DEVICE_SOUND_HIGH + level_cache = self._channel.data_cache.get(WD.Warning.SirenLevel.__name__) + siren_level = ( + level_cache.value if level_cache is not None else WARNING_DEVICE_SOUND_HIGH + ) + strobe_cache = self._channel.data_cache.get(Strobe.__name__) + should_strobe = ( + strobe_cache.value if strobe_cache is not None else Strobe.No_Strobe + ) + strobe_level_cache = self._channel.data_cache.get(WD.StrobeLevel.__name__) + strobe_level = ( + strobe_level_cache.value + if strobe_level_cache is not None + else WARNING_DEVICE_STROBE_HIGH + ) if (duration := kwargs.get(ATTR_DURATION)) is not None: siren_duration = duration if (tone := kwargs.get(ATTR_TONE)) is not None: @@ -117,7 +139,12 @@ class ZHASiren(ZhaEntity, SirenEntity): if (level := kwargs.get(ATTR_VOLUME_LEVEL)) is not None: siren_level = int(level) await self._channel.issue_start_warning( - mode=siren_tone, warning_duration=siren_duration, siren_level=siren_level + mode=siren_tone, + warning_duration=siren_duration, + siren_level=siren_level, + strobe=should_strobe, + strobe_duty_cycle=50 if should_strobe else 0, + strobe_intensity=strobe_level, ) self._attr_is_on = True self._off_listener = async_call_later( diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 38232e033cc..9bc52a784f6 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -10,6 +10,7 @@ import zigpy.zcl.foundation as zcl_f import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.zha import DOMAIN +from homeassistant.const import Platform from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -59,6 +60,30 @@ async def test_get_actions(hass, device_ias): expected_actions = [ {"domain": DOMAIN, "type": "squawk", "device_id": reg_device.id}, {"domain": DOMAIN, "type": "warn", "device_id": reg_device.id}, + { + "domain": Platform.SELECT, + "type": "select_option", + "device_id": reg_device.id, + "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_warningmode", + }, + { + "domain": Platform.SELECT, + "type": "select_option", + "device_id": reg_device.id, + "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_sirenlevel", + }, + { + "domain": Platform.SELECT, + "type": "select_option", + "device_id": reg_device.id, + "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_strobelevel", + }, + { + "domain": Platform.SELECT, + "type": "select_option", + "device_id": reg_device.id, + "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_strobe", + }, ] assert actions == expected_actions diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py new file mode 100644 index 00000000000..fb21c900838 --- /dev/null +++ b/tests/components/zha/test_select.py @@ -0,0 +1,151 @@ +"""Test ZHA select entities.""" + +import pytest +from zigpy.const import SIG_EP_PROFILE +import zigpy.profiles.zha as zha +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.security as security + +from homeassistant.const import ENTITY_CATEGORY_CONFIG, STATE_UNKNOWN, Platform +from homeassistant.helpers import entity_registry as er, restore_state +from homeassistant.util import dt as dt_util + +from .common import find_entity_id +from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE + + +@pytest.fixture +async def siren(hass, zigpy_device_mock, zha_device_joined_restored): + """Siren fixture.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [general.Basic.cluster_id, security.IasWd.cluster_id], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.IAS_WARNING_DEVICE, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + ) + + zha_device = await zha_device_joined_restored(zigpy_device) + return zha_device, zigpy_device.endpoints[1].ias_wd + + +@pytest.fixture +def core_rs(hass_storage): + """Core.restore_state fixture.""" + + def _storage(entity_id, state): + now = dt_util.utcnow().isoformat() + + hass_storage[restore_state.STORAGE_KEY] = { + "version": restore_state.STORAGE_VERSION, + "key": restore_state.STORAGE_KEY, + "data": [ + { + "state": { + "entity_id": entity_id, + "state": str(state), + "last_changed": now, + "last_updated": now, + "context": { + "id": "3c2243ff5f30447eb12e7348cfd5b8ff", + "user_id": None, + }, + }, + "last_seen": now, + } + ], + } + return + + return _storage + + +async def test_select(hass, siren): + """Test zha select platform.""" + + entity_registry = er.async_get(hass) + zha_device, cluster = siren + assert cluster is not None + select_name = security.IasWd.Warning.WarningMode.__name__ + entity_id = await find_entity_id( + Platform.SELECT, + zha_device, + hass, + qualifier=select_name.lower(), + ) + assert entity_id is not None + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes["options"] == [ + "Stop", + "Burglar", + "Fire", + "Emergency", + "Police Panic", + "Fire Panic", + "Emergency Panic", + ] + + entity_entry = entity_registry.async_get(entity_id) + assert entity_entry + assert entity_entry.entity_category == ENTITY_CATEGORY_CONFIG + + # Test select option with string value + await hass.services.async_call( + "select", + "select_option", + { + "entity_id": entity_id, + "option": security.IasWd.Warning.WarningMode.Burglar.name, + }, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == security.IasWd.Warning.WarningMode.Burglar.name + + +async def test_select_restore_state( + hass, + zigpy_device_mock, + core_rs, + zha_device_restored, +): + """Test zha select entity restore state.""" + + entity_id = "select.fakemanufacturer_fakemodel_e769900a_ias_wd_warningmode" + core_rs(entity_id, state="Burglar") + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [general.Basic.cluster_id, security.IasWd.cluster_id], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.IAS_WARNING_DEVICE, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + ) + + zha_device = await zha_device_restored(zigpy_device) + cluster = zigpy_device.endpoints[1].ias_wd + assert cluster is not None + select_name = security.IasWd.Warning.WarningMode.__name__ + entity_id = await find_entity_id( + Platform.SELECT, + zha_device, + hass, + qualifier=select_name.lower(), + ) + + assert entity_id is not None + state = hass.states.get(entity_id) + assert state + assert state.state == security.IasWd.Warning.WarningMode.Burglar.name diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index 1414879c0f0..17e12491f84 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -77,7 +77,7 @@ async def test_siren(hass, siren): assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][1] == 0 - assert cluster.request.call_args[0][3] == 54 # bitmask for default args + assert cluster.request.call_args[0][3] == 50 # bitmask for default args assert cluster.request.call_args[0][4] == 5 # duration in seconds assert cluster.request.call_args[0][5] == 0 assert cluster.request.call_args[0][6] == 2 @@ -125,7 +125,7 @@ async def test_siren(hass, siren): assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][1] == 0 - assert cluster.request.call_args[0][3] == 101 # bitmask for passed args + assert cluster.request.call_args[0][3] == 97 # bitmask for passed args assert cluster.request.call_args[0][4] == 10 # duration in seconds assert cluster.request.call_args[0][5] == 0 assert cluster.request.call_args[0][6] == 2 diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 90ee54197ea..9dd5c8f1de7 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -637,6 +637,11 @@ DEVICES = [ "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_zone", "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_rssi", "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_lqi", + "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_warningmode", + "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_sirenlevel", + "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobelevel", + "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobe", + "siren.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -659,6 +664,31 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_lqi", }, + ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { + DEV_SIG_CHANNELS: ["ias_wd"], + DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_warningmode", + }, + ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { + DEV_SIG_CHANNELS: ["ias_wd"], + DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_sirenlevel", + }, + ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { + DEV_SIG_CHANNELS: ["ias_wd"], + DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobelevel", + }, + ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { + DEV_SIG_CHANNELS: ["ias_wd"], + DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobe", + }, + ("siren", "00:11:22:33:44:55:66:77-1-1282"): { + DEV_SIG_CHANNELS: ["ias_wd"], + DEV_SIG_ENT_MAP_CLASS: "ZHASiren", + DEV_SIG_ENT_MAP_ID: "siren.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd", + }, }, }, { @@ -777,6 +807,11 @@ DEVICES = [ "binary_sensor.heiman_smokesensor_em_77665544_ias_zone", "sensor.heiman_smokesensor_em_77665544_basic_rssi", "sensor.heiman_smokesensor_em_77665544_basic_lqi", + "select.heiman_smokesensor_em_77665544_ias_wd_warningmode", + "select.heiman_smokesensor_em_77665544_ias_wd_sirenlevel", + "select.heiman_smokesensor_em_77665544_ias_wd_strobelevel", + "select.heiman_smokesensor_em_77665544_ias_wd_strobe", + "siren.heiman_smokesensor_em_77665544_ias_wd", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -804,6 +839,31 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_77665544_basic_lqi", }, + ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { + DEV_SIG_CHANNELS: ["ias_wd"], + DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_warningmode", + }, + ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { + DEV_SIG_CHANNELS: ["ias_wd"], + DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_sirenlevel", + }, + ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { + DEV_SIG_CHANNELS: ["ias_wd"], + DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_strobelevel", + }, + ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { + DEV_SIG_CHANNELS: ["ias_wd"], + DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_strobe", + }, + ("siren", "00:11:22:33:44:55:66:77-1-1282"): { + DEV_SIG_CHANNELS: ["ias_wd"], + DEV_SIG_ENT_MAP_CLASS: "ZHASiren", + DEV_SIG_ENT_MAP_ID: "siren.heiman_smokesensor_em_77665544_ias_wd", + }, }, }, { @@ -867,12 +927,36 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ "button.heiman_warningdevice_77665544_identify", - "siren.heiman_warningdevice_77665544_ias_wd", "binary_sensor.heiman_warningdevice_77665544_ias_zone", "sensor.heiman_warningdevice_77665544_basic_rssi", "sensor.heiman_warningdevice_77665544_basic_lqi", + "select.heiman_warningdevice_77665544_ias_wd_warningmode", + "select.heiman_warningdevice_77665544_ias_wd_sirenlevel", + "select.heiman_warningdevice_77665544_ias_wd_strobelevel", + "select.heiman_warningdevice_77665544_ias_wd_strobe", + "siren.heiman_warningdevice_77665544_ias_wd", ], DEV_SIG_ENT_MAP: { + ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { + DEV_SIG_CHANNELS: ["ias_wd"], + DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_warningmode", + }, + ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { + DEV_SIG_CHANNELS: ["ias_wd"], + DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_sirenlevel", + }, + ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { + DEV_SIG_CHANNELS: ["ias_wd"], + DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_strobelevel", + }, + ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { + DEV_SIG_CHANNELS: ["ias_wd"], + DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_strobe", + }, ("siren", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHASiren",