Fully automated, autonomous-focused charging of an electric car from your own PV
Motivation / Issue
Charging an electric car from your own produced electric energy is not that straight forward as you would assume.
My setup:
• 4kWp PV on the roof with Fronius Symo Hybrid 5.0-3-S Inverter (5kW)
• Hardy Barth Wallbox and eCB1 Control Unit
• electric storage battery (in the house): 6.4kWh
• electric car: Renault Zoe R240
Ideally you want to charge your electric car with as much energy produced by your own PV, right? Of course. You don’t want to use power from the external grid, – if you can avoid it. The charging system (Hardy Barth Wallbox and eCB1 Control Unit in my case) naturally supports this, but not fully automatically.
With my Renault Zoe, if I plug in the car while the PV is producing e.g. 3-4kW, then all of it goes into the car’s battery. But if the sky gets cloudy or if the production goes down later the day, the car continues to charge at the same level.
So when the own production from the PV drops, the energy to charge the cars battery is taken from the electric storage battery in the house. But this is what you absolutely don’t want. Because obviously you want to save that energy for the night when the sun is not shining.
So what can we do about this?
The Fronius and the eCB1 work very well together and there may even be a setting for that exact use case. The so-called “Eco” mode should be able to support this. Well, … maybe it does. But I could not find such a setting. If you read this and you know how to do it, please let me know.
Anyhow – For the fun of it, I decided to stop searching and just build a very flexible, custom solution myself.
I got the first inspiration from this great project (written in German)
And the great thing: not just the Fronius, but also the eCB1 already supports REST API: http://api.ecb1.de/doc/
(Fronius doc: https://www.fronius.com/en/photovoltaics/products/all-products/system-monitoring/open-interfaces/fronius-solar-api-json-)
So I purchased an AZDelivery NodeMCU ESP8266-12F with OLED Display and started.
I planned the following. A small housing with 3 TACT buttons. That should be enough.
3 TACT Buttons
• 11kW: Immediate start of 11kW charging
• ECO Auto: automatic charging
• main conditions:
• if state of battery > 90%, allow charging (this is the initial start condition; upper hysteresis flag is set)
• if house battery > 75% && <= 90% – check for min 75% of 2kW production over 10 minutes. If less, stop charging.
• if house battery < 75%, don’t allow charging (clears upper hysteresis flag)
• additional conditions:
• check for 5 minutes, if the pv production is extremely low. If so, don’t allow charging (otherwise charging the car would be done just from the house battery)
• check for 5 minutes, if 75% of the time power from the grid is needed to charge. If so, don’t allow charging
• OFF: Charging not allowed
I am currently testing this version (V1.1, 11.07.2020):
#include <ArduinoJson.h> // Arduino JSON (5.x) #include <ESP8266WiFi.h> // ESP8266 WiFi #include <WiFiClient.h> // ESP8266 Wifi-Client #include <ESP8266HTTPClient.h> // ESP8266 HTTP-Client #include <Wire.h> // Wire / I2C #include <SSD1306Ascii.h> // 1306 OLED Display #include <SSD1306AsciiWire.h> // 1306 OLED Display via I2C // HW used: ESP8266 microcontroller with integrated 0.91 "OLED display // https://www.az-delivery.de/en/products/esp8266-mikrocontroller-mit-integrierten-0-91-oled-display // Arduino IDE Config: use NodeMCU 1.0 (ESP-12E Module) from ESP8266 Boards // WiFi SSID and password const char *ssid = "your-ssid"; const char *password = "your-pwd"; // Fronius hostname or IP address const String inverterHostname = "10.0.0.10"; // Fronius Solar API URL "Fronius Flow Data" // https://www.fronius.com/en/photovoltaics/products/all-products/system-monitoring/open-interfaces/fronius-solar-api-json- String urlFronius = "http://" + inverterHostname + "/solar_api/v1/GetPowerFlowRealtimeData.fcgi"; // Hardy Barth Charging Station // http://api.ecb1.de/doc/ String urlECB1 = "http://ecb1.local/api/v1"; int count = 0; // used for char shifts on oled int timesProductionTooLow = 0; // counter to check if production decreases (e.g. half of production) int timesProductionVeryLow = 0; // counter to check if production is extremely low (e.g. 1/10 of production) int timesExternalPower = 0; // counter for every time power had to be taken from external grid int mode = 0; // operation modes; (0: off, 1: eco auto, 2: 11kW) int allowCharge = 0; int upperHysteresisFlag = 0; // TACT buttons int TACT0 = 12; // D6 (Off mode) int TACT1 = 13; // D7 (eco auto mode) int TACT2 = 15; // D8 (11kW mode) #define I2C_ADDRESS 0x3C // display setup #define RST_PIN 16 // reset pin of the display SSD1306AsciiWire oled; // initialise oled display void setup() { delay(1000); // wait 1s // I2C on, 400kHz Wire.begin(); Wire.setClock(400000L); // display on #if RST_PIN >= 0 oled.begin(&Adafruit128x32, I2C_ADDRESS, RST_PIN); #else oled.begin(&Adafruit128x32, I2C_ADDRESS); #endif Serial.begin(115200); // UART on, 115200/8/N/1 // set display font oled.setFont(fixed_bold10x15); oled.clear(); oled.println("Boot ..."); WiFi.mode(WIFI_OFF); // Wi-Fi off delay(1000); // wait 1s WiFi.mode(WIFI_STA); // Wi-Fi Client-Mode, AP-Mode Off WiFi.begin(ssid, password); // connect Wi-Fi Serial.println(); Serial.println("Fully automated, autonomous-focused charging of an electric car from your own PV"); Serial.println("tom@ee-toolkit.com"); Serial.println("www.ee-toolkit.com"); Serial.println(); Serial.print("Connecting "); while (WiFi.status() != WL_CONNECTED) { // wait until connected delay(500); // print a dot every 0.5s Serial.print("."); } Serial.println(" Connected."); // Wi-Fi stats Serial.println(); Serial.print("SSID: "); Serial.println(ssid); Serial.print("Channel: "); Serial.println(WiFi.channel()); Serial.print("Signal (RX): "); Serial.print(WiFi.RSSI()); Serial.println(" dBm"); Serial.print("IP-Address: "); Serial.println(WiFi.localIP()); Serial.print("MAC-Address: "); Serial.println(WiFi.macAddress()); Serial.println(); // set up directions and interrupts for TACT buttons (all with 10k Pull-Ups) pinMode(TACT0, INPUT); pinMode(TACT1, INPUT); pinMode(TACT2, INPUT); attachInterrupt(digitalPinToInterrupt(TACT0), TACT0_ButtonPressed, RISING); attachInterrupt(digitalPinToInterrupt(TACT1), TACT1_ButtonPressed, RISING); attachInterrupt(digitalPinToInterrupt(TACT2), TACT2_ButtonPressed, RISING); delay(1000); // wait 1s } void loop() { count++; if (count > 10) count = 0; // initialise HTTP-Clients HTTPClient httpFronius; const size_t capacityFronius = JSON_OBJECT_SIZE(3) + JSON_ARRAY_SIZE(2) + 60; // JSON Buffer init DynamicJsonBuffer jsonBufferFronius(capacityFronius); httpFronius.begin(urlFronius); // url for the flow data int httpCodeFronius = httpFronius.GET(); String httpResponseFronius = httpFronius.getString(); // http reply (JSON) httpFronius.end(); // end http client Serial.print("URL: "); Serial.println(urlFronius); Serial.print("HTTP Status: "); // print the reply Serial.println(httpCodeFronius); // parse the JSON and check if OK JsonObject& jsonFronius = jsonBufferFronius.parseObject(httpResponseFronius); if (!jsonFronius.success()) { Serial.println("JSON-Parser: Error"); } else { Serial.println("JSON-Parser: OK"); } Serial.println(); // Power from the grid float p_grid = ( jsonFronius["Body"]["Data"]["Site"]["P_Grid"] | 0 ); // Power consumption at this moment float p_load = ( jsonFronius["Body"]["Data"]["Site"]["P_Load"] | 0 ) * -1; // PV production at this moment float p_pv = ( jsonFronius["Body"]["Data"]["Site"]["P_PV"] | 0 ); // State of battery charge float soc = ( jsonFronius["Body"]["Data"]["Inverters"]["1"]["SOC"] | 0 ); Serial.print("p_grid: "); Serial.println(p_grid); Serial.print("p_load: "); Serial.println(p_load); Serial.print("soc: "); Serial.println(soc); Serial.print("p_pv: "); Serial.println(p_pv); HTTPClient httpECB1; const size_t capacityECB1 = JSON_OBJECT_SIZE(3) + JSON_ARRAY_SIZE(2) + 60; // JSON Buffer init DynamicJsonBuffer jsonBufferECB1(capacityECB1); // Fronius Data httpECB1.begin(urlECB1 + "/chargecontrols/1"); int httpCodeECB1 = httpECB1.GET(); String httpResponseECB1 = httpECB1.getString(); // http reply (JSON) httpECB1.end(); // end http client Serial.print("URL: "); Serial.println(urlECB1 + "/chargecontrols/1"); Serial.print("HTTP Status: "); // print the reply Serial.println(httpCodeECB1); // parse the JSON and check if OK JsonObject& jsonECB1 = jsonBufferECB1.parseObject(httpResponseECB1); if (!jsonECB1.success()) { Serial.println("JSON-Parser: Error"); } else { Serial.println("JSON-Parser: OK"); } Serial.println(); int stateid = ( jsonECB1["chargecontrol"]["stateid"] | 0 ); int modeid = ( jsonECB1["chargecontrol"]["modeid"] | 0 ); Serial.print("stateid: "); Serial.println(stateid); Serial.print("modeid: "); Serial.println(modeid); // Now a couple of checks for green light to charge. This can be fully customised. // I optimised this so it meets my own personal requirement. // if state of battery > 90%, allow charging (this is the initial start condition) if (soc > 90.0) { allowCharge = 1; upperHysteresisFlag = 1; // clear counters (all except timesProductionVeryLow) timesProductionTooLow = 0; timesExternalPower = 0; } else if ((soc > 75.0) && (soc <= 90.0)) { // this soc case is handled differently // check for 10 minutes if the current production is too low if (p_pv < 2000.0) { timesProductionTooLow++; if (timesProductionTooLow > 60) timesProductionTooLow = 60; } else { timesProductionTooLow--; if (timesProductionTooLow < 0) timesProductionTooLow = 0; } // if within 10 minutes, more than 75% of the time the PV production decreases, stop charging // 60 * 0.75 = 45 if (timesProductionTooLow > 45) allowCharge = 0; else { if (upperHysteresisFlag) allowCharge = 1; else allowCharge = 0; } } else { // if soc is lower than 75%, stop charging allowCharge = 0; upperHysteresisFlag = 0; } // check for 5 minutes, if the pv production is extremely low. Then charging would be done just from the battery if (p_pv < 500.0) { timesProductionVeryLow++; if (timesProductionVeryLow > 30) timesProductionVeryLow = 30; } else { timesProductionVeryLow--; if (timesProductionVeryLow < 0) timesProductionVeryLow = 0; } // if the production is very low 75% of the time, then stop charging // 30 * 0.75 = 22.5 if (timesProductionVeryLow > 22) allowCharge = 0; // check for 5 minutes if power from the grid is needed to charge if (p_grid > 500.0) { timesExternalPower++; if (timesExternalPower > 30) timesExternalPower = 30; } else { timesExternalPower--; if (timesExternalPower < 0) timesExternalPower = 0; } // if within 5 minutes, more than 75% of the time external power has to be taken, stop charging // 30 * 0.75 = 22.5 if (timesExternalPower > 22) allowCharge = 0; Serial.print("timesProductionTooLow: "); Serial.println(timesProductionTooLow); Serial.print("timesProductionVeryLow: "); Serial.println(timesProductionVeryLow); Serial.print("timesExternalPower: "); Serial.println(timesExternalPower); Serial.print("allowCharge: "); Serial.println(allowCharge); Serial.print("upperHysteresisFlag: "); Serial.println(upperHysteresisFlag); Serial.print("modeid: "); Serial.println(modeid); Serial.println(); if (mode == 0) { // disable charging Serial.println("DISABLE Charging"); if ((stateid == 5) || (stateid == 4)) { // means is charging or is waiting to start again Serial.println("Stop Charge"); stopCharging(); } } if (mode == 1) { // set Eco Mode in eCB1 if (modeid == 2) setChargeMode(1); // if current mode is set to quick, change to eco if (allowCharge) { // send OK to eCB if (stateid == 17) { // means is off Serial.println("Start Chargeing"); startCharging(); } } else { // send Stop to eCB1 if (stateid == 5) { // means is charging Serial.println("Stop Charging"); stopCharging(); } } } if (mode == 2) { // set 11kW Mode in eCB1 if (modeid == 1) setChargeMode(2); // if current mode is set to eco, change to quick if (stateid == 17) { // means is off Serial.println("Start Chargeing"); startCharging(); } } // now the OLED oled.clear(); String space = ""; if (count >= 5) space = " "; oled.print(space); if (mode == 0) oled.println(" OFF "); if (mode == 1) { oled.println(" ECO AUTO "); oled.print(space); if (stateid == 17) { // means is off oled.println(" stop "); } if (stateid == 5) { // means is charging oled.println(" charging "); } if (stateid == 4) { // charging done oled.println(" complete "); } } if (mode == 2) { oled.println(" 11kW "); oled.print(space); if (stateid == 17) { // means is off oled.println(" stop "); } if (stateid == 5) { // means is charging oled.println(" charging "); } if (stateid == 4) { // charging done oled.println(" complete "); } } delay(10000); // wait 10s (blocking) } ICACHE_RAM_ATTR void TACT0_ButtonPressed() { mode = 0; Serial.println("mode: Off"); } ICACHE_RAM_ATTR void TACT1_ButtonPressed() { mode = 1; Serial.println("Eco Auto Mode Set"); } ICACHE_RAM_ATTR void TACT2_ButtonPressed() { mode = 2; Serial.println("11kW Mode Set"); } void setChargeMode (int chargeMode) { // quick: curl -X POST --header 'Content-Type:encoded' --header 'Accept: text/html' -d 'pvmode=quick' 'http://ecb1.local/api/v1/pvmode' // eco: curl -X POST --header 'Content-Type:encoded' --header 'Accept: text/html' -d 'pvmode=eco' 'http://ecb1.local/api/v1/pvmode' HTTPClient http; http.begin(urlECB1 + "/pvmode"); http.addHeader("Content-Type", "encoded"); http.addHeader("Accept:", "text/html"); String httpRequestData = "pvmode=quick"; if (chargeMode == 1) { httpRequestData = "pvmode=eco"; } Serial.println(httpRequestData); int httpResponseCode = http.POST(httpRequestData); Serial.println("setChargeMode: "); Serial.println(httpResponseCode); http.end(); } void startCharging () { // start: curl -X POST --header 'Content-Type: application/json' --header 'Accept: text/html' --header 'Content-Length: 0' 'http://ecb1.local/api/v1/chargecontrols/1/start' HTTPClient http; http.begin(urlECB1 + "/chargecontrols/1/start"); http.addHeader("Content-Type", "application/json"); http.addHeader("Content-Length", "0"); http.POST("start"); http.end(); } void stopCharging () { // stop: curl -X POST --header 'Content-Type: application/json' --header 'Accept: text/html' --header 'Content-Length: 0' 'http://ecb1.local/api/v1/chargecontrols/1/stop' HTTPClient http; http.begin(urlECB1 + "/chargecontrols/1/stop"); http.addHeader("Content-Type", "application/json"); http.addHeader("Content-Length", "0"); http.POST("stop"); http.end(); }