From 46815be370c74cbf1716ff8f601922f1879ded3a Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Wed, 30 Sep 2026 09:19:01 +0200 Subject: [PATCH] Log MELCloud Home telemetry unavailability once (#183649) --- .../components/melcloud_home/coordinator.py | 54 ++++++++++++------- tests/components/melcloud_home/test_init.py | 42 +++++++++++++++ 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/melcloud_home/coordinator.py b/homeassistant/components/melcloud_home/coordinator.py index 51a59aeebec7..1cf1f5ce4461 100644 --- a/homeassistant/components/melcloud_home/coordinator.py +++ b/homeassistant/components/melcloud_home/coordinator.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from datetime import datetime, timedelta import logging -from typing import override +from typing import Any, override from aiomelcloudhome import ATAUnit, ATWUnit, MELCloudHome, UserContext from aiomelcloudhome.exceptions import ( @@ -180,35 +180,51 @@ class MelCloudHomeTelemetryCoordinator( update_interval=TELEMETRY_UPDATE_INTERVAL, ) self.client = client + self._unavailable_telemetry: set[tuple[str, str]] = set() + + async def _async_fetch_telemetry[_T]( + self, name: str, unit_id: str, coroutine: Coroutine[Any, Any, _T] + ) -> _T | None: + """Fetch telemetry for a unit, logging once when it becomes unavailable.""" + key = (name, unit_id) + try: + result = await coroutine + except ( + MelCloudHomeAuthenticationError, + MelCloudHomeConnectionError, + MelCloudHomeTimeoutError, + ): + if key not in self._unavailable_telemetry: + self._unavailable_telemetry.add(key) + _LOGGER.info("%s for %s is unavailable", name, unit_id) + return None + if key in self._unavailable_telemetry: + self._unavailable_telemetry.remove(key) + _LOGGER.info("%s for %s is available again", name, unit_id) + return result 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( + energy = await self._async_fetch_telemetry( + "Energy telemetry", + unit_id, + self.client.get_energy_telemetry( unit_id, from_dt=start_of_month, to_dt=now, interval="Day" - ) - except ( - MelCloudHomeAuthenticationError, - MelCloudHomeConnectionError, - MelCloudHomeTimeoutError, - ): - _LOGGER.warning("Failed to fetch energy telemetry for %s:", unit_id) + ), + ) + if energy is None: return None return sum(float(e.value) for e in energy) async def _async_get_outdoor_temperature(self, unit_id: str) -> float | None: """Fetch outdoor temperature for a unit without failing the whole update.""" - try: - return await self.client.get_outdoor_temperature(unit_id) - except ( - MelCloudHomeAuthenticationError, - MelCloudHomeConnectionError, - MelCloudHomeTimeoutError, - ): - _LOGGER.warning("Failed to fetch outdoor temperature for %s", unit_id) - return None + return await self._async_fetch_telemetry( + "Outdoor temperature", + unit_id, + self.client.get_outdoor_temperature(unit_id), + ) @override async def _async_update_data(self) -> MelCloudHomeTelemetryData: diff --git a/tests/components/melcloud_home/test_init.py b/tests/components/melcloud_home/test_init.py index 0e968a386e03..be2c9bfc7f4c 100644 --- a/tests/components/melcloud_home/test_init.py +++ b/tests/components/melcloud_home/test_init.py @@ -346,3 +346,45 @@ async def test_outdoor_temperature_update_cycle_fails( await hass.async_block_till_done(wait_background_tasks=True) assert telemetry_coordinator.data.outdoor_temperature["ata-unit-uuid-1"] is not None + + +@pytest.mark.parametrize( + ("method", "name"), + [ + pytest.param("get_energy_telemetry", "Energy telemetry", id="energy"), + pytest.param( + "get_outdoor_temperature", "Outdoor temperature", id="outdoor_temperature" + ), + ], +) +async def test_telemetry_unavailable_logged_once( + hass: HomeAssistant, + mock_melcloud_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, + method: str, + name: str, +) -> None: + """Test a failing telemetry fetch is logged once, and once when it recovers.""" + await setup_integration(hass, mock_config_entry) + unavailable = f"{name} for ata-unit-uuid-1 is unavailable" + available = f"{name} for ata-unit-uuid-1 is available again" + + getattr(mock_melcloud_client, method).side_effect = MelCloudHomeConnectionError + for _ in range(2): + freezer.tick(TELEMETRY_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert caplog.text.count(unavailable) == 1 + assert available not in caplog.text + + getattr(mock_melcloud_client, method).side_effect = None + for _ in range(2): + freezer.tick(TELEMETRY_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert caplog.text.count(unavailable) == 1 + assert caplog.text.count(available) == 1