Refactor apcupsd to use config flow (#64809)
* Add Config Flow to APCUPSd integration and remove YAML support. * Hide the binary sensor if user does not select STATFLAG resource. * Add tests for config flows. * Simplify config flow code. * Spell fix. * Fix pylint warnings. * Simplify the code for config flow. * First attempt to implement import flows to suppport legacy YAML configurations. * Remove unnecessary log calls. * Wrap synchronous update call with `hass.async_add_executor_job`. * Import the YAML configurations when sensor platform is set up. * Move the logger call since the variables are not properly set up. * Add codeowner. * Fix name field of manifest.json. * Fix linting issue. * Fix incorrect dependency due to incorrect rebase. * Update codeowner and config flows via hassfest. * Postpone the deprecation warning to 2022.7. * Import future annotations for init file. * Add an newline at the end to make prettier happy. * Update github id. * Add type hints for return types of steps in config flow. * Move the deprecation date for YAML config to 2022.12. * Update according to reviews. * Use async_forward_entry_setups. * Add helper properties to `APCUPSdData` class. * Add device_info for binary sensor. * Simplify config flow. * Remove options flow strings. * update the tests according to the changes. * Add `entity_registry_enabled_default` to entities and use imported CONF_RESOURCES to disable entities instead of skipping them. * Update according to reviews. * Do not use model of the UPS as the title for the integration. Instead, simply use "APCUPSd" as the integration title and let the device info serve as title for each device instead. * Change schema to be a global variable. * Add more comments. * Rewrite the tests for config flows. * Fix enabled_by_default. * Show friendly titles in the integration. * Add import check in `async_setup_platform` to avoid importing in sensor platform setup. * Add import check in `async_setup_platform` to avoid importing in sensor platform setup. * Update comments in test files. * Use parametrize instead of manually iterating different test cases. * Swap the order of the platform constants. * Avoid using broad exceptions. * Set up device info via `_attr_device_info`. * Remove unrelated test in `test_config_flow`. * Use `DeviceInfo` instead of dict to assign to `_attr_device_info`. * Add english translation. * Add `async_create_issue` for deprecated YAML configuration. * Enable UPS status by default since it could show "online, charging, on battery etc" which is meaningful for all users. * Apply suggestions from code review * Apply suggestion * Apply suggestion Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
18be5f1387
commit
52307708c8
13 changed files with 838 additions and 224 deletions
|
@ -62,7 +62,9 @@ omit =
|
||||||
homeassistant/components/androidtv/diagnostics.py
|
homeassistant/components/androidtv/diagnostics.py
|
||||||
homeassistant/components/anel_pwrctrl/switch.py
|
homeassistant/components/anel_pwrctrl/switch.py
|
||||||
homeassistant/components/anthemav/media_player.py
|
homeassistant/components/anthemav/media_player.py
|
||||||
homeassistant/components/apcupsd/*
|
homeassistant/components/apcupsd/__init__.py
|
||||||
|
homeassistant/components/apcupsd/binary_sensor.py
|
||||||
|
homeassistant/components/apcupsd/sensor.py
|
||||||
homeassistant/components/apple_tv/__init__.py
|
homeassistant/components/apple_tv/__init__.py
|
||||||
homeassistant/components/apple_tv/browse_media.py
|
homeassistant/components/apple_tv/browse_media.py
|
||||||
homeassistant/components/apple_tv/media_player.py
|
homeassistant/components/apple_tv/media_player.py
|
||||||
|
|
|
@ -78,6 +78,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/anthemav/ @hyralex
|
/tests/components/anthemav/ @hyralex
|
||||||
/homeassistant/components/apache_kafka/ @bachya
|
/homeassistant/components/apache_kafka/ @bachya
|
||||||
/tests/components/apache_kafka/ @bachya
|
/tests/components/apache_kafka/ @bachya
|
||||||
|
/homeassistant/components/apcupsd/ @yuxincs
|
||||||
|
/tests/components/apcupsd/ @yuxincs
|
||||||
/homeassistant/components/api/ @home-assistant/core
|
/homeassistant/components/api/ @home-assistant/core
|
||||||
/tests/components/api/ @home-assistant/core
|
/tests/components/api/ @home-assistant/core
|
||||||
/homeassistant/components/apple_tv/ @postlund
|
/homeassistant/components/apple_tv/ @postlund
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
"""Support for APCUPSd via its Network Information Server (NIS)."""
|
"""Support for APCUPSd via its Network Information Server (NIS)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
from apcaccess import status
|
from apcaccess import status
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
@ -13,22 +17,18 @@ from homeassistant.util import Throttle
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_HOST = "localhost"
|
DOMAIN: Final = "apcupsd"
|
||||||
DEFAULT_PORT = 3551
|
VALUE_ONLINE: Final = 8
|
||||||
DOMAIN = "apcupsd"
|
PLATFORMS: Final = (Platform.BINARY_SENSOR, Platform.SENSOR)
|
||||||
|
MIN_TIME_BETWEEN_UPDATES: Final = timedelta(seconds=60)
|
||||||
|
|
||||||
KEY_STATUS = "STATFLAG"
|
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
|
||||||
|
|
||||||
VALUE_ONLINE = 8
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
DOMAIN: vol.Schema(
|
DOMAIN: vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
vol.Optional(CONF_HOST, default="localhost"): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
vol.Optional(CONF_PORT, default=3551): cv.port,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -36,25 +36,67 @@ CONFIG_SCHEMA = vol.Schema(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Use config values to set up a function enabling status retrieval."""
|
"""Set up integration from legacy YAML configurations."""
|
||||||
conf = config[DOMAIN]
|
conf = config.get(DOMAIN)
|
||||||
host = conf[CONF_HOST]
|
if conf is None:
|
||||||
port = conf[CONF_PORT]
|
return True
|
||||||
|
|
||||||
apcups_data = APCUPSdData(host, port)
|
# We only import configs from YAML if it hasn't been imported. If there is a config
|
||||||
hass.data[DOMAIN] = apcups_data
|
# entry marked with SOURCE_IMPORT, it means the YAML config has been imported.
|
||||||
|
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||||
|
if entry.source == SOURCE_IMPORT:
|
||||||
|
return True
|
||||||
|
|
||||||
# It doesn't really matter why we're not able to get the status, just that
|
# Since the YAML configuration for apcupsd consists of two parts:
|
||||||
# we can't.
|
# apcupsd:
|
||||||
try:
|
# host: xxx
|
||||||
apcups_data.update(no_throttle=True)
|
# port: xxx
|
||||||
except Exception: # pylint: disable=broad-except
|
# sensor:
|
||||||
_LOGGER.exception("Failure while testing APCUPSd status retrieval")
|
# - platform: apcupsd
|
||||||
return False
|
# resource:
|
||||||
|
# - resource_1
|
||||||
|
# - resource_2
|
||||||
|
# - ...
|
||||||
|
# Here at the integration set up we do not have the entire information to be
|
||||||
|
# imported to config flow yet. So we temporarily store the configuration to
|
||||||
|
# hass.data[DOMAIN] under a special entry_id SOURCE_IMPORT (which shouldn't
|
||||||
|
# conflict with other entry ids). Later when the sensor platform setup is
|
||||||
|
# called we gather the resources information and from there we start the
|
||||||
|
# actual config entry imports.
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
hass.data[DOMAIN][SOURCE_IMPORT] = conf
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||||
|
"""Use config values to set up a function enabling status retrieval."""
|
||||||
|
data_service = APCUPSdData(
|
||||||
|
config_entry.data[CONF_HOST], config_entry.data[CONF_PORT]
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await hass.async_add_executor_job(data_service.update)
|
||||||
|
except OSError as ex:
|
||||||
|
_LOGGER.error("Failure while testing APCUPSd status retrieval: %s", ex)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Store the data service object.
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
hass.data[DOMAIN][config_entry.entry_id] = data_service
|
||||||
|
|
||||||
|
# Forward the config entries to the supported platforms.
|
||||||
|
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
class APCUPSdData:
|
class APCUPSdData:
|
||||||
"""Stores the data retrieved from APCUPSd.
|
"""Stores the data retrieved from APCUPSd.
|
||||||
|
|
||||||
|
@ -62,26 +104,52 @@ class APCUPSdData:
|
||||||
updates from the server.
|
updates from the server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, host, port):
|
def __init__(self, host: str, port: int) -> None:
|
||||||
"""Initialize the data object."""
|
"""Initialize the data object."""
|
||||||
|
|
||||||
self._host = host
|
self._host = host
|
||||||
self._port = port
|
self._port = port
|
||||||
self._status = None
|
self.status: dict[str, Any] = {}
|
||||||
self._get = status.get
|
|
||||||
self._parse = status.parse
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def name(self) -> str | None:
|
||||||
"""Get latest update if throttle allows. Return status."""
|
"""Return the name of the UPS, if available."""
|
||||||
self.update()
|
return self.status.get("UPSNAME")
|
||||||
return self._status
|
|
||||||
|
|
||||||
def _get_status(self):
|
@property
|
||||||
"""Get the status from APCUPSd and parse it into a dict."""
|
def model(self) -> str | None:
|
||||||
return self._parse(self._get(host=self._host, port=self._port))
|
"""Return the model of the UPS, if available."""
|
||||||
|
# Different UPS models may report slightly different keys for model, here we
|
||||||
|
# try them all.
|
||||||
|
for model_key in ("APCMODEL", "MODEL"):
|
||||||
|
if model_key in self.status:
|
||||||
|
return self.status[model_key]
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sw_version(self) -> str | None:
|
||||||
|
"""Return the software version of the APCUPSd, if available."""
|
||||||
|
return self.status.get("VERSION")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hw_version(self) -> str | None:
|
||||||
|
"""Return the firmware version of the UPS, if available."""
|
||||||
|
return self.status.get("FIRMWARE")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serial_no(self) -> str | None:
|
||||||
|
"""Return the unique serial number of the UPS, if available."""
|
||||||
|
return self.status.get("SERIALNO")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def statflag(self) -> str | None:
|
||||||
|
"""Return the STATFLAG indicating the status of the UPS, if available."""
|
||||||
|
return self.status.get("STATFLAG")
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
def update(self, **kwargs):
|
def update(self, **kwargs):
|
||||||
"""Fetch the latest status from APCUPSd."""
|
"""Fetch the latest status from APCUPSd.
|
||||||
self._status = self._get_status()
|
|
||||||
|
Note that the result dict uses upper case for each resource, where our
|
||||||
|
integration uses lower cases as keys internally.
|
||||||
|
"""
|
||||||
|
self.status = status.parse(status.get(host=self._host, port=self._port))
|
||||||
|
|
|
@ -1,43 +1,75 @@
|
||||||
"""Support for tracking the online status of a UPS."""
|
"""Support for tracking the online status of a UPS."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import voluptuous as vol
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
|
from homeassistant.components.binary_sensor import (
|
||||||
from homeassistant.const import CONF_NAME
|
BinarySensorEntity,
|
||||||
|
BinarySensorEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
|
||||||
|
|
||||||
from . import DOMAIN, KEY_STATUS, VALUE_ONLINE
|
from . import DOMAIN, VALUE_ONLINE, APCUPSdData
|
||||||
|
|
||||||
DEFAULT_NAME = "UPS Online Status"
|
_LOGGER = logging.getLogger(__name__)
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
_DESCRIPTION = BinarySensorEntityDescription(
|
||||||
{vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}
|
key="statflag",
|
||||||
|
name="UPS Online Status",
|
||||||
|
icon="mdi:heart",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: ConfigType,
|
config_entry: ConfigEntry,
|
||||||
add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up an APCUPSd Online Status binary sensor."""
|
"""Set up an APCUPSd Online Status binary sensor."""
|
||||||
apcups_data = hass.data[DOMAIN]
|
data_service: APCUPSdData = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
add_entities([OnlineStatus(config, apcups_data)], True)
|
# 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:
|
||||||
|
return
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
[OnlineStatus(data_service, _DESCRIPTION)],
|
||||||
|
update_before_add=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class OnlineStatus(BinarySensorEntity):
|
class OnlineStatus(BinarySensorEntity):
|
||||||
"""Representation of an UPS online status."""
|
"""Representation of a UPS online status."""
|
||||||
|
|
||||||
def __init__(self, config, data):
|
def __init__(
|
||||||
|
self,
|
||||||
|
data_service: APCUPSdData,
|
||||||
|
description: BinarySensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
"""Initialize the APCUPSd binary device."""
|
"""Initialize the APCUPSd binary device."""
|
||||||
self._data = data
|
# Set up unique id and device info if serial number is available.
|
||||||
self._attr_name = config[CONF_NAME]
|
if (serial_no := data_service.serial_no) is not None:
|
||||||
|
self._attr_unique_id = f"{serial_no}_{description.key}"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, serial_no)},
|
||||||
|
model=data_service.model,
|
||||||
|
manufacturer="APC",
|
||||||
|
hw_version=data_service.hw_version,
|
||||||
|
sw_version=data_service.sw_version,
|
||||||
|
)
|
||||||
|
self.entity_description = description
|
||||||
|
self._data_service = data_service
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
"""Get the status report from APCUPSd and set this entity's state."""
|
"""Get the status report from APCUPSd and set this entity's state."""
|
||||||
self._attr_is_on = int(self._data.status[KEY_STATUS], 16) & VALUE_ONLINE > 0
|
self._data_service.update()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
86
homeassistant/components/apcupsd/config_flow.py
Normal file
86
homeassistant/components/apcupsd/config_flow.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
"""Config flow for APCUPSd integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
from homeassistant.helpers import selector
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
from . import DOMAIN, APCUPSdData
|
||||||
|
|
||||||
|
_PORT_SELECTOR = vol.All(
|
||||||
|
selector.NumberSelector(
|
||||||
|
selector.NumberSelectorConfig(
|
||||||
|
min=1, max=65535, mode=selector.NumberSelectorMode.BOX
|
||||||
|
),
|
||||||
|
),
|
||||||
|
vol.Coerce(int),
|
||||||
|
)
|
||||||
|
|
||||||
|
_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST, default="localhost"): cv.string,
|
||||||
|
vol.Required(CONF_PORT, default=3551): _PORT_SELECTOR,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""APCUPSd integration config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(step_id="user", data_schema=_SCHEMA)
|
||||||
|
|
||||||
|
# Abort if an entry with same host and port is present.
|
||||||
|
self._async_abort_entries_match(
|
||||||
|
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test the connection to the host and get the current status for serial number.
|
||||||
|
data_service = APCUPSdData(user_input[CONF_HOST], user_input[CONF_PORT])
|
||||||
|
try:
|
||||||
|
await self.hass.async_add_executor_job(data_service.update)
|
||||||
|
except OSError:
|
||||||
|
errors = {"base": "cannot_connect"}
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=_SCHEMA, errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
if not data_service.status:
|
||||||
|
return self.async_abort(reason="no_status")
|
||||||
|
|
||||||
|
# We _try_ to use the serial number of the UPS as the unique id since this field
|
||||||
|
# is not guaranteed to exist on all APC UPS models.
|
||||||
|
await self.async_set_unique_id(data_service.serial_no)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
title = "APC UPS"
|
||||||
|
if data_service.name is not None:
|
||||||
|
title = data_service.name
|
||||||
|
elif data_service.model is not None:
|
||||||
|
title = data_service.model
|
||||||
|
elif data_service.serial_no is not None:
|
||||||
|
title = data_service.serial_no
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=title,
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_import(self, conf: dict[str, Any]) -> FlowResult:
|
||||||
|
"""Import a configuration from yaml configuration."""
|
||||||
|
# If we are importing from YAML configuration, user_input could contain a
|
||||||
|
# CONF_RESOURCES with a list of resources (sensors) to be enabled.
|
||||||
|
return await self.async_step_user(user_input=conf)
|
|
@ -1,9 +1,10 @@
|
||||||
{
|
{
|
||||||
"domain": "apcupsd",
|
"domain": "apcupsd",
|
||||||
"name": "apcupsd",
|
"name": "APC UPS Daemon",
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
|
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
|
||||||
"requirements": ["apcaccess==0.0.13"],
|
"requirements": ["apcaccess==0.0.13"],
|
||||||
"codeowners": [],
|
"codeowners": ["@yuxincs"],
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["apcaccess"]
|
"loggers": ["apcaccess"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,10 @@ from homeassistant.components.sensor import (
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PORT,
|
||||||
CONF_RESOURCES,
|
CONF_RESOURCES,
|
||||||
ELECTRIC_CURRENT_AMPERE,
|
ELECTRIC_CURRENT_AMPERE,
|
||||||
ELECTRIC_POTENTIAL_VOLT,
|
ELECTRIC_POTENTIAL_VOLT,
|
||||||
|
@ -26,368 +29,387 @@ from homeassistant.const import (
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from . import DOMAIN
|
from . import DOMAIN, APCUPSdData
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SENSOR_PREFIX = "UPS "
|
SENSORS: dict[str, SensorEntityDescription] = {
|
||||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
"alarmdel": SensorEntityDescription(
|
||||||
SensorEntityDescription(
|
|
||||||
key="alarmdel",
|
key="alarmdel",
|
||||||
name="Alarm Delay",
|
name="UPS Alarm Delay",
|
||||||
icon="mdi:alarm",
|
icon="mdi:alarm",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"ambtemp": SensorEntityDescription(
|
||||||
key="ambtemp",
|
key="ambtemp",
|
||||||
name="Ambient Temperature",
|
name="UPS Ambient Temperature",
|
||||||
icon="mdi:thermometer",
|
icon="mdi:thermometer",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"apc": SensorEntityDescription(
|
||||||
key="apc",
|
key="apc",
|
||||||
name="Status Data",
|
name="UPS Status Data",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"apcmodel": SensorEntityDescription(
|
||||||
key="apcmodel",
|
key="apcmodel",
|
||||||
name="Model",
|
name="UPS Model",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"badbatts": SensorEntityDescription(
|
||||||
key="badbatts",
|
key="badbatts",
|
||||||
name="Bad Batteries",
|
name="UPS Bad Batteries",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"battdate": SensorEntityDescription(
|
||||||
key="battdate",
|
key="battdate",
|
||||||
name="Battery Replaced",
|
name="UPS Battery Replaced",
|
||||||
icon="mdi:calendar-clock",
|
icon="mdi:calendar-clock",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"battstat": SensorEntityDescription(
|
||||||
key="battstat",
|
key="battstat",
|
||||||
name="Battery Status",
|
name="UPS Battery Status",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"battv": SensorEntityDescription(
|
||||||
key="battv",
|
key="battv",
|
||||||
name="Battery Voltage",
|
name="UPS Battery Voltage",
|
||||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||||
icon="mdi:flash",
|
icon="mdi:flash",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"bcharge": SensorEntityDescription(
|
||||||
key="bcharge",
|
key="bcharge",
|
||||||
name="Battery",
|
name="UPS Battery",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:battery",
|
icon="mdi:battery",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"cable": SensorEntityDescription(
|
||||||
key="cable",
|
key="cable",
|
||||||
name="Cable Type",
|
name="UPS Cable Type",
|
||||||
icon="mdi:ethernet-cable",
|
icon="mdi:ethernet-cable",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"cumonbatt": SensorEntityDescription(
|
||||||
key="cumonbatt",
|
key="cumonbatt",
|
||||||
name="Total Time on Battery",
|
name="UPS Total Time on Battery",
|
||||||
icon="mdi:timer-outline",
|
icon="mdi:timer-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"date": SensorEntityDescription(
|
||||||
key="date",
|
key="date",
|
||||||
name="Status Date",
|
name="UPS Status Date",
|
||||||
icon="mdi:calendar-clock",
|
icon="mdi:calendar-clock",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"dipsw": SensorEntityDescription(
|
||||||
key="dipsw",
|
key="dipsw",
|
||||||
name="Dip Switch Settings",
|
name="UPS Dip Switch Settings",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"dlowbatt": SensorEntityDescription(
|
||||||
key="dlowbatt",
|
key="dlowbatt",
|
||||||
name="Low Battery Signal",
|
name="UPS Low Battery Signal",
|
||||||
icon="mdi:clock-alert",
|
icon="mdi:clock-alert",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"driver": SensorEntityDescription(
|
||||||
key="driver",
|
key="driver",
|
||||||
name="Driver",
|
name="UPS Driver",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"dshutd": SensorEntityDescription(
|
||||||
key="dshutd",
|
key="dshutd",
|
||||||
name="Shutdown Delay",
|
name="UPS Shutdown Delay",
|
||||||
icon="mdi:timer-outline",
|
icon="mdi:timer-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"dwake": SensorEntityDescription(
|
||||||
key="dwake",
|
key="dwake",
|
||||||
name="Wake Delay",
|
name="UPS Wake Delay",
|
||||||
icon="mdi:timer-outline",
|
icon="mdi:timer-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"end apc": SensorEntityDescription(
|
||||||
key="end apc",
|
key="end apc",
|
||||||
name="Date and Time",
|
name="UPS Date and Time",
|
||||||
icon="mdi:calendar-clock",
|
icon="mdi:calendar-clock",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"extbatts": SensorEntityDescription(
|
||||||
key="extbatts",
|
key="extbatts",
|
||||||
name="External Batteries",
|
name="UPS External Batteries",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"firmware": SensorEntityDescription(
|
||||||
key="firmware",
|
key="firmware",
|
||||||
name="Firmware Version",
|
name="UPS Firmware Version",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"hitrans": SensorEntityDescription(
|
||||||
key="hitrans",
|
key="hitrans",
|
||||||
name="Transfer High",
|
name="UPS Transfer High",
|
||||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||||
icon="mdi:flash",
|
icon="mdi:flash",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"hostname": SensorEntityDescription(
|
||||||
key="hostname",
|
key="hostname",
|
||||||
name="Hostname",
|
name="UPS Hostname",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"humidity": SensorEntityDescription(
|
||||||
key="humidity",
|
key="humidity",
|
||||||
name="Ambient Humidity",
|
name="UPS Ambient Humidity",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:water-percent",
|
icon="mdi:water-percent",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"itemp": SensorEntityDescription(
|
||||||
key="itemp",
|
key="itemp",
|
||||||
name="Internal Temperature",
|
name="UPS Internal Temperature",
|
||||||
native_unit_of_measurement=TEMP_CELSIUS,
|
native_unit_of_measurement=TEMP_CELSIUS,
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"lastxfer": SensorEntityDescription(
|
||||||
key="lastxfer",
|
key="lastxfer",
|
||||||
name="Last Transfer",
|
name="UPS Last Transfer",
|
||||||
icon="mdi:transfer",
|
icon="mdi:transfer",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"linefail": SensorEntityDescription(
|
||||||
key="linefail",
|
key="linefail",
|
||||||
name="Input Voltage Status",
|
name="UPS Input Voltage Status",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"linefreq": SensorEntityDescription(
|
||||||
key="linefreq",
|
key="linefreq",
|
||||||
name="Line Frequency",
|
name="UPS Line Frequency",
|
||||||
native_unit_of_measurement=FREQUENCY_HERTZ,
|
native_unit_of_measurement=FREQUENCY_HERTZ,
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"linev": SensorEntityDescription(
|
||||||
key="linev",
|
key="linev",
|
||||||
name="Input Voltage",
|
name="UPS Input Voltage",
|
||||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||||
icon="mdi:flash",
|
icon="mdi:flash",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"loadpct": SensorEntityDescription(
|
||||||
key="loadpct",
|
key="loadpct",
|
||||||
name="Load",
|
name="UPS Load",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:gauge",
|
icon="mdi:gauge",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"loadapnt": SensorEntityDescription(
|
||||||
key="loadapnt",
|
key="loadapnt",
|
||||||
name="Load Apparent Power",
|
name="UPS Load Apparent Power",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:gauge",
|
icon="mdi:gauge",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"lotrans": SensorEntityDescription(
|
||||||
key="lotrans",
|
key="lotrans",
|
||||||
name="Transfer Low",
|
name="UPS Transfer Low",
|
||||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||||
icon="mdi:flash",
|
icon="mdi:flash",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"mandate": SensorEntityDescription(
|
||||||
key="mandate",
|
key="mandate",
|
||||||
name="Manufacture Date",
|
name="UPS Manufacture Date",
|
||||||
icon="mdi:calendar",
|
icon="mdi:calendar",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"masterupd": SensorEntityDescription(
|
||||||
key="masterupd",
|
key="masterupd",
|
||||||
name="Master Update",
|
name="UPS Master Update",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"maxlinev": SensorEntityDescription(
|
||||||
key="maxlinev",
|
key="maxlinev",
|
||||||
name="Input Voltage High",
|
name="UPS Input Voltage High",
|
||||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||||
icon="mdi:flash",
|
icon="mdi:flash",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"maxtime": SensorEntityDescription(
|
||||||
key="maxtime",
|
key="maxtime",
|
||||||
name="Battery Timeout",
|
name="UPS Battery Timeout",
|
||||||
icon="mdi:timer-off-outline",
|
icon="mdi:timer-off-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"mbattchg": SensorEntityDescription(
|
||||||
key="mbattchg",
|
key="mbattchg",
|
||||||
name="Battery Shutdown",
|
name="UPS Battery Shutdown",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:battery-alert",
|
icon="mdi:battery-alert",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"minlinev": SensorEntityDescription(
|
||||||
key="minlinev",
|
key="minlinev",
|
||||||
name="Input Voltage Low",
|
name="UPS Input Voltage Low",
|
||||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||||
icon="mdi:flash",
|
icon="mdi:flash",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"mintimel": SensorEntityDescription(
|
||||||
key="mintimel",
|
key="mintimel",
|
||||||
name="Shutdown Time",
|
name="UPS Shutdown Time",
|
||||||
icon="mdi:timer-outline",
|
icon="mdi:timer-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"model": SensorEntityDescription(
|
||||||
key="model",
|
key="model",
|
||||||
name="Model",
|
name="UPS Model",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"nombattv": SensorEntityDescription(
|
||||||
key="nombattv",
|
key="nombattv",
|
||||||
name="Battery Nominal Voltage",
|
name="UPS Battery Nominal Voltage",
|
||||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||||
icon="mdi:flash",
|
icon="mdi:flash",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"nominv": SensorEntityDescription(
|
||||||
key="nominv",
|
key="nominv",
|
||||||
name="Nominal Input Voltage",
|
name="UPS Nominal Input Voltage",
|
||||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||||
icon="mdi:flash",
|
icon="mdi:flash",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"nomoutv": SensorEntityDescription(
|
||||||
key="nomoutv",
|
key="nomoutv",
|
||||||
name="Nominal Output Voltage",
|
name="UPS Nominal Output Voltage",
|
||||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||||
icon="mdi:flash",
|
icon="mdi:flash",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"nompower": SensorEntityDescription(
|
||||||
key="nompower",
|
key="nompower",
|
||||||
name="Nominal Output Power",
|
name="UPS Nominal Output Power",
|
||||||
native_unit_of_measurement=POWER_WATT,
|
native_unit_of_measurement=POWER_WATT,
|
||||||
icon="mdi:flash",
|
icon="mdi:flash",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"nomapnt": SensorEntityDescription(
|
||||||
key="nomapnt",
|
key="nomapnt",
|
||||||
name="Nominal Apparent Power",
|
name="UPS Nominal Apparent Power",
|
||||||
native_unit_of_measurement=POWER_VOLT_AMPERE,
|
native_unit_of_measurement=POWER_VOLT_AMPERE,
|
||||||
icon="mdi:flash",
|
icon="mdi:flash",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"numxfers": SensorEntityDescription(
|
||||||
key="numxfers",
|
key="numxfers",
|
||||||
name="Transfer Count",
|
name="UPS Transfer Count",
|
||||||
icon="mdi:counter",
|
icon="mdi:counter",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"outcurnt": SensorEntityDescription(
|
||||||
key="outcurnt",
|
key="outcurnt",
|
||||||
name="Output Current",
|
name="UPS Output Current",
|
||||||
native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
|
native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
|
||||||
icon="mdi:flash",
|
icon="mdi:flash",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"outputv": SensorEntityDescription(
|
||||||
key="outputv",
|
key="outputv",
|
||||||
name="Output Voltage",
|
name="UPS Output Voltage",
|
||||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||||
icon="mdi:flash",
|
icon="mdi:flash",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"reg1": SensorEntityDescription(
|
||||||
key="reg1",
|
key="reg1",
|
||||||
name="Register 1 Fault",
|
name="UPS Register 1 Fault",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"reg2": SensorEntityDescription(
|
||||||
key="reg2",
|
key="reg2",
|
||||||
name="Register 2 Fault",
|
name="UPS Register 2 Fault",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"reg3": SensorEntityDescription(
|
||||||
key="reg3",
|
key="reg3",
|
||||||
name="Register 3 Fault",
|
name="UPS Register 3 Fault",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"retpct": SensorEntityDescription(
|
||||||
key="retpct",
|
key="retpct",
|
||||||
name="Restore Requirement",
|
name="UPS Restore Requirement",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:battery-alert",
|
icon="mdi:battery-alert",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"selftest": SensorEntityDescription(
|
||||||
key="selftest",
|
key="selftest",
|
||||||
name="Last Self Test",
|
name="UPS Last Self Test",
|
||||||
icon="mdi:calendar-clock",
|
icon="mdi:calendar-clock",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"sense": SensorEntityDescription(
|
||||||
key="sense",
|
key="sense",
|
||||||
name="Sensitivity",
|
name="UPS Sensitivity",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"serialno": SensorEntityDescription(
|
||||||
key="serialno",
|
key="serialno",
|
||||||
name="Serial Number",
|
name="UPS Serial Number",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"starttime": SensorEntityDescription(
|
||||||
key="starttime",
|
key="starttime",
|
||||||
name="Startup Time",
|
name="UPS Startup Time",
|
||||||
icon="mdi:calendar-clock",
|
icon="mdi:calendar-clock",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"statflag": SensorEntityDescription(
|
||||||
key="statflag",
|
key="statflag",
|
||||||
name="Status Flag",
|
name="UPS Status Flag",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"status": SensorEntityDescription(
|
||||||
key="status",
|
key="status",
|
||||||
name="Status",
|
name="UPS Status",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"stesti": SensorEntityDescription(
|
||||||
key="stesti",
|
key="stesti",
|
||||||
name="Self Test Interval",
|
name="UPS Self Test Interval",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"timeleft": SensorEntityDescription(
|
||||||
key="timeleft",
|
key="timeleft",
|
||||||
name="Time Left",
|
name="UPS Time Left",
|
||||||
icon="mdi:clock-alert",
|
icon="mdi:clock-alert",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"tonbatt": SensorEntityDescription(
|
||||||
key="tonbatt",
|
key="tonbatt",
|
||||||
name="Time on Battery",
|
name="UPS Time on Battery",
|
||||||
icon="mdi:timer-outline",
|
icon="mdi:timer-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"upsmode": SensorEntityDescription(
|
||||||
key="upsmode",
|
key="upsmode",
|
||||||
name="Mode",
|
name="UPS Mode",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"upsname": SensorEntityDescription(
|
||||||
key="upsname",
|
key="upsname",
|
||||||
name="Name",
|
name="UPS Name",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"version": SensorEntityDescription(
|
||||||
key="version",
|
key="version",
|
||||||
name="Daemon Info",
|
name="UPS Daemon Info",
|
||||||
icon="mdi:information-outline",
|
icon="mdi:information-outline",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"xoffbat": SensorEntityDescription(
|
||||||
key="xoffbat",
|
key="xoffbat",
|
||||||
name="Transfer from Battery",
|
name="UPS Transfer from Battery",
|
||||||
icon="mdi:transfer",
|
icon="mdi:transfer",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"xoffbatt": SensorEntityDescription(
|
||||||
key="xoffbatt",
|
key="xoffbatt",
|
||||||
name="Transfer from Battery",
|
name="UPS Transfer from Battery",
|
||||||
icon="mdi:transfer",
|
icon="mdi:transfer",
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
"xonbatt": SensorEntityDescription(
|
||||||
key="xonbatt",
|
key="xonbatt",
|
||||||
name="Transfer to Battery",
|
name="UPS Transfer to Battery",
|
||||||
icon="mdi:transfer",
|
icon="mdi:transfer",
|
||||||
),
|
),
|
||||||
)
|
}
|
||||||
SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
|
|
||||||
|
|
||||||
SPECIFIC_UNITS = {"ITEMP": TEMP_CELSIUS}
|
SPECIFIC_UNITS = {"ITEMP": TEMP_CELSIUS}
|
||||||
INFERRED_UNITS = {
|
INFERRED_UNITS = {
|
||||||
|
@ -406,36 +428,109 @@ INFERRED_UNITS = {
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_RESOURCES, default=[]): vol.All(
|
vol.Required(CONF_RESOURCES, default=[]): vol.All(
|
||||||
cv.ensure_list, [vol.In(SENSOR_KEYS)]
|
cv.ensure_list, [vol.In([desc.key for desc in SENSORS.values()])]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
async def async_setup_platform(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: ConfigType,
|
config: ConfigType,
|
||||||
add_entities: AddEntitiesCallback,
|
add_entities: AddEntitiesCallback,
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the APCUPSd sensors."""
|
"""Import the configurations from YAML to config flows."""
|
||||||
apcups_data = hass.data[DOMAIN]
|
# We only import configs from YAML if it hasn't been imported. If there is a config
|
||||||
resources = config[CONF_RESOURCES]
|
# entry marked with SOURCE_IMPORT, it means the YAML config has been imported.
|
||||||
|
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||||
|
if entry.source == SOURCE_IMPORT:
|
||||||
|
return
|
||||||
|
|
||||||
for resource in resources:
|
# This is the second step of YAML config imports, first see the comments in
|
||||||
if resource.upper() not in apcups_data.status:
|
# async_setup() of __init__.py to get an idea of how we import the YAML configs.
|
||||||
_LOGGER.warning(
|
# Here we retrieve the partial YAML configs from the special entry id.
|
||||||
"Sensor type: %s does not appear in the APCUPSd status output",
|
conf = hass.data[DOMAIN].get(SOURCE_IMPORT)
|
||||||
resource,
|
if conf is None:
|
||||||
)
|
return
|
||||||
|
|
||||||
entities = [
|
_LOGGER.warning(
|
||||||
APCUPSdSensor(apcups_data, description)
|
"Configuration of apcupsd in YAML is deprecated and will be "
|
||||||
for description in SENSOR_TYPES
|
"removed in Home Assistant 2022.12; Your existing configuration "
|
||||||
if description.key in resources
|
"has been imported into the UI automatically and can be safely removed "
|
||||||
]
|
"from your configuration.yaml file"
|
||||||
|
)
|
||||||
|
|
||||||
add_entities(entities, True)
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
"deprecated_yaml",
|
||||||
|
breaks_in_ha_version="2022.12.0",
|
||||||
|
is_fixable=False,
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
translation_key="deprecated_yaml",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove the artificial entry since it's no longer needed.
|
||||||
|
hass.data[DOMAIN].pop(SOURCE_IMPORT)
|
||||||
|
|
||||||
|
# Our config flow supports CONF_RESOURCES and will properly import it to disable
|
||||||
|
# entities not listed in CONF_RESOURCES by default. Note that this designed to
|
||||||
|
# support YAML config import only (i.e., not shown in UI during setup).
|
||||||
|
conf[CONF_RESOURCES] = config[CONF_RESOURCES]
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"YAML configurations loaded with host %s, port %s and resources %s",
|
||||||
|
conf[CONF_HOST],
|
||||||
|
conf[CONF_PORT],
|
||||||
|
conf[CONF_RESOURCES],
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the APCUPSd sensors from config entries."""
|
||||||
|
data_service: APCUPSdData = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
|
# The resources from data service are in upper-case by default, but we use
|
||||||
|
# lower cases throughout this integration.
|
||||||
|
available_resources: set[str] = {k.lower() for k, _ in data_service.status.items()}
|
||||||
|
|
||||||
|
# We use user-specified resources from imported YAML config (if available) to
|
||||||
|
# determine whether to enable the entity by default. Here, we first collect the
|
||||||
|
# specified resources
|
||||||
|
specified_resources = None
|
||||||
|
if (resources := config_entry.data.get(CONF_RESOURCES)) is not None:
|
||||||
|
assert isinstance(resources, list)
|
||||||
|
specified_resources = set(resources)
|
||||||
|
|
||||||
|
entities = []
|
||||||
|
for resource in available_resources:
|
||||||
|
if resource not in SENSORS:
|
||||||
|
_LOGGER.warning("Invalid resource from APCUPSd: %s", resource.upper())
|
||||||
|
continue
|
||||||
|
|
||||||
|
# To avoid breaking changes, we disable sensors not specified in resources.
|
||||||
|
description = SENSORS[resource]
|
||||||
|
enabled_by_default = description.entity_registry_enabled_default
|
||||||
|
if specified_resources is not None:
|
||||||
|
enabled_by_default = resource in specified_resources
|
||||||
|
|
||||||
|
entity = APCUPSdSensor(data_service, description, enabled_by_default)
|
||||||
|
entities.append(entity)
|
||||||
|
|
||||||
|
async_add_entities(entities, update_before_add=True)
|
||||||
|
|
||||||
|
|
||||||
def infer_unit(value):
|
def infer_unit(value):
|
||||||
|
@ -454,18 +549,39 @@ def infer_unit(value):
|
||||||
class APCUPSdSensor(SensorEntity):
|
class APCUPSdSensor(SensorEntity):
|
||||||
"""Representation of a sensor entity for APCUPSd status values."""
|
"""Representation of a sensor entity for APCUPSd status values."""
|
||||||
|
|
||||||
def __init__(self, data, description: SensorEntityDescription):
|
def __init__(
|
||||||
|
self,
|
||||||
|
data_service: APCUPSdData,
|
||||||
|
description: SensorEntityDescription,
|
||||||
|
enabled_by_default: bool,
|
||||||
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
|
# Set up unique id and device info if serial number is available.
|
||||||
|
if (serial_no := data_service.serial_no) is not None:
|
||||||
|
self._attr_unique_id = f"{serial_no}_{description.key}"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, serial_no)},
|
||||||
|
model=data_service.model,
|
||||||
|
manufacturer="APC",
|
||||||
|
hw_version=data_service.hw_version,
|
||||||
|
sw_version=data_service.sw_version,
|
||||||
|
)
|
||||||
|
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._data = data
|
self._attr_entity_registry_enabled_default = enabled_by_default
|
||||||
self._attr_name = f"{SENSOR_PREFIX}{description.name}"
|
self._data_service = data_service
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
"""Get the latest status and use it to update our sensor state."""
|
"""Get the latest status and use it to update our sensor state."""
|
||||||
|
self._data_service.update()
|
||||||
|
|
||||||
key = self.entity_description.key.upper()
|
key = self.entity_description.key.upper()
|
||||||
if key not in self._data.status:
|
if key not in self._data_service.status:
|
||||||
self._attr_native_value = None
|
self._attr_native_value = None
|
||||||
else:
|
return
|
||||||
self._attr_native_value, inferred_unit = infer_unit(self._data.status[key])
|
|
||||||
if not self.native_unit_of_measurement:
|
self._attr_native_value, inferred_unit = infer_unit(
|
||||||
self._attr_native_unit_of_measurement = inferred_unit
|
self._data_service.status[key]
|
||||||
|
)
|
||||||
|
if not self.native_unit_of_measurement:
|
||||||
|
self._attr_native_unit_of_measurement = inferred_unit
|
||||||
|
|
26
homeassistant/components/apcupsd/strings.json
Normal file
26
homeassistant/components/apcupsd/strings.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
"no_status": "No status is reported from [%key:common::config_flow::data::host%]"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
"port": "[%key:common::config_flow::data::port%]"
|
||||||
|
},
|
||||||
|
"description": "Enter the host and port on which the apcupsd NIS is being served."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"issues": {
|
||||||
|
"deprecated_yaml": {
|
||||||
|
"title": "The APC UPS Daemon YAML configuration is being removed",
|
||||||
|
"description": "Configuring APC UPS Daemon using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the APC UPS Daemon YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
homeassistant/components/apcupsd/translations/en.json
Normal file
26
homeassistant/components/apcupsd/translations/en.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Device is already configured",
|
||||||
|
"no_status": "No status is reported from Host"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port"
|
||||||
|
},
|
||||||
|
"description": "Enter the host and port on which the apcupsd NIS is being served."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"issues": {
|
||||||
|
"deprecated_yaml": {
|
||||||
|
"description": "Configuring APC UPS Daemon using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the APC UPS Daemon YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
|
||||||
|
"title": "The APC UPS Daemon YAML configuration is being removed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ FLOWS = {
|
||||||
"android_ip_webcam",
|
"android_ip_webcam",
|
||||||
"androidtv",
|
"androidtv",
|
||||||
"anthemav",
|
"anthemav",
|
||||||
|
"apcupsd",
|
||||||
"apple_tv",
|
"apple_tv",
|
||||||
"arcam_fmj",
|
"arcam_fmj",
|
||||||
"aseko_pool_live",
|
"aseko_pool_live",
|
||||||
|
|
|
@ -286,6 +286,9 @@ androidtv[async]==0.0.67
|
||||||
# homeassistant.components.anthemav
|
# homeassistant.components.anthemav
|
||||||
anthemav==1.4.1
|
anthemav==1.4.1
|
||||||
|
|
||||||
|
# homeassistant.components.apcupsd
|
||||||
|
apcaccess==0.0.13
|
||||||
|
|
||||||
# homeassistant.components.apprise
|
# homeassistant.components.apprise
|
||||||
apprise==1.0.0
|
apprise==1.0.0
|
||||||
|
|
||||||
|
|
66
tests/components/apcupsd/__init__.py
Normal file
66
tests/components/apcupsd/__init__.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
"""Tests for the APCUPSd component."""
|
||||||
|
from collections import OrderedDict
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
|
|
||||||
|
CONF_DATA: Final = {CONF_HOST: "test", CONF_PORT: 1234}
|
||||||
|
|
||||||
|
MOCK_STATUS: Final = OrderedDict(
|
||||||
|
[
|
||||||
|
("APC", "001,038,0985"),
|
||||||
|
("DATE", "1970-01-01 00:00:00 0000"),
|
||||||
|
("VERSION", "3.14.14 (31 May 2016) unknown"),
|
||||||
|
("CABLE", "USB Cable"),
|
||||||
|
("DRIVER", "USB UPS Driver"),
|
||||||
|
("UPSMODE", "Stand Alone"),
|
||||||
|
("MODEL", "Back-UPS ES 600"),
|
||||||
|
("STATUS", "ONLINE"),
|
||||||
|
("LINEV", "124.0 Volts"),
|
||||||
|
("LOADPCT", "14.0 Percent"),
|
||||||
|
("BCHARGE", "100.0 Percent"),
|
||||||
|
("TIMELEFT", "51.0 Minutes"),
|
||||||
|
("MBATTCHG", "5 Percent"),
|
||||||
|
("MINTIMEL", "3 Minutes"),
|
||||||
|
("MAXTIME", "0 Seconds"),
|
||||||
|
("SENSE", "Medium"),
|
||||||
|
("LOTRANS", "92.0 Volts"),
|
||||||
|
("HITRANS", "139.0 Volts"),
|
||||||
|
("ALARMDEL", "30 Seconds"),
|
||||||
|
("BATTV", "13.7 Volts"),
|
||||||
|
("LASTXFER", "Automatic or explicit self test"),
|
||||||
|
("NUMXFERS", "1"),
|
||||||
|
("XONBATT", "1970-01-01 00:00:00 0000"),
|
||||||
|
("TONBATT", "0 Seconds"),
|
||||||
|
("CUMONBATT", "8 Seconds"),
|
||||||
|
("XOFFBATT", "1970-01-01 00:00:00 0000"),
|
||||||
|
("LASTSTEST", "1970-01-01 00:00:00 0000"),
|
||||||
|
("SELFTEST", "NO"),
|
||||||
|
("STATFLAG", "0x05000008"),
|
||||||
|
("SERIALNO", "XXXXXXXXXXXX"),
|
||||||
|
("BATTDATE", "1970-01-01"),
|
||||||
|
("NOMINV", "120 Volts"),
|
||||||
|
("NOMBATTV", "12.0 Volts"),
|
||||||
|
("NOMPOWER", "330 Watts"),
|
||||||
|
("FIRMWARE", "928.a8 .D USB FW:a8"),
|
||||||
|
("END APC", "1970-01-01 00:00:00 0000"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Minimal status adapted from http://www.apcupsd.org/manual/manual.html#apcaccess-test.
|
||||||
|
# Most importantly, the "MODEL" and "SERIALNO" fields are removed to test the ability
|
||||||
|
# of the integration to handle such cases.
|
||||||
|
MOCK_MINIMAL_STATUS: Final = OrderedDict(
|
||||||
|
[
|
||||||
|
("APC", "001,012,0319"),
|
||||||
|
("DATE", "1970-01-01 00:00:00 0000"),
|
||||||
|
("RELEASE", "3.8.5"),
|
||||||
|
("CABLE", "APC Cable 940-0128A"),
|
||||||
|
("UPSMODE", "Stand Alone"),
|
||||||
|
("STARTTIME", "1970-01-01 00:00:00 0000"),
|
||||||
|
("LINEFAIL", "OK"),
|
||||||
|
("BATTSTAT", "OK"),
|
||||||
|
("STATFLAG", "0x008"),
|
||||||
|
("END APC", "1970-01-01 00:00:00 0000"),
|
||||||
|
]
|
||||||
|
)
|
185
tests/components/apcupsd/test_config_flow.py
Normal file
185
tests/components/apcupsd/test_config_flow.py
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
"""Test APCUPSd config flow setup process."""
|
||||||
|
from copy import copy
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.apcupsd import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_RESOURCES, CONF_SOURCE
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
from . import CONF_DATA, MOCK_MINIMAL_STATUS, MOCK_STATUS
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_setup():
|
||||||
|
return patch(
|
||||||
|
"homeassistant.components.apcupsd.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_flow_cannot_connect(hass: HomeAssistant) -> None:
|
||||||
|
"""Test config flow setup with connection error."""
|
||||||
|
with patch("apcaccess.status.get") as mock_get:
|
||||||
|
mock_get.side_effect = OSError()
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data=CONF_DATA,
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["errors"]["base"] == "cannot_connect"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_flow_no_status(hass: HomeAssistant) -> None:
|
||||||
|
"""Test config flow setup with successful connection but no status is reported."""
|
||||||
|
with patch(
|
||||||
|
"apcaccess.status.parse",
|
||||||
|
return_value={}, # Returns no status.
|
||||||
|
), patch("apcaccess.status.get", return_value=b""):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
assert result["type"] == FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "no_status"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_flow_duplicate(hass: HomeAssistant) -> None:
|
||||||
|
"""Test duplicate config flow setup."""
|
||||||
|
# First add an exiting config entry to hass.
|
||||||
|
mock_entry = MockConfigEntry(
|
||||||
|
version=1,
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="APCUPSd",
|
||||||
|
data=CONF_DATA,
|
||||||
|
unique_id=MOCK_STATUS["SERIALNO"],
|
||||||
|
source=SOURCE_USER,
|
||||||
|
)
|
||||||
|
mock_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with patch("apcaccess.status.parse") as mock_parse, patch(
|
||||||
|
"apcaccess.status.get", return_value=b""
|
||||||
|
), _patch_setup():
|
||||||
|
mock_parse.return_value = MOCK_STATUS
|
||||||
|
|
||||||
|
# Now, create the integration again using the same config data, we should reject
|
||||||
|
# the creation due same host / port.
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data=CONF_DATA,
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
# Then, we create the integration once again using a different port. However,
|
||||||
|
# the apcaccess patch is kept to report the same serial number, we should
|
||||||
|
# reject the creation as well.
|
||||||
|
another_host = {
|
||||||
|
CONF_HOST: CONF_DATA[CONF_HOST],
|
||||||
|
CONF_PORT: CONF_DATA[CONF_PORT] + 1,
|
||||||
|
}
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data=another_host,
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
# Now we change the serial number and add it again. This should be successful.
|
||||||
|
another_device_status = copy(MOCK_STATUS)
|
||||||
|
another_device_status["SERIALNO"] = MOCK_STATUS["SERIALNO"] + "ZZZ"
|
||||||
|
mock_parse.return_value = another_device_status
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data=another_host,
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["data"] == another_host
|
||||||
|
|
||||||
|
|
||||||
|
async def test_flow_works(hass: HomeAssistant) -> None:
|
||||||
|
"""Test successful creation of config entries via user configuration."""
|
||||||
|
with patch("apcaccess.status.parse", return_value=MOCK_STATUS), patch(
|
||||||
|
"apcaccess.status.get", return_value=b""
|
||||||
|
), _patch_setup() as mock_setup:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={CONF_SOURCE: SOURCE_USER},
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == SOURCE_USER
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=CONF_DATA
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == MOCK_STATUS["MODEL"]
|
||||||
|
assert result["data"] == CONF_DATA
|
||||||
|
|
||||||
|
mock_setup.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"extra_status,expected_title",
|
||||||
|
[
|
||||||
|
({"UPSNAME": "Friendly Name"}, "Friendly Name"),
|
||||||
|
({"MODEL": "MODEL X"}, "MODEL X"),
|
||||||
|
({"SERIALNO": "ZZZZ"}, "ZZZZ"),
|
||||||
|
({}, "APC UPS"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_flow_minimal_status(
|
||||||
|
hass: HomeAssistant, extra_status: dict[str, str], expected_title: str
|
||||||
|
) -> None:
|
||||||
|
"""Test successful creation of config entries via user configuration when minimal status is reported.
|
||||||
|
|
||||||
|
We test different combinations of minimal statuses, where the title of the
|
||||||
|
integration will vary.
|
||||||
|
"""
|
||||||
|
with patch("apcaccess.status.parse") as mock_parse, patch(
|
||||||
|
"apcaccess.status.get", return_value=b""
|
||||||
|
), _patch_setup() as mock_setup:
|
||||||
|
status = MOCK_MINIMAL_STATUS | extra_status
|
||||||
|
mock_parse.return_value = status
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["data"] == CONF_DATA
|
||||||
|
assert result["title"] == expected_title
|
||||||
|
mock_setup.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_flow_import(hass: HomeAssistant) -> None:
|
||||||
|
"""Test successful creation of config entries via YAML import."""
|
||||||
|
with patch("apcaccess.status.parse", return_value=MOCK_STATUS), patch(
|
||||||
|
"apcaccess.status.get", return_value=b""
|
||||||
|
), _patch_setup() as mock_setup:
|
||||||
|
# Importing from YAML will create an extra field CONF_RESOURCES in the config
|
||||||
|
# entry, here we test if it is properly stored.
|
||||||
|
resources = ["MODEL"]
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data=CONF_DATA | {CONF_RESOURCES: resources},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == MOCK_STATUS["MODEL"]
|
||||||
|
assert result["data"] == CONF_DATA | {CONF_RESOURCES: resources}
|
||||||
|
|
||||||
|
mock_setup.assert_called_once()
|
Loading…
Add table
Reference in a new issue