Skip to main content

EV Charger EVSE


 

Circuit Design


We made an EVSE from parts on Banggood & Aliexpress mostly

16A 3.7 kW EV charging controller board + Type 1 EV 

Current Sensor

This is the key component for controlling charge level and auto shut off at the end of the charge. Probably saves a wee bit of energy with the power to the EVSE disconected in the off state also.

    Irms = emon1.calcIrms(1480);  // Calculate Irms only	  
	Kw = (Irms * 230.0) / 1000.0;  
	float hours = (millis() - lastMillis) / (1000.0 * 3600.0) ;
	TotalChargeHours += hours;
	lastMillis = millis();  

	//CALC KWH
	if(abs(Kw)  > 0.1)
       KWh += (Kw * hours);

Note: You must add a suitable burden resistor if one is not on the CT board already or the high voltage will damage the Arduino. The one shown has a burden resistor.

PCB size30.0mm X 24.0mm X 1.6mm
Compatible interfaces2.54 3-pin interface and 4-pin Grove interface
Transformation coefficient1000:1
Input current0 - 5 A
Output current0 - 5 mA
Sampling resistor200Ω
Sampling voltage0 - 1 V
Working frequency20 - 20000 Hz
Working temperature-55 - 85 ℃
Dielectric strength6 - KAC/1min


Main Power Relay



Arduino Wemos D1 Mini




	
  
  
  // This example uses an Adafruit Huzzah ESP8266
// to connect to shiftr.io.
//

#include 
#include 
#include 
#include "EmonLib.h"             // Include Emon Library
#include 

EnergyMonitor emon1;             // Create an instance
double Irms = 0.0;
double Kw = 0.0;


#define REALY_PIN D1
#define LED_PIN LED_BUILTIN //LED_BUILTIN is built in LED

const char ssid[] = "???????????????";
const char pass[] = "???????????????";

WiFiClient net;
MQTTClient MQTTclient;


String inputString = "";         // a String to hold incoming data
bool stringComplete = false;  // whether the string is complete

unsigned long UpdateCount = 6;
unsigned long PacketCount = 0;
int RelayState  = HIGH;
unsigned long lastMillis = 0;
unsigned long PowerOnMillis = 0;
float KWh = 0.0;
unsigned long  ChargeKWh = 999;
float TotalChargeHours = 0.0;


// EEPROM data
struct { 
  float  TotalKWh = 0.0;
  float TotalChargeHours = 0.0;
  unsigned long  ChargeKWh = 999;
} SavedData;

uint addr = 0;

void setup() {

  pinMode(REALY_PIN, OUTPUT);
  digitalWrite(REALY_PIN, RelayState);

  pinMode(LED_PIN, OUTPUT);

  emon1.current(0, 13.8875);   //13.8875  or 111.1  // Current: input pin, calibration. 

  // commit 512 bytes of ESP8266 flash (for "EEPROM" emulation)
  // this step actually loads the content (512 bytes) of flash into 
  // a 512-byte-array cache in RAM
  EEPROM.begin(512);

  

  // read bytes (i.e. sizeof(data) from "EEPROM"),
  // in reality, reads from byte-array cache
  // cast bytes into structure called data
  EEPROM.get(addr,SavedData);
  Serial.println("Old values are: TotalKWh: "+String(SavedData.TotalKWh)+", ChargeKWh: "+ String(SavedData.ChargeKWh));

  KWh = SavedData.TotalKWh;
  ChargeKWh = SavedData.ChargeKWh;
  TotalChargeHours =  SavedData.TotalChargeHours;
  
  KWh = 0.0;
  
  PowerOnMillis = millis();

  Serial.begin(115200);

  WiFi.begin(ssid, pass);

  // Note: Local domain names (e.g. "Computer.local" on OSX) are not supported by Arduino.
  // You need to set the IP address directly.
  MQTTclient.begin("192.168.1.71", net);
  MQTTclient.onMessage(messageReceived);

  WiFiConnect();

  MQTTclient.publish("/charger/status", "run");

  JSONVar myArray;
  
  myArray[0] = SavedData.TotalKWh;
  myArray[1] = SavedData.ChargeKWh;
  myArray[2] = SavedData.TotalChargeHours;
  
  String jsonString = JSON.stringify(myArray);
    
  MQTTclient.publish("/charger/eeprom", jsonString );
  Serial.println("/charger/eeprom" + jsonString );
}


