From 56831c699fe9dd482a6283c03f15d80959c9b9eb Mon Sep 17 00:00:00 2001 From: HomeCore Admin Date: Wed, 16 Sep 2026 13:49:09 +0000 Subject: [PATCH] Add HomeCore Updater v1.0 --- homecore_updater/Dockerfile | 13 + homecore_updater/README.md | 10 + .../app/__pycache__/main.cpython-312.pyc | Bin 0 -> 13527 bytes homecore_updater/app/main.py | 374 ++++++++++++++++++ homecore_updater/config.yaml | 20 + homecore_updater/requirements.txt | 1 + homecore_updater/run.sh | 3 + 7 files changed, 421 insertions(+) create mode 100644 homecore_updater/Dockerfile create mode 100644 homecore_updater/README.md create mode 100644 homecore_updater/app/__pycache__/main.cpython-312.pyc create mode 100644 homecore_updater/app/main.py create mode 100644 homecore_updater/config.yaml create mode 100644 homecore_updater/requirements.txt create mode 100644 homecore_updater/run.sh diff --git a/homecore_updater/Dockerfile b/homecore_updater/Dockerfile new file mode 100644 index 0000000..68733ff --- /dev/null +++ b/homecore_updater/Dockerfile @@ -0,0 +1,13 @@ +FROM ghcr.io/home-assistant/base:latest +ARG BUILD_VERSION +ARG BUILD_ARCH +LABEL io.hass.version="$BUILD_VERSION" io.hass.type="app" io.hass.arch="$BUILD_ARCH" +RUN apk add --no-cache python3 py3-pip && python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" +COPY requirements.txt /tmp/requirements.txt +RUN pip install --no-cache-dir -r /tmp/requirements.txt +COPY app /opt/homecore-updater +COPY run.sh /run.sh +RUN chmod a+x /run.sh +WORKDIR /opt/homecore-updater +CMD ["/run.sh"] diff --git a/homecore_updater/README.md b/homecore_updater/README.md new file mode 100644 index 0000000..d22a8bb --- /dev/null +++ b/homecore_updater/README.md @@ -0,0 +1,10 @@ +# HomeCore Updater + +Internal HomeCore maintenance app. + +It has no customer UI. It watches `/share/homecore/update-request.json` for +locally signed requests from HomeCore Controller and performs the requested +HomeCore Controller update through Home Assistant Supervisor. + +Automatic rollout is not supported. HomeCore Admin must create each update +request for a specific controller. diff --git a/homecore_updater/app/__pycache__/main.cpython-312.pyc b/homecore_updater/app/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b34a2e4a4c53f48f5f27671bd886615c5fca6320 GIT binary patch literal 13527 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 literal 0 HcmV?d00001 diff --git a/homecore_updater/app/main.py b/homecore_updater/app/main.py new file mode 100644 index 0000000..6839579 --- /dev/null +++ b/homecore_updater/app/main.py @@ -0,0 +1,374 @@ +from pathlib import Path +import hashlib +import hmac +import json +import os +import secrets +import time + +import httpx + + +VERSION = "1.0.0" + +SHARE = Path("/share/homecore") +CONFIG = Path("/config") +SECRET_LOCAL = CONFIG / "updater-secret" +SECRET_SHARED = SHARE / "updater-secret" +REQUEST_FILE = SHARE / "update-request.json" +RESULT_FILE = SHARE / "update-result.json" +STATE_FILE = CONFIG / "updater-state.json" + +SUPERVISOR = "http://supervisor" + +SHARE.mkdir(parents=True, exist_ok=True) +CONFIG.mkdir(parents=True, exist_ok=True) + + +def atomic_write(path: Path, data: bytes): + tmp = path.with_suffix(path.suffix + ".tmp") + tmp.write_bytes(data) + os.replace(tmp, path) + + +def canonical(data): + return json.dumps( + data, + sort_keys=True, + separators=(",", ":"), + ).encode() + + +def ensure_secret(): + if SECRET_LOCAL.exists(): + secret = SECRET_LOCAL.read_text().strip() + else: + secret = secrets.token_hex(32) + SECRET_LOCAL.write_text(secret + "\n") + os.chmod(SECRET_LOCAL, 0o600) + + atomic_write(SECRET_SHARED, (secret + "\n").encode()) + try: + os.chmod(SECRET_SHARED, 0o600) + except Exception: + pass + + return secret.encode() + + +SECRET = ensure_secret() + + +def load_json(path): + try: + return json.loads(path.read_text()) + except Exception: + return {} + + +def save_json(path, data): + atomic_write( + path, + (json.dumps(data, indent=2, sort_keys=True) + "\n").encode(), + ) + + +def supervisor_headers(): + token = os.getenv("SUPERVISOR_TOKEN") + if not token: + raise RuntimeError("SUPERVISOR_TOKEN is not available") + return { + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + } + + +def supervisor_request(method, path, payload=None, timeout=120): + r = httpx.request( + method, + SUPERVISOR + path, + headers=supervisor_headers(), + json=payload, + timeout=timeout, + ) + r.raise_for_status() + + try: + body = r.json() + except Exception: + return {} + + if isinstance(body, dict): + if body.get("result") == "error": + raise RuntimeError(body.get("message") or "Supervisor error") + if "data" in body: + return body["data"] + + return body + + +def find_homecore_controller(): + data = supervisor_request("GET", "/store/addons", timeout=30) + apps = data.get("addons", []) if isinstance(data, dict) else (data or []) + + candidates = [] + for app in apps: + slug = str(app.get("slug", "")) + name = str(app.get("name", "")) + if ( + name == "HomeCore Controller" + or slug == "homecore_controller" + or slug.endswith("_homecore_controller") + ): + candidates.append(app) + + if not candidates: + raise RuntimeError( + "HomeCore Controller was not found in the Home Assistant App store" + ) + + installed = [x for x in candidates if x.get("installed")] + return (installed or candidates)[0] + + +def sign_result(body): + sig = hmac.new(SECRET, canonical(body), hashlib.sha256).hexdigest() + return {**body, "signature": sig} + + +def write_result(status, request, detail=""): + body = { + "schema": 1, + "request_id": str(request.get("request_id", "")), + "target_version": str(request.get("target_version", "")), + "status": status, + "detail": detail, + "updated_at": int(time.time()), + "updater_version": VERSION, + } + save_json(RESULT_FILE, sign_result(body)) + print( + "HOMECORE UPDATER:" + f" {status}" + f" request={body['request_id'] or '-'}" + f" target={body['target_version'] or '-'}" + f" {detail}", + flush=True, + ) + + +def verify_request(request): + if not isinstance(request, dict): + raise RuntimeError("Update request is not valid JSON") + + signature = str(request.get("signature", "")).strip() + if not signature: + raise RuntimeError("Update request signature is missing") + + body = {k: v for k, v in request.items() if k != "signature"} + expected = hmac.new( + SECRET, + canonical(body), + hashlib.sha256, + ).hexdigest() + + if not hmac.compare_digest(signature, expected): + raise RuntimeError("Update request signature is invalid") + + if int(body.get("schema", 0)) != 1: + raise RuntimeError("Unsupported update request schema") + + request_id = str(body.get("request_id", "")).strip() + target = str(body.get("target_version", "")).strip() + controller_id = str(body.get("controller_id", "")).strip() + + if not request_id or not target or not controller_id: + raise RuntimeError("Update request is incomplete") + + age = int(time.time()) - int(body.get("issued_at", 0) or 0) + if age < -120 or age > 900: + raise RuntimeError("Update request has expired") + + return body + + +def installed_version(app): + value = app.get("version") + return str(value or "") + + +def repository_version(app): + return str( + app.get("version_latest") + or app.get("version_available") + or app.get("version") + or "" + ) + + +def perform_update(request): + request_id = request["request_id"] + target = request["target_version"] + + write_result("preparing", request, "Manual update accepted by HomeCore Updater") + + supervisor_request("POST", "/store/reload", timeout=60) + time.sleep(3) + + app = find_homecore_controller() + slug = str(app.get("slug", "")).strip() + installed = installed_version(app) + latest = repository_version(app) + + print( + "HOMECORE UPDATER: repository check" + f" installed={installed or '-'}" + f" latest={latest or '-'}" + f" target={target}", + flush=True, + ) + + if not slug: + raise RuntimeError("HomeCore Controller slug could not be determined") + + if latest != target: + raise RuntimeError( + "Home Assistant repository currently offers " + + (latest or "no version") + + "; target is " + + target + ) + + if installed == target: + write_result( + "completed", + request, + "HomeCore Controller already has the requested version", + ) + return + + supervisor_request("GET", f"/store/addons/{slug}/availability", timeout=60) + + write_result( + "installing", + request, + "Backup requested; Supervisor is updating HomeCore Controller", + ) + + # This app is deliberately separate from HomeCore Controller, so this is + # not a self-update. Supervisor is allowed to update the other app. + supervisor_request( + "POST", + f"/store/addons/{slug}/update", + { + "backup": True, + "background": True, + }, + timeout=120, + ) + + deadline = time.time() + 600 + last_seen = "" + + while time.time() < deadline: + time.sleep(5) + + supervisor_request("POST", "/store/reload", timeout=60) + app = find_homecore_controller() + current = installed_version(app) + + if current != last_seen: + print( + "HOMECORE UPDATER: waiting for controller" + f" current={current or '-'} target={target}", + flush=True, + ) + last_seen = current + + if current == target: + write_result( + "completed", + request, + "HomeCore Controller update completed successfully", + ) + return + + raise RuntimeError( + "Timed out waiting for HomeCore Controller to reach " + target + ) + + +def main(): + print( + "HOMECORE UPDATER:" + f" version={VERSION}" + " supervisor_token=" + + ("available" if os.getenv("SUPERVISOR_TOKEN") else "missing"), + flush=True, + ) + print( + "HOMECORE UPDATER: waiting for signed requests in " + + str(REQUEST_FILE), + flush=True, + ) + + last_seen_request = "" + + while True: + try: + if REQUEST_FILE.exists(): + raw = load_json(REQUEST_FILE) + req_id = str(raw.get("request_id", "")) + + if req_id and req_id != last_seen_request: + request = verify_request(raw) + state = load_json(STATE_FILE) + + if str(state.get("request_id", "")) == req_id: + # Never execute an identical request twice. + last_seen_request = req_id + else: + save_json( + STATE_FILE, + { + "request_id": req_id, + "target_version": request["target_version"], + "status": "running", + "started_at": int(time.time()), + }, + ) + + try: + perform_update(request) + save_json( + STATE_FILE, + { + "request_id": req_id, + "target_version": request["target_version"], + "status": "completed", + "completed_at": int(time.time()), + }, + ) + except Exception as exc: + write_result("failed", request, str(exc)) + save_json( + STATE_FILE, + { + "request_id": req_id, + "target_version": request["target_version"], + "status": "failed", + "detail": str(exc), + "failed_at": int(time.time()), + }, + ) + + last_seen_request = req_id + + except Exception as exc: + print("HOMECORE UPDATER: watcher error:", exc, flush=True) + + time.sleep(2) + + +if __name__ == "__main__": + main() diff --git a/homecore_updater/config.yaml b/homecore_updater/config.yaml new file mode 100644 index 0000000..740eff3 --- /dev/null +++ b/homecore_updater/config.yaml @@ -0,0 +1,20 @@ +name: HomeCore Updater +version: "1.0.0" +slug: homecore_updater +description: HomeCore maintenance service for manual per-controller updates. +url: https://homecore.home-core.co.uk +init: false +arch: [aarch64] +hassio_api: true +hassio_role: admin +startup: services +stage: experimental +backup: hot +boot: auto +options: {} +schema: {} +map: + - type: addon_config + read_only: false + - type: share + read_only: false diff --git a/homecore_updater/requirements.txt b/homecore_updater/requirements.txt new file mode 100644 index 0000000..cee3237 --- /dev/null +++ b/homecore_updater/requirements.txt @@ -0,0 +1 @@ +httpx==0.28.1 diff --git a/homecore_updater/run.sh b/homecore_updater/run.sh new file mode 100644 index 0000000..87a9512 --- /dev/null +++ b/homecore_updater/run.sh @@ -0,0 +1,3 @@ +#!/usr/bin/with-contenv sh +set -eu +exec python /opt/homecore-updater/main.py