Skip to main content

Lua Scripting in MOS350 Automation

Version 1.1

MOS350 supports Lua scripting to extend the already powerful Automation Engine.


Script Structure

Automation scripts follow a simple lifecycle pattern inspired by Arduino-style scripting:

function setup()
-- Called once at automation start
end

function loop()
-- Called repeatedly at the configured automation frequency
refreshData()
end

Lifecycle Overview

FunctionDescription
setup()Initializes the script. Called once when automation starts.
loop()Executes repeatedly at the configured frequency. Use this to poll data, trigger actions, or update state.

Core Functions

refreshData([mode])

Refreshes automation data and populates the global Lua table Autodata.Id with system state and power metrics.

refreshData([mode])
  • mode (optional): A number between 0 and 255. Controls the depth of data refresh.
ModeDescription
0Refresh basic system state: Island, Time, DOW
1Refresh full power metrics in addition to basic state

Example:

function loop()
refreshData(1) -- Full refresh
print(AutoData.V1, AutoData.Usage)
end

Global Table: AutoData

Updated by refreshData(). Contains the following fields:

System State

FieldTypeDescription
IdstringHex Id of the Device or Automation
Islandnumber1 if in island mode, 0 otherwise
TimenumberCurrent time (minutes since midnight)
DOWnumberDay of week (0 = Sunday, 6 = Saturday)
YearnumberYear
MonthnumberMonth
DaynumberDay of the month

Site Level Power Metrics (mode 1 only)

FieldTypeDescription
V1, V2, V3numberVoltage readings
FZnumberFrequency
UsagenumberPower usage
ExportnumberExported power
GenerationnumberGenerated power

Automation Engine Functions

getUserVar(name)

Retrieves the value of a user-defined variable from the automation context.

ParameterTypeDescription
namestringThe user variable name

Returns: float — the variable's value.

local value = getUserVar("_Inv1")

getDeviceLock(deviceName, automationResetSeconds)

Acquires a device lock, ensuring the device remains under control of this automation for the specified duration.

ParameterTypeDescription
deviceNamestringDevice ID or user variable name
automationResetSecondsintegerNumber of seconds to hold the lock if not renewed

Returns: 1 if lock was successfully applied, 0 otherwise.

local status = getDeviceLock("_Inv1", 60)

releaseDeviceLock(deviceName)

Releases a previously acquired device lock.

ParameterTypeDescription
deviceNamestringDevice ID or user variable name

Returns: 1 if lock was successfully released, 0 otherwise.

local status = releaseDeviceLock("_Inv1")

overridePriority(priority)

Overrides the automation priority level. Useful when certain operations such as Grid Form need to run with higher priority.

ParameterTypeDescription
priorityintegerValue between 1 and 245, where 1 is highest priority

Returns: 1 if successful.

local status = overridePriority(2)

execModbus(deviceName, modbusExecName, priority, value)

Executes a Modbus function from the integration module, supporting optional variable resolution.

ParameterTypeDescription
deviceNamestringDevice ID or user variable name
modbusExecNamestringName of the integration execution
priorityintegerExecution priority (1 = highest)
valuefloatValue to pass for the Modbus execution (write operations only)

Returns: integer — execution status.

local status = execModbus("_Inv1", "_Grid_Form", 1, 1)

readModbusCMap(deviceName, mapName)

Reads a custom Modbus map value.

ParameterTypeDescription
deviceNamestringDevice ID or user variable name
mapNamestringName of the custom map to read

Returns: float — the evaluated value from the custom Modbus map.

local value = readModbusCMap("_Inv1", "SOC")

getDeviceState(deviceName)

Gets the current state of a device. Currently limited to GPIO devices.

ParameterTypeDescription
deviceNamestringDevice ID (e.g. "0x200001") or user variable name (e.g. "_GPIO1")

Returns: integer — the device's current state.

local state = getDeviceState("0x200001")
local state = getDeviceState("_GPIO1")

setDeviceState(deviceName, state)

Sets the state of a device. Currently limited to GPIO devices.

ParameterTypeDescription
deviceNamestringDevice ID (e.g. "0x200001") or user variable name (e.g. "_GPIO1")
stateintegerDesired state to set

Returns: integer — the device's state after the operation.

setDeviceState("0x200001", 1)
setDeviceState("_GPIO1", 1)

setDeviceData(deviceName, leg, mapIndex, value)

