LUA YPC source
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.
Contents
- 1 Basics of Lua
- 2 Specifics of IPCorder Lua environment
- 3 Per-device variables
- 4 Device actions
- 5 Events
- 6 Special variables
- 7 Standard Lua functions and libraries
- 8 IPCorder-specific functions
- 9 Differences from IPCorder 1.x rules and 2.0.x actions
- 10 Examples
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.
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
.
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
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
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
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:
|
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
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>
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>
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>
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
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
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.
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.
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 ineq(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 byaction(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>