#!/bin/bash # MQTT Push for Parallel/Cascade Inverters # Queries QPGS0 (inv1) and QPGS1 (inv2) directly - no serial discovery. # Intended for inverters connected in cascade on the same RS232 bus. # Detect environment (container vs development) if [ -f "/etc/inverter/mqtt.json" ] && [ -x "/opt/inverter-cli/bin/inverter_poller" ]; then MQTT_CONFIG="/etc/inverter/mqtt.json" INVERTER_BIN="/opt/inverter-cli/bin/inverter_poller" INVERTER_CONF="/etc/inverter/inverter.conf" else MQTT_CONFIG="/home/pi/Progetti/config/mqtt.json" INVERTER_BIN="/home/pi/Progetti/sources/inverter-cli/bin/inverter_poller" INVERTER_CONF="/home/pi/Progetti/config/inverter.conf" fi if ! command -v jq &> /dev/null; then echo "ERROR: jq is not installed. Install with: sudo apt-get install jq" exit 1 fi MQTT_SERVER=`cat $MQTT_CONFIG | jq '.server' -r` MQTT_PORT=`cat $MQTT_CONFIG | jq '.port' -r` MQTT_TOPIC=`cat $MQTT_CONFIG | jq '.topic' -r` MQTT_DEVICENAME=`cat $MQTT_CONFIG | jq '.devicename' -r` MQTT_USERNAME=`cat $MQTT_CONFIG | jq '.username' -r` MQTT_PASSWORD=`cat $MQTT_CONFIG | jq '.password' -r` MQTT_CLIENTID=`cat $MQTT_CONFIG | jq '.clientid' -r` INFLUX_ENABLED=`cat $MQTT_CONFIG | jq '.influx.enabled' -r` # Number of cascade inverters (default 2, override with CASCADE_COUNT env var) CASCADE_COUNT="${CASCADE_COUNT:-2}" pushMQTTData () { # $1 = inverter_id (1, 2, or "system"), $2 = metric, $3 = value local inverter_id=$1 local metric=$2 local value=$3 mosquitto_pub \ -h $MQTT_SERVER \ -p $MQTT_PORT \ -u "$MQTT_USERNAME" \ -P "$MQTT_PASSWORD" \ -i $MQTT_CLIENTID \ -r \ -t "$MQTT_TOPIC/sensor/${MQTT_DEVICENAME}_inv${inverter_id}_${metric}" \ -m "$value" if [[ $INFLUX_ENABLED == "true" ]]; then pushInfluxData $inverter_id $metric $value fi } pushInfluxData () { INFLUX_HOST=`cat $MQTT_CONFIG | jq '.influx.host' -r` INFLUX_USERNAME=`cat $MQTT_CONFIG | jq '.influx.username' -r` INFLUX_PASSWORD=`cat $MQTT_CONFIG | jq '.influx.password' -r` INFLUX_DEVICE=`cat $MQTT_CONFIG | jq '.influx.device' -r` INFLUX_PREFIX=`cat $MQTT_CONFIG | jq '.influx.prefix' -r` INFLUX_DATABASE=`cat $MQTT_CONFIG | jq '.influx.database' -r` INFLUX_MEASUREMENT_NAME=`cat $MQTT_CONFIG | jq '.influx.namingMap.'$2'' -r` curl -i -XPOST "$INFLUX_HOST/write?db=$INFLUX_DATABASE&precision=s" \ -u "$INFLUX_USERNAME:$INFLUX_PASSWORD" \ --data-binary "$INFLUX_PREFIX,device=${INFLUX_DEVICE}_inv${1} $INFLUX_MEASUREMENT_NAME=$3" \ > /dev/null 2>&1 } # Process QPGS data and publish to MQTT for one inverter. # Arguments: $1=inv_id, $2=QPGS_RAW string # Returns 0 on success, 1 on failure. processInverter () { local inv_id=$1 local QPGS_RAW=$2 if [ -z "$QPGS_RAW" ] || [ "$QPGS_RAW" = "NAK" ]; then echo " ✗ inv$inv_id: no data (NAK or empty)" return 1 fi IFS=' ' read -ra DATA <<< "$QPGS_RAW" # QPGS field mapping (per protocol HS_MS_MSX_RS232_Protocol): # 0=Exists 1=Serial 2=Mode 3=Fault # 4=GridV 5=GridF 6=OutV 7=OutF # 8=OutVA 9=OutW 10=LoadPct # 11=BattV 12=BattChgA 13=BattCap # 14=PVInputV 15=TotalChgA 16=TotalOutVA 17=TotalOutW 18=TotalOutPct # 19=StatusByte(b7b6b5b4b3b2b1b0) 20=OutMode 21=ChgSourcePriority # 22=MaxChgA 23=MaxChgRange 24=MaxAcChgA # 25=PV_in_current 26=Batt_discharge_current [ "${DATA[2]}" ] && pushMQTTData "$inv_id" "Inverter_mode" "${DATA[2]}" [ "${DATA[4]}" ] && pushMQTTData "$inv_id" "AC_grid_voltage" "${DATA[4]}" [ "${DATA[5]}" ] && pushMQTTData "$inv_id" "AC_grid_frequency" "${DATA[5]}" [ "${DATA[6]}" ] && pushMQTTData "$inv_id" "AC_out_voltage" "${DATA[6]}" [ "${DATA[7]}" ] && pushMQTTData "$inv_id" "AC_out_frequency" "${DATA[7]}" [ "${DATA[8]}" ] && pushMQTTData "$inv_id" "Load_va" "${DATA[8]}" [ "${DATA[9]}" ] && pushMQTTData "$inv_id" "Load_watt" "${DATA[9]}" [ "${DATA[10]}" ] && pushMQTTData "$inv_id" "Load_pct" "${DATA[10]}" [ "${DATA[11]}" ] && pushMQTTData "$inv_id" "Battery_voltage" "${DATA[11]}" [ "${DATA[12]}" ] && pushMQTTData "$inv_id" "Battery_charge_current" "${DATA[12]}" [ "${DATA[13]}" ] && pushMQTTData "$inv_id" "Battery_capacity" "${DATA[13]}" [ "${DATA[14]}" ] && pushMQTTData "$inv_id" "PV_in_voltage" "${DATA[14]}" [ "${DATA[25]}" ] && pushMQTTData "$inv_id" "SCC_current" "${DATA[25]}" [ "${DATA[26]}" ] && pushMQTTData "$inv_id" "Battery_discharge_current" "${DATA[26]}" # ─── Real PV panel power calculation via DC-bus energy balance ──────────── # The QPGS protocol does NOT expose the true PV panel input current directly. # DATA[25] = "PV input current for battery" = SCC output current to battery ONLY. # This is 0 when battery is full even though panels are producing. # DATA[26] = battery discharge current (at battery voltage). # DATA[9] = AC output load watt (power drawn from DC bus by the inverter). # # The DC bus balance equation gives us the real SCC output power: # SCC_out = Load_watt + Bat_charge − Bat_discharge # = Load + (V_batt × DATA[25]) − (V_batt × DATA[26]) # When SCC is the sole DC source: # P_pv ≈ SCC_out = V_batt×DATA[25] + max(0, Load − V_batt×DATA[26]) # # Guard: only calculate when SCC_charging bit (STATUS b5, index 2) = 1. # When SCC is off, PV production = 0 regardless of field values. local BATT_V="${DATA[11]:-0}" local SCC_A="${DATA[25]:-0}" local DISCH_A="${DATA[26]:-0}" local PV_V="${DATA[14]:-0}" local LOAD_W="${DATA[9]:-0}" local STATUS="${DATA[19]:-00000000}" # STATUS bit layout (b7b6b5b4b3b2b1b0 as string, index 0=b7): # index 2 = b5 = SCC_charging (1 = SCC actively converting solar) local SCC_CHARGING="${STATUS:2:1}" local PV_WATTS PV_CURRENT if [ "$SCC_CHARGING" = "1" ]; then # SCC is active: estimate total PV power via DC bus balance # P_pv = V_batt × I_scc_to_battery + max(0, Load − V_batt × I_bat_discharge) local BATT_CHARGE_W=$(echo "$BATT_V $SCC_A" | awk '{printf "%.1f", $1 * $2}') local BATT_DISCH_W=$(echo "$BATT_V $DISCH_A" | awk '{printf "%.1f", $1 * $2}') local LOAD_FROM_SCC=$(echo "$LOAD_W $BATT_DISCH_W" | awk '{v=$1-$2; printf "%.1f", (v>0)?v:0}') PV_WATTS=$(echo "$BATT_CHARGE_W $LOAD_FROM_SCC" | awk '{printf "%.1f", $1 + $2}') if awk -v v="$PV_V" 'BEGIN{exit !(v+0 > 0)}' 2>/dev/null; then PV_CURRENT=$(echo "$PV_WATTS $PV_V" | awk '{printf "%.2f", $1 / $2}') else PV_CURRENT="0.00" fi else # SCC off: no solar conversion PV_WATTS="0.0" PV_CURRENT="0.00" fi pushMQTTData "$inv_id" "PV_in_current" "$PV_CURRENT" pushMQTTData "$inv_id" "PV_in_watts" "$PV_WATTS" # Watt-hours (approximated from polling interval = 30s = 1/120 hour) local PV_WH=$(echo "$PV_WATTS" | awk '{printf "%.4f", $1 / 120}') pushMQTTData "$inv_id" "PV_in_watthour" "$PV_WH" local LOAD_WH=$(echo "$LOAD_W" | awk '{printf "%.4f", $1 / 120}') pushMQTTData "$inv_id" "Load_watthour" "$LOAD_WH" # Status flags from STATUS byte if [ ${#STATUS} -ge 8 ]; then pushMQTTData "$inv_id" "SCC_charge_on" "${STATUS:2:1}" # b5 pushMQTTData "$inv_id" "AC_charge_on" "${STATUS:1:1}" # b6 pushMQTTData "$inv_id" "Load_status_on" "${STATUS:6:1}" # b1 fi echo " ✓ inv$inv_id: OK (PV_watts=${PV_WATTS}W, Load=${LOAD_W}W)" return 0 } # ─── Main ───────────────────────────────────────────────────────────────────── SUDO_CMD="" if [ "$EUID" -ne 0 ]; then SUDO_CMD="sudo" fi echo "=== Cascade Inverter MQTT Push ===" echo "Inverter count: $CASCADE_COUNT" # ── Step 1: QPIRI (shared config, same for all cascade inverters) ───────────── QPIRI_RAW="" for attempt in 1 2 3; do QPIRI_RAW=`$SUDO_CMD "$INVERTER_BIN" -r "QPIRI" 2>&1 | grep "Reply:" | cut -d: -f2- | xargs` [ ! -z "$QPIRI_RAW" ] && [ "$QPIRI_RAW" != "NAK" ] && break sleep 1 done BATT_RECHARGE="" BATT_UNDER="" BATT_BULK="" BATT_FLOAT="" MAX_CHARGE_CURRENT="" MAX_GRID_CHARGE="" OUT_SOURCE_PRIORITY="" CHARGER_SOURCE_PRIORITY="" BATT_REDISCHARGE="" if [ ! -z "$QPIRI_RAW" ] && [ "$QPIRI_RAW" != "NAK" ]; then echo "✓ QPIRI retrieved" IFS=' ' read -ra QPIRI <<< "$QPIRI_RAW" BATT_RECHARGE="${QPIRI[8]}" BATT_UNDER="${QPIRI[9]}" BATT_BULK="${QPIRI[10]}" BATT_FLOAT="${QPIRI[11]}" MAX_CHARGE_CURRENT="${QPIRI[13]}" MAX_GRID_CHARGE="${QPIRI[14]}" OUT_SOURCE_PRIORITY="${QPIRI[15]}" CHARGER_SOURCE_PRIORITY="${QPIRI[16]}" BATT_REDISCHARGE="${QPIRI[22]}" else echo "⚠ QPIRI failed - config parameters unavailable" fi # ── Step 2: QPGS per each inverter ─────────────────────────────────────────── SUCCESS_IDS=() for inv_id in $(seq 1 $CASCADE_COUNT); do qpgs_idx=$((inv_id - 1)) echo "Querying QPGS${qpgs_idx} → inverter #${inv_id}..." QPGS_RAW="" for attempt in 1 2 3; do QPGS_RAW=`$SUDO_CMD "$INVERTER_BIN" -r "QPGS${qpgs_idx}" 2>&1 | grep "Reply:" | cut -d: -f2- | xargs` [ ! -z "$QPGS_RAW" ] && [ "$QPGS_RAW" != "NAK" ] && break sleep 0.5 done if processInverter "$inv_id" "$QPGS_RAW"; then # Publish shared QPIRI config for this inverter [ ! -z "$BATT_RECHARGE" ] && pushMQTTData "$inv_id" "Battery_recharge_voltage" "$BATT_RECHARGE" [ ! -z "$BATT_UNDER" ] && pushMQTTData "$inv_id" "Battery_under_voltage" "$BATT_UNDER" [ ! -z "$BATT_BULK" ] && pushMQTTData "$inv_id" "Battery_bulk_voltage" "$BATT_BULK" [ ! -z "$BATT_FLOAT" ] && pushMQTTData "$inv_id" "Battery_float_voltage" "$BATT_FLOAT" [ ! -z "$MAX_CHARGE_CURRENT" ] && pushMQTTData "$inv_id" "Max_charge_current" "$MAX_CHARGE_CURRENT" [ ! -z "$MAX_GRID_CHARGE" ] && pushMQTTData "$inv_id" "Max_grid_charge_current" "$MAX_GRID_CHARGE" [ ! -z "$OUT_SOURCE_PRIORITY" ] && pushMQTTData "$inv_id" "Out_source_priority" "$OUT_SOURCE_PRIORITY" [ ! -z "$CHARGER_SOURCE_PRIORITY" ] && pushMQTTData "$inv_id" "Charger_source_priority" "$CHARGER_SOURCE_PRIORITY" [ ! -z "$BATT_REDISCHARGE" ] && pushMQTTData "$inv_id" "Battery_redischarge_voltage" "$BATT_REDISCHARGE" SUCCESS_IDS+=("$inv_id") fi done # Publish overall active inverter count pushMQTTData "system" "parallel_count" "${#SUCCESS_IDS[@]}" echo "Push completed: ${#SUCCESS_IDS[@]}/$CASCADE_COUNT inverters OK"