diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index ee906662de5..7c125763703 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -46,30 +46,42 @@ async def async_setup_entry( class MatterSwitchEntityDescription(SwitchEntityDescription, MatterEntityDescription): """Describe Matter Switch entities.""" + inverted: bool = False + class MatterSwitch(MatterEntity, SwitchEntity): """Representation of a Matter switch.""" + entity_description: MatterSwitchEntityDescription _platform_translation_key = "switch" + def _get_command_for_value(self, value: bool) -> ClusterCommand: + """Get the appropriate command for the desired value. + + Applies inversion if needed (e.g., for inverted logic like mute). + """ + send_value = not value if self.entity_description.inverted else value + return ( + clusters.OnOff.Commands.On() + if send_value + else clusters.OnOff.Commands.Off() + ) + async def async_turn_on(self, **kwargs: Any) -> None: """Turn switch on.""" - await self.send_device_command( - clusters.OnOff.Commands.On(), - ) + await self.send_device_command(self._get_command_for_value(True)) async def async_turn_off(self, **kwargs: Any) -> None: """Turn switch off.""" - await self.send_device_command( - clusters.OnOff.Commands.Off(), - ) + await self.send_device_command(self._get_command_for_value(False)) @callback def _update_from_device(self) -> None: """Update from device.""" - self._attr_is_on = self.get_matter_attribute_value( - self._entity_info.primary_attribute - ) + value = self.get_matter_attribute_value(self._entity_info.primary_attribute) + if self.entity_description.inverted: + value = not value + self._attr_is_on = value class MatterGenericCommandSwitch(MatterSwitch): @@ -121,9 +133,7 @@ class MatterGenericCommandSwitch(MatterSwitch): @dataclass(frozen=True, kw_only=True) -class MatterGenericCommandSwitchEntityDescription( - SwitchEntityDescription, MatterEntityDescription -): +class MatterGenericCommandSwitchEntityDescription(MatterSwitchEntityDescription): """Describe Matter Generic command Switch entities.""" # command: a custom callback to create the command to send to the device @@ -133,9 +143,7 @@ class MatterGenericCommandSwitchEntityDescription( @dataclass(frozen=True, kw_only=True) -class MatterNumericSwitchEntityDescription( - SwitchEntityDescription, MatterEntityDescription -): +class MatterNumericSwitchEntityDescription(MatterSwitchEntityDescription): """Describe Matter Numeric Switch entities.""" @@ -146,11 +154,10 @@ class MatterNumericSwitch(MatterSwitch): async def _async_set_native_value(self, value: bool) -> None: """Update the current value.""" + send_value: Any = value if value_convert := self.entity_description.ha_to_device: send_value = value_convert(value) - await self.write_attribute( - value=send_value, - ) + await self.write_attribute(value=send_value) async def async_turn_on(self, **kwargs: Any) -> None: """Turn switch on.""" @@ -248,19 +255,12 @@ DISCOVERY_SCHEMAS = [ ), MatterDiscoverySchema( platform=Platform.SWITCH, - entity_description=MatterNumericSwitchEntityDescription( + entity_description=MatterSwitchEntityDescription( key="MatterMuteToggle", translation_key="speaker_mute", - device_to_ha={ - True: False, # True means volume is on, so HA should show mute as off - False: True, # False means volume is off (muted), so HA should show mute as on - }.get, - ha_to_device={ - False: True, # HA showing mute as off means volume is on, so send True - True: False, # HA showing mute as on means volume is off (muted), so send False - }.get, + inverted=True, ), - entity_class=MatterNumericSwitch, + entity_class=MatterSwitch, required_attributes=(clusters.OnOff.Attributes.OnOff,), device_type=(device_types.Speaker,), ), diff --git a/tests/components/matter/test_switch.py b/tests/components/matter/test_switch.py index 4155901fa8b..e89f3fc7fe6 100644 --- a/tests/components/matter/test_switch.py +++ b/tests/components/matter/test_switch.py @@ -232,3 +232,56 @@ async def test_evse_sensor( ), timed_request_timeout_ms=3000, ) + + +@pytest.mark.parametrize("node_fixture", ["mock_speaker"]) +async def test_speaker_mute_uses_onoff_commands( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test speaker mute switch uses On/Off commands instead of attribute writes.""" + + state = hass.states.get("switch.mock_speaker_mute") + assert state + assert state.state == "off" + + await hass.services.async_call( + "switch", + "turn_on", + {"entity_id": "switch.mock_speaker_mute"}, + 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.OnOff.Commands.Off(), + ) + + set_node_attribute(matter_node, 1, 6, 0, False) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("switch.mock_speaker_mute") + assert state + assert state.state == "on" + + await hass.services.async_call( + "switch", + "turn_off", + {"entity_id": "switch.mock_speaker_mute"}, + 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.OnOff.Commands.On(), + ) + + set_node_attribute(matter_node, 1, 6, 0, True) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("switch.mock_speaker_mute") + assert state + assert state.state == "off"