Update nut to use DataUpdateCoordinator (#33831)
* Convert nut to use DataUpdateCoordinator * Adjust per review * ups_list is a dict with {id: name, ...}
This commit is contained in:
parent
bdb998bdb3
commit
8be7cb4539
5 changed files with 81 additions and 61 deletions
|
@ -1,7 +1,9 @@
|
||||||
"""The nut component."""
|
"""The nut component."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import async_timeout
|
||||||
from pynut2.nut2 import PyNUTClient, PyNUTError
|
from pynut2.nut2 import PyNUTClient, PyNUTError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
@ -15,8 +17,10 @@ from homeassistant.const import (
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
COORDINATOR,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
PYNUT_DATA,
|
PYNUT_DATA,
|
||||||
|
@ -24,7 +28,6 @@ from .const import (
|
||||||
PYNUT_MANUFACTURER,
|
PYNUT_MANUFACTURER,
|
||||||
PYNUT_MODEL,
|
PYNUT_MODEL,
|
||||||
PYNUT_NAME,
|
PYNUT_NAME,
|
||||||
PYNUT_STATUS,
|
|
||||||
PYNUT_UNIQUE_ID,
|
PYNUT_UNIQUE_ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,7 +54,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
|
||||||
data = PyNUTData(host, port, alias, username, password)
|
data = PyNUTData(host, port, alias, username, password)
|
||||||
|
|
||||||
status = await hass.async_add_executor_job(pynutdata_status, data)
|
async def async_update_data():
|
||||||
|
"""Fetch data from NUT."""
|
||||||
|
async with async_timeout.timeout(10):
|
||||||
|
return await hass.async_add_executor_job(data.update)
|
||||||
|
|
||||||
|
coordinator = DataUpdateCoordinator(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name="NUT resource status",
|
||||||
|
update_method=async_update_data,
|
||||||
|
update_interval=timedelta(seconds=60),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch initial data so we have data when entities subscribe
|
||||||
|
await coordinator.async_refresh()
|
||||||
|
status = data.status
|
||||||
|
|
||||||
if not status:
|
if not status:
|
||||||
_LOGGER.error("NUT Sensor has no data, unable to set up")
|
_LOGGER.error("NUT Sensor has no data, unable to set up")
|
||||||
|
@ -60,8 +78,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
_LOGGER.debug("NUT Sensors Available: %s", status)
|
_LOGGER.debug("NUT Sensors Available: %s", status)
|
||||||
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = {
|
hass.data[DOMAIN][entry.entry_id] = {
|
||||||
|
COORDINATOR: coordinator,
|
||||||
PYNUT_DATA: data,
|
PYNUT_DATA: data,
|
||||||
PYNUT_STATUS: status,
|
|
||||||
PYNUT_UNIQUE_ID: _unique_id_from_status(status),
|
PYNUT_UNIQUE_ID: _unique_id_from_status(status),
|
||||||
PYNUT_MANUFACTURER: _manufacturer_from_status(status),
|
PYNUT_MANUFACTURER: _manufacturer_from_status(status),
|
||||||
PYNUT_MODEL: _model_from_status(status),
|
PYNUT_MODEL: _model_from_status(status),
|
||||||
|
@ -143,11 +161,6 @@ def find_resources_in_config_entry(config_entry):
|
||||||
return config_entry.data[CONF_RESOURCES]
|
return config_entry.data[CONF_RESOURCES]
|
||||||
|
|
||||||
|
|
||||||
def pynutdata_status(data):
|
|
||||||
"""Wrap for data update as a callable."""
|
|
||||||
return data.status
|
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
unload_ok = all(
|
unload_ok = all(
|
||||||
|
@ -180,12 +193,12 @@ class PyNUTData:
|
||||||
# Establish client with persistent=False to open/close connection on
|
# Establish client with persistent=False to open/close connection on
|
||||||
# each update call. This is more reliable with async.
|
# each update call. This is more reliable with async.
|
||||||
self._client = PyNUTClient(self._host, port, username, password, 5, False)
|
self._client = PyNUTClient(self._host, port, username, password, 5, False)
|
||||||
|
self.ups_list = None
|
||||||
self._status = None
|
self._status = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def status(self):
|
||||||
"""Get latest update if throttle allows. Return status."""
|
"""Get latest update if throttle allows. Return status."""
|
||||||
self.update()
|
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -193,18 +206,21 @@ class PyNUTData:
|
||||||
"""Return the name of the ups."""
|
"""Return the name of the ups."""
|
||||||
return self._alias
|
return self._alias
|
||||||
|
|
||||||
def list_ups(self):
|
|
||||||
"""List UPSes connected to the NUT server."""
|
|
||||||
return self._client.list_ups()
|
|
||||||
|
|
||||||
def _get_alias(self):
|
def _get_alias(self):
|
||||||
"""Get the ups alias from NUT."""
|
"""Get the ups alias from NUT."""
|
||||||
try:
|
try:
|
||||||
return next(iter(self.list_ups()))
|
ups_list = self._client.list_ups()
|
||||||
except PyNUTError as err:
|
except PyNUTError as err:
|
||||||
_LOGGER.error("Failure getting NUT ups alias, %s", err)
|
_LOGGER.error("Failure getting NUT ups alias, %s", err)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if not ups_list:
|
||||||
|
_LOGGER.error("Empty list while getting NUT ups aliases")
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.ups_list = ups_list
|
||||||
|
return list(ups_list)[0]
|
||||||
|
|
||||||
def _get_status(self):
|
def _get_status(self):
|
||||||
"""Get the ups status from NUT."""
|
"""Get the ups status from NUT."""
|
||||||
if self._alias is None:
|
if self._alias is None:
|
||||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
from . import PyNUTData, find_resources_in_config_entry, pynutdata_status
|
from . import PyNUTData, find_resources_in_config_entry
|
||||||
from .const import DEFAULT_HOST, DEFAULT_PORT, SENSOR_TYPES
|
from .const import DEFAULT_HOST, DEFAULT_PORT, SENSOR_TYPES
|
||||||
from .const import DOMAIN # pylint:disable=unused-import
|
from .const import DOMAIN # pylint:disable=unused-import
|
||||||
|
|
||||||
|
@ -54,9 +54,7 @@ def _resource_schema(available_resources, selected_resources):
|
||||||
|
|
||||||
def _ups_schema(ups_list):
|
def _ups_schema(ups_list):
|
||||||
"""UPS selection schema."""
|
"""UPS selection schema."""
|
||||||
ups_map = {ups: ups for ups in ups_list}
|
return vol.Schema({vol.Required(CONF_ALIAS): vol.In(ups_list)})
|
||||||
|
|
||||||
return vol.Schema({vol.Required(CONF_ALIAS): vol.In(ups_map)})
|
|
||||||
|
|
||||||
|
|
||||||
async def validate_input(hass: core.HomeAssistant, data):
|
async def validate_input(hass: core.HomeAssistant, data):
|
||||||
|
@ -72,16 +70,12 @@ async def validate_input(hass: core.HomeAssistant, data):
|
||||||
password = data.get(CONF_PASSWORD)
|
password = data.get(CONF_PASSWORD)
|
||||||
|
|
||||||
data = PyNUTData(host, port, alias, username, password)
|
data = PyNUTData(host, port, alias, username, password)
|
||||||
|
await hass.async_add_executor_job(data.update)
|
||||||
ups_list = await hass.async_add_executor_job(data.list_ups)
|
status = data.status
|
||||||
if not ups_list:
|
|
||||||
raise CannotConnect
|
|
||||||
|
|
||||||
status = await hass.async_add_executor_job(pynutdata_status, data)
|
|
||||||
if not status:
|
if not status:
|
||||||
raise CannotConnect
|
raise CannotConnect
|
||||||
|
|
||||||
return {"ups_list": ups_list, "available_resources": status}
|
return {"ups_list": data.ups_list, "available_resources": status}
|
||||||
|
|
||||||
|
|
||||||
def _format_host_port_alias(user_input):
|
def _format_host_port_alias(user_input):
|
||||||
|
@ -135,6 +129,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
self.ups_list = info["ups_list"]
|
self.ups_list = info["ups_list"]
|
||||||
return await self.async_step_ups()
|
return await self.async_step_ups()
|
||||||
|
|
||||||
|
if self._host_port_alias_already_configured(self.nut_config):
|
||||||
|
return self.async_abort(reason="already_configured")
|
||||||
self.available_resources.update(info["available_resources"])
|
self.available_resources.update(info["available_resources"])
|
||||||
return await self.async_step_resources()
|
return await self.async_step_resources()
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,9 @@ DEFAULT_PORT = 3493
|
||||||
KEY_STATUS = "ups.status"
|
KEY_STATUS = "ups.status"
|
||||||
KEY_STATUS_DISPLAY = "ups.status.display"
|
KEY_STATUS_DISPLAY = "ups.status.display"
|
||||||
|
|
||||||
|
COORDINATOR = "coordinator"
|
||||||
|
|
||||||
PYNUT_DATA = "data"
|
PYNUT_DATA = "data"
|
||||||
PYNUT_STATUS = "status"
|
|
||||||
PYNUT_UNIQUE_ID = "unique_id"
|
PYNUT_UNIQUE_ID = "unique_id"
|
||||||
PYNUT_MANUFACTURER = "manufacturer"
|
PYNUT_MANUFACTURER = "manufacturer"
|
||||||
PYNUT_MODEL = "model"
|
PYNUT_MODEL = "model"
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""Provides a sensor to track various status aspects of a UPS."""
|
"""Provides a sensor to track various status aspects of a UPS."""
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -21,6 +20,7 @@ import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
COORDINATOR,
|
||||||
DEFAULT_HOST,
|
DEFAULT_HOST,
|
||||||
DEFAULT_NAME,
|
DEFAULT_NAME,
|
||||||
DEFAULT_PORT,
|
DEFAULT_PORT,
|
||||||
|
@ -32,7 +32,6 @@ from .const import (
|
||||||
PYNUT_MANUFACTURER,
|
PYNUT_MANUFACTURER,
|
||||||
PYNUT_MODEL,
|
PYNUT_MODEL,
|
||||||
PYNUT_NAME,
|
PYNUT_NAME,
|
||||||
PYNUT_STATUS,
|
|
||||||
PYNUT_UNIQUE_ID,
|
PYNUT_UNIQUE_ID,
|
||||||
SENSOR_DEVICE_CLASS,
|
SENSOR_DEVICE_CLASS,
|
||||||
SENSOR_ICON,
|
SENSOR_ICON,
|
||||||
|
@ -45,9 +44,6 @@ from .const import (
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=60)
|
|
||||||
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
@ -75,13 +71,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up the NUT sensors."""
|
"""Set up the NUT sensors."""
|
||||||
|
|
||||||
pynut_data = hass.data[DOMAIN][config_entry.entry_id]
|
pynut_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
data = pynut_data[PYNUT_DATA]
|
|
||||||
status = pynut_data[PYNUT_STATUS]
|
|
||||||
unique_id = pynut_data[PYNUT_UNIQUE_ID]
|
unique_id = pynut_data[PYNUT_UNIQUE_ID]
|
||||||
manufacturer = pynut_data[PYNUT_MANUFACTURER]
|
manufacturer = pynut_data[PYNUT_MANUFACTURER]
|
||||||
model = pynut_data[PYNUT_MODEL]
|
model = pynut_data[PYNUT_MODEL]
|
||||||
firmware = pynut_data[PYNUT_FIRMWARE]
|
firmware = pynut_data[PYNUT_FIRMWARE]
|
||||||
name = pynut_data[PYNUT_NAME]
|
name = pynut_data[PYNUT_NAME]
|
||||||
|
coordinator = pynut_data[COORDINATOR]
|
||||||
|
data = pynut_data[PYNUT_DATA]
|
||||||
|
status = data.status
|
||||||
|
|
||||||
entities = []
|
entities = []
|
||||||
|
|
||||||
|
@ -100,8 +97,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
):
|
):
|
||||||
entities.append(
|
entities.append(
|
||||||
NUTSensor(
|
NUTSensor(
|
||||||
name.title(),
|
coordinator,
|
||||||
data,
|
data,
|
||||||
|
name.title(),
|
||||||
sensor_type,
|
sensor_type,
|
||||||
unique_id,
|
unique_id,
|
||||||
manufacturer,
|
manufacturer,
|
||||||
|
@ -123,10 +121,18 @@ class NUTSensor(Entity):
|
||||||
"""Representation of a sensor entity for NUT status values."""
|
"""Representation of a sensor entity for NUT status values."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, name, data, sensor_type, unique_id, manufacturer, model, firmware
|
self,
|
||||||
|
coordinator,
|
||||||
|
data,
|
||||||
|
name,
|
||||||
|
sensor_type,
|
||||||
|
unique_id,
|
||||||
|
manufacturer,
|
||||||
|
model,
|
||||||
|
firmware,
|
||||||
):
|
):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._data = data
|
self._coordinator = coordinator
|
||||||
self._type = sensor_type
|
self._type = sensor_type
|
||||||
self._manufacturer = manufacturer
|
self._manufacturer = manufacturer
|
||||||
self._firmware = firmware
|
self._firmware = firmware
|
||||||
|
@ -134,10 +140,8 @@ class NUTSensor(Entity):
|
||||||
self._device_name = name
|
self._device_name = name
|
||||||
self._name = f"{name} {SENSOR_TYPES[sensor_type][SENSOR_NAME]}"
|
self._name = f"{name} {SENSOR_TYPES[sensor_type][SENSOR_NAME]}"
|
||||||
self._unit = SENSOR_TYPES[sensor_type][SENSOR_UNIT]
|
self._unit = SENSOR_TYPES[sensor_type][SENSOR_UNIT]
|
||||||
self._state = None
|
self._data = data
|
||||||
self._unique_id = unique_id
|
self._unique_id = unique_id
|
||||||
self._display_state = None
|
|
||||||
self._available = False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_info(self):
|
def device_info(self):
|
||||||
|
@ -185,41 +189,42 @@ class NUTSensor(Entity):
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return entity state from ups."""
|
"""Return entity state from ups."""
|
||||||
return self._state
|
if self._type == KEY_STATUS_DISPLAY:
|
||||||
|
return _format_display_state(self._data.status)
|
||||||
|
return self._data.status.get(self._type)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
"""Return the unit of measurement of this entity, if any."""
|
"""Return the unit of measurement of this entity, if any."""
|
||||||
return self._unit
|
return self._unit
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""No need to poll. Coordinator notifies entity of updates."""
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
"""Return if the device is polling successfully."""
|
"""Return if entity is available."""
|
||||||
return self._available
|
return self._coordinator.last_update_success
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the sensor attributes."""
|
"""Return the sensor attributes."""
|
||||||
return {ATTR_STATE: self._display_state}
|
return {ATTR_STATE: _format_display_state(self._data.status)}
|
||||||
|
|
||||||
def update(self):
|
async def async_update(self):
|
||||||
"""Get the latest status and use it to update our sensor state."""
|
"""Update the entity.
|
||||||
status = self._data.status
|
|
||||||
|
|
||||||
if status is None:
|
Only used by the generic entity update service.
|
||||||
self._available = False
|
"""
|
||||||
return
|
await self._coordinator.async_request_refresh()
|
||||||
|
|
||||||
self._available = True
|
async def async_added_to_hass(self):
|
||||||
self._display_state = _format_display_state(status)
|
"""When entity is added to hass."""
|
||||||
# In case of the display status sensor, keep a human-readable form
|
self.async_on_remove(
|
||||||
# as the sensor state.
|
self._coordinator.async_add_listener(self.async_write_ha_state)
|
||||||
if self._type == KEY_STATUS_DISPLAY:
|
)
|
||||||
self._state = self._display_state
|
|
||||||
elif self._type not in status:
|
|
||||||
self._state = None
|
|
||||||
else:
|
|
||||||
self._state = status[self._type]
|
|
||||||
|
|
||||||
|
|
||||||
def _format_display_state(status):
|
def _format_display_state(status):
|
||||||
|
|
|
@ -86,7 +86,8 @@ async def test_form_user_multiple_ups(hass):
|
||||||
assert result["errors"] == {}
|
assert result["errors"] == {}
|
||||||
|
|
||||||
mock_pynut = _get_mock_pynutclient(
|
mock_pynut = _get_mock_pynutclient(
|
||||||
list_vars={"battery.voltage": "voltage"}, list_ups=["ups1", "ups2"]
|
list_vars={"battery.voltage": "voltage"},
|
||||||
|
list_ups={"ups1": "UPS 1", "ups2": "UPS2"},
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
|
@ -146,7 +147,8 @@ async def test_form_import(hass):
|
||||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
mock_pynut = _get_mock_pynutclient(
|
mock_pynut = _get_mock_pynutclient(
|
||||||
list_vars={"battery.voltage": "serial"}, list_ups=["ups1"]
|
list_vars={"battery.voltage": "serial"},
|
||||||
|
list_ups={"ups1": "UPS 1", "ups2": "UPS2"},
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue