Skip to content
Jan Gabrielsson edited this page Aug 13, 2018 · 45 revisions

EventRunnerLite is a scaled down version of the EventRunner framework. As such it may be easier to get started with and to get to understand the "single instance" and "events" model. The advantage with coding scenes in this style is that

  • 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 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...

Below is the code with a simple example.

--[[
%% properties
55 value
%% events
%% globals
counter
--]]

--[[
-- EventRunnerLite. Single scene instance framework
-- Copyright 2018 Jan Gabrielsson. All Rights Reserved.
-- Email: [email protected]
--]]

_version = "1.0" 
osTime = os.time
osDate = os.date
if dofile then dofile("EventRunnerDebug.lua") end -- Support for running off-line on PC/Mac

---- Single scene instance, all fibaro triggers call main(sourceTrigger) ------------
local count = 0
function printf(...) fibaro:debug(string.format(...)) end

function main(sourceTrigger)

  if sourceTrigger.deviceID == 55 and fibaro:getValue(55,'value') < "1" then 
     count=count+1
     printf("deviceID 55 turned off %s times",count)
  end

end -- main()

------------------------ Framework, do not change ---------------------------  
-- Spawned scene instances post triggers back to starting scene instance ----
local _trigger = fibaro:getSourceTrigger()
local _type, _source = _trigger.type, _trigger
local _MAILBOX = "MAILBOX"..__fibaroSceneId 

if _type == 'other' and fibaro:args() then
  _trigger,_type = fibaro:args()[1],'remote'
end

function post(event, time) return setTimeout(function() main(event) end,(time or 0)*1000) end
function cancel(ref) if ref then clearTimeout(ref) end return nil end
function postRemote(sceneID,event) event._from=__fibaroSceneId; fibaro:startScene(sceneID,{json.encode(event)}) end

---------- Producer(s) - Handing over incoming triggers to consumer --------------------
if ({property=true,global=true,event=true,remote=true})[_type] then
  local event = type(_trigger) ~= 'string' and json.encode(_trigger) or _trigger
  local ticket = string.format('<@>%s%s',tostring(_source),event)
  repeat 
    while(fibaro:getGlobal(_MAILBOX) ~= "") do fibaro:sleep(100) end -- try again in 100ms
    fibaro:setGlobal(_MAILBOX,ticket) -- try to acquire lock
  until fibaro:getGlobal(_MAILBOX) == ticket -- got lock
  fibaro:setGlobal(_MAILBOX,event) -- write msg
  fibaro:abort() -- and exit
end

local function _poll()
  local l = fibaro:getGlobal(_MAILBOX)
  if l and l ~= "" and l:sub(1,3) ~= '<@>' then -- Something in the mailbox
    fibaro:setGlobal(_MAILBOX,"") -- clear mailbox
    post(json.decode(l)) -- and "post" it to our "main()" in new "thread"
  end
  setTimeout(_poll,250) -- check every 250ms
end

if _type == 'autostart' or _type == 'other' then
  printf("Starting EventRunnerLite demo")
  if not _OFFLINE then 
    if not string.find(json.encode((api.get("/globalVariables/"))),"\"".._MAILBOX.."\"") then
      api.post("/globalVariables/",{name=_MAILBOX}) 
    end
    fibaro:setGlobal(_MAILBOX,"") 
    _poll()  -- start polling mailbox
    main(_trigger)
  else
    collectgarbage("collect") GC=collectgarbage("count")
    _System.runOffline(function() main(_trigger) end) 
  end
end

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, 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

local previousKey = nil
local time = osTime() 
function printf(...) fibaro:debug(string.format(...)) end

function main(sourceTrigger)
  local event = sourceTrigger

  if event.type == 'event' then
    local keyPressed = event.event.data.keyId
    if keyPressed == 1 then 
      previousKey=1
      time=osTime()
      printf("key 1 pressed at %s",osDate("%X"))
    elseif keyPressed == 2 and previousKey == 1 and osTime()-time <= 3 then
      previousKey = 2
      time=osTime()
      printf("key 2 pressed at %s",osDate("%X"))
    elseif keyPressed == 3 and previousKey == 2 and osTime()-time <= 3 then
      printf("Key 3 pressed at %s, Keys 1-2-3 pressed within 2x3sec",osDate("%X"))
    end
  end

end -- main()

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. setTimout.
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.

local previousKey = nil
local time = osTime() 
function printf(...) fibaro:debug(string.format(...)) end

function main(sourceTrigger)
  local event = sourceTrigger

  if event.type == 'event' then
    local keyPressed = event.event.data.keyId
    if keyPressed == 1 then 
      previousKey=1
      time=osTime()
      printf("key 1 pressed at %s",osDate("%X"))
    elseif keyPressed == 2 and previousKey == 1 and osTime()-time <= 3 then
      previousKey = 2
      time=osTime()
      printf("key 2 pressed at %s",osDate("%X"))
    elseif keyPressed == 3 and previousKey == 2 and osTime()-time <= 3 then
      printf("Key 3 pressed at %s, Keys 1-2-3 pressed within 2x3sec",osDate("%X"))
    end
  end

 -- Test logic by posting key events in 3,5, and 7 seconds
  if event.type=='autostart' or event.type=='other' then
    post({type='event',event={data={keyId=1}}},3)
    post({type='event',event={data={keyId=2}}},5)
    post({type='event',event={data={keyId=3}}},7)
  end

