Fix TemperatureOffset native min/max unit conversion for Matter thermostats (#180172)

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Ludovic BOUÉ
2026-08-28 19:02:43 +02:00
committed by GitHub
co-authored by Claude Sonnet 5
parent 456582ed2d
commit 3cf056ddb2
3 changed files with 77 additions and 31 deletions
+16 -7
View File
@@ -308,7 +308,8 @@ DISCOVERY_SCHEMAS = [
),
featuremap_contains=(clusters.Thermostat.Bitmaps.Feature.kSetback),
),
# Eve temperature offset with higher min/max
# Eve temperature offset; shares the bounds of the generic schema below
# and will be merged into it under that schema's unique ID (follow-up PR)
MatterDiscoverySchema(
platform=Platform.NUMBER,
entity_description=MatterNumberEntityDescription(
@@ -316,9 +317,13 @@ DISCOVERY_SCHEMAS = [
device_class=NumberDeviceClass.TEMPERATURE,
entity_category=EntityCategory.CONFIG,
translation_key="temperature_offset",
native_max_value=50,
native_min_value=-50,
native_step=0.5,
# Matter 1.4 raises this to the SignedTemperature type's usable
# range (±127 in 0.1°C units; -128 is reserved). Matter 1.3 is
# limited to ±2.5°C; that will be enforced via cluster_revision
# filtering in a follow-up PR.
native_max_value=12.7,
native_min_value=-12.7,
native_step=0.1,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_to_ha=lambda x: None if x is None else x / 10,
ha_to_device=lambda x: round(x * 10),
@@ -337,9 +342,13 @@ DISCOVERY_SCHEMAS = [
device_class=NumberDeviceClass.TEMPERATURE,
entity_category=EntityCategory.CONFIG,
translation_key="temperature_offset",
native_max_value=25, # Matter 1.3 limit
native_min_value=-25, # Matter 1.3 limit
native_step=0.5,
# Matter 1.4 raises this to the SignedTemperature type's usable
# range (±127 in 0.1°C units; -128 is reserved). Matter 1.3 is
# limited to ±2.5°C; that will be enforced via cluster_revision
# filtering in a follow-up PR.
native_max_value=12.7,
native_min_value=-12.7,
native_step=0.1,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_to_ha=lambda x: None if x is None else x / 10,
ha_to_device=lambda x: round(x * 10),
@@ -422,10 +422,10 @@
]),
'area_id': None,
'capabilities': dict({
<NumberEntityCapabilityAttribute.MAX: 'max'>: 25,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -25,
<NumberEntityCapabilityAttribute.MAX: 'max'>: 12.7,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -12.7,
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.BOX: 'box'>,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.5,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -462,10 +462,10 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Floor Heating Thermostat Temperature offset',
<NumberEntityCapabilityAttribute.MAX: 'max'>: 25,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -25,
<NumberEntityCapabilityAttribute.MAX: 'max'>: 12.7,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -12.7,
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.BOX: 'box'>,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.5,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.1,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
@@ -720,10 +720,10 @@
]),
'area_id': None,
'capabilities': dict({
<NumberEntityCapabilityAttribute.MAX: 'max'>: 50,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -50,
<NumberEntityCapabilityAttribute.MAX: 'max'>: 12.7,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -12.7,
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.BOX: 'box'>,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.5,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -760,10 +760,10 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Eve Thermo 20EBP1701 Temperature offset',
<NumberEntityCapabilityAttribute.MAX: 'max'>: 50,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -50,
<NumberEntityCapabilityAttribute.MAX: 'max'>: 12.7,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -12.7,
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.BOX: 'box'>,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.5,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.1,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
@@ -781,10 +781,10 @@
]),
'area_id': None,
'capabilities': dict({
<NumberEntityCapabilityAttribute.MAX: 'max'>: 50,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -50,
<NumberEntityCapabilityAttribute.MAX: 'max'>: 12.7,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -12.7,
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.BOX: 'box'>,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.5,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -821,10 +821,10 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Eve Thermo 20ECD1701 Temperature offset',
<NumberEntityCapabilityAttribute.MAX: 'max'>: 50,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -50,
<NumberEntityCapabilityAttribute.MAX: 'max'>: 12.7,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -12.7,
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.BOX: 'box'>,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.5,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.1,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
@@ -4654,10 +4654,10 @@
]),
'area_id': None,
'capabilities': dict({
<NumberEntityCapabilityAttribute.MAX: 'max'>: 25,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -25,
<NumberEntityCapabilityAttribute.MAX: 'max'>: 12.7,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -12.7,
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.BOX: 'box'>,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.5,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -4694,10 +4694,10 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Thermostat Temperature offset',
<NumberEntityCapabilityAttribute.MAX: 'max'>: 25,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -25,
<NumberEntityCapabilityAttribute.MAX: 'max'>: 12.7,
<NumberEntityCapabilityAttribute.MIN: 'min'>: -12.7,
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.BOX: 'box'>,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.5,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.1,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
+37
View File
@@ -339,6 +339,43 @@ async def test_thermostat_occupied_setback(
)
@pytest.mark.parametrize("node_fixture", ["aqara_thermostat_w500"])
async def test_thermostat_temperature_offset(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test thermostat temperature offset number entity."""
entity_id = "number.floor_heating_thermostat_temperature_offset"
state = hass.states.get(entity_id)
assert state
assert state.state == "0.0"
# Setting value to 0.3 °C writes 3 to LocalTemperatureCalibration (scale x10),
# a value only reachable with the 0.1 °C step
await hass.services.async_call(
"number",
"set_value",
{
"entity_id": entity_id,
"value": 0.3,
},
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.Thermostat.Attributes.LocalTemperatureCalibration,
),
value=3,
)
@pytest.mark.parametrize("node_fixture", ["aqara_multi_state_p100"])
async def test_boolean_state_configuration_current_sensitivity_level(
hass: HomeAssistant,