Refactor zwave_js add-on manager (#80883)
* Make addon slug an instance attribute * Extract addon name and addon config * Update docstringspull/80906/head
parent
4279d73800
commit
838691f22f
|
@ -854,24 +854,24 @@ async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) ->
|
|||
s2_unauthenticated_key: str = entry.data.get(CONF_S2_UNAUTHENTICATED_KEY, "")
|
||||
addon_state = addon_info.state
|
||||
|
||||
addon_config = {
|
||||
CONF_ADDON_DEVICE: usb_path,
|
||||
CONF_ADDON_S0_LEGACY_KEY: s0_legacy_key,
|
||||
CONF_ADDON_S2_ACCESS_CONTROL_KEY: s2_access_control_key,
|
||||
CONF_ADDON_S2_AUTHENTICATED_KEY: s2_authenticated_key,
|
||||
CONF_ADDON_S2_UNAUTHENTICATED_KEY: s2_unauthenticated_key,
|
||||
}
|
||||
|
||||
if addon_state == AddonState.NOT_INSTALLED:
|
||||
addon_manager.async_schedule_install_setup_addon(
|
||||
usb_path,
|
||||
s0_legacy_key,
|
||||
s2_access_control_key,
|
||||
s2_authenticated_key,
|
||||
s2_unauthenticated_key,
|
||||
addon_config,
|
||||
catch_error=True,
|
||||
)
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
if addon_state == AddonState.NOT_RUNNING:
|
||||
addon_manager.async_schedule_setup_addon(
|
||||
usb_path,
|
||||
s0_legacy_key,
|
||||
s2_access_control_key,
|
||||
s2_authenticated_key,
|
||||
s2_unauthenticated_key,
|
||||
addon_config,
|
||||
catch_error=True,
|
||||
)
|
||||
raise ConfigEntryNotReady
|
||||
|
|
|
@ -5,10 +5,10 @@ import asyncio
|
|||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from functools import partial
|
||||
from functools import partial, wraps
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from typing_extensions import ParamSpec
|
||||
from typing_extensions import Concatenate, ParamSpec
|
||||
|
||||
from homeassistant.components.hassio import (
|
||||
async_create_backup,
|
||||
|
@ -28,17 +28,9 @@ from homeassistant.core import HomeAssistant, callback
|
|||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
|
||||
from .const import (
|
||||
ADDON_SLUG,
|
||||
CONF_ADDON_DEVICE,
|
||||
CONF_ADDON_S0_LEGACY_KEY,
|
||||
CONF_ADDON_S2_ACCESS_CONTROL_KEY,
|
||||
CONF_ADDON_S2_AUTHENTICATED_KEY,
|
||||
CONF_ADDON_S2_UNAUTHENTICATED_KEY,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
from .const import ADDON_SLUG, DOMAIN, LOGGER
|
||||
|
||||
_AddonManagerT = TypeVar("_AddonManagerT", bound="AddonManager")
|
||||
_R = TypeVar("_R")
|
||||
_P = ParamSpec("_P")
|
||||
|
||||
|
@ -49,25 +41,33 @@ DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager"
|
|||
@callback
|
||||
def get_addon_manager(hass: HomeAssistant) -> AddonManager:
|
||||
"""Get the add-on manager."""
|
||||
return AddonManager(hass)
|
||||
return AddonManager(hass, "Z-Wave JS", ADDON_SLUG)
|
||||
|
||||
|
||||
def api_error(
|
||||
error_message: str,
|
||||
) -> Callable[[Callable[_P, Awaitable[_R]]], Callable[_P, Coroutine[Any, Any, _R]]]:
|
||||
) -> Callable[
|
||||
[Callable[Concatenate[_AddonManagerT, _P], Awaitable[_R]]],
|
||||
Callable[Concatenate[_AddonManagerT, _P], Coroutine[Any, Any, _R]],
|
||||
]:
|
||||
"""Handle HassioAPIError and raise a specific AddonError."""
|
||||
|
||||
def handle_hassio_api_error(
|
||||
func: Callable[_P, Awaitable[_R]]
|
||||
) -> Callable[_P, Coroutine[Any, Any, _R]]:
|
||||
func: Callable[Concatenate[_AddonManagerT, _P], Awaitable[_R]]
|
||||
) -> Callable[Concatenate[_AddonManagerT, _P], Coroutine[Any, Any, _R]]:
|
||||
"""Handle a HassioAPIError."""
|
||||
|
||||
async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
@wraps(func)
|
||||
async def wrapper(
|
||||
self: _AddonManagerT, *args: _P.args, **kwargs: _P.kwargs
|
||||
) -> _R:
|
||||
"""Wrap an add-on manager method."""
|
||||
try:
|
||||
return_value = await func(*args, **kwargs)
|
||||
return_value = await func(self, *args, **kwargs)
|
||||
except HassioAPIError as err:
|
||||
raise AddonError(f"{error_message}: {err}") from err
|
||||
raise AddonError(
|
||||
f"{error_message.format(addon_name=self.addon_name)}: {err}"
|
||||
) from err
|
||||
|
||||
return return_value
|
||||
|
||||
|
@ -100,12 +100,14 @@ class AddonManager:
|
|||
"""Manage the add-on.
|
||||
|
||||
Methods may raise AddonError.
|
||||
Only one instance of this class may exist
|
||||
Only one instance of this class may exist per add-on
|
||||
to keep track of running add-on tasks.
|
||||
"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
def __init__(self, hass: HomeAssistant, addon_name: str, addon_slug: str) -> None:
|
||||
"""Set up the add-on manager."""
|
||||
self.addon_name = addon_name
|
||||
self.addon_slug = addon_slug
|
||||
self._hass = hass
|
||||
self._install_task: asyncio.Task | None = None
|
||||
self._restart_task: asyncio.Task | None = None
|
||||
|
@ -123,21 +125,23 @@ class AddonManager:
|
|||
)
|
||||
)
|
||||
|
||||
@api_error("Failed to get Z-Wave JS add-on discovery info")
|
||||
@api_error("Failed to get {addon_name} add-on discovery info")
|
||||
async def async_get_addon_discovery_info(self) -> dict:
|
||||
"""Return add-on discovery info."""
|
||||
discovery_info = await async_get_addon_discovery_info(self._hass, ADDON_SLUG)
|
||||
discovery_info = await async_get_addon_discovery_info(
|
||||
self._hass, self.addon_slug
|
||||
)
|
||||
|
||||
if not discovery_info:
|
||||
raise AddonError("Failed to get Z-Wave JS add-on discovery info")
|
||||
raise AddonError(f"Failed to get {self.addon_name} add-on discovery info")
|
||||
|
||||
discovery_info_config: dict = discovery_info["config"]
|
||||
return discovery_info_config
|
||||
|
||||
@api_error("Failed to get the Z-Wave JS add-on info")
|
||||
@api_error("Failed to get the {addon_name} add-on info")
|
||||
async def async_get_addon_info(self) -> AddonInfo:
|
||||
"""Return and cache Z-Wave JS add-on info."""
|
||||
addon_store_info = await async_get_addon_store_info(self._hass, ADDON_SLUG)
|
||||
"""Return and cache manager add-on info."""
|
||||
addon_store_info = await async_get_addon_store_info(self._hass, self.addon_slug)
|
||||
LOGGER.debug("Add-on store info: %s", addon_store_info)
|
||||
if not addon_store_info["installed"]:
|
||||
return AddonInfo(
|
||||
|
@ -147,7 +151,7 @@ class AddonManager:
|
|||
version=None,
|
||||
)
|
||||
|
||||
addon_info = await async_get_addon_info(self._hass, ADDON_SLUG)
|
||||
addon_info = await async_get_addon_info(self._hass, self.addon_slug)
|
||||
addon_state = self.async_get_addon_state(addon_info)
|
||||
return AddonInfo(
|
||||
options=addon_info["options"],
|
||||
|
@ -158,7 +162,7 @@ class AddonManager:
|
|||
|
||||
@callback
|
||||
def async_get_addon_state(self, addon_info: dict[str, Any]) -> AddonState:
|
||||
"""Return the current state of the Z-Wave JS add-on."""
|
||||
"""Return the current state of the managed add-on."""
|
||||
addon_state = AddonState.NOT_RUNNING
|
||||
|
||||
if addon_info["state"] == "started":
|
||||
|
@ -170,25 +174,27 @@ class AddonManager:
|
|||
|
||||
return addon_state
|
||||
|
||||
@api_error("Failed to set the Z-Wave JS add-on options")
|
||||
@api_error("Failed to set the {addon_name} add-on options")
|
||||
async def async_set_addon_options(self, config: dict) -> None:
|
||||
"""Set Z-Wave JS add-on options."""
|
||||
"""Set manager add-on options."""
|
||||
options = {"options": config}
|
||||
await async_set_addon_options(self._hass, ADDON_SLUG, options)
|
||||
await async_set_addon_options(self._hass, self.addon_slug, options)
|
||||
|
||||
@api_error("Failed to install the Z-Wave JS add-on")
|
||||
@api_error("Failed to install the {addon_name} add-on")
|
||||
async def async_install_addon(self) -> None:
|
||||
"""Install the Z-Wave JS add-on."""
|
||||
await async_install_addon(self._hass, ADDON_SLUG)
|
||||
"""Install the managed add-on."""
|
||||
await async_install_addon(self._hass, self.addon_slug)
|
||||
|
||||
@callback
|
||||
def async_schedule_install_addon(self, catch_error: bool = False) -> asyncio.Task:
|
||||
"""Schedule a task that installs the Z-Wave JS add-on.
|
||||
"""Schedule a task that installs the managed add-on.
|
||||
|
||||
Only schedule a new install task if the there's no running task.
|
||||
"""
|
||||
if not self._install_task or self._install_task.done():
|
||||
LOGGER.info("Z-Wave JS add-on is not installed. Installing add-on")
|
||||
LOGGER.info(
|
||||
"%s add-on is not installed. Installing add-on", self.addon_name
|
||||
)
|
||||
self._install_task = self._async_schedule_addon_operation(
|
||||
self.async_install_addon, catch_error=catch_error
|
||||
)
|
||||
|
@ -197,85 +203,79 @@ class AddonManager:
|
|||
@callback
|
||||
def async_schedule_install_setup_addon(
|
||||
self,
|
||||
usb_path: str,
|
||||
s0_legacy_key: str,
|
||||
s2_access_control_key: str,
|
||||
s2_authenticated_key: str,
|
||||
s2_unauthenticated_key: str,
|
||||
addon_config: dict[str, Any],
|
||||
catch_error: bool = False,
|
||||
) -> asyncio.Task:
|
||||
"""Schedule a task that installs and sets up the Z-Wave JS add-on.
|
||||
"""Schedule a task that installs and sets up the managed add-on.
|
||||
|
||||
Only schedule a new install task if the there's no running task.
|
||||
"""
|
||||
if not self._install_task or self._install_task.done():
|
||||
LOGGER.info("Z-Wave JS add-on is not installed. Installing add-on")
|
||||
LOGGER.info(
|
||||
"%s add-on is not installed. Installing add-on", self.addon_name
|
||||
)
|
||||
self._install_task = self._async_schedule_addon_operation(
|
||||
self.async_install_addon,
|
||||
partial(
|
||||
self.async_configure_addon,
|
||||
usb_path,
|
||||
s0_legacy_key,
|
||||
s2_access_control_key,
|
||||
s2_authenticated_key,
|
||||
s2_unauthenticated_key,
|
||||
addon_config,
|
||||
),
|
||||
self.async_start_addon,
|
||||
catch_error=catch_error,
|
||||
)
|
||||
return self._install_task
|
||||
|
||||
@api_error("Failed to uninstall the Z-Wave JS add-on")
|
||||
@api_error("Failed to uninstall the {addon_name} add-on")
|
||||
async def async_uninstall_addon(self) -> None:
|
||||
"""Uninstall the Z-Wave JS add-on."""
|
||||
await async_uninstall_addon(self._hass, ADDON_SLUG)
|
||||
"""Uninstall the managed add-on."""
|
||||
await async_uninstall_addon(self._hass, self.addon_slug)
|
||||
|
||||
@api_error("Failed to update the Z-Wave JS add-on")
|
||||
@api_error("Failed to update the {addon_name} add-on")
|
||||
async def async_update_addon(self) -> None:
|
||||
"""Update the Z-Wave JS add-on if needed."""
|
||||
"""Update the managed add-on if needed."""
|
||||
addon_info = await self.async_get_addon_info()
|
||||
|
||||
if addon_info.state is AddonState.NOT_INSTALLED:
|
||||
raise AddonError("Z-Wave JS add-on is not installed")
|
||||
raise AddonError(f"{self.addon_name} add-on is not installed")
|
||||
|
||||
if not addon_info.update_available:
|
||||
return
|
||||
|
||||
await self.async_create_backup()
|
||||
await async_update_addon(self._hass, ADDON_SLUG)
|
||||
await async_update_addon(self._hass, self.addon_slug)
|
||||
|
||||
@callback
|
||||
def async_schedule_update_addon(self, catch_error: bool = False) -> asyncio.Task:
|
||||
"""Schedule a task that updates and sets up the Z-Wave JS add-on.
|
||||
"""Schedule a task that updates and sets up the managed add-on.
|
||||
|
||||
Only schedule a new update task if the there's no running task.
|
||||
"""
|
||||
if not self._update_task or self._update_task.done():
|
||||
LOGGER.info("Trying to update the Z-Wave JS add-on")
|
||||
LOGGER.info("Trying to update the %s add-on", self.addon_name)
|
||||
self._update_task = self._async_schedule_addon_operation(
|
||||
self.async_update_addon,
|
||||
catch_error=catch_error,
|
||||
)
|
||||
return self._update_task
|
||||
|
||||
@api_error("Failed to start the Z-Wave JS add-on")
|
||||
@api_error("Failed to start the {addon_name} add-on")
|
||||
async def async_start_addon(self) -> None:
|
||||
"""Start the Z-Wave JS add-on."""
|
||||
await async_start_addon(self._hass, ADDON_SLUG)
|
||||
"""Start the managed add-on."""
|
||||
await async_start_addon(self._hass, self.addon_slug)
|
||||
|
||||
@api_error("Failed to restart the Z-Wave JS add-on")
|
||||
@api_error("Failed to restart the {addon_name} add-on")
|
||||
async def async_restart_addon(self) -> None:
|
||||
"""Restart the Z-Wave JS add-on."""
|
||||
await async_restart_addon(self._hass, ADDON_SLUG)
|
||||
"""Restart the managed add-on."""
|
||||
await async_restart_addon(self._hass, self.addon_slug)
|
||||
|
||||
@callback
|
||||
def async_schedule_start_addon(self, catch_error: bool = False) -> asyncio.Task:
|
||||
"""Schedule a task that starts the Z-Wave JS add-on.
|
||||
"""Schedule a task that starts the managed add-on.
|
||||
|
||||
Only schedule a new start task if the there's no running task.
|
||||
"""
|
||||
if not self._start_task or self._start_task.done():
|
||||
LOGGER.info("Z-Wave JS add-on is not running. Starting add-on")
|
||||
LOGGER.info("%s add-on is not running. Starting add-on", self.addon_name)
|
||||
self._start_task = self._async_schedule_addon_operation(
|
||||
self.async_start_addon, catch_error=catch_error
|
||||
)
|
||||
|
@ -283,87 +283,67 @@ class AddonManager:
|
|||
|
||||
@callback
|
||||
def async_schedule_restart_addon(self, catch_error: bool = False) -> asyncio.Task:
|
||||
"""Schedule a task that restarts the Z-Wave JS add-on.
|
||||
"""Schedule a task that restarts the managed add-on.
|
||||
|
||||
Only schedule a new restart task if the there's no running task.
|
||||
"""
|
||||
if not self._restart_task or self._restart_task.done():
|
||||
LOGGER.info("Restarting Z-Wave JS add-on")
|
||||
LOGGER.info("Restarting %s add-on", self.addon_name)
|
||||
self._restart_task = self._async_schedule_addon_operation(
|
||||
self.async_restart_addon, catch_error=catch_error
|
||||
)
|
||||
return self._restart_task
|
||||
|
||||
@api_error("Failed to stop the Z-Wave JS add-on")
|
||||
@api_error("Failed to stop the {addon_name} add-on")
|
||||
async def async_stop_addon(self) -> None:
|
||||
"""Stop the Z-Wave JS add-on."""
|
||||
await async_stop_addon(self._hass, ADDON_SLUG)
|
||||
"""Stop the managed add-on."""
|
||||
await async_stop_addon(self._hass, self.addon_slug)
|
||||
|
||||
async def async_configure_addon(
|
||||
self,
|
||||
usb_path: str,
|
||||
s0_legacy_key: str,
|
||||
s2_access_control_key: str,
|
||||
s2_authenticated_key: str,
|
||||
s2_unauthenticated_key: str,
|
||||
addon_config: dict[str, Any],
|
||||
) -> None:
|
||||
"""Configure and start Z-Wave JS add-on."""
|
||||
"""Configure and start manager add-on."""
|
||||
addon_info = await self.async_get_addon_info()
|
||||
|
||||
if addon_info.state is AddonState.NOT_INSTALLED:
|
||||
raise AddonError("Z-Wave JS add-on is not installed")
|
||||
raise AddonError(f"{self.addon_name} add-on is not installed")
|
||||
|
||||
new_addon_options = {
|
||||
CONF_ADDON_DEVICE: usb_path,
|
||||
CONF_ADDON_S0_LEGACY_KEY: s0_legacy_key,
|
||||
CONF_ADDON_S2_ACCESS_CONTROL_KEY: s2_access_control_key,
|
||||
CONF_ADDON_S2_AUTHENTICATED_KEY: s2_authenticated_key,
|
||||
CONF_ADDON_S2_UNAUTHENTICATED_KEY: s2_unauthenticated_key,
|
||||
}
|
||||
|
||||
if new_addon_options != addon_info.options:
|
||||
await self.async_set_addon_options(new_addon_options)
|
||||
if addon_config != addon_info.options:
|
||||
await self.async_set_addon_options(addon_config)
|
||||
|
||||
@callback
|
||||
def async_schedule_setup_addon(
|
||||
self,
|
||||
usb_path: str,
|
||||
s0_legacy_key: str,
|
||||
s2_access_control_key: str,
|
||||
s2_authenticated_key: str,
|
||||
s2_unauthenticated_key: str,
|
||||
addon_config: dict[str, Any],
|
||||
catch_error: bool = False,
|
||||
) -> asyncio.Task:
|
||||
"""Schedule a task that configures and starts the Z-Wave JS add-on.
|
||||
"""Schedule a task that configures and starts the managed add-on.
|
||||
|
||||
Only schedule a new setup task if the there's no running task.
|
||||
Only schedule a new setup task if there's no running task.
|
||||
"""
|
||||
if not self._start_task or self._start_task.done():
|
||||
LOGGER.info("Z-Wave JS add-on is not running. Starting add-on")
|
||||
LOGGER.info("%s add-on is not running. Starting add-on", self.addon_name)
|
||||
self._start_task = self._async_schedule_addon_operation(
|
||||
partial(
|
||||
self.async_configure_addon,
|
||||
usb_path,
|
||||
s0_legacy_key,
|
||||
s2_access_control_key,
|
||||
s2_authenticated_key,
|
||||
s2_unauthenticated_key,
|
||||
addon_config,
|
||||
),
|
||||
self.async_start_addon,
|
||||
catch_error=catch_error,
|
||||
)
|
||||
return self._start_task
|
||||
|
||||
@api_error("Failed to create a backup of the Z-Wave JS add-on.")
|
||||
@api_error("Failed to create a backup of the {addon_name} add-on.")
|
||||
async def async_create_backup(self) -> None:
|
||||
"""Create a partial backup of the Z-Wave JS add-on."""
|
||||
"""Create a partial backup of the managed add-on."""
|
||||
addon_info = await self.async_get_addon_info()
|
||||
name = f"addon_{ADDON_SLUG}_{addon_info.version}"
|
||||
name = f"addon_{self.addon_slug}_{addon_info.version}"
|
||||
|
||||
LOGGER.debug("Creating backup: %s", name)
|
||||
await async_create_backup(
|
||||
self._hass,
|
||||
{"name": name, "addons": [ADDON_SLUG]},
|
||||
{"name": name, "addons": [self.addon_slug]},
|
||||
partial=True,
|
||||
)
|
||||
|
||||
|
@ -388,4 +368,4 @@ class AddonManager:
|
|||
|
||||
|
||||
class AddonError(HomeAssistantError):
|
||||
"""Represent an error with Z-Wave JS add-on."""
|
||||
"""Represent an error with the managed add-on."""
|
||||
|
|
|
@ -2,14 +2,29 @@
|
|||
import pytest
|
||||
|
||||
from homeassistant.components.zwave_js.addon import AddonError, get_addon_manager
|
||||
from homeassistant.components.zwave_js.const import (
|
||||
CONF_ADDON_DEVICE,
|
||||
CONF_ADDON_S0_LEGACY_KEY,
|
||||
CONF_ADDON_S2_ACCESS_CONTROL_KEY,
|
||||
CONF_ADDON_S2_AUTHENTICATED_KEY,
|
||||
CONF_ADDON_S2_UNAUTHENTICATED_KEY,
|
||||
)
|
||||
|
||||
|
||||
async def test_not_installed_raises_exception(hass, addon_not_installed):
|
||||
"""Test addon not installed raises exception."""
|
||||
addon_manager = get_addon_manager(hass)
|
||||
|
||||
addon_config = {
|
||||
CONF_ADDON_DEVICE: "/test",
|
||||
CONF_ADDON_S0_LEGACY_KEY: "123",
|
||||
CONF_ADDON_S2_ACCESS_CONTROL_KEY: "456",
|
||||
CONF_ADDON_S2_AUTHENTICATED_KEY: "789",
|
||||
CONF_ADDON_S2_UNAUTHENTICATED_KEY: "012",
|
||||
}
|
||||
|
||||
with pytest.raises(AddonError):
|
||||
await addon_manager.async_configure_addon("/test", "123", "456", "789", "012")
|
||||
await addon_manager.async_configure_addon(addon_config)
|
||||
|
||||
with pytest.raises(AddonError):
|
||||
await addon_manager.async_update_addon()
|
||||
|
|
Loading…
Reference in New Issue