void loop() {
  
  MQTTclient.loop();

  delay(10);  // <- -="" a="" client.connected="" every="" fixes="" if="" issues="" lastmillis="" message="" millis="" publish="" roughly="" second.="" some="" stability="" wifi="" wificonnect="" with=""> (UpdateCount * 1000)) 
  {
    Irms = emon1.calcIrms(1480);  // Calculate Irms only
  
    Kw = (Irms * 230.0) / 1000.0;  
  
    float hours = (millis() - lastMillis) / (1000.0 * 3600.0) ;
    TotalChargeHours += hours;
    lastMillis = millis();    
  
    //CALC KWH
    if(abs(Kw)  > 0.1)
    {
      KWh += (Kw * hours);
      
      SaveKWh();

      if(ChargeKWh > 0)
      {
         if((unsigned long)KWh >= ChargeKWh)
         {
            RelayState  = LOW;
            digitalWrite(REALY_PIN, RelayState);
            MQTTclient.publish("/charger/status", "charge complete " + String(KWh) );
         }
         else
         {
            RelayState  = HIGH;
            digitalWrite(REALY_PIN, RelayState);
            MQTTclient.publish("/charger/status", "charger ON");           
         }
      }
    }
      
    SendSensors();    
    
  }

  delay(100); 
}


void WiFiConnect() {
  
  Serial.print("checking wifi...");
  Serial.println(ssid);
  
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
    digitalWrite(LED_PIN, LOW);
    delay(500);
    digitalWrite(LED_PIN, HIGH);
  }
  
  Serial.println("\nWiFi Connected! ");
  Serial.println( WiFi.localIP());

  Serial.print("\nMQTT connecting...");
  while (!MQTTclient.connect(WiFi.localIP().toString().c_str() , "try", "try")) {
    Serial.print(".");
    delay(800);
    digitalWrite(LED_PIN, LOW);
    delay(200);
    digitalWrite(LED_PIN, HIGH);
  }

  Serial.println("\nMQTT Connected!");

  MQTTclient.subscribe("/charger/control");  

  MQTTclient.publish("/charger/status", "start");
  MQTTclient.publish("/charger/localip", "/charger/localip/" + WiFi.localIP().toString());
  
  digitalWrite(LED_PIN, HIGH);  
}

void messageReceived(String &topic, String &payload) {
  
  Serial.println("incoming: topic:" + topic + "  Payload:" + payload);

  digitalWrite(LED_PIN, LOW);

  if(topic.startsWith("/charger/control"))
  {
    if(payload.startsWith("ChargeKWh"))
    {
       ChargeKWh = (unsigned long)payload.substring(10).toInt();
       KWh = 0.0;
       TotalChargeHours = 0.0;
       
       SaveKWh();
     
       if(ChargeKWh == 0)
       {
          RelayState  = LOW;
          digitalWrite(REALY_PIN, RelayState);
          MQTTclient.publish("/charger/status", "charge complete " + String(KWh) );
       }
       else
       {
          RelayState  = HIGH;
          digitalWrite(REALY_PIN, RelayState);
          MQTTclient.publish("/charger/status", "charger ON");           
       }
      
    }
    else if(payload.startsWith("UpdateCount"))
    {
      UpdateCount = (long)payload.substring(12).toInt();
    }
    else if(payload.startsWith("read"))
    {
      SendSensors();
      MQTTclient.publish("/charger/localip", "/charger/localip/" + WiFi.localIP().toString());
    }
    else if(topic.indexOf("control") > 0)
    {
      if(payload.startsWith("ON"))
      {
        PowerOnMillis = millis();
        RelayState  = HIGH;
        digitalWrite(REALY_PIN, RelayState);
        MQTTclient.publish("/charger/status", "charger ON");
      }
      else if(payload.startsWith("OFF"))
      {
        PowerOnMillis = 0;
        RelayState  = LOW;
        digitalWrite(REALY_PIN, RelayState);
        MQTTclient.publish("/charger/status", "charger OFF");
      }
      SendSensors();      
    }
  
    MQTTclient.publish("/charger/status", topic + "/" + payload);
    
    digitalWrite(LED_PIN, HIGH);
  }
}

