diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-30 10:37:42 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-30 10:37:42 +0300 |
| commit | ec725188ebd541ae620064d5d294540f19f60ac6 (patch) | |
| tree | 6d8f3b374152c20d145bbde6051acabec97d3f34 | |
| parent | 028535204c5eeb4996e4186af25a54614fac341a (diff) | |
f3s/shelly: rack-fan boot-time auto-on rc.d service + plug control script
Add shellyfans rc.d service (boot-time auto-on for the rack fans on the
Shelly Plug M Gen 3 at 192.168.1.28) and a standalone shelly-plug.sh
helper to query/control the plug over its HTTP RPC API.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| -rw-r--r-- | f3s/freebsd-hosts/shelly-fans/README.md | 55 | ||||
| -rw-r--r-- | f3s/freebsd-hosts/shelly-fans/shelly-fans-on | 44 | ||||
| -rw-r--r-- | f3s/freebsd-hosts/shelly-fans/shellyfans.rc | 29 | ||||
| -rwxr-xr-x | playground/shelly-plug.sh | 87 |
4 files changed, 215 insertions, 0 deletions
diff --git a/f3s/freebsd-hosts/shelly-fans/README.md b/f3s/freebsd-hosts/shelly-fans/README.md new file mode 100644 index 0000000..85fd899 --- /dev/null +++ b/f3s/freebsd-hosts/shelly-fans/README.md @@ -0,0 +1,55 @@ +# f3s Rack Fans (Shelly Plug) + +The rack fans are powered by a **Shelly Plug M Gen 3** at `192.168.1.28`. Each +f-host (f0/f1/f2/f3) turns the plug on at boot so the fans always run while any +host is up. Turning the plug *off* is handled centrally by `wol-f3s shutdown-all` +(on earth / the Pis), not by the hosts. + +The plug has authentication enabled (HTTP digest, user `admin`). + +## Installed Files + +- `/usr/local/sbin/shelly-fans-on` calls the Shelly RPC API to switch the plug + on, retrying for ~60s in case networking or the plug is briefly unavailable. +- `/usr/local/etc/rc.d/shellyfans` runs the helper at boot, after networking + and after `f3skeys` (so the `/keys` USB stick is already mounted). +- `/keys/shelly_plug.secret` holds the plug password (first line). It lives on + the UFS USB key stick alongside the ZFS encryption keys — **not** in git and + not on the host's own disk. `/keys` is mounted read-only at boot, so adding + the file requires temporarily remounting read-write. + +## Host Configuration + +On each f-host install the scripts and enable the service: + +```sh +doas install -o root -g wheel -m 0555 shelly-fans-on /usr/local/sbin/shelly-fans-on +doas install -o root -g wheel -m 0555 shellyfans.rc /usr/local/etc/rc.d/shellyfans +doas sysrc shellyfans_enable=YES +``` + +Put the plug password on the USB key stick (mounted read-only at `/keys`): + +```sh +doas mount -u -o rw /keys +printf '%s\n' '<password>' | doas tee /keys/shelly_plug.secret >/dev/null +doas chmod 0400 /keys/shelly_plug.secret +doas chown root:wheel /keys/shelly_plug.secret +doas mount -u -o ro /keys +``` + +The same password file is placed on every host's stick (one shared plug). + +## Verification + +```sh +doas service shellyfans start +tail -f /var/log/messages | grep shellyfans # expect "Rack fans switched on" +``` + +After a reboot, confirm the plug reports `output:true`: + +```sh +curl -s --digest -u admin:"$(head -n1 /keys/shelly_plug.secret)" \ + 'http://192.168.1.28/rpc/Switch.GetStatus?id=0' +``` diff --git a/f3s/freebsd-hosts/shelly-fans/shelly-fans-on b/f3s/freebsd-hosts/shelly-fans/shelly-fans-on new file mode 100644 index 0000000..f41ef3d --- /dev/null +++ b/f3s/freebsd-hosts/shelly-fans/shelly-fans-on @@ -0,0 +1,44 @@ +#!/bin/sh +# Turn on the Shelly Plug M Gen 3 that powers the rack fans. +# +# Called at boot by the shellyfans rc service so the rack fans start whenever +# an f-host (f0/f1/f2/f3) comes up. The Shelly plug uses HTTP digest auth +# (user "admin"); the password is read from $SHELLY_PASS_FILE if present. +# +# The password lives on the UFS USB key stick mounted read-only at /keys, +# alongside the ZFS encryption keys. If the stick is missing the file will be +# unreadable and the fans simply will not be switched on (logged, non-fatal). +# +# Networking may not be fully ready the instant this runs at boot, and the +# plug itself may briefly be unreachable, so the call is retried a few times. + +# rc.d runs with a minimal PATH; curl lives in /usr/local/bin. +PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin +export PATH + +SHELLY_IP="${SHELLY_IP:-192.168.1.28}" +SHELLY_PASS_FILE="${SHELLY_PASS_FILE:-/keys/shelly_plug.secret}" + +PASS="" +if [ -r "$SHELLY_PASS_FILE" ]; then + PASS=$(head -n 1 "$SHELLY_PASS_FILE" | tr -d '\r\n') +fi + +AUTH="" +if [ -n "$PASS" ]; then + AUTH="--digest -u admin:$PASS" +fi + +i=0 +while [ "$i" -lt 12 ]; do + if curl -s --max-time 5 $AUTH \ + "http://${SHELLY_IP}/rpc/Switch.Set?id=0&on=true" >/dev/null 2>&1; then + logger -t shellyfans "Rack fans switched on via Shelly plug $SHELLY_IP" + exit 0 + fi + i=$((i + 1)) + sleep 5 +done + +logger -t shellyfans "Failed to reach Shelly plug $SHELLY_IP after retries" +exit 1 diff --git a/f3s/freebsd-hosts/shelly-fans/shellyfans.rc b/f3s/freebsd-hosts/shelly-fans/shellyfans.rc new file mode 100644 index 0000000..d1f4ddc --- /dev/null +++ b/f3s/freebsd-hosts/shelly-fans/shellyfans.rc @@ -0,0 +1,29 @@ +#!/bin/sh + +# PROVIDE: shellyfans +# REQUIRE: NETWORKING f3skeys +# KEYWORD: nojail + +. /etc/rc.subr + +name="shellyfans" +desc="Turn on the rack-fan Shelly plug at boot" +rcvar="shellyfans_enable" +start_cmd="shellyfans_start" +stop_cmd=":" + +: ${shellyfans_enable:="NO"} + +shellyfans_start() +{ + # Run in the background so a slow or unreachable plug cannot delay boot; + # the helper retries on its own. + if [ -x /usr/local/sbin/shelly-fans-on ]; then + /usr/local/sbin/shelly-fans-on & + else + logger -t shellyfans "/usr/local/sbin/shelly-fans-on is missing" + fi +} + +load_rc_config "$name" +run_rc_command "$1" diff --git a/playground/shelly-plug.sh b/playground/shelly-plug.sh new file mode 100755 index 0000000..5619f93 --- /dev/null +++ b/playground/shelly-plug.sh @@ -0,0 +1,87 @@ +#!/bin/sh +# Control and query a Shelly Plug M Gen 3 via its HTTP RPC API. +# +# Usage: +# shelly-plug.sh [host] <command> +# +# Commands: +# status Show power, voltage, current, energy and temperature +# on Turn the plug on +# off Turn the plug off +# toggle Toggle the plug +# info Show device info (model, firmware, mac) +# +# The host defaults to 192.168.1.28 and can be overridden by the first +# argument or the SHELLY_HOST environment variable. +# +# If the device has authentication enabled, the password is read from the +# SHELLY_PASS environment variable, or from ~/.shelly_plug if that is unset +# (the username is always "admin" on Shelly Gen3). Digest auth is used +# automatically when a password is available. + +set -eu + +DEFAULT_HOST="192.168.1.28" +PASS_FILE="${HOME}/.shelly_plug" + +# Argument forms: +# (none) -> host=default, command=status +# <command> -> host=default, command=<command> +# <host> <command> -> explicit host and command +if [ "$#" -ge 2 ]; then + HOST="$1" + CMD="$2" +elif [ "$#" -eq 1 ]; then + HOST="${SHELLY_HOST:-$DEFAULT_HOST}" + CMD="$1" +else + HOST="${SHELLY_HOST:-$DEFAULT_HOST}" + CMD="status" +fi + +BASE="http://${HOST}" + +# Resolve the password: SHELLY_PASS wins, otherwise read it from PASS_FILE. +PASS="${SHELLY_PASS:-}" +if [ -z "$PASS" ] && [ -r "$PASS_FILE" ]; then + PASS="$(head -n 1 "$PASS_FILE" | tr -d '\r\n')" +fi + +# Build optional digest-auth args when a password is available. +AUTH="" +if [ -n "$PASS" ]; then + AUTH="--digest -u admin:${PASS}" +fi + +rpc() { + # shellcheck disable=SC2086 + curl -s --max-time 5 $AUTH "${BASE}/rpc/$1" +} + +case "$CMD" in + status) + rpc "Switch.GetStatus?id=0" + echo + ;; + on) + rpc "Switch.Set?id=0&on=true" + echo + ;; + off) + rpc "Switch.Set?id=0&on=false" + echo + ;; + toggle) + rpc "Switch.Toggle?id=0" + echo + ;; + info) + curl -s --max-time 5 "${BASE}/shelly" + echo + ;; + *) + echo "Unknown command: $CMD" >&2 + echo "Use one of: status, on, off, toggle, info" >&2 + exit 1 + ;; +esac |
