439 lines
17 KiB
Python
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=[],
|
|
),
|
|
]
|