end -- main()

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 a loop.

function main(sourceTrigger)
  local event = sourceTrigger

  event.typ = 'autostart' or event.type = 'other' then
    post({type='loop', time=60*60, value='Ding!'})  -- start loop every 60min
    post({type='loop', time=40*60, value='Dong!'})  -- start loop every 40min
  end

  if event.type == 'loop' then
     printf(event.value)
     post(event, event.time)
  end
end -- main()

This starts 2 loops, one that prints "Ding! every 60 minutes and one 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.

local times = {
   {"09:30",function() fibaro:debug("Good morning!") end},
   {"13:10",function() fibaro:debug("Lunch!") end},
   {"17:00",function() fibaro:debug("Evening!") end}}
function printf(...) fibaro:debug(string.format(...)) end

function main(sourceTrigger)
  local event = sourceTrigger

  if event.type == 'time' then
    printf("It's time %s",event.time)
    -- carry out whatever actions...
    event.action()
    post(event,24*60*60) -- Re-post the event next day at the same time.
  end

  -- setUp initial posts of daily events
  if event.type == 'autostart' or event.type == 'other' then 
    local now = os.time()
    local t = osDate("*t")
    t.hour,t.min,t.sec = 0,0,0
    local midnight = osTime(t) 
    for _,ts in ipairs(times) do
       local h,m = ts[1]:match("(%d%d):(%d%d)")
       local tn = midnight+h*60*60+m*60
       if tn >= now then 
          post({type='time',time=ts[1], action=ts[2]},tn-now) -- Later today
       else
          post({type='time',time=ts[1], action=ts[2]},tn-now+24*60*60) -- Next day
       end
     end
  end

end -- main()

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.

function main(sourceTrigger)
  local event = sourceTrigger

  event.typ = 'autostart' or event.type = 'other' then
    ref = post({type='loop', action='run', time=60*60, value='Ding!'})  -- start loop every 60min
    post({type='loop', action='stop'},24*60*60) -- Post event that will stop loop in 24 hours
  end

  if event.type == 'loop' and event.action=='run' then
     printf(event.value)
     ref = post(event, event.time)
  end

  if event.type == 'loop' and event.action=='stop' then
     cancel(ref)
  end

end -- main()

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.

local ref = nil
local minutes = 3
function main(sourceTrigger)
  local event = sourceTrigger

  if event.deviceID == 88 then
    if fibaro:getValue(88,'value') > '0' then -- sensor breached
      if fibaro:getValue(99,'value') < '1' then fibaro:call(99,'turnOn') end -- turn on lamp, if off.
      ref = cancel(ref) -- cancel timer/post 
    else -- else sensor safe, post future event to turn off the lamp
      ref = post({type='call',f=function() printf("No movement within time!") fibaro:call(99,'turnOff'); ref=nil end},minutes*60)
    end
  end

  if event.type == 'call' then event.f() end -- Generic event for posting function calls

end -- main()

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 ref locals to keep track of the different posts we do.

local ref1,ref2 = nil, nil
local minutes1,minutes2 = 3,4
function main(sourceTrigger)
  local event = sourceTrigger

  if event.deviceID == 88 then
    if fibaro:getValue(88,'value') > '0' then -- sensor breached
      if fibaro:getValue(99,'value') < '1' then fibaro:call(99,'turnOn') end -- turn on lamp, if off.
      ref1 = cancel(ref1) -- cancel timer/post 
    else -- else sensor safe, post future event to turn off the lamp
      ref1 = post({type='call',f=function() printf("No movement within time!") fibaro:call(99,'turnOff'); ref1=nil end},minutes1*60)
    end
  end

  if event.deviceID == 89 then
    if fibaro:getValue(89,'value') > '0' then -- sensor breached
      if fibaro:getValue(98,'value') < '1' then fibaro:call(98,'turnOn') end -- turn on lamp, if off.
      ref2 = cancel(ref2) -- cancel timer/post 
    else -- else sensor safe, post future event to turn off the lamp
      ref2 = post({type='call',f=function() printf("No movement within time!") fibaro:call(98,'turnOff'); ref2=nil end},minutes2*60)
    end
  end

  if event.type == 'call' then event.f() end -- Generic event for posting function calls

end -- main()

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.

