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
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