Four Skill-tool skills under .claude/skills/ that let an agent monitor and
troubleshoot the install (2x LVX6048, 6x EG4 LifePower4, OpenEVSE), grounded
in the real MQTT/HA topology rather than generic advice:
- solar-health-check : whole-system sweep + cross-checks + R/Y/G verdict,
incl. cross-unit "silently-dead inverter" detection
- troubleshoot-inverter: FWS fault decode, parallel sync, USB link recovery
- troubleshoot-battery : per-pack imbalance vs SoC-counter-drift, RS485 silence
- power-usage : PV/load/grid/battery balance + EVSE sessions
Shared lib:
- solar-snapshot : live MQTT capture (creds from powermon.yaml, no hardcoding)
- ha-history : HA recorder lookback (token from ~/.config/ha/token)
REFERENCE.md documents topology, real HA entity_ids (doubled slug), known
issues, and a safe-remediation-only action policy (restarts yes; setters no).
Action boundary: diagnose + restart wedged daemons / recover USB links;
never touches inverter/battery setters or flash.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
98 lines
3.8 KiB
Bash
Executable File
98 lines
3.8 KiB
Bash
Executable File
#!/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)"
|
|
}'
|