mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 10:17:09 +00:00
Improve device action config entry lookup in NUT (#142133)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
53df69ee6e
commit
0752807aaf
@ -2,15 +2,18 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.device_automation import InvalidDeviceAutomationConfig
|
from homeassistant.components.device_automation import InvalidDeviceAutomationConfig
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
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 config_validation as cv, device_registry as dr
|
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||||
|
|
||||||
from . import NutRuntimeData
|
from . import NutConfigEntry, NutRuntimeData
|
||||||
from .const import DOMAIN, INTEGRATION_SUPPORTED_COMMANDS
|
from .const import DOMAIN, INTEGRATION_SUPPORTED_COMMANDS
|
||||||
|
|
||||||
ACTION_TYPES = {cmd.replace(".", "_") for cmd in INTEGRATION_SUPPORTED_COMMANDS}
|
ACTION_TYPES = {cmd.replace(".", "_") for cmd in INTEGRATION_SUPPORTED_COMMANDS}
|
||||||
@ -48,15 +51,10 @@ 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]
|
||||||
runtime_data = _get_runtime_data_from_device_id(hass, device_id)
|
|
||||||
if not runtime_data:
|
if runtime_data := _get_runtime_data_from_device_id_exception_on_failure(
|
||||||
raise InvalidDeviceAutomationConfig(
|
hass, device_id
|
||||||
translation_domain=DOMAIN,
|
):
|
||||||
translation_key="device_invalid",
|
|
||||||
translation_placeholders={
|
|
||||||
"device_id": device_id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await runtime_data.data.async_run_command(command_name)
|
await runtime_data.data.async_run_command(command_name)
|
||||||
|
|
||||||
|
|
||||||
@ -69,13 +67,55 @@ def _get_command_name(device_action_name: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _get_runtime_data_from_device_id(
|
def _get_runtime_data_from_device_id(
|
||||||
hass: HomeAssistant, device_id: str
|
hass: HomeAssistant,
|
||||||
|
device_id: str,
|
||||||
) -> NutRuntimeData | None:
|
) -> NutRuntimeData | None:
|
||||||
|
"""Find the runtime data for device ID and return None on error."""
|
||||||
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
|
||||||
entry = hass.config_entries.async_get_entry(
|
return _get_runtime_data_for_device(hass, device)
|
||||||
next(entry_id for entry_id in device.config_entries)
|
|
||||||
|
|
||||||
|
def _get_runtime_data_for_device(
|
||||||
|
hass: HomeAssistant, device: dr.DeviceEntry
|
||||||
|
) -> NutRuntimeData | None:
|
||||||
|
"""Find the runtime data for device and return None on error."""
|
||||||
|
for config_entry_id in device.config_entries:
|
||||||
|
entry = hass.config_entries.async_get_entry(config_entry_id)
|
||||||
|
if (
|
||||||
|
entry
|
||||||
|
and entry.domain == DOMAIN
|
||||||
|
and entry.state is ConfigEntryState.LOADED
|
||||||
|
and hasattr(entry, "runtime_data")
|
||||||
|
):
|
||||||
|
return cast(NutConfigEntry, entry).runtime_data
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_runtime_data_from_device_id_exception_on_failure(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
device_id: str,
|
||||||
|
) -> NutRuntimeData | None:
|
||||||
|
"""Find the runtime data for device ID and raise exception on error."""
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
if (device := device_registry.async_get(device_id)) is None:
|
||||||
|
raise InvalidDeviceAutomationConfig(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="device_not_found",
|
||||||
|
translation_placeholders={
|
||||||
|
"device_id": device_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if runtime_data := _get_runtime_data_for_device(hass, device):
|
||||||
|
return runtime_data
|
||||||
|
|
||||||
|
raise InvalidDeviceAutomationConfig(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="config_invalid",
|
||||||
|
translation_placeholders={
|
||||||
|
"device_id": device_id,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
assert entry and isinstance(entry.runtime_data, NutRuntimeData)
|
|
||||||
return entry.runtime_data
|
|
||||||
|
@ -312,13 +312,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
|
"config_invalid": {
|
||||||
|
"message": "Invalid configuration entries for NUT device with ID {device_id}"
|
||||||
|
},
|
||||||
"data_fetch_error": {
|
"data_fetch_error": {
|
||||||
"message": "Error fetching UPS state: {err}"
|
"message": "Error fetching UPS state: {err}"
|
||||||
},
|
},
|
||||||
"device_authentication": {
|
"device_authentication": {
|
||||||
"message": "Device authentication error: {err}"
|
"message": "Device authentication error: {err}"
|
||||||
},
|
},
|
||||||
"device_invalid": {
|
"device_not_found": {
|
||||||
"message": "Unable to find a NUT device with ID {device_id}"
|
"message": "Unable to find a NUT device with ID {device_id}"
|
||||||
},
|
},
|
||||||
"nut_command_error": {
|
"nut_command_error": {
|
||||||
|
@ -21,7 +21,7 @@ from homeassistant.setup import async_setup_component
|
|||||||
|
|
||||||
from .util import async_init_integration
|
from .util import async_init_integration
|
||||||
|
|
||||||
from tests.common import async_get_device_automations
|
from tests.common import MockConfigEntry, async_get_device_automations
|
||||||
|
|
||||||
|
|
||||||
async def test_get_all_actions_for_specified_user(
|
async def test_get_all_actions_for_specified_user(
|
||||||
@ -79,10 +79,10 @@ async def test_no_actions_for_anonymous_user(
|
|||||||
assert len(actions) == 0
|
assert len(actions) == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_no_actions_invalid_device(
|
async def test_no_actions_device_not_found(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test we get no actions for an invalid device."""
|
"""Test we get no actions for a device that cannot be found."""
|
||||||
list_commands_return_value = {"beeper.enable": None}
|
list_commands_return_value = {"beeper.enable": None}
|
||||||
await async_init_integration(
|
await async_init_integration(
|
||||||
hass,
|
hass,
|
||||||
@ -99,6 +99,30 @@ async def test_no_actions_invalid_device(
|
|||||||
assert len(actions) == 0
|
assert len(actions) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_actions_device_invalid(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test we get no actions for a device that is invalid."""
|
||||||
|
list_commands_return_value = {"beeper.enable": None}
|
||||||
|
entry = await async_init_integration(
|
||||||
|
hass,
|
||||||
|
list_vars={"ups.status": "OL"},
|
||||||
|
list_commands_return_value=list_commands_return_value,
|
||||||
|
)
|
||||||
|
device_entry = next(device for device in device_registry.devices.values())
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
platform = await device_automation.async_get_device_automation_platform(
|
||||||
|
hass, DOMAIN, DeviceAutomationType.ACTION
|
||||||
|
)
|
||||||
|
actions = await platform.async_get_actions(hass, device_entry.id)
|
||||||
|
|
||||||
|
assert len(actions) == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_list_commands_exception(
|
async def test_list_commands_exception(
|
||||||
hass: HomeAssistant, device_registry: dr.DeviceRegistry
|
hass: HomeAssistant, device_registry: dr.DeviceRegistry
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -227,8 +251,8 @@ async def test_run_command_exception(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_action_exception_invalid_device(hass: HomeAssistant) -> None:
|
async def test_action_exception_device_not_found(hass: HomeAssistant) -> None:
|
||||||
"""Test raises exception if invalid device."""
|
"""Test raises exception if device not found."""
|
||||||
list_commands_return_value = {"beeper.enable": None}
|
list_commands_return_value = {"beeper.enable": None}
|
||||||
await async_init_integration(
|
await async_init_integration(
|
||||||
hass,
|
hass,
|
||||||
@ -249,3 +273,64 @@ async def test_action_exception_invalid_device(hass: HomeAssistant) -> None:
|
|||||||
{},
|
{},
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_action_exception_invalid_config(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test raises exception if no NUT config entry found."""
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry()
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
device_entry = device_registry.async_get_or_create(
|
||||||
|
config_entry_id=config_entry.entry_id,
|
||||||
|
identifiers={(DOMAIN, "mock-identifier")},
|
||||||
|
)
|
||||||
|
|
||||||
|
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: device_entry.id},
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_action_exception_device_invalid(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test raises exception if config entry for device is invalid."""
|
||||||
|
list_commands_return_value = {"beeper.enable": None}
|
||||||
|
entry = await async_init_integration(
|
||||||
|
hass,
|
||||||
|
list_vars={"ups.status": "OL"},
|
||||||
|
list_commands_return_value=list_commands_return_value,
|
||||||
|
)
|
||||||
|
device_entry = next(device for device in device_registry.devices.values())
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
platform = await device_automation.async_get_device_automation_platform(
|
||||||
|
hass, DOMAIN, DeviceAutomationType.ACTION
|
||||||
|
)
|
||||||
|
|
||||||
|
error_message = (
|
||||||
|
f"Invalid configuration entries for NUT device with ID {device_entry.id}"
|
||||||
|
)
|
||||||
|
with pytest.raises(InvalidDeviceAutomationConfig, match=error_message):
|
||||||
|
await platform.async_call_action_from_config(
|
||||||
|
hass,
|
||||||
|
{CONF_TYPE: "beeper.enable", CONF_DEVICE_ID: device_entry.id},
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user