/arduinoEpeverSolarController/arduinoEpeverSolarController.ino |
@@ -11,7 +11,7 @@ |
// 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, // |
// * 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, // |
@@ -52,9 +52,16 @@ |
// 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 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: // |
// { // |
@@ -96,7 +103,7 @@ |
|
const char* STA_SSID = ""; |
const char* STA_PASSWORD = ""; |
const char* MQTT_SERVER = "l"; |
const char* MQTT_SERVER = ""; |
const int MQTT_PORT = 1883; |
const char* MQTT_CLIENT_ID = "EpEver Solar Monitor"; |
const char* MQTT_TOPIC_PUB = "epever-40a/talk"; |
@@ -123,16 +130,18 @@ |
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 EPEVER_METRICS_PAYLOAD_SIZE = 512; |
const int CONTROLLER_STATUS_PAYLOAD_SIZE = 32; |
const int EPEVER_CONTROL_PAYLOAD_SIZE = 256; |
|
StaticJsonDocument<EPEVER_METRICS_PAYLOAD_SIZE> epeverMetricsPayload; |
char tmpJsonPayloadBuffer[EPEVER_METRICS_PAYLOAD_SIZE]; |
JsonObject jsonObject; |
|
/////////////////////////////////////////////////////////////////////////// |
// modbus data definitions // |
/////////////////////////////////////////////////////////////////////////// |
@@ -353,42 +362,6 @@ |
} |
|
/////////////////////////////////////////////////////////////////////////// |
// 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() { |
@@ -471,7 +444,7 @@ |
// input sanitization |
Serial.print("Set sleep seconds to: "); |
int seconds = (unsigned int)epeverControlPayload["sleep"]; |
if(seconds < MIN_SLEEP_SECONDS) { |
if (seconds < MIN_SLEEP_SECONDS) { |
sleepSeconds = MIN_SLEEP_SECONDS; |
Serial.println(MIN_SLEEP_SECONDS); |
} |
@@ -496,7 +469,9 @@ |
Serial.println("Hello World! I'm an EpEver Solar Monitor!"); |
|
// Connect D0 to RST to wake up |
pinMode(D0, WAKEUP_PULLUP); |
if (USE_DEEP_SLEEP) { |
pinMode(D0, WAKEUP_PULLUP); |
} |
// init modbus in receive mode |
pinMode(MAX485_RE, OUTPUT); |
pinMode(MAX485_DE, OUTPUT); |
@@ -758,61 +733,90 @@ |
|
// publish via mqtt |
// |
Serial.println("Publishing: "); |
Serial.print("Publishing to MQTT: "); |
|
// create a new JSON document to hold the metrics |
StaticJsonDocument<EPEVER_METRICS_PAYLOAD_SIZE> epeverMetricsPayload; |
|
// time |
// panel |
// |
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); |
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); |
|
// panel |
serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); |
epeverMetricsPayload.clear(); |
mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "panel").c_str(), tmpJsonPayloadBuffer); |
|
// battery |
// |
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"] = 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]); |
|
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); |
serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); |
epeverMetricsPayload.clear(); |
mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "battery").c_str(), tmpJsonPayloadBuffer); |
|
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); |
// 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 ?!? |
epeverMetricsPayload["solar"]["load"]["state"] = epever_serialize_s("solar/load/state", (char*)(loadState == 1 ? "on" : "off")); |
switch (loadState) { |
case 1: |
epeverMetricsPayload["solar"]["load"]["state"].set("on"); |
break; |
default: |
epeverMetricsPayload["solar"]["load"]["state"].set("off"); |
break; |
} |
|
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); |
serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); |
epeverMetricsPayload.clear(); |
mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "load").c_str(), tmpJsonPayloadBuffer); |
|
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); |
// 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); |
|
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); |
serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); |
epeverMetricsPayload.clear(); |
mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "energy").c_str(), tmpJsonPayloadBuffer); |
|
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); |
// 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; |
|
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]); |
serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); |
epeverMetricsPayload.clear(); |
mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "extra").c_str(), tmpJsonPayloadBuffer); |
|
//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]); |
// settings |
// |
epeverMetricsPayload["solar"]["monitor"]["settings"]["sleep"].set(sleepSeconds); |
|
// 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); |
serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); |
epeverMetricsPayload.clear(); |
mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "settings").c_str(), tmpJsonPayloadBuffer); |
|
Serial.println("done"); |
|
// Do the Switching of the Load here - doesn't work in callback ?!? |
// |
if (switch_load == 1) { |
@@ -828,7 +832,6 @@ |
} |
|
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); |
|
@@ -847,7 +850,6 @@ |
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); |
|