feat(homeassistant): a scheduled OS update reboots the machine and is done once the new version runs (#6755)
Home Assistant writes an OS update and leaves the reboot to someone, so a scheduled one never finished unattended. The schedule now asks for the reboot once, follows it through Home Assistant going away, and counts it done when the Supervisor reports a different version with nothing pending. A machine that doesn't come back or falls back to the old version fails after 20 minutes, which ends the slot and raises update_failed. Install on the page and through MCP still leave the reboot to the person. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import asyncio
|
||||
import json
|
||||
import ssl
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
@@ -242,9 +243,9 @@ class _Socket:
|
||||
return await self.ask(type="supervisor/api", endpoint=endpoint, method="get")
|
||||
|
||||
|
||||
async def read_system(url: str, token: str, verify_tls: bool, connect=None) -> dict:
|
||||
"""What service.read_health needs, as Home Assistant answered it. `connect`
|
||||
stands in for the websocket in tests."""
|
||||
@asynccontextmanager
|
||||
async def _signed_in(url: str, token: str, verify_tls: bool, connect):
|
||||
"""A signed-in socket. `connect` stands in for the websocket in tests."""
|
||||
target = "ws" + url.rstrip("/").removeprefix("http") + "/api/websocket"
|
||||
options: dict = {"max_size": MAX_BYTES}
|
||||
if target.startswith("wss:") and not verify_tls:
|
||||
@@ -255,23 +256,37 @@ async def read_system(url: str, token: str, verify_tls: bool, connect=None) -> d
|
||||
async with (connect or ws_connect)(target, **options) as ws:
|
||||
socket = _Socket(ws)
|
||||
await socket.sign_in(token)
|
||||
found = {"config": await socket.ask(type="get_config"),
|
||||
"issues": (await socket.ask(type="repairs/list_issues") or {}).get("issues"),
|
||||
"resolution": await socket.supervisor("/resolution/info"),
|
||||
"host": await socket.supervisor("/host/info"), "titles": {}, "boots": {}}
|
||||
domains = sorted({i["domain"] for i in found["issues"] or []})
|
||||
if domains:
|
||||
titles = await socket.ask(type="frontend/get_translations", language="en",
|
||||
category="issues", integration=domains)
|
||||
found["titles"] = (titles or {}).get("resources") or {}
|
||||
found["addons"] = (await socket.supervisor("/addons") or {}).get("addons")
|
||||
# Whether one should be running is only on its own page, so a running one isn't asked.
|
||||
for addon in found["addons"] or []:
|
||||
if addon.get("state") != "started":
|
||||
info = await socket.supervisor(f"/addons/{addon['slug']}/info") or {}
|
||||
found["boots"][addon["slug"]] = info.get("boot")
|
||||
return found
|
||||
yield socket
|
||||
except TimeoutError:
|
||||
raise Unreachable(f"Home Assistant's system report didn't arrive within {SYSTEM_READ_SECONDS} s.") from None
|
||||
except (OSError, ValueError, KeyError, WebSocketException) as exc:
|
||||
raise Unreachable(f"Couldn't read Home Assistant's system report: {exc}") from None
|
||||
|
||||
|
||||
async def read_system(url: str, token: str, verify_tls: bool, connect=None) -> dict:
|
||||
"""What service.read_health needs, as Home Assistant answered it."""
|
||||
async with _signed_in(url, token, verify_tls, connect) as socket:
|
||||
found = {"config": await socket.ask(type="get_config"),
|
||||
"issues": (await socket.ask(type="repairs/list_issues") or {}).get("issues"),
|
||||
"resolution": await socket.supervisor("/resolution/info"),
|
||||
"host": await socket.supervisor("/host/info"), "titles": {}, "boots": {}}
|
||||
domains = sorted({i["domain"] for i in found["issues"] or []})
|
||||
if domains:
|
||||
titles = await socket.ask(type="frontend/get_translations", language="en",
|
||||
category="issues", integration=domains)
|
||||
found["titles"] = (titles or {}).get("resources") or {}
|
||||
found["addons"] = (await socket.supervisor("/addons") or {}).get("addons")
|
||||
# Whether one should be running is only on its own page, so a running one isn't asked.
|
||||
for addon in found["addons"] or []:
|
||||
if addon.get("state") != "started":
|
||||
info = await socket.supervisor(f"/addons/{addon['slug']}/info") or {}
|
||||
found["boots"][addon["slug"]] = info.get("boot")
|
||||
return found
|
||||
|
||||
|
||||
async def read_os(url: str, token: str, verify_tls: bool, connect=None) -> dict | None:
|
||||
"""The Supervisor's report on the OS: `version`, and `version_pending` while a
|
||||
written update waits for its reboot. None if it isn't answering yet, as just
|
||||
after a boot."""
|
||||
async with _signed_in(url, token, verify_tls, connect) as socket:
|
||||
return await socket.supervisor("/os/info")
|
||||
|
||||
Reference in New Issue
Block a user