diff --git a/zhaquirks/builder/builder.py b/zhaquirks/builder/builder.py index 2c7b60ba84..01baaffe48 100644 --- a/zhaquirks/builder/builder.py +++ b/zhaquirks/builder/builder.py @@ -58,6 +58,7 @@ PreventDefaultEntityCreationMetadata, QuirkDefinition, ReportingConfig, + SirenMetadata, SwitchMetadata, WriteAttributeButtonMetadata, ZCLCommandButtonMetadata, @@ -750,6 +751,59 @@ def switch( ) return self + def siren( + self, + attribute_name: str, + cluster_id: int, + cluster_type: ClusterType = ClusterType.Server, + endpoint_id: int = 1, + available_tones: dict[int, str] | None = None, + off_value: int = 0, + default_tone: int | None = None, + entity_type: EntityType = EntityType.STANDARD, + initially_disabled: bool = False, + attribute_initialized_from_cache: bool = True, + reporting_config: ReportingConfig | None = None, + unique_id_suffix: str | None = None, + translation_key: str | None = None, + fallback_name: str | None = None, + primary: bool | None = None, + *, + translation_placeholders: dict[str, str] | None = None, + ) -> Self: + """Add an EntityMetadata containing SirenMetadata and return self. + + This exposes a siren entity in Home Assistant that is controlled by + writing ``attribute_name``: turning on writes a tone value (the requested + tone, else ``default_tone``), turning off writes ``off_value``. The + entity's state follows the cached attribute value, so a device that + resets the attribute on its own keeps the entity in sync. Pass + ``available_tones`` (a ``{value: name}`` mapping) to let the user pick a + tone in Home Assistant. + """ + self._add_entity_metadata( + SirenMetadata( + endpoint_id=endpoint_id, + cluster_id=cluster_id, + cluster_type=cluster_type, + entity_platform=EntityPlatform.SIREN, + entity_type=entity_type, + initially_disabled=initially_disabled, + attribute_initialized_from_cache=attribute_initialized_from_cache, + reporting_config=reporting_config, + unique_id_suffix=unique_id_suffix, + translation_key=translation_key, + translation_placeholders=translation_placeholders or {}, + fallback_name=fallback_name, + attribute_name=attribute_name, + available_tones=available_tones or {}, + off_value=off_value, + default_tone=default_tone, + primary=primary, + ) + ) + return self + def number( self, attribute_name: str, diff --git a/zhaquirks/builder/discovery.py b/zhaquirks/builder/discovery.py index b390497ea1..d06088f88c 100644 --- a/zhaquirks/builder/discovery.py +++ b/zhaquirks/builder/discovery.py @@ -23,6 +23,7 @@ number, select, sensor, + siren, switch, ) from zigpy.zcl import ClusterType, ReportingConfig @@ -31,6 +32,7 @@ BinarySensorMetadata, EntityMetadata, NumberMetadata, + SirenMetadata, SwitchMetadata, WriteAttributeButtonMetadata, ZCLCommandButtonMetadata, @@ -53,6 +55,7 @@ (Platform.SENSOR, ZCLSensorMetadata): sensor.Sensor, (Platform.SELECT, ZCLEnumMetadata): select.ZCLEnumSelectEntity, (Platform.NUMBER, NumberMetadata): number.NumberConfigurationEntity, + (Platform.SIREN, SirenMetadata): siren.AttributeSiren, (Platform.SWITCH, SwitchMetadata): switch.ConfigurableAttributeSwitch, } @@ -108,6 +111,13 @@ def _platform_kwargs(entity_metadata: EntityMetadata) -> dict[str, Any]: "attribute_converter": entity_metadata.attribute_converter, "device_class": entity_metadata.device_class, } + if isinstance(entity_metadata, SirenMetadata): + return { + "attribute_name": entity_metadata.attribute_name, + "available_tones": dict(entity_metadata.available_tones), + "off_value": entity_metadata.off_value, + "default_tone": entity_metadata.default_tone, + } if isinstance(entity_metadata, ZCLEnumMetadata): return { "attribute_name": entity_metadata.attribute_name, diff --git a/zhaquirks/builder/metadata.py b/zhaquirks/builder/metadata.py index 612d7985db..945453c538 100644 --- a/zhaquirks/builder/metadata.py +++ b/zhaquirks/builder/metadata.py @@ -139,6 +139,19 @@ class BinarySensorMetadata(EntityMetadata): device_class: BinarySensorDeviceClass | None = attrs.field(default=None) +@attrs.define(frozen=True, kw_only=True, repr=True) +class SirenMetadata(EntityMetadata): + """Metadata for an exposed attribute-controlled siren entity.""" + + attribute_name: str = attrs.field() + available_tones: frozendict[int, str] = attrs.field( + factory=frozendict, converter=frozendict + ) + off_value: int = attrs.field(default=0) + default_tone: int | None = attrs.field(default=None) + reporting_config: ReportingConfig | None = attrs.field(default=None) + + @attrs.define(frozen=True, kw_only=True, repr=True) class WriteAttributeButtonMetadata(EntityMetadata): """Metadata for exposed button entity that writes an attribute when pressed.""" diff --git a/zhaquirks/heiman/hm_722_esy_e_plus.py b/zhaquirks/heiman/hm_722_esy_e_plus.py index ddd3869d19..540e0659cc 100644 --- a/zhaquirks/heiman/hm_722_esy_e_plus.py +++ b/zhaquirks/heiman/hm_722_esy_e_plus.py @@ -1,6 +1,5 @@ """Heiman HM-722-ESY-E-PLUS Co sensor.""" -from zha.quirks import SIREN_BASIC import zigpy.types as t from zigpy.zcl.clusters.security import IasWd, IasZone from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef @@ -87,14 +86,23 @@ class AttributeDefs(BaseAttributeDefs): .applies_to("HEIMAN", "HM-722ESY-E-PLUS") .friendly_name(manufacturer="HEIMAN", model="HM-722ESY-E-PLUS") .replaces(CustomHeimanCluster) - .exposes_feature(SIREN_BASIC) - .change_entity_metadata( - endpoint_id=1, - cluster_id=IasWd.cluster_id, - new_primary=False, - new_entity_category=EntityType.CONFIG, + # Replace the IAS WD siren (fixed tone, ZHA-limited to ~30 s) with an + # attribute-controlled siren that supports tone selection and sounds until + # turned off. Reuse the IAS WD siren's unique_id so existing entities migrate. + .prevent_default_entity_creation(endpoint_id=1, cluster_id=IasWd.cluster_id) + .siren( + CustomHeimanCluster.AttributeDefs.siren_for_automation.name, + CustomHeimanCluster.cluster_id, + available_tones={ + SmokeCoSirenEnum.Smoke_siren: "Smoke siren", + SmokeCoSirenEnum.CO_siren: "CO siren", + }, + off_value=SmokeCoSirenEnum.Stop, + default_tone=SmokeCoSirenEnum.CO_siren, + unique_id_suffix=str(IasWd.cluster_id), + translation_key="siren", + fallback_name="Siren", ) - # XXX: siren_for_automation should be added as a siren entity, needs zigpy API .binary_sensor( CustomHeimanCluster.AttributeDefs.sensor_self_check_state.name, CustomHeimanCluster.cluster_id, diff --git a/zhaquirks/heiman/hs1ca_e_plus.py b/zhaquirks/heiman/hs1ca_e_plus.py index 41c84c7fb2..348fb6370c 100644 --- a/zhaquirks/heiman/hs1ca_e_plus.py +++ b/zhaquirks/heiman/hs1ca_e_plus.py @@ -1,6 +1,5 @@ """Heiman HS1CA-E-PLUS CO sensor.""" -from zha.quirks import SIREN_BASIC import zigpy.types as t from zigpy.zcl.clusters.security import IasWd, IasZone from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef @@ -88,14 +87,23 @@ class AttributeDefs(BaseAttributeDefs): .applies_to("HEIMAN", "HS1CA-E-PLUS") .friendly_name(manufacturer="HEIMAN", model="HS1CA-E-PLUS") .replaces(CustomHeimanCluster) - .exposes_feature(SIREN_BASIC) - .change_entity_metadata( - endpoint_id=1, - cluster_id=IasWd.cluster_id, - new_primary=False, - new_entity_category=EntityType.CONFIG, + # Replace the IAS WD siren (fixed tone, ZHA-limited to ~30 s) with an + # attribute-controlled siren that supports tone selection and sounds until + # turned off. Reuse the IAS WD siren's unique_id so existing entities migrate. + .prevent_default_entity_creation(endpoint_id=1, cluster_id=IasWd.cluster_id) + .siren( + CustomHeimanCluster.AttributeDefs.siren_for_automation.name, + CustomHeimanCluster.cluster_id, + available_tones={ + SmokeCoSirenEnum.Smoke_siren: "Smoke siren", + SmokeCoSirenEnum.CO_siren: "CO siren", + }, + off_value=SmokeCoSirenEnum.Stop, + default_tone=SmokeCoSirenEnum.CO_siren, + unique_id_suffix=str(IasWd.cluster_id), + translation_key="siren", + fallback_name="Siren", ) - # XXX: siren_for_automation should be added as a siren entity, needs zigpy API .binary_sensor( CustomHeimanCluster.AttributeDefs.sensor_self_check_state.name, CustomHeimanCluster.cluster_id, diff --git a/zhaquirks/heiman/hs1sa_e_lover.py b/zhaquirks/heiman/hs1sa_e_lover.py index 1073dcdc47..c5bfe1b472 100644 --- a/zhaquirks/heiman/hs1sa_e_lover.py +++ b/zhaquirks/heiman/hs1sa_e_lover.py @@ -1,6 +1,5 @@ """Heiman HS1SA-E Lover smoke sensor.""" -from zha.quirks import SIREN_BASIC import zigpy.types as t from zigpy.zcl.clusters.security import IasWd, IasZone from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef @@ -52,14 +51,23 @@ class AttributeDefs(BaseAttributeDefs): .applies_to("HEIMAN", "SmokeSensor-EF2-3.0") .friendly_name(manufacturer="HEIMAN", model="HS1SA-E-Lover") .replaces(CustomHeimanCluster) - .exposes_feature(SIREN_BASIC) - .change_entity_metadata( - endpoint_id=1, - cluster_id=IasWd.cluster_id, - new_primary=False, - new_entity_category=EntityType.CONFIG, + # Replace the IAS WD siren (fixed tone, ZHA-limited to ~30 s) with an + # attribute-controlled siren that supports tone selection and sounds until + # turned off. Reuse the IAS WD siren's unique_id so existing entities migrate. + .prevent_default_entity_creation(endpoint_id=1, cluster_id=IasWd.cluster_id) + .siren( + CustomHeimanCluster.AttributeDefs.siren_for_automation.name, + CustomHeimanCluster.cluster_id, + available_tones={ + SmokeSirenEnum.Smoke_siren: "Smoke siren", + SmokeSirenEnum.CO_siren: "CO siren", + }, + off_value=SmokeSirenEnum.Stop, + default_tone=SmokeSirenEnum.Smoke_siren, + unique_id_suffix=str(IasWd.cluster_id), + translation_key="siren", + fallback_name="Siren", ) - # XXX: siren_for_automation should be added as a siren entity, needs zigpy API .command_button( IasZone.ServerCommandDefs.init_test_mode.name, IasZone.cluster_id, diff --git a/zhaquirks/heiman/hs1sa_efa.py b/zhaquirks/heiman/hs1sa_efa.py index d97f1519fb..b45aaa44c3 100644 --- a/zhaquirks/heiman/hs1sa_efa.py +++ b/zhaquirks/heiman/hs1sa_efa.py @@ -1,6 +1,5 @@ """Heiman HS1SA-E smoke sensor.""" -from zha.quirks import SIREN_BASIC import zigpy.types as t from zigpy.zcl.clusters.security import IasWd, IasZone from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef @@ -125,12 +124,22 @@ class AttributeDefs(BaseAttributeDefs): .applies_to("HEIMAN", "HS1SA-E-PLUS") .friendly_name(manufacturer="HEIMAN", model="HS1SA-E-PLUS") # Used by newer fw .replaces(CustomHeimanCluster) - .exposes_feature(SIREN_BASIC) - .change_entity_metadata( - endpoint_id=1, - cluster_id=IasWd.cluster_id, - new_primary=False, - new_entity_category=EntityType.CONFIG, + # Replace the IAS WD siren (fixed tone, ZHA-limited to ~30 s) with an + # attribute-controlled siren that supports tone selection and sounds until + # turned off. Reuse the IAS WD siren's unique_id so existing entities migrate. + .prevent_default_entity_creation(endpoint_id=1, cluster_id=IasWd.cluster_id) + .siren( + CustomHeimanCluster.AttributeDefs.siren_for_automation.name, + CustomHeimanCluster.cluster_id, + available_tones={ + SmokeSirenEnum.Smoke_siren: "Smoke siren", + SmokeSirenEnum.CO_siren: "CO siren", + }, + off_value=SmokeSirenEnum.Stop, + default_tone=SmokeSirenEnum.Smoke_siren, + unique_id_suffix=str(IasWd.cluster_id), + translation_key="siren", + fallback_name="Siren", ) .switch( CustomHeimanCluster.AttributeDefs.heartbeat_indicator.name, @@ -138,7 +147,6 @@ class AttributeDefs(BaseAttributeDefs): translation_key="heartbeat_indicator", fallback_name="Heartbeat indicator", ) - # XXX: siren_for_automation should be added as a siren entity, needs zigpy API .enum( CustomHeimanCluster.AttributeDefs.chamber_contamination.name, ChamberContaminationEnum,