Skip to content

Commit 242dd82

Browse files
committed
Add supervisor APIs to client library
1 parent 00c4938 commit 242dd82

File tree

7 files changed

+313
-0
lines changed

7 files changed

+313
-0
lines changed

aiohasupervisor/models/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@
4545
UpdateChannel,
4646
UpdateType,
4747
)
48+
from aiohasupervisor.models.supervisor import (
49+
SupervisorInfo,
50+
SupervisorOptions,
51+
SupervisorStats,
52+
SupervisorUpdateOptions,
53+
)
4854

4955
__all__ = [
5056
"HostFeature",
@@ -86,4 +92,8 @@
8692
"SuggestionType",
8793
"UnhealthyReason",
8894
"UnsupportedReason",
95+
"SupervisorInfo",
96+
"SupervisorOptions",
97+
"SupervisorStats",
98+
"SupervisorUpdateOptions",
8999
]
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Models for supervisor component."""
2+
3+
from dataclasses import dataclass
4+
5+
from .base import Options, Request, ResponseData
6+
from .root import LogLevel, UpdateChannel
7+
8+
9+
@dataclass(frozen=True, slots=True)
10+
class SupervisorInfo(ResponseData):
11+
"""SupervisorInfo model."""
12+
13+
version: str
14+
version_latest: str
15+
update_available: bool
16+
channel: UpdateChannel
17+
arch: str
18+
supported: bool
19+
healthy: bool
20+
ip_address: str
21+
timezone: str | None
22+
logging: LogLevel
23+
debug: bool
24+
debug_block: bool
25+
diagnostics: bool | None
26+
auto_update: bool
27+
28+
29+
@dataclass(frozen=True, slots=True)
30+
class SupervisorStats(ResponseData):
31+
"""SupervisorStats model."""
32+
33+
cpu_percent: float
34+
memory_usage: int
35+
memory_limit: int
36+
memory_percent: float
37+
network_rx: int
38+
network_tx: int
39+
blk_read: int
40+
blk_write: int
41+
42+
43+
@dataclass(frozen=True, slots=True)
44+
class SupervisorUpdateOptions(Request):
45+
"""SupervisorUpdateOptions model."""
46+
47+
version: str
48+
49+
50+
@dataclass(frozen=True, slots=True)
51+
class SupervisorOptions(Options):
52+
"""SupervisorOptions model."""
53+
54+
channel: UpdateChannel | None = None
55+
timezone: str | None = None
56+
logging: LogLevel | None = None
57+
debug: bool | None = None
58+
debug_block: bool | None = None
59+
diagnostics: bool | None = None
60+
content_trust: bool | None = None
61+
force_security: bool | None = None
62+
auto_update: bool | None = None

aiohasupervisor/root.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .models.root import AvailableUpdate, AvailableUpdates, RootInfo
1010
from .resolution import ResolutionClient
1111
from .store import StoreClient
12+
from .supervisor import SupervisorManagementClient
1213

1314

1415
class SupervisorClient:
@@ -26,6 +27,7 @@ def __init__(
2627
self._addons = AddonsClient(self._client)
2728
self._resolution = ResolutionClient(self._client)
2829
self._store = StoreClient(self._client)
30+
self._supervisor = SupervisorManagementClient(self._client)
2931

3032
@property
3133
def addons(self) -> AddonsClient:
@@ -42,6 +44,11 @@ def store(self) -> StoreClient:
4244
"""Get store component client."""
4345
return self._store
4446

47+
@property
48+
def supervisor(self) -> SupervisorManagementClient:
49+
"""Get supervisor component client."""
50+
return self._supervisor
51+
4552
async def info(self) -> RootInfo:
4653
"""Get root info."""
4754
result = await self._client.get("info")

aiohasupervisor/supervisor.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Supervisor client for supervisor."""
2+
3+
from .client import _SupervisorComponentClient
4+
from .const import ResponseType
5+
from .models.supervisor import (
6+
SupervisorInfo,
7+
SupervisorOptions,
8+
SupervisorStats,
9+
SupervisorUpdateOptions,
10+
)
11+
12+
13+
class SupervisorManagementClient(_SupervisorComponentClient):
14+
"""Handles supervisor access in supervisor."""
15+
16+
async def ping(self) -> None:
17+
"""Check connection to supervisor."""
18+
await self._client.get("supervisor/ping", response_type=ResponseType.NONE)
19+
20+
async def info(self) -> SupervisorInfo:
21+
"""Get supervisor info."""
22+
result = await self._client.get("supervisor/info")
23+
return SupervisorInfo.from_dict(result.data)
24+
25+
async def stats(self) -> SupervisorStats:
26+
"""Get supervisor stats."""
27+
result = await self._client.get("supervisor/stats")
28+
return SupervisorStats.from_dict(result.data)
29+
30+
async def update(self, options: SupervisorUpdateOptions | None = None) -> None:
31+
"""Update supervisor.
32+
33+
Providing a target version in options only works on development systems.
34+
On non-development systems this API will always update supervisor to the
35+
latest version and ignore that field.
36+
"""
37+
await self._client.post(
38+
"supervisor/update", json=options.to_dict() if options else None
39+
)
40+
41+
async def reload(self) -> None:
42+
"""Reload supervisor (add-ons, configuration, etc)."""
43+
await self._client.post("supervisor/reload")
44+
45+
async def restart(self) -> None:
46+
"""Restart supervisor."""
47+
await self._client.post("supervisor/restart")
48+
49+
async def options(self, options: SupervisorOptions) -> None:
50+
"""Set supervisor options."""
51+
await self._client.post("supervisor/options", json=options.to_dict())
52+
53+
async def repair(self) -> None:
54+
"""Repair local supervisor and docker setup."""
55+
await self._client.post("supervisor/repair")
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"result": "ok",
3+
"data": {
4+
"version": "2024.09.1",
5+
"version_latest": "2024.09.1",
6+
"update_available": true,
7+
"channel": "stable",
8+
"arch": "aarch64",
9+
"supported": true,
10+
"healthy": true,
11+
"ip_address": "172.30.32.2",
12+
"timezone": "America/New_York",
13+
"logging": "info",
14+
"debug": true,
15+
"debug_block": false,
16+
"diagnostics": false,
17+
"auto_update": true,
18+
"wait_boot": 5,
19+
"addons": [
20+
{
21+
"name": "Terminal & SSH",
22+
"slug": "core_ssh",
23+
"version": "9.14.0",
24+
"version_latest": "9.14.0",
25+
"update_available": false,
26+
"state": "started",
27+
"repository": "core",
28+
"icon": true
29+
},
30+
{
31+
"name": "Mosquitto broker",
32+
"slug": "core_mosquitto",
33+
"version": "6.4.1",
34+
"version_latest": "6.4.1",
35+
"update_available": false,
36+
"state": "started",
37+
"repository": "core",
38+
"icon": true
39+
}
40+
],
41+
"addons_repositories": [
42+
{ "name": "Local add-ons", "slug": "local" },
43+
{ "name": "Music Assistant", "slug": "d5369777" },
44+
{ "name": "Official add-ons", "slug": "core" },
45+
{ "name": "ESPHome", "slug": "5c53de3b" },
46+
{ "name": "Home Assistant Community Add-ons", "slug": "a0d7b954" }
47+
]
48+
}
49+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"result": "ok",
3+
"data": {
4+
"cpu_percent": 0.04,
5+
"memory_usage": 243982336,
6+
"memory_limit": 3899138048,
7+
"memory_percent": 6.26,
8+
"network_rx": 176623,
9+
"network_tx": 114204,
10+
"blk_read": 0,
11+
"blk_write": 0
12+
}
13+
}

