/arduinoEpeverSolarController/arduinoEpeverSolarController.ino |
@@ -0,0 +1,879 @@ |
/*************************************************************************/ |
/* 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); |
} |
} |