LUA YPC source

From wiki.netio-products.com
Revision as of 13:08, 1 September 2016 by Bbakala (talk | contribs) (Changed visibility of the article feedback tool on "LUA YPC source" ([Viditelnost=Zakázat pro všechny uživatele] (do odvolání)))
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

IPCorder 2.1 employs a redesigned rules engine, allowing better and easier use of its potential. The engine is now using Lua programming language, and allows the user to directly enter Lua code in IPCorder Action editor. Clickable interface for most common operations is still available.

Basics of Lua

A trivial action body might look like this: <source lang="lua"> log("Hello, world!") </source>

log function is an IPCorder specific function that writes a message to IPCorder's log. You can use it to verify your action gets triggered, or to provide other interesting information. In the screenshot below you can see a successful log message printed by triggerring the rule, and below it a notification that will alert you to any run-time errors that may come up during execution.

800px

log function also supports string substitution for device variables:

<source lang="lua"> log("Hello, world!"); -- multiple statements may be separated with semicolon(";") or whitespace log("camera1 input state is ${devices.camera1.input}"); -- prints value of camera1's digital input -- and "--" delimits start of a comment </source>

Lua also supports if statements: <source lang="lua"> if devices.camera1.input then

   log("gates are open");

else

   log("gates are closed");

end </source>

Further info

More detailed introduction into Lua can be found in book Programming in Lua. (The freely-available version describes Lua 5.0. IPCorder uses Lua 5.1, but the differences between those versions are non-significant.) Lua 5.1 also has an extensive Reference Manual describing all control structures and built-in functions.

Specifics of IPCorder Lua environment

Due to hardware platform limitations, IPCorder has Lua without floating point number support. Therefore, default numeric type is integer (as opposed to double-precision floating-point number in default Lua builds). All incoming non-integer values are multiplicated to maintain useful precision. For example, sensor temperature 24°C is represented by value 2400.

In order to maintain system integrity, all code of user actions runs in contained environment with limited access to system variables (for example, device variables providing access to current values are read-only). Additionally, run-time limits are imposed to user action to make sure they are not trapped in infinite loop and prevent normal system function. As of eventer-ng_pre2 (h41dc69e), this limit is 32 thousand Lua virtual machine instructions per one action rule execution.

Apart from that, Lua environment in IPCorder provides several specific functions for interoperability with IPCorder system and devices.

Per-device variables

Table devices has an entry for every device in the system. For example, if you have a camera with system identification gate1 and a single digital input, you can access its value in variable devices.gate1.input.

Template:Devnote

Device variables in actions, as of eventer-ng_pre2 (h41dc69e)
availability name contents description
any camera recording boolean indicates whether the camera is currently recording
fps framerate received by IPCorder * 1000 framerate streamed to IPCorder; this is only updated when IPCorder is streaming from device (e.g. not when it's in "recording: off" mode); this also has no relations to live view
bps byterate received by IPCorder (in bytes per second) as above, this is only relevant when streaming to IPCorder is active
any device connected boolean indicates whether the device is connected and working (set to 1 iff device state in settings overview is "OK")
devices with one digital input input boolean indicates the state of digital input
devices with more digital inputs input1 .. inputX boolean indicated the state of given digital input
devices with sensors (HWgroup devices) sensorList table Lua table names of available sensors, indexed by their id numbers. Since this is a complex variable it cannot be directly displayed (such as in device value monitors on Video screen), but can be used in code like this:

<source lang="lua">local output = ""; -- here we'll be making our string for id, name in pairs(devices.hwgroup_poseidon.sensorList) do

   -- ^ iterate through all items in the table
   local sensor_varname = 'sid_'..id -- here we're using the sid_<id> variable format
       -- ^ ".." is Lua's concatenation operator
   local sensor_value = devices.hwgroup_poseidon[sensor_varname]
       -- ^ we're using index notation to get item named by variable
   output = output .. string.format("sensor %s (id %d) = %s, ", name, id, tostring(sensor_value))

end logf("poseidon's sensor values are: %s", output) -- logf is a variant of log that takes substitution arguments as parameters -- plain log does not support local variables, so log("poseidon's sensor values are: ${output}") -- would not work in this case </source>

sensor_<name> or sid_<id> depends on sensor contains value of sensor with given name or ID
IPCorder system device sessionCount number number of users currently logged in
freeSpace disk space in megabytes free disk space
totalSpace disk space in megabytes total available disk space
averageLoad number contains five-minute average of system load, multiplied by 100; lower is better, higher number may be sign of performance issues, exact limits depend of IPCorder model
incomingTraffic network traffic in bytes per second network traffic coming to IPCorder (mainly recording from cameras)
outgoingTraffic network traffic in bytes per second network traffic going from IPCorder (mainly recording playbacks and other client communication)
swapTotal memory size in kilobytes total size of IPCorder system virtual memory page file
swapUsed memory size in kilobytes used size of IPCorder system virtual memory page file; less is better
system device (for models with internal temperature sensor) systemTemperature temperature in °C * 100 internal system temperature
any device name string name (identification of the device); can be used with self variable to print name of the device in device-activated events

<source lang="lua">log("event activated for device ${self.name}")</source>

Device actions

Most devices offer actions, that can be used to activate various operations on them.

In IPCorder code, actions are called with named arguments, like this: <source lang="lua">devices.cam1.SetOut{output=1, value=false}; -- deactivates DO number 1 on the device</source>

Template:Devnote Template:Devnote

Device actions in eventer-ng_pre4 (hf73a447)
availability name description argument argument description example
regular devices,
depending on model
SetOut sets state of digital output on the device output numeric ID of output (depends on device) <source lang="lua">devices.cam1.SetOut{output=0, value=true};</source>
value boolean to activate/deactivate the output
SetLED enables/disabled device status LED (do not confuse with LED lights on some camera models) enabled boolean <source lang="lua">devices.cam1.SetLED{enabled=false};</source>
Record triggers recording on camera that is either in "record on trigger" or "monitor events" mode seconds how long should the camera be recording after receiving the action <source lang="lua">devices.cam1.Record{seconds=30};</source>
AdminCGI sends CGI request with admin credentials set for device path request path <source lang="lua">devices.cam1.AdminCGI{path="/cgi-bin/set_rtsp?rtsp_mode=1"};</source>
UserCGI sends CGI request with viewer credentials set for device path request path <source lang="lua">devices.cam1.UserCGI{path="/axis-cgi/com/ptz.cgi?gotoserverpresetno=5"};</source>
Move moves PTZ camera one step in given direction direction one of left, right, up, down, also home for some models <source lang="lua">devices.cam1.Move{direction="left"};</source>
Zoom zooms PTZ camera one step direction one of wide, tele <source lang="lua">devices.cam1.Zoom{direction="tele"};</source>
Focus focuses PTZ camera by one step direction one of near, far, auto <source lang="lua">devices.cam1.Focus{direction="auto"};</source>
Iris manipulates iris by one step direction one of open, auto, close <source lang="lua">devices.cam1.Iris{direction="close"};</source>
PushMove these actions are the same as they step-PTZ counterparts, the only difference is that the movement is not in one step, but continues until second action is called, with stop direction direction same as step-PTZ, but with extra stop value <source lang="lua">devices.cam1.PushMove{direction="up"};</source>
PushZoom <source lang="lua">devices.cam1.PushZoom{direction="stop"};</source>
PushFocus <source lang="lua">devices.cam1.PushFocus{direction="near"};</source>
PushIris <source lang="lua">devices.cam1.PushIris{direction="stop"};</source>
Recall moves PTZ camera to preset position preset name of preset position to move to (some brands such as Panasonic require position number instead of name) <source lang="lua">devices.cam1.Recall{preset="home"};</source>
AddNote adds note to device note string containing the note <source lang="lua">devices.cam1.AddNote{note="apples 2kg"};</source>
xmlData custom XML data <source lang="lua">local rectangleXml = [[<display>
   <size><width>1920</width><height>1080</height></size>
   <rectangle>
       <x>0</x><y>0</y><width>960</width><height>1080</height>
       <color>#ff0000</color>
   </rectangle>

</display>]] devices.cam1.AddNote{note="detected a rectangle", xmlData=rectangleXml};</source>

time when to log the note; you can log note in the past, either by absolute time (in current time zone) or seconds relative to current time This can be use useful if you have an analytics device that produces event with a delay:

<source lang="lua">local car = 'black BMW X6, 1A18888' devices.cam1.AddNote{note=car, time=-5}</source>

Or if you receive an information with time from external source <source lang="lua">local noteTime = os.time{year=2015, month=1, day=13, hour=21, min=3, sec=21} devices.cam1.AddNote{note='some info', time=noteTime}</source>

system device CustomCGI sends HTTP GET command to given url url URL to send <source lang="lua">devices.system.CustomCGI{url="http://192.168.0.1/cgi-bin/foo.cgi"};</source>

Events

Events
availability name description argument argument description
cameras CamMotionDetect emitted when there is a motion detection on camera no content
Tamper on cameras which support this feature, this is emitted when there is tamper alarm on camera
CamRecording emitted whenever we start recording another audio/video segment from the camera
CamVideo These event are emitted whenever there is an audio/video segment recorded from the camera. filePath FTP path of file that was just recorded
CamAudio startTimestamp UNIX timestamp of start of the recording
lengthSec recording duration in seconds
CamManualRecordingStart Start/stop of manual recording username name of user who initiated the event (or actions if it was activated from action)
CamManualRecordingStop
Metadata incoming metadata from camera; currently used only for ONVIF devices xml XML payload of metatada packet of response that has arrived
FPS_update updates framerate and bitrate info from camera fps frames per 1000 seconds (i.e. fps * 1000)
bps bytes per second
clear present when the display should be cleared (for example when the device fails)
AddNote a note has been added to camera stream (this is emitted by AddNote action on corresponding camera) note note text
xmlData custom XML data associated with the note
time UNIX timestamp of the note
system device SmartUpdate emitted when there is updated SMART state info from a disk contains SMART disk attributes in format diskN_attribute
NETIO4 devices DoStateChanged emitted when outlet state changes contains state info for every outlet
various devices Input_update contains variables with updated system stats for system device, and DI info for connected devices
cameras DI_trigger emitted when a camera DI supplied no state, only "trigger" event no content
all connected devices DeviceDisconnect emitted when a connected device fails or becomes unavailable no content
DeviceReconnect emitted when a failing/disconnected device reconnects
system device DiskSpaceStats carries updated recording time calculations freeRecTimeCapacity unused recording capacity in hours
totalRecTimeCapacity unused recording capacity in hours
IncomingCgi incoming CGI request on system arguments supplied with the request (GET or POST)
SystemStarted emitted once on system startup
ScheduleStartStop emitted after schedule becomes active or inactive scheduleId ID of schedule that just changed state
scheduleActive true = schedule is now active; false = schedule has just ended
ScheduleDeleted emitted when a schedule has been deleted from the system scheduleId ID of schedule that has just been deleted
system device where serial communication is available SerialOpened emitted when serial port has been opened for usage with actions id serial port identifier
SerialClosed emitted when serial port has been closed for usage with actions id serial port identifier
SerialRead emitted when data have arrived from serial port id serial port identifier
message message that arrived from serial port
error error message (if there was an error)
SerialError emitted when there was an error on serial port id serial port identifier
error error message


IncomingCgi

IncomingCgi is a user-defined event that can be used to input arbitrary data to action from external source (i.e. a proprietary door sensor). The event is triggered by HTTP request on URI http://IPCorder_IP_Or_Hostname/event with arbitrary arguments. The request can be either GET or POST, i.e. both GET http://ipcorder/event?foo=bar&baz=qux and POST http://ipcorder/event with POST data foo=bar&baz=qux are considered equal.

In both cases, an IncomingCgi event on IPCorder device will be triggered. It will have two arguments, the first named foo with value bar and the second named baz and value qux.

For the request example stated above, a simple rule

<source lang="lua"> local output = "Incoming CGI request: "; for key,value in pairs(event.args) do

 output = output .. " (" .. key .. " = " .. value .. ")";

end logf("%s", output); </source>

will output the following line to system log:

Incoming CGI request: (foo = bar) (baz = qux).

This following rule will check whether incoming request has an argument called camNote, and if it does, it posts its content as a note for camera cam1: <source lang="lua"> if event.args.camNote ~= nil then

   devices.cam1.AddNote{note=event.args.camNote};

end </source>

Special variables

Special variables in actions
name description
devices read-only table of devices and their variables, described above
event Table filled with properties of currently processed event for event-triggered rules. Contains the following items:
  • device ... name of device that produced the event (such as "cam1")
  • name ... name of the event (such as "CamMotionDetect")
  • args ... table of event arguments; depends on the event
self shortcut to devices[event.device]; may be used in generic actions like the following, trigger for example by input update event of any device:

<source lang="lua"> if self.input ~= nil then

   log("device ${self.name} now has input set to ${self.input}")
   if self.SetOut then
       -- if the device has DO, turn it to the opposite state
       self.SetOut{output=0, value=not self.input}
       -- note that this might not work for all devices, as some might have
       -- the outputs numbered from 1
   end

end </source>

Apart from pre-defined device variables, you may set your own, and access them from other actions. For example, the following code will output a log message if there is a camera motion detect after more than an hour on one of your cameras. Add this on the camera's motion detect event: <source lang="lua"> local lastMd = self.lastMd self.lastMd = event.timestamp if self.lastMd == nil then

   logf('first MD on camera %s', event.device)

elseif lastMd + 3600 < event.timestamp then

   logf('first MD on camera after %d seconds', event.timestamp - lastMd)

end </source>

Standard Lua functions and libraries

As of eventer-ng_pre2 (h41dc69e), following standard Lua functions are available in actions:

  • assert
  • error
  • ipairs
  • next
  • pairs
  • pcall
  • select
  • tonumber
  • tostring
  • type
  • unpack

From os library, only the following functions are available:

  • os.date
  • os.difftime
  • os.time

Also, all functions from string and table libraries are available.

IPCorder-specific functions

delay

File:Delay-result.jpg
Log showing output of first example

delay(seconds, callback) runs a callback function after given number of seconds. The delay function returns immediately, and callback is run independently from main code.

Example: <source lang="lua"> -- make a local function that will be used as callback local function delayedDate()

   log("we've got delayed date print");

end

-- schedule the function delay(5, delayedDate); log("delayed print scheduled"); -- this message gets printed immediately </source>

Template:Unote

milliDelay

milliDelay(milliseconds, callback) works the same way as delay, only uses milliseconds instead of seconds. The minimum schedulable delay is 50 ms.

Callback functions can also be defined inline: <source lang="lua"> devices.cam1.SetOut{output=1, value=1}; -- enable cam1's output 1 milliDelay(500, function() devices.cam1.SetOut{output=1, value=0}; end); -- disable output after 500 ms </source>

log

log(message) prints a log message to IPCorder System Log, accesible through IPCorder's web interface. The message can contain substitution codes in form of ${variableName}, that get replaced with values of action engine's global variables.

Examples: <source lang="lua"> log("Hello, world!") log("Current IPCorder's load is ${devices.system.averageLoad}, gate contact state is ${devices.camera1.input}") log("We're currently processing event ${event.name} from device ${event.device}") </source>

Template:Unote

logf

logf(messageFormat, ...) logs the same way as log, but messageFormat is a string, which supports escape codes that are replaced with other logf function arguments. The most important format specifiers are the following:

  • %s ... outputs a string
  • %d ... outputs a number

Examples: <source lang="lua"> logf("device: %s, event: %s", event.device, event.name) -- ^ prints name of the device and event for automatically activated actions

local incomingBytesPerSec = devices.system.incomingTraffic local incomingKbitsps = incomingBytesPerSec * 8 / 1024 logf("incoming IPCorder traffic: %d kbps", incomingKbitsps)

-- of course, this can we written in much shorter way: logf("incoming IPCorder traffic: %d kbps", devices.system.incomingTraffic * 8 / 1024) </source>

Template:Unote

mail

File:Ipcorder-variable-summary.png
E-mail produced by the last example rule

mail(to, subject, text) sends e-mail to given recipient, with given text. E-mail subject and text use the same expansion of ${variable} codes as log function. By default, e-mail with the same subject will be sent at most once every 5 minutes.

<source lang="lua"> mail("john@example.com", "Current IPCorder load", "Current load is ${devices.system.averageLoad}") </source>

If the following code is set for action activated by device event, it will send at most one e-mail per minute for every device. If the action gets triggered for two different devices in a short time, two messages will be sent (because every one will have a different subject). <source lang="lua">mail("john@example.com", "We've got event from ${event.device}", "Incoming event");</source>

The following code has a fixed subject, so all messages will be considered identical and will be subject to default once-in-five-minutes interval check. <source lang="lua">mail("john@example.com", "We've got event", "Incoming event from ${event.device}");</source>

Controlling message frequency

mail(to, subject, text, minIntervalSec, intervalKey)

Maximal frequency of messages sent by mail function can be configured using two optional parameters: minIntervalSec and intervalKey. minIntervalSec allows you to configure the exact interval of messages, the default being 300 (5 minutes). intervalKey is used by the function internally to check whether the message was seen before in given interval; if this is not set, the message subject is used by default.

These optional parameters allow better control of e-mail repeating intervals. The following code sets minimal repeat interval to 30 minutes, and also sets a specific interval key to make all messages triggered by this command fall into same limit category, even though their subject might be different. <source lang="lua">mail("john@example.com", "We've got event from ${event.device}", "Some event is coming", 30*60, 'some-event-coming');</source>


mail function returns a boolean value (true or false) indicating whether the message was passed to e-mail sending facility, or blocked by frequency filter.

<source lang="lua"> local ret = mail("john@example.com", "mail", "hello", 60) if ret == true then

   log("we've tried to send the e-mail")
   -- note that actual sending of the message may still have failed;
   -- details about this will be available in IPCorder System log

else

   log("mail not sent, it would be more often than once per minute")

end </source>

Template:Devnote Using Lua's built-in functions, a complex e-mail text can be written:

<source lang="lua"> local output = 'Hello, this is your IPCorder variable summary\n' output = output .. 'for ' .. os.date() .. '\n\n'

for device, variables in pairs(devices) do

   -- for every device in the system, add a nice header
   output = output .. string.format('=== %s ===\n', device)
   
   -- then go through its variables
   for name, value in pairs(variables) do
       -- and add a line of output for every value that has some useful printable type
       if type(value) == 'number' or type(value) == 'string' or type(value) == 'boolean'  then
           output = output .. string.format('%s = %s\n', tostring(name), tostring(value))
       end
   end
   output = output .. '\n'

end

mail('my-address@example.com', 'IPCorder variable summary', output) </source>

ping

Template:Unote

ping{device="cam1", callback=function(o) if o.success then ... else ... end}
ping{address="www.google.com", callback=function(o) if o.success then ... else ... end}

ping a device or address and process the response in a callback. Callback and either device or address must be specified. Device and address options cannot be used together. There is an optional timeout argument, which sets the ping timeout in seconds (default is 30).

callback arguments

Callback function receives single table with these arguments:

  • success: ping success (true/false)
  • duration: ping duration in milliseconds
  • errorInfo: error description text

Examples: <source lang="lua"> -- ping www.seznam.cz and log the result local function logPingResult(o)

  if o.success then
     log("seznam ping OK")
  else
     log("seznam ping FAILED")
  end

end

ping{address="www.seznam.cz", callback=logPingResult}

-- ping device "cam1" with 60 seconds timeout ping{device="cam1", timeout=60, callback=function(o) log("duration: " .. o.duration); end} </source>

cgiGet

Template:Unote

cgiGet{{url='http://httpbin.org/ip', timeout=2, bufferSize=40, callback=function(o) if o.result == 0 then myip = string.match(output.buffer,"(%d+.%d+.%d+.%d+)") end end}

post cgi to given url. NOTE: no ssl (https) support

arguments

  • url: url to get
  • callback: callback function - function to process result of the cgi get
  • timeout: optional - timeout in seconds for the request (default 30s)
  • bufferSize: optional - size of response buffer +1 (default 4K, maximum 1M). size 0 means that the page will be discarded (callback will be still called but without buffer)

callback arguments

Callback function receives single table with these arguments:

  • result: result code
    • 0 means success
    • 22 url not found
    • 27 buffer overflow
    • 28 timeout
    • ...
  • errorInfo: error description in text
  • received: number of bytes received in response
  • buffer: buffer with response page. if received is 0 then buffer is not present
    • If received is 0 then buffer is not present.
    • NOTE: if received is bigger than bufferSize (result == 32) then buffer will contain bufferSize -1 valid characters.

Examples

<source lang="lua"> -- get our ip and log it local function logOurIp(o)

  if o.result == 0 then
     log(string.format("our ip is %s", string.match(o.buffer,"(%d+.%d+.%d+.%d+)")))
  else
     log(string.format("cgi to get our ip failed with error %d: %s", o.result, o.errorInfo))
  end

end

cgiGet{url='http://httpbin.org/ip', timeout=2, bufferSize=40, callback=logOurIp} </source>

toboolean

Template:Devnote toboolean(value) converts given value to boolean (e.g. true/false), and is currently used to normalize values for comparation in code generated from GUI.

toboolean uses the following conversions:
type conversion rule examples
numbers all non-zero numbers are true <source lang="lua">

toboolean(1) --> true toboolean(0) --> false toboolean(-1) --> true (-1 is non-zero) </source>

boolean false is false, true is true <source lang="lua">

toboolean(true) --> true toboolean(false) --> false </source>

strings all non-empty strings are true <source lang="lua">

toboolean('hello') --> true toboolean('false') --> true (this is also non-empty string) toboolean() --> false </source>

tables non-empty tables are true <source lang="lua">

toboolean({1}) --> true (non-empty table) toboolean({false}) --> true (also non-empty table) toboolean({}) --> false </source>

nil special value nil is considered false <source lang="lua">

toboolean(nil) --> false </source>

other all other values evaluate to true <source lang="lua">

toboolean(function() return false end) --> true </source>

bitwise operators

netio firmware starting 2.3.4 has luabit library included. for api see luabit api

xml

Template:Devnote xml module handles XML data processing. It provides the following functions:

xml.escape

Escapes a string to be used in XML structure. <source lang="lua"> xml.escape("A < B") --> "A < B" </source>

xml.check

Checks if given string is well-formed XML. Returns either true, or false and error message. <source lang="lua"> xml.check('<foo>something</foo>') --> true xml.check('<hello>') --> false, 'unclosed tag' </source>

xml.parse

Parses an XML string, so it can be inspected in Lua. Returns an "XmlElement" object representing an XML tree root, fails on error.

XmlElement properties and methods
name description usage
name element name <source lang="lua">

parsed = xml.parse('<hello><foo>cat</foo><foo color="blue">dog</foo></hello>') parsed.name --> "hello" parsed.children()[1].name --> "foo" </source>

text element text content <source lang="lua">

parsed.text --> "" parsed.child("foo").text --> "cat" </source>

attr(name) content of given attribute (or nil) <source lang="lua">

parsed.get("foo", 2).attr("color") --> "blue" parsed.attr("non-existing") --> nil </source>

attr() table of all the attributes <source lang="lua">

parsed.attr() --> {} parsed.get("foo", 2).attr() --> {color="blue"} </source>

child(name) first child with given name (or nil) <source lang="lua">

parsed.child("foo").name --> "foo" parsed.child("non-existing") --> nil </source>

children(name) array of children with given name <source lang="lua">

parsed.children("foo") --> {<Foo 1>, <Foo 2>} parsed.children("non-existing") --> {} </source>

children() array of all children <source lang="lua">

parsed.children() --> {<Foo 1>, <Foo 2>} parsed.child("foo").children() --> {} </source>

get(...) retrieves given element in the tree. for string arguments, looks up child with given name, goes to n-th element when argument is numeric. Returns nil for non-existing elements. <source lang="lua">

parsed.get("foo") --> <Foo 1> parsed.get("foo", 1) --> <Foo 1> parsed.get("foo", 2) --> <Foo 2>

complex = xml.parse("<hello><foo><bar><baz>shallow</baz><baz>deep</baz></bar></foo></hello>") complex.get("foo", "bar", "baz").text --> "shallow" complex.get("foo", "bar", "baz", 2).text --> "deep" complex.get("foo", "non-existing", 5, "another") --> nil </source>

next next sibling with the same name (or nil) <source lang="lua">

complex.get("foo", "bar", "baz").next.text --> "deep" </source>

xml outer XML of given element (may not work reliably) <source lang="lua">

complex.xml --> "<hello><foo><bar><baz>shallow</baz><baz>deep</baz></bar></foo></hello>" </source>

innerXml inner XML of given element <source lang="lua">

complex.get("foo", "bar").innerXml --> "<baz>shallow</baz><baz>deep</baz>" </source>

parent parent element (or nil for root) <source lang="lua">

complex.get("foo", "bar", "baz").parent.name --> "bar" complex.get("foo", "bar", "baz").parent.parent.name --> "foo" complex.get("foo", "bar", "baz").parent.parent.parent.name --> "hello" </source>

root root element which was parsed (nil for root) <source lang="lua">

complex.get("foo", "bar", "baz").root.name --> "hello" complex.root --> nil </source>

If the second parse argument is xml.STRIP_PREFIXES, the parser will strip XML namespace prefixes from element names and attributes. This is especially useful when parsing messages from ONVIF devices, which can theoretically use any prefixes they want. <source lang="lua"> local simplified = xml.parse('<bb:hello xmlns:bb="urn:foo:bb" xmlns:cc="urn:cc"><cc:bar bb:prop="value">named</cc:bar></bb:hello>', xml.STRIP_PREFIXES) simplified.child('bar').attr('prop') --> value </source>

xml.parseOnvifNotifications

Parses simple ONVIF-formatted notifications into Lua table structure.

<source lang="lua"> local sourceXml = [[<?xml version="1.0" encoding="UTF-8"?> <tt:MetaDataStream xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:tns1="http://www.onvif.org/ver10/topics"> <tt:Event><wsnt:NotificationMessage><wsnt:Topic Dialect="http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet">tns1:RuleEngine/VehicleDetector/Vehicle</wsnt:Topic> <wsnt:Message><tt:Message UtcTime="2015-04-08T07:49:42Z" PropertyOperation="Changed"><tt:Source><tt:SimpleItem Name="VideoSourceConfigurationToken" Value="VideoSourceToken"/> <tt:SimpleItem Name="VideoAnalyticsConfigurationToken" Value="VideoAnalyticsToken"/> <tt:SimpleItem Name="Rule" Value="MyVehicleDetector"/> </tt:Source> <tt:Key><tt:SimpleItem Name="LicensePlate" Value="3S80657"/></tt:Key> <tt:Data><tt:SimpleItem Name="Nation" Value="EU"/> <tt:SimpleItem Name="Country" Value="CzechRepublic"/> </tt:Data> </tt:Message> </wsnt:Message> </wsnt:NotificationMessage> </tt:Event> </tt:MetaDataStream>]]

local parsed = xml.parseOnvifNotifications(sourceXml) -- {{ -- Topic = 'tns1:RuleEngine/VehicleDetector/Vehicle', -- UtcTime = '2015-04-08T07:49:42Z', -- PropertyOperation = 'Changed', -- Source = { -- VideoSourceConfigurationToken = 'VideoSourceToken', -- VideoAnalyticsConfigurationToken = 'VideoAnalyticsToken', -- Rule = 'MyVehicleDetector', -- }, -- Key = { -- LicensePlate = '3S80657', -- }, -- Data = { -- Nation = 'EU', -- Country = 'CzechRepublic', -- }, -- }} </source>

This will add detected licensed plates from Hikvision cameras to device notes (when added on "Incoming metadata" event of the camera). <source lang="lua"> local parsed = xml.parseOnvifNotifications(event.args.xml) for _, message in pairs(parsed) do

   if message.Topic == 'tns1:RuleEngine/VehicleDetector/Vehicle' then
       local note = string.format("plate %s", message.Key.LicensePlate)
       devices[event.device].AddNote{note=note}
   end

end </source>

This will add notes for crossed lines from Vivotek Line Detector: <source lang="lua"> for _, message in pairs(xml.parseOnvifNotifications(event.args.xml)) do

   if message.Topic == 'tns1:RuleEngine/LineDetector/VVTK_Crossed' then
       local note = string.format('crossed line %s', message.Source.Rule)
       devices[event.device].AddNote{note=note}
   end

end </source>

Differences from IPCorder 1.x rules and 2.0.x actions

what IPCorder 1.x rules, IPCorder 2.0.x actions IPCorder 2.1 actions
Rule header rule code is formatted as
(DeviceName, EventName) -> rule body
action code contains only the body, triggering device and event is filled in separately
Rule stanzas separated by semicolon as in
eq(foo, 1), set(bar, 2), set(baz, 3); set(qux, 4)
classic programming language, statements may be separated by semicolon or by whitespace, so both it's possible either this:
<source lang="lua">

if foo == 1 then

   bar = 2
   baz = 3

end qux = 4</source> or this: <source lang="lua">if foo==1 then bar=2;baz=3;end;qux=4</source>

Conditions: AND Like in Prolog language, AND is done through comma:
eq(foo, 1), eq(bar, 2), set(baz, 3)
Lua supports classic if:<source lang="lua">if foo == 1 and bar == 2 then baz = 3; end</source>
including parentheses:<source lang="lua">if (foo == 1) or (foo ~= 3 and bar == 3) then baz = 5; end</source>
Conditions: OR OR has to be worked around using a temporary variable:
set(temp, 0), eq(foo, 1), set(temp, 1); eq(bar, 2), set(temp, 1); eq(temp, 1), set(baz, 3)
<source lang="lua">if foo == 1 or bar == 3 then baz = 3; end</source>
Device variables Uses dot notation: cam1.fps Uses dot notation, but all device variables are in devices table: devices.cam1.fps. Additionally, system-set variables (such as fps for cameras) are read-only for user rules.
Variable scope Variable without namespace (foo) refers to the device that triggered the event. Global variables are prefixed with G (G.foo) Variable without namespace (like foo) means a global variable. Global variables can be also referred to as parts of _G table (_G.foo). Rule-local variables can (and should) be declared as local with local foo, for example:<source lang="lua">local tmp; tmp = a; a = b; b = tmp</source>

Even though in Lua you can do this simply assigning<source lang="lua">a,b = b,a</source>

Default value of unset variables 0 nil (Lua's null value)
Comments None Both one line and multiline:<source lang="lua">foo = 1 --this is a comment until the end of line

bar = 2 --[[ this is a very long comment that does not end until I close it with]] baz = 3</source>

Device actions Called either by
action(cam1, cam1.SetOut, 3)
or with string as
straction(cam1, cam1.UserCGI, "/cgi-bin/admin_command.cgi?arg=42")
Actions are included as methods in device variables, and are called with named arguments:

<source lang="lua">devices.cam1.SetOut{output=1, value=1}</source> Strings are normally supported:<source lang="lua">devices.cam1.UserCGI{path="/cgi-bin/admin_command.cgi?arg=42"}</source> (see Lua manual for string syntax details)

E-mail sending sendMail("subject", "recipient@domain.com", "e-mail body") <source lang="lua">mail("subject", "recipient@domain.com", "e-mail body")</source>
Delay uses callback code in string:
delay('set(foo, 1)', 10)
uses callback code as a function object:<source lang="lua">local function setFoo() foo = 1; end;

delay(10, setFoo)</source> or inline: <source lang="lua">delay(10, function() foo = 1; end)</source>

Further info manual 1.1.0 Lua reference manual
Programming in Lua (first edition)

Examples

PTZ preset tour

Jak to funguje: akce definuje funkci která sama sebe spouští každých pár vteřin, a pak ji poprvé spustí, a ona v eventeru neustále cyklí dokola. Tohle je docela nepraktické, pokud tu akci chceš třeba změnit, a zároveň určitě nechceš aby ti tohle běželo víc než jednou.

Proto je v akci proměnná "myVersion", která se ukládá do globální proměnné, v podstatě říká že v prostředí je "instalovaná" (spuštěná) nějaká verze té funkce. To zajišťuje dvě věci:

  • Pokud v akci máš verzi X, a v systému už verze X běží, tak se nic měnit nebude
  • Pokud akci upravíš, tak v ní nastavíš nějaké jiné číslo verze, a to způsobí že se spustí ta nová funkce, a zároveň si ta původní funkce všimne že se něco změnilo a skončí. Tím se vyhneš otravné nutnosti zabíjet běžící cykly restartem IPCorderu.

<source lang="lua"> -- tady je definovana nejaka verze a test na to zda ji uz nemame local myVersion = 1 -- when editing code in IPCorder, change this number as well if _G.scriptVersion == myVersion then

   return

end

-- nastavime promennou indikujici ze pouzivame tuhle verzi logf("Running script version %d (from previous %s)", myVersion, tostring(_G.scriptVersion)) _G.scriptVersion = myVersion

-- pole s id presetu kteryma chceme prochazet local presets = {} presets[0] = 'home' presets[1] = 'second' presets[2] = 'third'

-- promenna urcujici nasi iteraci local moveIteration = 0

-- funkce ktera dela jednu iteraci function moveCamera()

   -- overeni ze nekdo nespustil nejakou novou verzi
   if _G.scriptVersion ~= myVersion then
       return
   end
   -- posun na dalsi pozici v nasem pocitadle
   moveIteration = moveIteration + 1
   if moveIteration > 2 then
       moveIteration = 0
   end
   -- tady je otoceni na to momentalni pozici
   logf("Rotating to preset %s", presets[moveIteration])
   -- devices.camera1.Recall{preset=presets[moveIteration]}
   -- a spustime sami sebe za 5 vterin
   delay(5, moveCamera)

end

-- tady pustime funkci poprve, a ona se pak bude v cyklu poustet furt moveCamera() </source>

Preset tour with stop/restart buttons

Extends previous case with user buttons to stop and restart the patrol. Patrol is still started automatically when IPCorder is turned on. Two more global variables are used:

  • doPatrol, which is used to signal whether the patrol should be in operation
  • cycleRunning, which is used to prevent running two patrol cycles simultaneously in case user stops and immediately restarts the patrol

system.Input_update event

<source lang="lua"> -- tady je definovana nejaka verze a test na to zda ji uz nemame local myVersion = 1 if _G.scriptVersion == myVersion then

   return

end

-- nastavime promennou indikujici ze pouzivame tuhle verzi logf("Running script version %d (from previous %s)", myVersion, tostring(_G.scriptVersion)) _G.scriptVersion = myVersion

-- chceme se tocit if _G.doPatrol == nil then

   _G.doPatrol = true

end

-- pole s id presetu kteryma chceme prochazet local presets = {} presets[0] = 'home' presets[1] = 'second' presets[2] = 'third'

-- promenna urcujici nasi iteraci local moveIteration = 0

-- funkce ktera dela jednu iteraci function moveCamera()

   -- overeni zda nemame koncit
   if _G.scriptVersion ~= myVersion then
       return
   end
   if not _G.doPatrol then
       _G.cycleRunning = false
       return
   end
   _G.cycleRunning = true
   -- posun na dalsi pozici v nasem pocitadle
   moveIteration = moveIteration + 1
   if moveIteration > 2 then
       moveIteration = 0
   end
   -- tady je otoceni na to momentalni pozici
   logf("Rotating to preset %s", presets[moveIteration])
   -- devices.camera1.Recall{preset=presets[moveIteration]}
   -- a spustime sami sebe za 5 vterin
   delay(5, moveCamera)

end

-- tady pustime funkci poprve, a ona se pak bude v cyklu poustet furt moveCamera() </source>

stop button

<source lang="lua"> _G.doPatrol = false </source>

restart button

<source lang="lua"> _G.doPatrol = true if not _G.cycleRunning then

   moveCamera()

end </source>

NETIO4 CGI Parser (similar to control.tgi on older netio)

Use Incomming CGI request as Action trigger

<source lang="lua"> -- function for parsing port arg value and performing its action local function portparse(s)

 local portnumber = 1;
 for c in string.gmatch(s, "%w") do -- take only alphanumerical chars
   if portnumber > 4 then return end; -- break
   if c=="0" then
     devices.system.SetOut{output=portnumber, value=false}
   elseif c=="1" then
     devices.system.SetOut{output=portnumber, value=true}
   elseif c=="i" then
     devices.system.ResetOut{output=portnumber}
   else -- do nothing
   end
   -- debug info (Uncomment line bellow to show debug info)
   -- logf("CGI parser: Port %d obtain value %s",portnumber,c);
   portnumber = portnumber+1;
  end

end

local port=event.args.port; local pass=event.args.pass;

-- here change accepted_pass value local accepted_pass="password";

-- Comment out following block when you are using more CGI triggered actions. if (not port) or (not pass) then

 log("CGI parser: PORT and/or PASS argument missing, please check your CGI command. Use following syntax"
    .. " for the control CGI http(s)://netio.ip/event?port=10iu&pass=\"secret\" Where accepting arguments"
    .. " for port 1 to 4 are: 0...off, 1...on, i...interrupt (reset), any other char for port skip (unused)");
 do return end; -- break (end of action)

end

if (pass==accepted_pass) then portparse(port) else log("CGI parser: Wrong password") end </source>