mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 09:17:10 +00:00
Matter TemperatureControl (#145706)
* TemperatureControl * Add tests * Commands.SetTemperature * Update homeassistant/components/matter/number.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update number.py * Update number.py * Update number.py * Update homeassistant/components/matter/number.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Refactor MatterRangeNumber to streamline command handling in async_set_native_value * testing requested changes --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
b630fb0520
commit
e3ba1f34ca
@ -2,9 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
from chip.clusters.ClusterObjects import ClusterAttributeDescriptor, ClusterCommand
|
||||
from matter_server.common import custom_clusters
|
||||
|
||||
from homeassistant.components.number import (
|
||||
@ -44,6 +47,23 @@ class MatterNumberEntityDescription(NumberEntityDescription, MatterEntityDescrip
|
||||
"""Describe Matter Number Input entities."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MatterRangeNumberEntityDescription(
|
||||
NumberEntityDescription, MatterEntityDescription
|
||||
):
|
||||
"""Describe Matter Number Input entities with min and max values."""
|
||||
|
||||
ha_to_native_value: Callable[[Any], Any]
|
||||
|
||||
# attribute descriptors to get the min and max value
|
||||
min_attribute: type[ClusterAttributeDescriptor]
|
||||
max_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
|
||||
command: Callable[[int], ClusterCommand]
|
||||
|
||||
|
||||
class MatterNumber(MatterEntity, NumberEntity):
|
||||
"""Representation of a Matter Attribute as a Number entity."""
|
||||
|
||||
@ -67,6 +87,42 @@ class MatterNumber(MatterEntity, NumberEntity):
|
||||
self._attr_native_value = value
|
||||
|
||||
|
||||
class MatterRangeNumber(MatterEntity, NumberEntity):
|
||||
"""Representation of a Matter Attribute as a Number entity with min and max values."""
|
||||
|
||||
entity_description: MatterRangeNumberEntityDescription
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the current value."""
|
||||
send_value = self.entity_description.ha_to_native_value(value)
|
||||
# custom command defined to set the new value
|
||||
await self.send_device_command(
|
||||
self.entity_description.command(send_value),
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.measurement_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),
|
||||
)
|
||||
/ 100
|
||||
)
|
||||
self._attr_native_max_value = (
|
||||
cast(
|
||||
int,
|
||||
self.get_matter_attribute_value(self.entity_description.max_attribute),
|
||||
)
|
||||
/ 100
|
||||
)
|
||||
|
||||
|
||||
# Discovery schema(s) to map Matter Attributes to HA entities
|
||||
DISCOVERY_SCHEMAS = [
|
||||
MatterDiscoverySchema(
|
||||
@ -213,4 +269,27 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_class=MatterNumber,
|
||||
required_attributes=(clusters.DoorLock.Attributes.AutoRelockTime,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterRangeNumberEntityDescription(
|
||||
key="TemperatureControlTemperatureSetpoint",
|
||||
name=None,
|
||||
translation_key="temperature_setpoint",
|
||||
command=lambda value: clusters.TemperatureControl.Commands.SetTemperature(
|
||||
targetTemperature=value
|
||||
),
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
measurement_to_ha=lambda x: None if x is None else x / 100,
|
||||
ha_to_native_value=lambda x: round(x * 100),
|
||||
min_attribute=clusters.TemperatureControl.Attributes.MinTemperature,
|
||||
max_attribute=clusters.TemperatureControl.Attributes.MaxTemperature,
|
||||
mode=NumberMode.SLIDER,
|
||||
),
|
||||
entity_class=MatterRangeNumber,
|
||||
required_attributes=(
|
||||
clusters.TemperatureControl.Attributes.TemperatureSetpoint,
|
||||
clusters.TemperatureControl.Attributes.MinTemperature,
|
||||
clusters.TemperatureControl.Attributes.MaxTemperature,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -183,6 +183,9 @@
|
||||
"temperature_offset": {
|
||||
"name": "Temperature offset"
|
||||
},
|
||||
"temperature_setpoint": {
|
||||
"name": "Temperature setpoint"
|
||||
},
|
||||
"pir_occupied_to_unoccupied_delay": {
|
||||
"name": "Occupied to unoccupied delay"
|
||||
},
|
||||
|
@ -1846,6 +1846,64 @@
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[oven][number.mock_oven_temperature_setpoint-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 288.0,
|
||||
'min': 76.0,
|
||||
'mode': <NumberMode.SLIDER: 'slider'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'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.mock_oven_temperature_setpoint',
|
||||
'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': 'Temperature setpoint',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'temperature_setpoint',
|
||||
'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-2-TemperatureControlTemperatureSetpoint-86-0',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[oven][number.mock_oven_temperature_setpoint-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Oven Temperature setpoint',
|
||||
'max': 288.0,
|
||||
'min': 76.0,
|
||||
'mode': <NumberMode.SLIDER: 'slider'>,
|
||||
'step': 1.0,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.mock_oven_temperature_setpoint',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '76.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[pump][number.mock_pump_on_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@ -1903,3 +1961,177 @@
|
||||
'state': '0',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[silabs_laundrywasher][number.laundrywasher_temperature_setpoint-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 0.0,
|
||||
'min': 0.0,
|
||||
'mode': <NumberMode.SLIDER: 'slider'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'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.laundrywasher_temperature_setpoint',
|
||||
'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': 'Temperature setpoint',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'temperature_setpoint',
|
||||
'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-TemperatureControlTemperatureSetpoint-86-0',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[silabs_laundrywasher][number.laundrywasher_temperature_setpoint-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'LaundryWasher Temperature setpoint',
|
||||
'max': 0.0,
|
||||
'min': 0.0,
|
||||
'mode': <NumberMode.SLIDER: 'slider'>,
|
||||
'step': 1.0,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.laundrywasher_temperature_setpoint',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[silabs_refrigerator][number.refrigerator_temperature_setpoint_2-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': -15.0,
|
||||
'min': -18.0,
|
||||
'mode': <NumberMode.SLIDER: 'slider'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'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.refrigerator_temperature_setpoint_2',
|
||||
'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': 'Temperature setpoint (2)',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'temperature_setpoint',
|
||||
'unique_id': '00000000000004D2-000000000000003A-MatterNodeDevice-2-TemperatureControlTemperatureSetpoint-86-0',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[silabs_refrigerator][number.refrigerator_temperature_setpoint_2-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Refrigerator Temperature setpoint (2)',
|
||||
'max': -15.0,
|
||||
'min': -18.0,
|
||||
'mode': <NumberMode.SLIDER: 'slider'>,
|
||||
'step': 1.0,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.refrigerator_temperature_setpoint_2',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '-18.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[silabs_refrigerator][number.refrigerator_temperature_setpoint_3-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 4.0,
|
||||
'min': 1.0,
|
||||
'mode': <NumberMode.SLIDER: 'slider'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'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.refrigerator_temperature_setpoint_3',
|
||||
'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': 'Temperature setpoint (3)',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'temperature_setpoint',
|
||||
'unique_id': '00000000000004D2-000000000000003A-MatterNodeDevice-3-TemperatureControlTemperatureSetpoint-86-0',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[silabs_refrigerator][number.refrigerator_temperature_setpoint_3-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Refrigerator Temperature setpoint (3)',
|
||||
'max': 4.0,
|
||||
'min': 1.0,
|
||||
'mode': <NumberMode.SLIDER: 'slider'>,
|
||||
'step': 1.0,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.refrigerator_temperature_setpoint_3',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '4.0',
|
||||
})
|
||||
# ---
|
||||
|
@ -2,6 +2,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 import custom_clusters
|
||||
from matter_server.common.errors import MatterError
|
||||
@ -101,6 +102,44 @@ async def test_eve_weather_sensor_altitude(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["silabs_refrigerator"])
|
||||
async def test_temperature_control_temperature_setpoint(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test TemperatureSetpoint from TemperatureControl."""
|
||||
# TemperatureSetpoint
|
||||
state = hass.states.get("number.refrigerator_temperature_setpoint_2")
|
||||
assert state
|
||||
assert state.state == "-18.0"
|
||||
|
||||
set_node_attribute(matter_node, 2, 86, 0, -1600)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("number.refrigerator_temperature_setpoint_2")
|
||||
assert state
|
||||
assert state.state == "-16.0"
|
||||
|
||||
# test set value
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{
|
||||
"entity_id": "number.refrigerator_temperature_setpoint_2",
|
||||
"value": -17,
|
||||
},
|
||||
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=2,
|
||||
command=clusters.TemperatureControl.Commands.SetTemperature(
|
||||
targetTemperature=-1700
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["dimmable_light"])
|
||||
async def test_matter_exception_on_write_attribute(
|
||||
hass: HomeAssistant,
|
||||
|
Loading…
x
Reference in New Issue
Block a user