arduino-sketches – Rev 10

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 object instead of publishing to multiple 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 a JSON payload and send it to the configured //
// MQTT server on a selected topic definied by MQTT_TOPIC_PUB.           //
//                                                                       //
// 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                                                       //
//   "value" :                                                           //
// }                                                                     //
//                                                                       //
///////////////////////////////////////////////////////////////////////////

#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>

///////////////////////////////////////////////////////////////////////////
//                           configuration                               //
///////////////////////////////////////////////////////////////////////////
// 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

const char* STA_SSID = "";
const char* STA_PASSWORD = "";
const char* MQTT_SERVER = "l";
const int MQTT_PORT = 1883;
const char* MQTT_CLIENT_ID = "EpEver Solar Monitor";
const char* MQTT_TOPIC_PUB = "epever-40a/talk";
const char* MQTT_TOPIC_SUB = "epever-40a/hear";
// seconds to wait for MQTT message to be delivered and processed
const int MQTT_SUBSCRIBE_WAIT = 10;

// The OTA hostname.
const char* OTA_HOSTNAME = "";
// The OTA port on which updates take place.
const unsigned int OTA_PORT = 8266;
// The authentication password to use for OTA updates.
// This should be set to the unsalted MD5 hash of the plaintext password.
const char* OTA_PASSWORD_HASH = "";

// Whether to use deep sleep or not (requires hardware modifications).
const bool USE_DEEP_SLEEP = true;
// the minimal amount that the ESP should sleep for.
const int MIN_SLEEP_SECONDS = 60;

///////////////////////////////////////////////////////////////////////////
//                   general variable declarations                       //
///////////////////////////////////////////////////////////////////////////
ModbusMaster node;
WiFiClient wifi_client;
PubSubClient mqtt_client(wifi_client);
char buf[256];
int switch_load = 0;
bool loadState = true;
// 1 minutes default
unsigned int sleepSeconds = 60;
char mqttMessageBuffer[64];
const int EPEVER_METRICS_PAYLOAD_SIZE = 256;
const int CONTROLLER_STATUS_PAYLOAD_SIZE = 32;
const int EPEVER_CONTROL_PAYLOAD_SIZE = 256;

///////////////////////////////////////////////////////////////////////////
//                        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 helper functions                       //
///////////////////////////////////////////////////////////////////////////
char* epever_serialize_s(const char* topic, char* msg) {
  Serial.print(topic);
  Serial.print(": ");

  memset(mqttMessageBuffer, 0, 64 * sizeof(char));
  snprintf(mqttMessageBuffer, 64, "%s", msg);
  Serial.println(msg);

  return msg;
}

char* epever_serialize_f(const char* topic, float value) {
  Serial.print(topic);
  Serial.print(": ");

  memset(mqttMessageBuffer, 0, 64 * sizeof(char));
  snprintf(mqttMessageBuffer, 64, "%7.3f", value);
  Serial.println(mqttMessageBuffer);

  return mqttMessageBuffer;
}

char* epever_serialize_i(const char* topic, int value) {
  Serial.print(topic);
  Serial.print(": ");

  memset(mqttMessageBuffer, 0, 64 * sizeof(char));
  snprintf(mqttMessageBuffer, 64, "%d", value);
  Serial.println(mqttMessageBuffer);

  return mqttMessageBuffer;
}

///////////////////////////////////////////////////////////////////////////
//                            MQTT event handling                        //
///////////////////////////////////////////////////////////////////////////
void mqtt_reconnect() {
  // Loop until we're reconnected
  Serial.println("Checking MQT connection...");
  while (!mqtt_client.connected()) {
    Serial.print("MQTT Reconnecting: ");

    // Create a client ID
    String clientId = MQTT_CLIENT_ID;

    // Attempt to connect
    if (mqtt_client.connect(clientId.c_str())) {
      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) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int 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: ");
  StaticJsonDocument<EPEVER_CONTROL_PAYLOAD_SIZE> epeverControlPayload;
  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")) {
    return;
  }

  if (epeverControlPayload["action"] == "switch") {
    // Switch - but i can't seem to switch a coil directly here ?!?
    if (epeverControlPayload["status"] == "on") {
      loadState = true;
      switch_load = 1;
    }

    if (epeverControlPayload["status"] == "off") {
      loadState = false;
      switch_load = 1;
    }

    return;
  }

  if (epeverControlPayload["action"] == "settings") {
    if (epeverControlPayload.containsKey("sleep")) {
      // input sanitization
      Serial.print("Set sleep seconds to: ");
      int seconds = (unsigned int)epeverControlPayload["sleep"];
      if(seconds < MIN_SLEEP_SECONDS) {
        sleepSeconds = MIN_SLEEP_SECONDS;
        Serial.println(MIN_SLEEP_SECONDS);
      }
      sleepSeconds = seconds;
      Serial.println(sleepSeconds);
    }

    return;
  }
}

