-
Notifications
You must be signed in to change notification settings - Fork 11
Script tutorial
Script rules comes in 3 flavors. Event pattern rules, Daily rules, and Trigger rules. Common is that they all are of the form
Rule.eval("<expression> => <statements>")
e.g. a rule contains the '=>' keyword. Rule.eval called on expressions/statements without '=>' are just executed and returning the result. Useful for setting up values and/or give immediate commands to devices.
Let's start with some expressions.
Rule.eval("2+2*3*(2+2)")
=> 26
Rule.eval("2 < 3 & 5 >= 4")
=> true
Time can be expressed as HH:MM or HH:MM:SS and is translated to seconds internally, but is a convenient way to express time in scripts.
Rule.eval("01:30")
=> 5400
Rule.eval("11:30+05:40 == 17:10")
=> true
From now on Rule.eval
is excluded in the examples.
Assume that there is a light switch/dimmer with ID 55.
55:on
=> true
This turns on the light with device ID 55. :
is an operator that on the left hand side takes an ID (number), 55 in this case, or a table of IDs, and on the right hand side takes a 'function' that retrieves or sets a device ID property.
{55,66}:on
turns on the light for device 55 and 66. Other functions available are:
-
55:isOn
, returns true if device 55 is turned on.{55,66}:isOn
returns true if any of 55 or 66 is turned on. The logic is that if you have a room with many lights and at least one light is on in the room there is light in the room. -
{55,66}:isAllOn
, like the above but all lamps have to be on. On a single device it behaves like:isOn
-
55:off
,55:isOff
.55:isAnyOff
, is the corresponding functions for setting/testing if devices are turned off. -
55:value
, return the device id value property. Callsfibaro:get(55,'value')
.{55,66}:value
returns a table with the IDs values. Ex.{55,66}:value => {'0','1'}
-
55:lux
,55:temp
, are identical to55:value
but more descriptive if lux or temperature values are accessed.55:safe
,55:breached
are the same as55:isOff
and55:isOn
respective but more descriptive when dealing with sensors. -
55:toggle
, toggle the light, if it's turned on it's turned off etc. -
55:scene
, return the 'sceneActivation' property of a device. -
55:last
, return seconds since the device last changed state. -
101:start
, and101:stop
, start resp. stop the scene with ID 101, works with table of IDs also.
All of the above works with tables of IDs.
Properties X that are not recognized are sent tofibaro:get(ID,X)
. Ex.55:batteryLevel
works.
Some 'properties' can be set also.
-
55:value=1
, sets the property 'value' to 1. E.g. `fibaro:call(55,'setValue','1') -
:R
, ':G',:B
,:color
,:armed
,:W
,:time
, are other properties with correspondingfibaro:call(55,'setX',Y)
actions. -
200:msg="Hello"
, push the message "Hello" to the registered phone with ID 200. -
87:btn=2
, presses button 2 on VD with ID 87.
One way to play with this is to include the following as the only rule in main()
function main()
Event.event({type='shell'},function(env)
io.write("Eval> ") expr = io.read()
if expr ~= 'exit' then
print(string.format("=> %s",tojson(Rule.eval(expr))))
Event.post({type='shell', _sh=true},'+/00:10')
end
end)
Event.post({type='shell', _sh=true})
end
This is a rule that every 10min stops and reads in an expression from the console and executes it. If run in an IDE with _speedtime
set there is no 10min wait, but it gives time to other rules to execute. If run in realtime I would advise to set the post
interval to +/00:00:02
, e.g. 2sec. Even more daring set the _REMOTE
flag to true in EventRunnerDebug.lua
to actually carry out the commands on devices on the HC2, kind of remote controlling lamps from a shell... In the example below, we don't and the fibaro commands are just logged to the console.
Fri May 04 04:32:06 Demo - EventRunner v0.999
Fri May 04 04:32:06 Loading rules
Fri May 04 04:32:06 Scene running
Fri May 04 04:32:06 Sunrise 06:00, Sunset 18:00
Eval> 88:on
Fri May 04 04:32:21 fibaro:call(88,'turnOn')
=> true
Eval> {77,88}:on
Fri May 04 04:42:33 fibaro:call(77,'turnOn')
Fri May 04 04:42:33 fibaro:call(88,'turnOn')
=> true
Eval> {200,201}:msg='Hello!'
Fri May 04 04:53:10 fibaro:call(200,'sendPush', 'Hello!')
Fri May 04 04:53:10 fibaro:call(201,'sendPush', 'Hello!')
=> "Hello!"
Eval> 55:value=100+1
Fri May 04 05:03:51 fibaro:call(55,'setValue', '101', '')
=> 101
Eval> myLamp=55
=> 55
Eval> myLamp:isOn
=> true
Last example sets a variable, myLamp
to 55 and used that in the following expressions. Variables are declared when they are first used. They can also be declared outside the script in Lua to setup variables. Useful for bringing in a home table etc.
Util.defvar('myLamp',55)
Rule.eval("myLamp:on")
local conf = json.decode(fibaro:getGlobalValue(_deviceTable))
--- assuming a structure of type {dev:{room:{lamp:66,door=77}...
for var, val in pairs(conf.dev) do Util.defvar(var,val) end
Rule.eval("room.lamp:isOn")
Fibaro's global variables are specified with a $
prefix. Ex. $Presence
is equal to fibaro:getGlobalValue('Presence')
Rule.eval("$Presence")
=> 1
Rule.eval("$Presence=$Presence+1")
Rule.eval("$Presence")
=> 2
So, only expressions are not that useful, we want rules that trigger on times and devices that change states.
Rule with a daily(<time>)
or daily({<time1>,...})
(or shorthand @<time>
) are treated as daily rules and is run every day at the time(s) specified. Ex.
@10:00 => log('10 o'clock')
@sunset-00:15 => log('Soon sunset')
@sunrise+00:10 & @sunset-00:10 => log('Sunrise or sunset')
The first rule is run at 10:00 every day. The second at sunset-15min, and the last will be run both at sunrise+10min and sunset-10min. They all call the function log
that also can take arguments and format like Lua's string.format
. Ex. log('%s+%s=&s',4,5,4+5)
Left-hand side or the right hand side can contain more conditions to further specify if the action on the right-hand side should be carried out).
@10:00 & wday('mon-wed,sun') => log('10 o'clock Monday,Tuesday,Wednesday, or Sunday')
@sunrise+00:15 & day('last')&month('jan-jun' => log('Sunrise+15min last day of the month in January to June')
@sunset-00:10 & $Presence>0 => room.lamp:on; log('Sunset and at home, turning on lamp')
Right hand side of rules are <statements>. A statement can be an <expr> or some other specific commands, separated with a semi-colon ';'. This is only allowed on the right-hand side of a rule, or in 'non-rules'. Ex. Rule.eval("a=77; a:on")
Rules that don't contain 'daily' have their left-hand side scanned for uses of deviceIDs or fibaro globals and time interval tests '..'. All IDs and globals found are used as triggers to run the rule, i.e. any state change the IDs or globals. For intervals, the starting time end the ending time+1 are also triggers. Ex.
66:isOn & $Presence>0 => log('lamp on and at home')
This will identify deviceID 66 and global 'Presence' and whenever they change state the rule will be called. The whole rule will be run so if 66 is not on or 'Presence' is not greater than 0, the right-hand statement will not be executed.
For this to work the ID 66 and global 'Presence' need to be declared in the header of the scene like normal scenes, or the EventRunner framework will not be notified about the state changes...
The reason for using intervals as triggers are that an expression like this:
66:isOn & $Presence>0 & 11:00..12:00 => log('lamp on and at home')
would not trigger if the lamp is turned on and presence set to >0 before 11:00, as the rule would not be run if there were no further state changes for the lamp or the global after 11:00. With 11:00 and 12:00:01 as trigger the rule is checked when we enter and leave the time interval too, making the intention of the rule to work.
If the left hand contains 'daily', test like above will just be tests and not triggers. Ex.
@10:00 & 66:isOn & $Presence>0 => log('lamp on and at home at 10 o'clock')
This will only be run at 10:00 every day and not when 66 or 'Presence' change state. However, the tests need to be true for the statement to be executed. If need to condition a trigger with a time interval, use ..
operator.
66:isOn & $Presence>0 & 10:00..11:00 => log('lamp on and at home between ten and eleven')
66:isOn & $Presence>0 & 10:00:00..10:00:03 & wday('fri') => log('lamp on and at home within 3 seconds at 10 on a Friday')
If no deviceID or global is found (or 'daily') in a rule, the framework will complain. It is important that deviceIDs used are declared before the rule is declared as the scanning is only done when the rule is declared. Ex. if variables are used for device IDs they need to be setup before. Ex.
Rule.eval('lamp=66')
Rule.eval("lamp:isOn & $Presence>0 & 10:00..11:00 => log('lamp on and at home between ten and eleven')")
Standard fibaro events from devices and globals are easiest handled with trigger rules as in the previous section. However, for 'user defined events', handlers for them can also be declared. Ex.
Rule.eval("#test{val=42} => log('Value is 42')")
Rule.eval("#test{val='$x'} => log('Value is %s',x)")
Rule.eval("#test{val='$x>52'} => log('Value is %s',x)")
Rule.eval("post(#test{val=42},t/10:00)")
=> Value is 42
=> Value is 42
Event rules can only have a single event on the left-hand side, no other test/expressions. Additional test/conditions need to be carried out on the right-hand side.
The first rule in the example sets up a handler that matches events of type {type='test', val=42}
.
In the second rule, 'val' can match any value but we have specified that it should bind the variable 'x' to whatever actual value 'val' has. ('$' is used here to mark a constraint in matching and has nothing to do with fibaro globals).
In the third rule, 'x' is bound to the value of 'val' but we have an additional constraint that val has to be > 52.
When we in the last expression post the event '#test{val=42}' (at 10:00) the first and the second handler will be triggered. 'x' can conveniently be used as a local variable in the rule. This allows the script expressions to be easily used together with the Event.event/Event.post primitives in the base framework.
Assume something needs to be done every 15min on some specific days
Rule.eval("#check => day('wed-sat') & checkWater() ; post(#check,+/00:15)")
Rule.eval("post(#check)”) -- start the loop/check
First rule triggers on an incoming {type='check'} event, tests if it is the right day, if so calls the checkWater function and then posts the event again in 15min. This will trigger the rule again and we have a loop checking the water every 15min. The second statement just makes the initial post to get things going.
There is a rule syntax for running actions at specific intervals that almost look like 'dailys';
Rule.eval("@@00:15 => day('wed-sat') & checkWater()") -- check water every 15min on wed-sat
User defined functions like 'checkWater' above is just script variables bound to Lua functions. Ex.
local function myFun(a,b) return a+b end
Util.defvar('myFun',myFun)
Rule.eval("2*myFun(4,5)+1")
=>20
If the function is a Lua global function, there is no need to declare it with Util.defvar. Script variables that are not found in the script engine is searched for in Lua's global _ENV table, where global variables and functions are available.
function myFun2(a,b) return a*b end
Rule.eval("2*myFun2(4,5)+1")
=>41
There is also a rudimentary script function syntax (that may change in the future)
Rule.eval("a=fn(a,b) return(a+b) end; a(4,5)")
=> 9
Rule.eval("(fn(a,b) return(a+b) end)(4,5)")
=> 9
There are some more built-in functions/statements that are useful.
Rule.eval($Presence=='away' || 21:00..09:00 >> log('away at night')|| 09:00-21:00 >> log('away at day')")
|| <expr> >> <statements>
should be read like a chain-able 'if-then'. The snag is that the ';' can not be used to end the 'if-then' statement as the 'then' part can be a list of statements separated by ';'. To overcome that there is a 'double-stop', ';;' that can be used. Ex.
Rule.eval("55:isOn => $Presence=='home' || 21:00..09:00 >> checkWater() ;; log('home')")
This will only check the water if it is between 21-09, but will always do the 'log'. With a single ';' the log would have belonged to the statements conditioned by '21:000..09:00'. The reason for this syntax is that I like to write rules like this:
Rule.eval([[$Presence=='away'
|| 06:00..12:00 >> log('Do things if away between 06-12')
|| 12:00..15:00 >> log('Do things if away between 12-15')
|| 15:00..18:00 >> log('Do things if away between 15-18')
|| 18:00..00:00 >> log('Do things if away between 18-00')
|| 00:00..06:00 >> log('Do things if away between 00-06')]])
Another useful function is wait
. fibaro:sleep
is not good as it stops anything going on in the scene while sleeping. To overcome that people use setTimeout
. wait
looks like a sleep but uses setTimeout
under the hood so other rules can run meanwhile.
Rule.eval("55:isOn => wait(00:10); || 55:last>=00:10 >> 55:isOff")
If device 55 is turned on it waits 10min, then checks if 55 hasn't changed state in the last 10min and if so turns off the light. wait
is also useful for writing expressions to test rules.
Rule.eval("wait(t/10:00); 55:on")
Rule.eval("wait(t/10:05); 55:off")
Rule.eval("wait(t/10:07); 55:on")
This turns on the lamp at 10:00, off again at 10:05, and then on again at 10:07.
once
is a special function that keeps state so that it only returns true if it's argument is true but was false before. This is a convenient way to stop an event from triggering multiple times. Ex.
Rule.eval("downstair.sensor:isOn & once(06:00..08:00) => speakWithSONOS('Good morning!')")
This rule triggers whenever the sensor triggers and then checks if it is between 6-8. If that is true the test 06:00..08:00
needs to turn false before it will be able to turn true again, effectively only greeting people once between 6-8 when the sensor is breached.
for
allows for checking if a condition is true for a specified duration.
Rule.eval("for(00:10,downstair.sensor:isOff) & downstair.lamp:isOn => downstair.lamp:isOff")
The for
expression needs in this case to be true for 10min before it will return true. First time the expression is true a timer is started that checks in 10min if the expression is still true and then returns true. If the expression turns false during that period, the timer is canceled.
Sometimes when the for
expression becomes true, one wants to reset the timer to get a new trigger the time again. Typical case is if a door is open for 10min send a notification, and continue to send an alarm for every 10min until the door is closed. So, there is a need to 'tell' the for
command to re-enable the timer. ¨repeat' does that.
Rule.eval("for(00:10,downstair.door:breached) => phone:msg='Door is open'; repeat()")
This will log a message every 10min as long as the door is open. repeat
returns the number of repetition it has currently done, and it can take an argument how many repeats it should do.
Rule.eval("for(00:10,downstair.door:breached) => phone:msg=log('Door is open for %s min',repeat(3)*10)")
=> Door is open for 10 min
=> Door is open for 20 min
=> Door is open for 30 min
In this case it will only repeat 3 times, and the for-timer is not reset. However, the next time the for expression becomes true it starts over again.