#!/usr/bin/env bash # solar-snapshot — capture the latest retained/published value of every MQTT # topic matching a filter, over a short listen window, and print a clean table. # # Why a listen window: powermon/eg4-battery STATE topics are NOT retained — they # are republished every poll cycle (GS ~5s, packs ~one cycle, EVSE on-change). # So we subscribe for a few seconds and keep the last value seen per topic. # (HA discovery `.../config` topics ARE retained and show up immediately.) # # Broker credentials are read from ~/.config/powermon/powermon.yaml (the same # source the openevse + lvx-control tools use) so nothing is hardcoded here. # # NOTE on MQTT wildcards: `+` matches exactly ONE whole level, so it cannot be # used as a name prefix. `homeassistant/sensor/lifepower4_+/state` matches NOTHING. # To grab a family of entities, subscribe to the level wildcard and filter with -g: # solar-snapshot -g lifepower4 'homeassistant/sensor/+/state' # # Usage: # solar-snapshot [-w SECONDS] [-f] [-g GREP_RE] TOPIC_FILTER [TOPIC_FILTER ...] # -w SECONDS listen window (default 12) # -f print full topic path (default: strip homeassistant// prefix) # -g GREP_RE keep only topics whose path matches this extended-regex # # Examples: # solar-snapshot -g 'lvx6048_1' 'homeassistant/sensor/+/state' # solar-snapshot -w 18 -g 'lifepower4_[1-6]_soc' 'homeassistant/sensor/+/state' # solar-snapshot 'openevse/#' # solar-snapshot -w 6 'homeassistant/sensor/lvx6048_1_battery_voltage/state' \ # 'homeassistant/sensor/lifepower4_1_pack_voltage/state' # # Exit status reflects the formatting stage, not mosquitto_sub's benign -W # window-expiry code, so callers don't misread a normal capture as a failure. set -eu WINDOW=12 FULL=0 GREP_RE="" while getopts "w:fg:" opt; do case "$opt" in w) WINDOW="$OPTARG" ;; f) FULL=1 ;; g) GREP_RE="$OPTARG" ;; *) echo "usage: solar-snapshot [-w SECONDS] [-f] [-g GREP_RE] TOPIC_FILTER..." >&2; exit 2 ;; esac done shift $((OPTIND - 1)) if [ "$#" -lt 1 ]; then echo "usage: solar-snapshot [-w SECONDS] [-f] [-g GREP_RE] TOPIC_FILTER..." >&2 exit 2 fi CONF="${POWERMON_CONF:-$HOME/.config/powermon/powermon.yaml}" if [ ! -r "$CONF" ]; then echo "solar-snapshot: cannot read broker config $CONF" >&2 exit 1 fi # Pull host/port/user/pass from the mqttbroker: block of powermon.yaml. # Keys are anchored to leading whitespace + exact key so `name:` doesn't also # match `username:`. read -r HOST PORT USER PASS < <(awk ' /^[^[:space:]]/ { inblk=0 } /^mqttbroker:/ { inblk=1; next } inblk && /^[[:space:]]+name:/ { h=$2 } inblk && /^[[:space:]]+port:/ { p=$2 } inblk && /^[[:space:]]+username:/ { u=$2 } inblk && /^[[:space:]]+password:/ { w=$2 } END { print h, (p?p:1883), u, w } ' "$CONF") if [ -z "${HOST:-}" ]; then echo "solar-snapshot: no mqttbroker.name found in $CONF" >&2 exit 1 fi # Build -t args from filters. TARGS=() for f in "$@"; do TARGS+=(-t "$f"); done # Subscribe for the window, then reduce to last-value-per-topic. timeout "$((WINDOW + 2))" mosquitto_sub -h "$HOST" -p "$PORT" -u "$USER" -P "$PASS" \ -W "$WINDOW" -v "${TARGS[@]}" 2>/dev/null \ | { [ -n "$GREP_RE" ] && grep -E "$GREP_RE" || cat; } \ | awk -v full="$FULL" ' { t=$1; $1=""; sub(/^ /,""); v=$0; last[t]=v; order[t]=NR } END { n=0 for (t in last) { keys[n++]=t } # stable-ish sort by topic name for (i=0;i