Jump to content

HC3 QuickApps coding - tips and tricks


Recommended Posts

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 a library of "nice to have" code and include them in QAs.

 

Here is Fibaro's manual for QuickApps. 

Here is Fibaro's manual for creating QuickAppChild devices

Here is Fibaro's manual for using MQTT client

Here is Fibaro's manual for WebSocket client

 

List of posts:

 

Readers note. I started to call QuickApp devices for QDs (as in QuickApp Device, thought QAs sounded like Question and Answers). So, I use the word QD here and there but I'm not religious about it...

Edited by jgab
  • Like 3
  • Thanks 5
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

We can call functions in other QAs with fibaro.call(ID,<function name>,<args>)

However, we can't get values back easily as fibaro.call returns nil...

We can use callback methods but it's usually not so convenient, we would really like fibaro.call to immediately return the value, i.e. be synchronous.

What we really want to do is to have something like (synchronous) RPC calls...

 

So here is a way to achieve synchronous remote function calls - e.g. for a shared library of Lua code or something.

(It's actually more than just shared code as it calls a QA that can keep a state between calls and share that state between many calling  clients, i.e RPC)

 

The QD (with deviceID 81)  that is exporting the functions looks like:

function test1(a,b) return a+b end
function test2(a,b) return tostring(a)..tostring(b) end

function QuickApp:onInit()
    exportFunctions{"test1","test2"} -- export our functions
    --- Alternatively, add a documentation string for each function that will be printed when the client import the functions
    exportFunctions{{name="test1",doc="Sum 2 values"},{name="test2",doc="Concatenates 2 strings"}} -- export our functions
end
exportFunctions(<table>)

<table> is a list of the global functions you want to export

 

Then we have another QuickApp that wants to use function 'test1' from QuickApp 81 and it will import the function like this (it will also import 'test2'):

function QuickApp:onInit() 

    importFunctions(81,nil,true) -- Import exported functions from QuickApp with ID 81

    local sum = 0
    local s0 = os.time()
    local n = 250         -- Make 250 calls
  
    for i=1,n do
        -- call remote function like a regular function that returns the value immediatly (i.e. synchronous)
        sum=sum+test1(10,10)
    end
  
    self:debug("Time:",(os.time()-s0)/n) -- Calculate average time to call test1
    self:debug("Sum is ",sum)
end
importFunctions(<deviceID>,<table>,<log>)

<deviceID> is the deviceID of the QuickApp exporting the functions

<table>is the table we want to put the functions in - default is _G which will make them global

<log> is a boolean that if true logs imported functions to the console

 

The second QD calls the test1 function a number of times and prints the average time a call took:

[DEBUG] 10.02.2020 20:18:25: [Sys] importing fun 81:test1 
[DEBUG] 10.02.2020 20:18:25: [Sys] importing fun 81:test2 
[DEBUG] 10.02.2020 20:18:43: [L] Time:0.072

Average call takes ~70ms - not that bad.

 

The magic is the functions that we create when we import them. Those "imported" functions need to send the name of the function and the arguments to the exporting QD, and wait for the answer to come back.

Because it's a synchronous call we can do 

self:debug("Sum of 12 and 11 is %s",test1(12,11))

...which would have been more cumbersome if it was an asynchronous call.

 

The code for import/export is

function createRemoteFunctionSupport(timeout)
  local TIMEOUT = timeout or 3
  local FUNRES = "RPC"..Event.deviceID
  api.post("/globalVariables/",{name=FUNRES}) -- Create mailbox for return value

  -- Client
  local function funCall(deviceID,fun,args)
    local timeout,res = os.time()+TIMEOUT,nil
    fibaro.setGlobalVariable(FUNRES,"")
    fibaro.call(deviceID,"RPC_CALL",json.encode({name=fun,args=args,from=FUNRES}))
    repeat res=fibaro.getGlobalVariable(FUNRES) until res~="" or timeout < os.time()
    if res~="" then
      res = json.decode(res)
      if res[1] then return table.unpack(res[2])
      else error(string.format("Remote function error %s:%s - %s",deviceID,fun,res[2]),3) end
    end 
    error(string.format("Remote function %s:%s timed out",deviceID,fun),3)
  end
  
  function defineRemoteFun(name,deviceID,tab) (tab or _G)[name] = function(...) return funCall(deviceID,name,{...}) end end
  
  function importFunctions(deviceID,tab,log)
    log = log==nil and true or log    
    for _,v in ipairs(fibaro.getValue(deviceID,'quickAppVariables') or {}) do
      if v.name == "ExportedFuns" then
        for _,ef in ipairs(json.decode(v.value) or {}) do
          ef = type(ef)=='string' and {name=ef} or ef
          if log then print(string.format("importing fun %s:%s%s",deviceID,ef.name,ef.doc and (" - "..ef.doc) or "")) end
          defineRemoteFun(ef.name,deviceID,tab)
        end
        return
      end
    end
    print("No exported functions from deviceID:"..deviceID)
  end

  -- Server
  function exportFunctions(funList) quickSelf:setVariable("ExportedFuns",json.encode(funList)) end

  function QuickApp:RPC_CALL(call) -- Receieve function call and return reponse
    local stat,res = pcall(function() return {_G[call.name](table.unpack(call.args))} end)
    fibaro.setGlobalVariable(call.from,json.encode({stat,res}))
  end
end -- createRemoteFunctionSupport 

 

Shouldn't be used for functions called 1000's per second - but for larger utility functions this works quite ok.

 

If Fibaro would trust us programmers and allow Lua's coroutines to be used in scenes/QDs this could be much more efficiently (and elegantly) implemented.... we wouldn't need to busy wait for the return value, we wouldn't need to use a fibaro global for the return value, and we would only need to suspend the calling "thread". 

Lua's coroutines are deemed safe for Lua sandboxes since v5.2 so please let us have them!

 

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

This post is not updated anymore as the discussion on the fibaroapiHC3.lua SDK has moved to 

 

--------------------------------------------------------------------------------

 

File:  fibaroapiHC3.lua(v0.65)

 

I've started to run and test HC3 QDs offline (using ZeroBrane studio, free for Mac,Windows, and Linux) and have made a fibaroapi.lua file that can be included to emulate the fibaro calls and call out to the HC3.

 

It's similar to the old fibaroapi for the HC2 but with more extensive support to create a better "offline experience"... 

define

hc3_emulator.credentials = {ip=<HC3_IP>, user=<username>, pwd=<password>}

and include 

dofile("fibaroapiHC3.lua")

most of the functions are there and will be improved over time.

  • There are support for net.HTTPClient() and setTimeout/clearTimeout and api.*
  • There are support for getting triggers and events from the HC3
    • Support for auto-creating a QuickApp proxy with UI elements that sends events back to the code being debugged.
  • There are support for both QuickApps and Scenes (with conditions)

 

Currently supported (v 0.38)

fibaro.debug(type,str) 
fibaro.warning(type,str) 
fibaro.trace(type,str) 
fibaro.error(type,str)

fibaro.call(deviceID, actionName, ...) 
fibaro.getType(deviceID) 
fibaro.getValue(deviceID, propertyName)
fibaro.getName(deviceID) 
fibaro.get(deviceID,propertyName) 
fibaro.getGlobalVariable(varName)
fibaro.setGlobalVariable(varName ,value) 
fibaro.getRoomName(roomID)
fibaro.getRoomID(deviceID)
fibaro.getRoomNameByDeviceID(deviceID) 
fibaro.getSectionID(deviceID)
fibaro.getIds(devices)
fibaro.getAllDeviceIds()
fibaro.getDevicesID(filter)
fibaro.scene(action, sceneIDs)
fibaro.profile(profile_id, action)
fibaro.callGroupAction(action,args)
fibaro.alert(alert_type, user_ids, notification_content) 
fibaro.alarm(partition_id, action)
fibaro.setTimeout(ms, func)
fibaro.emitCustomEvent(name)
fibaro.wakeUpDeadDevice(deviceID)
fibaro.sleep(ms)

net.HTTPClient()
net.TCPSocket()
api.get(call) 
api.put(call <, data>) 
api.post(call <, data>)
api.delete(call <, data>)

setTimeout(func, ms)
clearTimeout(ref)
setInterval(func, ms)
clearInterval(ref)

plugin.mainDeviceId

QuickApp:onInit() -- called at startup if defined
QuickApp - self:setVariable(name,value) 
QuickApp - self:getVariable(name)
QuickApp - self:debug(...)
QuickApp - self:updateView(elm,type,value)
QuickApp - self:updateProperty(name,value)
QuickApp - self:addInterfaces(<list of interfaces>)

sourceTrigger - scene trigger
Supported scene events:
  {type='device', id=<number>, property=<string>, value=<value>}
  {type='global-variable', property=<string>, value=<value>}
  {type='date', property="cron", value={ <time> }}
  {type='date', property="sunset", value={ <time> }}
  {type='date', property="sunrise", value={ <time> }}
  {type='manual', property='execute'}
  {type='custom-event', name=<string>}
  {type='device' property='centralSceneEvent', id=<number>, value={keyId=<number>, keyAttribute=<string>}}

json.encode(expr)
json.decode(string)

fhc3_emulator.start{                        -- start QuickApp/Scene
              id=<QuickApp ID>,       -- default 999
              poll=<poll intervall>,  -- default false. Interval i ms to poll for new rigger from the HC3. 1000-2000 is reasonable...
              type=<type>,            -- default "com.fibaro.binarySwitch"
              speed=<speedtime>,      -- default false
              proxy=<boolean>         -- default false. Creates a proxy on the HC3, ignore id.
              quickApp=<boolean>      -- default false. Connects to an existing quickapp on the HC3 with givem id
              UI=<UI table>,          -- default {}
              quickvars=<table>,      -- default {}
              } 
hc3_emulator.createQuickApp{               -- creates and deploys QuickApp on HC3
              name=<string>,            
              type=<string>,
              code=<string>,
              UI=<table>,
              quickvars=<table>,
              dryrun=<boolean>
              } 
hc3_emulator.createProxy(<name>,<type>,<UI>,<quickVars>)       -- create QuickApp proxy on HC3 (usually called with fibaro._start)
hc3_emulator.post(ev,t)                                        -- post event/sourceTrigger 
hc3_emulator.offline = <boolean>                               -- Set to true in beginning of file if running simulated devices..
hc3_emulator.createDevice(deviceID,deviceType)                 -- Create an offline device of a given type. When offline device are also autocreated of type binarySwitch. More doc on this is coming

If someone wants to try this in another IDE than Zerobrane that I use (like Visual Studio) the only thing that could be an issue is to have access to the Lua libraries

require ("ssl.https") 
require("socket.http")
require("socket")
require("ltn12")

They are pretty standard lua libraries - based on LuaSocket. If someone manage to get it running in VisualStudio I would be interested to know.

 

Any improvements are happily received (in code) and credits will be due granted.

Here is another post with some examples of coding QuickApps:

...and one for coding scenes

 

 

v 0.7 fixed bug in offline updateProperty

v 0.8 bug using fibaro:...

v 0.9 fixed bug  in getValue

v 0.10 fixed offline quickvars

v 0.12 fixed clearTimeout bug

v 0.14 fixed clearTimeout bug for real....

v 0.15 rudimentary support for scenes...

v 0.16 fix getVariable

v 0.18 https fix, support for auto creating proxy on HC3

v 0.20 stability fix, + more events.

v 0.21 more scene conditions and "speed time"

v 0.23 fix bug plugins->plugin

v 0.24 caching values allow speed-time to run faster and create less traffic to the HC3.  global-variables triggers in conditions

v 0.25 fibaro.warning etc. Bug fixes.

v 0.29 fixed bug in setTimeout (again) when running at "speed time"

v 0.30 more event types + general fixes

v 0.35 New approach to polling for triggers as emit customevent REST API hangs ~60s on 5.021.38

v 0.38 Fixed bug in createQuickApp's rendering of UI elements. Added fibaro.sleep....

v 0.40 Fixes.

v 0.44 Changed calling conventions for fibaro._createQuickApp

v 0.45 Support for changing UI layout of existing QuickApp. fibaro.__updateViewLayout(id,UI)

v 0.50 Refactoring, new calling convention for fibaro._start(), first attempt to simulated off-line devices...

v 0.60 More refactoring. Fixed a bug with setTimeout and "speed time". Additional bug-fixes and additions from @petergebruers, moved emulator functions from fibaro._* to hc3_emulator.* . This means a new calling convention for starting the scene...  Because it's a major restructuring of the code new bugs could have cropped up - please let me know.

v 0.64 Bugfixes, better offline support.

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

Another thing that can be useful to do in QD's is to receive triggers - because scenes are so "functionality challenged" in the HC3 it can be useful to use QD's as scenes and let them react on events in the systems.

Here is the standard way of polling /refreshStates and then posting the incoming events to a user defined QuickApp:event(event)

 

function startPolling(self,interval)
  local INTERVAL = interval or 1000 -- every second, could do more often...
  local tickEvent = "TICK"
  local function Log(_,...) 
    local a = {...}
    for i=1,#a do local e=a[i]; a[i] = type(e)=='table' and json.encode(e) or tostring(e) end
    self:debug("[L] "..string.format(table.unpack(a))) 
  end
  local LOG = {LOG=""}

  local function post(event) self:event(event) end
  api.post("/globalVariables",{name=tickEvent,value="Tock!"})

  local EventTypes = { -- There are more, but these are what I seen so far...
    AlarmPartitionArmedEvent = function(self,d) post({type='alarm', property='armed', id = d.partitionId, value=d.armed}) end,
    AlarmPartitionBreachedEvent = function(self,d) post({type='alarm', property='breached', id = d.partitionId, value=d.breached}) end,
    HomeArmStateChangedEvent = function(self,d) post({type='alarm', property='homeArmed', value=d.newValue}) end,
    HomeBreachedEvent = function(self,d) post({type='alarm', property='homeBreached', value=d.breached}) end,
    WeatherChangedEvent = function(self,d) post({type='weather',property=d.change, value=d.newValue, old=d.oldValue}) end,
    GlobalVariableChangedEvent = function(self,d)
      if d.variableName == tickEvent then return end
      post({type='global', name=d.variableName, value=d.newValue, old=d.oldValue})
    end,
    DevicePropertyUpdatedEvent = function(self,d)
      if d.property=='quickAppVariables' then 
        local old={}; for _,v in ipairs(d.oldValue) do old[v.name] = v.value end -- Todo: optimize
        for _,v in ipairs(d.newValue) do
          if v.value ~= old[v.name] then
            post({type='quickvar', name=v.name, value=v.value, old=old[v.name]})
          end
        end
      else
        --if d.property:match("^ui%.") then return end
        post({type='property', deviceID=d.id, propertyName=d.property, value=d.newValue, old=d.oldValue})
      end
    end,
    CentralSceneEvent = function(self,d) post({type='centralSceneEvent', data=d}) end,
    AccessControlEvent = function(self,d)  post({type='accessControlEvent', data=d}) end,
    CustomEvent = function(self,d) if d.name == tickEvent then return else post({type='customevent', name=d.name}) end end,
    PluginChangedViewEvent = function(self,d) post({type='PluginChangedViewEvent', value=d}) end,
    WizardStepStateChangedEvent = function(self,d) post({type='WizardStepStateChangedEvent', value=d})  end,
    UpdateReadyEvent = function(self,d) post({type='UpdateReadyEvent', value=d}) end,
    SceneRunningInstancesEvent = function(self,d) post({type='SceneRunningInstancesEvent', value=d}) end,
    DeviceRemovedEvent = function(self,d)  post({type='DeviceRemovedEvent', value=d}) end,
    DeviceChangedRoomEvent = function(self,d)  post({type='DeviceChangedRoomEvent', value=d}) end,    
    DeviceCreatedEvent = function(self,d)  post({type='DeviceCreatedEvent', value=d}) end,
    DeviceModifiedEvent = function(self,d) post({type='DeviceModifiedEvent', value=d}) end,
    SceneStartedEvent = function(self,d)   post({type='SceneStartedEvent', value=d}) end,
    SceneFinishedEvent = function(self,d)  post({type='SceneFinishedEvent', value=d})end,
    -- {"data":{"id":219},"type":"RoomModifiedEvent"}
    SceneRemovedEvent = function(self,d)  post({type='SceneRemovedEvent', value=d}) end,
    PluginProcessCrashedEvent = function(self,d) post({type='PluginProcessCrashedEvent', value=d}) end,
    onUIEvent = function(self,d) post({type='uievent', deviceID=d.deviceId, name=d.elementName}) end,
    ActiveProfileChangedEvent = function(self,d) 
      post({type='profile',property='activeProfile',value=d.newActiveProfile, old=d.oldActiveProfile}) 
    end,
  }

  local function checkEvents(events)
    for _,e in ipairs(events) do
      if EventTypes[e.type] then EventTypes[e.type](self,e.data)
      else Log(LOG.LOG,"Unhandled event:%s -- please report",json.encode(e)) end
    end
  end

  local lastRefresh = 0
  local function pollRefresh()
    local states = api.get("/refreshStates?last=" .. lastRefresh)
    if states then
      lastRefresh=states.last
      if states.events and #states.events>0 then checkEvents(states.events) end
    end
    fibaro.setGlobalVariable(tickEvent,tostring(os.clock()) -- hack because refreshState hang if no events...
  end

  setInterval(pollRefresh,INTERVAL)
end

------------ main program ------------------

function QuickApp:event(sourceTrigger) -- Do whatever at incoming events
   local event = sourceTrigger
   self:debug(json.encode(event))
   if event.type=='property' and event.deviceID==88 and event.value==true then
       self:debug("Lamp 88 turned on")
   end
end

function QuickApp:onInit()
    self:debug("onInit")
    startPolling(self,1000)
end

There could be support for more events and some kind of filter function.

In general, errors in setTimeout timers never give any error messages so it's good to have a routine that is debugged and works. Also, at the moment, /refreshStates hangs if there are no events available so we emit our own "tick" event to drive the loop.

 

(this kind of loop is what I'm using in my EventRunner port)

 

Note. This code has evolved now and I would recommend to have a look in the fibaroapiHC3.lua file how to poll for events

Edited by jgab
Fix, debug
  • Like 2
Link to post
Share on other sites

As I said, in QDs if you call setTimeout(fun, ms) and there is a bug/error in the fun so it crashes, you will not get any error messages. This can be very annoying trying to pinpoint what's gone wrong in your code.

Hopefully Fibaro will fix this but while we wait (can't hold my breath more than 2min)  we can patch setTimeout to print out the error

do
  local oldSetTimeout = setTimeout
  function setTimeout(fun,ms)
     return oldSetTimeout(function()
     stat,res = pcall(fun)
     if not stat then print("Error in setTimeout:"..res) end
     end,ms)
  end
end

function QuickApp:onInit()
    self:debug("onInit")
    setTimeout(function() bar() self:debug("OK") end,1000)  -- will crash because 'bar' is not defined
end

There is still the problem with line numbers being wrong. We can fix that too

do
  local oldSetTimeout = setTimeout
  local stat,res = pcall(function() x() end)
  local line = 4-res:match("lua:(%d+)") -- '4' should be the line number of the previous line
  function setTimeout(fun,ms)
     return oldSetTimeout(function()
     stat,res = pcall(fun)
     if not stat then
          local cline,msg = res:match("lua:(%d+):(.*)")
          print(string.format("Error in setTimeout (line:%d):%s",line+cline,msg)) end
     end,ms)
  end
end

 

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

First of all thanks for info sharing,.

My questions is, why I need "ton" of code just to make HC3 more user-friendly and to have useful functionality?

It's insane to create so many functions just to have a nice workaround and all that is just the beginning. I believe moving forward you'll find more and more "workarounds".

13 hours ago, jgab said:

 

The verdict is still out there... However, I'm a nerd so any hardware is a temptation for me ;-) 

Looks like you did provide the verdict... 

Edited by AR27690
  • Like 1
Link to post
Share on other sites
23 minutes ago, AR27690 said:

First of all thanks for info sharing,.

My questions is, why I need "ton" of code just to make HC3 more user-friendly and to have useful functionality?

It's insane to create so many functions just to have a nice workaround and all that is just the beginning. I believe moving forward you'll find more and more "workarounds".

Looks like you did provide the verdict... 

 

Well it depends on where you come from.

 

-As a user I would like to have ready made functionality like schedulers and nice block scenes with great UI graphics. I would not like to see "a ton of code". Do you get that in the HC3? Maybe, profiles and block scenes may solve some tasks for some people.

 

-As a developer I would like to have powerful APIs and the full expressiveness of Lua to create great code (that would be easy for other user to use without having to see my code). In principle I have nothing against a lot of code if it's useful code.

-- So far Fibaro have been reluctant to invest in the "developer experience" which forces developers to do all kind of workarounds to get things done... (at the moment they seem to want to limit the possibilities for developers to make life easier for installer - an approach that I believe is misguided..)

 

Another way to put it: I'm not sharing this code for people looking for easy to use functionality. I'm sharing the code for developers that would like to make easy to use functionality for themselves or others.

Could Fibaro have provided more functionality for developers? - yes - but we can build our own "frameworks" ontop of what they provide that suits our own development style.

Edited by jgab
  • Like 1
Link to post
Share on other sites
39 minutes ago, jgab said:

-As a developer I would like to have powerful APIs and the full expressiveness of Lua to create great code (that would be easy for other user to use without having to see my code). In principle I have nothing against a lot of code if it's useful code.

Now I'm confused... you're saying that you intend to provide easy-use for other users, but honestly to copy all code and to learn how to use it is not so easy.

I'm not a SW person, so to me just to understand how to use your code (what and when to call which function) is very hard.

I feel that your work and code intended for users with moderate and above SW knowledge.

Based on my programming skills, I'm probably in wrong topic.

Any way according to users' reaction your tips are very helpful - well done

 

 

Edited by AR27690
Link to post
Share on other sites
1 minute ago, AR27690 said:

Now I'm confused... you're saying that you intend to provide easy-use for other users, but honestly to copy all code and to learn how to use it is not so easy.

I'm not a SW person, so to me just to understand how to use your code (what and when to call which function) is very hard.

I feel that your work and code intended for users with moderate and above SW knowledge.

Based on my programming skills, I'm probably in wrong topic.

 

Yes, I share the code for developers (SW persons) - not for regular users. I agree that users (non SW people) should not need to code, or feel forced to cut&paste code they don't understand to get the functionality they need. SW developers should be able to wrap new functionality in QDs that can be easily used by others.

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

 

Yes, I share the code for developers (SW persons) - not for regular users. I agree that users (non SW people) should not need to code, or feel forced to cut&paste code they don't understand to get the functionality they need. SW developers should be able to wrap new functionality in QDs that can be easily used by others.

So regular users left alone by fibaro and developers...😋

Sorry for my ignorance, what QDs means?

  • Like 1
Link to post
Share on other sites

A generic template for a QuickApp Device (QD) could look like this:

--[[
-- Available functions from 'self'
function self:debug(…) — logs all arguments. Does not support html formatting - not even strings with line breaks…
function self:updateProperty(<property>, <value>)   — updates a device property
function self:updateView(<component ID>, <property>, <value>) — updates the UI element of the QD.
function self:setVariable(<variable name>,<string value>) -- sets a device specific variable
function self:getVariable(<variable name>) -- retrieves a device specific variable
--]]

-- Setup handlers for call actions on this device. It's a multi level switch type. (A binary switch needs true/false on as value property)
function QuickApp:setValue(value) 
  -- things to do when someone calls fibaro.call(<myDevice ID>,"setValue",<value>)
  self:updateProperty("value", value) 
end
function QuickApp:turnOn(value) self:updateProperty("value", 99) end -- fibaro.call(<myDevice ID>,"turnOn") 
function QuickApp:turnOff(value) self:updateProperty("value", 0) end -- fibaro.call(<myDevice ID>,"turnOff") 

-- Setup handlers for button call-backs in device UI  <name of element>.."Clicked"
function QuickApp:button1Clicked()  b1State=not b1State; self:updateView("button1", "text", b1State and "ON" or "OFF") end -- Ex. Toogle button
function QuickApp:mySliderClicked(value) self:updateView("myLabel","text", string.format("Value is %s",value)) end


local function main(self)
  fibaro.printf("Starting main (ID:%s)",fibaro.ID)
  --print("Starting main") -- 'print' is also available when onInit() have been called
 
 -- Here we do regular setup or...
--[[
  -- ...if we want a looping main we do this at the end...
  _nextTime = _nextTime or os.time()
  _nextTime = _nextTime + 60*1000 -- Time between loops, ex. 1min
  setTimeout(main,1000*(_nextTime-os.time()))
  
  -- ...or if we don't care about a little timedrift we can just do
  setInterval(main,60*1000) -- call main again every minute
--]]

end

function QuickApp:onInit()  -- onInit() sets up stuff...
  self:debug("Starting up - deviceId:",self.id) -- Always good to remembered of the ID
  function fibaro.debug(tag,...) self:debug(...) end -- Add 'scene' style debug missing in QuickApps
  function fibaro.printf(fmt,...) -- add formatting print function that does auto-json on tables.
    local args = {...}
    for i=1,#args do local a = args[i]; args[i]=type(a)=='table' and json.encode(a) or tostring(a) end
    self:debug(string.format(fmt,table.unpack(args)))
  end
  self.HT = json.decode(self:getVariable("myConfigurationData") or "[]") -- Your "HomeTable". You can store dev specific conf in a quickvar
  setTimeout(function() main(self) end,0)   -- make sure that our own code starts when onInit() done it's stuff.
end

Different types of devices should support different QuickApp:* methods. This is a multilevel switch and support setValue,turnOn, and turnOff.

It also has a UI with a slides, button and a label.

 

One take-away here is that the functions you declare on QuickApp, ex. function QuickApp:turnOn() etc becomes the "API" of the device and is callable from other devices or even the external REST API. Therefore you should keep you business logic in "normal" Lua functions or "objects" that you define and call from QuickApp:onInit() at startup. Here we call our function main().

 

On 2/11/2020 at 10:53 AM, AR27690 said:

So regular users left alone by fibaro and developers...😋

Sorry for my ignorance, what QDs means?

 

No, we developers are trying our darnest to create value to you that you don't get directly from Fibaro.

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

Ok, back to code.

 

The QD has support for UI buttons, labels, and sliders like the old VD (no improvements here).

We declared handler in the QD, so if we have a button with id 'button1' it will call 

function QuickApp:button1Clicked() .... end

when we click on the button. 

Disclaimer: Still some bugs on the HC3 that makes these UI's not working on mobile apps, and sometimes flaky even in the web gui... but that will hopefully be fixed.

 

So, on to an example:

Assume, we make a simple QD to work as a code lock - here we make the QD to be of type ''door sensor' (we chose the type when we create the QuickApp device).

It could look like this:

-- Door lock type should handle actions secure, unsecure
-- To update door lock state, update property "secured" with integer value (1 - secured, 0 - unsecured)

TimeOut = 4

function QuickApp:secure()
    self:updateView("label","text","Code:")
    self:debug("door lock secured")
    self:updateProperty("secured", 1)
    attempt=""
end

function QuickApp:unsecure()
    self:updateView("label","text","Code:OK")
    self:debug("door lock unsecured")
    self:updateProperty("secured", 0)
    attempt=""   
    setTimeout(function() self:secure() end,TimeOut*1000) 
end

passcode = "0123"
attempt = ""
p = 0

function add(key,self)
    p=p+1
    attempt=attempt.."*"
    if key ~= passcode:sub(p,p) then 
       p = 0
    end
    self:updateView("label","text","Code:"..attempt)
    if p==#passcode then self:unsecure() end
end

function QuickApp:button0Clicked() add("0",self) end
function QuickApp:button1Clicked() add("1",self) end
function QuickApp:button2Clicked() add("2",self) end
function QuickApp:button3Clicked() add("3",self) end
function QuickApp:button4Clicked() add("4",self) end
function QuickApp:button5Clicked() add("5",self) end
function QuickApp:button6Clicked() add("6",self) end
function QuickApp:button7Clicked() add("7",self)end
function QuickApp:button8Clicked() add("8",self) end
function QuickApp:button9Clicked() add("9",self) end
function QuickApp:buttonClearClicked() p=0; attempt=""; self:updateView("label","text","Code:") end


function QuickApp:onInit()
    self:secure() -- start locked
end

We lay out the controls like this

codelock.png.b819d5f737ce3dd0c7ea24544782762a.png

and name the handlers 'button0' etc.

This device will then act as a code lock, that will get 'unsecured' when the right code is typed and auto-secure again after 4s.

Other scenes and devices (or block scenes) could trigger on this device being unsecured and do something - like disable an alarm.

FQA:Code_lock.fqa

Next post, a bit more advanced...

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

A bit more elaborate QD example.

 

First, we can update the names of the UI elements (e.g buttons) programmatically and it's reflected immediately in the web GUI (still issues with the mobile app it seems),

Anyway , that means that we can change the names of buttons to reflects states.

Here is a control to schedule activations of profiles on the HC3 (profiles are pre-sets of device states that you can give names like "Home"; "Away" etc. and easily activate from scenes or devices.

This control is used to schedule a time when we want a particular profile to be activated.

profiles.png.0b0262961596fc86949e49feddfd51cf.png

The upper left button [sunset+07:20] will toggle between time, sunset+/- time or sunrise+/- time when pressed.

The upper right, [Home] will toggle between the different available profiles when pressed.

The 4 numbers are used to set the time (increases their digit when pressed)

[Save] should save the scheduling data associated with the profile.

[Enabled] toggles between enabled/disabled for the schedule for the profile in question

 

The actual logic to schedule the activation of the profile is not there yet (another exercise)., but it demonstrates the type of GUI interaction that can be done.

The element IDs are

[id='time'][id='profile']

[id='hour1'][id='hour2'][id='min1'][id='min2']

[id='save'][id='enabled']

...and the code looks like

local data = {}  -- Should be stored/retrieved from a quickVar
local curr = 1
local mode = {
    function(data) data.type='time' end, 
    function(data) data.type='sunrise'; data.modifier='+' end, 
    function(data) data.type='sunrise'; data.modifier='-' end, 
    function(data) data.type='sunset'; data.modifier='+' end, 
    function(data) data.type='sunset'; data.modifier='-' end, 
}

local function setUp()
  for _,p in ipairs((api.get("/profiles")).profiles) do
    data[#data+1] = {name=p.name,id=p.id,enabled=false,time="07:00",type='time', modifier="", mode=1}
  end
end
 
local function update(self)
    local p = data[curr]
    self:debug(json.encode(p))
    self:updateView("profile","text",p.name)
    local h1,h2,m1,m2 = p.time:match("(%d)(%d):(%d)(%d)")
    self:updateView("hour1","text",h1)
    self:updateView("hour2","text",h2)
    self:updateView("min1","text",m1)  
    self:updateView("min2","text",m2)          
    if p.type~='time' then
      self:updateView("time","text",p.type..p.modifier..p.time)
    else
      self:updateView("time","text",p.time)
    end
    self:updateView("enabled","text",p.enabled and "Enabled" or "Disabled")
end

function add(hour,min,self)
    local p = data[curr]
    local h,m = p.time:match("(%d%d):(%d%d)")
    h,m=hour+h,min+m
    h,m = h>23 and 0 or h,m> 59 and 0 or m
    p.time=string.format("%02d:%02d",h,m)
    update(self)
end

function QuickApp:timeClicked()
   local p = data[curr]
   p.mode = p.mode+1; if p.mode > #mode then p.mode=1 end
   mode[p.mode](p)
   update(self)
end
function QuickApp:profileClicked()
  curr = curr+1
  if curr > #data then curr=1 end
  update(self)
end
function QuickApp:hour1Clicked() add(10,0,self) end
function QuickApp:hour2Clicked() add(1,0,self) end
function QuickApp:min1Clicked() add(0,10,self) end
function QuickApp:min2Clicked() add(0,1,self) end
function QuickApp:saveClicked() self:debug("Save") end
function QuickApp:enabledClicked() local p = data[curr]; p.enabled=not p.enabled; update(self) end

function QuickApp:onInit()
    self:debug("onInit")
    setUp()
    update(self)
end

(sometimes the web GUI doesn't update the new button texts, sometimes by restarting or opening/closing the editor makes it work)

  • Like 1
Link to post
Share on other sites
3 hours ago, jgab said:

A bit more elaborate QD example.

 

Here is a control to schedule activations of profiles on the HC3 (profiles are pre-sets of device states that you can give names like "Home"; "Away" etc. and easily activate from scenes or devices.

 

 

Hi! Seriously @jgab, did you read my mind? I was just thinking about how to schedule the profiles, and then I searched your thread and found your example! :)

 

I will code this in, I want to use it for workdays to turn on lights, the Sonos etc. and a bunch of other things.

 

Thank you!

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

 

Hi! Seriously @jgab, did you read my mind? I was just thinking about how to schedule the profiles, and then I searched your thread and found your example! :)

 

I will code this in, I want to use it for workdays to turn on lights, the Sonos etc. and a bunch of other things.

 

Thank you!

Beware that the UI updates are still wonky-

but I have seen it work with my own eyes 😁

There is a rumor of an update on Friday. Have you heard anything? (one of the Swedish resellers that inform buyers of a new fw coming)

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

Beware that the UI updates are still wonky-

but I have seen it work with my own eyes 😁

There is a rumor of an update on Friday. Have you heard anything? (one of the Swedish resellers that inform buyers of a new fw coming)

 

Sorry, I have not heard anything. I got my HC3 from Kjell.com, and they don't report anything. Hopefully we'll see a new firmware then! Thank's for all your tips and tricks here!

Cheers,

Link to post
Share on other sites

 

On 2/11/2020 at 2:49 PM, jgab said:
On 2/11/2020 at 11:53 AM, AR27690 said:

So regular users left alone by fibaro and developers...😋

Sorry for my ignorance, what QDs means?

 

No, we developers are trying our darnest to create value to you that you don't get directly from Fibaro.

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

 

On 2/11/2020 at 11:47 AM, jgab said:

Yes, I share the code for developers (SW persons) - not for regular users. I agree that users (non SW people) should not need to code, or feel forced to cut&paste code they don't understand to get the functionality they need. SW developers should be able to wrap new functionality in QDs that can be easily used by others.

 

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. 

Edited by AR27690
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...