local assocs={
  {[88] = {lamp=99,time=3*60,ref=nil},
  {[89] = {lamp=98,time=4*60,ref=nil}
}
function main(sourceTrigger)
  local event = sourceTrigger

  if assocs[event.deviceID] then
    local id = event.deviceID
    local lamp = assocs[id].lamp
    if fibaro:getValue(id,'value') > '0' then -- sensor breached
      if fibaro:getValue(lamp,'value') < '1' then fibaro:call(lamp,'turnOn') end -- turn on lamp, if off.
      assocs[id].ref = cancel(assocs[id].ref) -- cancel timer/post 
    else -- else sensor safe, post future event to turn off the lamp
      assocs[id].ref = post({type='call',f=function() printf("No movement within time!") fibaro:call(lamp,'turnOff'); ref=nil end},assocs[id].time)
    end
  end

  if event.type == 'call' then event.f() end -- Generic event for posting function calls

end -- main()

However, 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 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.

local sensors = {[99]=true,[199]=true,[201]=true,[301]=true}
local breached, ref3 = false, nil
local lamps2 = {55,66,77}
local ref4 = nil
function printf(...) fibaro:debug(string.format(...)) end

function main(sourceTrigger)
  local event = sourceTrigger

  -- Presence detection
  if event.deviceID and sensors[event.deviceID] then
    local n = 0  -- count how many sensors are breached
    for id,_ in pairs(sensors) do if fibaro:getValue(id,'value') > '0' then n=n+1 end end
    if n > 0 and not breached then
      breached = true
      ref3 = cancel(ref3)
      post({type='presence',state='stop'})
    elseif n == 0 and breached then
      breached = false
      ref3 = post({type='away'},10*60) -- Assume away if no breach in 10 minutes
    end
  end

  if event.type == 'away' then
    post({type='presence',state='start'})
  end

  -- Presence simulation
  if event.type == 'presence' and event.state=='start' then
    printf("Starting presence simulation")
    post({type='simulate'})
  end

  if event.type == 'simulate' then
    local id = lamps2[math.random(1,#lamps2)] -- choose a lamp
    fibaro:call(id,fibaro:getValue(id,'value') > '0' and 'turnOff' or 'turnOn') -- toggle light
    ref4 = post(event,math.random(5,15)*60) -- Run again in 5-15 minutes
  end

  if event.type=='presence' and event.state == 'stop' then
    printf("Stopping presence simulation")
    ref4 = cancel(ref4)
  end
end -- main()

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.

local presenceScene = 133
local sensors = {[99]=true,[199]=true,[201]=true,[301]=true}
local breached, ref3 = false, nil

function main(sourceTrigger)
  local event = sourceTrigger

  -- Presence detection
  if event.deviceID and sensors[event.deviceID] then
    local n = 0  -- count how many sensors are breached
    for id,_ in pairs(sensors) do if fibaro:getValue(id,'value') > '0' then n=n+1 end end
    if n > 0 and not breached then
      breached = true
      ref3 = cancel(ref3)
      postRemote(presenceScene,{type='presence',state='stop'})
    elseif n == 0 and breached then
      breached = false
      ref3 = post({type='away'},10*60) -- Assume away if no breach in 10 minutes
    end
  end

  if event.type == 'away' then
    postRemote(presenceScene,{type='presence',state='start'})
  end
end -- main()

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.

local lamps2 = {55,66,77}
local ref4 = nil
function printf(...) fibaro:debug(string.format(...)) end

function main(sourceTrigger)
  local event = sourceTrigger

  -- Presence simulation
  if event.type == 'presence' and event.state=='start' then
    printf("Starting presence simulation")
    post({type='simulate'})
  end

  if event.type == 'simulate' then
    local id = lamps2[math.random(1,#lamps2)] -- choose a lamp
    fibaro:call(id,fibaro:getValue(id,'value') > '0' and 'turnOff' or 'turnOn') -- toggle light
    ref4 = post(event,math.random(5,15)*60) -- Run again in 5-15 minutes
  end

  if event.type=='presence' and event.state == 'stop' then
    printf("Stopping presence simulation")
    ref4 = cancel(ref4)
  end
end -- main()

This makes it convenient to distribute logic between scenes. In fact it is easy to do client/server type of models.

local mathServer = 134
function printf(...) fibaro:debug(string.format(...)) end

function main(sourceTrigger)
   local event = sourceTrigger

   if event.type=='autostart' or event.type=='other' then
     postRemote(mathServer,{type='add',val1=77, val2=88})
   end

   if event.type == 'result' then printf("The sum of 77 and 88 is %s",event.result) end
end - main()
-- mathServer
function main(sourceTrigger)
   local event = sourceTrigger

   if event.type == 'add' then
     postRemote(event._from,{type='result',result=event.val1+event.val2})
   elseif event.type == 'diff' then
     postRemote(event._from,{type='result',result=event.val1-event.val2})
   end
end -- main()

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.

Ok, to summerize some of the advantages of the event/single scene 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 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 in the EventRunnerLite framework for a while it can be a bit tedious with all the if-then-else inside the main() function to check for types of events. The full blown EventRunner framework 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 instead declared that will get the events as they arrive. When the event handler model is mastered it is time to move on to the script model...