jgab 898 Author Share Posted October 27, 2018 (edited) 12 hours ago, petrkl12 said: I have tried your scene it looks that it's still doesn't work fully (restart scene). I have stoped manualy one of watch scene and I can see this message in that scene every minute: "Aborting: Server not started yet" and scene is not starting ... Hi, I have been able to fully debug the ping/pong logic and made another change the original post (needed to wait between doing another 'watchScene') what I noticed is that when I cut and paste from the forum to the HC2 I get strange characters in the code that the HC2 doesn't like and I just chased a bug where the HC2 Lua compiler ignored a large part of a table initialiser without saying anything because of some strange invisible characters there... Anyway. Yes because the the scene is not running it logs "Aborting: Server not started yet" when it gets a ping, and doesn't send a pong. However, after a while when the client doesn't get a pong back it will restart the scene. Because I just recently started to urlencode/urldecode the payload in fibaro:startScene (to cope with foreign chars), the log statement for startScene was not so nice, so I fixed that to url decode the payload when loging. That's the change in the version I just pushed for iOSLocator and EventRunner. Edited October 27, 2018 by jgab Quote Link to post Share on other sites
jgab 898 Author Share Posted October 27, 2018 (edited) 3 hours ago, petrkl12 said: Another question: I have a lot of rules that use timers: rule("@@hh:mm") it means that after restart all that rules run in one time at the beginning - impact is big load i.e. during Fibaro restart or scene restart Is there possibility to add some parameter that first run will be after end of first interval? Ok, a bit of a hack but... Rule.eval("@@-00:10 => log('HUPP')") i.e. a negative time will run the interval after the first time. In this case after 10 min, and then run it every 10 minute. Just pushed the new version. Edited October 27, 2018 by jgab Quote Link to post Share on other sites
petrkl12 66 Share Posted October 27, 2018 (edited) Thanks for adding. 55 minutes ago, jgab said: Hi, I have been able to fully debug the ping/pong logic and made another change the original post (needed to wait between doing another 'watchScene') what I noticed is that when I cut and paste from the forum to the HC2 I get strange characters in the code that the HC2 doesn't like and I just chased a bug where the HC2 Lua compiler ignored a large part of a table initialiser without saying anything because of some strange invisible characters there... Anyway. Yes because the the scene is not running it logs "Aborting: Server not started yet" when it gets a ping, and doesn't send a pong. However, after a while when the client doesn't get a pong back it will restart the scene. Because I just recently started to urlencode/urldecode the payload in fibaro:startScene (to cope with foreign chars), the log statement for startScene was not so nice, so I fixed that to url decode the payload when loging. That's the change in the version I just pushed for iOSLocator and EventRunner. Special characters are OK. I use PSPad But in new version there is still problem with watchRefs that is not defined Edited October 27, 2018 by petrkl12 Quote Link to post Share on other sites
jgab 898 Author Share Posted October 27, 2018 8 minutes ago, petrkl12 said: Thanks for adding. Special characters are OK. I use PSPad But in new version there is still problem with watchRefs that is not defined That code is cursed! Ok, fixed it. Talking about the the delayed '@@', Another way to do it is to do this inside main() for _,rule in ipairs({ "@@00:10 => log('A')", "@@00:10 => log('B')" }) do Event.post(function() Rule.eval(rule) end, osTime()+math.random(10,60)) end This will run the Rule.eval(<script>) randomly 10-60 seconds later, just not to run them all at the same time at startup Quote Link to post Share on other sites
petrkl12 66 Share Posted October 27, 2018 Testing scene is working now. Thanks! For solving problem with @@ I prefer first solution. It' s more simple for adding Quote Link to post Share on other sites
petrkl12 66 Share Posted October 27, 2018 Into testing scene I need to add some of waiting time after Fibaro reboot. How to do it? Thanks Quote Link to post Share on other sites
petrkl12 66 Share Posted October 27, 2018 In your wiki you have following example: define('test',function test(a,b) return a+b end) rule("2+test(4,5)*2") it doesn't work ... I need to define some function inside rule Quote Link to post Share on other sites
jgab 898 Author Share Posted October 28, 2018 (edited) 17 hours ago, petrkl12 said: Into testing scene I need to add some of waiting time after Fibaro reboot. How to do it? Thanks You could just do a fibaro:sleep(...) the first thing inside main(). Is it because you want your scenes to start in a specific order? I was thinking of having a supervisor scene that starts up the other scenes (and could also do the ping keep.alive logic) 9 hours ago, petrkl12 said: In your wiki you have following example: define('test',function test(a,b) return a+b end) rule("2+test(4,5)*2") it doesn't work ... I need to define some function inside rule The syntax for "script functions" are Rule.eval("a=fn(a,b) return(a+b) end") Rule.eval("a(7,9)",true) However, I'm not sure how useful the construct is. Normally I just use Lua function. If a Lua function is global (no "local function...") it can be used inside a script expression. Ex. function main() function foo(a,b) return a+b end Rule.eval("log('a+b=%s',foo(3,4))",true) end It means one can call some useful HC2 functions directly Rule.eval("@sunrise => w=api.get('/weather'); phone:msg=log('Godmorning! It is %s and %s degrees',w.WeatherCondition,w.Temperature)") Edited October 28, 2018 by jgab Quote Link to post Share on other sites
petrkl12 66 Share Posted October 28, 2018 (edited) 1 hour ago, jgab said: You could just do a fibaro:sleep(...) the first thing inside main(). Is it because you want your scenes to start in a specific order? I was thinking of having a supervisor scene that starts up the other scenes (and could also do the ping keep.alive logic) OK, I have created simple supervisor scene that waching of other EventRunner scenes Functions - OK Thanks Another question because I'm rebuilding all my scenes into EventRunner framework : Is it possible to create some rule that will be possible to call from other rules? ie. I will have separate rules that switch off lights in ground floor, 1st floor and in garden (different types VD for Hue, Fibaro etc. - complicated) and I would like to use those rules in main rule that will switch off all lights in my house Edited October 28, 2018 by petrkl12 Quote Link to post Share on other sites
petrkl12 66 Share Posted October 28, 2018 (edited) Why this rule doesn't work? rule([[#property{deviceID=Pracovna.Vypinacustolu} => d=env.event.deviceID || label(d,'lblButtonID')=='1002' >> log('%s - %s - Short Button ON id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) || label(d,'lblButtonID')=='1003' >> log('%s - %s - Long Button ON id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) || label(d,'lblButtonID')=='2002' >> log('%s - %s - Short Button DIM UP id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) || label(d,'lblButtonID')=='2003' >> log('%s - %s - Long Button DIM UP id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) || label(d,'lblButtonID')=='3002' >> log('%s - %s - Short Button DIM DOWN id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) || label(d,'lblButtonID')=='3003' >> log('%s - %s - Long Button DIM DOWN id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) || label(d,'lblButtonID')=='4002' >> log('%s - %s - Short Button OFF id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) || label(d,'lblButtonID')=='4003' >> log('%s - %s - Long Button OFF id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) ]]) I think issue will be with d=env.event.deviceID following II but I don't know how to solve it .... and also I would like to use somethink like rule([[#property{deviceID in ListOfSwitches} to have optimized and effective structure Edited October 28, 2018 by petrkl12 Quote Link to post Share on other sites
jgab 898 Author Share Posted October 28, 2018 (edited) 2 hours ago, petrkl12 said: Why this rule doesn't work? rule([[#property{deviceID=Pracovna.Vypinacustolu} => d=env.event.deviceID || label(d,'lblButtonID')=='1002' >> log('%s - %s - Short Button ON id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) || label(d,'lblButtonID')=='1003' >> log('%s - %s - Long Button ON id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) || label(d,'lblButtonID')=='2002' >> log('%s - %s - Short Button DIM UP id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) || label(d,'lblButtonID')=='2003' >> log('%s - %s - Long Button DIM UP id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) || label(d,'lblButtonID')=='3002' >> log('%s - %s - Short Button DIM DOWN id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) || label(d,'lblButtonID')=='3003' >> log('%s - %s - Long Button DIM DOWN id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) || label(d,'lblButtonID')=='4002' >> log('%s - %s - Short Button OFF id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) || label(d,'lblButtonID')=='4003' >> log('%s - %s - Long Button OFF id=%s lblLastAction=%s',d:roomName,d:name,d,label(d,'lblLastAction')) ]]) I think issue will be with d=env.event.deviceID following II but I don't know how to solve it .... and also I would like to use somethink like rule([[#property{deviceID in ListOfSwitches} to have optimized and effective structure Ok, as far as I understand it, buttons in VDs don't create scene triggers (Labels and Sliders do) - so you never get a trigger. The way around it is to add some Lua code to each button in the VD that do event = {type='btnClicked', value='<id of this button>'} fibaro:startScene(scene,{urlencode(json.encode(event))}) then in the scene you do Rule.eval("#btnClicked{value='$btn'} => log('Button %s was clicked',btn)") Well, if you have a list of switches you can do Rule.eval("switches={33,44,55,66,77}") Rule.eval("switches:isOn => ...) ...or something similar.. You can also do Rule.eval("#property{deviceID={66,77}} => log('device is %s',env.event.deviceID)") This is a special case that only works for {type='property', deviceID=...} and that expands to Rule.eval("#property{deviceID=66} => log('device is %s',env.event.deviceID)") Rule.eval("#property{deviceID=77} => log('device is %s',env.event.deviceID)") Edited October 28, 2018 by jgab Quote Link to post Share on other sites
petrkl12 66 Share Posted October 28, 2018 Thanks but this doesn't work: rule("#property{deviceID={66,77}} => log('device is %s',env.event.deviceID)") Error: table index is nil Quote Link to post Share on other sites
jgab 898 Author Share Posted October 28, 2018 (edited) 12 minutes ago, petrkl12 said: Thanks but this doesn't work: rule("#property{deviceID={66,77}} => log('device is %s',env.event.deviceID)") Error: table index is nil Ops, should work in the new version I just pushed. It has worked using Event.event({type='property', deviceID={88,99}}, function(env) ...end) for a while now but I had some old code in the script compiler hat barfed on that. (i'm working on a new version of the EventRunner framework were this already works, so I missed that the old version didn't support this). Edited October 28, 2018 by jgab Quote Link to post Share on other sites
petrkl12 66 Share Posted October 28, 2018 (edited) Thanks, your freamewrok is number ONE! but In respository there is only testing lines ... some other syntax problem: this doesn't work: rule("#property{deviceID=66} => d=env.event.deviceID || label(d,'test')=='aha' >> log('aha')") this is OK: rule("#property{deviceID=66} => || label(env.event.deviceID,'test')=='aha' >> log('aha')") so I can't do d=env.event.deviceID before || ? Edited October 28, 2018 by petrkl12 Quote Link to post Share on other sites
jgab 898 Author Share Posted October 28, 2018 Ops again, I pushed the old version. Now it should work. The "|| <expr> >> <statements>" is a statement in itself, and statements are separated by ,;, so it should be rule("#property{deviceID=66} => d=env.event.deviceID; || label(d,'test')=='aha' >> log('aha')") A semicolon after the assignment to 'd'. It should be an syntax error reported here so I will check on that. Because the it is "|| <expr> >> <statements>", where we can have a list of statements after the '>>' like Rule.eval("|| test() >> log('Hello'); log('world!')") Both 'Hello' and 'Worl'd will be logged if test() is true. If we want to terminate the '|| >>' before the last 'log' we use a double semicolon. Rule.eval("|| test() >> log('Hello') ;; log('world!')") In this case, 'Hello' will be logged if test() is true, but 'World!' will always be logged as it belongs to another statement, outside the '|| >>' Quote Link to post Share on other sites
petrkl12 66 Share Posted October 28, 2018 For testing purpose I want to use: rule("wait(t/19:00); dev:power=30") rule("wait(t/19:10); dev:power=3") rule("wait(t/19:15); dev:power=0") but it doesn't work.... How should I setup power? Quote Link to post Share on other sites
jgab 898 Author Share Posted October 28, 2018 Ok, power isn't supported to set... because it's impossible. However, I pushed a new version that compiles '66:power=30 to fibaro:call(66,'setPower',30) and do the right thing if you run offline (i.e. remember the value and send an event that power has changed). If you run on the HC2 you get an error as setPower isn't supported, New version of EventRunner and EventRunnerDebug pushed. Quote Link to post Share on other sites
jgab 898 Author Share Posted October 29, 2018 (edited) This post is about the Event model that the EventRunner framework is based on, and how to program using that model. The EventRunner framework also supports an EventScript syntax for writing rules that is a concise way to express common home automation tasks. However, sometimes more complex computations need to be invoked when an event happens, and then it is easier to write the complete rule in Lua (and then we don't call them rules, we call them 'event handlers'). In fact, EventScript rules are "compiled" to Lua expressions using the Event model, and EventScript rules and Lua event handlers can coexists, share local variables, and trigger each other by posting events. Another advantage of Lua event handlers are that they are a bit easier to debug. EventScripts compiles to an internal representation that in general is hard to debug (well, we can have some simple trace of which rules are invoked and what they return). Lua handlers can be debugged like any other Lua code and break-points can be set inside the event’s handler function, if we run offline that is. So, let’s see how we can write event handlers. Assume that we want to turn on the light with deviceID 88 if the motion sensor with ID 66 is breached (i.e. its value is set to '1') --[[ %% properties 66 value %% events %% globals HomeStatus %% autostart --]] local trigger = fibaro:getSourceTrigger() if trigger.type == 'property' and trigger.deviceID==66 then if fibaro:getValue(66,'value') > 0 then fibaro:call(88,'turnOn') end end This is the normal way to code it in a Lua scene. However, in the EventRunner framework we code it like this: --[[ %% properties 66 value %% events %% globals HomeStatus %% autostart --]] function main() -- Declare event handler - Event.event(<source trigger table>,<Lua function to call>) Event.event({type='property', deviceID=66}, function(env) if fibaro:getValue(66,'value') > 0 then fibaro:call(88,'turnOn') end end) end The Event.event(<table>,<function>) registers an "event handler" that will match incoming sourceTriggers against the first argument, the <table>. If there is a match the second argument, the <function>, will be called. SourceTriggers are always Lua tables with a 'type' field {type='...', ...}. In the EventRunner framework we call that data structure an 'event', i.e. any table structure with a 'type' field. So, what do we win by using this? Not much in this case. However, if you receive an event from the HC2 with 'property' and 'deviceID', it always comes with a 'propertyName'. E.g. {type='property', deviceID=66, propertyName='value'} and the EventRunner framework is clever enough to see that and realize that you probably want to know the value of this device too and thus fetches the property value and add it to the event before sending it to your defined event handler. {type='property', deviceID=66, propertyName='value', value='1'} so, if it is a sensor you get a value field of '0' or '1' and if it is a dimmer you get something between '0' and '99' This means that we can write the rule above as (we don't include the header declarations needed in the scene for triggers) function main() -- Declare event handler - Event.event(<source trigger table>,<Lua function to call>) Event.event({type='property', deviceID=66, value='1'}, function(env) fibaro:call(88,'turnOn') end) end The "event handler" will only match against an incoming event where the value field is '1', so we are safe to turn on the light every time it matches. Look, it's almost getting simpler than the original Lua scene! (besides the 1000 lines of extra code you need to add to the end of the scene, however that is code you shouldn't need to bother with) There is another event that the framework fills in for you. {type='global', name='HomeStatus'} If you get this event because a global have changed its value it's completed with the global’s current value {type='global', name='HomeStatus', value='Away'} So you can write your handler like this function main() Event.event({type='global', name='HomeStatus', value='Away'}, function(env) fibaro:call(100,'setArmed',1) end) Event.event({type='global', name='HomeStatus', value='Home'}, function(env) fibaro:call(100,'setArmed',0) end) end So, in the previous example we turned on a light when the sensor is breached. How about turning off the light if the sensor is safe for 5 minutes? That is a little more involved but quite simple. function main() local timer = nil -- Turn on light if someone moves... Event.event({type='property', deviceID=66, value='1'}, function(env) fibaro:call(88,'turnOn') timer=Event.cancel(timer) -- if we have a timer cancel the timer end) -- Ok, sensor safe, start timer and turn off light if still safe after 5 minutes. Event.event({type='property', deviceID=66, value='0'}, function(env) timer=Event.post(function() fibaro:call(88,'turnOff') end, "+/00:05") -- post a 'timer' that turns off the light on 5 minutes end) end We introduce some new concepts here. Let's start with the second handler. It matches when the sensor 66 becomes safe (value == '0'). When the sensor becomes safe, we post a timer (a Lua function in this example) that should be called in 5 minutes from now ("+/00:05"). The function we use is Event.post(<function> or <event>, <time>). It is a little bit like the 'setTimeout' function available in scenes as it calls a function or posts an event at a specified time in the future. Here we tell it to execute the function 'function() fibaro:call(88,'turnOff') end' in 5 minutes from now. Event.post returns a reference to this future 'post', in our example we store it in the variable 'timer'. That reference we can use to cancel this future post (if it has not already happened, then it is too late). In the first event handler, we besides turning on the light when the sensor is breached, we also cancel the timer. It could be that the sensor became safe and we have started the timer, but the sensor was then breached, and we thus need to cancel the timer. A side note; To call Event.cancel on nil or a timer reference that doesn't exist is no problem, and that is why we always call cancel when the sensor is breached. It is a little more efficient to call cancel on nil then on a nonexistent reference, in the first case cancel returns immediately, in the second case it has to look through all existing timer references. Event.cancel(timer) always returns nil, and thus we usually code 'timer=Event.cancel(timer)' to also set the variable to nil. Friend of order would argue that this is easy to code without the EventRunner framework, like this: local trigger = fibaro:getSourceTrigger() if trigger.type == 'property' and trigger.deviceID==66 then if fibaro:getValue(66,'value') > '0' then -- sensor breached fibaro:call(88,'turnOn') -- turn on light local time = 0 while (true) do fibaro:sleep(1000*5) -- sleep 5 seconds -- sleep 5 seconds if fibaro:getValue(66,'value') == '0' then -- if still safe, add 5s to time time=time+5 else -- if breached, zero time time=0 end if time > 5*60*time then -- if we have 5 minutes of safe timer fibaro:call(88,'turnOff') -- turn off light and end scene. fibaro:abort() end end end end end This code also solves the task, but now we are starting to see the advantage with the framework. First, I would argue that the two event handlers in the previous example make it easier to understand what is going on, but I'm biased. Secondly, we have to poll the timer in a loop - here we do it every 5s so worst case we turn off the lamp 5min and 5s after the sensor became safe - not a disaster, but there are other cases where we need more direct reactions. The EventRunner framework runs immediate on incoming even/triggers so that is an advantage. Thirdly, how do we do this if we want many sensors looking after many lamps? We probably need to poll both on sensor being breached and becoming safe... then if we want extra conditions when lamps should be turned on etc... soon it becomes messy. With EventRunner framework we can easily duplicate the rule to handle many sensors and lamps, without the rules clashing with each other. function main() local timer1, timer2 = nil,nil -- Turn on light if someone moves... Event.event({type='property', deviceID=66, value='1'}, function(env) fibaro:call(88,'turnOn') timer1=Event.cancel(timer1) -- if we have a timer cancel the timer end) -- Ok, sensor safe, start timer and turn off light if still safe after 5 minutes. Event.event({type='property', deviceID=66, value='0'}, function(env) timer1=Event.post(function() fibaro:call(88,'turnOff') end, "+/00:05") -- post a 'timer' that turns off the light on 5 minutes end) -- Turn on light if someone moves... Event.event({type='property', deviceID=67, value='1'}, function(env) fibaro:call(89,'turnOn') timer2=Event.cancel(timer2) -- if we have a timer cancel the timer end) -- Ok, sensor safe, start timer and turn off light if still safe after 4 minutes. Event.event({type='property', deviceID=67, value='0'}, function(env) timer2=Event.post(function() fibaro:call(89,'turnOff') end, "+/00:04") -- post a 'timer' that turns off the light on 4 minutes end) end If this is something we do a lot we can easily define a Lua function that define these handlers (rules) for us for any combination of sensor and lamp function main() function watchLight(light,sensor,timeout) local timer = nil Event.event({type='property', deviceID=sensor, value='1'}, function(env) fibaro:call(light,'turnOn') timer=Event.cancel(timer) -- if we have a timer cancel the timer end) Event.event({type='property', deviceID=sensor, value='0'}, function(env) timer=Event.post(function() fibaro:call(light,'turnOff') end, timeout) end) end watchLight(88,66,"+/00:05") watchLight(89,67,"+/00:04") watchLight(91,92,"+/00:07") watchLight(101,102,"+/00:10") end These four calls to watchLight will create eight (4x2) event handlers that looks after the lights and sensors specified. Having many event handlers does not slow down the framework as handlers are looked up efficiently with a hash function based on deviceID and property or other fields in the event. Let's get back to the Event.post function. In the example above, we posted a Lua function to turn off the light in the future (and cancelled that post if the sensor was breached again). We can also post our own events. Event.event({type='myEvent', value='Hello'}, function(env) fibaro:debug("Hello") end) Event.post({type='myEvent', value='Hello'},"+/00:10") Here we define an event handler for the event {type='myEvent', value='Hello'}, something that we would never get from the HC2 as a response to a device. However, we can still define it and we can post such an event ourselves. In the example above above we post it 10 minutes into the future. The event will then match the handler we have defined, its function will be called and "Hello" will be printed. Event.event({type='event',event={type='CentralSceneEvent',data={keyAttribute='Pressed'}}}, function(env) Event.post({type='keyPressed', deviceID=env.event.event.data.deviceId,key=env.event.event.data.keyId}) end) Here we have a keyfob from Fibaro with keys that send CentralSceneEvents when we press buttons. If we are only interested when buttons are pressed, we can write an event handler that just transforms the rather complex CentralSceneEvents to something easier to read. Our handler matches against any CentralSceneEvent that has a keyAttribute of 'Pressed'. The event handler function gets a parameter (env) when called, which stands for 'environment'. That is the context when the event handler is called. One important part of the context is the complete event that triggered the handler and is available in 'env.event'. In our case, even if we just matched against a part of the event, 'env.event' has the complete event structure {type="event",event={data={icon={path="fibaro\/icons\/com.fibaro.FGKF601\/com.fibaro.FGKF601-2Pressed.png",source="HC"},keyAttribute="Pressed",keyId=2,deviceId=5},type="CentralSceneEvent"}} What we do is that we pick out the deviceId and keyId field and post it as a new event {type='keyPessed', deviceID=<ID>, key=<keyID>} Then we can do a simpler handler that handles key press events... Event.rule({type='keyPressed', deviceID=5}, function(env) if env.event.key==1 then ... do whatever elseif env.event.key==2 then ... do whatever elseif env.event.key==3 then ... do whatever end end) Event.rule({type='keyPressed', deviceID=5, key=6}, function(env) fibaro:debug("Button 6 was pressed on keyfob with id 5") end) So, this is a way that we can make our code clearer - we can react on obscure sourceTriggers events and repost them as events that makes sense to us. Event.event({type='property', deviceID=door, value='1'}, function(env) Event.post({type='doorOpened'}) end) Event.event({type='doorOpened'}, function(env) startAlarm() end) Another advantage is that if we want to test out the logic we can just post an {type='doorOpened'} event, and see what happens, instead of having to run off to the door and open it... Back to the previous 'MyEvent' example. If we modify it like this: Event.event({type='myEvent', value='Hello'}, function(env) fibaro:debug("Hello") Event.post({type='myEvent', value='Hello'},"+/00:10") end) Event.post({type='myEvent', value='Hello'},"+/00:10") The event will be posted the first time 10min into the future. The event handler will trigger on the event and print "Hello" and then post the event again 10min into the future... then the event handler will get the event again and it will repeat itself. We have created an infinite loop that runs every 10 minutes. This is not very different from doing a "loop" with setTimeout where the function calls setTimeout again at the end. However, it's a bit more powerful/reusable Event.event({type='myEvent'}, function(env) fibaro:debug(env.event.value) Event.post(env.event,env.event.time) end) Event.post({type='myEvent', value='Hello', time='+/00:10'}) Event.post({type='myEvent', value='World', time="+/00:15"}) Two tricks here. We let the event handler print out the 'value' field of the event, so we can print more than "Hello". Secondly, we let the handler repost the whole event (available in env.event) at the time specified in the 'time' field of the event. The advantage is that we can post two events with different 'value' and with different 'time' values. In effect creating two loops that run on 10min and 15min intervals respectively. The 'myEvent' handler is in reality a generic loop handler. Event.event({type='loop'}, function(env) env.event.fun() Event.post(env.event,env.event.time) end) Event.post({type='loop', fun=function() fibaro:debug('Hello') end, time='+/00:10'}) Event.post({type='loop', fun=function() fibaro:debug('World') end, time="+/00:15"}) Event.post({type='loop', fun=function() fibaro:debug('Again') end, time='+/00:07'}) Event.post({type='loop', fun=function() fibaro:debug('Always') end, time="+/02:20"}) Event.post({type='loop', fun=function() fibaro:debug('Sometimes') end, time='+/01:10'}) Event.post({type='loop', fun=function() fibaro:debug('Goodbye') end, time="+/00:27"}) Here we easily create 6 loops with different time intervals printing different messages. In a home automation system this is an easy way to spawn tasks that should be done periodically. It is also a way to structure the code to make it easier to read and understand (when you got the hang of thinking in events). A made-up example could look like this, where our loop construct re-post a specified event at a specified interval. Event.event({type='loop'}, function(env) Event.post(env.event.ev) Event.post(env.event,env.event.time) end) Event.post({type='loop', ev={type='checkWater'}, time='+/00:05'}) Event.post({type='loop', ev={type='checkWindows'}, time="+/00:15"}) Event.post({type='loop', ev={type='checkTemperature}, time='+/01:00'}) Event.event({type='checkWater}, -- happens every 5min function(env) ... whatever needs to be done to check the water... end) Event.event({type='checkWindows}, -- happens every 15min function(env) ... whatever needs to be done to check the windows end) Event.event({type='checkTemperature}, -- happens every hour function(env) ... whatever needs to be done to check house temperature end) We have so far specified the time to Event.post as, e.g. "+/00:15", which should be read as "plus 15 min from now". There is also another time specification we can use "n/10:00", and it should be read as "the next 10 o'clock, either today or if we passed it, 10 o'clock tomorrow" That turns out to be a useful construct because we can reuse the previously defined loop handler and easily check the water at 15:00 every day by doing Event.post({type='loop', ev={type='checkWater'}, time='n/15:00'},'n/15:00') This will post the first water check for 15:00, and the loop handler will repost it for the next 15:00, i.e. the next day at 15:00. If we had done Event.post({type='loop', ev={type='checkWater'}, time='n/15:00'}) The loop handler would have got called immediately and water would have been check immediately (which sometimes are ok), but then it would have start to be called at 15:00 every day by the loop. There is yet another time format, "t/HH:MM" that creates a time (t)oday. And finally, there is a format that can specify sunset and sunrise with an offset in minutes; "sunset+10" and "sunrise-10" gives the number of seconds from midnight to sunset plus 10min and sunrise minus 10 minutes, respectively. However, to give the value to Event.post we need to prefix it with "t/" that produces an epoc time that Event.post requires. If we do an Event.post({type='test'],"t/10:00") it will post it at 10 o'clock today or if we already passed 10:00, it will not be posted at all (which is the difference to "n/10:00") Why bring this up? Well, if we want to schedule something for sunrise tomorrow, "n/sunset" doesn't work as it will compute the sunset today and add 24 hour to that, which is close but not the sunset tomorrow. The only way we can guarantee to get the right value is if we compute the sunset the same day as the sunset in question. One way to do that is to schedule all activities for the day at midnight, dailyEvents = {} Event.event({type='Midnight'}, function(env) for _,e in ipairs(dailyEvents) do Event.post(e.action,e.time) end -- schedule registered events Event.post(env.event,"n/00:00") -- do it again next midnight end) Event.post({type='Midnight'},"n/00:00") -- Start midnight loop function daily(time,action) if toTime(time) > osTime() then Event.post(action,time) end -- Later today?, post it dailyEvents[#dailyEvents+1] = {action=action, time=time} -- register it to re-schedule at midnight end daily("t/sunrise-10",function() Log(LOG.LOG,"Hello") end) daily("t/16:00",function() Log(LOG.LOG,"16!") end) daily("t/20:00",function() Log(LOG.LOG,"20!") end) daily("t/sunset-15",function() Log(LOG.LOG,"Goodbye") end) Here we have the start for a simple scheduler. Maybe we want to filter for some specific days too: dailyEvents = {} Event.event({type='Midnight'}, function(env) for _,e in ipairs(dailyEvents) do Event.post(e.action,e.time) end -- schedule registered events Event.post(env.event,"n/00:00") -- do it again next midnight end) Event.post({type='Midnight'},"n/00:00") -- Start midnight loop function daily(args) local time,action1,days = args.time,args.action,args.days or "Mon,Tue,Wed,Thu,Fri,Sat,Sun" local action2 = function() if days:match(osDate("%a")) then action1() end end if toTime(time) > osTime() then Event.post(action2,time) end -- Later today?, post it dailyEvents[#dailyEvents+1] = {action=action2, time=time} -- register it to re-schedule at midnight end daily{time="t/sunrise-10",days="Mon,Wed",action=function() Log(LOG.LOG,"Hello") end} daily{time="t/16:00",days="Fri",action=function() Log(LOG.LOG,"16!") end} daily{time="t/20:00",days="Sat,Sun",action=function() Log(LOG.LOG,"20!") end} daily{time="t/sunset-15",action=function() Log(LOG.LOG,"Goodbye") end} It's easy to build on this to create a scheduler tailored for a specific use-case. Event.event also allows some special syntax to deal with multiple events. Event.event({type='property', deviceID={ID1,ID2,...IDn}}, function(env) ... end) is translated to local fun = function(env) ... end Event.event({type='property', deviceID=ID1},fun) Event.event({type='property', deviceID=ID2},fun) : Event.event({type='property', deviceID=IDn},fun) This only works with type 'property' and deviceID being a list of deviceIDs. The advantage is that we can easily trigger on something happening to a set of devices. Event.event({type='property', deviceID={66,77,88}, value='1'}, function(env) fibaro:debug("A sensor upstairs was breached!"); startAlarm() end) This will react on any of the devices being breached. If we want to react on all devices having a specific state, we need to check it ourselves. Event.event({type='property', deviceID={66,77,88}, value='0'}, function(env) for _,id in ipairs({66,77,88}) do if fibaro:getValue(id,"value") > "0" then return end end fibaro:debug("All sensors are safe, house empty"); enableAlarm() end) ...but we only do the check whenever one of the devices turns safe. Another special syntax with Event.event is that we can give a list of events it should trigger on Event.event({Event1,Event2,...Eventn}, function(env) ... end) is translated to local fun = function(env) ... end Event.event(Event1,fun) Event.event(Event2,fun) : Event.event(Eventn,fun) Assume, you want to start the alarm when a sensor is breached and the fibaro global 'HomeStatus' is equal to "Away". It's not enough to trigger only on the sensor being breached. The sensor becomes breached, but the 'HomeStatus' is not set to 'Away', no reaction. However, a few seconds later the HomeStatus is set to 'Away', the sensor still being breached but we don't trigger on HomeStatus and thus don't react. The solution is of course to trigger both on sensor being breached and HomeStatus being changed to "Away". Event,event({{type='property', deviceID=sensor, value='1'},{type='global', name='HomeStatus', value='Away'}}, function(env) if fibaro:getValue(sensor,"value") == "1" and fibaro:getGlobalValue("HomeStatus")=="Away" then startAlarm() end end) We could of course break it down to 2 event handlers, but this makes it easier to see what is going on. If you are familiar with EventScript, the rule Rule.eval("sensor:breached & $HomeStatus=='Away' => startAlarm()") would translate to almost exactly the code above... To be continued.... Edited October 30, 2018 by jgab Quote Link to post Share on other sites
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.