Add graceful handling for LASTSTEST sensor in APCUPSD (#113125)

* Add handling for LASTSTEST sensor

* Set the state to unknown instead of unavailable

* Use LASTSTEST constant and revise the logic to add it to the entity list

* Use LASTSTEST constant
This commit is contained in:
Yuxin Wang 2024-07-31 07:01:48 -04:00 committed by GitHub
parent c8dccec956
commit bf3a2cf393
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 54 additions and 5 deletions

View File

@ -4,3 +4,6 @@ from typing import Final
DOMAIN: Final = "apcupsd"
CONNECTION_TIMEOUT: int = 10
# Field name of last self test retrieved from apcupsd.
LASTSTEST: Final = "laststest"

View File

@ -13,6 +13,7 @@ from homeassistant.components.sensor import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
PERCENTAGE,
STATE_UNKNOWN,
UnitOfApparentPower,
UnitOfElectricCurrent,
UnitOfElectricPotential,
@ -25,7 +26,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .const import DOMAIN, LASTSTEST
from .coordinator import APCUPSdCoordinator
PARALLEL_UPDATES = 0
@ -156,8 +157,8 @@ SENSORS: dict[str, SensorEntityDescription] = {
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
"laststest": SensorEntityDescription(
key="laststest",
LASTSTEST: SensorEntityDescription(
key=LASTSTEST,
translation_key="last_self_test",
),
"lastxfer": SensorEntityDescription(
@ -417,7 +418,12 @@ async def async_setup_entry(
available_resources: set[str] = {k.lower() for k, _ in coordinator.data.items()}
entities = []
for resource in available_resources:
# "laststest" is a special sensor that only appears when the APC UPS daemon has done a
# periodical (or manual) self test since last daemon restart. It might not be available
# when we set up the integration, and we do not know if it would ever be available. Here we
# add it anyway and mark it as unknown initially.
for resource in available_resources | {LASTSTEST}:
if resource not in SENSORS:
_LOGGER.warning("Invalid resource from APCUPSd: %s", resource.upper())
continue
@ -473,6 +479,14 @@ class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity):
def _update_attrs(self) -> None:
"""Update sensor attributes based on coordinator data."""
key = self.entity_description.key.upper()
# For most sensors the key will always be available for each refresh. However, some sensors
# (e.g., "laststest") will only appear after certain event occurs (e.g., a self test is
# performed) and may disappear again after certain event. So we mark the state as "unknown"
# when it becomes unknown after such events.
if key not in self.coordinator.data:
self._attr_native_value = STATE_UNKNOWN
return
self._attr_native_value, inferred_unit = infer_unit(self.coordinator.data[key])
if not self.native_unit_of_measurement:
self._attr_native_unit_of_measurement = inferred_unit

View File

@ -15,6 +15,7 @@ from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
PERCENTAGE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfElectricPotential,
UnitOfPower,
UnitOfTime,
@ -25,7 +26,7 @@ from homeassistant.setup import async_setup_component
from homeassistant.util import slugify
from homeassistant.util.dt import utcnow
from . import MOCK_STATUS, async_init_integration
from . import MOCK_MINIMAL_STATUS, MOCK_STATUS, async_init_integration
from tests.common import async_fire_time_changed
@ -237,3 +238,34 @@ async def test_multiple_manual_update_entity(hass: HomeAssistant) -> None:
blocking=True,
)
assert mock_request_status.call_count == 1
async def test_sensor_unknown(hass: HomeAssistant) -> None:
"""Test if our integration can properly certain sensors as unknown when it becomes so."""
await async_init_integration(hass, status=MOCK_MINIMAL_STATUS)
assert hass.states.get("sensor.mode").state == MOCK_MINIMAL_STATUS["UPSMODE"]
# Last self test sensor should be added even if our status does not report it initially (it is
# a sensor that appears only after a periodical or manual self test is performed).
assert hass.states.get("sensor.last_self_test") is not None
assert hass.states.get("sensor.last_self_test").state == STATE_UNKNOWN
# Simulate an event (a self test) such that "LASTSTEST" field is being reported, the state of
# the sensor should be properly updated with the corresponding value.
with patch("aioapcaccess.request_status") as mock_request_status:
mock_request_status.return_value = MOCK_MINIMAL_STATUS | {
"LASTSTEST": "1970-01-01 00:00:00 0000"
}
future = utcnow() + timedelta(minutes=2)
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
assert hass.states.get("sensor.last_self_test").state == "1970-01-01 00:00:00 0000"
# Simulate another event (e.g., daemon restart) such that "LASTSTEST" is no longer reported.
with patch("aioapcaccess.request_status") as mock_request_status:
mock_request_status.return_value = MOCK_MINIMAL_STATUS
future = utcnow() + timedelta(minutes=2)
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
# The state should become unknown again.
assert hass.states.get("sensor.last_self_test").state == STATE_UNKNOWN