Add independent Home Assistant addon
This commit is contained in:
13
README.md
13
README.md
@@ -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
14
addon/Dockerfile
Normal 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
29
addon/config.yaml
Normal 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
31
addon/run.sh
Normal 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}"
|
||||||
|
|
||||||
@@ -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"}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
4
repository.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
name: SillyHome Future Add-ons
|
||||||
|
url: http://192.168.6.31:3000/Otto/sillyhome-future
|
||||||
|
maintainer: Otto
|
||||||
|
|
||||||
21
tests/test_addon_config.py
Normal file
21
tests/test_addon_config.py
Normal 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"
|
||||||
@@ -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"}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user