Jump to content
Guides for the Forum Read more... ×
Poradniki na Forum Read more... ×

Recommended Posts

Posted (edited)

Here is an example of how to code in a "single instance / event" style. A style I'm using today for all of my scenes.

The idea is that instead of having to deal with a new instance being spawned with every scene trigger, all triggers are dealt with from within a single scene instance that is continuously running. It becomes something close to a traditional event loop model found in most modern GUI frameworks.  The advantages with coding scenes in this style are:

  • Scene can keep state in local lua variables between scene invocations/triggers
  • Easy to keep different rules/logic in the same scene without causing conflicts, e.g. combining continuous running loops/schedules with reacting on incoming triggers
  • Easy to distribute different rules/logic between different scenes and allow them to communicate
  • Easy to schedule actions to do in the future - that can be easily cancelled if new information is gained.
  • Because the scene is continuously running it doesn't matter if there is a heavy initialisation when the scene starts up (parsing HomeTables etc.) as it is only done once...
  • The framework has extensive support to run and debug the scene offline on a PC/Mac to get things right before deploying the scene on the HC2. Offline it is easy to simulate trigger/events to understand if the logic is correct, something that is not always easy to detect in a asynchronous environment...(to be detailed in a follow up post)

 

The framework is available in two version, a 'light' version and a full blow version with a lot bells and whistles. The latter will be described in posts to come.

  • Please login or register to see this link.

    .
  • There will be a post on writing trigger rules
  • Please login or register to see this link.

 

The rest of this post will describe the EventRunnerLite, a minimal version of the EventRunner framework.

 

The lastest version of the code is kept in my

Please login or register to see this link.

. For this examples the

Please login or register to see this link.

and optionally the

Please login or register to see this link.

files are needed. The background of this framework and a thread discussing it can be found 

Please login or register to see this link.

, however the code has evolved a bit from when originally posted there.

 

Below is the light version of the "framework" code with a simple example. To run the framework on the HC2 set the "Max. running instances:" parameter for the scene as high as possible, e.g. 10. Normally the scene only run one instance, but when external triggers (devices, globals etc) come in, temporary scene instances are started and terminated.

Please login or register to see this code.

So, in a normal scene, you get a new instance of the scene when a trigger happens. You then look at the fibaro:getSourceTrigger to determine what trigger happened and carry out the actions needed, and then the instance terminates. If you need to remember something between scene invocations you need to store it in a fibaro global variable.

The difference is that in the EventRunnerLite framework above, the main() function gets called with every new sourceTrigger, in the same scene instance(!). The advantage is that you can keep values in local lua variables between triggers. In the above example we increment counter, a local lua variable, every time the light is turned off.
The way to think about this is that you have a scene that is continuously running and your main()function gets called with every new event that happens. This actually makes it much easier to code complex scenes as we will see.

Some examples (we only show the main() part of the framework)
Example code triggering on Fibaro remote keys 1-2-3 within 2x3seconds. The KeyFob has deviceID 5 in this case.

Please login or register to see this code.

Here we check if 1-2-3 is pressed on a fibaro KeyFob, with max 3 seconds between keypresses. We have 2 local variables that keep the state of the last key pressed and at what time. We need to declare them before the main() function so they survive between calls to main().   

In the framework we can not use fibaro:sleep() as it stops everything and we can not receive events while sleeping. The way to do something after a specified amount of time is to start a timer that then carry out the action, e.g. setTimeout.   

Note that we use 'osTime' and 'osDate' here and it is just to make sure that the code also works when debugged off-line as we do some tricks with the time then. However, when running on the HC2 they are just aliases to 'os.time' and 'os.date'


The framework has a convenience function post(event, time) that sets up a timer that then calls the main() function. The timeparameter is the number of seconds in the future this event will be posted to main().

Having this we can add code to simulate keypress events (sourceTriggers) to see if the logic of the example above works.

Please login or register to see this code.

Here we also check for sourceTriggers of type autostart or other to only post our 'simulated' events at startup of the scene.

post is not only for debugging but is a way to drive the flow of the scene. Posting and reposting an event in the future is a way to create loops.

