Store runtime data inside the config entry in NUT (#116771)

* store runtime data inside the config entry

* remove unsued constants

* add test for InvalidDeviceAutomationConfig exception

* assert entry

* add more specific type hint
This commit is contained in:
Michael 2024-05-04 23:08:01 +02:00 committed by GitHub
parent 4a25e67234
commit 90a3c2e357
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 77 additions and 64 deletions

View file

@ -26,22 +26,30 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ( from .const import (
COORDINATOR,
DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
DOMAIN, DOMAIN,
INTEGRATION_SUPPORTED_COMMANDS, INTEGRATION_SUPPORTED_COMMANDS,
PLATFORMS, PLATFORMS,
PYNUT_DATA,
PYNUT_UNIQUE_ID,
USER_AVAILABLE_COMMANDS,
) )
NUT_FAKE_SERIAL = ["unknown", "blank"] NUT_FAKE_SERIAL = ["unknown", "blank"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
NutConfigEntry = ConfigEntry["NutRuntimeData"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@dataclass
class NutRuntimeData:
"""Runtime data definition."""
coordinator: DataUpdateCoordinator
data: PyNUTData
unique_id: str
user_available_commands: set[str]
async def async_setup_entry(hass: HomeAssistant, entry: NutConfigEntry) -> bool:
"""Set up Network UPS Tools (NUT) from a config entry.""" """Set up Network UPS Tools (NUT) from a config entry."""
# strip out the stale options CONF_RESOURCES, # strip out the stale options CONF_RESOURCES,
@ -110,13 +118,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
else: else:
user_available_commands = set() user_available_commands = set()
hass.data.setdefault(DOMAIN, {}) entry.runtime_data = NutRuntimeData(
hass.data[DOMAIN][entry.entry_id] = { coordinator, data, unique_id, user_available_commands
COORDINATOR: coordinator, )
PYNUT_DATA: data,
PYNUT_UNIQUE_ID: unique_id,
USER_AVAILABLE_COMMANDS: user_available_commands,
}
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device_registry.async_get_or_create( device_registry.async_get_or_create(
@ -135,9 +139,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:

View file

@ -15,15 +15,8 @@ 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"
DEFAULT_SCAN_INTERVAL = 60 DEFAULT_SCAN_INTERVAL = 60
PYNUT_DATA = "data"
PYNUT_UNIQUE_ID = "unique_id"
USER_AVAILABLE_COMMANDS = "user_available_commands"
STATE_TYPES = { STATE_TYPES = {
"OL": "Online", "OL": "Online",
"OB": "On Battery", "OB": "On Battery",

View file

@ -4,19 +4,15 @@ from __future__ import annotations
import voluptuous as vol import voluptuous as vol
from homeassistant.components.device_automation import InvalidDeviceAutomationConfig
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE
from homeassistant.core import Context, HomeAssistant from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import PyNUTData from . import NutRuntimeData
from .const import ( from .const import DOMAIN, INTEGRATION_SUPPORTED_COMMANDS
DOMAIN,
INTEGRATION_SUPPORTED_COMMANDS,
PYNUT_DATA,
USER_AVAILABLE_COMMANDS,
)
ACTION_TYPES = {cmd.replace(".", "_") for cmd in INTEGRATION_SUPPORTED_COMMANDS} ACTION_TYPES = {cmd.replace(".", "_") for cmd in INTEGRATION_SUPPORTED_COMMANDS}
@ -31,18 +27,15 @@ async def async_get_actions(
hass: HomeAssistant, device_id: str hass: HomeAssistant, device_id: str
) -> list[dict[str, str]]: ) -> list[dict[str, str]]:
"""List device actions for Network UPS Tools (NUT) devices.""" """List device actions for Network UPS Tools (NUT) devices."""
if (entry_id := _get_entry_id_from_device_id(hass, device_id)) is None: if (runtime_data := _get_runtime_data_from_device_id(hass, device_id)) is None:
return [] return []
base_action = { base_action = {
CONF_DEVICE_ID: device_id, CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN, CONF_DOMAIN: DOMAIN,
} }
user_available_commands: set[str] = hass.data[DOMAIN][entry_id][
USER_AVAILABLE_COMMANDS
]
return [ return [
{CONF_TYPE: _get_device_action_name(command_name)} | base_action {CONF_TYPE: _get_device_action_name(command_name)} | base_action
for command_name in user_available_commands for command_name in runtime_data.user_available_commands
] ]
@ -56,9 +49,12 @@ async def async_call_action_from_config(
device_action_name: str = config[CONF_TYPE] device_action_name: str = config[CONF_TYPE]
command_name = _get_command_name(device_action_name) command_name = _get_command_name(device_action_name)
device_id: str = config[CONF_DEVICE_ID] device_id: str = config[CONF_DEVICE_ID]
entry_id = _get_entry_id_from_device_id(hass, device_id) runtime_data = _get_runtime_data_from_device_id(hass, device_id)
data: PyNUTData = hass.data[DOMAIN][entry_id][PYNUT_DATA] if not runtime_data:
await data.async_run_command(command_name) raise InvalidDeviceAutomationConfig(
f"Unable to find a NUT device with id {device_id}"
)
await runtime_data.data.async_run_command(command_name)
def _get_device_action_name(command_name: str) -> str: def _get_device_action_name(command_name: str) -> str:
@ -69,8 +65,14 @@ def _get_command_name(device_action_name: str) -> str:
return device_action_name.replace("_", ".") return device_action_name.replace("_", ".")
def _get_entry_id_from_device_id(hass: HomeAssistant, device_id: str) -> str | None: def _get_runtime_data_from_device_id(
hass: HomeAssistant, device_id: str
) -> NutRuntimeData | None:
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
if (device := device_registry.async_get(device_id)) is None: if (device := device_registry.async_get(device_id)) is None:
return None return None
return next(entry for entry in device.config_entries) entry = hass.config_entries.async_get_entry(
next(entry_id for entry_id in device.config_entries)
)
assert entry and isinstance(entry.runtime_data, NutRuntimeData)
return entry.runtime_data

View file

@ -7,27 +7,26 @@ from typing import Any
import attr import attr
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from . import PyNUTData from . import NutConfigEntry
from .const import DOMAIN, PYNUT_DATA, PYNUT_UNIQUE_ID, USER_AVAILABLE_COMMANDS from .const import DOMAIN
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME} TO_REDACT = {CONF_PASSWORD, CONF_USERNAME}
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, entry: NutConfigEntry
) -> dict[str, dict[str, Any]]: ) -> dict[str, dict[str, Any]]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
data = {"entry": async_redact_data(entry.as_dict(), TO_REDACT)} data = {"entry": async_redact_data(entry.as_dict(), TO_REDACT)}
hass_data = hass.data[DOMAIN][entry.entry_id] hass_data = entry.runtime_data
# Get information from Nut library # Get information from Nut library
nut_data: PyNUTData = hass_data[PYNUT_DATA] nut_data = hass_data.data
nut_cmd: set[str] = hass_data[USER_AVAILABLE_COMMANDS] nut_cmd = hass_data.user_available_commands
data["nut_data"] = { data["nut_data"] = {
"ups_list": nut_data.ups_list, "ups_list": nut_data.ups_list,
"status": nut_data.status, "status": nut_data.status,
@ -38,7 +37,7 @@ async def async_get_config_entry_diagnostics(
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
hass_device = device_registry.async_get_device( hass_device = device_registry.async_get_device(
identifiers={(DOMAIN, hass_data[PYNUT_UNIQUE_ID])} identifiers={(DOMAIN, hass_data.unique_id)}
) )
if not hass_device: if not hass_device:
return data return data

View file

@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_MANUFACTURER, ATTR_MANUFACTURER,
ATTR_MODEL, ATTR_MODEL,
@ -36,16 +35,8 @@ from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator, DataUpdateCoordinator,
) )
from . import PyNUTData from . import NutConfigEntry, PyNUTData
from .const import ( from .const import DOMAIN, KEY_STATUS, KEY_STATUS_DISPLAY, STATE_TYPES
COORDINATOR,
DOMAIN,
KEY_STATUS,
KEY_STATUS_DISPLAY,
PYNUT_DATA,
PYNUT_UNIQUE_ID,
STATE_TYPES,
)
NUT_DEV_INFO_TO_DEV_INFO: dict[str, str] = { NUT_DEV_INFO_TO_DEV_INFO: dict[str, str] = {
"manufacturer": ATTR_MANUFACTURER, "manufacturer": ATTR_MANUFACTURER,
@ -968,15 +959,15 @@ def _get_nut_device_info(data: PyNUTData) -> DeviceInfo:
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: NutConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the NUT sensors.""" """Set up the NUT sensors."""
pynut_data = hass.data[DOMAIN][config_entry.entry_id] pynut_data = config_entry.runtime_data
coordinator = pynut_data[COORDINATOR] coordinator = pynut_data.coordinator
data = pynut_data[PYNUT_DATA] data = pynut_data.data
unique_id = pynut_data[PYNUT_UNIQUE_ID] unique_id = pynut_data.unique_id
status = coordinator.data status = coordinator.data
resources = [sensor_id for sensor_id in SENSOR_TYPES if sensor_id in status] resources = [sensor_id for sensor_id in SENSOR_TYPES if sensor_id in status]

View file

@ -7,9 +7,13 @@ import pytest
from pytest_unordered import unordered from pytest_unordered import unordered
from homeassistant.components import automation, device_automation from homeassistant.components import automation, device_automation
from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation import (
DeviceAutomationType,
InvalidDeviceAutomationConfig,
)
from homeassistant.components.nut import DOMAIN from homeassistant.components.nut import DOMAIN
from homeassistant.components.nut.const import INTEGRATION_SUPPORTED_COMMANDS from homeassistant.components.nut.const import INTEGRATION_SUPPORTED_COMMANDS
from homeassistant.const import CONF_DEVICE_ID, CONF_TYPE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -229,3 +233,25 @@ async def test_rund_command_exception(
await hass.async_block_till_done() await hass.async_block_till_done()
assert error_message in caplog.text assert error_message in caplog.text
async def test_action_exception_invalid_device(hass: HomeAssistant) -> None:
"""Test raises exception if invalid device."""
list_commands_return_value = {"beeper.enable": None}
await async_init_integration(
hass,
list_vars={"ups.status": "OL"},
list_commands_return_value=list_commands_return_value,
)
platform = await device_automation.async_get_device_automation_platform(
hass, DOMAIN, DeviceAutomationType.ACTION
)
with pytest.raises(InvalidDeviceAutomationConfig):
await platform.async_call_action_from_config(
hass,
{CONF_TYPE: "beeper.enable", CONF_DEVICE_ID: "invalid_device_id"},
{},
None,
)