Skip to main content

EV Charger



Alexa Integration

 

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