From 1cdf874d0763e730bcf55c915522eb9f3ea2e808 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Wed, 30 Sep 2026 17:05:01 +0200 Subject: [PATCH] Add HVAC action to MELCloud Home ATW zones (#183771) --- .../components/melcloud_home/climate.py | 21 +++++++++ .../melcloud_home/snapshots/test_climate.ambr | 2 + .../components/melcloud_home/test_climate.py | 43 +++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/homeassistant/components/melcloud_home/climate.py b/homeassistant/components/melcloud_home/climate.py index c65aa18f85cb..f2f8cff92436 100644 --- a/homeassistant/components/melcloud_home/climate.py +++ b/homeassistant/components/melcloud_home/climate.py @@ -7,6 +7,7 @@ from aiomelcloudhome import ( ATAOperationMode, ATAVaneHorizontal, ATAVaneVertical, + ATWOperationMode, ATWZoneMode, ) @@ -14,6 +15,7 @@ from homeassistant.components.climate import ( ClimateEntity, ClimateEntityDescription, ClimateEntityFeature, + HVACAction, HVACMode, ) from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature @@ -92,6 +94,15 @@ HVAC_MODE_TO_ATW_ZONE_MODE: dict[HVACMode, ATWZoneMode] = { HVACMode.COOL: ATWZoneMode.COOL_ROOM_TEMPERATURE, } +# The unit heats either the tank or the zones, so heating the tank idles the zones +ATW_OPERATION_TO_HVAC_ACTION: dict[ATWOperationMode, HVACAction] = { + ATWOperationMode.STOP: HVACAction.IDLE, + ATWOperationMode.HOT_WATER: HVACAction.IDLE, + ATWOperationMode.HEAT: HVACAction.HEATING, + ATWOperationMode.HEAT_ZONES: HVACAction.HEATING, + ATWOperationMode.COOL: HVACAction.COOLING, +} + async def async_setup_entry( hass: HomeAssistant, @@ -441,6 +452,16 @@ class ATWZoneClimateEntity(MelCloudHomeATWZoneEntity, ClimateEntity): else HVACMode.OFF ) + @property + @override + def hvac_action(self) -> HVACAction | None: + """Return what the unit is doing for this zone.""" + if self.hvac_mode == HVACMode.OFF: + return HVACAction.OFF + if self.unit.operation_mode is None: + return None + return ATW_OPERATION_TO_HVAC_ACTION[self.unit.operation_mode] + @override async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC mode.""" diff --git a/tests/components/melcloud_home/snapshots/test_climate.ambr b/tests/components/melcloud_home/snapshots/test_climate.ambr index 526c0c4e2366..b374b798c416 100644 --- a/tests/components/melcloud_home/snapshots/test_climate.ambr +++ b/tests/components/melcloud_home/snapshots/test_climate.ambr @@ -49,6 +49,7 @@ 'attributes': ReadOnlyDict({ : 20.0, : 'Heat Pump Zone 1', + : , : list([ , , @@ -117,6 +118,7 @@ 'attributes': ReadOnlyDict({ : 21.0, : 'Heat Pump Zone 2', + : , : list([ , , diff --git a/tests/components/melcloud_home/test_climate.py b/tests/components/melcloud_home/test_climate.py index c655409fe7ba..bd155257a35b 100644 --- a/tests/components/melcloud_home/test_climate.py +++ b/tests/components/melcloud_home/test_climate.py @@ -19,6 +19,7 @@ import pytest from homeassistant.components.climate import ( ATTR_FAN_MODE, + ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_SWING_HORIZONTAL_MODE, ATTR_SWING_MODE, @@ -28,6 +29,7 @@ from homeassistant.components.climate import ( SERVICE_SET_SWING_HORIZONTAL_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, + HVACAction, HVACMode, ) from homeassistant.components.melcloud_home.const import DOMAIN @@ -527,6 +529,47 @@ async def test_atw_zone_temperature_range( assert state2.attributes["max_temp"] == 28.0 +@pytest.mark.parametrize( + ("overrides", "expected_action"), + [ + pytest.param({"OperationMode": "Stop"}, HVACAction.IDLE, id="idle"), + pytest.param({"OperationMode": "HotWater"}, HVACAction.IDLE, id="hot_water"), + pytest.param({"OperationMode": "Heat"}, HVACAction.HEATING, id="heat"), + pytest.param( + {"OperationMode": "HeatZones"}, HVACAction.HEATING, id="heat_zones" + ), + pytest.param({"OperationMode": "Cool"}, HVACAction.COOLING, id="cool"), + pytest.param({"OperationMode": "Unknown"}, None, id="unknown"), + pytest.param( + {"OperationMode": "Heat", "Power": "False"}, HVACAction.OFF, id="off" + ), + ], +) +async def test_atw_zone_hvac_action( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_melcloud_client: AsyncMock, + overrides: dict[str, str], + expected_action: HVACAction | None, +) -> None: + """Test the ATW zone HVAC action follows the unit's operation mode.""" + context: dict[str, Any] = await async_load_json_object_fixture( + hass, "context.json", DOMAIN + ) + settings = { + setting["name"]: setting + for setting in context["buildings"][0]["airToWaterUnits"][0]["settings"] + } + for name, value in overrides.items(): + settings[name]["value"] = value + mock_melcloud_client.get_context.return_value = UserContext.model_validate(context) + + await setup_integration(hass, mock_config_entry) + + assert (state := hass.states.get(ATW_ZONE1_ENTITY_ID)) + assert state.attributes.get(ATTR_HVAC_ACTION) == expected_action + + @pytest.mark.parametrize( ("entity_id", "method", "service", "service_data"), [