core/homeassistant/components/zwave_js/addon.py

327 lines
12 KiB
Python

"""Provide add-on management."""
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from enum import Enum
from functools import partial
from typing import Any, Callable, TypeVar, cast
from homeassistant.components.hassio import (
async_create_backup,
async_get_addon_discovery_info,
async_get_addon_info,
async_install_addon,
async_restart_addon,
async_set_addon_options,
async_start_addon,
async_stop_addon,
async_uninstall_addon,
async_update_addon,
)
from homeassistant.components.hassio.handler import HassioAPIError
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_NETWORK_KEY, DOMAIN, LOGGER
F = TypeVar("F", bound=Callable[..., Any]) # pylint: disable=invalid-name
DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager"
@singleton(DATA_ADDON_MANAGER)
@callback
def get_addon_manager(hass: HomeAssistant) -> AddonManager:
"""Get the add-on manager."""
return AddonManager(hass)
def api_error(error_message: str) -> Callable[[F], F]:
"""Handle HassioAPIError and raise a specific AddonError."""
def handle_hassio_api_error(func: F) -> F:
"""Handle a HassioAPIError."""
async def wrapper(*args, **kwargs): # type: ignore
"""Wrap an add-on manager method."""
try:
return_value = await func(*args, **kwargs)
except HassioAPIError as err:
raise AddonError(f"{error_message}: {err}") from err
return return_value
return cast(F, wrapper)
return handle_hassio_api_error
@dataclass
class AddonInfo:
"""Represent the current add-on info state."""
options: dict[str, Any]
state: AddonState
update_available: bool
version: str | None
class AddonState(Enum):
"""Represent the current state of the add-on."""
NOT_INSTALLED = "not_installed"
INSTALLING = "installing"
UPDATING = "updating"
NOT_RUNNING = "not_running"
RUNNING = "running"
class AddonManager:
"""Manage the add-on.
Methods may raise AddonError.
Only one instance of this class may exist
to keep track of running add-on tasks.
"""
def __init__(self, hass: HomeAssistant) -> None:
"""Set up the add-on manager."""
self._hass = hass
self._install_task: asyncio.Task | None = None
self._restart_task: asyncio.Task | None = None
self._start_task: asyncio.Task | None = None
self._update_task: asyncio.Task | None = None
def task_in_progress(self) -> bool:
"""Return True if any of the add-on tasks are in progress."""
return any(
task and not task.done()
for task in (
self._install_task,
self._start_task,
self._update_task,
)
)
@api_error("Failed to get Z-Wave JS 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)
if not discovery_info:
raise AddonError("Failed to get Z-Wave JS 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")
async def async_get_addon_info(self) -> AddonInfo:
"""Return and cache Z-Wave JS add-on info."""
addon_info: dict = await async_get_addon_info(self._hass, ADDON_SLUG)
addon_state = self.async_get_addon_state(addon_info)
return AddonInfo(
options=addon_info["options"],
state=addon_state,
update_available=addon_info["update_available"],
version=addon_info["version"],
)
@callback
def async_get_addon_state(self, addon_info: dict[str, Any]) -> AddonState:
"""Return the current state of the Z-Wave JS add-on."""
addon_state = AddonState.NOT_INSTALLED
if addon_info["version"] is not None:
addon_state = AddonState.NOT_RUNNING
if addon_info["state"] == "started":
addon_state = AddonState.RUNNING
if self._install_task and not self._install_task.done():
addon_state = AddonState.INSTALLING
if self._update_task and not self._update_task.done():
addon_state = AddonState.UPDATING
return addon_state
@api_error("Failed to set the Z-Wave JS add-on options")
async def async_set_addon_options(self, config: dict) -> None:
"""Set Z-Wave JS add-on options."""
options = {"options": config}
await async_set_addon_options(self._hass, ADDON_SLUG, options)
@api_error("Failed to install the Z-Wave JS add-on")
async def async_install_addon(self) -> None:
"""Install the Z-Wave JS add-on."""
await async_install_addon(self._hass, 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.
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")
self._install_task = self._async_schedule_addon_operation(
self.async_install_addon, catch_error=catch_error
)
return self._install_task
@callback
def async_schedule_install_setup_addon(
self, usb_path: str, network_key: str, catch_error: bool = False
) -> asyncio.Task:
"""Schedule a task that installs and sets up the Z-Wave JS 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")
self._install_task = self._async_schedule_addon_operation(
self.async_install_addon,
partial(self.async_configure_addon, usb_path, network_key),
self.async_start_addon,
catch_error=catch_error,
)
return self._install_task
@api_error("Failed to uninstall the Z-Wave JS add-on")
async def async_uninstall_addon(self) -> None:
"""Uninstall the Z-Wave JS add-on."""
await async_uninstall_addon(self._hass, ADDON_SLUG)
@api_error("Failed to update the Z-Wave JS add-on")
async def async_update_addon(self) -> None:
"""Update the Z-Wave JS add-on if needed."""
addon_info = await self.async_get_addon_info()
if addon_info.version is None:
raise AddonError("Z-Wave JS 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)
@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.
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")
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")
async def async_start_addon(self) -> None:
"""Start the Z-Wave JS add-on."""
await async_start_addon(self._hass, ADDON_SLUG)
@api_error("Failed to restart the Z-Wave JS add-on")
async def async_restart_addon(self) -> None:
"""Restart the Z-Wave JS add-on."""
await async_restart_addon(self._hass, 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.
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")
self._start_task = self._async_schedule_addon_operation(
self.async_start_addon, catch_error=catch_error
)
return self._start_task
@callback
def async_schedule_restart_addon(self, catch_error: bool = False) -> asyncio.Task:
"""Schedule a task that restarts the Z-Wave JS 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")
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")
async def async_stop_addon(self) -> None:
"""Stop the Z-Wave JS add-on."""
await async_stop_addon(self._hass, ADDON_SLUG)
async def async_configure_addon(self, usb_path: str, network_key: str) -> None:
"""Configure and start Z-Wave JS add-on."""
addon_info = await self.async_get_addon_info()
new_addon_options = {
CONF_ADDON_DEVICE: usb_path,
CONF_ADDON_NETWORK_KEY: network_key,
}
if new_addon_options != addon_info.options:
await self.async_set_addon_options(new_addon_options)
@callback
def async_schedule_setup_addon(
self, usb_path: str, network_key: str, catch_error: bool = False
) -> asyncio.Task:
"""Schedule a task that configures and starts the Z-Wave JS add-on.
Only schedule a new setup 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")
self._start_task = self._async_schedule_addon_operation(
partial(self.async_configure_addon, usb_path, network_key),
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.")
async def async_create_backup(self) -> None:
"""Create a partial backup of the Z-Wave JS add-on."""
addon_info = await self.async_get_addon_info()
name = f"addon_{ADDON_SLUG}_{addon_info.version}"
LOGGER.debug("Creating backup: %s", name)
await async_create_backup(
self._hass,
{"name": name, "addons": [ADDON_SLUG]},
partial=True,
)
@callback
def _async_schedule_addon_operation(
self, *funcs: Callable, catch_error: bool = False
) -> asyncio.Task:
"""Schedule an add-on task."""
async def addon_operation() -> None:
"""Do the add-on operation and catch AddonError."""
for func in funcs:
try:
await func()
except AddonError as err:
if not catch_error:
raise
LOGGER.error(err)
break
return self._hass.async_create_task(addon_operation())
class AddonError(HomeAssistantError):
"""Represent an error with Z-Wave JS add-on."""