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
|
# 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.
|
||||||
|
|||||||
@ -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 "
|
||||||
|
|||||||
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user