Sets a specific measurement value on a device's energy data structure. Use the DataMap constants to specify which measurement to write.

ParameterTypeDescription
deviceNamestringDevice ID or user variable name (e.g. "_Met1")
legintegerLeg index for multi-leg devices (e.g. 3-phase meters), for consolidated values like Output power, use 1
mapIndexintegerData field to set — use a DataMap constant
valuefloatValue to write

Returns: booleantrue on success. Throws an error if the device is not found or mapIndex is out of range.

DataMap Constants

ConstantDescription
DataMap.ICurrent - Per Leg
DataMap.VVoltage - Per Leg
DataMap.FZFrequency
DataMap.PFPower Factor - Per Leg
DataMap.PWRActive Power - Per Leg
DataMap.PWR_VAApparent Power - Per Leg
DataMap.PWR_VARReactive Power - Per Leg
DataMap.ENE_NNegative Energy
DataMap.ENE_PPositive Energy
DataMap.ENE_VA_NNegative Apparent Energy
DataMap.ENE_VA_PPositive Apparent Energy
DataMap.ENE_VAR_NNegative Reactive Energy
DataMap.ENE_VAR_PPositive Reactive Energy
DataMap.BAT_SOCBattery State of Charge
DataMap.OUT_PWROutput Power
DataMap.BAT_PWRBattery Power
DataMap.GEN_PWRGenerator Power

Example:

function loop()
refreshData()

-- Write voltage on leg 1 of a meter
setDeviceData("_Met1", 1, DataMap.V, 230.5)

-- Write battery state of charge (leg 0, single-leg device)
setDeviceData("_Bat1", 0, DataMap.BAT_SOC, 87.3)

-- Write active power across all 3 legs of a 3-phase meter
for leg = 1, 3 do
setDeviceData("_Met1", leg, DataMap.PWR, getPhasePower(leg))
end
end
note

setDeviceData only applies to devices with an energy measurements data pointer (MCTRL_DEVICE_DATA_PTR_TYPE_EMEASUREMENTS). Calls on unsupported device types will return true but have no effect.


readModbus(device, modbusID, functionType, reg, length, [dataType], [byteOrder])

Reads a Modbus register with optional decoding parameters.

ParameterTypeDescription
devicestringModbus device identifier
modbusIDintegerModbus ID
functionTypeintegerModbus function type. Should be below 0x05.
regintegerRegister start address
lengthintegerNumber of registers to read
dataTypeinteger (optional)See data type table below (default: UINT_8 = 1)
byteOrderinteger (optional)0 = Big Endian (default), 1 = Little Endian

Data Types:

ValueType
1UINT_8 (default)
2INT_8
3UINT_16
4INT_16
5UINT_32
6INT_32
7UINT_64
8INT_64
9FLOAT

Returns: status (boolean), value (float).

status, value = readModbus("_Met1", 1, 0x3, 0, 1, 9)

Reads from _Met1, Modbus ID 1, function code 3, register 0, length 1, as float.


writeModbus(device, modbusID, functionType, reg, length, value, [dataType], [byteOrder])

Writes a value to a Modbus register.

ParameterTypeDescription
devicestringModbus device identifier
modbusIDintegerModbus ID
functionTypeintegerModbus function type. Should be 0x05 or above.
regintegerRegister start address
valuerealValue to write
lengthinteger (optional)Number of registers to write (default 1)
dataTypeinteger (optional)See data type table below (default: UINT_16 = 3)
byteOrderinteger (optional)0 = Big Endian (default), 1 = Little Endian

Data Types:

ValueType
1UINT_8
2INT_8
3UINT_16 (default)
4INT_16
5UINT_32
6INT_32
7UINT_64
8INT_64
9FLOAT

Returns: status (boolean).

status = writeModbus("_Met1", 1, 6, 0, 10)

Writes value 10 to _Met1, Modbus ID 1, function code 6, register 0.

tip

checkWriteModbus works identically to writeModbus but performs a read (0x03) on the register first. If the current value differs from the one being written, it then proceeds with the write using 0x06. Use this to avoid unnecessary writes to the device.


saveVar(name, value)

Saves a user variable to permanent memory within the Automation Engine's cached status block.

ParameterTypeDescription
namestringVariable name (max 16 characters)
valuefloatValue to save

Returns: None. Throws an error on failure.

saveVar("OpState", 1)

loadVar(name)

Loads a previously saved user variable from the Automation Engine's cached status block.

