1 Commits

Author SHA1 Message Date
1336bc36fa Add independent Home Assistant addon 2026-06-18 12:23:24 +02:00
10 changed files with 121 additions and 7 deletions

View File

@@ -22,6 +22,18 @@ pip install -e ".[dev]"
uvicorn app.main:app --reload 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 ## Test
```bash ```bash
@@ -29,4 +41,3 @@ pytest
ruff check . ruff check .
mypy app tests mypy app tests
``` ```

14
addon/Dockerfile Normal file
View File

@@ -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"]

29
addon/config.yaml Normal file
View File

@@ -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)

31
addon/run.sh Normal file
View File

@@ -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}"

View File

@@ -21,7 +21,7 @@ from app.core.stores import FutureStores
app = FastAPI( app = FastAPI(
title="SillyHome Future API", title="SillyHome Future API",
description="SillyHome v2 event-core side project.", 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")) stores = FutureStores(os.getenv("SILLYHOME_FUTURE_STORE", ".future_store"))
event_core = EventCore(stores) event_core = EventCore(stores)
@@ -110,4 +110,3 @@ def export_backup() -> BackupBundle:
def restore_backup(bundle: BackupBundle) -> dict[str, str]: def restore_backup(bundle: BackupBundle) -> dict[str, str]:
stores.restore_backup(bundle) stores.restore_backup(bundle)
return {"status": "restored"} return {"status": "restored"}

View File

@@ -27,3 +27,9 @@ state, but switching decisions must remain inspectable and testable.
6. Dashboard v2 6. Dashboard v2
7. Add-on hardening 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.

View File

@@ -4,13 +4,14 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "sillyhome-future" name = "sillyhome-future"
version = "2.0.0-alpha.1" version = "2.0.0-alpha.2"
description = "SillyHome v2 event-core prototype" description = "SillyHome v2 event-core prototype"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
"fastapi>=0.115", "fastapi>=0.115",
"pydantic>=2.8", "pydantic>=2.8",
"uvicorn[standard]>=0.30", "uvicorn[standard]>=0.30",
"pyyaml>=6.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]
@@ -32,4 +33,3 @@ target-version = "py311"
python_version = "3.11" python_version = "3.11"
strict = true strict = true
plugins = [] plugins = []

4
repository.yaml Normal file
View File

@@ -0,0 +1,4 @@
name: SillyHome Future Add-ons
url: http://192.168.6.31:3000/Otto/sillyhome-future
maintainer: Otto

View File

@@ -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"

View File

@@ -15,8 +15,7 @@ def test_health_and_backup_roundtrip(tmp_path, monkeypatch) -> None: # type: ig
assert stores is not None assert stores is not None
assert health.status_code == 200 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 backup.status_code == 200
assert restore.status_code == 200 assert restore.status_code == 200
assert restore.json() == {"status": "restored"} assert restore.json() == {"status": "restored"}