-
Compteur de contenus
206 -
Inscription
-
Dernière visite
-
Jours gagnés
41
Tout ce qui a été posté par jang
-
This kind of works - it's not super efficient but for functions used once in a while it's ok. The library, ex QA with id 1061 function foo(a,b) return a+b end function bar(a,b) return a*b end -------------------------------------------------- function QuickApp:RPC_CALL(path2,var2,n2,fun,args,qaf) local res if qaf then res = {n2,pcall(self[fun],self,table.unpack(args))} else res = {n2,pcall(_G[fun],table.unpack(args))} end api.put(path2,{name=var2, value=res}) end function print() end Then the client QA do -- could be hidden in a separate QA file local var,cid,n = "RPC"..plugin.mainDeviceId,plugin.mainDeviceId,0 local vinit,path = { name=var, value=""},"/plugins/"..cid.."/variables/"..var api.post("/plugins/"..cid.."/variables",{ name=var, value=""}) -- create var if not exist function fibaro._rpc(id,fun,args,timeout,qaf) n = n + 1 api.put(path,vinit) fibaro.call(id,"RPC_CALL",path,var,n,fun,args,qaf) timeout = os.time()+(timeout or 3) while os.time() < timeout do local r,_ = api.get(path) if r and r.value~="" then r = r.value if r[1] == n then if not r[2] then error(r[3],3) else return select(3,table.unpack(r)) end end end end error(string.format("RPC timeout %s:%d",fun,id),3) end function fibaro.rpc(id,name,timeout) return function(...) return fibaro._rpc(id,name,{...},timeout) end end end local foo = fibaro.rpc(1061,'foo') -- create function for remote function in QA 1061 with name foo local bar = fibaro.rpc(1061,'bar') -- create function for remote function in QA 1061 with name bar function QuickApp:onInit() self:debug("onInit") print(foo(2,4)) print(bar(2,4)) end 100 calls to foo(10,29) takes ~3s, so around 0.03s per call...
-
For a given event it becomes <time to select relevant rule(s)> + <time to execute rule(s)> time-to-execute may be hard to do something about, but If you have a lot of trigger rules, time-to-select will increase, even if the event doesn't trigger any rule at all. It could be optimized by some smart hash-function with the event as the key and possible matching rules as the value, reducing the number of rules needed to be searched through and filtered. Another thing about GEA as I understand it, is that it runs the non-trigger rules every 30s. I guess that every 30s then there is a peak when all rules evaluate their tests - a peak that increase with the number of rules? Here again, and it's a bit more complicated, one could analyze the rule tests when the rules are defined to see what events they depend on. ...and then only invoke the rules that have tests that depends on what events have happened the last 30s. (treating also time as an event...)
-
-- TEST QA function QuickApp:onInit() self:debug("onInit") end local timer function QuickApp:Cligno_ON() self:setVariable("ClignoRouge", "ON") self.cligno = self:getVariable("ClignoRouge") self:trace("var cligno = "..cligno) if not timer then self:loop() end self:trace("Mise en marche du cligno via le bouton") end function QuickApp:Cligno_OFF() self:setVariable("ClignoRouge", "OFF") self.cligno = self:getVariable("ClignoRouge") self:trace("var cligno = "..cligno) if timer then timer=fibaro.clearTimeout(timer) end self:trace("Mise en arret du cligno via le bouton") end ---------------------------------------------------------- --- Boucle loop ---------------------------------------------------------- function QuickApp:loop() self:trace("loop") hub.sleep(1000) hub.call(79, 'turnOn') hub.sleep(1000) hub.call(79, 'turnOff') timer = fibaro.setTimeout(2000, function() self:loop() end) end
-
function fibaro.getQAVariable(id,name) __assert_type(id,"number") __assert_type(name,"string") local props = (api.get("/devices/"..id) or {}).properties or {} for _, v in ipairs(props.quickAppVariables or {}) do if v.name==name then return v.value end end end function fibaro.setQAVariable(id,name,value) __assert_type(id,"number") __assert_type(name,"string") return fibaro.call(id,"setVariable",name,value) end function fibaro.getAllQAVariables(id) __assert_type(id,"number") local props = (api.get("/devices/"..id) or {}).properties or {} local res = {} for _, v in ipairs(props.quickAppVariables or {}) do res[v.name]=v.value end return res end Can be used from both Scenes and QuickApps
-
Your while loop will not give any time for button callbacks to call your Lua functions or update UI i.e. function QuickApp:Cligno_ON() and function QuickApp:Cligno_OFF() You need to use something like setTimeout to give time to your button handlers...
-
If you want "sourceTriggers" in your QA you can use https://forum.fibaro.com/topic/62600-detect-keyid-andor-keyattribute-in-a-qa-loop-from-a-device/?do=findComment&comment=255490 Ex. for customEvents function QuickApp:sourceTrigger(event) -- callback for subscribed events print("Event:",event.name) end function QuickApp:onInit() self:debug("Started") self:clearSubscriptions() -- Every restart, re-subscribe self:subscribe({type='custom-event', name='E1'}) -- Subscribe to custom event self:subscribe({type='custom-event', name='E2'}) -- Subscribe to custom event end ----------- Helper functions -------------- function QuickApp:clearSubscriptions() self:setVariable('TRIGGER_SUB',{}) end function QuickApp:subscribe(event) local s = self:getVariable('TRIGGER_SUB') if type(s)~='table' then s = {} end s[#s+1]=event self:setVariable('TRIGGER_SUB',s) end
- 16 réponses
-
- 2
-
-
-
- déclencheur
- event
-
(et 1 en plus)
Étiqueté avec :
-
Sorry, 'time' was not necessary in the last example (cut&paste error...) Another example is json.decode that takes a second optional value in QAs, not in Scenes! They have different implementations. In QAs this will then give an error a = json.decode(fibaro.getGlobalVariable("X")) as json.decode will expect the second argument to be a table with parsing options, and in this case gets the modification time, and will throw an error. Solution, like the last example is a = json.decode((fibaro.getGlobalVariable("X")))
-
fibaro.getGlobalVariable returns 2 values. String value of variable and time it was last changed. Both these values are then passed to tonumber as arguments The problem is that tonumber can take a second argument, the base for the string->number conversion. In this case it will interpret the change time as the base which is probably out of range. You can throw away all but the first returned value by putting the expression within parentheses. UserIDA = tonumber (( fibaro.getGlobalVariable ( " UserIDA " ))) This will work...
-
function QuickApp:pipup() self:debug("envoi du pipup") local data = { duration = "30", position = "2", -- 0 à 4 title = "Test PIPUP", titleColor = "#0066cc", titleSize = "20", message = "Message de Test Pipup", messageColor = "#000000", messageSize = "14", backgroundColor = "#ffffff", media = { image = { uri="https://mir-s3-cdn-cf.behance.net/project_modules/max_1200/cfcc3137009463.5731d08bd66a1.png", width=480 } } } local pipup = net.HTTPClient():request("http://192.168.0.22:7979/notify",{ options = { method = "POST", headers = { ["Content-Type"] = "application/json" }, data = (json.encode(data)) }, success = function(response) self:debug(json.encode(response)) self:debug(json.encode(data)) end, error = function(err) self:error(err) end }) end
-
Don't lose any sleep over the post. The simple take-away was that Fibaro could have let us have both synchronous net.HTTPClient calls and asynchronous setTimeout, that would have made it easier for developers - and that with just a few lines of code. (then to prove it I gave an implementation, but that may be a bit advanced to follow, and put you to sleep )
-
If net.HTTPClient() was synchronous (returning the result immediately) it would have looked like local success,res = net.HTTPClient():request("http://myurl",options) if success then print(res) end a little like in the old HC2 VDs. If the function takes other functions for success/error or callback it's a hint that the function is asynchronous. E.g. will return immediately and later call your functions when it is ready. This is usually much more efficient if you have something else to do while you wait for the work to be completed. ...IF you have something else to do while you wait. Most code in the forums don't and a lot of beginners stumble on this. Fibaro could have let us both have the cake and eat it, however Fibaro is not very "developer friendly". With a few lines of code in the QA we could have had synchronous net.HTTPClient() and asynchronous setTimeout calls. If we wanted "parallel" net.HTTPClient() calls we would just call them in separate setTimeout functions. Just look at the section "Your QA code" in the code below QuickApp = {} ------------- Your QA code ------------ function QuickApp:ping() -- setTimeout loop logging "ping" every second self:debug("ping") setTimeout(function() self:ping() end,1000) end function QuickApp:onInit() self:debug("MyQA") setTimeout(function() self:ping() end,0) -- Start ping loop local success,res = net.HTTPClient():request("http://myurl") -- Run http request and wait for result if success==true then -- If success print result self:debug("Success:",res) end end ------------- End ---------------------- local http = {} net = { HTTPClient = function() return http end } function http:request(url) -- Synchronous http call that respect other timers print("net.HTTPClient: Calling ",url,"-will take 5 seconds") local co = coroutine.running() setTimeout(function() print("net.HTTPClient: ready") coroutine.resume(co,true,"This is the result") -- We always succeed end,5000) return coroutine.yield() end local timers = {} function setTimeout(f,t) table.insert(timers,{coroutine.wrap(f),t/1000+os.time()}) table.sort(timers,function(a,b) return a[2] <= b[2] end) end if QuickApp.onInit then function QuickApp:debug(...) print(os.date("%c"),...) end setTimeout(function() QuickApp:onInit() end,0) end while true do local t = table.remove(timers,1) while t[2] > os.time() do end t[1]() end It will run the "ping" loop while the net.HTTPClient() call waits for its result. We can write much more "natural" code and everyone would be more happy, and we would see less buggy code... However, it doesn't work on the HC3 as Fibaro in its infinite wisdom has decided to disable coroutines that is one of the biggest features of Lua. (You can try to run the example in any other "full" Lua environment to see it in action...) 'coroutines', in short, allow us to suspend execution of the code in the middle of your function (yield) and then later "wake up" the function so that it continues to run from where it was suspended (resume). In our code we let setTimeout wrap the function in a coroutine so that the function can be suspended and resumed. In the case above we suspend the net.HTTPClient() call, and then when it's ready (5s later) we wake it up so the function can return the result to the waiting caller. local success,res = net.HTTPClient():request("http://myurl") So, Lua is a very powerful language but we are unfortunately only allowed to use a small part of it...
-
local id = resp[1].nukiId
-
The problem you have running several 'setAudioAlarm' in parallel is that you store values in self. that is shared between all calls to 'setAudioAlarm'. It could work better if you sent them as parameters to the call, so each call had it's own version of the parameters. Running them with a random delay is like playing the lottery... where the prize is a buggy program. You still have the problem with nesting calls in request callbacks. Each event has it's own set of parameters in the .p key. The advantage of sending user/pwd with each event is that cameras can have different credentials. If not we could have used self.use and self.pwd instead in 'setAudioAlarm'. I also don't understand the enable/toggle logic. I guess you want to alternate between turning on and turning off the camera? To make it correct you should first read the enable state with a call and then set it to the opposite with another call. Otherwise you may get out of sync... So, first call checkSiren, set the p.enable to the opposite and call changeSirenToggle You could also have 2 buttons, turnOn and turnOff and send the enable state 1/0 with them (like I tried in turnOn, should enable be 1 or 0 ?) Btw, I put the QuickApp:turnOn function in the same QA as the rest of the code (using local 'post' function). Maybe you wanted to do it from another QA? I would have coded it this way, using the model of events. This way you avoid the nesting. It may be a bit advanced, but worth studying if you have trouble sleeping Disclaimer: I don't have access to a Reolink camera so I really don't know what I'm doing local testing = true -- simulate api calls local post function QuickApp:turnOn() self:debug( "binary switch turned on" ) self:updateProperty( "value" , true ) post({type='setAudioAlarm',p={camera="192.168.1.46", tool="1",user=self.user,pwd=self.pwd,enable=1}}) -- 'p' stands for 'parameters' post({type='setAudioAlarm',p={camera="192.168.1.48", tool="1",user=self.user,pwd=self.pwd,enable=1}}) -- should enable be 1 ? post({type='setAudioAlarm',p={camera="192.168.1.45", tool="1",user=self.user,pwd=self.pwd,enable=1}}) end function QuickApp:turnOff() self:debug( "binary switch turned off" ) self:updateProperty( "value" , false ) post({type='setAudioAlarm',p={camera="192.168.1.46", tool="1",user=self.user,pwd=self.pwd,enable=0}}) post({type='setAudioAlarm',p={camera="192.168.1.48", tool="1",user=self.user,pwd=self.pwd,enable=0}}) post({type='setAudioAlarm',p={camera="192.168.1.45", tool="1",user=self.user,pwd=self.pwd,enable=0}}) end local debug = true local EVENT={} local function DEBUG(...) if debug then print(...) end end function post(event) setTimeout(function() assert(EVENT[tostring(event.type)],"Undefined event ",tostring(event.type)) DEBUG("Event:",event.type) EVENT[event.type](event) end,0) end -------------------------------------------------- -------------------------------------------------- -- senCmd -------------------------------------------------- -------------------------------------------------- local function sendCmd(event,method,cmd,data) local url = "https://"..event.p.camera..":443/api.cgi?cmd="..cmd self.http:request(url,{ options = { checkCertificate= false , method= method or "GET" , headers = { [ "content-type" ] = "application/json" , [ "Accept" ] = "application/json" , }, data = data and json.encode(data) or nil }, success = function(resp) if resp.status == 200 then DEBUG("success ",url,resp.data) post({type=event.type.."_success",p=event.p,url=url,data=json.decode(resp.data)}) else post({type=event.type.."_error",p=event.p,url=url,error="status="..resp.status}) end end, error = function(err) post({type=event.type.."_error",p=event.p,url=url,error=err}) end } ) end if testing then -- redefine sendCmd for test purpose when no access to Relink function sendCmd(event,method,path,data) local data = {} if path:match("Login") then data = {{value={Token={name="myToken"}}}} elseif path:match("GetAudioAlarmV20") then data = {{value={Audio={enable=0}}}} end DEBUG("success ","https://"..event.p.camera..":443/api.cgi?cmd="..path,json.encode(data)) post({type=event.type.."_success",p=event.p,url=url,data=data}) end end -------------------------------------------------- -------------------------------------------------- -- connection to the camera and recovery of the token, if ok call ChangeSirenToggle -------------------------------------------------- -------------------------------------------------- function EVENT.setAudioAlarm(event) sendCmd(event,"POST","Login", { [ 'cmd' ]= "Login" , [ 'param' ]={ [ 'User' ]={ [ 'userName' ]=event.p.user, [ 'password' ]=event.p.pwd } } } ) end local function add(t,a) for k,v in pairs(a) do t[k]=v end return t end function EVENT.setAudioAlarm_success(event) local token = event.data[ 1 ][ "value" ][ "Token" ][ "name" ] -- login token DEBUG( "Camera(): Connected" ) -- Call to change the siren parameter post({type='ChangeSirenToggle',p=add(event.p,{token=token})}) end function EVENT.setAudioAlarm_error(event) quickApp:error( "Identification problem - Error - " , json.encode(event.error,event.url or "")) end -------------------------------------------------- -------------------------------------------------- -- Change siren parameter -------------------------------------------------- -------------------------------------------------- function EVENT.ChangeSirenToggle(event) sendCmd(event,"POST","SetAudioAlarmV20&token="..event.p.token, { [ 'cmd' ]= "SetAudioAlarmV20" , [ 'param' ]= { [ 'Audio' ]= { [ 'enable' ]= event.p.enable, [ 'schedule' ]= { [ 'channel' ]= 0 , [ 'table' ]= { [ 'MD' ]= string.rep("1",168) } } } } } ) end function EVENT.ChangeSirenToggle_success(event) post({type='checkSiren',p=event.p}) --- print siren value after update end function EVENT.ChangeSirenToggle_error(event) quickApp:updateView( "message" , "Title" , "Identification problem" ..json.encode(event.error)) quickApp:error( "Problem updating Siren parameter - Error - " , json.encode(event.error)) end --------------------------- -- Check the siren parameter value ---------------------------- function EVENT.checkSiren(event) sendCmd(event,"GET","GetAudioAlarmV20&token=" ..event.p.token, { [ 'cmd' ]= "GetAudioAlarmV20" , [ 'param' ]= { [ 'channel' ]= 0 } } ) end function EVENT.checkSiren_success(event) local siren=event.data[ 1 ][ "value" ][ "Audio" ][ "enable" ] print(event.p.camera.. "'s siren is on" , siren) post({type='logout',p=event.p}) --(self.camera, self.token) end function EVENT.checkSiren_error(event) quickApp:error( "Problem updating Siren parameter - Error - " , json.encode(event.error)) --self:logout(self.camera, self.enable) post({type='logout',p=event.p}) end -------------------------------------------------- -------------------------------------------------- -- Logout -------------------------------------------------- -------------------------------------------------- function EVENT.logout(event) sendCmd(event,"POST","Logout&token=" ..event.p.token, { [ 'cmd' ]= "Logout" , [ 'param' ]= {} } ) end function EVENT.logout_success(event) print( "Logout from " ..event.p.camera.. " done" ) end function EVENT.logout_error(event) quickApp:error( "Problem logging out - " , json.encode(event.error)) end -------------------------------------------------- -------------------------------------------------- -------------------- -- main loop -------------------------------------------------- -------------------------------------------------- -------------------- function QuickApp:mainLoop() -- self:logout(self.camera, self.enable) --self:setAudioAlarm(self.camera, self.enable) end -------------------------------------------------- -------------------------------------------------- ----------------- -- -------------------------------------------------- -------------------------------------------------- ----------------- function QuickApp:onInit() __TAG="QA_Reolink"..plugin.mainDeviceId self:trace( "------------------------------------------------------------ -----" ) self:trace( "-- Starting Reolink QA" ) self:trace( "------------------------------------------------------------ -----" ) --- login self.user = self:getVariable( "login" ) ---password self.pwd = self:getVariable( "password" ) --- IP URL -- self.camera = self:getVariable( "IP_Reolink" ) self.http = net.HTTPClient({ timeout = 3000 }) -- Toggle -- self.enable = 0 -- self:mainLoop() if testing then setTimeout(function() self:turnOn() end,1) end -- Just testing to push button... end
-
a = 1 b = 24 print(a << 8 | b) > 280
-
Ok, I have seen those errors too, despite the protective code. Maybe protect the net.HTTPClient():request(...) call too? and maybe the QuickApp:onInit().... ...just thinking out loud.
-
What is your QA doing? HTTP calls?
-
Normally I would tend to agree with you. (actually to be more philosophical, the problem is when people by-pass functions and rewrite what they are doing (example). Here we just wrap an existing function and honour the existing contract - we still call the original HTTPClient etc. The only problem is if the function goes away - but that is the same problem for the rest of your code). However, these functions have been around since the beginning and are essential for most QAs out there. I would really doubt that they are going to deprecate them. In fact, I argue that the patched functions now works as they should have in the first place. This is such an easy fix that Fibaro should have done it themselves when the HC3 came out. Instead there are 100s of posts in the forum by users that complains about QAs crashes without any error message, and blame the HC3... and Fibaro puts up with it... I don't understand how that company is run.... The easiest and cheapest customer support is to have good error messages... I see that I used fibaro.* instead of hub.* - but today they both point to the same function table - also here I doubt thet they would remove fibaro.* as too many QAs would stop working.
-
The idea with protecting json.encode/decode was to get an error line pointing to where json.encode was called from in user code, and not an error pointing deep into the json library - which is seldom helpful. setTimeout and setInterval will not log an error if the function crashes. This prints out the error.
-
Instead of do : end do local debug = true if debug then : end and set it to false when you found the bug
-
Forgot a 'copy' function in original post - added.
-
Remove your pcalls and add this as a separate file in your QA do local function perror(...) fibaro.error(__TAG,...) end function copy(obj) if type(obj) == 'table' then local res = {} for k,v in pairs(obj) do res[k] = copy(v) end return res else return obj end end local httpClient = net.HTTPClient -- protect success/error with pcall and print error function net.HTTPClient(args) local http = httpClient() return { request = function(_,url,opts) opts = copy(opts) local success,err = opts.success,opts.error if opts then opts.timeout = opts.timeout or args and args.timeout end if success then opts.success=function(res) local stat,r=pcall(success,res) if not stat then perror(r) end end end if err then opts.error=function(res) local stat,r=pcall(err,res) if not stat then perror(r) end end end return http:request(url,opts) end } end local settimeout, setinterval, encode, decode = -- gives us a better error messages setTimeout, setInterval, json.encode, json.decode function setTimeout(fun,ms) return settimeout(function() local stat,res = pcall(fun) if not stat then perror(res) end end,ms) end fibaro.setTimeout = function(ms,fun) return setTimeout(fun,ms) end function setInterval(fun,ms) return setinterval(function() local stat,res = pcall(fun) if not stat then perror(res) end end,ms) end fibaro.setInterval = function(ms,fun) return setInterval(fun,ms) end function json.decode(...) local stat,res = pcall(decode,...) if not stat then error(res,2) else return res end end function json.encode(...) local stat,res = pcall(encode,...) if not stat then error(res,2) else return res end end end do local function perror(...) fibaro.error(__TAG,...) end local httpClient = net.HTTPClient -- protect success/error with pcall and print error function net.HTTPClient(args) local http = httpClient() return { request = function(_,url,opts) opts = copy(opts) local success,err = opts.success,opts.error if opts then opts.timeout = opts.timeout or args and args.timeout end if success then opts.success=function(res) local stat,r=pcall(success,res) if not stat then perror(r) end end end if err then opts.error=function(res) local stat,r=pcall(err,res) if not stat then perror(r) end end end return http:request(url,opts) end } end local settimeout, setinterval, encode, decode = -- gives us a better error messages setTimeout, setInterval, json.encode, json.decode function setTimeout(fun,ms) return settimeout(function() local stat,res = pcall(fun) if not stat then perror(res) end end,ms) end fibaro.setTimeout = function(ms,fun) return setTimeout(fun,ms) end function setInterval(fun,ms) return setinterval(function() local stat,res = pcall(fun) if not stat then perror(res) end end,ms) end fibaro.setInterval = function(ms,fun) return setInterval(fun,ms) end function json.decode(...) local stat,res = pcall(decode,...) if not stat then error(res,2) else return res end end function json.encode(...) local stat,res = pcall(encode,...) if not stat then error(res,2) else return res end end end
-
1. hub.call(175, "updateView","label", "text", "Good day")
-
Sorry for this long post in English in a French forum. However, I don't trust Google to translate my non-native English into French ;-) Anyway, it's the 'beginner's questions' and we are discussing how to structure our programs. Week-end reading for everyone interested in writing their own rule system – or just structure programs in a more “declarative” way. condition_1 => action_1 condition_2 => action_2 : condition_n => action_n If you have this way of programming, you typically need to test your conditions in a loop that runs every x second local function loop() if condition_1 then action_1 end if condition_2 then action_2 end : if condition_n then action_n end setTimeout(loop,100*30) -- check every 30s end loop() Ex. In this case checking every 30s. This is the way standard GEA rules behaves. However, GEA has another model with the -1 trigger, that reacts directly on external events, like devices changing states etc., and then run the loop checking the rules. That way rules can respond immediately to ex. sensors being breached. We can add that immediate check too local function loop() if fibaro.getValue(88,'value')==true then fibaro.call(99,'turnOff') end if condition_2 then action_2 end : if condition_n then action_n end setTimeout(loop,100*30) -- check every 30s end loop() local function check_sensor() if fibaro.getValue(88,'value')==true then loop() end setTimeout(check_sensor,1000*1) -- check every second end check_sensor() We have a loop that runs every 30s and checks all our "rules". We have another function, check_sensor, that runs every second and if the sensor is breached it calls the main loop that checks all rules. The ‘loop’ function still carries out the action when the light is on. This gives us the flexibility to check everything every 30s but also to react immediately when a sensor is breached. We can improve this. Now we check the sensor in both loops (fibaro.getValue). We have already detected that the sensor was breached so we can tell the main loop what has happened. local function loop(event) if event=='sensor_88_breached' then fibaro.call(99,'turnOff') end if condition_2 then action_2 end : if condition_n then action_n end setTimeout(loop,100*30) -- check every 30s end loop() local function check_sensor() if fibaro.getValue(88,'value')==true then loop('sensor_88_breached') end setTimeout(check_sensor,1000*1) -- check every second end check_sensor() Let's make the check_sensor code be more generic and check more devices local function rule_checker(event) if event.type=='device' and event.id==88 and event.value==true then fibaro.call(99,'turnOff') end if condition_2 then action_2 end : if condition_n then action_n end setTimeout(function() rule_checker({type='loop'}) end,100*30) -- check every 30s end rule_checker({type='loop') local myDevices = { 88, 99, 101, 120, 444, 55, 6 } -- ID of devices to check local myDeviceValues = { } -- Starts empty but will store last values of our devices local function check_devices() for _,id in ipairs(myDevices) do local value = fibaro.getValue(id,'value') -- Fetch device value if myDeviceValues[id]~=value then -- Has the value changed? myDeviceValues[id]=value -- Remember new value rule_checker({type='device', id=id, value=value}) -- Call our rule checker with new value end end end setInterval(check_devices,1000*1) -- check devices every second First our 'loop' function is renamed to 'rule_checker' and takes one argument 'event' 'event' is a Lua table with at least a key that is 'type'. When rule_checker calls itself every 30s it sends the argument {type='loop'} to itself. This is to make sure that there is always an argument 'event' that we can check against. Our check_sensor has become check_devices, and checks multiple devices if they have changed state and then calls our check rules with an argument (event) that is of type 'device' with information what deviceId it was and what new value it has: {type='device', id=<id of device>, value=<new device value>} Now our rule checks if the type of event was 'device' and the id was 88, and in that case turns off device 99. Note that the rule will not act every 30s as it now requires the event type is 'device' - not 'loop' We could keep both rules if that would make sense. if event.type=='device' and event.id==88 and event.value==true then fibaro.call(99,'turnOff') end if event.type=='loop' and fibaro.getValue(88,'value')==true then fibaro.call(99,'turnOff') end The observant reader will have discovered a bug. Every time check_devices calls 'rule_checker' it will start a new loop as rule_checker ends with a call to itself (the 'setTimeout'). Well,that will be fixed in the next example. Now let’s make one more abstraction of this code. Sending the event to our rule_checker should be a function - 'post' local post -- Forward declaration... local function rule_checker(event) if event.type=='device' and event.id==88 and event.value==true then fibaro.call(99,'turnOff') end if condition_2 then action_2 end : if condition_n then action_n end end local myDevices = { 88, 99, 101, 120, 444, 55, 6 } -- ID of devices to check local myDeviceValues = { } -- Starts empty but will store last values of our devices local function check_devices() for _,id in ipairs(myDevices) do local value = fibaro.getValue(id,'value') -- Fetch device value if myDeviceValues[id]~=value then -- Has the value changed? myDeviceValues[id]=value -- Remember new value post({type='device', id=id, value=value}) -- Post event end end end function post(event) rule_checker(event) end -- Posting an event means calling check rules with event setInterval(check_devices,1000*1) -- Check devices every second setInterval(function() post({type='loop'}) end,1000*30) -- Post 'loop' event every 30s Instead of the rule_checker calling itself with setTimeout, we have a separate setInterval, "posting" the 'loop' event every 30s to the rule_checker function. The abstraction is a bit better, and we integrate periodic and immediate rule checks. The next steps is to abstract the rule_checker function. it's just checking a number of rules in sequential order. Let’s break that apart in 2 steps. local rules = { device = function(even) if event.id==88 and event.value==true then fibaro.call(99,'turnOff') end end, <type_2> = function(event) action_2 end, <type_n> = function(event) action_n end, end We make it a Lua table with the event type as key, associated to a function carrying out the action. Our 'post' function then becomes function post(event) if rules[event.type] then -- Do we have a rule for this type ? local action = rules[event.type] -- Then get the action -- action(event) -- ..and call it setTimeout(function() action(event) end,0) -- Lets call the action with setTimeout instead of calling it directly... end end We can also add rules to the table like this local rules = {} local function addRule(eventType,action) rules[eventType]=action end addRule('device', function(even) if event.id==88 and event.value==true then fibaro.call(99,'turnOff') end end) addRule('loop',function(event) action_2 end) There is a big problem with the approach to store rules using the event type as key. We can only have one rule for each key, because keys are unique in a Lua table (or we overwrite the old value) Instead of using the type as key we can just store them in an array and let post search for matching keys local function addRule(eventType,action) table.insert(rules,{type=eventType,action=action}) end function post(event) for _,rule in ipairs(rules) do if event.type==rule.type then setTimeout(function() rule.action(event) end,0) end end end This means that we can define rules like this addRule('device', function(even) if event.id==88 and event.value==true then fibaro.call(99,'turnOff') end end) addRule('device', function(even) if event.id==101 and event.value==true then fibaro.call(99,'turnOff') end end) addRule('loop',function(event) action_2 end) addRule('loop',function(event) action_n end) If we have a 'device' event, both rules will run, checking if it was 88 or 101 that was breached. A drawback with this is that we run both device rules even though we know that if one match the other will not. And we have to do the if event.id == .. test in both. We can do better. Our post functions now only look at the 'type' key of the event. Instead it could look at all fields in the event. local function addRule(event,action) table.insert(rules,{event=event,action=action}) end local function match(event1,event2) for key,value in ipairs(event1) if event2[key]~=value then return false end -- If a key in event1 is not the same as in event2, no match - return false end return true -- All keys matched, return true end function post(event) for _,rule in ipairs(rules) do if match(rule.event,event.type) then setTimeout(function() rule.action(event) end,0) end end end Now when we call addRule to add a rule we provide the whole event it should match for the action to be called. addRule({type='device',id=88, value=true}, function(even) fibaro.call(99,'turnOff') end) addRule({type='device',id=101, value=true}, function(even) fibaro.call(99,'turnOff') end) addRule({type='loop'},function(event) action_2 end) addRule({type='loop'},function(event) action_n end) The 'post' function may not be the most efficient as it needs to look through all the rules and see if they match. Don't worry about that now (we can make it much more efficient). Instead enjoy the abstraction :-) Let’s improve the 'post' function with one more feature. local function postEvent(event) for _,rule in ipairs(rules) do if match(rule.event,event.type) then rule.action(event) end end end function post(event,delay) return setTimeout(function() postEvent(event) end,1000*(delay or 0)) end 'post' can now delay the invocation of the matching rules. (If we don't specify a delay it becomes zero and is invoked immediately) Why is that a nice feature? local rules = {} local function addRule(event,action) table.insert(rules,{event=event,action=action}) end local function match(event1,event2) for key,value in ipairs(event1) if event2[key]~=value then return false end -- If a key in event1 is not the same as in event2, no match - return false end return true -- All keys matched end local function postEvent(event) for _,rule in ipairs(rules) do if match(rule.event,event.type) then rule.action(event) end end end function post(event,delay) return setTimeout(function() postEvent(event) end,1000*(delay or 0)) end addRule({type='device',id=88, value=true}, function(even) fibaro.call(99,'turnOff') end) addRule({type='device',id=101, value=true}, function(even) fibaro.call(99,'turnOff') end) addRule({type='start'},function(event) post({type='check_devices', interval=event.check_interval}) -- Start checking devices every second post({type='loop', interval=event.loop_interval}) -- Start period 'loop' end) addRule({type='loop'},function(event) -- 'loop' event respost then'loop' event to get a periodic loop post(event,event.interval) -- and we delay it with the interval value specified in the event end) addRule({type='loop'},function(event) -- Do something every interval print("Periodic check") end) local myDevices = { 88, 99, 101, 120, 444, 55, 6 } -- ID of devices to check local myDeviceValues = { } -- Starts empty but will store last values of our devices addRule({type='check_devices'},function(event) for _,id in ipairs(myDevices) do local value = fibaro.getValue(id,'value') -- Fetch device value if myDeviceValues[id]~=value then -- Has the value changed? myDeviceValues[id]=value -- Remember new value post({type='device', id=id, value=value}) -- Post event end end post(event,event.interval) -- Loop and check devices end) post({type='start', check_interval=1, loop_interval=30}) - Post 'start' event to get things going... (This is the "whole" system) It's cool, because we have a 'start' rule that invokes another rules ('loop' and 'check_devices') by posting events. The 'loop' rule then just reposts the event after a delay to create a periodic loop. Then we can have other rules that trigger on the 'loop' event and carries out some actions. We have abstracted away all setInterval's as "looping" is done with rules that repost events they trigger on (check_devices and loop). We also see, that events can carry “arguments”, like the interval value that the loops use to decide how much to delay their posts to themselves. So, this becomes a powerful abstraction model where our logic is expressed in rules and events, and where posting events connects our rules into a logic flow. Another classic example. Assume that we want a rule that turns on a light (99) if a sensor (88) is breached and then turns it off if the sensor has been safe for 60s. For this case we need one more function, ‘cancel’, that allow us to cancel an event posted in the future - in case we change our mind… local function cancel(timer) if timer~=nil then clearTimeout(timer) end -- cancel event, e.g. the setTimeout that will post the event return nil end addRule({type='device', id=88, value=true},function(event) timer=cancel(timer) -- Cancel timer, if there were a timer running fibaro.call(99,'turnOn') -- Turn on light end) addRule({type='device', id=88, value=false},function(event) timer = post({type='turnOff', id=99},60) -- Post event in 60s that will tuen off light end) addRule({type='turnOff'},function(event) timer=nil fibaro.call(event.id,'turnOn') -- Turn off light end) This takes care of the resting of the interval if the sensor is breached while the timer is counting down. ...This is my all-time favourite model how to structure my programs.... especially when they becomes large and the logic complex and there are several parallell things going on. (and it integrates well with other asynchronous functions like net.HTTPClient requests.)
-
I guess the reason is that as you define more and more conditions and actions it will stabilise over time and you will only focus on the rules e.g. local rules = { {condition=condition1,action=action1}, {condition=condition2,action=action2}, {condition=condition3,action=action3} } combining existing conditions and actions to new rules. You could have conditions that combine other conditions (AND). You could have conditions that watch if another conditions is true for a certain time. You could improve the "rule engine" to stop evaluating rules if an action returns the "break. You could add automatic logging etc. Of course you will end up with something like GEA at the end.... The point is that the abstraction allow you to focus on the problem - home automation rules - like, ex. GEA does. When the number of rules/conditions/actions are as small as in the example above the overhead may not be justified, but when you sit there with 100+ rules, you would like an abstraction that allows reuse of logic and an easy way to add features... So, I don't write all my code in this style - but dealing with large complex test logic is tempting to write a rule engine - done it many times. My other favourite is "event style" coding. Defining event handlers and posting events to drive execution between the handlers. It's really suitable in asynchronous logic (like home automation tends to be) and it integrates well my app logic with both the asynchronous net.HTTPClient:request(...) we have as well as the triggers from /refreshStates etc. Your code becomes like a state-machine, but a bit more flexible. Here is an old post from the HC2 days https://forum.fibaro.com/topic/25214-event-based-programming/ ...and it has improved a lot since then.
-
Updated TriggerQA to v1.21 Supported triggers {type='alarm', property='armed', id=<id>, value=<value>} {type='alarm', property='breached', id=<id>, value=<value>} {type='alarm', property='homeArmed', value=<value>} {type='alarm', property='homeBreached', value=<value>} {type='alarm', property='activated', id=<id>, seconds=<seconds>} {type='weather', property=<prop>, value=<value>, old=<value>} {type='global-variable', property=<name>, value=<value>, old=<value>} {type='quickvar', id=<id>, name=<name>, value=<value>; old=<old>} {type='device', id=<id>, property=<property>, value=<value>, old=<value>} {type='device', id=<id>, property='centralSceneEvent', value={keyId=<value>, keyAttribute=<value>}} {type='device', id=<id>, property='accessControlEvent', value=<value>} {type='profile', property='activeProfile', value=<value>, old=<value>} {type='custom-event', name=<name>} {type='deviceEvent', id=<id>, value='removed'} {type='deviceEvent', id=<id>, value='changedRoom'} {type='deviceEvent', id=<id>, value='created'} {type='deviceEvent', id=<id>, value='modified'} {type='sceneEvent', id=<id>, value='started'} {type='sceneEvent', id=<id>, value='finished'} {type='sceneEvent', id=<id<, value='instance', instance=<number>} {type='sceneEvent', id=<id>, value='removed'} {type='sceneEvent', id=<id>, value='modified'} {type='sceneEvent', id=<id>, value='created'} {type='onlineEvent', value=<boolean>} {type='room', id=<id>, value='created'} {type='room', id=<id>, value='removed'} {type='room', id=<id>, value='modified'} {type='section', id=<id>, value='created'} {type='section', id=<id>, value='removede'} {type='section', id=<id>, value='modified'} {type='location',id=<userid>,property=<locationid>,value=<geofenceaction>,timestamp=<number>} {type='ClimateZone',...} {type='ClimateZoneSetpoint',...} It also supports time/cron events Time subscription: {type='cron', time=<cronString>, tag=<string>} cron string format: "<min> <hour> <day> <month> <wday> <year>" min: 0-59 hour: 0-23 day: 1-31 month: 1-12 wday: 1-7 1=sunday year: YYYY Ex. "0 * * * * *" Every hour "0/15 * * * * *" Every 15 minutes "0,20 * * * * *" At even hour and 20min past "0 * * 1-3 * *" Every hour, January to March "0 7 lastw-last * 1 *" 7:00, every sunday in the last week of the month "sunset -10 lastw-last * 1 *" 10min before sunset every sunday in the last week of the month Ex. self:subscribeTrigger({type='cron', time="0/15 * * * * *", tag="Quarter"}) will send back a source trigger ever 15min in the format {type='cron', time="0/15 * * * * *", tag="Quarter"} 'tag' can be useful to match timers to actions... This gives more or less the same or more functionality than what you get from Scene conditions.