mirror of
https://github.com/home-assistant/core.git
synced 2026-09-25 17:04:04 -04:00
Add outdoor temperature to MELCloud Home (#181216)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
a9337d2a8f
commit
fc78d26a74
@@ -9,8 +9,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from .coordinator import (
|
||||
MelCloudHomeConfigEntry,
|
||||
MelCloudHomeCoordinator,
|
||||
MelCloudHomeEnergyCoordinator,
|
||||
MelCloudHomeRuntimeData,
|
||||
MelCloudHomeTelemetryCoordinator,
|
||||
)
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
@@ -35,14 +35,15 @@ async def async_setup_entry(
|
||||
client = MELCloudHome(auth=auth, session=session)
|
||||
|
||||
coordinator = MelCloudHomeCoordinator(hass, entry, client)
|
||||
energy_coordinator = MelCloudHomeEnergyCoordinator(hass, entry, client)
|
||||
telemetry_coordinator = MelCloudHomeTelemetryCoordinator(hass, entry, client)
|
||||
|
||||
# It has to be this order, to avoid a race condition
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
await energy_coordinator.async_config_entry_first_refresh()
|
||||
await telemetry_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = MelCloudHomeRuntimeData(
|
||||
coordinator=coordinator, energy_coordinator=energy_coordinator
|
||||
coordinator=coordinator,
|
||||
telemetry_coordinator=telemetry_coordinator,
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UPDATE_INTERVAL = timedelta(seconds=60)
|
||||
ENERGY_UPDATE_INTERVAL = timedelta(minutes=15)
|
||||
TELEMETRY_UPDATE_INTERVAL = timedelta(minutes=15)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
@@ -34,7 +34,7 @@ class MelCloudHomeRuntimeData:
|
||||
"""Runtime data for the MELCloud Home config entry."""
|
||||
|
||||
coordinator: MelCloudHomeCoordinator
|
||||
energy_coordinator: MelCloudHomeEnergyCoordinator
|
||||
telemetry_coordinator: MelCloudHomeTelemetryCoordinator
|
||||
|
||||
|
||||
type MelCloudHomeConfigEntry = ConfigEntry[MelCloudHomeRuntimeData]
|
||||
@@ -150,8 +150,18 @@ class MelCloudHomeCoordinator(DataUpdateCoordinator[UserContext]):
|
||||
self._notify_new_units(self.data)
|
||||
|
||||
|
||||
class MelCloudHomeEnergyCoordinator(DataUpdateCoordinator[dict[str, float | None]]):
|
||||
"""Coordinator to manage fetching MELCloud Home energy telemetry."""
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class MelCloudHomeTelemetryData:
|
||||
"""Telemetry data fetched periodically for MELCloud Home units."""
|
||||
|
||||
energy: dict[str, float | None]
|
||||
outdoor_temperature: dict[str, float | None]
|
||||
|
||||
|
||||
class MelCloudHomeTelemetryCoordinator(
|
||||
DataUpdateCoordinator[MelCloudHomeTelemetryData]
|
||||
):
|
||||
"""Coordinator to manage fetching MELCloud Home energy and outdoor temperature telemetry."""
|
||||
|
||||
config_entry: MelCloudHomeConfigEntry
|
||||
|
||||
@@ -166,8 +176,8 @@ class MelCloudHomeEnergyCoordinator(DataUpdateCoordinator[dict[str, float | None
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"{DOMAIN}_energy",
|
||||
update_interval=ENERGY_UPDATE_INTERVAL,
|
||||
name=f"{DOMAIN}_telemetry",
|
||||
update_interval=TELEMETRY_UPDATE_INTERVAL,
|
||||
)
|
||||
self.client = client
|
||||
|
||||
@@ -188,9 +198,21 @@ class MelCloudHomeEnergyCoordinator(DataUpdateCoordinator[dict[str, float | 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
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> dict[str, float | None]:
|
||||
"""Fetch energy telemetry for all units with an energy meter."""
|
||||
async def _async_update_data(self) -> MelCloudHomeTelemetryData:
|
||||
"""Fetch energy and outdoor temperature telemetry for all supported units."""
|
||||
try:
|
||||
data = await self.client.get_context()
|
||||
except MelCloudHomeAuthenticationError as err:
|
||||
@@ -215,6 +237,9 @@ class MelCloudHomeEnergyCoordinator(DataUpdateCoordinator[dict[str, float | None
|
||||
now = utcnow()
|
||||
|
||||
energy_coroutines: dict[str, Coroutine[None, None, float | None]] = {}
|
||||
outdoor_temperature_coroutine: dict[
|
||||
str, Coroutine[None, None, float | None]
|
||||
] = {}
|
||||
for building in data.buildings:
|
||||
for ata_unit in building.air_to_air_units:
|
||||
if (
|
||||
@@ -224,6 +249,14 @@ class MelCloudHomeEnergyCoordinator(DataUpdateCoordinator[dict[str, float | None
|
||||
energy_coroutines[ata_unit.id] = self._async_get_energy(
|
||||
ata_unit.id, start_of_month, now
|
||||
)
|
||||
if (
|
||||
ata_unit.capabilities
|
||||
and ata_unit.capabilities.has_outdoor_temperature_sensor
|
||||
):
|
||||
outdoor_temperature_coroutine[ata_unit.id] = (
|
||||
self._async_get_outdoor_temperature(ata_unit.id)
|
||||
)
|
||||
|
||||
for atw_unit in building.air_to_water_units:
|
||||
if (
|
||||
atw_unit.capabilities
|
||||
@@ -233,10 +266,18 @@ class MelCloudHomeEnergyCoordinator(DataUpdateCoordinator[dict[str, float | None
|
||||
atw_unit.id, start_of_month, now
|
||||
)
|
||||
|
||||
return dict(
|
||||
zip(
|
||||
energy_coroutines,
|
||||
await asyncio.gather(*energy_coroutines.values()),
|
||||
strict=True,
|
||||
)
|
||||
energy_values, outdoor_temperature_values = await asyncio.gather(
|
||||
asyncio.gather(*energy_coroutines.values()),
|
||||
asyncio.gather(*outdoor_temperature_coroutine.values()),
|
||||
)
|
||||
|
||||
return MelCloudHomeTelemetryData(
|
||||
energy=dict(zip(energy_coroutines, energy_values, strict=True)),
|
||||
outdoor_temperature=dict(
|
||||
zip(
|
||||
outdoor_temperature_coroutine,
|
||||
outdoor_temperature_values,
|
||||
strict=True,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Diagnostics for MELCloud Home integration."""
|
||||
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
@@ -30,7 +31,7 @@ async def async_get_config_entry_diagnostics(
|
||||
config_entry.runtime_data.coordinator.data.model_dump(mode="json"),
|
||||
TO_REDACT,
|
||||
),
|
||||
"energy_coordinator": async_redact_data(
|
||||
config_entry.runtime_data.energy_coordinator.data, TO_REDACT
|
||||
"telemetry_coordinator": async_redact_data(
|
||||
asdict(config_entry.runtime_data.telemetry_coordinator.data), TO_REDACT
|
||||
),
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"outdoor_temperature": {
|
||||
"default": "mdi:thermometer"
|
||||
},
|
||||
"room_temperature": {
|
||||
"default": "mdi:home-thermometer"
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ from .common import async_setup_unit_entities
|
||||
from .coordinator import (
|
||||
MelCloudHomeConfigEntry,
|
||||
MelCloudHomeCoordinator,
|
||||
MelCloudHomeEnergyCoordinator,
|
||||
MelCloudHomeTelemetryCoordinator,
|
||||
)
|
||||
from .entity import MelCloudHomeATAUnitEntity, MelCloudHomeATWUnitEntity
|
||||
|
||||
@@ -44,6 +44,15 @@ ENERGY_CONSUMED_DESCRIPTION = SensorEntityDescription(
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
)
|
||||
|
||||
OUTDOOR_TEMPERATURE_DESCRIPTION = SensorEntityDescription(
|
||||
key="outdoor_temperature",
|
||||
translation_key="outdoor_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=1,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MelCloudHomeSensorEntityDescription[_UnitT: ATAUnit | ATWUnit](
|
||||
@@ -128,7 +137,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MELCloud Home sensors."""
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
energy_coordinator = entry.runtime_data.energy_coordinator
|
||||
telemetry_coordinator = entry.runtime_data.telemetry_coordinator
|
||||
|
||||
async_setup_unit_entities(
|
||||
coordinator,
|
||||
@@ -141,11 +150,15 @@ async def async_setup_entry(
|
||||
if entity_description.exists_fn(unit)
|
||||
),
|
||||
(
|
||||
ATAEnergySensor(coordinator, energy_coordinator, unit)
|
||||
ATAEnergySensor(coordinator, telemetry_coordinator, unit)
|
||||
for unit in units
|
||||
if bool(
|
||||
unit.capabilities and unit.capabilities.has_energy_consumed_meter
|
||||
)
|
||||
if unit.capabilities and unit.capabilities.has_energy_consumed_meter
|
||||
),
|
||||
(
|
||||
ATAOutdoorTemperatureSensor(coordinator, telemetry_coordinator, unit)
|
||||
for unit in units
|
||||
if unit.capabilities
|
||||
and unit.capabilities.has_outdoor_temperature_sensor
|
||||
),
|
||||
),
|
||||
lambda units: chain(
|
||||
@@ -156,11 +169,9 @@ async def async_setup_entry(
|
||||
if entity_description.exists_fn(unit)
|
||||
),
|
||||
(
|
||||
ATWEnergySensor(coordinator, energy_coordinator, unit)
|
||||
ATWEnergySensor(coordinator, telemetry_coordinator, unit)
|
||||
for unit in units
|
||||
if bool(
|
||||
unit.capabilities and unit.capabilities.has_energy_consumed_meter
|
||||
)
|
||||
if unit.capabilities and unit.capabilities.has_energy_consumed_meter
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -212,41 +223,47 @@ class ATWSensor(MelCloudHomeATWUnitEntity, SensorEntity):
|
||||
return self.entity_description.value_fn(self.unit, self.coordinator)
|
||||
|
||||
|
||||
class ATAEnergySensor(MelCloudHomeATAUnitEntity, SensorEntity):
|
||||
"""Representation of a MELCloud Home ATA energy sensor."""
|
||||
|
||||
entity_description = ENERGY_CONSUMED_DESCRIPTION
|
||||
class MelCloudHomeATATelemetrySensor(MelCloudHomeATAUnitEntity, SensorEntity):
|
||||
"""Base class for MELCloud Home ATA sensors backed by the telemetry coordinator."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MelCloudHomeCoordinator,
|
||||
energy_coordinator: MelCloudHomeEnergyCoordinator,
|
||||
telemetry_coordinator: MelCloudHomeTelemetryCoordinator,
|
||||
unit: ATAUnit,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator, unit)
|
||||
self._energy_coordinator = energy_coordinator
|
||||
self._attr_unique_id = f"{unit.id}_{ENERGY_CONSUMED_DESCRIPTION.key}"
|
||||
self._telemetry_coordinator = telemetry_coordinator
|
||||
self._attr_unique_id = f"{unit.id}_{self.entity_description.key}"
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Also react to updates from the energy coordinator."""
|
||||
"""Also react to updates from the telemetry coordinator."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self._energy_coordinator.async_add_listener(self._handle_coordinator_update)
|
||||
self._telemetry_coordinator.async_add_listener(
|
||||
self._handle_coordinator_update
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def available(self) -> bool:
|
||||
"""Return if the entity is available."""
|
||||
return super().available and self._energy_coordinator.last_update_success
|
||||
return super().available and self._telemetry_coordinator.last_update_success
|
||||
|
||||
|
||||
class ATAEnergySensor(MelCloudHomeATATelemetrySensor):
|
||||
"""Representation of a MELCloud Home ATA energy sensor."""
|
||||
|
||||
entity_description = ENERGY_CONSUMED_DESCRIPTION
|
||||
|
||||
@property
|
||||
@override
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self._energy_coordinator.data.get(self._unit_id)
|
||||
return self._telemetry_coordinator.data.energy.get(self._unit_id)
|
||||
|
||||
@property
|
||||
@override
|
||||
@@ -255,6 +272,18 @@ class ATAEnergySensor(MelCloudHomeATAUnitEntity, SensorEntity):
|
||||
return utcnow().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
|
||||
class ATAOutdoorTemperatureSensor(MelCloudHomeATATelemetrySensor):
|
||||
"""Representation of a MELCloud Home ATA outdoor temperature sensor."""
|
||||
|
||||
entity_description = OUTDOOR_TEMPERATURE_DESCRIPTION
|
||||
|
||||
@property
|
||||
@override
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self._telemetry_coordinator.data.outdoor_temperature.get(self._unit_id)
|
||||
|
||||
|
||||
class ATWEnergySensor(MelCloudHomeATWUnitEntity, SensorEntity):
|
||||
"""Representation of a MELCloud Home ATW energy sensor."""
|
||||
|
||||
@@ -263,33 +292,35 @@ class ATWEnergySensor(MelCloudHomeATWUnitEntity, SensorEntity):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MelCloudHomeCoordinator,
|
||||
energy_coordinator: MelCloudHomeEnergyCoordinator,
|
||||
telemetry_coordinator: MelCloudHomeTelemetryCoordinator,
|
||||
unit: ATWUnit,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator, unit)
|
||||
self._energy_coordinator = energy_coordinator
|
||||
self._telemetry_coordinator = telemetry_coordinator
|
||||
self._attr_unique_id = f"{unit.id}_{ENERGY_CONSUMED_DESCRIPTION.key}"
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Also react to updates from the energy coordinator."""
|
||||
"""Also react to updates from the telemetry coordinator."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self._energy_coordinator.async_add_listener(self._handle_coordinator_update)
|
||||
self._telemetry_coordinator.async_add_listener(
|
||||
self._handle_coordinator_update
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def available(self) -> bool:
|
||||
"""Return if the entity is available."""
|
||||
return super().available and self._energy_coordinator.last_update_success
|
||||
return super().available and self._telemetry_coordinator.last_update_success
|
||||
|
||||
@property
|
||||
@override
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self._energy_coordinator.data.get(self._unit_id)
|
||||
return self._telemetry_coordinator.data.energy.get(self._unit_id)
|
||||
|
||||
@property
|
||||
@override
|
||||
|
||||
@@ -124,6 +124,9 @@
|
||||
"energy_consumed": {
|
||||
"name": "Energy consumed (monthly)"
|
||||
},
|
||||
"outdoor_temperature": {
|
||||
"name": "Outdoor temperature"
|
||||
},
|
||||
"room_temperature": {
|
||||
"name": "Room temperature"
|
||||
},
|
||||
|
||||
@@ -47,6 +47,7 @@ def mock_melcloud_client() -> Generator[AsyncMock]:
|
||||
TelemetryValue.model_validate(value)
|
||||
for value in load_json_array_fixture("energy.json", DOMAIN)
|
||||
]
|
||||
client.get_outdoor_temperature.return_value = 19.5
|
||||
|
||||
with (
|
||||
patch(
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"hasDemandSideControl": false,
|
||||
"hasHalfDegreeIncrements": true,
|
||||
"supportsWideVane": false,
|
||||
"hasOutdoorTemperatureSensor": false,
|
||||
"hasOutdoorTemperatureSensor": true,
|
||||
"hasVaneVertical": true,
|
||||
"hasVaneHorizontal": true,
|
||||
"hasStandbyMode": false
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
'has_energy_consumed_meter': True,
|
||||
'has_fan_operation_mode': None,
|
||||
'has_half_degree_increments': True,
|
||||
'has_outdoor_temperature_sensor': False,
|
||||
'has_outdoor_temperature_sensor': True,
|
||||
'has_standby_mode': False,
|
||||
'has_vane_horizontal': True,
|
||||
'has_vane_vertical': True,
|
||||
@@ -269,9 +269,14 @@
|
||||
'number_of_guest_devices_allowed': 10,
|
||||
'number_of_guests_allowed_per_unit': 5,
|
||||
}),
|
||||
'energy_coordinator': dict({
|
||||
'ata-unit-uuid-1': 450.5,
|
||||
'atw-unit-uuid-1': 450.5,
|
||||
'telemetry_coordinator': dict({
|
||||
'energy': dict({
|
||||
'ata-unit-uuid-1': 450.5,
|
||||
'atw-unit-uuid-1': 450.5,
|
||||
}),
|
||||
'outdoor_temperature': dict({
|
||||
'ata-unit-uuid-1': 19.5,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -352,6 +352,64 @@
|
||||
'state': '0.4505',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.living_room_ac_outdoor_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.living_room_ac_outdoor_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Outdoor temperature',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Outdoor temperature',
|
||||
'platform': 'melcloud_home',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'outdoor_temperature',
|
||||
'unique_id': 'ata-unit-uuid-1_outdoor_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.living_room_ac_outdoor_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Living Room AC Outdoor temperature',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.living_room_ac_outdoor_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '19.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.living_room_ac_room_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -13,7 +13,7 @@ import pytest
|
||||
|
||||
from homeassistant.components.melcloud_home.const import DOMAIN
|
||||
from homeassistant.components.melcloud_home.coordinator import (
|
||||
ENERGY_UPDATE_INTERVAL,
|
||||
TELEMETRY_UPDATE_INTERVAL,
|
||||
UPDATE_INTERVAL,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
@@ -216,27 +216,27 @@ async def test_energy_update_cycle_fails(
|
||||
) -> None:
|
||||
"""Test that a failing energy fetch clears the value without unloading the entry."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
energy_coordinator = mock_config_entry.runtime_data.energy_coordinator
|
||||
telemetry_coordinator = mock_config_entry.runtime_data.telemetry_coordinator
|
||||
|
||||
assert energy_coordinator.data["ata-unit-uuid-1"] is not None
|
||||
assert energy_coordinator.data["atw-unit-uuid-1"] is not None
|
||||
assert telemetry_coordinator.data.energy["ata-unit-uuid-1"] is not None
|
||||
assert telemetry_coordinator.data.energy["atw-unit-uuid-1"] is not None
|
||||
|
||||
mock_melcloud_client.get_energy_telemetry.side_effect = exception
|
||||
freezer.tick(ENERGY_UPDATE_INTERVAL)
|
||||
freezer.tick(TELEMETRY_UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert energy_coordinator.data["ata-unit-uuid-1"] is None
|
||||
assert energy_coordinator.data["atw-unit-uuid-1"] is None
|
||||
assert telemetry_coordinator.data.energy["ata-unit-uuid-1"] is None
|
||||
assert telemetry_coordinator.data.energy["atw-unit-uuid-1"] is None
|
||||
|
||||
# Demonstrate a recovery
|
||||
mock_melcloud_client.get_energy_telemetry.side_effect = None
|
||||
freezer.tick(ENERGY_UPDATE_INTERVAL)
|
||||
freezer.tick(TELEMETRY_UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert energy_coordinator.data["ata-unit-uuid-1"] is not None
|
||||
assert energy_coordinator.data["atw-unit-uuid-1"] is not None
|
||||
assert telemetry_coordinator.data.energy["ata-unit-uuid-1"] is not None
|
||||
assert telemetry_coordinator.data.energy["atw-unit-uuid-1"] is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -258,11 +258,13 @@ async def test_energy_telemetry_fetch_failure(
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
mock_melcloud_client.get_energy_telemetry.side_effect = exception
|
||||
freezer.tick(ENERGY_UPDATE_INTERVAL)
|
||||
freezer.tick(TELEMETRY_UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.runtime_data.energy_coordinator.last_update_success is True
|
||||
assert (
|
||||
mock_config_entry.runtime_data.telemetry_coordinator.last_update_success is True
|
||||
)
|
||||
assert mock_config_entry.runtime_data.coordinator.last_update_success is True
|
||||
|
||||
|
||||
@@ -274,19 +276,19 @@ async def test_energy_telemetry_fetch_failure(
|
||||
pytest.param(MelCloudHomeTimeoutError("timeout"), id="timeout"),
|
||||
],
|
||||
)
|
||||
async def test_energy_coordinator_context_fetch_failure(
|
||||
async def test_telemetry_coordinator_context_fetch_failure(
|
||||
hass: HomeAssistant,
|
||||
mock_melcloud_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test that a failing energy coordinator refresh doesn't affect the main coordinator."""
|
||||
"""Test that a failing telemetry coordinator refresh doesn't affect the main coordinator."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
# Split the margin so the main coordinator's rescheduled refresh doesn't land
|
||||
# exactly on the energy coordinator's, which would make both fail below.
|
||||
freezer.tick(ENERGY_UPDATE_INTERVAL - UPDATE_INTERVAL / 2)
|
||||
# exactly on the telemetry coordinator's, which would make both fail below.
|
||||
freezer.tick(TELEMETRY_UPDATE_INTERVAL - UPDATE_INTERVAL / 2)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -308,3 +310,39 @@ async def test_energy_coordinator_context_fetch_failure(
|
||||
)
|
||||
)
|
||||
assert room_temperature_sensor.state != STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
pytest.param(MelCloudHomeAuthenticationError("bad creds"), id="auth"),
|
||||
pytest.param(MelCloudHomeConnectionError("cannot connect"), id="connection"),
|
||||
pytest.param(MelCloudHomeTimeoutError("timeout"), id="timeout"),
|
||||
],
|
||||
)
|
||||
async def test_outdoor_temperature_update_cycle_fails(
|
||||
hass: HomeAssistant,
|
||||
mock_melcloud_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test that a failing outdoor temperature fetch clears the value without unloading the entry."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
telemetry_coordinator = mock_config_entry.runtime_data.telemetry_coordinator
|
||||
|
||||
assert telemetry_coordinator.data.outdoor_temperature["ata-unit-uuid-1"] is not None
|
||||
|
||||
mock_melcloud_client.get_outdoor_temperature.side_effect = exception
|
||||
freezer.tick(TELEMETRY_UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert telemetry_coordinator.data.outdoor_temperature["ata-unit-uuid-1"] is None
|
||||
|
||||
mock_melcloud_client.get_outdoor_temperature.side_effect = None
|
||||
freezer.tick(TELEMETRY_UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert telemetry_coordinator.data.outdoor_temperature["ata-unit-uuid-1"] is not None
|
||||
|
||||
Reference in New Issue
Block a user