///////////////////////////////////////////////////////////////////////////
//                           Arduino functions                           //
///////////////////////////////////////////////////////////////////////////
void setup() {
  // say hello
  Serial.begin(115200);
  while (!Serial) {
    ;
  }
  Serial.println();
  Serial.println("Hello World! I'm an EpEver Solar Monitor!");

  // Connect D0 to RST to wake up
  pinMode(D0, WAKEUP_PULLUP);
  // init modbus in receive mode
  pinMode(MAX485_RE, OUTPUT);
  pinMode(MAX485_DE, OUTPUT);
  digitalWrite(MAX485_RE, 0);
  digitalWrite(MAX485_DE, 0);
  // EPEver Device ID 1
  node.begin(1, Serial);
  // modbus callbacks
  node.preTransmission(preTransmission);
  node.postTransmission(postTransmission);

  // Initialize the LED_BUILTIN pin as an output, low active
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);
}

void loop() {
  uint8_t i, result;

  // flash the led
  for (i = 0; i < 3; i++) {
    digitalWrite(LED_BUILTIN, LOW);
    delay(200);
    digitalWrite(LED_BUILTIN, HIGH);
    delay(200);
  }

  // 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
  //
  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();

  // Go Online to publish via mqtt
  //
  // get wifi going
  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
  ArduinoOTA.setPort(OTA_PORT);

  // Hostname defaults to esp8266-[ChipID]
  if (strlen(OTA_HOSTNAME) != 0) {
    ArduinoOTA.setHostname(OTA_HOSTNAME);
  }

  // No authentication by default
  if (strlen(OTA_PASSWORD_HASH) != 0) {
    ArduinoOTA.setPasswordHash(OTA_PASSWORD_HASH);
  }

  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.
  StaticJsonDocument<CONTROLLER_STATUS_PAYLOAD_SIZE> controllerStatusPayload;
  controllerStatusPayload["status"] = "online";
  char tmpJsonStatusBuffer[CONTROLLER_STATUS_PAYLOAD_SIZE];
  serializeJson(controllerStatusPayload, tmpJsonStatusBuffer, CONTROLLER_STATUS_PAYLOAD_SIZE);
  mqtt_client.publish(MQTT_TOPIC_PUB, tmpJsonStatusBuffer, CONTROLLER_STATUS_PAYLOAD_SIZE);

  // publish via mqtt
  //
  Serial.println("Publishing: ");

  // create a new JSON document to hold the metrics
  StaticJsonDocument<EPEVER_METRICS_PAYLOAD_SIZE> epeverMetricsPayload;

  // time
  //
  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"]["status"]["time"] = epever_serialize_s("solar/status/time", buf);
  epeverMetricsPayload["solar"]["status"]["updates"] = epever_serialize_i("solar/status/updates", sleepSeconds);

  // panel
  //
  epeverMetricsPayload["solar"]["panel"]["V"] = epever_serialize_f("solar/panel/V", live.l.pV / 100.f);
  epeverMetricsPayload["solar"]["panel"]["I"] = epever_serialize_f("solar/panel/I", live.l.pI / 100.f);
  epeverMetricsPayload["solar"]["panel"]["P"] = epever_serialize_f("solar/panel/P", live.l.pP / 100.f);

  epeverMetricsPayload["solar"]["battery"]["V"] = epever_serialize_f("solar/battery/V", live.l.bV / 100.f);
  epeverMetricsPayload["solar"]["battery"]["I"] = epever_serialize_f("solar/battery/I", live.l.bI / 100.f);
  epeverMetricsPayload["solar"]["battery"]["P"] = epever_serialize_f("solar/battery/P", live.l.bP / 100.f);

  epeverMetricsPayload["solar"]["load"]["V"] = epever_serialize_f("solar/load/V", live.l.lV / 100.f);
  epeverMetricsPayload["solar"]["load"]["I"] = epever_serialize_f("solar/load/I", live.l.lI / 100.f);
  epeverMetricsPayload["solar"]["load"]["P"] = epever_serialize_f("solar/load/P", live.l.lP / 100.f);

  epeverMetricsPayload["solar"]["co2reduction"]["t"] = epever_serialize_f("solar/co2reduction/t", stats.s.c02Reduction / 100.f);
  epeverMetricsPayload["solar"]["battery"]["SOC"] = epever_serialize_f("solar/battery/SOC", batterySOC / 1.0f);
  epeverMetricsPayload["solar"]["battery"]["netI"] = epever_serialize_f("solar/battery/netI", batteryCurrent / 100.0f);
  // pimatic state topic does not work with integers or floats ?!?
  epeverMetricsPayload["solar"]["load"]["state"] = epever_serialize_s("solar/load/state", (char*)(loadState == 1 ? "on" : "off"));

  epeverMetricsPayload["solar"]["battery"]["minV"] = epever_serialize_f("solar/battery/minV", stats.s.bVmin / 100.f);
  epeverMetricsPayload["solar"]["battery"]["maxV"] = epever_serialize_f("solar/battery/maxV", stats.s.bVmax / 100.f);

  epeverMetricsPayload["solar"]["panel"]["minV"] = epever_serialize_f("solar/panel/minV", stats.s.pVmin / 100.f);
  epeverMetricsPayload["solar"]["panel"]["maxV"] = epever_serialize_f("solar/panel/maxV", stats.s.pVmax / 100.f);

  epeverMetricsPayload["solar"]["energy"]["consumed_day"] = epever_serialize_f("solar/energy/consumed_day", stats.s.consEnerDay / 100.f);
  epeverMetricsPayload["solar"]["energy"]["consumed_all"] = epever_serialize_f("solar/energy/consumed_all", stats.s.consEnerTotal / 100.f);

  epeverMetricsPayload["solar"]["energy"]["generated_day"] = epever_serialize_f("solar/energy/generated_day", stats.s.genEnerDay / 100.f);
  epeverMetricsPayload["solar"]["energy"]["generated_all"] = epever_serialize_f("solar/energy/generated_all", stats.s.genEnerTotal / 100.f);

  epeverMetricsPayload["solar"]["status"]["batt_volt"] = epever_serialize_s("solar/status/batt_volt", batt_volt_status[status_batt.volt]);
  epeverMetricsPayload["solar"]["status"]["batt_temp"] = epever_serialize_s("solar/status/batt_temp", batt_temp_status[status_batt.temp]);

  //epever_serialize_s( "solar/status/charger_input", charger_input_status[ charger_input ]  );
  epeverMetricsPayload["solar"]["status"]["charger_mode"] = epever_serialize_s("solar/status/charger_mode", charger_charging_status[charger_mode]);

  // publish the object
  char tmpJsonPayloadBuffer[EPEVER_METRICS_PAYLOAD_SIZE];
  serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer, EPEVER_METRICS_PAYLOAD_SIZE);
  mqtt_client.publish(MQTT_TOPIC_PUB, tmpJsonPayloadBuffer, EPEVER_METRICS_PAYLOAD_SIZE);

  // Do the Switching of the Load here - doesn't work in callback ?!?
  //
  if (switch_load == 1) {
    switch_load = 0;
    Serial.print("Switching Load ");
    Serial.println((loadState ? "On" : "Off"));

    result = node.writeSingleCoil(0x0002, loadState);
    if (result != node.ku8MBSuccess) {
      Serial.print("Miss write loadState, ret val:");
      Serial.println(result, HEX);
    }
  }

  controllerStatusPayload["status"] = "waiting";
  memset(tmpJsonStatusBuffer, 0, CONTROLLER_STATUS_PAYLOAD_SIZE);
  serializeJson(controllerStatusPayload, tmpJsonStatusBuffer, CONTROLLER_STATUS_PAYLOAD_SIZE);
  mqtt_client.publish(MQTT_TOPIC_PUB, tmpJsonStatusBuffer, CONTROLLER_STATUS_PAYLOAD_SIZE);

  // wait for MQTT subscription processing
  Serial.print("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.");

  controllerStatusPayload["status"] = "offline";
  memset(tmpJsonStatusBuffer, 0, CONTROLLER_STATUS_PAYLOAD_SIZE);
  serializeJson(controllerStatusPayload, tmpJsonStatusBuffer, CONTROLLER_STATUS_PAYLOAD_SIZE);
  mqtt_client.publish(MQTT_TOPIC_PUB, tmpJsonStatusBuffer, CONTROLLER_STATUS_PAYLOAD_SIZE);

  // 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);

  // DeepSleep n microseconds
  //
  Serial.print("\nDeepSleep for ");
  Serial.print(sleepSeconds);
  Serial.println(" Seconds");
  if (USE_DEEP_SLEEP) {
    ESP.deepSleep(sleepSeconds * 1000000);
  } else {
    delay(sleepSeconds * 1000);
  }
}

Generated by GNU Enscript 1.6.5.90.