281 lines
10 KiB
Python
281 lines
10 KiB
Python
"""Provide add-on management."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from functools import partial
|
|
from typing import Any, Callable, TypeVar, cast
|
|
|
|
from homeassistant.components.hassio import (
|
|
async_create_snapshot,
|
|
async_get_addon_discovery_info,
|
|
async_get_addon_info,
|
|
async_install_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(error_message) from err
|
|
|
|
return return_value
|
|
|
|
return cast(F, wrapper)
|
|
|
|
return handle_hassio_api_error
|
|
|
|
|
|
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._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) -> dict:
|
|
"""Return and cache Z-Wave JS add-on info."""
|
|
addon_info: dict = await async_get_addon_info(self._hass, ADDON_SLUG)
|
|
return addon_info
|
|
|
|
async def async_is_addon_running(self) -> bool:
|
|
"""Return True if Z-Wave JS add-on is running."""
|
|
addon_info = await self.async_get_addon_info()
|
|
return bool(addon_info["state"] == "started")
|
|
|
|
async def async_is_addon_installed(self) -> bool:
|
|
"""Return True if Z-Wave JS add-on is installed."""
|
|
addon_info = await self.async_get_addon_info()
|
|
return addon_info["version"] is not None
|
|
|
|
async def async_get_addon_options(self) -> dict:
|
|
"""Get Z-Wave JS add-on options."""
|
|
addon_info = await self.async_get_addon_info()
|
|
return cast(dict, addon_info["options"])
|
|
|
|
@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()
|
|
addon_version = addon_info["version"]
|
|
update_available = addon_info["update_available"]
|
|
|
|
if addon_version is None:
|
|
raise AddonError("Z-Wave JS add-on is not installed")
|
|
|
|
if not update_available:
|
|
return
|
|
|
|
await self.async_create_snapshot()
|
|
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)
|
|
|
|
@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
|
|
|
|
@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_options = await self.async_get_addon_options()
|
|
|
|
new_addon_options = {
|
|
CONF_ADDON_DEVICE: usb_path,
|
|
CONF_ADDON_NETWORK_KEY: network_key,
|
|
}
|
|
|
|
if new_addon_options != addon_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 snapshot of the Z-Wave JS add-on.")
|
|
async def async_create_snapshot(self) -> None:
|
|
"""Create a partial snapshot of the Z-Wave JS add-on."""
|
|
addon_info = await self.async_get_addon_info()
|
|
addon_version = addon_info["version"]
|
|
name = f"addon_{ADDON_SLUG}_{addon_version}"
|
|
|
|
LOGGER.debug("Creating snapshot: %s", name)
|
|
await async_create_snapshot(
|
|
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."""
|