Add relative humidity to matter climate entities (#152554)

OK after talking with Marcel.
This commit is contained in:
Björn Ebbinghaus
2025-10-02 20:44:19 +02:00
committed by GitHub
parent 571b2e3ab6
commit d2aa0573de
8 changed files with 490 additions and 0 deletions

View File

@@ -30,6 +30,7 @@ from .entity import MatterEntity
from .helpers import get_matter
from .models import MatterDiscoverySchema
HUMIDITY_SCALING_FACTOR = 100
TEMPERATURE_SCALING_FACTOR = 100
HVAC_SYSTEM_MODE_MAP = {
HVACMode.OFF: 0,
@@ -261,6 +262,18 @@ class MatterClimate(MatterEntity, ClimateEntity):
self._attr_current_temperature = self._get_temperature_in_degrees(
clusters.Thermostat.Attributes.LocalTemperature
)
self._attr_current_humidity = (
int(raw_measured_humidity) / HUMIDITY_SCALING_FACTOR
if (
raw_measured_humidity := self.get_matter_attribute_value(
clusters.RelativeHumidityMeasurement.Attributes.MeasuredValue
)
)
is not None
else None
)
if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False:
# special case: the appliance has a dedicated Power switch on the OnOff cluster
# if the mains power is off - treat it as if the HVAC mode is off
@@ -428,6 +441,7 @@ DISCOVERY_SCHEMAS = [
clusters.Thermostat.Attributes.TemperatureSetpointHold,
clusters.Thermostat.Attributes.UnoccupiedCoolingSetpoint,
clusters.Thermostat.Attributes.UnoccupiedHeatingSetpoint,
clusters.RelativeHumidityMeasurement.Attributes.MeasuredValue,
clusters.OnOff.Attributes.OnOff,
),
device_type=(device_types.Thermostat, device_types.RoomAirConditioner),

View File

@@ -351,6 +351,7 @@ DISCOVERY_SCHEMAS = [
required_attributes=(
clusters.RelativeHumidityMeasurement.Attributes.MeasuredValue,
),
allow_multi=True, # also used for climate entity
),
MatterDiscoverySchema(
platform=Platform.SENSOR,

View File

@@ -121,6 +121,7 @@ async def integration_fixture(
"smoke_detector",
"solar_power",
"switch_unit",
"tado_smart_radiator_thermostat_x",
"temperature_sensor",
"thermostat",
"vacuum_cleaner",

View File

@@ -0,0 +1,198 @@
{
"node_id": 12,
"date_commissioned": "2024-11-30T14:42:32.255793",
"last_interview": "2025-09-02T11:11:02.931246",
"interview_version": 6,
"available": true,
"is_bridge": false,
"attributes": {
"0/29/0": [
{
"0": 22,
"1": 1
}
],
"0/29/1": [29, 31, 40, 48, 49, 51, 60, 62, 63],
"0/29/2": [],
"0/29/3": [1],
"0/29/65532": 0,
"0/29/65533": 1,
"0/29/65528": [],
"0/29/65529": [],
"0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/31/0": [
{
"1": 5,
"2": 2,
"3": [112233],
"4": null,
"254": 4
}
],
"0/31/1": [],
"0/31/2": 4,
"0/31/3": 3,
"0/31/4": 4,
"0/31/65532": 0,
"0/31/65533": 1,
"0/31/65528": [],
"0/31/65529": [],
"0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
"0/40/0": 1,
"0/40/1": "tado\u00b0 GmbH",
"0/40/2": 4942,
"0/40/3": "Smart Radiator Thermostat X",
"0/40/4": 1,
"0/40/5": "",
"0/40/6": "**REDACTED**",
"0/40/7": 1,
"0/40/8": "VA04",
"0/40/9": 64,
"0/40/10": "1.0",
"0/40/18": "86A085E50D5A98E9",
"0/40/19": {
"0": 3,
"1": 3
},
"0/40/65532": 0,
"0/40/65533": 1,
"0/40/65528": [],
"0/40/65529": [],
"0/40/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 18, 19, 65528, 65529, 65531, 65532,
65533
],
"0/48/0": 0,
"0/48/1": {
"0": 60,
"1": 900
},
"0/48/2": 0,
"0/48/3": 0,
"0/48/4": true,
"0/48/65532": 0,
"0/48/65533": 1,
"0/48/65528": [1, 3, 5],
"0/48/65529": [0, 2, 4],
"0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
"0/49/0": 1,
"0/49/1": [
{
"0": "DghqP9mExis=",
"1": true
}
],
"0/49/2": 10,
"0/49/3": 20,
"0/49/4": true,
"0/49/5": 0,
"0/49/6": "DghqP9mExis=",
"0/49/7": null,
"0/49/65532": 2,
"0/49/65533": 1,
"0/49/65528": [1, 5, 7],
"0/49/65529": [0, 3, 4, 6, 8],
"0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533],
"0/51/0": [
{
"0": "ieee802154",
"1": true,
"2": null,
"3": null,
"4": "JgVorK4gwNo=",
"5": [],
"6": [
"/cSCg76PeeDU8k9/8VDoCg==",
"/oAAAAAAAAAkBWisriDA2g==",
"/YyzDI0GAAEI590S93bZ+g=="
],
"7": 4
}
],
"0/51/1": 23,
"0/51/2": 110,
"0/51/3": 6840,
"0/51/4": 1,
"0/51/8": false,
"0/51/65532": 0,
"0/51/65533": 1,
"0/51/65528": [],
"0/51/65529": [0],
"0/51/65531": [0, 1, 2, 3, 4, 8, 65528, 65529, 65531, 65532, 65533],
"0/60/0": 0,
"0/60/1": null,
"0/60/2": null,
"0/60/65532": 0,
"0/60/65533": 1,
"0/60/65528": [],
"0/60/65529": [0, 1, 2],
"0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533],
"0/62/0": [],
"0/62/1": [],
"0/62/2": 5,
"0/62/3": 5,
"0/62/4": [
"FTABAQAkAgE3AycU3mS65o4n65AmFdZw72wYJgQxwoAuJAUANwYnFN5kuuaOJ+uQJhXWcO9sGCQHASQIATAJQQQdNLSJLh6Ew+9dc42ZSEaQD2i1mavRjPh7ERTyLn8CmfJWgG9s4LZKLdh1Qu5gz5wiKQtzQwLmvjEVyMbO7YwDNwo1ASkBGCQCYDAEFG7exdou0CWA9KDmSWy1OVdhMBKHMAUUbt7F2i7QJYD0oOZJbLU5V2EwEocYMAtAF3IcZnJT290miGeEgwDYwxCO383N3BO+F5ESozS503RetTDlxunlA1cPDTKdyPRksfD14zu5erZ51aPKHxa2Qhg=",
"FTABAQAkAgE3AycUi1H2tJ00+fUkFQEYJgRfkd0uJAUANwYnFItR9rSdNPn1JBUBGCQHASQIATAJQQS9bdXZ/ocAnGmFJBkbm6+buMcdLgy3kQnyiIJ0gPArOweblS5eFfXnRSBWP7QcV7Nd7yiAUNncF+0kMrbpjEX+Nwo1ASkBGCQCYDAEFON8FiGqis2G9n3okV7J/BquBFbUMAUU43wWIaqKzYb2feiRXsn8Gq4EVtQYMAtAVYvBt/DVrSHJdjHZ7Spdtn3amDLOsTNzjsQcBOyESjCH43ZsgKQXmgqSXh+DS4qBNJm0eVo+Vn2gbhOlqubYMBg=",
"FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEUVnmOqdwGAsJNKvBP6t8dNPIV8vb+7vMEdmLTlDtli9YsaJCIhfOAGWRQROt8++O953j/fnjmO6BiAKctAnrxTcKNQEpARgkAmAwBBQrF7Zs6XmGG6lbxviD1v3sViKTrDAFFCsXtmzpeYYbqVvG+IPW/exWIpOsGDALQOe8gq02WhNZYr3kUdGqSKmcl1yFgBY80ebOduJb4lzLWgCq527c8xUZjxx4fFsP9A/K8GqHwQ3mZ2+5/riGunsY",
"FTABAQEkAgE3AyyEAlVTLAcGR29vZ2xlLAELTWF0dGVyIFJvb3QnFAEAAAD+////GCYEf9JDKSYFf5Rb5TcGLIQCVVMsBwZHb29nbGUsAQtNYXR0ZXIgUm9vdCcUAQAAAP7///8YJAcBJAgBMAlBBFs332VJwg3I1yKmuKy2YKinZM57r2xsIk9+6ENJaErX2An/ZQAz0VJ9zx+6rGqcOti0HtrJCfe1x2D9VCyJI3U3CjUBKQEkAgEYJAJgMAQUcsIB91cZE7NIygDKe0X0d0ZoyX4wBRRywgH3VxkTs0jKAMp7RfR3RmjJfhgwC0BlFksWat/xjBVhCozpG9cD6cH2d7cRzhM1BRUt8NoVERZ1rFWRzueGhRzdnv2tKWZ0vryyo6Mgm83nswnbVSxvGA==",
"FTABAQAkAgE3AyYU4K5SDiYVI+Px/RgmBFfPHS8kBQA3BiYU4K5SDiYVI+Px/RgkBwEkCAEwCUEE/TWWQD6IXIqrlp/p0JaU1cWtFS88ERh82o2TP6qME9opV5HUntiUCAhRLHnIWtYZ4pubaOWUFoIp61NEP7tuUDcKNQEpARgkAmAwBBQ6xz8FGl9kRhSgC0R+nqgacfJGiDAFFDrHPwUaX2RGFKALRH6eqBpx8kaIGDALQLo8R2G//5ZeXJcE5MQ3YbJ0AJl0Ik97fKD6i/Kx2aGK2oumz3pyAsWd4gVWQxShlFdhoBhv27/HxvP3C9U++k0Y"
],
"0/62/5": 4,
"0/62/65532": 0,
"0/62/65533": 1,
"0/62/65528": [1, 3, 5, 8],
"0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11],
"0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533],
"0/63/0": [],
"0/63/1": [],
"0/63/2": 4,
"0/63/3": 3,
"0/63/65532": 0,
"0/63/65533": 1,
"0/63/65528": [2, 5],
"0/63/65529": [0, 1, 3, 4],
"0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"1/3/0": 0,
"1/3/1": 4,
"1/3/65532": 0,
"1/3/65533": 4,
"1/3/65528": [],
"1/3/65529": [0, 64],
"1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/29/0": [
{
"0": 769,
"1": 2
}
],
"1/29/1": [3, 29, 513, 1029],
"1/29/2": [],
"1/29/3": [],
"1/29/65532": 0,
"1/29/65533": 1,
"1/29/65528": [],
"1/29/65529": [],
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"1/513/0": 2090,
"1/513/3": 500,
"1/513/4": 3000,
"1/513/18": 1800,
"1/513/27": 2,
"1/513/28": 0,
"1/513/65532": 1,
"1/513/65533": 5,
"1/513/65528": [],
"1/513/65529": [0],
"1/513/65531": [0, 3, 4, 18, 27, 28, 65528, 65529, 65531, 65532, 65533],
"1/1029/0": 7492,
"1/1029/1": 0,
"1/1029/2": 10000,
"1/1029/65532": 0,
"1/1029/65533": 3,
"1/1029/65528": [],
"1/1029/65529": [],
"1/1029/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533]
},
"attribute_subscriptions": []
}

View File

@@ -2282,6 +2282,55 @@
'state': 'unknown',
})
# ---
# name: test_buttons[tado_smart_radiator_thermostat_x][button.smart_radiator_thermostat_x_identify-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.smart_radiator_thermostat_x_identify',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[tado_smart_radiator_thermostat_x][button.smart_radiator_thermostat_x_identify-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Smart Radiator Thermostat X Identify',
}),
'context': <ANY>,
'entity_id': 'button.smart_radiator_thermostat_x_identify',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[temperature_sensor][button.mock_temperature_sensor_identify-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -199,6 +199,71 @@
'state': 'off',
})
# ---
# name: test_climates[tado_smart_radiator_thermostat_x][climate.smart_radiator_thermostat_x-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 30.0,
'min_temp': 5.0,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'climate',
'entity_category': None,
'entity_id': 'climate.smart_radiator_thermostat_x',
'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': None,
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <ClimateEntityFeature: 385>,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-MatterThermostat-513-0',
'unit_of_measurement': None,
})
# ---
# name: test_climates[tado_smart_radiator_thermostat_x][climate.smart_radiator_thermostat_x-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_humidity': 74.92,
'current_temperature': 20.9,
'friendly_name': 'Smart Radiator Thermostat X',
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 30.0,
'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 385>,
'temperature': 18.0,
}),
'context': <ANY>,
'entity_id': 'climate.smart_radiator_thermostat_x',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_climates[thermostat][climate.longan_link_hvac-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -6791,6 +6791,115 @@
'state': '234.899',
})
# ---
# name: test_sensors[tado_smart_radiator_thermostat_x][sensor.smart_radiator_thermostat_x_humidity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.smart_radiator_thermostat_x_humidity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
'original_icon': None,
'original_name': 'Humidity',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-HumiditySensor-1029-0',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[tado_smart_radiator_thermostat_x][sensor.smart_radiator_thermostat_x_humidity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'humidity',
'friendly_name': 'Smart Radiator Thermostat X Humidity',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.smart_radiator_thermostat_x_humidity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '74.92',
})
# ---
# name: test_sensors[tado_smart_radiator_thermostat_x][sensor.smart_radiator_thermostat_x_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.smart_radiator_thermostat_x_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Temperature',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-ThermostatLocalTemperature-513-0',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[tado_smart_radiator_thermostat_x][sensor.smart_radiator_thermostat_x_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Smart Radiator Thermostat X Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.smart_radiator_thermostat_x_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '20.9',
})
# ---
# name: test_sensors[temperature_sensor][sensor.mock_temperature_sensor_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -162,6 +162,59 @@ async def test_thermostat_base(
assert state.attributes["temperature"] == 20
@pytest.mark.parametrize("node_fixture", ["thermostat"])
async def test_thermostat_humidity(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test thermostat humidity attribute and state updates."""
# test entity attributes
state = hass.states.get("climate.longan_link_hvac")
assert state
measured_value = clusters.RelativeHumidityMeasurement.Attributes.MeasuredValue
# test current humidity update from device
set_node_attribute(
matter_node,
1,
measured_value.cluster_id,
measured_value.attribute_id,
1234,
)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("climate.longan_link_hvac")
assert state
assert state.attributes["current_humidity"] == 12.34
# test current humidity update from device with zero value
set_node_attribute(
matter_node,
1,
measured_value.cluster_id,
measured_value.attribute_id,
0,
)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("climate.longan_link_hvac")
assert state
assert state.attributes["current_humidity"] == 0.0
# test current humidity update from device with None value
set_node_attribute(
matter_node,
1,
measured_value.cluster_id,
measured_value.attribute_id,
None,
)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("climate.longan_link_hvac")
assert state
assert "current_humidity" not in state.attributes
@pytest.mark.parametrize("node_fixture", ["thermostat"])
async def test_thermostat_service_calls(
hass: HomeAssistant,