Avoid using coordinator in config flow of APCUPSD (#112121)

* Separate data class out of coordinator

* Further fix the imports

* Update homeassistant/components/apcupsd/coordinator.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Use `or` to make it a bit cleaner when trying to find the UPS model

Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>

* Use or to make it a bit cleaner when trying to find the UPS model

Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>

* Use plain dict instead of `OrderedDict`

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>
This commit is contained in:
Yuxin Wang 2024-03-04 03:40:59 -05:00 committed by GitHub
parent 38f9285bd6
commit 2c5510df30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 49 additions and 53 deletions

View file

@ -13,7 +13,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import DOMAIN, APCUPSdCoordinator from .const import DOMAIN
from .coordinator import APCUPSdCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_DESCRIPTION = BinarySensorEntityDescription( _DESCRIPTION = BinarySensorEntityDescription(
@ -53,7 +54,7 @@ class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity):
super().__init__(coordinator, context=description.key.upper()) super().__init__(coordinator, context=description.key.upper())
# Set up unique id and device info if serial number is available. # Set up unique id and device info if serial number is available.
if (serial_no := coordinator.ups_serial_no) is not None: if (serial_no := coordinator.data.serial_no) is not None:
self._attr_unique_id = f"{serial_no}_{description.key}" self._attr_unique_id = f"{serial_no}_{description.key}"
self.entity_description = description self.entity_description = description
self._attr_device_info = coordinator.device_info self._attr_device_info = coordinator.device_info

View file

@ -1,17 +1,19 @@
"""Config flow for APCUPSd integration.""" """Config flow for APCUPSd integration."""
from __future__ import annotations from __future__ import annotations
import asyncio
from typing import Any from typing import Any
import aioapcaccess
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.helpers import selector from homeassistant.helpers import selector
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.update_coordinator import UpdateFailed
from . import DOMAIN, APCUPSdCoordinator from .const import CONNECTION_TIMEOUT, DOMAIN
from .coordinator import APCUPSdData
_PORT_SELECTOR = vol.All( _PORT_SELECTOR = vol.All(
selector.NumberSelector( selector.NumberSelector(
@ -49,32 +51,22 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port}) self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port})
# Test the connection to the host and get the current status for serial number. # Test the connection to the host and get the current status for serial number.
coordinator = APCUPSdCoordinator(self.hass, host, port) try:
await coordinator.async_request_refresh() async with asyncio.timeout(CONNECTION_TIMEOUT):
data = APCUPSdData(await aioapcaccess.request_status(host, port))
if isinstance(coordinator.last_exception, (UpdateFailed, TimeoutError)): except (OSError, asyncio.IncompleteReadError, TimeoutError):
errors = {"base": "cannot_connect"} errors = {"base": "cannot_connect"}
return self.async_show_form( return self.async_show_form(
step_id="user", data_schema=_SCHEMA, errors=errors step_id="user", data_schema=_SCHEMA, errors=errors
) )
if not coordinator.data: if not data:
return self.async_abort(reason="no_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 # 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. # is not guaranteed to exist on all APC UPS models.
await self.async_set_unique_id(coordinator.ups_serial_no) await self.async_set_unique_id(data.serial_no)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
title = "APC UPS" title = data.name or data.model or data.serial_no or "APC UPS"
if coordinator.ups_name is not None: return self.async_create_entry(title=title, data=user_input)
title = coordinator.ups_name
elif coordinator.ups_model is not None:
title = coordinator.ups_model
elif coordinator.ups_serial_no is not None:
title = coordinator.ups_serial_no
return self.async_create_entry(
title=title,
data=user_input,
)

View file

@ -2,3 +2,4 @@
from typing import Final from typing import Final
DOMAIN: Final = "apcupsd" DOMAIN: Final = "apcupsd"
CONNECTION_TIMEOUT: int = 10

View file

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections import OrderedDict
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Final from typing import Final
@ -19,14 +18,35 @@ from homeassistant.helpers.update_coordinator import (
UpdateFailed, UpdateFailed,
) )
from .const import DOMAIN from .const import CONNECTION_TIMEOUT, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL: Final = timedelta(seconds=60) UPDATE_INTERVAL: Final = timedelta(seconds=60)
REQUEST_REFRESH_COOLDOWN: Final = 5 REQUEST_REFRESH_COOLDOWN: Final = 5
class APCUPSdCoordinator(DataUpdateCoordinator[OrderedDict[str, str]]): class APCUPSdData(dict[str, str]):
"""Store data about an APCUPSd and provide a few helper methods for easier accesses."""
@property
def name(self) -> str | None:
"""Return the name of the UPS, if available."""
return self.get("UPSNAME")
@property
def model(self) -> str | None:
"""Return the model of the UPS, if available."""
# Different UPS models may report slightly different keys for model, here we
# try them all.
return self.get("APCMODEL") or self.get("MODEL")
@property
def serial_no(self) -> str | None:
"""Return the unique serial number of the UPS, if available."""
return self.get("SERIALNO")
class APCUPSdCoordinator(DataUpdateCoordinator[APCUPSdData]):
"""Store and coordinate the data retrieved from APCUPSd for all sensors. """Store and coordinate the data retrieved from APCUPSd for all sensors.
For each entity to use, acts as the single point responsible for fetching For each entity to use, acts as the single point responsible for fetching
@ -52,46 +72,27 @@ class APCUPSdCoordinator(DataUpdateCoordinator[OrderedDict[str, str]]):
self._host = host self._host = host
self._port = port self._port = port
@property
def ups_name(self) -> str | None:
"""Return the name of the UPS, if available."""
return self.data.get("UPSNAME")
@property
def ups_model(self) -> str | None:
"""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.data:
return self.data[model_key]
return None
@property
def ups_serial_no(self) -> str | None:
"""Return the unique serial number of the UPS, if available."""
return self.data.get("SERIALNO")
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return the DeviceInfo of this APC UPS, if serial number is available.""" """Return the DeviceInfo of this APC UPS, if serial number is available."""
return DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self.ups_serial_no or self.config_entry.entry_id)}, identifiers={(DOMAIN, self.data.serial_no or self.config_entry.entry_id)},
model=self.ups_model, model=self.data.model,
manufacturer="APC", manufacturer="APC",
name=self.ups_name if self.ups_name else "APC UPS", name=self.data.name or "APC UPS",
hw_version=self.data.get("FIRMWARE"), hw_version=self.data.get("FIRMWARE"),
sw_version=self.data.get("VERSION"), sw_version=self.data.get("VERSION"),
) )
async def _async_update_data(self) -> OrderedDict[str, str]: async def _async_update_data(self) -> APCUPSdData:
"""Fetch the latest status from APCUPSd. """Fetch the latest status from APCUPSd.
Note that the result dict uses upper case for each resource, where our Note that the result dict uses upper case for each resource, where our
integration uses lower cases as keys internally. integration uses lower cases as keys internally.
""" """
async with asyncio.timeout(10): async with asyncio.timeout(CONNECTION_TIMEOUT):
try: try:
return await aioapcaccess.request_status(self._host, self._port) data = await aioapcaccess.request_status(self._host, self._port)
return APCUPSdData(data)
except (OSError, asyncio.IncompleteReadError) as error: except (OSError, asyncio.IncompleteReadError) as error:
raise UpdateFailed(error) from error raise UpdateFailed(error) from error

View file

@ -24,7 +24,8 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import DOMAIN, APCUPSdCoordinator from .const import DOMAIN
from .coordinator import APCUPSdCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -493,7 +494,7 @@ class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity):
super().__init__(coordinator=coordinator, context=description.key.upper()) super().__init__(coordinator=coordinator, context=description.key.upper())
# Set up unique id and device info if serial number is available. # Set up unique id and device info if serial number is available.
if (serial_no := coordinator.ups_serial_no) is not None: if (serial_no := coordinator.data.serial_no) is not None:
self._attr_unique_id = f"{serial_no}_{description.key}" self._attr_unique_id = f"{serial_no}_{description.key}"
self.entity_description = description self.entity_description = description