ParameterTypeDescription
namestringVariable name (max 16 characters)

Returns: float — the loaded value, or 0 if not found.

local operationalState = loadVar("OpState")

addAlertTag(device, alertTag)

Adds an alert tag to a device. The tag is posted to the backend alert system for processing.

ParameterTypeDescription
devicestringDevice ID or user variable name
alertTagstringAlert tag string (max 14 characters)

Returns: None. Throws an error on failure.

addAlertTag("20000", "DIV_E1")

Key-Value Store Functions

These functions allow Lua scripts to store and retrieve key-value pairs against a device. The store is managed in C and can be flushed to JSON at any time using kvToJSON().


kvSet(deviceName, key, value)

Sets a key-value pair in the device's KV store. Creates a new entry or updates an existing one.

ParameterTypeDescription
deviceNamestringDevice ID or user variable name (typically Autodata.Id)
keystringKey name (max 64 characters)
valuenumber or stringValue to store

Returns: booleantrue if set successfully, false otherwise.

function loop()
refreshData()

local status, v1 = readModbusCMap(Autodata.Id, "_V1")
if status then
kvSet(Autodata.Id, "_V1", v1)
end

kvSet(Autodata.Id, "_Status", "Running")
end

kvGet(deviceName, key)

Retrieves a value from the device's KV store by key.

ParameterTypeDescription
deviceNamestringDevice ID or user variable name (typically Autodata.Id)
keystringKey name

Returns: The stored number or string value, or nil if the key does not exist.

local v1 = kvGet(Autodata.Id, "_V1")
if v1 then
print("V1:", v1)
end

local status = kvGet(Autodata.Id, "_Status")
if status then
print("Status:", status)
end

SQLite Database Functions

These functions allow Lua scripts to store and retrieve data persistently using a SQLite database. SQLite must be available on the target system.


isDBOpen()

Returns whether a database connection is currently open. Useful for checking availability before executing SQL or for implementing retry logic in loop().

Returns: booleantrue if the database is open, false otherwise.

local function tryOpenDB()
if not isDBOpen() then
local ok, err = openDB("/data/mydb.sqlite")
if ok then
execSQL([[
CREATE TABLE IF NOT EXISTS SensorData (
TIM INTEGER PRIMARY KEY,
Value REAL
);
]])
else
print("DB not ready: " .. err)
return false
end
end
return true
end

function loop()
refreshData()

if not tryOpenDB() then
return -- Drive not ready, skip this loop iteration
end

-- Normal loop logic here...
end

openDB(path)

Opens a SQLite database file.

ParameterTypeDescription
pathstringFull file path to the SQLite database file

Returns: success (boolean), error (string, only on failure).

local function tryOpenDB()
if not isDBOpen() then
local ok, err = openDB("/data/mydb.sqlite")
if ok then
execSQL([[
CREATE TABLE IF NOT EXISTS SensorData (
TIM INTEGER PRIMARY KEY,
Value REAL
);
]])
else
print("DB not ready: " .. err)
return false
end
end
return true
end

function loop()
refreshData()

if not tryOpenDB() then
return -- DB not ready, skip this cycle
end

-- Normal loop logic here...
end

execSQL(sql)

Executes a SQL statement against the open database. Supports any valid SQLite statement including CREATE, INSERT, UPDATE, and DELETE.

note

execSQL does not return query results from SELECT statements. It is intended for data modification and schema operations only.

ParameterTypeDescription
sqlstringA valid SQLite SQL statement

Returns: success (boolean), error (string, only on failure).

local function tryOpenDB()
if not isDBOpen() then
local ok, err = openDB("/data/mydb.sqlite")
if ok then
execSQL([[
CREATE TABLE IF NOT EXISTS SensorData (
TIM INTEGER PRIMARY KEY,
Value REAL
);
]])
else
print("DB not ready: " .. err)
return false
end
end
return true
end

function loop()
refreshData()

if not tryOpenDB() then
return
end

-- Single line
execSQL("INSERT INTO SensorData (TIM, Value) VALUES (1700000000, 3.14);")

-- Multi-line using [[ ]] long string syntax
execSQL([[
UPDATE SensorData
SET Value = 99.5
WHERE TIM = 1700000000;
]])

-- With error handling
local ok, err = execSQL("DELETE FROM SensorData WHERE TIM < 1000;")
if not ok then
print("SQL failed: " .. err)
end
end
tip

