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