Rewrite APCUPSD sensors using DataUpdateCoordinator (#88467)

* Add test sensor.

* Fix sensor test file name.

* Add binary sensor test.

* Fix comments and styling.

* Remove apcupsd from omissions in coveragerc.

* Revert "Remove apcupsd from omissions in coveragerc."

This reverts commit 66b05fcb8829619a771a650a3d70174089e15d91.

* Implement the data coordinator for apcupsd.

* Add tests for sensor updates and throttles.

* Reorder the statement for better code clarity.

* Update docstring.

* Add more tests for checking if the coordinator works ok.

* Implement a custom debouncer with 5 second cooldown for the coordinator.

* Add more tests for checking if our integration is able to properly mark entity's availability.

* Make apcupsd a silver integration.

* Try to fix non-deterministic test behaviors

* Fix JSON format

* Use new `with` format in python 3.10 for better readability

* Update tests.

* Rebase and simplify code.

* Add an ups prefix to the property methods of the coordinator

* Replace init_integration with async_init_integration

* Lint fixes

* Fix imports

* Update BinarySensor implementation to add initial update of attributes

* Fix test failures due to rebases

* Reorder the statements for better code clarity

* Fix incorrect references to the ups_name property

* Simplify BinarySensor value getter code

* No need to update when adding coordinator-controlled sensors
This commit is contained in:
Yuxin Wang
2023-11-21 16:40:05 -05:00
committed by GitHub
parent f45d373e17
commit 33c5d1855d
10 changed files with 296 additions and 142 deletions

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import logging
from typing import Final
from homeassistant.components.binary_sensor import (
BinarySensorEntity,
@@ -10,8 +11,9 @@ from homeassistant.components.binary_sensor import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import DOMAIN, VALUE_ONLINE, APCUPSdData
from . import DOMAIN, APCUPSdCoordinator
_LOGGER = logging.getLogger(__name__)
_DESCRIPTION = BinarySensorEntityDescription(
@@ -19,6 +21,8 @@ _DESCRIPTION = BinarySensorEntityDescription(
name="UPS Online Status",
icon="mdi:heart",
)
# The bit in STATFLAG that indicates the online status of the APC UPS.
_VALUE_ONLINE_MASK: Final = 0b1000
async def async_setup_entry(
@@ -27,50 +31,36 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up an APCUPSd Online Status binary sensor."""
data_service: APCUPSdData = hass.data[DOMAIN][config_entry.entry_id]
coordinator: APCUPSdCoordinator = hass.data[DOMAIN][config_entry.entry_id]
# Do not create the binary sensor if APCUPSd does not provide STATFLAG field for us
# to determine the online status.
if data_service.statflag is None:
if _DESCRIPTION.key.upper() not in coordinator.data:
return
async_add_entities(
[OnlineStatus(data_service, _DESCRIPTION)],
update_before_add=True,
)
async_add_entities([OnlineStatus(coordinator, _DESCRIPTION)])
class OnlineStatus(BinarySensorEntity):
class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity):
"""Representation of a UPS online status."""
def __init__(
self,
data_service: APCUPSdData,
coordinator: APCUPSdCoordinator,
description: BinarySensorEntityDescription,
) -> None:
"""Initialize the APCUPSd binary device."""
super().__init__(coordinator, context=description.key.upper())
# Set up unique id and device info if serial number is available.
if (serial_no := data_service.serial_no) is not None:
if (serial_no := coordinator.ups_serial_no) is not None:
self._attr_unique_id = f"{serial_no}_{description.key}"
self._attr_device_info = data_service.device_info
self.entity_description = description
self._data_service = data_service
self._attr_device_info = coordinator.device_info
def update(self) -> None:
"""Get the status report from APCUPSd and set this entity's state."""
try:
self._data_service.update()
except OSError as ex:
if self._attr_available:
self._attr_available = False
_LOGGER.exception("Got exception while fetching state: %s", ex)
return
self._attr_available = True
@property
def is_on(self) -> bool | None:
"""Returns true if the UPS is online."""
# Check if ONLINE bit is set in STATFLAG.
key = self.entity_description.key.upper()
if key not in self._data_service.status:
self._attr_is_on = None
return
self._attr_is_on = int(self._data_service.status[key], 16) & VALUE_ONLINE > 0
return int(self.coordinator.data[key], 16) & _VALUE_ONLINE_MASK != 0