void SaveKWh()
{

    SavedData.TotalKWh = KWh;
    SavedData.ChargeKWh = ChargeKWh;
    SavedData.TotalChargeHours = TotalChargeHours;

    // replace values in byte-array cache with modified data
    // no changes made to flash, all in local byte-array cache
    EEPROM.put(addr,SavedData);
    
    // actually write the content of byte-array cache to
    // hardware flash.  flash write occurs if and only if one or more byte
    // in byte-array cache has been changed, but if so, ALL 512 bytes are 
    // written to flash
    EEPROM.commit();  

      
    JSONVar myArray;
    
    myArray[0] = SavedData.TotalKWh;
    myArray[1] = SavedData.ChargeKWh;
    myArray[2] = SavedData.TotalChargeHours;
    
    String jsonString = JSON.stringify(myArray);
    
    MQTTclient.publish("/charger/eeprom", jsonString );
    Serial.println("/charger/eeprom" + jsonString );

}

void SendSensors()
{
    digitalWrite(LED_PIN, LOW);

    int rssi = WiFi.RSSI();
    
    JSONVar myArray;
    
    myArray[0] = PacketCount++;
    myArray[1] = Irms;
    myArray[2] = Kw;
    myArray[3] = RelayState;
    myArray[4] = KWh;    
    myArray[5] = ChargeKWh;    
    myArray[6] = rssi;

    String jsonString = JSON.stringify(myArray);
    
    MQTTclient.publish("/charger/sensors", jsonString );
    Serial.println("/charger/sensors" + jsonString );

    digitalWrite(LED_PIN, HIGH);

}

  
  

Hubitat Dashboard



Hubitat Device




Hubitat Driver Code

	

