98 lines
3.8 KiB
Plaintext
98 lines
3.8 KiB
Plaintext
|
|
#!/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/<class>/ 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<n;i++) for (j=i+1;j<n;j++) if (keys[j]<keys[i]) { tmp=keys[i];keys[i]=keys[j];keys[j]=tmp }
|
||
|
|
for (i=0;i<n;i++) {
|
||
|
|
t=keys[i]; disp=t
|
||
|
|
if (!full) { sub(/^homeassistant\/[^/]+\//,"",disp); sub(/\/state$/,"",disp) }
|
||
|
|
printf "%-44s %s\n", disp, last[t]
|
||
|
|
}
|
||
|
|
if (n==0) print "(no messages in window — topics idle, broker unreachable, or filter wrong)"
|
||
|
|
}'
|