Log MELCloud Home telemetry unavailability once (#183649)

This commit is contained in:
Erwin Douna
2026-09-30 09:19:01 +02:00
committed by GitHub
parent 659eba7659
commit 46815be370
2 changed files with 77 additions and 19 deletions
@@ -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:
@@ -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