arduino-sketches – Blame information for rev 21
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
10 | office | 1 | /*************************************************************************/ |
2 | /* Copyright (C) 2019 Don M/glitterkitty - License: GNU GPLv3 */ |
||
3 | /* Copyright (C) 2023 Wizardry and Steamworks - License: GNU GPLv3 */ |
||
4 | /*************************************************************************/ |
||
5 | // v1.0 // |
||
6 | // Reworked from: https://github.com/glitterkitty/EpEverSolarMonitor // |
||
7 | // // |
||
8 | // Current documentation @ // |
||
9 | // https://grimore.org/hardware/epever/generic_controller_monitor // |
||
10 | // // |
||
11 | // Differences & Highlights: // |
||
12 | // * added OTA update capabilities, // |
||
13 | // * settings are made via MQTT instead of hardware (debug), // |
||
11 | office | 14 | // * used a JSON objects intead of MQTT topics, // |
10 | office | 15 | // * made deep sleep optional, // |
16 | // * ensured re-entrancy when resuming out of suspend, // |
||
17 | // * made time to process MQTT messages configurable, // |
||
18 | // * added some compatiblity with ESP32, // |
||
19 | // * made multiple parameters configurable, // |
||
20 | // * small retouches and aesthetics // |
||
21 | // // |
||
22 | // About: // |
||
23 | // This is an Arduino sketch for retrieving metrics off a solar panel // |
||
24 | // controller made by EpEver and should work for most MPPT models. // |
||
25 | // // |
||
26 | // The original documentation by glitterkitty mentioned that the EPEVER // |
||
27 | // outputs +7.5V on the orange-white RJ45 connector but at least for the // |
||
28 | // EPEVER 40A the voltage between the orange and the brown wire of the // |
||
29 | // RJ45 connector has been measured to be +5.11V which is great because // |
||
30 | // no stepdown is needed and the EPEVER can power both the MAX485 TTL // |
||
31 | // as well as the ESP. In any case, the voltage should be measured // |
||
32 | // again before implementing the circuit diagram just to make sure. // |
||
33 | // // |
||
34 | // Essential wiring diagram: // |
||
35 | // // |
||
36 | // +---------+ // |
||
37 | // | | // |
||
38 | // orange +<--------+--- 5V --+------------------------> + VCC 5V // |
||
39 | // | + DI <-------------------> + TX // |
||
40 | // green +<--------+ A + DE <-------------------> + D2 // |
||
41 | // EPEVER | | ESP // |
||
42 | // RJ45 | MAX485 | 8266 / 32 // |
||
43 | // blue +<--------+ B + RE <-------------------> + D1 // |
||
44 | // | + RO <-------------------> + RX // |
||
45 | // brown +<--------+-- GND --+------------------------> + GND // |
||
46 | // | | // |
||
47 | // +---------+ // |
||
48 | // // |
||
49 | // Optional wiring: // |
||
50 | // * connect the ESP D0 GPIO to RST via <= 1kOhm resistor and then set // |
||
51 | // the variable USE_DEEP_SLEEP in this sketch to true in order to // |
||
52 | // allow the ESP to go into deep sleep mode and consume less power. // |
||
53 | // // |
||
54 | // Software usage: // |
||
11 | office | 55 | // The sketch will generate JSON payloads and send them to the following // |
56 | // MQTT topics: // |
||
10 | office | 57 | // // |
11 | office | 58 | // MQTT_TOPIC_PUB / panel | battery | load | energy | extra | monitor // |
59 | // // |
||
60 | // where panel, battery, load, energy, extra and monitor are various // |
||
61 | // subtopics of the configurable MQTT_TOPIC_PUB topic. For example to // |
||
62 | // just listen for battery notifications, subscribe to the following // |
||
63 | // MQTT topic: MQTT_TOPIC_PUB/battery // |
||
64 | // // |
||
10 | office | 65 | // The the sketch will subscribe to the topic defined by MQTT_TOPIC_SUB // |
66 | // and listen to the following JSON grammar: // |
||
67 | // { // |
||
68 | // "action" : "settings" | "switch" // |
||
69 | // "sleep" (when "action" is "settings") : time in seconds // |
||
70 | // "switch" (when "action" is "switch") : "on" | "off" // |
||
71 | // } // |
||
72 | // // |
||
73 | // For example, to set the sleep time between metric updates to about // |
||
74 | // 5 minutes, send the following JSON payload on the MQTT_TOPIC_SUB // |
||
75 | // topic: // |
||
76 | // { // |
||
77 | // "action" : "settings" // |
||
78 | // "sleep" : 120 // |
||
79 | // } // |
||
80 | // // |
||
12 | office | 81 | // Note that for the switch to work, the Epever device has to be set to // |
82 | // "manual mode". From the manual, that seems to be setting the load // |
||
83 | // setting for the first timer to 117 and the second should display 2, n // |
||
84 | // and then the load can be toggled using this sketch. // |
||
10 | office | 85 | /////////////////////////////////////////////////////////////////////////// |
86 | |||
87 | /////////////////////////////////////////////////////////////////////////// |
||
88 | // configuration // |
||
89 | /////////////////////////////////////////////////////////////////////////// |
||
21 | office | 90 | // Whether to send data over the serial port. |
91 | #define SERIAL_DATA 1 |
||
10 | office | 92 | // RS-485 module pin DE maps to D2 GPIO on ESP. |
93 | #define MAX485_DE D2 |
||
94 | // RS-485 module pin RE maps to D1 GPIO on ESP. |
||
95 | #define MAX485_RE D1 |
||
96 | |||
21 | office | 97 | #define STA_SSID "" |
98 | #define STA_PASSWORD "" |
||
99 | #define MQTT_SERVER "" |
||
100 | #define MQTT_PORT 1883 |
||
101 | #define MQTT_CLIENT_ID "EpEver Solar Monitor" |
||
102 | #define MQTT_TOPIC_PUB "epever-40a/talk" |
||
103 | #define MQTT_TOPIC_SUB "epever-40a/hear" |
||
104 | // Seconds to wait for MQTT message to be delivered and processed. |
||
105 | #define MQTT_SUBSCRIBE_WAIT 10 |
||
10 | office | 106 | |
107 | // The OTA hostname. |
||
21 | office | 108 | //#define OTA_HOSTNAME |
10 | office | 109 | // The OTA port on which updates take place. |
21 | office | 110 | //#define OTA_PORT 8266 |
10 | office | 111 | // The authentication password to use for OTA updates. |
112 | // This should be set to the unsalted MD5 hash of the plaintext password. |
||
21 | office | 113 | //#define OTA_PASSWORD_HASH |
10 | office | 114 | |
115 | // Whether to use deep sleep or not (requires hardware modifications). |
||
21 | office | 116 | //#define USE_DEEP_SLEEP 1 |
10 | office | 117 | // the minimal amount that the ESP should sleep for. |
21 | office | 118 | #define MIN_SLEEP_SECONDS 60 |
10 | office | 119 | |
120 | /////////////////////////////////////////////////////////////////////////// |
||
21 | office | 121 | // general variable declarations and libraries // |
10 | office | 122 | /////////////////////////////////////////////////////////////////////////// |
21 | office | 123 | #include <ModbusMaster.h> |
124 | #if defined(ARDUINO_ARCH_ESP8266) |
||
125 | #include <ESP8266WiFi.h> |
||
126 | #include <ESP8266mDNS.h> |
||
127 | #elif defined(ARDUINO_ARCH_ESP32) |
||
128 | #include <WiFi.h> |
||
129 | #include <ESPmDNS.h> |
||
130 | #endif |
||
131 | #include <PubSubClient.h> |
||
132 | #include <ArduinoJson.h> |
||
133 | #include <ArduinoOTA.h> |
||
134 | #if defined(ARDUINO_ARCH_ESP8266) |
||
135 | #include <ESP_EEPROM.h> |
||
136 | #elif defined(ARDUINO_ARCH_ESP32) |
||
137 | #include <EEPROM.h> |
||
138 | #endif |
||
139 | |||
10 | office | 140 | ModbusMaster node; |
141 | WiFiClient wifi_client; |
||
142 | PubSubClient mqtt_client(wifi_client); |
||
143 | bool loadState = true; |
||
13 | office | 144 | int sleepSeconds; |
12 | office | 145 | const int JSON_DOCUMENT_SIZE = 512; |
10 | office | 146 | |
12 | office | 147 | StaticJsonDocument<JSON_DOCUMENT_SIZE> controllerStatusPayload; |
148 | StaticJsonDocument<JSON_DOCUMENT_SIZE> epeverMetricsPayload; |
||
149 | StaticJsonDocument<JSON_DOCUMENT_SIZE> epeverControlPayload; |
||
150 | char tmpJsonPayloadBuffer[JSON_DOCUMENT_SIZE]; |
||
11 | office | 151 | |
10 | office | 152 | /////////////////////////////////////////////////////////////////////////// |
153 | // modbus data definitions // |
||
154 | /////////////////////////////////////////////////////////////////////////// |
||
155 | // ModBus Register Locations |
||
156 | // Start of live-data |
||
157 | #define LIVE_DATA 0x3100 |
||
158 | // 16 registers |
||
159 | #define LIVE_DATA_CNT 16 |
||
160 | // just for reference, not used in code |
||
161 | #define PANEL_VOLTS 0x00 |
||
162 | #define PANEL_AMPS 0x01 |
||
163 | #define PANEL_POWER_L 0x02 |
||
164 | #define PANEL_POWER_H 0x03 |
||
165 | #define BATT_VOLTS 0x04 |
||
166 | #define BATT_AMPS 0x05 |
||
167 | #define BATT_POWER_L 0x06 |
||
168 | #define BATT_POWER_H 0x07 |
||
169 | // dummy * 4 |
||
170 | #define LOAD_VOLTS 0x0C |
||
171 | #define LOAD_AMPS 0x0D |
||
172 | #define LOAD_POWER_L 0x0E |
||
173 | #define LOAD_POWER_H 0x0F |
||
174 | // D7-0 Sec, D15-8 Min : D7-0 Hour, D15-8 Day : D7-0 Month, D15-8 Year |
||
175 | #define RTC_CLOCK 0x9013 |
||
176 | // 3 regs |
||
177 | #define RTC_CLOCK_CNT 3 |
||
178 | // State of Charge in percent, 1 reg |
||
179 | #define BATTERY_SOC 0x311A |
||
180 | // Battery current L |
||
181 | #define BATTERY_CURRENT_L 0x331B |
||
182 | // Battery current H |
||
183 | #define BATTERY_CURRENT_H 0x331C |
||
184 | // start of statistical data |
||
185 | #define STATISTICS 0x3300 |
||
186 | // 22 registers |
||
187 | #define STATISTICS_CNT 22 |
||
188 | // just for reference, not used in code |
||
189 | // Maximum input volt (PV) today |
||
190 | #define PV_MAX 0x00 |
||
191 | // Minimum input volt (PV) today |
||
192 | #define PV_MIN 0x01 |
||
193 | // Maximum battery volt today |
||
194 | #define BATT_MAX 0x02 |
||
195 | // Minimum battery volt today |
||
196 | #define BATT_MIN 0x03 |
||
197 | // Consumed energy today L |
||
198 | #define CONS_ENERGY_DAY_L 0x04 |
||
199 | // Consumed energy today H |
||
200 | #define CONS_ENGERY_DAY_H 0x05 |
||
201 | // Consumed energy this month L |
||
202 | #define CONS_ENGERY_MON_L 0x06 |
||
203 | // Consumed energy this month H |
||
204 | #define CONS_ENGERY_MON_H 0x07 |
||
205 | // Consumed energy this year L |
||
206 | #define CONS_ENGERY_YEAR_L 0x08 |
||
207 | // Consumed energy this year H |
||
208 | #define CONS_ENGERY_YEAR_H 0x09 |
||
209 | // Total consumed energy L |
||
210 | #define CONS_ENGERY_TOT_L 0x0A |
||
211 | // Total consumed energy H |
||
212 | #define CONS_ENGERY_TOT_H 0x0B |
||
213 | // Generated energy today L |
||
214 | #define GEN_ENERGY_DAY_L 0x0C |
||
215 | // Generated energy today H |
||
216 | #define GEN_ENERGY_DAY_H 0x0D |
||
217 | // Generated energy this month L |
||
218 | #define GEN_ENERGY_MON_L 0x0E |
||
219 | // Generated energy this month H |
||
220 | #define GEN_ENERGY_MON_H 0x0F |
||
221 | // Generated energy this year L |
||
222 | #define GEN_ENERGY_YEAR_L 0x10 |
||
223 | // Generated energy this year H |
||
224 | #define GEN_ENERGY_YEAR_H 0x11 |
||
225 | // Total generated energy L |
||
226 | #define GEN_ENERGY_TOT_L 0x12 |
||
227 | // Total Generated energy H |
||
228 | #define GEN_ENERGY_TOT_H 0x13 |
||
229 | // Carbon dioxide reduction L |
||
230 | #define CO2_REDUCTION_L 0x14 |
||
231 | // Carbon dioxide reduction H |
||
232 | #define CO2_REDUCTION_H 0x15 |
||
233 | // r/w load switch state |
||
234 | #define LOAD_STATE 0x02 |
||
235 | #define STATUS_FLAGS 0x3200 |
||
236 | // Battery status register |
||
237 | #define STATUS_BATTERY 0x00 |
||
238 | // Charging equipment status register |
||
239 | #define STATUS_CHARGER 0x01 |
||
240 | |||
241 | /////////////////////////////////////////////////////////////////////////// |
||
242 | // datastructures, also for buffer to values conversion // |
||
243 | /////////////////////////////////////////////////////////////////////////// |
||
244 | // clock |
||
245 | union { |
||
246 | struct { |
||
247 | uint8_t s; |
||
248 | uint8_t m; |
||
249 | uint8_t h; |
||
250 | uint8_t d; |
||
251 | uint8_t M; |
||
252 | uint8_t y; |
||
253 | } r; |
||
254 | uint16_t buf[3]; |
||
255 | } rtc; |
||
256 | |||
257 | // live data |
||
258 | union { |
||
259 | struct { |
||
260 | |||
261 | int16_t pV; |
||
262 | int16_t pI; |
||
263 | int32_t pP; |
||
264 | |||
265 | int16_t bV; |
||
266 | int16_t bI; |
||
267 | int32_t bP; |
||
268 | |||
269 | |||
270 | uint16_t dummy[4]; |
||
271 | |||
272 | int16_t lV; |
||
273 | int16_t lI; |
||
274 | int32_t lP; |
||
275 | |||
276 | } l; |
||
277 | uint16_t buf[16]; |
||
278 | } live; |
||
279 | |||
280 | // statistics |
||
281 | union { |
||
282 | struct { |
||
283 | |||
284 | // 4*1 = 4 |
||
285 | uint16_t pVmax; |
||
286 | uint16_t pVmin; |
||
287 | uint16_t bVmax; |
||
288 | uint16_t bVmin; |
||
289 | |||
290 | // 4*2 = 8 |
||
291 | uint32_t consEnerDay; |
||
292 | uint32_t consEnerMon; |
||
293 | uint32_t consEnerYear; |
||
294 | uint32_t consEnerTotal; |
||
295 | |||
296 | // 4*2 = 8 |
||
297 | uint32_t genEnerDay; |
||
298 | uint32_t genEnerMon; |
||
299 | uint32_t genEnerYear; |
||
300 | uint32_t genEnerTotal; |
||
301 | |||
302 | // 1*2 = 2 |
||
303 | uint32_t c02Reduction; |
||
304 | |||
305 | } s; |
||
306 | uint16_t buf[22]; |
||
307 | } stats; |
||
308 | |||
309 | // these are too far away for the union conversion trick |
||
310 | uint16_t batterySOC = 0; |
||
311 | int32_t batteryCurrent = 0; |
||
312 | |||
313 | // battery status |
||
314 | struct { |
||
315 | uint8_t volt; // d3-d0 Voltage: 00H Normal, 01H Overvolt, 02H UnderVolt, 03H Low Volt Disconnect, 04H Fault |
||
316 | uint8_t temp; // d7-d4 Temperature: 00H Normal, 01H Over warning settings, 02H Lower than the warning settings |
||
317 | uint8_t resistance; // d8 abnormal 1, normal 0 |
||
318 | uint8_t rated_volt; // d15 1-Wrong identification for rated voltage |
||
319 | } status_batt; |
||
320 | |||
321 | char batt_volt_status[][20] = { |
||
322 | "Normal", |
||
323 | "Overvolt", |
||
324 | "Low Volt Disconnect", |
||
325 | "Fault" |
||
326 | }; |
||
327 | |||
328 | char batt_temp_status[][16] = { |
||
329 | "Normal", |
||
330 | "Over WarnTemp", |
||
331 | "Below WarnTemp" |
||
332 | }; |
||
333 | |||
334 | // charging equipment status (not fully impl. yet) |
||
335 | //uint8_t charger_operation = 0; |
||
336 | //uint8_t charger_state = 0; |
||
337 | //uint8_t charger_input = 0; |
||
338 | uint8_t charger_mode = 0; |
||
339 | |||
340 | //char charger_input_status[][20] = { |
||
341 | // "Normal", |
||
342 | // "No power connected", |
||
343 | // "Higher volt input", |
||
344 | // "Input volt error" |
||
345 | //}; |
||
346 | |||
347 | char charger_charging_status[][12] = { |
||
348 | "Off", |
||
349 | "Float", |
||
350 | "Boost", |
||
351 | "Equlization" |
||
352 | }; |
||
353 | |||
354 | /////////////////////////////////////////////////////////////////////////// |
||
355 | // ModBus helper functions // |
||
356 | /////////////////////////////////////////////////////////////////////////// |
||
357 | void preTransmission() { |
||
358 | digitalWrite(MAX485_RE, 1); |
||
359 | digitalWrite(MAX485_DE, 1); |
||
360 | |||
361 | digitalWrite(LED_BUILTIN, LOW); |
||
362 | } |
||
363 | |||
364 | void postTransmission() { |
||
365 | digitalWrite(MAX485_RE, 0); |
||
366 | digitalWrite(MAX485_DE, 0); |
||
367 | |||
368 | digitalWrite(LED_BUILTIN, HIGH); |
||
369 | } |
||
370 | |||
371 | /////////////////////////////////////////////////////////////////////////// |
||
372 | // MQTT event handling // |
||
373 | /////////////////////////////////////////////////////////////////////////// |
||
374 | void mqtt_reconnect() { |
||
375 | // Loop until we're reconnected |
||
376 | Serial.println("Checking MQT connection..."); |
||
377 | while (!mqtt_client.connected()) { |
||
378 | Serial.print("MQTT Reconnecting: "); |
||
379 | |||
380 | // Attempt to connect |
||
21 | office | 381 | if (mqtt_client.connect(MQTT_CLIENT_ID)) { |
10 | office | 382 | Serial.println("success"); |
383 | |||
384 | Serial.print("Subscribing MQTT: "); |
||
385 | mqtt_client.subscribe(MQTT_TOPIC_SUB); |
||
386 | Serial.println(MQTT_TOPIC_SUB); |
||
387 | } else { |
||
388 | Serial.print("failure, rc="); |
||
389 | Serial.print(mqtt_client.state()); |
||
390 | Serial.println(" try again in 1 second"); |
||
391 | |||
392 | delay(1000); |
||
393 | } |
||
394 | } |
||
395 | } |
||
396 | |||
397 | // control load on / off here, setting sleep duration |
||
398 | // |
||
399 | void mqtt_callback(char* topic, byte* payload, unsigned int length) { |
||
12 | office | 400 | uint8_t i, result; |
401 | |||
10 | office | 402 | Serial.print("Message arrived ["); |
403 | Serial.print(topic); |
||
404 | Serial.print("] "); |
||
12 | office | 405 | for (i = 0; i < length; i++) { |
10 | office | 406 | Serial.print((char)payload[i]); |
407 | } |
||
408 | Serial.println(); |
||
409 | |||
410 | payload[length] = '\0'; |
||
411 | |||
412 | // ignore messages not sent on the subscribed topic |
||
413 | if (strncmp(topic, MQTT_TOPIC_SUB, strlen(MQTT_TOPIC_SUB)) != 0) { |
||
414 | return; |
||
415 | } |
||
416 | |||
417 | // Parse the payload sent to the MQTT topic as a JSON document. |
||
418 | Serial.print("Deserializing message: "); |
||
419 | DeserializationError error = deserializeJson(epeverControlPayload, payload); |
||
420 | if (error) { |
||
421 | Serial.print("failed, error="); |
||
422 | Serial.println(error.c_str()); |
||
423 | return; |
||
424 | } else { |
||
425 | Serial.println("success"); |
||
426 | } |
||
427 | |||
428 | if (!epeverControlPayload.containsKey("action")) { |
||
12 | office | 429 | epeverControlPayload.clear(); |
10 | office | 430 | return; |
431 | } |
||
432 | |||
433 | if (epeverControlPayload["action"] == "switch") { |
||
434 | // Switch - but i can't seem to switch a coil directly here ?!? |
||
435 | if (epeverControlPayload["status"] == "on") { |
||
436 | loadState = true; |
||
437 | } |
||
438 | |||
439 | if (epeverControlPayload["status"] == "off") { |
||
440 | loadState = false; |
||
441 | } |
||
442 | |||
12 | office | 443 | Serial.print("Setting load state:"); |
444 | node.clearResponseBuffer(); |
||
445 | node.writeSingleCoil(0x0001, 1); |
||
446 | result = node.writeSingleCoil(0x0002, loadState); |
||
13 | office | 447 | if (result == node.ku8MBSuccess) { |
12 | office | 448 | Serial.println("success"); |
13 | office | 449 | } else { |
12 | office | 450 | Serial.println("failure"); |
451 | Serial.print("Miss write loadState, ret val: "); |
||
452 | Serial.println(result, HEX); |
||
453 | } |
||
10 | office | 454 | } |
455 | |||
456 | if (epeverControlPayload["action"] == "settings") { |
||
457 | if (epeverControlPayload.containsKey("sleep")) { |
||
458 | // input sanitization |
||
459 | int seconds = (unsigned int)epeverControlPayload["sleep"]; |
||
13 | office | 460 | if (seconds == sleepSeconds) { |
461 | Serial.println("no change"); |
||
462 | return; |
||
463 | } |
||
464 | |||
11 | office | 465 | if (seconds < MIN_SLEEP_SECONDS) { |
10 | office | 466 | sleepSeconds = MIN_SLEEP_SECONDS; |
13 | office | 467 | } else { |
468 | sleepSeconds = seconds; |
||
10 | office | 469 | } |
13 | office | 470 | |
471 | EEPROM.put(0, sleepSeconds); |
||
472 | if (!EEPROM.commit()) { |
||
473 | Serial.println("Failure setting sleep seconds."); |
||
474 | return; |
||
475 | } |
||
476 | |||
477 | Serial.print("Set sleep seconds to: "); |
||
10 | office | 478 | Serial.println(sleepSeconds); |
479 | } |
||
480 | |||
12 | office | 481 | epeverControlPayload.clear(); |
10 | office | 482 | return; |
483 | } |
||
484 | } |
||
485 | |||
21 | office | 486 | /////////////////////////////////////////////////////////////////////////// |
10 | office | 487 | // Arduino functions // |
488 | /////////////////////////////////////////////////////////////////////////// |
||
489 | void setup() { |
||
13 | office | 490 | Serial.begin(115200); // DO NOT CHANGE! |
491 | while (!Serial) |
||
10 | office | 492 | ; |
493 | Serial.println(); |
||
494 | Serial.println("Hello World! I'm an EpEver Solar Monitor!"); |
||
495 | |||
496 | // init modbus in receive mode |
||
497 | pinMode(MAX485_RE, OUTPUT); |
||
498 | pinMode(MAX485_DE, OUTPUT); |
||
499 | digitalWrite(MAX485_RE, 0); |
||
500 | digitalWrite(MAX485_DE, 0); |
||
12 | office | 501 | |
10 | office | 502 | // modbus callbacks |
503 | node.preTransmission(preTransmission); |
||
504 | node.postTransmission(postTransmission); |
||
13 | office | 505 | // EPEver Device ID 1 |
506 | node.begin(1, Serial); |
||
10 | office | 507 | |
12 | office | 508 | // Connect D0 to RST to wake up |
21 | office | 509 | #ifdef USE_DEEP_SLEEP |
510 | pinMode(D0, WAKEUP_PULLUP); |
||
511 | #endif |
||
12 | office | 512 | |
13 | office | 513 | // variable handling |
514 | EEPROM.begin(16); |
||
515 | EEPROM.get(0, sleepSeconds); |
||
516 | if (sleepSeconds < MIN_SLEEP_SECONDS) { |
||
517 | sleepSeconds = 60; |
||
518 | EEPROM.put(0, sleepSeconds); |
||
519 | if (!EEPROM.commit()) { |
||
520 | Serial.println("Unable to set default sleep."); |
||
521 | } |
||
522 | } |
||
523 | |||
10 | office | 524 | // Initialize the LED_BUILTIN pin as an output, low active |
525 | pinMode(LED_BUILTIN, OUTPUT); |
||
526 | digitalWrite(LED_BUILTIN, HIGH); |
||
527 | } |
||
528 | |||
529 | void loop() { |
||
530 | uint8_t i, result; |
||
531 | |||
21 | office | 532 | // Turn on LED |
533 | digitalWrite(LED_BUILTIN, HIGH); |
||
10 | office | 534 | |
21 | office | 535 | // Clear old data |
10 | office | 536 | memset(rtc.buf, 0, sizeof(rtc.buf)); |
537 | memset(live.buf, 0, sizeof(live.buf)); |
||
538 | memset(stats.buf, 0, sizeof(stats.buf)); |
||
539 | |||
540 | // Read registers for clock |
||
541 | node.clearResponseBuffer(); |
||
542 | result = node.readHoldingRegisters(RTC_CLOCK, RTC_CLOCK_CNT); |
||
543 | if (result == node.ku8MBSuccess) { |
||
544 | rtc.buf[0] = node.getResponseBuffer(0); |
||
545 | rtc.buf[1] = node.getResponseBuffer(1); |
||
546 | rtc.buf[2] = node.getResponseBuffer(2); |
||
547 | |||
548 | } else { |
||
549 | Serial.print("Miss read rtc-data, ret val:"); |
||
550 | Serial.println(result, HEX); |
||
551 | } |
||
552 | |||
21 | office | 553 | // Read LIVE-Data |
10 | office | 554 | node.clearResponseBuffer(); |
555 | result = node.readInputRegisters(LIVE_DATA, LIVE_DATA_CNT); |
||
556 | |||
557 | if (result == node.ku8MBSuccess) { |
||
558 | |||
559 | for (i = 0; i < LIVE_DATA_CNT; i++) live.buf[i] = node.getResponseBuffer(i); |
||
560 | |||
561 | } else { |
||
562 | Serial.print("Miss read liva-data, ret val:"); |
||
563 | Serial.println(result, HEX); |
||
564 | } |
||
565 | |||
566 | // Statistical Data |
||
567 | node.clearResponseBuffer(); |
||
568 | result = node.readInputRegisters(STATISTICS, STATISTICS_CNT); |
||
569 | if (result == node.ku8MBSuccess) { |
||
21 | office | 570 | for (i = 0; i < STATISTICS_CNT; i++) { |
571 | stats.buf[i] = node.getResponseBuffer(i); |
||
572 | } |
||
10 | office | 573 | } else { |
574 | Serial.print("Miss read statistics, ret val:"); |
||
575 | Serial.println(result, HEX); |
||
576 | } |
||
577 | |||
578 | // Battery SOC |
||
579 | node.clearResponseBuffer(); |
||
580 | result = node.readInputRegisters(BATTERY_SOC, 1); |
||
581 | if (result == node.ku8MBSuccess) { |
||
582 | batterySOC = node.getResponseBuffer(0); |
||
583 | } else { |
||
584 | Serial.print("Miss read batterySOC, ret val:"); |
||
585 | Serial.println(result, HEX); |
||
586 | } |
||
587 | |||
588 | // Battery Net Current = Icharge - Iload |
||
589 | node.clearResponseBuffer(); |
||
590 | result = node.readInputRegisters(BATTERY_CURRENT_L, 2); |
||
591 | if (result == node.ku8MBSuccess) { |
||
592 | batteryCurrent = node.getResponseBuffer(0); |
||
593 | batteryCurrent |= node.getResponseBuffer(1) << 16; |
||
594 | } else { |
||
595 | Serial.print("Miss read batteryCurrent, ret val:"); |
||
596 | Serial.println(result, HEX); |
||
597 | } |
||
598 | |||
599 | // State of the Load Switch |
||
600 | node.clearResponseBuffer(); |
||
601 | result = node.readCoils(LOAD_STATE, 1); |
||
602 | if (result == node.ku8MBSuccess) { |
||
603 | loadState = node.getResponseBuffer(0); |
||
604 | } else { |
||
605 | Serial.print("Miss read loadState, ret val:"); |
||
606 | Serial.println(result, HEX); |
||
607 | } |
||
608 | |||
609 | // Read Status Flags |
||
610 | node.clearResponseBuffer(); |
||
611 | result = node.readInputRegisters(0x3200, 2); |
||
612 | if (result == node.ku8MBSuccess) { |
||
613 | uint16_t temp = node.getResponseBuffer(0); |
||
614 | Serial.print("Batt Flags : "); |
||
615 | Serial.println(temp); |
||
616 | |||
617 | status_batt.volt = temp & 0b1111; |
||
618 | status_batt.temp = (temp >> 4) & 0b1111; |
||
619 | status_batt.resistance = (temp >> 8) & 0b1; |
||
620 | status_batt.rated_volt = (temp >> 15) & 0b1; |
||
621 | |||
622 | temp = node.getResponseBuffer(1); |
||
623 | Serial.print("Chrg Flags : "); |
||
624 | Serial.println(temp, HEX); |
||
625 | |||
626 | //for(i=0; i<16; i++) Serial.print( (temp >> (15-i) ) & 1 ); |
||
627 | //Serial.println(); |
||
628 | |||
629 | //charger_input = ( temp & 0b0000000000000000 ) >> 15 ; |
||
630 | charger_mode = (temp & 0b0000000000001100) >> 2; |
||
631 | //charger_input = ( temp & 0b0000000000000000 ) >> 12 ; |
||
632 | //charger_operation = ( temp & 0b0000000000000000 ) >> 0 ; |
||
633 | |||
634 | //Serial.print( "charger_input : "); Serial.println( charger_input ); |
||
635 | Serial.print("charger_mode : "); |
||
636 | Serial.println(charger_mode); |
||
637 | //Serial.print( "charger_oper : "); Serial.println( charger_operation ); |
||
638 | //Serial.print( "charger_state : "); Serial.println( charger_state ); |
||
639 | } else { |
||
640 | Serial.print("Miss read ChargeState, ret val:"); |
||
641 | Serial.println(result, HEX); |
||
642 | } |
||
643 | |||
644 | // Print out to serial |
||
21 | office | 645 | #ifdef SERIAL_DATA |
10 | office | 646 | 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); |
647 | Serial.print("\nLive-Data: Volt Amp Watt "); |
||
648 | 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); |
||
649 | 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); |
||
650 | 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); |
||
651 | Serial.println(); |
||
652 | Serial.printf("\n Battery Current: %7.3f A ", batteryCurrent / 100.f); |
||
653 | Serial.printf("\n Battery SOC: %7.0f %% ", batterySOC / 1.0f); |
||
654 | Serial.printf("\n Load Switch: %s ", (loadState == 1 ? " On" : "Off")); |
||
655 | Serial.print("\n\nStatistics: "); |
||
656 | Serial.printf("\n Panel: min: %7.3f max: %7.3f V", stats.s.pVmin / 100.f, stats.s.pVmax / 100.f); |
||
657 | Serial.printf("\n Battery: min: %7.3f max: %7.3f V", stats.s.bVmin / 100.f, stats.s.bVmax / 100.f); |
||
658 | Serial.println(); |
||
659 | Serial.printf("\n Consumed: day: %7.3f mon: %7.3f year: %7.3f total: %7.3f kWh", |
||
660 | stats.s.consEnerDay / 100.f, stats.s.consEnerMon / 100.f, stats.s.consEnerYear / 100.f, stats.s.consEnerTotal / 100.f); |
||
661 | Serial.printf("\n Generated: day: %7.3f mon: %7.3f year: %7.3f total: %7.3f kWh", |
||
662 | stats.s.genEnerDay / 100.f, stats.s.genEnerMon / 100.f, stats.s.genEnerYear / 100.f, stats.s.genEnerTotal / 100.f); |
||
663 | Serial.printf("\n CO2-Reduction: %7.3f t ", stats.s.c02Reduction / 100.f); |
||
664 | Serial.println(); |
||
665 | Serial.print("\nStatus:"); |
||
666 | Serial.printf("\n batt.volt: %s ", batt_volt_status[status_batt.volt]); |
||
667 | Serial.printf("\n batt.temp: %s ", batt_temp_status[status_batt.temp]); |
||
668 | Serial.printf("\n charger.charging: %s ", charger_charging_status[charger_mode]); |
||
669 | Serial.println(); |
||
670 | Serial.println(); |
||
21 | office | 671 | #endif |
10 | office | 672 | |
21 | office | 673 | // Start WiFi connection. |
10 | office | 674 | digitalWrite(LED_BUILTIN, LOW); |
675 | WiFi.mode(WIFI_STA); |
||
676 | WiFi.begin(STA_SSID, STA_PASSWORD); |
||
677 | if (WiFi.waitForConnectResult() != WL_CONNECTED) { |
||
678 | Serial.println("Connection Failed! Rebooting..."); |
||
679 | delay(5000); |
||
680 | ESP.restart(); |
||
681 | } |
||
682 | |||
683 | Serial.println(""); |
||
684 | Serial.println("WiFi connected"); |
||
685 | Serial.println("IP address: "); |
||
686 | Serial.println(WiFi.localIP()); |
||
687 | |||
688 | // Port defaults to 8266 |
||
21 | office | 689 | #ifdef OTA_PORT |
10 | office | 690 | ArduinoOTA.setPort(OTA_PORT); |
21 | office | 691 | #endif |
10 | office | 692 | |
693 | // Hostname defaults to esp8266-[ChipID] |
||
21 | office | 694 | #ifdef OTA_HOSTNAME |
10 | office | 695 | if (strlen(OTA_HOSTNAME) != 0) { |
696 | ArduinoOTA.setHostname(OTA_HOSTNAME); |
||
697 | } |
||
21 | office | 698 | #endif |
10 | office | 699 | |
700 | // No authentication by default |
||
21 | office | 701 | #ifdef OTA_PASSWORD_HASH |
10 | office | 702 | if (strlen(OTA_PASSWORD_HASH) != 0) { |
703 | ArduinoOTA.setPasswordHash(OTA_PASSWORD_HASH); |
||
704 | } |
||
21 | office | 705 | #endif |
10 | office | 706 | |
707 | ArduinoOTA.onStart([]() { |
||
708 | String type; |
||
709 | if (ArduinoOTA.getCommand() == U_FLASH) { |
||
710 | type = "sketch"; |
||
711 | } else { // U_FS |
||
712 | type = "filesystem"; |
||
713 | } |
||
714 | |||
715 | // NOTE: if updating FS this would be the place to unmount FS using FS.end() |
||
716 | Serial.println("Start updating " + type); |
||
717 | }); |
||
718 | ArduinoOTA.onEnd([]() { |
||
719 | Serial.println("\nEnd"); |
||
720 | }); |
||
721 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { |
||
722 | Serial.printf("Progress: %u%%\r", (progress / (total / 100))); |
||
723 | }); |
||
724 | ArduinoOTA.onError([](ota_error_t error) { |
||
725 | Serial.printf("Error[%u]: ", error); |
||
726 | if (error == OTA_AUTH_ERROR) { |
||
727 | Serial.println("Auth Failed"); |
||
728 | } else if (error == OTA_BEGIN_ERROR) { |
||
729 | Serial.println("Begin Failed"); |
||
730 | } else if (error == OTA_CONNECT_ERROR) { |
||
731 | Serial.println("Connect Failed"); |
||
732 | } else if (error == OTA_RECEIVE_ERROR) { |
||
733 | Serial.println("Receive Failed"); |
||
734 | } else if (error == OTA_END_ERROR) { |
||
735 | Serial.println("End Failed"); |
||
736 | } |
||
737 | }); |
||
738 | ArduinoOTA.begin(); |
||
739 | |||
21 | office | 740 | // Establish/keep mqtt connection |
10 | office | 741 | mqtt_client.setServer(MQTT_SERVER, MQTT_PORT); |
742 | mqtt_client.setCallback(mqtt_callback); |
||
743 | mqtt_reconnect(); |
||
744 | digitalWrite(LED_BUILTIN, HIGH); |
||
745 | |||
746 | // Once connected, publish an announcement. |
||
12 | office | 747 | controllerStatusPayload["solar"]["monitor"]["status"] = "online"; |
748 | serializeJson(controllerStatusPayload, tmpJsonPayloadBuffer); |
||
749 | controllerStatusPayload.clear(); |
||
750 | mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "status").c_str(), tmpJsonPayloadBuffer); |
||
10 | office | 751 | |
13 | office | 752 | controllerStatusPayload["solar"]["monitor"]["status"] = "waiting"; |
753 | serializeJson(controllerStatusPayload, tmpJsonPayloadBuffer); |
||
754 | controllerStatusPayload.clear(); |
||
755 | mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "status").c_str(), tmpJsonPayloadBuffer); |
||
756 | |||
21 | office | 757 | // Wait for MQTT subscription processing |
13 | office | 758 | Serial.println("Waiting for MQTT and OTA events."); |
759 | unsigned int now = millis(); |
||
760 | while (millis() - now < MQTT_SUBSCRIBE_WAIT * 1000) { |
||
761 | // Loop for MQTT. |
||
762 | if (!mqtt_client.loop() || WiFi.status() != WL_CONNECTED) { |
||
763 | break; |
||
764 | } |
||
765 | // Loop for OTA. |
||
766 | ArduinoOTA.handle(); |
||
767 | delay(100); |
||
768 | } |
||
769 | Serial.println("Done waiting for MQTT and OTA events."); |
||
770 | |||
21 | office | 771 | // Publish to MQTT |
11 | office | 772 | Serial.print("Publishing to MQTT: "); |
10 | office | 773 | |
21 | office | 774 | // Panel |
11 | office | 775 | epeverMetricsPayload["solar"]["panel"]["V"] = String(live.l.pV / 100.f, 2); |
776 | epeverMetricsPayload["solar"]["panel"]["I"] = String(live.l.pI / 100.f, 2); |
||
777 | epeverMetricsPayload["solar"]["panel"]["P"] = String(live.l.pP / 100.f, 2); |
||
778 | epeverMetricsPayload["solar"]["panel"]["minV"] = String(stats.s.pVmin / 100.f, 3); |
||
779 | epeverMetricsPayload["solar"]["panel"]["maxV"] = String(stats.s.pVmax / 100.f, 3); |
||
10 | office | 780 | |
11 | office | 781 | serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); |
782 | epeverMetricsPayload.clear(); |
||
783 | mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "panel").c_str(), tmpJsonPayloadBuffer); |
||
784 | |||
21 | office | 785 | // Battery |
11 | office | 786 | epeverMetricsPayload["solar"]["battery"]["V"] = String(live.l.bV / 100.f, 2); |
787 | epeverMetricsPayload["solar"]["battery"]["I"] = String(live.l.bI / 100.f, 2); |
||
788 | epeverMetricsPayload["solar"]["battery"]["P"] = String(live.l.bP / 100.f, 2); |
||
789 | epeverMetricsPayload["solar"]["battery"]["minV"] = String(stats.s.bVmin / 100.f, 2); |
||
790 | epeverMetricsPayload["solar"]["battery"]["maxV"] = String(stats.s.bVmax / 100.f, 2); |
||
791 | epeverMetricsPayload["solar"]["battery"]["SOC"] = String(batterySOC / 1.0f, 2); |
||
792 | epeverMetricsPayload["solar"]["battery"]["netI"] = String(batteryCurrent / 100.0f, 2); |
||
793 | epeverMetricsPayload["solar"]["battery"]["status"]["voltage"].set(batt_volt_status[status_batt.volt]); |
||
794 | epeverMetricsPayload["solar"]["battery"]["status"]["temperature"].set(batt_temp_status[status_batt.temp]); |
||
10 | office | 795 | |
11 | office | 796 | serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); |
797 | epeverMetricsPayload.clear(); |
||
798 | mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "battery").c_str(), tmpJsonPayloadBuffer); |
||
10 | office | 799 | |
21 | office | 800 | // Load |
11 | office | 801 | epeverMetricsPayload["solar"]["load"]["V"] = String(live.l.lV / 100.f, 2); |
802 | epeverMetricsPayload["solar"]["load"]["I"] = String(live.l.lI / 100.f, 2); |
||
803 | epeverMetricsPayload["solar"]["load"]["P"] = String(live.l.lP / 100.f, 2); |
||
10 | office | 804 | // pimatic state topic does not work with integers or floats ?!? |
11 | office | 805 | switch (loadState) { |
806 | case 1: |
||
807 | epeverMetricsPayload["solar"]["load"]["state"].set("on"); |
||
808 | break; |
||
809 | default: |
||
810 | epeverMetricsPayload["solar"]["load"]["state"].set("off"); |
||
811 | break; |
||
812 | } |
||
10 | office | 813 | |
11 | office | 814 | serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); |
815 | epeverMetricsPayload.clear(); |
||
816 | mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "load").c_str(), tmpJsonPayloadBuffer); |
||
10 | office | 817 | |
21 | office | 818 | // Energy |
11 | office | 819 | epeverMetricsPayload["solar"]["energy"]["consumed_day"] = String(stats.s.consEnerDay / 100.f, 3); |
820 | epeverMetricsPayload["solar"]["energy"]["consumed_all"] = String(stats.s.consEnerTotal / 100.f, 3); |
||
821 | epeverMetricsPayload["solar"]["energy"]["generated_day"] = String(stats.s.genEnerDay / 100.f, 3); |
||
822 | epeverMetricsPayload["solar"]["energy"]["generated_all"] = String(stats.s.genEnerTotal / 100.f, 3); |
||
10 | office | 823 | |
11 | office | 824 | serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); |
825 | epeverMetricsPayload.clear(); |
||
826 | mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "energy").c_str(), tmpJsonPayloadBuffer); |
||
10 | office | 827 | |
21 | office | 828 | // Extra |
11 | office | 829 | epeverMetricsPayload["solar"]["extra"]["CO2"]["t"] = String(stats.s.c02Reduction / 100.f, 2); |
830 | //epever_serialize_s( "solar/status/charger_input", charger_input_status[ charger_input ] |
||
831 | epeverMetricsPayload["solar"]["extra"]["charger_mode"] = charger_charging_status[charger_mode]; |
||
832 | char buf[21]; |
||
833 | sprintf(buf, "20%02d-%02d-%02d %02d:%02d:%02d", |
||
834 | rtc.r.y, rtc.r.M, rtc.r.d, rtc.r.h, rtc.r.m, rtc.r.s); |
||
835 | epeverMetricsPayload["solar"]["extra"]["time"] = buf; |
||
10 | office | 836 | |
11 | office | 837 | serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); |
838 | epeverMetricsPayload.clear(); |
||
839 | mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "extra").c_str(), tmpJsonPayloadBuffer); |
||
10 | office | 840 | |
21 | office | 841 | // Settings |
11 | office | 842 | epeverMetricsPayload["solar"]["monitor"]["settings"]["sleep"].set(sleepSeconds); |
10 | office | 843 | |
11 | office | 844 | serializeJson(epeverMetricsPayload, tmpJsonPayloadBuffer); |
845 | epeverMetricsPayload.clear(); |
||
846 | mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "settings").c_str(), tmpJsonPayloadBuffer); |
||
10 | office | 847 | |
11 | office | 848 | Serial.println("done"); |
849 | |||
12 | office | 850 | controllerStatusPayload["solar"]["monitor"]["status"] = "offline"; |
851 | serializeJson(controllerStatusPayload, tmpJsonPayloadBuffer); |
||
852 | controllerStatusPayload.clear(); |
||
853 | mqtt_client.publish((String(MQTT_TOPIC_PUB) + "/" + "status").c_str(), tmpJsonPayloadBuffer); |
||
10 | office | 854 | |
21 | office | 855 | // Ensure all messages are sent |
10 | office | 856 | mqtt_client.unsubscribe(MQTT_TOPIC_SUB); |
857 | mqtt_client.disconnect(); |
||
858 | while (mqtt_client.state() != -1) { |
||
859 | delay(100); |
||
860 | } |
||
861 | |||
862 | // disconnect wifi |
||
863 | WiFi.disconnect(true); |
||
864 | |||
865 | // power down MAX485_DE |
||
866 | // low active |
||
867 | digitalWrite(MAX485_RE, 0); |
||
868 | digitalWrite(MAX485_DE, 0); |
||
869 | |||
21 | office | 870 | // Sleep |
871 | Serial.print("\nSleep for "); |
||
10 | office | 872 | Serial.print(sleepSeconds); |
873 | Serial.println(" Seconds"); |
||
21 | office | 874 | digitalWrite(LED_BUILTIN, LOW); |
875 | #ifdef USE_DEEP_SLEEP |
||
10 | office | 876 | if (USE_DEEP_SLEEP) { |
877 | ESP.deepSleep(sleepSeconds * 1000000); |
||
878 | } else { |
||
879 | delay(sleepSeconds * 1000); |
||
880 | } |
||
21 | office | 881 | #else |
882 | delay(sleepSeconds * 1000); |
||
883 | #endif |
||
10 | office | 884 | } |