HomeCore Controller v0.1.15 staged from HomeCore Admin

This commit is contained in:
HomeCore Admin 2026-09-16 13:37:41 +00:00
parent ad4b262a78
commit 589c4778aa
4 changed files with 95 additions and 97 deletions

View File

@ -1,5 +1,14 @@
# HomeCore Controller Changelog # 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 ## 0.1.14
- End-to-end validation release for the Home Assistant Core `update.install` self-update path. - End-to-end validation release for the Home Assistant Core `update.install` self-update path.

View File

@ -16,7 +16,7 @@ from cryptography.hazmat.primitives import serialization
import uvicorn import uvicorn
from updater import start_update_manager from updater import start_update_manager
VERSION = "0.1.14" VERSION = "0.1.15"
print( print(
"HOMECORE SUPERVISOR: token " "HOMECORE SUPERVISOR: token "

View File

@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
import asyncio
import json import json
import os import os
import re import re
@ -6,6 +7,7 @@ import threading
import time import time
import httpx import httpx
import websockets
UPDATE_STATE = Path("/config/homecore_update.json") UPDATE_STATE = Path("/config/homecore_update.json")
@ -62,72 +64,83 @@ def supervisor_request(method, path, payload=None, timeout=90):
return body 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): async def _homeassistant_update_app_async(slug, backup=True):
r = httpx.request( token = os.getenv("SUPERVISOR_TOKEN")
method, if not token:
HOME_ASSISTANT_API + path, raise RuntimeError("SUPERVISOR_TOKEN is not available")
headers=supervisor_headers(),
json=payload, async with websockets.connect(
timeout=timeout, 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(): def find_homecore_app():
@ -386,47 +399,23 @@ class HomeCoreUpdateManager:
"installing", "installing",
request_id, request_id,
target, 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( print(
"HOMECORE UPDATE: Home Assistant update action " "HOMECORE UPDATE: Home Assistant WebSocket update "
f"entity={update_entity_id} target={target}", f"slug={slug} target={target}",
flush=True, flush=True,
) )
# Home Assistant Supervisor deliberately blocks an app from calling # Home Assistant Supervisor blocks an app from calling its own
# its own /store/addons/<slug>/update endpoint. Ask Home Assistant Core # /store/addons/<slug>/update endpoint directly. Route the request
# to execute the normal update.install action instead. Core performs # through Home Assistant Core's admin WebSocket command instead.
# the Supervisor update, exactly as if an administrator installed the # Core then performs the normal Supervisor app update on our behalf.
# app update from the Home Assistant UI. homeassistant_update_app(slug, backup=True)
#
# 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,
)
print( print(
"HOMECORE UPDATE: Home Assistant accepted manual update action", "HOMECORE UPDATE: Home Assistant accepted manual app update",
flush=True, flush=True,
) )

View File

@ -1,5 +1,5 @@
name: HomeCore Controller name: HomeCore Controller
version: "0.1.14" version: "0.1.15"
slug: homecore_controller slug: homecore_controller
description: HomeCore local controller agent for Home Assistant Green. description: HomeCore local controller agent for Home Assistant Green.
url: https://homecore.home-core.co.uk url: https://homecore.home-core.co.uk