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_validpull/123749/head
parent
b392d61391
commit
766733b3b2
|
@ -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 {}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue