Skip to main content

Lua Scripting in MOS350 Automation

Version 1.4

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.

Device Identifier

Many functions accept a device parameter to identify a target device. This can be provided in one of the following forms:

FormDescriptionExample
Hex IDThe device's hexadecimal ID as shown in the Admin Portal"200001"
User variableA user-defined variable name mapped to a device"_Inv1"
AutoData.IdIn integration scripts, resolves to the current device ID. In automation scripts, resolves to the automation ID insteadAutoData.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): 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.UsagePwr)
end

Global Table: AutoData

Updated by refreshData(). Contains the following fields:

System State

FieldTypeDescription
IdstringIn integration scripts: the hex ID of the current device. In automation scripts: the automation ID
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
UsagePwrnumberPower usage
ExportPwrnumberExported power
GenPwrnumberGenerated power

Global Table: Device

Device Configuration Parameters (loaded by default)

FieldTypeDescription
IdstringHex ID of the device
MaxPwrnumberMaximum power rating of the device in watts
MaxDCharnumberMaximum discharge limit in percentage
MaxCharnumberMaximum charge limit in percentage
SetValuenumberSet value passed by the automation engine for control operations
note

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.

FunctionDescription
__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
note

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.

ParameterTypeDescription
levelintegerBitmask specifying which log bucket(s) the message belongs to. See level bits below
...anyOne or more values to log. Numbers, strings and booleans are all accepted

Level Bits

BitValueLog Bucket
00x01Level 1
10x02Level 2
20x04Level 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.

ParameterTypeDescription
namestringThe 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.

ParameterTypeDescription
namestringThe 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.

ParameterTypeDescription
deviceNamestringDevice hex ID (e.g. "200001") or user variable name (e.g. "_Inv1")
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 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.

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 hex ID (e.g. "200001") or user variable name (e.g. "_Inv1")
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)

runAutoFunction(deviceName, functionType, priority, value, automationResetSeconds)

note

Call getDeviceLock(deviceName, automationResetSeconds) first and confirm it succeeded before calling this. runAutoFunction does not acquire the device lock itself - calling it without holding the lock risks writing to a device another automation currently owns.

ParameterTypeDescription
deviceNamestringDevice hex ID (e.g. "200001") or user variable name (e.g. "_Inv1")
functionTypeintegerCompare against AutoFunctionType constants
priorityintegerExecution priority (1 = highest)
valuenumberSet value passed to the function (e.g. charge/discharge rate)
automationResetSecondsintegerWatchdog renewal duration - same meaning as in getDeviceLock()

Returns: integer1 if the device was found and eligible to receive the command, 0 otherwise. This does not confirm the underlying Modbus write itself succeeded.

AutoFunctionType Constants

ConstantDescription
AutoFunctionType.RESET_STANDBYReset the device to a neutral/standby state
AutoFunctionType.CHARGECharge the device
AutoFunctionType.DISCHARGEDischarge the device
local status = getDeviceLock("_Inv1", 180)
if status == 1 then
runAutoFunction("_Inv1", AutoFunctionType.CHARGE, 1, 5.0, 180)
end

readModbusMap(deviceName, mapName)

Reads a custom Modbus map value.

ParameterTypeDescription
deviceNamestringDevice 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
mapNamestringName 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.

ParameterTypeDescription
deviceNamestringDevice 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.

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

Returns: booleantrue 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.

ParameterTypeDescription
deviceNamestringDevice hex ID (e.g. "200001") 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
devicestringDevice hex ID (e.g. "200001") or user variable name (e.g. "_Met1")
modbusIDintegerModbus ID
functionTypeintegerModbus function type. Must 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).

-- 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.

ParameterTypeDescription
devicestringDevice hex ID (e.g. "200001") or user variable name (e.g. "_Met1")
modbusIDintegerModbus ID
functionTypeintegerModbus function type. Must be 0x05 or above
regintegerRegister start address
valuenumber or tableSingle value to write, or a table of values for multi-register writes (0x10 / 0xF1 only)
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).

-- 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})
note

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.

ParameterTypeDescription
devicestringDevice hex ID (e.g. "200001") or user variable name (e.g. "_Met1")
modbusIDintegerModbus ID
readFunctionTypeintegerModbus function type for the read operation. Must be below 0x05
writeFunctionTypeintegerModbus function type for the write operation. Must be 0x05 or above
regintegerRegister start address
valuenumberValue to write if different from the current register value
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).

-- 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.

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")

clearVar(name)

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

ParameterTypeDescription
namestringVariable 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.

ParameterTypeDescription
devicestringDevice hex ID (e.g. "200001") or user variable name
alertTagstringAlert 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.

ParameterTypeDescription
keystringThe key identifying the data namespace
labelstringThe label for the value being published
valuenumber or stringThe value to publish. Numbers are formatted to 2 decimal places

Returns: booleantrue 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:

KeyDescription
S_PWRSolar power (W)
G_PWRGrid power (W). Positive = import, negative = export
B_PWRBattery power (W). Positive = charging, negative = discharging
L_PWRLoad power (W). Actual measured load from the gateway
SOCBattery 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)
note

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.


Time of Use (TOU) Functions

TOU (Time of Use) automation resolves the currently active TOU profile's rate period (peak / off-peak / import / export rate) and, independently, its Battery Control period (charge / discharge target), if one is configured and active. Both are exposed to scripts via refreshTOUData() and the TOUData global table.

note

refreshTOUData() and the TOUData/TOUPeriodType/TOUBattMode/TOUBattModeFlag globals are available in both automation and device-level scripts.

refreshTOUData()

Refreshes the global Lua table TOUData with the most recently resolved TOU rate period and Battery Control period.

refreshTOUData()

Returns: None.

function loop()
refreshTOUData()
print(TOUData.ImportRate)
end

Global Table: TOUData

Updated by refreshTOUData(). Contains the following fields:

Rate Period

FieldTypeDescription
Validnumber1 once a rate period has been resolved, 0 if no TOU profile is loaded or no rate period is currently active
PeriodTypenumberCompare against TOUPeriodType constants (NA, PEAK, OFFPEAK)
ImportRatenumberCurrent import rate
ExportRatenumberCurrent export rate
RateUnitnumber1 = kWh, 2 = kVAh

Battery Control Period

note

Battery Control is a separate schedule axis from the rate period above — the two can be active at the same time, one without the other, or neither. Only read the Batt* fields below when TOUData.BattValid == 1; otherwise they hold stale values from whenever a Battery Control period was last active.

FieldTypeDescription
BattValidnumber1 while a Battery Control period is currently active, 0 otherwise
BattModenumberCompare against TOUBattMode constants (CHARGE, DISCHARGE)
BattModeFlagnumberCompare against TOUBattModeFlag constants (SOLAR_ONLY, LOAD_FOLLOW)
BattValuenumberCharge/discharge rate target, kW
BattSOCnumberTarget state of charge, %
BattMinLoadTriggernumberMinimum load trigger, kW. Only meaningful when BattModeFlag == TOUBattModeFlag.LOAD_FOLLOW

TOUPeriodType Constants

ConstantDescription
TOUPeriodType.NANo specific rate period
TOUPeriodType.PEAKPeak rate period
TOUPeriodType.OFFPEAKOff-peak rate period
TOUPeriodType.BATTERYReserved for the period-type enum. Battery Control periods are tracked separately via the Batt* fields, not returned in TOUData.PeriodType

TOUBattMode Constants

ConstantDescription
TOUBattMode.NONENo Battery Control mode
TOUBattMode.CHARGEBattery Control period is targeting a charge
TOUBattMode.DISCHARGEBattery Control period is targeting a discharge

TOUBattModeFlag Constants

ConstantDescription
TOUBattModeFlag.NONENo modifier set — charge/discharge at the fixed BattValue rate toward BattSOC
TOUBattModeFlag.SOLAR_ONLYCharge only from surplus solar generation (only meaningful when BattMode == TOUBattMode.CHARGE)
TOUBattModeFlag.LOAD_FOLLOWDischarge only enough to follow load, gated by BattMinLoadTrigger (only meaningful when BattMode == TOUBattMode.DISCHARGE)

Example: Reacting to TOU State

function loop()
refreshTOUData()

if TOUData.Valid == 1 then
if TOUData.PeriodType == TOUPeriodType.PEAK then
print("On-peak, import rate:", TOUData.ImportRate)
elseif TOUData.PeriodType == TOUPeriodType.OFFPEAK then
print("Off-peak, import rate:", TOUData.ImportRate)
end
end

if TOUData.BattValid == 1 then
if TOUData.BattMode == TOUBattMode.CHARGE then
print("TOU charging toward", TOUData.BattSOC, "% at", TOUData.BattValue, "kW")
elseif TOUData.BattMode == TOUBattMode.DISCHARGE then
if TOUData.BattModeFlag == TOUBattModeFlag.LOAD_FOLLOW then
print("Load-follow discharge, min trigger:", TOUData.BattMinLoadTrigger, "kW")
else
print("TOU discharging toward", TOUData.BattSOC, "% at", TOUData.BattValue, "kW")
end
end
end
end

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 hex ID (e.g. "200001"), user variable name, or AutoData.Id (integration scripts only)
keystringKey name (max 64 characters)
valuenumber or stringValue to store

Returns: booleantrue 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.

ParameterTypeDescription
deviceNamestringDevice hex ID (e.g. "200001"), user variable name, or AutoData.Id (integration scripts only)
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 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.

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).

-- 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
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.

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:

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.