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
| Function | Description |
|---|---|
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): Anumberbetween0and255. Controls the depth of data refresh.
| Mode | Description |
|---|---|
0 | Refresh basic system state: Island, Time, DOW |
1 | Refresh 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
| Field | Type | Description |
|---|---|---|
Id | string | Hex Id of the Device or Automation |
Island | number | 1 if in island mode, 0 otherwise |
Time | number | Current time (minutes since midnight) |
DOW | number | Day of week (0 = Sunday, 6 = Saturday) |
Year | number | Year |
Month | number | Month |
Day | number | Day of the month |
Site Level Power Metrics (mode 1 only)
| Field | Type | Description |
|---|---|---|
V1, V2, V3 | number | Voltage readings |
FZ | number | Frequency |
Usage | number | Power usage |
Export | number | Exported power |
Generation | number | Generated power |
Automation Engine Functions
getUserVar(name)
Retrieves the value of a user-defined variable from the automation context.
| Parameter | Type | Description |
|---|---|---|
name | string | The 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.
| Parameter | Type | Description |
|---|---|---|
deviceName | string | Device ID or user variable name |
automationResetSeconds | integer | Number 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.
| Parameter | Type | Description |
|---|---|---|
deviceName | string | Device 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.
| Parameter | Type | Description |
|---|---|---|
priority | integer | Value 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.
| Parameter | Type | Description |
|---|---|---|
deviceName | string | Device ID or user variable name |
modbusExecName | string | Name of the integration execution |
priority | integer | Execution priority (1 = highest) |
value | float | Value 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.
| Parameter | Type | Description |
|---|---|---|
deviceName | string | Device ID or user variable name |
mapName | string | Name 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.
| Parameter | Type | Description |
|---|---|---|
deviceName | string | Device 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.
| Parameter | Type | Description |
|---|---|---|
deviceName | string | Device ID (e.g. "0x200001") or user variable name (e.g. "_GPIO1") |
state | integer | Desired 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.
| Parameter | Type | Description |
|---|---|---|
deviceName | string | Device ID or user variable name (e.g. "_Met1") |
leg | integer | Leg index for multi-leg devices (e.g. 3-phase meters), for consolidated values like Output power, use 1 |
mapIndex | integer | Data field to set — use a DataMap constant |
value | float | Value to write |
Returns: boolean — true on success. Throws an error if the device is not found or mapIndex is out of range.
DataMap Constants
| Constant | Description |
|---|---|
DataMap.I | Current - Per Leg |
DataMap.V | Voltage - Per Leg |
DataMap.FZ | Frequency |
DataMap.PF | Power Factor - Per Leg |
DataMap.PWR | Active Power - Per Leg |
DataMap.PWR_VA | Apparent Power - Per Leg |
DataMap.PWR_VAR | Reactive Power - Per Leg |
DataMap.ENE_N | Negative Energy |
DataMap.ENE_P | Positive Energy |
DataMap.ENE_VA_N | Negative Apparent Energy |
DataMap.ENE_VA_P | Positive Apparent Energy |
DataMap.ENE_VAR_N | Negative Reactive Energy |
DataMap.ENE_VAR_P | Positive Reactive Energy |
DataMap.BAT_SOC | Battery State of Charge |
DataMap.OUT_PWR | Output Power |
DataMap.BAT_PWR | Battery Power |
DataMap.GEN_PWR | Generator 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
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.
| Parameter | Type | Description |
|---|---|---|
device | string | Modbus device identifier |
modbusID | integer | Modbus ID |
functionType | integer | Modbus function type. Should be below 0x05. |
reg | integer | Register start address |
length | integer | Number of registers to read |
dataType | integer (optional) | See data type table below (default: UINT_8 = 1) |
byteOrder | integer (optional) | 0 = Big Endian (default), 1 = Little Endian |
Data Types:
| Value | Type |
|---|---|
1 | UINT_8 (default) |
2 | INT_8 |
3 | UINT_16 |
4 | INT_16 |
5 | UINT_32 |
6 | INT_32 |
7 | UINT_64 |
8 | INT_64 |
9 | FLOAT |
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.
| Parameter | Type | Description |
|---|---|---|
device | string | Modbus device identifier |
modbusID | integer | Modbus ID |
functionType | integer | Modbus function type. Should be 0x05 or above. |
reg | integer | Register start address |
value | real | Value to write |
length | integer (optional) | Number of registers to write (default 1) |
dataType | integer (optional) | See data type table below (default: UINT_16 = 3) |
byteOrder | integer (optional) | 0 = Big Endian (default), 1 = Little Endian |
Data Types:
| Value | Type |
|---|---|
1 | UINT_8 |
2 | INT_8 |
3 | UINT_16 (default) |
4 | INT_16 |
5 | UINT_32 |
6 | INT_32 |
7 | UINT_64 |
8 | INT_64 |
9 | FLOAT |
Returns: status (boolean).
status = writeModbus("_Met1", 1, 6, 0, 10)
Writes value 10 to _Met1, Modbus ID 1, function code 6, register 0.
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.
| Parameter | Type | Description |
|---|---|---|
name | string | Variable name (max 16 characters) |
value | float | Value 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.
| Parameter | Type | Description |
|---|---|---|
name | string | Variable 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.
| Parameter | Type | Description |
|---|---|---|
device | string | Device ID or user variable name |
alertTag | string | Alert 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.
| Parameter | Type | Description |
|---|---|---|
deviceName | string | Device ID or user variable name (typically Autodata.Id) |
key | string | Key name (max 64 characters) |
value | number or string | Value to store |
Returns: boolean — true 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.
| Parameter | Type | Description |
|---|---|---|
deviceName | string | Device ID or user variable name (typically Autodata.Id) |
key | string | Key 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: boolean — true 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.
| Parameter | Type | Description |
|---|---|---|
path | string | Full 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.
execSQL does not return query results from SELECT statements. It is intended for data modification and schema operations only.
| Parameter | Type | Description |
|---|---|---|
sql | string | A 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
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.
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:
| Condition | Action |
|---|---|
sense == 0 and mode == 1 | Grid has returned — switch inverter to grid follow (handleGridForm(0)) |
sense == 1 and mode == 0 | Grid 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.