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)

https://www.onderka.com/hausautomation-technik-und-elektronik/nodemcu-json-daten-von-fronius-wechselrichter-einlesen

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();
}