Lua - dzurikmiroslav/esp32-evse GitHub Wiki

Script capability allows custom functionality of controller, is based on Lua.

Script settings are in web interface, when you can enable/disable this feature and browsing print output. In Files web interface, you can edit Lua sources. At startup file /usr/lua/init.lua (or /usr/lua/init.luac if it's provided) will be executed. Lua sources also can be edited through WebDAV, just open in file browser url: dav://<IP>/dav/usr/. I recommended this way, because you can use favorite Lua IDE...

In addition to the standard Lua functionality, there is additional modules for controlling charging controller:

Component module

This module provide core functionality for registering user components. Globally loaded, not need require this module.

At start, only once ist called startup file (/usr/lua/init.lua), When you need write some functionally stuff, which is called periodically or on some event must be provided as component. Component is based on Lua coroutines and adds user friendly persistent configuration trought parameters. Currently registered components are displayed in web interface, also with parameters form.

Function Signature Description
component.register (table):nil Register user component

Component table definition

Key Signature Description
id string Component unique id
name string Component name
description string|nil Component description
params table|nil Parameters definition
start (table|nil):coroutine Function where is created coroutine, argument is parameters value table

Component parameter table definition

Key Signature Description
type string Type of parameter, possible values: "string"|"number"|"boolean"
name string Parameter name
default string|number|boolean|nil Default value

Component example:

component.register({
    id = "component1",
    name = "Test component 1",
    description = "The test component 1",
    params = {
        message = { type = "string", name = "Message", default = "Hello world!" }
    },
    start = function(params)
        message = params.message
        return coroutine.create(function()
            while true do
                print(message)
                coroutine.yield(1000)
            end
        end)
    end
})

Created coroutine in start function may have infinity loop, but must be in it placed calling coroutine.yield. Paramter of coroutine.yield is optional number, which mean number of miliseconds wich coroutine is ommited from resumining.
No need to worry about started coroutine terminating, is terminated during script VM reloading or when user change component parameter values.

This will looks like in web interface:

Script settings

Evse module

Root level module, for controlling charging controller.

Constant Description
evse.STATEA State A
evse.STATEB1 State B1
evse.STATEB2 State B2
evse.STATEC1 State C1
evse.STATEC2 State C2
evse.STATED1 State D1
evse.STATED2 State D2
evse.STATEE State E
evse.STATEF State F
evse.ERRPILOTFAULTBIT Error pilot_fault
evse.ERRDIODESHORTBIT Error diode_short
evse.ERRLOCKFAULTBIT Error lock_fault
evse.ERRUNLOCKFAULTBIT Error unlock_fault
evse.ERRRCMTRIGGEREDBIT Error rcm_triggered
evse.ERRRCMSELFTESTFAULTBIT Error rcm_selftest_fault
evse.ERRTEMPERATUREHIGHBIT Error temperature_high
evse.ERRTEMPERATUREFAULTBIT Error temperature_fault
Function Signature Description
evse.getstate ():number Get state, possible values evse.STATE...
evse.geterror ():number Get error bits, possible values evse.ERR... bits
evse.getenabled ():boolean Get charging enabled
evse.setenabled (boolean):nil Set charging enabled
evse.getavailable ():boolean Get controller available
evse.setavailable (boolean):nil Set controller available
evse.getchargingcurrent ():number Get charging current in A
evse.setchargingcurrent (number):nil Set charging current in A
evse.getpower ():number Get charging power in W
evse.getchargingtime ():number Get charging time in s
evse.getsessiontime ():number Get session time in s
evse.getconsumption ():number Get consumption in Wh
evse.getvoltage ():number,number,number Get voltages in V
evse.getcurrent ():number,number,number Get current in A
evse.getlowtemperature ():number Get low temperature
evse.gethightemperature ():number Get high temperature

Boardconfig module

This module providing values from board.cfg. Not globally loaded, need require this module.

Constant Description
boardconfig.ENERGYMETERNONE Energy meter none
boardconfig.ENERGYMETERCUR Energy meter current
boardconfig.ENERGYMETERCURVLT Energy meter current and voltage
boardconfig.SERIALTYPENONE Serial type none
boardconfig.SERIALTYPEUART Serial type UART
boardconfig.SERIALTYPERS485 Serial type RS485
Value Description
boardconfig.devicename Name of the device (string)
boardconfig.proximity Has PP detection (boolean)
boardconfig.socketlock Has socket lock (boolean)
boardconfig.rcm Has residual current monitor (boolean)
boardconfig.energymeter Energy meter (number), possible values boardconfig.ENERGYMETER...
boardconfig.energymeterthreephases Is energy meter three phases (boolean)
boardconfig.serials Type of serials (number array), possible values boardconfig.SERIALTYPE...
boardconfig.onewire Has onewire bus (boolean)
boardconfig.temperaturesensor Has temperature sensor on onewire (boolean)
boardconfig.auxinputs AUX digital input names (string array)
boardconfig.auxoutputs AUX digital output names (string array)
boardconfig.auxanaloginputs AUX analog input names (string array)

Example:

local boardconfig = require("boardconfig")

print("device name:", boardconfig.devicename)

Aux module

This module providing access to AUX. Not globally loaded, need require this module.

Function Signature Description
aux.write (string):boolean Set digital output value
aux.read (string):boolean Get digital input value
aux.analogread (string):number Get analog input value

Mqtt module

This module providing access to MQTT broker. Not globally loaded, need require this module.

Function Signature Description
mqtt.client (uri: string [, user: string] [, password: string]]):table Create mqtt client
mqtt.client:connect ():boolean Connects to the broker, durring wail call coroutine.yield, on succes return true
mqtt.client:disconnect ():nil Disconnect from the broker
mqtt.client:subscribe (topic: string, callback: function(topic: string, data: string)):nil Subscribe to topic
mqtt.client:unsubscribe (topic: string):nil Unsubscribe from topic
mqtt.client:publish (topic: string, data: string [, qos: number = 1] [, retry: number = 0]]):nil Publish message