Use the [[ ]] long string syntax for multi-line SQL — it avoids the need to escape quotes inside the statement.


dbChanges()

Returns the number of rows affected by the most recent INSERT, UPDATE, or DELETE statement. Useful for implementing upsert (insert-or-update) logic.

Returns: integer — number of rows affected, or 0 if no rows were affected.

note

Does not count rows affected by triggers. Has no effect for SELECT statements.

-- Upsert pattern: update if row exists, insert if not
function upsertData(tableName, colName, value, epoch)
execSQL(string.format(
"UPDATE %s SET %s = %f WHERE TIM = %d;",
tableName, colName, value, epoch
))

if dbChanges() == 0 then
-- No rows were updated, so the row doesn't exist yet
execSQL(string.format(
"INSERT INTO %s (TIM, %s) VALUES (%d, %f);",
tableName, colName, epoch, value
))
end
end

closeDB()

Closes the open database connection and releases all associated resources.

Returns: None.

local function tryOpenDB()
if not isDBOpen() then
local ok, err = openDB("/data/mydb.sqlite")
if ok then
execSQL([[
CREATE TABLE IF NOT EXISTS SensorData (
TIM INTEGER PRIMARY KEY,
Value REAL
);
]])
else
print("DB not ready: " .. err)
return false
end
end
return true
end

function loop()
refreshData()

if tryOpenDB() then
closeDB()
print("Database closed")
end
end

SQLite Full Example

local function tryOpenDB()
if not isDBOpen() then
local ok, err = openDB("/data/mydb.sqlite")
if ok then
execSQL([[
CREATE TABLE IF NOT EXISTS SensorData (
TIM INTEGER PRIMARY KEY,
Value REAL
);
]])
else
print("DB not ready: " .. err)
return false
end
end
return true
end

function setup()
end

function loop()
refreshData()

if not tryOpenDB() then
return -- DB not ready, skip this cycle
end

local now = Autodata.Id.Time
local value = Autodata.Id.Usage

-- Try to update existing row
execSQL(string.format(
"UPDATE SensorData SET Value = %f WHERE TIM = %d;", value, now
))

-- If no row was updated, insert a new one
if dbChanges() == 0 then
execSQL(string.format(
"INSERT INTO SensorData (TIM, Value) VALUES (%d, %f);", now, value
))
end
end

Example: Grid Management

This script manages an inverter's operating mode (grid follow or grid form) based on grid presence detected via a GPIO input.

local counter = 0

function setup()
counter = 0
end

function loop()
counter = counter + 1
print("Counter:", counter)

local status, gridSense = getDeviceState("_GPIO1")
if status then
print("Grid Sense:", gridSense)

status, gridMode = readModbusCMap("_INV1", "_GRID_MODE")
if status then
print("Grid Mode:", gridMode)

local sense = math.tointeger(gridSense)
local mode = math.tointeger(gridMode)

if sense == 0 and mode == 1 then
-- Grid present, switch to grid follow
handleGridForm(0)
elseif sense == 1 and mode == 0 then
-- Grid absent, switch to grid form
handleGridForm(1)
end
else
print("Failed to read GRID_MODE")
end
else
print("Failed to get GPIO1 state")
end
end

function handleGridForm(value)
local gridFormStatus = execModbus("_INV1", "_GRID_FORM", 1, value)
if gridFormStatus then
print("Status Grid Form:", gridFormStatus)
else
print("Failed to execute GRID_FORM with value:", value)
end
end

How It Works

Initialization: setup() runs once on start and resets counter to 0. loop() runs repeatedly and increments counter each cycle.

Step 1 — Read GPIO State: getDeviceState("_GPIO1") reads the grid detection input. _GPIO1 is a user-defined variable for the GPIO device. gridSense will be 0 (grid present) or 1 (grid absent).

Step 2 — Read Inverter Mode: readModbusCMap("_INV1", "_GRID_MODE") reads the inverter's current operating mode. _INV1 is the user-defined variable for the inverter. gridMode will be 0 (grid form) or 1 (grid follow).

Step 3 — Decision Logic:

ConditionAction
sense == 0 and mode == 1Grid has returned — switch inverter to grid follow (handleGridForm(0))
sense == 1 and mode == 0Grid outage — switch inverter to grid form (handleGridForm(1))

handleGridForm(value): Calls execModbus() targeting _INV1 with execution name _GRID_FORM at priority 1. Pass 0 for grid follow, 1 for grid form.