fix: use stable by-id path for dongle, add flashing docs, BL706 firmware v1.00.01

This commit is contained in:
2026-08-01 21:22:45 -07:00
parent 9e85a6d538
commit da72ab3cba
6 changed files with 448 additions and 164 deletions
+177 -163
View File
@@ -1,88 +1,6 @@
# nr-flow-validator — Declarative Node-RED flows with conftest validation
# nr-flow-validator — Declarative Node-RED flows + infra stack config
set export
# Default: generate a flow, validate, and show results
default: validate
# ---- Flow generation ----
# Generate Node-RED flow JSON from a YAML spec
generate FLOW="zigbee-monitor":
@echo "=== Generating {{FLOW}} ==="
python3 scripts/flow2json.py flows/{{FLOW}}.yaml > output/{{FLOW}}.json
@echo "Wrote output/{{FLOW}}.json"
# Generate all flows
generate-all:
@for f in flows/*.yaml; do \
name=$(basename "$f" .yaml); \
just generate "$name"; \
done
# ---- Validation ----
# Validate a generated flow against Rego policies
validate FLOW="zigbee-monitor": generate
@echo "=== Validating {{FLOW}} ==="
conftest test output/{{FLOW}}.json -p policy/ --namespace nodered --no-color
# Validate all flows
validate-all:
@for f in output/*.json; do \
echo "=== Validating $(basename "$f") ==="; \
conftest test "$$f" -p policy/ --namespace nodered --no-color; \
done
# ---- Deployment ----
# Deploy flow to Node-RED instance
deploy FLOW="zigbee-monitor" HOST="http://192.168.81.147:1880": generate validate
@echo "=== Deploying {{FLOW}} to {{HOST}} ==="
curl -s -o /dev/null -w "HTTP %{http_code}" \
-X POST {{HOST}}/flows \
-H "Content-Type: application/json" \
-H "Node-RED-Deployment-Type: full" \
-d @output/{{FLOW}}.json
@echo ""
# Quick deploy (skip validation)
deploy-quick FLOW="zigbee-monitor" HOST="http://192.168.81.147:1880": generate
@echo "=== Quick deploy {{FLOW}} to {{HOST}} ==="
curl -s -o /dev/null -w "HTTP %{http_code}" \
-X POST {{HOST}}/flows \
-H "Content-Type: application/json" \
-H "Node-RED-Deployment-Type: full" \
-d @output/{{FLOW}}.json
@echo ""
# ---- Viewing ----
# Fetch and display current flows from Node-RED
pull HOST="http://192.168.81.147:1880":
curl -s {{HOST}}/flows | python3 -m json.tool
# Open dashboard in browser
dashboard:
xdg-open http://192.168.81.147:1880/dashboard 2>/dev/null || open http://192.168.81.147:1880/dashboard 2>/dev/null || echo "Open: http://192.168.81.147:1880/dashboard"
# ---- Development ----
# Watch for changes and re-validate
watch FLOW="zigbee-monitor":
@while true; do \
inotifywait -q -e modify flows/{{FLOW}}.yaml policy/*.rego; \
clear; just validate {{FLOW}}; \
done
# Create a new flow from template
new NAME:
@cp flows/zigbee-monitor.yaml flows/{{NAME}}.yaml
@echo "Created flows/{{NAME}}.yaml — edit and run 'just validate {{NAME}}'"
# ---- Diagnostics ----
# All commands target the live RPi at $RPI_HOST (default root@192.168.81.147).
# Override with: just RPI_HOST=user@host mqtt-pub topic msg
PATH := "/home/david/.local/share/mise/shims:" + env("PATH")
RPI_HOST := "root@192.168.81.147"
MQTT_HOST := "192.168.81.147"
@@ -90,124 +8,220 @@ MQTT_PORT := "1883"
RPI_DIR := "/root"
NR_PORT := "1880"
# Run a command on the RPi over SSH
rpi CMD *ARGS:
ssh -o ConnectTimeout=5 {{RPI_HOST}} '{{CMD}} {{ARGS}}'
# Default: generate + validate the zigbee-monitor flow + validate infra config
default: nodered-validate infra-validate
# Publish to MQTT broker on the RPi
mqtt-pub TOPIC PAYLOAD:
uv run --with paho-mqtt python3 scripts/mqtt.py pub --topic '{{TOPIC}}' --payload '{{PAYLOAD}}'
# ============================================================
# NODERED — flow generation, validation, deployment, inspection
# ============================================================
# Subscribe to MQTT broker on the RPi for N seconds
mqtt-sub TOPIC SECONDS="5":
uv run --with paho-mqtt python3 scripts/mqtt.py sub --topic '{{TOPIC}}' --seconds {{SECONDS}}
# Compile a flow YAML into deployable Node-RED JSON
nodered-generate FLOW="zigbee-monitor":
@echo "=== Generating {{FLOW}} ==="
python3 scripts/flow2json.py flows/{{FLOW}}.yaml > output/{{FLOW}}.json
@echo "Wrote output/{{FLOW}}.json"
# Publish a button action and watch what /set topics fire (E2E test)
mqtt-button ACTION="single":
uv run --with paho-mqtt python3 scripts/diag.py mqtt-button --action {{ACTION}}
# Compile all flow YAMLs
nodered-generate-all:
@for f in flows/*.yaml; do \
name=$(basename "$f" .yaml); \
just nodered-generate "$name"; \
done
# Show live Node-RED flow summary (node count, function names, button wires, mqtt-out topics)
nr-show:
# Run Rego policy checks against a generated Node-RED flow
nodered-validate FLOW="zigbee-monitor": nodered-generate
@echo "=== Validating {{FLOW}} ==="
conftest test output/{{FLOW}}.json -p policy/ --namespace nodered --no-color
# Run Rego checks against all generated flows
nodered-validate-all:
@for f in output/*.json; do \
echo "=== Validating $(basename "$f") ==="; \
conftest test "$$f" -p policy/ --namespace nodered --no-color; \
done
# Deploy a validated flow to the Node-RED server, then validate the compose stack
nodered-deploy FLOW="zigbee-monitor" HOST="http://192.168.81.147:1880": nodered-generate nodered-validate
@echo "=== Deploying {{FLOW}} to {{HOST}} ==="
curl -s -o /dev/null -w "HTTP %{http_code}" \
-X POST {{HOST}}/flows \
-H "Content-Type: application/json" \
-H "Node-RED-Deployment-Type: full" \
-d @output/{{FLOW}}.json
@echo ""
just infra-validate
# Deploy a flow without Rego validation (faster, use after local edits)
nodered-deploy-skip-checks FLOW="zigbee-monitor" HOST="http://192.168.81.147:1880": nodered-generate
@echo "=== Deploying {{FLOW}} to {{HOST}} (skip Rego checks) ==="
curl -s -o /dev/null -w "HTTP %{http_code}" \
-X POST {{HOST}}/flows \
-H "Content-Type: application/json" \
-H "Node-RED-Deployment-Type: full" \
-d @output/{{FLOW}}.json
@echo ""
# Fetch current flows from the Node-RED server (raw JSON)
nodered-pull HOST="http://192.168.81.147:1880":
curl -s {{HOST}}/flows | python3 -m json.tool
# Show live Node-RED flow summary (node types, function names, wire topology)
nodered-show:
python3 scripts/diag.py show
# Diff deployed Node-RED flow against repo (excluding random uuid ids)
nr-diff FLOW="zigbee-monitor": generate
# Diff the generated flow against what's deployed (ids excluded)
nodered-diff FLOW="zigbee-monitor": nodered-generate
python3 scripts/diag.py diff output/{{FLOW}}.json
# Tail Node-RED container logs
nr-logs:
# Tail the Node-RED container logs
nodered-logs:
ssh {{RPI_HOST}} 'docker logs --tail 50 -f nodered'
# Tail zigbee2mqtt container logs
z2m-logs:
ssh {{RPI_HOST}} 'docker logs --tail 50 -f zigbee2mqtt'
# Open the Node-RED editor in browser
nodered-editor:
xdg-open http://{{MQTT_HOST}}:{{NR_PORT}} 2>/dev/null || open http://{{MQTT_HOST}}:{{NR_PORT}} 2>/dev/null || echo "Open: http://{{MQTT_HOST}}:{{NR_PORT}}"
# Tail mosquitto container logs
mqtt-logs:
ssh {{RPI_HOST}} 'docker logs --tail 50 -f mosquitto'
# Open the Node-RED Dashboard 2.0 SPA in browser
nodered-dashboard:
xdg-open http://{{MQTT_HOST}}:{{NR_PORT}}/dashboard 2>/dev/null || open http://{{MQTT_HOST}}:{{NR_PORT}}/dashboard 2>/dev/null || echo "Open: http://{{MQTT_HOST}}:{{NR_PORT}}/dashboard"
# Restart a service container
restart SERVICE:
# Watch flow YAML and Rego policy files; re-validate on change
nodered-watch FLOW="zigbee-monitor":
@while true; do \
inotifywait -q -e modify flows/{{FLOW}}.yaml policy/*.rego; \
clear; just nodered-validate {{FLOW}}; \
done
# Scaffold a new flow YAML from the zigbee-monitor template
nodered-new NAME:
@cp flows/zigbee-monitor.yaml flows/{{NAME}}.yaml
@echo "Created flows/{{NAME}}.yaml — edit and run 'just nodered-validate {{NAME}}'"
# ============================================================
# INFRA — git sync, compose validation, remote host management
# ============================================================
# Commit local changes, push to Gitea, pull on RPi, validate compose
infra-deploy MESSAGE="sync infra":
@echo "=== Committing ==="
git add -A
git diff --cached --quiet || git commit -m '{{MESSAGE}}'
@echo "=== Pushing to Gitea ==="
git push
@echo "=== Pulling on RPi ==="
ssh {{RPI_HOST}} "cd {{RPI_DIR}}/nr-flow-validator && git pull"
@echo "=== Validating compose stack ==="
just infra-validate
@echo "=== Done ==="
# Run Rego policy checks against the compose file and zigbee2mqtt config
infra-validate:
conftest test infra/compose.yaml -p policy/ --namespace compose \
--data infra/zigbee2mqtt/configuration.yaml
# Run an arbitrary command on the RPi over SSH
infra-ssh CMD *ARGS:
ssh -o ConnectTimeout=5 {{RPI_HOST}} '{{CMD}} {{ARGS}}'
# Restart any Docker service on the RPi (e.g. just infra-restart mosquitto)
infra-restart SERVICE:
ssh {{RPI_HOST}} 'docker restart {{SERVICE}}'
# Restart the full stack
restart-all:
# Restart the full stack on the RPi
infra-restart-all:
ssh {{RPI_HOST}} 'docker restart mosquitto zigbee2mqtt nodered doorbell-listener'
# Tail doorbell-listener container logs
doorbell-logs:
ssh {{RPI_HOST}} 'docker logs --tail 50 -f doorbell-listener'
# Tail the mosquitto MQTT broker logs
mosquitto-logs:
ssh {{RPI_HOST}} 'docker logs --tail 50 -f mosquitto'
# Restart just the doorbell-listener
restart-doorbell:
ssh {{RPI_HOST}} 'docker restart doorbell-listener'
# ============================================================
# ZIGBEE — zigbee2mqtt management
# ============================================================
# Publish a test alert to ntfy (used for E2E testing the listener).
# Usage: just doorbell-publish "Title" "Body"
doorbell-publish TITLE BODY="Someone is at the door":
curl -sS -X POST "https://ntfy.sh/ALERT_klubhaus_topic_test" \
-H "Title: {{TITLE}}" \
-H "Priority: high" \
-d "{{BODY}}" \
-w "\nHTTP %{http_code}\n"
# Tail the zigbee2mqtt container logs
zigbee-logs:
ssh {{RPI_HOST}} 'docker logs --tail 50 -f zigbee2mqtt'
# Build + (re)start the listener (after edits to infra/doorbell-listener/)
doorbell-rebuild:
ssh {{RPI_HOST}} 'cd {{RPI_DIR}}/nr-flow-validator && docker compose build doorbell-listener && docker compose up -d doorbell-listener'
# Patch zigbee2mqtt's bridge.js for BLZ adapter compatibility.
# The Pine64 latest-dev image crashes on start because Bridge.start() calls
# firstCoordinatorEndpoint().deviceIeeeAddress BEFORE registering the
# onMQTTMessage listener, so permit_join (and every other bridge MQTT
# request) silently no-ops.
#
# Run this after every fresh image pull OR after the zigbee2mqtt container
# is recreated (the patch is lost on image rebuild).
z2m-patch-bridge:
# Apply the BLZ adapter bridge.js patch (idempotent).
# Required after every fresh ghcr.io/pine64/zigbee2mqtt image pull.
zigbee-patch-bridge:
@echo "Copying patch script into zigbee2mqtt container..."
ssh {{RPI_HOST}} 'docker cp {{justfile_directory()}}/infra/zigbee2mqtt/scripts/blz-bridge-patch.js zigbee2mqtt:/tmp/blz-bridge-patch.js'
@echo "Applying patch (idempotent)..."
ssh {{RPI_HOST}} 'docker exec zigbee2mqtt node /tmp/blz-bridge-patch.js'
@echo "Restarting zigbee2mqtt..."
ssh {{RPI_HOST}} 'docker restart zigbee2mqtt'
@echo "Done. Verify with: just z2m-logs"
@echo "Done. Verify with: just zigbee-logs"
# Open permit_join + watch for device_joined events; auto-rename any
# rejoining device back to its original friendly name. Use after the
# zigbee2mqtt database has been wiped, to recover from a fresh state
# without manually re-pairing each device via the web UI.
#
# First put each device into pairing mode (ThirdReality smart plugs:
# hold the side button for ~5 seconds until the LED blinks rapidly).
# Then run this and press a device's button within the time window.
z2m-repair TIME="120":
# Open permit_join and watch for device_joined events; auto-rename rejoining devices
zigbee-repair TIME="120":
uv run --with paho-mqtt python3 {{justfile_directory()}}/scripts/z2m_repair.py --time {{TIME}}
# Rename a zigbee2mqtt device by IEEE address. Use after a device
# rejoins with a fresh ieee (or for any rename).
z2m-rename IEEE NAME:
# Rename a zigbee2mqtt device by its IEEE address
zigbee-rename IEEE NAME:
ssh {{RPI_HOST}} 'mosquitto_pub -h localhost \
-t "zigbee2mqtt/bridge/request/device/rename" \
-m "{\"from\":\"{{IEEE}}\",\"to\":\"{{NAME}}\",\"transaction\":\"rename-{{NAME}}\"}"'
@echo "Renamed {{IEEE}} -> {{NAME}}"
# Show paired zigbee devices and their friendly names
z2m-devices:
# List all paired zigbee devices with their friendly names
zigbee-devices:
ssh {{RPI_HOST}} 'cat {{RPI_DIR}}/nr-flow-validator/infra/zigbee2mqtt/configuration.yaml' > /tmp/z2m.yaml
python3 scripts/diag.py z2m-devices /tmp/z2m.yaml
# Show per-device persisted state from z2m
z2m-state:
# Show per-device persisted state (on/off, brightness, etc.)
zigbee-state:
ssh {{RPI_HOST}} 'cat {{RPI_DIR}}/zigbee2mqtt/state.json' | python3 -m json.tool | head -60
# Open the Node-RED editor
nr-editor:
xdg-open http://{{MQTT_HOST}}:{{NR_PORT}} 2>/dev/null || open http://{{MQTT_HOST}}:{{NR_PORT}} 2>/dev/null || echo "Open: http://{{MQTT_HOST}}:{{NR_PORT}}"
# ============================================================
# MQTT — publish, subscribe, watch, simulate devices
# ============================================================
# Open the Dashboard 2.0 SPA
nr-dashboard:
xdg-open http://{{MQTT_HOST}}:{{NR_PORT}}/dashboard 2>/dev/null || open http://{{MQTT_HOST}}:{{NR_PORT}}/dashboard 2>/dev/null || echo "Open: http://{{MQTT_HOST}}:{{NR_PORT}}/dashboard"
# Publish a message to any MQTT topic on the RPi broker
mqtt-pub TOPIC PAYLOAD:
uv run --with paho-mqtt python3 scripts/mqtt.py pub --topic '{{TOPIC}}' --payload '{{PAYLOAD}}'
# Watch live MQTT traffic (matches the z2m base_topic by default)
# Subscribe to an MQTT topic for N seconds and print messages
mqtt-sub TOPIC SECONDS="5":
uv run --with paho-mqtt python3 scripts/mqtt.py sub --topic '{{TOPIC}}' --seconds {{SECONDS}}
# Simulate a door button press (single|double|hold) and watch the /set response
mqtt-simulate-doorbell ACTION="single":
uv run --with paho-mqtt python3 scripts/diag.py mqtt-button --action {{ACTION}}
# Print every MQTT message matching a topic pattern (default: all zigbee2mqtt traffic)
mqtt-watch TOPIC="zigbee2mqtt/#":
uv run --with paho-mqtt python3 scripts/mqtt.py watch --topic '{{TOPIC}}'
# ============================================================
# DOORBELL — ntfy-to-smart-plug bridge management
# ============================================================
# Tail the doorbell-listener container logs
doorbell-logs:
ssh {{RPI_HOST}} 'docker logs --tail 50 -f doorbell-listener'
# Build and restart the doorbell listener after local edits
doorbell-rebuild:
ssh {{RPI_HOST}} 'cd {{RPI_DIR}}/nr-flow-validator && docker compose build doorbell-listener && docker compose up -d doorbell-listener'
# Send a test ntfy alert to trigger the doorbell listener
doorbell-test TITLE BODY="Someone is at the door":
curl -sS -X POST "https://ntfy.sh/ALERT_klubhaus_topic_test" \
-H "Title: {{TITLE}}" \
-H "Priority: high" \
-d "{{BODY}}" \
-w "\nHTTP %{http_code}\n"
# ============================================================
# MISC — standalone service logs
# ============================================================
# Tail the audio-bridge container logs
audio-bridge-logs:
ssh {{RPI_HOST}} 'docker logs --tail 50 -f audio-bridge'
# Tail the party-lock container logs
party-lock-logs:
ssh {{RPI_HOST}} 'docker logs --tail 50 -f party-lock'