Example:

local mqtt = require("mqtt")

local client = mqtt.client("mqtt://broker.hivemq.com:1883")

if client:connect() then
  client:subscribe("test/message", function(topic, data)
    print(topic, data)
  end)
end

Json module

This module providing JSON serialization & deserialization. Not globally loaded, need require this module.

Function Signature Description
json.encode (table|number|boolean|string|nil[, formated: boolean]):string Encode Lua value to JSON
json.decode (string):table|number|boolean|string|nil Decode from JSON to Lua value

Example:

local json = require("json")

local value = {
  num = 123,
  str = "abc",
  logical = true
}
print(json.encode(value, true))

Serial module

This module providing serial port communication. Not globally loaded, need require this module.

Function Signature Description
serial.open ():table|nil Open serial port, if is available or not already opened
serial:read ():string|nil Read bytes from RX buffer or return nil if is empty
serial:write (string):nil Write bytes to TX buffer
serial:flush ():nil Flush TX buffer

Example:

local serial = require("serial")

local port = serial.open()

local rcv = port:read()
print(rcv)

port:write("AT?")

Complex example

Here is example with two components, first handling stop button, second make integration to ThingsBoard cloud. For better code readability, each component is in its own script file, the main script init.lua imports them.

File /usr/lua/init.lua:

component.register(require("stopbutton"))
component.register(require("thingsboard"))

Fili /usr/lua/stopbutton.lua:

local evse = require("evse")
local aux = require("aux")
local boardconfig = require("boardconfig")

function contains(table, value)
    for _, v in ipairs(table) do
        if v == value then
            return true
        end
    end
    return false
end

return {
    id = "stopbutton",
    name = "Stop button",
    description = "Set charger not available when AUX input has targeted value",
    params = {
        aux = { type = "string", name = "Input name", default = "IN1" },
        activehigh = { type = "boolean", name = "Active high", default = true }
    },
    start = function(params)
        print("starting stopbutton...")

        if not contains(boardconfig.auxinputs, params.aux) then
            error("unknow aux input")
        end

        return coroutine.create(function()
            while true do
                local value = aux.read(params.aux)
                if (not params.activehigh) then
                    value = not value
                end
                if not value ~= evse.getavailable() then
                    evse.setavailable(not value)
                end
                coroutine.yield(1000)
            end
        end)
    end
}

File /usr/lua/thingsboard.lua:

local evse = require("evse")
local mqtt = require("mqtt")
local json = require("json")

return {
    id = "thingsboard",
    name = "ThingsBooard",
    description = "ThingsBooard MQTT connector",
    params = {
        token = { type = "string", name = "Access token" }
    },
    start = function(params)
        print("starting thingsboard connector...")

        if not params.token then
            error("require access token")
        end

        local client = mqtt.client("mqtt://demo.thingsboard.io:1883", params.token)

        return coroutine.create(function()
            repeat
                print("connecting...")
            until client:connect()
            print("conected!")            

            client:subscribe("v1/devices/me/rpc/request/+", function(topic, data)
                local payload = json.decode(data)
                local reqid = string.sub(topic, #"v1/devices/me/rpc/request/" + 1)
                local restopic = "v1/devices/me/rpc/response/" .. reqid

                function response(value)
                    client:publish(restopic, json.encode({ value = value }))
                end

                local methods = {
                    ["getEnabled"] = function()
                        response(evse.getenabled())
                    end,
                    ["setEnabled"] = function(param)
                        evse.setenabled(param)
                    end,
                    ["getChargingCurrent"] = function()
                        response(evse.getchargingcurrent())
                    end,
                    ["setChargingCurrent"] = function(param)
                        evse.setchargingcurrent(param)
                    end
                }

                if methods[payload.method] ~= nil then
                    methods[payload.method](payload.params)
                end
            end)

            while true do
                local payload = {}
                local state = evse.getstate()
                payload["session"] = state >= evse.STATEB1 and state <= evse.STATED2
                payload["charging"] = state == evse.STATEC2 or state == evse.STATED2
                payload["enabled"] = evse.getenabled()
                payload["available"] = evse.getavailable()
                payload["error"] = evse.geterror() > 0
                payload["consumption"] = evse.getconsumption()
                payload["power"] = evse.getpower()
                payload["temperature"] = evse.gethightemperature()
                payload["uptime"] = os.clock()
                payload["currentL1"], payload["currentL2"], payload["currentL3"] = evse.getcurrent()
                payload["voltageL1"], payload["voltageL3"], payload["voltageL3"] = evse.getvoltage()

                client:publish("v1/devices/me/telemetry", json.encode(payload))

                coroutine.yield(60000)
            end
        end)
    end
}