From 932838840b3e81ab1fb642beafcbb19d23905f53 Mon Sep 17 00:00:00 2001 From: WardZhou <33411000+wardmatter@users.noreply.github.com> Date: Sat, 5 Sep 2026 21:43:16 +0800 Subject: [PATCH] Add Matter Boolean State Configuration alarm switches (#173233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ludovic BOUÉ Co-authored-by: Ludovic BOUÉ <938089+lboue@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- homeassistant/components/matter/icons.json | 6 + homeassistant/components/matter/strings.json | 6 + homeassistant/components/matter/switch.py | 145 +++++++- .../nodes/ikea_klippbok_water_leak.json | 342 ++++++++++++++++++ .../matter/snapshots/test_switch.ambr | 100 +++++ tests/components/matter/test_switch.py | 115 ++++++ 6 files changed, 713 insertions(+), 1 deletion(-) create mode 100644 tests/components/matter/fixtures/nodes/ikea_klippbok_water_leak.json diff --git a/homeassistant/components/matter/icons.json b/homeassistant/components/matter/icons.json index dc5f2a960619..b8a4d041baa6 100644 --- a/homeassistant/components/matter/icons.json +++ b/homeassistant/components/matter/icons.json @@ -154,6 +154,9 @@ } }, "switch": { + "audible_alarm_enabled": { + "default": "mdi:bullhorn" + }, "child_lock": { "default": "mdi:lock", "state": { @@ -173,6 +176,9 @@ "off": "mdi:volume-high", "on": "mdi:volume-mute" } + }, + "visual_alarm_enabled": { + "default": "mdi:alarm-light" } } }, diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 943f7eb131b2..772ad9fc9e69 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -690,6 +690,9 @@ } }, "switch": { + "audible_alarm_enabled": { + "name": "Audible alarm enabled" + }, "child_lock": { "name": "Child lock" }, @@ -710,6 +713,9 @@ }, "switch": { "name": "[%key:component::switch::title%]" + }, + "visual_alarm_enabled": { + "name": "Visual alarm enabled" } }, "vacuum": { diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index 2fe5982e4a7c..2237cecd264c 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -1,12 +1,15 @@ """Matter switches.""" +from asyncio import Lock from collections.abc import Callable -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any, override +from weakref import WeakKeyDictionary from chip.clusters import Objects as clusters from chip.clusters.Objects import ClusterCommand, NullValue from matter_server.client.models import device_types +from matter_server.client.models.node import MatterEndpoint from homeassistant.components.switch import ( SwitchDeviceClass, @@ -28,6 +31,29 @@ EVSE_SUPPLY_STATE_MAP = { clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabledDiagnostics: False, } +ALARM_MODE_VISUAL = clusters.BooleanStateConfiguration.Bitmaps.AlarmModeBitmap.kVisual +ALARM_MODE_AUDIBLE = clusters.BooleanStateConfiguration.Bitmaps.AlarmModeBitmap.kAudible + +BOOLEAN_STATE_CONFIGURATION_FEATURE_VISUAL = ( + clusters.BooleanStateConfiguration.Bitmaps.Feature.kVisual +) +BOOLEAN_STATE_CONFIGURATION_FEATURE_AUDIBLE = ( + clusters.BooleanStateConfiguration.Bitmaps.Feature.kAudible +) + + +@dataclass +class _AlarmEnabledState: + """Track pending alarm state for an endpoint.""" + + lock: Lock = field(default_factory=Lock) + pending_alarms_enabled: int | None = None + + +ALARM_ENABLED_STATES: WeakKeyDictionary[MatterEndpoint, _AlarmEnabledState] = ( + WeakKeyDictionary() +) + async def async_setup_entry( hass: HomeAssistant, @@ -183,6 +209,83 @@ class MatterNumericSwitch(MatterSwitch): self._attr_is_on = value +@dataclass(frozen=True, kw_only=True) +class MatterAlarmEnabledSwitchEntityDescription(MatterSwitchEntityDescription): + """Describe Matter alarm enabled Switch entities.""" + + alarm_mode: int + + +class MatterAlarmEnabledSwitch(MatterSwitch): + """Representation of a Matter Boolean State Configuration alarm switch.""" + + entity_description: MatterAlarmEnabledSwitchEntityDescription + + async def _async_set_alarm_enabled(self, value: bool) -> None: + """Set the enabled state for an alarm mode.""" + state = ALARM_ENABLED_STATES.setdefault(self._endpoint, _AlarmEnabledState()) + async with state.lock: + alarms_enabled = state.pending_alarms_enabled + if alarms_enabled is None: + alarms_enabled = ( + self.get_matter_attribute_value( + clusters.BooleanStateConfiguration.Attributes.AlarmsEnabled + ) + or 0 + ) + if value: + alarms_enabled |= self.entity_description.alarm_mode + else: + alarms_enabled &= ~self.entity_description.alarm_mode + + state.pending_alarms_enabled = alarms_enabled + try: + await self.send_device_command( + clusters.BooleanStateConfiguration.Commands.EnableDisableAlarm( + alarmsToEnableDisable=alarms_enabled, + ) + ) + except BaseException: + if state.pending_alarms_enabled == alarms_enabled: + state.pending_alarms_enabled = None + raise + + @override + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn alarm mode on.""" + await self._async_set_alarm_enabled(True) + + @override + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn alarm mode off.""" + await self._async_set_alarm_enabled(False) + + @callback + @override + def _update_from_device(self) -> None: + """Update from device.""" + alarm_mode = self.entity_description.alarm_mode + alarms_supported = ( + self.get_matter_attribute_value( + clusters.BooleanStateConfiguration.Attributes.AlarmsSupported + ) + or 0 + ) + self._attr_available = self._attr_available and bool( + alarms_supported & alarm_mode + ) + + alarms_enabled = ( + self.get_matter_attribute_value( + clusters.BooleanStateConfiguration.Attributes.AlarmsEnabled + ) + or 0 + ) + if state := ALARM_ENABLED_STATES.get(self._endpoint): + state.pending_alarms_enabled = alarms_enabled + self._attr_is_on = bool(alarms_enabled & alarm_mode) + + # Discovery schema(s) to map Matter Attributes to HA entities DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( @@ -346,4 +449,44 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterNumericSwitch, required_attributes=(clusters.EveCluster.Attributes.ChildLock,), ), + MatterDiscoverySchema( + platform=Platform.SWITCH, + entity_description=MatterAlarmEnabledSwitchEntityDescription( + key="BooleanStateConfigurationVisualAlarmEnabled", + entity_category=EntityCategory.CONFIG, + translation_key="visual_alarm_enabled", + alarm_mode=ALARM_MODE_VISUAL, + ), + entity_class=MatterAlarmEnabledSwitch, + required_attributes=( + clusters.BooleanStateConfiguration.Attributes.AlarmsEnabled, + clusters.BooleanStateConfiguration.Attributes.AcceptedCommandList, + clusters.BooleanStateConfiguration.Attributes.AlarmsSupported, + ), + secondary_value_contains=( + clusters.BooleanStateConfiguration.Commands.EnableDisableAlarm.command_id + ), + featuremap_contains=BOOLEAN_STATE_CONFIGURATION_FEATURE_VISUAL, + allow_multi=True, + ), + MatterDiscoverySchema( + platform=Platform.SWITCH, + entity_description=MatterAlarmEnabledSwitchEntityDescription( + key="BooleanStateConfigurationAudibleAlarmEnabled", + entity_category=EntityCategory.CONFIG, + translation_key="audible_alarm_enabled", + alarm_mode=ALARM_MODE_AUDIBLE, + ), + entity_class=MatterAlarmEnabledSwitch, + required_attributes=( + clusters.BooleanStateConfiguration.Attributes.AlarmsEnabled, + clusters.BooleanStateConfiguration.Attributes.AcceptedCommandList, + clusters.BooleanStateConfiguration.Attributes.AlarmsSupported, + ), + secondary_value_contains=( + clusters.BooleanStateConfiguration.Commands.EnableDisableAlarm.command_id + ), + featuremap_contains=BOOLEAN_STATE_CONFIGURATION_FEATURE_AUDIBLE, + allow_multi=True, + ), ] diff --git a/tests/components/matter/fixtures/nodes/ikea_klippbok_water_leak.json b/tests/components/matter/fixtures/nodes/ikea_klippbok_water_leak.json new file mode 100644 index 000000000000..7440fea831b6 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/ikea_klippbok_water_leak.json @@ -0,0 +1,342 @@ +{ + "node_id": 320, + "date_commissioned": "2026-02-07T03:40:58.175000", + "last_interview": "2026-04-15T04:08:35.891000", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/65533": 2, + "0/29/65532": 0, + "0/29/0": [ + { + "0": 18, + "1": 1 + }, + { + "0": 17, + "1": 1 + }, + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 42, 47, 48, 49, 51, 53, 60, 62, 63, 70], + "0/29/2": [41], + "0/29/3": [1], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/29/65529": [], + "0/29/65528": [], + "0/31/1": [], + "0/31/65533": 1, + "0/31/65532": 0, + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 3 + } + ], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/31/65529": [], + "0/31/65528": [], + "0/40/11": "20251122", + "0/40/12": "E2493", + "0/40/16": false, + "0/40/17": true, + "0/40/24": 1, + "0/40/65533": 3, + "0/40/0": 17, + "0/40/1": "IKEA of Sweden", + "0/40/2": 4476, + "0/40/3": "KLIPPBOK water leak sensor", + "0/40/4": 32774, + "0/40/5": "", + "0/40/6": "**REDACTED**", + "0/40/7": 512, + "0/40/8": "P2.0", + "0/40/9": 16777229, + "0/40/10": "1.0.13", + "0/40/18": "5022e21a483c1d4f68fa7b871fb705f9", + "0/40/19": { + "0": 3, + "1": 3 + }, + "0/40/21": 16973824, + "0/40/22": 1, + "0/40/65532": 0, + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16, 17, 18, 19, 21, 22, 65528, + 65529, 65531, 65532, 65533 + ], + "0/40/65529": [], + "0/40/65528": [], + "0/42/65533": 1, + "0/42/0": [], + "0/42/1": true, + "0/42/2": 1, + "0/42/3": null, + "0/42/65532": 0, + "0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/42/65529": [0], + "0/42/65528": [], + "0/47/65532": 10, + "0/47/11": 2991, + "0/47/12": 200, + "0/47/14": 0, + "0/47/15": false, + "0/47/16": 2, + "0/47/19": "AAA", + "0/47/20": 1, + "0/47/25": 1, + "0/47/65533": 2, + "0/47/0": 1, + "0/47/1": 0, + "0/47/2": "Primary Battery", + "0/47/31": [], + "0/47/65531": [ + 0, 1, 2, 11, 12, 14, 15, 16, 19, 20, 25, 31, 65528, 65529, 65531, 65532, + 65533 + ], + "0/47/65529": [], + "0/47/65528": [], + "0/48/65533": 1, + "0/48/65532": 0, + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 0, + "0/48/4": true, + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/48/65529": [0, 2, 4], + "0/48/65528": [1, 3, 5], + "0/49/65532": 2, + "0/49/2": 10, + "0/49/3": 20, + "0/49/9": 4, + "0/49/10": 4, + "0/49/65533": 2, + "0/49/0": 1, + "0/49/1": [ + { + "0": "/yXnNCiUSvw=", + "1": true + } + ], + "0/49/4": true, + "0/49/5": 0, + "0/49/6": "/yXnNCiUSvw=", + "0/49/7": null, + "0/49/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 65528, 65529, 65531, 65532, 65533 + ], + "0/49/65529": [0, 3, 4, 6, 8], + "0/49/65528": [1, 5, 7], + "0/51/3": 1565, + "0/51/4": 6, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/65533": 2, + "0/51/65532": 0, + "0/51/0": [ + { + "0": "MyHome1895415629", + "1": true, + "2": null, + "3": null, + "4": "2kBsLtRvnPA=", + "5": [], + "6": [ + "/Rfu+JATAAAvUPrv0kGOrQ==", + "/dH8OtkWot0AAAD//gDMNg==", + "/dH8OtkWot29tss/JqJ3Mw==", + "/oAAAAAAAADYQGwu1G+c8A==" + ], + "7": 4 + } + ], + "0/51/1": 7, + "0/51/2": 218, + "0/51/8": false, + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ], + "0/51/65529": [0, 1], + "0/51/65528": [2], + "0/53/63": null, + "0/53/64": null, + "0/53/65533": 2, + "0/53/65532": 0, + "0/53/0": 25, + "0/53/1": 2, + "0/53/2": "MyHome1895415629", + "0/53/3": 49399, + "0/53/4": 18385355265015040764, + "0/53/5": "QP3R/DrZFqLd", + "0/53/7": [ + { + "0": 6260342793904889918, + "1": 218, + "2": 52224, + "3": 878085, + "4": 189075, + "5": 3, + "6": -47, + "7": -48, + "8": 59, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + } + ], + "0/53/8": [ + { + "0": 6260342793904889918, + "1": 52224, + "2": 51, + "3": 0, + "4": 0, + "5": 3, + "6": 0, + "7": 218, + "8": true, + "9": true + } + ], + "0/53/9": 2137771856, + "0/53/10": 68, + "0/53/11": 140, + "0/53/12": 230, + "0/53/13": 30, + "0/53/59": { + "0": 672, + "1": 8335 + }, + "0/53/60": "AB//wA==", + "0/53/61": { + "0": true, + "1": false, + "2": true, + "3": true, + "4": true, + "5": true, + "6": false, + "7": true, + "8": true, + "9": true, + "10": true, + "11": true + }, + "0/53/62": [], + "0/53/65531": [ + 0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 59, 60, 61, 62, 65528, 65529, + 65531, 65532, 65533 + ], + "0/53/65529": [], + "0/53/65528": [], + "0/60/65532": 1, + "0/60/65533": 1, + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/60/65529": [0, 1, 2], + "0/60/65528": [], + "0/62/65533": 1, + "0/62/0": [ + { + "1": "FTABAQgkAgE3AyQTAhgmBL3MNy8mBT0D5UM3BiQVAiURQAEYJAcBJAgBMAlBBB5ZHrs6JznmDwMq4t9+CjNwTfXtyTVZjyVzVOjlI+cY/hetXDmsCC0TNewgULVLEXlQxezlY85QYFPMlaSruqU3CjUBKAEYJAIBNgMEAgQBGDAEFPMDw70atbx08VczmceWWiv83ov+MAUU/eZfRuhWvUFT8WNU1R/sUE0q70YYMAtAIdqlW4sHK/DRdVQpbywUoGRVIc3G7esTDJgnEG3OcDLWXSHwdDfuuFNLN589EbBOEuOVCPCn+6Im65TuI1tJoxg=", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEJB6axSR6Xj7Ab+FB5+C+slsdDtj0qCvcRHCCpCYTX6svgMPs/yVVEfvJgIUXZ5gkLS9jK1CpsF4u8MZR6qsNZzcKNQEpARgkAmAwBBT95l9G6Fa9QVPxY1TVH+xQTSrvRjAFFLyrP0JigOFmlOJIyXL9CANMKs5NGDALQNykW7UIqcgXgx+UezCVYPRU8/CpHh9CJBqL/7wKfTM62ujWJlrH0P5DEZ5bV9ZihCk4Wg/DMM2BUuUcTOEEqzEY", + "254": 3 + } + ], + "0/62/1": [ + { + "1": "BCZ12LdJK3WZUiquu2PD6iWSaeQK6J6DWw86GihFX4HiWOG1JQip6ILp0IFNffrIGwriEteEhksN56MylydpF/s=", + "2": 4939, + "3": 2, + "4": 320, + "5": "Home", + "254": 3 + } + ], + "0/62/2": 5, + "0/62/3": 3, + "0/62/4": [ + "FTABAQEkAgE3AycUZweWCkFUxWIYJgQAxikuJgUAzMFTNwYnFGcHlgpBVMViGCQHASQIATAJQQTNp4X8uzw+lDd+17mb6etHu369TrObsUl8Z+oc0Tv7ETpM91vCk6f7cF6b1UzZnUOizSZh/oir5rLjiYmaiyNsNwo1ASkBGCQCYDAEFNTPAx7gkSWXYv+kfe6NSGPo/UiMMAUU1M8DHuCRJZdi/6R97o1IY+j9SIwYMAtA2+qZy7xOxqbY0kyFYHKHQ7oPs6lduN8CwhHGHO11PViHOa8Ghkg8k9z8WbloMSd5Q/ZQyIRJsBFVd2hIBNu1/Rg=", + "FTABFAPDl5npnEb4b11grHG1AdwYLxLOJAIBNwMnFGHDECgeEBh7JxVfPgmR153o9xgmBK5z+S8mBa6C9Y03BicUYcMQKB4QGHsnFV8+CZHXnej3GCQHASQIATAJQQRHUJH4AsX5PMwf+CaBm3BgQsag6Si880dCh4Z/nWvAzlsLLY9CTa0+LBwePhweon0IdT3tsHIq1K3OOg5YsbG4Nwo1ASkBGCQCYDAEFLtqXB3f+f0UnYFZUcYXZ654P2A6MAUUu2pcHd/5/RSdgVlRxhdnrng/YDoYMAtAtfPiyCDVwl93HpnYluDMZCQ2mEpiZNTgZSHQDfsMbdSQoYery2KtqPJkglA5+XUKRm57nbTJKENE9evkOD0Ctxg=", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEJnXYt0krdZlSKq67Y8PqJZJp5AronoNbDzoaKEVfgeJY4bUlCKnogunQgU19+sgbCuIS14SGSw3nozKXJ2kX+zcKNQEpARgkAmAwBBS8qz9CYoDhZpTiSMly/QgDTCrOTTAFFLyrP0JigOFmlOJIyXL9CANMKs5NGDALQIfPjG1LeoSoRd3sJ2NeaS3VrHyftI8l6dOwafhoGMQdCRwyadYABiUG/Po1BnWmg4laSh88nP3zAAnQ2j0l4tAY" + ], + "0/62/5": 3, + "0/62/65532": 0, + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65528": [1, 3, 5, 8], + "0/63/65533": 2, + "0/63/65532": 0, + "0/63/0": [], + "0/63/1": [], + "0/63/2": 4, + "0/63/3": 3, + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/63/65529": [0, 1, 3, 4], + "0/63/65528": [2, 5], + "0/70/65533": 2, + "0/70/65532": 0, + "0/70/0": 120, + "0/70/1": 1000, + "0/70/2": 1000, + "0/70/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/70/65529": [], + "0/70/65528": [], + "1/29/65533": 2, + "1/29/65532": 0, + "1/29/0": [ + { + "0": 67, + "1": 1 + } + ], + "1/29/1": [3, 29, 69, 128], + "1/29/2": [], + "1/29/3": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/29/65529": [], + "1/29/65528": [], + "1/128/65532": 7, + "1/128/3": 0, + "1/128/4": 0, + "1/128/5": 3, + "1/128/6": 3, + "1/128/65533": 1, + "1/128/65531": [3, 4, 5, 6, 65528, 65529, 65531, 65532, 65533], + "1/128/65529": [0, 1], + "1/128/65528": [], + "1/3/65533": 4, + "1/3/0": 0, + "1/3/1": 2, + "1/3/65532": 0, + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/3/65529": [0], + "1/3/65528": [], + "1/69/65533": 1, + "1/69/0": false, + "1/69/65532": 0, + "1/69/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/69/65529": [], + "1/69/65528": [] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/snapshots/test_switch.ambr b/tests/components/matter/snapshots/test_switch.ambr index 68d98a37a6ce..8c51fb7c5538 100644 --- a/tests/components/matter/snapshots/test_switch.ambr +++ b/tests/components/matter/snapshots/test_switch.ambr @@ -1,4 +1,104 @@ # serializer version: 1 +# name: test_boolean_state_configuration_alarm_enabled_switches[ikea_klippbok_water_leak][switch.klippbok_water_leak_sensor_audible_alarm_enabled-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.klippbok_water_leak_sensor_audible_alarm_enabled', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Audible alarm enabled', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Audible alarm enabled', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'audible_alarm_enabled', + 'unique_id': '00000000000004D2-0000000000000140-MatterNodeDevice-1-BooleanStateConfigurationAudibleAlarmEnabled-128-5', + 'unit_of_measurement': None, + }) +# --- +# name: test_boolean_state_configuration_alarm_enabled_switches[ikea_klippbok_water_leak][switch.klippbok_water_leak_sensor_audible_alarm_enabled-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'KLIPPBOK water leak sensor Audible alarm enabled', + }), + 'context': , + 'entity_id': 'switch.klippbok_water_leak_sensor_audible_alarm_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_boolean_state_configuration_alarm_enabled_switches[ikea_klippbok_water_leak][switch.klippbok_water_leak_sensor_visual_alarm_enabled-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.klippbok_water_leak_sensor_visual_alarm_enabled', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Visual alarm enabled', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Visual alarm enabled', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'visual_alarm_enabled', + 'unique_id': '00000000000004D2-0000000000000140-MatterNodeDevice-1-BooleanStateConfigurationVisualAlarmEnabled-128-5', + 'unit_of_measurement': None, + }) +# --- +# name: test_boolean_state_configuration_alarm_enabled_switches[ikea_klippbok_water_leak][switch.klippbok_water_leak_sensor_visual_alarm_enabled-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'KLIPPBOK water leak sensor Visual alarm enabled', + }), + 'context': , + 'entity_id': 'switch.klippbok_water_leak_sensor_visual_alarm_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_switches[eve_energy_20ecn4101][switch.eve_energy_20ecn4101_child_lock_top-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/matter/test_switch.py b/tests/components/matter/test_switch.py index 98dc323b0473..ed224d10b123 100644 --- a/tests/components/matter/test_switch.py +++ b/tests/components/matter/test_switch.py @@ -1,5 +1,6 @@ """Test Matter switches.""" +import asyncio from unittest.mock import MagicMock, call from chip.clusters import Objects as clusters @@ -234,6 +235,120 @@ async def test_evse_sensor( ) +@pytest.mark.parametrize("node_fixture", ["ikea_klippbok_water_leak"]) +async def test_boolean_state_configuration_alarm_enabled_switches( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + matter_client: MagicMock, + matter_node: MatterNode, + snapshot: SnapshotAssertion, +) -> None: + """Test Boolean State Configuration alarm enabled switches.""" + + visual_entity_id = "switch.klippbok_water_leak_sensor_visual_alarm_enabled" + audible_entity_id = "switch.klippbok_water_leak_sensor_audible_alarm_enabled" + visual_entry = entity_registry.async_get(visual_entity_id) + audible_entry = entity_registry.async_get(audible_entity_id) + assert visual_entry + assert audible_entry + + visual_state = hass.states.get(visual_entity_id) + audible_state = hass.states.get(audible_entity_id) + assert visual_state + assert audible_state + assert visual_entry == snapshot(name=f"{visual_entity_id}-entry") + assert visual_state == snapshot(name=f"{visual_entity_id}-state") + assert audible_entry == snapshot(name=f"{audible_entity_id}-entry") + assert audible_state == snapshot(name=f"{audible_entity_id}-state") + assert visual_state.state == "on" + assert audible_state.state == "on" + + await hass.services.async_call( + "switch", + "turn_off", + {"entity_id": visual_entity_id}, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=matter_node.node_id, + endpoint_id=1, + command=clusters.BooleanStateConfiguration.Commands.EnableDisableAlarm( + alarmsToEnableDisable=2, + ), + ) + + set_node_attribute(matter_node, 1, 128, 5, 2) + await trigger_subscription_callback(hass, matter_client) + + visual_state = hass.states.get(visual_entity_id) + audible_state = hass.states.get(audible_entity_id) + assert visual_state + assert audible_state + assert visual_state.state == "off" + assert audible_state.state == "on" + + await hass.services.async_call( + "switch", + "turn_on", + {"entity_id": visual_entity_id}, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 2 + assert matter_client.send_device_command.call_args == call( + node_id=matter_node.node_id, + endpoint_id=1, + command=clusters.BooleanStateConfiguration.Commands.EnableDisableAlarm( + alarmsToEnableDisable=3, + ), + ) + + +@pytest.mark.parametrize("node_fixture", ["ikea_klippbok_water_leak"]) +async def test_boolean_state_configuration_alarm_enabled_switches_are_serialized( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test alarm switch changes to the shared bitmap are serialized.""" + visual_entity_id = "switch.klippbok_water_leak_sensor_visual_alarm_enabled" + audible_entity_id = "switch.klippbok_water_leak_sensor_audible_alarm_enabled" + command_started = asyncio.Event() + allow_commands = asyncio.Event() + + async def send_device_command(*args: object, **kwargs: object) -> None: + command_started.set() + await allow_commands.wait() + + matter_client.send_device_command.side_effect = send_device_command + task = hass.async_create_task( + hass.services.async_call( + "switch", + "turn_off", + {"entity_id": [visual_entity_id, audible_entity_id]}, + blocking=True, + ) + ) + await command_started.wait() + await asyncio.sleep(0) + + assert matter_client.send_device_command.call_count == 1 + + allow_commands.set() + await task + + assert matter_client.send_device_command.call_count == 2 + assert matter_client.send_device_command.call_args_list[-1] == call( + node_id=matter_node.node_id, + endpoint_id=1, + command=clusters.BooleanStateConfiguration.Commands.EnableDisableAlarm( + alarmsToEnableDisable=0, + ), + ) + + @pytest.mark.parametrize("node_fixture", ["mock_speaker"]) async def test_speaker_mute_uses_onoff_commands( hass: HomeAssistant,