From 6527e19a40551c8c3710c57e66a0844e62b53ff7 Mon Sep 17 00:00:00 2001 From: Mateo Date: Mon, 12 Oct 2026 13:46:43 +0200 Subject: [PATCH] chore: move Shelly setup scripts to pyro-sys-setup Only watchdog.js, which runs on the Shelly, stays here. --- watchdog/shelly/TESTING.md | 3 +- watchdog/shelly/check_station.py | 4 +- watchdog/shelly/device/.gitignore | 4 - watchdog/shelly/device/README.md | 60 ++------ watchdog/shelly/device/harden_shelly.sh | 138 ------------------ .../shelly/device/setup_shelly_watchdog.sh | 85 ----------- .../shelly/device/update_shelly_watchdog.sh | 67 --------- 7 files changed, 13 insertions(+), 348 deletions(-) delete mode 100644 watchdog/shelly/device/.gitignore delete mode 100755 watchdog/shelly/device/harden_shelly.sh delete mode 100755 watchdog/shelly/device/setup_shelly_watchdog.sh delete mode 100755 watchdog/shelly/device/update_shelly_watchdog.sh diff --git a/watchdog/shelly/TESTING.md b/watchdog/shelly/TESTING.md index 96160767..8eae67c1 100644 --- a/watchdog/shelly/TESTING.md +++ b/watchdog/shelly/TESTING.md @@ -62,6 +62,7 @@ python3 watchdog/shelly/check_station.py --cycle-pi # Pi or PC Trigger the Shelly watchdog for real: stop the `/health` service and wait 3 check intervals (~30 min) — the Shelly must cut outputs 0 and 1 for 2 min. To speed it up, temporarily re-upload `watchdog.js` with a short -`CHECK_INTERVAL_MS`, then restore it with `update_shelly_watchdog.sh`. +`CHECK_INTERVAL_MS`, then restore it with `setup_shelly_watchdog.sh` from +pyro-sys-setup. Exit code is non-zero if any check fails. diff --git a/watchdog/shelly/check_station.py b/watchdog/shelly/check_station.py index 0a2a3acc..f0a4cedf 100644 --- a/watchdog/shelly/check_station.py +++ b/watchdog/shelly/check_station.py @@ -210,7 +210,7 @@ def check_shelly(shelly_ip: str) -> str | None: scripts = (rpc_optional(shelly_ip, "Script.List") or {}).get("scripts", []) match = [s for s in scripts if s.get("name") == SCRIPT_NAME] if not match: - ko(f"no script named {SCRIPT_NAME} on the device (run setup_shelly_watchdog.sh)") + ko(f"no script named {SCRIPT_NAME} on the device (run setup_shelly_watchdog.sh from pyro-sys-setup)") else: script = match[0] if script.get("enable"): @@ -240,7 +240,7 @@ def check_shelly(shelly_ip: str) -> str | None: else: ko( f"output {out_id} initial_state is {config.get('initial_state')!r}, " - "expected 'on' (run harden_shelly.sh)" + "expected 'on' (run harden_shelly.sh from pyro-sys-setup)" ) # hardening (warn-level: the watchdog works without it) diff --git a/watchdog/shelly/device/.gitignore b/watchdog/shelly/device/.gitignore deleted file mode 100644 index 810ef785..00000000 --- a/watchdog/shelly/device/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Runtime artifacts produced by the helper scripts -payload.json -shelly_methods.json -shelly_config_after_hardening.json diff --git a/watchdog/shelly/device/README.md b/watchdog/shelly/device/README.md index ef928e63..33278b21 100644 --- a/watchdog/shelly/device/README.md +++ b/watchdog/shelly/device/README.md @@ -1,7 +1,7 @@ # Shelly Pi watchdog -This setup installs a Shelly script on a Shelly Pro device (works with any Pro -model that exposes the scripting RPC API, e.g. Pro 2PM, Pro 4PM). +`watchdog.js` runs on a Shelly Pro device (works with any Pro model that +exposes the scripting RPC API, e.g. Pro 2PM, Pro 4PM). It is the Shelly side of the Shelly-based watchdog. The Pi side lives in `../main_pi/watchdog.py` and power-cycles output 0 when the internet or the @@ -47,57 +47,15 @@ Make sure the Shelly is not powered by one of the outputs it cuts. ## Setup procedure -### 1. Put the Shelly on the network +### 1. Set up the Shelly -Power the Shelly, then use the **Shelly Smart Control** app (iOS / Android): +Provisioning (Wi-Fi, static IP, firmware, hardening) and uploading +`watchdog.js` are done from +[pyro-sys-setup](https://github.com/pyronear/pyro-sys-setup#shelly-setup), +which reads `watchdog.js` from a pyro-engine checkout. After changing +`watchdog.js`, upload it again with `setup_shelly_watchdog.sh` from there. -- The app connects to the device over **Bluetooth**. -- Add the device and configure it to join your local **Wi-Fi** (station mode). -- Give it a fixed address (static IP on the device, or a DHCP reservation on the - router) so the watchdog can always reach it. The examples here assume - `192.168.1.97`. - -Check it is reachable from the machine that will run the scripts: - -```bash -curl "http://192.168.1.97/rpc/Shelly.GetDeviceInfo" -``` - -### 2. Harden the Shelly - -The Shelly is dedicated to running the watchdog, so disable everything else -with `harden_shelly.sh` (Cloud, MQTT, outbound WebSocket, Wi-Fi access point, -BLE, and any webhooks/schedules), enable eco mode to reduce heat, and force the -watchdog outputs (0 and 1) to power on at boot so a Shelly restart does not -leave the Pi unpowered: - -```bash -cd watchdog/shelly/device -chmod +x harden_shelly.sh setup_shelly_watchdog.sh update_shelly_watchdog.sh -SHELLY_IP="192.168.1.97" ./harden_shelly.sh -``` - -After this the Shelly is only reachable over your main Wi-Fi (station mode), so -make sure step 1 worked first. If that link is later lost you need the physical -reset button to bring the access point back. - -### 3. Install the Shelly watchdog script - -Upload and start `watchdog.js` on the Shelly: - -```bash -SHELLY_IP="192.168.1.97" ./setup_shelly_watchdog.sh -``` - -This is the Shelly side: it polls the Pi `/health` endpoint and reboots outputs -0 and 1 if the Pi is unhealthy. To change the target URL or timings, edit -`watchdog.js` and re-run: - -```bash -SHELLY_IP="192.168.1.97" ./update_shelly_watchdog.sh -``` - -### 4. Install the Pi-side watchdog +### 2. Install the Pi-side watchdog The Pi side (`../main_pi/watchdog.py`) covers the other direction: it checks internet connectivity and the cameras, and asks the Shelly to power-cycle diff --git a/watchdog/shelly/device/harden_shelly.sh b/watchdog/shelly/device/harden_shelly.sh deleted file mode 100755 index f69c1a81..00000000 --- a/watchdog/shelly/device/harden_shelly.sh +++ /dev/null @@ -1,138 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Harden a Shelly Pro used only to run the pi_watchdog script. -# Disables cloud, remote access, radio features, webhooks and schedules that -# the local watchdog does not need, enables eco mode to reduce heat, and forces -# the watchdog outputs to power on at boot. -# -# The firmware may be a beta, so some RPC methods can be missing. Every call -# tolerates failure and the script keeps going. -# -# WARNING: this turns off the WiFi access point. -# Afterwards the device is only reachable over the main network. -# If that link is lost, physical access or a reset may be required. -# -# WARNING: this deletes ALL webhooks and schedules on the device. Only run it -# on a Shelly dedicated to the watchdog, not one shared with other automations. - -SHELLY_IP="${SHELLY_IP:-192.168.1.97}" -BASE="http://${SHELLY_IP}/rpc" -OUT_DIR="${OUT_DIR:-.}" - -# Outputs the watchdog manages; forced ON at boot so a Shelly restart does not -# leave the Pi / cameras powered off. Space-separated RPC ids. -WATCHDOG_OUTPUTS="${WATCHDOG_OUTPUTS:-0 1}" - -CURL=(curl --connect-timeout 5 --max-time 20 -fsS) - -rpc_post() { - local method="$1" payload="$2" rc=0 - - echo "-> ${method} ${payload}" - "${CURL[@]}" -X POST -H "Content-Type: application/json" -d "${payload}" "${BASE}/${method}" || rc=$? - echo - if [ "$rc" -ne 0 ]; then - echo "Warning: ${method} failed or is not supported on this firmware (curl exit ${rc})" - fi -} - -rpc_get() { - "${CURL[@]}" "${BASE}/$1" -} - -# Read-only call that never aborts the script. Optional second arg = output file. -rpc_get_optional() { - local method="$1" outfile="${2:-}" rc=0 - - if [ -n "$outfile" ]; then - rpc_get "$method" > "$outfile" || rc=$? - else - rpc_get "$method" || rc=$? - fi - if [ "$rc" -ne 0 ]; then - echo "Warning: ${method} failed or is not supported on this firmware (curl exit ${rc})" - fi -} - -count_list() { - local method="$1" key="$2" - - rpc_get "$method" 2>/dev/null | python3 -c ' -import json -import sys - -key = sys.argv[1] -data = json.load(sys.stdin) -print(len(data.get(key, []))) -' "$key" -} - -# Echo the number of items in an RPC list, or 0 if it cannot be determined. -safe_count() { - local n - n=$(count_list "$1" "$2" 2>/dev/null || true) - [[ "$n" =~ ^[0-9]+$ ]] || n=0 - echo "$n" -} - -echo "Hardening Shelly at ${SHELLY_IP}" - -echo "== Device info ==" -rpc_get_optional "Shelly.GetDeviceInfo" -echo - -echo "== Available methods ==" -rpc_get_optional "Shelly.ListMethods" "${OUT_DIR}/shelly_methods.json" -echo "Saved methods to ${OUT_DIR}/shelly_methods.json" - -echo "== Cloud off ==" -rpc_post "Cloud.SetConfig" '{"config":{"enable":false}}' - -echo "== MQTT off ==" -rpc_post "Mqtt.SetConfig" '{"config":{"enable":false}}' - -echo "== Outbound WebSocket off ==" -rpc_post "WS.SetConfig" '{"config":{"enable":false}}' - -echo "== WiFi access point off ==" -rpc_post "Wifi.SetConfig" '{"config":{"ap":{"enable":false}}}' - -echo "== BLE off ==" -rpc_post "BLE.SetConfig" '{"config":{"enable":false,"rpc":{"enable":false}}}' - -echo "== Eco mode on ==" -# Lowers CPU/radio activity when idle to reduce heat and power draw. The -# watchdog does not need fast RPC responses, so the extra latency is harmless. -rpc_post "Sys.SetConfig" '{"config":{"device":{"eco_mode":true}}}' - -echo "== Outputs ON by default at boot ==" -# Without this the outputs default to match_input and stay off after a Shelly -# restart, leaving the Pi / cameras unpowered until manual intervention. -for id in ${WATCHDOG_OUTPUTS}; do - rpc_post "Switch.SetConfig" "{\"id\":${id},\"config\":{\"initial_state\":\"on\"}}" -done - -echo "== Webhooks delete if any ==" -hooks=$(safe_count "Webhook.List" "hooks") -if [ "$hooks" -gt 0 ]; then - echo "Found ${hooks} webhook(s), deleting all" - rpc_post "Webhook.DeleteAll" '{}' -else - echo "No webhooks, nothing to do" -fi - -echo "== Schedules delete if any ==" -jobs=$(safe_count "Schedule.List" "jobs") -if [ "$jobs" -gt 0 ]; then - echo "Found ${jobs} schedule(s), deleting all" - rpc_post "Schedule.DeleteAll" '{}' -else - echo "No schedules, nothing to do" -fi - -echo "== Final config ==" -rpc_get_optional "Shelly.GetConfig" "${OUT_DIR}/shelly_config_after_hardening.json" -echo "Saved final config to ${OUT_DIR}/shelly_config_after_hardening.json" - -echo "Done." diff --git a/watchdog/shelly/device/setup_shelly_watchdog.sh b/watchdog/shelly/device/setup_shelly_watchdog.sh deleted file mode 100755 index 84bf2117..00000000 --- a/watchdog/shelly/device/setup_shelly_watchdog.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/bash -set -euo pipefail - -SHELLY_IP="${SHELLY_IP:-192.168.1.97}" -SCRIPT_NAME="${SCRIPT_NAME:-pi_watchdog}" -SCRIPT_FILE="${SCRIPT_FILE:-watchdog.js}" - -trap 'rm -f payload.json' EXIT - -echo "Checking Shelly at http://${SHELLY_IP}" -curl -s "http://${SHELLY_IP}/rpc/Shelly.GetDeviceInfo" > /tmp/shelly_device_info.json -cat /tmp/shelly_device_info.json -echo - -echo "Looking for existing script named ${SCRIPT_NAME}" -SCRIPT_ID=$(curl -s "http://${SHELLY_IP}/rpc/Script.List" | python3 -c ' -import json -import sys - -script_name = sys.argv[1] -data = json.load(sys.stdin) - -for script in data.get("scripts", []): - if script.get("name") == script_name: - print(script["id"]) - sys.exit(0) - -print("") -' "$SCRIPT_NAME") - -if [ -z "$SCRIPT_ID" ]; then - echo "Creating script ${SCRIPT_NAME}" - SCRIPT_ID=$(curl -s -X POST \ - -H "Content-Type: application/json" \ - -d "{\"name\":\"${SCRIPT_NAME}\"}" \ - "http://${SHELLY_IP}/rpc/Script.Create" \ - | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])') -else - echo "Using existing script id ${SCRIPT_ID}" -fi - -echo "Preparing payload for script id ${SCRIPT_ID}" -SCRIPT_ID="$SCRIPT_ID" SCRIPT_FILE="$SCRIPT_FILE" python3 - <<'PY' -import json -import os - -script_id = int(os.environ["SCRIPT_ID"]) -script_file = os.environ["SCRIPT_FILE"] - -with open(script_file, "r", encoding="utf-8") as f: - code = f.read() - -with open("payload.json", "w", encoding="utf-8") as f: - json.dump({"id": script_id, "code": code}, f) -PY - -echo "Stopping script if running" -curl -s "http://${SHELLY_IP}/rpc/Script.Stop?id=${SCRIPT_ID}" || true -echo - -echo "Uploading script code" -curl -s -X POST \ - -H "Content-Type: application/json" \ - -d @payload.json \ - "http://${SHELLY_IP}/rpc/Script.PutCode" -echo - -echo "Enabling script on startup" -curl -s -X POST \ - -H "Content-Type: application/json" \ - -d "{\"id\":${SCRIPT_ID},\"config\":{\"enable\":true}}" \ - "http://${SHELLY_IP}/rpc/Script.SetConfig" -echo - -echo "Starting script" -curl -s "http://${SHELLY_IP}/rpc/Script.Start?id=${SCRIPT_ID}" -echo - -echo "Script list" -curl -s "http://${SHELLY_IP}/rpc/Script.List" -echo - -echo "Script status" -curl -s "http://${SHELLY_IP}/rpc/Script.GetStatus?id=${SCRIPT_ID}" -echo diff --git a/watchdog/shelly/device/update_shelly_watchdog.sh b/watchdog/shelly/device/update_shelly_watchdog.sh deleted file mode 100755 index 0fd6a7c6..00000000 --- a/watchdog/shelly/device/update_shelly_watchdog.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash -set -euo pipefail - -SHELLY_IP="${SHELLY_IP:-192.168.1.97}" -SCRIPT_NAME="${SCRIPT_NAME:-pi_watchdog}" -SCRIPT_FILE="${SCRIPT_FILE:-watchdog.js}" - -trap 'rm -f payload.json' EXIT - -echo "Finding script named ${SCRIPT_NAME}" - -SCRIPT_ID=$(curl -s "http://${SHELLY_IP}/rpc/Script.List" | python3 -c ' -import json -import sys - -script_name = sys.argv[1] -data = json.load(sys.stdin) - -for script in data.get("scripts", []): - if script.get("name") == script_name: - print(script["id"]) - sys.exit(0) - -raise SystemExit(f"Script not found: {script_name}") -' "$SCRIPT_NAME") - -echo "Updating script id ${SCRIPT_ID}" - -SCRIPT_ID="$SCRIPT_ID" SCRIPT_FILE="$SCRIPT_FILE" python3 - <<'PY' -import json -import os - -script_id = int(os.environ["SCRIPT_ID"]) -script_file = os.environ["SCRIPT_FILE"] - -with open(script_file, "r", encoding="utf-8") as f: - code = f.read() - -with open("payload.json", "w", encoding="utf-8") as f: - json.dump({"id": script_id, "code": code}, f) -PY - -echo "Stopping script" -curl -s "http://${SHELLY_IP}/rpc/Script.Stop?id=${SCRIPT_ID}" || true -echo - -echo "Uploading code" -curl -s -X POST \ - -H "Content-Type: application/json" \ - -d @payload.json \ - "http://${SHELLY_IP}/rpc/Script.PutCode" -echo - -echo "Enabling script on startup" -curl -s -X POST \ - -H "Content-Type: application/json" \ - -d "{\"id\":${SCRIPT_ID},\"config\":{\"enable\":true}}" \ - "http://${SHELLY_IP}/rpc/Script.SetConfig" -echo - -echo "Starting script" -curl -s "http://${SHELLY_IP}/rpc/Script.Start?id=${SCRIPT_ID}" -echo - -echo "Script status" -curl -s "http://${SHELLY_IP}/rpc/Script.GetStatus?id=${SCRIPT_ID}" -echo