From 766733b3b26104552700365ef2f51b32f60d4f4d Mon Sep 17 00:00:00 2001 From: wittypluck Date: Sun, 11 Aug 2024 19:14:43 +0200 Subject: [PATCH] Avoid Exception on Glances missing key (#114628) * Handle case of sensors removed server side * Update available state on value update * Set uptime to None if key is missing * Replace _attr_available by _data_valid --- .../components/glances/coordinator.py | 10 ++--- homeassistant/components/glances/sensor.py | 25 ++++++------ tests/components/glances/test_sensor.py | 38 +++++++++++++++++++ 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/glances/coordinator.py b/homeassistant/components/glances/coordinator.py index 4e5bdcc1543..8882b097ba9 100644 --- a/homeassistant/components/glances/coordinator.py +++ b/homeassistant/components/glances/coordinator.py @@ -45,15 +45,13 @@ class GlancesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): except exceptions.GlancesApiError as err: raise UpdateFailed from err # Update computed values - uptime: datetime | None = self.data["computed"]["uptime"] if self.data else None + uptime: datetime | None = None up_duration: timedelta | None = None - if up_duration := parse_duration(data.get("uptime")): + if "uptime" in data and (up_duration := parse_duration(data["uptime"])): + uptime = self.data["computed"]["uptime"] if self.data else None # Update uptime if previous value is None or previous uptime is bigger than # new uptime (i.e. server restarted) - if ( - self.data is None - or self.data["computed"]["uptime_duration"] > up_duration - ): + if uptime is None or self.data["computed"]["uptime_duration"] > up_duration: uptime = utcnow() - up_duration data["computed"] = {"uptime_duration": up_duration, "uptime": uptime} return data or {} diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index a1cb8e47b9d..59eba69d60a 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -325,6 +325,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit entity_description: GlancesSensorEntityDescription _attr_has_entity_name = True + _data_valid: bool = False def __init__( self, @@ -351,14 +352,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit @property def available(self) -> bool: """Set sensor unavailable when native value is invalid.""" - if super().available: - return ( - not self._numeric_state_expected - or isinstance(value := self.native_value, (int, float)) - or isinstance(value, str) - and value.isnumeric() - ) - return False + return super().available and self._data_valid @callback def _handle_coordinator_update(self) -> None: @@ -368,10 +362,19 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit def _update_native_value(self) -> None: """Update sensor native value from coordinator data.""" - data = self.coordinator.data[self.entity_description.type] - if dict_val := data.get(self._sensor_label): + data = self.coordinator.data.get(self.entity_description.type) + if data and (dict_val := data.get(self._sensor_label)): self._attr_native_value = dict_val.get(self.entity_description.key) - elif self.entity_description.key in data: + elif data and (self.entity_description.key in data): self._attr_native_value = data.get(self.entity_description.key) else: self._attr_native_value = None + self._update_data_valid() + + def _update_data_valid(self) -> None: + self._data_valid = self._attr_native_value is not None and ( + not self._numeric_state_expected + or isinstance(self._attr_native_value, (int, float)) + or isinstance(self._attr_native_value, str) + and self._attr_native_value.isnumeric() + ) diff --git a/tests/components/glances/test_sensor.py b/tests/components/glances/test_sensor.py index 7dee47680ed..8e0367a712c 100644 --- a/tests/components/glances/test_sensor.py +++ b/tests/components/glances/test_sensor.py @@ -7,6 +7,7 @@ from freezegun.api import FrozenDateTimeFactory from syrupy import SnapshotAssertion from homeassistant.components.glances.const import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -71,3 +72,40 @@ async def test_uptime_variation( async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("sensor.0_0_0_0_uptime").state == "2024-02-15T12:49:52+00:00" + + +async def test_sensor_removed( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_api: AsyncMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test sensor removed server side.""" + + # Init with reference time + freezer.move_to(MOCK_REFERENCE_DATE) + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, entry_id="test") + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("sensor.0_0_0_0_ssl_disk_used").state != STATE_UNAVAILABLE + assert hass.states.get("sensor.0_0_0_0_memory_use").state != STATE_UNAVAILABLE + assert hass.states.get("sensor.0_0_0_0_uptime").state != STATE_UNAVAILABLE + + # Remove some sensors from Glances API data + mock_data = HA_SENSOR_DATA.copy() + mock_data.pop("fs") + mock_data.pop("mem") + mock_data.pop("uptime") + mock_api.return_value.get_ha_sensor_data = AsyncMock(return_value=mock_data) + + # Server stops providing some sensors, so state should switch to Unavailable + freezer.move_to(MOCK_REFERENCE_DATE + timedelta(minutes=2)) + freezer.tick(delta=timedelta(seconds=120)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("sensor.0_0_0_0_ssl_disk_used").state == STATE_UNAVAILABLE + assert hass.states.get("sensor.0_0_0_0_memory_use").state == STATE_UNAVAILABLE + assert hass.states.get("sensor.0_0_0_0_uptime").state == STATE_UNAVAILABLE