284 lines
7.8 KiB
Markdown
284 lines
7.8 KiB
Markdown
# Pi
|
|
|
|
Rust service and deployment workflow for the Raspberry Pi at Noisebridge.
|
|
|
|
The current recommended setup is:
|
|
|
|
1. run Raspberry Pi OS Lite on the Pi
|
|
2. keep the Pi itself free of Nix
|
|
3. build a static `aarch64` Noisebell binary on your laptop with Nix
|
|
4. copy the binary, secrets, and systemd service to the Pi over SSH
|
|
|
|
This avoids the Raspberry Pi Zero 2 W NixOS boot issues while still keeping the application build reproducible.
|
|
|
|
## What stays on Raspberry Pi OS
|
|
|
|
- bootloader
|
|
- kernel
|
|
- firmware
|
|
- Wi-Fi and local networking
|
|
- SSH base access
|
|
- Tailscale package/runtime
|
|
- Avahi package/runtime
|
|
|
|
## What Nix manages
|
|
|
|
- building a static `noisebell` binary for `aarch64-linux`
|
|
- the exact app binary you deploy
|
|
- encrypted secrets in the repo
|
|
- repeatable deployment from your laptop
|
|
|
|
## Initial Pi OS setup
|
|
|
|
### 1. Flash Raspberry Pi OS Lite
|
|
|
|
```sh
|
|
curl -L "https://downloads.raspberrypi.org/raspios_lite_arm64_latest" | xz -d -c | sudo dd of=/dev/sdb bs=16M conv=fsync status=progress && sync
|
|
```
|
|
|
|
### 2. Configure the flashed SD card
|
|
|
|
Configure it for:
|
|
|
|
- Wi-Fi on `Noisebridge`
|
|
- SSH enabled
|
|
- serial enabled if you want a recovery console
|
|
|
|
The helper script is:
|
|
|
|
```sh
|
|
sudo scripts/configure-pios-sd.sh /run/media/jet/bootfs /run/media/jet/rootfs
|
|
```
|
|
|
|
This setup expects SSH key login for user `pi`; it does not configure a password.
|
|
|
|
### 3. Boot the Pi and verify SSH
|
|
|
|
After boot, verify SSH works:
|
|
|
|
```sh
|
|
ssh pi@noisebell-pi.local
|
|
```
|
|
|
|
## Add the Pi host key to age recipients
|
|
|
|
The deploy flow decrypts secrets locally on your laptop, but the Pi host key should still be a recipient for the Pi-facing secrets so the repo stays accurate.
|
|
|
|
Grab the Pi host key:
|
|
|
|
```sh
|
|
ssh-keyscan noisebell-pi.local 2>/dev/null | grep ed25519
|
|
```
|
|
|
|
Add that key to `secrets/secrets.nix` for:
|
|
|
|
- `pi-to-cache-key.age`
|
|
- `cache-to-pi-key.age`
|
|
- `tailscale-auth-key.age`
|
|
|
|
Then refresh recipients if needed:
|
|
|
|
```sh
|
|
cd secrets
|
|
agenix -r
|
|
```
|
|
|
|
## Edit secrets
|
|
|
|
```sh
|
|
cd secrets
|
|
agenix -e pi-to-cache-key.age
|
|
agenix -e cache-to-pi-key.age
|
|
agenix -e tailscale-auth-key.age
|
|
```
|
|
|
|
These stay encrypted in git. The deploy script decrypts them locally on your laptop and copies the plaintext files to the Pi as root-only files.
|
|
|
|
## Deploy to Raspberry Pi OS
|
|
|
|
From your laptop:
|
|
|
|
```sh
|
|
scripts/deploy-pios-pi.sh pi@noisebell-pi.local
|
|
```
|
|
|
|
If Home Assistant is on a fixed LAN IP, set that explicitly during deploy:
|
|
|
|
```sh
|
|
HOME_ASSISTANT_BASE_URL=http://10.21.0.43:8123 scripts/deploy-pios-pi.sh pi@100.66.45.36
|
|
```
|
|
|
|
If you only know the IP:
|
|
|
|
```sh
|
|
scripts/deploy-pios-pi.sh pi@10.21.x.x
|
|
```
|
|
|
|
That script:
|
|
|
|
1. builds `.#packages.aarch64-linux.noisebell-static` locally
|
|
2. builds `.#packages.aarch64-linux.noisebell-relay-static` locally
|
|
3. decrypts the Pi-facing secrets locally with `agenix`
|
|
4. uploads the binaries and secrets to the Pi
|
|
5. installs Tailscale and Avahi if needed
|
|
6. writes `/etc/noisebell/noisebell.env`
|
|
7. writes `/etc/noisebell/noisebell-relay.env`
|
|
8. installs `noisebell.service` and `noisebell-relay.service`
|
|
9. enables and starts both services
|
|
10. runs `tailscale up` with the decrypted auth key
|
|
|
|
## Files written on the Pi
|
|
|
|
The deploy script creates:
|
|
|
|
- `/opt/noisebell/releases/<timestamp>/noisebell`
|
|
- `/opt/noisebell/releases/<timestamp>/noisebell-relay`
|
|
- `/opt/noisebell/current` -> current release symlink
|
|
- `/etc/noisebell/pi-to-cache-key`
|
|
- `/etc/noisebell/cache-to-pi-key`
|
|
- `/etc/noisebell/relay-webhook-secret`
|
|
- `/etc/noisebell/homeassistant-webhook-id`
|
|
- `/etc/noisebell/tailscale-auth-key`
|
|
- `/etc/noisebell/noisebell.env`
|
|
- `/etc/noisebell/noisebell-relay.env`
|
|
- `/etc/systemd/system/noisebell.service`
|
|
- `/etc/systemd/system/noisebell-relay.service`
|
|
|
|
All secret files are root-only.
|
|
|
|
## Tailscale
|
|
|
|
Tailscale is kept on Raspberry Pi OS rather than NixOS.
|
|
|
|
The deploy script:
|
|
|
|
- installs the Tailscale package if missing
|
|
- enables `tailscaled`
|
|
- runs `tailscale up --auth-key=... --hostname=noisebell-pi`
|
|
|
|
So Tailscale stays part of the base OS, while its auth key is still managed as an encrypted `age` secret in this repo.
|
|
|
|
## Later updates
|
|
|
|
Normal iteration is just rerunning the deploy script:
|
|
|
|
```sh
|
|
scripts/deploy-pios-pi.sh pi@noisebell-pi.local
|
|
```
|
|
|
|
That rebuilds the binary locally, uploads a new release, refreshes secrets, and restarts the service.
|
|
|
|
## Service configuration
|
|
|
|
The deployed service uses these environment variables:
|
|
|
|
| Variable | Default | Description |
|
|
|---|---|---|
|
|
| `NOISEBELL_GPIO_PIN` | `17` | GPIO pin number |
|
|
| `NOISEBELL_DEBOUNCE_MS` | `50` | Debounce delay in milliseconds |
|
|
| `NOISEBELL_PORT` | `80` | HTTP server port |
|
|
| `NOISEBELL_ENDPOINT_URL` | required | Webhook URL to POST state changes to |
|
|
| `NOISEBELL_RETRY_ATTEMPTS` | `3` | Webhook retry count |
|
|
| `NOISEBELL_RETRY_BASE_DELAY_SECS` | `1` | Exponential backoff base delay |
|
|
| `NOISEBELL_HTTP_TIMEOUT_SECS` | `10` | Outbound request timeout |
|
|
| `NOISEBELL_BIND_ADDRESS` | `0.0.0.0` | HTTP bind address |
|
|
| `NOISEBELL_ACTIVE_LOW` | `true` | Low GPIO = door open |
|
|
|
|
## Relay service configuration
|
|
|
|
The optional relay service accepts authenticated webhooks from cache-service and forwards them to Home Assistant on the local network.
|
|
|
|
| Variable | Default | Description |
|
|
|---|---|---|
|
|
| `NOISEBELL_RELAY_PORT` | `8090` | HTTP port for the relay webhook endpoint |
|
|
| `NOISEBELL_RELAY_BIND_ADDRESS` | `0.0.0.0` | HTTP bind address |
|
|
| `NOISEBELL_RELAY_TARGET_BASE_URL` | `http://10.21.0.43:8123` | Base URL for Home Assistant |
|
|
| `NOISEBELL_RELAY_TARGET_WEBHOOK_ID` | required | Home Assistant webhook ID |
|
|
| `NOISEBELL_RELAY_INBOUND_API_KEY` | required | Bearer token expected from cache-service |
|
|
| `NOISEBELL_RELAY_RETRY_ATTEMPTS` | `3` | Forward retry count |
|
|
| `NOISEBELL_RELAY_RETRY_BASE_DELAY_SECS` | `1` | Exponential backoff base delay |
|
|
| `NOISEBELL_RELAY_HTTP_TIMEOUT_SECS` | `10` | Outbound request timeout |
|
|
|
|
If `.local` resolution is reliable on your Pi, you can override the deploy default with `HOME_ASSISTANT_BASE_URL=http://homeassistant.local:8123`.
|
|
|
|
Example cache target for the relay:
|
|
|
|
```nix
|
|
{
|
|
services.noisebell-cache.outboundWebhooks = [
|
|
{
|
|
url = "http://noisebell-pi.local:8090/webhook";
|
|
secretFile = /run/agenix/noisebell-relay-webhook-secret;
|
|
}
|
|
];
|
|
}
|
|
```
|
|
|
|
## Home Assistant workflow
|
|
|
|
The working Home Assistant path is:
|
|
|
|
```text
|
|
Pi door sensor -> cache-service -> Pi relay -> Home Assistant webhook automation
|
|
```
|
|
|
|
This keeps cache-service as the fanout source while still letting Home Assistant stay LAN-only.
|
|
|
|
Setup summary:
|
|
|
|
1. Pi still posts raw door events to cache via `NOISEBELL_ENDPOINT_URL`
|
|
2. cache-service fans out to `http://noisebell-pi:8090/webhook` using `relay-webhook-secret.age`
|
|
3. `noisebell-relay` forwards the payload to Home Assistant using `homeassistant-webhook-id.age`
|
|
4. Home Assistant automation triggers on the webhook and switches devices based on `trigger.json.status`
|
|
|
|
Payload received by Home Assistant:
|
|
|
|
```json
|
|
{
|
|
"status": "open",
|
|
"timestamp": 1774336193
|
|
}
|
|
```
|
|
|
|
Example Home Assistant automation:
|
|
|
|
```yaml
|
|
alias: noisebell
|
|
description: ""
|
|
triggers:
|
|
- trigger: webhook
|
|
allowed_methods:
|
|
- POST
|
|
local_only: false
|
|
webhook_id: "-roWWM0JVCWSispwyHXlcKtjI"
|
|
conditions: []
|
|
actions:
|
|
- if:
|
|
- condition: template
|
|
value_template: "{{ trigger.json.status == 'open' }}"
|
|
then:
|
|
- action: switch.turn_on
|
|
target:
|
|
entity_id: switch.mini_smart_plug_socket_1
|
|
else:
|
|
- if:
|
|
- condition: template
|
|
value_template: "{{ trigger.json.status == 'closed' }}"
|
|
then:
|
|
- action: switch.turn_off
|
|
target:
|
|
entity_id: switch.mini_smart_plug_socket_1
|
|
mode: single
|
|
```
|
|
|
|
Important: Home Assistant webhook IDs are exact. If the automation shows a leading `-`, keep that same leading `-` in `homeassistant-webhook-id.age`.
|
|
|
|
## API
|
|
|
|
All endpoints require `Authorization: Bearer <token>`.
|
|
|
|
**`GET /`**
|
|
|
|
```json
|
|
{"status": "open", "timestamp": 1710000000}
|
|
```
|