Skip to content

Commit 7ed83a1

Browse files
authored
Add availability API for addons (#6140)
* Add availability API for addons * Add cast back and test for latest version of installed addon * Make error responses more translation/client library friendly * Add test cases for install/update APIs
1 parent a3a5f6b commit 7ed83a1

File tree

10 files changed

+360
-24
lines changed

10 files changed

+360
-24
lines changed

supervisor/addons/addon.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@
6767
from ..docker.stats import DockerStats
6868
from ..exceptions import (
6969
AddonConfigurationError,
70+
AddonNotSupportedError,
7071
AddonsError,
7172
AddonsJobError,
72-
AddonsNotSupportedError,
7373
ConfigurationFileError,
7474
DockerError,
7575
HomeAssistantAPIError,
@@ -1172,7 +1172,7 @@ async def stats(self) -> DockerStats:
11721172
async def write_stdin(self, data) -> None:
11731173
"""Write data to add-on stdin."""
11741174
if not self.with_stdin:
1175-
raise AddonsNotSupportedError(
1175+
raise AddonNotSupportedError(
11761176
f"Add-on {self.slug} does not support writing to stdin!", _LOGGER.error
11771177
)
11781178

@@ -1419,7 +1419,7 @@ def _extract_tarfile() -> tuple[TemporaryDirectory, dict[str, Any]]:
14191419

14201420
# If available
14211421
if not self._available(data[ATTR_SYSTEM]):
1422-
raise AddonsNotSupportedError(
1422+
raise AddonNotSupportedError(
14231423
f"Add-on {self.slug} is not available for this platform",
14241424
_LOGGER.error,
14251425
)

supervisor/addons/manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
from ..const import AddonBoot, AddonStartup, AddonState
1515
from ..coresys import CoreSys, CoreSysAttributes
1616
from ..exceptions import (
17+
AddonNotSupportedError,
1718
AddonsError,
1819
AddonsJobError,
19-
AddonsNotSupportedError,
2020
CoreDNSError,
2121
DockerError,
2222
HassioError,
@@ -307,7 +307,7 @@ async def rebuild(self, slug: str, *, force: bool = False) -> asyncio.Task | Non
307307
"Version changed, use Update instead Rebuild", _LOGGER.error
308308
)
309309
if not force and not addon.need_build:
310-
raise AddonsNotSupportedError(
310+
raise AddonNotSupportedError(
311311
"Can't rebuild a image based add-on", _LOGGER.error
312312
)
313313

supervisor/addons/model.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,12 @@
8989
)
9090
from ..coresys import CoreSys
9191
from ..docker.const import Capabilities
92-
from ..exceptions import AddonsNotSupportedError
92+
from ..exceptions import (
93+
AddonNotSupportedArchitectureError,
94+
AddonNotSupportedError,
95+
AddonNotSupportedHomeAssistantVersionError,
96+
AddonNotSupportedMachineTypeError,
97+
)
9398
from ..jobs.const import JOB_GROUP_ADDON
9499
from ..jobs.job_group import JobGroup
95100
from ..utils import version_is_new_enough
@@ -680,19 +685,17 @@ def _validate_availability(
680685
"""Validate if addon is available for current system."""
681686
# Architecture
682687
if not self.sys_arch.is_supported(config[ATTR_ARCH]):
683-
raise AddonsNotSupportedError(
684-
f"Add-on {self.slug} not supported on this platform, supported architectures: {', '.join(config[ATTR_ARCH])}",
685-
logger,
688+
raise AddonNotSupportedArchitectureError(
689+
logger, slug=self.slug, architectures=config[ATTR_ARCH]
686690
)
687691

688692
# Machine / Hardware
689693
machine = config.get(ATTR_MACHINE)
690694
if machine and (
691695
f"!{self.sys_machine}" in machine or self.sys_machine not in machine
692696
):
693-
raise AddonsNotSupportedError(
694-
f"Add-on {self.slug} not supported on this machine, supported machine types: {', '.join(machine)}",
695-
logger,
697+
raise AddonNotSupportedMachineTypeError(
698+
logger, slug=self.slug, machine_types=machine
696699
)
697700

698701
# Home Assistant
@@ -701,16 +704,15 @@ def _validate_availability(
701704
if version and not version_is_new_enough(
702705
self.sys_homeassistant.version, version
703706
):
704-
raise AddonsNotSupportedError(
705-
f"Add-on {self.slug} not supported on this system, requires Home Assistant version {version} or greater",
706-
logger,
707+
raise AddonNotSupportedHomeAssistantVersionError(
708+
logger, slug=self.slug, version=str(version)
707709
)
708710

709711
def _available(self, config) -> bool:
710712
"""Return True if this add-on is available on this platform."""
711713
try:
712714
self._validate_availability(config)
713-
except AddonsNotSupportedError:
715+
except AddonNotSupportedError:
714716
return False
715717

716718
return True

supervisor/api/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,10 @@ def _register_store(self) -> None:
735735
"/store/addons/{addon}/documentation",
736736
api_store.addons_addon_documentation,
737737
),
738+
web.get(
739+
"/store/addons/{addon}/availability",
740+
api_store.addons_addon_availability,
741+
),
738742
web.post(
739743
"/store/addons/{addon}/install", api_store.addons_addon_install
740744
),

supervisor/api/store.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,12 @@ async def addons_addon_documentation(self, request: web.Request) -> str:
326326
_read_static_text_file, addon.path_documentation
327327
)
328328

329+
@api_process
330+
async def addons_addon_availability(self, request: web.Request) -> None:
331+
"""Check add-on availability for current system."""
332+
addon = cast(AddonStore, self._extract_addon(request))
333+
addon.validate_availability()
334+
329335
@api_process
330336
async def repositories_list(self, request: web.Request) -> list[dict[str, Any]]:
331337
"""Return all repositories."""

supervisor/api/utils.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
HEADER_TOKEN,
1717
HEADER_TOKEN_OLD,
1818
JSON_DATA,
19+
JSON_ERROR_KEY,
20+
JSON_EXTRA_FIELDS,
1921
JSON_JOB_ID,
2022
JSON_MESSAGE,
23+
JSON_MESSAGE_TEMPLATE,
2124
JSON_RESULT,
2225
REQUEST_FROM,
2326
RESULT_ERROR,
@@ -136,10 +139,11 @@ async def wrap_api(
136139

137140

138141
def api_return_error(
139-
error: Exception | None = None,
142+
error: HassioError | None = None,
140143
message: str | None = None,
141144
error_type: str | None = None,
142145
status: int = 400,
146+
*,
143147
job_id: str | None = None,
144148
) -> web.Response:
145149
"""Return an API error message."""
@@ -158,12 +162,18 @@ def api_return_error(
158162
body=message.encode(), content_type=error_type, status=status
159163
)
160164
case _:
161-
result = {
165+
result: dict[str, Any] = {
162166
JSON_RESULT: RESULT_ERROR,
163167
JSON_MESSAGE: message,
164168
}
165169
if job_id:
166170
result[JSON_JOB_ID] = job_id
171+
if error and error.error_key:
172+
result[JSON_ERROR_KEY] = error.error_key
173+
if error and error.message_template:
174+
result[JSON_MESSAGE_TEMPLATE] = error.message_template
175+
if error and error.extra_fields:
176+
result[JSON_EXTRA_FIELDS] = error.extra_fields
167177

168178
return web.json_response(
169179
result,

supervisor/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@
7676
JSON_MESSAGE = "message"
7777
JSON_RESULT = "result"
7878
JSON_JOB_ID = "job_id"
79+
JSON_ERROR_KEY = "error_key"
80+
JSON_MESSAGE_TEMPLATE = "message_template"
81+
JSON_EXTRA_FIELDS = "extra_fields"
7982

8083
RESULT_ERROR = "error"
8184
RESULT_OK = "ok"

supervisor/exceptions.py

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
11
"""Core Exceptions."""
22

33
from collections.abc import Callable
4+
from typing import Any
45

56

67
class HassioError(Exception):
78
"""Root exception."""
89

10+
error_key: str | None = None
11+
message_template: str | None = None
12+
913
def __init__(
1014
self,
1115
message: str | None = None,
1216
logger: Callable[..., None] | None = None,
17+
*,
18+
extra_fields: dict[str, Any] | None = None,
1319
) -> None:
1420
"""Raise & log."""
21+
self.extra_fields = extra_fields or {}
22+
23+
if not message and self.message_template:
24+
message = (
25+
self.message_template.format(**self.extra_fields)
26+
if self.extra_fields
27+
else self.message_template
28+
)
29+
1530
if logger is not None and message is not None:
1631
logger(message)
1732

@@ -235,8 +250,71 @@ class AddonConfigurationError(AddonsError):
235250
"""Error with add-on configuration."""
236251

237252

238-
class AddonsNotSupportedError(HassioNotSupportedError):
239-
"""Addons don't support a function."""
253+
class AddonNotSupportedError(HassioNotSupportedError):
254+
"""Addon doesn't support a function."""
255+
256+
257+
class AddonNotSupportedArchitectureError(AddonNotSupportedError):
258+
"""Addon does not support system due to architecture."""
259+
260+
error_key = "addon_not_supported_architecture_error"
261+
message_template = "Add-on {slug} not supported on this platform, supported architectures: {architectures}"
262+
263+
def __init__(
264+
self,
265+
logger: Callable[..., None] | None = None,
266+
*,
267+
slug: str,
268+
architectures: list[str],
269+
) -> None:
270+
"""Initialize exception."""
271+
super().__init__(
272+
None,
273+
logger,
274+
extra_fields={"slug": slug, "architectures": ", ".join(architectures)},
275+
)
276+
277+
278+
class AddonNotSupportedMachineTypeError(AddonNotSupportedError):
279+
"""Addon does not support system due to machine type."""
280+
281+
error_key = "addon_not_supported_machine_type_error"
282+
message_template = "Add-on {slug} not supported on this machine, supported machine types: {machine_types}"
283+
284+
def __init__(
285+
self,
286+
logger: Callable[..., None] | None = None,
287+
*,
288+
slug: str,
289+
machine_types: list[str],
290+
) -> None:
291+
"""Initialize exception."""
292+
super().__init__(
293+
None,
294+
logger,
295+
extra_fields={"slug": slug, "machine_types": ", ".join(machine_types)},
296+
)
297+
298+
299+
class AddonNotSupportedHomeAssistantVersionError(AddonNotSupportedError):
300+
"""Addon does not support system due to Home Assistant version."""
301+
302+
error_key = "addon_not_supported_home_assistant_version_error"
303+
message_template = "Add-on {slug} not supported on this system, requires Home Assistant version {version} or greater"
304+
305+
def __init__(
306+
self,
307+
logger: Callable[..., None] | None = None,
308+
*,
309+
slug: str,
310+
version: str,
311+
) -> None:
312+
"""Initialize exception."""
313+
super().__init__(
314+
None,
315+
logger,
316+
extra_fields={"slug": slug, "version": version},
317+
)
240318

241319

242320
class AddonsJobError(AddonsError, JobException):
@@ -319,10 +397,17 @@ def __init__(
319397
self,
320398
message: str | None = None,
321399
logger: Callable[..., None] | None = None,
400+
*,
322401
job_id: str | None = None,
402+
error: HassioError | None = None,
323403
) -> None:
324404
"""Raise & log, optionally with job."""
325-
super().__init__(message, logger)
405+
# Allow these to be set from another error here since APIErrors essentially wrap others to add a status
406+
self.error_key = error.error_key if error else None
407+
self.message_template = error.message_template if error else None
408+
super().__init__(
409+
message, logger, extra_fields=error.extra_fields if error else None
410+
)
326411
self.job_id = job_id
327412

328413

0 commit comments

Comments
 (0)