From 1336bc36fa44c8acb1730d62f9837ba645476bb3 Mon Sep 17 00:00:00 2001 From: Otto Date: Thu, 18 Jun 2026 12:23:24 +0200 Subject: [PATCH] Add independent Home Assistant addon --- README.md | 13 ++++++++++++- addon/Dockerfile | 14 ++++++++++++++ addon/config.yaml | 29 +++++++++++++++++++++++++++++ addon/run.sh | 31 +++++++++++++++++++++++++++++++ app/main.py | 3 +-- docs/V2_ARCHITECTURE.md | 6 ++++++ pyproject.toml | 4 ++-- repository.yaml | 4 ++++ tests/test_addon_config.py | 21 +++++++++++++++++++++ tests/test_future_api.py | 3 +-- 10 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 addon/Dockerfile create mode 100644 addon/config.yaml create mode 100644 addon/run.sh create mode 100644 repository.yaml create mode 100644 tests/test_addon_config.py diff --git a/README.md b/README.md index b1baf59..3c75c43 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,18 @@ pip install -e ".[dev]" uvicorn app.main:app --reload ``` +## Home Assistant Add-on + +Add this repository in Home Assistant: + +```text +http://192.168.6.31:3000/Otto/sillyhome-future +``` + +Then install **SillyHome Future**. The add-on is intentionally independent from +`sillyhome-next` and starts its own v2 event-core API on port `8099` behind +Ingress. + ## Test ```bash @@ -29,4 +41,3 @@ pytest ruff check . mypy app tests ``` - diff --git a/addon/Dockerfile b/addon/Dockerfile new file mode 100644 index 0000000..a8d5b09 --- /dev/null +++ b/addon/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.13-slim + +WORKDIR /app + +COPY pyproject.toml README.md ./ +COPY app ./app +RUN python -m pip install --no-cache-dir . + +COPY addon/run.sh /run.sh +RUN chmod 0755 /run.sh \ + && useradd --create-home --uid 10001 sillyhome + +ENTRYPOINT ["/run.sh"] + diff --git a/addon/config.yaml b/addon/config.yaml new file mode 100644 index 0000000..193ecc4 --- /dev/null +++ b/addon/config.yaml @@ -0,0 +1,29 @@ +name: SillyHome Future +version: "2.0.0-alpha.2" +slug: sillyhome_future +description: Event-first SillyHome v2 test controller +url: http://192.168.6.31:3000/Otto/sillyhome-future +arch: + - amd64 +startup: application +boot: manual +watchdog: http://[HOST]:[PORT:8099]/health +init: false +ingress: true +ingress_port: 8099 +panel_title: SillyHome Future +panel_icon: mdi:home-lightning-bolt +panel_admin: true +homeassistant_api: true +hassio_api: false +auth_api: false +map: + - type: addon_config + read_only: false +options: + store_path: /data/future_store + log_level: info +schema: + store_path: str + log_level: list(debug|info|warning|error) + diff --git a/addon/run.sh b/addon/run.sh new file mode 100644 index 0000000..9936232 --- /dev/null +++ b/addon/run.sh @@ -0,0 +1,31 @@ +#!/bin/sh +set -eu + +export PYTHONUNBUFFERED=1 + +STORE_PATH="/data/future_store" +LOG_LEVEL="info" +if [ -f /data/options.json ]; then + eval "$(python - <<'PY' +import json +import shlex + +with open("/data/options.json", encoding="utf-8") as stream: + options = json.load(stream) +for name, env_name in { + "store_path": "STORE_PATH", + "log_level": "LOG_LEVEL", +}.items(): + if name in options: + print(f"{env_name}={shlex.quote(str(options[name]))}") +PY +)" +fi + +export SILLYHOME_FUTURE_STORE="$STORE_PATH" +mkdir -p "$SILLYHOME_FUTURE_STORE" +chown -R sillyhome:sillyhome "$SILLYHOME_FUTURE_STORE" + +exec su -s /bin/sh sillyhome -c \ + "uvicorn app.main:app --host 0.0.0.0 --port 8099 --log-level ${LOG_LEVEL}" + diff --git a/app/main.py b/app/main.py index 0f020e4..81e0677 100644 --- a/app/main.py +++ b/app/main.py @@ -21,7 +21,7 @@ from app.core.stores import FutureStores app = FastAPI( title="SillyHome Future API", description="SillyHome v2 event-core side project.", - version="2.0.0-alpha.1", + version="2.0.0-alpha.2", ) stores = FutureStores(os.getenv("SILLYHOME_FUTURE_STORE", ".future_store")) event_core = EventCore(stores) @@ -110,4 +110,3 @@ def export_backup() -> BackupBundle: def restore_backup(bundle: BackupBundle) -> dict[str, str]: stores.restore_backup(bundle) return {"status": "restored"} - diff --git a/docs/V2_ARCHITECTURE.md b/docs/V2_ARCHITECTURE.md index 0fc460e..cef5048 100644 --- a/docs/V2_ARCHITECTURE.md +++ b/docs/V2_ARCHITECTURE.md @@ -27,3 +27,9 @@ state, but switching decisions must remain inspectable and testable. 6. Dashboard v2 7. Add-on hardening +## Add-on Contract + +The Home Assistant add-on is owned by this repository and does not copy code +from `sillyhome-next`. It starts the v2 API with `SILLYHOME_FUTURE_STORE` +pointing at `/data/future_store`, so runtime, learning and control stores are +kept inside the add-on data volume. diff --git a/pyproject.toml b/pyproject.toml index d744ba8..448df0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,13 +4,14 @@ build-backend = "setuptools.build_meta" [project] name = "sillyhome-future" -version = "2.0.0-alpha.1" +version = "2.0.0-alpha.2" description = "SillyHome v2 event-core prototype" requires-python = ">=3.11" dependencies = [ "fastapi>=0.115", "pydantic>=2.8", "uvicorn[standard]>=0.30", + "pyyaml>=6.0", ] [project.optional-dependencies] @@ -32,4 +33,3 @@ target-version = "py311" python_version = "3.11" strict = true plugins = [] - diff --git a/repository.yaml b/repository.yaml new file mode 100644 index 0000000..217d21c --- /dev/null +++ b/repository.yaml @@ -0,0 +1,4 @@ +name: SillyHome Future Add-ons +url: http://192.168.6.31:3000/Otto/sillyhome-future +maintainer: Otto + diff --git a/tests/test_addon_config.py b/tests/test_addon_config.py new file mode 100644 index 0000000..6bde48e --- /dev/null +++ b/tests/test_addon_config.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from pathlib import Path + +import yaml # type: ignore[import-untyped] + + +def test_addon_config_declares_future_addon() -> None: + config = yaml.safe_load(Path("addon/config.yaml").read_text(encoding="utf-8")) + + assert config["slug"] == "sillyhome_future" + assert config["version"] == "2.0.0-alpha.2" + assert config["ingress"] is True + assert config["ingress_port"] == 8099 + assert config["homeassistant_api"] is True + + +def test_repository_points_to_gitea_repo() -> None: + repository = yaml.safe_load(Path("repository.yaml").read_text(encoding="utf-8")) + + assert repository["url"] == "http://192.168.6.31:3000/Otto/sillyhome-future" diff --git a/tests/test_future_api.py b/tests/test_future_api.py index e183094..650404b 100644 --- a/tests/test_future_api.py +++ b/tests/test_future_api.py @@ -15,8 +15,7 @@ def test_health_and_backup_roundtrip(tmp_path, monkeypatch) -> None: # type: ig assert stores is not None assert health.status_code == 200 - assert health.json()["version"] == "2.0.0-alpha.1" + assert health.json()["version"] == "2.0.0-alpha.2" assert backup.status_code == 200 assert restore.status_code == 200 assert restore.json() == {"status": "restored"} -