Please login or register to see this code.

This starts 2 loops, one that prints "Ding! every 60 minutes and another that prints "Dong!" every 40 minutes. The trick here is that the part that catches the 'loop' event, also re-posts the event with the specified time delay.

 

Lets create a scene that does something at specific times of the day.

Please login or register to see this code.

At start of the scene we go through all specified times and posts future events that we can trigger on. This is the only tricky part as we need to understand if the next event happens today or if we passed the time and the next event will be the next day. We define our own custom event of type time with the time string and the action to be carried out. We then add code that watch for that custom event and calls the action function. We also make sure to re-post the event 24 hours from now so that we continue to get the events on a daily basis.

 

postcan post an event immediately if the time parameter is left out or set to zero. Sometimes if we post something in the future we may want to cancel that post because we changed our mind. post returns a reference that can be used to cancel the post if it has not expired. Lets go back to the 'loop' example.

Please login or register to see this code.

By calling cancel(ref) we cancel the last {type='loop', action='run', time=60*60, value='Ding!'} we have posted, thus effectively stopping the loop.

An example is a sensor (id 88) that turns on a lamp (id 99) when breached and turns it off if there are no breaches with a specified amount of time. Note, need to declare 88 valuein the scene header to get the trigger.

Please login or register to see this code.

We also add an call event type that makes it easy to post a lua function to execute in the future.

It is possible to code the above example with fibaro:sleepbut the advantage here is that we can easily duplicate the code in the same scene to look for different sensors and lamps - something that is tricky if we use fibaro:sleep. The only thing to be aware of is that we need different reflocals to keep track of the different posts we do.

Please login or register to see this code.

The code does not conflict with each other, and we can add as many as we need. However, there is a pattern here that we can leverage.

Please login or register to see this code.

Because we have this event driven approach, all of the examples above, the key fob checks, the scheduling of times, and the sensors and lamps can coexist in the same scene. That makes this model very powerful.

 

Lets make an example of a simple presence simulation scene. First we have logic to see if any sensor is breached or all sensors have been safe for 10 minutes. Depending on what was detected it sends a {type='presence', state='start'}or a {type='presence', state='stop'} event that is picked up by another part of the scene that starts and stops a turning on and off lamps at random.

Please login or register to see this code.

The presence detection part does not start the simulation directly when all sensors are safe, rather it posts an "away" event 10 minutes into the future that starts the simulation. A breached sensor will cancel the "away" post. The presence parts reacts to the start/stop event by starting and stopping a loop that toggles lamps at random.

 

So, this example can also be combined with all the other examples in one big scene without causing conflicts. However, it is also easy to break the scene apart and distribute them over many scenes.
postRemote(sceneID,event) is a version of post that sends an event to another scene. If that scene is running the same framework the event will be delivered to the main() function as usual.
Lets split the presence/simulation apart. First the detection part.

Please login or register to see this code.

The only thing we needed to change was the post(event) to postRemote(presenceScene,event). That will send the event to the presence scene (assume it has ID 133).
The simulation scene does not have to change at all.

Please login or register to see this code.

This makes it convenient to distribute logic between scenes. In fact it is easy to do client/server type of models. A way to overcome the lack of sharable Lua libraries...

Client.

Please login or register to see this code.

Server.

Please login or register to see this code.

The trick is that post adds the sceneID of the calling scene to the event in the field event._from. The server can then just send the result back to the scene in the event._from field and thus serve any number of clients. Other uses for this is to implement a ping/pong model between scenes for a keep-alive logic.

 

So, to summarise the advantages of the event/single instance model:

  • Scene can keep state in local lua variables between scene invocations/triggers
  • Easy to keep different rules/logic in the same scene without causing conflicts
  • Easy to distribute different rules/logic between scenes and allow them to communicate
  • Easy to schedule actions to do in the future - that can then be easily cancelled if new information is gained.
  • Because the scene is continuously running it doesn't matter if there is a heavy initialisation when the scene starts up (parsing HomeTables etc.) as it is only done once...
  • The framework has extensive support to run and debug the scene offline on a PC/Mac to get things right before deploying the scene on the HC2. Offline it is easy to simulate trigger/events to understand if the logic is correct, something that is not always easy to detect in a asynchronous environment...

 

