1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-19 18:38:58 +00:00
Files
core/homeassistant/components/matter/select.py

653 lines
25 KiB
Python

"""Matter ModeSelect Cluster Support."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast
from chip.clusters import Objects as clusters
from chip.clusters.ClusterObjects import ClusterAttributeDescriptor, ClusterCommand
from chip.clusters.Types import Nullable
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
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 get_matter
from .models import MatterDiscoverySchema
DOOR_LOCK_OPERATING_MODE_MAP = {
clusters.DoorLock.Enums.OperatingModeEnum.kNormal: "normal",
clusters.DoorLock.Enums.OperatingModeEnum.kVacation: "vacation",
clusters.DoorLock.Enums.OperatingModeEnum.kPrivacy: "privacy",
clusters.DoorLock.Enums.OperatingModeEnum.kNoRemoteLockUnlock: "no_remote_lock_unlock",
clusters.DoorLock.Enums.OperatingModeEnum.kPassage: "passage",
}
DOOR_LOCK_OPERATING_MODE_MAP_REVERSE = {
v: k for k, v in DOOR_LOCK_OPERATING_MODE_MAP.items()
}
NUMBER_OF_RINSES_STATE_MAP = {
clusters.LaundryWasherControls.Enums.NumberOfRinsesEnum.kNone: "off",
clusters.LaundryWasherControls.Enums.NumberOfRinsesEnum.kNormal: "normal",
clusters.LaundryWasherControls.Enums.NumberOfRinsesEnum.kExtra: "extra",
clusters.LaundryWasherControls.Enums.NumberOfRinsesEnum.kMax: "max",
clusters.LaundryWasherControls.Enums.NumberOfRinsesEnum.kUnknownEnumValue: None,
}
NUMBER_OF_RINSES_STATE_MAP_REVERSE = {
v: k for k, v in NUMBER_OF_RINSES_STATE_MAP.items()
}
PUMP_OPERATION_MODE_MAP = {
clusters.PumpConfigurationAndControl.Enums.OperationModeEnum.kNormal: "normal",
clusters.PumpConfigurationAndControl.Enums.OperationModeEnum.kMinimum: "minimum",
clusters.PumpConfigurationAndControl.Enums.OperationModeEnum.kMaximum: "maximum",
clusters.PumpConfigurationAndControl.Enums.OperationModeEnum.kLocal: "local",
}
PUMP_OPERATION_MODE_MAP_REVERSE = {v: k for k, v in PUMP_OPERATION_MODE_MAP.items()}
type SelectCluster = (
clusters.ModeSelect
| clusters.OvenMode
| clusters.LaundryWasherMode
| clusters.RefrigeratorAndTemperatureControlledCabinetMode
| clusters.RvcRunMode
| clusters.RvcCleanMode
| clusters.DishwasherMode
| clusters.EnergyEvseMode
| clusters.DeviceEnergyManagementMode
| clusters.WaterHeaterMode
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Matter ModeSelect from Config Entry."""
matter = get_matter(hass)
matter.register_platform_handler(Platform.SELECT, async_add_entities)
@dataclass(frozen=True, kw_only=True)
class MatterSelectEntityDescription(SelectEntityDescription, MatterEntityDescription):
"""Describe Matter select entities."""
@dataclass(frozen=True, kw_only=True)
class MatterMapSelectEntityDescription(MatterSelectEntityDescription):
"""Describe Matter select entities for MatterMapSelectEntityDescription."""
device_to_ha: Callable[[int], str | None]
ha_to_device: Callable[[str], int | None]
# list attribute: the attribute descriptor to get the list of values (= list of integers)
list_attribute: type[ClusterAttributeDescriptor]
@dataclass(frozen=True, kw_only=True)
class MatterListSelectEntityDescription(MatterSelectEntityDescription):
"""Describe Matter select entities for MatterListSelectEntity."""
# list attribute: the attribute descriptor to get the list of values (= list of strings)
list_attribute: type[ClusterAttributeDescriptor]
# command: a custom callback to create the command to send to the device
# the callback's argument will be the index of the selected list value
# if omitted the command will just be a write_attribute command to the primary attribute
command: Callable[[int], ClusterCommand] | None = None
class MatterAttributeSelectEntity(MatterEntity, SelectEntity):
"""Representation of a select entity from Matter Attribute read/write."""
entity_description: MatterSelectEntityDescription
async def async_select_option(self, option: str) -> None:
"""Change the selected mode."""
value_convert = self.entity_description.ha_to_device
if TYPE_CHECKING:
assert value_convert is not None
await self.write_attribute(
value=value_convert(option),
)
@callback
def _update_from_device(self) -> None:
"""Update from device."""
value: Nullable | int | None
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
value_convert = self.entity_description.device_to_ha
if TYPE_CHECKING:
assert value_convert is not None
self._attr_current_option = value_convert(value)
class MatterMapSelectEntity(MatterAttributeSelectEntity):
"""Representation of a Matter select entity where the options are defined in a State map."""
entity_description: MatterMapSelectEntityDescription
@callback
def _update_from_device(self) -> None:
"""Update from device."""
# the options can dynamically change based on the state of the device
available_values = cast(
list[int],
self.get_matter_attribute_value(self.entity_description.list_attribute),
)
# map available (int) values to string representation
self._attr_options = [
mapped_value
for value in available_values
if (mapped_value := self.entity_description.device_to_ha(value))
]
# use base implementation from MatterAttributeSelectEntity to set the current option
super()._update_from_device()
class MatterModeSelectEntity(MatterAttributeSelectEntity):
"""Representation of a select entity from Matter (Mode) Cluster attribute(s)."""
async def async_select_option(self, option: str) -> None:
"""Change the selected mode."""
cluster: SelectCluster = self._endpoint.get_cluster(
self._entity_info.primary_attribute.cluster_id
)
# select the mode ID from the label string
for mode in cluster.supportedModes:
if mode.label != option:
continue
await self.send_device_command(
cluster.Commands.ChangeToMode(newMode=mode.mode),
)
break
@callback
def _update_from_device(self) -> None:
"""Update from device."""
# NOTE: cluster can be ModeSelect or a variant of that,
# such as DishwasherMode. They all have the same characteristics.
cluster: SelectCluster = self._endpoint.get_cluster(
self._entity_info.primary_attribute.cluster_id
)
modes = {mode.mode: mode.label for mode in cluster.supportedModes}
self._attr_options = list(modes.values())
self._attr_current_option = modes.get(cluster.currentMode)
# handle optional Description attribute as descriptive name for the mode
if desc := getattr(cluster, "description", None):
self._attr_name = desc
class MatterDoorLockOperatingModeSelectEntity(MatterAttributeSelectEntity):
"""Representation of a Door Lock Operating Mode select entity.
This entity dynamically filters available operating modes based on the device's
`SupportedOperatingModes` bitmap attribute. In this bitmap, bit=0 indicates a
supported mode and bit=1 indicates unsupported (inverted from typical bitmap conventions).
If the bitmap is unavailable, only mandatory modes are included. The mapping from
bitmap bits to operating mode values is defined by the Matter specification.
"""
entity_description: MatterMapSelectEntityDescription
@callback
def _update_from_device(self) -> None:
"""Update from device."""
# Get the bitmap of supported operating modes
supported_modes_bitmap = self.get_matter_attribute_value(
self.entity_description.list_attribute
)
# Convert bitmap to list of supported mode values
# NOTE: The Matter spec inverts the usual meaning: bit=0 means supported,
# bit=1 means not supported, undefined bits must be 1. Mandatory modes are
# bits 0 (Normal) and 3 (NoRemoteLockUnlock).
num_mode_bits = supported_modes_bitmap.bit_length()
supported_mode_values = [
bit_position
for bit_position in range(num_mode_bits)
if not supported_modes_bitmap & (1 << bit_position)
]
# Map supported mode values to their string representations
self._attr_options = [
mapped_value
for mode_value in supported_mode_values
if (mapped_value := self.entity_description.device_to_ha(mode_value))
]
# Use base implementation to set the current option
super()._update_from_device()
class MatterListSelectEntity(MatterEntity, SelectEntity):
"""Representation of a select entity from Matter list and selected item Cluster attribute(s)."""
entity_description: MatterListSelectEntityDescription
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
option_id = self._attr_options.index(option)
if TYPE_CHECKING:
assert option_id is not None
if self.entity_description.command:
# custom command defined to set the new value
await self.send_device_command(
self.entity_description.command(option_id),
)
return
# regular write attribute to set the new value
await self.write_attribute(
value=option_id,
)
@callback
def _update_from_device(self) -> None:
"""Update from device."""
list_values_raw = self.get_matter_attribute_value(
self.entity_description.list_attribute
)
if TYPE_CHECKING:
assert list_values_raw is not None
# Accept both list[str] and list[int], convert to str
list_values = [str(v) for v in list_values_raw]
self._attr_options = list_values
current_option_idx: int = self.get_matter_attribute_value(
self._entity_info.primary_attribute
)
try:
self._attr_current_option = list_values[current_option_idx]
except IndexError:
self._attr_current_option = None
# Discovery schema(s) to map Matter Attributes to HA entities
DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="MatterModeSelect",
entity_category=EntityCategory.CONFIG,
translation_key="mode",
),
entity_class=MatterModeSelectEntity,
required_attributes=(
clusters.ModeSelect.Attributes.CurrentMode,
clusters.ModeSelect.Attributes.SupportedModes,
),
# don't discover this entry if the supported modes list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="MatterOvenMode",
translation_key="mode",
),
entity_class=MatterModeSelectEntity,
required_attributes=(
clusters.OvenMode.Attributes.CurrentMode,
clusters.OvenMode.Attributes.SupportedModes,
),
# don't discover this entry if the supported modes list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="MatterLaundryWasherMode",
translation_key="mode",
),
entity_class=MatterModeSelectEntity,
required_attributes=(
clusters.LaundryWasherMode.Attributes.CurrentMode,
clusters.LaundryWasherMode.Attributes.SupportedModes,
),
# don't discover this entry if the supported modes list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="MatterRefrigeratorAndTemperatureControlledCabinetMode",
translation_key="mode",
),
entity_class=MatterModeSelectEntity,
required_attributes=(
clusters.RefrigeratorAndTemperatureControlledCabinetMode.Attributes.CurrentMode,
clusters.RefrigeratorAndTemperatureControlledCabinetMode.Attributes.SupportedModes,
),
# don't discover this entry if the supported modes list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="MatterRvcCleanMode",
translation_key="clean_mode",
),
entity_class=MatterModeSelectEntity,
required_attributes=(
clusters.RvcCleanMode.Attributes.CurrentMode,
clusters.RvcCleanMode.Attributes.SupportedModes,
),
# don't discover this entry if the supported modes list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="MatterDishwasherMode",
translation_key="mode",
),
entity_class=MatterModeSelectEntity,
required_attributes=(
clusters.DishwasherMode.Attributes.CurrentMode,
clusters.DishwasherMode.Attributes.SupportedModes,
),
# don't discover this entry if the supported modes list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="MatterEnergyEvseMode",
translation_key="mode",
),
entity_class=MatterModeSelectEntity,
required_attributes=(
clusters.EnergyEvseMode.Attributes.CurrentMode,
clusters.EnergyEvseMode.Attributes.SupportedModes,
),
# don't discover this entry if the supported modes list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="MatterDeviceEnergyManagementMode",
translation_key="device_energy_management_mode",
),
entity_class=MatterModeSelectEntity,
required_attributes=(
clusters.DeviceEnergyManagementMode.Attributes.CurrentMode,
clusters.DeviceEnergyManagementMode.Attributes.SupportedModes,
),
# don't discover this entry if the supported modes list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="MatterStartUpOnOff",
entity_category=EntityCategory.CONFIG,
translation_key="startup_on_off",
options=["on", "off", "toggle", "previous"],
device_to_ha={
0: "off",
1: "on",
2: "toggle",
None: "previous",
}.get,
ha_to_device={
"off": 0,
"on": 1,
"toggle": 2,
"previous": None,
}.get,
),
entity_class=MatterAttributeSelectEntity,
required_attributes=(clusters.OnOff.Attributes.StartUpOnOff,),
# allow None value for previous state
allow_none_value=True,
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="SmokeCOSmokeSensitivityLevel",
entity_category=EntityCategory.CONFIG,
translation_key="sensitivity_level",
options=["high", "standard", "low"],
device_to_ha={
0: "high",
1: "standard",
2: "low",
}.get,
ha_to_device={
"high": 0,
"standard": 1,
"low": 2,
}.get,
),
entity_class=MatterAttributeSelectEntity,
required_attributes=(clusters.SmokeCoAlarm.Attributes.SmokeSensitivityLevel,),
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="TrvTemperatureDisplayMode",
entity_category=EntityCategory.CONFIG,
translation_key="temperature_display_mode",
options=["Celsius", "Fahrenheit"],
device_to_ha={
0: "Celsius",
1: "Fahrenheit",
}.get,
ha_to_device={
"Celsius": 0,
"Fahrenheit": 1,
}.get,
),
entity_class=MatterAttributeSelectEntity,
required_attributes=(
clusters.ThermostatUserInterfaceConfiguration.Attributes.TemperatureDisplayMode,
),
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterListSelectEntityDescription(
key="TemperatureControlSelectedTemperatureLevel",
translation_key="temperature_level",
command=lambda selected_index: clusters.TemperatureControl.Commands.SetTemperature(
targetTemperatureLevel=selected_index
),
list_attribute=clusters.TemperatureControl.Attributes.SupportedTemperatureLevels,
),
entity_class=MatterListSelectEntity,
required_attributes=(
clusters.TemperatureControl.Attributes.SelectedTemperatureLevel,
clusters.TemperatureControl.Attributes.SupportedTemperatureLevels,
),
# don't discover this entry if the supported levels list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterListSelectEntityDescription(
key="LaundryWasherControlsSpinSpeed",
translation_key="laundry_washer_spin_speed",
list_attribute=clusters.LaundryWasherControls.Attributes.SpinSpeeds,
),
entity_class=MatterListSelectEntity,
required_attributes=(
clusters.LaundryWasherControls.Attributes.SpinSpeedCurrent,
clusters.LaundryWasherControls.Attributes.SpinSpeeds,
),
# don't discover this entry if the spinspeeds list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterMapSelectEntityDescription(
key="MatterLaundryWasherNumberOfRinses",
translation_key="laundry_washer_number_of_rinses",
list_attribute=clusters.LaundryWasherControls.Attributes.SupportedRinses,
device_to_ha=NUMBER_OF_RINSES_STATE_MAP.get,
ha_to_device=NUMBER_OF_RINSES_STATE_MAP_REVERSE.get,
),
entity_class=MatterMapSelectEntity,
required_attributes=(
clusters.LaundryWasherControls.Attributes.NumberOfRinses,
clusters.LaundryWasherControls.Attributes.SupportedRinses,
),
# don't discover this entry if the supported rinses list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterListSelectEntityDescription(
key="MicrowaveOvenControlSelectedWattIndex",
translation_key="power_level",
command=lambda selected_index: clusters.MicrowaveOvenControl.Commands.SetCookingParameters(
wattSettingIndex=selected_index
),
list_attribute=clusters.MicrowaveOvenControl.Attributes.SupportedWatts,
),
entity_class=MatterListSelectEntity,
required_attributes=(
clusters.MicrowaveOvenControl.Attributes.SelectedWattIndex,
clusters.MicrowaveOvenControl.Attributes.SupportedWatts,
),
# don't discover this entry if the supported state list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="DoorLockSoundVolume",
entity_category=EntityCategory.CONFIG,
translation_key="door_lock_sound_volume",
options=["silent", "low", "medium", "high"],
device_to_ha={
0: "silent",
1: "low",
3: "medium",
2: "high",
}.get,
ha_to_device={
"silent": 0,
"low": 1,
"medium": 3,
"high": 2,
}.get,
),
entity_class=MatterAttributeSelectEntity,
required_attributes=(clusters.DoorLock.Attributes.SoundVolume,),
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="PumpConfigurationAndControlOperationMode",
translation_key="pump_operation_mode",
options=list(PUMP_OPERATION_MODE_MAP.values()),
device_to_ha=PUMP_OPERATION_MODE_MAP.get,
ha_to_device=PUMP_OPERATION_MODE_MAP_REVERSE.get,
),
entity_class=MatterAttributeSelectEntity,
required_attributes=(
clusters.PumpConfigurationAndControl.Attributes.OperationMode,
),
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="AqaraBooleanStateConfigurationCurrentSensitivityLevel",
entity_category=EntityCategory.CONFIG,
translation_key="sensitivity_level",
options=["10 mm", "20 mm", "30 mm"],
device_to_ha={
0: "10 mm", # 10 mm => CurrentSensitivityLevel=0 / highest sensitivity level
1: "20 mm", # 20 mm => CurrentSensitivityLevel=1 / medium sensitivity level
2: "30 mm", # 30 mm => CurrentSensitivityLevel=2 / lowest sensitivity level
}.get,
ha_to_device={
"10 mm": 0,
"20 mm": 1,
"30 mm": 2,
}.get,
),
entity_class=MatterAttributeSelectEntity,
required_attributes=(
clusters.BooleanStateConfiguration.Attributes.CurrentSensitivityLevel,
),
vendor_id=(4447,),
product_id=(8194,),
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="AqaraOccupancySensorBooleanStateConfigurationCurrentSensitivityLevel",
entity_category=EntityCategory.CONFIG,
translation_key="sensitivity_level",
options=["low", "standard", "high"],
device_to_ha={
0: "low",
1: "standard",
2: "high",
}.get,
ha_to_device={
"low": 0,
"standard": 1,
"high": 2,
}.get,
),
entity_class=MatterAttributeSelectEntity,
required_attributes=(
clusters.BooleanStateConfiguration.Attributes.CurrentSensitivityLevel,
),
vendor_id=(4447,),
product_id=(
8197,
8195,
),
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="HeimanOccupancySensorBooleanStateConfigurationCurrentSensitivityLevel",
entity_category=EntityCategory.CONFIG,
translation_key="sensitivity_level",
options=["low", "standard", "high"],
device_to_ha={
0: "low",
1: "standard",
2: "high",
}.get,
ha_to_device={
"low": 0,
"standard": 1,
"high": 2,
}.get,
),
entity_class=MatterAttributeSelectEntity,
required_attributes=(
clusters.BooleanStateConfiguration.Attributes.CurrentSensitivityLevel,
),
vendor_id=(4619,),
product_id=(4097,),
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterMapSelectEntityDescription(
key="DoorLockOperatingMode",
translation_key="door_lock_operating_mode",
list_attribute=clusters.DoorLock.Attributes.SupportedOperatingModes,
device_to_ha=DOOR_LOCK_OPERATING_MODE_MAP.get,
ha_to_device=DOOR_LOCK_OPERATING_MODE_MAP_REVERSE.get,
),
entity_class=MatterDoorLockOperatingModeSelectEntity,
required_attributes=(
clusters.DoorLock.Attributes.OperatingMode,
clusters.DoorLock.Attributes.SupportedOperatingModes,
),
),
]