From 4adf5ce82641139b154b3689cf42363630942edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Fri, 25 Apr 2025 15:28:28 +0200 Subject: [PATCH] Support for Matter 1.4 Water Heater device type (#131505) * Create water_heater.json * Update water_heater.json * Update water_heater.json * TankVolume * TankPercentage * WaterHeaterMode WaterHeaterMode * Update sensor.py * ruff-format * Update water_heater.json Attributes of WaterHeaterManagement Cluster on Endpoint 2 ClusterId 148 (0x0094) * Update test_sensor.py water_heater fixture * Update test_sensor.py * SensorDeviceClass=VOLUME_STORAGE for `TankVolume` * `BoostStateEnum` map * WaterHeaterManagementBoostState * Update sensor.py * WaterHeaterManagementEstimatedHeatRequired * Fix UnitOfEnergy * Format * Add `device_types.WaterHeater` to Climate * Strings for Tank sensors * WaterHeater icons * Update icons.json * Update strings.json * Update water_heater.json * ruff-format * Fix tests * Fix sensor.py * Fix icons * WaterHeaterManagementEstimatedHeatRequired * WaterHeaterManagementBoostState * BoostState as a binary sensor * ElectricalPowerMeasurement values * Fix tests * Create water_heater.py * Update climate.py from dev branch * Resolve conflicts * ruff-format * Add Platform.WATER_HEATER * Update water_heater.py * Update water_heater.py * Update water_heater.py * Update water_heater.py * Add WaterHeaterManagement sensors * Update tests * Add select test * Add strings * First try with water_heater * Testing current_operation * BoostState attribute * target_temperature attributes * target_temperature attribute * set_temperature and set_operation_mode * turn_on / turn_off * Trigger Boost command * Fix WaterHeaterBoostInfoStruct * Add test file * Add climate cluster to fixture * Add climate cluster to fixture * Add tests * Add ON_OFF feature * Update tests * Update tests * Translate WaterHeaterMode * Change description * Update test and snapshots * Update snapshots * Set entity name to None to make the device name be the name of the entity * Format * Update water_heater.py * Fix format * ruff-format * Import ServiceValidationError * Update homeassistant/components/matter/water_heater.py Co-authored-by: Joost Lekkerkerker * Update water_heater.py * Update test_water_heater.py * Update test_water_heater.ambr * Update test_water_heater.py * Update select.py * Update snapshots * Rename to boost_info * Set WaterHeaterMode * Update snapshots * Update snapshots * Fix for warning W7431: Argument 3 should be of type AddConfigEntryEntitiesCallback in async_setup_entry (hass-argument-type) * Update strings.json * Update strings and tests * Fix missing brace * Update tests * fix test * Updates strings * Fix async_set_temperature * Update tests * Update tests * Update homeassistant/components/matter/water_heater.py Co-authored-by: Martin Hjelmare * Sort strings in strings.json * Update homeassistant/components/matter/water_heater.py Co-authored-by: Martin Hjelmare * Remove unused line * Remove min/max target temperatures * Remove BOOST_STATE_MAP * Add comment * Remove SUPPORT_FLAGS_HEATER * Remove system_mode_value check * Update homeassistant/components/matter/water_heater.py Co-authored-by: Martin Hjelmare * Reformat async_set_temperature() * Update snapshots * Remove MatterWaterHeaterMode selector * Update snapshots * Rename test to test_water_heater_set_temperature * Add test_water_heater_set_operation_mode * Remove reset_mock * Update tests/components/matter/test_water_heater.py Co-authored-by: Martin Hjelmare * Add test_update_from_water_heater * Add test_water_heater_turn_on_off * Add test_water_heater_boostmode * Fix SystemMode value for STATE_HIGH_DEMAND * Add disable boost from water heater device side test * Remove unused lines * Remove unused lines * Fix test indentation * Fix water heater tests * Check for None --------- Co-authored-by: Joost Lekkerkerker Co-authored-by: Martin Hjelmare --- .../components/matter/binary_sensor.py | 12 + homeassistant/components/matter/discovery.py | 2 + homeassistant/components/matter/icons.json | 6 + homeassistant/components/matter/select.py | 1 + homeassistant/components/matter/sensor.py | 48 +- homeassistant/components/matter/strings.json | 20 + .../components/matter/water_heater.py | 189 +++++++ tests/components/matter/conftest.py | 1 + .../fixtures/nodes/silabs_water_heater.json | 534 ++++++++++++++++++ .../matter/snapshots/test_binary_sensor.ambr | 47 ++ .../matter/snapshots/test_select.ambr | 62 ++ .../matter/snapshots/test_sensor.ambr | 335 +++++++++++ .../matter/snapshots/test_water_heater.ambr | 69 +++ tests/components/matter/test_binary_sensor.py | 20 + tests/components/matter/test_sensor.py | 44 ++ tests/components/matter/test_water_heater.py | 246 ++++++++ 16 files changed, 1635 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/matter/water_heater.py create mode 100644 tests/components/matter/fixtures/nodes/silabs_water_heater.json create mode 100644 tests/components/matter/snapshots/test_water_heater.ambr create mode 100644 tests/components/matter/test_water_heater.py diff --git a/homeassistant/components/matter/binary_sensor.py b/homeassistant/components/matter/binary_sensor.py index a55df58cac7..95375d5fc49 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -322,4 +322,16 @@ DISCOVERY_SCHEMAS = [ required_attributes=(clusters.EnergyEvse.Attributes.SupplyState,), allow_multi=True, # also used for sensor entity ), + MatterDiscoverySchema( + platform=Platform.BINARY_SENSOR, + entity_description=MatterBinarySensorEntityDescription( + key="WaterHeaterManagementBoostStateSensor", + translation_key="boost_state", + measurement_to_ha=lambda x: ( + x == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive + ), + ), + entity_class=MatterBinarySensor, + required_attributes=(clusters.WaterHeaterManagement.Attributes.BoostState,), + ), ] diff --git a/homeassistant/components/matter/discovery.py b/homeassistant/components/matter/discovery.py index 7102b693e45..8042b7505f4 100644 --- a/homeassistant/components/matter/discovery.py +++ b/homeassistant/components/matter/discovery.py @@ -27,6 +27,7 @@ from .switch import DISCOVERY_SCHEMAS as SWITCH_SCHEMAS from .update import DISCOVERY_SCHEMAS as UPDATE_SCHEMAS from .vacuum import DISCOVERY_SCHEMAS as VACUUM_SCHEMAS from .valve import DISCOVERY_SCHEMAS as VALVE_SCHEMAS +from .water_heater import DISCOVERY_SCHEMAS as WATER_HEATER_SCHEMAS DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = { Platform.BINARY_SENSOR: BINARY_SENSOR_SCHEMAS, @@ -44,6 +45,7 @@ DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = { Platform.UPDATE: UPDATE_SCHEMAS, Platform.VACUUM: VACUUM_SCHEMAS, Platform.VALVE: VALVE_SCHEMAS, + Platform.WATER_HEATER: WATER_HEATER_SCHEMAS, } SUPPORTED_PLATFORMS = tuple(DISCOVERY_SCHEMAS) diff --git a/homeassistant/components/matter/icons.json b/homeassistant/components/matter/icons.json index fed51708870..82e45e0383a 100644 --- a/homeassistant/components/matter/icons.json +++ b/homeassistant/components/matter/icons.json @@ -66,6 +66,12 @@ "operational_state": { "default": "mdi:play-pause" }, + "tank_volume": { + "default": "mdi:water-boiler" + }, + "tank_percentage": { + "default": "mdi:water-boiler" + }, "valve_position": { "default": "mdi:valve" }, diff --git a/homeassistant/components/matter/select.py b/homeassistant/components/matter/select.py index e78c34391cd..6e77be93705 100644 --- a/homeassistant/components/matter/select.py +++ b/homeassistant/components/matter/select.py @@ -41,6 +41,7 @@ type SelectCluster = ( | clusters.DishwasherMode | clusters.EnergyEvseMode | clusters.DeviceEnergyManagementMode + | clusters.WaterHeaterMode ) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 82d8ec1727c..f1704b45c50 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -37,6 +37,7 @@ from homeassistant.const import ( UnitOfPower, UnitOfPressure, UnitOfTemperature, + UnitOfVolume, UnitOfVolumeFlowRate, ) from homeassistant.core import HomeAssistant, callback @@ -65,7 +66,6 @@ CONTAMINATION_STATE_MAP = { clusters.SmokeCoAlarm.Enums.ContaminationStateEnum.kCritical: "critical", } - OPERATIONAL_STATE_MAP = { # enum with known Operation state values which we can translate clusters.OperationalState.Enums.OperationalStateEnum.kStopped: "stopped", @@ -77,6 +77,12 @@ OPERATIONAL_STATE_MAP = { clusters.RvcOperationalState.Enums.OperationalStateEnum.kDocked: "docked", } +BOOST_STATE_MAP = { + clusters.WaterHeaterManagement.Enums.BoostStateEnum.kInactive: "inactive", + clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive: "active", + clusters.WaterHeaterManagement.Enums.BoostStateEnum.kUnknownEnumValue: None, +} + EVSE_FAULT_STATE_MAP = { clusters.EnergyEvse.Enums.FaultStateEnum.kNoError: "no_error", clusters.EnergyEvse.Enums.FaultStateEnum.kMeterFailure: "meter_failure", @@ -996,4 +1002,44 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterSensor, required_attributes=(clusters.EnergyEvse.Attributes.UserMaximumChargeCurrent,), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="WaterHeaterManagementTankVolume", + translation_key="tank_volume", + device_class=SensorDeviceClass.VOLUME_STORAGE, + native_unit_of_measurement=UnitOfVolume.LITERS, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=(clusters.WaterHeaterManagement.Attributes.TankVolume,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="WaterHeaterManagementTankPercentage", + translation_key="tank_percentage", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=(clusters.WaterHeaterManagement.Attributes.TankPercentage,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="WaterHeaterManagementEstimatedHeatRequired", + translation_key="estimated_heat_required", + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfEnergy.MILLIWATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=3, + state_class=SensorStateClass.TOTAL, + ), + entity_class=MatterSensor, + required_attributes=( + clusters.WaterHeaterManagement.Attributes.EstimatedHeatRequired, + ), + ), ] diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index fedb026bf25..b8e8c63502c 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -85,6 +85,9 @@ }, "evse_supply_charging_state": { "name": "Supply charging state" + }, + "boost_state": { + "name": "Boost state" } }, "button": { @@ -229,6 +232,9 @@ }, "laundry_washer_spin_speed": { "name": "Spin speed" + }, + "water_heater_mode": { + "name": "Water heater mode" } }, "sensor": { @@ -279,6 +285,15 @@ "switch_current_position": { "name": "Current switch position" }, + "estimated_heat_required": { + "name": "Required heating energy" + }, + "tank_volume": { + "name": "Tank volume" + }, + "tank_percentage": { + "name": "Hot water level" + }, "valve_position": { "name": "Valve position" }, @@ -348,6 +363,11 @@ "valve": { "name": "[%key:component::valve::title%]" } + }, + "water_heater": { + "water_heater": { + "name": "[%key:component::water_heater::title%]" + } } }, "issues": { diff --git a/homeassistant/components/matter/water_heater.py b/homeassistant/components/matter/water_heater.py new file mode 100644 index 00000000000..07c011554fa --- /dev/null +++ b/homeassistant/components/matter/water_heater.py @@ -0,0 +1,189 @@ +"""Matter water heater platform.""" + +from __future__ import annotations + +from typing import Any, cast + +from chip.clusters import Objects as clusters +from matter_server.client.models import device_types +from matter_server.common.helpers.util import create_attribute_path_from_attribute + +from homeassistant.components.water_heater import ( + STATE_ECO, + STATE_HIGH_DEMAND, + STATE_OFF, + WaterHeaterEntity, + WaterHeaterEntityDescription, + WaterHeaterEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_WHOLE, + Platform, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .entity import MatterEntity +from .helpers import get_matter +from .models import MatterDiscoverySchema + +TEMPERATURE_SCALING_FACTOR = 100 + +# Map HA WH system mode to Matter ThermostatRunningMode attribute of the Thermostat cluster (Heat = 4) +WATER_HEATER_SYSTEM_MODE_MAP = { + STATE_ECO: 4, + STATE_HIGH_DEMAND: 4, + STATE_OFF: 0, +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Matter WaterHeater platform from Config Entry.""" + matter = get_matter(hass) + matter.register_platform_handler(Platform.WATER_HEATER, async_add_entities) + + +class MatterWaterHeater(MatterEntity, WaterHeaterEntity): + """Representation of a Matter WaterHeater entity.""" + + _attr_current_temperature: float | None = None + _attr_current_operation: str + _attr_operation_list = [ + STATE_ECO, + STATE_HIGH_DEMAND, + STATE_OFF, + ] + _attr_precision = PRECISION_WHOLE + _attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.ON_OFF + | WaterHeaterEntityFeature.OPERATION_MODE + ) + _attr_target_temperature: float | None = None + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _platform_translation_key = "water_heater" + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + target_temperature: float | None = kwargs.get(ATTR_TEMPERATURE) + if ( + target_temperature is not None + and self.target_temperature != target_temperature + ): + matter_attribute = clusters.Thermostat.Attributes.OccupiedHeatingSetpoint + await self.write_attribute( + value=round(target_temperature * TEMPERATURE_SCALING_FACTOR), + matter_attribute=matter_attribute, + ) + + async def async_set_operation_mode(self, operation_mode: str) -> None: + """Set new operation mode.""" + self._attr_current_operation = operation_mode + # Boost 1h (3600s) + boost_info: type[ + clusters.WaterHeaterManagement.Structs.WaterHeaterBoostInfoStruct + ] = clusters.WaterHeaterManagement.Structs.WaterHeaterBoostInfoStruct( + duration=3600 + ) + system_mode_value = WATER_HEATER_SYSTEM_MODE_MAP[operation_mode] + await self.write_attribute( + value=system_mode_value, + matter_attribute=clusters.Thermostat.Attributes.SystemMode, + ) + system_mode_path = create_attribute_path_from_attribute( + endpoint_id=self._endpoint.endpoint_id, + attribute=clusters.Thermostat.Attributes.SystemMode, + ) + self._endpoint.set_attribute_value(system_mode_path, system_mode_value) + self._update_from_device() + # Trigger Boost command + if operation_mode == STATE_HIGH_DEMAND: + await self.send_device_command( + clusters.WaterHeaterManagement.Commands.Boost(boostInfo=boost_info) + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on water heater.""" + await self.async_set_operation_mode("eco") + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off water heater.""" + await self.async_set_operation_mode("off") + + @callback + def _update_from_device(self) -> None: + """Update from device.""" + self._attr_current_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.LocalTemperature + ) + self._attr_target_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedHeatingSetpoint + ) + boost_state = self.get_matter_attribute_value( + clusters.WaterHeaterManagement.Attributes.BoostState + ) + if boost_state == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive: + self._attr_current_operation = STATE_HIGH_DEMAND + else: + self._attr_current_operation = STATE_ECO + self._attr_temperature = cast( + float, + self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedHeatingSetpoint + ), + ) + self._attr_min_temp = cast( + float, + self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.AbsMinHeatSetpointLimit + ), + ) + self._attr_max_temp = cast( + float, + self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit + ), + ) + + @callback + def _get_temperature_in_degrees( + self, attribute: type[clusters.ClusterAttributeDescriptor] + ) -> float | None: + """Return the scaled temperature value for the given attribute.""" + if (value := self.get_matter_attribute_value(attribute)) is not None: + return float(value) / TEMPERATURE_SCALING_FACTOR + return None + + +# Discovery schema(s) to map Matter Attributes to HA entities +DISCOVERY_SCHEMAS = [ + MatterDiscoverySchema( + platform=Platform.WATER_HEATER, + entity_description=WaterHeaterEntityDescription( + key="MatterWaterHeater", + name=None, + ), + entity_class=MatterWaterHeater, + required_attributes=( + clusters.Thermostat.Attributes.OccupiedHeatingSetpoint, + clusters.Thermostat.Attributes.AbsMinHeatSetpointLimit, + clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit, + clusters.Thermostat.Attributes.LocalTemperature, + clusters.WaterHeaterManagement.Attributes.FeatureMap, + ), + optional_attributes=( + clusters.WaterHeaterManagement.Attributes.HeaterTypes, + clusters.WaterHeaterManagement.Attributes.BoostState, + clusters.WaterHeaterManagement.Attributes.HeatDemand, + ), + device_type=(device_types.WaterHeater,), + allow_multi=True, # also used for sensor entity + ), +] diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index a085a1e3540..e180b9e9363 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -106,6 +106,7 @@ async def integration_fixture( "silabs_dishwasher", "silabs_evse_charging", "silabs_laundrywasher", + "silabs_water_heater", "smoke_detector", "switch_unit", "temperature_sensor", diff --git a/tests/components/matter/fixtures/nodes/silabs_water_heater.json b/tests/components/matter/fixtures/nodes/silabs_water_heater.json new file mode 100644 index 00000000000..7b764f3b3f1 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/silabs_water_heater.json @@ -0,0 +1,534 @@ +{ + "node_id": 25, + "date_commissioned": "2024-11-21T20:21:44.371473", + "last_interview": "2024-11-21T20:21:44.371503", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 43, 44, 45, 48, 49, 51, 60, 62, 63], + "0/29/2": [], + "0/29/3": [2], + "0/29/65532": 0, + "0/29/65533": 2, + "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": 3 + } + ], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 2, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 18, + "0/40/1": "Silabs", + "0/40/2": 65521, + "0/40/3": "Water Heater", + "0/40/4": 32773, + "0/40/5": "", + "0/40/6": "**REDACTED**", + "0/40/7": 0, + "0/40/8": "TEST_VERSION", + "0/40/9": 1, + "0/40/10": "v1.3-fix-energy-man-app-comp-2d92654525-dirty", + "0/40/15": "", + "0/40/18": "1868F000380F300B", + "0/40/19": { + "0": 3, + "1": 3 + }, + "0/40/21": 17039360, + "0/40/22": 1, + "0/40/65532": 0, + "0/40/65533": 4, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 18, 19, 21, 22, 65528, 65529, 65531, + 65532, 65533 + ], + "0/43/0": "", + "0/43/1": [ + "en-US", + "de-DE", + "fr-FR", + "en-GB", + "es-ES", + "zh-CN", + "it-IT", + "ja-JP" + ], + "0/43/65532": 0, + "0/43/65533": 1, + "0/43/65528": [], + "0/43/65529": [], + "0/43/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "0/44/0": 0, + "0/44/65532": 0, + "0/44/65533": 1, + "0/44/65528": [], + "0/44/65529": [], + "0/44/65531": [0, 65528, 65529, 65531, 65532, 65533], + "0/45/65532": 0, + "0/45/65533": 1, + "0/45/65528": [], + "0/45/65529": [], + "0/45/65531": [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": 2, + "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": "p0jbsOzJRNw=", + "1": true + } + ], + "0/49/4": true, + "0/49/5": 0, + "0/49/6": "p0jbsOzJRNw=", + "0/49/7": null, + "0/49/8": [0], + "0/49/65532": 2, + "0/49/65533": 2, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 3, 4, 6, 8], + "0/49/65531": [0, 1, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533], + "0/51/0": [ + { + "0": "MyHome", + "1": true, + "2": null, + "3": null, + "4": "0ln4A+M/qdU=", + "5": [], + "6": [ + "/QANuACgAAAAAAD//gCEAA==", + "/akBUIsgAADu+RflBK+awg==", + "/QANuACgAACOGElK6AMfiw==", + "/oAAAAAAAADQWfgD4z+p1Q==" + ], + "7": 4 + } + ], + "0/51/1": 2, + "0/51/2": 970, + "0/51/8": true, + "0/51/65532": 0, + "0/51/65533": 2, + "0/51/65528": [2], + "0/51/65529": [0, 1], + "0/51/65531": [0, 1, 2, 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, 2], + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/62/0": [ + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRLBgkBwEkCAEwCUEET/Kg7i1M+NQnTtjldQKCfg81STfZkuBWKlnUUolYjkKNUkOEGf/CAMckg3BH/vbbS8wbC17pWG8EvB7D6RSUfDcKNQEoARgkAgE2AwQCBAEYMAQUBAW4lb/V1fEJebN5Z4UTmE5XrEowBRRv4WHQKIysaFy3b/zkFJmrjWlt7hgwC0Cl0ZjooRQMxjnO0liVKSiIwY+sl0S34aMXNR/PAU89ZqTlHJocegee54S4ajdVZsj1LMV6YWQA3GNw61sC79aFGA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEERIK+dKrh7jNjamMZKV9Ir5gyKBMyce881JnXvjjdrJI3B3OjB6DbhqXvpgk96gZam85WxwGWrRlJEjVl2YQu6DcKNQEpARgkAmAwBBRv4WHQKIysaFy3b/zkFJmrjWlt7jAFFLsR9bzzqpjG9Z5aOkD8b8KMO7AQGDALQAK1q01Umn5ER39/84eai6HfZDKTNsGsuLyhIfpQa6XZQXenGbFDeenDLy8zv5NOLtwu8b44Zv0IrqONItfZqOMY", + "254": 3 + } + ], + "0/62/1": [ + { + "1": "BNI+NL43G+mbJrQUfyNKwd2SHwAPJT3lgk8Ru5z0mzaXqXtfF8C4nYRSBypr7WVg2dx5dzDPTQQfiwGZQhav3nY=", + "2": 4939, + "3": 2, + "4": 44, + "5": "HA_test", + "254": 3 + } + ], + "0/62/2": 5, + "0/62/3": 3, + "0/62/4": [ + "FTABAQAkAgE3AyYUyakYCSYVj6gLsxgmBP2G+CskBQA3BiYUyakYCSYVj6gLsxgkBwEkCAEwCUEEgYwxrTB+tyiEGfrRwjlXTG34MiQtJXbg5Qqd0ohdRW7MfwYY7vZiX/0h9hI8MqUralFaVPcnghAP0MSJm1YrqTcKNQEpARgkAmAwBBS3BS9aJzt+p6i28Nj+trB2Uu+vdzAFFLcFL1onO36nqLbw2P62sHZS7693GDALQIrLt7Uq3S9HEe7apdzYSR+j3BLWNXSTLWD4YbrdyYLpm6xqHDV/NPARcIp4skZdtz91WwFBDfuS4jO5aVoER1sY", + "FTABAQAkAgE3AycUQhmZbaIbYjokFQIYJgRWZLcqJAUANwYnFEIZmW2iG2I6JBUCGCQHASQIATAJQQT2AlKGW/kOMjqayzeO0md523/fuhrhGEUU91uQpTiKo0I7wcPpKnmrwfQNPX6g0kEQl+VGaXa3e22lzfu5Tzp0Nwo1ASkBGCQCYDAEFOOMk13ScMKuT2hlaydi1yEJnhTqMAUU44yTXdJwwq5PaGVrJ2LXIQmeFOoYMAtAv2jJd1qd5miXbYesH1XrJ+vgyY0hzGuZ78N6Jw4Cb1oN1sLSpA+PNM0u7+hsEqcSvvn2eSV8EaRR+hg5YQjHDxg=", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEE0j40vjcb6ZsmtBR/I0rB3ZIfAA8lPeWCTxG7nPSbNpepe18XwLidhFIHKmvtZWDZ3Hl3MM9NBB+LAZlCFq/edjcKNQEpARgkAmAwBBS7EfW886qYxvWeWjpA/G/CjDuwEDAFFLsR9bzzqpjG9Z5aOkD8b8KMO7AQGDALQIgQgt5asUGXO0ZyTWWKdjAmBSoJAzRMuD4Z+tQYZanQ3s0OItL07MU2In6uyXhjNBfjJlRqon780lhjTsm2Y+8Y" + ], + "0/62/5": 3, + "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": 2, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "2/3/0": 0, + "2/3/1": 0, + "2/3/65532": 0, + "2/3/65533": 5, + "2/3/65528": [], + "2/3/65529": [0], + "2/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "2/29/0": [ + { + "0": 1293, + "1": 1 + }, + { + "0": 1296, + "1": 1 + }, + { + "0": 1295, + "1": 1 + } + ], + "2/29/1": [3, 29, 144, 145, 148, 152, 156, 158, 159], + "2/29/2": [], + "2/29/3": [], + "2/29/65532": 0, + "2/29/65533": 2, + "2/29/65528": [], + "2/29/65529": [], + "2/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "2/144/0": 0, + "2/144/1": 3, + "2/144/2": [ + { + "0": 5, + "1": true, + "2": -50000000, + "3": 50000000, + "4": [ + { + "0": -50000000, + "1": -10000000, + "2": 5000, + "3": 2000, + "4": 3000 + }, + { + "0": -9999999, + "1": 9999999, + "2": 1000, + "3": 100, + "4": 500 + }, + { + "0": 10000000, + "1": 50000000, + "2": 5000, + "3": 2000, + "4": 3000 + } + ] + }, + { + "0": 2, + "1": true, + "2": -100000, + "3": 100000, + "4": [ + { + "0": -100000, + "1": -5000, + "2": 5000, + "3": 2000, + "4": 3000 + }, + { + "0": -4999, + "1": 4999, + "2": 1000, + "3": 100, + "4": 500 + }, + { + "0": 5000, + "1": 100000, + "2": 5000, + "3": 2000, + "4": 3000 + } + ] + }, + { + "0": 1, + "1": true, + "2": -500000, + "3": 500000, + "4": [ + { + "0": -500000, + "1": -100000, + "2": 5000, + "3": 2000, + "4": 3000 + }, + { + "0": -99999, + "1": 99999, + "2": 1000, + "3": 100, + "4": 500 + }, + { + "0": 100000, + "1": 500000, + "2": 5000, + "3": 2000, + "4": 3000 + } + ] + } + ], + "2/144/3": [], + "2/144/4": 230000, + "2/144/5": 100, + "2/144/6": null, + "2/144/7": null, + "2/144/8": 23000, + "2/144/9": null, + "2/144/10": null, + "2/144/11": null, + "2/144/12": null, + "2/144/13": null, + "2/144/14": 50, + "2/144/15": [ + { + "0": 1, + "1": 100000 + } + ], + "2/144/16": [ + { + "0": 1, + "1": 100000 + } + ], + "2/144/17": null, + "2/144/18": null, + "2/144/65532": 31, + "2/144/65533": 1, + "2/144/65528": [], + "2/144/65529": [], + "2/144/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 65528, + 65529, 65531, 65532, 65533 + ], + "2/145/0": { + "0": 14, + "1": true, + "2": 0, + "3": 1000000000000000, + "4": [ + { + "0": 0, + "1": 0 + } + ] + }, + "2/145/1": null, + "2/145/2": null, + "2/145/3": null, + "2/145/4": null, + "2/145/5": { + "0": 0, + "1": 0, + "2": 0, + "3": 0 + }, + "2/145/65532": 15, + "2/145/65533": 1, + "2/145/65528": [], + "2/145/65529": [], + "2/145/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "2/148/0": 1, + "2/148/1": 0, + "2/148/2": 200, + "2/148/3": 4000000, + "2/148/4": 40, + "2/148/5": 0, + "2/148/65532": 3, + "2/148/65533": 2, + "2/148/65528": [], + "2/148/65529": [0, 1], + "2/148/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "2/152/0": 2, + "2/152/1": false, + "2/152/2": 1, + "2/152/3": 1200000, + "2/152/4": 7600000, + "2/152/5": null, + "2/152/6": null, + "2/152/7": 0, + "2/152/65532": 123, + "2/152/65533": 4, + "2/152/65528": [], + "2/152/65529": [0, 1, 2, 3, 4, 5, 6, 7], + "2/152/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533], + "2/156/65532": 1, + "2/156/65533": 1, + "2/156/65528": [], + "2/156/65529": [], + "2/156/65531": [65528, 65529, 65531, 65532, 65533], + "2/158/0": [ + { + "0": "Off", + "1": 0, + "2": [ + { + "1": 16384 + } + ] + }, + { + "0": "Manual", + "1": 1, + "2": [ + { + "1": 16385 + } + ] + }, + { + "0": "Timed", + "1": 2, + "2": [ + { + "1": 16386 + } + ] + } + ], + "2/158/1": 1, + "2/158/65532": 0, + "2/158/65533": 1, + "2/158/65528": [1], + "2/158/65529": [0], + "2/158/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "2/159/0": [ + { + "0": "No energy management (forecast only)", + "1": 0, + "2": [ + { + "1": 16384 + } + ] + }, + { + "0": "Device optimizes (no local or grid control)", + "1": 1, + "2": [ + { + "1": 16385 + } + ] + }, + { + "0": "Optimized within building", + "1": 2, + "2": [ + { + "1": 16386 + }, + { + "1": 16385 + } + ] + }, + { + "0": "Optimized for grid", + "1": 3, + "2": [ + { + "1": 16385 + }, + { + "1": 16387 + } + ] + }, + { + "0": "Optimized for grid and building", + "1": 4, + "2": [ + { + "1": 16386 + }, + { + "1": 16385 + }, + { + "1": 16387 + } + ] + } + ], + "2/159/1": 0, + "2/159/65532": 0, + "2/159/65533": 2, + "2/159/65528": [1], + "2/159/65529": [0], + "2/159/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "2/513/0": 5000, + "2/513/3": 4000, + "2/513/4": 6500, + "2/513/18": 6500, + "2/513/21": 4000, + "2/513/22": 6500, + "2/513/27": 2, + "2/513/28": 4, + "2/513/65532": 1, + "2/513/65533": 7, + "2/513/65528": [], + "2/513/65529": [0], + "2/513/65531": [0, 27, 28, 65528, 65529, 65531, 65532, 65533] + }, + "2/513/0": 5000, + "2/513/3": 4000, + "2/513/4": 6500, + "2/513/18": 6500, + "2/513/21": 4000, + "2/513/22": 6500, + "2/513/27": 2, + "2/513/28": 4, + "2/513/65532": 1, + "2/513/65533": 7, + "2/513/65528": [], + "2/513/65529": [0], + "2/513/65531": [0, 27, 28, 65528, 65529, 65531, 65532, 65533], + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/snapshots/test_binary_sensor.ambr b/tests/components/matter/snapshots/test_binary_sensor.ambr index ec5317ba808..feca62ffa31 100644 --- a/tests/components/matter/snapshots/test_binary_sensor.ambr +++ b/tests/components/matter/snapshots/test_binary_sensor.ambr @@ -527,6 +527,53 @@ 'state': 'on', }) # --- +# name: test_binary_sensors[silabs_water_heater][binary_sensor.water_heater_boost_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.water_heater_boost_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Boost state', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'boost_state', + 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-WaterHeaterManagementBoostStateSensor-148-5', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[silabs_water_heater][binary_sensor.water_heater_boost_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Water Heater Boost state', + }), + 'context': , + 'entity_id': 'binary_sensor.water_heater_boost_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_binary_sensors[smoke_detector][binary_sensor.smoke_sensor_battery_alert-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_select.ambr b/tests/components/matter/snapshots/test_select.ambr index 8ad579214d0..5222dda1ab5 100644 --- a/tests/components/matter/snapshots/test_select.ambr +++ b/tests/components/matter/snapshots/test_select.ambr @@ -1839,6 +1839,68 @@ 'state': 'Colors', }) # --- +# name: test_selects[silabs_water_heater][select.water_heater_energy_management_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'No energy management (forecast only)', + 'Device optimizes (no local or grid control)', + 'Optimized within building', + 'Optimized for grid', + 'Optimized for grid and building', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.water_heater_energy_management_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Energy management mode', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_energy_management_mode', + 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-MatterDeviceEnergyManagementMode-159-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[silabs_water_heater][select.water_heater_energy_management_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Water Heater Energy management mode', + 'options': list([ + 'No energy management (forecast only)', + 'Device optimizes (no local or grid control)', + 'Optimized within building', + 'Optimized for grid', + 'Optimized for grid and building', + ]), + }), + 'context': , + 'entity_id': 'select.water_heater_energy_management_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'No energy management (forecast only)', + }) +# --- # name: test_selects[switch_unit][select.mock_switchunit_power_on_behavior_on_startup-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index b3395551d74..2c6ef8ad51b 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -3535,6 +3535,341 @@ 'state': '120.0', }) # --- +# name: test_sensors[silabs_water_heater][sensor.water_heater_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.water_heater_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-ElectricalPowerMeasurementActiveCurrent-144-5', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[silabs_water_heater][sensor.water_heater_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Water Heater Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.water_heater_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.1', + }) +# --- +# name: test_sensors[silabs_water_heater][sensor.water_heater_hot_water_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.water_heater_hot_water_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hot water level', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tank_percentage', + 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-WaterHeaterManagementTankPercentage-148-4', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[silabs_water_heater][sensor.water_heater_hot_water_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Water Heater Hot water level', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.water_heater_hot_water_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_sensors[silabs_water_heater][sensor.water_heater_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.water_heater_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-ElectricalPowerMeasurementWatt-144-8', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[silabs_water_heater][sensor.water_heater_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Water Heater Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.water_heater_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.0', + }) +# --- +# name: test_sensors[silabs_water_heater][sensor.water_heater_required_heating_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.water_heater_required_heating_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Required heating energy', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'estimated_heat_required', + 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-WaterHeaterManagementEstimatedHeatRequired-148-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[silabs_water_heater][sensor.water_heater_required_heating_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Water Heater Required heating energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.water_heater_required_heating_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.0', + }) +# --- +# name: test_sensors[silabs_water_heater][sensor.water_heater_tank_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.water_heater_tank_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tank volume', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tank_volume', + 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-WaterHeaterManagementTankVolume-148-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[silabs_water_heater][sensor.water_heater_tank_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_storage', + 'friendly_name': 'Water Heater Tank volume', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.water_heater_tank_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '200', + }) +# --- +# name: test_sensors[silabs_water_heater][sensor.water_heater_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.water_heater_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-ElectricalPowerMeasurementVoltage-144-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[silabs_water_heater][sensor.water_heater_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Water Heater Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.water_heater_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '230.0', + }) +# --- # name: test_sensors[smoke_detector][sensor.smoke_sensor_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_water_heater.ambr b/tests/components/matter/snapshots/test_water_heater.ambr new file mode 100644 index 00000000000..fcf9a7665fd --- /dev/null +++ b/tests/components/matter/snapshots/test_water_heater.ambr @@ -0,0 +1,69 @@ +# serializer version: 1 +# name: test_water_heaters[silabs_water_heater][water_heater.water_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 65, + 'min_temp': 40, + 'operation_list': list([ + 'eco', + 'high_demand', + 'off', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.water_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-MatterWaterHeater-513-18', + 'unit_of_measurement': None, + }) +# --- +# name: test_water_heaters[silabs_water_heater][water_heater.water_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 50, + 'friendly_name': 'Water Heater', + 'max_temp': 65, + 'min_temp': 40, + 'operation_list': list([ + 'eco', + 'high_demand', + 'off', + ]), + 'operation_mode': 'eco', + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'temperature': 65, + }), + 'context': , + 'entity_id': 'water_heater.water_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'eco', + }) +# --- diff --git a/tests/components/matter/test_binary_sensor.py b/tests/components/matter/test_binary_sensor.py index acd150d9131..c20c5cb7f29 100644 --- a/tests/components/matter/test_binary_sensor.py +++ b/tests/components/matter/test_binary_sensor.py @@ -197,3 +197,23 @@ async def test_evse_sensor( state = hass.states.get(entity_id) assert state assert state.state == "off" + + +@pytest.mark.parametrize("node_fixture", ["silabs_water_heater"]) +async def test_water_heater( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test water heater sensor.""" + # BoostState + state = hass.states.get("binary_sensor.water_heater_boost_state") + assert state + assert state.state == "off" + + set_node_attribute(matter_node, 2, 148, 5, 1) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("binary_sensor.water_heater_boost_state") + assert state + assert state.state == "on" diff --git a/tests/components/matter/test_sensor.py b/tests/components/matter/test_sensor.py index bcdb573b3c8..03ffa31125e 100644 --- a/tests/components/matter/test_sensor.py +++ b/tests/components/matter/test_sensor.py @@ -467,3 +467,47 @@ async def test_evse_sensor( state = hass.states.get("sensor.evse_user_max_charge_current") assert state assert state.state == "63.0" + + +@pytest.mark.parametrize("node_fixture", ["silabs_water_heater"]) +async def test_water_heater( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test water heater sensor.""" + # TankVolume + state = hass.states.get("sensor.water_heater_tank_volume") + assert state + assert state.state == "200" + + set_node_attribute(matter_node, 2, 148, 2, 100) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.water_heater_tank_volume") + assert state + assert state.state == "100" + + # EstimatedHeatRequired + state = hass.states.get("sensor.water_heater_required_heating_energy") + assert state + assert state.state == "4.0" + + set_node_attribute(matter_node, 2, 148, 3, 1000000) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.water_heater_required_heating_energy") + assert state + assert state.state == "1.0" + + # TankPercentage + state = hass.states.get("sensor.water_heater_hot_water_level") + assert state + assert state.state == "40" + + set_node_attribute(matter_node, 2, 148, 4, 50) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.water_heater_hot_water_level") + assert state + assert state.state == "50" diff --git a/tests/components/matter/test_water_heater.py b/tests/components/matter/test_water_heater.py new file mode 100644 index 00000000000..eb2ea9eb40e --- /dev/null +++ b/tests/components/matter/test_water_heater.py @@ -0,0 +1,246 @@ +"""Test Matter sensors.""" + +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.helpers.util import create_attribute_path_from_attribute +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.water_heater import ( + STATE_ECO, + STATE_HIGH_DEMAND, + STATE_OFF, + WaterHeaterEntityFeature, +) +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .common import ( + set_node_attribute, + snapshot_matter_entities, + trigger_subscription_callback, +) + + +@pytest.mark.usefixtures("matter_devices") +async def test_water_heaters( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test water heaters.""" + snapshot_matter_entities(hass, entity_registry, snapshot, Platform.WATER_HEATER) + + +@pytest.mark.parametrize("node_fixture", ["silabs_water_heater"]) +async def test_water_heater( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test water heater entity.""" + state = hass.states.get("water_heater.water_heater") + assert state + assert state.attributes["min_temp"] == 40 + assert state.attributes["max_temp"] == 65 + assert state.attributes["temperature"] == 65 + assert state.attributes["operation_list"] == ["eco", "high_demand", "off"] + assert state.state == STATE_ECO + + # test supported features correctly parsed + mask = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.ON_OFF + | WaterHeaterEntityFeature.OPERATION_MODE + ) + assert state.attributes["supported_features"] & mask == mask + + +@pytest.mark.parametrize("node_fixture", ["silabs_water_heater"]) +async def test_water_heater_set_temperature( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test water_heater set temperature service.""" + # test single-setpoint temperature adjustment when eco mode is active + state = hass.states.get("water_heater.water_heater") + + assert state + assert state.state == STATE_ECO + await hass.services.async_call( + "water_heater", + "set_temperature", + { + "entity_id": "water_heater.water_heater", + "temperature": 52, + }, + 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="2/513/18", + value=5200, + ) + matter_client.write_attribute.reset_mock() + + +@pytest.mark.parametrize("node_fixture", ["silabs_water_heater"]) +@pytest.mark.parametrize( + ("operation_mode", "matter_attribute_value"), + [(STATE_OFF, 0), (STATE_ECO, 4), (STATE_HIGH_DEMAND, 4)], +) +async def test_water_heater_set_operation_mode( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, + operation_mode: str, + matter_attribute_value: int, +) -> None: + """Test water_heater set operation mode service.""" + state = hass.states.get("water_heater.water_heater") + assert state + + # test change mode to each operation_mode + await hass.services.async_call( + "water_heater", + "set_operation_mode", + { + "entity_id": "water_heater.water_heater", + "operation_mode": operation_mode, + }, + 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=2, + attribute=clusters.Thermostat.Attributes.SystemMode, + ), + value=matter_attribute_value, + ) + + +@pytest.mark.parametrize("node_fixture", ["silabs_water_heater"]) +async def test_water_heater_boostmode( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test water_heater set operation mode service.""" + # Boost 1h (3600s) + boost_info: type[ + clusters.WaterHeaterManagement.Structs.WaterHeaterBoostInfoStruct + ] = clusters.WaterHeaterManagement.Structs.WaterHeaterBoostInfoStruct(duration=3600) + state = hass.states.get("water_heater.water_heater") + assert state + + # enable water_heater boostmode + await hass.services.async_call( + "water_heater", + "set_operation_mode", + { + "entity_id": "water_heater.water_heater", + "operation_mode": STATE_HIGH_DEMAND, + }, + 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=2, + attribute=clusters.Thermostat.Attributes.SystemMode, + ), + value=4, + ) + 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.WaterHeaterManagement.Commands.Boost(boostInfo=boost_info), + ) + + +@pytest.mark.parametrize("node_fixture", ["silabs_water_heater"]) +async def test_update_from_water_heater( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test enable boost from water heater device side.""" + entity_id = "water_heater.water_heater" + + # confirm initial BoostState (as stored in the fixture) + state = hass.states.get(entity_id) + assert state + + # confirm thermostat state is 'high_demand' by setting the BoostState to 1 + set_node_attribute(matter_node, 2, 148, 5, 1) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_HIGH_DEMAND + + # confirm thermostat state is 'eco' by setting the BoostState to 0 + set_node_attribute(matter_node, 2, 148, 5, 0) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ECO + + +@pytest.mark.parametrize("node_fixture", ["silabs_water_heater"]) +async def test_water_heater_turn_on_off( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test water_heater set turn_off/turn_on.""" + state = hass.states.get("water_heater.water_heater") + assert state + + # turn_off water_heater + await hass.services.async_call( + "water_heater", + "turn_off", + { + "entity_id": "water_heater.water_heater", + }, + 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=2, + attribute=clusters.Thermostat.Attributes.SystemMode, + ), + value=0, + ) + + matter_client.write_attribute.reset_mock() + + # turn_on water_heater + await hass.services.async_call( + "water_heater", + "turn_on", + { + "entity_id": "water_heater.water_heater", + }, + 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=2, + attribute=clusters.Thermostat.Attributes.SystemMode, + ), + value=4, + )