/*************************************************************************/ /* Copyright (C) 2019 Don M/glitterkitty - License: GNU GPLv3 */ /* Copyright (C) 2023 Wizardry and Steamworks - License: GNU GPLv3 */ /*************************************************************************/ // v1.0 // // Reworked from: https://github.com/glitterkitty/EpEverSolarMonitor // // // // Current documentation @ // // https://grimore.org/hardware/epever/generic_controller_monitor // // // // Differences & Highlights: // // * added OTA update capabilities, // // * settings are made via MQTT instead of hardware (debug), // // * used a JSON objects intead of MQTT topics, // // * made deep sleep optional, // // * ensured re-entrancy when resuming out of suspend, // // * made time to process MQTT messages configurable, // // * added some compatiblity with ESP32, // // * made multiple parameters configurable, // // * small retouches and aesthetics // // // // About: // // This is an Arduino sketch for retrieving metrics off a solar panel // // controller made by EpEver and should work for most MPPT models. // // // // The original documentation by glitterkitty mentioned that the EPEVER // // outputs +7.5V on the orange-white RJ45 connector but at least for the // // EPEVER 40A the voltage between the orange and the brown wire of the // // RJ45 connector has been measured to be +5.11V which is great because // // no stepdown is needed and the EPEVER can power both the MAX485 TTL // // as well as the ESP. In any case, the voltage should be measured // // again before implementing the circuit diagram just to make sure. // // // // Essential wiring diagram: // // // // +---------+ // // | | // // orange +<--------+--- 5V --+------------------------> + VCC 5V // // | + DI <-------------------> + TX // // green +<--------+ A + DE <-------------------> + D2 // // EPEVER | | ESP // // RJ45 | MAX485 | 8266 / 32 // // blue +<--------+ B + RE <-------------------> + D1 // // | + RO <-------------------> + RX // // brown +<--------+-- GND --+------------------------> + GND // // | | // // +---------+ // // // // Optional wiring: // // * connect the ESP D0 GPIO to RST via <= 1kOhm resistor and then set // // the variable USE_DEEP_SLEEP in this sketch to true in order to // // allow the ESP to go into deep sleep mode and consume less power. // // // // Software usage: // // The sketch will generate JSON payloads and send them to the following // // MQTT topics: // // // // MQTT_TOPIC_PUB / panel | battery | load | energy | extra | monitor // // // // where panel, battery, load, energy, extra and monitor are various // // subtopics of the configurable MQTT_TOPIC_PUB topic. For example to // // just listen for battery notifications, subscribe to the following // // MQTT topic: MQTT_TOPIC_PUB/battery // // // // The the sketch will subscribe to the topic defined by MQTT_TOPIC_SUB // // and listen to the following JSON grammar: // // { // // "action" : "settings" | "switch" // // "sleep" (when "action" is "settings") : time in seconds // // "switch" (when "action" is "switch") : "on" | "off" // // } // // // // For example, to set the sleep time between metric updates to about // // 5 minutes, send the following JSON payload on the MQTT_TOPIC_SUB // // topic: // // { // // "action" : "settings" // // "sleep" : 120 // // } // // // // Note that for the switch to work, the Epever device has to be set to // // "manual mode". From the manual, that seems to be setting the load // // setting for the first timer to 117 and the second should display 2, n // // and then the load can be toggled using this sketch. // /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// // configuration // /////////////////////////////////////////////////////////////////////////// // Whether to send data over the serial port. #define SERIAL_DATA 1 // RS-485 module pin DE maps to D2 GPIO on ESP. #define MAX485_DE D2 // RS-485 module pin RE maps to D1 GPIO on ESP. #define MAX485_RE D1 #define STA_SSID "" #define STA_PASSWORD "" #define MQTT_SERVER "" #define MQTT_PORT 1883 #define MQTT_CLIENT_ID "EpEver Solar Monitor" #define MQTT_TOPIC_PUB "epever-40a/talk" #define MQTT_TOPIC_SUB "epever-40a/hear" // Seconds to wait for MQTT message to be delivered and processed. #define MQTT_SUBSCRIBE_WAIT 10 // The OTA hostname. //#define OTA_HOSTNAME // The OTA port on which updates take place. //#define OTA_PORT 8266 // The authentication password to use for OTA updates. // This should be set to the unsalted MD5 hash of the plaintext password. //#define OTA_PASSWORD_HASH // Whether to use deep sleep or not (requires hardware modifications). //#define USE_DEEP_SLEEP 1 // the minimal amount that the ESP should sleep for. #define MIN_SLEEP_SECONDS 60 /////////////////////////////////////////////////////////////////////////// // general variable declarations and libraries // /////////////////////////////////////////////////////////////////////////// #include #if defined(ARDUINO_ARCH_ESP8266) #include #include #elif defined(ARDUINO_ARCH_ESP32) #include #include #endif #include #include #include #if defined(ARDUINO_ARCH_ESP8266) #include #elif defined(ARDUINO_ARCH_ESP32) #include #endif ModbusMaster node; WiFiClient wifi_client; PubSubClient mqtt_client(wifi_client); bool loadState = true; int sleepSeconds; const int JSON_DOCUMENT_SIZE = 512; StaticJsonDocument controllerStatusPayload; StaticJsonDocument epeverMetricsPayload; StaticJsonDocument epeverControlPayload; char tmpJsonPayloadBuffer[JSON_DOCUMENT_SIZE]; /////////////////////////////////////////////////////////////////////////// // modbus data definitions // /////////////////////////////////////////////////////////////////////////// // ModBus Register Locations // Start of live-data #define LIVE_DATA 0x3100 // 16 registers #define LIVE_DATA_CNT 16 // just for reference, not used in code #define PANEL_VOLTS 0x00 #define PANEL_AMPS 0x01 #define PANEL_POWER_L 0x02 #define PANEL_POWER_H 0x03 #define BATT_VOLTS 0x04 #define BATT_AMPS 0x05 #define BATT_POWER_L 0x06 #define BATT_POWER_H 0x07 // dummy * 4 #define LOAD_VOLTS 0x0C #define LOAD_AMPS 0x0D #define LOAD_POWER_L 0x0E #define LOAD_POWER_H 0x0F // D7-0 Sec, D15-8 Min : D7-0 Hour, D15-8 Day : D7-0 Month, D15-8 Year #define RTC_CLOCK 0x9013 // 3 regs #define RTC_CLOCK_CNT 3 // State of Charge in percent, 1 reg #define BATTERY_SOC 0x311A // Battery current L #define BATTERY_CURRENT_L 0x331B // Battery current H #define BATTERY_CURRENT_H 0x331C // start of statistical data #define STATISTICS 0x3300 // 22 registers #define STATISTICS_CNT 22 // just for reference, not used in code // Maximum input volt (PV) today #define PV_MAX 0x00 // Minimum input volt (PV) today #define PV_MIN 0x01 // Maximum battery volt today #define BATT_MAX 0x02 // Minimum battery volt today #define BATT_MIN 0x03 // Consumed energy today L #define CONS_ENERGY_DAY_L 0x04 // Consumed energy today H #define CONS_ENGERY_DAY_H 0x05 // Consumed energy this month L #define CONS_ENGERY_MON_L 0x06 // Consumed energy this month H #define CONS_ENGERY_MON_H 0x07 // Consumed energy this year L #define CONS_ENGERY_YEAR_L 0x08 // Consumed energy this year H #define CONS_ENGERY_YEAR_H 0x09 // Total consumed energy L #define CONS_ENGERY_TOT_L 0x0A // Total consumed energy H #define CONS_ENGERY_TOT_H 0x0B // Generated energy today L #define GEN_ENERGY_DAY_L 0x0C // Generated energy today H #define GEN_ENERGY_DAY_H 0x0D // Generated energy this month L #define GEN_ENERGY_MON_L 0x0E // Generated energy this month H #define GEN_ENERGY_MON_H 0x0F // Generated energy this year L #define GEN_ENERGY_YEAR_L 0x10 // Generated energy this year H #define GEN_ENERGY_YEAR_H 0x11 // Total generated energy L #define GEN_ENERGY_TOT_L 0x12 // Total Generated energy H #define GEN_ENERGY_TOT_H 0x13 // Carbon dioxide reduction L #define CO2_REDUCTION_L 0x14 // Carbon dioxide reduction H #define CO2_REDUCTION_H 0x15 // r/w load switch state #define LOAD_STATE 0x02 #define STATUS_FLAGS 0x3200 // Battery status register #define STATUS_BATTERY 0x00 // Charging equipment status register #define STATUS_CHARGER 0x01 /////////////////////////////////////////////////////////////////////////// // datastructures, also for buffer to values conversion // /////////////////////////////////////////////////////////////////////////// // clock union { struct { uint8_t s; uint8_t m; uint8_t h; uint8_t d; uint8_t M; uint8_t y; } r; uint16_t buf[3]; } rtc; // live data union { struct { int16_t pV; int16_t pI; int32_t pP; int16_t bV; int16_t bI; int32_t bP; uint16_t dummy[4]; int16_t lV; int16_t lI; int32_t lP; } l; uint16_t buf[16]; } live; // statistics union { struct { // 4*1 = 4 uint16_t pVmax; uint16_t pVmin; uint16_t bVmax; uint16_t bVmin; // 4*2 = 8 uint32_t consEnerDay; uint32_t consEnerMon; uint32_t consEnerYear; uint32_t consEnerTotal; // 4*2 = 8 uint32_t genEnerDay; uint32_t genEnerMon; uint32_t genEnerYear; uint32_t genEnerTotal; // 1*2 = 2 uint32_t c02Reduction; } s; uint16_t buf[22]; } stats; // these are too far away for the union conversion trick uint16_t batterySOC = 0; int32_t batteryCurrent = 0; // battery status struct { uint8_t volt; // d3-d0 Voltage: 00H Normal, 01H Overvolt, 02H UnderVolt, 03H Low Volt Disconnect, 04H Fault uint8_t temp; // d7-d4 Temperature: 00H Normal, 01H Over warning settings, 02H Lower than the warning settings uint8_t resistance; // d8 abnormal 1, normal 0 uint8_t rated_volt; // d15 1-Wrong identification for rated voltage } status_batt; char batt_volt_status[][20] = { "Normal", "Overvolt", "Low Volt Disconnect", "Fault" }; char batt_temp_status[][16] = { "Normal", "Over WarnTemp", "Below WarnTemp" }; // charging equipment status (not fully impl. yet) //uint8_t charger_operation = 0; //uint8_t charger_state = 0; //uint8_t charger_input = 0; uint8_t charger_mode = 0; //char charger_input_status[][20] = { // "Normal", // "No power connected", // "Higher volt input", // "Input volt error" //}; char charger_charging_status[][12] = { "Off", "Float", "Boost", "Equlization" }; /////////////////////////////////////////////////////////////////////////// // ModBus helper functions // /////////////////////////////////////////////////////////////////////////// void preTransmission() { digitalWrite(MAX485_RE, 1); digitalWrite(MAX485_DE, 1); digitalWrite(LED_BUILTIN, LOW); } void postTransmission() { digitalWrite(MAX485_RE, 0); digitalWrite(MAX485_DE, 0); digitalWrite(LED_BUILTIN, HIGH); } /////////////////////////////////////////////////////////////////////////// // MQTT event handling // /////////////////////////////////////////////////////////////////////////// void mqtt_reconnect() { // Loop until we're reconnected Serial.println("Checking MQT connection..."); while (!mqtt_client.connected()) { Serial.print("MQTT Reconnecting: "); // Attempt to connect if (mqtt_client.connect(MQTT_CLIENT_ID)) { Serial.println("success"); Serial.print("Subscribing MQTT: "); mqtt_client.subscribe(MQTT_TOPIC_SUB); Serial.println(MQTT_TOPIC_SUB); } else { Serial.print("failure, rc="); Serial.print(mqtt_client.state()); Serial.println(" try again in 1 second"); delay(1000); } } } // control load on / off here, setting sleep duration // void mqtt_callback(char* topic, byte* payload, unsigned int length) { uint8_t i, result; Serial.print("Message arrived ["); Serial.print(topic); Serial.print("] "); for (i = 0; i < length; i++) { Serial.print((char)payload[i]); } Serial.println(); payload[length] = '\0'; // ignore messages not sent on the subscribed topic if (strncmp(topic, MQTT_TOPIC_SUB, strlen(MQTT_TOPIC_SUB)) != 0) { return; } // Parse the payload sent to the MQTT topic as a JSON document. Serial.print("Deserializing message: "); DeserializationError error = deserializeJson(epeverControlPayload, payload); if (error) { Serial.print("failed, error="); Serial.println(error.c_str()); return; } else { Serial.println("success"); } if (!epeverControlPayload.containsKey("action")) { epeverControlPayload.clear(); return; } if (epeverControlPayload["action"] == "switch") { // Switch - but i can't seem to switch a coil directly here ?!? if (epeverControlPayload["status"] == "on") { loadState = true; } if (epeverControlPayload["status"] == "off") { loadState = false; } Serial.print("Setting load state:"); node.clearResponseBuffer(); node.writeSingleCoil(0x0001, 1); result = node.writeSingleCoil(0x0002, loadState); if (result == node.ku8MBSuccess) { Serial.println("success"); } else { Serial.println("failure"); Serial.print("Miss write loadState, ret val: "); Serial.println(result, HEX); } } if (epeverControlPayload["action"] == "settings") { if (epeverControlPayload.containsKey("sleep")) { // input sanitization int seconds = (unsigned int)epeverControlPayload["sleep"]; if (seconds == sleepSeconds) { Serial.println("no change"); return; } if (seconds < MIN_SLEEP_SECONDS) { sleepSeconds = MIN_SLEEP_SECONDS; } else { sleepSeconds = seconds; } EEPROM.put(0, sleepSeconds); if (!EEPROM.commit()) { Serial.println("Failure setting sleep seconds."); return; } Serial.print("Set sleep seconds to: "); Serial.println(sleepSeconds); } epeverControlPayload.clear(); return; } } /////////////////////////////////////////////////////////////////////////// // Arduino functions // /////////////////////////////////////////////////////////////////////////// void setup() { Serial.begin(115200); // DO NOT CHANGE! while (!Serial) ; Serial.println(); Serial.println("Hello World! I'm an EpEver Solar Monitor!"); // init modbus in receive mode pinMode(MAX485_RE, OUTPUT); pinMode(MAX485_DE, OUTPUT); digitalWrite(MAX485_RE, 0); digitalWrite(MAX485_DE, 0); // modbus callbacks node.preTransmission(preTransmission); node.postTransmission(postTransmission); // EPEver Device ID 1 node.begin(1, Serial); // Connect D0 to RST to wake up #ifdef USE_DEEP_SLEEP pinMode(D0, WAKEUP_PULLUP); #endif // variable handling EEPROM.begin(16); EEPROM.get(0, sleepSeconds); if (sleepSeconds < MIN_SLEEP_SECONDS) { sleepSeconds = 60; EEPROM.put(0, sleepSeconds); if (!EEPROM.commit()) { Serial.println("Unable to set default sleep."); } } // Initialize the LED_BUILTIN pin as an output, low active pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); } void loop() { uint8_t i, result; // Turn on LED digitalWrite(LED_BUILTIN, HIGH); // Clear old data memset(rtc.buf, 0, sizeof(rtc.buf)); memset(live.buf, 0, sizeof(live.buf)); memset(stats.buf, 0, sizeof(stats.buf)); // Read registers for clock node.clearResponseBuffer(); result = node.readHoldingRegisters(RTC_CLOCK, RTC_CLOCK_CNT); if (result == node.ku8MBSuccess) { rtc.buf[0] = node.getResponseBuffer(0); rtc.buf[1] = node.getResponseBuffer(1); rtc.buf[2] = node.getResponseBuffer(2); } else { Serial.print("Miss read rtc-data, ret val:"); Serial.println(result, HEX); } // Read LIVE-Data node.clearResponseBuffer(); result = node.readInputRegisters(LIVE_DATA, LIVE_DATA_CNT); if (result == node.ku8MBSuccess) { for (i = 0; i < LIVE_DATA_CNT; i++) live.buf[i] = node.getResponseBuffer(i); } else { Serial.print("Miss read liva-data, ret val:"); Serial.println(result, HEX); } // Statistical Data node.clearResponseBuffer(); result = node.readInputRegisters(STATISTICS, STATISTICS_CNT); if (result == node.ku8MBSuccess) { for (i = 0; i < STATISTICS_CNT; i++) { stats.buf[i] = node.getResponseBuffer(i); } } else { Serial.print("Miss read statistics, ret val:"); Serial.println(result, HEX); } // Battery SOC node.clearResponseBuffer(); result = node.readInputRegisters(BATTERY_SOC, 1); if (result == node.ku8MBSuccess) { batterySOC = node.getResponseBuffer(0); } else { Serial.print("Miss read batterySOC, ret val:"); Serial.println(result, HEX); } // Battery Net Current = Icharge - Iload node.clearResponseBuffer(); result = node.readInputRegisters(BATTERY_CURRENT_L, 2); if (result == node.ku8MBSuccess) { batteryCurrent = node.getResponseBuffer(0); batteryCurrent |= node.getResponseBuffer(1) << 16; } else { Serial.print("Miss read batteryCurrent, ret val:"); Serial.println(result, HEX); } // State of the Load Switch node.clearResponseBuffer(); result = node.readCoils(LOAD_STATE, 1); if (result == node.ku8MBSuccess) { loadState = node.getResponseBuffer(0); } else { Serial.print("Miss read loadState, ret val:"); Serial.println(result, HEX); } // Read Status Flags node.clearResponseBuffer(); result = node.readInputRegisters(0x3200, 2); if (result == node.ku8MBSuccess) { uint16_t temp = node.getResponseBuffer(0); Serial.print("Batt Flags : "); Serial.println(temp); status_batt.volt = temp & 0b1111; status_batt.temp = (temp >> 4) & 0b1111; status_batt.resistance = (temp >> 8) & 0b1; status_batt.rated_volt = (temp >> 15) & 0b1; temp = node.getResponseBuffer(1); Serial.print("Chrg Flags : "); Serial.println(temp, HEX); //for(i=0; i<16; i++) Serial.print( (temp >> (15-i) ) & 1 ); //Serial.println(); //charger_input = ( temp & 0b0000000000000000 ) >> 15 ; charger_mode = (temp & 0b0000000000001100) >> 2; //charger_input = ( temp & 0b0000000000000000 ) >> 12 ; //charger_operation = ( temp & 0b0000000000000000 ) >> 0 ; //Serial.print( "charger_input : "); Serial.println( charger_input ); Serial.print("charger_mode : "); Serial.println(charger_mode); //Serial.print( "charger_oper : "); Serial.println( charger_operation ); //Serial.print( "charger_state : "); Serial.println( charger_state ); } else { Serial.print("Miss read ChargeState, ret val:"); Serial.println(result, HEX); } // Print out to serial #ifdef SERIAL_DATA Serial.printf("\n\nTime: 20%02d-%02d-%02d %02d:%02d:%02d \n", rtc.r.y, rtc.r.M, rtc.r.d, rtc.r.h, rtc.r.m, rtc.r.s); Serial.print("\nLive-Data: Volt Amp Watt "); Serial.printf("\n Panel: %7.3f %7.3f %7.3f ", live.l.pV / 100.f, live.l.pI / 100.f, live.l.pP / 100.0f); Serial.printf("\n Batt: %7.3f %7.3f %7.3f ", live.l.bV / 100.f, live.l.bI / 100.f, live.l.bP / 100.0f); Serial.printf("\n Load: %7.3f %7.3f %7.3f ", live.l.lV / 100.f, live.l.lI / 100.f, live.l.lP / 100.0f); Serial.println(); Serial.printf("\n Battery Current: %7.3f A ", batteryCurrent / 100.f); Serial.printf("\n Battery SOC: %7.0f %% ", batterySOC / 1.0f); Serial.printf("\n Load Switch: %s ", (loadState == 1 ? " On" : "Off")); Serial.print("\n\nStatistics: "); Serial.printf("\n Panel: min: %7.3f max: %7.3f V", stats.s.pVmin / 100.f, stats.s.pVmax / 100.f); Serial.printf("\n Battery: min: %7.3f max: %7.3f V", stats.s.bVmin / 100.f, stats.s.bVmax / 100.f); Serial.println(); Serial.printf("\n Consumed: day: %7.3f mon: %7.3f year: %7.3f total: %7.3f kWh", stats.s.consEnerDay / 100.f, stats.s.consEnerMon / 100.f, stats.s.consEnerYear / 100.f, stats.s.consEnerTotal / 100.f); Serial.printf("\n Generated: day: %7.3f mon: %7.3f year: %7.3f total: %7.3f kWh", stats.s.genEnerDay / 100.f, stats.s.genEnerMon / 100.f, stats.s.genEnerYear / 100.f, stats.s.genEnerTotal / 100.f); Serial.printf("\n CO2-Reduction: %7.3f t ", stats.s.c02Reduction / 100.f); Serial.println(); Serial.print("\nStatus:"); Serial.printf("\n batt.volt: %s ", batt_volt_status[status_batt.volt]); Serial.printf("\n batt.temp: %s ", batt_temp_status[status_batt.temp]); Serial.printf("\n charger.charging: %s ", charger_charging_status[charger_mode]); Serial.println(); Serial.println(); #endif // Start WiFi connection. digitalWrite(LED_BUILTIN, LOW); WiFi.mode(WIFI_STA); WiFi.begin(STA_SSID, STA_PASSWORD); if (WiFi.waitForConnectResult() != WL_CONNECTED) { Serial.println("Connection Failed! Rebooting..."); delay(5000); ESP.restart(); } Serial.println(""); Serial.println("WiFi connected"); Serial.println("IP address: "); Serial.println(WiFi.localIP()); // Port defaults to 8266 #ifdef OTA_PORT ArduinoOTA.setPort(OTA_PORT); #endif // Hostname defaults to esp8266-[ChipID] #ifdef OTA_HOSTNAME if (strlen(OTA_HOSTNAME) != 0) { ArduinoOTA.setHostname(OTA_HOSTNAME); } #endif // No authentication by default #ifdef OTA_PASSWORD_HASH if (strlen(OTA_PASSWORD_HASH) != 0) { ArduinoOTA.setPasswordHash(OTA_PASSWORD_HASH); } #endif ArduinoOTA.onStart([]() { String type; if (ArduinoOTA.getCommand() == U_FLASH) { type = "sketch"; } else { // U_FS type = "filesystem"; } // NOTE: if updating FS this would be the place to unmount FS using FS.end() Serial.println("Start updating " + type); }); ArduinoOTA.onEnd([]() { Serial.println("\nEnd"); }); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { Serial.printf("Progress: %u%%\r", (progress / (total / 100))); }); ArduinoOTA.onError([](ota_error_t error) { Serial.printf("Error[%u]: ", error); if (error == OTA_AUTH_ERROR) { Serial.println("Auth Failed"); } else if (error == OTA_BEGIN_ERROR) { Serial.println("Begin Failed"); } else if (error == OTA_CONNECT_ERROR) { Serial.println("Connect Failed"); } else if (error == OTA_RECEIVE_ERROR) { Serial.println("Receive Failed"); } else if (error == OTA_END_ERROR) { Serial.println("End Failed"); } }); ArduinoOTA.begin(); // Establish/keep mqtt connection mqtt_client.setServer(MQTT_SERVER, MQTT_PORT); mqtt_client.setCallback(mqtt_callback); mqtt_reconnect(); digitalWrite(LED_BUILTIN, HIGH); // Once connected, publish an announcement. controllerStatusPayload["solar"]["monitor"]["status"] = "online"; serializeJson(controllerStatusPayload, tmpJsonPayloadBuffer); controllerStatusPayload.clear(); mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "status").c_str(), tmpJsonPayloadBuffer); controllerStatusPayload["solar"]["monitor"]["status"] = "waiting"; serializeJson(controllerStatusPayload, tmpJsonPayloadBuffer); controllerStatusPayload.clear(); mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "status").c_str(), tmpJsonPayloadBuffer); // Wait for MQTT subscription processing Serial.println("Waiting for MQTT and OTA events."); unsigned int now = millis(); while (millis() - now < MQTT_SUBSCRIBE_WAIT * 1000) { // Loop for MQTT. if (!mqtt_client.loop() || WiFi.status() != WL_CONNECTED) { break; } // Loop for OTA. ArduinoOTA.handle(); delay(100); } Serial.println("Done waiting for MQTT and OTA events."); // Publish to MQTT Serial.print("Publishing to MQTT: "); // Panel epeverMetricsPayload["solar"]["panel"]["V"] = String(live.l.pV / 100.f, 2); epeverMetricsPayload["solar"]["panel"]["I"] = String(live.l.pI / 100.f, 2); epeverMetricsPayload["solar"]["panel"]["P"] = String(live.l.pP / 100.f, 2); epeverMetricsPayload["solar"]["panel"]["minV"] = String(stats.s.pVmin / 100.f, 3); epeverMetricsPayload["solar"]["panel"]["maxV"] = String(stats.s.pVmax / 100.f, 3); serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); epeverMetricsPayload.clear(); mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "panel").c_str(), tmpJsonPayloadBuffer); // Battery epeverMetricsPayload["solar"]["battery"]["V"] = String(live.l.bV / 100.f, 2); epeverMetricsPayload["solar"]["battery"]["I"] = String(live.l.bI / 100.f, 2); epeverMetricsPayload["solar"]["battery"]["P"] = String(live.l.bP / 100.f, 2); epeverMetricsPayload["solar"]["battery"]["minV"] = String(stats.s.bVmin / 100.f, 2); epeverMetricsPayload["solar"]["battery"]["maxV"] = String(stats.s.bVmax / 100.f, 2); epeverMetricsPayload["solar"]["battery"]["SOC"] = String(batterySOC / 1.0f, 2); epeverMetricsPayload["solar"]["battery"]["netI"] = String(batteryCurrent / 100.0f, 2); epeverMetricsPayload["solar"]["battery"]["status"]["voltage"].set(batt_volt_status[status_batt.volt]); epeverMetricsPayload["solar"]["battery"]["status"]["temperature"].set(batt_temp_status[status_batt.temp]); serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); epeverMetricsPayload.clear(); mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "battery").c_str(), tmpJsonPayloadBuffer); // Load epeverMetricsPayload["solar"]["load"]["V"] = String(live.l.lV / 100.f, 2); epeverMetricsPayload["solar"]["load"]["I"] = String(live.l.lI / 100.f, 2); epeverMetricsPayload["solar"]["load"]["P"] = String(live.l.lP / 100.f, 2); // pimatic state topic does not work with integers or floats ?!? switch (loadState) { case 1: epeverMetricsPayload["solar"]["load"]["state"].set("on"); break; default: epeverMetricsPayload["solar"]["load"]["state"].set("off"); break; } serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); epeverMetricsPayload.clear(); mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "load").c_str(), tmpJsonPayloadBuffer); // Energy epeverMetricsPayload["solar"]["energy"]["consumed_day"] = String(stats.s.consEnerDay / 100.f, 3); epeverMetricsPayload["solar"]["energy"]["consumed_all"] = String(stats.s.consEnerTotal / 100.f, 3); epeverMetricsPayload["solar"]["energy"]["generated_day"] = String(stats.s.genEnerDay / 100.f, 3); epeverMetricsPayload["solar"]["energy"]["generated_all"] = String(stats.s.genEnerTotal / 100.f, 3); serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); epeverMetricsPayload.clear(); mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "energy").c_str(), tmpJsonPayloadBuffer); // Extra epeverMetricsPayload["solar"]["extra"]["CO2"]["t"] = String(stats.s.c02Reduction / 100.f, 2); //epever_serialize_s( "solar/status/charger_input", charger_input_status[ charger_input ] epeverMetricsPayload["solar"]["extra"]["charger_mode"] = charger_charging_status[charger_mode]; char buf[21]; sprintf(buf, "20%02d-%02d-%02d %02d:%02d:%02d", rtc.r.y, rtc.r.M, rtc.r.d, rtc.r.h, rtc.r.m, rtc.r.s); epeverMetricsPayload["solar"]["extra"]["time"] = buf; serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); epeverMetricsPayload.clear(); mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "extra").c_str(), tmpJsonPayloadBuffer); // Settings epeverMetricsPayload["solar"]["monitor"]["settings"]["sleep"].set(sleepSeconds); serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); epeverMetricsPayload.clear(); mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "settings").c_str(), tmpJsonPayloadBuffer); Serial.println("done"); controllerStatusPayload["solar"]["monitor"]["status"] = "offline"; serializeJson(controllerStatusPayload, tmpJsonPayloadBuffer); controllerStatusPayload.clear(); mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "status").c_str(), tmpJsonPayloadBuffer); // Ensure all messages are sent mqtt_client.unsubscribe(MQTT_TOPIC_SUB); mqtt_client.disconnect(); while (mqtt_client.state() != -1) { delay(100); } // disconnect wifi WiFi.disconnect(true); // power down MAX485_DE // low active digitalWrite(MAX485_RE, 0); digitalWrite(MAX485_DE, 0); // Sleep Serial.print("\nSleep for "); Serial.print(sleepSeconds); Serial.println(" Seconds"); digitalWrite(LED_BUILTIN, LOW); #ifdef USE_DEEP_SLEEP if (USE_DEEP_SLEEP) { ESP.deepSleep(sleepSeconds * 1000000); } else { delay(sleepSeconds * 1000); } #else delay(sleepSeconds * 1000); #endif }