Home Assistant Unofficial Reference 2024.12.1
addon_manager.py
Go to the documentation of this file.
1 """Provide add-on management."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Awaitable, Callable, Coroutine
7 from dataclasses import dataclass
8 from enum import Enum
9 from functools import partial, wraps
10 import logging
11 from typing import Any, Concatenate
12 
13 from aiohasupervisor import SupervisorError
14 from aiohasupervisor.models import (
15  AddonsOptions,
16  AddonState as SupervisorAddonState,
17  InstalledAddonComplete,
18  StoreAddonUpdate,
19 )
20 
21 from homeassistant.core import HomeAssistant, callback
22 from homeassistant.exceptions import HomeAssistantError
23 
24 from .handler import HassioAPIError, async_create_backup, get_supervisor_client
25 
26 type _FuncType[_T, **_P, _R] = Callable[Concatenate[_T, _P], Awaitable[_R]]
27 type _ReturnFuncType[_T, **_P, _R] = Callable[
28  Concatenate[_T, _P], Coroutine[Any, Any, _R]
29 ]
30 
31 
32 def api_error[_AddonManagerT: AddonManager, **_P, _R](
33  error_message: str,
34  *,
35  expected_error_type: type[HassioAPIError | SupervisorError] | None = None,
36 ) -> Callable[
37  [_FuncType[_AddonManagerT, _P, _R]], _ReturnFuncType[_AddonManagerT, _P, _R]
38 ]:
39  """Handle HassioAPIError and raise a specific AddonError."""
40  error_type = expected_error_type or (HassioAPIError, SupervisorError)
41 
42  def handle_hassio_api_error(
43  func: _FuncType[_AddonManagerT, _P, _R],
44  ) -> _ReturnFuncType[_AddonManagerT, _P, _R]:
45  """Handle a HassioAPIError."""
46 
47  @wraps(func)
48  async def wrapper(
49  self: _AddonManagerT, *args: _P.args, **kwargs: _P.kwargs
50  ) -> _R:
51  """Wrap an add-on manager method."""
52  try:
53  return_value = await func(self, *args, **kwargs)
54  except error_type as err:
55  raise AddonError(
56  f"{error_message.format(addon_name=self.addon_name)}: {err}"
57  ) from err
58 
59  return return_value
60 
61  return wrapper
62 
63  return handle_hassio_api_error
64 
65 
66 @dataclass
67 class AddonInfo:
68  """Represent the current add-on info state."""
69 
70  available: bool
71  hostname: str | None
72  options: dict[str, Any]
73  state: AddonState
74  update_available: bool
75  version: str | None
76 
77 
78 class AddonState(Enum):
79  """Represent the current state of the add-on."""
80 
81  NOT_INSTALLED = "not_installed"
82  INSTALLING = "installing"
83  UPDATING = "updating"
84  NOT_RUNNING = "not_running"
85  RUNNING = "running"
86 
87 
89  """Manage the add-on.
90 
91  Methods may raise AddonError.
92  Only one instance of this class may exist per add-on
93  to keep track of running add-on tasks.
94  """
95 
96  def __init__(
97  self,
98  hass: HomeAssistant,
99  logger: logging.Logger,
100  addon_name: str,
101  addon_slug: str,
102  ) -> None:
103  """Set up the add-on manager."""
104  self.addon_nameaddon_name = addon_name
105  self.addon_slugaddon_slug = addon_slug
106  self._hass_hass = hass
107  self._logger_logger = logger
108  self._install_task_install_task: asyncio.Task | None = None
109  self._restart_task_restart_task: asyncio.Task | None = None
110  self._start_task_start_task: asyncio.Task | None = None
111  self._update_task_update_task: asyncio.Task | None = None
112  self._supervisor_client_supervisor_client = get_supervisor_client(hass)
113 
114  def task_in_progress(self) -> bool:
115  """Return True if any of the add-on tasks are in progress."""
116  return any(
117  task and not task.done()
118  for task in (
119  self._restart_task_restart_task,
120  self._install_task_install_task,
121  self._start_task_start_task,
122  self._update_task_update_task,
123  )
124  )
125 
126  @api_error( "Failed to get the {addon_name} add-on discovery info", expected_error_type=SupervisorError, )
127  async def async_get_addon_discovery_info(self) -> dict:
128  """Return add-on discovery info."""
129  discovery_info = next(
130  (
131  msg
132  for msg in await self._supervisor_client_supervisor_client.discovery.list()
133  if msg.addon == self.addon_slugaddon_slug
134  ),
135  None,
136  )
137 
138  if not discovery_info:
139  raise AddonError(f"Failed to get {self.addon_name} add-on discovery info")
140 
141  return discovery_info.config
142 
143  @api_error( "Failed to get the {addon_name} add-on info", expected_error_type=SupervisorError, )
144  async def async_get_addon_info(self) -> AddonInfo:
145  """Return and cache manager add-on info."""
146  addon_store_info = await self._supervisor_client_supervisor_client.store.addon_info(
147  self.addon_slugaddon_slug
148  )
149  self._logger_logger.debug("Add-on store info: %s", addon_store_info.to_dict())
150  if not addon_store_info.installed:
151  return AddonInfo(
152  available=addon_store_info.available,
153  hostname=None,
154  options={},
155  state=AddonState.NOT_INSTALLED,
156  update_available=False,
157  version=None,
158  )
159 
160  addon_info = await self._supervisor_client_supervisor_client.addons.addon_info(self.addon_slugaddon_slug)
161  addon_state = self.async_get_addon_stateasync_get_addon_state(addon_info)
162  return AddonInfo(
163  available=addon_info.available,
164  hostname=addon_info.hostname,
165  options=addon_info.options,
166  state=addon_state,
167  update_available=addon_info.update_available,
168  version=addon_info.version,
169  )
170 
171  @callback
172  def async_get_addon_state(self, addon_info: InstalledAddonComplete) -> AddonState:
173  """Return the current state of the managed add-on."""
174  addon_state = AddonState.NOT_RUNNING
175 
176  if addon_info.state == SupervisorAddonState.STARTED:
177  addon_state = AddonState.RUNNING
178  if self._install_task_install_task and not self._install_task_install_task.done():
179  addon_state = AddonState.INSTALLING
180  if self._update_task_update_task and not self._update_task_update_task.done():
181  addon_state = AddonState.UPDATING
182 
183  return addon_state
184 
185  @api_error( "Failed to set the {addon_name} add-on options", expected_error_type=SupervisorError, )
186  async def async_set_addon_options(self, config: dict) -> None:
187  """Set manager add-on options."""
188  await self._supervisor_client_supervisor_client.addons.set_addon_options(
189  self.addon_slugaddon_slug, AddonsOptions(config=config)
190  )
191 
192  def _check_addon_available(self, addon_info: AddonInfo) -> None:
193  """Check if the managed add-on is available."""
194  if not addon_info.available:
195  raise AddonError(f"{self.addon_name} add-on is not available")
196 
197  @api_error( "Failed to install the {addon_name} add-on", expected_error_type=SupervisorError )
198  async def async_install_addon(self) -> None:
199  """Install the managed add-on."""
200  addon_info = await self.async_get_addon_infoasync_get_addon_info()
201 
202  self._check_addon_available_check_addon_available(addon_info)
203 
204  await self._supervisor_client_supervisor_client.store.install_addon(self.addon_slugaddon_slug)
205 
206  @api_error( "Failed to uninstall the {addon_name} add-on", expected_error_type=SupervisorError, )
207  async def async_uninstall_addon(self) -> None:
208  """Uninstall the managed add-on."""
209  await self._supervisor_client_supervisor_client.addons.uninstall_addon(self.addon_slugaddon_slug)
210 
211  @api_error("Failed to update the {addon_name} add-on")
212  async def async_update_addon(self) -> None:
213  """Update the managed add-on if needed."""
214  addon_info = await self.async_get_addon_infoasync_get_addon_info()
215 
216  self._check_addon_available_check_addon_available(addon_info)
217 
218  if addon_info.state is AddonState.NOT_INSTALLED:
219  raise AddonError(f"{self.addon_name} add-on is not installed")
220 
221  if not addon_info.update_available:
222  return
223 
224  await self.async_create_backupasync_create_backup()
225  await self._supervisor_client_supervisor_client.store.update_addon(
226  self.addon_slugaddon_slug, StoreAddonUpdate(backup=False)
227  )
228 
229  @api_error( "Failed to start the {addon_name} add-on", expected_error_type=SupervisorError )
230  async def async_start_addon(self) -> None:
231  """Start the managed add-on."""
232  await self._supervisor_client_supervisor_client.addons.start_addon(self.addon_slugaddon_slug)
233 
234  @api_error( "Failed to restart the {addon_name} add-on", expected_error_type=SupervisorError )
235  async def async_restart_addon(self) -> None:
236  """Restart the managed add-on."""
237  await self._supervisor_client_supervisor_client.addons.restart_addon(self.addon_slugaddon_slug)
238 
239  @api_error( "Failed to stop the {addon_name} add-on", expected_error_type=SupervisorError )
240  async def async_stop_addon(self) -> None:
241  """Stop the managed add-on."""
242  await self._supervisor_client_supervisor_client.addons.stop_addon(self.addon_slugaddon_slug)
243 
244  @api_error("Failed to create a backup of the {addon_name} add-on")
245  async def async_create_backup(self) -> None:
246  """Create a partial backup of the managed add-on."""
247  addon_info = await self.async_get_addon_infoasync_get_addon_info()
248  name = f"addon_{self.addon_slug}_{addon_info.version}"
249 
250  self._logger_logger.debug("Creating backup: %s", name)
251  await async_create_backup(
252  self._hass_hass,
253  {"name": name, "addons": [self.addon_slugaddon_slug]},
254  partial=True,
255  )
256 
258  self,
259  addon_config: dict[str, Any],
260  ) -> None:
261  """Configure the manager add-on, if needed."""
262  addon_info = await self.async_get_addon_infoasync_get_addon_info()
263 
264  if addon_info.state is AddonState.NOT_INSTALLED:
265  raise AddonError(f"{self.addon_name} add-on is not installed")
266 
267  if addon_config != addon_info.options:
268  await self.async_set_addon_optionsasync_set_addon_options(addon_config)
269 
270  @callback
271  def async_schedule_install_addon(self, catch_error: bool = False) -> asyncio.Task:
272  """Schedule a task that installs the managed add-on.
273 
274  Only schedule a new install task if the there's no running task.
275  """
276  if not self._install_task_install_task or self._install_task_install_task.done():
277  self._logger_logger.info(
278  "%s add-on is not installed. Installing add-on", self.addon_nameaddon_name
279  )
280  self._install_task_install_task = self._async_schedule_addon_operation_async_schedule_addon_operation(
281  self.async_install_addonasync_install_addon, catch_error=catch_error
282  )
283  return self._install_task_install_task
284 
285  @callback
287  self,
288  addon_config: dict[str, Any],
289  catch_error: bool = False,
290  ) -> asyncio.Task:
291  """Schedule a task that installs and sets up the managed add-on.
292 
293  Only schedule a new install task if the there's no running task.
294  """
295  if not self._install_task_install_task or self._install_task_install_task.done():
296  self._logger_logger.info(
297  "%s add-on is not installed. Installing add-on", self.addon_nameaddon_name
298  )
299  self._install_task_install_task = self._async_schedule_addon_operation_async_schedule_addon_operation(
300  self.async_install_addonasync_install_addon,
301  partial(
302  self.async_configure_addonasync_configure_addon,
303  addon_config,
304  ),
305  self.async_start_addonasync_start_addon,
306  catch_error=catch_error,
307  )
308  return self._install_task_install_task
309 
310  @callback
311  def async_schedule_update_addon(self, catch_error: bool = False) -> asyncio.Task:
312  """Schedule a task that updates and sets up the managed add-on.
313 
314  Only schedule a new update task if the there's no running task.
315  """
316  if not self._update_task_update_task or self._update_task_update_task.done():
317  self._logger_logger.info("Trying to update the %s add-on", self.addon_nameaddon_name)
318  self._update_task_update_task = self._async_schedule_addon_operation_async_schedule_addon_operation(
319  self.async_update_addonasync_update_addon,
320  catch_error=catch_error,
321  )
322  return self._update_task_update_task
323 
324  @callback
325  def async_schedule_start_addon(self, catch_error: bool = False) -> asyncio.Task:
326  """Schedule a task that starts the managed add-on.
327 
328  Only schedule a new start task if the there's no running task.
329  """
330  if not self._start_task_start_task or self._start_task_start_task.done():
331  self._logger_logger.info(
332  "%s add-on is not running. Starting add-on", self.addon_nameaddon_name
333  )
334  self._start_task_start_task = self._async_schedule_addon_operation_async_schedule_addon_operation(
335  self.async_start_addonasync_start_addon, catch_error=catch_error
336  )
337  return self._start_task_start_task
338 
339  @callback
340  def async_schedule_restart_addon(self, catch_error: bool = False) -> asyncio.Task:
341  """Schedule a task that restarts the managed add-on.
342 
343  Only schedule a new restart task if the there's no running task.
344  """
345  if not self._restart_task_restart_task or self._restart_task_restart_task.done():
346  self._logger_logger.info("Restarting %s add-on", self.addon_nameaddon_name)
347  self._restart_task_restart_task = self._async_schedule_addon_operation_async_schedule_addon_operation(
348  self.async_restart_addonasync_restart_addon, catch_error=catch_error
349  )
350  return self._restart_task_restart_task
351 
352  @callback
354  self,
355  addon_config: dict[str, Any],
356  catch_error: bool = False,
357  ) -> asyncio.Task:
358  """Schedule a task that configures and starts the managed add-on.
359 
360  Only schedule a new setup task if there's no running task.
361  """
362  if not self._start_task_start_task or self._start_task_start_task.done():
363  self._logger_logger.info(
364  "%s add-on is not running. Starting add-on", self.addon_nameaddon_name
365  )
366  self._start_task_start_task = self._async_schedule_addon_operation_async_schedule_addon_operation(
367  partial(
368  self.async_configure_addonasync_configure_addon,
369  addon_config,
370  ),
371  self.async_start_addonasync_start_addon,
372  catch_error=catch_error,
373  )
374  return self._start_task_start_task
375 
376  @callback
378  self, *funcs: Callable, catch_error: bool = False
379  ) -> asyncio.Task:
380  """Schedule an add-on task."""
381 
382  async def addon_operation() -> None:
383  """Do the add-on operation and catch AddonError."""
384  for func in funcs:
385  try:
386  await func()
387  except AddonError as err:
388  if not catch_error:
389  raise
390  self._logger_logger.error(err)
391  break
392 
393  return self._hass_hass.async_create_task(addon_operation(), eager_start=False)
394 
395 
397  """Represent an error with the managed add-on."""
398 
asyncio.Task async_schedule_update_addon(self, bool catch_error=False)
None async_configure_addon(self, dict[str, Any] addon_config)
asyncio.Task _async_schedule_addon_operation(self, *Callable funcs, bool catch_error=False)
None __init__(self, HomeAssistant hass, logging.Logger logger, str addon_name, str addon_slug)
asyncio.Task async_schedule_setup_addon(self, dict[str, Any] addon_config, bool catch_error=False)
AddonState async_get_addon_state(self, InstalledAddonComplete addon_info)
asyncio.Task async_schedule_install_setup_addon(self, dict[str, Any] addon_config, bool catch_error=False)
asyncio.Task async_schedule_install_addon(self, bool catch_error=False)
asyncio.Task async_schedule_start_addon(self, bool catch_error=False)
asyncio.Task async_schedule_restart_addon(self, bool catch_error=False)
SupervisorClient get_supervisor_client(HomeAssistant hass)
Definition: handler.py:344