Matter MicrowaveOven device (#148219)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Ludovic BOUÉ 2025-07-28 11:33:03 +02:00 committed by GitHub
parent 05935bbc01
commit a68e722c92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 300 additions and 18 deletions

View File

@ -40,6 +40,9 @@
"laundry_washer_spin_speed": {
"default": "mdi:reload"
},
"power_level": {
"default": "mdi:power-settings"
},
"temperature_level": {
"default": "mdi:thermometer"
}
@ -115,6 +118,11 @@
"default": "mdi:pump"
}
},
"number": {
"cook_time": {
"default": "mdi:microwave"
}
},
"switch": {
"child_lock": {
"default": "mdi:lock",

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, cast
from typing import Any
from chip.clusters import Objects as clusters
from chip.clusters.ClusterObjects import ClusterAttributeDescriptor, ClusterCommand
@ -55,12 +55,16 @@ class MatterRangeNumberEntityDescription(
):
"""Describe Matter Number Input entities with min and max values."""
ha_to_device: Callable[[Any], Any]
ha_to_device: Callable[[Any], Any] = lambda x: x
# attribute descriptors to get the min and max value
min_attribute: type[ClusterAttributeDescriptor]
min_attribute: type[ClusterAttributeDescriptor] | None = None
max_attribute: type[ClusterAttributeDescriptor]
# Functions to format the min and max values for display or conversion
format_min_value: Callable[[float], float] = lambda x: x
format_max_value: Callable[[float], float] = lambda x: x
# 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
command: Callable[[int], ClusterCommand]
@ -105,24 +109,29 @@ class MatterRangeNumber(MatterEntity, NumberEntity):
@callback
def _update_from_device(self) -> None:
"""Update from device."""
# get the value from the primary attribute and convert it to the HA value if needed
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
if value_convert := self.entity_description.device_to_ha:
value = value_convert(value)
self._attr_native_value = value
self._attr_native_min_value = (
cast(
int,
self.get_matter_attribute_value(self.entity_description.min_attribute),
# min case 1: get min from the attribute and convert it
if self.entity_description.min_attribute:
min_value = self.get_matter_attribute_value(
self.entity_description.min_attribute
)
/ 100
)
self._attr_native_max_value = (
cast(
int,
self.get_matter_attribute_value(self.entity_description.max_attribute),
)
/ 100
min_convert = self.entity_description.format_min_value
self._attr_native_min_value = min_convert(min_value)
# min case 2: get the min from entity_description
elif self.entity_description.native_min_value is not None:
self._attr_native_min_value = self.entity_description.native_min_value
# get max from the attribute and convert it
max_value = self.get_matter_attribute_value(
self.entity_description.max_attribute
)
max_convert = self.entity_description.format_max_value
self._attr_native_max_value = max_convert(max_value)
class MatterLevelControlNumber(MatterEntity, NumberEntity):
@ -302,6 +311,27 @@ DISCOVERY_SCHEMAS = [
clusters.OccupancySensing.Attributes.PIROccupiedToUnoccupiedDelay,
),
),
MatterDiscoverySchema(
platform=Platform.NUMBER,
entity_description=MatterRangeNumberEntityDescription(
key="MicrowaveOvenControlCookTime",
translation_key="cook_time",
device_class=NumberDeviceClass.DURATION,
command=lambda value: clusters.MicrowaveOvenControl.Commands.SetCookingParameters(
cookTime=int(value)
),
native_min_value=1, # 1 second minimum cook time
native_step=1, # 1 second
native_unit_of_measurement=UnitOfTime.SECONDS,
max_attribute=clusters.MicrowaveOvenControl.Attributes.MaxCookTime,
mode=NumberMode.SLIDER,
),
entity_class=MatterRangeNumber,
required_attributes=(
clusters.MicrowaveOvenControl.Attributes.CookTime,
clusters.MicrowaveOvenControl.Attributes.MaxCookTime,
),
),
MatterDiscoverySchema(
platform=Platform.NUMBER,
entity_description=MatterNumberEntityDescription(
@ -328,6 +358,8 @@ DISCOVERY_SCHEMAS = [
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_to_ha=lambda x: None if x is None else x / 100,
ha_to_device=lambda x: round(x * 100),
format_min_value=lambda x: x / 100,
format_max_value=lambda x: x / 100,
min_attribute=clusters.TemperatureControl.Attributes.MinTemperature,
max_attribute=clusters.TemperatureControl.Attributes.MaxTemperature,
mode=NumberMode.SLIDER,

View File

@ -197,10 +197,14 @@ class MatterListSelectEntity(MatterEntity, SelectEntity):
@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),
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
@ -443,6 +447,24 @@ DISCOVERY_SCHEMAS = [
# 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(

View File

@ -180,6 +180,9 @@
"altitude": {
"name": "Altitude above sea level"
},
"cook_time": {
"name": "Cook time"
},
"pump_setpoint": {
"name": "Setpoint"
},
@ -222,6 +225,9 @@
"device_energy_management_mode": {
"name": "Energy management mode"
},
"power_level": {
"name": "Power level (W)"
},
"sensitivity_level": {
"name": "Sensitivity",
"state": {

View File

@ -397,6 +397,8 @@
"1/96/5": {
"0": 0
},
"1/96/6": [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000],
"1/96/7": 5,
"1/96/65532": 2,
"1/96/65533": 2,
"1/96/65528": [4],

View File

@ -693,6 +693,65 @@
'state': '255',
})
# ---
# name: test_numbers[microwave_oven][number.microwave_oven_cook_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 86400,
'min': 1,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.microwave_oven_cook_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Cook time',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'cook_time',
'unique_id': '00000000000004D2-000000000000009D-MatterNodeDevice-1-MicrowaveOvenControlCookTime-95-0',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_numbers[microwave_oven][number.microwave_oven_cook_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'Microwave Oven Cook time',
'max': 86400,
'min': 1,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'number.microwave_oven_cook_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '30',
})
# ---
# name: test_numbers[mounted_dimmable_load_control_fixture][number.mock_mounted_dimmable_load_control_on_level-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -981,6 +981,79 @@
'state': 'Low',
})
# ---
# name: test_selects[microwave_oven][select.microwave_oven_power_level_w-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'100',
'200',
'300',
'400',
'500',
'600',
'700',
'800',
'900',
'1000',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.microwave_oven_power_level_w',
'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': 'Power level (W)',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'power_level',
'unique_id': '00000000000004D2-000000000000009D-MatterNodeDevice-1-MicrowaveOvenControlSelectedWattIndex-95-7',
'unit_of_measurement': None,
})
# ---
# name: test_selects[microwave_oven][select.microwave_oven_power_level_w-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Microwave Oven Power level (W)',
'options': list([
'100',
'200',
'300',
'400',
'500',
'600',
'700',
'800',
'900',
'1000',
]),
}),
'context': <ANY>,
'entity_id': 'select.microwave_oven_power_level_w',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1000',
})
# ---
# name: test_selects[mounted_dimmable_load_control_fixture][select.mock_mounted_dimmable_load_control_power_on_behavior_on_startup-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -201,3 +201,36 @@ async def test_pump_level(
), # 75 * 2 = 150, as the value is multiplied by 2 in the HA to native value conversion
)
)
@pytest.mark.parametrize("node_fixture", ["microwave_oven"])
async def test_microwave_oven(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test Cooktime for microwave oven."""
# Cooktime on MicrowaveOvenControl cluster (1/96/2)
state = hass.states.get("number.microwave_oven_cook_time")
assert state
assert state.state == "30"
# test set value
await hass.services.async_call(
"number",
"set_value",
{
"entity_id": "number.microwave_oven_cook_time",
"value": 60, # 60 seconds
},
blocking=True,
)
assert matter_client.send_device_command.call_count == 1
assert matter_client.send_device_command.call_args == call(
node_id=matter_node.node_id,
endpoint_id=1,
command=clusters.MicrowaveOvenControl.Commands.SetCookingParameters(
cookTime=60, # 60 seconds
),
)

View File

@ -235,3 +235,50 @@ async def test_pump(
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("select.mock_pump_mode")
assert state.state == "local"
@pytest.mark.parametrize("node_fixture", ["microwave_oven"])
async def test_microwave_oven(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test ListSelect entity is discovered and working from a microwave oven fixture."""
# SupportedWatts from MicrowaveOvenControl cluster (1/96/6)
# SelectedWattIndex from MicrowaveOvenControl cluster (1/96/7)
matter_client.write_attribute.reset_mock()
state = hass.states.get("select.microwave_oven_power_level_w")
assert state
assert state.state == "1000"
assert state.attributes["options"] == [
"100",
"200",
"300",
"400",
"500",
"600",
"700",
"800",
"900",
"1000",
]
# test select option
await hass.services.async_call(
"select",
"select_option",
{
"entity_id": "select.microwave_oven_power_level_w",
"option": "900",
},
blocking=True,
)
assert matter_client.send_device_command.call_count == 1
assert matter_client.send_device_command.call_args == call(
node_id=matter_node.node_id,
endpoint_id=1,
command=clusters.MicrowaveOvenControl.Commands.SetCookingParameters(
wattSettingIndex=8
),
)