jgab 903 Share Posted February 10, 2020 (edited) 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: Introduction to the QuickApp anatomy - tutorial Part 1. Lua functions and object-oriented programming. (QuickApp is a OO class, so we need that base) Part 2. The basic QuickApp functions and what they do... and how. Part 3. More on QuickApp event handling - interaction with the UI and fibaro.call(<quickApp>,"name",...) Part 4. QuickAppChildren and how to raise them... what makes them tick? All functions and variables available in the QuickApp Lua environment Logging functions (replacement for color/html tags + tostring for customised data structure) Shared functions calls between QuickApps (Here is an improved version) Off-line HC3api to use fibaro.* calls on PCs/Linux/Mac (fibaroapiHC3.lua) Polling for triggers in a QuickApps (like fibaro.getSourceTrigger()) Here is another method using a helper QA Patching 'setTimeout' so you get an error message if the function crashes A generic template for a QuickApp A simple code-lock QuickApp (demonstrating the UI with buttons) A QuickApp for scheduling user profiles (demonstrates UI buttons that change labels/text to present options) It doesn't' actually schedules the profile yet. (here is a working version) Structuring a QuickApp using event handlers to cope with asynchronous calls - like when using net.HTTPClient() instead of FHTTP(). looping with setInterval (without drifting) A QD reporting if other QDs are crashing (leveraging the "polling for triggers" code) Coding and debugging HC3 QuickApps offline using PC/Mac/Linux and a Lua IDE (and auto-creating a proxy on the HC3) An example of a QuickApp that download and installs scenes and QuickApps from a repository (files in a flat format) Coding and debugging of HC3 scenes using fibaroapiHC3.lua (not strictly about QuickApps but related) - can speed-up time A more complex QD that reads Google calendars or iPhone calendars and schedule custom events (uses the QuickApp structure for asynchronous calls in a previous tip) A substitute for Lua's loadstring() Here is another method of loading code dynamically into a QA Creating proxy devices on the HC3 to share devices between HC2 and HC3 A "webhook" QD - pushing events to external apps Adding interfaces to QA's - ex. power and battery and updating the properties (updates the little battery and power icon UI) @tinman Using '/plugin/publishEvent' to emit 'centralSceneEvent' (and a few other) .... Ex. keyfob QA by @tinman QA Toolbox. A modular toolbox of add-on functions to QAs that makes it easier to develop QAs 'basic' - Generic QA functions for loggin, loading modules, and management - used by all other modules. (some documentation) 'childs' - QA functions to easily manage quickAppChild devices. Loading, saving state, getting UI events etc. 'events' - QA functions for defining event handlers and structuring your code accordingly. Makes it easy to setup timers in various ways... 'triggers' QA functions for recieving triggers like Scenes do. The events module will receive triggers if loaded, but own handler can be defined. 'rpc' - QA functions for declaring (synchronous) remote functions from other QAs files - QA functionality for copying files between QAs pubsub - QA functions for event publish/subscribe... ui - QA functions for manipulation the UI elements of a QA lua parser/compiler - QA function for emulating loadstring and evaluating Lua expression from strings profiler - Functions for timing code used in QA Reading label/button/slider values. Sha2.lib crypto libs for HC3 (MD5, HMAC, SHA-1, SHA-224, SHA-256, SHA-512/224, SHA-512/256, SHA-384, SHA-512, SHA3-224, SHA3-256, SHA3-384, SHA3-512, SHAKE128, SHAKE256) @tinman aes crypto lib @tinman 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 December 30, 2020 by jgab 6 5 Quote Link to post Share on other sites
jgab 903 Author Share Posted February 10, 2020 (edited) 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 April 22, 2020 by jgab 2 Quote Link to post Share on other sites
jgab 903 Author Share Posted February 10, 2020 (edited) 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 March 8, 2020 by jgab 1 4 Quote Link to post Share on other sites
petrkl12 66 Share Posted February 10, 2020 @jgab Thanks for sharing! Very helpful! Quote Link to post Share on other sites
jgab 903 Author Share Posted February 10, 2020 (edited) 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 March 1, 2020 by jgab Fix, debug 2 Quote Link to post Share on other sites
jgab 903 Author Share Posted February 10, 2020 (edited) 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 February 11, 2020 by jgab 1 Quote Link to post Share on other sites
Tony270570 66 Share Posted February 10, 2020 Thank you @jgab for all your sharing. Quote Link to post Share on other sites
robw 49 Share Posted February 11, 2020 Thank you, jgab, for all tips! Quote Link to post Share on other sites
AR27690 11 Share Posted February 11, 2020 (edited) 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 February 11, 2020 by AR27690 1 Quote Link to post Share on other sites
jgab 903 Author Share Posted February 11, 2020 (edited) 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 February 11, 2020 by jgab 1 Quote Link to post Share on other sites
AR27690 11 Share Posted February 11, 2020 (edited) 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 February 11, 2020 by AR27690 Quote Link to post Share on other sites
jgab 903 Author Share Posted February 11, 2020 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. Quote Link to post Share on other sites
AR27690 11 Share Posted February 11, 2020 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? 1 Quote Link to post Share on other sites
jgab 903 Author Share Posted February 11, 2020 (edited) 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 October 27, 2020 by jgab 1 1 Quote Link to post Share on other sites
jgab 903 Author Share Posted February 12, 2020 (edited) 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 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 February 12, 2020 by jgab 2 2 Quote Link to post Share on other sites
jgab 903 Author Share Posted February 12, 2020 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. 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) 1 Quote Link to post Share on other sites
robw 49 Share Posted February 12, 2020 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! Quote Link to post Share on other sites
jgab 903 Author Share Posted February 12, 2020 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) Quote Link to post Share on other sites
robw 49 Share Posted February 12, 2020 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, Quote Link to post Share on other sites
AR27690 11 Share Posted February 12, 2020 (edited) 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 February 12, 2020 by AR27690 Quote Link to post Share on other sites
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.