arduino-sketches – Rev 21

Subversion Repositories:
Rev:
/*************************************************************************/
/*      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 <ModbusMaster.h>
#if defined(ARDUINO_ARCH_ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#elif defined(ARDUINO_ARCH_ESP32)
#include <WiFi.h>
#include <ESPmDNS.h>
#endif
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <ArduinoOTA.h>
#if defined(ARDUINO_ARCH_ESP8266)
#include <ESP_EEPROM.h>
#elif defined(ARDUINO_ARCH_ESP32)
#include <EEPROM.h>
#endif

ModbusMaster node;
WiFiClient wifi_client;
PubSubClient mqtt_client(wifi_client);
bool loadState = true;
int sleepSeconds;
const int JSON_DOCUMENT_SIZE = 512;

StaticJsonDocument<JSON_DOCUMENT_SIZE> controllerStatusPayload;
StaticJsonDocument<JSON_DOCUMENT_SIZE> epeverMetricsPayload;
StaticJsonDocument<JSON_DOCUMENT_SIZE> 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
}

Generated by GNU Enscript 1.6.5.90.