summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-30 10:37:42 +0300
committerPaul Buetow <paul@buetow.org>2026-06-30 10:37:42 +0300
commitec725188ebd541ae620064d5d294540f19f60ac6 (patch)
tree6d8f3b374152c20d145bbde6051acabec97d3f34
parent028535204c5eeb4996e4186af25a54614fac341a (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.md55
-rw-r--r--f3s/freebsd-hosts/shelly-fans/shelly-fans-on44
-rw-r--r--f3s/freebsd-hosts/shelly-fans/shellyfans.rc29
-rwxr-xr-xplayground/shelly-plug.sh87
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