From e8d09f40f66e0e6dae4853e9eec3554c669b1c40 Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Sun, 10 May 2026 14:55:55 +0200 Subject: [PATCH 01/24] feat(home-assistant): enable ZHA for ZBT-2 Zigbee dongle Adds the `zha` extra component so Home Assistant can drive the Nabu Casa Connect ZBT-2 radio, and puts the `hass` service user in `dialout` so it can open `/dev/serial/by-id/usb-Nabu_Casa_..._ZBT-2_*`. Pairing is then handled through the standard ZHA wizard in the HA UI. Co-Authored-By: Claude Opus 4.7 --- modules/environments/home-assistant/default.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/environments/home-assistant/default.nix b/modules/environments/home-assistant/default.nix index e3e8fe4..fc77dba 100644 --- a/modules/environments/home-assistant/default.nix +++ b/modules/environments/home-assistant/default.nix @@ -24,6 +24,7 @@ in extraComponents = [ "matter" "mobile_app" + "zha" ]; }; services.home-assistant.config = { @@ -32,6 +33,9 @@ in mobile_app = {}; }; + # Allow the `hass` service user to open the ZBT-2 USB-CDC serial endpoint. + users.users.hass.extraGroups = [ "dialout" ]; + my.homepage.services = [ { group = "Services"; From dbeda276e14117219b637ddb0adf927b043a0088 Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Sun, 10 May 2026 15:29:21 +0200 Subject: [PATCH 02/24] docs(home-assistant): design spec for ZBT-2 Thread + OTBR setup Captures the architecture, operator workflow, and verification for running the Connect ZBT-2 as an OpenThread Border Router on jupiter (via nixos-unstable's services.openthread-border-router module), with HA's otbr + thread integrations driving the Thread network and the existing matter-server consuming credentials for Matter-over-Thread device commissioning. Supersedes the ZHA-direction commit on this branch (e8d09f4), which will be reverted at the start of implementation. Co-Authored-By: Claude Opus 4.7 --- .../2026-05-10-zbt2-thread-otbr-design.md | 241 ++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-10-zbt2-thread-otbr-design.md diff --git a/docs/superpowers/specs/2026-05-10-zbt2-thread-otbr-design.md b/docs/superpowers/specs/2026-05-10-zbt2-thread-otbr-design.md new file mode 100644 index 0000000..83cd144 --- /dev/null +++ b/docs/superpowers/specs/2026-05-10-zbt2-thread-otbr-design.md @@ -0,0 +1,241 @@ +# ZBT-2 as a Thread Border Router for Home Assistant on `jupiter` + +**Date:** 2026-05-10 +**Branch:** `feature/ha-zbt-2-thread` +**Status:** Design — pending implementation plan + +## Context + +Home Assistant on `jupiter` already runs natively (`services.home-assistant`) with the Matter integration and `services.matter-server` enabled, but has no Zigbee or Thread radio. The user has acquired a **Home Assistant Connect ZBT-2** (Nabu Casa's Silicon Labs EFR32MG24‑based USB Zigbee/Thread radio). + +The user wants the dongle running as an **OpenThread Border Router (OTBR)** — Thread only, not Zigbee — so Matter-over-Thread devices can be onboarded through the existing HA Matter integration. + +A previous iteration of this work shipped `zha` enablement on the same branch (commit `e8d09f4`). That commit will be reverted as part of implementation; this design supersedes it. + +## Goals + +- Bring up `otbr-agent` on jupiter against the ZBT-2. +- Have Home Assistant auto-discover the OTBR via mDNS and use its REST API to manage the Thread network. +- Have `services.matter-server` (already enabled) consume Thread credentials from HA so Matter-over-Thread devices commission through the ZBT-2. +- One-time, manual firmware flash from Zigbee NCP to OpenThread RCP via `universal-silabs-flasher` (option B from brainstorming — no HA-driven update flow). + +## Non-goals + +- **Multipan / multiprotocol** (Zigbee + Thread on one radio). Out of scope; the dongle will be Thread-only. +- **Falling back to ZHA** if Thread misbehaves. Thread-only by choice; if it fails the response is to debug, not to dual-stack. +- **HA-UI-driven firmware updates.** The HAOS "Silicon Labs Multiprotocol" add-on workflow doesn't translate to native NixOS without faking a supervisor; the user explicitly accepted CLI-only flashing. +- **Thread network credential backups.** HA owns the dataset; standard HA backup hygiene (separate concern) covers it. + +## Architecture + +``` + ┌────────────────────────── jupiter (NixOS) ──────────────────────────┐ + │ │ + ZBT-2 USB ──►│ /dev/serial/by-id/usb-Nabu_Casa_..._ZBT-2_-... │ + │ │ │ + │ │ spinel+hdlc+uart, 115200 baud │ + │ ▼ │ + │ ┌───────────────┐ REST :8081 (loopback) ┌──────────────────┐ │ + │ │ otbr-agent │ ◄─────────────────────►│ home-assistant │ │ + │ │ (systemd) │ │ + matter-server │ │ + │ │ wpan0 ───────┼── advertises via ─┐ │ extraComponents:│ │ + │ └───────────────┘ avahi (_meshcop) │ │ matter, │ │ + │ ▼ │ mobile_app, │ │ + │ enp3s0 (LAN — backbone) │ otbr, thread │ │ + │ └──────────────────┘ │ + └────────────────────────────────────┬──────────────────────────────────┘ + │ + home LAN ◄─┘ + (Matter-over-Thread devices join here) +``` + +### Components + +1. **The radio.** ZBT-2, USB-attached, running OpenThread RCP firmware after a one-time flash. +2. **`otbr-agent`** (systemd). Managed by the unstable `services.openthread-border-router` NixOS module imported via `inputs.nixpkgs-unstable`. Owns `wpan0`, talks Spinel to the dongle, exposes the OTBR REST API on `127.0.0.1:8081`, advertises `_meshcop._udp` over `enp3s0` via avahi. +3. **Home Assistant** (already running). Gains the `otbr` and `thread` extra components. Discovers OTBR via mDNS, drives the REST API, supplies Thread operational datasets to `matter-server` during Matter commissioning. + +### Data flows + +- **OTBR ↔ ZBT-2:** Spinel-over-HDLC over UART. Built automatically by the module from `radio.device` as `spinel+hdlc+uart://?uart-baudrate=115200`. +- **HA ↔ OTBR:** mDNS discovery (`_meshcop._udp`) → REST calls to `127.0.0.1:8081` for network management. +- **Matter commissioning:** HA scans QR → `matter-server` does BLE commissioning → asks HA for Thread dataset → HA fetches from OTBR → ships to device → device joins Thread mesh through the ZBT-2. + +HA never opens the serial port directly; `matter-server` never talks to OTBR directly. HA brokers between them — that's why all four extra components are needed. + +## NixOS-side changes + +All changes live in **`modules/environments/home-assistant/default.nix`**. No host-level changes in `machines/jupiter/` (the existing profile activation handles that), no flake-level changes (the existing `_module.args.self = self;` wiring is sufficient). + +### Edited module sketch + +```nix +{ config, lib, pkgs, self, ... }: +let + cfg = config.my.profiles.home-assistant; + hostName = config.networking.hostName; +in +{ + imports = [ + # OTBR module isn't in 25.11 yet; use unstable's directly. Package + # comes from the existing `unstable` overlay. + "${self.inputs.nixpkgs-unstable}/nixos/modules/services/home-automation/openthread-border-router.nix" + ]; + + options.my.profiles.home-assistant.enable = lib.mkEnableOption "Home Automation"; + + config = lib.mkIf cfg.enable { + services.matter-server.enable = true; + + services.home-assistant = { + enable = true; + openFirewall = true; + extraComponents = [ + "matter" + "mobile_app" + "otbr" + "thread" + ]; + }; + + services.home-assistant.config = { + name = "Home - Rechberg"; + unit_system = "metric"; + mobile_app = { }; + }; + + services.openthread-border-router = { + enable = true; + package = pkgs.unstable.openthread-border-router; + openFirewall = true; + backboneInterfaces = [ "enp3s0" ]; # verify with `ip link` post-deploy + radio.device = "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_-..."; + # web.enable left default (off) — HA UI is the management surface + }; + + my.homepage.services = [ + { + group = "Services"; + name = "Home Assistant"; + description = "Home automation"; + href = "http://${hostName}:8123"; + icon = "si-homeassistant"; + } + ]; + }; +} +``` + +### Reverts of the prior ZHA commit + +Drop both lines from commit `e8d09f4`: + +- `"zha"` from `extraComponents` (replaced by `"otbr"` + `"thread"`). +- `users.users.hass.extraGroups = [ "dialout" ];` — `otbr-agent` runs as root and owns the device directly; HA never opens the serial port itself. + +Done by `git revert e8d09f4` at the start of implementation, before applying the new diff. + +### Decisions captured + +- **No `universal-silabs-flasher` in `environment.systemPackages`.** Flashing is a once-or-twice-a-year operation; `nix shell nixpkgs#python313Packages.universal-silabs-flasher` is sufficient when needed and avoids a perma-dep on a tool that's idle most of the time. +- **No firmware pinning in the flake.** Consistent with option B (CLI-only manual flashing). The user fetches the `.gbl` from at update time. +- **`backboneInterfaces = [ "enp3s0" ]`** as a starting value (per `machines/jupiter/hardware-configuration.nix:64`). To be verified against `ip link` after first deploy; correctable in a follow-up commit if the actual primary interface differs. + +## Operator workflow + +All commands the user runs themselves; nothing is SSH'd from the dev session. + +### Step 0 — branch hygiene (dev Mac) +``` +git switch feature/ha-zbt-2-thread # already renamed +git revert --no-edit e8d09f4 # drops ZHA + dialout commit +``` + +### Step 1 — apply the module changes (dev Mac) +Edit `modules/environments/home-assistant/default.nix` per the sketch above. Leave `` as a placeholder; fill after Step 3. + +### Step 2 — eval-only sanity check (dev Mac) +``` +nix flake check +``` +or, equivalently, +``` +nixos-rebuild dry-build --flake .#jupiter +``` +Catches: bad import path, option typos, version skew between unstable and stable. + +### Step 3 — plug ZBT-2 into jupiter (still on stock Zigbee firmware) +On jupiter: +``` +ls -l /dev/serial/by-id/ +``` +Then on dev Mac: copy the full `usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_-...` path into `radio.device`, commit on the feature branch. + +### Step 4 — flash OpenThread RCP firmware (one-time, on jupiter) +``` +nix shell nixpkgs#python313Packages.universal-silabs-flasher -c \ + universal-silabs-flasher \ + --device /dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_-... \ + flash --firmware ~/ot-rcp-zbt-2-.gbl +``` +Firmware download: latest ZBT-2 OpenThread RCP `.gbl` from . + +OTBR isn't running yet at this point, so there's no contention on the device. + +### Step 5 — rebuild (on jupiter) +``` +sudo nixos-rebuild switch --flake .#jupiter +``` +Brings up `otbr-agent.service`, opens TCP/8081, loads `otbr` + `thread` integrations in HA. + +### Step 6 — confirm HA discovered it +- `http://jupiter:8123` → Settings → Devices & Services → "Open Thread Border Router" appears as auto-discovered within ~30 s. +- Click "Configure", form a new Thread network (or import an existing dataset). +- "Matter" integration page now shows Thread credentials available. + +### Step 7 — Matter-over-Thread smoke test +Pair one Matter-over-Thread device end-to-end via the HA Companion app. Pairing should complete in 30–90 s. If it does, merge `feature/ha-zbt-2-thread` into `master`. + +### Future updates +Identical to Step 4: stop `otbr-agent.service`, run the flasher with a new `.gbl`, start the service. + +## Failure modes + +| Symptom | Likely cause | Mitigation | +|---|---|---| +| `otbr-agent.service` fails: "Failed to open device" | Dongle unplugged or `radio.device` path stale (e.g. after replacement) | Module sets `Restart = "on-failure"`; check `systemctl status otbr-agent`, re-check `/dev/serial/by-id/`, update path. | +| OTBR up but HA never discovers it | mDNS not propagating on `enp3s0` (most often: `backboneInterfaces` wrong) | `avahi-browse -r _meshcop._udp` should show one entry. If not: `ip link`, fix `backboneInterfaces`, rebuild. | +| HA shows OTBR but Matter pairing times out | Thread mesh prefix not routed to LAN, or matter-server can't reach the device's IPv6 ULA | `nft list ruleset` should show OTBR's forwarding rules; `ip -6 route` should include the Thread mesh prefix. | +| Dongle stuck after a half-completed flash | Flasher interrupted mid-write | Re-run the flash; bootloader stays addressable even if RCP firmware is corrupt. The tool detects bootloader-mode automatically. | +| `nixos-rebuild` fails: "option `services.openthread-border-router` does not exist" | Unstable module import path wrong / not in scope | Caught by Step 2 (eval-only). Fix before deploy. | + +## Verification + +### Eval-only (dev Mac, before deploy) +``` +nix flake check +nix eval --json .#nixosConfigurations.jupiter.config.services.openthread-border-router.radio.url +nix eval --json .#nixosConfigurations.jupiter.config.services.home-assistant.extraComponents +``` +Expected: flake check passes; `radio.url` is a `spinel+hdlc+uart://...` string built from the by-id path; `extraComponents` includes `"otbr"` and `"thread"`. + +### Service-level (jupiter, after rebuild) +``` +systemctl status otbr-agent.service +journalctl -u otbr-agent.service -n 50 --no-pager +ip link show wpan0 +avahi-browse -r -t _meshcop._udp +curl -s http://127.0.0.1:8081/node/state +``` +Expected: service active; `wpan0` exists (DOWN until HA forms a network — correct); one `_meshcop._udp` entry; REST returns a JSON state string. + +### Functional (HA UI) +- "Open Thread Border Router" appears under auto-discovered integrations. +- Forming a Thread network from the integration UI succeeds. +- Pairing one Matter-over-Thread device end-to-end succeeds. + +## Open questions / risks + +- **Unstable module ABI.** The `services.openthread-border-router` module is in `nixos-unstable` and may change shape before landing in 26.05. If options rename, the eval-only step catches it before deploy. Acceptable risk; we can pin the unstable input revision if churn becomes annoying. +- **Backbone interface name.** `enp3s0` is a best guess from `hardware-configuration.nix:64`'s commented-out line. Definitive answer comes from `ip link` on the actual host. Trivial to correct if wrong. +- **First-flash chicken-and-egg.** Deferred to `nix shell` rather than baked into the system, because the dongle must be flashed *before* `otbr-agent` claims it. This is documented in Step 4. From 6d12940205b147468b5f03e06bd7c32f01a69285 Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Sun, 10 May 2026 15:36:12 +0200 Subject: [PATCH 03/24] docs(home-assistant): implementation plan for ZBT-2 Thread + OTBR Task-by-task plan covering: revert of prior ZHA commit, unstable OTBR module import, OTBR enablement against the ZBT-2, firmware flash via universal-silabs-flasher, rebuild on jupiter, and end-to-end smoke test through the HA UI. Designed for execution via superpowers:subagent-driven-development or superpowers:executing-plans, with operator handoffs marked explicitly (per the 'no SSH' workflow rule). Co-Authored-By: Claude Opus 4.7 --- .../plans/2026-05-10-zbt2-thread-otbr.md | 555 ++++++++++++++++++ 1 file changed, 555 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-10-zbt2-thread-otbr.md diff --git a/docs/superpowers/plans/2026-05-10-zbt2-thread-otbr.md b/docs/superpowers/plans/2026-05-10-zbt2-thread-otbr.md new file mode 100644 index 0000000..2b2c114 --- /dev/null +++ b/docs/superpowers/plans/2026-05-10-zbt2-thread-otbr.md @@ -0,0 +1,555 @@ +# ZBT-2 Thread + OTBR Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Run the Home Assistant Connect ZBT-2 as an OpenThread Border Router on `jupiter`, fully integrated with the existing native `services.home-assistant` + `services.matter-server` stack so Matter-over-Thread devices commission through the dongle. + +**Architecture:** Single NixOS module file (`modules/environments/home-assistant/default.nix`) is edited to import the `services.openthread-border-router` module from `nixos-unstable` (not yet in 25.11 stable), enable it against the ZBT-2's `/dev/serial/by-id/...` path, and add HA's `otbr` + `thread` extra components. The previous ZHA-direction commit on this branch is reverted first. The dongle is one-time-flashed from Zigbee NCP firmware to OpenThread RCP firmware via `universal-silabs-flasher` outside the NixOS lifecycle (per design decision: option B, CLI-only). + +**Tech Stack:** Nix flakes (flake-parts), NixOS 25.11 stable + nixos-unstable, `services.openthread-border-router`, `services.home-assistant`, `services.matter-server`, `python313Packages.universal-silabs-flasher`. + +**Spec:** [`docs/superpowers/specs/2026-05-10-zbt2-thread-otbr-design.md`](../specs/2026-05-10-zbt2-thread-otbr-design.md) — read this before starting. + +**User feedback rules in force:** +- Never commit to `master`; this branch is `feature/ha-zbt-2-thread`. Final merge happens at the end via PR or operator-driven merge. +- Do not SSH to `jupiter`. All commands targeting jupiter are operator handoffs — present the command, the user runs it and pastes output back. + +--- + +## File Map + +| Action | File | Responsibility | +|--------|------|----------------| +| Modify | `modules/environments/home-assistant/default.nix` | Import unstable OTBR module; enable OTBR for the ZBT-2; add `otbr` + `thread` HA components | +| Create (auto) | _(no new files)_ | All work fits in the one module | + +The `git revert` of `e8d09f4` automatically un-modifies the same file (drops `"zha"` and the `dialout` line). No host-level (`machines/jupiter/`) changes; no flake-level changes (the existing `_module.args.self = self;` in `machines/configuration.nix:21` already exposes `self.inputs.nixpkgs-unstable` to every module). + +--- + +## Validation Approach (instead of unit tests) + +This is a NixOS configuration change; there's no test framework. We use `nix eval` against `nixosConfigurations.jupiter.config.*` as the equivalent of unit tests — assert option resolution **before** the change (red), then **after** the change (green). Functional / smoke tests happen post-`nixos-rebuild` on jupiter via systemctl, mDNS, and the HA UI. + +All `nix eval` commands run on the dev Mac. All `systemctl` / `journalctl` / `nixos-rebuild` commands run on jupiter (operator handoff). + +--- + +### Task 1: Revert the prior ZHA commit + +**Files:** +- Modify: `modules/environments/home-assistant/default.nix` (via `git revert`) + +- [ ] **Step 1: Verify pre-state** + +On dev Mac, in the repo root: + +```bash +git log --oneline -3 +``` + +Expected: `dbeda27` (design spec) on top of `e8d09f4` (the ZHA commit) on top of `098e632`. + +Also confirm current `extraComponents` includes `"zha"`: + +```bash +nix eval --json .#nixosConfigurations.jupiter.config.services.home-assistant.extraComponents +``` + +Expected: `["matter","mobile_app","zha"]` + +- [ ] **Step 2: Revert** + +```bash +git revert --no-edit e8d09f4 +``` + +Expected: revert commit created cleanly (no merge conflicts), single file changed. + +- [ ] **Step 3: Verify post-state** + +```bash +nix eval --json .#nixosConfigurations.jupiter.config.services.home-assistant.extraComponents +``` + +Expected: `["matter","mobile_app"]` — `zha` is gone. + +```bash +nix eval --json .#nixosConfigurations.jupiter.config.users.users.hass.extraGroups +``` + +Expected: `[]` — `dialout` is gone. + +```bash +git log --oneline -4 +``` + +Expected: revert commit on top of `dbeda27` on top of `e8d09f4`. + +(No explicit `git commit` step — `git revert` produced its own commit.) + +--- + +### Task 2: Wire the unstable OTBR module import (still disabled) + +This task gets the module into scope so options become available, but leaves `services.openthread-border-router.enable = false` (the default). The point is to confirm the import path works before adding device-specific config. + +**Files:** +- Modify: `modules/environments/home-assistant/default.nix` + +- [ ] **Step 1: Write the failing eval check** + +On dev Mac: + +```bash +nix eval --json .#nixosConfigurations.jupiter.options.services.openthread-border-router.enable.description 2>&1 | head -3 +``` + +Expected: error containing `attribute 'openthread-border-router' missing` or similar — the option doesn't exist yet because the module isn't imported. + +- [ ] **Step 2: Add `self` to the module's argument list and add the `imports` block** + +Current header (`modules/environments/home-assistant/default.nix` lines 1–11): + +```nix +# manages home automations +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.my.profiles.home-assistant; + hostName = config.networking.hostName; +in +``` + +Replace lines 1–11 with: + +```nix +# manages home automations +{ + config, + lib, + pkgs, + self, + ... +}: +let + cfg = config.my.profiles.home-assistant; + hostName = config.networking.hostName; +in +``` + +Then, immediately after the opening brace on line 12 of the modified file (i.e. at the top of the attribute set body, before `options.my.profiles.home-assistant`), add: + +```nix + imports = [ + # services.openthread-border-router isn't in nixos-25.11; pull from + # nixpkgs-unstable. Package comes from the existing unstable overlay. + "${self.inputs.nixpkgs-unstable}/nixos/modules/services/home-automation/openthread-border-router.nix" + ]; + +``` + +- [ ] **Step 3: Re-run the eval check** + +```bash +nix eval --json .#nixosConfigurations.jupiter.options.services.openthread-border-router.enable.description 2>&1 | head -3 +``` + +Expected: a JSON string describing the option (e.g. `"Whether to enable the OpenThread Border Router."`). + +- [ ] **Step 4: Verify the service is currently disabled** + +```bash +nix eval --json .#nixosConfigurations.jupiter.config.services.openthread-border-router.enable +``` + +Expected: `false`. + +- [ ] **Step 5: Verify whole config still evaluates** + +```bash +nix eval .#nixosConfigurations.jupiter.config.system.build.toplevel.drvPath +``` + +Expected: a `/nix/store/...drv` path. Pre-existing trace warnings (the `*.service ordered after network-online.target` ones) are fine; no errors. + +- [ ] **Step 6: Commit** + +```bash +git add modules/environments/home-assistant/default.nix +git commit -m "$(cat <<'EOF' +feat(home-assistant): import openthread-border-router module from unstable + +Pulls the services.openthread-border-router NixOS module directly from +nixpkgs-unstable since it isn't in 25.11 yet. Service stays disabled +in this commit; configuration follows. + +Co-Authored-By: Claude Opus 4.7 +EOF +)" +``` + +--- + +### Task 3: Operator handoff — get the ZBT-2 device path from jupiter + +This task has no code. It collects the runtime parameter (USB serial number) that Task 4 needs. + +**Files:** _(none)_ + +- [ ] **Step 1: Hand off** + +Tell the operator: + +> "Plug the ZBT-2 into a USB-2 port on jupiter (it's still on stock Zigbee firmware — that's fine for this step). Then run `ls -l /dev/serial/by-id/` on jupiter and paste the full output back. We're after the line that contains `Nabu_Casa_Home_Assistant_Connect_ZBT-2`." + +- [ ] **Step 2: Wait for the operator's pasted output** + +Expected shape: a line like +`lrwxrwxrwx 1 root root 13 May 10 14:30 usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_-if00 -> ../../ttyACM0` + +- [ ] **Step 3: Record the by-id path** + +Capture the value `/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_-if00` for use in Task 4. Use the **by-id** path (not `/dev/ttyACM0`) so USB renumbering can't break OTBR. + +--- + +### Task 4: Enable OTBR + add HA otbr/thread components + +**Files:** +- Modify: `modules/environments/home-assistant/default.nix` + +- [ ] **Step 1: Write the failing eval checks** + +On dev Mac: + +```bash +nix eval --json .#nixosConfigurations.jupiter.config.services.openthread-border-router.enable +``` + +Expected: `false` (still disabled from Task 2). + +```bash +nix eval --json .#nixosConfigurations.jupiter.config.services.home-assistant.extraComponents +``` + +Expected: `["matter","mobile_app"]` — no `otbr`, no `thread` yet. + +- [ ] **Step 2: Add `"otbr"` and `"thread"` to `extraComponents`** + +In `modules/environments/home-assistant/default.nix`, locate the `extraComponents` list (currently `[ "matter" "mobile_app" ]`) and replace it with: + +```nix + extraComponents = [ + "matter" + "mobile_app" + "otbr" + "thread" + ]; +``` + +- [ ] **Step 3: Add the `services.openthread-border-router` block** + +In the same file, **after** the `services.home-assistant.config = { ... };` block and **before** `my.homepage.services`, add: + +```nix + services.openthread-border-router = { + enable = true; + package = pkgs.unstable.openthread-border-router; + openFirewall = true; + backboneInterfaces = [ "enp3s0" ]; + radio.device = ""; + }; + +``` + +Replace `` with the literal string captured in Task 3 step 3 (e.g. `"/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_AB12CD34-if00"`). + +- [ ] **Step 4: Run the green eval checks** + +```bash +nix eval --json .#nixosConfigurations.jupiter.config.services.home-assistant.extraComponents +``` + +Expected: `["matter","mobile_app","otbr","thread"]`. + +```bash +nix eval --json .#nixosConfigurations.jupiter.config.services.openthread-border-router.enable +``` + +Expected: `true`. + +```bash +nix eval --raw .#nixosConfigurations.jupiter.config.services.openthread-border-router.radio.url +``` + +Expected: a string like `spinel+hdlc+uart:///dev/serial/by-id/usb-Nabu_Casa_..._ZBT-2_-if00?uart-baudrate=115200` (the module composes this from `radio.device` automatically). + +- [ ] **Step 5: Full eval — system derivation must build** + +```bash +nix eval .#nixosConfigurations.jupiter.config.system.build.toplevel.drvPath +``` + +Expected: a `/nix/store/...drv` path with no eval errors. + +- [ ] **Step 6: `nix flake check` for good measure** + +```bash +nix flake check +``` + +Expected: no errors. (Same pre-existing trace warnings as before are acceptable.) + +- [ ] **Step 7: Commit** + +```bash +git add modules/environments/home-assistant/default.nix +git commit -m "$(cat <<'EOF' +feat(home-assistant): enable OTBR for ZBT-2 + add HA otbr/thread components + +Brings up otbr-agent against the ZBT-2 over Spinel/UART, opens the +REST API on :8081, and wires HA's otbr + thread integrations so +Matter-over-Thread devices can commission through the existing +matter-server. + +Co-Authored-By: Claude Opus 4.7 +EOF +)" +``` + +--- + +### Task 5: Operator handoff — flash OpenThread RCP firmware on the dongle + +The dongle is currently running Zigbee NCP firmware and won't speak Spinel until reflashed. This must happen **before** Task 6's rebuild (otherwise `otbr-agent` will try to talk to a Zigbee-firmware dongle and fail). + +**Files:** _(none on dev Mac)_ + +- [ ] **Step 1: Hand off — fetch firmware** + +Tell the operator: + +> "On any machine with a browser: download the latest **ZBT-2 OpenThread RCP** `.gbl` from . The asset name will look like `ot-rcp-zbt-2-.gbl`. Get it onto jupiter — `scp` it over, or just `curl` from jupiter's shell. Confirm by running `ls ~/ot-rcp-zbt-2-*.gbl` on jupiter and pasting the result." + +- [ ] **Step 2: Wait for confirmation** + +Expected: a single matching path, e.g. `/home/finn/ot-rcp-zbt-2-2025.10.0.gbl`. + +- [ ] **Step 3: Hand off — flash** + +Tell the operator: + +> "On jupiter, run (substituting the actual by-id path from Task 3 and the actual `.gbl` filename): +> +> ```bash +> nix shell nixpkgs#python313Packages.universal-silabs-flasher -c \ +> universal-silabs-flasher \ +> --device /dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_-if00 \ +> flash --firmware ~/ot-rcp-zbt-2-.gbl +> ``` +> +> Paste the full output. Expected duration: ~30 seconds. The tool detects the running firmware, drops the dongle into bootloader mode, writes the `.gbl`, and reboots back to RCP." + +- [ ] **Step 4: Verify the flash succeeded** + +Expected output ends with something like `Firmware update complete` (or equivalent success message). If the tool reports CRC failure / partial write — re-run; the bootloader stays addressable. + +If the operator reports `--help` shows different subcommand syntax (universal-silabs-flasher's CLI has changed across versions), have them check `universal-silabs-flasher --help` and adapt — but the `flash --firmware ` form has been stable since 1.0.x. + +--- + +### Task 6: Operator handoff — `nixos-rebuild switch` on jupiter + +**Files:** _(none on dev Mac)_ + +- [ ] **Step 1: Push the branch so jupiter can fetch it** + +On dev Mac: + +```bash +git push -u origin feature/ha-zbt-2-thread +``` + +(If the operator pulls via a different mechanism — local checkout, fileshare — adapt accordingly. The standard pattern in this repo is `git pull` on jupiter.) + +- [ ] **Step 2: Hand off — pull + rebuild** + +Tell the operator: + +> "On jupiter: +> +> ```bash +> cd ~/development/nixos # or wherever the flake lives on jupiter +> git fetch origin +> git checkout feature/ha-zbt-2-thread +> sudo nixos-rebuild switch --flake .#jupiter +> ``` +> +> Paste the tail of the output (everything from the first `building ...` line onward). Expected: build completes, switch to the new generation, no errors." + +- [ ] **Step 3: Verify the switch succeeded** + +If the operator's pasted output includes `error:` or the switch failed mid-activation, **stop here**. Common failure: option name mismatch with whatever version of nixos-unstable is locked in the flake. Fix on dev Mac, push, ask operator to pull + rebuild again. + +If the rebuild succeeded, proceed to Task 7. + +--- + +### Task 7: Operator handoff — service-level verification on jupiter + +**Files:** _(none)_ + +- [ ] **Step 1: Hand off — service health** + +Tell the operator: + +> "On jupiter, run each command and paste output: +> +> ```bash +> systemctl status otbr-agent.service --no-pager +> journalctl -u otbr-agent.service -n 50 --no-pager +> ip link show wpan0 +> ```" + +- [ ] **Step 2: Verify** + +Expected: +- `systemctl status` reports `active (running)`. +- `journalctl` shows OTBR startup messages, no repeated restart loops. +- `ip link show wpan0` shows the interface exists; state DOWN is correct (HA hasn't formed a network yet). + +If `otbr-agent` is in restart loop with `Failed to open device`: device path mismatch. Re-check Task 3's path. + +- [ ] **Step 3: Hand off — mDNS publication** + +Tell the operator: + +> "On jupiter: +> +> ```bash +> avahi-browse -r -t _meshcop._udp +> ```" + +Expected: one entry whose hostname matches jupiter, advertising port 8081. + +If empty: `backboneInterfaces` is wrong. On jupiter, run `ip link show` and tell operator to paste; pick the actual primary LAN interface, update `backboneInterfaces`, re-rebuild. + +- [ ] **Step 4: Hand off — REST API reachability** + +Tell the operator: + +> "On jupiter: +> +> ```bash +> curl -s http://127.0.0.1:8081/node/state +> ```" + +Expected: a JSON state string, most likely `"disabled"` (HA hasn't formed a network yet). + +If connection refused: OTBR isn't actually listening — re-check `journalctl`. + +--- + +### Task 8: Operator handoff — HA UI smoke test + +**Files:** _(none)_ + +- [ ] **Step 1: Hand off — confirm discovery** + +Tell the operator: + +> "Open `http://jupiter:8123` in a browser. Go to **Settings → Devices & Services**. Within ~30s of the rebuild, you should see **'Open Thread Border Router'** under 'Discovered'. Click **Configure**. Let HA form a new Thread network (or import existing dataset if you have one). Tell me when that's done — and paste any errors if it doesn't work." + +- [ ] **Step 2: Wait for confirmation** + +Expected: HA reports the Thread network is formed; the OTBR integration appears under 'Configured'. + +If discovery doesn't happen: cross-check with Task 7 step 3 (`avahi-browse`). HA reads from the system's avahi cache. + +- [ ] **Step 3: Hand off — Matter-over-Thread pairing** + +Tell the operator: + +> "Pick one Matter-over-Thread device. Use the HA Companion app, scan its Matter QR code, and follow the prompts. Tell me when it's paired — or paste any errors. Pairing should complete in 30–90s." + +- [ ] **Step 4: Wait for confirmation** + +Expected: device appears under both Matter and Thread integrations in HA, and is controllable from the dashboard. + +If pairing times out: see "Failure modes" table in the spec — most likely Thread mesh prefix isn't routed back to LAN. Operator runs `nft list ruleset` and `ip -6 route` on jupiter; debug from there. + +--- + +### Task 9: Merge to master + +**Files:** _(none)_ + +- [ ] **Step 1: Final branch state** + +On dev Mac: + +```bash +git log --oneline master..feature/ha-zbt-2-thread +``` + +Expected (in chronological order from oldest to newest): +1. `e8d09f4` — original ZHA commit +2. `dbeda27` — design spec +3. `` — Revert "feat(home-assistant): enable ZHA for ZBT-2 Zigbee dongle" +4. `` — feat(home-assistant): import openthread-border-router module from unstable +5. `` — feat(home-assistant): enable OTBR for ZBT-2 + add HA otbr/thread components + +That's a fine history to merge as-is (the ZHA→revert pair is honest about the pivot). + +- [ ] **Step 2: Hand off — merge** + +The user runs the merge themselves (per repo policy: never commit to master without explicit consent). Tell the operator: + +> "If the smoke tests in Task 8 worked, merge with: +> +> ```bash +> git switch master +> git merge --no-ff feature/ha-zbt-2-thread +> git push origin master +> ``` +> +> Or open a merge request / PR if you prefer review first." + +- [ ] **Step 3: Optional cleanup** + +After merge: + +```bash +git branch -d feature/ha-zbt-2-thread +git push origin --delete feature/ha-zbt-2-thread +``` + +--- + +## Self-Review + +**Spec coverage:** +- Goals (4 bullets) → Tasks 2 (OTBR module wiring), 4 (OTBR enable + HA components), 5 (firmware flash), 8 (Matter-over-Thread smoke test) ✓ +- Non-goals → respected; no multipan, no auto-flash, no fallback paths ✓ +- Architecture diagram → Task 4 produces the wiring shown; Tasks 6–8 verify it ✓ +- File changes (one module) → Tasks 1, 2, 4 ✓ +- Reverts of prior ZHA commit → Task 1 ✓ +- Operator workflow steps 0–7 → Tasks 1, 2, 3, 4, 5, 6, 7, 8 ✓ +- Verification (eval-only / service-level / functional) → Tasks 2/4/6/7/8 ✓ +- Failure-mode table → referenced in Tasks 6, 7, 8 for triage ✓ + +**Placeholder scan:** +- `` in Task 4 step 3 is intentional — it's a runtime parameter the operator fills in, captured in Task 3. +- ``, `` in shell commands are intentional placeholders for operator substitution. +- No "TBD", "TODO", "implement later", or vague "handle errors" steps. + +**Type / name consistency:** +- `services.openthread-border-router` used consistently (matches the unstable module's option path). +- `pkgs.unstable.openthread-border-router` matches the overlay (`machines/configuration.nix:11`). +- `extraComponents` strings (`"otbr"`, `"thread"`) match HA Core integration names. +- `radio.device` ↔ `radio.url` relationship documented (module composes `url` from `device`). From 9ff3603d40575582e2bd82949f36d2a480ebda66 Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Sun, 10 May 2026 15:39:10 +0200 Subject: [PATCH 04/24] Revert "feat(home-assistant): enable ZHA for ZBT-2 Zigbee dongle" This reverts commit e8d09f40f66e0e6dae4853e9eec3554c669b1c40. --- modules/environments/home-assistant/default.nix | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/environments/home-assistant/default.nix b/modules/environments/home-assistant/default.nix index fc77dba..e3e8fe4 100644 --- a/modules/environments/home-assistant/default.nix +++ b/modules/environments/home-assistant/default.nix @@ -24,7 +24,6 @@ in extraComponents = [ "matter" "mobile_app" - "zha" ]; }; services.home-assistant.config = { @@ -33,9 +32,6 @@ in mobile_app = {}; }; - # Allow the `hass` service user to open the ZBT-2 USB-CDC serial endpoint. - users.users.hass.extraGroups = [ "dialout" ]; - my.homepage.services = [ { group = "Services"; From 311e358d88aa59a6ddeb97448967ce7b49ad0423 Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Sun, 10 May 2026 15:52:20 +0200 Subject: [PATCH 05/24] =?UTF-8?q?docs(plan):=20correct=20Task=202=20scope?= =?UTF-8?q?=20=E2=80=94=20specialArgs=20needed=20for=20self=20in=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original plan claimed no flake-level changes were needed because machines/configuration.nix:21 already passes `_module.args.self = self;`. That's only true for `config`-time evaluation; `imports` are collected before `config` is available, so referencing `self` in `imports` causes infinite recursion. Fix: promote `self` to `specialArgs` on each nixosSystem call. The implementer of Task 2 caught this on first dispatch. Co-Authored-By: Claude Opus 4.7 --- .../plans/2026-05-10-zbt2-thread-otbr.md | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/plans/2026-05-10-zbt2-thread-otbr.md b/docs/superpowers/plans/2026-05-10-zbt2-thread-otbr.md index 2b2c114..4f4af8c 100644 --- a/docs/superpowers/plans/2026-05-10-zbt2-thread-otbr.md +++ b/docs/superpowers/plans/2026-05-10-zbt2-thread-otbr.md @@ -21,9 +21,12 @@ | Action | File | Responsibility | |--------|------|----------------| | Modify | `modules/environments/home-assistant/default.nix` | Import unstable OTBR module; enable OTBR for the ZBT-2; add `otbr` + `thread` HA components | -| Create (auto) | _(no new files)_ | All work fits in the one module | +| Modify | `machines/configuration.nix` | Pass `self` via `specialArgs` so it's available during NixOS module **imports** evaluation (not just config) | +| Create (auto) | _(no new files)_ | All work fits in the two modules | -The `git revert` of `e8d09f4` automatically un-modifies the same file (drops `"zha"` and the `dialout` line). No host-level (`machines/jupiter/`) changes; no flake-level changes (the existing `_module.args.self = self;` in `machines/configuration.nix:21` already exposes `self.inputs.nixpkgs-unstable` to every module). +The `git revert` of `e8d09f4` automatically un-modifies the home-assistant module (drops `"zha"` and the `dialout` line). No host-level (`machines/jupiter/`) changes. + +**Why the flake-level edit is needed:** the existing `_module.args.self = self;` in `machines/configuration.nix:21` makes `self` available in module bodies (option definitions, `config` blocks). It does **not** make `self` available during `imports` evaluation — `_module.args` is resolved from `config`, but `imports` are collected **before** `config` is evaluated, so `self` in `imports` causes an infinite recursion error. Promoting `self` to `specialArgs` short-circuits that and is the conventional fix. --- @@ -95,6 +98,7 @@ Expected: revert commit on top of `dbeda27` on top of `e8d09f4`. This task gets the module into scope so options become available, but leaves `services.openthread-border-router.enable = false` (the default). The point is to confirm the import path works before adding device-specific config. **Files:** +- Modify: `machines/configuration.nix` (add `specialArgs = { inherit self; };` to each `nixosSystem` call) - Modify: `modules/environments/home-assistant/default.nix` - [ ] **Step 1: Write the failing eval check** @@ -107,6 +111,50 @@ nix eval --json .#nixosConfigurations.jupiter.options.services.openthread-border Expected: error containing `attribute 'openthread-border-router' missing` or similar — the option doesn't exist yet because the module isn't imported. +- [ ] **Step 1a: Promote `self` to `specialArgs` in `machines/configuration.nix`** + +`self` must be reachable during `imports` evaluation (not just `config` evaluation). The existing `_module.args.self = self;` only covers `config`-time access. Edit each `nixosSystem` call (`jupiter` and `mibook`) to add `specialArgs`. + +Current shape (lines 50–56 and 57–63): + +```nix +jupiter = nixosSystem { + system = "x86_64-linux"; + modules = defaultModules ++ [ + # nixos-hardware.nixosModules.bmax-b7-power + ./jupiter/configuration.nix + ]; +}; +mibook = nixosSystem { + system = "x86_64-linux"; + modules = defaultModules ++ [ + # nixos-hardware.nixosModules.mibook + ./mibook/configuration.nix + ]; +}; +``` + +Add `specialArgs = { inherit self; };` to each: + +```nix +jupiter = nixosSystem { + system = "x86_64-linux"; + specialArgs = { inherit self; }; + modules = defaultModules ++ [ + # nixos-hardware.nixosModules.bmax-b7-power + ./jupiter/configuration.nix + ]; +}; +mibook = nixosSystem { + system = "x86_64-linux"; + specialArgs = { inherit self; }; + modules = defaultModules ++ [ + # nixos-hardware.nixosModules.mibook + ./mibook/configuration.nix + ]; +}; +``` + - [ ] **Step 2: Add `self` to the module's argument list and add the `imports` block** Current header (`modules/environments/home-assistant/default.nix` lines 1–11): @@ -180,7 +228,7 @@ Expected: a `/nix/store/...drv` path. Pre-existing trace warnings (the `*.servic - [ ] **Step 6: Commit** ```bash -git add modules/environments/home-assistant/default.nix +git add machines/configuration.nix modules/environments/home-assistant/default.nix git commit -m "$(cat <<'EOF' feat(home-assistant): import openthread-border-router module from unstable @@ -188,6 +236,10 @@ Pulls the services.openthread-border-router NixOS module directly from nixpkgs-unstable since it isn't in 25.11 yet. Service stays disabled in this commit; configuration follows. +Also promotes `self` from `_module.args` to `specialArgs` in +machines/configuration.nix, since `imports` are evaluated before +`config` and so can't reach `_module.args.self`. + Co-Authored-By: Claude Opus 4.7 EOF )" From 6251c8edefe5dea5a35fa4e94cb37d0ae81de97d Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Sun, 10 May 2026 18:52:43 +0200 Subject: [PATCH 06/24] feat(home-assistant): import openthread-border-router module from unstable Pulls the services.openthread-border-router NixOS module directly from nixpkgs-unstable since it isn't in 25.11 yet. Service stays disabled in this commit; configuration follows. Also promotes `self` from `_module.args` to `specialArgs` in machines/configuration.nix, since `imports` are evaluated before `config` and so can't reach `_module.args.self`. Co-Authored-By: Claude Opus 4.7 --- machines/configuration.nix | 2 ++ modules/environments/home-assistant/default.nix | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/machines/configuration.nix b/machines/configuration.nix index ac64523..c6af679 100644 --- a/machines/configuration.nix +++ b/machines/configuration.nix @@ -49,6 +49,7 @@ in # use your hardware- model from this list: https://github.com/NixOS/nixos-hardware/blob/master/flake.nix jupiter = nixosSystem { system = "x86_64-linux"; + specialArgs = { inherit self; }; modules = defaultModules ++ [ # nixos-hardware.nixosModules.bmax-b7-power ./jupiter/configuration.nix @@ -56,6 +57,7 @@ in }; mibook = nixosSystem { system = "x86_64-linux"; + specialArgs = { inherit self; }; modules = defaultModules ++ [ # nixos-hardware.nixosModules.mibook ./mibook/configuration.nix diff --git a/modules/environments/home-assistant/default.nix b/modules/environments/home-assistant/default.nix index e3e8fe4..d27393d 100644 --- a/modules/environments/home-assistant/default.nix +++ b/modules/environments/home-assistant/default.nix @@ -1,8 +1,9 @@ -# manages home automations +# manages home automations { config, lib, pkgs, + self, ... }: let @@ -10,6 +11,12 @@ let hostName = config.networking.hostName; in { + imports = [ + # services.openthread-border-router isn't in nixos-25.11; pull from + # nixpkgs-unstable. Package comes from the existing unstable overlay. + "${self.inputs.nixpkgs-unstable}/nixos/modules/services/home-automation/openthread-border-router.nix" + ]; + options.my.profiles.home-assistant = with lib; { enable = mkEnableOption "Home Automation"; From 787427e7c815557c7c0db6585e1080e823877bfd Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Sun, 10 May 2026 20:21:30 +0200 Subject: [PATCH 07/24] feat(home-assistant): enable OTBR for ZBT-2 + add HA otbr/thread components Brings up otbr-agent against the ZBT-2 over Spinel/UART, opens the REST API on :8081, and wires HA's otbr + thread integrations so Matter-over-Thread devices can commission through the existing matter-server. Co-Authored-By: Claude Opus 4.7 --- modules/environments/home-assistant/default.nix | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modules/environments/home-assistant/default.nix b/modules/environments/home-assistant/default.nix index d27393d..e790392 100644 --- a/modules/environments/home-assistant/default.nix +++ b/modules/environments/home-assistant/default.nix @@ -31,6 +31,8 @@ in extraComponents = [ "matter" "mobile_app" + "otbr" + "thread" ]; }; services.home-assistant.config = { @@ -39,6 +41,14 @@ in mobile_app = {}; }; + services.openthread-border-router = { + enable = true; + package = pkgs.unstable.openthread-border-router; + openFirewall = true; + backboneInterfaces = [ "enp3s0" ]; + radio.device = "/dev/serial/by-id/usb-Nabu_Casa_ZBT-2_DCB4D9149C7C-if00"; + }; + my.homepage.services = [ { group = "Services"; From 68f2c6524668080f3ad3945193bfe5fca7fb77ab Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Sun, 10 May 2026 20:41:49 +0200 Subject: [PATCH 08/24] fix(home-assistant): set OTBR radio.baudRate to 460800 for ZBT-2 RCP firmware Nabu Casa's prebuilt OpenThread RCP image for the ZBT-2 (zbt2_openthread_rcp_2.7.2.0_GitHub-fb0446f53_gsdk_2025.6.2.gbl) runs at 460800 baud, not the module's 115200 default. Aligns the radio URL with the firmware so otbr-agent can actually open the Spinel link after rebuild. Co-Authored-By: Claude Opus 4.7 --- modules/environments/home-assistant/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/environments/home-assistant/default.nix b/modules/environments/home-assistant/default.nix index e790392..cbc5651 100644 --- a/modules/environments/home-assistant/default.nix +++ b/modules/environments/home-assistant/default.nix @@ -47,6 +47,7 @@ in openFirewall = true; backboneInterfaces = [ "enp3s0" ]; radio.device = "/dev/serial/by-id/usb-Nabu_Casa_ZBT-2_DCB4D9149C7C-if00"; + radio.baudRate = 460800; }; my.homepage.services = [ From 96fbeb04ef9b75832ea0c36c6a3980e87b2bbfa1 Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Sun, 10 May 2026 21:35:44 +0200 Subject: [PATCH 09/24] fix(home-assistant): nest name/unit_system under homeassistant block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HA expects \`name\` and \`unit_system\` as keys of the top-level \`homeassistant:\` block, not as top-level integration names. Recent HA versions tightened config validation, so this surfaced as "Integration 'unit_system' not found" / "Integration 'name' not found" warnings, blocking the automation reload that runs after saving an automation in the UI. Pre-existing bug, unrelated to OTBR — surfaced now because automation edits trigger strict validation. Bonus: NixOS auto-populates \`time_zone\` from the system locale once \`homeassistant\` is a real block. Co-Authored-By: Claude Opus 4.7 --- modules/environments/home-assistant/default.nix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/environments/home-assistant/default.nix b/modules/environments/home-assistant/default.nix index cbc5651..3a849cf 100644 --- a/modules/environments/home-assistant/default.nix +++ b/modules/environments/home-assistant/default.nix @@ -36,8 +36,10 @@ in ]; }; services.home-assistant.config = { - name = "Home - Rechberg"; - unit_system = "metric"; + homeassistant = { + name = "Home - Rechberg"; + unit_system = "metric"; + }; mobile_app = {}; }; From d1299ed1129034e0bbccf90c77f00569bae46c61 Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Sun, 10 May 2026 21:50:41 +0200 Subject: [PATCH 10/24] fix(home-assistant): wire automation/script/scene !include directives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI-saved automations land in /var/lib/hass/automations.yaml, but HA won't load them unless configuration.yaml has \`automation: !include automations.yaml\`. The Nix-managed config didn't have it, so HA's post-save reload found no automations and timed out. The NixOS HA module's renderYAMLFile post-processes the generated YAML to convert quoted bang-strings into real YAML tags (see the sed step in nixos/modules/services/home-automation/home-assistant.nix), so a plain Nix string is enough — no escape-hatch needed. Pre-create the three include targets via systemd.tmpfiles so HA doesn't fail at startup if the user hasn't saved anything yet. Co-Authored-By: Claude Opus 4.7 --- modules/environments/home-assistant/default.nix | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/environments/home-assistant/default.nix b/modules/environments/home-assistant/default.nix index 3a849cf..a9012af 100644 --- a/modules/environments/home-assistant/default.nix +++ b/modules/environments/home-assistant/default.nix @@ -41,8 +41,19 @@ in unit_system = "metric"; }; mobile_app = {}; + automation = "!include automations.yaml"; + script = "!include scripts.yaml"; + scene = "!include scenes.yaml"; }; + # `!include` targets must exist or HA fails at startup. Create them empty + # so HA's UI editor can write to them; `f` only acts if the file is absent. + systemd.tmpfiles.rules = [ + "f /var/lib/hass/automations.yaml 0644 hass hass - []" + "f /var/lib/hass/scripts.yaml 0644 hass hass - {}" + "f /var/lib/hass/scenes.yaml 0644 hass hass - []" + ]; + services.openthread-border-router = { enable = true; package = pkgs.unstable.openthread-border-router; From 933c2f8b416886817d61ab3b0f39dafe6538d12a Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Sun, 17 May 2026 18:01:16 +0200 Subject: [PATCH 11/24] feat: HA Xiaomi Support --- modules/environments/home-assistant/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/environments/home-assistant/default.nix b/modules/environments/home-assistant/default.nix index a9012af..bd08017 100644 --- a/modules/environments/home-assistant/default.nix +++ b/modules/environments/home-assistant/default.nix @@ -33,6 +33,7 @@ in "mobile_app" "otbr" "thread" + "xiaomi" ]; }; services.home-assistant.config = { From 4cb4455d3764fd9af6c83e108eeec46c477475c9 Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Sun, 17 May 2026 18:23:16 +0200 Subject: [PATCH 12/24] fix(home-assistant): use xiaomi_miio for Mi Home devices The bare "xiaomi" component only ships the legacy IP-camera platform. Mi Home Wi-Fi devices (vacuums, air purifiers, Yeelight, fans, etc.) are provided by the xiaomi_miio integration. Co-Authored-By: Claude Opus 4.7 --- modules/environments/home-assistant/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/environments/home-assistant/default.nix b/modules/environments/home-assistant/default.nix index bd08017..eb4eb99 100644 --- a/modules/environments/home-assistant/default.nix +++ b/modules/environments/home-assistant/default.nix @@ -33,7 +33,7 @@ in "mobile_app" "otbr" "thread" - "xiaomi" + "xiaomi_miio" ]; }; services.home-assistant.config = { From d2775e35d904c6b47fa47fb2a6f7d4f5fd798b1e Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Mon, 18 May 2026 12:07:31 +0200 Subject: [PATCH 13/24] feat(home-assistant): set internal/external URLs for mobile_app push The HA Companion app needs an external_url for clickable notification deep-links to resolve when the phone is off the home Wi-Fi. Reach is via Tailscale (Headscale tailnet solar.internal), so external_url points at the FQDN jupiter.solar.internal:8123; internal_url stays on the bare hostname for LAN-attached devices. Phase A only: device registration + notify group + smoke-test land in a follow-up commit once Companion has registered real mobile_app_ service names. See docs/superpowers/specs/2026-05-18-ha-push-notifications.md. Co-Authored-By: Claude Opus 4.7 --- .../specs/2026-05-18-ha-push-notifications.md | 111 ++++++++++++++++++ .../environments/home-assistant/default.nix | 2 + 2 files changed, 113 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-18-ha-push-notifications.md diff --git a/docs/superpowers/specs/2026-05-18-ha-push-notifications.md b/docs/superpowers/specs/2026-05-18-ha-push-notifications.md new file mode 100644 index 0000000..635ade8 --- /dev/null +++ b/docs/superpowers/specs/2026-05-18-ha-push-notifications.md @@ -0,0 +1,111 @@ +# Smartphone push notifications from Home Assistant + +## Goal + +Send push notifications from jupiter's Home Assistant to the user's +smartphones over the home Tailscale (Headscale) tailnet `solar.internal`. + +## Architecture + +- HA's `mobile_app` integration is enabled (already in `extraComponents` + and present as `mobile_app = {}` in config). +- Each smartphone runs the **HA Companion app**, signs in to HA, and + auto-registers as a `notify.mobile_app_` service. +- Reach: phones connect to HA via Tailscale, so HA's `external_url` is + set to the Headscale FQDN `http://jupiter.solar.internal:8123`. The + `internal_url` is `http://jupiter:8123` for LAN-attached devices. +- No public exposure, no reverse proxy, no TLS termination in scope. + +## Phase A — done in this branch + +NixOS module change in `modules/environments/home-assistant/default.nix`: + +```nix +homeassistant = { + name = "Home - Rechberg"; + unit_system = "metric"; + internal_url = "http://${hostName}:8123"; + external_url = "http://jupiter.solar.internal:8123"; +}; +``` + +After deploy, perform the user-side registration: + +1. Install the Companion app: + - iOS: search "Home Assistant" in the App Store. + - Android: search "Home Assistant" in Google Play. +2. Ensure Tailscale is running and connected on the phone. +3. Open the Companion app. When asked to connect, enter + `http://jupiter.solar.internal:8123` and sign in with the HA account. +4. Approve the registration prompt in HA. +5. In HA, go to **Settings → Devices & Services → Mobile App** and + confirm the phone appears as a device. +6. In HA, go to **Developer Tools → Services**, type `notify.mobile_app_` + and note the exact service slug for each phone (e.g. + `notify.mobile_app_iphone_finn`). These slugs are needed for Phase B. + +### Verifying Phase A end-to-end + +Build-time: + +``` +nix eval '.#nixosConfigurations.jupiter.config.services.home-assistant.config.homeassistant' \ + --extra-experimental-features 'nix-command flakes' +``` + +Expect the rendered attrset to contain both `external_url` and +`internal_url`. + +Deploy on jupiter: + +``` +sudo nixos-rebuild switch --flake '.#jupiter' +systemctl status home-assistant +journalctl -u home-assistant -n 50 --no-pager +``` + +Functional check after Companion sign-in: + +- HA UI → **Developer Tools → Services** → choose + `notify.mobile_app_` → service data + `{ "message": "Phase A test" }` → **Call Service** → push arrives on + the phone. + +## Phase B — follow-up commit (after registration) + +Once device slugs are known, a separate commit adds: + +1. A `notify` group fanning out to every registered phone: + + ```nix + notify = [ + { + name = "all_phones"; + platform = "group"; + services = [ + { service = "mobile_app_"; } + { service = "mobile_app_"; } + ]; + } + ]; + ``` + +2. A smoke-test mechanism. Approach to be decided in Phase B based on + whether future Nix-managed automations are expected: + + - Pragmatic: document a one-time UI call to `notify.all_phones` from + Developer Tools (no automation in YAML). + - Compromise: switch `automation = "!include automations.yaml"` to + `!include_dir_merge_list automations/` so a Nix-managed + `00-smoke-test.yaml` can coexist with UI-editable automations. + +### Verifying Phase B end-to-end + +- Restart HA, watch `journalctl -u home-assistant` for YAML schema errors. +- Call `notify.all_phones` from Developer Tools — every registered phone + receives the push. + +## Open items + +- After Companion registration, collect the `mobile_app_` service + names from HA and update this spec + open Phase B PR. diff --git a/modules/environments/home-assistant/default.nix b/modules/environments/home-assistant/default.nix index eb4eb99..759a54f 100644 --- a/modules/environments/home-assistant/default.nix +++ b/modules/environments/home-assistant/default.nix @@ -40,6 +40,8 @@ in homeassistant = { name = "Home - Rechberg"; unit_system = "metric"; + internal_url = "http://${hostName}:8123"; + external_url = "http://jupiter.solar.internal:8123"; }; mobile_app = {}; automation = "!include automations.yaml"; From 62875c0b1a9523201a8ede113824dc6548cbee61 Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Mon, 25 May 2026 11:53:36 +0200 Subject: [PATCH 14/24] docs(spec): mibook claude-code execution machine design Co-Authored-By: Claude Sonnet 4.6 --- .../2026-05-25-mibook-claude-code-design.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-25-mibook-claude-code-design.md diff --git a/docs/superpowers/specs/2026-05-25-mibook-claude-code-design.md b/docs/superpowers/specs/2026-05-25-mibook-claude-code-design.md new file mode 100644 index 0000000..ea73c63 --- /dev/null +++ b/docs/superpowers/specs/2026-05-25-mibook-claude-code-design.md @@ -0,0 +1,59 @@ +# MiBook Claude Code Execution Machine + +**Date:** 2026-05-25 +**Branch:** feature/ha-zbt-2-thread +**Status:** Approved + +## Goal + +Configure the MiBook as an interactive Claude Code execution machine by installing the Claude Code CLI and its companion tooling via a new, independently-toggleable NixOS profile module. + +## Scope + +Phase 1 (this spec): interactive CLI session — user logs in or SSHs in and runs `claude` directly. + +Phase 2 (future): headless systemd user service exposing Claude Code over an HTTP API or Unix socket, turning the MiBook into a remotely-triggerable execution node without an interactive session. + +## Module Structure + +``` +modules/environments/claude-code/default.nix ← new module +modules/environments/default.nix ← add import +machines/mibook/environments.nix ← my.profiles.claude-code.enable = true +``` + +Follows the exact same pattern as every other environment module in this repo. + +## Option Declaration + +``` +my.profiles.claude-code.enable (mkEnableOption) +``` + +No sub-options for Phase 1. Phase 2 would add `my.profiles.claude-code.service.enable`. + +## Packages (users.users.finn.packages) + +| Package | Source | Purpose | +|---|---|---| +| `claude-code` | `pkgs.unstable` | Claude Code CLI | +| `ripgrep` | `pkgs` | Primary file search tool used by Claude Code | +| `fd` | `pkgs` | Fast `find` replacement used by Claude Code | +| `gh` | `pkgs` | GitHub CLI for PR/issue operations | +| `jq` | `pkgs` | JSON processing in shell pipelines | + +`git`, `nodejs`, and `docker` are already provided by existing modules and are not duplicated. + +## No Secrets Management + +The Anthropic API key is handled manually by the user (browser login / `claude` interactive setup). No sops-nix or shell-profile injection needed. + +## Future: Headless Service + +A commented block in the module documents the upgrade path: a systemd user service that accepts work via an HTTP API or Unix socket, triggerable over SSH or a local network endpoint, without requiring an interactive session. + +## Files Changed + +1. `modules/environments/claude-code/default.nix` — new profile module +2. `modules/environments/default.nix` — add `./claude-code` import +3. `machines/mibook/environments.nix` — set `my.profiles.claude-code.enable = true` From 45c6d978f21faf7184db72b5bf98d1b1e57d3e84 Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Mon, 25 May 2026 11:54:09 +0200 Subject: [PATCH 15/24] docs(spec): add pkgs.unstable contingency note to claude-code spec Co-Authored-By: Claude Sonnet 4.6 --- docs/superpowers/specs/2026-05-25-mibook-claude-code-design.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/superpowers/specs/2026-05-25-mibook-claude-code-design.md b/docs/superpowers/specs/2026-05-25-mibook-claude-code-design.md index ea73c63..9e04ace 100644 --- a/docs/superpowers/specs/2026-05-25-mibook-claude-code-design.md +++ b/docs/superpowers/specs/2026-05-25-mibook-claude-code-design.md @@ -44,6 +44,8 @@ No sub-options for Phase 1. Phase 2 would add `my.profiles.claude-code.service.e `git`, `nodejs`, and `docker` are already provided by existing modules and are not duplicated. +**Contingency:** If `pkgs.unstable.claude-code` does not exist at eval time, the fallback is a custom derivation in `pkgs/claude-code/default.nix` added to the local overlay — the same mechanism used for other absent packages. + ## No Secrets Management The Anthropic API key is handled manually by the user (browser login / `claude` interactive setup). No sops-nix or shell-profile injection needed. From b44220adf1f1d220b37abf9149b400568b535cd2 Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Mon, 25 May 2026 11:56:53 +0200 Subject: [PATCH 16/24] docs(plan): mibook claude-code module implementation plan Co-Authored-By: Claude Sonnet 4.6 --- .../plans/2026-05-25-mibook-claude-code.md | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-25-mibook-claude-code.md diff --git a/docs/superpowers/plans/2026-05-25-mibook-claude-code.md b/docs/superpowers/plans/2026-05-25-mibook-claude-code.md new file mode 100644 index 0000000..40e9ee3 --- /dev/null +++ b/docs/superpowers/plans/2026-05-25-mibook-claude-code.md @@ -0,0 +1,232 @@ +# MiBook Claude Code Module Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a `my.profiles.claude-code` NixOS module that installs the Claude Code CLI and companion tooling on the MiBook. + +**Architecture:** A single new environment module (`modules/environments/claude-code/default.nix`) declares the `my.profiles.claude-code.enable` option and installs packages for user `finn`. It is registered in `modules/environments/default.nix` and toggled on in `machines/mibook/environments.nix`. + +**Tech Stack:** Nix flakes, NixOS module system, `pkgs.unstable` overlay (already present in repo) + +--- + +## File Map + +| Action | Path | Responsibility | +|---|---|---| +| Create | `modules/environments/claude-code/default.nix` | Declares option + installs packages | +| Modify | `modules/environments/default.nix` | Registers the new module so NixOS loads it | +| Modify | `machines/mibook/environments.nix` | Enables the profile for the MiBook host | + +--- + +### Task 1: Verify `claude-code` exists in `nixpkgs-unstable` + +**Files:** +- Read-only check — no file changes + +- [ ] **Step 1: Check if the package is available** + +Run: +```bash +nix eval --extra-experimental-features 'nix-command flakes' 'github:NixOS/nixpkgs/nixos-unstable#claude-code.version' 2>&1 +``` + +Expected (success): prints a version string like `"0.2.x"` + +Expected (failure): `error: attribute 'claude-code' missing` + +- [ ] **Step 2: If the package exists — note the attribute path and continue to Task 2** + +The module will use `pkgs.unstable.claude-code`. + +- [ ] **Step 3: If the package does NOT exist — add a custom derivation first** + +Create `pkgs/claude-code/default.nix`: +```nix +{ lib, buildNpmPackage, fetchFromGitHub }: +buildNpmPackage rec { + pname = "claude-code"; + version = "0.2.116"; # update to latest release tag + + src = fetchFromGitHub { + owner = "anthropics"; + repo = "claude-code"; + rev = "v${version}"; + hash = lib.fakeHash; # run nix build to get real hash + }; + + npmDepsHash = lib.fakeHash; # run nix build to get real hash + + meta = { + description = "Claude Code CLI by Anthropic"; + homepage = "https://github.com/anthropics/claude-code"; + license = lib.licenses.unfree; + mainProgram = "claude"; + }; +} +``` + +Then register it in `pkgs/default.nix`: +```nix +final: prev: { + claude-code = final.callPackage ./claude-code { }; +} +``` + +And use `pkgs.claude-code` (not `pkgs.unstable.claude-code`) in the module. + +--- + +### Task 2: Create the `claude-code` module + +**Files:** +- Create: `modules/environments/claude-code/default.nix` + +- [ ] **Step 1: Create the module file** + +Create `modules/environments/claude-code/default.nix` with this exact content: +```nix +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.my.profiles.claude-code; +in +{ + options.my.profiles.claude-code = with lib; { + enable = mkEnableOption "Claude Code CLI"; + }; + + config = lib.mkIf cfg.enable { + users.users.finn.packages = with pkgs; [ + unstable.claude-code + ripgrep + fd + gh + jq + ]; + + # Future: headless Claude Code service + # A natural next step is exposing Claude Code as a persistent background service — + # e.g. a systemd user service that accepts work via an HTTP API or Unix socket, + # triggerable over SSH or a local network endpoint. This would turn the MiBook + # into a true remote execution node without requiring an interactive session. + # See: my.profiles.claude-code.service.enable (not yet implemented) + }; +} +``` + +> Note: if Task 1 Step 3 was taken (custom derivation), replace `unstable.claude-code` with `claude-code`. + +--- + +### Task 3: Register the module + +**Files:** +- Modify: `modules/environments/default.nix` + +- [ ] **Step 1: Add the import** + +In `modules/environments/default.nix`, add `./claude-code` to the imports list (alphabetical order puts it between `./audiobookshelf` and `./development`): + +```nix +{ ... }: +{ + imports = [ + ./actual + ./apps + ./audiobookshelf + ./claude-code + ./development + ./home-assistant + ./hyprland + ./zsh + ./paperless + ./prowlarr + ./radarr + ./docker + ./homepage + ./kde-desktop + ./readarr + ./sonarr + ./jellyfin + ./jellyseerr + ]; +} +``` + +- [ ] **Step 2: Verify the option is now defined (without enabling it)** + +Run: +```bash +nix eval --extra-experimental-features 'nix-command flakes' '.#nixosConfigurations.mibook.options.my.profiles.claude-code.enable.description' +``` + +Expected: `"Whether to enable Claude Code CLI."` + +If this errors, the module isn't loading — re-check the import path. + +--- + +### Task 4: Enable on MiBook and verify the build + +**Files:** +- Modify: `machines/mibook/environments.nix` + +- [ ] **Step 1: Enable the profile** + +In `machines/mibook/environments.nix`, add `claude-code.enable = true` inside the `my.profiles` block: + +```nix + my.profiles = { + kde-desktop.enable = true; + zsh.enable = true; + apps = { + desktop_apps = true; + dev_apps = true; + }; + development.enable = true; + docker.enable = true; + claude-code.enable = true; + }; +``` + +- [ ] **Step 2: Verify the package appears in finn's user packages** + +Run: +```bash +nix eval --extra-experimental-features 'nix-command flakes' '.#nixosConfigurations.mibook.config.users.users.finn.packages' --apply 'builtins.map (p: p.name)' 2>&1 | grep -i claude +``` + +Expected: a line containing `claude-code-` + +- [ ] **Step 3: Dry-run build to confirm the full config evaluates** + +Run: +```bash +nix build '.#nixosConfigurations.mibook.config.system.build.toplevel' --extra-experimental-features 'nix-command flakes' --dry-run 2>&1 | tail -5 +``` + +Expected: exits 0, output lists derivations to build (or "nothing to do" if already cached). No evaluation errors. + +--- + +### Task 5: Commit + +**Files:** +- All changed files from Tasks 1–4 + +- [ ] **Step 1: Stage and commit** + +```bash +git add modules/environments/claude-code/default.nix \ + modules/environments/default.nix \ + machines/mibook/environments.nix +git commit -m "feat(mibook): add claude-code profile module" +``` + +If Task 1 Step 3 was taken, also stage `pkgs/claude-code/default.nix` and `pkgs/default.nix`. From 2e5568611a195e3e88cf2d6e694880533627f338 Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Mon, 25 May 2026 12:03:54 +0200 Subject: [PATCH 17/24] feat(mibook): add claude-code profile module Installs claude-code (stable), ripgrep, fd, gh, and jq for user finn. Includes annotation for future headless service upgrade path. Co-Authored-By: Claude Sonnet 4.6 --- machines/mibook/environments.nix | 1 + modules/environments/claude-code/default.nix | 31 ++++++++++++++++++++ modules/environments/default.nix | 1 + 3 files changed, 33 insertions(+) create mode 100644 modules/environments/claude-code/default.nix diff --git a/machines/mibook/environments.nix b/machines/mibook/environments.nix index 3ea5812..17e0b08 100644 --- a/machines/mibook/environments.nix +++ b/machines/mibook/environments.nix @@ -13,6 +13,7 @@ in }; development.enable = true; docker.enable = true; + claude-code.enable = true; }; my.hardware = { diff --git a/modules/environments/claude-code/default.nix b/modules/environments/claude-code/default.nix new file mode 100644 index 0000000..385b927 --- /dev/null +++ b/modules/environments/claude-code/default.nix @@ -0,0 +1,31 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.my.profiles.claude-code; +in +{ + options.my.profiles.claude-code = with lib; { + enable = mkEnableOption "Claude Code CLI"; + }; + + config = lib.mkIf cfg.enable { + users.users.finn.packages = with pkgs; [ + claude-code + ripgrep + fd + gh + jq + ]; + + # Future: headless Claude Code service + # A natural next step is exposing Claude Code as a persistent background service — + # e.g. a systemd user service that accepts work via an HTTP API or Unix socket, + # triggerable over SSH or a local network endpoint. This would turn the MiBook + # into a true remote execution node without requiring an interactive session. + # See: my.profiles.claude-code.service.enable (not yet implemented) + }; +} diff --git a/modules/environments/default.nix b/modules/environments/default.nix index 64ccba6..eb0d299 100644 --- a/modules/environments/default.nix +++ b/modules/environments/default.nix @@ -4,6 +4,7 @@ ./actual ./apps ./audiobookshelf + ./claude-code ./development ./home-assistant ./hyprland From ff6e25b7087daf73340aa2dcfd6381a91ff78b45 Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Mon, 25 May 2026 12:16:59 +0200 Subject: [PATCH 18/24] fix: Duplicated NVIDIA PCI --- machines/mibook/configuration.nix | 8 -------- 1 file changed, 8 deletions(-) diff --git a/machines/mibook/configuration.nix b/machines/mibook/configuration.nix index e518687..648b882 100644 --- a/machines/mibook/configuration.nix +++ b/machines/mibook/configuration.nix @@ -42,14 +42,6 @@ intelBusId = "PCI:00:2:0"; }; - - hardware.nvidia.prime = { - sync.enable = true; - - nvidiaBusId = "PCI:01:00:0"; - intelBusId = "PCI:00:02:0"; - }; - system = { stateVersion = "23.05"; autoUpgrade.enable = true; From 9ed0fb6f07295f40332ce1be509f73b9c971e870 Mon Sep 17 00:00:00 2001 From: marthsincemelee Date: Mon, 25 May 2026 12:33:40 +0200 Subject: [PATCH 19/24] feat: SSH for mibook --- machines/mibook/configuration.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/machines/mibook/configuration.nix b/machines/mibook/configuration.nix index 648b882..c195f11 100644 --- a/machines/mibook/configuration.nix +++ b/machines/mibook/configuration.nix @@ -42,6 +42,8 @@ intelBusId = "PCI:00:2:0"; }; + services.openssh.enable = true; + system = { stateVersion = "23.05"; autoUpgrade.enable = true; From b44775e3e5c86c1942ae9cd68dea46258e5d7111 Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Mon, 25 May 2026 12:25:35 +0200 Subject: [PATCH 20/24] docs: add CLAUDE.md with repo architecture and development commands Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f480c60 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,92 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Apply configuration (switch/boot/test) +sudo nixos-rebuild switch --flake '.#jupiter' +sudo nixos-rebuild switch --flake '.#mibook' + +# Build without switching (CI-style check) +nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel' + +# Update all flake inputs +nix flake update + +# Format Nix files +nixfmt-rfc-style # or: find . -name '*.nix' | xargs nixfmt-rfc-style +``` + +## Architecture + +This is a flake-parts NixOS configuration for two machines: + +- **jupiter** — home server running media/automation services +- **mibook** — laptop running KDE desktop + development tools + +### Module Loading Chain + +``` +flake.nix +└── machines/configuration.nix # flake-parts module; defines nixosConfigurations + ├── machines/core/ # base modules applied to every machine + │ ├── core.nix # system packages, locale, timezone + │ ├── network.nix + │ ├── nix.nix + │ └── users.nix + ├── modules/ # custom NixOS option modules (my.profiles.*, my.hardware.*, my.services.*) + │ ├── environments/ # per-service/app profiles + │ ├── hardware/ # hardware profiles (nvidia, bluetooth, sound, wifi) + │ └── services/ # infrastructure services (vpn, webserver) + └── machines// + ├── configuration.nix # machine-specific NixOS settings + ├── environments.nix # enables profiles via my.profiles.* / my.hardware.* options + ├── disks.nix + └── hardware-configuration.nix +``` + +### Profile / Module Pattern + +Every module under `modules/` follows the same structure: + +```nix +{ config, lib, pkgs, ... }: +let cfg = config.my.profiles.; in +{ + options.my.profiles..enable = lib.mkEnableOption "..."; + config = lib.mkIf cfg.enable { ... }; +} +``` + +Namespaces in use: +- `my.profiles.*` — application/service profiles +- `my.hardware.*` — hardware profiles +- `my.services.*` — infrastructure services + +Profiles are enabled per-machine in `machines//environments.nix`. + +### Unstable Packages + +`pkgs.unstable` is available everywhere via an overlay defined in `machines/configuration.nix`. Use it when a package isn't in the pinned stable channel (`nixpkgs/nixos-25.11`). + +### Homepage Dashboard Integration + +Modules that expose a web UI can self-register with the homepage dashboard by adding to `my.homepage.services`: + +```nix +my.homepage.services = [{ + group = "Services"; + name = "My Service"; + description = "..."; + href = "http://${hostName}:PORT"; + icon = "si-iconname"; # optional +}]; +``` + +### Adding a New Service Module + +1. Create `modules/environments//default.nix` following the profile pattern above. +2. Add `./environments/` to `modules/environments/default.nix` (or the relevant `default.nix`). +3. Enable it in the target machine's `machines//environments.nix`. From cbdb42f333323e6a46086015c18b83de5cf6d383 Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Mon, 1 Jun 2026 10:02:27 +0200 Subject: [PATCH 21/24] chore: Flake Update --- flake.lock | 68 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/flake.lock b/flake.lock index fd055f3..104113c 100644 --- a/flake.lock +++ b/flake.lock @@ -21,11 +21,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1777988971, - "narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=", + "lastModified": 1778716662, + "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff", + "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", "type": "github" }, "original": { @@ -42,11 +42,11 @@ ] }, "locked": { - "lastModified": 1775087534, - "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", + "lastModified": 1778716662, + "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", + "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", "type": "github" }, "original": { @@ -69,11 +69,11 @@ ] }, "locked": { - "lastModified": 1776796298, - "narHash": "sha256-PcRvlWayisPSjd0UcRQbhG8Oqw78AcPE6x872cPRHN8=", + "lastModified": 1778507602, + "narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "3cfd774b0a530725a077e17354fbdb87ea1c4aad", + "rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a", "type": "github" }, "original": { @@ -92,11 +92,11 @@ "nixpkgs-regression": "nixpkgs-regression" }, "locked": { - "lastModified": 1778249748, - "narHash": "sha256-D+3kaW4NUUaiFTsEpmregtGZVniljBThg/O0JIjRL8k=", + "lastModified": 1780261128, + "narHash": "sha256-5lhByGNj1cXg9sKllVyJs0F1pMMiujTa2ErK1El2S+8=", "owner": "NixOS", "repo": "nix", - "rev": "616df97974fd29b79f83502f63854d6d471ee055", + "rev": "b4f997014c512b1513b0b61418e693d1c6bba7c1", "type": "github" }, "original": { @@ -142,12 +142,15 @@ } }, "nixos-hardware": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, "locked": { - "lastModified": 1778143761, - "narHash": "sha256-lkesY6x2X2qxlqLM7CT2iM/0rP2JB7fruPN3h8POXmI=", + "lastModified": 1780065812, + "narHash": "sha256-SCSLUKBmwlSLGQ8Xbr8PjRFtiHNk0l9ktqkcmqdBkfE=", "owner": "NixOS", "repo": "nixos-hardware", - "rev": "3bcaa367d4c550d687a17ac792fd5cda214ee871", + "rev": "b76b5639c0593e0aeb0b5879ad62d4b30596c144", "type": "github" }, "original": { @@ -158,11 +161,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1771903837, - "narHash": "sha256-jEA8WggGKtMFeNeCKq3NK8cLEjJmG6/RLUElYYbBZ0E=", - "rev": "e764fc9a405871f1f6ca3d1394fb422e0a0c3951", + "lastModified": 1778003029, + "narHash": "sha256-amc4Y3GF3+anUi7IJeLVzf7hVqLb3ZqCGzYtkVyp7Qw=", + "rev": "0c88e1f2bdb93d5999019e99cb0e61e1fe2af4c5", "type": "tarball", - "url": "https://releases.nixos.org/nixos/25.11/nixos-25.11.6495.e764fc9a4058/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixos/25.11/nixos-25.11.10470.0c88e1f2bdb9/nixexprs.tar.xz" }, "original": { "type": "tarball", @@ -218,11 +221,11 @@ }, "nixpkgs-unstable": { "locked": { - "lastModified": 1777954456, - "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", + "lastModified": 1779560665, + "narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", + "rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786", "type": "github" }, "original": { @@ -233,11 +236,24 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1778003029, - "narHash": "sha256-q/nkKLDtHIyLjZpKhWk3cSK5IYsFqtMd6UtXF3ddjgA=", + "lastModified": 1767892417, + "narHash": "sha256-8bW3q88CEg2u4hSP66Vf4lpbLonHz7hqDNBMcCY7E9U=", + "rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre924538.3497aa5c9457/nixexprs.tar.xz" + }, + "original": { + "type": "tarball", + "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1779796641, + "narHash": "sha256-ZsIrKmhp4vbBXoXXmR/tBXA/UCsAQiJL9vsgZEduhVY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "0c88e1f2bdb93d5999019e99cb0e61e1fe2af4c5", + "rev": "25f538306313eae3927264466c70d7001dcea1df", "type": "github" }, "original": { @@ -252,7 +268,7 @@ "nix": "nix", "nixos-generators": "nixos-generators", "nixos-hardware": "nixos-hardware", - "nixpkgs": "nixpkgs_2", + "nixpkgs": "nixpkgs_3", "nixpkgs-unstable": "nixpkgs-unstable" } } From 18d1ce711dabf8bdfec6e790a882475c60333e04 Mon Sep 17 00:00:00 2001 From: Finn Date: Tue, 9 Jun 2026 08:10:45 +0200 Subject: [PATCH 22/24] chore(HA): Removed uneccessary Unstable Overlay import --- modules/environments/home-assistant/default.nix | 5 ----- 1 file changed, 5 deletions(-) diff --git a/modules/environments/home-assistant/default.nix b/modules/environments/home-assistant/default.nix index 759a54f..07cbc0c 100644 --- a/modules/environments/home-assistant/default.nix +++ b/modules/environments/home-assistant/default.nix @@ -11,11 +11,6 @@ let hostName = config.networking.hostName; in { - imports = [ - # services.openthread-border-router isn't in nixos-25.11; pull from - # nixpkgs-unstable. Package comes from the existing unstable overlay. - "${self.inputs.nixpkgs-unstable}/nixos/modules/services/home-automation/openthread-border-router.nix" - ]; options.my.profiles.home-assistant = with lib; { enable = mkEnableOption "Home Automation"; From 17c3a3189fe8d5d6322e16caf79d3801b1efcd9f Mon Sep 17 00:00:00 2001 From: Finn Date: Tue, 9 Jun 2026 08:16:08 +0200 Subject: [PATCH 23/24] chore(NixOS Version): Update to 26.05 --- flake.nix | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 8785dae..362e14f 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,7 @@ inputs = { nix.url = "github:NixOS/nix"; - nixpkgs.url = "nixpkgs/nixos-25.11"; + nixpkgs.url = "nixpkgs/nixos-26.05"; nixpkgs-unstable.url = "nixpkgs/nixos-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; nixos-hardware.url = "github:NixOS/nixos-hardware"; @@ -11,7 +11,6 @@ url = "github:nix-community/nixos-generators"; inputs.nixpkgs.follows = "nixpkgs"; }; - # hyprland.url = "github:hyprwm/Hyprland"; }; From 6d0684610ee01c70fdc47c38476036e02c7e0ede Mon Sep 17 00:00:00 2001 From: "finn.markwitz" Date: Tue, 9 Jun 2026 08:28:11 +0200 Subject: [PATCH 24/24] chore: Flake Update --- flake.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/flake.lock b/flake.lock index 104113c..da68131 100644 --- a/flake.lock +++ b/flake.lock @@ -92,11 +92,11 @@ "nixpkgs-regression": "nixpkgs-regression" }, "locked": { - "lastModified": 1780261128, - "narHash": "sha256-5lhByGNj1cXg9sKllVyJs0F1pMMiujTa2ErK1El2S+8=", + "lastModified": 1780953488, + "narHash": "sha256-L6W9WD6QmaMHqZnO0OXO+B1bWvqBIkXcHQdvDfrL0ZI=", "owner": "NixOS", "repo": "nix", - "rev": "b4f997014c512b1513b0b61418e693d1c6bba7c1", + "rev": "5c38ac738636cf70537f02c30ab4ebdd0c7c4078", "type": "github" }, "original": { @@ -146,11 +146,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1780065812, - "narHash": "sha256-SCSLUKBmwlSLGQ8Xbr8PjRFtiHNk0l9ktqkcmqdBkfE=", + "lastModified": 1780310866, + "narHash": "sha256-fPBRVf6A5xlACYcOI59shGrjURuvwu0lRsDoSCEXt/I=", "owner": "NixOS", "repo": "nixos-hardware", - "rev": "b76b5639c0593e0aeb0b5879ad62d4b30596c144", + "rev": "4ed851c979641e28597a05086332d75cdc9e395f", "type": "github" }, "original": { @@ -161,15 +161,15 @@ }, "nixpkgs": { "locked": { - "lastModified": 1778003029, - "narHash": "sha256-amc4Y3GF3+anUi7IJeLVzf7hVqLb3ZqCGzYtkVyp7Qw=", - "rev": "0c88e1f2bdb93d5999019e99cb0e61e1fe2af4c5", + "lastModified": 1780453794, + "narHash": "sha256-hhAl/iKiurXPn7rdzDgiSuRB8tqOB6f0buWkh8Y9mkY=", + "rev": "6b316287bae2ee04c9b93c8c858d930fd07d7338", "type": "tarball", - "url": "https://releases.nixos.org/nixos/25.11/nixos-25.11.10470.0c88e1f2bdb9/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.1183.6b316287bae2/nixexprs.tar.xz" }, "original": { "type": "tarball", - "url": "https://channels.nixos.org/nixos-25.11/nixexprs.tar.xz" + "url": "https://channels.nixos.org/nixos-26.05/nixexprs.tar.xz" } }, "nixpkgs-23-11": { @@ -221,11 +221,11 @@ }, "nixpkgs-unstable": { "locked": { - "lastModified": 1779560665, - "narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=", + "lastModified": 1780749050, + "narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786", + "rev": "a799d3e3886da994fa307f817a6bc705ae538eeb", "type": "github" }, "original": { @@ -249,16 +249,16 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1779796641, - "narHash": "sha256-ZsIrKmhp4vbBXoXXmR/tBXA/UCsAQiJL9vsgZEduhVY=", + "lastModified": 1780902259, + "narHash": "sha256-q8yYEC5f1mFlQO9RGna4LTc9QrcvWunX6FYp83munkQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "25f538306313eae3927264466c70d7001dcea1df", + "rev": "bd0ff2d3eac24699c3664d5966b9ef36f388e2ca", "type": "github" }, "original": { "id": "nixpkgs", - "ref": "nixos-25.11", + "ref": "nixos-26.05", "type": "indirect" } },