HomeCore Controller v0.1.15 staged from HomeCore Admin
This commit is contained in:
parent
ad4b262a78
commit
589c4778aa
@ -1,5 +1,14 @@
|
||||
# HomeCore Controller Changelog
|
||||
|
||||
## 0.1.15
|
||||
|
||||
- Fixes HomeCore self-update on Home Assistant OS using Home Assistant Core's dedicated `hassio/update/addon` WebSocket command.
|
||||
- Removes the dependency on a HomeCore update entity being present in Home Assistant state data.
|
||||
- Home Assistant Core performs the Supervisor app update, avoiding Supervisor's direct self-update restriction.
|
||||
- Keeps exact repository-version validation, backup-before-update, restart recovery and completion reporting.
|
||||
- Automatic rollout remains disabled; deployment remains manual and per-controller from HomeCore Admin.
|
||||
- No functional home-control changes from 0.1.14.
|
||||
|
||||
## 0.1.14
|
||||
|
||||
- End-to-end validation release for the Home Assistant Core `update.install` self-update path.
|
||||
|
||||
@ -16,7 +16,7 @@ from cryptography.hazmat.primitives import serialization
|
||||
import uvicorn
|
||||
from updater import start_update_manager
|
||||
|
||||
VERSION = "0.1.14"
|
||||
VERSION = "0.1.15"
|
||||
|
||||
print(
|
||||
"HOMECORE SUPERVISOR: token "
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from pathlib import Path
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@ -6,6 +7,7 @@ import threading
|
||||
import time
|
||||
|
||||
import httpx
|
||||
import websockets
|
||||
|
||||
|
||||
UPDATE_STATE = Path("/config/homecore_update.json")
|
||||
@ -62,72 +64,83 @@ def supervisor_request(method, path, payload=None, timeout=90):
|
||||
return body
|
||||
|
||||
|
||||
HOME_ASSISTANT_API = "http://supervisor/core/api"
|
||||
HOME_ASSISTANT_WEBSOCKET = "ws://supervisor/core/websocket"
|
||||
|
||||
|
||||
def homeassistant_request(method, path, payload=None, timeout=90):
|
||||
r = httpx.request(
|
||||
method,
|
||||
HOME_ASSISTANT_API + path,
|
||||
headers=supervisor_headers(),
|
||||
json=payload,
|
||||
timeout=timeout,
|
||||
async def _homeassistant_update_app_async(slug, backup=True):
|
||||
token = os.getenv("SUPERVISOR_TOKEN")
|
||||
if not token:
|
||||
raise RuntimeError("SUPERVISOR_TOKEN is not available")
|
||||
|
||||
async with websockets.connect(
|
||||
HOME_ASSISTANT_WEBSOCKET,
|
||||
open_timeout=20,
|
||||
close_timeout=5,
|
||||
ping_interval=None,
|
||||
) as ws:
|
||||
first = json.loads(await asyncio.wait_for(ws.recv(), timeout=20))
|
||||
if first.get("type") != "auth_required":
|
||||
raise RuntimeError(
|
||||
"Unexpected Home Assistant WebSocket greeting: "
|
||||
+ str(first.get("type"))
|
||||
)
|
||||
|
||||
await ws.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "auth",
|
||||
"access_token": token,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
auth = json.loads(await asyncio.wait_for(ws.recv(), timeout=20))
|
||||
if auth.get("type") != "auth_ok":
|
||||
raise RuntimeError(
|
||||
"Home Assistant WebSocket authentication failed: "
|
||||
+ str(auth.get("message") or auth.get("type"))
|
||||
)
|
||||
|
||||
command_id = 1
|
||||
await ws.send(
|
||||
json.dumps(
|
||||
{
|
||||
"id": command_id,
|
||||
"type": "hassio/update/addon",
|
||||
"addon": slug,
|
||||
"backup": bool(backup),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
result = json.loads(await asyncio.wait_for(ws.recv(), timeout=300))
|
||||
|
||||
if result.get("id") != command_id:
|
||||
raise RuntimeError(
|
||||
"Unexpected Home Assistant update response ID"
|
||||
)
|
||||
|
||||
if not result.get("success"):
|
||||
error = result.get("error") or {}
|
||||
raise RuntimeError(
|
||||
str(error.get("message") or error or "Home Assistant update failed")
|
||||
)
|
||||
|
||||
|
||||
def homeassistant_update_app(slug, backup=True):
|
||||
"""
|
||||
Ask Home Assistant Core to update this app.
|
||||
|
||||
Calling Supervisor's /store/addons/<slug>/update endpoint directly from
|
||||
the app is blocked because an app cannot update itself. Home Assistant
|
||||
Core exposes the admin WebSocket command hassio/update/addon specifically
|
||||
for normal app updates. The Supervisor proxy authenticates this app using
|
||||
SUPERVISOR_TOKEN.
|
||||
"""
|
||||
return asyncio.run(
|
||||
_homeassistant_update_app_async(slug, backup=backup)
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
try:
|
||||
return r.json()
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def find_homecore_update_entity(current_version, target_version):
|
||||
states = homeassistant_request("GET", "/states", timeout=30)
|
||||
if not isinstance(states, list):
|
||||
raise RuntimeError("Home Assistant did not return entity states")
|
||||
|
||||
candidates = []
|
||||
|
||||
for state in states:
|
||||
entity_id = str(state.get("entity_id", ""))
|
||||
if not entity_id.startswith("update."):
|
||||
continue
|
||||
|
||||
attrs = state.get("attributes") or {}
|
||||
title = str(attrs.get("title") or "")
|
||||
friendly = str(attrs.get("friendly_name") or "")
|
||||
installed = str(attrs.get("installed_version") or "")
|
||||
latest = str(attrs.get("latest_version") or "")
|
||||
|
||||
name_match = (
|
||||
"homecore controller" in title.lower()
|
||||
or "homecore controller" in friendly.lower()
|
||||
)
|
||||
|
||||
version_match = (
|
||||
installed == current_version
|
||||
and latest == target_version
|
||||
)
|
||||
|
||||
if name_match:
|
||||
candidates.append((state, version_match))
|
||||
|
||||
exact = [state for state, version_match in candidates if version_match]
|
||||
if exact:
|
||||
return exact[0]
|
||||
|
||||
if len(candidates) == 1:
|
||||
return candidates[0][0]
|
||||
|
||||
if not candidates:
|
||||
raise RuntimeError(
|
||||
"HomeCore Controller update entity was not found in Home Assistant"
|
||||
)
|
||||
|
||||
raise RuntimeError(
|
||||
"Multiple HomeCore Controller update entities were found; "
|
||||
"cannot choose safely"
|
||||
)
|
||||
|
||||
|
||||
def find_homecore_app():
|
||||
@ -386,47 +399,23 @@ class HomeCoreUpdateManager:
|
||||
"installing",
|
||||
request_id,
|
||||
target,
|
||||
"Backup requested; Supervisor update starting",
|
||||
"Backup requested; Home Assistant Core app update starting",
|
||||
)
|
||||
|
||||
update_entity = find_homecore_update_entity(
|
||||
self.current_version,
|
||||
target,
|
||||
)
|
||||
update_entity_id = str(update_entity.get("entity_id", "")).strip()
|
||||
|
||||
if not update_entity_id:
|
||||
raise RuntimeError(
|
||||
"HomeCore Controller update entity ID is missing"
|
||||
)
|
||||
|
||||
print(
|
||||
"HOMECORE UPDATE: Home Assistant update action "
|
||||
f"entity={update_entity_id} target={target}",
|
||||
"HOMECORE UPDATE: Home Assistant WebSocket update "
|
||||
f"slug={slug} target={target}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
# Home Assistant Supervisor deliberately blocks an app from calling
|
||||
# its own /store/addons/<slug>/update endpoint. Ask Home Assistant Core
|
||||
# to execute the normal update.install action instead. Core performs
|
||||
# the Supervisor update, exactly as if an administrator installed the
|
||||
# app update from the Home Assistant UI.
|
||||
#
|
||||
# The state above is written before this call because updating this
|
||||
# app stops the current container. On restart, mark_completed_if_required
|
||||
# reports completion back to HomeCore Admin.
|
||||
homeassistant_request(
|
||||
"POST",
|
||||
"/services/update/install",
|
||||
{
|
||||
"entity_id": update_entity_id,
|
||||
"backup": True,
|
||||
},
|
||||
timeout=300,
|
||||
)
|
||||
# Home Assistant Supervisor blocks an app from calling its own
|
||||
# /store/addons/<slug>/update endpoint directly. Route the request
|
||||
# through Home Assistant Core's admin WebSocket command instead.
|
||||
# Core then performs the normal Supervisor app update on our behalf.
|
||||
homeassistant_update_app(slug, backup=True)
|
||||
|
||||
print(
|
||||
"HOMECORE UPDATE: Home Assistant accepted manual update action",
|
||||
"HOMECORE UPDATE: Home Assistant accepted manual app update",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
name: HomeCore Controller
|
||||
version: "0.1.14"
|
||||
version: "0.1.15"
|
||||
slug: homecore_controller
|
||||
description: HomeCore local controller agent for Home Assistant Green.
|
||||
url: https://homecore.home-core.co.uk
|
||||
|
||||
Loading…
Reference in New Issue
Block a user