-
Notifications
You must be signed in to change notification settings - Fork 11
Lite
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 time
parameter 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.
post
can 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 value
in 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:sleep
but 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...