Add config flow for nut (#33457)
* Convert nut to config flow * Add a test for importing * lint * Address review items (part 1) * Address review items (part 1) * Cleanup unique id handling * Update tests for new naming scheme * No unique id, no device_info * Remove sensor types * Update tests to use resources that still exist
This commit is contained in:
parent
2cfa0af532
commit
cc443ff37a
12 changed files with 774 additions and 215 deletions
|
@ -2,10 +2,10 @@
|
|||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pynut2.nut2 import PyNUTClient, PyNUTError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import (
|
||||
ATTR_STATE,
|
||||
CONF_ALIAS,
|
||||
|
@ -15,140 +15,33 @@ from homeassistant.const import (
|
|||
CONF_PORT,
|
||||
CONF_RESOURCES,
|
||||
CONF_USERNAME,
|
||||
POWER_WATT,
|
||||
STATE_UNKNOWN,
|
||||
TEMP_CELSIUS,
|
||||
TIME_SECONDS,
|
||||
UNIT_PERCENTAGE,
|
||||
)
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import (
|
||||
DEFAULT_HOST,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_PORT,
|
||||
DOMAIN,
|
||||
KEY_STATUS,
|
||||
KEY_STATUS_DISPLAY,
|
||||
PYNUT_DATA,
|
||||
PYNUT_FIRMWARE,
|
||||
PYNUT_MANUFACTURER,
|
||||
PYNUT_MODEL,
|
||||
PYNUT_STATUS,
|
||||
PYNUT_UNIQUE_ID,
|
||||
SENSOR_TYPES,
|
||||
STATE_TYPES,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "NUT UPS"
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_PORT = 3493
|
||||
|
||||
KEY_STATUS = "ups.status"
|
||||
KEY_STATUS_DISPLAY = "ups.status.display"
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
SENSOR_TYPES = {
|
||||
"ups.status.display": ["Status", "", "mdi:information-outline"],
|
||||
"ups.status": ["Status Data", "", "mdi:information-outline"],
|
||||
"ups.alarm": ["Alarms", "", "mdi:alarm"],
|
||||
"ups.time": ["Internal Time", "", "mdi:calendar-clock"],
|
||||
"ups.date": ["Internal Date", "", "mdi:calendar"],
|
||||
"ups.model": ["Model", "", "mdi:information-outline"],
|
||||
"ups.mfr": ["Manufacturer", "", "mdi:information-outline"],
|
||||
"ups.mfr.date": ["Manufacture Date", "", "mdi:calendar"],
|
||||
"ups.serial": ["Serial Number", "", "mdi:information-outline"],
|
||||
"ups.vendorid": ["Vendor ID", "", "mdi:information-outline"],
|
||||
"ups.productid": ["Product ID", "", "mdi:information-outline"],
|
||||
"ups.firmware": ["Firmware Version", "", "mdi:information-outline"],
|
||||
"ups.firmware.aux": ["Firmware Version 2", "", "mdi:information-outline"],
|
||||
"ups.temperature": ["UPS Temperature", TEMP_CELSIUS, "mdi:thermometer"],
|
||||
"ups.load": ["Load", UNIT_PERCENTAGE, "mdi:gauge"],
|
||||
"ups.load.high": ["Overload Setting", UNIT_PERCENTAGE, "mdi:gauge"],
|
||||
"ups.id": ["System identifier", "", "mdi:information-outline"],
|
||||
"ups.delay.start": ["Load Restart Delay", TIME_SECONDS, "mdi:timer"],
|
||||
"ups.delay.reboot": ["UPS Reboot Delay", TIME_SECONDS, "mdi:timer"],
|
||||
"ups.delay.shutdown": ["UPS Shutdown Delay", TIME_SECONDS, "mdi:timer"],
|
||||
"ups.timer.start": ["Load Start Timer", TIME_SECONDS, "mdi:timer"],
|
||||
"ups.timer.reboot": ["Load Reboot Timer", TIME_SECONDS, "mdi:timer"],
|
||||
"ups.timer.shutdown": ["Load Shutdown Timer", TIME_SECONDS, "mdi:timer"],
|
||||
"ups.test.interval": ["Self-Test Interval", TIME_SECONDS, "mdi:timer"],
|
||||
"ups.test.result": ["Self-Test Result", "", "mdi:information-outline"],
|
||||
"ups.test.date": ["Self-Test Date", "", "mdi:calendar"],
|
||||
"ups.display.language": ["Language", "", "mdi:information-outline"],
|
||||
"ups.contacts": ["External Contacts", "", "mdi:information-outline"],
|
||||
"ups.efficiency": ["Efficiency", UNIT_PERCENTAGE, "mdi:gauge"],
|
||||
"ups.power": ["Current Apparent Power", "VA", "mdi:flash"],
|
||||
"ups.power.nominal": ["Nominal Power", "VA", "mdi:flash"],
|
||||
"ups.realpower": ["Current Real Power", POWER_WATT, "mdi:flash"],
|
||||
"ups.realpower.nominal": ["Nominal Real Power", POWER_WATT, "mdi:flash"],
|
||||
"ups.beeper.status": ["Beeper Status", "", "mdi:information-outline"],
|
||||
"ups.type": ["UPS Type", "", "mdi:information-outline"],
|
||||
"ups.watchdog.status": ["Watchdog Status", "", "mdi:information-outline"],
|
||||
"ups.start.auto": ["Start on AC", "", "mdi:information-outline"],
|
||||
"ups.start.battery": ["Start on Battery", "", "mdi:information-outline"],
|
||||
"ups.start.reboot": ["Reboot on Battery", "", "mdi:information-outline"],
|
||||
"ups.shutdown": ["Shutdown Ability", "", "mdi:information-outline"],
|
||||
"battery.charge": ["Battery Charge", UNIT_PERCENTAGE, "mdi:gauge"],
|
||||
"battery.charge.low": ["Low Battery Setpoint", UNIT_PERCENTAGE, "mdi:gauge"],
|
||||
"battery.charge.restart": [
|
||||
"Minimum Battery to Start",
|
||||
UNIT_PERCENTAGE,
|
||||
"mdi:gauge",
|
||||
],
|
||||
"battery.charge.warning": [
|
||||
"Warning Battery Setpoint",
|
||||
UNIT_PERCENTAGE,
|
||||
"mdi:gauge",
|
||||
],
|
||||
"battery.charger.status": ["Charging Status", "", "mdi:information-outline"],
|
||||
"battery.voltage": ["Battery Voltage", "V", "mdi:flash"],
|
||||
"battery.voltage.nominal": ["Nominal Battery Voltage", "V", "mdi:flash"],
|
||||
"battery.voltage.low": ["Low Battery Voltage", "V", "mdi:flash"],
|
||||
"battery.voltage.high": ["High Battery Voltage", "V", "mdi:flash"],
|
||||
"battery.capacity": ["Battery Capacity", "Ah", "mdi:flash"],
|
||||
"battery.current": ["Battery Current", "A", "mdi:flash"],
|
||||
"battery.current.total": ["Total Battery Current", "A", "mdi:flash"],
|
||||
"battery.temperature": ["Battery Temperature", TEMP_CELSIUS, "mdi:thermometer"],
|
||||
"battery.runtime": ["Battery Runtime", TIME_SECONDS, "mdi:timer"],
|
||||
"battery.runtime.low": ["Low Battery Runtime", TIME_SECONDS, "mdi:timer"],
|
||||
"battery.runtime.restart": [
|
||||
"Minimum Battery Runtime to Start",
|
||||
TIME_SECONDS,
|
||||
"mdi:timer",
|
||||
],
|
||||
"battery.alarm.threshold": [
|
||||
"Battery Alarm Threshold",
|
||||
"",
|
||||
"mdi:information-outline",
|
||||
],
|
||||
"battery.date": ["Battery Date", "", "mdi:calendar"],
|
||||
"battery.mfr.date": ["Battery Manuf. Date", "", "mdi:calendar"],
|
||||
"battery.packs": ["Number of Batteries", "", "mdi:information-outline"],
|
||||
"battery.packs.bad": ["Number of Bad Batteries", "", "mdi:information-outline"],
|
||||
"battery.type": ["Battery Chemistry", "", "mdi:information-outline"],
|
||||
"input.sensitivity": ["Input Power Sensitivity", "", "mdi:information-outline"],
|
||||
"input.transfer.low": ["Low Voltage Transfer", "V", "mdi:flash"],
|
||||
"input.transfer.high": ["High Voltage Transfer", "V", "mdi:flash"],
|
||||
"input.transfer.reason": ["Voltage Transfer Reason", "", "mdi:information-outline"],
|
||||
"input.voltage": ["Input Voltage", "V", "mdi:flash"],
|
||||
"input.voltage.nominal": ["Nominal Input Voltage", "V", "mdi:flash"],
|
||||
"input.frequency": ["Input Line Frequency", "hz", "mdi:flash"],
|
||||
"input.frequency.nominal": ["Nominal Input Line Frequency", "hz", "mdi:flash"],
|
||||
"input.frequency.status": ["Input Frequency Status", "", "mdi:information-outline"],
|
||||
"output.current": ["Output Current", "A", "mdi:flash"],
|
||||
"output.current.nominal": ["Nominal Output Current", "A", "mdi:flash"],
|
||||
"output.voltage": ["Output Voltage", "V", "mdi:flash"],
|
||||
"output.voltage.nominal": ["Nominal Output Voltage", "V", "mdi:flash"],
|
||||
"output.frequency": ["Output Frequency", "hz", "mdi:flash"],
|
||||
"output.frequency.nominal": ["Nominal Output Frequency", "hz", "mdi:flash"],
|
||||
}
|
||||
|
||||
STATE_TYPES = {
|
||||
"OL": "Online",
|
||||
"OB": "On Battery",
|
||||
"LB": "Low Battery",
|
||||
"HB": "High Battery",
|
||||
"RB": "Battery Needs Replaced",
|
||||
"CHRG": "Battery Charging",
|
||||
"DISCHRG": "Battery Discharging",
|
||||
"BYPASS": "Bypass Active",
|
||||
"CAL": "Runtime Calibration",
|
||||
"OFF": "Offline",
|
||||
"OVER": "Overloaded",
|
||||
"TRIM": "Trimming Voltage",
|
||||
"BOOST": "Boosting Voltage",
|
||||
"FSD": "Forced Shutdown",
|
||||
"ALARM": "Alarm",
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
|
@ -164,34 +57,48 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Import the platform into a config entry."""
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the NUT sensors."""
|
||||
name = config[CONF_NAME]
|
||||
host = config[CONF_HOST]
|
||||
port = config[CONF_PORT]
|
||||
|
||||
alias = config.get(CONF_ALIAS)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
data = PyNUTData(host, port, alias, username, password)
|
||||
|
||||
if data.status is None:
|
||||
_LOGGER.error("NUT Sensor has no data, unable to set up")
|
||||
raise PlatformNotReady
|
||||
|
||||
_LOGGER.debug("NUT Sensors Available: %s", data.status)
|
||||
config = config_entry.data
|
||||
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]
|
||||
manufacturer = pynut_data[PYNUT_MANUFACTURER]
|
||||
model = pynut_data[PYNUT_MODEL]
|
||||
firmware = pynut_data[PYNUT_FIRMWARE]
|
||||
|
||||
entities = []
|
||||
|
||||
for resource in config[CONF_RESOURCES]:
|
||||
name = config[CONF_NAME]
|
||||
if CONF_RESOURCES in config_entry.options:
|
||||
resources = config_entry.options[CONF_RESOURCES]
|
||||
else:
|
||||
resources = config_entry.data[CONF_RESOURCES]
|
||||
|
||||
for resource in resources:
|
||||
sensor_type = resource.lower()
|
||||
|
||||
# Display status is a special case that falls back to the status value
|
||||
# of the UPS instead.
|
||||
if sensor_type in data.status or (
|
||||
sensor_type == KEY_STATUS_DISPLAY and KEY_STATUS in data.status
|
||||
if sensor_type in status or (
|
||||
sensor_type == KEY_STATUS_DISPLAY and KEY_STATUS in status
|
||||
):
|
||||
entities.append(NUTSensor(name, data, sensor_type))
|
||||
entities.append(
|
||||
NUTSensor(
|
||||
name, data, sensor_type, unique_id, manufacturer, model, firmware
|
||||
)
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Sensor type: %s does not appear in the NUT status "
|
||||
|
@ -199,30 +106,53 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
sensor_type,
|
||||
)
|
||||
|
||||
try:
|
||||
data.update(no_throttle=True)
|
||||
except data.pynuterror as err:
|
||||
_LOGGER.error(
|
||||
"Failure while testing NUT status retrieval. Cannot continue setup: %s", err
|
||||
)
|
||||
raise PlatformNotReady
|
||||
|
||||
add_entities(entities, True)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class NUTSensor(Entity):
|
||||
"""Representation of a sensor entity for NUT status values."""
|
||||
|
||||
def __init__(self, name, data, sensor_type):
|
||||
def __init__(
|
||||
self, name, data, sensor_type, unique_id, manufacturer, model, firmware
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
self._data = data
|
||||
self.type = sensor_type
|
||||
self._type = sensor_type
|
||||
self._manufacturer = manufacturer
|
||||
self._firmware = firmware
|
||||
self._model = model
|
||||
self._device_name = name
|
||||
self._name = "{} {}".format(name, SENSOR_TYPES[sensor_type][0])
|
||||
self._unit = SENSOR_TYPES[sensor_type][1]
|
||||
self._state = None
|
||||
self._unique_id = unique_id
|
||||
self._display_state = None
|
||||
self._available = False
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Device info for the ups."""
|
||||
if not self._unique_id:
|
||||
return None
|
||||
device_info = {
|
||||
"identifiers": {(DOMAIN, self._unique_id)},
|
||||
"name": self._device_name,
|
||||
}
|
||||
if self._model:
|
||||
device_info["model"] = self._model
|
||||
if self._manufacturer:
|
||||
device_info["manufacturer"] = self._manufacturer
|
||||
if self._firmware:
|
||||
device_info["sw_version"] = self._firmware
|
||||
return device_info
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Sensor Unique id."""
|
||||
if not self._unique_id:
|
||||
return None
|
||||
return f"{self._unique_id}_{self._type}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the UPS sensor."""
|
||||
|
@ -231,7 +161,7 @@ class NUTSensor(Entity):
|
|||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return SENSOR_TYPES[self.type][2]
|
||||
return SENSOR_TYPES[self._type][2]
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
@ -265,12 +195,12 @@ class NUTSensor(Entity):
|
|||
self._display_state = _format_display_state(status)
|
||||
# In case of the display status sensor, keep a human-readable form
|
||||
# as the sensor state.
|
||||
if self.type == KEY_STATUS_DISPLAY:
|
||||
if self._type == KEY_STATUS_DISPLAY:
|
||||
self._state = self._display_state
|
||||
elif self.type not in status:
|
||||
elif self._type not in status:
|
||||
self._state = None
|
||||
else:
|
||||
self._state = status[self.type]
|
||||
self._state = status[self._type]
|
||||
|
||||
|
||||
def _format_display_state(status):
|
||||
|
@ -281,58 +211,3 @@ def _format_display_state(status):
|
|||
return " ".join(STATE_TYPES[state] for state in status[KEY_STATUS].split())
|
||||
except KeyError:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
|
||||
class PyNUTData:
|
||||
"""Stores the data retrieved from NUT.
|
||||
|
||||
For each entity to use, acts as the single point responsible for fetching
|
||||
updates from the server.
|
||||
"""
|
||||
|
||||
def __init__(self, host, port, alias, username, password):
|
||||
"""Initialize the data object."""
|
||||
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._alias = alias
|
||||
self._username = username
|
||||
self._password = password
|
||||
|
||||
self.pynuterror = PyNUTError
|
||||
# Establish client with persistent=False to open/close connection on
|
||||
# each update call. This is more reliable with async.
|
||||
self._client = PyNUTClient(
|
||||
self._host, self._port, self._username, self._password, 5, False
|
||||
)
|
||||
|
||||
self._status = None
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Get latest update if throttle allows. Return status."""
|
||||
self.update()
|
||||
return self._status
|
||||
|
||||
def _get_alias(self):
|
||||
"""Get the ups alias from NUT."""
|
||||
try:
|
||||
return next(iter(self._client.list_ups()))
|
||||
except self.pynuterror as err:
|
||||
_LOGGER.error("Failure getting NUT ups alias, %s", err)
|
||||
return None
|
||||
|
||||
def _get_status(self):
|
||||
"""Get the ups status from NUT."""
|
||||
if self._alias is None:
|
||||
self._alias = self._get_alias()
|
||||
|
||||
try:
|
||||
return self._client.list_vars(self._alias)
|
||||
except (self.pynuterror, ConnectionResetError) as err:
|
||||
_LOGGER.debug("Error getting NUT vars for host %s: %s", self._host, err)
|
||||
return None
|
||||
|
||||
def update(self, **kwargs):
|
||||
"""Fetch the latest status from NUT."""
|
||||
self._status = self._get_status()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue