Separate energy endpoint MELCloud Home (#179124)

This commit is contained in:
Erwin Douna
2026-08-14 10:44:54 +02:00
committed by GitHub
parent b00ddc1db1
commit f4f71749e7
2 changed files with 79 additions and 36 deletions
@@ -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
@@ -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