diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 9082048badf..cab07f4287f 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -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 diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py index 610fc850e90..3e27235ef84 100644 --- a/homeassistant/components/zwave_js/addon.py +++ b/homeassistant/components/zwave_js/addon.py @@ -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.""" diff --git a/tests/components/zwave_js/test_addon.py b/tests/components/zwave_js/test_addon.py index 754be808cea..45f732c1aa2 100644 --- a/tests/components/zwave_js/test_addon.py +++ b/tests/components/zwave_js/test_addon.py @@ -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()