Hubitat Controlled Alexa enabled EV Charger EVSE level 2 Nissan Leaf 16 Amp 3.6KW

Hubitat Driver
The driver lets you set the percentage to add to the battery if you don't want a full charge. Operating the battery between 20 and 80% is said to be better for HV battery life. The Hubitat driver does all the math and send the setpoints to the Arduino and allows monitoring the charge state using MQTT. My apologies in advance I'm lazy and I know there is a magic number in the driver dval = ((Double.parseDouble(val) / 30) * 100).round(1) for a 30KWH battery, there may be more in the Arduino like calibrations.

By adding the charger device which is a dimmer to the Hubitat Amazon Echo Skill you can plug in and tell Alexa "Alexa:... car charger 50%" and the charger will add 50% and then turn off.

WiFi MQTT Arduino

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

Circuit Design

The pin allocation details can be determined from the Arduino sketch

EVSE 16A 3.7 kW  (2)

The actual EVSE controller is a bought one, so you likley won't damage your cars charger or electronics unless you wire the plug incorrectly of course.

And the remaining major components

Main power relay
Main disconnect above and beyond the relays on the EVSE board. This allows us to control the charge.

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.

Type 2 Socket
This approach lets you use your existing Type 2 charging lead

This suites the follwing type of lead and as Type 2 is a standard you can purchase the appropriate leat for your car. This one suits a Nissan Leaf which is J1772

You need a suitable strong box for it mine is a IP55 composite.

Arduino Sketch

Runs fine on a Wemos D1 Mini baord

	#include <ESP8266WiFi.h>
	#include <MQTT.h>
	#include <Arduino_JSON.h>
	#include "EmonLib.h"             // Include Emon Library
	#include <EEPROM.h>

	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[] = "Cosmo";
	const char pass[] = "Fairline19!";

	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);  // <- fixes some issues with WiFi stability

	  if (!MQTTclient.connected()) {
		WiFiConnect();
	  }
	  

	  // publish a message roughly every second.
	  if (millis() - lastMillis > (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);

	}

Being used in anger at Arthurs pass motel New Zealand

4 Likes

wow that's really cool. I often wish I had any electrical prowess then I too could do cool stuff like this.

Glad you like it I've been tinkering with electrical stuff since I was a kid. A lot of people aren't comfortable with mains voltage stuff. I love the Hubitat hub keeps me entertained when I need a tech fix, node-red is not as productive I found and not as reliable too many cooks probably.

Great project man. Well done.
I hope you kept the Kea’s away from those power cords ?

I know what you mean they are little buggers! The cables are really tough actually

1 Like

I absolutely love this project. I will incorporate it into my Solar system. Should my day be cloudy I must fire up a natural gas power generator to charge the main 21 Kwh batteries. This will advance that project by leaps & bounds.

Thank you very much for this post!