tests/test_supervisor.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""Test for supervisor management client."""
2+
3+
from aioresponses import aioresponses
4+
from yarl import URL
5+
6+
from aiohasupervisor import SupervisorClient
7+
from aiohasupervisor.models import SupervisorOptions
8+
9+
from . import load_fixture
10+
from .const import SUPERVISOR_URL
11+
12+
13+
async def test_supervisor_ping(
14+
responses: aioresponses, supervisor_client: SupervisorClient
15+
) -> None:
16+
"""Test supervisor ping API."""
17+
responses.get(f"{SUPERVISOR_URL}/supervisor/ping", status=200)
18+
assert await supervisor_client.supervisor.ping() is None
19+
assert responses.requests.keys() == {
20+
("GET", URL(f"{SUPERVISOR_URL}/supervisor/ping"))
21+
}
22+
23+
24+
async def test_supervisor_info(
25+
responses: aioresponses, supervisor_client: SupervisorClient
26+
) -> None:
27+
"""Test supervisor info API."""
28+
responses.get(
29+
f"{SUPERVISOR_URL}/supervisor/info",
30+
status=200,
31+
body=load_fixture("supervisor_info.json"),
32+
)
33+
info = await supervisor_client.supervisor.info()
34+
35+
assert info.version == "2024.09.1"
36+
assert info.channel == "stable"
37+
assert info.arch == "aarch64"
38+
assert info.supported is True
39+
assert info.healthy is True
40+
assert info.logging == "info"
41+
42+
43+
async def test_supervisor_stats(
44+
responses: aioresponses, supervisor_client: SupervisorClient
45+
) -> None:
46+
"""Test supervisor stats API."""
47+
responses.get(
48+
f"{SUPERVISOR_URL}/supervisor/stats",
49+
status=200,
50+
body=load_fixture("supervisor_stats.json"),
51+
)
52+
stats = await supervisor_client.supervisor.stats()
53+
54+
assert stats.cpu_percent == 0.04
55+
assert stats.memory_usage == 243982336
56+
assert stats.memory_limit == 3899138048
57+
assert stats.memory_percent == 6.26
58+
59+
60+
async def test_supervisor_update(
61+
responses: aioresponses, supervisor_client: SupervisorClient
62+
) -> None:
63+
"""Test supervisor update API."""
64+
responses.post(f"{SUPERVISOR_URL}/supervisor/update", status=200)
65+
assert await supervisor_client.supervisor.update() is None
66+
assert responses.requests.keys() == {
67+
("POST", URL(f"{SUPERVISOR_URL}/supervisor/update"))
68+
}
69+
70+
71+
async def test_supervisor_reload(
72+
responses: aioresponses, supervisor_client: SupervisorClient
73+
) -> None:
74+
"""Test supervisor reload API."""
75+
responses.post(f"{SUPERVISOR_URL}/supervisor/reload", status=200)
76+
assert await supervisor_client.supervisor.reload() is None
77+
assert responses.requests.keys() == {
78+
("POST", URL(f"{SUPERVISOR_URL}/supervisor/reload"))
79+
}
80+
81+
82+
async def test_supervisor_restart(
83+
responses: aioresponses, supervisor_client: SupervisorClient
84+
) -> None:
85+
"""Test supervisor restart API."""
86+
responses.post(f"{SUPERVISOR_URL}/supervisor/restart", status=200)
87+
assert await supervisor_client.supervisor.restart() is None
88+
assert responses.requests.keys() == {
89+
("POST", URL(f"{SUPERVISOR_URL}/supervisor/restart"))
90+
}
91+
92+
93+
async def test_supervisor_options(
94+
responses: aioresponses, supervisor_client: SupervisorClient
95+
) -> None:
96+
"""Test supervisor options API."""
97+
responses.post(f"{SUPERVISOR_URL}/supervisor/options", status=200)
98+
assert (
99+
await supervisor_client.supervisor.options(
100+
SupervisorOptions(debug=True, debug_block=True)
101+
)
102+
is None
103+
)
104+
assert responses.requests.keys() == {
105+
("POST", URL(f"{SUPERVISOR_URL}/supervisor/options"))
106+
}
107+
108+
109+
async def test_supervisor_repair(
110+
responses: aioresponses, supervisor_client: SupervisorClient
111+
) -> None:
112+
"""Test supervisor repair API."""
113+
responses.post(f"{SUPERVISOR_URL}/supervisor/repair", status=200)
114+
assert await supervisor_client.supervisor.repair() is None
115+
assert responses.requests.keys() == {
116+
("POST", URL(f"{SUPERVISOR_URL}/supervisor/repair"))
117+
}

0 commit comments

Comments
 (0)