core/homeassistant/components/matter/select.py

439 lines
17 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
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()
}
type SelectCluster = (
clusters.ModeSelect
| clusters.OvenMode
| clusters.LaundryWasherMode
| clusters.RefrigeratorAndTemperatureControlledCabinetMode
| clusters.RvcRunMode
| clusters.RvcCleanMode
| clusters.DishwasherMode
| clusters.EnergyEvseMode
| clusters.DeviceEnergyManagementMode
)
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)
class MatterSelectEntityDescription(SelectEntityDescription, MatterEntityDescription):
"""Describe Matter select entities."""
@dataclass(frozen=True, kw_only=True)
class MatterMapSelectEntityDescription(MatterSelectEntityDescription):
"""Describe Matter select entities for MatterMapSelectEntityDescription."""
measurement_to_ha: Callable[[int], str | None]
ha_to_native_value: 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_native_value
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.measurement_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.measurement_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 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 = cast(
list[str],
self.get_matter_attribute_value(self.entity_description.list_attribute),
)
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"],
measurement_to_ha={
0: "off",
1: "on",
2: "toggle",
None: "previous",
}.get,
ha_to_native_value={
"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"],
measurement_to_ha={
0: "high",
1: "standard",
2: "low",
}.get,
ha_to_native_value={
"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"],
measurement_to_ha={
0: "Celsius",
1: "Fahrenheit",
}.get,
ha_to_native_value={
"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,
measurement_to_ha=NUMBER_OF_RINSES_STATE_MAP.get,
ha_to_native_value=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=[],
),
]