mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
Move zwave_js addon manager to hassio integration (#81354)
This commit is contained in:
parent
0bd04068de
commit
9ded232522
@ -47,6 +47,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
|||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
|
from .addon_manager import AddonError, AddonInfo, AddonManager, AddonState # noqa: F401
|
||||||
from .addon_panel import async_setup_addon_panel
|
from .addon_panel import async_setup_addon_panel
|
||||||
from .auth import async_setup_auth_view
|
from .auth import async_setup_auth_view
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -55,7 +56,6 @@ from .const import (
|
|||||||
ATTR_AUTO_UPDATE,
|
ATTR_AUTO_UPDATE,
|
||||||
ATTR_CHANGELOG,
|
ATTR_CHANGELOG,
|
||||||
ATTR_COMPRESSED,
|
ATTR_COMPRESSED,
|
||||||
ATTR_DISCOVERY,
|
|
||||||
ATTR_FOLDERS,
|
ATTR_FOLDERS,
|
||||||
ATTR_HOMEASSISTANT,
|
ATTR_HOMEASSISTANT,
|
||||||
ATTR_INPUT,
|
ATTR_INPUT,
|
||||||
@ -74,7 +74,25 @@ from .const import (
|
|||||||
SupervisorEntityModel,
|
SupervisorEntityModel,
|
||||||
)
|
)
|
||||||
from .discovery import HassioServiceInfo, async_setup_discovery_view # noqa: F401
|
from .discovery import HassioServiceInfo, async_setup_discovery_view # noqa: F401
|
||||||
from .handler import HassIO, HassioAPIError, api_data
|
from .handler import ( # noqa: F401
|
||||||
|
HassIO,
|
||||||
|
HassioAPIError,
|
||||||
|
async_create_backup,
|
||||||
|
async_get_addon_discovery_info,
|
||||||
|
async_get_addon_info,
|
||||||
|
async_get_addon_store_info,
|
||||||
|
async_install_addon,
|
||||||
|
async_restart_addon,
|
||||||
|
async_set_addon_options,
|
||||||
|
async_start_addon,
|
||||||
|
async_stop_addon,
|
||||||
|
async_uninstall_addon,
|
||||||
|
async_update_addon,
|
||||||
|
async_update_core,
|
||||||
|
async_update_diagnostics,
|
||||||
|
async_update_os,
|
||||||
|
async_update_supervisor,
|
||||||
|
)
|
||||||
from .http import HassIOView
|
from .http import HassIOView
|
||||||
from .ingress import async_setup_ingress_view
|
from .ingress import async_setup_ingress_view
|
||||||
from .repairs import SupervisorRepairs
|
from .repairs import SupervisorRepairs
|
||||||
@ -221,202 +239,6 @@ HARDWARE_INTEGRATIONS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
async def async_get_addon_info(hass: HomeAssistant, slug: str) -> dict:
|
|
||||||
"""Return add-on info.
|
|
||||||
|
|
||||||
The add-on must be installed.
|
|
||||||
The caller of the function should handle HassioAPIError.
|
|
||||||
"""
|
|
||||||
hassio = hass.data[DOMAIN]
|
|
||||||
return await hassio.get_addon_info(slug)
|
|
||||||
|
|
||||||
|
|
||||||
@api_data
|
|
||||||
async def async_get_addon_store_info(hass: HomeAssistant, slug: str) -> dict:
|
|
||||||
"""Return add-on store info.
|
|
||||||
|
|
||||||
The caller of the function should handle HassioAPIError.
|
|
||||||
"""
|
|
||||||
hassio: HassIO = hass.data[DOMAIN]
|
|
||||||
command = f"/store/addons/{slug}"
|
|
||||||
return await hassio.send_command(command, method="get")
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> dict:
|
|
||||||
"""Update Supervisor diagnostics toggle.
|
|
||||||
|
|
||||||
The caller of the function should handle HassioAPIError.
|
|
||||||
"""
|
|
||||||
hassio = hass.data[DOMAIN]
|
|
||||||
return await hassio.update_diagnostics(diagnostics)
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
@api_data
|
|
||||||
async def async_install_addon(hass: HomeAssistant, slug: str) -> dict:
|
|
||||||
"""Install add-on.
|
|
||||||
|
|
||||||
The caller of the function should handle HassioAPIError.
|
|
||||||
"""
|
|
||||||
hassio = hass.data[DOMAIN]
|
|
||||||
command = f"/addons/{slug}/install"
|
|
||||||
return await hassio.send_command(command, timeout=None)
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
@api_data
|
|
||||||
async def async_uninstall_addon(hass: HomeAssistant, slug: str) -> dict:
|
|
||||||
"""Uninstall add-on.
|
|
||||||
|
|
||||||
The caller of the function should handle HassioAPIError.
|
|
||||||
"""
|
|
||||||
hassio = hass.data[DOMAIN]
|
|
||||||
command = f"/addons/{slug}/uninstall"
|
|
||||||
return await hassio.send_command(command, timeout=60)
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
@api_data
|
|
||||||
async def async_update_addon(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
slug: str,
|
|
||||||
backup: bool = False,
|
|
||||||
) -> dict:
|
|
||||||
"""Update add-on.
|
|
||||||
|
|
||||||
The caller of the function should handle HassioAPIError.
|
|
||||||
"""
|
|
||||||
hassio = hass.data[DOMAIN]
|
|
||||||
command = f"/addons/{slug}/update"
|
|
||||||
return await hassio.send_command(
|
|
||||||
command,
|
|
||||||
payload={"backup": backup},
|
|
||||||
timeout=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
@api_data
|
|
||||||
async def async_start_addon(hass: HomeAssistant, slug: str) -> dict:
|
|
||||||
"""Start add-on.
|
|
||||||
|
|
||||||
The caller of the function should handle HassioAPIError.
|
|
||||||
"""
|
|
||||||
hassio = hass.data[DOMAIN]
|
|
||||||
command = f"/addons/{slug}/start"
|
|
||||||
return await hassio.send_command(command, timeout=60)
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
@api_data
|
|
||||||
async def async_restart_addon(hass: HomeAssistant, slug: str) -> dict:
|
|
||||||
"""Restart add-on.
|
|
||||||
|
|
||||||
The caller of the function should handle HassioAPIError.
|
|
||||||
"""
|
|
||||||
hassio = hass.data[DOMAIN]
|
|
||||||
command = f"/addons/{slug}/restart"
|
|
||||||
return await hassio.send_command(command, timeout=None)
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
@api_data
|
|
||||||
async def async_stop_addon(hass: HomeAssistant, slug: str) -> dict:
|
|
||||||
"""Stop add-on.
|
|
||||||
|
|
||||||
The caller of the function should handle HassioAPIError.
|
|
||||||
"""
|
|
||||||
hassio = hass.data[DOMAIN]
|
|
||||||
command = f"/addons/{slug}/stop"
|
|
||||||
return await hassio.send_command(command, timeout=60)
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
@api_data
|
|
||||||
async def async_set_addon_options(
|
|
||||||
hass: HomeAssistant, slug: str, options: dict
|
|
||||||
) -> dict:
|
|
||||||
"""Set add-on options.
|
|
||||||
|
|
||||||
The caller of the function should handle HassioAPIError.
|
|
||||||
"""
|
|
||||||
hassio = hass.data[DOMAIN]
|
|
||||||
command = f"/addons/{slug}/options"
|
|
||||||
return await hassio.send_command(command, payload=options)
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
async def async_get_addon_discovery_info(hass: HomeAssistant, slug: str) -> dict | None:
|
|
||||||
"""Return discovery data for an add-on."""
|
|
||||||
hassio = hass.data[DOMAIN]
|
|
||||||
data = await hassio.retrieve_discovery_messages()
|
|
||||||
discovered_addons = data[ATTR_DISCOVERY]
|
|
||||||
return next((addon for addon in discovered_addons if addon["addon"] == slug), None)
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
@api_data
|
|
||||||
async def async_create_backup(
|
|
||||||
hass: HomeAssistant, payload: dict, partial: bool = False
|
|
||||||
) -> dict:
|
|
||||||
"""Create a full or partial backup.
|
|
||||||
|
|
||||||
The caller of the function should handle HassioAPIError.
|
|
||||||
"""
|
|
||||||
hassio = hass.data[DOMAIN]
|
|
||||||
backup_type = "partial" if partial else "full"
|
|
||||||
command = f"/backups/new/{backup_type}"
|
|
||||||
return await hassio.send_command(command, payload=payload, timeout=None)
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
@api_data
|
|
||||||
async def async_update_os(hass: HomeAssistant, version: str | None = None) -> dict:
|
|
||||||
"""Update Home Assistant Operating System.
|
|
||||||
|
|
||||||
The caller of the function should handle HassioAPIError.
|
|
||||||
"""
|
|
||||||
hassio = hass.data[DOMAIN]
|
|
||||||
command = "/os/update"
|
|
||||||
return await hassio.send_command(
|
|
||||||
command,
|
|
||||||
payload={"version": version},
|
|
||||||
timeout=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
@api_data
|
|
||||||
async def async_update_supervisor(hass: HomeAssistant) -> dict:
|
|
||||||
"""Update Home Assistant Supervisor.
|
|
||||||
|
|
||||||
The caller of the function should handle HassioAPIError.
|
|
||||||
"""
|
|
||||||
hassio = hass.data[DOMAIN]
|
|
||||||
command = "/supervisor/update"
|
|
||||||
return await hassio.send_command(command, timeout=None)
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
@api_data
|
|
||||||
async def async_update_core(
|
|
||||||
hass: HomeAssistant, version: str | None = None, backup: bool = False
|
|
||||||
) -> dict:
|
|
||||||
"""Update Home Assistant Core.
|
|
||||||
|
|
||||||
The caller of the function should handle HassioAPIError.
|
|
||||||
"""
|
|
||||||
hassio = hass.data[DOMAIN]
|
|
||||||
command = "/core/update"
|
|
||||||
return await hassio.send_command(
|
|
||||||
command,
|
|
||||||
payload={"version": version, "backup": backup},
|
|
||||||
timeout=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@bind_hass
|
@bind_hass
|
||||||
def get_info(hass):
|
def get_info(hass):
|
||||||
|
373
homeassistant/components/hassio/addon_manager.py
Normal file
373
homeassistant/components/hassio/addon_manager.py
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
"""Provide add-on management."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from collections.abc import Awaitable, Callable, Coroutine
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from functools import partial, wraps
|
||||||
|
import logging
|
||||||
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
|
from typing_extensions import Concatenate, ParamSpec
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
|
from .handler import (
|
||||||
|
HassioAPIError,
|
||||||
|
async_create_backup,
|
||||||
|
async_get_addon_discovery_info,
|
||||||
|
async_get_addon_info,
|
||||||
|
async_get_addon_store_info,
|
||||||
|
async_install_addon,
|
||||||
|
async_restart_addon,
|
||||||
|
async_set_addon_options,
|
||||||
|
async_start_addon,
|
||||||
|
async_stop_addon,
|
||||||
|
async_uninstall_addon,
|
||||||
|
async_update_addon,
|
||||||
|
)
|
||||||
|
|
||||||
|
_AddonManagerT = TypeVar("_AddonManagerT", bound="AddonManager")
|
||||||
|
_R = TypeVar("_R")
|
||||||
|
_P = ParamSpec("_P")
|
||||||
|
|
||||||
|
|
||||||
|
def api_error(
|
||||||
|
error_message: str,
|
||||||
|
) -> 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[Concatenate[_AddonManagerT, _P], Awaitable[_R]]
|
||||||
|
) -> Callable[Concatenate[_AddonManagerT, _P], Coroutine[Any, Any, _R]]:
|
||||||
|
"""Handle a HassioAPIError."""
|
||||||
|
|
||||||
|
@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(self, *args, **kwargs)
|
||||||
|
except HassioAPIError as err:
|
||||||
|
raise AddonError(
|
||||||
|
f"{error_message.format(addon_name=self.addon_name)}: {err}"
|
||||||
|
) from err
|
||||||
|
|
||||||
|
return return_value
|
||||||
|
|
||||||
|
return 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 per add-on
|
||||||
|
to keep track of running add-on tasks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
logger: logging.Logger,
|
||||||
|
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._logger = logger
|
||||||
|
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._restart_task,
|
||||||
|
self._install_task,
|
||||||
|
self._start_task,
|
||||||
|
self._update_task,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@api_error("Failed to get the {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, self.addon_slug
|
||||||
|
)
|
||||||
|
|
||||||
|
if not 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 {addon_name} add-on info")
|
||||||
|
async def async_get_addon_info(self) -> AddonInfo:
|
||||||
|
"""Return and cache manager add-on info."""
|
||||||
|
addon_store_info = await async_get_addon_store_info(self._hass, self.addon_slug)
|
||||||
|
self._logger.debug("Add-on store info: %s", addon_store_info)
|
||||||
|
if not addon_store_info["installed"]:
|
||||||
|
return AddonInfo(
|
||||||
|
options={},
|
||||||
|
state=AddonState.NOT_INSTALLED,
|
||||||
|
update_available=False,
|
||||||
|
version=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
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"],
|
||||||
|
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 managed add-on."""
|
||||||
|
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 {addon_name} add-on options")
|
||||||
|
async def async_set_addon_options(self, config: dict) -> None:
|
||||||
|
"""Set manager add-on options."""
|
||||||
|
options = {"options": config}
|
||||||
|
await async_set_addon_options(self._hass, self.addon_slug, options)
|
||||||
|
|
||||||
|
@api_error("Failed to install the {addon_name} add-on")
|
||||||
|
async def async_install_addon(self) -> None:
|
||||||
|
"""Install the managed add-on."""
|
||||||
|
await async_install_addon(self._hass, self.addon_slug)
|
||||||
|
|
||||||
|
@api_error("Failed to uninstall the {addon_name} add-on")
|
||||||
|
async def async_uninstall_addon(self) -> None:
|
||||||
|
"""Uninstall the managed add-on."""
|
||||||
|
await async_uninstall_addon(self._hass, self.addon_slug)
|
||||||
|
|
||||||
|
@api_error("Failed to update the {addon_name} add-on")
|
||||||
|
async def async_update_addon(self) -> None:
|
||||||
|
"""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(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, self.addon_slug)
|
||||||
|
|
||||||
|
@api_error("Failed to start the {addon_name} add-on")
|
||||||
|
async def async_start_addon(self) -> None:
|
||||||
|
"""Start the managed add-on."""
|
||||||
|
await async_start_addon(self._hass, self.addon_slug)
|
||||||
|
|
||||||
|
@api_error("Failed to restart the {addon_name} add-on")
|
||||||
|
async def async_restart_addon(self) -> None:
|
||||||
|
"""Restart the managed add-on."""
|
||||||
|
await async_restart_addon(self._hass, self.addon_slug)
|
||||||
|
|
||||||
|
@api_error("Failed to stop the {addon_name} add-on")
|
||||||
|
async def async_stop_addon(self) -> None:
|
||||||
|
"""Stop the managed add-on."""
|
||||||
|
await async_stop_addon(self._hass, self.addon_slug)
|
||||||
|
|
||||||
|
@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 managed add-on."""
|
||||||
|
addon_info = await self.async_get_addon_info()
|
||||||
|
name = f"addon_{self.addon_slug}_{addon_info.version}"
|
||||||
|
|
||||||
|
self._logger.debug("Creating backup: %s", name)
|
||||||
|
await async_create_backup(
|
||||||
|
self._hass,
|
||||||
|
{"name": name, "addons": [self.addon_slug]},
|
||||||
|
partial=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_configure_addon(
|
||||||
|
self,
|
||||||
|
addon_config: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Configure the manager add-on, if needed."""
|
||||||
|
addon_info = await self.async_get_addon_info()
|
||||||
|
|
||||||
|
if addon_info.state is AddonState.NOT_INSTALLED:
|
||||||
|
raise AddonError(f"{self.addon_name} add-on is not installed")
|
||||||
|
|
||||||
|
if addon_config != addon_info.options:
|
||||||
|
await self.async_set_addon_options(addon_config)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_schedule_install_addon(self, catch_error: bool = False) -> asyncio.Task:
|
||||||
|
"""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():
|
||||||
|
self._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
|
||||||
|
)
|
||||||
|
return self._install_task
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_schedule_install_setup_addon(
|
||||||
|
self,
|
||||||
|
addon_config: dict[str, Any],
|
||||||
|
catch_error: bool = False,
|
||||||
|
) -> asyncio.Task:
|
||||||
|
"""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():
|
||||||
|
self._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,
|
||||||
|
addon_config,
|
||||||
|
),
|
||||||
|
self.async_start_addon,
|
||||||
|
catch_error=catch_error,
|
||||||
|
)
|
||||||
|
return self._install_task
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_schedule_update_addon(self, catch_error: bool = False) -> asyncio.Task:
|
||||||
|
"""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():
|
||||||
|
self._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
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_schedule_start_addon(self, catch_error: bool = False) -> asyncio.Task:
|
||||||
|
"""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():
|
||||||
|
self._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
|
||||||
|
)
|
||||||
|
return self._start_task
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_schedule_restart_addon(self, catch_error: bool = False) -> asyncio.Task:
|
||||||
|
"""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():
|
||||||
|
self._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
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_schedule_setup_addon(
|
||||||
|
self,
|
||||||
|
addon_config: dict[str, Any],
|
||||||
|
catch_error: bool = False,
|
||||||
|
) -> asyncio.Task:
|
||||||
|
"""Schedule a task that configures and starts the managed add-on.
|
||||||
|
|
||||||
|
Only schedule a new setup task if there's no running task.
|
||||||
|
"""
|
||||||
|
if not self._start_task or self._start_task.done():
|
||||||
|
self._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,
|
||||||
|
addon_config,
|
||||||
|
),
|
||||||
|
self.async_start_addon,
|
||||||
|
catch_error=catch_error,
|
||||||
|
)
|
||||||
|
return self._start_task
|
||||||
|
|
||||||
|
@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
|
||||||
|
self._logger.error(err)
|
||||||
|
break
|
||||||
|
|
||||||
|
return self._hass.async_create_task(addon_operation())
|
||||||
|
|
||||||
|
|
||||||
|
class AddonError(HomeAssistantError):
|
||||||
|
"""Represent an error with the managed add-on."""
|
@ -1,4 +1,6 @@
|
|||||||
"""Handler for Hass.io."""
|
"""Handler for Hass.io."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import logging
|
import logging
|
||||||
@ -12,6 +14,10 @@ from homeassistant.components.http import (
|
|||||||
CONF_SSL_CERTIFICATE,
|
CONF_SSL_CERTIFICATE,
|
||||||
)
|
)
|
||||||
from homeassistant.const import SERVER_PORT
|
from homeassistant.const import SERVER_PORT
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.loader import bind_hass
|
||||||
|
|
||||||
|
from .const import ATTR_DISCOVERY, DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -47,6 +53,202 @@ def api_data(funct):
|
|||||||
return _wrapper
|
return _wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
async def async_get_addon_info(hass: HomeAssistant, slug: str) -> dict:
|
||||||
|
"""Return add-on info.
|
||||||
|
|
||||||
|
The add-on must be installed.
|
||||||
|
The caller of the function should handle HassioAPIError.
|
||||||
|
"""
|
||||||
|
hassio = hass.data[DOMAIN]
|
||||||
|
return await hassio.get_addon_info(slug)
|
||||||
|
|
||||||
|
|
||||||
|
@api_data
|
||||||
|
async def async_get_addon_store_info(hass: HomeAssistant, slug: str) -> dict:
|
||||||
|
"""Return add-on store info.
|
||||||
|
|
||||||
|
The caller of the function should handle HassioAPIError.
|
||||||
|
"""
|
||||||
|
hassio: HassIO = hass.data[DOMAIN]
|
||||||
|
command = f"/store/addons/{slug}"
|
||||||
|
return await hassio.send_command(command, method="get")
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> dict:
|
||||||
|
"""Update Supervisor diagnostics toggle.
|
||||||
|
|
||||||
|
The caller of the function should handle HassioAPIError.
|
||||||
|
"""
|
||||||
|
hassio = hass.data[DOMAIN]
|
||||||
|
return await hassio.update_diagnostics(diagnostics)
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
@api_data
|
||||||
|
async def async_install_addon(hass: HomeAssistant, slug: str) -> dict:
|
||||||
|
"""Install add-on.
|
||||||
|
|
||||||
|
The caller of the function should handle HassioAPIError.
|
||||||
|
"""
|
||||||
|
hassio = hass.data[DOMAIN]
|
||||||
|
command = f"/addons/{slug}/install"
|
||||||
|
return await hassio.send_command(command, timeout=None)
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
@api_data
|
||||||
|
async def async_uninstall_addon(hass: HomeAssistant, slug: str) -> dict:
|
||||||
|
"""Uninstall add-on.
|
||||||
|
|
||||||
|
The caller of the function should handle HassioAPIError.
|
||||||
|
"""
|
||||||
|
hassio = hass.data[DOMAIN]
|
||||||
|
command = f"/addons/{slug}/uninstall"
|
||||||
|
return await hassio.send_command(command, timeout=60)
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
@api_data
|
||||||
|
async def async_update_addon(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
slug: str,
|
||||||
|
backup: bool = False,
|
||||||
|
) -> dict:
|
||||||
|
"""Update add-on.
|
||||||
|
|
||||||
|
The caller of the function should handle HassioAPIError.
|
||||||
|
"""
|
||||||
|
hassio = hass.data[DOMAIN]
|
||||||
|
command = f"/addons/{slug}/update"
|
||||||
|
return await hassio.send_command(
|
||||||
|
command,
|
||||||
|
payload={"backup": backup},
|
||||||
|
timeout=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
@api_data
|
||||||
|
async def async_start_addon(hass: HomeAssistant, slug: str) -> dict:
|
||||||
|
"""Start add-on.
|
||||||
|
|
||||||
|
The caller of the function should handle HassioAPIError.
|
||||||
|
"""
|
||||||
|
hassio = hass.data[DOMAIN]
|
||||||
|
command = f"/addons/{slug}/start"
|
||||||
|
return await hassio.send_command(command, timeout=60)
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
@api_data
|
||||||
|
async def async_restart_addon(hass: HomeAssistant, slug: str) -> dict:
|
||||||
|
"""Restart add-on.
|
||||||
|
|
||||||
|
The caller of the function should handle HassioAPIError.
|
||||||
|
"""
|
||||||
|
hassio = hass.data[DOMAIN]
|
||||||
|
command = f"/addons/{slug}/restart"
|
||||||
|
return await hassio.send_command(command, timeout=None)
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
@api_data
|
||||||
|
async def async_stop_addon(hass: HomeAssistant, slug: str) -> dict:
|
||||||
|
"""Stop add-on.
|
||||||
|
|
||||||
|
The caller of the function should handle HassioAPIError.
|
||||||
|
"""
|
||||||
|
hassio = hass.data[DOMAIN]
|
||||||
|
command = f"/addons/{slug}/stop"
|
||||||
|
return await hassio.send_command(command, timeout=60)
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
@api_data
|
||||||
|
async def async_set_addon_options(
|
||||||
|
hass: HomeAssistant, slug: str, options: dict
|
||||||
|
) -> dict:
|
||||||
|
"""Set add-on options.
|
||||||
|
|
||||||
|
The caller of the function should handle HassioAPIError.
|
||||||
|
"""
|
||||||
|
hassio = hass.data[DOMAIN]
|
||||||
|
command = f"/addons/{slug}/options"
|
||||||
|
return await hassio.send_command(command, payload=options)
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
async def async_get_addon_discovery_info(hass: HomeAssistant, slug: str) -> dict | None:
|
||||||
|
"""Return discovery data for an add-on."""
|
||||||
|
hassio = hass.data[DOMAIN]
|
||||||
|
data = await hassio.retrieve_discovery_messages()
|
||||||
|
discovered_addons = data[ATTR_DISCOVERY]
|
||||||
|
return next((addon for addon in discovered_addons if addon["addon"] == slug), None)
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
@api_data
|
||||||
|
async def async_create_backup(
|
||||||
|
hass: HomeAssistant, payload: dict, partial: bool = False
|
||||||
|
) -> dict:
|
||||||
|
"""Create a full or partial backup.
|
||||||
|
|
||||||
|
The caller of the function should handle HassioAPIError.
|
||||||
|
"""
|
||||||
|
hassio = hass.data[DOMAIN]
|
||||||
|
backup_type = "partial" if partial else "full"
|
||||||
|
command = f"/backups/new/{backup_type}"
|
||||||
|
return await hassio.send_command(command, payload=payload, timeout=None)
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
@api_data
|
||||||
|
async def async_update_os(hass: HomeAssistant, version: str | None = None) -> dict:
|
||||||
|
"""Update Home Assistant Operating System.
|
||||||
|
|
||||||
|
The caller of the function should handle HassioAPIError.
|
||||||
|
"""
|
||||||
|
hassio = hass.data[DOMAIN]
|
||||||
|
command = "/os/update"
|
||||||
|
return await hassio.send_command(
|
||||||
|
command,
|
||||||
|
payload={"version": version},
|
||||||
|
timeout=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
@api_data
|
||||||
|
async def async_update_supervisor(hass: HomeAssistant) -> dict:
|
||||||
|
"""Update Home Assistant Supervisor.
|
||||||
|
|
||||||
|
The caller of the function should handle HassioAPIError.
|
||||||
|
"""
|
||||||
|
hassio = hass.data[DOMAIN]
|
||||||
|
command = "/supervisor/update"
|
||||||
|
return await hassio.send_command(command, timeout=None)
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
@api_data
|
||||||
|
async def async_update_core(
|
||||||
|
hass: HomeAssistant, version: str | None = None, backup: bool = False
|
||||||
|
) -> dict:
|
||||||
|
"""Update Home Assistant Core.
|
||||||
|
|
||||||
|
The caller of the function should handle HassioAPIError.
|
||||||
|
"""
|
||||||
|
hassio = hass.data[DOMAIN]
|
||||||
|
command = "/core/update"
|
||||||
|
return await hassio.send_command(
|
||||||
|
command,
|
||||||
|
payload={"version": version, "backup": backup},
|
||||||
|
timeout=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HassIO:
|
class HassIO:
|
||||||
"""Small API wrapper for Hass.io."""
|
"""Small API wrapper for Hass.io."""
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ from zwave_js_server.model.notification import (
|
|||||||
)
|
)
|
||||||
from zwave_js_server.model.value import Value, ValueNotification
|
from zwave_js_server.model.value import Value, ValueNotification
|
||||||
|
|
||||||
|
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_DEVICE_ID,
|
ATTR_DEVICE_ID,
|
||||||
@ -41,7 +42,7 @@ from homeassistant.helpers.issue_registry import (
|
|||||||
)
|
)
|
||||||
from homeassistant.helpers.typing import UNDEFINED, ConfigType
|
from homeassistant.helpers.typing import UNDEFINED, ConfigType
|
||||||
|
|
||||||
from .addon import AddonError, AddonManager, AddonState, get_addon_manager
|
from .addon import get_addon_manager
|
||||||
from .api import async_register_api
|
from .api import async_register_api
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_ACKNOWLEDGED_FRAMES,
|
ATTR_ACKNOWLEDGED_FRAMES,
|
||||||
|
@ -1,39 +1,12 @@
|
|||||||
"""Provide add-on management."""
|
"""Provide add-on management."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
from homeassistant.components.hassio import AddonManager
|
||||||
from collections.abc import Awaitable, Callable, Coroutine
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import Enum
|
|
||||||
from functools import partial, wraps
|
|
||||||
from typing import Any, TypeVar
|
|
||||||
|
|
||||||
from typing_extensions import Concatenate, ParamSpec
|
|
||||||
|
|
||||||
from homeassistant.components.hassio import (
|
|
||||||
async_create_backup,
|
|
||||||
async_get_addon_discovery_info,
|
|
||||||
async_get_addon_info,
|
|
||||||
async_get_addon_store_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.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
from homeassistant.helpers.singleton import singleton
|
from homeassistant.helpers.singleton import singleton
|
||||||
|
|
||||||
from .const import ADDON_SLUG, DOMAIN, LOGGER
|
from .const import ADDON_SLUG, DOMAIN, LOGGER
|
||||||
|
|
||||||
_AddonManagerT = TypeVar("_AddonManagerT", bound="AddonManager")
|
|
||||||
_R = TypeVar("_R")
|
|
||||||
_P = ParamSpec("_P")
|
|
||||||
|
|
||||||
DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager"
|
DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager"
|
||||||
|
|
||||||
|
|
||||||
@ -41,331 +14,4 @@ DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager"
|
|||||||
@callback
|
@callback
|
||||||
def get_addon_manager(hass: HomeAssistant) -> AddonManager:
|
def get_addon_manager(hass: HomeAssistant) -> AddonManager:
|
||||||
"""Get the add-on manager."""
|
"""Get the add-on manager."""
|
||||||
return AddonManager(hass, "Z-Wave JS", ADDON_SLUG)
|
return AddonManager(hass, LOGGER, "Z-Wave JS", ADDON_SLUG)
|
||||||
|
|
||||||
|
|
||||||
def api_error(
|
|
||||||
error_message: str,
|
|
||||||
) -> 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[Concatenate[_AddonManagerT, _P], Awaitable[_R]]
|
|
||||||
) -> Callable[Concatenate[_AddonManagerT, _P], Coroutine[Any, Any, _R]]:
|
|
||||||
"""Handle a HassioAPIError."""
|
|
||||||
|
|
||||||
@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(self, *args, **kwargs)
|
|
||||||
except HassioAPIError as err:
|
|
||||||
raise AddonError(
|
|
||||||
f"{error_message.format(addon_name=self.addon_name)}: {err}"
|
|
||||||
) from err
|
|
||||||
|
|
||||||
return return_value
|
|
||||||
|
|
||||||
return 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 per add-on
|
|
||||||
to keep track of running add-on tasks.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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
|
|
||||||
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 {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, self.addon_slug
|
|
||||||
)
|
|
||||||
|
|
||||||
if not 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 {addon_name} add-on info")
|
|
||||||
async def async_get_addon_info(self) -> AddonInfo:
|
|
||||||
"""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(
|
|
||||||
options={},
|
|
||||||
state=AddonState.NOT_INSTALLED,
|
|
||||||
update_available=False,
|
|
||||||
version=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
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"],
|
|
||||||
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 managed add-on."""
|
|
||||||
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 {addon_name} add-on options")
|
|
||||||
async def async_set_addon_options(self, config: dict) -> None:
|
|
||||||
"""Set manager add-on options."""
|
|
||||||
options = {"options": config}
|
|
||||||
await async_set_addon_options(self._hass, self.addon_slug, options)
|
|
||||||
|
|
||||||
@api_error("Failed to install the {addon_name} add-on")
|
|
||||||
async def async_install_addon(self) -> None:
|
|
||||||
"""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 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(
|
|
||||||
"%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
|
|
||||||
)
|
|
||||||
return self._install_task
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_schedule_install_setup_addon(
|
|
||||||
self,
|
|
||||||
addon_config: dict[str, Any],
|
|
||||||
catch_error: bool = False,
|
|
||||||
) -> asyncio.Task:
|
|
||||||
"""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(
|
|
||||||
"%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,
|
|
||||||
addon_config,
|
|
||||||
),
|
|
||||||
self.async_start_addon,
|
|
||||||
catch_error=catch_error,
|
|
||||||
)
|
|
||||||
return self._install_task
|
|
||||||
|
|
||||||
@api_error("Failed to uninstall the {addon_name} add-on")
|
|
||||||
async def async_uninstall_addon(self) -> None:
|
|
||||||
"""Uninstall the managed add-on."""
|
|
||||||
await async_uninstall_addon(self._hass, self.addon_slug)
|
|
||||||
|
|
||||||
@api_error("Failed to update the {addon_name} add-on")
|
|
||||||
async def async_update_addon(self) -> None:
|
|
||||||
"""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(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, 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 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 %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 {addon_name} add-on")
|
|
||||||
async def async_start_addon(self) -> None:
|
|
||||||
"""Start the managed add-on."""
|
|
||||||
await async_start_addon(self._hass, self.addon_slug)
|
|
||||||
|
|
||||||
@api_error("Failed to restart the {addon_name} add-on")
|
|
||||||
async def async_restart_addon(self) -> None:
|
|
||||||
"""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 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("%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
|
|
||||||
)
|
|
||||||
return self._start_task
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_schedule_restart_addon(self, catch_error: bool = False) -> asyncio.Task:
|
|
||||||
"""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 %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 {addon_name} add-on")
|
|
||||||
async def async_stop_addon(self) -> None:
|
|
||||||
"""Stop the managed add-on."""
|
|
||||||
await async_stop_addon(self._hass, self.addon_slug)
|
|
||||||
|
|
||||||
async def async_configure_addon(
|
|
||||||
self,
|
|
||||||
addon_config: dict[str, Any],
|
|
||||||
) -> None:
|
|
||||||
"""Configure and start manager add-on."""
|
|
||||||
addon_info = await self.async_get_addon_info()
|
|
||||||
|
|
||||||
if addon_info.state is AddonState.NOT_INSTALLED:
|
|
||||||
raise AddonError(f"{self.addon_name} add-on is not installed")
|
|
||||||
|
|
||||||
if addon_config != addon_info.options:
|
|
||||||
await self.async_set_addon_options(addon_config)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_schedule_setup_addon(
|
|
||||||
self,
|
|
||||||
addon_config: dict[str, Any],
|
|
||||||
catch_error: bool = False,
|
|
||||||
) -> asyncio.Task:
|
|
||||||
"""Schedule a task that configures and starts the managed add-on.
|
|
||||||
|
|
||||||
Only schedule a new setup task if there's no running task.
|
|
||||||
"""
|
|
||||||
if not self._start_task or self._start_task.done():
|
|
||||||
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,
|
|
||||||
addon_config,
|
|
||||||
),
|
|
||||||
self.async_start_addon,
|
|
||||||
catch_error=catch_error,
|
|
||||||
)
|
|
||||||
return self._start_task
|
|
||||||
|
|
||||||
@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 managed add-on."""
|
|
||||||
addon_info = await self.async_get_addon_info()
|
|
||||||
name = f"addon_{self.addon_slug}_{addon_info.version}"
|
|
||||||
|
|
||||||
LOGGER.debug("Creating backup: %s", name)
|
|
||||||
await async_create_backup(
|
|
||||||
self._hass,
|
|
||||||
{"name": name, "addons": [self.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 the managed add-on."""
|
|
||||||
|
@ -14,7 +14,14 @@ from zwave_js_server.version import VersionInfo, get_server_version
|
|||||||
|
|
||||||
from homeassistant import config_entries, exceptions
|
from homeassistant import config_entries, exceptions
|
||||||
from homeassistant.components import usb
|
from homeassistant.components import usb
|
||||||
from homeassistant.components.hassio import HassioServiceInfo, is_hassio
|
from homeassistant.components.hassio import (
|
||||||
|
AddonError,
|
||||||
|
AddonInfo,
|
||||||
|
AddonManager,
|
||||||
|
AddonState,
|
||||||
|
HassioServiceInfo,
|
||||||
|
is_hassio,
|
||||||
|
)
|
||||||
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
||||||
from homeassistant.const import CONF_NAME, CONF_URL
|
from homeassistant.const import CONF_NAME, CONF_URL
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
@ -27,7 +34,7 @@ from homeassistant.data_entry_flow import (
|
|||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from . import disconnect_client
|
from . import disconnect_client
|
||||||
from .addon import AddonError, AddonInfo, AddonManager, AddonState, get_addon_manager
|
from .addon import get_addon_manager
|
||||||
from .const import (
|
from .const import (
|
||||||
ADDON_SLUG,
|
ADDON_SLUG,
|
||||||
CONF_ADDON_DEVICE,
|
CONF_ADDON_DEVICE,
|
||||||
|
1128
tests/components/hassio/test_addon_manager.py
Normal file
1128
tests/components/hassio/test_addon_manager.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -26,7 +26,7 @@ def addon_info_side_effect_fixture():
|
|||||||
def mock_addon_info(addon_info_side_effect):
|
def mock_addon_info(addon_info_side_effect):
|
||||||
"""Mock Supervisor add-on info."""
|
"""Mock Supervisor add-on info."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.zwave_js.addon.async_get_addon_info",
|
"homeassistant.components.hassio.addon_manager.async_get_addon_info",
|
||||||
side_effect=addon_info_side_effect,
|
side_effect=addon_info_side_effect,
|
||||||
) as addon_info:
|
) as addon_info:
|
||||||
addon_info.return_value = {
|
addon_info.return_value = {
|
||||||
@ -48,7 +48,7 @@ def addon_store_info_side_effect_fixture():
|
|||||||
def mock_addon_store_info(addon_store_info_side_effect):
|
def mock_addon_store_info(addon_store_info_side_effect):
|
||||||
"""Mock Supervisor add-on info."""
|
"""Mock Supervisor add-on info."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.zwave_js.addon.async_get_addon_store_info",
|
"homeassistant.components.hassio.addon_manager.async_get_addon_store_info",
|
||||||
side_effect=addon_store_info_side_effect,
|
side_effect=addon_store_info_side_effect,
|
||||||
) as addon_store_info:
|
) as addon_store_info:
|
||||||
addon_store_info.return_value = {
|
addon_store_info.return_value = {
|
||||||
@ -112,7 +112,7 @@ def set_addon_options_side_effect_fixture(addon_options):
|
|||||||
def mock_set_addon_options(set_addon_options_side_effect):
|
def mock_set_addon_options(set_addon_options_side_effect):
|
||||||
"""Mock set add-on options."""
|
"""Mock set add-on options."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.zwave_js.addon.async_set_addon_options",
|
"homeassistant.components.hassio.addon_manager.async_set_addon_options",
|
||||||
side_effect=set_addon_options_side_effect,
|
side_effect=set_addon_options_side_effect,
|
||||||
) as set_options:
|
) as set_options:
|
||||||
yield set_options
|
yield set_options
|
||||||
@ -139,7 +139,7 @@ def install_addon_side_effect_fixture(addon_store_info, addon_info):
|
|||||||
def mock_install_addon(install_addon_side_effect):
|
def mock_install_addon(install_addon_side_effect):
|
||||||
"""Mock install add-on."""
|
"""Mock install add-on."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.zwave_js.addon.async_install_addon",
|
"homeassistant.components.hassio.addon_manager.async_install_addon",
|
||||||
side_effect=install_addon_side_effect,
|
side_effect=install_addon_side_effect,
|
||||||
) as install_addon:
|
) as install_addon:
|
||||||
yield install_addon
|
yield install_addon
|
||||||
@ -149,7 +149,7 @@ def mock_install_addon(install_addon_side_effect):
|
|||||||
def mock_update_addon():
|
def mock_update_addon():
|
||||||
"""Mock update add-on."""
|
"""Mock update add-on."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.zwave_js.addon.async_update_addon"
|
"homeassistant.components.hassio.addon_manager.async_update_addon"
|
||||||
) as update_addon:
|
) as update_addon:
|
||||||
yield update_addon
|
yield update_addon
|
||||||
|
|
||||||
@ -174,7 +174,7 @@ def start_addon_side_effect_fixture(addon_store_info, addon_info):
|
|||||||
def mock_start_addon(start_addon_side_effect):
|
def mock_start_addon(start_addon_side_effect):
|
||||||
"""Mock start add-on."""
|
"""Mock start add-on."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.zwave_js.addon.async_start_addon",
|
"homeassistant.components.hassio.addon_manager.async_start_addon",
|
||||||
side_effect=start_addon_side_effect,
|
side_effect=start_addon_side_effect,
|
||||||
) as start_addon:
|
) as start_addon:
|
||||||
yield start_addon
|
yield start_addon
|
||||||
@ -184,7 +184,7 @@ def mock_start_addon(start_addon_side_effect):
|
|||||||
def stop_addon_fixture():
|
def stop_addon_fixture():
|
||||||
"""Mock stop add-on."""
|
"""Mock stop add-on."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.zwave_js.addon.async_stop_addon"
|
"homeassistant.components.hassio.addon_manager.async_stop_addon"
|
||||||
) as stop_addon:
|
) as stop_addon:
|
||||||
yield stop_addon
|
yield stop_addon
|
||||||
|
|
||||||
@ -199,7 +199,7 @@ def restart_addon_side_effect_fixture():
|
|||||||
def mock_restart_addon(restart_addon_side_effect):
|
def mock_restart_addon(restart_addon_side_effect):
|
||||||
"""Mock restart add-on."""
|
"""Mock restart add-on."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.zwave_js.addon.async_restart_addon",
|
"homeassistant.components.hassio.addon_manager.async_restart_addon",
|
||||||
side_effect=restart_addon_side_effect,
|
side_effect=restart_addon_side_effect,
|
||||||
) as restart_addon:
|
) as restart_addon:
|
||||||
yield restart_addon
|
yield restart_addon
|
||||||
@ -209,7 +209,7 @@ def mock_restart_addon(restart_addon_side_effect):
|
|||||||
def uninstall_addon_fixture():
|
def uninstall_addon_fixture():
|
||||||
"""Mock uninstall add-on."""
|
"""Mock uninstall add-on."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.zwave_js.addon.async_uninstall_addon"
|
"homeassistant.components.hassio.addon_manager.async_uninstall_addon"
|
||||||
) as uninstall_addon:
|
) as uninstall_addon:
|
||||||
yield uninstall_addon
|
yield uninstall_addon
|
||||||
|
|
||||||
@ -218,7 +218,7 @@ def uninstall_addon_fixture():
|
|||||||
def create_backup_fixture():
|
def create_backup_fixture():
|
||||||
"""Mock create backup."""
|
"""Mock create backup."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.zwave_js.addon.async_create_backup"
|
"homeassistant.components.hassio.addon_manager.async_create_backup"
|
||||||
) as create_backup:
|
) as create_backup:
|
||||||
yield create_backup
|
yield create_backup
|
||||||
|
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
"""Tests for Z-Wave JS addon module."""
|
|
||||||
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(addon_config)
|
|
||||||
|
|
||||||
with pytest.raises(AddonError):
|
|
||||||
await addon_manager.async_update_addon()
|
|
@ -88,7 +88,7 @@ def discovery_info_side_effect_fixture():
|
|||||||
def mock_get_addon_discovery_info(discovery_info, discovery_info_side_effect):
|
def mock_get_addon_discovery_info(discovery_info, discovery_info_side_effect):
|
||||||
"""Mock get add-on discovery info."""
|
"""Mock get add-on discovery info."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.zwave_js.addon.async_get_addon_discovery_info",
|
"homeassistant.components.hassio.addon_manager.async_get_addon_discovery_info",
|
||||||
side_effect=discovery_info_side_effect,
|
side_effect=discovery_info_side_effect,
|
||||||
return_value=discovery_info,
|
return_value=discovery_info,
|
||||||
) as get_addon_discovery_info:
|
) as get_addon_discovery_info:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user