mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 17:27:10 +00:00
Add select platform discovery schemas for the Matter LaundryWasherControls cluster (#136261)
This commit is contained in:
parent
9a687e7f94
commit
32829596eb
@ -37,6 +37,9 @@
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"laundry_washer_spin_speed": {
|
||||
"default": "mdi:reload"
|
||||
},
|
||||
"temperature_level": {
|
||||
"default": "mdi:thermometer"
|
||||
}
|
||||
|
@ -20,6 +20,17 @@ 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
|
||||
@ -48,15 +59,27 @@ class MatterSelectEntityDescription(SelectEntityDescription, MatterEntityDescrip
|
||||
"""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."""
|
||||
|
||||
# command: a callback to create the command to send to the device
|
||||
# the callback's argument will be the index of the selected list value
|
||||
command: Callable[[int], ClusterCommand]
|
||||
# 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):
|
||||
@ -84,6 +107,29 @@ class MatterAttributeSelectEntity(MatterEntity, SelectEntity):
|
||||
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)."""
|
||||
|
||||
@ -125,8 +171,19 @@ class MatterListSelectEntity(MatterEntity, SelectEntity):
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
option_id = self._attr_options.index(option)
|
||||
await self.send_device_command(
|
||||
self.entity_description.command(option_id),
|
||||
|
||||
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
|
||||
@ -328,4 +385,32 @@ DISCOVERY_SCHEMAS = [
|
||||
clusters.TemperatureControl.Attributes.SupportedTemperatureLevels,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -205,6 +205,18 @@
|
||||
},
|
||||
"temperature_display_mode": {
|
||||
"name": "Temperature display mode"
|
||||
},
|
||||
"laundry_washer_number_of_rinses": {
|
||||
"name": "Number of rinses",
|
||||
"state": {
|
||||
"off": "[%key:common::state::off%]",
|
||||
"normal": "Normal",
|
||||
"extra": "Extra",
|
||||
"max": "Max"
|
||||
}
|
||||
},
|
||||
"laundry_washer_spin_speed": {
|
||||
"name": "Spin speed"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
@ -656,7 +656,7 @@
|
||||
"1/83/0": ["Off", "Low", "Medium", "High"],
|
||||
"1/83/1": 0,
|
||||
"1/83/2": 0,
|
||||
"1/83/3": [1, 2],
|
||||
"1/83/3": [0, 1],
|
||||
"1/83/65532": 3,
|
||||
"1/83/65533": 1,
|
||||
"1/83/65528": [],
|
||||
|
@ -1620,6 +1620,120 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[silabs_laundrywasher][select.laundrywasher_number_of_rinses-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'off',
|
||||
'normal',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.laundrywasher_number_of_rinses',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Number of rinses',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'laundry_washer_number_of_rinses',
|
||||
'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-MatterLaundryWasherNumberOfRinses-83-2',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[silabs_laundrywasher][select.laundrywasher_number_of_rinses-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'LaundryWasher Number of rinses',
|
||||
'options': list([
|
||||
'off',
|
||||
'normal',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.laundrywasher_number_of_rinses',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[silabs_laundrywasher][select.laundrywasher_spin_speed-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'Off',
|
||||
'Low',
|
||||
'Medium',
|
||||
'High',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.laundrywasher_spin_speed',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Spin speed',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'laundry_washer_spin_speed',
|
||||
'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-LaundryWasherControlsSpinSpeed-83-1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[silabs_laundrywasher][select.laundrywasher_spin_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'LaundryWasher Spin speed',
|
||||
'options': list([
|
||||
'Off',
|
||||
'Low',
|
||||
'Medium',
|
||||
'High',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.laundrywasher_spin_speed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Off',
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[silabs_laundrywasher][select.laundrywasher_temperature_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
@ -4,6 +4,7 @@ from unittest.mock import MagicMock, call
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
from matter_server.client.models.node import MatterNode
|
||||
from matter_server.common.helpers.util import create_attribute_path_from_attribute
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
@ -144,3 +145,56 @@ async def test_list_select_entities(
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("select.laundrywasher_temperature_level")
|
||||
assert state.state == "unknown"
|
||||
|
||||
# SpinSpeedCurrent
|
||||
matter_client.write_attribute.reset_mock()
|
||||
state = hass.states.get("select.laundrywasher_spin_speed")
|
||||
assert state
|
||||
assert state.state == "Off"
|
||||
assert state.attributes["options"] == ["Off", "Low", "Medium", "High"]
|
||||
set_node_attribute(matter_node, 1, 83, 1, 3)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("select.laundrywasher_spin_speed")
|
||||
assert state.state == "High"
|
||||
# test select option
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
"select_option",
|
||||
{
|
||||
"entity_id": "select.laundrywasher_spin_speed",
|
||||
"option": "High",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert matter_client.write_attribute.call_count == 1
|
||||
assert matter_client.write_attribute.call_args == call(
|
||||
node_id=matter_node.node_id,
|
||||
attribute_path=create_attribute_path_from_attribute(
|
||||
endpoint_id=1,
|
||||
attribute=clusters.LaundryWasherControls.Attributes.SpinSpeedCurrent,
|
||||
),
|
||||
value=3,
|
||||
)
|
||||
# test that an invalid value (e.g. 253) leads to an unknown state
|
||||
set_node_attribute(matter_node, 1, 83, 1, 253)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("select.laundrywasher_spin_speed")
|
||||
assert state.state == "unknown"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["silabs_laundrywasher"])
|
||||
async def test_map_select_entities(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test MatterMapSelectEntity entities are discovered and working from a laundrywasher fixture."""
|
||||
# NumberOfRinses
|
||||
state = hass.states.get("select.laundrywasher_number_of_rinses")
|
||||
assert state
|
||||
assert state.state == "off"
|
||||
assert state.attributes["options"] == ["off", "normal"]
|
||||
set_node_attribute(matter_node, 1, 83, 2, 1)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("select.laundrywasher_number_of_rinses")
|
||||
assert state.state == "normal"
|
||||
|
Loading…
x
Reference in New Issue
Block a user