[BETA] OpenEVSE driver

Wanted to get something out there. This reads the current charging rate and lets you set the target rate in amps.
Don't have a GitHub repo for this yet, need to resurrect my login credentials.

/**
 *
 *  File: OpenEVSE.groovy
 *  Platform: Hubitat
 *
 *  Requirements:
 *     1) openEVSE EV Charger https://openevse.com/index.html
 *
 *  Copyright (c)Tom Duffy
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *  Change History:
 *
 *    Date        Who            What
 *    ----        ---            ----
 *    2022-06-28  Tom Duffy    Original Creation

 * Notes : does not do checksums, how could a TCP connection ever corrupt data?
 *
 */

def version() {"v0.1"}

metadata {
    definition (name: "OpenEVSE", namespace: "tomuo", author: "Tom") {
        capability "Initialize"
        capability "Refresh"
        capability "PowerMeter"
        capability "EnergyMeter"
        capability "Presence Sensor"  // used to determine is the openEVSE microcontroller is still reporting data or not

        attribute  "Status", "string"
        attribute  "Firmware", "string"

        command "ChargeRate", [[name:"chargerate", type:"NUMBER", description:"Max Charge Amps"]]
    }
    preferences
    {
        input "deviceIP", "text", title: "openEVSE IP Address", description: "in form of 192.168.1.138", required: true, displayDuringSetup: true
        input "pollingInterval", "number", title: "Polling Interval", description: "in seconds", range: "10..300", defaultValue: 15, displayDuringSetup: true
        input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
    }
}


def logsOff() {
    log.warn "debug logging disabled..."
    device.updateSetting("logEnable",[value:"false",type:"bool"])
}

def refresh() {
   polling() 
}

def installed() { // built in
    initialize()
}

def updated() {    // built in
    log.info "updated() called"
    unschedule()
    if (logEnable) runIn(1800, logsOff)
    initialize()
}

def initialize() {    // built in
    state.version = version()
    log.info "initialize() called"
    if (deviceIP) {
        doRapi('GV')                    // Get Version
 //       doRapi('ST%2021%2010%208%2030')    // sleep timer
        polling()
    }
    else {
        log.warn "Please enter the openEVSE IP Address and then click SAVE"
    }
}

def polling()
{
    getData()
    runIn(pollingInterval, polling)
}


def getData()
{
    doRapi('GG')                        // Get Gauges
    doRapi('GS')                        // Get State
    doRapi('GU')                        // Get Usage
}


void ChargeRate(amps)
{
    log.info("charge ${amps}")
    if (amps < 6) {
        doRapi('FD')    // disable
    }
    else {
        doRapi('FE')    // enable
        doRapi('SC%20' + amps.toString())
    }
    polling()
}


