From 892be150acf038b2d695693fa0b3a8735ced538c Mon Sep 17 00:00:00 2001 From: krulkip Date: Sun, 19 May 2024 22:39:04 +0200 Subject: [PATCH 1/8] Add files via upload --- soyosource-powercontroller_V0012.ino | 1365 ++++++++++++++++++++++++++ 1 file changed, 1365 insertions(+) create mode 100644 soyosource-powercontroller_V0012.ino diff --git a/soyosource-powercontroller_V0012.ino b/soyosource-powercontroller_V0012.ino new file mode 100644 index 0000000..4997758 --- /dev/null +++ b/soyosource-powercontroller_V0012.ino @@ -0,0 +1,1365 @@ +/*************************************************************************** + soyosource-powercontroller @matlen67 + + Version: 1.240508BK + + 16.03.2024 -> Speichern der Checkboxzustände: aktiv Timer1 / Timer2 + 03.04.2024 -> Statusübersicht bei geschlossenen details/summary boxen + 14.04.2024 -> Falls Batterieschutz aktiviert, deaktiviere Regelung der Nulleinspeisung + 25.04.2024 -> Leistungspunkt bei Nulleinspeisung festlegen + (Bei mir funktioniert gut Intervall Shelly 1000ms & Intervall Nulleinspeisung 4000ms) + 26.04.2024 -> Auswahl der aktiven Leiter (L1, L2, L3) beim Shelly + 27.04.2024 -> Fehlerbehebung Shelly 3EM, Shelly Plus 1PM mit zugefügt + 28.04.2024 -> Teiler unter 'SoyoSource Output' hinzugefügt, um die Leistung auf mehere Geräte aufzuteilen + 29.04.2024 -> Telnet entfernt + 05.05.2024 -> update ArduinoJson to 7.0.4 + 08.05.2024 -> mqtt topic voltage & soc bearbeitbar + + + ************************* + Wiring + NodeMCU D1 - RS485 RO + NodeMCU D3 - RS485 DE/RE + NodeMCU D4 - RS485 DI + +****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "html.h" + +#define DEBUG_SERIAL Serial + +#define DEBUG + +#ifdef DEBUG + #define DBG_PRINT(x) DEBUG_SERIAL.print(x) + #define DBG_PRINTLN(x) DEBUG_SERIAL.println(x) +#else + #define DBG_PRINT(x) + #define DBG_PRINTLN(x) +#endif + +//***************************************************************************** +// da Serial.printf(x,x) mit define nicht funktioniert als workaround sprintf +// sprintf(dbgbuffer,"ESP_%02X%02X%02X", mac[3], mac[4], mac[5]); +// DBG_PRINTLN(dbgbuffer); +//***************************************************************************** +char dbgbuffer[128]; +#define D1 5 +#define D3 0 +#define D4 2 + +#define RXPin D1 // Serial Receive pin (D1) +#define TXPin D4 // Serial Transmit pin (D4) + +//RS485 control +#define SERIAL_COMMUNICATION_CONTROL_PIN D3 // Transmission set pin (D3) +#define RS485_TX_PIN_VALUE HIGH +#define RS485_RX_PIN_VALUE LOW + +// time server +#define MY_NTP_SERVER "de.pool.ntp.org" +#define MY_TZ "CET-1CEST,M3.5.0/2,M10.5.0/3" + + +SoftwareSerial RS485Serial(RXPin, TXPin); // RX, TX +WiFiClient espClient; +PubSubClient client(espClient); +AsyncWebServer server(80); +AsyncEventSource events("/events"); +AsyncDNSServer dns; + + +// Uptime Global Variables +Uptime uptime; +uint8_t Uptime_Years = 0U, Uptime_Months = 0U, Uptime_Days = 0U, Uptime_Hours = 0U, Uptime_Minutes = 0U, Uptime_Seconds = 0U; +uint16_t Uptime_TotalDays = 0U; // Total Uptime Days +char uptime_str[37]; + +// Wifi to percent +const int RSSI_MAX =-50; // max strength signal in dBm +const int RSSI_MIN =-100; // min strength signal in dBm + +//Timer +unsigned long timerSoyoSource = 555; +unsigned long lastTimerSoyoSource = 0; + +unsigned long timerUptime = 1000; +unsigned long lastTimerUptime = 0; + +unsigned long meterinterval = 2000; +unsigned long lastMeterinterval = 0; + +unsigned long nullinterval = 5000; +unsigned long lastNullinterval = 0; + + + +//mqtt +char mqtt_server[16] = "192.168.178.30"; +char mqtt_port[5] = "1889"; +char msgData[64]; +String msg = ""; +char mqtt_topic_bat_voltage [48] = "VenusOS/SmartShunt/voltage"; +char mqtt_topic_bat_soc [48] = "VenusOS/SmartShunt/soc"; + +String dataReceived; +int data; +bool isDataReceived = false; +uint8_t byte0, byte1, byte2, byte3, byte4, byte5, byte6, byte7; +int byteSend; +int data_array[8]; +int soyo_hello_data[8] = {0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // bit7 org 0x00, CRC 0xFF +int soyo_power_data[8] = {0x24, 0x56, 0x00, 0x21, 0x00, 0x00, 0x80, 0x08}; // 0 Watt +int soyo_text_data[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +char buffer[8]; +int old_soyo_power = 0; +int soyo_power = 0; +int new_soyo_power = 0; +int teiler_output = 1; + +unsigned char mac[6]; +char mqtt_root[32] = "SoyoSource/"; +char clientId[16]; +char topic_power[40]; +char soyo_text[40]; + +float mqtt_bat_soc = 0.0; +float mqtt_bat_voltage = 0.0; + +long rssi; + +time_t now; +tm timeInfo; + + +// timer +char currentTime[20]; +char timer1_time[6] = "06:00"; +char timer2_time[6] = "20:00"; +char meteripaddr[16] = ""; + +int timer1_watt = 0; +int timer2_watt = 0; +int maxwatt = 0; + +//state checkboxes +bool checkbox_timer1 = false; +bool checkbox_timer2 = false; +bool checkbox_mqttenabled = false; +bool checkbox_nulleinspeisung = false; +bool checkbox_batschutz = false; +bool checkbox_meter_l1 = true; +bool checkbox_meter_l2 = true; +bool checkbox_meter_l3 = true; + +char metername[24] = "Meter"; +char mqtt_state[20] = "disabled"; + +// variablen Shelly 3em +const int shelly_3em_pro = 1; // ip/rpc/Shelly.GetStatus +const int shelly_plus_1pm = 2; // ip/rpc/Shelly.GetStatus + +const int shelly_3em = 10; // ip/status +const int shelly_em = 11; // ip/status +const int shelly_1pm = 12; // ip/status + +const int homewizard = 20; // ip/api/v1/data + +String meter_ip = ""; +int meter_model = 0 ; + +//nulleinspeisung +int nulloffset = 0; +int meter_power = 0; +int meterpower = 0; +int meterl1 = 0; +int meterl2 = 0; +int meterl3 = 0; + +//batterieüberwachung +int batsocstop = 15; +int batsocstart = 50; +bool output_enabled = true; + + +bool new_connect = true; + +const char* PARAM_MESSAGE = "message"; + +//flag for saving data +bool shouldSaveConfig = false; + + +//callback notifying us of the need to save config +void saveConfigCallback () { + DBG_PRINTLN("Should save config"); + shouldSaveConfig = true; +} + + +void notFound(AsyncWebServerRequest *request) { + request->send(404, "text/plain", "Not found"); +} + + +int dBmtoPercent(int dBm){ + int percent; + if(dBm <= RSSI_MIN){ + percent = 0; + } else if(dBm >= RSSI_MAX) { + percent = 100; + } else { + percent = 2 * (dBm + 100); + } + + return percent; +} + + +void myUptime(){ + uptime.calculateUptime(); + + // Get The Uptime Values To Global Variables + Uptime_Years = uptime.getYears(); + Uptime_Months = uptime.getMonths(); + Uptime_Days = uptime.getDays(); + Uptime_Hours = uptime.getHours(); + Uptime_Minutes = uptime.getMinutes(); + Uptime_Seconds = uptime.getSeconds(); + Uptime_TotalDays = uptime.getTotalDays(); + + if (Uptime_Years == 0U) { // Uptime Is Less Than One Year + // First 60 Seconds + if (Uptime_Minutes == 0U && Uptime_Hours == 0U && Uptime_Days == 0U && Uptime_Months == 0U) + sprintf(uptime_str, "00:00:%02i", Uptime_Seconds); + // First Minute + else if (Uptime_Minutes == 1U && Uptime_Hours == 0U && Uptime_Days == 0U && Uptime_Months == 0U) + sprintf(uptime_str, "00:%02i:%02i", Uptime_Minutes, Uptime_Seconds); + // Second Minute And More But Less Than Hours, Days, Months + else if (Uptime_Minutes >= 2U && Uptime_Hours == 0U && Uptime_Days == 0U && Uptime_Months == 0U) + sprintf(uptime_str, "00:%02i:%02i", Uptime_Minutes, Uptime_Seconds); + // First Hour And More But Less Than Days, Months + else if (Uptime_Hours >= 1U && Uptime_Days == 0U && Uptime_Months == 0U) + sprintf(uptime_str, "%02i:%02i:%02i", Uptime_Hours, Uptime_Minutes, Uptime_Seconds); + // First Day And Less Than Month + else if (Uptime_Days == 1U && Uptime_Months == 0U) + sprintf(uptime_str, "%iday %02i:%02i:%02i", Uptime_Days, Uptime_Hours, Uptime_Minutes, Uptime_Seconds); + // Second Day And More But Less Than Month + else if (Uptime_Days >= 2U && Uptime_Months == 0U) + sprintf(uptime_str, "%idays %02i:%02i:%02i", Uptime_Days, Uptime_Hours, Uptime_Minutes, Uptime_Seconds); + // First Month And More But Less Than One Year + else if (Uptime_Months >= 1U) + sprintf(uptime_str, "%im, %id %02i:%02i", Uptime_Months, Uptime_Days, Uptime_Hours, Uptime_Minutes); + // If There Is Any Error In This If Loop Then Make Full String. + else sprintf(uptime_str, "%iy %im %id %02i:%02i", Uptime_Years, Uptime_Months, Uptime_Days, Uptime_Hours, Uptime_Minutes); + } else // Uptime Is More Than One Year + sprintf(uptime_str, "%iy %im %id %02i:%02i", Uptime_Years, Uptime_Months, Uptime_Days, Uptime_Hours, Uptime_Minutes); +} + + +//callback from mqtt +void mqtt_callback(char* topic, byte* payload, unsigned int length) { + unsigned int i = 0; + + for (i=0;i= 0 && arrived_value_i <= 3000) { + soyo_power = arrived_value_i; + } + } + + if(strcmp(topic, mqtt_topic_bat_soc) == 0){ + float arrived_value_f = atof(buffer); + mqtt_bat_soc = arrived_value_f; + } + + if(strcmp(topic, mqtt_topic_bat_voltage) == 0){ + float arrived_value_f = atof(buffer); + mqtt_bat_voltage = arrived_value_f; + } +} + + +String processor(const String& var){ + return String(); +} + + +void reconnect() { + DBG_PRINTLN("reconnect MQTT connection!"); + + //set callback again + client.setCallback(mqtt_callback); + + uint8_t timeout = 15; + + // wait for connection + while (!client.connected()){ + + DBG_PRINTLN(""); + + if (client.connect(clientId)) { + DBG_PRINTLN("connection established"); + + client.publish(topic_power, "0"); + client.subscribe(topic_power); + client.subscribe(mqtt_topic_bat_soc); + client.subscribe(mqtt_topic_bat_voltage); + + strcpy(mqtt_state, "connect"); + + DBG_PRINT("subscrible: "); + DBG_PRINT(topic_power); + DBG_PRINTLN(""); + + DBG_PRINT("subscrible: "); + DBG_PRINT(mqtt_topic_bat_soc); + DBG_PRINTLN(""); + + DBG_PRINT("subscrible: "); + DBG_PRINT(mqtt_topic_bat_voltage); + DBG_PRINTLN(""); + + } else { + DBG_PRINTLN("reconnect failed! "); + strcpy(mqtt_state, "connect error"); + + while (timeout){ + DBG_PRINT("."); + timeout--; + delay(1000); + } + } + } + +} + + +int calc_checksumme(int b1, int b2, int b3, int b4, int b5, int b6 ){ + int calc = (0xFF - b1 - b2 - b3 - b4 - b5 - b6) % 256; + return calc & 0xFF; +} + + +void sendSoyoPowerData(int power){ + soyo_power_data[0] = 0x24; + soyo_power_data[1] = 0x56; + soyo_power_data[2] = 0x00; + soyo_power_data[3] = 0x21; + soyo_power_data[4] = power >> 0x08; + soyo_power_data[5] = power & 0xFF; + soyo_power_data[6] = 0x80; + soyo_power_data[7] = calc_checksumme(soyo_power_data[1], soyo_power_data[2], soyo_power_data[3], soyo_power_data[4], soyo_power_data[5], soyo_power_data[6]); + + for(int i=0; i<8; i++) { + RS485Serial.write(soyo_power_data[i]); // send data to RS485 + //DBG_PRINTLN(soyo_power_data[i], HEX); + } +} + +//read config.json +void readConfig(){ + //read configuration from json + DBG_PRINTLN("mounting FS..."); + + if (LittleFS.begin()) { + DBG_PRINTLN("mounted file system"); + if (LittleFS.exists("/config.json")) { + //file exists, reading and loading + DBG_PRINTLN("reading config file"); + File configFile = LittleFS.open("/config.json", "r"); + if (configFile) { + DBG_PRINTLN("opened config file"); + size_t size = configFile.size(); + // Allocate a buffer to store contents of the file. + std::unique_ptr buf(new char[size]); + + configFile.readBytes(buf.get(), size); + + JsonDocument json; + auto deserializeError = deserializeJson(json, buf.get()); + serializeJson(json, Serial); + if (!deserializeError) { + DBG_PRINTLN("\nparsed json"); + strcpy(mqtt_server, json["mqtt_server"]); + strcpy(mqtt_port, json["mqtt_port"]); + + if(json.containsKey("mqtt_bat_vol")){ + strcpy(mqtt_topic_bat_voltage, json["mqtt_bat_vol"]); + } + + if(json.containsKey("mqtt_bat_soc")){ + strcpy(mqtt_topic_bat_soc, json["mqtt_bat_soc"]); + } + + char key_value[2]; + + if(json.containsKey("mqtt_on")){ + strcpy(key_value, json["mqtt_on"]); + if(strcmp(key_value, "1") == 0){ + checkbox_mqttenabled = true; + }else{ + checkbox_mqttenabled = false; + } + } + + if(json.containsKey("zft_on")){ + strcpy(key_value, json["zft_on"]); + + if(strcmp(key_value, "1") == 0){ + checkbox_nulleinspeisung = true; + }else{ + checkbox_nulleinspeisung = false; + } + } + + if(json.containsKey("batp_on")){ + strcpy(key_value, json["batp_on"]); + + if(strcmp(key_value, "1") == 0){ + checkbox_batschutz = true; + }else{ + checkbox_batschutz = false; + } + } + + if(json.containsKey("t1_on")){ + strcpy(key_value, json["t1_on"]); + + if(strcmp(key_value, "1") == 0){ + checkbox_timer1 = true; + }else{ + checkbox_timer1 = false; + } + } + + if(json.containsKey("t2_on")){ + strcpy(key_value, json["t2_on"]); + + if(strcmp(key_value, "1") == 0){ + checkbox_timer2 = true; + }else{ + checkbox_timer2 = false; + } + } + + if(json.containsKey("mtr_l1_on")){ + strcpy(key_value, json["mtr_l1_on"]); + + if(strcmp(key_value, "1") == 0){ + checkbox_meter_l1 = true; + }else{ + checkbox_meter_l1 = false; + } + } + + if(json.containsKey("mtr_l2_on")){ + strcpy(key_value, json["mtr_l2_on"]); + + if(strcmp(key_value, "1") == 0){ + checkbox_meter_l2 = true; + }else{ + checkbox_meter_l2 = false; + } + } + + if(json.containsKey("mtr_l3_on")){ + strcpy(key_value, json["mtr_l3_on"]); + + if(strcmp(key_value, "1") == 0){ + checkbox_meter_l3 = true; + }else{ + checkbox_meter_l3 = false; + } + } + + + if(json.containsKey("t1_t")){ + strcpy(timer1_time, json["t1_t"]); + } + + if(json.containsKey("t2_t")){ + strcpy(timer2_time, json["t2_t"]); + } + + if(json.containsKey("t1_p")){ + timer1_watt = json["t1_p"]; + } + + if(json.containsKey("t2_p")){ + timer2_watt = json["t2_p"]; + } + + if(json.containsKey("mp")){ + maxwatt = json["mp"]; + } + + if(json.containsKey("mtr_ip")){ + strcpy(meteripaddr, json["mtr_ip"]); + meter_ip = String(meteripaddr); + } + + if(json.containsKey("mtr_iv")){ + meterinterval = json["mtr_iv"]; + } + + if(json.containsKey("z_iv")){ + nullinterval = json["z_iv"]; + } + + if(json.containsKey("z_ofs")){ + nulloffset = json["z_ofs"]; + } + + if(json.containsKey("soc_stop")){ + batsocstop = json["soc_stop"]; + } + + if(json.containsKey("soc_start")){ + batsocstart = json["soc_start"]; + } + + if(json.containsKey("tout")){ + teiler_output = json["tout"]; + } + + } else { + DBG_PRINTLN("failed to load json config"); + } + } + } + } else { + DBG_PRINTLN("failed to mount FS"); + } + //end read config data +} + + +// write config.json +void saveConfig(){ + DBG_PRINTLN(F("save data to config.json")); + JsonDocument json; + + json["mqtt_server"] = mqtt_server; + json["mqtt_port"] = mqtt_port; + json["mqtt_bat_vol"] = mqtt_topic_bat_voltage; + json["mqtt_bat_soc"] = mqtt_topic_bat_soc; + + + if(checkbox_mqttenabled){ + json["mqtt_on"] = "1"; + }else{ + json["mqtt_on"] = "0"; + } + + if(checkbox_nulleinspeisung){ + json["zft_on"] = "1"; + }else{ + json["zft_on"] = "0"; + } + + if(checkbox_batschutz){ + json["batp_on"] = "1"; + }else{ + json["batp_on"] = "0"; + } + + if(checkbox_timer1){ + json["t1_on"] = "1"; + }else{ + json["t1_on"] = "0"; + } + + if(checkbox_timer2){ + json["t2_on"] = "1"; + }else{ + json["t2_on"] = "0"; + } + + if(checkbox_meter_l1){ + json["mtr_l1_on"] = "1"; + }else{ + json["mtr_l1_on"] = "0"; + } + + if(checkbox_meter_l2){ + json["mtr_l2_on"] = "1"; + }else{ + json["mtr_l2_on"] = "0"; + } + + if(checkbox_meter_l3){ + json["mtr_l3_on"] = "1"; + }else{ + json["mtr_l3_on"] = "0"; + } + + json["t1_t"] = timer1_time; + json["t1_p"] = timer1_watt; + json["t2_t"] = timer2_time; + json["t2_p"] = timer2_watt; + json["mp"] = maxwatt; + json["mtr_ip"] = meteripaddr; + json["mtr_iv"] = meterinterval; + json["z_iv"] = nullinterval; + json["z_ofs"] = nulloffset; + json["soc_stop"] = batsocstop; + json["soc_start"] = batsocstart; + json["tout"] = teiler_output; + + File configFile = LittleFS.open("/config.json", "w"); + if (!configFile) { + DBG_PRINTLN("failed to open config file for writing"); + return; + } + + serializeJson(json, configFile); + configFile.close(); + + serializeJson(json, Serial); + DBG_PRINTLN(); +} + + +// get meter type(3EM PRO, 3EM, EM, 1PM, Plus 1PM, Homewizard) +int getMeterType(){ + //String meter_url = "http://" + meter_ip + "/shelly"; + meter_ip="192.168.178.141"; + String meter_url = "http://" + meter_ip + "/api"; + int type = 0; + + memset(metername, 0, sizeof(metername)); + strcat(metername, "no device"); + + JsonDocument doc; + + WiFiClient client_meter; + HTTPClient http; + + if (http.begin(client_meter, meter_url)) { + int httpCode = http.GET(); + + if (httpCode > 0) { + if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { + String payload = http.getString(); + + DeserializationError error = deserializeJson(doc, payload); + + if (error) { + DBG_PRINT(F("deserializeJson() failed: ")); + DBG_PRINTLN(error.f_str()); + } + + String json_type = doc["type"]; + String json_model = doc["model"]; + String json_product_name = doc["product_name"]; + if(json_type != NULL){ + + //test auf Shelly 1PM + if(json_type.equals("SHSW-PM")){ + type = shelly_1pm; + memset(metername, 0, sizeof(metername)); + strcat(metername, "Shelly 1PM"); + } + //test auf Shelly EM + if(json_type.equals("SHEM")){ + type = shelly_em; + memset(metername, 0, sizeof(metername)); + strcat(metername, "Shelly EM"); + } + + //test auf Shelly 3EM + if(json_type.equals("SHEM-3")){ + type = shelly_3em; + memset(metername, 0, sizeof(metername)); + strcat(metername, "Shelly 3EM"); + } + } + + + if(json_model != NULL){ + + //test auf Shelly 3EM Pro + if(json_model.equals("SPEM-003CEBEU")) { + type = shelly_3em_pro; + memset(metername, 0, sizeof(metername)); + strcat(metername, "Shelly 3EM Pro"); + } + + //test auf Shelly Plus 1PM + if(json_model.equals("SNSW-001P16EU")) { + type = shelly_plus_1pm; + memset(metername, 0, sizeof(metername)); + strcat(metername, "Shelly Plus 1PM"); + } + } + + if(json_product_name != NULL){ + //test auf Homewizard + if(json_product_name.equals("P1 meter")) { + type = homewizard; + DBG_PRINTLN(type); + memset(metername, 0, sizeof(metername)); + strcat(metername, "Homewizard"); + } + } + } + } + http.end(); + } + DBG_PRINT("getMeterType() = "); + DBG_PRINTLN(String(metername)); + + return type; +} + + +// read meter +int getMeterData(int type) { + String meter_url; + int power = 0; + int power1 = 0; + int power2 = 0; + int power3 = 0; + + JsonDocument doc; + WiFiClient client_meter; + HTTPClient http; + + if (type > 0 && type < 10) { + meter_url = "http://" + meter_ip + "/rpc/Shelly.GetStatus"; // Shelly PRO 3EM + } else if(type >= 10 && type < 15) { + meter_url = "http://" + meter_ip + "/status"; // Shelly 3EM und Andere + } else if (type >=16) { + meter_url = "http://" + meter_ip + "/api/v1/data"; // Homewizard + } else { + return 0; + } + + if (http.begin(client_meter, meter_url)) { + int httpCode = http.GET(); + if (httpCode > 0) { + if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { + String payload = http.getString(); + DeserializationError error = deserializeJson(doc, payload); + + if (error) { + DBG_PRINT(F("deserializeJson() failed: ")); + DBG_PRINTLN(error.f_str()); + } + + + if (type == shelly_3em_pro) { + power1 = doc["em:0"]["a_act_power"]; + power2 = doc["em:0"]["b_act_power"]; + power3 = doc["em:0"]["c_act_power"]; + } else if (type == shelly_3em) { + power1 = doc["emeters"][0]["power"]; + power2 = doc["emeters"][1]["power"]; + power3 = doc["emeters"][2]["power"]; + } else if (type == shelly_em) { + power1 = doc["meters"][0]["power"]; + power2 = doc["meters"][1]["power"]; + power3 = 0; + } else if (type == shelly_1pm) { + power1 = doc["meters"][0]["power"]; + power2 = 0; + power3 = 0; + } else if (type == shelly_plus_1pm) { + power1 = doc["switch:0"]["apower"]; + power2 = 0; + power3 = 0; + } else if (type == homewizard) { + power1 = doc["active_power_l1_w"]; + power2 = doc["active_power_l2_w"]; + power3 = doc["active_power_l3_w"]; + } + + + if(!checkbox_meter_l1){ + power1 = 0; + } + + if(!checkbox_meter_l2){ + power2 = 0; + } + + if(!checkbox_meter_l3){ + power3 = 0; + } + + power = power1 + power2 + power3; + + meterpower = power; + meterl1 = power1; + meterl2 = power2; + meterl3 = power3; + } + + } else { + sprintf(dbgbuffer,"[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str()); + DBG_PRINTLN(dbgbuffer); + meter_model = 0; + } + + http.end(); + } else { + DBG_PRINTLN("[HTTP] Unable to connect\n"); + meter_model = 0; + } + + return power; +} + + +void checkTimer(){ + + time(&now); + localtime_r(&now, &timeInfo); + + if (checkbox_timer1 == true){ + int t1_hour = String(timer1_time).substring(0,2).toInt(); + int t1_min = String(timer1_time).substring(3).toInt(); + + if((timeInfo.tm_hour == t1_hour && timeInfo.tm_min == t1_min && timeInfo.tm_sec == 0) || (timeInfo.tm_hour == t1_hour && timeInfo.tm_min == t1_min && timeInfo.tm_sec == 1) ){ + soyo_power = timer1_watt; + } + } + + if (checkbox_timer2 == true){ + int t2_hour = String(timer2_time).substring(0,2).toInt(); + int t2_min = String(timer2_time).substring(3).toInt(); + + if((timeInfo.tm_hour == t2_hour && timeInfo.tm_min == t2_min && timeInfo.tm_sec == 0) || (timeInfo.tm_hour == t2_hour && timeInfo.tm_min == t2_min && timeInfo.tm_sec == 1)){ + soyo_power = timer2_watt; + } + } + +} + + +//#################### SETUP ####################### +void setup() { + + DEBUG_SERIAL.begin(115200); + delay(500); + + DBG_PRINTLN(""); + DBG_PRINT(F("CPU Frequency = ")); + DBG_PRINT(F_CPU / 1000000); + DBG_PRINTLN(F(" MHz")); + + WiFi.macAddress(mac); + WiFi.persistent(true); // sonst verliert er nach einem Neustart die IP !!! + + sprintf(dbgbuffer,"ESP_%02X%02X%02X", mac[3], mac[4], mac[5]); + DBG_PRINTLN(dbgbuffer); + + //configTime(MY_TZ, MY_NTP_SERVER); + + sprintf(clientId, "soyo_%02x%02x%02x", mac[3], mac[4], mac[5] ); + + //mqtt_root = "SoyoSource/soyo_xxxxxx"; + strcat(mqtt_root, clientId); + + //topic_power = "SoyoSource/soyo_xxxxxx/power"; + strcat(topic_power, mqtt_root); + strcat(topic_power, "/power"); + + pinMode(SERIAL_COMMUNICATION_CONTROL_PIN, OUTPUT); + digitalWrite(SERIAL_COMMUNICATION_CONTROL_PIN, RS485_RX_PIN_VALUE); + RS485Serial.begin(4800); // set RS485 baud + + readConfig(); + + + ESPAsync_WMParameter custom_mqtt_server("server", "mqtt server", mqtt_server, 40); + ESPAsync_WMParameter custom_mqtt_port("port", "mqtt port", mqtt_port, 6); + + ESPAsync_WiFiManager wifiManager(&server, &dns); + wifiManager.setSaveConfigCallback(saveConfigCallback); + wifiManager.setConfigPortalTimeout(60); + wifiManager.addParameter(&custom_mqtt_server); + wifiManager.addParameter(&custom_mqtt_port); + configTime(MY_TZ, MY_NTP_SERVER); + + bool res = wifiManager.autoConnect(clientId); + + if(!res) { + DBG_PRINTLN("Failed to connect"); + ESP.restart(); + } else { + //if you get here you have connected to the WiFi + DBG_PRINT("WiFi connected to "); + DBG_PRINTLN(String(WiFi.SSID())); + DBG_PRINT("RSSI = "); + DBG_PRINT(String(WiFi.RSSI())); + DBG_PRINTLN(" dBm"); + DBG_PRINT("IP address "); + DBG_PRINTLN(WiFi.localIP()); + DBG_PRINTLN(); + + //read updated parameters + strcpy(mqtt_server, custom_mqtt_server.getValue()); + strcpy(mqtt_port, custom_mqtt_port.getValue()); + + //save the custom parameters to FS + if (shouldSaveConfig) { + saveConfig(); + } + + DBG_PRINTLN(String("mqttenabled: ") + checkbox_mqttenabled); + if(checkbox_mqttenabled){ + DBG_PRINTLN("set mqtt server!"); + DBG_PRINTLN(String("mqtt_server: ") + mqtt_server); + DBG_PRINTLN(String("mqtt_port: ") + mqtt_port); + + client.setServer(mqtt_server, atoi(mqtt_port)); + client.setCallback(mqtt_callback); + } + + // Handle Web Server Events + events.onConnect([](AsyncEventSourceClient *client){ + if(client->lastId()){ + DBG_PRINTLN(""); + //DEBUG_SERIAL.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId()); + sprintf(dbgbuffer,"Client reconnected! Last message ID that it got is: %u\n", client->lastId()); + DBG_PRINTLN(dbgbuffer); + //DEBUG_SERIAL.println(""); + } + client->send("hello!", NULL, millis(), 10000); + }); + + // Handle Web Server + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ + new_connect = true; + request->send_P(200, "text/html", index_html, processor); + }); + + // crate json and fetch data + server.on("/json", HTTP_GET, [] (AsyncWebServerRequest *request){ + JsonDocument myJson; + String message = ""; + + rssi = WiFi.RSSI(); + + myJson["WIFIRSSI"] = rssi; + myJson["CLIENTID"] = clientId; + myJson["METERNAME"] = metername; + myJson["MAXWATTINPUT"] = maxwatt; + myJson["TOUT"] = teiler_output; + myJson["NULLINTERVAL"] = nullinterval; + myJson["NULLOFFSET"] = nulloffset; + myJson["METERIP"] = meteripaddr; + myJson["METERINTERVAL"] = meterinterval; + myJson["TIMER1TIME"] = timer1_time; + myJson["TIMER1WATT"] = timer1_watt; + myJson["TIMER2TIME"] = timer2_time; + myJson["TIMER2WATT"] = timer2_watt; + myJson["MQTTROOT"] = mqtt_root; + myJson["MQTTSTATECL"] = mqtt_state; + + myJson["CBNULL"] = checkbox_nulleinspeisung; //checkbox + if(checkbox_nulleinspeisung){ // Stausanzeige + myJson["NULLSTATE"] = "EIN"; + }else{ + myJson["NULLSTATE"] = "AUS"; + } + + myJson["CBMQTTSTATE"] = checkbox_mqttenabled; //checkbox + if(checkbox_mqttenabled){ + myJson["MQTTSTATE"] = "EIN"; + }else{ + myJson["MQTTSTATE"] = "AUS"; + } + + myJson["CBTIMER1"] = checkbox_timer1; //checkbox + myJson["CBTIMER2"] = checkbox_timer2; //checkbox + if(checkbox_timer1 || checkbox_timer2){ + myJson["TIMERSTATE"] = "EIN"; + }else{ + myJson["TIMERSTATE"] = "AUS"; + } + + myJson["CBBATSCHUTZ"] = checkbox_batschutz; //checkbox + if(checkbox_batschutz){ + myJson["BATTSTATE"] = "EIN"; + }else{ + myJson["BATTSTATE"] = "AUS"; + } + + myJson["CBMETERL1"] = checkbox_meter_l1; //checkbox Shelly L1 + myJson["CBMETERL2"] = checkbox_meter_l2; //checkbox Shelly L2 + myJson["CBMETERL3"] = checkbox_meter_l3; //checkbox Shelly L3 + + myJson["MQTTSERVER"] = mqtt_server; + myJson["MQTTPORT"] = mqtt_port; + myJson["MQTTBATVOL"] = mqtt_topic_bat_voltage; + myJson["MQTTBATSOC"] = mqtt_topic_bat_soc; + + myJson["UPTIME"] = uptime_str; + myJson["SOYOPOWER"] = soyo_power; + myJson["METERNAME"] = metername; + myJson["METERPOWER"] = meterpower; + myJson["METERL1"] = meterl1; + myJson["METERL2"] = meterl2; + myJson["METERL3"] = meterl3; + myJson["MQTT_SUB_1"] = String(soyo_power) + " W"; + myJson["MQTT_BAT_SOC"] = String(mqtt_bat_soc, 1) + " %"; + myJson["MQTT_BAT_V"] = String(mqtt_bat_voltage, 1) + " V"; + myJson["BATSOCSTOP"] = batsocstop; + myJson["BATSOCSTART"] = batsocstart; + myJson["WIFIQUALITI"] = dBmtoPercent(rssi); + + + serializeJson(myJson, message); + + request->send(200, "application/json", message); + }); + + // start AP Mode + server.on("/apmode", HTTP_GET, [](AsyncWebServerRequest *request) { + ESPAsync_WiFiManager wifiManager(&server,&dns); + wifiManager.resetSettings(); + + ESP.restart(); + request->send_P(200, "text/html", index_html, processor); + }); + + // restart system + server.on("/restart", HTTP_GET, [](AsyncWebServerRequest *request) { + DBG_PRINTLN("/restart"); + ESP.restart(); + request->send_P(200, "text/html", index_html, processor); + }); + + server.on("/acoutput", HTTP_GET, [] (AsyncWebServerRequest *request) { + String parm1; + + if (request->hasParam("value") ) { + parm1 = request->getParam("value")->value(); + DBG_PRINT("/acoutput?value = "); + DBG_PRINTLN(parm1); + + if(parm1.equals("/s0") ){ + soyo_power = 0; + sprintf(msgData, "%d", soyo_power); + if(checkbox_mqttenabled){ + client.publish(topic_power, msgData); + } + } + else if(parm1.equals("/p1")){ + soyo_power +=1; + sprintf(msgData, "%d", soyo_power); + if(checkbox_mqttenabled){ + client.publish(topic_power, msgData); + } + } + else if(parm1.equals("/p10")){ + soyo_power +=10; + sprintf(msgData, "%d", soyo_power); + if(checkbox_mqttenabled){ + client.publish(topic_power, msgData); + } + } + else if(parm1.equals("/m1")){ + soyo_power -=1; + if(soyo_power < 0){ + soyo_power = 0; + } + sprintf(msgData, "%d", soyo_power); + if(checkbox_mqttenabled){ + client.publish(topic_power, msgData); + } + } + else if(parm1.equals("/m10")){ + soyo_power -=10; + if(soyo_power < 0){ + soyo_power = 0; + } + sprintf(msgData, "%d", soyo_power); + if(checkbox_mqttenabled){ + client.publish(topic_power, msgData); + } + } + } + request->send_P(200, "text/html", index_html, processor); + }); + + + server.on("/checkbox", HTTP_GET, [] (AsyncWebServerRequest *request) { + String checkbox_id; + String checkbox_value; + + if (request->hasParam("cbid") && request->hasParam("state")) { + checkbox_id = request->getParam("cbid")->value(); + checkbox_value = request->getParam("state")->value(); + + if(checkbox_id.equals("CBTIMER1")){ + if(checkbox_value.equals("1")){ + checkbox_timer1 = true; + } else { + checkbox_timer1 = false; + } + } + else if(checkbox_id.equals("CBTIMER2")){ + if(checkbox_value.equals("1")){ + checkbox_timer2 = true; + } else { + checkbox_timer2 = false; + } + } + else if(checkbox_id.equals("CBMQTTSTATE")){ + if(checkbox_value.equals("1")){ + checkbox_mqttenabled = true; + } else { + checkbox_mqttenabled = false; + } + } + else if(checkbox_id.equals("CBNULL")){ + if(checkbox_value.equals("1")){ + checkbox_nulleinspeisung = true; + } else { + checkbox_nulleinspeisung = false; + soyo_power = 0; + } + } + else if(checkbox_id.equals("CBBATSCHUTZ")){ + if(checkbox_value.equals("1")){ + checkbox_batschutz = true; + } else { + checkbox_batschutz = false; + output_enabled = true; //wenn batschutz aus, dann freigabe fuer soyo output + } + } + else if(checkbox_id.equals("CBMETERL1")){ + if(checkbox_value.equals("1")){ + checkbox_meter_l1 = true; + } else { + checkbox_meter_l1 = false; + } + } + else if(checkbox_id.equals("CBMETERL2")){ + if(checkbox_value.equals("1")){ + checkbox_meter_l2 = true; + } else { + checkbox_meter_l2 = false; + } + } + else if(checkbox_id.equals("CBMETERL3")){ + if(checkbox_value.equals("1")){ + checkbox_meter_l3 = true; + } else { + checkbox_meter_l3 = false; + } + } + } + request->send_P(200, "text/html", index_html, processor); + }); + + + server.on("/savesettings", HTTP_GET, [] (AsyncWebServerRequest *request) { + String value; + + value = request->getParam("t1")->value(); + memset(timer1_time, 0, sizeof(timer1_time)); + strcat(timer1_time, value.c_str()); + + value = request->getParam("w1")->value(); + timer1_watt = atoi(value.c_str()); + + value = request->getParam("t2")->value(); + memset(timer2_time, 0, sizeof(timer2_time)); + strcat(timer2_time, value.c_str()); + + value = request->getParam("w2")->value(); + timer2_watt = atoi(value.c_str()); + + value = request->getParam("maxwatt")->value(); + maxwatt = atoi(value.c_str()); + + value = request->getParam("meteripaddr")->value(); + memset(meteripaddr, 0, sizeof(meteripaddr)); + strcat(meteripaddr, value.c_str()); + + value = request->getParam("tout")->value(); + teiler_output = atoi(value.c_str()); + + value = request->getParam("meterinterval")->value(); + meterinterval = atol(value.c_str()); + + value = request->getParam("nullinterval")->value(); + nullinterval = atol(value.c_str()); + + value = request->getParam("nulloffset")->value(); + nulloffset = atoi(value.c_str()); + + value = request->getParam("mqttserver")->value(); + memset(mqtt_server, 0, sizeof(mqtt_server)); + strcat(mqtt_server, value.c_str()); + + value = request->getParam("mqttport")->value(); + memset(mqtt_port, 0, sizeof(mqtt_port)); + strcat(mqtt_port, value.c_str()); + + value = request->getParam("mqttbatvol")->value(); + memset(mqtt_topic_bat_voltage, 0, sizeof(mqtt_topic_bat_voltage)); + strcat(mqtt_topic_bat_voltage, value.c_str()); + + value = request->getParam("mqttbatsoc")->value(); + memset(mqtt_topic_bat_soc, 0, sizeof(mqtt_topic_bat_soc)); + strcat(mqtt_topic_bat_soc, value.c_str()); + + value = request->getParam("batsocstop")->value(); + batsocstop = atoi(value.c_str()); + + value = request->getParam("batsocstart")->value(); + batsocstart = atoi(value.c_str()); + + saveConfig(); + + meter_ip = String(meteripaddr); + + request->send_P(200, "text/html", index_html, processor); + }); + + AsyncElegantOTA.begin(&server); + server.onNotFound(notFound); + server.addHandler(&events); + server.begin(); + + rssi = WiFi.RSSI(); + + meter_model = getMeterType(); // get shelly typ, 3em / 3empro + + digitalWrite(SERIAL_COMMUNICATION_CONTROL_PIN, RS485_TX_PIN_VALUE); // RS485 Modul -> set board to transmit + } + + // end setup() +} + + +void loop() { + + if(checkbox_mqttenabled){ + if (!client.connected()) { + DBG_PRINTLN("lost mqtt connection -> start reconncect"); + reconnect(); + } + client.loop(); + } + + + // send current power to SoyoSource + if ((millis() - lastTimerSoyoSource) > timerSoyoSource) { + + if(checkbox_batschutz == true && output_enabled == false){ // wenn batterie soc < limit dann soyo_power = 0 + soyo_power = 0; + } + + new_soyo_power = soyo_power / teiler_output; // Last auf mehrere Soyo's aufteilen + if(new_soyo_power < 0){ + new_soyo_power = 0; + } + + //sendSoyoPowerData(soyo_power); + sendSoyoPowerData(new_soyo_power); + + if(new_soyo_power != old_soyo_power) { // nur für Debug, damit nur Laständerungen ausgegeben werden + old_soyo_power = new_soyo_power; + sprintf(dbgbuffer,"new soyo_power = %i ( %02X %02X %02X %02X %02X %02X %02X %02X )",new_soyo_power, soyo_power_data[0],soyo_power_data[1],soyo_power_data[2],soyo_power_data[3],soyo_power_data[4],soyo_power_data[5],soyo_power_data[6],soyo_power_data[7]); + DBG_PRINTLN(dbgbuffer); + } + + if(checkbox_mqttenabled){ + sprintf(msgData, "%d", soyo_power); + client.publish(topic_power, msgData); + } + + lastTimerSoyoSource = millis(); + } + + + // timer to get Shelly3EM data + if ((millis() - lastMeterinterval) > meterinterval) { + if (meter_model > 0){ + meter_power = getMeterData(meter_model); + } else{ + meter_model = getMeterType(); + DBG_PRINTLN("Kein Shelly erkannt! Bitte IP eintragen, speichern und ESP neu starten."); + } + + lastMeterinterval = millis(); + } + + + // timer to manage Nulleinspeisung + if ((millis() - lastNullinterval) > nullinterval) { + if(checkbox_nulleinspeisung && output_enabled){ + if(meter_power > nulloffset + 10){ + soyo_power += meter_power - nulloffset; + + if(soyo_power > maxwatt){ + soyo_power = maxwatt; + } + } + + if(meter_power < 0 + nulloffset ){ + soyo_power += meter_power - nulloffset; + + if(soyo_power < 0){ + soyo_power = 0; + } + } + + } + lastNullinterval = millis(); + } + + + // timer für uptime, SoyoSource Timer und BatSOCLimit + if ((millis() - lastTimerUptime) > timerUptime) { + myUptime(); + + if(checkbox_timer1 || checkbox_timer2){ + checkTimer(); + } + + // check ob Batterie SOC < oder > eingestelltem Limit + float mqttbatsoc_float = mqtt_bat_soc + 0.5; + int mqttbatsoc_int = (int)mqttbatsoc_float; + + if(checkbox_batschutz == true && mqttbatsoc_int > 1){ // falls mqtt noch nicht verbunden oder nicht aktiv + if(mqttbatsoc_int <= batsocstop){ + output_enabled = false; + }else if(mqttbatsoc_int >= batsocstart){ + output_enabled = true; + } + } + + lastTimerUptime = millis(); + } + + +} From 15e6583760b63a9cd85c7705f937b26dfc5d2e6a Mon Sep 17 00:00:00 2001 From: krulkip Date: Sun, 19 May 2024 22:58:07 +0200 Subject: [PATCH 2/8] Add files via upload --- SoyoSource.jpg | Bin 0 -> 75198 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 SoyoSource.jpg diff --git a/SoyoSource.jpg b/SoyoSource.jpg new file mode 100644 index 0000000000000000000000000000000000000000..479c7e008ed8154b3b6792379b7eb379702fbb15 GIT binary patch literal 75198 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 E0FH%8fB*mh literal 0 HcmV?d00001 From 7713b02696dd10506335c16e505a2bb71ac888a0 Mon Sep 17 00:00:00 2001 From: krulkip Date: Sun, 19 May 2024 23:01:08 +0200 Subject: [PATCH 3/8] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index da64514..f9ae98f 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,12 @@ Hinweis: Das RS485 Entwicklungsboard verwendet einen MAX485 Pegelwandler der fü Hier muss 'Bat AutoLimit Grid' auf Y stehen - + ## Webif +### Bild 3: Addition of Homewizard + + From 3927d91bb126cc9170c88c9724fa404aad4b6654 Mon Sep 17 00:00:00 2001 From: krulkip Date: Mon, 20 May 2024 07:16:53 +0200 Subject: [PATCH 4/8] Add files via upload --- src/soyosource-powercontroller_V0014.ino | 1373 ++++++++++++++++++++++ 1 file changed, 1373 insertions(+) create mode 100644 src/soyosource-powercontroller_V0014.ino diff --git a/src/soyosource-powercontroller_V0014.ino b/src/soyosource-powercontroller_V0014.ino new file mode 100644 index 0000000..f919f7f --- /dev/null +++ b/src/soyosource-powercontroller_V0014.ino @@ -0,0 +1,1373 @@ +/*************************************************************************** + soyosource-powercontroller @matlen67 + + Version: 1.240508BK + + 16.03.2024 -> Speichern der Checkboxzustände: aktiv Timer1 / Timer2 + 03.04.2024 -> Statusübersicht bei geschlossenen details/summary boxen + 14.04.2024 -> Falls Batterieschutz aktiviert, deaktiviere Regelung der Nulleinspeisung + 25.04.2024 -> Leistungspunkt bei Nulleinspeisung festlegen + (Bei mir funktioniert gut Intervall Shelly 1000ms & Intervall Nulleinspeisung 4000ms) + 26.04.2024 -> Auswahl der aktiven Leiter (L1, L2, L3) beim Shelly + 27.04.2024 -> Fehlerbehebung Shelly 3EM, Shelly Plus 1PM mit zugefügt + 28.04.2024 -> Teiler unter 'SoyoSource Output' hinzugefügt, um die Leistung auf mehere Geräte aufzuteilen + 29.04.2024 -> Telnet entfernt + 05.05.2024 -> update ArduinoJson to 7.0.4 + 08.05.2024 -> mqtt topic voltage & soc bearbeitbar + + + ************************* + Wiring + NodeMCU D1 - RS485 RO + NodeMCU D3 - RS485 DE/RE + NodeMCU D4 - RS485 DI + +****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "html.h" + +#define DEBUG_SERIAL Serial + +#define DEBUG + +#ifdef DEBUG + #define DBG_PRINT(x) DEBUG_SERIAL.print(x) + #define DBG_PRINTLN(x) DEBUG_SERIAL.println(x) +#else + #define DBG_PRINT(x) + #define DBG_PRINTLN(x) +#endif + +//***************************************************************************** +// da Serial.printf(x,x) mit define nicht funktioniert als workaround sprintf +// sprintf(dbgbuffer,"ESP_%02X%02X%02X", mac[3], mac[4], mac[5]); +// DBG_PRINTLN(dbgbuffer); +//***************************************************************************** +char dbgbuffer[128]; +#define D1 5 +#define D3 0 +#define D4 2 + +#define RXPin D1 // Serial Receive pin (D1) +#define TXPin D4 // Serial Transmit pin (D4) + +//RS485 control +#define SERIAL_COMMUNICATION_CONTROL_PIN D3 // Transmission set pin (D3) +#define RS485_TX_PIN_VALUE HIGH +#define RS485_RX_PIN_VALUE LOW + +// time server +#define MY_NTP_SERVER "de.pool.ntp.org" +#define MY_TZ "CET-1CEST,M3.5.0/2,M10.5.0/3" + +//IMPORTANT: Uncomment this line if you want to enable SHELLY: +#define ENABLE_HOMEWIZARD +#ifndef ENABLE_HOMEWIZARD +#define SHELLY +#endif + +SoftwareSerial RS485Serial(RXPin, TXPin); // RX, TX +WiFiClient espClient; +PubSubClient client(espClient); +AsyncWebServer server(80); +AsyncEventSource events("/events"); +AsyncDNSServer dns; + + +// Uptime Global Variables +Uptime uptime; +uint8_t Uptime_Years = 0U, Uptime_Months = 0U, Uptime_Days = 0U, Uptime_Hours = 0U, Uptime_Minutes = 0U, Uptime_Seconds = 0U; +uint16_t Uptime_TotalDays = 0U; // Total Uptime Days +char uptime_str[37]; + +// Wifi to percent +const int RSSI_MAX =-50; // max strength signal in dBm +const int RSSI_MIN =-100; // min strength signal in dBm + +//Timer +unsigned long timerSoyoSource = 555; +unsigned long lastTimerSoyoSource = 0; + +unsigned long timerUptime = 1000; +unsigned long lastTimerUptime = 0; + +unsigned long meterinterval = 2000; +unsigned long lastMeterinterval = 0; + +unsigned long nullinterval = 5000; +unsigned long lastNullinterval = 0; + + + +//mqtt +char mqtt_server[16] = "192.168.178.30"; +char mqtt_port[5] = "1889"; +char msgData[64]; +String msg = ""; +char mqtt_topic_bat_voltage [48] = "VenusOS/SmartShunt/voltage"; +char mqtt_topic_bat_soc [48] = "VenusOS/SmartShunt/soc"; + +String dataReceived; +int data; +bool isDataReceived = false; +uint8_t byte0, byte1, byte2, byte3, byte4, byte5, byte6, byte7; +int byteSend; +int data_array[8]; +int soyo_hello_data[8] = {0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // bit7 org 0x00, CRC 0xFF +int soyo_power_data[8] = {0x24, 0x56, 0x00, 0x21, 0x00, 0x00, 0x80, 0x08}; // 0 Watt +int soyo_text_data[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +char buffer[8]; +int old_soyo_power = 0; +int soyo_power = 0; +int new_soyo_power = 0; +int teiler_output = 1; + +unsigned char mac[6]; +char mqtt_root[32] = "SoyoSource/"; +char clientId[16]; +char topic_power[40]; +char soyo_text[40]; + +float mqtt_bat_soc = 0.0; +float mqtt_bat_voltage = 0.0; + +long rssi; + +time_t now; +tm timeInfo; + + +// timer +char currentTime[20]; +char timer1_time[6] = "06:00"; +char timer2_time[6] = "20:00"; +char meteripaddr[16] = ""; + +int timer1_watt = 0; +int timer2_watt = 0; +int maxwatt = 0; + +//state checkboxes +bool checkbox_timer1 = false; +bool checkbox_timer2 = false; +bool checkbox_mqttenabled = false; +bool checkbox_nulleinspeisung = false; +bool checkbox_batschutz = false; +bool checkbox_meter_l1 = true; +bool checkbox_meter_l2 = true; +bool checkbox_meter_l3 = true; + +char metername[24] = "Meter"; +char mqtt_state[20] = "disabled"; + +// variablen Shelly 3em +const int shelly_3em_pro = 1; // ip/rpc/Shelly.GetStatus +const int shelly_plus_1pm = 2; // ip/rpc/Shelly.GetStatus + +const int shelly_3em = 10; // ip/status +const int shelly_em = 11; // ip/status +const int shelly_1pm = 12; // ip/status + +const int homewizard = 20; // ip/api/v1/data + +String meter_ip = ""; +int meter_model = 0 ; + +//nulleinspeisung +int nulloffset = 0; +int meter_power = 0; +int meterpower = 0; +int meterl1 = 0; +int meterl2 = 0; +int meterl3 = 0; + +//batterieüberwachung +int batsocstop = 15; +int batsocstart = 50; +bool output_enabled = true; + + +bool new_connect = true; + +const char* PARAM_MESSAGE = "message"; + +//flag for saving data +bool shouldSaveConfig = false; + + +//callback notifying us of the need to save config +void saveConfigCallback () { + DBG_PRINTLN("Should save config"); + shouldSaveConfig = true; +} + + +void notFound(AsyncWebServerRequest *request) { + request->send(404, "text/plain", "Not found"); +} + + +int dBmtoPercent(int dBm){ + int percent; + if(dBm <= RSSI_MIN){ + percent = 0; + } else if(dBm >= RSSI_MAX) { + percent = 100; + } else { + percent = 2 * (dBm + 100); + } + + return percent; +} + + +void myUptime(){ + uptime.calculateUptime(); + + // Get The Uptime Values To Global Variables + Uptime_Years = uptime.getYears(); + Uptime_Months = uptime.getMonths(); + Uptime_Days = uptime.getDays(); + Uptime_Hours = uptime.getHours(); + Uptime_Minutes = uptime.getMinutes(); + Uptime_Seconds = uptime.getSeconds(); + Uptime_TotalDays = uptime.getTotalDays(); + + if (Uptime_Years == 0U) { // Uptime Is Less Than One Year + // First 60 Seconds + if (Uptime_Minutes == 0U && Uptime_Hours == 0U && Uptime_Days == 0U && Uptime_Months == 0U) + sprintf(uptime_str, "00:00:%02i", Uptime_Seconds); + // First Minute + else if (Uptime_Minutes == 1U && Uptime_Hours == 0U && Uptime_Days == 0U && Uptime_Months == 0U) + sprintf(uptime_str, "00:%02i:%02i", Uptime_Minutes, Uptime_Seconds); + // Second Minute And More But Less Than Hours, Days, Months + else if (Uptime_Minutes >= 2U && Uptime_Hours == 0U && Uptime_Days == 0U && Uptime_Months == 0U) + sprintf(uptime_str, "00:%02i:%02i", Uptime_Minutes, Uptime_Seconds); + // First Hour And More But Less Than Days, Months + else if (Uptime_Hours >= 1U && Uptime_Days == 0U && Uptime_Months == 0U) + sprintf(uptime_str, "%02i:%02i:%02i", Uptime_Hours, Uptime_Minutes, Uptime_Seconds); + // First Day And Less Than Month + else if (Uptime_Days == 1U && Uptime_Months == 0U) + sprintf(uptime_str, "%iday %02i:%02i:%02i", Uptime_Days, Uptime_Hours, Uptime_Minutes, Uptime_Seconds); + // Second Day And More But Less Than Month + else if (Uptime_Days >= 2U && Uptime_Months == 0U) + sprintf(uptime_str, "%idays %02i:%02i:%02i", Uptime_Days, Uptime_Hours, Uptime_Minutes, Uptime_Seconds); + // First Month And More But Less Than One Year + else if (Uptime_Months >= 1U) + sprintf(uptime_str, "%im, %id %02i:%02i", Uptime_Months, Uptime_Days, Uptime_Hours, Uptime_Minutes); + // If There Is Any Error In This If Loop Then Make Full String. + else sprintf(uptime_str, "%iy %im %id %02i:%02i", Uptime_Years, Uptime_Months, Uptime_Days, Uptime_Hours, Uptime_Minutes); + } else // Uptime Is More Than One Year + sprintf(uptime_str, "%iy %im %id %02i:%02i", Uptime_Years, Uptime_Months, Uptime_Days, Uptime_Hours, Uptime_Minutes); +} + + +//callback from mqtt +void mqtt_callback(char* topic, byte* payload, unsigned int length) { + unsigned int i = 0; + + for (i=0;i= 0 && arrived_value_i <= 3000) { + soyo_power = arrived_value_i; + } + } + + if(strcmp(topic, mqtt_topic_bat_soc) == 0){ + float arrived_value_f = atof(buffer); + mqtt_bat_soc = arrived_value_f; + } + + if(strcmp(topic, mqtt_topic_bat_voltage) == 0){ + float arrived_value_f = atof(buffer); + mqtt_bat_voltage = arrived_value_f; + } +} + + +String processor(const String& var){ + return String(); +} + + +void reconnect() { + DBG_PRINTLN("reconnect MQTT connection!"); + + //set callback again + client.setCallback(mqtt_callback); + + uint8_t timeout = 15; + + // wait for connection + while (!client.connected()){ + + DBG_PRINTLN(""); + + if (client.connect(clientId)) { + DBG_PRINTLN("connection established"); + + client.publish(topic_power, "0"); + client.subscribe(topic_power); + client.subscribe(mqtt_topic_bat_soc); + client.subscribe(mqtt_topic_bat_voltage); + + strcpy(mqtt_state, "connect"); + + DBG_PRINT("subscrible: "); + DBG_PRINT(topic_power); + DBG_PRINTLN(""); + + DBG_PRINT("subscrible: "); + DBG_PRINT(mqtt_topic_bat_soc); + DBG_PRINTLN(""); + + DBG_PRINT("subscrible: "); + DBG_PRINT(mqtt_topic_bat_voltage); + DBG_PRINTLN(""); + + } else { + DBG_PRINTLN("reconnect failed! "); + strcpy(mqtt_state, "connect error"); + + while (timeout){ + DBG_PRINT("."); + timeout--; + delay(1000); + } + } + } + +} + + +int calc_checksumme(int b1, int b2, int b3, int b4, int b5, int b6 ){ + int calc = (0xFF - b1 - b2 - b3 - b4 - b5 - b6) % 256; + return calc & 0xFF; +} + + +void sendSoyoPowerData(int power){ + soyo_power_data[0] = 0x24; + soyo_power_data[1] = 0x56; + soyo_power_data[2] = 0x00; + soyo_power_data[3] = 0x21; + soyo_power_data[4] = power >> 0x08; + soyo_power_data[5] = power & 0xFF; + soyo_power_data[6] = 0x80; + soyo_power_data[7] = calc_checksumme(soyo_power_data[1], soyo_power_data[2], soyo_power_data[3], soyo_power_data[4], soyo_power_data[5], soyo_power_data[6]); + + for(int i=0; i<8; i++) { + RS485Serial.write(soyo_power_data[i]); // send data to RS485 + //DBG_PRINTLN(soyo_power_data[i], HEX); + } +} + +//read config.json +void readConfig(){ + //read configuration from json + DBG_PRINTLN("mounting FS..."); + + if (LittleFS.begin()) { + DBG_PRINTLN("mounted file system"); + if (LittleFS.exists("/config.json")) { + //file exists, reading and loading + DBG_PRINTLN("reading config file"); + File configFile = LittleFS.open("/config.json", "r"); + if (configFile) { + DBG_PRINTLN("opened config file"); + size_t size = configFile.size(); + // Allocate a buffer to store contents of the file. + std::unique_ptr buf(new char[size]); + + configFile.readBytes(buf.get(), size); + + JsonDocument json; + auto deserializeError = deserializeJson(json, buf.get()); + serializeJson(json, Serial); + if (!deserializeError) { + DBG_PRINTLN("\nparsed json"); + strcpy(mqtt_server, json["mqtt_server"]); + strcpy(mqtt_port, json["mqtt_port"]); + + if(json.containsKey("mqtt_bat_vol")){ + strcpy(mqtt_topic_bat_voltage, json["mqtt_bat_vol"]); + } + + if(json.containsKey("mqtt_bat_soc")){ + strcpy(mqtt_topic_bat_soc, json["mqtt_bat_soc"]); + } + + char key_value[2]; + + if(json.containsKey("mqtt_on")){ + strcpy(key_value, json["mqtt_on"]); + if(strcmp(key_value, "1") == 0){ + checkbox_mqttenabled = true; + }else{ + checkbox_mqttenabled = false; + } + } + + if(json.containsKey("zft_on")){ + strcpy(key_value, json["zft_on"]); + + if(strcmp(key_value, "1") == 0){ + checkbox_nulleinspeisung = true; + }else{ + checkbox_nulleinspeisung = false; + } + } + + if(json.containsKey("batp_on")){ + strcpy(key_value, json["batp_on"]); + + if(strcmp(key_value, "1") == 0){ + checkbox_batschutz = true; + }else{ + checkbox_batschutz = false; + } + } + + if(json.containsKey("t1_on")){ + strcpy(key_value, json["t1_on"]); + + if(strcmp(key_value, "1") == 0){ + checkbox_timer1 = true; + }else{ + checkbox_timer1 = false; + } + } + + if(json.containsKey("t2_on")){ + strcpy(key_value, json["t2_on"]); + + if(strcmp(key_value, "1") == 0){ + checkbox_timer2 = true; + }else{ + checkbox_timer2 = false; + } + } + + if(json.containsKey("mtr_l1_on")){ + strcpy(key_value, json["mtr_l1_on"]); + + if(strcmp(key_value, "1") == 0){ + checkbox_meter_l1 = true; + }else{ + checkbox_meter_l1 = false; + } + } + + if(json.containsKey("mtr_l2_on")){ + strcpy(key_value, json["mtr_l2_on"]); + + if(strcmp(key_value, "1") == 0){ + checkbox_meter_l2 = true; + }else{ + checkbox_meter_l2 = false; + } + } + + if(json.containsKey("mtr_l3_on")){ + strcpy(key_value, json["mtr_l3_on"]); + + if(strcmp(key_value, "1") == 0){ + checkbox_meter_l3 = true; + }else{ + checkbox_meter_l3 = false; + } + } + + + if(json.containsKey("t1_t")){ + strcpy(timer1_time, json["t1_t"]); + } + + if(json.containsKey("t2_t")){ + strcpy(timer2_time, json["t2_t"]); + } + + if(json.containsKey("t1_p")){ + timer1_watt = json["t1_p"]; + } + + if(json.containsKey("t2_p")){ + timer2_watt = json["t2_p"]; + } + + if(json.containsKey("mp")){ + maxwatt = json["mp"]; + } + + if(json.containsKey("mtr_ip")){ + strcpy(meteripaddr, json["mtr_ip"]); + meter_ip = String(meteripaddr); + } + + if(json.containsKey("mtr_iv")){ + meterinterval = json["mtr_iv"]; + } + + if(json.containsKey("z_iv")){ + nullinterval = json["z_iv"]; + } + + if(json.containsKey("z_ofs")){ + nulloffset = json["z_ofs"]; + } + + if(json.containsKey("soc_stop")){ + batsocstop = json["soc_stop"]; + } + + if(json.containsKey("soc_start")){ + batsocstart = json["soc_start"]; + } + + if(json.containsKey("tout")){ + teiler_output = json["tout"]; + } + + } else { + DBG_PRINTLN("failed to load json config"); + } + } + } + } else { + DBG_PRINTLN("failed to mount FS"); + } + //end read config data +} + + +// write config.json +void saveConfig(){ + DBG_PRINTLN(F("save data to config.json")); + JsonDocument json; + + json["mqtt_server"] = mqtt_server; + json["mqtt_port"] = mqtt_port; + json["mqtt_bat_vol"] = mqtt_topic_bat_voltage; + json["mqtt_bat_soc"] = mqtt_topic_bat_soc; + + + if(checkbox_mqttenabled){ + json["mqtt_on"] = "1"; + }else{ + json["mqtt_on"] = "0"; + } + + if(checkbox_nulleinspeisung){ + json["zft_on"] = "1"; + }else{ + json["zft_on"] = "0"; + } + + if(checkbox_batschutz){ + json["batp_on"] = "1"; + }else{ + json["batp_on"] = "0"; + } + + if(checkbox_timer1){ + json["t1_on"] = "1"; + }else{ + json["t1_on"] = "0"; + } + + if(checkbox_timer2){ + json["t2_on"] = "1"; + }else{ + json["t2_on"] = "0"; + } + + if(checkbox_meter_l1){ + json["mtr_l1_on"] = "1"; + }else{ + json["mtr_l1_on"] = "0"; + } + + if(checkbox_meter_l2){ + json["mtr_l2_on"] = "1"; + }else{ + json["mtr_l2_on"] = "0"; + } + + if(checkbox_meter_l3){ + json["mtr_l3_on"] = "1"; + }else{ + json["mtr_l3_on"] = "0"; + } + + json["t1_t"] = timer1_time; + json["t1_p"] = timer1_watt; + json["t2_t"] = timer2_time; + json["t2_p"] = timer2_watt; + json["mp"] = maxwatt; + json["mtr_ip"] = meteripaddr; + json["mtr_iv"] = meterinterval; + json["z_iv"] = nullinterval; + json["z_ofs"] = nulloffset; + json["soc_stop"] = batsocstop; + json["soc_start"] = batsocstart; + json["tout"] = teiler_output; + + File configFile = LittleFS.open("/config.json", "w"); + if (!configFile) { + DBG_PRINTLN("failed to open config file for writing"); + return; + } + + serializeJson(json, configFile); + configFile.close(); + + serializeJson(json, Serial); + DBG_PRINTLN(); +} + + +// get meter type(3EM PRO, 3EM, EM, 1PM, Plus 1PM, Homewizard) +int getMeterType(){ +#ifdef ENABLE_SHELLY + String meter_url = "http://" + meter_ip + "/shelly"; +#endif +#ifdef ENABLE_HOMEWIZARD + String meter_url = "http://" + meter_ip + "/api"; +#endif + int type = 0; + + memset(metername, 0, sizeof(metername)); + strcat(metername, "no device"); + + JsonDocument doc; + + WiFiClient client_meter; + HTTPClient http; + + if (http.begin(client_meter, meter_url)) { + int httpCode = http.GET(); + + if (httpCode > 0) { + if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { + String payload = http.getString(); + + DeserializationError error = deserializeJson(doc, payload); + + if (error) { + DBG_PRINT(F("deserializeJson() failed: ")); + DBG_PRINTLN(error.f_str()); + } + + String json_type = doc["type"]; + String json_model = doc["model"]; + String json_product_name = doc["product_name"]; + if(json_type != NULL){ + + //test auf Shelly 1PM + if(json_type.equals("SHSW-PM")){ + type = shelly_1pm; + memset(metername, 0, sizeof(metername)); + strcat(metername, "Shelly 1PM"); + } + //test auf Shelly EM + if(json_type.equals("SHEM")){ + type = shelly_em; + memset(metername, 0, sizeof(metername)); + strcat(metername, "Shelly EM"); + } + + //test auf Shelly 3EM + if(json_type.equals("SHEM-3")){ + type = shelly_3em; + memset(metername, 0, sizeof(metername)); + strcat(metername, "Shelly 3EM"); + } + } + + + if(json_model != NULL){ + + //test auf Shelly 3EM Pro + if(json_model.equals("SPEM-003CEBEU")) { + type = shelly_3em_pro; + memset(metername, 0, sizeof(metername)); + strcat(metername, "Shelly 3EM Pro"); + } + + //test auf Shelly Plus 1PM + if(json_model.equals("SNSW-001P16EU")) { + type = shelly_plus_1pm; + memset(metername, 0, sizeof(metername)); + strcat(metername, "Shelly Plus 1PM"); + } + } + + if(json_product_name != NULL){ + //test auf Homewizard + if(json_product_name.equals("P1 meter")) { + type = homewizard; + DBG_PRINTLN(type); + memset(metername, 0, sizeof(metername)); + strcat(metername, "Homewizard"); + } + } + } + } + http.end(); + } + DBG_PRINT("getMeterType() = "); + DBG_PRINTLN(String(metername)); + + return type; +} + + +// read meter +int getMeterData(int type) { + String meter_url; + int power = 0; + int power1 = 0; + int power2 = 0; + int power3 = 0; + + JsonDocument doc; + WiFiClient client_meter; + HTTPClient http; + + if (type > 0 && type < 10) { + meter_url = "http://" + meter_ip + "/rpc/Shelly.GetStatus"; // Shelly PRO 3EM + } else if(type >= 10 && type < 15) { + meter_url = "http://" + meter_ip + "/status"; // Shelly 3EM und Andere + } else if (type >=16) { + meter_url = "http://" + meter_ip + "/api/v1/data"; // Homewizard + } else { + return 0; + } + + if (http.begin(client_meter, meter_url)) { + int httpCode = http.GET(); + if (httpCode > 0) { + if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { + String payload = http.getString(); + DeserializationError error = deserializeJson(doc, payload); + + if (error) { + DBG_PRINT(F("deserializeJson() failed: ")); + DBG_PRINTLN(error.f_str()); + } + + + if (type == shelly_3em_pro) { + power1 = doc["em:0"]["a_act_power"]; + power2 = doc["em:0"]["b_act_power"]; + power3 = doc["em:0"]["c_act_power"]; + } else if (type == shelly_3em) { + power1 = doc["emeters"][0]["power"]; + power2 = doc["emeters"][1]["power"]; + power3 = doc["emeters"][2]["power"]; + } else if (type == shelly_em) { + power1 = doc["meters"][0]["power"]; + power2 = doc["meters"][1]["power"]; + power3 = 0; + } else if (type == shelly_1pm) { + power1 = doc["meters"][0]["power"]; + power2 = 0; + power3 = 0; + } else if (type == shelly_plus_1pm) { + power1 = doc["switch:0"]["apower"]; + power2 = 0; + power3 = 0; + } else if (type == homewizard) { + power1 = doc["active_power_l1_w"]; + power2 = doc["active_power_l2_w"]; + power3 = doc["active_power_l3_w"]; + } + + + if(!checkbox_meter_l1){ + power1 = 0; + } + + if(!checkbox_meter_l2){ + power2 = 0; + } + + if(!checkbox_meter_l3){ + power3 = 0; + } + + power = power1 + power2 + power3; + + meterpower = power; + meterl1 = power1; + meterl2 = power2; + meterl3 = power3; + } + + } else { + sprintf(dbgbuffer,"[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str()); + DBG_PRINTLN(dbgbuffer); + meter_model = 0; + } + + http.end(); + } else { + DBG_PRINTLN("[HTTP] Unable to connect\n"); + meter_model = 0; + } + + return power; +} + + +void checkTimer(){ + + time(&now); + localtime_r(&now, &timeInfo); + + if (checkbox_timer1 == true){ + int t1_hour = String(timer1_time).substring(0,2).toInt(); + int t1_min = String(timer1_time).substring(3).toInt(); + + if((timeInfo.tm_hour == t1_hour && timeInfo.tm_min == t1_min && timeInfo.tm_sec == 0) || (timeInfo.tm_hour == t1_hour && timeInfo.tm_min == t1_min && timeInfo.tm_sec == 1) ){ + soyo_power = timer1_watt; + } + } + + if (checkbox_timer2 == true){ + int t2_hour = String(timer2_time).substring(0,2).toInt(); + int t2_min = String(timer2_time).substring(3).toInt(); + + if((timeInfo.tm_hour == t2_hour && timeInfo.tm_min == t2_min && timeInfo.tm_sec == 0) || (timeInfo.tm_hour == t2_hour && timeInfo.tm_min == t2_min && timeInfo.tm_sec == 1)){ + soyo_power = timer2_watt; + } + } + +} + + +//#################### SETUP ####################### +void setup() { + + DEBUG_SERIAL.begin(115200); + delay(500); + + DBG_PRINTLN(""); + DBG_PRINT(F("CPU Frequency = ")); + DBG_PRINT(F_CPU / 1000000); + DBG_PRINTLN(F(" MHz")); + + WiFi.macAddress(mac); + WiFi.persistent(true); // sonst verliert er nach einem Neustart die IP !!! + + sprintf(dbgbuffer,"ESP_%02X%02X%02X", mac[3], mac[4], mac[5]); + DBG_PRINTLN(dbgbuffer); + + //configTime(MY_TZ, MY_NTP_SERVER); + + sprintf(clientId, "soyo_%02x%02x%02x", mac[3], mac[4], mac[5] ); + + //mqtt_root = "SoyoSource/soyo_xxxxxx"; + strcat(mqtt_root, clientId); + + //topic_power = "SoyoSource/soyo_xxxxxx/power"; + strcat(topic_power, mqtt_root); + strcat(topic_power, "/power"); + + pinMode(SERIAL_COMMUNICATION_CONTROL_PIN, OUTPUT); + digitalWrite(SERIAL_COMMUNICATION_CONTROL_PIN, RS485_RX_PIN_VALUE); + RS485Serial.begin(4800); // set RS485 baud + + readConfig(); + + + ESPAsync_WMParameter custom_mqtt_server("server", "mqtt server", mqtt_server, 40); + ESPAsync_WMParameter custom_mqtt_port("port", "mqtt port", mqtt_port, 6); + + ESPAsync_WiFiManager wifiManager(&server, &dns); + wifiManager.setSaveConfigCallback(saveConfigCallback); + wifiManager.setConfigPortalTimeout(60); + wifiManager.addParameter(&custom_mqtt_server); + wifiManager.addParameter(&custom_mqtt_port); + configTime(MY_TZ, MY_NTP_SERVER); + + bool res = wifiManager.autoConnect(clientId); + + if(!res) { + DBG_PRINTLN("Failed to connect"); + ESP.restart(); + } else { + //if you get here you have connected to the WiFi + DBG_PRINT("WiFi connected to "); + DBG_PRINTLN(String(WiFi.SSID())); + DBG_PRINT("RSSI = "); + DBG_PRINT(String(WiFi.RSSI())); + DBG_PRINTLN(" dBm"); + DBG_PRINT("IP address "); + DBG_PRINTLN(WiFi.localIP()); + DBG_PRINTLN(); + + //read updated parameters + strcpy(mqtt_server, custom_mqtt_server.getValue()); + strcpy(mqtt_port, custom_mqtt_port.getValue()); + + //save the custom parameters to FS + if (shouldSaveConfig) { + saveConfig(); + } + + DBG_PRINTLN(String("mqttenabled: ") + checkbox_mqttenabled); + if(checkbox_mqttenabled){ + DBG_PRINTLN("set mqtt server!"); + DBG_PRINTLN(String("mqtt_server: ") + mqtt_server); + DBG_PRINTLN(String("mqtt_port: ") + mqtt_port); + + client.setServer(mqtt_server, atoi(mqtt_port)); + client.setCallback(mqtt_callback); + } + + // Handle Web Server Events + events.onConnect([](AsyncEventSourceClient *client){ + if(client->lastId()){ + DBG_PRINTLN(""); + //DEBUG_SERIAL.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId()); + sprintf(dbgbuffer,"Client reconnected! Last message ID that it got is: %u\n", client->lastId()); + DBG_PRINTLN(dbgbuffer); + //DEBUG_SERIAL.println(""); + } + client->send("hello!", NULL, millis(), 10000); + }); + + // Handle Web Server + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ + new_connect = true; + request->send_P(200, "text/html", index_html, processor); + }); + + // crate json and fetch data + server.on("/json", HTTP_GET, [] (AsyncWebServerRequest *request){ + JsonDocument myJson; + String message = ""; + + rssi = WiFi.RSSI(); + + myJson["WIFIRSSI"] = rssi; + myJson["CLIENTID"] = clientId; + myJson["METERNAME"] = metername; + myJson["MAXWATTINPUT"] = maxwatt; + myJson["TOUT"] = teiler_output; + myJson["NULLINTERVAL"] = nullinterval; + myJson["NULLOFFSET"] = nulloffset; + myJson["METERIP"] = meteripaddr; + myJson["METERINTERVAL"] = meterinterval; + myJson["TIMER1TIME"] = timer1_time; + myJson["TIMER1WATT"] = timer1_watt; + myJson["TIMER2TIME"] = timer2_time; + myJson["TIMER2WATT"] = timer2_watt; + myJson["MQTTROOT"] = mqtt_root; + myJson["MQTTSTATECL"] = mqtt_state; + + myJson["CBNULL"] = checkbox_nulleinspeisung; //checkbox + if(checkbox_nulleinspeisung){ // Stausanzeige + myJson["NULLSTATE"] = "EIN"; + }else{ + myJson["NULLSTATE"] = "OFF"; + } + + myJson["CBMQTTSTATE"] = checkbox_mqttenabled; //checkbox + if(checkbox_mqttenabled){ + myJson["MQTTSTATE"] = "EIN"; + }else{ + myJson["MQTTSTATE"] = "AUS"; + } + + myJson["CBTIMER1"] = checkbox_timer1; //checkbox + myJson["CBTIMER2"] = checkbox_timer2; //checkbox + if(checkbox_timer1 || checkbox_timer2){ + myJson["TIMERSTATE"] = "EIN"; + }else{ + myJson["TIMERSTATE"] = "AUS"; + } + + myJson["CBBATSCHUTZ"] = checkbox_batschutz; //checkbox + if(checkbox_batschutz){ + myJson["BATTSTATE"] = "EIN"; + }else{ + myJson["BATTSTATE"] = "AUS"; + } + + myJson["CBMETERL1"] = checkbox_meter_l1; //checkbox Shelly L1 + myJson["CBMETERL2"] = checkbox_meter_l2; //checkbox Shelly L2 + myJson["CBMETERL3"] = checkbox_meter_l3; //checkbox Shelly L3 + + myJson["MQTTSERVER"] = mqtt_server; + myJson["MQTTPORT"] = mqtt_port; + myJson["MQTTBATVOL"] = mqtt_topic_bat_voltage; + myJson["MQTTBATSOC"] = mqtt_topic_bat_soc; + + myJson["UPTIME"] = uptime_str; + myJson["SOYOPOWER"] = soyo_power; + myJson["METERNAME"] = metername; + myJson["METERPOWER"] = meterpower; + myJson["METERL1"] = meterl1; + myJson["METERL2"] = meterl2; + myJson["METERL3"] = meterl3; + myJson["MQTT_SUB_1"] = String(soyo_power) + " W"; + myJson["MQTT_BAT_SOC"] = String(mqtt_bat_soc, 1) + " %"; + myJson["MQTT_BAT_V"] = String(mqtt_bat_voltage, 1) + " V"; + myJson["BATSOCSTOP"] = batsocstop; + myJson["BATSOCSTART"] = batsocstart; + myJson["WIFIQUALITI"] = dBmtoPercent(rssi); + + + serializeJson(myJson, message); + + request->send(200, "application/json", message); + }); + + // start AP Mode + server.on("/apmode", HTTP_GET, [](AsyncWebServerRequest *request) { + ESPAsync_WiFiManager wifiManager(&server,&dns); + wifiManager.resetSettings(); + + ESP.restart(); + request->send_P(200, "text/html", index_html, processor); + }); + + // restart system + server.on("/restart", HTTP_GET, [](AsyncWebServerRequest *request) { + DBG_PRINTLN("/restart"); + ESP.restart(); + request->send_P(200, "text/html", index_html, processor); + }); + + server.on("/acoutput", HTTP_GET, [] (AsyncWebServerRequest *request) { + String parm1; + + if (request->hasParam("value") ) { + parm1 = request->getParam("value")->value(); + DBG_PRINT("/acoutput?value = "); + DBG_PRINTLN(parm1); + + if(parm1.equals("/s0") ){ + soyo_power = 0; + sprintf(msgData, "%d", soyo_power); + if(checkbox_mqttenabled){ + client.publish(topic_power, msgData); + } + } + else if(parm1.equals("/p1")){ + soyo_power +=1; + sprintf(msgData, "%d", soyo_power); + if(checkbox_mqttenabled){ + client.publish(topic_power, msgData); + } + } + else if(parm1.equals("/p10")){ + soyo_power +=10; + sprintf(msgData, "%d", soyo_power); + if(checkbox_mqttenabled){ + client.publish(topic_power, msgData); + } + } + else if(parm1.equals("/m1")){ + soyo_power -=1; + if(soyo_power < 0){ + soyo_power = 0; + } + sprintf(msgData, "%d", soyo_power); + if(checkbox_mqttenabled){ + client.publish(topic_power, msgData); + } + } + else if(parm1.equals("/m10")){ + soyo_power -=10; + if(soyo_power < 0){ + soyo_power = 0; + } + sprintf(msgData, "%d", soyo_power); + if(checkbox_mqttenabled){ + client.publish(topic_power, msgData); + } + } + } + request->send_P(200, "text/html", index_html, processor); + }); + + + server.on("/checkbox", HTTP_GET, [] (AsyncWebServerRequest *request) { + String checkbox_id; + String checkbox_value; + + if (request->hasParam("cbid") && request->hasParam("state")) { + checkbox_id = request->getParam("cbid")->value(); + checkbox_value = request->getParam("state")->value(); + + if(checkbox_id.equals("CBTIMER1")){ + if(checkbox_value.equals("1")){ + checkbox_timer1 = true; + } else { + checkbox_timer1 = false; + } + } + else if(checkbox_id.equals("CBTIMER2")){ + if(checkbox_value.equals("1")){ + checkbox_timer2 = true; + } else { + checkbox_timer2 = false; + } + } + else if(checkbox_id.equals("CBMQTTSTATE")){ + if(checkbox_value.equals("1")){ + checkbox_mqttenabled = true; + } else { + checkbox_mqttenabled = false; + } + } + else if(checkbox_id.equals("CBNULL")){ + if(checkbox_value.equals("1")){ + checkbox_nulleinspeisung = true; + } else { + checkbox_nulleinspeisung = false; + soyo_power = 0; + } + } + else if(checkbox_id.equals("CBBATSCHUTZ")){ + if(checkbox_value.equals("1")){ + checkbox_batschutz = true; + } else { + checkbox_batschutz = false; + output_enabled = true; //wenn batschutz aus, dann freigabe fuer soyo output + } + } + else if(checkbox_id.equals("CBMETERL1")){ + if(checkbox_value.equals("1")){ + checkbox_meter_l1 = true; + } else { + checkbox_meter_l1 = false; + } + } + else if(checkbox_id.equals("CBMETERL2")){ + if(checkbox_value.equals("1")){ + checkbox_meter_l2 = true; + } else { + checkbox_meter_l2 = false; + } + } + else if(checkbox_id.equals("CBMETERL3")){ + if(checkbox_value.equals("1")){ + checkbox_meter_l3 = true; + } else { + checkbox_meter_l3 = false; + } + } + } + request->send_P(200, "text/html", index_html, processor); + }); + + + server.on("/savesettings", HTTP_GET, [] (AsyncWebServerRequest *request) { + String value; + + value = request->getParam("t1")->value(); + memset(timer1_time, 0, sizeof(timer1_time)); + strcat(timer1_time, value.c_str()); + + value = request->getParam("w1")->value(); + timer1_watt = atoi(value.c_str()); + + value = request->getParam("t2")->value(); + memset(timer2_time, 0, sizeof(timer2_time)); + strcat(timer2_time, value.c_str()); + + value = request->getParam("w2")->value(); + timer2_watt = atoi(value.c_str()); + + value = request->getParam("maxwatt")->value(); + maxwatt = atoi(value.c_str()); + + value = request->getParam("meteripaddr")->value(); + memset(meteripaddr, 0, sizeof(meteripaddr)); + strcat(meteripaddr, value.c_str()); + + value = request->getParam("tout")->value(); + teiler_output = atoi(value.c_str()); + + value = request->getParam("meterinterval")->value(); + meterinterval = atol(value.c_str()); + + value = request->getParam("nullinterval")->value(); + nullinterval = atol(value.c_str()); + + value = request->getParam("nulloffset")->value(); + nulloffset = atoi(value.c_str()); + + value = request->getParam("mqttserver")->value(); + memset(mqtt_server, 0, sizeof(mqtt_server)); + strcat(mqtt_server, value.c_str()); + + value = request->getParam("mqttport")->value(); + memset(mqtt_port, 0, sizeof(mqtt_port)); + strcat(mqtt_port, value.c_str()); + + value = request->getParam("mqttbatvol")->value(); + memset(mqtt_topic_bat_voltage, 0, sizeof(mqtt_topic_bat_voltage)); + strcat(mqtt_topic_bat_voltage, value.c_str()); + + value = request->getParam("mqttbatsoc")->value(); + memset(mqtt_topic_bat_soc, 0, sizeof(mqtt_topic_bat_soc)); + strcat(mqtt_topic_bat_soc, value.c_str()); + + value = request->getParam("batsocstop")->value(); + batsocstop = atoi(value.c_str()); + + value = request->getParam("batsocstart")->value(); + batsocstart = atoi(value.c_str()); + + saveConfig(); + + meter_ip = String(meteripaddr); + + request->send_P(200, "text/html", index_html, processor); + }); + + AsyncElegantOTA.begin(&server); + server.onNotFound(notFound); + server.addHandler(&events); + server.begin(); + + rssi = WiFi.RSSI(); + + meter_model = getMeterType(); // get shelly typ, 3em / 3empro + + digitalWrite(SERIAL_COMMUNICATION_CONTROL_PIN, RS485_TX_PIN_VALUE); // RS485 Modul -> set board to transmit + } + + // end setup() +} + + +void loop() { + + if(checkbox_mqttenabled){ + if (!client.connected()) { + DBG_PRINTLN("lost mqtt connection -> start reconncect"); + reconnect(); + } + client.loop(); + } + + + // send current power to SoyoSource + if ((millis() - lastTimerSoyoSource) > timerSoyoSource) { + + if(checkbox_batschutz == true && output_enabled == false){ // wenn batterie soc < limit dann soyo_power = 0 + soyo_power = 0; + } + + new_soyo_power = soyo_power / teiler_output; // Last auf mehrere Soyo's aufteilen + if(new_soyo_power < 0){ + new_soyo_power = 0; + } + + //sendSoyoPowerData(soyo_power); + sendSoyoPowerData(new_soyo_power); + + if(new_soyo_power != old_soyo_power) { // nur für Debug, damit nur Laständerungen ausgegeben werden + old_soyo_power = new_soyo_power; + sprintf(dbgbuffer,"new soyo_power = %i ( %02X %02X %02X %02X %02X %02X %02X %02X )",new_soyo_power, soyo_power_data[0],soyo_power_data[1],soyo_power_data[2],soyo_power_data[3],soyo_power_data[4],soyo_power_data[5],soyo_power_data[6],soyo_power_data[7]); + DBG_PRINTLN(dbgbuffer); + } + + if(checkbox_mqttenabled){ + sprintf(msgData, "%d", soyo_power); + client.publish(topic_power, msgData); + } + + lastTimerSoyoSource = millis(); + } + + + // timer to get Shelly3EM data + if ((millis() - lastMeterinterval) > meterinterval) { + if (meter_model > 0){ + meter_power = getMeterData(meter_model); + } else{ + meter_model = getMeterType(); + DBG_PRINTLN("Kein Shelly erkannt! Bitte IP eintragen, speichern und ESP neu starten."); + } + + lastMeterinterval = millis(); + } + + + // timer to manage Nulleinspeisung + if ((millis() - lastNullinterval) > nullinterval) { + if(checkbox_nulleinspeisung && output_enabled){ + if(meter_power > nulloffset + 10){ + soyo_power += meter_power - nulloffset; + + if(soyo_power > maxwatt){ + soyo_power = maxwatt; + } + } + + if(meter_power < 0 + nulloffset ){ + soyo_power += meter_power - nulloffset; + + if(soyo_power < 0){ + soyo_power = 0; + } + } + + } + lastNullinterval = millis(); + } + + + // timer für uptime, SoyoSource Timer und BatSOCLimit + if ((millis() - lastTimerUptime) > timerUptime) { + myUptime(); + + if(checkbox_timer1 || checkbox_timer2){ + checkTimer(); + } + + // check ob Batterie SOC < oder > eingestelltem Limit + float mqttbatsoc_float = mqtt_bat_soc + 0.5; + int mqttbatsoc_int = (int)mqttbatsoc_float; + + if(checkbox_batschutz == true && mqttbatsoc_int > 1){ // falls mqtt noch nicht verbunden oder nicht aktiv + if(mqttbatsoc_int <= batsocstop){ + output_enabled = false; + }else if(mqttbatsoc_int >= batsocstart){ + output_enabled = true; + } + } + + lastTimerUptime = millis(); + } + + +} From 033a0ac0312f6127d41b315f5a3205da411d5d17 Mon Sep 17 00:00:00 2001 From: krulkip Date: Mon, 20 May 2024 07:17:26 +0200 Subject: [PATCH 5/8] Delete src/main.cpp --- src/main.cpp | 1345 -------------------------------------------------- 1 file changed, 1345 deletions(-) delete mode 100644 src/main.cpp diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index 4d39742..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,1345 +0,0 @@ -/*************************************************************************** - soyosource-powercontroller @matlen67 - - Version: 1.240508 - - 16.03.2024 -> Speichern der Checkboxzustände: aktiv Timer1 / Timer2 - 03.04.2024 -> Statusübersicht bei geschlossenen details/summary boxen - 14.04.2024 -> Falls Batterieschutz aktiviert, deaktiviere Regelung der Nulleinspeisung - 25.04.2024 -> Leistungspunkt bei Nulleinspeisung festlegen - (Bei mir funktioniert gut Intervall Shelly 1000ms & Intervall Nulleinspeisung 4000ms) - 26.04.2024 -> Auswahl der aktiven Leiter (L1, L2, L3) beim Shelly - 27.04.2024 -> Fehlerbehebung Shelly 3EM, Shelly Plus 1PM mit zugefügt - 28.04.2024 -> Teiler unter 'SoyoSource Output' hinzugefügt, um die Leistung auf mehere Geräte aufzuteilen - 29.04.2024 -> Telnet entfernt - 05.05.2024 -> update ArduinoJson to 7.0.4 - 08.05.2024 -> mqtt topic voltage & soc bearbeitbar - - - ************************* - Wiring - NodeMCU D1 - RS485 RO - NodeMCU D3 - RS485 DE/RE - NodeMCU D4 - RS485 DI - -****************************************************************************/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "html.h" - -#define DEBUG_SERIAL Serial - -#define DEBUG - -#ifdef DEBUG - #define DBG_PRINT(x) DEBUG_SERIAL.print(x) - #define DBG_PRINTLN(x) DEBUG_SERIAL.println(x) -#else - #define DBG_PRINT(x) - #define DBG_PRINTLN(x) -#endif - -//***************************************************************************** -// da Serial.printf(x,x) mit define nicht funktioniert als workaround sprintf -// sprintf(dbgbuffer,"ESP_%02X%02X%02X", mac[3], mac[4], mac[5]); -// DBG_PRINTLN(dbgbuffer); -//***************************************************************************** -char dbgbuffer[128]; - -#define RXPin D1 // Serial Receive pin (D1) -#define TXPin D4 // Serial Transmit pin (D4) - -//RS485 control -#define SERIAL_COMMUNICATION_CONTROL_PIN D3 // Transmission set pin (D3) -#define RS485_TX_PIN_VALUE HIGH -#define RS485_RX_PIN_VALUE LOW - -// time server -#define MY_NTP_SERVER "de.pool.ntp.org" -#define MY_TZ "CET-1CEST,M3.5.0/2,M10.5.0/3" - - -SoftwareSerial RS485Serial(RXPin, TXPin); // RX, TX -WiFiClient espClient; -PubSubClient client(espClient); -AsyncWebServer server(80); -AsyncEventSource events("/events"); -AsyncDNSServer dns; - - -// Uptime Global Variables -Uptime uptime; -uint8_t Uptime_Years = 0U, Uptime_Months = 0U, Uptime_Days = 0U, Uptime_Hours = 0U, Uptime_Minutes = 0U, Uptime_Seconds = 0U; -uint16_t Uptime_TotalDays = 0U; // Total Uptime Days -char uptime_str[37]; - -// Wifi to percent -const int RSSI_MAX =-50; // max strength signal in dBm -const int RSSI_MIN =-100; // min strength signal in dBm - -//Timer -unsigned long timerSoyoSource = 555; -unsigned long lastTimerSoyoSource = 0; - -unsigned long timerUptime = 1000; -unsigned long lastTimerUptime = 0; - -unsigned long meterinterval = 2000; -unsigned long lastMeterinterval = 0; - -unsigned long nullinterval = 5000; -unsigned long lastNullinterval = 0; - - - -//mqtt -char mqtt_server[16] = "192.168.178.10"; -char mqtt_port[5] = "1889"; -char msgData[64]; -String msg = ""; -char mqtt_topic_bat_voltage [48] = "VenusOS/SmartShunt/voltage"; -char mqtt_topic_bat_soc [48] = "VenusOS/SmartShunt/soc"; - -String dataReceived; -int data; -bool isDataReceived = false; -uint8_t byte0, byte1, byte2, byte3, byte4, byte5, byte6, byte7; -int byteSend; -int data_array[8]; -int soyo_hello_data[8] = {0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // bit7 org 0x00, CRC 0xFF -int soyo_power_data[8] = {0x24, 0x56, 0x00, 0x21, 0x00, 0x00, 0x80, 0x08}; // 0 Watt -int soyo_text_data[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -char buffer[8]; -int old_soyo_power = 0; -int soyo_power = 0; -int new_soyo_power = 0; -int teiler_output = 1; - -unsigned char mac[6]; -char mqtt_root[32] = "SoyoSource/"; -char clientId[16]; -char topic_power[40]; -char soyo_text[40]; - -float mqtt_bat_soc = 0.0; -float mqtt_bat_voltage = 0.0; - -long rssi; - -time_t now; -tm timeInfo; - - -// timer -char currentTime[20]; -char timer1_time[6] = "06:00"; -char timer2_time[6] = "20:00"; -char meteripaddr[16] = ""; - -int timer1_watt = 0; -int timer2_watt = 0; -int maxwatt = 0; - -//state checkboxes -bool checkbox_timer1 = false; -bool checkbox_timer2 = false; -bool checkbox_mqttenabled = false; -bool checkbox_nulleinspeisung = false; -bool checkbox_batschutz = false; -bool checkbox_meter_l1 = true; -bool checkbox_meter_l2 = true; -bool checkbox_meter_l3 = true; - -char metername[24] = "Meter"; -char mqtt_state[20] = "disabled"; - -// variablen Shelly 3em -const int shelly_3em_pro = 1; // ip/rpc/Shelly.GetStatus -const int shelly_plus_1pm = 2; // ip/rpc/Shelly.GetStatus - -const int shelly_3em = 10; // ip/status -const int shelly_em = 11; // ip/status -const int shelly_1pm = 12; // ip/status - - -String shelly_ip = ""; -int shelly_model = 0 ; - -//nulleinspeisung -int nulloffset = 0; -int meter_power = 0; -int meterpower = 0; -int meterl1 = 0; -int meterl2 = 0; -int meterl3 = 0; - -//batterieüberwachung -int batsocstop = 15; -int batsocstart = 50; -bool output_enabled = true; - - -bool new_connect = true; - -const char* PARAM_MESSAGE = "message"; - -//flag for saving data -bool shouldSaveConfig = false; - - -//callback notifying us of the need to save config -void saveConfigCallback () { - DBG_PRINTLN("Should save config"); - shouldSaveConfig = true; -} - - -void notFound(AsyncWebServerRequest *request) { - request->send(404, "text/plain", "Not found"); -} - - -int dBmtoPercent(int dBm){ - int percent; - if(dBm <= RSSI_MIN){ - percent = 0; - } else if(dBm >= RSSI_MAX) { - percent = 100; - } else { - percent = 2 * (dBm + 100); - } - - return percent; -} - - -void myUptime(){ - uptime.calculateUptime(); - - // Get The Uptime Values To Global Variables - Uptime_Years = uptime.getYears(); - Uptime_Months = uptime.getMonths(); - Uptime_Days = uptime.getDays(); - Uptime_Hours = uptime.getHours(); - Uptime_Minutes = uptime.getMinutes(); - Uptime_Seconds = uptime.getSeconds(); - Uptime_TotalDays = uptime.getTotalDays(); - - if (Uptime_Years == 0U) { // Uptime Is Less Than One Year - // First 60 Seconds - if (Uptime_Minutes == 0U && Uptime_Hours == 0U && Uptime_Days == 0U && Uptime_Months == 0U) - sprintf(uptime_str, "00:00:%02i", Uptime_Seconds); - // First Minute - else if (Uptime_Minutes == 1U && Uptime_Hours == 0U && Uptime_Days == 0U && Uptime_Months == 0U) - sprintf(uptime_str, "00:%02i:%02i", Uptime_Minutes, Uptime_Seconds); - // Second Minute And More But Less Than Hours, Days, Months - else if (Uptime_Minutes >= 2U && Uptime_Hours == 0U && Uptime_Days == 0U && Uptime_Months == 0U) - sprintf(uptime_str, "00:%02i:%02i", Uptime_Minutes, Uptime_Seconds); - // First Hour And More But Less Than Days, Months - else if (Uptime_Hours >= 1U && Uptime_Days == 0U && Uptime_Months == 0U) - sprintf(uptime_str, "%02i:%02i:%02i", Uptime_Hours, Uptime_Minutes, Uptime_Seconds); - // First Day And Less Than Month - else if (Uptime_Days == 1U && Uptime_Months == 0U) - sprintf(uptime_str, "%iday %02i:%02i:%02i", Uptime_Days, Uptime_Hours, Uptime_Minutes, Uptime_Seconds); - // Second Day And More But Less Than Month - else if (Uptime_Days >= 2U && Uptime_Months == 0U) - sprintf(uptime_str, "%idays %02i:%02i:%02i", Uptime_Days, Uptime_Hours, Uptime_Minutes, Uptime_Seconds); - // First Month And More But Less Than One Year - else if (Uptime_Months >= 1U) - sprintf(uptime_str, "%im, %id %02i:%02i", Uptime_Months, Uptime_Days, Uptime_Hours, Uptime_Minutes); - // If There Is Any Error In This If Loop Then Make Full String. - else sprintf(uptime_str, "%iy %im %id %02i:%02i", Uptime_Years, Uptime_Months, Uptime_Days, Uptime_Hours, Uptime_Minutes); - } else // Uptime Is More Than One Year - sprintf(uptime_str, "%iy %im %id %02i:%02i", Uptime_Years, Uptime_Months, Uptime_Days, Uptime_Hours, Uptime_Minutes); -} - - -//callback from mqtt -void mqtt_callback(char* topic, byte* payload, unsigned int length) { - unsigned int i = 0; - - for (i=0;i= 0 && arrived_value_i <= 3000) { - soyo_power = arrived_value_i; - } - } - - if(strcmp(topic, mqtt_topic_bat_soc) == 0){ - float arrived_value_f = atof(buffer); - mqtt_bat_soc = arrived_value_f; - } - - if(strcmp(topic, mqtt_topic_bat_voltage) == 0){ - float arrived_value_f = atof(buffer); - mqtt_bat_voltage = arrived_value_f; - } -} - - -String processor(const String& var){ - return String(); -} - - -void reconnect() { - DBG_PRINTLN("reconnect MQTT connection!"); - - //set callback again - client.setCallback(mqtt_callback); - - uint8_t timeout = 15; - - // wait for connection - while (!client.connected()){ - - DBG_PRINTLN(""); - - if (client.connect(clientId)) { - DBG_PRINTLN("connection established"); - - client.publish(topic_power, "0"); - client.subscribe(topic_power); - client.subscribe(mqtt_topic_bat_soc); - client.subscribe(mqtt_topic_bat_voltage); - - strcpy(mqtt_state, "connect"); - - DBG_PRINT("subscrible: "); - DBG_PRINT(topic_power); - DBG_PRINTLN(""); - - DBG_PRINT("subscrible: "); - DBG_PRINT(mqtt_topic_bat_soc); - DBG_PRINTLN(""); - - DBG_PRINT("subscrible: "); - DBG_PRINT(mqtt_topic_bat_voltage); - DBG_PRINTLN(""); - - } else { - DBG_PRINTLN("reconnect failed! "); - strcpy(mqtt_state, "connect error"); - - while (timeout){ - DBG_PRINT("."); - timeout--; - delay(1000); - } - } - } - -} - - -int calc_checksumme(int b1, int b2, int b3, int b4, int b5, int b6 ){ - int calc = (0xFF - b1 - b2 - b3 - b4 - b5 - b6) % 256; - return calc & 0xFF; -} - - -void sendSoyoPowerData(int power){ - soyo_power_data[0] = 0x24; - soyo_power_data[1] = 0x56; - soyo_power_data[2] = 0x00; - soyo_power_data[3] = 0x21; - soyo_power_data[4] = power >> 0x08; - soyo_power_data[5] = power & 0xFF; - soyo_power_data[6] = 0x80; - soyo_power_data[7] = calc_checksumme(soyo_power_data[1], soyo_power_data[2], soyo_power_data[3], soyo_power_data[4], soyo_power_data[5], soyo_power_data[6]); - - for(int i=0; i<8; i++) { - RS485Serial.write(soyo_power_data[i]); // send data to RS485 - //DBG_PRINTLN(soyo_power_data[i], HEX); - } -} - -//read config.json -void readConfig(){ - //read configuration from json - DBG_PRINTLN("mounting FS..."); - - if (LittleFS.begin()) { - DBG_PRINTLN("mounted file system"); - if (LittleFS.exists("/config.json")) { - //file exists, reading and loading - DBG_PRINTLN("reading config file"); - File configFile = LittleFS.open("/config.json", "r"); - if (configFile) { - DBG_PRINTLN("opened config file"); - size_t size = configFile.size(); - // Allocate a buffer to store contents of the file. - std::unique_ptr buf(new char[size]); - - configFile.readBytes(buf.get(), size); - - JsonDocument json; - auto deserializeError = deserializeJson(json, buf.get()); - serializeJson(json, Serial); - if (!deserializeError) { - DBG_PRINTLN("\nparsed json"); - strcpy(mqtt_server, json["mqtt_server"]); - strcpy(mqtt_port, json["mqtt_port"]); - - if(json.containsKey("mqtt_bat_vol")){ - strcpy(mqtt_topic_bat_voltage, json["mqtt_bat_vol"]); - } - - if(json.containsKey("mqtt_bat_soc")){ - strcpy(mqtt_topic_bat_soc, json["mqtt_bat_soc"]); - } - - char key_value[2]; - - if(json.containsKey("mqtt_on")){ - strcpy(key_value, json["mqtt_on"]); - if(strcmp(key_value, "1") == 0){ - checkbox_mqttenabled = true; - }else{ - checkbox_mqttenabled = false; - } - } - - if(json.containsKey("zft_on")){ - strcpy(key_value, json["zft_on"]); - - if(strcmp(key_value, "1") == 0){ - checkbox_nulleinspeisung = true; - }else{ - checkbox_nulleinspeisung = false; - } - } - - if(json.containsKey("batp_on")){ - strcpy(key_value, json["batp_on"]); - - if(strcmp(key_value, "1") == 0){ - checkbox_batschutz = true; - }else{ - checkbox_batschutz = false; - } - } - - if(json.containsKey("t1_on")){ - strcpy(key_value, json["t1_on"]); - - if(strcmp(key_value, "1") == 0){ - checkbox_timer1 = true; - }else{ - checkbox_timer1 = false; - } - } - - if(json.containsKey("t2_on")){ - strcpy(key_value, json["t2_on"]); - - if(strcmp(key_value, "1") == 0){ - checkbox_timer2 = true; - }else{ - checkbox_timer2 = false; - } - } - - if(json.containsKey("mtr_l1_on")){ - strcpy(key_value, json["mtr_l1_on"]); - - if(strcmp(key_value, "1") == 0){ - checkbox_meter_l1 = true; - }else{ - checkbox_meter_l1 = false; - } - } - - if(json.containsKey("mtr_l2_on")){ - strcpy(key_value, json["mtr_l2_on"]); - - if(strcmp(key_value, "1") == 0){ - checkbox_meter_l2 = true; - }else{ - checkbox_meter_l2 = false; - } - } - - if(json.containsKey("mtr_l3_on")){ - strcpy(key_value, json["mtr_l3_on"]); - - if(strcmp(key_value, "1") == 0){ - checkbox_meter_l3 = true; - }else{ - checkbox_meter_l3 = false; - } - } - - - if(json.containsKey("t1_t")){ - strcpy(timer1_time, json["t1_t"]); - } - - if(json.containsKey("t2_t")){ - strcpy(timer2_time, json["t2_t"]); - } - - if(json.containsKey("t1_p")){ - timer1_watt = json["t1_p"]; - } - - if(json.containsKey("t2_p")){ - timer2_watt = json["t2_p"]; - } - - if(json.containsKey("mp")){ - maxwatt = json["mp"]; - } - - if(json.containsKey("mtr_ip")){ - strcpy(meteripaddr, json["mtr_ip"]); - shelly_ip = String(meteripaddr); - } - - if(json.containsKey("mtr_iv")){ - meterinterval = json["mtr_iv"]; - } - - if(json.containsKey("z_iv")){ - nullinterval = json["z_iv"]; - } - - if(json.containsKey("z_ofs")){ - nulloffset = json["z_ofs"]; - } - - if(json.containsKey("soc_stop")){ - batsocstop = json["soc_stop"]; - } - - if(json.containsKey("soc_start")){ - batsocstart = json["soc_start"]; - } - - if(json.containsKey("tout")){ - teiler_output = json["tout"]; - } - - } else { - DBG_PRINTLN("failed to load json config"); - } - } - } - } else { - DBG_PRINTLN("failed to mount FS"); - } - //end read config data -} - - -// write config.json -void saveConfig(){ - DBG_PRINTLN(F("save data to config.json")); - JsonDocument json; - - json["mqtt_server"] = mqtt_server; - json["mqtt_port"] = mqtt_port; - json["mqtt_bat_vol"] = mqtt_topic_bat_voltage; - json["mqtt_bat_soc"] = mqtt_topic_bat_soc; - - - if(checkbox_mqttenabled){ - json["mqtt_on"] = "1"; - }else{ - json["mqtt_on"] = "0"; - } - - if(checkbox_nulleinspeisung){ - json["zft_on"] = "1"; - }else{ - json["zft_on"] = "0"; - } - - if(checkbox_batschutz){ - json["batp_on"] = "1"; - }else{ - json["batp_on"] = "0"; - } - - if(checkbox_timer1){ - json["t1_on"] = "1"; - }else{ - json["t1_on"] = "0"; - } - - if(checkbox_timer2){ - json["t2_on"] = "1"; - }else{ - json["t2_on"] = "0"; - } - - if(checkbox_meter_l1){ - json["mtr_l1_on"] = "1"; - }else{ - json["mtr_l1_on"] = "0"; - } - - if(checkbox_meter_l2){ - json["mtr_l2_on"] = "1"; - }else{ - json["mtr_l2_on"] = "0"; - } - - if(checkbox_meter_l3){ - json["mtr_l3_on"] = "1"; - }else{ - json["mtr_l3_on"] = "0"; - } - - json["t1_t"] = timer1_time; - json["t1_p"] = timer1_watt; - json["t2_t"] = timer2_time; - json["t2_p"] = timer2_watt; - json["mp"] = maxwatt; - json["mtr_ip"] = meteripaddr; - json["mtr_iv"] = meterinterval; - json["z_iv"] = nullinterval; - json["z_ofs"] = nulloffset; - json["soc_stop"] = batsocstop; - json["soc_start"] = batsocstart; - json["tout"] = teiler_output; - - File configFile = LittleFS.open("/config.json", "w"); - if (!configFile) { - DBG_PRINTLN("failed to open config file for writing"); - return; - } - - serializeJson(json, configFile); - configFile.close(); - - serializeJson(json, Serial); - DBG_PRINTLN(); -} - - -// get shelly type(3EM PRO, 3EM, EM, 1PM, Plus 1PM) -int getShellyType(){ - String shelly_url = "http://" + shelly_ip + "/shelly"; - int type = 0; - - memset(metername, 0, sizeof(metername)); - strcat(metername, "no device"); - - JsonDocument doc; - - WiFiClient client_shelly; - HTTPClient http; - - if (http.begin(client_shelly, shelly_url)) { - int httpCode = http.GET(); - if (httpCode > 0) { - if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { - String payload = http.getString(); - DeserializationError error = deserializeJson(doc, payload); - - if (error) { - DBG_PRINT(F("deserializeJson() failed: ")); - DBG_PRINTLN(error.f_str()); - } - - String json_type = doc["type"]; - String json_model = doc["model"]; - - if(json_type != NULL){ - - //test auf Shelly 1PM - if(json_type.equals("SHSW-PM")){ - type = shelly_1pm; - memset(metername, 0, sizeof(metername)); - strcat(metername, "Shelly 1PM"); - } - //test auf Shelly EM - if(json_type.equals("SHEM")){ - type = shelly_em; - memset(metername, 0, sizeof(metername)); - strcat(metername, "Shelly EM"); - } - - //test auf Shelly 3EM - if(json_type.equals("SHEM-3")){ - type = shelly_3em; - memset(metername, 0, sizeof(metername)); - strcat(metername, "Shelly 3EM"); - } - } - - - if(json_model != NULL){ - - //test auf Shelly 3EM Pro - if(json_model.equals("SPEM-003CEBEU")) { - type = shelly_3em_pro; - memset(metername, 0, sizeof(metername)); - strcat(metername, "Shelly 3EM Pro"); - } - - //test auf Shelly Plus 1PM - if(json_model.equals("SNSW-001P16EU")) { - type = shelly_plus_1pm; - memset(metername, 0, sizeof(metername)); - strcat(metername, "Shelly Plus 1PM"); - } - } - - } - } - http.end(); - } - DBG_PRINT("getShellyType() = "); - DBG_PRINTLN(String(metername)); - - return type; -} - - -// read shelly3EM -int getMeterData(int type) { - String shelly_url; - int power = 0; - int power1 = 0; - int power2 = 0; - int power3 = 0; - - JsonDocument doc; - WiFiClient client_shelly; - HTTPClient http; - - if (type > 0 && type < 10) { - shelly_url = "http://" + shelly_ip + "/rpc/Shelly.GetStatus"; // Shelly PRO 3EM - } else if(type >= 10) { - shelly_url = "http://" + shelly_ip + "/status"; // Shelly 3EM und Andere - } else{ - return 0; - } - - if (http.begin(client_shelly, shelly_url)) { - int httpCode = http.GET(); - if (httpCode > 0) { - if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { - String payload = http.getString(); - DeserializationError error = deserializeJson(doc, payload); - - if (error) { - DBG_PRINT(F("deserializeJson() failed: ")); - DBG_PRINTLN(error.f_str()); - } - - - if (type == shelly_3em_pro) { - power1 = doc["em:0"]["a_act_power"]; - power2 = doc["em:0"]["b_act_power"]; - power3 = doc["em:0"]["c_act_power"]; - } else if (type == shelly_3em) { - power1 = doc["emeters"][0]["power"]; - power2 = doc["emeters"][1]["power"]; - power3 = doc["emeters"][2]["power"]; - } else if (type == shelly_em) { - power1 = doc["meters"][0]["power"]; - power2 = doc["meters"][1]["power"]; - power3 = 0; - } else if (type == shelly_1pm) { - power1 = doc["meters"][0]["power"]; - power2 = 0; - power3 = 0; - } else if (type == shelly_plus_1pm) { - power1 = doc["switch:0"]["apower"]; - power2 = 0; - power3 = 0; - } - - - if(!checkbox_meter_l1){ - power1 = 0; - } - - if(!checkbox_meter_l2){ - power2 = 0; - } - - if(!checkbox_meter_l3){ - power3 = 0; - } - - power = power1 + power2 + power3; - - meterpower = power; - meterl1 = power1; - meterl2 = power2; - meterl3 = power3; - } - - } else { - sprintf(dbgbuffer,"[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str()); - DBG_PRINTLN(dbgbuffer); - shelly_model = 0; - } - - http.end(); - } else { - DBG_PRINTLN("[HTTP] Unable to connect\n"); - shelly_model = 0; - } - - return power; -} - - -void checkTimer(){ - - time(&now); - localtime_r(&now, &timeInfo); - - if (checkbox_timer1 == true){ - int t1_hour = String(timer1_time).substring(0,2).toInt(); - int t1_min = String(timer1_time).substring(3).toInt(); - - if((timeInfo.tm_hour == t1_hour && timeInfo.tm_min == t1_min && timeInfo.tm_sec == 0) || (timeInfo.tm_hour == t1_hour && timeInfo.tm_min == t1_min && timeInfo.tm_sec == 1) ){ - soyo_power = timer1_watt; - } - } - - if (checkbox_timer2 == true){ - int t2_hour = String(timer2_time).substring(0,2).toInt(); - int t2_min = String(timer2_time).substring(3).toInt(); - - if((timeInfo.tm_hour == t2_hour && timeInfo.tm_min == t2_min && timeInfo.tm_sec == 0) || (timeInfo.tm_hour == t2_hour && timeInfo.tm_min == t2_min && timeInfo.tm_sec == 1)){ - soyo_power = timer2_watt; - } - } - -} - - -//#################### SETUP ####################### -void setup() { - - DEBUG_SERIAL.begin(115200); - delay(500); - - DBG_PRINTLN(""); - DBG_PRINT(F("CPU Frequency = ")); - DBG_PRINT(F_CPU / 1000000); - DBG_PRINTLN(F(" MHz")); - - WiFi.macAddress(mac); - WiFi.persistent(true); // sonst verliert er nach einem Neustart die IP !!! - - sprintf(dbgbuffer,"ESP_%02X%02X%02X", mac[3], mac[4], mac[5]); - DBG_PRINTLN(dbgbuffer); - - //configTime(MY_TZ, MY_NTP_SERVER); - - sprintf(clientId, "soyo_%02x%02x%02x", mac[3], mac[4], mac[5] ); - - //mqtt_root = "SoyoSource/soyo_xxxxxx"; - strcat(mqtt_root, clientId); - - //topic_power = "SoyoSource/soyo_xxxxxx/power"; - strcat(topic_power, mqtt_root); - strcat(topic_power, "/power"); - - pinMode(SERIAL_COMMUNICATION_CONTROL_PIN, OUTPUT); - digitalWrite(SERIAL_COMMUNICATION_CONTROL_PIN, RS485_RX_PIN_VALUE); - RS485Serial.begin(4800); // set RS485 baud - - readConfig(); - - - ESPAsync_WMParameter custom_mqtt_server("server", "mqtt server", mqtt_server, 40); - ESPAsync_WMParameter custom_mqtt_port("port", "mqtt port", mqtt_port, 6); - - ESPAsync_WiFiManager wifiManager(&server, &dns); - wifiManager.setSaveConfigCallback(saveConfigCallback); - wifiManager.setConfigPortalTimeout(60); - wifiManager.addParameter(&custom_mqtt_server); - wifiManager.addParameter(&custom_mqtt_port); - configTime(MY_TZ, MY_NTP_SERVER); - - bool res = wifiManager.autoConnect(clientId); - - if(!res) { - DBG_PRINTLN("Failed to connect"); - ESP.restart(); - } else { - //if you get here you have connected to the WiFi - DBG_PRINT("WiFi connected to "); - DBG_PRINTLN(String(WiFi.SSID())); - DBG_PRINT("RSSI = "); - DBG_PRINT(String(WiFi.RSSI())); - DBG_PRINTLN(" dBm"); - DBG_PRINT("IP address "); - DBG_PRINTLN(WiFi.localIP()); - DBG_PRINTLN(); - - //read updated parameters - strcpy(mqtt_server, custom_mqtt_server.getValue()); - strcpy(mqtt_port, custom_mqtt_port.getValue()); - - //save the custom parameters to FS - if (shouldSaveConfig) { - saveConfig(); - } - - DBG_PRINTLN(String("mqttenabled: ") + checkbox_mqttenabled); - if(checkbox_mqttenabled){ - DBG_PRINTLN("set mqtt server!"); - DBG_PRINTLN(String("mqtt_server: ") + mqtt_server); - DBG_PRINTLN(String("mqtt_port: ") + mqtt_port); - - client.setServer(mqtt_server, atoi(mqtt_port)); - client.setCallback(mqtt_callback); - } - - // Handle Web Server Events - events.onConnect([](AsyncEventSourceClient *client){ - if(client->lastId()){ - DBG_PRINTLN(""); - //DEBUG_SERIAL.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId()); - sprintf(dbgbuffer,"Client reconnected! Last message ID that it got is: %u\n", client->lastId()); - DBG_PRINTLN(dbgbuffer); - //DEBUG_SERIAL.println(""); - } - client->send("hello!", NULL, millis(), 10000); - }); - - // Handle Web Server - server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ - new_connect = true; - request->send_P(200, "text/html", index_html, processor); - }); - - // crate json and fetch data - server.on("/json", HTTP_GET, [] (AsyncWebServerRequest *request){ - JsonDocument myJson; - String message = ""; - - rssi = WiFi.RSSI(); - - myJson["WIFIRSSI"] = rssi; - myJson["CLIENTID"] = clientId; - myJson["METERNAME"] = metername; - myJson["MAXWATTINPUT"] = maxwatt; - myJson["TOUT"] = teiler_output; - myJson["NULLINTERVAL"] = nullinterval; - myJson["NULLOFFSET"] = nulloffset; - myJson["METERIP"] = meteripaddr; - myJson["METERINTERVAL"] = meterinterval; - myJson["TIMER1TIME"] = timer1_time; - myJson["TIMER1WATT"] = timer1_watt; - myJson["TIMER2TIME"] = timer2_time; - myJson["TIMER2WATT"] = timer2_watt; - myJson["MQTTROOT"] = mqtt_root; - myJson["MQTTSTATECL"] = mqtt_state; - - myJson["CBNULL"] = checkbox_nulleinspeisung; //checkbox - if(checkbox_nulleinspeisung){ // Stausanzeige - myJson["NULLSTATE"] = "EIN"; - }else{ - myJson["NULLSTATE"] = "AUS"; - } - - myJson["CBMQTTSTATE"] = checkbox_mqttenabled; //checkbox - if(checkbox_mqttenabled){ - myJson["MQTTSTATE"] = "EIN"; - }else{ - myJson["MQTTSTATE"] = "AUS"; - } - - myJson["CBTIMER1"] = checkbox_timer1; //checkbox - myJson["CBTIMER2"] = checkbox_timer2; //checkbox - if(checkbox_timer1 || checkbox_timer2){ - myJson["TIMERSTATE"] = "EIN"; - }else{ - myJson["TIMERSTATE"] = "AUS"; - } - - myJson["CBBATSCHUTZ"] = checkbox_batschutz; //checkbox - if(checkbox_batschutz){ - myJson["BATTSTATE"] = "EIN"; - }else{ - myJson["BATTSTATE"] = "AUS"; - } - - myJson["CBMETERL1"] = checkbox_meter_l1; //checkbox Shelly L1 - myJson["CBMETERL2"] = checkbox_meter_l2; //checkbox Shelly L2 - myJson["CBMETERL3"] = checkbox_meter_l3; //checkbox Shelly L3 - - myJson["MQTTSERVER"] = mqtt_server; - myJson["MQTTPORT"] = mqtt_port; - myJson["MQTTBATVOL"] = mqtt_topic_bat_voltage; - myJson["MQTTBATSOC"] = mqtt_topic_bat_soc; - - myJson["UPTIME"] = uptime_str; - myJson["SOYOPOWER"] = soyo_power; - myJson["METERNAME"] = metername; - myJson["METERPOWER"] = meterpower; - myJson["METERL1"] = meterl1; - myJson["METERL2"] = meterl2; - myJson["METERL3"] = meterl3; - myJson["MQTT_SUB_1"] = String(soyo_power) + " W"; - myJson["MQTT_BAT_SOC"] = String(mqtt_bat_soc, 1) + " %"; - myJson["MQTT_BAT_V"] = String(mqtt_bat_voltage, 1) + " V"; - myJson["BATSOCSTOP"] = batsocstop; - myJson["BATSOCSTART"] = batsocstart; - myJson["WIFIQUALITI"] = dBmtoPercent(rssi); - - - serializeJson(myJson, message); - - request->send(200, "application/json", message); - }); - - // start AP Mode - server.on("/apmode", HTTP_GET, [](AsyncWebServerRequest *request) { - ESPAsync_WiFiManager wifiManager(&server,&dns); - wifiManager.resetSettings(); - - ESP.restart(); - request->send_P(200, "text/html", index_html, processor); - }); - - // restart system - server.on("/restart", HTTP_GET, [](AsyncWebServerRequest *request) { - DBG_PRINTLN("/restart"); - ESP.restart(); - request->send_P(200, "text/html", index_html, processor); - }); - - server.on("/acoutput", HTTP_GET, [] (AsyncWebServerRequest *request) { - String parm1; - - if (request->hasParam("value") ) { - parm1 = request->getParam("value")->value(); - DBG_PRINT("/acoutput?value = "); - DBG_PRINTLN(parm1); - - if(parm1.equals("/s0") ){ - soyo_power = 0; - sprintf(msgData, "%d", soyo_power); - if(checkbox_mqttenabled){ - client.publish(topic_power, msgData); - } - } - else if(parm1.equals("/p1")){ - soyo_power +=1; - sprintf(msgData, "%d", soyo_power); - if(checkbox_mqttenabled){ - client.publish(topic_power, msgData); - } - } - else if(parm1.equals("/p10")){ - soyo_power +=10; - sprintf(msgData, "%d", soyo_power); - if(checkbox_mqttenabled){ - client.publish(topic_power, msgData); - } - } - else if(parm1.equals("/m1")){ - soyo_power -=1; - if(soyo_power < 0){ - soyo_power = 0; - } - sprintf(msgData, "%d", soyo_power); - if(checkbox_mqttenabled){ - client.publish(topic_power, msgData); - } - } - else if(parm1.equals("/m10")){ - soyo_power -=10; - if(soyo_power < 0){ - soyo_power = 0; - } - sprintf(msgData, "%d", soyo_power); - if(checkbox_mqttenabled){ - client.publish(topic_power, msgData); - } - } - } - request->send_P(200, "text/html", index_html, processor); - }); - - - server.on("/checkbox", HTTP_GET, [] (AsyncWebServerRequest *request) { - String checkbox_id; - String checkbox_value; - - if (request->hasParam("cbid") && request->hasParam("state")) { - checkbox_id = request->getParam("cbid")->value(); - checkbox_value = request->getParam("state")->value(); - - if(checkbox_id.equals("CBTIMER1")){ - if(checkbox_value.equals("1")){ - checkbox_timer1 = true; - } else { - checkbox_timer1 = false; - } - } - else if(checkbox_id.equals("CBTIMER2")){ - if(checkbox_value.equals("1")){ - checkbox_timer2 = true; - } else { - checkbox_timer2 = false; - } - } - else if(checkbox_id.equals("CBMQTTSTATE")){ - if(checkbox_value.equals("1")){ - checkbox_mqttenabled = true; - } else { - checkbox_mqttenabled = false; - } - } - else if(checkbox_id.equals("CBNULL")){ - if(checkbox_value.equals("1")){ - checkbox_nulleinspeisung = true; - } else { - checkbox_nulleinspeisung = false; - soyo_power = 0; - } - } - else if(checkbox_id.equals("CBBATSCHUTZ")){ - if(checkbox_value.equals("1")){ - checkbox_batschutz = true; - } else { - checkbox_batschutz = false; - output_enabled = true; //wenn batschutz aus, dann freigabe fuer soyo output - } - } - else if(checkbox_id.equals("CBMETERL1")){ - if(checkbox_value.equals("1")){ - checkbox_meter_l1 = true; - } else { - checkbox_meter_l1 = false; - } - } - else if(checkbox_id.equals("CBMETERL2")){ - if(checkbox_value.equals("1")){ - checkbox_meter_l2 = true; - } else { - checkbox_meter_l2 = false; - } - } - else if(checkbox_id.equals("CBMETERL3")){ - if(checkbox_value.equals("1")){ - checkbox_meter_l3 = true; - } else { - checkbox_meter_l3 = false; - } - } - } - request->send_P(200, "text/html", index_html, processor); - }); - - - server.on("/savesettings", HTTP_GET, [] (AsyncWebServerRequest *request) { - String value; - - value = request->getParam("t1")->value(); - memset(timer1_time, 0, sizeof(timer1_time)); - strcat(timer1_time, value.c_str()); - - value = request->getParam("w1")->value(); - timer1_watt = atoi(value.c_str()); - - value = request->getParam("t2")->value(); - memset(timer2_time, 0, sizeof(timer2_time)); - strcat(timer2_time, value.c_str()); - - value = request->getParam("w2")->value(); - timer2_watt = atoi(value.c_str()); - - value = request->getParam("maxwatt")->value(); - maxwatt = atoi(value.c_str()); - - value = request->getParam("meteripaddr")->value(); - memset(meteripaddr, 0, sizeof(meteripaddr)); - strcat(meteripaddr, value.c_str()); - - value = request->getParam("tout")->value(); - teiler_output = atoi(value.c_str()); - - value = request->getParam("meterinterval")->value(); - meterinterval = atol(value.c_str()); - - value = request->getParam("nullinterval")->value(); - nullinterval = atol(value.c_str()); - - value = request->getParam("nulloffset")->value(); - nulloffset = atoi(value.c_str()); - - value = request->getParam("mqttserver")->value(); - memset(mqtt_server, 0, sizeof(mqtt_server)); - strcat(mqtt_server, value.c_str()); - - value = request->getParam("mqttport")->value(); - memset(mqtt_port, 0, sizeof(mqtt_port)); - strcat(mqtt_port, value.c_str()); - - value = request->getParam("mqttbatvol")->value(); - memset(mqtt_topic_bat_voltage, 0, sizeof(mqtt_topic_bat_voltage)); - strcat(mqtt_topic_bat_voltage, value.c_str()); - - value = request->getParam("mqttbatsoc")->value(); - memset(mqtt_topic_bat_soc, 0, sizeof(mqtt_topic_bat_soc)); - strcat(mqtt_topic_bat_soc, value.c_str()); - - value = request->getParam("batsocstop")->value(); - batsocstop = atoi(value.c_str()); - - value = request->getParam("batsocstart")->value(); - batsocstart = atoi(value.c_str()); - - saveConfig(); - - shelly_ip = String(meteripaddr); - - request->send_P(200, "text/html", index_html, processor); - }); - - AsyncElegantOTA.begin(&server); - server.onNotFound(notFound); - server.addHandler(&events); - server.begin(); - - rssi = WiFi.RSSI(); - - shelly_model = getShellyType(); // get shelly typ, 3em / 3empro - - digitalWrite(SERIAL_COMMUNICATION_CONTROL_PIN, RS485_TX_PIN_VALUE); // RS485 Modul -> set board to transmit - } - - // end setup() -} - - -void loop() { - - if(checkbox_mqttenabled){ - if (!client.connected()) { - DBG_PRINTLN("lost mqtt connection -> start reconncect"); - reconnect(); - } - client.loop(); - } - - - // send current power to SoyoSource - if ((millis() - lastTimerSoyoSource) > timerSoyoSource) { - - if(checkbox_batschutz == true && output_enabled == false){ // wenn batterie soc < limit dann soyo_power = 0 - soyo_power = 0; - } - - new_soyo_power = soyo_power / teiler_output; // Last auf mehrere Soyo's aufteilen - if(new_soyo_power < 0){ - new_soyo_power = 0; - } - - //sendSoyoPowerData(soyo_power); - sendSoyoPowerData(new_soyo_power); - - if(new_soyo_power != old_soyo_power) { // nur für Debug, damit nur Laständerungen ausgegeben werden - old_soyo_power = new_soyo_power; - sprintf(dbgbuffer,"new soyo_power = %i ( %02X %02X %02X %02X %02X %02X %02X %02X )",new_soyo_power, soyo_power_data[0],soyo_power_data[1],soyo_power_data[2],soyo_power_data[3],soyo_power_data[4],soyo_power_data[5],soyo_power_data[6],soyo_power_data[7]); - DBG_PRINTLN(dbgbuffer); - } - - if(checkbox_mqttenabled){ - sprintf(msgData, "%d", soyo_power); - client.publish(topic_power, msgData); - } - - lastTimerSoyoSource = millis(); - } - - - // timer to get Shelly3EM data - if ((millis() - lastMeterinterval) > meterinterval) { - if (shelly_model > 0){ - meter_power = getMeterData(shelly_model); - } else{ - shelly_model = getShellyType(); - DBG_PRINTLN("Kein Shelly erkannt! Bitte IP eintragen, speichern und ESP neu starten."); - } - - lastMeterinterval = millis(); - } - - - // timer to manage Nulleinspeisung - if ((millis() - lastNullinterval) > nullinterval) { - if(checkbox_nulleinspeisung && output_enabled){ - if(meter_power > nulloffset + 10){ - soyo_power += meter_power - nulloffset; - - if(soyo_power > maxwatt){ - soyo_power = maxwatt; - } - } - - if(meter_power < 0 + nulloffset ){ - soyo_power += meter_power - nulloffset; - - if(soyo_power < 0){ - soyo_power = 0; - } - } - - } - lastNullinterval = millis(); - } - - - // timer für uptime, SoyoSource Timer und BatSOCLimit - if ((millis() - lastTimerUptime) > timerUptime) { - myUptime(); - - if(checkbox_timer1 || checkbox_timer2){ - checkTimer(); - } - - // check ob Batterie SOC < oder > eingestelltem Limit - float mqttbatsoc_float = mqtt_bat_soc + 0.5; - int mqttbatsoc_int = (int)mqttbatsoc_float; - - if(checkbox_batschutz == true && mqttbatsoc_int > 1){ // falls mqtt noch nicht verbunden oder nicht aktiv - if(mqttbatsoc_int <= batsocstop){ - output_enabled = false; - }else if(mqttbatsoc_int >= batsocstart){ - output_enabled = true; - } - } - - lastTimerUptime = millis(); - } - - -} - - - From 086570ca20b7aa373218376f5c78db7b72dfea19 Mon Sep 17 00:00:00 2001 From: krulkip Date: Mon, 20 May 2024 07:19:34 +0200 Subject: [PATCH 6/8] Update and rename src/soyosource-powercontroller_V0014.ino to main.cpp --- src/soyosource-powercontroller_V0014.ino => main.cpp | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/soyosource-powercontroller_V0014.ino => main.cpp (100%) diff --git a/src/soyosource-powercontroller_V0014.ino b/main.cpp similarity index 100% rename from src/soyosource-powercontroller_V0014.ino rename to main.cpp From c60cdf9ef90ce5418a6185c7414d236f7bbf865e Mon Sep 17 00:00:00 2001 From: krulkip Date: Mon, 20 May 2024 07:20:01 +0200 Subject: [PATCH 7/8] Delete soyosource-powercontroller_V0012.ino --- soyosource-powercontroller_V0012.ino | 1365 -------------------------- 1 file changed, 1365 deletions(-) delete mode 100644 soyosource-powercontroller_V0012.ino diff --git a/soyosource-powercontroller_V0012.ino b/soyosource-powercontroller_V0012.ino deleted file mode 100644 index 4997758..0000000 --- a/soyosource-powercontroller_V0012.ino +++ /dev/null @@ -1,1365 +0,0 @@ -/*************************************************************************** - soyosource-powercontroller @matlen67 - - Version: 1.240508BK - - 16.03.2024 -> Speichern der Checkboxzustände: aktiv Timer1 / Timer2 - 03.04.2024 -> Statusübersicht bei geschlossenen details/summary boxen - 14.04.2024 -> Falls Batterieschutz aktiviert, deaktiviere Regelung der Nulleinspeisung - 25.04.2024 -> Leistungspunkt bei Nulleinspeisung festlegen - (Bei mir funktioniert gut Intervall Shelly 1000ms & Intervall Nulleinspeisung 4000ms) - 26.04.2024 -> Auswahl der aktiven Leiter (L1, L2, L3) beim Shelly - 27.04.2024 -> Fehlerbehebung Shelly 3EM, Shelly Plus 1PM mit zugefügt - 28.04.2024 -> Teiler unter 'SoyoSource Output' hinzugefügt, um die Leistung auf mehere Geräte aufzuteilen - 29.04.2024 -> Telnet entfernt - 05.05.2024 -> update ArduinoJson to 7.0.4 - 08.05.2024 -> mqtt topic voltage & soc bearbeitbar - - - ************************* - Wiring - NodeMCU D1 - RS485 RO - NodeMCU D3 - RS485 DE/RE - NodeMCU D4 - RS485 DI - -****************************************************************************/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "html.h" - -#define DEBUG_SERIAL Serial - -#define DEBUG - -#ifdef DEBUG - #define DBG_PRINT(x) DEBUG_SERIAL.print(x) - #define DBG_PRINTLN(x) DEBUG_SERIAL.println(x) -#else - #define DBG_PRINT(x) - #define DBG_PRINTLN(x) -#endif - -//***************************************************************************** -// da Serial.printf(x,x) mit define nicht funktioniert als workaround sprintf -// sprintf(dbgbuffer,"ESP_%02X%02X%02X", mac[3], mac[4], mac[5]); -// DBG_PRINTLN(dbgbuffer); -//***************************************************************************** -char dbgbuffer[128]; -#define D1 5 -#define D3 0 -#define D4 2 - -#define RXPin D1 // Serial Receive pin (D1) -#define TXPin D4 // Serial Transmit pin (D4) - -//RS485 control -#define SERIAL_COMMUNICATION_CONTROL_PIN D3 // Transmission set pin (D3) -#define RS485_TX_PIN_VALUE HIGH -#define RS485_RX_PIN_VALUE LOW - -// time server -#define MY_NTP_SERVER "de.pool.ntp.org" -#define MY_TZ "CET-1CEST,M3.5.0/2,M10.5.0/3" - - -SoftwareSerial RS485Serial(RXPin, TXPin); // RX, TX -WiFiClient espClient; -PubSubClient client(espClient); -AsyncWebServer server(80); -AsyncEventSource events("/events"); -AsyncDNSServer dns; - - -// Uptime Global Variables -Uptime uptime; -uint8_t Uptime_Years = 0U, Uptime_Months = 0U, Uptime_Days = 0U, Uptime_Hours = 0U, Uptime_Minutes = 0U, Uptime_Seconds = 0U; -uint16_t Uptime_TotalDays = 0U; // Total Uptime Days -char uptime_str[37]; - -// Wifi to percent -const int RSSI_MAX =-50; // max strength signal in dBm -const int RSSI_MIN =-100; // min strength signal in dBm - -//Timer -unsigned long timerSoyoSource = 555; -unsigned long lastTimerSoyoSource = 0; - -unsigned long timerUptime = 1000; -unsigned long lastTimerUptime = 0; - -unsigned long meterinterval = 2000; -unsigned long lastMeterinterval = 0; - -unsigned long nullinterval = 5000; -unsigned long lastNullinterval = 0; - - - -//mqtt -char mqtt_server[16] = "192.168.178.30"; -char mqtt_port[5] = "1889"; -char msgData[64]; -String msg = ""; -char mqtt_topic_bat_voltage [48] = "VenusOS/SmartShunt/voltage"; -char mqtt_topic_bat_soc [48] = "VenusOS/SmartShunt/soc"; - -String dataReceived; -int data; -bool isDataReceived = false; -uint8_t byte0, byte1, byte2, byte3, byte4, byte5, byte6, byte7; -int byteSend; -int data_array[8]; -int soyo_hello_data[8] = {0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // bit7 org 0x00, CRC 0xFF -int soyo_power_data[8] = {0x24, 0x56, 0x00, 0x21, 0x00, 0x00, 0x80, 0x08}; // 0 Watt -int soyo_text_data[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -char buffer[8]; -int old_soyo_power = 0; -int soyo_power = 0; -int new_soyo_power = 0; -int teiler_output = 1; - -unsigned char mac[6]; -char mqtt_root[32] = "SoyoSource/"; -char clientId[16]; -char topic_power[40]; -char soyo_text[40]; - -float mqtt_bat_soc = 0.0; -float mqtt_bat_voltage = 0.0; - -long rssi; - -time_t now; -tm timeInfo; - - -// timer -char currentTime[20]; -char timer1_time[6] = "06:00"; -char timer2_time[6] = "20:00"; -char meteripaddr[16] = ""; - -int timer1_watt = 0; -int timer2_watt = 0; -int maxwatt = 0; - -//state checkboxes -bool checkbox_timer1 = false; -bool checkbox_timer2 = false; -bool checkbox_mqttenabled = false; -bool checkbox_nulleinspeisung = false; -bool checkbox_batschutz = false; -bool checkbox_meter_l1 = true; -bool checkbox_meter_l2 = true; -bool checkbox_meter_l3 = true; - -char metername[24] = "Meter"; -char mqtt_state[20] = "disabled"; - -// variablen Shelly 3em -const int shelly_3em_pro = 1; // ip/rpc/Shelly.GetStatus -const int shelly_plus_1pm = 2; // ip/rpc/Shelly.GetStatus - -const int shelly_3em = 10; // ip/status -const int shelly_em = 11; // ip/status -const int shelly_1pm = 12; // ip/status - -const int homewizard = 20; // ip/api/v1/data - -String meter_ip = ""; -int meter_model = 0 ; - -//nulleinspeisung -int nulloffset = 0; -int meter_power = 0; -int meterpower = 0; -int meterl1 = 0; -int meterl2 = 0; -int meterl3 = 0; - -//batterieüberwachung -int batsocstop = 15; -int batsocstart = 50; -bool output_enabled = true; - - -bool new_connect = true; - -const char* PARAM_MESSAGE = "message"; - -//flag for saving data -bool shouldSaveConfig = false; - - -//callback notifying us of the need to save config -void saveConfigCallback () { - DBG_PRINTLN("Should save config"); - shouldSaveConfig = true; -} - - -void notFound(AsyncWebServerRequest *request) { - request->send(404, "text/plain", "Not found"); -} - - -int dBmtoPercent(int dBm){ - int percent; - if(dBm <= RSSI_MIN){ - percent = 0; - } else if(dBm >= RSSI_MAX) { - percent = 100; - } else { - percent = 2 * (dBm + 100); - } - - return percent; -} - - -void myUptime(){ - uptime.calculateUptime(); - - // Get The Uptime Values To Global Variables - Uptime_Years = uptime.getYears(); - Uptime_Months = uptime.getMonths(); - Uptime_Days = uptime.getDays(); - Uptime_Hours = uptime.getHours(); - Uptime_Minutes = uptime.getMinutes(); - Uptime_Seconds = uptime.getSeconds(); - Uptime_TotalDays = uptime.getTotalDays(); - - if (Uptime_Years == 0U) { // Uptime Is Less Than One Year - // First 60 Seconds - if (Uptime_Minutes == 0U && Uptime_Hours == 0U && Uptime_Days == 0U && Uptime_Months == 0U) - sprintf(uptime_str, "00:00:%02i", Uptime_Seconds); - // First Minute - else if (Uptime_Minutes == 1U && Uptime_Hours == 0U && Uptime_Days == 0U && Uptime_Months == 0U) - sprintf(uptime_str, "00:%02i:%02i", Uptime_Minutes, Uptime_Seconds); - // Second Minute And More But Less Than Hours, Days, Months - else if (Uptime_Minutes >= 2U && Uptime_Hours == 0U && Uptime_Days == 0U && Uptime_Months == 0U) - sprintf(uptime_str, "00:%02i:%02i", Uptime_Minutes, Uptime_Seconds); - // First Hour And More But Less Than Days, Months - else if (Uptime_Hours >= 1U && Uptime_Days == 0U && Uptime_Months == 0U) - sprintf(uptime_str, "%02i:%02i:%02i", Uptime_Hours, Uptime_Minutes, Uptime_Seconds); - // First Day And Less Than Month - else if (Uptime_Days == 1U && Uptime_Months == 0U) - sprintf(uptime_str, "%iday %02i:%02i:%02i", Uptime_Days, Uptime_Hours, Uptime_Minutes, Uptime_Seconds); - // Second Day And More But Less Than Month - else if (Uptime_Days >= 2U && Uptime_Months == 0U) - sprintf(uptime_str, "%idays %02i:%02i:%02i", Uptime_Days, Uptime_Hours, Uptime_Minutes, Uptime_Seconds); - // First Month And More But Less Than One Year - else if (Uptime_Months >= 1U) - sprintf(uptime_str, "%im, %id %02i:%02i", Uptime_Months, Uptime_Days, Uptime_Hours, Uptime_Minutes); - // If There Is Any Error In This If Loop Then Make Full String. - else sprintf(uptime_str, "%iy %im %id %02i:%02i", Uptime_Years, Uptime_Months, Uptime_Days, Uptime_Hours, Uptime_Minutes); - } else // Uptime Is More Than One Year - sprintf(uptime_str, "%iy %im %id %02i:%02i", Uptime_Years, Uptime_Months, Uptime_Days, Uptime_Hours, Uptime_Minutes); -} - - -//callback from mqtt -void mqtt_callback(char* topic, byte* payload, unsigned int length) { - unsigned int i = 0; - - for (i=0;i= 0 && arrived_value_i <= 3000) { - soyo_power = arrived_value_i; - } - } - - if(strcmp(topic, mqtt_topic_bat_soc) == 0){ - float arrived_value_f = atof(buffer); - mqtt_bat_soc = arrived_value_f; - } - - if(strcmp(topic, mqtt_topic_bat_voltage) == 0){ - float arrived_value_f = atof(buffer); - mqtt_bat_voltage = arrived_value_f; - } -} - - -String processor(const String& var){ - return String(); -} - - -void reconnect() { - DBG_PRINTLN("reconnect MQTT connection!"); - - //set callback again - client.setCallback(mqtt_callback); - - uint8_t timeout = 15; - - // wait for connection - while (!client.connected()){ - - DBG_PRINTLN(""); - - if (client.connect(clientId)) { - DBG_PRINTLN("connection established"); - - client.publish(topic_power, "0"); - client.subscribe(topic_power); - client.subscribe(mqtt_topic_bat_soc); - client.subscribe(mqtt_topic_bat_voltage); - - strcpy(mqtt_state, "connect"); - - DBG_PRINT("subscrible: "); - DBG_PRINT(topic_power); - DBG_PRINTLN(""); - - DBG_PRINT("subscrible: "); - DBG_PRINT(mqtt_topic_bat_soc); - DBG_PRINTLN(""); - - DBG_PRINT("subscrible: "); - DBG_PRINT(mqtt_topic_bat_voltage); - DBG_PRINTLN(""); - - } else { - DBG_PRINTLN("reconnect failed! "); - strcpy(mqtt_state, "connect error"); - - while (timeout){ - DBG_PRINT("."); - timeout--; - delay(1000); - } - } - } - -} - - -int calc_checksumme(int b1, int b2, int b3, int b4, int b5, int b6 ){ - int calc = (0xFF - b1 - b2 - b3 - b4 - b5 - b6) % 256; - return calc & 0xFF; -} - - -void sendSoyoPowerData(int power){ - soyo_power_data[0] = 0x24; - soyo_power_data[1] = 0x56; - soyo_power_data[2] = 0x00; - soyo_power_data[3] = 0x21; - soyo_power_data[4] = power >> 0x08; - soyo_power_data[5] = power & 0xFF; - soyo_power_data[6] = 0x80; - soyo_power_data[7] = calc_checksumme(soyo_power_data[1], soyo_power_data[2], soyo_power_data[3], soyo_power_data[4], soyo_power_data[5], soyo_power_data[6]); - - for(int i=0; i<8; i++) { - RS485Serial.write(soyo_power_data[i]); // send data to RS485 - //DBG_PRINTLN(soyo_power_data[i], HEX); - } -} - -//read config.json -void readConfig(){ - //read configuration from json - DBG_PRINTLN("mounting FS..."); - - if (LittleFS.begin()) { - DBG_PRINTLN("mounted file system"); - if (LittleFS.exists("/config.json")) { - //file exists, reading and loading - DBG_PRINTLN("reading config file"); - File configFile = LittleFS.open("/config.json", "r"); - if (configFile) { - DBG_PRINTLN("opened config file"); - size_t size = configFile.size(); - // Allocate a buffer to store contents of the file. - std::unique_ptr buf(new char[size]); - - configFile.readBytes(buf.get(), size); - - JsonDocument json; - auto deserializeError = deserializeJson(json, buf.get()); - serializeJson(json, Serial); - if (!deserializeError) { - DBG_PRINTLN("\nparsed json"); - strcpy(mqtt_server, json["mqtt_server"]); - strcpy(mqtt_port, json["mqtt_port"]); - - if(json.containsKey("mqtt_bat_vol")){ - strcpy(mqtt_topic_bat_voltage, json["mqtt_bat_vol"]); - } - - if(json.containsKey("mqtt_bat_soc")){ - strcpy(mqtt_topic_bat_soc, json["mqtt_bat_soc"]); - } - - char key_value[2]; - - if(json.containsKey("mqtt_on")){ - strcpy(key_value, json["mqtt_on"]); - if(strcmp(key_value, "1") == 0){ - checkbox_mqttenabled = true; - }else{ - checkbox_mqttenabled = false; - } - } - - if(json.containsKey("zft_on")){ - strcpy(key_value, json["zft_on"]); - - if(strcmp(key_value, "1") == 0){ - checkbox_nulleinspeisung = true; - }else{ - checkbox_nulleinspeisung = false; - } - } - - if(json.containsKey("batp_on")){ - strcpy(key_value, json["batp_on"]); - - if(strcmp(key_value, "1") == 0){ - checkbox_batschutz = true; - }else{ - checkbox_batschutz = false; - } - } - - if(json.containsKey("t1_on")){ - strcpy(key_value, json["t1_on"]); - - if(strcmp(key_value, "1") == 0){ - checkbox_timer1 = true; - }else{ - checkbox_timer1 = false; - } - } - - if(json.containsKey("t2_on")){ - strcpy(key_value, json["t2_on"]); - - if(strcmp(key_value, "1") == 0){ - checkbox_timer2 = true; - }else{ - checkbox_timer2 = false; - } - } - - if(json.containsKey("mtr_l1_on")){ - strcpy(key_value, json["mtr_l1_on"]); - - if(strcmp(key_value, "1") == 0){ - checkbox_meter_l1 = true; - }else{ - checkbox_meter_l1 = false; - } - } - - if(json.containsKey("mtr_l2_on")){ - strcpy(key_value, json["mtr_l2_on"]); - - if(strcmp(key_value, "1") == 0){ - checkbox_meter_l2 = true; - }else{ - checkbox_meter_l2 = false; - } - } - - if(json.containsKey("mtr_l3_on")){ - strcpy(key_value, json["mtr_l3_on"]); - - if(strcmp(key_value, "1") == 0){ - checkbox_meter_l3 = true; - }else{ - checkbox_meter_l3 = false; - } - } - - - if(json.containsKey("t1_t")){ - strcpy(timer1_time, json["t1_t"]); - } - - if(json.containsKey("t2_t")){ - strcpy(timer2_time, json["t2_t"]); - } - - if(json.containsKey("t1_p")){ - timer1_watt = json["t1_p"]; - } - - if(json.containsKey("t2_p")){ - timer2_watt = json["t2_p"]; - } - - if(json.containsKey("mp")){ - maxwatt = json["mp"]; - } - - if(json.containsKey("mtr_ip")){ - strcpy(meteripaddr, json["mtr_ip"]); - meter_ip = String(meteripaddr); - } - - if(json.containsKey("mtr_iv")){ - meterinterval = json["mtr_iv"]; - } - - if(json.containsKey("z_iv")){ - nullinterval = json["z_iv"]; - } - - if(json.containsKey("z_ofs")){ - nulloffset = json["z_ofs"]; - } - - if(json.containsKey("soc_stop")){ - batsocstop = json["soc_stop"]; - } - - if(json.containsKey("soc_start")){ - batsocstart = json["soc_start"]; - } - - if(json.containsKey("tout")){ - teiler_output = json["tout"]; - } - - } else { - DBG_PRINTLN("failed to load json config"); - } - } - } - } else { - DBG_PRINTLN("failed to mount FS"); - } - //end read config data -} - - -// write config.json -void saveConfig(){ - DBG_PRINTLN(F("save data to config.json")); - JsonDocument json; - - json["mqtt_server"] = mqtt_server; - json["mqtt_port"] = mqtt_port; - json["mqtt_bat_vol"] = mqtt_topic_bat_voltage; - json["mqtt_bat_soc"] = mqtt_topic_bat_soc; - - - if(checkbox_mqttenabled){ - json["mqtt_on"] = "1"; - }else{ - json["mqtt_on"] = "0"; - } - - if(checkbox_nulleinspeisung){ - json["zft_on"] = "1"; - }else{ - json["zft_on"] = "0"; - } - - if(checkbox_batschutz){ - json["batp_on"] = "1"; - }else{ - json["batp_on"] = "0"; - } - - if(checkbox_timer1){ - json["t1_on"] = "1"; - }else{ - json["t1_on"] = "0"; - } - - if(checkbox_timer2){ - json["t2_on"] = "1"; - }else{ - json["t2_on"] = "0"; - } - - if(checkbox_meter_l1){ - json["mtr_l1_on"] = "1"; - }else{ - json["mtr_l1_on"] = "0"; - } - - if(checkbox_meter_l2){ - json["mtr_l2_on"] = "1"; - }else{ - json["mtr_l2_on"] = "0"; - } - - if(checkbox_meter_l3){ - json["mtr_l3_on"] = "1"; - }else{ - json["mtr_l3_on"] = "0"; - } - - json["t1_t"] = timer1_time; - json["t1_p"] = timer1_watt; - json["t2_t"] = timer2_time; - json["t2_p"] = timer2_watt; - json["mp"] = maxwatt; - json["mtr_ip"] = meteripaddr; - json["mtr_iv"] = meterinterval; - json["z_iv"] = nullinterval; - json["z_ofs"] = nulloffset; - json["soc_stop"] = batsocstop; - json["soc_start"] = batsocstart; - json["tout"] = teiler_output; - - File configFile = LittleFS.open("/config.json", "w"); - if (!configFile) { - DBG_PRINTLN("failed to open config file for writing"); - return; - } - - serializeJson(json, configFile); - configFile.close(); - - serializeJson(json, Serial); - DBG_PRINTLN(); -} - - -// get meter type(3EM PRO, 3EM, EM, 1PM, Plus 1PM, Homewizard) -int getMeterType(){ - //String meter_url = "http://" + meter_ip + "/shelly"; - meter_ip="192.168.178.141"; - String meter_url = "http://" + meter_ip + "/api"; - int type = 0; - - memset(metername, 0, sizeof(metername)); - strcat(metername, "no device"); - - JsonDocument doc; - - WiFiClient client_meter; - HTTPClient http; - - if (http.begin(client_meter, meter_url)) { - int httpCode = http.GET(); - - if (httpCode > 0) { - if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { - String payload = http.getString(); - - DeserializationError error = deserializeJson(doc, payload); - - if (error) { - DBG_PRINT(F("deserializeJson() failed: ")); - DBG_PRINTLN(error.f_str()); - } - - String json_type = doc["type"]; - String json_model = doc["model"]; - String json_product_name = doc["product_name"]; - if(json_type != NULL){ - - //test auf Shelly 1PM - if(json_type.equals("SHSW-PM")){ - type = shelly_1pm; - memset(metername, 0, sizeof(metername)); - strcat(metername, "Shelly 1PM"); - } - //test auf Shelly EM - if(json_type.equals("SHEM")){ - type = shelly_em; - memset(metername, 0, sizeof(metername)); - strcat(metername, "Shelly EM"); - } - - //test auf Shelly 3EM - if(json_type.equals("SHEM-3")){ - type = shelly_3em; - memset(metername, 0, sizeof(metername)); - strcat(metername, "Shelly 3EM"); - } - } - - - if(json_model != NULL){ - - //test auf Shelly 3EM Pro - if(json_model.equals("SPEM-003CEBEU")) { - type = shelly_3em_pro; - memset(metername, 0, sizeof(metername)); - strcat(metername, "Shelly 3EM Pro"); - } - - //test auf Shelly Plus 1PM - if(json_model.equals("SNSW-001P16EU")) { - type = shelly_plus_1pm; - memset(metername, 0, sizeof(metername)); - strcat(metername, "Shelly Plus 1PM"); - } - } - - if(json_product_name != NULL){ - //test auf Homewizard - if(json_product_name.equals("P1 meter")) { - type = homewizard; - DBG_PRINTLN(type); - memset(metername, 0, sizeof(metername)); - strcat(metername, "Homewizard"); - } - } - } - } - http.end(); - } - DBG_PRINT("getMeterType() = "); - DBG_PRINTLN(String(metername)); - - return type; -} - - -// read meter -int getMeterData(int type) { - String meter_url; - int power = 0; - int power1 = 0; - int power2 = 0; - int power3 = 0; - - JsonDocument doc; - WiFiClient client_meter; - HTTPClient http; - - if (type > 0 && type < 10) { - meter_url = "http://" + meter_ip + "/rpc/Shelly.GetStatus"; // Shelly PRO 3EM - } else if(type >= 10 && type < 15) { - meter_url = "http://" + meter_ip + "/status"; // Shelly 3EM und Andere - } else if (type >=16) { - meter_url = "http://" + meter_ip + "/api/v1/data"; // Homewizard - } else { - return 0; - } - - if (http.begin(client_meter, meter_url)) { - int httpCode = http.GET(); - if (httpCode > 0) { - if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { - String payload = http.getString(); - DeserializationError error = deserializeJson(doc, payload); - - if (error) { - DBG_PRINT(F("deserializeJson() failed: ")); - DBG_PRINTLN(error.f_str()); - } - - - if (type == shelly_3em_pro) { - power1 = doc["em:0"]["a_act_power"]; - power2 = doc["em:0"]["b_act_power"]; - power3 = doc["em:0"]["c_act_power"]; - } else if (type == shelly_3em) { - power1 = doc["emeters"][0]["power"]; - power2 = doc["emeters"][1]["power"]; - power3 = doc["emeters"][2]["power"]; - } else if (type == shelly_em) { - power1 = doc["meters"][0]["power"]; - power2 = doc["meters"][1]["power"]; - power3 = 0; - } else if (type == shelly_1pm) { - power1 = doc["meters"][0]["power"]; - power2 = 0; - power3 = 0; - } else if (type == shelly_plus_1pm) { - power1 = doc["switch:0"]["apower"]; - power2 = 0; - power3 = 0; - } else if (type == homewizard) { - power1 = doc["active_power_l1_w"]; - power2 = doc["active_power_l2_w"]; - power3 = doc["active_power_l3_w"]; - } - - - if(!checkbox_meter_l1){ - power1 = 0; - } - - if(!checkbox_meter_l2){ - power2 = 0; - } - - if(!checkbox_meter_l3){ - power3 = 0; - } - - power = power1 + power2 + power3; - - meterpower = power; - meterl1 = power1; - meterl2 = power2; - meterl3 = power3; - } - - } else { - sprintf(dbgbuffer,"[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str()); - DBG_PRINTLN(dbgbuffer); - meter_model = 0; - } - - http.end(); - } else { - DBG_PRINTLN("[HTTP] Unable to connect\n"); - meter_model = 0; - } - - return power; -} - - -void checkTimer(){ - - time(&now); - localtime_r(&now, &timeInfo); - - if (checkbox_timer1 == true){ - int t1_hour = String(timer1_time).substring(0,2).toInt(); - int t1_min = String(timer1_time).substring(3).toInt(); - - if((timeInfo.tm_hour == t1_hour && timeInfo.tm_min == t1_min && timeInfo.tm_sec == 0) || (timeInfo.tm_hour == t1_hour && timeInfo.tm_min == t1_min && timeInfo.tm_sec == 1) ){ - soyo_power = timer1_watt; - } - } - - if (checkbox_timer2 == true){ - int t2_hour = String(timer2_time).substring(0,2).toInt(); - int t2_min = String(timer2_time).substring(3).toInt(); - - if((timeInfo.tm_hour == t2_hour && timeInfo.tm_min == t2_min && timeInfo.tm_sec == 0) || (timeInfo.tm_hour == t2_hour && timeInfo.tm_min == t2_min && timeInfo.tm_sec == 1)){ - soyo_power = timer2_watt; - } - } - -} - - -//#################### SETUP ####################### -void setup() { - - DEBUG_SERIAL.begin(115200); - delay(500); - - DBG_PRINTLN(""); - DBG_PRINT(F("CPU Frequency = ")); - DBG_PRINT(F_CPU / 1000000); - DBG_PRINTLN(F(" MHz")); - - WiFi.macAddress(mac); - WiFi.persistent(true); // sonst verliert er nach einem Neustart die IP !!! - - sprintf(dbgbuffer,"ESP_%02X%02X%02X", mac[3], mac[4], mac[5]); - DBG_PRINTLN(dbgbuffer); - - //configTime(MY_TZ, MY_NTP_SERVER); - - sprintf(clientId, "soyo_%02x%02x%02x", mac[3], mac[4], mac[5] ); - - //mqtt_root = "SoyoSource/soyo_xxxxxx"; - strcat(mqtt_root, clientId); - - //topic_power = "SoyoSource/soyo_xxxxxx/power"; - strcat(topic_power, mqtt_root); - strcat(topic_power, "/power"); - - pinMode(SERIAL_COMMUNICATION_CONTROL_PIN, OUTPUT); - digitalWrite(SERIAL_COMMUNICATION_CONTROL_PIN, RS485_RX_PIN_VALUE); - RS485Serial.begin(4800); // set RS485 baud - - readConfig(); - - - ESPAsync_WMParameter custom_mqtt_server("server", "mqtt server", mqtt_server, 40); - ESPAsync_WMParameter custom_mqtt_port("port", "mqtt port", mqtt_port, 6); - - ESPAsync_WiFiManager wifiManager(&server, &dns); - wifiManager.setSaveConfigCallback(saveConfigCallback); - wifiManager.setConfigPortalTimeout(60); - wifiManager.addParameter(&custom_mqtt_server); - wifiManager.addParameter(&custom_mqtt_port); - configTime(MY_TZ, MY_NTP_SERVER); - - bool res = wifiManager.autoConnect(clientId); - - if(!res) { - DBG_PRINTLN("Failed to connect"); - ESP.restart(); - } else { - //if you get here you have connected to the WiFi - DBG_PRINT("WiFi connected to "); - DBG_PRINTLN(String(WiFi.SSID())); - DBG_PRINT("RSSI = "); - DBG_PRINT(String(WiFi.RSSI())); - DBG_PRINTLN(" dBm"); - DBG_PRINT("IP address "); - DBG_PRINTLN(WiFi.localIP()); - DBG_PRINTLN(); - - //read updated parameters - strcpy(mqtt_server, custom_mqtt_server.getValue()); - strcpy(mqtt_port, custom_mqtt_port.getValue()); - - //save the custom parameters to FS - if (shouldSaveConfig) { - saveConfig(); - } - - DBG_PRINTLN(String("mqttenabled: ") + checkbox_mqttenabled); - if(checkbox_mqttenabled){ - DBG_PRINTLN("set mqtt server!"); - DBG_PRINTLN(String("mqtt_server: ") + mqtt_server); - DBG_PRINTLN(String("mqtt_port: ") + mqtt_port); - - client.setServer(mqtt_server, atoi(mqtt_port)); - client.setCallback(mqtt_callback); - } - - // Handle Web Server Events - events.onConnect([](AsyncEventSourceClient *client){ - if(client->lastId()){ - DBG_PRINTLN(""); - //DEBUG_SERIAL.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId()); - sprintf(dbgbuffer,"Client reconnected! Last message ID that it got is: %u\n", client->lastId()); - DBG_PRINTLN(dbgbuffer); - //DEBUG_SERIAL.println(""); - } - client->send("hello!", NULL, millis(), 10000); - }); - - // Handle Web Server - server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ - new_connect = true; - request->send_P(200, "text/html", index_html, processor); - }); - - // crate json and fetch data - server.on("/json", HTTP_GET, [] (AsyncWebServerRequest *request){ - JsonDocument myJson; - String message = ""; - - rssi = WiFi.RSSI(); - - myJson["WIFIRSSI"] = rssi; - myJson["CLIENTID"] = clientId; - myJson["METERNAME"] = metername; - myJson["MAXWATTINPUT"] = maxwatt; - myJson["TOUT"] = teiler_output; - myJson["NULLINTERVAL"] = nullinterval; - myJson["NULLOFFSET"] = nulloffset; - myJson["METERIP"] = meteripaddr; - myJson["METERINTERVAL"] = meterinterval; - myJson["TIMER1TIME"] = timer1_time; - myJson["TIMER1WATT"] = timer1_watt; - myJson["TIMER2TIME"] = timer2_time; - myJson["TIMER2WATT"] = timer2_watt; - myJson["MQTTROOT"] = mqtt_root; - myJson["MQTTSTATECL"] = mqtt_state; - - myJson["CBNULL"] = checkbox_nulleinspeisung; //checkbox - if(checkbox_nulleinspeisung){ // Stausanzeige - myJson["NULLSTATE"] = "EIN"; - }else{ - myJson["NULLSTATE"] = "AUS"; - } - - myJson["CBMQTTSTATE"] = checkbox_mqttenabled; //checkbox - if(checkbox_mqttenabled){ - myJson["MQTTSTATE"] = "EIN"; - }else{ - myJson["MQTTSTATE"] = "AUS"; - } - - myJson["CBTIMER1"] = checkbox_timer1; //checkbox - myJson["CBTIMER2"] = checkbox_timer2; //checkbox - if(checkbox_timer1 || checkbox_timer2){ - myJson["TIMERSTATE"] = "EIN"; - }else{ - myJson["TIMERSTATE"] = "AUS"; - } - - myJson["CBBATSCHUTZ"] = checkbox_batschutz; //checkbox - if(checkbox_batschutz){ - myJson["BATTSTATE"] = "EIN"; - }else{ - myJson["BATTSTATE"] = "AUS"; - } - - myJson["CBMETERL1"] = checkbox_meter_l1; //checkbox Shelly L1 - myJson["CBMETERL2"] = checkbox_meter_l2; //checkbox Shelly L2 - myJson["CBMETERL3"] = checkbox_meter_l3; //checkbox Shelly L3 - - myJson["MQTTSERVER"] = mqtt_server; - myJson["MQTTPORT"] = mqtt_port; - myJson["MQTTBATVOL"] = mqtt_topic_bat_voltage; - myJson["MQTTBATSOC"] = mqtt_topic_bat_soc; - - myJson["UPTIME"] = uptime_str; - myJson["SOYOPOWER"] = soyo_power; - myJson["METERNAME"] = metername; - myJson["METERPOWER"] = meterpower; - myJson["METERL1"] = meterl1; - myJson["METERL2"] = meterl2; - myJson["METERL3"] = meterl3; - myJson["MQTT_SUB_1"] = String(soyo_power) + " W"; - myJson["MQTT_BAT_SOC"] = String(mqtt_bat_soc, 1) + " %"; - myJson["MQTT_BAT_V"] = String(mqtt_bat_voltage, 1) + " V"; - myJson["BATSOCSTOP"] = batsocstop; - myJson["BATSOCSTART"] = batsocstart; - myJson["WIFIQUALITI"] = dBmtoPercent(rssi); - - - serializeJson(myJson, message); - - request->send(200, "application/json", message); - }); - - // start AP Mode - server.on("/apmode", HTTP_GET, [](AsyncWebServerRequest *request) { - ESPAsync_WiFiManager wifiManager(&server,&dns); - wifiManager.resetSettings(); - - ESP.restart(); - request->send_P(200, "text/html", index_html, processor); - }); - - // restart system - server.on("/restart", HTTP_GET, [](AsyncWebServerRequest *request) { - DBG_PRINTLN("/restart"); - ESP.restart(); - request->send_P(200, "text/html", index_html, processor); - }); - - server.on("/acoutput", HTTP_GET, [] (AsyncWebServerRequest *request) { - String parm1; - - if (request->hasParam("value") ) { - parm1 = request->getParam("value")->value(); - DBG_PRINT("/acoutput?value = "); - DBG_PRINTLN(parm1); - - if(parm1.equals("/s0") ){ - soyo_power = 0; - sprintf(msgData, "%d", soyo_power); - if(checkbox_mqttenabled){ - client.publish(topic_power, msgData); - } - } - else if(parm1.equals("/p1")){ - soyo_power +=1; - sprintf(msgData, "%d", soyo_power); - if(checkbox_mqttenabled){ - client.publish(topic_power, msgData); - } - } - else if(parm1.equals("/p10")){ - soyo_power +=10; - sprintf(msgData, "%d", soyo_power); - if(checkbox_mqttenabled){ - client.publish(topic_power, msgData); - } - } - else if(parm1.equals("/m1")){ - soyo_power -=1; - if(soyo_power < 0){ - soyo_power = 0; - } - sprintf(msgData, "%d", soyo_power); - if(checkbox_mqttenabled){ - client.publish(topic_power, msgData); - } - } - else if(parm1.equals("/m10")){ - soyo_power -=10; - if(soyo_power < 0){ - soyo_power = 0; - } - sprintf(msgData, "%d", soyo_power); - if(checkbox_mqttenabled){ - client.publish(topic_power, msgData); - } - } - } - request->send_P(200, "text/html", index_html, processor); - }); - - - server.on("/checkbox", HTTP_GET, [] (AsyncWebServerRequest *request) { - String checkbox_id; - String checkbox_value; - - if (request->hasParam("cbid") && request->hasParam("state")) { - checkbox_id = request->getParam("cbid")->value(); - checkbox_value = request->getParam("state")->value(); - - if(checkbox_id.equals("CBTIMER1")){ - if(checkbox_value.equals("1")){ - checkbox_timer1 = true; - } else { - checkbox_timer1 = false; - } - } - else if(checkbox_id.equals("CBTIMER2")){ - if(checkbox_value.equals("1")){ - checkbox_timer2 = true; - } else { - checkbox_timer2 = false; - } - } - else if(checkbox_id.equals("CBMQTTSTATE")){ - if(checkbox_value.equals("1")){ - checkbox_mqttenabled = true; - } else { - checkbox_mqttenabled = false; - } - } - else if(checkbox_id.equals("CBNULL")){ - if(checkbox_value.equals("1")){ - checkbox_nulleinspeisung = true; - } else { - checkbox_nulleinspeisung = false; - soyo_power = 0; - } - } - else if(checkbox_id.equals("CBBATSCHUTZ")){ - if(checkbox_value.equals("1")){ - checkbox_batschutz = true; - } else { - checkbox_batschutz = false; - output_enabled = true; //wenn batschutz aus, dann freigabe fuer soyo output - } - } - else if(checkbox_id.equals("CBMETERL1")){ - if(checkbox_value.equals("1")){ - checkbox_meter_l1 = true; - } else { - checkbox_meter_l1 = false; - } - } - else if(checkbox_id.equals("CBMETERL2")){ - if(checkbox_value.equals("1")){ - checkbox_meter_l2 = true; - } else { - checkbox_meter_l2 = false; - } - } - else if(checkbox_id.equals("CBMETERL3")){ - if(checkbox_value.equals("1")){ - checkbox_meter_l3 = true; - } else { - checkbox_meter_l3 = false; - } - } - } - request->send_P(200, "text/html", index_html, processor); - }); - - - server.on("/savesettings", HTTP_GET, [] (AsyncWebServerRequest *request) { - String value; - - value = request->getParam("t1")->value(); - memset(timer1_time, 0, sizeof(timer1_time)); - strcat(timer1_time, value.c_str()); - - value = request->getParam("w1")->value(); - timer1_watt = atoi(value.c_str()); - - value = request->getParam("t2")->value(); - memset(timer2_time, 0, sizeof(timer2_time)); - strcat(timer2_time, value.c_str()); - - value = request->getParam("w2")->value(); - timer2_watt = atoi(value.c_str()); - - value = request->getParam("maxwatt")->value(); - maxwatt = atoi(value.c_str()); - - value = request->getParam("meteripaddr")->value(); - memset(meteripaddr, 0, sizeof(meteripaddr)); - strcat(meteripaddr, value.c_str()); - - value = request->getParam("tout")->value(); - teiler_output = atoi(value.c_str()); - - value = request->getParam("meterinterval")->value(); - meterinterval = atol(value.c_str()); - - value = request->getParam("nullinterval")->value(); - nullinterval = atol(value.c_str()); - - value = request->getParam("nulloffset")->value(); - nulloffset = atoi(value.c_str()); - - value = request->getParam("mqttserver")->value(); - memset(mqtt_server, 0, sizeof(mqtt_server)); - strcat(mqtt_server, value.c_str()); - - value = request->getParam("mqttport")->value(); - memset(mqtt_port, 0, sizeof(mqtt_port)); - strcat(mqtt_port, value.c_str()); - - value = request->getParam("mqttbatvol")->value(); - memset(mqtt_topic_bat_voltage, 0, sizeof(mqtt_topic_bat_voltage)); - strcat(mqtt_topic_bat_voltage, value.c_str()); - - value = request->getParam("mqttbatsoc")->value(); - memset(mqtt_topic_bat_soc, 0, sizeof(mqtt_topic_bat_soc)); - strcat(mqtt_topic_bat_soc, value.c_str()); - - value = request->getParam("batsocstop")->value(); - batsocstop = atoi(value.c_str()); - - value = request->getParam("batsocstart")->value(); - batsocstart = atoi(value.c_str()); - - saveConfig(); - - meter_ip = String(meteripaddr); - - request->send_P(200, "text/html", index_html, processor); - }); - - AsyncElegantOTA.begin(&server); - server.onNotFound(notFound); - server.addHandler(&events); - server.begin(); - - rssi = WiFi.RSSI(); - - meter_model = getMeterType(); // get shelly typ, 3em / 3empro - - digitalWrite(SERIAL_COMMUNICATION_CONTROL_PIN, RS485_TX_PIN_VALUE); // RS485 Modul -> set board to transmit - } - - // end setup() -} - - -void loop() { - - if(checkbox_mqttenabled){ - if (!client.connected()) { - DBG_PRINTLN("lost mqtt connection -> start reconncect"); - reconnect(); - } - client.loop(); - } - - - // send current power to SoyoSource - if ((millis() - lastTimerSoyoSource) > timerSoyoSource) { - - if(checkbox_batschutz == true && output_enabled == false){ // wenn batterie soc < limit dann soyo_power = 0 - soyo_power = 0; - } - - new_soyo_power = soyo_power / teiler_output; // Last auf mehrere Soyo's aufteilen - if(new_soyo_power < 0){ - new_soyo_power = 0; - } - - //sendSoyoPowerData(soyo_power); - sendSoyoPowerData(new_soyo_power); - - if(new_soyo_power != old_soyo_power) { // nur für Debug, damit nur Laständerungen ausgegeben werden - old_soyo_power = new_soyo_power; - sprintf(dbgbuffer,"new soyo_power = %i ( %02X %02X %02X %02X %02X %02X %02X %02X )",new_soyo_power, soyo_power_data[0],soyo_power_data[1],soyo_power_data[2],soyo_power_data[3],soyo_power_data[4],soyo_power_data[5],soyo_power_data[6],soyo_power_data[7]); - DBG_PRINTLN(dbgbuffer); - } - - if(checkbox_mqttenabled){ - sprintf(msgData, "%d", soyo_power); - client.publish(topic_power, msgData); - } - - lastTimerSoyoSource = millis(); - } - - - // timer to get Shelly3EM data - if ((millis() - lastMeterinterval) > meterinterval) { - if (meter_model > 0){ - meter_power = getMeterData(meter_model); - } else{ - meter_model = getMeterType(); - DBG_PRINTLN("Kein Shelly erkannt! Bitte IP eintragen, speichern und ESP neu starten."); - } - - lastMeterinterval = millis(); - } - - - // timer to manage Nulleinspeisung - if ((millis() - lastNullinterval) > nullinterval) { - if(checkbox_nulleinspeisung && output_enabled){ - if(meter_power > nulloffset + 10){ - soyo_power += meter_power - nulloffset; - - if(soyo_power > maxwatt){ - soyo_power = maxwatt; - } - } - - if(meter_power < 0 + nulloffset ){ - soyo_power += meter_power - nulloffset; - - if(soyo_power < 0){ - soyo_power = 0; - } - } - - } - lastNullinterval = millis(); - } - - - // timer für uptime, SoyoSource Timer und BatSOCLimit - if ((millis() - lastTimerUptime) > timerUptime) { - myUptime(); - - if(checkbox_timer1 || checkbox_timer2){ - checkTimer(); - } - - // check ob Batterie SOC < oder > eingestelltem Limit - float mqttbatsoc_float = mqtt_bat_soc + 0.5; - int mqttbatsoc_int = (int)mqttbatsoc_float; - - if(checkbox_batschutz == true && mqttbatsoc_int > 1){ // falls mqtt noch nicht verbunden oder nicht aktiv - if(mqttbatsoc_int <= batsocstop){ - output_enabled = false; - }else if(mqttbatsoc_int >= batsocstart){ - output_enabled = true; - } - } - - lastTimerUptime = millis(); - } - - -} From 560945095aa3af6332ef6bc7923d4b5e917f77d2 Mon Sep 17 00:00:00 2001 From: krulkip Date: Mon, 20 May 2024 07:20:25 +0200 Subject: [PATCH 8/8] Update and rename main.cpp to src/main.cpp --- main.cpp => src/main.cpp | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename main.cpp => src/main.cpp (100%) diff --git a/main.cpp b/src/main.cpp similarity index 100% rename from main.cpp rename to src/main.cpp