From 9e72d4f5a77004d1c25f19b49f836fa66c93e1bb Mon Sep 17 00:00:00 2001 From: Pi Developer Date: Tue, 10 Feb 2026 21:59:47 +0100 Subject: [PATCH] Migliora discovery e pubblicazione MQTT per inverter in cascata --- sources/inverter-cli/inverter.cpp | 293 +++++++++++++-- sources/inverter-cli/inverter.h | 8 + sources/inverter-cli/main.cpp | 22 ++ sources/inverter-mqtt/entrypoint.sh | 18 +- sources/inverter-mqtt/mqtt-push-parallel.sh | 390 ++++++++++++++++---- sources/inverter-mqtt/test-loop-parallel.sh | 93 +++-- 6 files changed, 696 insertions(+), 128 deletions(-) diff --git a/sources/inverter-cli/inverter.cpp b/sources/inverter-cli/inverter.cpp index 288e2ac..8a240de 100644 --- a/sources/inverter-cli/inverter.cpp +++ b/sources/inverter-cli/inverter.cpp @@ -7,6 +7,8 @@ #include "tools.h" #include "main.h" +#include + #include #include @@ -20,6 +22,7 @@ cInverter::cInverter(std::string devicename, int qpiri, int qpiws, int qmod, int buf_qpiws = qpiws; buf_qmod = qmod; buf_qpigs = qpigs; + last_reply_size = 0; lprintf("INVERTER: Initialized with buffer sizes - QPIRI:%d QPIWS:%d QMOD:%d QPIGS:%d", buf_qpiri, buf_qpiws, buf_qmod, buf_qpigs); } @@ -96,9 +99,12 @@ bool cInverter::query(const char *cmd, int replysize) { settings.c_cflag &= ~PARENB; // no parity settings.c_cflag &= ~CSTOPB; // 1 stop bit settings.c_cflag &= ~CSIZE; - settings.c_cflag |= CS8 | CLOCAL; // 8 bits - // settings.c_lflag = ICANON; // canonical mode - settings.c_oflag &= ~OPOST; // raw output + settings.c_cflag |= CS8 | CLOCAL | CREAD; // 8 bits, local, enable receiver + settings.c_iflag = 0; // raw input + settings.c_oflag = 0; // raw output + settings.c_lflag = 0; // no canonical/echo + settings.c_cc[VMIN] = 0; + settings.c_cc[VTIME] = 1; // 0.1s read timeout tcsetattr(fd, TCSANOW, &settings); // apply the settings @@ -141,8 +147,12 @@ bool cInverter::query(const char *cmd, int replysize) { memset(buf, 0, sizeof(buf)); time(&started); + bool started_frame = false; + unsigned char ch = 0; + int max_read = (int)sizeof(buf) - 1; + do { - n = read(fd, (void*)buf+i, 1); // Read one byte at a time for reliable terminator detection + n = read(fd, (void*)&ch, 1); // Read one byte at a time for reliable terminator detection if (n < 0) { if (time(NULL) - started > 2) { lprintf("INVERTER: %s read timeout", cmd); @@ -154,18 +164,35 @@ bool cInverter::query(const char *cmd, int replysize) { } if (n > 0) { - i += n; + if (!started_frame) { + if (ch != '(') { + continue; + } + started_frame = true; + buf[0] = ch; + i = 1; + continue; + } + + buf[i++] = ch; + // Check if we've received the terminator (CR or LF) - if (i > 0 && (buf[i-1] == 0x0d || buf[i-1] == 0x0a)) { - lprintf("INVERTER: %s received terminator (0x%02X) at byte %d", cmd, buf[i-1], i); + if (ch == 0x0d || ch == 0x0a) { + lprintf("INVERTER: %s received terminator (0x%02X) at byte %d", cmd, ch, i); + break; + } + + if (i >= (int)sizeof(buf) - 1) { + lprintf("INVERTER: %s buffer full before terminator", cmd); break; } } - } while (i 0) { + if (i > 0 && started_frame) { + last_reply_size = i; lprintf("INVERTER: %s reply size (%d bytes, expected %d)", cmd, i, replysize); @@ -197,6 +224,12 @@ bool cInverter::query(const char *cmd, int replysize) { buf[i-3] = '\0'; //nullterminating on first CRC byte lprintf("INVERTER: %s: %d bytes read: %s", cmd, i, buf); + // Treat NAK as a failed query + if (strncmp((const char*)buf + 1, "NAK", 3) == 0) { + lprintf("INVERTER: %s: NAK reply", cmd); + return false; + } + lprintf("INVERTER: %s query finished", cmd); // If expected size doesn't match actual size, log it @@ -207,6 +240,7 @@ bool cInverter::query(const char *cmd, int replysize) { return true; } else { + last_reply_size = 0; lprintf("INVERTER: %s reply too short (%d bytes)", cmd, i); return false; } @@ -227,6 +261,7 @@ void cInverter::poll() { ups_qmod_changed = true; fprintf(stderr, "[POLL] QMOD completed\n"); } + usleep(200000); // allow inverter to settle between commands } // reading status (QPIGS) @@ -239,6 +274,7 @@ void cInverter::poll() { ups_qpigs_changed = true; fprintf(stderr, "[POLL] QPIGS completed\n"); } + usleep(200000); // allow inverter to settle between commands } // Reading QPIRI status @@ -251,6 +287,7 @@ void cInverter::poll() { ups_qpiri_changed = true; fprintf(stderr, "[POLL] QPIRI completed\n"); } + usleep(200000); // allow inverter to settle between commands } // Get any device warnings... @@ -263,6 +300,7 @@ void cInverter::poll() { ups_qpiws_changed = true; fprintf(stderr, "[POLL] QPIWS completed\n"); } + usleep(200000); // allow inverter to settle between commands } // If runOnce mode and all data collected, exit the thread @@ -332,6 +370,135 @@ bool cInverter::CheckCRC(unsigned char *data, int len) { return data[len-3]==(crc>>8) && data[len-2]==(crc&0xff); } +int cInverter::openSerial() { + int fd = open(this->device.data(), O_RDWR | O_NOCTTY); + if (fd == -1) { + lprintf("INVERTER: Unable to open device file (errno=%d %s)", errno, strerror(errno)); + return -1; + } + + speed_t baud = B2400; + struct termios settings; + tcgetattr(fd, &settings); + + cfsetospeed(&settings, baud); + cfsetispeed(&settings, baud); + settings.c_cflag &= ~PARENB; + settings.c_cflag &= ~CSTOPB; + settings.c_cflag &= ~CSIZE; + settings.c_cflag |= CS8 | CLOCAL | CREAD; + settings.c_iflag = 0; + settings.c_oflag = 0; + settings.c_lflag = 0; + settings.c_cc[VMIN] = 0; + settings.c_cc[VTIME] = 1; + + tcsetattr(fd, TCSANOW, &settings); + tcflush(fd, TCIOFLUSH); + usleep(300000); + + return fd; +} + +void cInverter::closeSerial(int fd) { + if (fd >= 0) { + close(fd); + } +} + +bool cInverter::sendCommand(int fd, const std::string &cmd) { + unsigned char outbuf[128]; + size_t cmd_len = cmd.size(); + if (cmd_len + 3 > sizeof(outbuf)) { + return false; + } + + tcflush(fd, TCIFLUSH); + + uint16_t crc = cal_crc_half((uint8_t*)cmd.data(), cmd_len); + memcpy(outbuf, cmd.data(), cmd_len); + outbuf[cmd_len++] = crc >> 8; + outbuf[cmd_len++] = crc & 0xff; + outbuf[cmd_len++] = 0x0d; + + int written = write(fd, outbuf, cmd_len); + if (written != (int)cmd_len) { + lprintf("INVERTER: %s write failed (wrote %d of %d bytes)", cmd.c_str(), written, (int)cmd_len); + return false; + } + + tcdrain(fd); + usleep(500000); + return true; +} + +bool cInverter::readReply(int fd, std::string &payload) { + unsigned char localbuf[1024]; + memset(localbuf, 0, sizeof(localbuf)); + + time_t started; + time(&started); + + bool started_frame = false; + unsigned char ch = 0; + int i = 0; + + while (i < (int)sizeof(localbuf) - 1 && (time(NULL) - started < 7)) { + int n = read(fd, &ch, 1); + if (n > 0) { + if (!started_frame) { + if (ch != '(') { + continue; + } + started_frame = true; + localbuf[0] = ch; + i = 1; + continue; + } + + if (ch == '(') { + localbuf[0] = ch; + i = 1; + continue; + } + + localbuf[i++] = ch; + if (ch == 0x0d || ch == 0x0a) { + break; + } + } else { + usleep(10000); + } + } + + if (!started_frame || i <= 0) { + return false; + } + + if (localbuf[i-1] != 0x0d && localbuf[i-1] != 0x0a) { + if (debugFlag) { + lprintf("INVERTER: readReply missing terminator (size=%d)", i); + } + return false; + } + + if (!CheckCRC(localbuf, i)) { + if (debugFlag) { + lprintf("INVERTER: readReply CRC failed (size=%d)", i); + } + return false; + } + + localbuf[i-3] = '\0'; + if (strncmp((const char*)localbuf + 1, "NAK", 3) == 0) { + return false; + } + + payload = std::string((char*)localbuf + 1); + last_reply_size = i; + return true; +} + // Auto-discover the correct buffer size for a command by reading until CR int cInverter::query_auto(const char *cmd, int max_size) { time_t started; @@ -341,7 +508,7 @@ int cInverter::query_auto(const char *cmd, int max_size) { memset(temp_buf, 0, sizeof(temp_buf)); - fd = open(this->device.data(), O_RDWR | O_NONBLOCK); + fd = open(this->device.data(), O_RDWR | O_NOCTTY); if (fd == -1) { lprintf("INVERTER: Unable to open device file for auto-discovery"); return -1; @@ -356,13 +523,17 @@ int cInverter::query_auto(const char *cmd, int max_size) { settings.c_cflag &= ~PARENB; settings.c_cflag &= ~CSTOPB; settings.c_cflag &= ~CSIZE; - settings.c_cflag |= CS8 | CLOCAL; - settings.c_oflag &= ~OPOST; + settings.c_cflag |= CS8 | CLOCAL | CREAD; + settings.c_iflag = 0; + settings.c_oflag = 0; + settings.c_lflag = 0; + settings.c_cc[VMIN] = 0; + settings.c_cc[VTIME] = 5; // 0.5s read timeout tcsetattr(fd, TCSANOW, &settings); // Flush all buffers tcflush(fd, TCIOFLUSH); - usleep(200000); // 200ms delay to ensure clean state + usleep(100000); // 100ms delay to ensure clean state // Generate and send command with CRC uint16_t crc = cal_crc_half((uint8_t*)cmd, strlen(cmd)); @@ -374,18 +545,32 @@ int cInverter::query_auto(const char *cmd, int max_size) { write(fd, &temp_buf, n); tcdrain(fd); + usleep(500000); // allow inverter to respond // Clear buffer for reading memset(temp_buf, 0, sizeof(temp_buf)); time(&started); - // Read until we find CR (0x0d) or timeout + // Read until we find CR/LF or timeout + bool started_frame = false; + unsigned char ch = 0; while (i < max_size && (time(NULL) - started < 5)) { - n = read(fd, temp_buf+i, 1); // Read one byte at a time + n = read(fd, &ch, 1); // Read one byte at a time if (n > 0) { - i += n; + if (!started_frame) { + if (ch != '(') { + continue; + } + started_frame = true; + temp_buf[0] = ch; + i = 1; + continue; + } + + temp_buf[i++] = ch; + // Found the terminator - if (temp_buf[i-1] == 0x0d) { + if (ch == 0x0d || ch == 0x0a) { lprintf("INVERTER: Auto-discovery for %s: found CR at byte %d", cmd, i); break; } @@ -397,7 +582,11 @@ int cInverter::query_auto(const char *cmd, int max_size) { close(fd); // Validate the response - if (i > 0 && temp_buf[0] == '(' && temp_buf[i-1] == 0x0d) { + if (i > 0 && temp_buf[0] == '(' && (temp_buf[i-1] == 0x0d || temp_buf[i-1] == 0x0a)) { + if (i >= 5 && temp_buf[1] == 'N' && temp_buf[2] == 'A' && temp_buf[3] == 'K') { + lprintf("INVERTER: Auto-discovery for %s got NAK", cmd); + return -1; + } lprintf("INVERTER: Auto-discovery for %s successful: %d bytes", cmd, i); return i; } else { @@ -425,32 +614,32 @@ int cInverter::query_auto(const char *cmd, int max_size) { void cInverter::AutoDiscoverBufferSizes() { printf("\n=== AUTO-DISCOVERY MODE ===\n"); printf("Testing inverter to find correct buffer sizes...\n\n"); - - int qmod_size = query_auto("QMOD", 20); + + int qmod_size = query("QMOD", buf_qmod) ? last_reply_size : query_auto("QMOD", 20); if (qmod_size > 0) { printf("✓ QMOD buffer size: %d\n", qmod_size); } else { printf("✗ QMOD auto-discovery failed\n"); } sleep(1); - - int qpigs_size = query_auto("QPIGS", 150); + + int qpigs_size = query("QPIGS", buf_qpigs) ? last_reply_size : query_auto("QPIGS", 150); if (qpigs_size > 0) { printf("✓ QPIGS buffer size: %d\n", qpigs_size); } else { printf("✗ QPIGS auto-discovery failed\n"); } sleep(1); - - int qpiri_size = query_auto("QPIRI", 150); + + int qpiri_size = query("QPIRI", buf_qpiri) ? last_reply_size : query_auto("QPIRI", 150); if (qpiri_size > 0) { printf("✓ QPIRI buffer size: %d\n", qpiri_size); } else { printf("✗ QPIRI auto-discovery failed\n"); } sleep(1); - - int qpiws_size = query_auto("QPIWS", 100); + + int qpiws_size = query("QPIWS", buf_qpiws) ? last_reply_size : query_auto("QPIWS", 100); if (qpiws_size > 0) { printf("✓ QPIWS buffer size: %d\n", qpiws_size); } else { @@ -468,8 +657,8 @@ void cInverter::AutoDiscoverBufferSizes() { // Output in parsable format for scripts printf("DISCOVERY_QMOD=%d\n", qmod_size > 0 ? qmod_size : 5); printf("DISCOVERY_QPIGS=%d\n", qpigs_size > 0 ? qpigs_size : 110); - printf("DISCOVERY_QPIRI=%d\n", qpiri_size > 0 ? qpiri_size : 98); - printf("DISCOVERY_QPIWS=%d\n", qpiws_size > 0 ? qpiws_size : 36); + printf("DISCOVERY_QPIRI=%d\n", qpiri_size > 0 ? qpiri_size : 103); + printf("DISCOVERY_QPIWS=%d\n", qpiws_size > 0 ? qpiws_size : 40); printf("DISCOVERY_SUCCESS=%s\n", (qmod_size > 0 && qpigs_size > 0 && qpiri_size > 0 && qpiws_size > 0) ? "true" : "false"); } @@ -496,12 +685,19 @@ int cInverter::DiscoverParallelInverters() { serial[j++] = buf[k]; } - // Check if serial is valid (not all zeros and not empty) + // Check if serial is valid (not empty, not all zeros, not "0.0") bool valid_serial = false; - for (int k = 0; k < j; k++) { - if (serial[k] != '0') { + if (j > 0) { + bool all_zero = true; + for (int k = 0; k < j; k++) { + if (serial[k] != '0') { + all_zero = false; + break; + } + } + + if (!all_zero && strcmp(serial, "0.0") != 0) { valid_serial = true; - break; } } @@ -552,3 +748,36 @@ string cInverter::GetParallelStatus(int inverter_num) { return ""; } + +int cInverter::QueryParallelQpgs(int count, std::vector &replies) { + replies.clear(); + if (count <= 0) { + return 0; + } + + replies.resize(count); + int fd = openSerial(); + if (fd < 0) { + return 0; + } + + int ok = 0; + for (int i = 0; i < count; i++) { + std::string cmd = "QPGS" + std::to_string(i); + for (int attempt = 0; attempt < 3; attempt++) { + if (sendCommand(fd, cmd)) { + std::string payload; + if (readReply(fd, payload)) { + replies[i] = payload; + ok++; + break; + } + } + usleep(600000); + } + usleep(500000); + } + + closeSerial(fd); + return ok; +} diff --git a/sources/inverter-cli/inverter.h b/sources/inverter-cli/inverter.h index 59cc1b6..91e0cd0 100644 --- a/sources/inverter-cli/inverter.h +++ b/sources/inverter-cli/inverter.h @@ -4,6 +4,7 @@ #include #include #include +#include using namespace std; @@ -23,6 +24,12 @@ class cInverter { int buf_qpiws; int buf_qmod; int buf_qpigs; + int last_reply_size; + + int openSerial(); + void closeSerial(int fd); + bool sendCommand(int fd, const std::string &cmd); + bool readReply(int fd, std::string &payload); void SetMode(char newmode); bool CheckCRC(unsigned char *buff, int len); @@ -47,6 +54,7 @@ class cInverter { void AutoDiscoverBufferSizes(); int DiscoverParallelInverters(); // Returns number of parallel inverters string GetParallelStatus(int inverter_num); // Get QPGS data for specific inverter + int QueryParallelQpgs(int count, std::vector &replies); }; #endif // ___INVERTER_H diff --git a/sources/inverter-cli/main.cpp b/sources/inverter-cli/main.cpp index 811e093..78c780a 100644 --- a/sources/inverter-cli/main.cpp +++ b/sources/inverter-cli/main.cpp @@ -175,6 +175,7 @@ int main(int argc, char* argv[]) { // Get command flag settings from the arguments (if any) InputParser cmdArgs(argc, argv); const string &rawcmd = cmdArgs.getCmdOption("-r"); + const string ¶llelDataCount = cmdArgs.getCmdOption("-P"); if(cmdArgs.cmdOptionExists("-h") || cmdArgs.cmdOptionExists("--help")) { return print_help(); @@ -209,6 +210,27 @@ int main(int argc, char* argv[]) { exit(0); } + // Parallel inverter QPGS read (single session) + if(cmdArgs.cmdOptionExists("-P") || cmdArgs.cmdOptionExists("--parallel-qpgs")) { + int count = 2; + if (!parallelDataCount.empty()) { + try { + count = stoi(parallelDataCount); + } catch (...) { + count = 2; + } + } + + vector replies; + int ok = ups->QueryParallelQpgs(count, replies); + printf("PARALLEL_COUNT=%d\n", count); + for (int i = 0; i < count; i++) { + printf("QPGS%d_REPLY=%s\n", i, replies[i].c_str()); + } + printf("PARALLEL_OK=%d\n", ok); + exit(0); + } + // Logic to send 'raw commands' to the inverter.. if (!rawcmd.empty()) { ups->ExecuteCmd(rawcmd); diff --git a/sources/inverter-mqtt/entrypoint.sh b/sources/inverter-mqtt/entrypoint.sh index 023e919..b8148bf 100755 --- a/sources/inverter-mqtt/entrypoint.sh +++ b/sources/inverter-mqtt/entrypoint.sh @@ -236,7 +236,11 @@ fi echo "" echo "=== Starting MQTT Bridge Services ===" -echo "Using parallel inverter mode (2 inverters)" +if [ -n "$INVERTER_DEVICES" ]; then + echo "Using multi-device mode (INVERTER_DEVICES=${INVERTER_DEVICES})" +else + echo "Using parallel inverter mode (2 inverters)" +fi echo "" # Wait a bit for the device to be ready @@ -267,7 +271,12 @@ echo "✓ MQTT discovery topics initialized" # Init the mqtt server every 5 minutes (300 seconds) # This will re-create the auto-created topics in the MQTT server if HA is restarted... echo "Starting MQTT initialization service (every 5 minutes)..." -watch -n 300 "$MQTT_INIT_SCRIPT" > /dev/null 2>&1 & +( + while true; do + "$MQTT_INIT_SCRIPT" > /dev/null 2>&1 + sleep 300 + done +) & # Run the MQTT Subscriber process in the background (so that way we can change the configuration on the inverter from home assistant) echo "Starting MQTT subscriber for commands..." @@ -280,4 +289,7 @@ echo "✓ All services started successfully!" echo " Logs will appear below..." echo "" -watch -n 30 "$MQTT_PUSH_SCRIPT" > /dev/null 2>&1 +while true; do + "$MQTT_PUSH_SCRIPT" > /dev/null 2>&1 + sleep 30 +done diff --git a/sources/inverter-mqtt/mqtt-push-parallel.sh b/sources/inverter-mqtt/mqtt-push-parallel.sh index ce0b365..a0fc4fe 100755 --- a/sources/inverter-mqtt/mqtt-push-parallel.sh +++ b/sources/inverter-mqtt/mqtt-push-parallel.sh @@ -8,12 +8,14 @@ if [ -f "/etc/inverter/mqtt.json" ] && [ -x "/opt/inverter-cli/bin/inverter_poll MQTT_CONFIG="/etc/inverter/mqtt.json" INVERTER_BIN="/opt/inverter-cli/bin/inverter_poller" MQTT_FALLBACK="/opt/inverter-mqtt/mqtt-push.sh" + INVERTER_CONF="/etc/inverter/inverter.conf" CONTAINER_MODE=true else # Development mode MQTT_CONFIG="/home/pi/Progetti/config/mqtt.json" INVERTER_BIN="/home/pi/Progetti/sources/inverter-cli/bin/inverter_poller" MQTT_FALLBACK="/home/pi/Progetti/sources/inverter-mqtt/mqtt-push.sh" + INVERTER_CONF="/home/pi/Progetti/config/inverter.conf" CONTAINER_MODE=false fi @@ -180,8 +182,36 @@ if [ "$EUID" -ne 0 ] && [ -c "/dev/ttyUSB0" ]; then SUDO_CMD="sudo" fi +# Multi-device mode: if INVERTER_DEVICES is set, poll each device separately +if [ -n "$INVERTER_DEVICES" ]; then + IFS=':' read -ra DEVICE_ARRAY <<< "$INVERTER_DEVICES" + if [ ${#DEVICE_ARRAY[@]} -gt 0 ]; then + echo "Multi-device mode enabled (${#DEVICE_ARRAY[@]} devices)" + for idx in "${!DEVICE_ARRAY[@]}"; do + inv_id=$((idx + 1)) + device_path="${DEVICE_ARRAY[$idx]}" + temp_dir=$(mktemp -d) + cp "$INVERTER_CONF" "$temp_dir/inverter.conf" 2>/dev/null + sed -i "s|^device=.*|device=$device_path|g" "$temp_dir/inverter.conf" + + INVERTER_DATA=$(cd "$temp_dir" && $SUDO_CMD "$INVERTER_BIN" -1 2>/dev/null | tr -d '\r') + rm -rf "$temp_dir" + + if [ -z "$INVERTER_DATA" ] || ! echo "$INVERTER_DATA" | jq -e . >/dev/null 2>&1; then + echo "⚠ No valid JSON from $device_path (inv$inv_id)" + continue + fi + + extractAndPublishAllData "$inv_id" "$INVERTER_DATA" + done + + echo "Multi-device MQTT push completed" + exit 0 + fi +fi + # Try parallel discovery with retry -MAX_RETRIES=1 # Reduced to 1 since we'll fallback to direct QPGS anyway +MAX_RETRIES=3 RETRY_DELAY=2 PARALLEL_COUNT=0 @@ -205,44 +235,80 @@ done # If still no count or discovery failed, try direct QPGS commands if [ -z "$PARALLEL_COUNT" ] || [ "$PARALLEL_COUNT" -eq 0 ]; then echo "⚠ Discovery reports 0 inverters, trying direct QPGS0/QPGS1 commands..." - - # Test QPGS0 to see if parallel mode is active - TEST_QPGS0=`$SUDO_CMD "$INVERTER_BIN" -r "QPGS0" 2>&1 | grep "Reply:"` - TEST_QPGS1=`$SUDO_CMD "$INVERTER_BIN" -r "QPGS1" 2>&1 | grep "Reply:"` - - if [ ! -z "$TEST_QPGS0" ] && [[ "$TEST_QPGS0" != *"NAK"* ]]; then - echo "✓ QPGS0 responds, assuming 2 inverters in cascade" - PARALLEL_COUNT=2 - - # Build fake discovery output - PARALLEL_DISCOVERY="INVERTER_1_SERIAL=unknown + + for attempt in 1 2 3; do + # Test QPGS0 to see if parallel mode is active + TEST_QPGS0=`$SUDO_CMD "$INVERTER_BIN" -r "QPGS0" 2>&1 | grep "Reply:"` + TEST_QPGS1=`$SUDO_CMD "$INVERTER_BIN" -r "QPGS1" 2>&1 | grep "Reply:"` + + if [ ! -z "$TEST_QPGS0" ] && [[ "$TEST_QPGS0" != *"NAK"* ]]; then + echo "✓ QPGS0 responds, assuming 2 inverters in cascade" + PARALLEL_COUNT=2 + + # Build fake discovery output + PARALLEL_DISCOVERY="INVERTER_1_SERIAL=unknown INVERTER_1_QPGS=0 INVERTER_2_SERIAL=unknown INVERTER_2_QPGS=1" - elif [ ! -z "$TEST_QPGS1" ] && [[ "$TEST_QPGS1" != *"NAK"* ]]; then - echo "✓ QPGS1 responds, assuming 2 inverters in cascade" - PARALLEL_COUNT=2 - - # Build fake discovery output - PARALLEL_DISCOVERY="INVERTER_1_SERIAL=unknown + break + elif [ ! -z "$TEST_QPGS1" ] && [[ "$TEST_QPGS1" != *"NAK"* ]]; then + echo "✓ QPGS1 responds, assuming 2 inverters in cascade" + PARALLEL_COUNT=2 + + # Build fake discovery output + PARALLEL_DISCOVERY="INVERTER_1_SERIAL=unknown INVERTER_1_QPGS=0 INVERTER_2_SERIAL=unknown INVERTER_2_QPGS=1" - else + break + else + echo "⚠ QPGS commands failed (attempt $attempt/3)" + sleep 1 + fi + done + + if [ "$PARALLEL_COUNT" -eq 0 ]; then echo "✗ QPGS commands also failed" - PARALLEL_COUNT=0 fi fi -echo "Processing $PARALLEL_COUNT parallel inverters" +DISCOVERY_SERIALS=() +DISCOVERY_QPGS=() +MAX_QPGS_IDX=-1 -# Publish discovery info -pushMQTTData "system" "parallel_count" "$PARALLEL_COUNT" +for i in $(seq 1 $PARALLEL_COUNT); do + serial=$(echo "$PARALLEL_DISCOVERY" | grep "INVERTER_${i}_SERIAL=" | cut -d= -f2) + qpgs_idx=$(echo "$PARALLEL_DISCOVERY" | grep "INVERTER_${i}_QPGS=" | cut -d= -f2) + + if [ -z "$serial" ] || [ "$serial" = "0.0" ]; then + continue + fi + if echo "$serial" | grep -qE '^0+$'; then + continue + fi + if [ -z "$qpgs_idx" ]; then + continue + fi + + DISCOVERY_SERIALS+=("$serial") + DISCOVERY_QPGS+=("$qpgs_idx") + + if [ "$qpgs_idx" -gt "$MAX_QPGS_IDX" ]; then + MAX_QPGS_IDX=$qpgs_idx + fi +done # Get QPIRI data once (shared configuration for all inverters in cascade) echo "" echo "Getting shared configuration (QPIRI)..." -QPIRI_RAW=`$SUDO_CMD "$INVERTER_BIN" -r "QPIRI" 2>&1 | grep "Reply:" | cut -d: -f2- | xargs` +QPIRI_RAW="" +for attempt in 1 2 3 4 5; do + QPIRI_RAW=`$SUDO_CMD "$INVERTER_BIN" -r "QPIRI" 2>&1 | grep "Reply:" | cut -d: -f2- | xargs` + if [ ! -z "$QPIRI_RAW" ] && [ "$QPIRI_RAW" != "NAK" ]; then + break + fi + sleep 1 +done QPIRI_SUCCESS=false if [ ! -z "$QPIRI_RAW" ] && [ "$QPIRI_RAW" != "NAK" ]; then @@ -270,18 +336,145 @@ else echo "⚠ QPIRI failed, configuration parameters unavailable" fi -# Extract runtime data for each inverter using QPGS +# Extract runtime data for each inverter using QPGS (single session) PARALLEL_SUCCESS=false -for i in $(seq 1 $PARALLEL_COUNT); do - QPGS_IDX=$((i - 1)) # 0-based index - SERIAL=`echo "$PARALLEL_DISCOVERY" | grep "INVERTER_${i}_SERIAL=" | cut -d= -f2` - +PARALLEL_QPGS_COUNT=$PARALLEL_COUNT +if [ "$PARALLEL_QPGS_COUNT" -lt 2 ]; then + PARALLEL_QPGS_COUNT=2 +fi +if [ "$MAX_QPGS_IDX" -ge 0 ] && [ $((MAX_QPGS_IDX + 1)) -gt "$PARALLEL_QPGS_COUNT" ]; then + PARALLEL_QPGS_COUNT=$((MAX_QPGS_IDX + 1)) +fi + +PARALLEL_QPGS_OUTPUT=`$SUDO_CMD "$INVERTER_BIN" -P "$PARALLEL_QPGS_COUNT" 2>/dev/null` +HAS_PARALLEL_QPGS=false +if echo "$PARALLEL_QPGS_OUTPUT" | grep -q "^QPGS[0-9]_REPLY="; then + HAS_PARALLEL_QPGS=true + PARALLEL_QPGS_OUTPUT=`$SUDO_CMD "$INVERTER_BIN" -P "$PARALLEL_QPGS_COUNT" 2>/dev/null` +fi + +VALID_SERIALS=() +VALID_QPGS=() + +DIRECT_SERIALS=() +DIRECT_QPGS=() +if [ "$PARALLEL_COUNT" -lt 2 ]; then + for idx in 0 1; do + reply_value=`$SUDO_CMD "$INVERTER_BIN" -r "QPGS$idx" 2>&1 | grep "Reply:" | cut -d: -f2- | xargs` + if [ -z "$reply_value" ]; then + continue + fi + serial_from_reply=`echo "$reply_value" | awk '{print $2}'` + if [ -z "$serial_from_reply" ] || [ "$serial_from_reply" = "0.0" ]; then + continue + fi + if echo "$serial_from_reply" | grep -qE '^0+$'; then + continue + fi + if ! echo "$serial_from_reply" | grep -qE '^[0-9]{10,}$'; then + continue + fi + duplicate=false + for existing in "${DIRECT_SERIALS[@]}"; do + if [ "$existing" = "$serial_from_reply" ]; then + duplicate=true + break + fi + done + if [ "$duplicate" = true ]; then + continue + fi + DIRECT_SERIALS+=("$serial_from_reply") + DIRECT_QPGS+=("$idx") + done +fi + +if [ ${#DIRECT_SERIALS[@]} -gt 0 ]; then + VALID_SERIALS=("${DIRECT_SERIALS[@]}") + VALID_QPGS=("${DIRECT_QPGS[@]}") +fi + +if [ ${#VALID_SERIALS[@]} -eq 0 ] && [ "$HAS_PARALLEL_QPGS" = true ]; then + for attempt in 1 2 3; do + VALID_SERIALS=() + VALID_QPGS=() + + for idx in $(seq 0 $((PARALLEL_QPGS_COUNT - 1))); do + reply_line=`echo "$PARALLEL_QPGS_OUTPUT" | grep "^QPGS${idx}_REPLY="` + reply_value=`echo "$reply_line" | cut -d= -f2- | xargs` + if [ -z "$reply_value" ]; then + continue + fi + + serial_from_reply=`echo "$reply_value" | awk '{print $2}'` + if [ -z "$serial_from_reply" ] || [ "$serial_from_reply" = "0.0" ]; then + continue + fi + if echo "$serial_from_reply" | grep -qE '^0+$'; then + continue + fi + if ! echo "$serial_from_reply" | grep -qE '^[0-9]{10,}$'; then + continue + fi + + duplicate=false + for existing in "${VALID_SERIALS[@]}"; do + if [ "$existing" = "$serial_from_reply" ]; then + duplicate=true + break + fi + done + if [ "$duplicate" = true ]; then + continue + fi + + VALID_SERIALS+=("$serial_from_reply") + VALID_QPGS+=("$idx") + done + + if [ ${#VALID_SERIALS[@]} -ge 2 ]; then + break + fi + + PARALLEL_QPGS_OUTPUT=`$SUDO_CMD "$INVERTER_BIN" -P "$PARALLEL_QPGS_COUNT" 2>/dev/null` + sleep 1 + done +fi + +if [ ${#VALID_SERIALS[@]} -eq 0 ]; then + VALID_SERIALS=("${DISCOVERY_SERIALS[@]}") + VALID_QPGS=("${DISCOVERY_QPGS[@]}") +fi + +VALID_COUNT=${#VALID_SERIALS[@]} + +SUCCESS_INV_IDS=() + +if [ "$VALID_COUNT" -eq 0 ]; then + echo "⚠ No valid inverter serials found (excluding 0.0/all-zero)" +else + echo "Processing $VALID_COUNT valid parallel inverters" +fi + +# Publish discovery info (valid inverters only) +pushMQTTData "system" "parallel_count" "$VALID_COUNT" + +for idx in "${!VALID_SERIALS[@]}"; do + inv_id=$((idx + 1)) + QPGS_IDX="${VALID_QPGS[$idx]}" + SERIAL="${VALID_SERIALS[$idx]}" + echo "" - echo "Processing Inverter #$i (Serial: $SERIAL, QPGS$QPGS_IDX)" - - # Get QPGS data: 1 Serial Mode Status GridV GridF OutV OutF VA W PCT BattV ChgA Cap PVV PVA ... - QPGS_RAW=`$SUDO_CMD "$INVERTER_BIN" -r "QPGS$QPGS_IDX" 2>&1 | grep "Reply:" | cut -d: -f2- | xargs` + echo "Processing Inverter #$inv_id (Serial: $SERIAL, QPGS$QPGS_IDX)" + + # Get QPGS data from single-session output, fallback to direct query + if [ "$HAS_PARALLEL_QPGS" = true ]; then + QPGS_RAW=`echo "$PARALLEL_QPGS_OUTPUT" | grep "^QPGS${QPGS_IDX}_REPLY=" | cut -d= -f2- | xargs` + fi + if [ -z "$QPGS_RAW" ]; then + QPGS_RAW=`$SUDO_CMD "$INVERTER_BIN" -r "QPGS$QPGS_IDX" 2>&1 | grep "Reply:" | cut -d: -f2- | xargs` + fi if [ ! -z "$QPGS_RAW" ] && [ "$QPGS_RAW" != "NAK" ]; then echo " ✓ QPGS$QPGS_IDX successful" @@ -295,46 +488,57 @@ for i in $(seq 1 $PARALLEL_COUNT); do # 8=VA, 9=W, 10=PCT, 11=BattV, 12=ChgA, 13=Cap, 14=PVV, 15=PVA # 16=?, 17=?, 18=?, 19-26=additional fields + # Prefer serial from QPGS payload if valid + serial_from_data="${DATA[1]}" + if echo "$serial_from_data" | grep -qE '^[0-9]{10,}$'; then + SERIAL="$serial_from_data" + fi + # Publish serial - pushMQTTData "$i" "serial" "$SERIAL" + pushMQTTData "$inv_id" "serial" "$SERIAL" # Runtime data from QPGS (19 parameters) - [ "${DATA[2]}" ] && pushMQTTData "$i" "Inverter_mode" "${DATA[2]}" - [ "${DATA[4]}" ] && pushMQTTData "$i" "AC_grid_voltage" "${DATA[4]}" - [ "${DATA[5]}" ] && pushMQTTData "$i" "AC_grid_frequency" "${DATA[5]}" - [ "${DATA[6]}" ] && pushMQTTData "$i" "AC_out_voltage" "${DATA[6]}" - [ "${DATA[7]}" ] && pushMQTTData "$i" "AC_out_frequency" "${DATA[7]}" - [ "${DATA[8]}" ] && pushMQTTData "$i" "Load_va" "${DATA[8]}" - [ "${DATA[9]}" ] && pushMQTTData "$i" "Load_watt" "${DATA[9]}" - [ "${DATA[10]}" ] && pushMQTTData "$i" "Load_pct" "${DATA[10]}" - [ "${DATA[11]}" ] && pushMQTTData "$i" "Battery_voltage" "${DATA[11]}" - [ "${DATA[12]}" ] && pushMQTTData "$i" "Battery_charge_current" "${DATA[12]}" - [ "${DATA[13]}" ] && pushMQTTData "$i" "Battery_capacity" "${DATA[13]}" - [ "${DATA[14]}" ] && pushMQTTData "$i" "PV_in_voltage" "${DATA[14]}" - [ "${DATA[15]}" ] && pushMQTTData "$i" "PV_in_current" "${DATA[15]}" + [ "${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[15]}" ] && pushMQTTData "$inv_id" "PV_in_current" "${DATA[15]}" # Calculate PV watts (V * A) if [ ! -z "${DATA[14]}" ] && [ ! -z "${DATA[15]}" ]; then PV_WATTS=`echo "${DATA[14]} ${DATA[15]}" | awk '{printf "%.1f", $1 * $2}'` - pushMQTTData "$i" "PV_in_watts" "$PV_WATTS" + pushMQTTData "$inv_id" "PV_in_watts" "$PV_WATTS" fi # Status flags (parse from field 18 bitmap if available) [ "${DATA[18]}" ] && { BITMAP="${DATA[18]}" - # Binary flags: bit0=Load_on, bit1=SCC_on, bit2=AC_charge_on - LOAD_ON=$((($BITMAP & 1) ? 1 : 0)) - SCC_ON=$((($BITMAP & 2) ? 1 : 0)) - AC_CHG_ON=$((($BITMAP & 4) ? 1 : 0)) - - pushMQTTData "$i" "Load_status_on" "$LOAD_ON" - pushMQTTData "$i" "SCC_charge_on" "$SCC_ON" - pushMQTTData "$i" "AC_charge_on" "$AC_CHG_ON" + if [[ "$BITMAP" =~ ^[0-9]+$ ]]; then + BITMAP=$((10#$BITMAP)) + # Binary flags: bit0=Load_on, bit1=SCC_on, bit2=AC_charge_on + LOAD_ON=$((($BITMAP & 1) ? 1 : 0)) + SCC_ON=$((($BITMAP & 2) ? 1 : 0)) + AC_CHG_ON=$((($BITMAP & 4) ? 1 : 0)) + + pushMQTTData "$inv_id" "Load_status_on" "$LOAD_ON" + pushMQTTData "$inv_id" "SCC_charge_on" "$SCC_ON" + pushMQTTData "$inv_id" "AC_charge_on" "$AC_CHG_ON" + fi } - echo " ✓ Published 17 runtime parameters for inverter #$i" + echo " ✓ Published 17 runtime parameters for inverter #$inv_id" echo " (QPGS data + calculated PV_watts + status flags)" echo " ⚠ Missing 5 params: PV_watthour, Load_watthour, Bus_voltage, Heatsink_temp, Warnings" + + SUCCESS_INV_IDS+=("$inv_id") else echo " ✗ QPGS$QPGS_IDX failed (NAK or empty)" @@ -342,27 +546,71 @@ for i in $(seq 1 $PARALLEL_COUNT); do done # Publish shared configuration parameters for ALL inverters (replicate QPIRI data) -if [ "$QPIRI_SUCCESS" = true ]; then +if [ "$QPIRI_SUCCESS" = true ] && [ ${#SUCCESS_INV_IDS[@]} -gt 0 ]; then echo "" echo "Publishing shared configuration to all inverters..." - - for i in $(seq 1 $PARALLEL_COUNT); do - echo " Replicating QPIRI config to inverter #$i..." + + for inv_id in "${SUCCESS_INV_IDS[@]}"; do + echo " Replicating QPIRI config to inverter #$inv_id..." - [ ! -z "$BATT_RECHARGE" ] && pushMQTTData "$i" "Battery_recharge_voltage" "$BATT_RECHARGE" - [ ! -z "$BATT_UNDER" ] && pushMQTTData "$i" "Battery_under_voltage" "$BATT_UNDER" - [ ! -z "$BATT_BULK" ] && pushMQTTData "$i" "Battery_bulk_voltage" "$BATT_BULK" - [ ! -z "$BATT_FLOAT" ] && pushMQTTData "$i" "Battery_float_voltage" "$BATT_FLOAT" - [ ! -z "$MAX_CHARGE_CURRENT" ] && pushMQTTData "$i" "Max_charge_current" "$MAX_CHARGE_CURRENT" - [ ! -z "$MAX_GRID_CHARGE" ] && pushMQTTData "$i" "Max_grid_charge_current" "$MAX_GRID_CHARGE" - [ ! -z "$OUT_SOURCE_PRIORITY" ] && pushMQTTData "$i" "Out_source_priority" "$OUT_SOURCE_PRIORITY" - [ ! -z "$CHARGER_SOURCE_PRIORITY" ] && pushMQTTData "$i" "Charger_source_priority" "$CHARGER_SOURCE_PRIORITY" - [ ! -z "$BATT_REDISCHARGE" ] && pushMQTTData "$i" "Battery_redischarge_voltage" "$BATT_REDISCHARGE" + [ ! -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" - echo " ✓ Published 9 shared config parameters to inv$i" + echo " ✓ Published 9 shared config parameters to inv$inv_id" done fi +# Retry shared config once after QPGS if needed +if [ "$QPIRI_SUCCESS" = false ] && [ ${#SUCCESS_INV_IDS[@]} -gt 0 ]; then + echo "" + echo "Retrying shared configuration (QPIRI) after QPGS..." + for attempt in 1 2 3; do + QPIRI_RAW=`$SUDO_CMD "$INVERTER_BIN" -r "QPIRI" 2>&1 | grep "Reply:" | cut -d: -f2- | xargs` + if [ ! -z "$QPIRI_RAW" ] && [ "$QPIRI_RAW" != "NAK" ]; then + QPIRI_SUCCESS=true + break + fi + sleep 1 + done + + if [ "$QPIRI_SUCCESS" = true ]; then + 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]}" + + echo "Publishing shared configuration to all inverters..." + for inv_id in "${SUCCESS_INV_IDS[@]}"; do + echo " Replicating QPIRI config to inverter #$inv_id..." + [ ! -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" + echo " ✓ Published 9 shared config parameters to inv$inv_id" + done + else + echo "⚠ QPIRI retry failed, shared configuration not published" + fi +fi + # Fallback: use standard mode with full JSON output if [ "$PARALLEL_SUCCESS" = false ]; then echo "" diff --git a/sources/inverter-mqtt/test-loop-parallel.sh b/sources/inverter-mqtt/test-loop-parallel.sh index e1357ac..8de47b2 100755 --- a/sources/inverter-mqtt/test-loop-parallel.sh +++ b/sources/inverter-mqtt/test-loop-parallel.sh @@ -111,12 +111,55 @@ if [ -z "$PARALLEL_COUNT" ]; then PARALLEL_COUNT=0 fi +VALID_SERIALS=() +VALID_QPGS=() +MAX_QPGS_IDX=-1 + if [ $PARALLEL_COUNT -gt 0 ]; then - echo -e "${GREEN}✓ Found $PARALLEL_COUNT parallel inverter(s)${NC}" - for i in $(seq 1 $PARALLEL_COUNT); do - SERIAL=$(echo "$PARALLEL_OUTPUT" | grep "INVERTER_${i}_SERIAL=" | cut -d= -f2) - QPGS_IDX=$(echo "$PARALLEL_OUTPUT" | grep "INVERTER_${i}_QPGS=" | cut -d= -f2) - echo " • Inverter #$i: Serial $SERIAL (QPGS$QPGS_IDX)" + QPGS_COUNT=$PARALLEL_COUNT + if [ "$QPGS_COUNT" -lt 2 ]; then + QPGS_COUNT=2 + fi + + QPGS_OUTPUT=$($SUDO_CMD "$INVERTER_BIN" -P "$QPGS_COUNT" 2>/dev/null) + + for idx in $(seq 0 $((QPGS_COUNT - 1))); do + QPGS_DATA=$(echo "$QPGS_OUTPUT" | grep "^QPGS${idx}_REPLY=" | cut -d= -f2- | xargs) + if [ -z "$QPGS_DATA" ]; then + continue + fi + SERIAL=$(echo "$QPGS_DATA" | awk '{print $2}') + if [ -z "$SERIAL" ] || [ "$SERIAL" = "0.0" ]; then + continue + fi + if echo "$SERIAL" | grep -qE '^0+$'; then + continue + fi + if ! echo "$SERIAL" | grep -qE '^[0-9]{10,}$'; then + continue + fi + duplicate=false + for existing in "${VALID_SERIALS[@]}"; do + if [ "$existing" = "$SERIAL" ]; then + duplicate=true + break + fi + done + if [ "$duplicate" = true ]; then + continue + fi + VALID_SERIALS+=("$SERIAL") + VALID_QPGS+=("$idx") + if [ "$idx" -gt "$MAX_QPGS_IDX" ]; then + MAX_QPGS_IDX=$idx + fi + done + + VALID_COUNT=${#VALID_SERIALS[@]} + echo -e "${GREEN}✓ Found $VALID_COUNT valid parallel inverter(s)${NC}" + for idx in "${!VALID_SERIALS[@]}"; do + inv_id=$((idx + 1)) + echo " • Inverter #$inv_id: Serial ${VALID_SERIALS[$idx]} (QPGS${VALID_QPGS[$idx]})" done else echo -e "${YELLOW}⚠ No parallel inverters found, using single mode${NC}" @@ -151,25 +194,30 @@ while true; do echo "" # Get parallel data - if [ $PARALLEL_COUNT -gt 0 ]; then + if [ ${#VALID_SERIALS[@]} -gt 0 ]; then echo -e "${YELLOW}[2.2] Reading parallel inverters data${NC}" - for i in $(seq 1 $PARALLEL_COUNT); do - SERIAL=$(echo "$PARALLEL_OUTPUT" | grep "INVERTER_${i}_SERIAL=" | cut -d= -f2) - QPGS_IDX=$(echo "$PARALLEL_OUTPUT" | grep "INVERTER_${i}_QPGS=" | cut -d= -f2) + QPGS_COUNT=$((MAX_QPGS_IDX + 1)) + if [ "$QPGS_COUNT" -lt 1 ]; then + QPGS_COUNT=2 + fi + QPGS_OUTPUT=$($SUDO_CMD "$INVERTER_BIN" -P "$QPGS_COUNT" 2>/dev/null) + + for idx in "${!VALID_SERIALS[@]}"; do + inv_id=$((idx + 1)) + SERIAL="${VALID_SERIALS[$idx]}" + QPGS_IDX="${VALID_QPGS[$idx]}" + QPGS_DATA=$(echo "$QPGS_OUTPUT" | grep "^QPGS${QPGS_IDX}_REPLY=" | cut -d= -f2- | xargs) - QPGS_DATA=$($SUDO_CMD "$INVERTER_BIN" -r "QPGS$QPGS_IDX" 2>&1 | grep "Reply:" | cut -d: -f2- | xargs) - - if [ ! -z "$QPGS_DATA" ] && [ "$QPGS_DATA" != "NAK" ]; then - # Parse key values + if [ ! -z "$QPGS_DATA" ]; then IFS=' ' read -ra DATA <<< "$QPGS_DATA" MODE="${DATA[2]}" GRID_V="${DATA[4]}" BATT_V="${DATA[11]}" LOAD_W="${DATA[9]}" - - echo -e " ${GREEN}✓${NC} Inverter #$i ($SERIAL): Mode=$MODE, Grid=${GRID_V}V, Battery=${BATT_V}V, Load=${LOAD_W}W" + + echo -e " ${GREEN}✓${NC} Inverter #$inv_id ($SERIAL): Mode=$MODE, Grid=${GRID_V}V, Battery=${BATT_V}V, Load=${LOAD_W}W" else - echo -e " ${RED}✗${NC} Inverter #$i ($SERIAL): No data" + echo -e " ${RED}✗${NC} Inverter #$inv_id ($SERIAL): No data" fi done echo "" @@ -184,13 +232,14 @@ while true; do echo "$MQTT_OUTPUT" | sed 's/^/ /' # Show sample topics published - if [ $PARALLEL_COUNT -gt 0 ]; then + if [ ${#VALID_SERIALS[@]} -gt 0 ]; then echo -e " ${GREEN}Sample topics published:${NC}" - for i in $(seq 1 $PARALLEL_COUNT); do - echo " • $MQTT_TOPIC/sensor/${MQTT_DEVICE}_inv${i}_serial" - echo " • $MQTT_TOPIC/sensor/${MQTT_DEVICE}_inv${i}_Battery_voltage" - echo " • $MQTT_TOPIC/sensor/${MQTT_DEVICE}_inv${i}_Load_watt" - [ $i -eq 1 ] && echo " • ... (and more)" + for idx in "${!VALID_SERIALS[@]}"; do + inv_id=$((idx + 1)) + echo " • $MQTT_TOPIC/sensor/${MQTT_DEVICE}_inv${inv_id}_serial" + echo " • $MQTT_TOPIC/sensor/${MQTT_DEVICE}_inv${inv_id}_Battery_voltage" + echo " • $MQTT_TOPIC/sensor/${MQTT_DEVICE}_inv${inv_id}_Load_watt" + [ $inv_id -eq 1 ] && echo " • ... (and more)" done fi