From f4f71749e7b905ce2a6d3618dd03b54b88fef0b3 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Fri, 14 Aug 2026 10:44:54 +0200 Subject: [PATCH] Separate energy endpoint MELCloud Home (#179124) --- .../components/melcloud_home/coordinator.py | 76 ++++++++++--------- tests/components/melcloud_home/test_init.py | 39 ++++++++++ 2 files changed, 79 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/melcloud_home/coordinator.py b/homeassistant/components/melcloud_home/coordinator.py index c24f246d8371..bb2257c94136 100644 --- a/homeassistant/components/melcloud_home/coordinator.py +++ b/homeassistant/components/melcloud_home/coordinator.py @@ -1,7 +1,7 @@ """Coordinator for MELCloud Home.""" from collections.abc import Callable -from datetime import timedelta +from datetime import datetime, timedelta import logging from typing import override @@ -104,44 +104,24 @@ class MelCloudHomeCoordinator(DataUpdateCoordinator[UserContext]): _LOGGER.debug("Removing stale device: %s", device.identifiers) registry.async_remove_device(device.id) + async def _async_get_energy( + self, unit_id: str, start_of_month: datetime, now: datetime + ) -> float | None: + """Fetch energy telemetry for a unit without failing the whole update.""" + try: + energy = await self.client.get_energy_telemetry( + unit_id, from_dt=start_of_month, to_dt=now + ) + except MelCloudHomeConnectionError, MelCloudHomeTimeoutError: + _LOGGER.warning("Failed to fetch energy telemetry for %s:", unit_id) + return None + return sum(float(e.value) for e in energy) + @override async def _async_update_data(self) -> UserContext: """Fetch data from the MELCloud Home API.""" try: data = await self.client.get_context() - - start_of_month = utcnow().replace( - day=1, hour=0, minute=0, second=0, microsecond=0 - ) - for building in data.buildings: - for ata_unit in building.air_to_air_units: - self.ata_units[ata_unit.id] = ata_unit - if ( - ata_unit.capabilities - and ata_unit.capabilities.has_energy_consumed_meter - ): - energy = await self.client.get_energy_telemetry( - ata_unit.id, - from_dt=start_of_month, - to_dt=utcnow(), - ) - self.ata_energy[ata_unit.id] = sum( - float(e.value) for e in energy - ) - for atw_unit in building.air_to_water_units: - self.atw_units[atw_unit.id] = atw_unit - if ( - atw_unit.capabilities - and atw_unit.capabilities.has_energy_consumed_meter - ): - energy = await self.client.get_energy_telemetry( - atw_unit.id, - from_dt=start_of_month, - to_dt=utcnow(), - ) - self.atw_energy[atw_unit.id] = sum( - float(e.value) for e in energy - ) except MelCloudHomeAuthenticationError as err: raise ConfigEntryAuthFailed( translation_domain=DOMAIN, @@ -157,8 +137,32 @@ class MelCloudHomeCoordinator(DataUpdateCoordinator[UserContext]): translation_domain=DOMAIN, translation_key="timeout_connect", ) from err - else: - return data + + start_of_month = utcnow().replace( + day=1, hour=0, minute=0, second=0, microsecond=0 + ) + now = utcnow() + for building in data.buildings: + for ata_unit in building.air_to_air_units: + self.ata_units[ata_unit.id] = ata_unit + if ( + ata_unit.capabilities + and ata_unit.capabilities.has_energy_consumed_meter + ): + self.ata_energy[ata_unit.id] = await self._async_get_energy( + ata_unit.id, start_of_month, now + ) + for atw_unit in building.air_to_water_units: + self.atw_units[atw_unit.id] = atw_unit + if ( + atw_unit.capabilities + and atw_unit.capabilities.has_energy_consumed_meter + ): + self.atw_energy[atw_unit.id] = await self._async_get_energy( + atw_unit.id, start_of_month, now + ) + + return data @callback @override diff --git a/tests/components/melcloud_home/test_init.py b/tests/components/melcloud_home/test_init.py index ccebad14a559..c17af212e7b3 100644 --- a/tests/components/melcloud_home/test_init.py +++ b/tests/components/melcloud_home/test_init.py @@ -193,3 +193,42 @@ async def test_new_atw_unit_callback( if "heat_pump" in entity.entity_id ] assert atw_entities + + +@pytest.mark.parametrize( + "exception", + [ + pytest.param(MelCloudHomeConnectionError("cannot connect"), id="connection"), + pytest.param(MelCloudHomeTimeoutError("timeout"), id="timeout"), + ], +) +async def test_energy_update_cycle_fails( + hass: HomeAssistant, + mock_melcloud_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + exception: Exception, +) -> None: + """Test that a failing energy fetch clears the value without unloading the entry.""" + await setup_integration(hass, mock_config_entry) + coordinator = mock_config_entry.runtime_data + + assert coordinator.ata_energy["ata-unit-uuid-1"] is not None + assert coordinator.atw_energy["atw-unit-uuid-1"] is not None + + mock_melcloud_client.get_energy_telemetry.side_effect = exception + freezer.tick(UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert coordinator.ata_energy["ata-unit-uuid-1"] is None + assert coordinator.atw_energy["atw-unit-uuid-1"] is None + + # Demonstrate a recovery + mock_melcloud_client.get_energy_telemetry.side_effect = None + freezer.tick(UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert coordinator.ata_energy["ata-unit-uuid-1"] is not None + assert coordinator.atw_energy["atw-unit-uuid-1"] is not None