diff --git a/homeassistant/components/melcloud_home/__init__.py b/homeassistant/components/melcloud_home/__init__.py index 99bc92ad46ed..a8212afb31cd 100644 --- a/homeassistant/components/melcloud_home/__init__.py +++ b/homeassistant/components/melcloud_home/__init__.py @@ -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) diff --git a/homeassistant/components/melcloud_home/coordinator.py b/homeassistant/components/melcloud_home/coordinator.py index a415327eb8c8..4e2f85bf4606 100644 --- a/homeassistant/components/melcloud_home/coordinator.py +++ b/homeassistant/components/melcloud_home/coordinator.py @@ -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, + ) + ), ) diff --git a/homeassistant/components/melcloud_home/diagnostics.py b/homeassistant/components/melcloud_home/diagnostics.py index 042a36ecfeef..3d288f57f5b1 100644 --- a/homeassistant/components/melcloud_home/diagnostics.py +++ b/homeassistant/components/melcloud_home/diagnostics.py @@ -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 ), } diff --git a/homeassistant/components/melcloud_home/icons.json b/homeassistant/components/melcloud_home/icons.json index dc98ac2263b0..96704ec4f40b 100644 --- a/homeassistant/components/melcloud_home/icons.json +++ b/homeassistant/components/melcloud_home/icons.json @@ -44,6 +44,9 @@ } }, "sensor": { + "outdoor_temperature": { + "default": "mdi:thermometer" + }, "room_temperature": { "default": "mdi:home-thermometer" }, diff --git a/homeassistant/components/melcloud_home/sensor.py b/homeassistant/components/melcloud_home/sensor.py index 8573eeea344b..91ea29c20faf 100644 --- a/homeassistant/components/melcloud_home/sensor.py +++ b/homeassistant/components/melcloud_home/sensor.py @@ -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 diff --git a/homeassistant/components/melcloud_home/strings.json b/homeassistant/components/melcloud_home/strings.json index 4455082d2375..e964ae34019c 100644 --- a/homeassistant/components/melcloud_home/strings.json +++ b/homeassistant/components/melcloud_home/strings.json @@ -124,6 +124,9 @@ "energy_consumed": { "name": "Energy consumed (monthly)" }, + "outdoor_temperature": { + "name": "Outdoor temperature" + }, "room_temperature": { "name": "Room temperature" }, diff --git a/tests/components/melcloud_home/conftest.py b/tests/components/melcloud_home/conftest.py index 1eb04056809e..ac781bd42129 100644 --- a/tests/components/melcloud_home/conftest.py +++ b/tests/components/melcloud_home/conftest.py @@ -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( diff --git a/tests/components/melcloud_home/fixtures/context.json b/tests/components/melcloud_home/fixtures/context.json index db95462950fb..e5209c5f044c 100644 --- a/tests/components/melcloud_home/fixtures/context.json +++ b/tests/components/melcloud_home/fixtures/context.json @@ -43,7 +43,7 @@ "hasDemandSideControl": false, "hasHalfDegreeIncrements": true, "supportsWideVane": false, - "hasOutdoorTemperatureSensor": false, + "hasOutdoorTemperatureSensor": true, "hasVaneVertical": true, "hasVaneHorizontal": true, "hasStandbyMode": false diff --git a/tests/components/melcloud_home/snapshots/test_diagnostics.ambr b/tests/components/melcloud_home/snapshots/test_diagnostics.ambr index 808999cdc776..6308ca8c60a2 100644 --- a/tests/components/melcloud_home/snapshots/test_diagnostics.ambr +++ b/tests/components/melcloud_home/snapshots/test_diagnostics.ambr @@ -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, + }), }), }) # --- diff --git a/tests/components/melcloud_home/snapshots/test_sensor.ambr b/tests/components/melcloud_home/snapshots/test_sensor.ambr index 755967d4fe20..31547eb2d23d 100644 --- a/tests/components/melcloud_home/snapshots/test_sensor.ambr +++ b/tests/components/melcloud_home/snapshots/test_sensor.ambr @@ -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({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + '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': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Outdoor temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + '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': , + }) +# --- +# name: test_all_entities[sensor.living_room_ac_outdoor_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'temperature', + : 'Living Room AC Outdoor temperature', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.living_room_ac_outdoor_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '19.5', + }) +# --- # name: test_all_entities[sensor.living_room_ac_room_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/melcloud_home/test_init.py b/tests/components/melcloud_home/test_init.py index 5049dae794e0..0e968a386e03 100644 --- a/tests/components/melcloud_home/test_init.py +++ b/tests/components/melcloud_home/test_init.py @@ -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