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
pull/123749/head
wittypluck 2024-08-11 19:14:43 +02:00 committed by GitHub
parent b392d61391
commit 766733b3b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 56 additions and 17 deletions

View File

@ -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 {}

View File

@ -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()
)

View File

@ -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