From 589c4778aad55a2bd30ce5bbf9769d7927dac509 Mon Sep 17 00:00:00 2001 From: HomeCore Admin Date: Wed, 16 Sep 2026 13:37:41 +0000 Subject: [PATCH] HomeCore Controller v0.1.15 staged from HomeCore Admin --- homecore_controller/CHANGELOG.md | 9 ++ homecore_controller/app/main.py | 2 +- homecore_controller/app/updater.py | 179 ++++++++++++++--------------- homecore_controller/config.yaml | 2 +- 4 files changed, 95 insertions(+), 97 deletions(-) diff --git a/homecore_controller/CHANGELOG.md b/homecore_controller/CHANGELOG.md index c756635..61ce53b 100644 --- a/homecore_controller/CHANGELOG.md +++ b/homecore_controller/CHANGELOG.md @@ -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. diff --git a/homecore_controller/app/main.py b/homecore_controller/app/main.py index 864cbea..0a82469 100644 --- a/homecore_controller/app/main.py +++ b/homecore_controller/app/main.py @@ -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 " diff --git a/homecore_controller/app/updater.py b/homecore_controller/app/updater.py index 4b61d6f..63b1269 100644 --- a/homecore_controller/app/updater.py +++ b/homecore_controller/app/updater.py @@ -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//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//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//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, ) diff --git a/homecore_controller/config.yaml b/homecore_controller/config.yaml index 471e78d..b5f7c2c 100644 --- a/homecore_controller/config.yaml +++ b/homecore_controller/config.yaml @@ -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