mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 10:17:09 +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": {
|
"select": {
|
||||||
|
"laundry_washer_spin_speed": {
|
||||||
|
"default": "mdi:reload"
|
||||||
|
},
|
||||||
"temperature_level": {
|
"temperature_level": {
|
||||||
"default": "mdi:thermometer"
|
"default": "mdi:thermometer"
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,17 @@ from .entity import MatterEntity, MatterEntityDescription
|
|||||||
from .helpers import get_matter
|
from .helpers import get_matter
|
||||||
from .models import MatterDiscoverySchema
|
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 = (
|
type SelectCluster = (
|
||||||
clusters.ModeSelect
|
clusters.ModeSelect
|
||||||
| clusters.OvenMode
|
| clusters.OvenMode
|
||||||
@ -48,15 +59,27 @@ class MatterSelectEntityDescription(SelectEntityDescription, MatterEntityDescrip
|
|||||||
"""Describe Matter select entities."""
|
"""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)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class MatterListSelectEntityDescription(MatterSelectEntityDescription):
|
class MatterListSelectEntityDescription(MatterSelectEntityDescription):
|
||||||
"""Describe Matter select entities for MatterListSelectEntity."""
|
"""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: the attribute descriptor to get the list of values (= list of strings)
|
||||||
list_attribute: type[ClusterAttributeDescriptor]
|
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):
|
class MatterAttributeSelectEntity(MatterEntity, SelectEntity):
|
||||||
@ -84,6 +107,29 @@ class MatterAttributeSelectEntity(MatterEntity, SelectEntity):
|
|||||||
self._attr_current_option = value_convert(value)
|
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):
|
class MatterModeSelectEntity(MatterAttributeSelectEntity):
|
||||||
"""Representation of a select entity from Matter (Mode) Cluster attribute(s)."""
|
"""Representation of a select entity from Matter (Mode) Cluster attribute(s)."""
|
||||||
|
|
||||||
@ -125,9 +171,20 @@ class MatterListSelectEntity(MatterEntity, SelectEntity):
|
|||||||
async def async_select_option(self, option: str) -> None:
|
async def async_select_option(self, option: str) -> None:
|
||||||
"""Change the selected option."""
|
"""Change the selected option."""
|
||||||
option_id = self._attr_options.index(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(
|
await self.send_device_command(
|
||||||
self.entity_description.command(option_id),
|
self.entity_description.command(option_id),
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
# regular write attribute to set the new value
|
||||||
|
await self.write_attribute(
|
||||||
|
value=option_id,
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _update_from_device(self) -> None:
|
def _update_from_device(self) -> None:
|
||||||
@ -328,4 +385,32 @@ DISCOVERY_SCHEMAS = [
|
|||||||
clusters.TemperatureControl.Attributes.SupportedTemperatureLevels,
|
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": {
|
"temperature_display_mode": {
|
||||||
"name": "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": {
|
"sensor": {
|
||||||
|
@ -656,7 +656,7 @@
|
|||||||
"1/83/0": ["Off", "Low", "Medium", "High"],
|
"1/83/0": ["Off", "Low", "Medium", "High"],
|
||||||
"1/83/1": 0,
|
"1/83/1": 0,
|
||||||
"1/83/2": 0,
|
"1/83/2": 0,
|
||||||
"1/83/3": [1, 2],
|
"1/83/3": [0, 1],
|
||||||
"1/83/65532": 3,
|
"1/83/65532": 3,
|
||||||
"1/83/65533": 1,
|
"1/83/65533": 1,
|
||||||
"1/83/65528": [],
|
"1/83/65528": [],
|
||||||
|
@ -1620,6 +1620,120 @@
|
|||||||
'state': 'unknown',
|
'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]
|
# name: test_selects[silabs_laundrywasher][select.laundrywasher_temperature_level-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
|
@ -4,6 +4,7 @@ from unittest.mock import MagicMock, call
|
|||||||
|
|
||||||
from chip.clusters import Objects as clusters
|
from chip.clusters import Objects as clusters
|
||||||
from matter_server.client.models.node import MatterNode
|
from matter_server.client.models.node import MatterNode
|
||||||
|
from matter_server.common.helpers.util import create_attribute_path_from_attribute
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy import SnapshotAssertion
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
@ -144,3 +145,56 @@ async def test_list_select_entities(
|
|||||||
await trigger_subscription_callback(hass, matter_client)
|
await trigger_subscription_callback(hass, matter_client)
|
||||||
state = hass.states.get("select.laundrywasher_temperature_level")
|
state = hass.states.get("select.laundrywasher_temperature_level")
|
||||||
assert state.state == "unknown"
|
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