Jump to content

Welcome to Smart Home Forum by FIBARO

Dear Guest,

 

as you can notice parts of Smart Home Forum by FIBARO is not available for you. You have to register in order to view all content and post in our community. Don't worry! Registration is a simple free process that requires minimal information for you to sign up. Become a part of of Smart Home Forum by FIBARO by creating an account.

 

As a member you can:

  •     Start new topics and reply to others
  •     Follow topics and users to get email updates
  •     Get your own profile page and make new friends
  •     Send personal messages
  •     ... and learn a lot about our system!

 

Regards,

Smart Home Forum by FIBARO Team


HC3 QuickApps coding - tips and tricks


jgab

Recommended Posts

  • Topic Author
  • 52 minutes ago, 10der said:

    make GlobalVariable great again!

    Jan. I do not think what GlobalVariable is good approach.  GlobalVariable is not reentrant (if you understading what I mean)

     

    UPD: 

    Please login or register to see this link.


    To really understand a rule means to understand when it’s not a rule?

     

    Because the global is associated with the calling QA, and a QA can never run multiple instances in parallel  (like H2C scenes could) - there is no true reentrance problem. We have cooperative threads (ex setTimeout) but because we make the calls synchronous we will not experience race conditions due to that.

     

    So In this case a global is an excellent solution. I would have liked to use quickVars but the they are worse....

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

    ok. got it. thank you!

    .. but hidden label wil be good and very easy for it.

    call method -> QA:calcs -> QA:updateView -> QA:labelEnergy -> read labelEnergy

    Link to comment
    Share on other sites

  • Topic Author
  • On 5/16/2020 at 11:30 AM, 10der said:

    ok. got it. thank you!

    .. but hidden label wil be good and very easy for it.

    call method -> QA:calcs -> QA:updateView -> QA:labelEnergy -> read labelEnergy

    When do the calling QA know when the QA:calc is ready and the labelEnergy has been updated?

    The other problem is that the label needs to belong to the caller, not the QA you are calling.

    The client asks the remote function to stuff the result in the clients own mailbox, and the client is single-threaded so no 2 remote  functions will write at the same time.

    If the calc method in your example takes arguments and the labelEnergy is the result, if some other caller calls before you read the result you are in trouble.

    ...and there will have the same issue as with "reentrant" global variables.

    ...and updating a label is slower than updating a global.

     

    However, there will be a virtual prize for whoever comes up with a more clever and efficient place to store return values than globals... :-)

     

       

    Link to comment
    Share on other sites

    On 3/17/2020 at 1:24 AM, jgab said:

    In would be useful if conditions in scenes allowed for specifying a condition that would be true if the condition had been true for a specific time.

    In addition to "any" and "all" there could be a "trueFor", with a property "time" set to a time ex."00:05"...

    Ex. if a motion sensor has been safe for 5min, or if a set of motion sensors have been safe for 5min etc.

     

    Anyway, this is a thread about QAs, so let's do the second best thing...

     

    We compile expressions of type #<eventName>/<time to be true>/<interval for checks>/<expression>

    Please login or register to see this code.

    We actually put the rule in the QA'a quickappVariables

    name:#test

    value:00:05/2/100:safe & 101:safe & sunset..sunrise

     

    Anyway, we compile the expression into a Lua function that looks like

    Please login or register to see this code.

    numToBool converts a number to true or false if its >0, sunset and sunrise are variables bound to resp. time. (we have 'midnight' and 'now' also)

    We run it at every interval (2s in this case) and when it has been true for 5min we emit a CustomEvent with name 'test'

    We choose the interval depending on how quickly we need to be notified that an expression has been true.

     

    This allows us to use most of the power of Lua to craft our rules, we can do

    Please login or register to see this code.

    etc.

    The total code when we don't have to write a parser from scratch and interpret the result becomes quite compact (that's why load/loadstring would be appreciated)

    This code works as is but would probably need some more error checks etc before being "production ready"

    (Sorry for the long listing, but there are some techniques in the code that code be worth stealing...). QA attached at the end.

     

    Updated with same fixes and 'repeat' options.

    value=R00:05/2/88:on

    The starting 'R' indicates that the rule should continue to fire events every 5min the expression is true.

    Good to have if you want to have notification and reminders if a door/window is left open.

     

    New version. May 4, 2020

    Please login or register to see this attachment.

    supports day and month in .. test.

    Ex.

    mon..fri

    may..jun

     

     

    @jgab I like the idea of this QA very much , thank you.  But i do not understand how to make it work :(

     

    Test2 QA variable works, but test QA variable does not as i do not know how to change whats needed.  

    Can you please make a short explanation for non coders as to what needs to be changed and what some of the values represent ?

     

    For example my current code is :

    Please login or register to see this code.

    and my QA variables:
    • #test
      00:05:30/15/470:safe & 470:safe & (533:lux > 50 | 533:lux> 50) & sunset-00:10..sunrise+00:10
    • #test2
      R00:00:01/30/mon..wed

    The result is :

     

    image.png.5654c5a5c4ca3b94420b9f0f22346294.png

     

     

    Please help me change the "code='22%3Asafe%20%26%2022%3Asafe%20%26%20%2822%3Alux%20%3E%20200%20%7C%2022%3Alux%3E%20200%29%20%26%20sunset-00%3A10..sunrise%2B00%3A10'" into something readable for non coders ? so i can make it work.

     

    Also it looks that there are 2 of the same code lines , one being local and the other not.  Are they both needed ?

     

    What do tine, timeStr, rep do ?  I modified them randomly ?

     

    sorry for the completly easy and maybe dumb questions....

     

    Thank you

     

    Link to comment
    Share on other sites

  • Topic Author
  • 1 hour ago, Momos said:

    and my QA variables:

    • #test
      00:05:30/15/470:safe & 470:safe & (533:lux > 50 | 533:lux> 50) & sunset-00:10..sunrise+00:10
    • #test2
      R00:00:01/30/mon..wed

    The result is :

     

     

    Please help me change the "code='22%3Asafe%20%26%2022%3Asafe%20%26%20%2822%3Alux%20%3E%20200%20%7C%2022%3Alux%3E%20200%29%20%26%20sunset-00%3A10..sunrise%2B00%3A10'" into something readable for non coders ? so i can make it work

    Also it looks that there are 2 of the same code lines , one being local and the other not.  Are they both needed ?

    What do tine, timeStr, rep do ?  I modified them randomly ?

     

    sorry for the completly easy and maybe dumb questions....

     

    Thank you

     

     

    Hi,

    First, you should never get into the code inside QA. The QA reads the QA variables and "compile" them to the code inside the QA.

    You only change the QA variables. (The double line of code inside the QA is just for debugging purposes - I need to keep a lua and a string representation around for the rules...)

     

    The QA's sole purpose is to watch any number of provided expressions and when they have been true for the specified time fire a 'customEvent' that can be used to trigger scenes.

    Ex. If the lamp has been on for 10min and it's Monday to Friday, fire an event (that a scene can react to and turn off the light)

    Ex. If the sensor in the room has been  safe for 10min and it's Monday to Friday, fire an event (that a scene can react to and turn off the lights in the room)

    Ex. If the door has been open for 10min and it's Monday to Friday, fire an event that the doors is left open (that a scene can react to and maybe send a message). We can also specify that as long as the door is open it will repeat sending and event every 10min (by prefixing the time in the rule with an "R").

     

    Often we don't want to repeat the rule. In the case of the sensor above, we don't want to continue sending events every 10min when it's safe. (We wait until the rule becomes false before we start to watch for it to become true again).

     

    The format of a a test (that you set the QA variable to) is:

    #<event name>/<time the expression should be true before firing an event>/<How often to test the expression>/<the expression>

     

    So your first expression says.

    If (470:safe & 470:safe & (533:lux > 50 | 533:lux> 50) & sunset-00:10..sunrise+00:10) is true for 5min and 30s, fire an event named "test". It also checks the expression every 15s.

    There is no need to test 470 and 533 twice in the rule so this is equivalent:

    470:safe & 533:lux > 50 & sunset-00:10..sunrise+00:10

    So if deviceID 470 is safe and deviceID 533 lux value is > 50 is true for 5min and 30s  and it's between sunset - then fire an event.

    'safe' means that the device value is 0 or false. 'lux' is the same as 'value'. A light sensors lux value is reported with the 'value' property.

     

    The second expresion says

    If (mon..wed) is true for 1s fire an event named "test2". It also checks the expression every 30s, and it repeats firing events (the "R" infront of the time)

    This is what you see in the log. Because it's between monday and wednesday and it checks every 30s and you have repeat "R",  it will fire an event every 30w.

     

    So the trick is to express what you need in the "rule format" supported.

    The format is really a Lua fragment but with some changes to make the format more compact.

     

    <number>:<property> fetches the property from the deviceID with the numbers. So 55:value is fibaro.getValue(55,"value")

    '&' is logical 'and'

    '|' is logical 'or'

    <timeA>..<timeB> is true if the time is between timeA and timeB

    <dayA>..<dayB> is true if the time is between dayA and dayB

    <monthA>..<monthB> is true if the time is between monthA and monthB

    otherwise you can sneak in proper lua code in the expression too.

     

    So, I admit that it can still be complicated.

    My suggestion is that you tell me what you want to achieve and I show you how to form the rule - that's a way to learn...

    Link to comment
    Share on other sites

    3 hours ago, jgab said:

    You only change the QA variables

    Ahh :(  and there i was desperately trying to change the code inside the QA to make it work....

     

    Changing only the variables is easier :) 

     

    Just to try an example: 

    R00:00:01/01/32:off & 34:off & 470:motion & 533:lux < 1000 & sunrise-00:10..sunset+00:10

     

    32 and 34 are lights (switches), 533 is a lux sensor and 470 a motion sensor.  I am trying to make a trigger if the motion sensor is breached , lights are off, lux under 1000 and time between sunrise and sunset lets say :)  I set the repeat to 1 sec and time to 1 sec also to catch the motion sensor when it is breached.  Obviously it doesnt do anything as i do not think 470:motion is correct.

     

    Is there a list of arguments to be passed in the variables ?  Damn why did they have to chage that LUA, took me a long time to learn the old one ?

     

    This test variable works as expected :  R00:00:10/05/470:safe & 470:safe & (533:lux > 50 | 533:lux> 50) & sunrise-00:10..sunset+00:10

     

     

    Many thanks ?

     

    PS: I figured it out by looking into the QA mapping of the states:  ON for motion sensor is breached.    It works now :) 

     

     

     

    Edited by Momos
    Link to comment
    Share on other sites

  • Topic Author
  • Here is another example of creating QuickAppChildren.

    It has two types of child classes MyBinarySwitchA and MyBinarySwitchB

    It maps both classes to the same fibaro type - com.fibaro.binarySwitch

     

    When it starts up and there is no children, it will create two children.

    If when it starts up and children exists, it will instantiate the child objects with the correct class.

    The className is stored in a child quickAppVariable (yes it's possible) and used as the name for the constructor to call.

    In many cases you will not create the children in the startUp of device controller (:onInit) but when a user clicks a button or the device controller "discovers" that there is a new child device around...

    It also keeps a separate piece of data "otherData" in a local quickVar of the child and lifts it up as a self.otherData field in the child class object. That can be useful for storing info to connect to the real external child device...

    Please login or register to see this code.

     

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

  • Topic Author
  • 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. It's not so much "call this function to do this", it's more like "How and why will calling this function achieve this?".

    We will slowly dive into QuickApps and try to gain some understanding how they work under the hood. The first post is mostly a Lua functions introduction and objects, a base that we need, but the next post will go deeper into the QuickApp class and how it works. It will get progressively more difficult... ;-)

     

    The anatomy of QuickApps - Part 1.

     

    QuickApps are written in the programming language Lua. Let's have a look behind the curtain of a QuickApp and try to understand how it works.

    To understand this, you need a very basic understanding of Lua – You should have tried to make a simple QuickApp and some knowledge of Lua tables would be good. However, I'll try to explain most concepts I use in my example.

    Even for more seasoned Lua coders it may explain why Lua behaves the way it does sometimes. Or, you are welcome to correct me :-) 

    Some concepts like global and local contexts and scopes would benefit from illustrations and may be added in the future.

     

    First, QuickApps are based on a Lua object-oriented model, using 'classes' - what is that?

    To answer that questions we have to make a longer journey (is this the longest post in the forum? except for people dumping code? :-) )

    • Recap of Lua functions
    • Lua tables and and they are constructed/accessed
    • How to do object orientated programming in Lua
    • What are classes and objects

    ...and finally we will be able to talk about the QuickApp class and how it works internally...

     

    Lets start; we know that

    Please login or register to see this code.

    defines a Lua function named 'test' that takes one argument x and return 2*x. Note that x has to be a number, or we get an error. Only numbers can be added.

    We can call the function 

    Please login or register to see this code.

    and it will print 42. Don't be confused that the parameter of 'test' is 'x' and we assign 'x' the result. It's a different 'x'. We say that the parameter 'x' is scoped to the function 'test', more on this later. We can create locally scoped variables inside 'test' also

    Please login or register to see this code.

    Here is the first insight. When Lua runs your code, it runs it top to bottom, starting with the first line. This means that functions has to be defined before you can call them.

    Please login or register to see this code.

    do not work. It will complain that the function 'test' doesn't exist when it sees  x = test(21). It's first on the next line the function becomes defined.

      

    What happens when a function is defined?

    It turns out that

    Please login or register to see this code.

    is just a convenient way that Lua allow us to write

    Please login or register to see this code.

    From this we learn two things:

    1. That we have "anonymous" functions, or just "functions" for short.
    2. That a "named" function is just a variable that is assigned an "anonymous" function

    A functions in Lua is a data type just like any other data type in Lua. In fact, we don't have many types, there are only 8 of them in Lua, so let’s list them. To see what type something is you can use the 'type' function that returns the type of a value as a string. Ex. print(type(42)) prints 'number'

    • 'boolean'. Logical true/false
    • 'string'. The string type. Ex. print("This is a string")
    • 'number'. The number type. We only have one type of numbers, no floats and fixed. Ex. print(21+21)
    • 'table'. The table type. It can be used both as an array or a key-value table (hashmap)
    • 'function'. The function objects we talk about in this post. Ex. function(x) return x+x end
    • 'userdata'. Data that is not defined in Lua but typical comes from C/C++ code.
      •  The HC3 implementation of the 'class' function we will talk about later is defined in C/C++ and the
      •  'class object' it creates are of type "userdata". We will see more about that later.
      • Ex. print(type(QuickApp)) will print 'userdata'
    • 'thread'. Only used for Lua's coroutines but we are not allowed to use them on the HC3.
    • 'nil'. The "nothing". 
      • To test if we get nothing back from a function; 'if test(7)==nil then ... end'. 
      • To clear a variable 'test = nil'. If 'test' is the defined function in the example above, we just "deleted" the function.
      •  'nil' is actually an interesting data type whose sole purpose is "to be different from all other Lua values"

        

    So, functions are like any other data type (number, string etc) and we can assign them to variables. That's how we get named functions, and we can pass them as arguments.

    Please login or register to see this code.

     We define a function 'test', that takes a parameter 'f' as argument. 'test' then assumes that 'f' is bound/assigned to a function object and applies (calls) that function with the argument 21 and then adds 11 and returns the result. 

    In the next line we call 'test' with an "anonymous" function (function object) that adds its arguments together.

    'test' will apply the function to 21 and add 11 and return it. The result is 11+21+21 = 53

    If we by accident call 'test(21)' it will crash as it tries to use the number 21 as a function.

     

    We could also have done

    Please login or register to see this code.

    We set the variable 'myFun' to the function and give 'myFun' as the argument. We achieve the same thing but in our case it's unnecessary to assign 'myFun' as an intermediary step. 

     

    So, the takeaway so far is that functions in Lua are a data type in itself. Functions are values that we can pass around as arguments or assign to variables.

     

    Another thing that's concerns Lua functions is that we can define functions that take a variable number of arguments, and they can return multiple arguments.

    Normally we define a function

    Please login or register to see this code.

    It takes two arguments and sum them.

    What if we want to make a function that can take any number of arguments and sum them?

    We could pass all our arguments in an array to the function

    Please login or register to see this code.

    Note. It turns out that if you pass a table to a Lua function you can omit the surrounding parentheses.

    Please login or register to see this code.

    This style is more common when we have a "named parameters" style of programming

    Please login or register to see this code.

    Will print "My name is Bob and I'm a dog"

     

    However, we can make it a bit easier for the caller

    Please login or register to see this code.

    The '...' in the argument binds to all arguments the function is called with (we can also do function(x,y,...) and it binds the '...' to all the  arguments after x and y)

    To make an array of that 'multiple value' value we put them in a table {...}, and then we can treat is as a table and loop over it.

    A function can also return multiple values.

    Please login or register to see this code.

    will assign a=42 and b=17

    Assume that we do

    Please login or register to see this code.

    What will it print? Well, our test function sends 2 arguments to sum that will sum them, printing 59.

    This is a bit beyond what we need to understand for getting into QuickApps and objects so lets stop here.

     

    One more thing, before we can tackle 'classes'...

    What is global and local variables in Lua?

     

    Global variables in Lua are variables that are defined in the "global context". Lua keeps a table with all globally defined variables and their values.

    When we do

    Please login or register to see this code.

    Lua's "global context" table is updated to remember that the symbol "test" has the number value '42'

    Please login or register to see this code.

    ...and Lua's "global context" table is updated to remember that the symbol "test" now has the function value 'function(x) return x+x end'

     

    It turns out that we can access this "context table". It's a Lua table and its name is '_G'

    Please login or register to see this code.

    will print 42.

     

    There is only one '_G' table and it's our "global context" e.g. our global values.

    However, we always have a local context also.

    If we do 

    Please login or register to see this code.

    We will find 'test2' in _G, but we will not find 'test1'. 

    'test1' is added to our current local context. There is no Lua table, like _G that we easily can access to see what local variables we have defined. In standard Lua there is a 'debug' function that allows us to access the local variables but not on the HC3.

    We also say that local variables belong to the current 'scope'

    When we start we have a global scope, like for 'test1' in the above example.

    When we enter a function, like the previous example with the function that defined a local variable 'y', we get a new scope for that function. All local variables defined inside the function will belong to that functions scope and will be removed when we exit the function. You can think of the parameter variables to the function as local variables to the function.

    Local scopes are created for Lua block constructs also:

    'do .. end'

    'if then ... else ... end' 

    'repeat ... until ...'

    'while .. do .. end'

    Ex.

    Please login or register to see this code.

    Here we also see that local variables can 'shadow' other variables. The global variable x=6 is shadowed by the local x=3 that is printed out inside the 'do -- end' block.

    After the block we print out x and it's 6 again. Globals can't shadow each other. They just replace each other’s' values.

    In the example in the beginning we had

    Please login or register to see this code.

    The parameter 'x' in 'test' is a local variable. The 'x' that we assign the result of 'test(21)' is a global variable.

     

    Ok, assume that you have two functions that need to call each other

    Please login or register to see this code.

    It's a strange example but it's just for illustration. We call 'foo' with 0 and it will call 'bar' that calls 'foo' with 42 this time that will be printed out.

    The point is that they call each other.

    This example work because they are both globally defined functions.

    When 'foo' is defined, it "compiles" the function. The call to 'bar' is assumed to be a globally defined function because it can't find 'bar' in the local scope.

     

    In effect the function 'foo' looks like (more on table syntax/access later in the post)

    Please login or register to see this code.

     Yes, 'print' is just another defined Lua function available in the global context. All built-in Lua functions are also available there.

    However, if the 'compiler' finds a locally defined function with name 'bar' it will become

    Please login or register to see this code.

    There is no table '_LOCAL_CONTEXT' in Lua but for illustration this is equivalent.

    It's important to understand that this decision, if 'bar' is global or local, is done when the function is defined/compiled. Lua could have chosen another approach. It could have compiled the code to first look if the function was local and if not look in the global context.

    Please login or register to see this code.

    But it doesn't.

    When is this important? It is important when our functions are locally defined

    Please login or register to see this code.

    This will crash and say that 'bar' is undefined. Actually "attempt to call global 'bar' (a nil value)"

    The reason is that when 'foo' is defined, no local variable 'bar' exists yet (first when we come to the next line) and 'bar' is assumed to be global. But the global 'bar' doesn't exist either.

     

    Problem: How can we fix this?

    Solution:

    Please login or register to see this code.

     This is the same as if we had written

    Please login or register to see this code.

     

    and because we declare the local 'bar' before we define our local function 'foo' the call to 'bar' will be to the local variable 'bar'. Because here exist a local variable 'bar' even though its value is nil.

    We in the next step define 'bar' to be our function, and in the step after that call 'foo'. Now when the function executes, 'bar' is assigned a function value and it works.

     

    This is often called to 'forward declare' a variable. We just declare the variable to be local but don't give it a value yet. Code that comes afterwards will know that this is a local variable, and not a global variable, and treat it accordingly. Later we assign the local the value it should have and then the code can run properly.

     

    Takeaways so far

    1. Global and local variables
    2. The global context (_G) that our global variables are kept in
    3. Local scopes that our local variables are assigned to.
    4. Local scopes can be nested, from out to inner. We start with a global scopes and nested blocks or function calls creates inner scopes.
    5. Variables in inner scopes shadows variables with same name in out scopes.

     

    Because named functions are just function values assigned to variables, we can use this to "re-define" built-in functions.

    There is a function 'tostring' that takes any value and creates a string of it. Ex. tostring(42) return "42".

    Please login or register to see this code.

     This will print "V:42". It turns out that the built-in function print uses the builtin-in function 'tostring' to convert its values to strings so they can be printed out. Now it will use our redefined 'tostring' that adds "V:" in front of the value that the old 'tostring' returns.

    This example may not be so useful. 

    On the HC3 we have a function 'json.encode' that converts a Lua table to a json string.

    Please login or register to see this code.

    Will print "{"a":8}" instead of the not so helpful "table: 0x7676876" we get with the original print function.

     

    Side note. If you define a function that uses tostring and you don't want anyone to redefine tostring for you and thus risking that your function would not work as expected you can do
     

    Please login or register to see this code.

    we save-away the definition of tostring in a local variable that we use. If someone later redefines tostring it doesn't matter as we have the original definition. This is a very common technique in Lua libraries that uses a built-in functions and don't want users to accidentally redefine them in an incompatible way...

     

    Now we will do some more magic.

    Please login or register to see this code.

    This will print 50. It adds its parameter y=8 with the global variable x=42. No surprise.

    Please login or register to see this code.

     Will also print 50. We know that the local 'x' inside the block is a local variable and not to be mixed up with the global 'x' that we assign the value 55 after the block.

    However, 'foo' seems to keep the local 'x' value around so it can print 50.

    We have said that when a local scope exits - when we exit the 'do ...end' block above, local variables are destroyed. That is not the whole truth.

    In fact, local variables that are referenced within function will survive.

    Please login or register to see this code.

     This will print 50, and then print 18

    In our case, the function 'getXplusY' and the function 'setX' shares a local variable 'x'. When 'setX' changes 'x' our function 'getXplusY' will use the new value of the local 'x'

     

    This shared variable 'x' is something that no other function then 'setX' and 'getXplusY' can access. It's kind of a private shared variable.

    If we had used a global variable any other part of the code could have changed the value of 'x'

     

    We said that functions were just like any other value and could be passed around - which means that they can be returned from other functions.

    Please login or register to see this code.

     This will print 50. 

    Our function 'makeAdder' takes a parameter 'x' (a local variable) and returns a function that takes a parameter 'y’ and adds 'x' to it.

    The function we return will keep a reference to the local variable 'x' that is assigned 42 when it is called in our example.

    Here we have a function that creates other functions, functions that add a constant value to its argument. There is no way to change 'x' after we have returned the function. The next call to 'makeAdder' it will be a new local variable 'x' that is assigned the value we pass.

     

    A function that besides its code (the "body" of the function) also remembers the local variables available when it was created is usually called a "closure" - a "package" of function and associated variables. It's quite common in programming languages.

     

    If you have kept up this far you have a pretty good grasp of functions in Lua.

     

    Now let's do some object-oriented programming.

    Lua doesn't really have object-orientation (OO) built-in, but there are some support for rolling our own OO model.

    We will not dive too deep into the semantics of OO but let’s say that an object is an encapsulation of data with associated functions (or methods).

     

    We can make a Lua table

    Please login or register to see this code.

    The variable 'test' is assigned a key-value table, where the key 'a' is associated to the value 42 and the key 'y' is associated to the value 17.

     

    It's important to understand that the table is created/constructed with this statement.

    Please login or register to see this code.

    So, we can do this

    Please login or register to see this code.

    and it will assign 'test2' the table {['ab'] = 70}

     

    We can access the previous table values like this

    Please login or register to see this code.

    This will print 42 and then print 17

    So, we have <table>[<key>] to access a key value.

    If the key is also a valid representation of a variable name (well, type) , we can also use the syntax

    Please login or register to see this code.

    So, we have the equivalent <table>.<key> to access a key value.

    When doesn't it work?

    Please login or register to see this code.

    We can't do

    Please login or register to see this code.

    instead we need to do test['a b'] to access the key.

    This is also needed if you want to have keys that uses characters not allowed keys, like from a local language. For Swedish I need to do 

    Please login or register to see this code.

    I can't do

    Please login or register to see this code.

     

    If the keys are valid string constants (think variable names) we can also define a table like this

    Please login or register to see this code.

    and we can assign new value to keys

    Please login or register to see this code.

    If a key doesn't exist, it is created, like 'c' in the example above.

    Tables can contain tables

    Please login or register to see this code.

    will print 8

     

    A special case of key-value tables are tables with consecutive number keys.

    Please login or register to see this code.

    We can access them as 

    Please login or register to see this code.

    but not as test.2

    We can also omit the keys

    Please login or register to see this code.

    and they will be stored at key/index 1,2,3. Note that Lua starts its index with 1.

    So, arrays are a special case of key-value tables in Lua. As long as the table looks like values stored with key 1,2 and upwards, Lua is smart and stores it as a simple array internally, however, if you make huge leaps in the array, last value stored at 10, and then you store the next value at 1000, it will convert the array to a key-value table. Also, if you start to use non-number keys. For our OO-leasson arrays are not so interesting as it will be mostly about key-value tables.

     

    Ok, so we have a way to "encapsulate" data/values - we can use a table.

    Please login or register to see this code.

    will print 42 and 43 respectively. We have 2 objects, test1 and test2, that encapsulate their values.

    Now we need to associate functions to our objects.

    We know that functions are just values that we can assign variables. Well, we can assign them to keys in a table too, like we assign the number values in our example above.

    Please login or register to see this code.

    This will print 59 and then 61.

    We assign the key 'f' our functions. The functions gets the 'a' key and the 'y' key from their tables and returns the result.

    We then access the key 'f' and call it for each table. 

    One thing here is problematic, inside the 'f' functions we need to reference the table. ex. test1.a. This works because the tables 'test1' and 'test2' are global variables so when 'f' is executed we get the table value.

    However, note that the two 'f' functions need to reference 'test1' and 'test2' respectively.

    It's a pity because the 'f' functions are otherwise identical. It would be nice if we only needed to define the 'f' function once.

    Please login or register to see this code.

     Here we define a local function 'f' outside, and let it have a table as argument, it access the 'a' and the 'y' key of the table given and return the sum.

     

    In our "object" tables, 'test1' and 'test2' we assign they key 'f' the function 'f' (one is a key and the other is a variable)

    Now when we call test1.f, but we need to send the 'test1' as the argument so that the function 'f' knows what table to get the keys from.

     

    Lua has a function syntax when calling functions stored in tables that makes this easier for us.

    Please login or register to see this code.

    is the same as test1.f(test1)

    We can now do

    Please login or register to see this code.

    The 'f' function will get the same table that it is stored in as the first argument. In that table is the other key values associated with our object and it can access it.

     

    Normally we name this first parameter 'self'.

    Please login or register to see this code.

     If we only needed one object, test1, we could have defined 'f' like this

    Please login or register to see this code.

     We can access the 'f' key as test1.f, and we thus can use that as the name for the function definitions because it translates to

    Please login or register to see this code.

     In fact, we can define functions with the ':' syntax too.

    Please login or register to see this code.

    Here we see why we call it 'self'. With this syntax we can't choose our own name for the first parameter, Lua name it 'self' for us.

    If we have more parameters?

    Please login or register to see this code.

    This is the same as

    Please login or register to see this code.

     

    We would of course like to have a function that creates object of a specific sort for us. 

    That function that defines/creates objects for us we call a 'class' - and surprise, it's a function.

    We can think of the class as a template for the type of objects we later want to create.

    So the function that creates classes for us we will call 'createClass' and it takes a table that is going to serve as the template for the objects that it is going to create later.

    We will also have a 'createObject' function that creates new objects by creating copies of the class template.

    Please login or register to see this code.

     Lets define the functions 'createClass' and 'createObject'

    Please login or register to see this code.

    That was simple. Our class just return the template table

    Please login or register to see this code.

    'createObject' copies the class template, and adds parameters we give it. Usually we like to create the same type of objects but initialise them with different values. Like dogs with different names...

     

    Please login or register to see this code.

    myClass will be assigned the table

    Please login or register to see this code.

    Lets add a function method to the class

    Please login or register to see this code.

    Remember this was the same as

    Please login or register to see this code.

    So this will add another key to our class table template

    Please login or register to see this code.

    Remember that ':' defined functions with a 'self' parameter first.

    Please login or register to see this code.

    The 'createObject' will copy the class table and return a new table, but with the added extra key-values. If we give it {a = 10} we get our own 'a' value.

    Please login or register to see this code.

    We have a new object that looks like the class, it has the same values and functions, except that a=10.

    Please login or register to see this code.

    will call a1.test(a1) and it will print 27.

    Please login or register to see this code.

    will print 28

     

    We have a class 'myClass' and we can create objects that uses that class as a template to have similar values (keys) and functions.

    We can then create any number of objects of that class, we create two in our case; a1 and a2.

    Nice.

     

    Takeaways:

    1. We can do object-oriented programming with Lua
    2. Objects are tables with values and functions.
    3. Object functions (or methods) are passed the object table as its first argument in the self parameter when called with the ':' syntax.

     

    In real object-oriented programming you typically want classes that inherits from other classes. But we will wait with that.

     

    We are finally at the point where we can talk about QuickApps(!)

     

    When the HC3 starts up your QuickApp it looks like this

    Please login or register to see this code.

    It defines the QuickApp class. It then lets you add new functions to the class.

    When you are finished it will create the QuickApp object and call your :onInit() method - if you have defined it,

     

    To be honest, we don't have the 'createClass' and 'createObject' functions. Instead we have a builtin function named 'class'

     It has a slightly different syntax, but achieves more or less the same, but we will dive into it in the next post.

    Please login or register to see this code.

     

    Our 'createClass' used ordinary Lua tables. The builtin function protects the table that is returned and shows it as of type 'userdata'

    This means that we can still access the fields in the QuickApp as normal keys

    Please login or register to see this code.

    However, we can't loop over the keys like we can with a table or convert it to a json string.

    So, we can't peek inside the object - we have to poke and guess.

     

    This was a long post but it sets up the base for the next post where we will dive deeper into the QuickApp class and object to understand how it works, and how stuff like QuickAppChildren are implemented...

     

    Edited by jgab
    Added variable number of arguments and "named parameter" style
    • Like 11
    • Thanks 5
    Link to comment
    Share on other sites

    Thank you Jan @jgab,

     

    this is almost complete LUA course :-D, thank you very much!
    Keep up that excellent work.

    Edited by Bodyart
    Link to comment
    Share on other sites

  • Topic Author
  • The anatomy of QuickApps – Part 2

    (

    Please login or register to see this link.

    )

     

    Disclaimer1: We are now venturing into undocumented land. That means that Fibaro is free to change how things work at any time. Well, Fibaro is free to change how documented things work too... ;-)

    Disclaimer2: I'm explaining how I believe some QuickApp functionality is implemented by Fibaro. This is most likely not exactly how Fibaro have implemeneted it. However, the intention is that it should mimic the observable behaviour correctly. If I'm wrong, please correct me.

     

    (Please also have a look at

    Please login or register to see this link.

    to get some better background)

     

    Let’s recap from previous post.

    • Classes are templates for how to create objects.
    • Objects are implemented as Lua tables, with key-value pairs. Some keys represent values and some keys represent functions (or 'methods' as OO-people like to call them - I will call them both 'functions' and 'methods').
    • 'QuickApp' is a class provided by Fibaro that we use to implement QA devices.
    • We extend the 'QuickApp' with our own method definitions, like a start function 'QuickApp:onInit()' and device actions like 'QuickApp:turnOn()'.
    • Fibaro loads and run our QA code that makes these definitions
    • After our QA code have run, Fibaro creates an object of the 'QuickApp' class and calls the 'QuickApp:onInit()' method, if it exists. So, it doesn't matter where in the code 'onInit' is defined, it is always called after the code has been run through (loaded).

     

    Just to prove that QuickApp is a real class...

    Please login or register to see this code.

    Unfortunately, Fibaro's implementation of the 'class' function makes the object a "userdata" object that we can't easily peak into. However, behind the scene it's a Lua table in all important meanings as we can access the fields of the object like it was a Lua table. (

    Please login or register to see this link.

    )

     

    So, it's not only methods we can add to the QuickApp class, we can add fields/vars too. It may be clearer to define and document your "QuickApp" fields in this way instead of just create them by ex. self.myField = "Hello" inside onInit(). Well, it's a matter of taste.

    You don't have access to the QuickApp object then so you can't use self:<methods>

     

    When we refer to the "QA code", we mean the code you see in the QA editor when you edit your QA. As we said in the past, Lua runs code from top to bottom.

    The main intention with that code is to allow us to extend the 'QuickApp' class with our own methods, and after the code has been loaded (run), Fibaro creates the QuickApp object and calls our QuickApp:onInit() where we can initialize our app and get going.

    However, we don't need to define an ‘onInit’ function. We can initialize our own global and local variables in the code. We just need to be a bit careful in what order as we saw in the previous post.

    However, it's a good practice to do your main intialization from QuickApp:onInit()

    • When 'onInit' is called, we know that our code has loaded and the QuickApp object is created
    • We can access the 'self' variable for the first time inside QuickApp:onInit() that is the actual QuickApp object with all the methods that interacts with the QA, like self:getVariable(varName) etc.
    • We know that the variable 'self' is actually handed to us as the first invisible parameter of QuickApp:onInit(). The ':' notion when defining a function adds that invisible first 'self' argument.

     

    Nothing stops us from doing

    Please login or register to see this code.

     Now the global 'myQuickApp' is assigned the QuickApp object and we can use that to access all the functions (and fields) of the QuickApp. Like from the local function 'myPrint' in the example.

    A word of caution: We can't use 'myPrint' until 'onInit()' has run and setup the variable. Before that 'myQuickApp' is nil.

     

    Fibaro actually assign the QuickApp object to a global variable, 'quickApp' that we can access in our code. However it doesn't get assigned until we exit :onInit() 

     

    Try this

    Please login or register to see this code.

    You get an "attempt to index a nil value (global 'quickApp')" error.

     If you do this

    Please login or register to see this code.

    it will print "OK"

    The reason is that when setTimeout calls our function 'foo' we have returned from the :onInit() function and the variable 'quickApp' has been assigned the QuickApp object that was created.

     

    The most common methods that are predefined for the QuickApp class are:

    • function QuickApp:trace(...) 
    • function QuickApp:warning(...) 
    • function QuickApp:error(...) 
    • function QuickApp:getVariable(varName)
    • function QuickApp:setVariable(varName,value) end 
    • function QuickApp:updateProperty(property,value)
    • function QuickApp:updateView(element,type,value)  
    • function QuickApp:createChildDevice(properties,constructor) 
    • function QuickApp:removeChildDevice(id)
    • function QuickApp:initChildDevices(map)

    There are some more but we will talk about them in the next post.

     

    But what fields (keys) does the QuickApp object have from start? Well, quite a lot it turns out.

     

    First of all, devices are represented on the HC3 (and the HC2) as a Lua table structure. We can access that table with an api call. (Now we are coming to the "anatomy part")

    Please login or register to see this code.

    (Note, go the the "Swagger" page of your HC3. It's accessible with the button icon in the lower left of the Web UI, button with a "{...}". There you can see all the api calls possible and even try them out)

     

    The api.get call will return the table for device with deviceId 78. The table contains the key 'id' that is the deviceId of the device and that is printed out.

     

    All types of devices have a table definition, z-wave devices, plugins, and QuickApp devices

    A typical QuickApp definition looks like below. Not all fields are included, and some fields are only available in certain types of devices. Also, fields like "properties" can vary between type of devices.

     

    Please login or register to see this code.

    Update: The code is not stored in the device struct anymore. Now the code is divided up into files associated with the QuickApp. See <

    Please login or register to see this link.

    >

     

    You can update most of this struct with api calls. 

    Please login or register to see this code.

    updates the fields provided in the <device table>

    Please login or register to see this code.

    updates the property value of device 78 to false

    Please login or register to see this code.

    disables device 78.

    api.* calls can also be done from Scenes.

     

    Anyway, here we are focusing on QuickApps, so have this structure in mind.

     

    It turns out that we have several predefined classes available in the QuickApp Lua environment

    There is a 'Device' class that seems to be the mother of all the device classes. Exactly what that class' responsibility is is still to be determined.

    Then there is a 'QuickAppBase'. We assume that it is the base class for 'QuickApp' and 'QuickAppChild' that are the two types of classes that we normally encounter. We never create a 'QuickApp' object. Fibaro does that for us when our QuickApp device starts. (Later we will create 'QuickAppChild' objects).

     

    Anyway, when the QuickApp object is created it is given the device definition table, like the one above.

    If you remember the last example in the previous post:

     

    Please login or register to see this code.

    This is most likely not exactly how it is implemented, but the net result is the same.

     

    What does the QuickApp class do with deviceTable it gets as an argument? It turns out that it adds the values to the new QuickApp object created. That is why we can do:

    Please login or register to see this code.

    Accessing some of the fields from the deviceTable with the 'self' variable.

     

    We will have reasons to come back to these fields and how we can use them, but let's go through the methods available one by one.

     

    function QuickApp:getVariable(varName)

    function QuickApp:setVariable(varName,value)

     

    The function self:getVariable(varName) is implemented in the way that it looks through the quickApp variable list in the properties field of the device table.

    The quickAppVariables list is defined as

    Please login or register to see this code.

    and the function looks something like

    Please login or register to see this code.

     Here some issues with quickAppVariables can be seen. First it's a list that we have to search through, which means that the more variables we add the longer time it takes to find a variable. Why they didn't use the key-value feature of Lua tables is hard to understand. Then a variable could have been looked up with a single table access.

    Secondly it returns "", the empty string if the variable is not found.

    It is probably to try to be helpful, and the fact that setting a variable that has not been defined automatically creates it. 

    However, this gives us no chance to know if the user has not defined the variable at all, or if the user has set the variable to "".

    The proper Lua way would have been to return nil. That's why we have the 'nil' value. Remember, "the value different from all other values".  Of course, we can look directly into the list, bypassing self:getVariable... Not a deal breaker but a "non-Luaism"

     

    Just to give an example. Assume we want to provide a default value for a field and allow the user to override it with a value from a quickAppVariable

    Please login or register to see this code.

     If self:getVariable("myVar") had returned nil instead of "" we could have written it

    Please login or register to see this code.

    Another thing to be aware of. When users add quickAppVariables with the web UI for your QA, the values are stored as strings.

    However, you can add any type from your code with self:setVariable, ex. 

    Please login or register to see this code.

    and it is stored and retrieved as a table

     

    function QuickApp:updateProperty(propertyName,value)

     

    We can access the properties of the QuickApp, ex. the 'value' property with 

    self.properties.value

    What happens if we update the value?

    Please login or register to see this code.

    Well, the value gets updated.

    However, here is why we should treat the values we get with self.* as "read-only" values; if we update the values directly they will not be saved back into the device table definition - they will be lost when the QA restart. Secondly. when some properties are updated an event/trigger will be posted so that other components, like scenes, will be aware that the QA has updated its values.

     

    That is why we have

    self:updateProperty(<property>,<value>)

    To really update the value property of a QA do

    Please login or register to see this code.

    It will update self.property.value and it will persist it in the device table struct and it will send an event/trigger for some properties. Not all, but 'value' is one of those.

    Also, when you do self:setVariable(<varName>,<value>), it updates the self.properties.quickAppVariables list, similar to our setVariable version above. It also fires an event/trigger that the QA's quickAppVariables property have changed. And it sends the whole quickAppVariable list as value of the event, even if we just changed one quickApp variable. I'm unsure how costly this is, but it's not for free (and I'm always reminded how Fibaro encourage us to be write resource efficient code...)

     

    It turns out that when we modify a QA, it sends out a trigger/event with, yes you guessed it

    ...it sends a 'DevicePropertyUpdatedEvent' with the whole chunk of code in the 'mainFunction' property.

    We appreciate that we get a DeviceChangedEvent, but maybe posting the whole chunk of code is not optimal. I have a QA with 3000 lines of code...

    Exactly how it's implemented we need to guess. But most like when you change the code of the QA and save it does

    Please login or register to see this code.

    and when you do a self:setVariable(varName,value) it does a

    Please login or register to see this code.

    It's good, we get a trigger in the system...

     

    You can update other fields in the deviceTable structure too, besides the properties and quickAppVariables. Be aware that when you do it the QA is often restarted.

    The is the api.put("/devices/"..deviceId",<table>) function that we can use. You want to disable the QA 88?

    Please login or register to see this code.

     Takeaways.

    • We get all the device table definitions as key values in our QuickApps self variable.
    • Be aware of what you store in your self variable (the names you choose) as it can clash with some device table values.
    • Be aware that if you update the values in self.* they are not persisted, unless you use the update functions like self:updateProperty and self:setVariable
    • When fields defining the QA is updated with api.put the QA is restarted. When properties are updated with self:updateProperty/SetVariable we seem to avoid that. There are exceptions like updating the 'vewLayout' property etc.

     

    function QuickApp:updateView(element,type,value)  

     

    This function updates the UI elements that we have defined for our QuickApp. The buttons, labels, and sliders.

    The definition of the buttons etc are stored in the QA property 'viewLayout', and normally when you update text of elements it will reflect that in the 'viewLayout' structure.

    self:updateView("myButton","text","New text for this button")

    Here we update the text of the button in the UI. At the moment (5.030.45) there is an issue with values and how they get reflected in the 'viewLayout' property.

    How is updateView implemented we don't know, but we can achieve the same by calling 

    Please login or register to see this code.

    If this is what makes the work or if it calls the device updateView we don't know. 

    Be aware that value needs to be a string, or the value doesn't seem to be updated. This is especially annoying when updating the slider value

    Please login or register to see this code.

    Also, how do you access the values of the elements?

    Well, buttons and labels don't have values besides their names and the text on them. Sliders also have a slider value.

    We will talk more about the UI/action model in another post but when you click a button you get a callback with an event.

    Remember the 'uiCallbacks' table in the deviceTable in the beginning?

    That defines the name of the function that will be called when you interact with a button and a label

     

    A button event for a QA 78 with a button named "button1" looks like

    Please login or register to see this code.

    A slider event look likes (slider with name "slider" and the current value is 39)

    Please login or register to see this code.

    You catch them with defining QuickApp methods for

    Please login or register to see this code.

    In our case

    Please login or register to see this code.

    Sliders have some issues with keeping the value. I have as a practice to always update the value in my slider functions

    Ex.

    Please login or register to see this code.

    It seems to help.

    Outside a slider method, how do you access the value? And how do you access the text of a button, or label?

    Turns out there is no easy function for that. We can look through the 'viewLayout' table structure to do it.

    Here is the code if you need to lookup a property of a UI element. This files under the section "just do like this...",

    Please login or register to see this code.

    We will go into what happens with the UI a bit more in our next post.

     

    function QuickApp:debug(...)

    function QuickApp:trace(...) -- Same as :debug but with another tag

    function QuickApp:warning(...) -- Same as :debug but with another tag

    function QuickApp:error(...) -- Same as :debug but with another tag

     

    These seems to be exactly like the fibaro.debug(tag,...) and friends.

    The difference is that you don't need to provide tha tag. In fact, the tag comes from a global variable

    __TAG that seems to be set to "QuickApp"..self.id as default.

    You can set __TAG to whatever you like, and it will be the prefix for the log printouts.

    Probably debug is implemented as

    Please login or register to see this code.

     The '...' is a Lua construct that is bound to all arguments passed to the function.

    This is why debug can take a variable number of arguments to print out.

    To create a table out of '...' do {...}

    Then to create a long string of all the elements on args we can do

    local str = table.concat({...})

    table.concat is a builtin Lua function that does 'tostring' on all values in the table and concatenates them together

    Then we print out the string with fibaro.debug and provide the __TAG value.

     

    We will wait with the QuickAppChild functions to another post. Let us instead focus a bit more on the provided QuickApp methods and the ones that we can add to the class.

     

    It turns out that all methods of our QuickApp class can be called with

     

    fibaro.call(<QuickApp deviceId>,<method name>,<method arguments>,...)

     

    Ex. to set a quickAppVariable "Test" to 77 of another QA, ex with id 55 we can do

    Please login or register to see this code.

     This means that all methods you extend the QuickApp class with become publicly accessible from other QAs. In fact, they also become available for external system through the REST web api that the HC3 has.

    Please login or register to see this code.

    The external api is protected with the passwords you set so that may be ok.

    However, it can be wise to define some of the logic of your QuickApp outside the QuickApp class.

    Please login or register to see this code.

     

    Here loop is a function outside the QuickApp class. No reason why we should expose it to other QAs

    We could avoid the local quickApp variable too if we wanted

    Please login or register to see this code.

     What is better looking is a matter of taste again. However, if you have a lot of code it can be tedious and error prone to always remember passing the self variable around. And to be realistic, there is only one QuickApp object so why not have it in a local/global variable? 

     

    If we have defined

    Please login or register to see this code.

    and we do

    Please login or register to see this code.

    it's just a simple function call to our QuickApp object.

    Remember?

    Please login or register to see this code.

     However, when we do

    Please login or register to see this code.

    It's more complicated. 

     

    It turns on that there is only one person working in each QuickApp we have (we say that the QAs are single-threaded).

    Let’s called it the "Operator"

     

    Assume that we are QA 55 and we call another QA, 77 in this case.

    Operator-55 (we) do a fibaro.call(77,"turnOn") and wait for the other side to acknowledge the call.

    Operator-77 will get an incoming request in its "mailbox"

    -"someone wants to call your function 'turnOn' with zero arguments, please do"

    ...and Operator-77 needs to lookup if it has a method named 'turnOn' and in this case call it.

     

    We know it's easy for Operator-77 to check if he has the method defined. It's just to see if the key 'turnOn' exists in his QuickApp object

    Please login or register to see this code.

     

    Operator-77 calls the function and acknowledge back to Operator-55 that the action was completed.

    That works quite well - but is more complicated than just a Lua function call.

     

    What happens if Operator-55 does a 

    Please login or register to see this code.

    i.e. call himself? 

    To make it short, while Operator-55 waits for an acknowledge that the operation has been carried out, he will not have time to look in the "mailbox" to see if he has gotten any request to call "turnOn". Remember it's only one person and probably a man as he can only do one thing at the time. It becomes a "dead-lock".

    ..or at least it used to become up until firmware 5.031.33 .

    There Fibaro introduced fibaro.calls to be asynchronous by default.

    There is a function

    Please login or register to see this code.

    that decides if calls should be carried out asynchronous or not.

    What does it mean? It means that the Operator-55 will make his call but don't wait for the result. Instead it will take a loop and check the mailbox and see that there is a request.

     

    If we just for the example does a fibaro.useAsyncHandler(false)

    we will see a different behaviour


    Try this

    Please login or register to see this code.

    It will take 5+ seconds between the two log print outs.

    Fortunately is seems like Fibaro has a timeout of a few seconds, that breaks the wait for an acknowledge" and allows the Operator to look in the mailbox...

     

    Note here that fibaro.call doesn't return a value (not even error messages) so in principle it shouldn't need to wait for the "acknowledge" from the QA being called. So, the async behaviour just skips waiting for the acknowledge and let the Operator continue with his other tasks (like discovering that there were a message in the mailbox requesting a "turnOn" call). So, the "acknowledge" has no practical purpose currently - at least not for us coders.

    In the future, Fibaro may decide to return error messages or even return values, and then it becomes valuable.

    Note 2, if you call the method with the REST api outlined above you do get error messages if the deviceID or method doesn't exist, so that's a bit more helpful.

     

    Ok, we end here. Next post will go deeper into the mechanism behind calling QuickApp methods.

    Edited by jgab
    • Like 5
    • Thanks 4
    Link to comment
    Share on other sites

    Good mornin @jgab,

    Thank you very much for the clear written first part.

    After reading it I discoveredthat I need to read it a few times more to let it stay between my gray cells.

    It cleared a few things and other are still "misty".

    Let me now start part  two.

    //Sjakie

    Link to comment
    Share on other sites

  • Topic Author
  • In the QuickApp environment we have the 'class' function that defines Lua classes and is used to define things like the QuickApp class, and we use it when we define our own QuickAppChild classes.

    The 'class' implementation we get comes from the luabind library in C/C++ that is used to start our Scene and QA code by the HC3.

    The 'class' function also creates objects of type "userdata" that we can't peak into. Thats why we can't easily discover what fields and methods the QuickApp class have - beyond what is documented - and that may be the reason.

    Anyway, for completeness, this is a Lua version I created to mimic the luabind class function that I use in the fibaroapiHC3.lua sdk.

    Please login or register to see this code.

    It does the same "copy parents fields" as we see in luabinds class.

    This means that this doesn't work

    Please login or register to see this code.

    Class 'B' copies whatever methods class 'A' has when class 'B' is defined. In this case class 'A' has not yet defined the test method, and thus 'B' will not inherit it.

    Correct way

    Please login or register to see this code.

     

     

     

     

    Link to comment
    Share on other sites

  • Topic Author
  • 1 hour ago, Sjakie said:

    Good mornin @jgab,

    Thank you very much for the clear written first part.

    After reading it I discoveredthat I need to read it a few times more to let it stay between my gray cells.

    It cleared a few things and other are still "misty".

    Let me now start part  two.

    //Sjakie

    Please feel free to comment on topics you feel could be clearer or that you are missing and I will do my best to improve the text.

    Link to comment
    Share on other sites

    Hello @jgab

    Thank you so much for your wonderful description.

    It helps in understanding the HC3 possibilities.

    You are writing one of your next posts will be about QuickAppChild.
    In this regard, I would like to know how to specify which UI fields my child devices should have.
    Example: Multilevel Switch has Power Switch Off / On and Brightness.
    I want a UI with 3 state: Off / Auto / On and a label

    //Allan

    Link to comment
    Share on other sites

  • Topic Author
  • 1 minute ago, [email protected] said:

    Hello @jgab
    Thank you so much for your wonderful description.
    It helps in understanding the HC3 possibilities.

    You are writing one of your next posts will be about QuickAppChild.
    In this regard, I would like to know how to specify which UI fields my child devices should have.
    Example: Multilevel Switch has Power Switch Off / On and Brightness.
    I want a UI with 3 state: Off / Auto / On and a label
    //Allan

    Yes, I will write a post on using the QuickAppChild class.

    However, the answer to your question - QuickAppChild's come with "predefined" UIs that are not defined the regular way. They don't use the regular 'viewLayout' structure that QuickApps use for user defined UI elements. There may be possibilities to "hack" the viewXML struct that they use but it's really difficult as there is no documentation or examples to draw from. 

    With some luck Fibaro may document the layout format for plugins - the UI element available for plugins/children are prettier than the one we have for user defined elements, imho.

    Link to comment
    Share on other sites

    Thanks for your reply.
    I'm still looking forward to your next post.
    I will see if I can achieve the same functionality by dynamically changing the layout of the QuickApp parent.

    Link to comment
    Share on other sites

  • Topic Author
  • The anatomy of QuickApps – Part 3

    (

    Please login or register to see this link.

    ,

    Please login or register to see this link.

    )

     

    Short recap.

    In previous post we dived into the QuickApp class and its methods. Even though we couldn't peek into the object it's mainly a Lua table with fields and functions that we can extend. It looks something like this:

    Please login or register to see this code.

    (clarification, we don't get the id, name, and type fields until the QuickApp object is created)

     

    We can extend the class by adding functions like

    Please login or register to see this code.

    and it adds the field:

    Please login or register to see this code.

    ...to the class.

     

    When the code you have written is loaded, Fibaro will create an object (instance) of the QuickApp class, which mainly means making a copy of the class Lua table. It will then call the 'onInit' function of that object, if there is one.

    There is no more magic behind the scenes.

     

    The magic mostly is in the provided functions that manipulates the QuickApp device table structure that we could retrieve by doing api.get("/devices/78"). In the previous post we talked about self:updateProperty and self:setVariable as examples of such functions.

     

    We have said a couple of times that your code is loaded and then the 'onInit' method is called. 

    Let's be clear what that means

    Please login or register to see this code.

    This will print "Hello", because that statement is executed when the code is run. The next statement that is executed is the definition of the 'onInit' function, but nothing is printed - the function is just defined.

    Then, when your whole piece of code have run, then Fibaro creates the QuickApp object and calls your 'onInit' that prints "Good bye".

     

    This is a valid QuickApp:

    Please login or register to see this code.

    'setInterval' will call the function provided (remember, "anonymous" function) every minute and update a fibaro global to a string with the current time.

    There is no 'QuickApp:onInit' needed. In fact, there is no mentioning of 'QuickApp' anywhere. The reason is of course that we don't need the help of anything that we find in the QuickApp table (object) that we listed above. The HC3 will still load and run our QuickApp...

     

    Just one more thing. Can we add methods to the QuickApp object (self) after the object has been created?

    Please login or register to see this code.

    Yes, here we define a new function 'myNewMethod' on self, what it will do is:

    Please login or register to see this code.

    It means that we can decide when our QuickApp is running to add additional methods...

     

    End of recap.

     

    How do our QuickApp methods get called?

    When we call them within our code with self:<my method>(...) we know exactly what happens.

    We find the key <my method> in the self object and hope it's a function and apply it to the argument

    Please login or register to see this code.

     However, we could also call QuickApp methods in other QuickApp devices with:

    Please login or register to see this code.

    Here is the second piece of magic.

    In the previous post we tried to explain with the "Operator" analogy why it could create a dead-lock when we called ourselves with fibaro.call and that Fibaro introduced a new behaviour that makes fibaro.calls asynchronous by default.

    Please login or register to see this code.

    We will come back to this later in the post, but it mainly came down to men and their lack of simultaneous capacity :-)

     

    Assume that you have a QuickApp that only have one line of code

    Please login or register to see this code.

    There is no 'onInit', no 'setInterval' that continously do anything, nothing.

    Why doesn't the QuickApp just terminate when it's finished with the print statement. I mean, there is nothing else to do.

     

    It's actually a good question.

     

    However, even if your QuickApp has no Lua code, other QuickApp devices or Scenes can call you:

    Please login or register to see this code.

    ...and someone needs to be at home to answer that request - our "Operator" in the previous post.

     

    You can actually try that.

    If you call a QuickApp, say deviceId 78

    Please login or register to see this code.

    If we inspect QA 78 afterwards, we see that it got the variable "A" but not "B". No one was at home when we tried to set "B" because we had disabled the QuickApp. The Operator had left for a coffee.

     

    I think we are ready to "code" our Operator - the Operator is the "main loop" of our QA framework.

    Please login or register to see this code.

    The code is extremely simplified, but I believe it gets the main points.

     

    A couple of points may need explanation.

    The Lua code of the QuickApp is stored in the properties.mainFunction field of the device table we get when we read it in with api.get("/devices/78")

    It's just a plain Lua string with the code, ex "print(42)"

    'loadstring' (or just 'load' in Lua 5.3) is a built-in Lua function that takes a string and compiles it and load the code. It's a very useful function but unfortunately we don't have access to it in the QuickApp Lua environment.

    So, the code is loaded and the QuickApp object is created and the 'onInit' is called, if it exists.

     

    Then our Operator loop is started.

    It checks if there are any incoming request from other QAs, Scenes, buttons and sliders being pressed, or code within the system. Here we have the 'getNextRequest' function that fetches the next request or nil if there is none (I invented it for this example, but there is probably something similar).

    The Operator then checks if the requested method is part of the quickApp object we have created, and if so calls the method.

    Assume that the request looks like

    Please login or register to see this code.

    If quickApp['turnOn'] exists it will call that function

    Please login or register to see this code.

    Methods need the object itself as the first argument (the self variable) so it need to pass quickApp as the first argument

    Please login or register to see this code.

    table.unpack takes a table and "unpacks" it to multiple values that are added to the arguments that are passed to 'turnOn'. In our case, 'turnOn' doesn't take arguments so the table is empty.

    Please login or register to see this code.

    will print 6, as the 'rest' arguments will be passed to the 'b' and 'c' parameters respectively.

    It turns out there is a method available in the QuickApp class for this

    Please login or register to see this code.

    That does this – but now we know how to make this call ourselves :-)

     

    So, when it's ready with that it calls itself by doing a 

    Please login or register to see this code.

    and starts over and does the next check for incoming requests.

     

    Why 'setTimeout' and not just a "while true do end" loop?

     

    Well, it turns out that nothing runs in parallel inside a QuickApp.

    The 'setTimeout' function that is a built-in function (not part of standard Lua), adds the function to a list of functions that should be run in the future. The time is specified in milliseconds, and when we call the Operator in the example we say to run this in 0 milliseconds.

    When nothing is running, the 'setTimeout' logic is checking if there are any functions on the list that should run now, i.e. their time has come. Just, because we specify a time of 0 the Operator function may not be called immediately as there may be other functions that have been "scheduled" before.

    What other functions besides the Operator would use 'setTimeout'? 

     

    Well, your own code can, and should use 'setTimeout' to run loops.

     

    Let's have another look at the code above. Assume that you in your 'onInit' does this.

    Please login or register to see this code.

    Your 'onInit' will never exit. We will never reach the code afterwards that starts the Operator. No one will be able to call your QuickApp. We won't be able to do fibaro.call(<your QA>,"turnOn").

    The same thing happens if you do a "busy" loop inside any QuickApp function that you have defined.

    A note here. fibaro.sleep does a "busy sleep", it doesn't give time to other 'setTimeout' functions like the Operator in our case.

    Fibaro could have engineered it in a way that fibaro.sleep gave up time to other "threads" but they don't which I think is a missed opportunity...

     

    Takeaway.

    • Always use 'setTimeout' or 'setInterval' (that is based on 'setTimeout') when looping in your QuickApp. That will ensure that the QuickApp (our Operator) can continue to receive incoming requests like fibaro.calls or button presses. Whenever your code is busy running, in loops or what not, nothing else can happen...

     

    (Note. Here is a very simple "

    Please login or register to see this link.

    ", ~20 lines of code. It can run in any standard Lua environment that support co-routines (not the HC3/HC2). It implements the setTimeout and :onInit logic to run a small example QA. There is no "Operator" concept in it as it doesn't take external events into considerations but it's easy to see how that could be added)

     

    Now, the code above is simplified. Incoming requests are in reality sorted into two categories, 'actions' and 'UIEvents'

    It turns out that if we have defined

    Please login or register to see this code.

    the Operator will handle the incoming request to our function instead of trying to call the method requested.

    Likewise, if we define

    Please login or register to see this code.

    the Operator will send the UI events that are a result of the user pressing buttons or sliding sliders in our defined UI for the QA.

    You see this events in the slightly "annoying"  log messages. Ex:

    Please login or register to see this code.

    Beware if we define these functions we need to carry out the task of the Operator, like calling the requested function, or nothing will work in our QA.

     

    What can we use these handlers for? Well, in normal cases not that much. However, it allows us to write our own "Operator" dispatch logic if we want. Ex. instead of defining QuickApp functions for buttons and sliders you could send all events to a single function where you deal with UI logic. And there can be other cases where it could be useful. We will see in the next post that we can overcome some deficits with the current QuickAppChild logic with this.

     

    Well, I think that's enough for this post. Remember, 'setTimeout' is your friend :-)

    Edited by jgab
    • Like 4
    Link to comment
    Share on other sites

  • Topic Author
  • I posted this QA in another thread, but I repost here with maybe better explanation.

    It's a really simple QA. 

    Please login or register to see this attachment.

    • It's a QA of type "com.fibaro.binarySensor" (the blue person standing/running)
    • It can be configured to watch other sensors/doors/lights etc.
    • It's breached/on when any of the devices it's watching is breached/on
    • It's safe/off when all the devices it's watching is safe/off
    • Optionally, a delay can be configured that is the time that all devices need to be safe/off before it turns safe/off itself.

    Please login or register to see this attachment.

    In the example we have 4 rooms and 2 floors, and we make 7 copies of the DeviceSensor QA (Q1-Q7).

    We have made 4 DeviceSensor QAs ( Q1,Q2,Q3,Q4), one for each  room, watching 1-3 physical motion sensors each (S1-S8).

    We have med 2 DeviceSensors QAs (Q5,Q6), one for each floor, watching the room sensors.

    We have one DeviceSensor (Q7) covering the whole house by watching the floor sensors.

    All, except the house sensor, has a delay of 60s that their sensors need to be safe until they turn safe. (I would have put the floor delay to 0 too, they add up..)

    (You can divide your house/estate up in any logical divisions, with any number of levels in the hierarchy)

     

    Some use cases:

    • They behave like real binary sensors so we can easily use them in (block) scenes to tell if a room, floor or the whole house have any motion.
    • We can turn on lights in rooms when any sensor/door in room gets breached/opened
    • We can add delays until they become safe, turning off lights when a room, floor, or the whole house been safe for a while. etc.
    • We can create different hierarchies that share devices and DeviceSensor QAs. Ex. We could have a separate hierarchy for windows and doors to check if everything is closed when leaving, and if not on what floor and room....

     

    Very simple QA but quite useful. We could of course have made one QA/Scene that covers everything, but this makes it very flexible usage and allows for simple rewiring of the logic at anytime.

    Each QA is configured with two quickApp variables:

    • "devices" that is a list of device IDs to watch, Ex. "21 214 155"
    • "delay" that is the delay in seconds until the sensors should be considered safe.

    The QA can watch any device where the "value" property turns false/0 when it's safe/off. 

     

    P.S This is a sensor QA. One could do a similar binary switch QA that could a create a hierarchical switch structure allowing us to turn off/on rooms, floor, house...

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

    @jgab

    Perfect! Thanks for sharing.

    I have something similar in HC2 via your ER ?

    Logic is same but there are some diferences - it generates events/motion also in following cases:

    - any open/close of windows/doors (for doors it generates events for 2 rooms where applicable). I think be safe means only closed window/door.

    - only physical switch (on/off) light buttons (not for lights that were switched via script or in applications)

    - growing CO2 for defined period ?

     

    It’s just for inspiration. For cases above I can create QAs that will generate events for your QA.

    Thanks again for your ideas and explanations!
     

     

    Link to comment
    Share on other sites

    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...