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