def doRapi(query)
{
    def params = [
        uri: "http://${deviceIP}/r?json=1&rapi=\$${query}",
        contentType: "application/json",
        requestContentType: "application/json",
        timeout: 5
    ]
    
    if (deviceIP) {
        try {
            httpGet(params){response ->
                if(response.status != 200) {
                    if (device.currentValue("presence") != "not present") {
                        sendEvent(name: "presence", value: "not present", descriptionText: "openEVSE not responding")
                    }
                    log.error "Received HTTP error ${response.status}. Check your IP Address and openEVSE!"
                }
                else {
                    if (device.currentValue("presence") != "present") {
                        sendEvent(name: "presence", value: "present", isStateChange: true, descriptionText: "openEVSE responding")
                    }
                    def (ret, checksum) = response.data.ret.tokenize('^')        // ignoring checksum after the carat
                    cmd = response.data.cmd.substring(1)
                    switch (cmd) {
                        case "GG":    // $GG -> $OK 9900 240000
                            def (status, milliamps, millivolts) = ret.tokenize(' ')
                            if (status == "\$OK") {
                                float amps = (milliamps as float) / 1000.0
                                float volts = (millivolts as float) / 1000.0
                                if (logEnable) {
                                    log.debug "GG ${status} ${milliamps} ${millivolts} ${amps} ${volts}"
                                }
                                powerF = amps * volts
                                powerI = powerF.toInteger()
                                sendEvent(name: "power", value: powerI, unit: "W")
                            }
                            break
                        case "GS":      // $GS -> $OK fe 10636 03 0200
                            def (status, evsestate, elapsed, pilotstate, vflags) = ret.tokenize(' ')       // hex seconds hex hex
                            int flags = Integer.decode('0x' + vflags)
                            if (evsestate == "00") {
                                evsestate = "Unknown"
                            }
                            else if (evsestate == "fe") {
                                evsestate = "Sleeping"
                            }
                            else if (evsestate == "ff") {
                                evsestate = "Disabled"
                            }
                            else if (evsestate == "01") {
                                evsestate = "idle"
                            }
                            else if (evsestate == "02") {
                                evsestate = "ready"
                            }
                            else if (evsestate == "03") {
                                evsestate = "charging"
                            }
                            if (flags & 0x0100) {
                                evsestate += ", Connected"
                            }
                            else {
                                evsestate += ", No EV"
                            }
                            sendEvent(name: "Status", value: evsestate)
                            break
                        case "GV":     // $OK firmware_version protocol_version
                            def (status, firmware, protocol) = ret.tokenize(' ')
                            if (status == "\$OK") {
                                sendEvent(name: "Firmware", value: firmware)
                            }
                            break
                        case "GM":      // $OK voltcalefactor voltoffset
                            def (status, scalefactor, offset) = ret.tokenize(' ')
                            if (logEnable) {
                                log.debug "GM ${status} ${scalefactor} ${offset}"
                            }
                            break
                        case "GU":        // OK Wattseconds Whacc
                            def (status, sessionwsecs, lifetimewh) = ret.tokenize(' ')
                            if (status == "\$OK") {
                                float session = (((sessionwsecs as float) / 3600.0) + 50) / 1000.0
                                session = session.round(1)
                                sendEvent(name: "energy", value: session, unit: "kWh")
                                float lifetime = (lifetimewh as int) / 1000.0
                                lifetime = lifetime.round(0)
                                if (logEnable) {
                                    log.debug "Session ${session}kWh Lifetime ${lifetime}"
                                }
                            }
                            break;
                        case "SC":        // Set Current.
                        case "FE":        // Enable
                        case "FD":        // Disable
                        default:
                            if (logEnable) {
                                log.debug "Unhandled response from openEVSE CMD = ${response.data.cmd} ret = ${ret}"
                            }
                            break
                    }
                }
            }
        } catch (Exception e) {
            if (device.currentValue("presence") != "not present") {
                sendEvent(name: "presence", value: "not present", descriptionText: "Error trying to communicate with openEVSE device")
            }
            log.warn "openEVSE Server Returned: ${e}"
            if (e == "java.net.NoRouteToHostException: No route to host (Host unreachable)") {
                //Give the openEVSE extra time to recover
                runIn((pollingInterval * 2), polling)
            }
        } 
    } else {
        log.error "IP Address '${deviceIP}' is not properly formatted!"
    }
}


def uninstalled()
{
    unschedule()
}


3 Likes

Sweet. If this works, I might have to buy one of those. I miss my "You forgot to plug in" notification I had back when I charged on 120v.

Thanks. I got this working once I realized that I needed to remove the USERNAME/Password from my OpenEVSE. It works great. By any chance have you updated it to include the ability to send Sleep & Enable commands? It would be great if these could mimic a switch.

So I ended up adding a virtual switch and creating rules that can toggle the EVSE between Sleep and Enable and will also update the switch if the mode of the EVSE changes.

1 Like

Cool.
I have been working on this driver and a small app to do "divert only excess solar to the EVSE" calculation.
It generally works but is not really stable enough to release - the OpenEVSE WiFi module has had a couple of major updates in the meantime, and I think it's pretty buggy. They have their own solar diversion built in, and I've seen it where it fights the commands I send.
Sends "Enable"
Log shows "Enable" then "Disable"

Also I got it stuck once where it stayed at 6A minimum rate charging, ignoring all commands sent.
The Web UI was able to unstick it, so the remote API is definitely a second class citizen.

It feels weird that the API command to modulate the charge rate is changing the "max circuit capacity" value, instead of the current pilot value, and the actual max circuit capacity value isn't settable (and always reads as 80amp from the API).

So I'll be following the github development of the WiFi firmware until they stabilize.

For the enable/disable, my current solution is to incorporate it into the charge current request.
Anything under 6Amp normally gets rounded to 6A in the EVSE, but I change it to disable.
That was useful for the solar diversion app, but my EV doesn't like it when charging gets interrupted and restarted more than once, it goes into an error state and won't charge again until turned off/on via the key.
That's fine if the charging ends because the sun went down, but not ideal if the sky is cloudy and the excess solar is barely enough to maintain 6A. I'm still thinking about hysterisis for that, and incorporating with TOU hours.

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.