Lua Scripting in MOS350 Automation
Version 1.2
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. |
Device Identifier
Many functions accept a device parameter to identify a target device. This can be provided in one of the following forms:
| Form | Description | Example |
|---|---|---|
| Hex ID | The device's hexadecimal ID as shown in the Admin Portal | "200001" |
| User variable | A user-defined variable name mapped to a device | "_Inv1" |
AutoData.Id | In integration scripts, resolves to the current device ID. In automation scripts, resolves to the automation ID instead | AutoData.Id |
-- Hex ID from Admin Portal
readModbusMap("200001", "SOC")
-- User variable
readModbusMap("_Inv1", "SOC")
-- Integration script: resolves to the current device ID
-- Automation script: resolves to the automation ID
readModbusMap(AutoData.Id, "SOC")
Core Functions
refreshData([mode])
Refreshes automation data and populates the global Lua table AutoData 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.UsagePwr)
end
Global Table: AutoData
Updated by refreshData(). Contains the following fields:
System State
| Field | Type | Description |
|---|---|---|
Id | string | In integration scripts: the hex ID of the current device. In automation scripts: the automation ID |
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 |
UsagePwr | number | Power usage |
ExportPwr | number | Exported power |
GenPwr | number | Generated power |
Global Table: Device
Device Configuration Parameters (loaded by default)
| Field | Type | Description |
|---|---|---|
Id | string | Hex ID of the device |
MaxPwr | number | Maximum power rating of the device in watts |
MaxDChar | number | Maximum discharge limit in percentage |
MaxChar | number | Maximum charge limit in percentage |
SetValue | number | Set value passed by the automation engine for control operations |
The Device table is only available for device-level scripts.
Control Functions
Control functions are optional callbacks invoked by the Automation Engine when a specific operation is requested for the corresponding device. Device.SetValue is set by the engine before each call.
| Function | Description |
|---|---|
__reset__() | Called when the device is requested to reset |
__charge__() | Called when the device is requested to charge |
__discharge__() | Called when the device is requested to discharge |
Example:
function __reset__()
local setValue = Device.SetValue
-- ...
end
function __charge__()
local setValue = Device.SetValue
-- ...
end
function __discharge__()
local setValue = Device.SetValue
-- ...
end
Only applicable to device-level scripts.
Generic Functions
logStream(level, ...)
Writes a message to the application log stream at the specified debug level. Multiple values are accepted and will be concatenated with a tab separator, matching the behaviour of print.
| Parameter | Type | Description |
|---|---|---|
level | integer | Bitmask specifying which log bucket(s) the message belongs to. See level bits below |
... | any | One or more values to log. Numbers, strings and booleans are all accepted |
Level Bits
| Bit | Value | Log Bucket |
|---|---|---|
| 0 | 0x01 | Level 1 |
| 1 | 0x02 | Level 2 |
| 2 | 0x04 | Level 3 |
Messages are only output when the corresponding bucket is enabled in the application log configuration. A message can be assigned to multiple buckets by combining values with the bitwise OR operator (|).
Returns: nothing.
-- Single bucket
logStream(0x01, "Charge started")
logStream(0x02, "SetValue:", setValue)
logStream(0x04, "Device:", deviceName, "Level:", setValue)
-- Multiple buckets combined
logStream(0x01 | 0x02, "Shown when level 1 or level 2 is enabled")
logStream(0x07, "Shown when any level is enabled")
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")
getUserGrp(name)
Retrieves all device IDs belonging to a named automation group.
| Parameter | Type | Description |
|---|---|---|
name | string | The group name |
Returns: A table of device IDs (integers) indexed from 1, or nil if the group was not found.
Use the # operator to get the device count — it is O(1) on a sequence table.
local devices = getUserGrp("_MyGroup")
if devices then
print("Device count:", #devices)
for i, deviceId in ipairs(devices) do
local hexId = string.format("%X", deviceId)
print("Device:", hexId)
-- Use with other functions
local status, soc = readModbusMap(hexId, "SOC")
if status then
print("SOC:", soc)
end
end
end
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 hex ID (e.g. "200001") or user variable name (e.g. "_Inv1") |
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 hex ID (e.g. "200001") or user variable name (e.g. "_Inv1") |
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 hex ID (e.g. "200001") or user variable name (e.g. "_Inv1") |
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)
readModbusMap(deviceName, mapName)
Reads a custom Modbus map value.
| Parameter | Type | Description |
|---|---|---|
deviceName | string | Device hex ID (e.g. "200001") or user variable name (e.g. "_Inv1"). AutoData.Id resolves to the current device ID in integration scripts, or the automation ID in automation scripts |
mapName | string | Name of the custom map to read |
Returns: status (boolean), value (float).
local status, value = readModbusMap("_Inv1", "SOC")
-- Using AutoData.Id (integration script: current device; automation script: automation ID)
local status, value = readModbusMap(AutoData.Id, "SOC")
getDeviceState(deviceName)
Gets the current state of a device. Currently limited to GPIO devices.
| Parameter | Type | Description |
|---|---|---|
deviceName | string | Device hex ID (e.g. "200001") or user variable name (e.g. "_GPIO1") |
Returns: status (boolean), state (integer).
local status, state = getDeviceState("200001")
local status, state = getDeviceState("_GPIO1")
setDeviceState(deviceName, state)
Sets the state of a device. Currently limited to GPIO devices.
| Parameter | Type | Description |
|---|---|---|
deviceName | string | Device hex ID (e.g. "200001") or user variable name (e.g. "_GPIO1") |
state | integer | Desired state to set |
Returns: boolean — true on success.
setDeviceState("200001", 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 hex ID (e.g. "200001") 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 | Device hex ID (e.g. "200001") or user variable name (e.g. "_Met1") |
modbusID | integer | Modbus ID |
functionType | integer | Modbus function type. Must 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).
-- Read from _Met1, Modbus ID 1, function code 3, register 0, length 1, as float
local status, value = readModbus("_Met1", 1, 0x3, 0, 1, 9)
writeModbus(device, modbusID, functionType, reg, value, [dataType], [byteOrder])
Writes one or more values to a Modbus register.
| Parameter | Type | Description |
|---|---|---|
device | string | Device hex ID (e.g. "200001") or user variable name (e.g. "_Met1") |
modbusID | integer | Modbus ID |
functionType | integer | Modbus function type. Must be 0x05 or above |
reg | integer | Register start address |
value | number or table | Single value to write, or a table of values for multi-register writes (0x10 / 0xF1 only) |
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).
-- Write single value: value 10 to _Met1, Modbus ID 1, function code 6, register 0
local status = writeModbus("_Met1", 1, 0x06, 0, 10)
-- Write multiple registers using function code 0x10
local status = writeModbus("_Inv1", 1, 0x10, 100, {0x0001, 0x0002, 500})
Passing a table of values is only supported for function codes 0x10 and 0xF1. The number of registers written is derived automatically from the table length.
checkWriteModbus(device, modbusID, readFunctionType, writeFunctionType, reg, value, [dataType], [byteOrder])
Reads a Modbus register and writes a new value only if it differs from the current value.
| Parameter | Type | Description |
|---|---|---|
device | string | Device hex ID (e.g. "200001") or user variable name (e.g. "_Met1") |
modbusID | integer | Modbus ID |
readFunctionType | integer | Modbus function type for the read operation. Must be below 0x05 |
writeFunctionType | integer | Modbus function type for the write operation. Must be 0x05 or above |
reg | integer | Register start address |
value | number | Value to write if different from the current register value |
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).
-- Read register 0 using function code 3; write value 10 using function code 6 only if different
local status = checkWriteModbus("_Met1", 1, 0x03, 0x06, 0, 10)
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")
clearVar(name)
Clears a previously saved user variable from the Automation Engine's cached status block.
| Parameter | Type | Description |
|---|---|---|
name | string | Variable name (max 16 characters) |
Returns: None. Throws an error on failure.
clearVar("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 hex ID (e.g. "200001") or user variable name |
alertTag | string | Alert tag string (max 14 characters) |
Returns: None. Throws an error on failure.
addAlertTag("20000", "DIV_E1")
pushRealTime(key, label, value)
Publishes a real-time key-value entry to the communication layer. The entry is made available for external consumers such as dashboards or monitoring interfaces.
| Parameter | Type | Description |
|---|---|---|
key | string | The key identifying the data namespace |
label | string | The label for the value being published |
value | number or string | The value to publish. Numbers are formatted to 2 decimal places |
Returns: boolean — true on success. Throws an error if value is not a number or string.
Admin Portal Keys
The following keys are recognised by the Admin Portal dashboard. Use these when publishing real-time power and energy data:
| Key | Description |
|---|---|
S_PWR | Solar power (W) |
G_PWR | Grid power (W). Positive = import, negative = export |
B_PWR | Battery power (W). Positive = charging, negative = discharging |
L_PWR | Load power (W). Actual measured load from the gateway |
SOC | Battery state of charge (%) |
-- Integration script: read from current device and publish
local status, value = readModbusMap(AutoData.Id, "V1")
pushRealTime("V1", "Voltage 1", value)
-- Publish a status string
pushRealTime("MODE", "OpMode", "GridFollow")
-- Publish Admin Portal dashboard values
pushRealTime("S_PWR", "Solar", solarPwr)
pushRealTime("G_PWR", "Grid", gridPwr) -- positive = import, negative = export
pushRealTime("B_PWR", "Battery", batteryPwr) -- positive = charging, negative = discharging
pushRealTime("L_PWR", "Load", loadPwr)
pushRealTime("SOC", "State of Charge", soc)
Numeric values are formatted with %.2f and rounded to 2 decimal places. String values are silently truncated to 127 characters if they exceed the buffer limit.
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 hex ID (e.g. "200001"), user variable name, or AutoData.Id (integration scripts only) |
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 = readModbusMap(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 hex ID (e.g. "200001"), user variable name, or AutoData.Id (integration scripts only) |
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 ok, err = openDB("/data/mydb.sqlite")
if not ok then
print("Failed to open DB: " .. err)
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).
-- 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
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.
closeDB()
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.Time
local value = AutoData.UsagePwr
-- 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 = readModbusMap("_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: readModbusMap("_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.