"""Matter switches.""" from asyncio import Lock from collections.abc import Callable 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, SwitchEntity, SwitchEntityDescription, ) from homeassistant.const import EntityCategory, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .entity import MatterEntity, MatterEntityDescription from .helpers import MatterConfigEntry from .models import MatterDiscoverySchema EVSE_SUPPLY_STATE_MAP = { clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabled: False, clusters.EnergyEvse.Enums.SupplyStateEnum.kChargingEnabled: True, clusters.EnergyEvse.Enums.SupplyStateEnum.kDischargingEnabled: False, 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, config_entry: MatterConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Matter switches from Config Entry.""" matter = config_entry.runtime_data.adapter matter.register_platform_handler(Platform.SWITCH, async_add_entities) @dataclass(frozen=True, kw_only=True) 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() ) @override async def async_turn_on(self, **kwargs: Any) -> None: """Turn switch on.""" await self.send_device_command(self._get_command_for_value(True)) @override async def async_turn_off(self, **kwargs: Any) -> None: """Turn switch off.""" await self.send_device_command(self._get_command_for_value(False)) @callback @override def _update_from_device(self) -> None: """Update from device.""" 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): """Representation of a Matter switch.""" entity_description: MatterGenericCommandSwitchEntityDescription _platform_translation_key = "switch" @override async def async_turn_on(self, **kwargs: Any) -> None: """Turn switch on.""" if self.entity_description.on_command: # custom command defined to set the new value await self.send_device_command( self.entity_description.on_command(), self.entity_description.command_timeout, ) @override async def async_turn_off(self, **kwargs: Any) -> None: """Turn switch off.""" if self.entity_description.off_command: await self.send_device_command( self.entity_description.off_command(), self.entity_description.command_timeout, ) @callback @override def _update_from_device(self) -> None: """Update from device.""" value = self.get_matter_attribute_value(self._entity_info.primary_attribute) if value_convert := self.entity_description.device_to_ha: value = value_convert(value) self._attr_is_on = value @override async def send_device_command( self, command: ClusterCommand, command_timeout: int | None = None, **kwargs: Any, ) -> None: """Send device command with timeout.""" await self.matter_client.send_device_command( node_id=self._endpoint.node.node_id, endpoint_id=self._endpoint.endpoint_id, command=command, timed_request_timeout_ms=command_timeout, **kwargs, ) @dataclass(frozen=True, kw_only=True) class MatterGenericCommandSwitchEntityDescription(MatterSwitchEntityDescription): """Describe Matter Generic command Switch entities.""" # command: a custom callback to create the command to send to the device on_command: Callable[[], Any] | None = None off_command: Callable[[], Any] | None = None command_timeout: int | None = None @dataclass(frozen=True, kw_only=True) class MatterNumericSwitchEntityDescription(MatterSwitchEntityDescription): """Describe Matter Numeric Switch entities.""" class MatterNumericSwitch(MatterSwitch): """Representation of a Matter Enum Attribute as a Switch entity.""" entity_description: MatterNumericSwitchEntityDescription 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) @override async def async_turn_on(self, **kwargs: Any) -> None: """Turn switch on.""" await self._async_set_native_value(True) @override async def async_turn_off(self, **kwargs: Any) -> None: """Turn switch off.""" await self._async_set_native_value(False) @callback @override def _update_from_device(self) -> None: """Update from device.""" value = self.get_matter_attribute_value(self._entity_info.primary_attribute) if value_convert := self.entity_description.device_to_ha: value = value_convert(value) 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( platform=Platform.SWITCH, entity_description=MatterSwitchEntityDescription( key="MatterPlug", device_class=SwitchDeviceClass.OUTLET, name=None, ), entity_class=MatterSwitch, required_attributes=(clusters.OnOff.Attributes.OnOff,), device_type=(device_types.OnOffPlugInUnit,), ), MatterDiscoverySchema( platform=Platform.SWITCH, entity_description=MatterSwitchEntityDescription( key="MatterPowerToggle", device_class=SwitchDeviceClass.SWITCH, translation_key="power", ), entity_class=MatterSwitch, required_attributes=(clusters.OnOff.Attributes.OnOff,), device_type=( device_types.AirPurifier, device_types.BasicVideoPlayer, device_types.CastingVideoPlayer, device_types.CookSurface, device_types.Cooktop, device_types.Dishwasher, device_types.ExtractorHood, device_types.LaundryDryer, device_types.LaundryWasher, device_types.Oven, device_types.Pump, device_types.PumpController, device_types.Refrigerator, device_types.RoboticVacuumCleaner, device_types.RoomAirConditioner, ), ), MatterDiscoverySchema( platform=Platform.SWITCH, entity_description=MatterSwitchEntityDescription( key="MatterSwitch", entity_category=EntityCategory.CONFIG, translation_key="display", ), entity_class=MatterSwitch, required_attributes=(clusters.OnOff.Attributes.OnOff,), vendor_id=(4476,), # IKEA of Sweden product_id=(12289,), # ALPSTUGA air quality monitor ), MatterDiscoverySchema( platform=Platform.SWITCH, entity_description=MatterSwitchEntityDescription( key="MatterSwitch", device_class=SwitchDeviceClass.OUTLET, name=None, ), entity_class=MatterSwitch, required_attributes=(clusters.OnOff.Attributes.OnOff,), not_device_type=( device_types.ColorTemperatureLight, device_types.DimmableLight, device_types.ExtendedColorLight, device_types.DimmerSwitch, device_types.ColorDimmerSwitch, device_types.OnOffLight, device_types.AirPurifier, device_types.BasicVideoPlayer, device_types.CastingVideoPlayer, device_types.CookSurface, device_types.Cooktop, device_types.Dishwasher, device_types.ExtractorHood, device_types.Fan, device_types.LaundryDryer, device_types.LaundryWasher, device_types.Oven, device_types.Pump, device_types.PumpController, device_types.Refrigerator, device_types.RoboticVacuumCleaner, device_types.RoomAirConditioner, device_types.Speaker, ), ), MatterDiscoverySchema( platform=Platform.SWITCH, entity_description=MatterSwitchEntityDescription( key="MatterMuteToggle", translation_key="speaker_mute", inverted=True, ), entity_class=MatterSwitch, required_attributes=(clusters.OnOff.Attributes.OnOff,), device_type=(device_types.Speaker,), ), MatterDiscoverySchema( platform=Platform.SWITCH, entity_description=MatterNumericSwitchEntityDescription( key="EveTrvChildLock", entity_category=EntityCategory.CONFIG, translation_key="child_lock", device_to_ha={ 0: False, 1: True, }.get, ha_to_device={ False: 0, True: 1, }.get, ), entity_class=MatterNumericSwitch, required_attributes=( clusters.ThermostatUserInterfaceConfiguration.Attributes.KeypadLockout, ), vendor_id=(4874,), ), MatterDiscoverySchema( platform=Platform.SWITCH, entity_description=MatterNumericSwitchEntityDescription( key="DoorLockEnablePrivacyModeButton", entity_category=EntityCategory.CONFIG, translation_key="privacy_mode_button", device_to_ha=bool, ha_to_device=int, ), entity_class=MatterNumericSwitch, required_attributes=(clusters.DoorLock.Attributes.EnablePrivacyModeButton,), ), MatterDiscoverySchema( platform=Platform.SWITCH, entity_description=MatterNumericSwitchEntityDescription( key="DoorLockEnableOneTouchLocking", entity_category=EntityCategory.CONFIG, translation_key="one_touch_locking", ), entity_class=MatterNumericSwitch, required_attributes=(clusters.DoorLock.Attributes.EnableOneTouchLocking,), ), MatterDiscoverySchema( platform=Platform.SWITCH, entity_description=MatterGenericCommandSwitchEntityDescription( key="EnergyEvseChargingSwitch", translation_key="evse_charging_switch", on_command=lambda: clusters.EnergyEvse.Commands.EnableCharging( chargingEnabledUntil=NullValue, minimumChargeCurrent=0, maximumChargeCurrent=0, ), off_command=clusters.EnergyEvse.Commands.Disable, command_timeout=3000, device_to_ha=EVSE_SUPPLY_STATE_MAP.get, ), entity_class=MatterGenericCommandSwitch, required_attributes=( clusters.EnergyEvse.Attributes.SupplyState, clusters.EnergyEvse.Attributes.AcceptedCommandList, ), value_contains=clusters.EnergyEvse.Commands.EnableCharging.command_id, allow_multi=True, ), MatterDiscoverySchema( platform=Platform.SWITCH, entity_description=MatterNumericSwitchEntityDescription( key="EveChildLock", entity_category=EntityCategory.CONFIG, translation_key="child_lock", ), 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, ), ]