Skip to content

Script tutorial

Jan Gabrielsson edited this page Sep 5, 2018 · 48 revisions

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.

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. Calls fibaro: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 to 55:value but more descriptive if lux or temperature values are accessed. 55:safe, 55:breached are the same as 55:isOff and 55: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, and 101: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 to fibaro:get(ID,X). Ex. 55:batteryLevelworks.

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 corresponding fibaro: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 postinterval 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. $Presenceis equal to fibaro:getGlobalValue('Presence')

Rule.eval("$Presence")
=> 1
Rule.eval("$Presence=$Presence+1")
Rule.eval("$Presence")
=> 2

Daily rules

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

Trigger rules

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

Event rules

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

Functions

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:sleepis 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 setTimeoutunder 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. waitis 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.

onceis 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:00needs 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.

forallows 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 forexpression 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 forcommand 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.