SmartestHome/hosts/door-panel
Amir Alexander Abdelbaki ea82ee70ad Add tools/ and CoreSystemConfig.json — one source of truth for every build
Installation was six scripts each carrying its own copy of the container host's
IP, three that had to agree on IDENTITY_TOKEN, and every service URL typed by
hand with a port in it. Any one could be wrong, and the symptom was always the
same and always late: an image that boots fine and then can't reach something,
found after a 40-minute build and a reboot.

Two properties fix that class of bug:

- Nothing is written twice. No script in tools/ contains an IP, port or token.
- Anything derivable is derived. You give the subnet prefix once and one last
  octet per host; every address and service URL is computed from those.

THE TWINNED PAIR. container_host.ip_last_octet 12 and llm_host 13 mean the
container host's OLLAMA_HOST *is* http://<prefix>.13:11434 — computed in the
same build, not typed into two files and kept in sync. Move the LLM host to .21
and the container host's Ollama URL follows; change the subnet and both halves
move along with every kiosk's URLs. Neither image can be built pointing at an
address the other isn't using. Both carry the same SMARTHOME_PAIR_ID (a hash of
the config's meaning, not its bytes) so two USB sticks can be checked against
each other later.

validate-config.py runs before every build and refuses to start on an error, so
a mistake costs seconds not an hour. It catches duplicate ports (including the
music_assistant/pantry_vision 8095 clash that Compose can't see because MA runs
network_mode:host — open decision #31), both hosts on one address, duplicate
hostnames across kiosks and audio endpoints, placeholder tokens (checked before
the length check, so padding "changeme" to 32 chars doesn't pass), a private key
pasted where the public one goes, and a kiosk pointed at a disabled service.

build-all.sh is the normal entry point — the images are a set that has to agree
with itself, so building one is the exception. It builds the core pair, every
kiosk, and every audio endpoint including both architectures (amd64 live-build
ISO and arm64 rpi-image-gen img are different toolchains, not one image).

The two new host ISOs install unattended with everything burnt in, including
service env files generated from derived values — which permanently removes the
class of bug that had chores.env shipping IDENTITY_URL=http://127.0.0.1:8097.
setup-container-host.sh and setup-llm-host.sh now read every config value as
${VAR:-default} so the images configure them without editing.

That also makes every ISO a credential: Wi-Fi PSK, tokens, MQTT and HA
credentials are readable by anyone holding the stick. .gitignore covers the
filled-in CoreSystemConfig.json and build-output/.

Tested: 43 config validation/derivation checks and 44 builder checks against the
real code paths with only `lb` stubbed — every generated env file, preseed,
network config, first-boot unit and build stamp is verified, including that a
port collision refuses the build before writing anything. No ISO has been built;
`lb build` needs live-build, root and a long fetch. tools/README.md says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:22:25 +02:00
..
agent Add identity registration system and Phase 18 door panel 2026-07-30 13:40:04 +02:00
configs Add identity registration system and Phase 18 door panel 2026-07-30 13:40:04 +02:00
live-build/config Add identity registration system and Phase 18 door panel 2026-07-30 13:40:04 +02:00
README.md Add tools/ and CoreSystemConfig.json — one source of truth for every build 2026-07-31 13:22:25 +02:00

README.md

Sway door panel

Phase 18 of docs/project-plan.md. Builds a Debian 12 live ISO for a wall panel by the door/wardrobe: weather and what to wear on the way out, who's home, groceries running low, and — the household's actual registration point — "register me as <name>" by voice.

Structurally this is ../kitchen-display/'s twin, not a new shape: one Sway workspace, one Chromium kiosk window, a thin agent that only ever switches which page is showing. The differences are what it shows by default (identity's dashboard.html instead of pantry-vision's scan/inventory/recipes) and that its microphone is expected to actually be used, not an edge-case opt-in.

The actual logic does not live in this directory../../identity/ owns person/presence data and the registration flow; ../../pantry-vision/ owns the "running low" data. This host is deliberately thin: Sway, one kiosk window, and door-panel-agent, which only controls which screen is showing — the same split every other kiosk host in this project already uses.

Compositor Sway, one workspace, one Chromium kiosk window
Autologin greetd, straight into /usr/local/bin/kiosk-session
Touch input Native Wayland wl_touch, same assumption as ../touch-panel//../kitchen-display/
Default screen identity's dashboard.html — weather + clothing suggestion, who's home, groceries running low
Registration identity's register.html — voice ("register me as <name>") or the touchscreen form
Voice wyoming-satellite + openWakeWord — opt-in like every other host's mic, but this device's actual purpose, so the build script defaults it to on
Remote control (HA/LLM) door-panel-agentShow home / Show registration buttons only
Remote control (human) SSH only — no wayvnc, same scope decision as ../touch-panel/

Hardware

No specific unit has been chosen. What's assumed: a touchscreen (native wl_touch, same caveat as every other touch host in this project) mounted by the door or wardrobe, with a microphone and speaker for voice registration/Assist playback. Unlike ../kitchen-display/, no camera is strictly required — the photo captured during registration (identity's profile-picture feature) is a nice- to-have audit artifact, not load-bearing, since identity resolution here is BLE/IRK- based, not camera-based. See docs/project-plan.md §1.16 for the (unverified, non-specific) hardware line item.

Before you build

Deploy identity/identity-web first (ENABLE_IDENTITY in tools/setup-container-host.sh) — this image builds and boots fine without it, but the dashboard will show connection errors until it exists. pantry-vision is optional (only "Running low" needs it). Then edit the # CONFIGURATION block at the top of tools/build-door-panel-iso.sh:

Variable What to put in it
MQTT_BROKER_HOST LAN IP of the container host running Mosquitto
IDENTITY_WEB_URL / IDENTITY_URL / IDENTITY_TOKEN identity's static-serving and API URLs; token must match identity/identity.env's own
PANTRY_VISION_URL / PANTRY_VISION_TOKEN Optional — only "Running low" needs these; must match pantry-vision/pantry-vision.env's own token
ENABLE_VOICE_SATELLITE Defaults to true here (unlike every other host's identical flag) — this device's whole point is voice registration, but it still needs a real mic on the specific unit before flashing it
KIOSK_USERNAME / IMAGE_HOSTNAME / DOOR_PANEL_NAME Per-device identity
SSH_AUTHORIZED_KEY Optional — password auth is disabled and there is no wayvnc

Build

sudo -E tools/build-door-panel-iso.sh

Same directory-split convention as every other host: configs/ and agent/ are human-edited and git-tracked; live-build/config/includes.chroot/ is generated, gitignored, never hand-edited.

Why one workspace, two kiosk destinations, not three

../touch-panel/ juggles three real apps (Spotify, Home Assistant, a browser) and needs a persistent touch dock to switch between them. This device only ever shows one of two pages — the everyday dashboard, or the registration form — and switches between them rarely (on an MQTT command or a voice trigger), so a dock would be overhead with nothing to navigate day-to-day. configs/sway/home-kiosk and configs/sway/identity-kiosk are separate scripts on separate Chromium profiles, same shape as ../kitchen-display/'s pantry-kiosk/identity-kiosk pair — see that host's README for why both scripts kill any Chromium instance before launching (this device also has only one workspace).

Home Assistant entities

door-panel-agent publishes two buttons on connect: Show home, Show registration — each kills and relaunches the kiosk Chromium window at the corresponding page. That's the entire MQTT surface. Reading presence/weather/ groceries, or registering a person, is a separate path — Home Assistant/the LLM calling identity's or pantry-vision's own published APIs directly, not through this agent. Voice registration in particular never touches this agent at all — see ../../identity/README.md's worked HA intent-script example.

Security boundary

Same shape and reasoning as every other host's agent in this project: door_panel_agent/mqtt_discovery.py is the entire inbound MQTT control surface of this machine — LLM tool call → HA service call → MQTT → this agent, no HTTP listener, no websocket, no exposed Sway IPC socket, no VNC. identity and pantry-vision are separate, deliberately published services with their own bearer-token boundaries — see their own READMEs' "A real network listener, unlike admin-canvas" sections for why that trust model is different on purpose.

Manual verification still outstanding

None of this has been run on hardware. In rough order — on top of everything already flagged as unverified in ../../identity/README.md (TRUSTED_ENTITY_PREFIXES above all):

  1. The ISO builds at all.
  2. greetd lands in Sway with no login prompt.
  3. Touch input as native wl_touch — same open item as every other touch host.
  4. door-panel-agent connects to Mosquitto and the device appears in HA.
  5. Register via the touchscreen form (Show registration) first, before trying voice — this exercises the whole identity path without needing HA's intent-script wiring set up. See identity/README.md's worked example for that next step.
  6. Voice registration end-to-end: wake word → "register me as <name>" → a spoken confirmation. This is the number of moving pieces (wyoming-satellite, HA's Assist pipeline, the custom sentence/intent script, identity's /register) that has the most to go wrong and the least individual testing so far.
  7. The dashboard's clothing suggestion (identity/frontend/dashboard.js's clothingSuggestion()) is a plain lookup table, untuned against real weather payloads or real household clothing preferences — reasonable starting thresholds, not measured.
  8. Switching between home-kiosk and identity-kiosk actually replaces the window rather than leaving two Chromium instances up — same untested assumption as ../kitchen-display/'s identical pair.
  9. Idle-blank timeout (20 minutes, configs/sway/config) and touch-resume — untested.