When coding with this framework and style for a while it can be a bit tedious with all the if-then-else inside the main() checking for types of events. It is quite easy to build on this to make the coding even more convenient. I have a myself a full blown 

Please login or register to see this link.

 framework that changes the coding model a bit. Instead of main() being called for every new trigger/event, main() is only called once and inside main() event handlers are declared that will get the events as they arrive. On top of that I have an elaborate 

Please login or register to see this link.

 supporting many of the tasks I need to code in my home automation system.

 

 

Edited by jgab
  • Like 4
  • Thanks 3

Share this post


Link to post
Share on other sites
  • Topic Author
  • Posted (edited)

    One way to to make the framework easier is to enhance the 'post' function. 'post' not only takes time in seconds from now that the event should be posted. It is also able to post the event at specific times of the day or next day .

    Below is a variant of 'post' we can do if the time parameter is a string:

    Please login or register to see this code.

    Post uses the 'toTime' function that converts a time string to seconds from now (it could be negative if a time in the past is specified).

    Having 'toTime' we can do some simple time math with sunset/sunrise, like 'post(event,"t/sunset+15")' that will post the event 15 min past sunset.

    The time schedule example in the first post can then be easier coded as:

    Please login or register to see this code.

    A more powerful 'post' function makes it much easier to deal with time.   

    One issue is that if you try to post something to happen at sunrise tomorrow, a 'post(event,"n/sunrise")' will post tomorrow, but at today's sunrise time. This is because when the post is made, sunrise returns today's sunrise. A way around this if doing a scheduler is to have a scheduling loop that make all the daily schedules at midnight. Then sunset/sunrise will return the correct values.

    Please login or register to see this code.

    The only snag is that in the startup we have to post events happening today, because the setup loop only run at midnight. Well, we can live with that.

    Edited by jgab
    • Like 2

    Share this post


    Link to post
    Share on other sites

    Nice :) thank you very much for this!

     

    Do I understand that essentially it is a daily loop that 'posts' returning actions (although it is not really a 'daily loop' as a post triggers a repost) and that any device events (e.g. sceneactivation, value) results in posting additional triggers? 

     

    I do however not fully understand 1 thing:

    How are the additional triggers included? And how can you include multiple events from different devices? Is it as simple as including all of them in the header? But how (technically) are these then all combined in one scene instance?

     

    Small other thing: in case of failure / bug, will the scene autorestart?

     

    Thanks!

    Share this post


    Link to post
    Share on other sites
  • Topic Author
  • Posted (edited)

    Yes, device events arrives as posted events. You have to declare device properties/events/weather/globals triggers you want to receieve in the scene header. I've excluded the header part from my examples. The last version of the time scheduling is actually a daily loop that at midnight post the events for each day.

    Your question, yes you include them in the header and they will all arrive as calls to 'main(sourceTrigger)' in the same scene instance (thats the magic of the rest of the framework code). So for instance, the 'presence detection' example rely on that the sensors 99,199,201, and 301 are declared in the scene header. The code react on any of them being triggered and check if the others are also safe or breached. In theory we could keep a local bit-array with the status of the sensors to directly know if they are all safe or someone is breached without re-checking all the sensor as is done in the code. However, I wanted to keep the example simple.

     

    Second question, no it doesn't autorestart - that is a bit tricky. What I do is that in my scenes I implement a 'keep-alive' mechanism. In all scenes I have a

    Please login or register to see this code.

    and in some selected scene(s) watching the others, there is a 'pong' routine that pings the other scenes and if they don't respond with a pong within certain time they are restarted.

    Please login or register to see this code.

     

    Edited by jgab
    Added delay after receiving a 'pong' - otherwise we have a lot of ping-pong...
    • Like 1

    Share this post


    Link to post
    Share on other sites
  • Topic Author
  • Another problem when coding loops with fibaro:sleep (or setTimeout for that matter) is time drift.

    Please login or register to see this code.

    The loop doesn't take 1 minute, but 1 minute + the time the stuff before the sleep requires to execute. So if you hoped to do everything on the minute things start to drift after a while. Sometimes that is not a problem but if you want thing to happen at specific times it is.

    Please login or register to see this code.

    This post the first "ding" at 15:00, and then the "ding" loop re-post the "ding" event 24 hour from now. Even though the 'printf' takes a very little time (ms), after a while it will start to drift and "dings" will start to happen 15:00:01, 15:00:02 etc.. The problem is of course that we do a relative post ("+/24:00"). One solution is to try to time how long it takes for the other stuff to execute and deduct that from the 24 hours. The easier solution here is to instead do a 'post("n/15:00")', that takes care to post at the time specified. This will run forever without drift (true for "t/..." too).

    If you want to run on a relative time advance, like every hour, you either schedule "n/00:00", "n/01:00", "n/02:00", ... etc or you need to do some calculation...

    However, the framework always do a post in a separate timer thread which kind of helps minimizing the drift. Just by doing the post(event,"+/01:00") as soon as possible, before we do the other stuff, it will minimize the drift. Even if, like in this case, the printf will take a long time...

    Please login or register to see this code.

     

     

    Share this post


    Link to post
    Share on other sites
    Posted (edited)

    Thanks @jgab! thinks are getting more clear!

    (Although I still do not understand technically what in the code ensures that only one scene instance is running :) )

     

    I have been able to create triggers, but not scheduled events.. Can you help me with the following?

    For some reason this code (as part of the main function):

    Please login or register to see this code.

    does yield a posting, but nothing happens 30 secs later.

     

    Weirdness seems to happen in the debug as the posting is scheduled for the same time as the post (11:47:15):

     

    Quote

     

    [DEBUG] 11:47:15: EventMgt - EventRunner v1.0
    [DEBUG] 11:47:15: Loading rules
    [DEBUG] 11:47:15: Posting {"type":"scheduler","event":{"type":"action","action":function: 0x9851628},"next":"+/00:00:30"} for Thu Aug 16 11:47:15
    [DEBUG] 11:47:15: Scene running
    [DEBUG] 11:47:15: Sunrise 06:24, Sunset 21:02

     

    Do you have any clue what is going wrong here?

    (FYI: it seems that the mailbox variable remains empty; plus in this case the posting happens before the code reports "scene running" but the latter does not seem to be the issue, because the same happens if the scheduled action is triggered by an event)

     

    Thanks!

    Edited by 3JL

    Share this post


    Link to post
    Share on other sites
  • Topic Author
  • Posted (edited)

    Ok,

    you are now using the full blown EventRunner framework, which is ok. In the 'Lite' version described above a 30s scheduled loop would look like this:

    Please login or register to see this code.

    In the EventRunner framework there are many ways to achieve this.

    First, the main() function becomes an initialization functions that runs before the scene starts - that's why you see the 'post' before the message that the scene has started, So inside main you do initialization stuff (read HomeTable, set up variables etc, and declare event handlers or script rules).

    What you do in your example is that you post an event but you have not declared any event handlers to do anything with it, so nothing happens. (the mailbox variable is only used for fibaro triggers coming in, internally posted events are handled more efficiently)

    I will come back with another post describing the syntax of the EventRunner framework better, but here are 3 ways to achieve a 30s loop:

    Please login or register to see this code.

    or because this is a very common pattern there is a Event.schedule function that creates a loop for us: (the action can be either an event or a lua function like in this case)

    Please login or register to see this code.

    or using the script rules:

    Please login or register to see this code.

     

    Edited by jgab

    Share this post


    Link to post
    Share on other sites
    Posted (edited)

    Thanks! I got confused between Event.schedule and post({type=schedule,...})

    This works now!

     

    What would be the syntax for a single action within 30 secs, rather than repeated every 30 secs?

     

    edit: for e.g. the case to turn off a lamp again 30 secs after an event. My use case: I have several events that can turn on the ventilation for different periods, but they should not interfere (e.g. if it is turned on for 30 mins, and another trigger wants to turn in it on for 10 mins shortly thereafter, the 30 mins should prevail). Your framework seems excellent to manage this.  

    Edited by 3JL

    Share this post


    Link to post
    Share on other sites
  • Topic Author
  • Posted (edited)

    Well, then you just post something +30s and don't re-post the event.

    Please login or register to see this code.

    Assume that you have a switch with deviceID 33 and you want to do something 30s after the switch is turned on

    Please login or register to see this code.

    or with the scripts

    Please login or register to see this code.

     

    Edited by jgab
    • Like 1

    Share this post


    Link to post
    Share on other sites

    Great!!! Thanks!!!

    Share this post


    Link to post
    Share on other sites

    @jgab i am testing your "bathroom" settings but i get this error when start scene. What have i miss?

    Please login or register to see this code.

     

    Share this post


    Link to post
    Share on other sites
  • Topic Author
  • 6 minutes ago, jompa68 said:

    @jgab i am testing your "bathroom" settings but i get this error when start scene. What have i miss?

    Please login or register to see this code.

     

    Does the version in example_rules.lua work? It seems like it doesn't think that wc.door is defined which is strange. Can you send me the whole main() setup code?

     

    Share this post


    Link to post
    Share on other sites
    1 hour ago, jgab said:

    t seems like it doesn't think that wc.door is defined which is strange

    Well, my own fault. Different name in "HomeTable" and code.

     

    Q2, code starts both lights in WC but does only turnOff 1 of them. Strange...

    Please login or register to see this code.

     

    Share this post


    Link to post
    Share on other sites
  • Topic Author
  • Posted (edited)

    well, the 

    Please login or register to see this code.

    is one statement, so the inBathrom only affect the "tak"

    Please login or register to see this code.

    is another that is always carried out if the door is open and the motion is safe for 10min

    I would write it as 

    Please login or register to see this code.

    then if inBathroom is true it will turn off both lights.

    or

    Please login or register to see this code.

    here we move the inBathroom test to the left side so if the 'for' is true, the inBathroom need to be true too for the right hand to execute. Keep it outside the 'for' expression though.

    or

    Please login or register to see this code.

    ||>> is a kind of if-then syntax, "|| <test> >> <expression> ; <expression> ; ..."

    Edited by jgab

    Share this post


    Link to post
    Share on other sites

    @jgab in one of your examples you send notification every week at a specific day. Can your scene also handle odd or even week numbers?

    Share this post


    Link to post
    Share on other sites
  • Topic Author
  • Ok, never thought about week numbers. 

    I have pushed a new version of EventRunner.lua to Github with week number support. The "constant" 'wnum' returns the current week number. (operator '%' is reminder)

    A rule could look like this then

    Please login or register to see this code.

     

    Share this post


    Link to post
    Share on other sites
  • Topic Author
  • It makes sense to have % and wnum in the script language. However, it is quite easy to add functionality with lua functions.

    Please login or register to see this code.

    and its reasonable efficient. I just realized that os.date("%V") only works on Mac and HC2. On windows it's os.date("%W"). Need to add test in the code...

    Share this post


    Link to post
    Share on other sites

    Hi @jgab, can 'rule.eval'-scheduled actions be cancelled / overruled?

    In the example you have provided above: the action should be performed only 30 seconds after last trigger of device 33 (assuming multiple triggers can happen within 30 seconds).

     

    On 8/16/2018 at 2:04 PM, jgab said:

     

    Please login or register to see this code.

     

     

    Share this post


    Link to post
    Share on other sites
  • Topic Author
  • If the device is a switch and you want the switch to turn off when its been on for 30s one can use the "for" construct

    Please login or register to see this code.

    for(<time>,<expr>) monitors the <expr> and when it has been true for <time>  it returns true. In the example above if it turns off before 30s the timer is cancelled and started next time the device trigger on.

    if you want to monitor if a sensor has been safe for 30s and then do something it is similar

    Please login or register to see this code.

    Does that answer your question? If you have a specific case you like to share I can help you with suggestion how to express it.

    Share this post


    Link to post
    Share on other sites

    Create an account or sign in to comment

    You need to be a member in order to leave a comment

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now

    ×