Jump to content

HC3 QuickApps coding - tips and tricks


Recommended Posts

@jgab

May I suggest few tips...

  1. Instead of code or in parallel to code could you please post QuickApp itself. Could be exported and easily uploaded by users like VD and no need to create buttons and labels. (Don't see control button code)
  2. Instead of using  fibaro.call() function to call QuickApp takes ~5 seconds!!!! (I have reported about it to fibaro) you can use self  which is an object that can be used inside the Quick App class. It is a reference to ‘itself’. Similar solutions can be found in programming languages such as python or this in javascript.  For example:

fibaro.call(plugin.mainDeviceId,"secure")

use

self:secure()

executed immidieatly .

 

 

Overall I'm impress of your capacity and work. 

 

Edited by cag014
Link to post
Share on other sites
  • Replies 876
  • Created
  • Last Reply

Top Posters In This Topic

Top Posters In This Topic

Popular Posts

This thread is most about QuickApp tricks and usually requires some deeper understanding of Lua. I'm going to make an attempt to do something more tutorial wise that could work for newcomers to Lua. I

The anatomy of QuickApps – Part 2 (Part 1)   Disclaimer1: We are now venturing into undocumented land. That means that Fibaro is free to change how things work at any time. Well, Fibaro

A thread to share some coding techniques for QuickApps?  Because QAs are "long running scenes" (they don't have to be loaded and restarted for every event) - it is actually worthwhile to build up

Posted Images

26 minutes ago, AR27690 said:

 

Now I'm totally confused, you said and I quote:

 

 

But may be I'm wrong... for other hand have tried to use your door lock code (my friend has HC3) and we've lost on buttons and labels creation. I do believe some code in button control required, right?

Anyhow thanks for your enthusiasm. 

  I think all below functions should be in button control code for each button... try it

 add("0",self)
 add("1",self) 
 add("2",self)  
   ...
   ...
add("9",self)  
Spoiler

Capture.thumb.PNG.5bb16a9e4880f303cc7123c22f335b9b.PNG

 

Edited by cag014
  • Thanks 1
Link to post
Share on other sites
1 hour ago, cag014 said:

@jgab

May I suggest few tips...

  1. Instead of code or in parallel to code could you please post QuickApp itself. Could be exported and easily uploaded by users like VD and no need to create buttons and labels. (Don't see control button code)

Yes, maybe I should. It was just  unfinished code/examples. When the QDs become more complex I will start to post the whole package.

(A side note to @AR27690, this is a thread for sw developers, when we make ready-to-use QDs and upload them to the download area - then we add value to regular users)

 

Quote
  1. Instead of using  fibaro.call() function to call QuickApp takes ~5 seconds!!!! (I have reported about it to fibaro) you can use self  which is an object that can be used inside the Quick App class. It is a reference to ‘itself’. Similar solutions can be found in programming languages such as python or this in javascript.  For example:

Thanks, I'm well aware of the concept but I was to into it to realise that I also had the class defs ended up in self, I just saw the fibaro provided funcs like updateView,debug etc. ... Everything completely makes sense now and I will update the example.

1 hour ago, AR27690 said:

But may be I'm wrong... for other hand have tried to use your door lock code (my friend has HC3) and we've lost on buttons and labels creation. I do believe some code in button control required, right?

Sorry, will attach the QuickApp device to the original post so you can just upload it (and by downloading the FQA you agree to be a SW developer :-) )

Edited by jgab
Link to post
Share on other sites
2 minutes ago, jgab said:

Yes, maybe I should. It was just  unfinished code/examples. When the QDs become more complex I will start to post the whole package.

(A side note to @AR27690, this is a thread for sw developers, when we make ready-to-use QDs and upload them to the download area - then we add value to regular users)

Excellent... agree

Link to post
Share on other sites

Another topic that is not HC3 specific - but relevant again because the http functions moved from scenes to VDs (or QuickApps).

Disclaimer: a long post...

 

In VDs there used to be FHTTP function that was synchronous. I.e. the call (:GET :POST etc) returned the result immediately.

 local h = Net.FHttp(ipaddress, port)
 local resp, status, result = h:GET(url)

In QuickApps we have the net.HTTPClient() function that used to be in scenes on the HC2. net.* are asynchronous. The request returns immediately and we provide call-back functions that the request will call when the result returns.

net.HTTPClient():request(url,{
    options = {...},
    success = function(result) ... end,
    error = function(result) ... end
    })

Here the success resp. the error functions are called when the result arrives. This means that you need to 'chain' the requests throughout your program in some way - a "setTimeout loop" is one such model where function calls itself (very short chain).

 

It has been numerous posts on this topic in the past. One example:

 

Anyway, people trying to convert old VDs that used FHTTP to QuickApps with net.* will have to deal with this somehow.

Here is a "design pattern", a way to structure a program in general (and QuickApps in this case) to deal with asynchronous calls. In this example a net.HTTPClient():request call.

function post(e,t) setTimeout(function() main(e) end,t or 0) end

function httpCall(method,url,data,success,error)
  net.HTTPClient():request(url,{
      success = function(resp) post({type=success,value=resp}) end,
      error = function(resp) post({type=error,value=resp}) end,
      options = {
        method = method,
        data = data and json.encode(data),
        headers={['Accept']='application/json',['Content-Type']= data and 'application/json'}
      }
    })
end

errs = 0
function main(e)
  ({
      loop = function(e) 
        httpCall("GET","http://worldtimeapi.org:80/api/ip",nil,"success","error") 
      end,

      success = function(e) 
        local time = json.decode(e.value.data).unixtime
        quickSelf:updateView('label','text',os.date("%X,",time)) -- update label text
        post({type='loop'},60*1000) -- and fetch a new value in 1 minute
        errs = 0 -- reset error count
      end,

      error = function(e) 
        errs = errs + 1 -- increment the error count
        self:debug("Error:"..e,value)
        if errs < 3 then post({type='loop'},2*60*1000) end -- try again in 2 minutes
      end,

      })[e.type](e)
end

function QuickApp:onInit()  -- onInit() sets up stuff...
  self:debug("onInit")
  quickSelf = self
  post({type='loop'})
end

The program fetches the time for our location every 1 minute and updates a text label in the UI.

 

So, the simple idea is that we have a main() function that have "handlers", functions that gets called for each type of event it receives. In a language with a switch statement we had used that - in Lua, tables of functions is the nest best (it's more efficient to lift out the table from main, but for this example it's ok)

 

We have 3 types of events, 'loop', 'success', and 'error'. We have defined a function 'post' that takes an event and optionally a time and calls main(event) with that event (after the optional time)

 

So the logic is that we start from QuickApp:onInit() by posting the event 'start'

main's 'start' handler calls the http function and gives the events that should be called when there is a success or an error.

On 'success' we update a label with our time (we get from the http call) and post a new 'loop' event in 1 minute - to start over.

On 'error' we print an error message and post a 'loop' event in 2min - trying again unless we have got 3 consecutive errors, then we stop the loop.

 

One could argue that this example is as easy if we just chain the success handler of the HTTPClient():request directly. Or a simple setTimeout loop of 1 minute calling the http function. However, in the latter case we can't be sure that the result have arrived before we try again in 1 minute - we have to wait for the response somehow.

 

For clarity it's still good to list the 'events' that happens in the program in one place, instead of having functions spread out through the program that call each other.

 

However, the real advantage comes when we need to do many different asynchronous calls in specific orders.

Assume that we update the time every minute as in the example, but before we do that we need to call and get the timezone. So we have to wait for one asynchronous call before we can start our loop with another asynchronous call.

 

The example above is trivial to extend.

function post(e,t) setTimeout(function() main(e) end,t or 0) end

function httpCall(method,url,data,success,error)
  net.HTTPClient():request(url,{
      success = function(resp) post({type=success,value=resp}) end,
      error = function(resp) post({type=error,value=resp}) end,
      options = {
        method = method,
        data = data and json.encode(data),
        headers={['Accept']='application/json',['Content-Type']= data and 'application/json'}
      }
    })
end

errs = 0
function main(e)
  ({
      start = function(e)
        httpCall("GET","http://worldtimeapi.org/api/timezone/Europe/Stockholm",nil,"tz","err_tz")  
      end,

      tz = function(e)    -- Got Timezone, continue with regular loop
        quickSelf:debug(e.value.data) 
        post({type='loop'},1000)
      end,

      err_tz = function(e) quickSelf:debug("Sorry, time zone needed:"..e.value) end, -- No timezone, halt.

      loop = function(e) 
        httpCall("GET","http://worldtimeapi.org:80/api/ip",nil,"success","error") 
      end,

      success = function(e) 
        local time = json.decode(e.value.data).unixtime
        quickSelf:updateView('label','text',os.date("%X,",time)) -- update label text
        post({type='loop'},60*1000) -- and fetch a new value in 1 minute
        errs = 0 -- reset error count
      end,

      error = function(e) 
        errs = errs + 1 -- increment the error count
        self:debug("Error:"..e,value)
        if errs < 3 then post({type='loop'},2*60*1000) end -- try again in 2 minutes if less than 3 errors
      end,

      })[e.type](e)
end

function QuickApp:onInit()  -- onInit() sets up stuff...
  self:debug("onInit")
  quickSelf = self
  post({type='start'})
end

We extend main() with handlers for events 'start', 'tz', and 'err_tz'. 

Instead of calling 'loop' from :onInit() we call 'start'.

The 'start' handler make a timezone request and tells the http function to post a 'tz' event on success or a 'err_tz' event on failure

On a 'tz' event we continue to call 'loop' starting the main 1 minute polling for time.

 

So this model of having an event handler (main) and a post function allows for structuring of the program.

 

It's easy to extend to run several loops in parallel. Here we leverage the fact that events can be of one type and have different values.

function post(e,t) setTimeout(function() main(e) end,t or 0) end

function main(e)
  ({

      loop = function(e) 
        self:debug(e.message)
        post(e,e.delay)
      end,

      })[e.type](e)
end

function QuickApp:onInit()  -- onInit() sets up stuff...
  self:debug("onInit")
  quickSelf = self
  post({type='loop', message='Hello', delay=4*1000}) -- loop every 4s logging "Hello"
  post({type='loop', message='Goodbye', delay=6*1000}) -- loop every 6s logging "Goodbye'
end

Here we kick off 2 loops with a 4 and 6 second delay - reusing the same "loop handler" in main.

With many call-backs being asynchronous in QuickApps (all the action methods and UI callbacks) it can make sense to always post to a common handler to get a clear structure on what's going on.

 

Anyway, this is a way to structure your program when you find yourself doing many different things in the QuickApp and they rely on asynchronous results (run in kind of "parallell")

 

It is also possible to achieve more advanced event handling like "joins" of events etc... That is discussed in the link in the beginning of this post and uses a continuation-passing style that is not as easy to read imho .

Edited by jgab
  • Like 1
  • Thanks 2
Link to post
Share on other sites

Here is a functioning version of the user profile scheduler that was demonstrated in the post 

I don't consider it complete. There are some limitations.

-You can only schedule a profile once per day.

-The time input buttons are a bit quirky.

-It doesn't persist the schedules, so they are lost if the QD restarts. (May come in an update)

ProfSched-2.fqa

 

Edited by jgab
  • Like 1
Link to post
Share on other sites
On 2/12/2020 at 3:59 PM, cag014 said:

@jgab

May I suggest few tips...

  1. ....you can use self  which is an object that can be used inside the Quick App class. It is a reference to ‘itself’. Similar solutions can be found in programming languages such as python or this in javascript

You had access to the QuickApp manual 😀

Link to post
Share on other sites
2 minutes ago, jgab said:

You had access to the QuickApp manual 😀

Yes.. good catch

Edited by cag014
Link to post
Share on other sites

Task: Looping in a QuickApp, because we need to read a value regularly from an external device with http or something similar...

Looping is often done with setTimeout

function loop()
   print("Tick!")
   setTimeout(loop,60*1000) -- call function loop again in 1 minute
end

function QuickApp:onInit()
   loop()
end

 

If we always want the same time delay we can instead use 'setInterval'

function QuickApp:onInit()
    setInterval(function() print("Tick!) end, 60*1000) -- call function every minute
end

which is shorter and sweeter... it's like setTimeout but it will repeatedly call the function at the interval specified. 

setInterval returns a reference so that we can cancel with clearInterval(<ref>).

setInterval also exist on the HC2 - been there since setTimeout was introduced.

 

This is ok, but the setInterval function suffers from the same problem as the setTimeout loop above - it will drift if we don't compensate for the time the code inside the loop takes.

Sometimes that doesn't matter - but sometimes it does and I been on a crusade to inform people about it because it can cause very rare bugs... and they are the worst.

 

We could redefine setInterval to try to be less "drifty". This works resonable. The millisecond part can fluctuate a bit but I don't dare using os.clock - now it tries to stay within the second at least.

function setInterval(fun,ms)
  local ref,nxt={}
  local function loop()
    nxt=nxt or os.time()
    if ref[1] then
      fun()
      nxt = nxt+ms/1000
      ref[1]=setTimeout(loop,1000*(nxt-os.time()))
    end
  end
  ref[1] = setTimeout(loop,ms)
  return ref
end

function clearInterval(ref) if ref[1] then clearTimeout(ref[1]) ref[1]=nil end end

 

Edited by jgab
Link to post
Share on other sites

Here is an example of using the code for polling for triggers shown in an earlier post

 

This is a QD that looks for an event (trigger) of type 'PluginProcessCrashedEvent' and report that in the QD UI (it will list the 3 last events)

crash.png.37e379d1bf3ae2a761185237c90d5d9c.png

Optionally it will do a 'sendPush' to the user ID defined in the device variable (quickVariable) 'pushID'.

 

This is very handy as it will easily give an overview what devices are crashing as you experiment with coding. When debugMessages for scenes will start to work we could easily extend the DQ.

 

QD's that crash will be automatically "restarted" after a ~minute by the system so there will be additional crashes - an extension would be to not push repeated errors.

 

CrashNotifier-2.fqa

  • Thanks 1
Link to post
Share on other sites

I've earlier uploaded a fibaroAPI file that can be used to call the HC3 from a PC/Mac/Linux while testing and debugging functionality on the HC3.

 

 

With this API you can do stuff like

if dofile and not hc3_emulator then
  hc3_emulator.credentials = {ip="192.168.1.X", user="<user>", pwd="<password>"}
  dofile("fibaroapiHC3.lua")
end
local function printf(...) print(string.format(...)) end

local VARNAME = "HC3test"
api.delete("/globalVariables/"..VARNAME)
api.post("/globalVariables",{name=VARNAME,value=""})
fibaro.setGlobalVariable(VARNAME,42)
__assert(fibaro.getGlobalVariable(VARNAME) == "42","getGlobalVariable")
api.delete("/globalVariables/"..VARNAME)

local devices = api.get("/devices")
for _,d in ipairs(devices) do printf("Device:%s, name:'%s'",d.id,d.name) end

local scenes = api.get("/scenes")
for _,d in ipairs(scenes) do printf("Scene:%s, name:'%s'",d.id,d.name) end

local profiles = api.get("/profiles")
printf("Active profile id:%s",profiles.activeProfile)
for _,d in ipairs(profiles.profiles) do printf("Profile:%s, name:'%s'",d.id,d.name) end

local rooms = api.get("/rooms")
for _,d in ipairs(rooms) do printf("Room:%s, name:'%s'",d.id,d.name) end

local sections = api.get("/sections")
for _,d in ipairs(sections) do printf("Section:%s, name:'%s'",d.id,d.name) end

local ds = fibaro.getDevicesID({}) 
for _,d in ipairs(ds) do printf("Filter - Device:%s, name:'%s'",d.id,d.name) end

Most of the fibaro.* APIs works - some stuff are not implemented yet but on the todo list. (contributions are welcome)

 

It also supports QuickApps

This will work

if dofile and not hc3_emulator then
  hc3_emulator = { credentials = {ip="192.168.1.X", user="<user>", pwd="<password>"} }
  hc3_emulator.poll=2000
  dofile("fibaroapiHC3.lua")
end

fibaro.debug("","OK")

function QuickApp:turnOn() 
  self:debug("ON") 
  self:updateProperty("value",true)
end
function QuickApp:turnOff() 
  self:debug("OFF") 
  self:updateProperty("value",false)
end

function QuickApp:onInit()
  self:debug("onInit")
  self:updateProperty("value",true)
  local function loop()
      self:debug("PING")
      setTimeout(loop,2000)
   end
   loop()
end

It looks and it behaves almost as a QuickApp - however it only exists offline and doesn't have a deviceID. Not terrible exciting, but we could have coded a loop calling netHTTPClient() polling for some external value and debug if we get the right thing, manage to decode with json.decode  etc.

 

The hc3_emulator.start(nil,2000) needs to be called last in the code as it starts running the timers needed to make setTimeout working.

 

A bit more interesting is if we make a "proxy" QuickApp on the HC3 that looks like this This is outdated. Proxies are automatically generated and deployed on the HC3 with the hc3_emulator.proxy=true keyword

-- Binary switch type should handle actions turnOn, turnOff
-- To update binary switch state, update property "value" with boolean

function EVENT(ev)
    COUNT = (COUNT or 0)+1
    local name="PROXY"..plugin.mainDeviceId.."_"..COUNT
    api.delete("/customEvents/"..name)
    api.post("/customEvents",{name=name,userDescription=json.encode(ev)})
    fibaro.emitCustomEvent(name)
end
function QuickApp:ACTION(name) self[name] = function(self,...) EVENT({name=name,args={...}}) end end
 
    QuickApp:ACTION("turnOn")
    QuickApp:ACTION("turnOff")
    QuickApp:ACTION("button1Clicked")
    QuickApp:ACTION("button2Clicked")
    QuickApp:ACTION("slider1Clicked")
 
function QuickApp:onInit()
    self:debug("onInit")
    print("OK")
end

(downloaded version hereMyQuickApp-2.fqa )

It has a UI with 2 buttons and a slider. Buttons are called 'button1' and 'button2' and slider is called 'slider1'

Instead of declaring methods on QuickApp we declare the functions using the provided function ACTION - both UI functions and device actions. The ACTION methods send the event back to the code in the IDE... or rather, it's post a customEvent on the HC3 that is picked up by the offline code that polls refreshState/...

 

This is a binary switch so it needs to react on turnOn/turnOff

 

If we have this QD and the deviceID is 21, we can run this code offline in our IDE:

if dofile and not hc3_emulator then
  hc3_emulator = { credentials = {ip="192.168.1.X", user="<user>", pwd="<password>"}}
  hc3_emulator.id=21
  hc3_emulator.poll=2000
  dofile("fibaroapiHC3.lua")
end

fibaro.debug("","OK")

function QuickApp:turnOn() 
  self:debug("ON") 
  self:updateProperty("value",true)
end
function QuickApp:turnOff() 
  self:debug("OFF") 
  self:updateProperty("value",false)
end

function QuickApp:button1Clicked() 
  self:turnOn()
  self:updateView("label","text","Button1 clicked") 
end
function QuickApp:button2Clicked() 
  self:turnOff()
  self:updateView("label","text","Button2 clicked") 
end

function QuickApp:onInit()
  self:debug("onInit")
  self:updateProperty("value",true)
end

hc3_emulator.start(21,2000) means get events from HC3 device with id 21, and poll the HC3 every 2000ms. 1000 will poll every second - but when debugging there's no reason to stress the HC3...

 

Now if we click on UI buttons or change the state of the device it will call the correct functions in our offline code(!) We use the UI of the QuickApp on the HC3 but all the code runs offline in our IDE. Because we also trap all the actions we also run the turnOn/turnOff or other action handlers in our IDE.

 

self:updateView works sometimes, your mileage may vary - hopefully they will fix the bugs in the next release.

 

net.HTTPClient() is also supported. 

This makes it relatively easy to code and debug QuickApps offline. Just being able to set breakpoints and inspect values helps a lot... 

Btw, I use ZeroBrane studio

 

P.S I will extend this to support scenes when we get the next update and have fibaro.getSourceTrigger. Before that it's quite useless.

 

There is a potential bug that if events are generated too fast on the HC3 the IDE will miss some - but I'm working on a fix. Fixed.

New version of fibaroapiHC3 0.6 - fix bug with updateProperty

Edited by jgab
  • Like 1
  • Thanks 1
Link to post
Share on other sites

Oops, the last fix for the fibaroapi makes events pile up on the HC3 - wait for a patch...

Ok, now it's fixed. Everytime the offline QuickApp starts it clears stale events, and events are removed when received.

The events will show up as "PROXY<deviceID>_<sequence number>" in the customEvents panel, but will be removed as soon as used.

Edited by jgab
Link to post
Share on other sites

When developing QuickApp scenes offline using the fibaroapiHC3 code in previous post, a nice trick is to structure it like this:

if dofile and not hc3_emulator then
  hc3_emulator.poll = 2000
  hc3_emulator.quickVars = { ['secret'] = "$CREDS.secret" }
  dofile("fibaroapiHC3.lua")
end

function QuickApp:onInit()
   local secret = self:getVariable("secret")
end

The symbol 'dofile' is not defined on the HC3 so the code between if dofile ... end is only run when we develop the code offline.

In the beginning we include the 'fibaroapiHC3.lua' file that gives us "HC3" compatibility.

We also create a file 'credentials.lua' that will be loaded and assigned to hc3_emulator.credentials when we start up.

return {
   ip = "192.168.1.75",
   user='admin',
  pwd = 'amin'
  secret = 'my other secret'
}

Here we set the credentials needed to access the HC3, ip,user,pwd.

We can also add other fields. Like 'secret' here. 

It's defined as the value '$CREDS.secret' in the header because the credentials file is not loaded when the header is run. It will later be resolved to the value in hc3_emulator.credentials.secret - so the quickVar will get the right value.

 

The reason to go through this trouble is that we can copy the code 'as-is- without any changes and paste it into the HC3 QuickApp editor and it works. 

Not having credentials visible in our main code also reduces the risk that we accidentally copy and paste the code to the forum (or upload to Github) with our credentials visible - have happened to me :-) 

Edited by jgab
Link to post
Share on other sites
On 2/16/2020 at 1:33 PM, petrkl12 said:

@jgab Do you know how to get device ID in QA?

 

function QuickApp:onInit()
   self:debug("deviceID:",self.id)
end

 

Edited by jgab
Link to post
Share on other sites

@jgab I have this bug in fibaroapiHC3.lua with timers: "fibaroapiHC3.lua:206: attempt to index global '_gTimers' (a nil value)"

 

here is testing code:

function tttTimer()
  Log("", "tttTimer")
end

function TestTimer()
  Log("", "TestTimer")
  dd = setTimeout(function() tttTimer() end, 10000)
  if dd then clearTimeout(dd) end
end

function QuickApp:onInit()
    setUpUtilities(self); self.debug = function(self,...) Log(LOG.LOG,...) end
    self:debug("onInit")
    TestTimer()
end

 

Link to post
Share on other sites
38 minutes ago, petrkl12 said:

@jgab I have this bug in fibaroapiHC3.lua with timers: "fibaroapiHC3.lua:206: attempt to index global '_gTimers' (a nil value)"

 

here is testing code:

function tttTimer()
  Log("", "tttTimer")
end

function TestTimer()
  Log("", "TestTimer")
  dd = setTimeout(function() tttTimer() end, 10000)
  if dd then clearTimeout(dd) end
end

function QuickApp:onInit()
    setUpUtilities(self); self.debug = function(self,...) Log(LOG.LOG,...) end
    self:debug("onInit")
    TestTimer()
end

 

 

Thanks for finding a bug. I had copied/pasted clearTimeout from ER which needed a tweak to work here.

I fixed it in v0.12 that is linked in the post (at least I can run your example)

 

 

3 minutes ago, jgab said:

 

Thanks for finding a bug. I had copied/pasted clearTimeout from ER which needed a tweak to work here.

 

Yes, but it doesn't cancel the timer - wait for 0.13 with a fix...

Link to post
Share on other sites
  • jgab changed the title to SDK for remote and offline HC3 development.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • Create New...