172 lines
34 KiB
Markdown
172 lines
34 KiB
Markdown
# AI-Managed Smart Home
|
||
|
||
Local-first, open-source smart home: Home Assistant + Zigbee + RuView (CSI presence)
|
||
+ Bermuda (BLE identity) + local LLM (Ollama) + Frigate (peephole face recognition)
|
||
+ Grocy (kitchen kiosk) + Nextcloud calendar sync + a Sway thin-client media station
|
||
+ a Sway touch panel + a camera-vision kitchen/fridge display + a voice/touch
|
||
identity-registration door panel + a quarter-daily LLM-generated digest.
|
||
|
||
See [`docs/project-plan.md`](docs/project-plan.md) for the full hardware list,
|
||
software stack, and phased implementation plan.
|
||
|
||
## Repo layout
|
||
|
||
```
|
||
docs/ Project plan and design notes
|
||
hosts/
|
||
container-host/ Docker Compose stack: HA, Mosquitto, Zigbee2MQTT,
|
||
Frigate, Grocy, Node-RED, monitoring, etc.
|
||
configs/ Per-service config files (mosquitto.conf, etc.)
|
||
llm-host/ Ollama + GPU host (separate physical machine)
|
||
thin-client/ Sway kiosk/media-station ISO (live-build) + thinclient-agent
|
||
audio-endpoint/ Headless Spotify Connect appliance for rooms with no thin
|
||
client — arm64 (rpi-image-gen) + amd64 (live-build) images
|
||
touch-panel/ Touch-driven Sway panel: full Spotify GUI, a dedicated
|
||
Home Assistant Chromium kiosk window, and a general web
|
||
browser, switched via an always-on touch dock or by HA/
|
||
the local LLM over MQTT
|
||
kitchen-display/ Single-purpose Sway kiosk for the fridge/pantry: one
|
||
Chromium window showing pantry-vision's unload/consume/
|
||
expired/edit frontend, camera capture via the browser
|
||
itself
|
||
door-panel/ Single-purpose Sway kiosk by the door/wardrobe:
|
||
identity's weather+clothing/who's-home/groceries-low
|
||
dashboard by default, registration on demand — this
|
||
host's twin relationship to kitchen-display, but the
|
||
mic is the point here, not an opt-in edge case
|
||
firmware/
|
||
ruview/ RuView ESP32-S3 CSI presence node configs
|
||
esphome-ble-proxy/ ESPHome configs for Bermuda BLE proxy nodes
|
||
esp32-s3-touch-lcd-1.85c/ ESPHome voice satellite + status display (round LCD,
|
||
media/cover-art priority over an idle weather/time/
|
||
date cycle, voice-state visualizer)
|
||
tools/ All build + setup scripts, driven by one
|
||
CoreSystemConfig.json: twinned container-host/LLM-host
|
||
ISO pair, every kiosk ISO, and a validator that refuses
|
||
a build on port collisions, placeholder tokens, or a
|
||
kiosk pointed at a disabled service
|
||
identity/ Person <-> BLE-identifier registry: multi-phone support,
|
||
anti-spoofing (allowlisted IRK-resolved/fixed-tag
|
||
entities only, never a raw MAC), voice/touch
|
||
registration, presence resolution, visit history
|
||
("who was home when, with whom"), nicknames, per-device
|
||
rights, and an admin panel (write API + frontend/
|
||
static serving, consumed by kitchen-display and
|
||
door-panel)
|
||
digest-engine/ Quarter-daily LLM digest: mail/message/news/financial
|
||
ingestion, LLM synthesis, digest-canvas SDK rendering
|
||
admin-canvas/ On-demand sys-admin-llm display surface for the thin
|
||
clients: stats/graphics/media, pushed on demand
|
||
rather than on a schedule (write API + admin-web
|
||
static serving)
|
||
pantry-vision/ Kitchen-display backend: a photo held up to the camera
|
||
-> an Ollama vision-model proposal -> human-confirmed
|
||
write into Grocy stock. All four stock movements are
|
||
camera-driven (unload / consume / bin what expired /
|
||
correct by hand), counted in individual units and folded
|
||
brand-free; also proxies Grocy's inventory
|
||
(soonest-expiring first) and recipes to the kiosk
|
||
frontend (write API + frontend/ static serving)
|
||
trash-calendar/ Reads Kennelbach's personal trash-collection ICS feed,
|
||
writes matching events onto the shared CalDAV calendar
|
||
(daily systemd timer, read-only against the feed)
|
||
transit/ Public transit: GET /departures (static GTFS lookup)
|
||
and GET /plan (OpenTripPlanner proxy, voice-usable via
|
||
HA Assist) — weekly GTFS refresh via systemd timer
|
||
chores/ Presence/calendar-driven household chore nudging + a
|
||
passive fairness tally + camera-verified trash-bin/
|
||
dishes/litter checks (systemd-timed oneshot, no
|
||
long-lived service, no LLM-picked assignment)
|
||
```
|
||
|
||
## Status
|
||
|
||
- [x] Project plan drafted
|
||
- [x] Container host setup script v1 (HA, Mosquitto, Zigbee2MQTT USB, Frigate, Grocy)
|
||
- [x] Node-RED + monitoring (Netdata) + dashboard (Homepage) + ntfy + Portainer added to compose stack
|
||
- [x] Backup (restic) setup — scripted, off by default until a backup target is picked (`ENABLE_BACKUPS`)
|
||
- [ ] Bermuda / ESPHome BLE proxy configs — `firmware/esphome-ble-proxy/` built (stock ESPHome `bluetooth_proxy` component), not yet flashed to real hardware, see that directory's README
|
||
- [ ] RuView node configs — `firmware/ruview/` documents the real upstream project ([github.com/ruvnet/ruview](https://github.com/ruvnet/ruview), integrated not forked) + a per-room provisioning wrapper + `automations.yaml.example` (sleep → dim lights, possible-distress → whole-household alert, concurrent elevated heart rate → colored lighting, bathroom occupancy → an external door indicator). **Every automation's entity_id is an unconfirmed placeholder**, and the concurrent-two-person-heart-rate rule rests on an unconfirmed assumption about RuView's multi-target vital-sign capability — see `firmware/ruview/README.md` §5–6 and `docs/project-plan.md` open decisions #32–33 before relying on any of it
|
||
- [ ] Frigate peephole camera config (real RTSP details)
|
||
- [ ] Grocy kiosk (Pi + touchscreen) setup
|
||
- [ ] LLM host (Ollama) setup script — `hosts/llm-host/` written: auto-detects a GPU vs CPU-only tier, runs Ollama as a pinned container (no `curl | sh` into root), pulls the Phase 3 text model + a vision model for `pantry-vision`/`chores`, and takes a reasoned position on Assist-vs-digest GPU contention (`MAX_LOADED_MODELS=1` — swap predictably rather than OOM mid-request). **Never run — no Debian machine, no GPU, no Ollama server**; the script is syntax-checked and its generated compose validated as YAML for both tiers, and that's the whole of the testing. The vision-model pick is still unmade (open decision #18), and the contention defaults are reasoned, not measured (#4) — see `hosts/llm-host/README.md`
|
||
- [x] CalDAV / Nextcloud calendar integration notes — `docs/caldav-integration.md`: the four independent clients (digest-engine read-only, chores' busy-check read-only, trash-calendar create-only, HA's own bridge), why they share one Nextcloud **app password** and what that costs (rotation touches three env files; **the read-only invariant is a code property, not a permission boundary** — an app password can't be scoped read-only or to one calendar), `trash-calendar`'s UID-prefix ownership invariant, and the two traps that cost real debugging time (unexpanded recurrence reporting a meeting on the day it was created; `CALDAV_VERIFY_TLS=false`). Nextcloud itself is pre-existing — nothing here deploys it
|
||
- [x] Sway thin-client ISO (live-build) + thinclient-agent — built, not yet boot-tested on real hardware; RDP replaced by wayvnc (resolved), remaining open items (mic-enabled rooms, exact hardware target, wayvnc password provisioning) in `docs/project-plan.md` §4
|
||
- [ ] Thin-client follow-ups in progress: fullscreen-aware now-playing widget (cover art + controls), minimal Firefox chrome + uBlock Origin/SponsorBlock, persistent audio-output selection, outbound RDP/VNC client (`rdp-vnc.json`), HA mobile-app browser remote control (text input + mouse buttons), capture-card ("receiver box") video source selection on a new `5:capture` workspace, idle-gallery weather/clock overlay (clock always, weather via a new `smarthome/weather/current` MQTT topic an HA automation has to publish) — built, not yet tried against real capture-card hardware or a real weather automation, see `hosts/thin-client/README.md`
|
||
- [x] Quarter-daily digest engine (mail/Signal/Telegram/Discord/WhatsApp, news, financial ingestion; LLM synthesis; digest-canvas SDK) — built and wired into `setup-container-host.sh` (`ENABLE_DIGEST_ENGINE`, off by default), not yet run against real credentials; household/calendar ingest (CalDAV/Grocy) still needs a real data source wired in, see `docs/project-plan.md` §4
|
||
- [x] admin-canvas + admin-web (sys-admin-llm on-demand display surface for the thin clients) — built and wired into `setup-container-host.sh` (`ENABLE_ADMIN_CANVAS`, off by default); the HA-side tool/rest_command wiring and the specific entities it surfaces (e.g. power-monitoring) are still undecided, see `docs/project-plan.md` §4
|
||
- [ ] ESP32-S3-Touch-LCD-1.85C-V2 voice satellite + status display (`firmware/esp32-s3-touch-lcd-1.85c/`) — ESPHome config written and passes `esphome config`, not yet flashed to real hardware; `media_player`/`weather` entity IDs still need to be chosen, see `docs/project-plan.md` §4
|
||
- [ ] Headless audio endpoint (`hosts/audio-endpoint/`) — per-room independent Spotify Connect appliance for rooms without a thin client, arm64 (Raspberry Pi + HiFiBerry Amp2, rpi-image-gen) and amd64 (mini PC + USB DAC/amp, live-build) build pipelines written, **neither built/flashed/booted on real hardware** — rpi-image-gen's exact config schema in particular is unverified, see `hosts/audio-endpoint/README.md`
|
||
- [ ] Sway touch panel (`hosts/touch-panel/`) — touch-driven Sway image: full Spotify GUI (Flathub), a dedicated Home Assistant Chromium kiosk window, a general web browser, an always-on touch dock for app switching, an on-screen keyboard (toggled manually, no auto-show), and `touchpanel-agent` (HA MQTT control, same LLM-mediated-through-HA security model as the thin client) — built, **no touch-panel hardware chosen and nothing booted on real metal**, see `hosts/touch-panel/README.md`
|
||
- [ ] Kitchen/fridge display + `pantry-vision` (`hosts/kitchen-display/`, `pantry-vision/`) — hold a grocery item up to the camera, an Ollama vision model proposes what it is and roughly how long it keeps, a human confirms (never auto-committed) before it's written into Grocy stock; the display then shows inventory sorted by soonest-to-expire, groceries running low, and Grocy's recipes. All four stock movements now run off the camera — **unload** (a scan loop, one confirm per item, with pack size and where to put it away), **consume** (asks which brand and how many), **list expired** (cleared by scanning what you're binning, booked out as spoiled), and **edit inventory** (the deliberately camera-free correction screen) — with two invariants: stock counts *individual units* (a twelve-pack of eggs is twelve) and folds *brand-free* via Grocy product groups (12 of brand X + 10 of brand Y = 22 eggs, expandable per brand). Built and wired into `setup-container-host.sh` (`ENABLE_PANTRY_VISION`, off by default), **nothing run against a real camera, vision model, or Grocy instance** — the Grocy API call shapes in particular are written from documentation only, see `pantry-vision/README.md` and `hosts/kitchen-display/README.md`
|
||
- [x] Where-is-it-actually, for multiple fridges — `docs/fridge-item-location.md`: separates "which appliance" (a software-only change: Grocy locations + a transfer action) from "which shelf" (a *hint* at best) and "exact position" (occlusion makes it unbuildable), and rules out interior cameras on power/condensation/−18 °C grounds — which is exactly the compartment the question starts from. **Built**: appliances as Grocy locations, `POST /transfer` with a "Move to…" picker on the edit screen, and the door-sensor→camera→hint path (`pantry-vision/doorway.py`, `POST /doorway-event` — an HA automation on a Zigbee contact sensor, answered 202 with the camera burst running off-thread; what it recognises is stored in its own SQLite file with a timestamp and a confidence and is **never** written to stock). **Not bought, not tested**: no door sensor, no doorway camera, and the assumption the camera half rests on — that a local vision model can identify an item in a moving hand at ~1.5 m — has never been checked (open decision #40). An appliance can be configured with a sensor and no camera, which is still the recommended way to start
|
||
- [x] Per-person **colour** and a settable **profile picture** in `identity` — eight colours assigned automatically at registration, avoiding any colour already worn by somebody with the same initial (an Anna and an Amir are two identical "A"s on a wall panel, and the colour is what makes that readable), then least-used overall; editable in the admin panel, backfilled oldest-first for existing people so nobody's colour reshuffles on restart. `color` + `initial` now ride on `/people`, `/presence` and every `/floorplan/presence` occupant, so no consumer derives an initial or invents a palette. New `POST /people/<id>/photo` sets a picture without a walk to the door panel — the registration capture was the only source before, which left a device-less household member unable to have a face at all. **The palette values sit on the 2-bits-per-channel lattice a colour Pebble renders natively**, so the colour on a watch is the colour in the panel, see below
|
||
- [x] **Rooms are one vocabulary, and devices declare theirs** — `docs/rooms-and-endpoints.md`: the room id is an HA `area_id` everywhere (`CoreSystemConfig.json` → `config-export.py` → the ISO builder → the agent → `suggested_area` in MQTT discovery), which is the same string `identity`'s floorplan rooms join on. Every kiosk and audio endpoint now carries `room`, `tools/validate-config.py` rejects anything that isn't already an area_id (helpfully slugifying it is how you end up with two rooms) and warns rather than errors when it's missing. **The honest limit: `suggested_area` is only honoured at first discovery** — move a device and you move it in HA once, by hand
|
||
- [x] **`workshop/`** — built: project notebook (`workshop.db`), a **never-pruned knowledge store** (`workshop-knowledge.db`: workflow instructions by activity, keyword facts, project learnings, and the household's **one** hardware inventory), `GET /context` to hand an assistant everything that applies at once, a **web inventory editor**, **Gitea repos with append-only history** (commit/push/branch yes; force-push/rebase/reset/filter-repo never, enforced by branch protection server-side, with a print-only scrub-request for the token-in-history case), a **health poller** for CheckMK + every OPNsense firewall, a **cameras** tab over go2rtc, and a **fleet-scripts admin surface**: one monitoring-agent script per kind of machine (Debian x86, arm64 Pi, the Docker host, the GPU host, plus slots for the ESP32s and network appliances that *can't* run a script and whose CheckMK-server-side config goes there instead), fetched by each endpoint's `fleet-bootstrap` timer. It's remote code execution by design, so the constraints are the design: **upload is a draft, publishing is a separate click**, the service never executes anything, scripts live in SQLite rather than on the writable share (a share credential must not be a whole-fleet root-execution credential), every version is kept, and the endpoint verifies the checksum, runs a version once, and **reports back pass or fail** — a script that was served is not a script that succeeded. Off by default (`ENABLE_WORKSHOP`), **nothing run against a real deployment** — the CheckMK API shape and Gitea's branch-protection payload are both written from documentation and have version-sensitive field names
|
||
- [x] **Multi-firewall + CheckMK, and a config that says so** — `opnsense` is a **list** now (each with its own name and key pair, keyed by name rather than by index), `checkmk` is a first-class block, and both feed the digest (`ingest/infra_health.py`, which reads the *poller* rather than polling, so the digest can say "critical since Tuesday" instead of quoting a six-hour-old snapshot) **and** the workshop's `infra_status` table. Three states — `ok`/`problem`/**`unreachable`** — because "I could not ask" is not "nothing is wrong". Validator rejects duplicate firewall names and half-configured credentials for both
|
||
- [x] **Photo web frontend (Immich)** alongside the SMB share — `ENABLE_PHOTOS_WEB`, search/albums/faces over the **same** directory, mounted **read-only**: two writers to one photo tree with different ideas of the layout is how a collection gets quietly reorganised. The share stays right for bulk copy and for keeping photos openable with no software at all
|
||
- [x] **Keycloak + FreeIPA config blocks** — `identity_provider` and `freeipa`, validated and exported, **declaration-only**: nothing implements SSO or the directory mirror yet, and the validator says so out loud rather than letting a filled-in block imply it works. **They are one decision, not two** — Keycloak federates FreeIPA rather than being its own user store, so the validator errors on either configured alone; a Keycloak without the directory behind it is a second parallel set of household accounts, which is the thing a directory exists to prevent. Groups follow **`USR_HA_<parameter>`** (`USR_HA_household`, `USR_HA_chore_exempt`, `USR_HA_admins`), warned-not-errored so an existing directory's convention isn't overridden by a config file. Two rules recorded before the work: the mirror is **one-way** (a directory serving real logins must not become a store of chore-reminder preferences), and a group may **grant** a chore exemption but never **remove** one somebody set by hand
|
||
- [ ] Workshop/office assistant — `docs/workshop-assistant.md`: **read the label, don't recognise the object** (a closed T480 and T490 are the same black rectangle; the identity is in the service tag and the PCB silkscreen, so the pipeline is OCR/barcode-first with the VLM only locating the label). Specs must be **quoted from a fetched document with its URL, never generated** — a hallucinated pinout destroys hardware — which also makes this the first component here that deliberately reaches the open internet (outbound-only, allowlisted, cached). Per-room scoping = the room selects the toolset, riding the `room` plumbing above. An SMB **workspace share** for everything it produces (reusing the existing Samba container — a second one would collide on 445, which the gallery already holds — with its own volume, own account, and read-write where the gallery is read-only). Display widgets collapse to **one `svg` window kind plus server-side renderers** (Graphviz for code-flow/data-structure, netlistsvg for schematics, KiCad export for board plans), because a megabyte of JS on a kiosk buys a picture the server could render once — **IEC/EU notation is a symbol-library decision**, so "can I supply my own symbols?" disqualifies a tool before output quality does. Purple/magenta holo theme is one CSS variable override over the existing `glow.css`, with the drawings left untinted on purpose. Analysis only, nothing built
|
||
- [x] **Now-playing visualiser + 3D floorplan** — built, in `render/` (shared, vendored, dependency-free, config from `?query=params`). `media-visualiser/`: circular spectrum, album-art palette, LRC lyrics — **two-tier by necessity**, since most endpoints have no local audio (a kitchen panel showing what the living room plays cannot analyse anything), so it is real FFT where audio is local and a tempo-driven ring elsewhere, **which says on screen that it is not an analysis**. Palette rejects near-greys/near-blacks before ranking and lifts each colour until it clears the background — the step whose absence makes art-coloured visualisers invisible on dark covers. Plain lyrics are shown but never auto-scrolled at a guessed rate. `floorplan-3d/`: the same `/floorplan/presence` payload as the Pebble app, extruded — **canvas 2D, not three.js**, a change from the plan made while building it: the scene is prisms on a plane, which an isometric projection with painter's sorting draws in ~200 lines, predictably on weak panels, with the frontend still at zero dependencies. Lit/dark rooms plus the dashed third state for rooms HA never reports on, photos-or-initials in the person's colour ring, and a visible shelf for people who are home but unlocatable. Geometry unit-tested headlessly; **never opened in a real browser**
|
||
- [ ] ~~Now-playing visualiser + 3D floorplan (design)~~ — `docs/endpoint-surfaces.md`: a circular CAVA-style ring behind every playing screen, coloured from the album art, lyrics under the cover when they exist; and the Pebble app's presence view in 3D on any endpoint. Two findings shape both: **most endpoints have no local audio** (a kitchen panel showing what the living room plays cannot analyse anything), so the visualiser is explicitly two-tier — real FFT where audio is local, tempo-driven "mood light" elsewhere, and the synthetic tier must never claim to be the real one; and the 3D plan is an **extrusion of the existing 2D polygons**, not a hand-authored model, so it can't go stale when a room is redrawn. Three.js is ~1MB and a deliberate break with the dependency-free SDK rule — make it once, explicitly. Analysis only, nothing built
|
||
- [x] **Pebble watchapp built** — `pebble-presence/`: the floorplan and who's in each room, plus Up/Down room cycling into a plain-text list (with "Somewhere in the house" as the last entry in the cycle — the people it can't place are exactly who you picked the watch up to find). Lit/dark/dotted room states, positions when fused, hollow rings for radar targets nobody can name. The last plan is persisted and drawn **with its age** on launch, because the app's JS only lives while it's open so every launch starts with a fetch. **The wire format is round-trip tested** between the JS writer and the C reader against a stub SDK — two implementations of one format in two languages that never share a process, where drift decodes as plausible garbage rather than an error; it already caught one real disagreement. Unverified: the Round 2's platform identifier and its round-screen detection (nothing hardcodes a resolution, so both are one-liners once the SDK names them), and it has never run on a watch
|
||
- [ ] ~~Pebble presence watchapp (design)~~ — `docs/pebble-presence-watchface.md`: the floorplan drawn as quantised polygons (~200–250 bytes for a whole floor, against a ~2 KB AppMessage budget), occupants as initial-on-colour rather than photos (a face gets ~20×20px in 64 colours — four skin-toned blobs, and three of them exceed the message budget the entire floor fits in). Targets the **Pebble Round 2** that's actually owned: the plan is inscribed in the circle (`w = D·a/√(a²+1)`, so ~170×170 for a squarish plan at a 240px usable diameter — comparable to a Time 2 after chrome), the projection change is one line of phone-side JS, and the only real loss is corner furniture like a status footer. Occupancy reads as **dark room / lit room** in lightness rather than hue, since colour is already spoken for by *who* — with a third dashed state for rooms HA never reports on, because drawing "no data" as "empty" is a quiet lie. **Button-cycling through rooms forces a watchapp, not a watchface** — Pebble gives watchfaces no button events and restricts touch to apps — and the watchapp is the decision: Up/Down cycles rooms into a plain-text list of who is in each (plus a final "somewhere in the house" entry for people who are home but unlocatable, who are exactly who you picked the watch up to find). The cost is that it's a menu entry rather than your default screen, and its data only lives while it's open; a watchface variant later is a second `main()`, not a second project. Analysis only, nothing built. The finding that actually decides it is upstream: room-level presence has never been measured in this house, and the admin panel's floorplan tab with **Live** ticked tests it today for free
|
||
- [ ] "Put the groceries away" chore — `chores`' new `groceries_out_of_place` watch point: `litter`'s twin (same "whoever the camera just saw" attribution, same exemption-proof, same unassignable) with a **30-minute** neglect fuse instead of four hours, because the failure is spoiled food rather than untidiness. Its prompt names what to *ignore* (fruit bowls, bread bins, coffee, cookware) as well as what to look for — the general question gets a YES for any normal kitchen, and a false chore every two hours is how a household learns to mute the channel (open decision #41). Knows food is out, never what the food is; nothing here touches Grocy. Untested against a real camera or model, like every other watch point
|
||
- [ ] `identity` + door panel (`identity/`, `hosts/door-panel/`) — the person <-> BLE-identifier registry: "register me as `<name>`" by voice or touchscreen, multi-phone support (multiple identifiers per person), anti-spoofing (only allowlisted IRK-resolved/fixed-tag entities are ever accepted as candidates, never a raw MAC), device-less people (a "no device" flag plus a hand-operated Home/Away toggle — the concrete case: a grandmother without a smartphone), and an anonymous "Guest" path. Backs `hosts/door-panel/`'s weather+clothing/who's-home/groceries-running-low dashboard and `hosts/kitchen-display/`'s "Show registration" screen — built and wired into `setup-container-host.sh` (`ENABLE_IDENTITY`, off by default), **nothing run against a real HA instance, real Private BLE Device entities, or a real voice pipeline** — `TRUSTED_ENTITY_PREFIXES` above all needs checking against Developer Tools -> States, see `identity/README.md` and `hosts/door-panel/README.md`
|
||
- [ ] `identity` also corroborates presence from Frigate face recognition (Phase 20, Tapo pan/tilt cameras) — an OR-ed-in second signal only, **never** a registration signal; and owns the per-person chore-system settings (`chore_exempt`, `chore_reminder_style`, plus chore assignment) consumed by `chores/`, see `identity/README.md`
|
||
- [ ] `identity`'s admin panel (`identity/frontend/admin.html`, Phase 6b) — managing people/guests: edit every field, **nicknames** (an input alias only — `/resolve` accepts them, but the assistant always speaks the real `speak_name`), **visit history** sampled from `/presence` plus a derived "who was home with whom" view, **"select all that last visited before `<date>`"** pruning (the filter selects, a human confirms the exact list, the filter is never re-run at delete time), **per-device rights** for self-entry via a smart lock (`identity` only ever *answers* `GET /device-access` — HA asks and HA acts, deny is the default), chore assignment, a **floorplan editor** (draw levels and rooms as polygons, map each to an HA area, and watch occupied rooms light up — resolves the long-deferred open decision #22), and **opt-in arrival push notifications** ("tell me when someone gets home", via the self-hosted ntfy this stack already runs — `identity` itself never touches the WAN; ntfy stays LAN-only and remote delivery rides a WireGuard split tunnel — see `docs/network-integration.md` §2.2 for why a DMZ/port-forward was weighed and rejected). Deliberately **not** a kiosk page and not linked from any wall panel. Covered by API-level tests; **never opened in a real browser**, and `DEPARTURE_GRACE_SECONDS` is an untuned guess — see `identity/README.md`
|
||
- [ ] `trash-calendar` + `transit` (Phase 19, Kennelbach AT trash pickup + Vorarlberg public transit) — built and wired into `setup-container-host.sh` (`ENABLE_TRASH_CALENDAR`/`ENABLE_TRANSIT`/`ENABLE_TRIP_PLANNING`, all off by default), **nothing run against a live ICS feed, a live GTFS feed, or a real OpenTripPlanner instance** — trip planning also needs a manually-built OTP graph this repo does not build for you, see `trash-calendar/README.md` and `transit/README.md`'s "Route planning scope"
|
||
- [ ] `chores` (Phase 20) — presence/calendar-driven household chore nudging: "I don't care who does it, as long as it gets done" — prefers whoever's been assigned a chore in `identity`'s admin panel but falls through to whoever's actually home rather than waiting (`CHORE_ASSIGNMENT_STRICT` flips that), redirects to someone else if a chore goes neglected, keeps a passive fairness tally that never feeds back into who gets nudged, and camera-checks trash bins/dishes/litter/groceries-left-out via Frigate + an Ollama vision model. **Litter and groceries-left-out remain the exception to everything** — both ignore chore-exemption and assignment, because cleaning up what you left out was never a task anyone could be assigned; groceries additionally get a 30-minute fuse instead of four hours, since that failure spoils food rather than merely looking untidy. Built and wired into `setup-container-host.sh` (`ENABLE_CHORES`, off by default, every-2-hours systemd timer), **no Tapo camera hardware chosen and nothing run against real hardware**, see `chores/README.md`
|
||
- [ ] Music Assistant (optional, additive multi-room audio) — wired into `setup-container-host.sh` (`ENABLE_MUSIC_ASSISTANT`, off by default), **its default port is an unverified guess that collides with `PANTRY_VISION_PORT`** if both are enabled together, see `docs/project-plan.md` open decision #31
|
||
- [ ] `docs/network-integration.md` (OPNsense VLAN segmentation, the WireGuard split tunnel that carries arrival notifications, and why nothing here — ntfy included — gets port-forwarded to the WAN) — written, not run against a real OPNsense instance
|
||
- [ ] `tools/` + `CoreSystemConfig.json` — every build and setup script in one place, reading one config. The container host and LLM host build as a **twinned pair**: you set two last octets and the container host's `OLLAMA_HOST` is *derived* from the LLM host's, so the two ISOs cannot be built disagreeing about where the other one is; every kiosk's service URLs derive from the container host's address the same way. `build-all.sh` builds the set, `validate-config.py` refuses a build on duplicate ports (the `music_assistant`/`pantry_vision` 8095 clash, open decision #31), placeholder or padded tokens, duplicate hostnames, or a kiosk pointed at a disabled service. All secrets are burnt into the images so installs are unattended — **which makes every ISO a credential**; the filled-in config and `iso-out/` are gitignored. **No ISO has ever been built with this** (`lb build` needs live-build, root and a long fetch) — what is tested is config validation/derivation and every generated artifact, with `lb` stubbed. See `tools/README.md`
|
||
|
||
## Quick start
|
||
|
||
Everything is built from **one** config file — `CoreSystemConfig.json` — so an address
|
||
or token can only ever be wrong in one place:
|
||
|
||
```bash
|
||
cp CoreSystemConfig.json.template CoreSystemConfig.json
|
||
$EDITOR CoreSystemConfig.json # subnet, two host octets, tokens, your kiosks
|
||
tools/validate-config.py # catches typos in seconds, not after a 40-min build
|
||
sudo -E tools/build-all.sh # every ISO, all agreeing with each other
|
||
```
|
||
|
||
The container host and LLM host come out as a **twinned pair**: you give each a last
|
||
octet, and the container host's `OLLAMA_HOST` is *derived* from the LLM host's — change
|
||
one and the other follows on the next build, with nothing to keep in sync by hand. Every
|
||
kiosk's service URLs derive from the container host's address the same way. See
|
||
[`tools/README.md`](tools/README.md).
|
||
|
||
Built ISOs contain every secret in the config, by design (nothing to configure
|
||
post-install) — which makes each one a credential. `.gitignore` covers both the
|
||
filled-in config and `iso-out/`.
|
||
|
||
To set up a host by hand instead of from an ISO:
|
||
|
||
```bash
|
||
sudo -E tools/setup-container-host.sh
|
||
```
|
||
|
||
Edit the variables at the top of the script first (timezone, Zigbee USB device
|
||
path, Mealie/hardware-accel toggles, and whether to enable Node-RED/Netdata/
|
||
Homepage/ntfy/Portainer and restic backups — all but backups are on by
|
||
default). See `docs/project-plan.md` for the full phased rollout order — don't
|
||
skip straight to Phase 4+ automations before the Phase 2 reactive baseline
|
||
(presence -> light, no LLM in the loop) is working.
|
||
|
||
The script brings up everything that runs on this one Debian host: Home
|
||
Assistant, Mosquitto, Zigbee2MQTT, Node-RED, Frigate, Grocy, Netdata, a
|
||
Homepage dashboard, ntfy, and Portainer, plus an optional scheduled restic
|
||
backup timer and an optional quarter-daily digest engine (`ENABLE_DIGEST_ENGINE`,
|
||
off by default — needs `digest-engine/` checked out on the host and its `.env`
|
||
filled in first, see `digest-engine/README.md`). What it can't do for you,
|
||
because they need separate hardware, credentials, or physical setup: pairing
|
||
Zigbee sensors, flashing RuView/ESPHome/Bermuda BLE proxy boards, pointing
|
||
Frigate at a real camera RTSP URL, the Grocy kiosk touchscreen, the separate
|
||
LLM/GPU host, wiring up the Nextcloud CalDAV integration, building/flashing
|
||
the thin-client ISO (`hosts/thin-client/`), and provisioning real credentials
|
||
for the digest engine's mail/message/news/financial sources — see the Status
|
||
checklist above and `docs/project-plan.md` for those.
|