metadata {

metadata {
    definition(name: "Greenway MQTT Charger Driver", namespace: "Greenway", author: "Nick Goodey", importURL: "https://raw.githubusercontent.com/shomegit/MQTT-Virtual-Switch-Control-Driver/master/MQTT-Virtual-Switch-Control-Driver.groovy") {
        capability "Initialize"
        capability "Switch"
        capability "Switch Level"
        capability "Sensor"
        capability "Polling"
        capability "Battery"
     
        command "on"
        command "off"
        command "ClearStates" // Clear all device states

        attribute "switch", "string"
        attribute "switch", "ENUM", ["on", "off"]

        //CHARGER
        attribute "ChargeAmps", "Number"
        attribute "ChargeRate", "Number"
        attribute "ChargeLimit", "Number"
        attribute "ThisCharge", "Number"
        attribute "ThisChargeKw", "Number"
        attribute "ChargerTile", "String"
        attribute "EnergyCost", "number"

    }

    preferences {

        input name: "MQTTBroker", type: "text", title: "MQTT Broker Address:", required: true, displayDuringSetup: true
        input name: "username", type: "text", title: "MQTT Username:", description: "(blank if none)", required: false, displayDuringSetup: true
        input name: "password", type: "password", title: "MQTT Password:", description: "(blank if none)", required: false, displayDuringSetup: true
        input name: "topicSub", type: "text", title: "Topic to Subscribe:", description: "Example Topic (topic/device/#)", required: false, displayDuringSetup: true
        input name: "topicCon", type: "text", title: "Topic to control:", description: "Example Topic (topic/device/#)", required: false, displayDuringSetup: true
        input name: "topicPoll", type: "text", title: "Topic to poll device:", description: "Example Topic (topic/device/#)", required: false, displayDuringSetup: true

        input("logEnable", "bool", title: "Enable logging", required: true, defaultValue: true)

         input("DollarsPerKwh", "number", title: "Price of energy in NZ\$ per KWh", required: false, defaultValue: 0.08)

    }

}


def installed() {
    log.info "installed..."
}


def poll() {

    try {
        topic = settings?.topicPoll
        if (logEnable) log.debug "Poll: $topic/read"
        interfaces.mqtt.publish(topic, "read", 1, false)

    } catch (e) {
        log.error "Device Poll error: ${e.message}"
    }

}

// Parse incoming device messages to generate events
def parse(String description) {

        did = device.getId()

        state.DeviceID = did

        // parse message
        mqtt = interfaces.mqtt.parseMessage(description)

        if (logEnable) log.debug mqtt.topic
        if (logEnable) log.debug mqtt.payload

        state.topic = mqtt.topic

        json = new groovy.json.JsonSlurper().parseText(mqtt.payload)

         ParseCharger()


            //log.debug "$topic"
            //log.info "$topic OFF"

            //RELAY    
            if (state.Relay != json[0]) {
                state.Relay = json[0]

                if (json[0] == 1) {
                    sendEvent(name: "switch", value: "on")
                } else if (json[0] == 0) {
                    sendEvent(name: "switch", value: "off")
                }
            }

    
}


def ParseCharger() {

    /*
    CHARGER
    myArray[0] = PacketCount++;
    myArray[1] = Irms;
    myArray[2] = Kw;
    myArray[3] = RelayState;
    myArray[4] = KWh;    
    myArray[5] = ChargeKWh;    
    myArray[6] = rssi;
    */

    //log.debug json

    //RELAY    
    if (state.Relay != json[3]) {
        state.Relay = json[3]

        if (json[3] == 1) {
            sendEvent(name: "switch", value: "on")
        } else if (json[3] == 0) {
            sendEvent(name: "switch", value: "off")
        }
    }
    
    if (json[3] == 1) 
       tileHTML = "
Relay ON
" else tileHTML = "
Relay OFF
" //CHARGE RATE String val = json[2] String units = "KW" Double dval = Double.parseDouble(val).round(3) //ChargeKW = dval; sendEvent(name: "ChargeRate", value: "$dval") tileHTML += "Charge Rate $dval $units
" //CHARGE LIMIT KW val = json[5] state.ChargeLimit = val units = "%" dval = ((Double.parseDouble(val) / 30) * 100).round(1) sendEvent(name: "ChargeLimit", value: "$dval") sendEvent(name: "level", value: dval) tileHTML += "Add $dval %
" //CHARGE TOTAL val = json[4] state.ThisCharge = val units = "%" dval = ((Double.parseDouble(val) / 30) * 100).round(1) sendEvent(name: "ThisCharge", value: "$dval") tileHTML += "Added $dval $units
" dval = Double.parseDouble(val).round(1) sendEvent(name: "ThisChargeKw", value: "$dval") tileHTML += "Charge $dval KWh
" //DollarsPerKwh = DPKwh = Double.parseDouble(settings?.DollarsPerKwh) units = "NZ\$" dval = (Double.parseDouble(val) * DPKwh).round(2) sendEvent(name: "EnergyCost", value: "$dval") tileHTML += "Cost $units $dval
" //CHARGE AMPS val = json[1] units = "A" dval = Double.parseDouble(val).round(3) sendEvent(name: "ChargeAmps", value: "$dval") sendEvent(name: "ChargerTile", value: "$tileHTML") } def updated() { if (logEnable) log.info "Updated..." initialize() } def uninstalled() { if (logEnable) log.info "Disconnecting from mqtt" interfaces.mqtt.disconnect() } def initialize() { if (logEnable) runIn(900, logsOff) state.ThisCharge = 0 state.ChargeLimit = 0 state.Relay = -1 MQTTconnect() unschedule() schedule("0/10 * * * * ? *", MQTTconnect) //schedule("0/10 * * * * ? *", ZeroDailyCounters) schedule("0 1 0 1/1 * ? *", ZeroDailyCounters) } def ZeroDailyCounters() { log.debug "ZeroDailyCounters" } def MQTTconnect() { try { def mqttInt = interfaces.mqtt if (mqttInt.isConnected()) { //log.info "Connected to: $MQTTBroker $topicSub" return } def clientID = "hubitat-" + device.deviceNetworkId state.clientID = clientID //open connection mqttbroker = "tcp://" + settings?.MQTTBroker + ":1883" mqttInt.connect(mqttbroker, clientID, settings?.username, settings?.password) //give it a chance to start pauseExecution(500) mqttInt.subscribe(settings?.topicSub) log.info "Connection established: $MQTTBroker $topicSub" log.info "clientID: $clientID" log.info "Subscribed to: $topicSub" } catch (e) { log.error "MQTTconnect error: ${e.message}" } } def mqttClientStatus(String status) { log.error "MQTTStatus- error: ${status}" } def logsOff() { log.warn "Debug logging disabled." device.updateSetting("logEnable", [value: "false", type: "bool"]) } def off() { try { topic = settings?.topicCon log.info "$topic OFF" interfaces.mqtt.publish(topic, "OFF", 1, false) sendEvent(name: "switch", value: "off") } catch (e) { log.error "MQTTconnect error: ${e.message}" } } def on() { try { topic = settings?.topicCon log.info "$topic ON" interfaces.mqtt.publish(topic, "ON", 1, false) sendEvent(name: "switch", value: "on") } catch (e) { log.error "MQTTconnect error: ${e.message}" } } def setLevel(value, rate = null) { //e.g. ChargeKWh 3 kwh = ((value / 100) * 30); String cmnd = "ChargeKWh $kwh" log.debug cmnd topic = settings?.topicCon interfaces.mqtt.publish(topic, cmnd, 1, false) topic = settings?.topicCon interfaces.mqtt.publish(topic, "read", 1, false) if (value == 0) { sendEvent(name: "switch", value: "off") } sendEvent(name: "level", value: value) } private def displayDebugLog(message) { if (logEnable) log.debug "${device.displayName}: ${message}" } private def displayInfoLog(message) { log.info "${device.displayName}: ${message}" } /** * Clear States * * Clears all device states * **/ def ClearStates() { log.warn("ClearStates(): Clearing device states") state.clear() ZeroDailyCounters() }

Popular Posts

The Heat Exchanger

  Under construction It took around 6 hours to layer the core Based on a 60 Litre Sistema box with a 400 X 400 X 350 6 mm Coroplast core. The fan speed controller is mounted on the end so it takes a 12V 10 Amp supply from the power system and 2 analogue 0 to 5 V signals for speed demand. The pink foam is a NZ$12 exercise matt which insulates it for both sound and heat quite nicely. 60 Litre Plenum on the vent side See  https://greenwayerv.blogspot.com/2020/10/we-added-60-litre-plenum-on-vent-side.html

Home Ventilation Heat Exchanger

 

The Experiment

First things first an experiment. I had been looking around at various homemade ERV core designs and eventually decided to try a Coroplast design. The experimental core was tiny 12 X 12 X 12 cm  using 3 mm Coroplast Nice isn't it :) 20 80 mm PC cooling fans at a fixed speed four temperature probes. Here is the full scale one around 40 X 40 X 35 cm using 6 mm Coroplast A Wemos D1/R2 for instrumentation sending temperatures via MQTT to Node-Red. I'm was not totally convinced by the numbers but it did definitely exchange significant amounts of heat. I checked the calibration and it was good compared to my multi-meter TC probe. In any case it seems to work even at such a small scale. Never did understand why it seems to gain more heat that it lost from the exhaust steam, checked for leaks there were none. Any way  a hairdryer experiment showed it worked and heat was transferred too so I was happy that it would probably work at a larger scale.

Heat Exchanger Low Ambient 86% Heat Recovery

  Drop on exiting air 2.9C Heating of incoming air 2.5C So 86% of the heat recovered

The ERV Data Acquisition System

  Progress over the weekend with the ERV DAS The 999's are me testing the sensor disconnected response. I have a Pitot tube left over from an RC plane I think I might put it in the inlet air stream Pin assignment in the Arduino sketch DHT dht[ 4 ] = {DHT( 4 , DHTTYPE),DHT( 0 , DHTTYPE),DHT( 2 , DHTTYPE),DHT( 14 , DHTTYPE)}; There are 4 DHT22's inside heat exchanger lid, shown below. Sensor locations Arduino code #include < Arduino_JSON . h > #include < ESP8266WiFi . h > #include < MQTT . h > #define LED_PIN LED_BUILTIN //LED_BUILTIN is built in LED #include "DHT.h" // Uncomment whatever type you're using! #define DHTTYPE DHT11 // DHT 11 //#define DHTTYPE DHT22 // DHT 22 (AM2302), AM2321 //#define DHTTYPE DHT21 // DHT 21 (AM2301) WiFiClient net ; MQTTClient MQTTclient ; const char ssid [ ] = "Network" ; const char pass [ ] = "DF@#$%" ; // the IP address for the MQTT server char MQTTip [

Drivers and Arduino Sketches for ERV Fans Control Testing

  The Hookup For proof of concept seems to run reliably in the lab. I don't want to be up and down ladders to make update I intend keeping the control system in Hubitat so I can tinker to my Hearts content. Boost fan PWM control as well as start relays. The valve position is a separate driver and device in Hubitat, This will use one of the PWM Four boost fans in the ducts for each room inlet. One fan in the two way turbo exhaust valve. The valve position is a separate driver and device in Hubitat, This will use one of the PWM pins on the Wemos and its analogue input for the position pot Hubitat Valve Position Driver import groovy.json.JsonSlurper metadata { definition(name: "Greenway ERV Turbo Valve", namespace: "Greenway", author: "Nick Goodey") { capability "Initialize" capability "Switch" capability "Switch Level" command "on" command "off" } preferences { section("Device&qu