mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
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:
parent
c8dccec956
commit
bf3a2cf393
@ -4,3 +4,6 @@ from typing import Final
|
|||||||
|
|
||||||
DOMAIN: Final = "apcupsd"
|
DOMAIN: Final = "apcupsd"
|
||||||
CONNECTION_TIMEOUT: int = 10
|
CONNECTION_TIMEOUT: int = 10
|
||||||
|
|
||||||
|
# Field name of last self test retrieved from apcupsd.
|
||||||
|
LASTSTEST: Final = "laststest"
|
||||||
|
@ -13,6 +13,7 @@ from homeassistant.components.sensor import (
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
|
STATE_UNKNOWN,
|
||||||
UnitOfApparentPower,
|
UnitOfApparentPower,
|
||||||
UnitOfElectricCurrent,
|
UnitOfElectricCurrent,
|
||||||
UnitOfElectricPotential,
|
UnitOfElectricPotential,
|
||||||
@ -25,7 +26,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN, LASTSTEST
|
||||||
from .coordinator import APCUPSdCoordinator
|
from .coordinator import APCUPSdCoordinator
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@ -156,8 +157,8 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
|||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
"laststest": SensorEntityDescription(
|
LASTSTEST: SensorEntityDescription(
|
||||||
key="laststest",
|
key=LASTSTEST,
|
||||||
translation_key="last_self_test",
|
translation_key="last_self_test",
|
||||||
),
|
),
|
||||||
"lastxfer": SensorEntityDescription(
|
"lastxfer": SensorEntityDescription(
|
||||||
@ -417,7 +418,12 @@ async def async_setup_entry(
|
|||||||
available_resources: set[str] = {k.lower() for k, _ in coordinator.data.items()}
|
available_resources: set[str] = {k.lower() for k, _ in coordinator.data.items()}
|
||||||
|
|
||||||
entities = []
|
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:
|
if resource not in SENSORS:
|
||||||
_LOGGER.warning("Invalid resource from APCUPSd: %s", resource.upper())
|
_LOGGER.warning("Invalid resource from APCUPSd: %s", resource.upper())
|
||||||
continue
|
continue
|
||||||
@ -473,6 +479,14 @@ class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity):
|
|||||||
def _update_attrs(self) -> None:
|
def _update_attrs(self) -> None:
|
||||||
"""Update sensor attributes based on coordinator data."""
|
"""Update sensor attributes based on coordinator data."""
|
||||||
key = self.entity_description.key.upper()
|
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])
|
self._attr_native_value, inferred_unit = infer_unit(self.coordinator.data[key])
|
||||||
if not self.native_unit_of_measurement:
|
if not self.native_unit_of_measurement:
|
||||||
self._attr_native_unit_of_measurement = inferred_unit
|
self._attr_native_unit_of_measurement = inferred_unit
|
||||||
|
@ -15,6 +15,7 @@ from homeassistant.const import (
|
|||||||
ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
|
STATE_UNKNOWN,
|
||||||
UnitOfElectricPotential,
|
UnitOfElectricPotential,
|
||||||
UnitOfPower,
|
UnitOfPower,
|
||||||
UnitOfTime,
|
UnitOfTime,
|
||||||
@ -25,7 +26,7 @@ from homeassistant.setup import async_setup_component
|
|||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
from homeassistant.util.dt import utcnow
|
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
|
from tests.common import async_fire_time_changed
|
||||||
|
|
||||||
@ -237,3 +238,34 @@ async def test_multiple_manual_update_entity(hass: HomeAssistant) -> None:
|
|||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
assert mock_request_status.call_count == 1
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user