# nr-flow-validator — Declarative Node-RED flows with conftest validation 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 RPI_HOST := "root@192.168.81.147" MQTT_HOST := "192.168.81.147" 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}}' # 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}}' # 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}} # 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}} # Show live Node-RED flow summary (node count, function names, button wires, mqtt-out topics) nr-show: python3 scripts/diag.py show # Diff deployed Node-RED flow against repo (excluding random uuid ids) nr-diff FLOW="zigbee-monitor": generate python3 scripts/diag.py diff output/{{FLOW}}.json # Tail Node-RED container logs nr-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' # Tail mosquitto container logs mqtt-logs: ssh {{RPI_HOST}} 'docker logs --tail 50 -f mosquitto' # Restart a service container restart SERVICE: ssh {{RPI_HOST}} 'docker restart {{SERVICE}}' # Restart the full stack 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' # Restart just the doorbell-listener restart-doorbell: ssh {{RPI_HOST}} 'docker restart doorbell-listener' # 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" # 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: @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" # Show paired zigbee devices and their friendly names z2m-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: 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}}" # 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" # Watch live MQTT traffic (matches the z2m base_topic by default) mqtt-watch TOPIC="zigbee2mqtt/#": uv run --with paho-mqtt python3 scripts/mqtt.py watch --topic '{{TOPIC}}'