# nr-flow-validator — Declarative Node-RED flows + infra stack config set export PATH := "/home/david/.local/share/mise/shims:" + env("PATH") RPI_HOST := "root@192.168.81.147" MQTT_HOST := "192.168.81.147" MQTT_PORT := "1883" RPI_DIR := "/root" NR_PORT := "1880" # Default: generate + validate the zigbee-monitor flow + validate infra config default: nodered-validate infra-validate # ============================================================ # NODERED — flow generation, validation, deployment, inspection # ============================================================ # 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" # Compile all flow YAMLs nodered-generate-all: @for f in flows/*.yaml; do \ name=$(basename "$f" .yaml); \ just nodered-generate "$name"; \ done # 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 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 the Node-RED container logs nodered-logs: ssh {{RPI_HOST}} 'docker logs --tail 50 -f nodered' # 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}}" # 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" # 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 on the RPi infra-restart-all: ssh {{RPI_HOST}} 'docker restart mosquitto zigbee2mqtt nodered doorbell-listener' # Tail the mosquitto MQTT broker logs mosquitto-logs: ssh {{RPI_HOST}} 'docker logs --tail 50 -f mosquitto' # ============================================================ # ZIGBEE — zigbee2mqtt management # ============================================================ # Tail the zigbee2mqtt container logs zigbee-logs: ssh {{RPI_HOST}} 'docker logs --tail 50 -f zigbee2mqtt' # 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 zigbee-logs" # 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 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}}" # 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 (on/off, brightness, etc.) zigbee-state: ssh {{RPI_HOST}} 'cat {{RPI_DIR}}/zigbee2mqtt/state.json' | python3 -m json.tool | head -60 # ============================================================ # MQTT — publish, subscribe, watch, simulate devices # ============================================================ # 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}}' # 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'