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


  • 0

nested setTimeout in qa how to do it better?


Question

Posted

i have a qa to play music to a variable amount of sonos speakers and to play a combination of songs, because i cant get the queue to work on speakers i have had to use a nested timeout, but have a strong feeling that there must be a neater way of doing this, any help would be great.

 

here is my function in the qa.

 

function QuickApp:erev() 

    if MusicOffVariable == "false" then

        currentTime = os.date("%H:%M")

        songName = MusicPathPi..jMT[1].Title..MusicPathEnd

        songTime = tonumber(jMT[1].Length)

        print(os.date(),songName,songTime)

        songVolume = 10 

        self:playErev(songName,songVolume)

        fibaro.setTimeout(songTime * 1000, function() 

            songName = MusicPathPi..jMT[2].Title..MusicPathEnd

            songTime = tonumber(jMT[2].Length)

            self:playErev(songName,songVolume)

            print(os.date(),songName,songTime)

            fibaro.setTimeout(songTime * 1000, function()

                songName = MusicPathPi..jMT[3].Title..MusicPathEnd

                songTime = tonumber(jMT[3].Length)

                self:playErev(songName,songVolume)

                print(os.date(),songName,songTime)

                fibaro.setTimeout(songTime * 1000, function()

                    songName = MusicPathPi..jMT[4].Title..MusicPathEnd

                    songTime = tonumber(jMT[4].Length)

                    self:playErev(songName,songVolume)

                    print(os.date(),songName,songTime)

                end)

            end)

        end)

        

    end

end

16 answers to this question

Recommended Posts

  • 0
Posted
4 hours ago, Jay Ess said:

i have a qa to play music to a variable amount of sonos speakers and to play a combination of songs, because i cant get the queue to work on speakers i have had to use a nested timeout, but have a strong feeling that there must be a neater way of doing this, any help would be great.

 

here is my function in the qa.

 

function QuickApp:erev() 

 

        fibaro.setTimeout(songTime * 1000, function() 

 

            fibaro.setTimeout(songTime * 1000, function()

 

                fibaro.setTimeout(songTime * 1000, function()

                end)

            end)

        end)

        

    end

end

 

modern async / await development in LUA :) 

sorry for sarcasm but I can do nothing except give you advice to wrap setTimeout to own function.

maybe guru of LUA @jgabknows.

 

  • 0
Posted

In your case you seem to want to play 4 songs after each other. You chain a number of timers (setTimeout), guessing when the next one should start using the length of the song I guess...

It's not so neat as you say. The time may not be exact missing the last beat(s) or a unwanted pause... and assume you want to stop/pause the play, you need to stop the last timer..

 

I guess the only real way to make  it work  is if you can get some kind of status back from the player when the song has finished - then you could schedule the  play of the next one. What player are you using and is it possible? (Ex, the Sonos player will give you the playing status back)

  • 0
Posted

but to answer your question, the way to  get away from nested setTimeout's and make flatten it out using an event model. I keep preaching the use of an event style for programming on the HC2/HC3 but I haven't seen anyone catching on. It's the only way to keep your logic and program structure together when programming in a highly asynchronous world. Home automation with sensors that can send signals at any time tends to be very asynchronous - things can happen in any order at any time and you still need your programming logic to make sense and not become a chaos of special cases  that needs to be dealt with...

 

In your example you fire 4 timers to play songs with almost the identical code (except for the song) - that should trigger an "coding alarm" asking for a better way to do it - which you did....

 

So, how do we do "event programming"? Well we can make it advanced or really easy - let's start really  easy.

 

The basic concept is that you still set a timer, but instead of running a function, the timer posts an event. An event is just a piece of data that is associated with a handler (Lua function) that is executed whenever the event is posted.

 

Please login or register to see this code.

 

This gives a flavour of  the style - the code is all about posting events (with optional delays) and defining handlers for these events.  You get structure and reuse. The beauty is that it scales to handle HC3 device triggers, asynchronous http requests and also handling multiple events in parallel....

In the case above, if you could detect the player finished playing the song and then  post the endSong event it  would make it neat....

  • Like 2
  • Thanks 1
  • 0
  • Inquirer
  • Posted

    thank you, i am unsure what you mean by events, where is it being stored? can you clarify a little or point me where i can understand the event model a bit more please?

    yes during testing i did come up with the timeout does not allow me to quit the qa lol, which does need looking at too.

    i am playing it on a sonos speakers, actaully have modified the code somewhat as am playing them to a group of speakers, but i dont think changes much in my orginal question and your answer, and have managed to start a small function which can get back the state of the player using the code below, it does need cleaning up as am working my way through it and have written it more to see what data i can get back. i am using a pi with some setup which has gone from my mind at the moment but it shows me the state of the player and the song etc...

    --check if music is playing

    function QuickApp:checkPlayState()

        local http = net.HTTPClient({timeout=3000})

        local arcHttp = "http://10.XXX.1.XXX:5005/Living%20Room/state"

        local oneHttp = "http://10.XXX.1.XXX:5005/Upstairs%20Hallway/state"

        local httpstring = "http://10.XXX.1.XXX:5005/Living%20Room/state"

        --fibaro.sleep(1000)

        http:request(httpstring,{options={headers={Accept="application/json"},method='GET'},

            success = function(response)

                if counter ~= 0 then counter = counter + 1 end            

                local data = json.decode(response.data)

                d1 = data

                --print(d1)

                --print("run by is",musicStateCheck)

                for k,v in pairs(data)do

                    --print("D1",k,v)

                    if k == "playbackState" then

                        if string.sub(data.currentTrack.uri,1,24)=="x-sonos-htastream:RINCON" then

                            stringState = "TV"

                        else

                            stringState = data.currentTrack.title

                        end

                        print(data.currentTrack.uri,"here is the current stream.")

                        print(data.currentTrack.title,"this is what we are looking for.")

                        

                        self:updateView("btnStatus", "text", "Music State is "..v.." "..stringState)

                        

                        if counter  ~= 0 then

                            if v == "STOPPED" then

                                print("We have an issue as song has not played","We might need to wait for a few seconds as not sure how long the delay might be.")

                                --self:stopMusic()

                                fibaro.call(SonosHub, "restore",SonosPlayerArc)

                                fibaro.sleep(1000)

                                --self:playErev()

                                

                            else

                                print("We seem to have success as we are now in state",v)

                            end

                        end

                    end

                end

     

                --print("New line now going through current track lines")

                --data = data.currentTrack

                

                --for k,v in pairs(data) do

                --    print("Print",k,v)

                --end

            end,

            error = function(error)

                print("error",json.encode(error))

            end

            }

        )

    end

    • 0
    Posted
    18 hours ago, jgab said:

    I keep preaching the use of an event style for programming on the HC2/HC3 but I haven't seen anyone catching on


    The events on the HC3 are underestimated. Also a bit hidden in the dashboard. Several times I looked into it, but didn’t made the step to use them. 

    • 0
    Posted (edited)
    13 hours ago, Jay Ess said:

    thank you, i am unsure what you mean by events, where is it being stored? can you clarify a little or point me where i can understand the event model a bit more please?

     

    An event is just a pice of data that we can post. We then define "event handlers", functions that react (or trigger) on specific events.

    In the example above they are not "stored" anywhere.  We post 'playList' events and we have an event handler that triggers on any 'playList' being posted.

     

    Inside the HC3, any change (ex. devices changing states or globals being set) generates an internal event. We can see them in the "History panel". Some of these events are "transformed" to triggers and are used to trigger scenes that have specified conditions for specific triggers. In scenes, we also get the triggers as a Lua table in the variable 'sourceTrigger'

    Ex. if sensor 88 is breached, the sourceTrigger we get looks like

    Please login or register to see this code.

     

    In QAs we unfortunately don't get these sourceTriggers. What we can do is to fetch the raw events with the /refreshStates api and then create our own "event mechanism" in our QA.

    In my fibaroExtra.lua library I do this. I listen to the /refreshStates api, get events, transform them into "sourceTrigger format" and "post" them using a similar mechanism as in my example above. The main difference is that the definition of event handlers is more advanced and allows matching the whole event instead of just using the type of the event as a selection mechanism. 

     

    But the cool thing is that I code in the same style even if it is my own "invented" event type like 'playList' in the example above, or if I define an event handler for a sensor getting breached. 

    This is the way it looks using the fibaroExtra.lua library

    Please login or register to see this code.

    This is a trivial handler that will react if sensor 88 is breached and call the handler function that logs a message and turns on device 99.

    I can define any number of these handlers and my event library hides all the complexity for me. It is also very efficient in matching events to handler, allowing several hundreds of event handlers to be defined without causing a lot of processing overhead.

     

    An advantage if this style of programming is also that if I want to debug my code and fake that the sensor is triggered I can just post the event myself.

    Please login or register to see this code.

    and my previously defined event handler will trigger the same way as if the real device was breached. 

     

    11 hours ago, SmartHomeEddy said:

    The events on the HC3 are underestimated. Also a bit hidden in the dashboard. Several times I looked into it, but didn’t made the step to use them. 

     

    Yes, but the custom events that FIbaro gives us are a bit limited. 

    btw, the sourceTrigger they generate is

    Please login or register to see this code.

     

    The problem with Fibaro's custom events is that they can't carry any values. The have a userDescription field that is a string, but all events need to be predefined before they can be sent. That mean that we can't fire 2 events of the same type with different userDescriptions.

    Example. it would be nice if your Tibber QA sends a price event with the price when it changes. 

    The QA could then define a custom event 'price' with the userDescription '1 euro" and then post it. It then discover that the price has changed and redefine the 'price' event to have the userDescription "2 euro" and post it.

    The problem is that if the receivers (the scenes) are not fast enough to react on the events, and react after both events are posted, they will get 2 events, but each time they read the userDescription of the event they will see "2 euro". That's because they will read the last userDescription value we set. 

    In this case it may not be a disaster that we receive 2 identical events but we can come up with other scenarios where a missed event would be bad.

     

    The solution would be to allow us to post custom events of type ex. "price", but allow us to tack on extra parameters to the event when it's posted. Like in my example above. This is nothing new and is how event systems works on all other platforms.... The problem now is that the Fibaro events are actually "stateful" if we want to leverage the userDescription field as they have a stored value which kind of defeats the whole purpose with events that should be stateless...

     

    The good thing is that we can use them to easily trigger Scenes as they can use custom events as triggers in scene conditions - but in a limited way that kind of make less uninteresting.

    Edited by jgab
    • 0
  • Inquirer
  • Posted

    ok have added this to my qa in an attempt to get my head around what you are doing as the programming seems neat and the best way for me to learn is to actually try and implement it.

     

    --Event Utilities

    local EVENT = {}

    function post(event,time) return setTimeout(function() handleEvent(event) end,1000*(time or 0)) end

    function handleEvent(event)

        local handler = EVENT(event.type)

        handler(event)

    end

    function EVENT.playSong(event)

        currentTime = os.date("%H:%M")

        songName = MusicPathPi..jMT[event.id].Title..MusicPathEnd

        songTime = tonumber(jMT[event.id].Length)

        print(os.date(),songName,songTime)

        songVolume = 10--event.volume

        self:playErev(songName,songVolume)

        post({type='endSong',cont=event.cont},songTime)

    end

    function EVENT.endSong(event) post(event.cont) end

    function EVENT.playList(event)

        local list = event.list

        if #list>0 then

            local song = table.remove(list,1)

            post({type='playSong',id=song.id,volume=song.volume,cont={type='playList',list=list}})

        end

    end

    function QuickApp:btnErev()

        local volumeErev = 10

        post({type='playList',list={{id=1,volume=volumeErev},{id=2,volume=volumeErev},{id=3,volume=volumeErev},{id=4,volume=volumeErev}}})

    end

     

    when i press the btn conntected to btnErev i get this.

    [05.01.2022] [16:13:36] [TRACE] [QUICKAPP478]: UIEvent: {"elementName":"btErev","deviceId":478,"values":[null],"eventType":"onReleased"}

     

    and then nothing, i am missing something but cant figure out what, unless i am doing it completly wrong?

    • 0
    Posted
    40 minutes ago, Jay Ess said:

    ok have added this to my qa in an attempt to get my head around what you are doing as the programming seems neat and the best way for me to learn is to actually try and implement it.

     

    --Event Utilities

    local EVENT = {}

    function post(event,time) return setTimeout(function() handleEvent(event) end,1000*(time or 0)) end

    function handleEvent(event)

        local handler = EVENT(event.type)

        handler(event)

    end

    function EVENT.playSong(event)

        currentTime = os.date("%H:%M")

        songName = MusicPathPi..jMT[event.id].Title..MusicPathEnd

        songTime = tonumber(jMT[event.id].Length)

        print(os.date(),songName,songTime)

        songVolume = 10--event.volume

        self:playErev(songName,songVolume)

        post({type='endSong',cont=event.cont},songTime)

    end

    function EVENT.endSong(event) post(event.cont) end

    function EVENT.playList(event)

        local list = event.list

        if #list>0 then

            local song = table.remove(list,1)

            post({type='playSong',id=song.id,volume=song.volume,cont={type='playList',list=list}})

        end

    end

    function QuickApp:btnErev()

        local volumeErev = 10

        post({type='playList',list={{id=1,volume=volumeErev},{id=2,volume=volumeErev},{id=3,volume=volumeErev},{id=4,volume=volumeErev}}})

    end

     

    when i press the btn conntected to btnErev i get this.

    [05.01.2022] [16:13:36] [TRACE] [QUICKAPP478]: UIEvent: {"elementName":"btErev","deviceId":478,"values":[null],"eventType":"onReleased"}

     

    and then nothing, i am missing something but cant figure out what, unless i am doing it completly wrong?

     

    The first thing I notice is that you have parenthesis instead of brackets here

    Please login or register to see this code.

     

    fix that.

    You can also add a debug line in handleEvent to see if your event are "handled"

     

    Please login or register to see this code.

     

    • 0
  • Inquirer
  • Posted

    amazing yes it did fix it ok now that i am getting some results time to get to know it properly LOL

    thank you ever so much.

    will update on how it goes.

    • Like 1
    • 0
    Posted

    You can think about adding the fibaroExtra.lua file

    into you QA as an extra file. It's backwards compatible with the standard QA functions but you will get some additional error checks that are hard to catch using the plain QA environment.

    Then you are also ready to move over to the real event library included when you you get a taste for it....

     

    • 0
    Posted (edited)

    To stop playing the queue we need to do some modification to our event code.

    We need to add a 'stopList' event that we can post to stop playing and we need to add a cancel(timer) command to our event toolbox so that we can cancel a posted event. 

    In our event handlers when we post an event we save the reference to that post (post() returns a reference to the setTimeout timer we started). In our case we save it in a variable 'lastTimer'. So when we want to stop the playing of the list we can check if lastTimer ~= nil and assume that we are playing a list. We then stop the actual paying (I guessed there was a quickApp:stopErev() command) and then we cancel the posted event that lastTimer refer to.

     

    Please login or register to see this code.

     

    There is another neat advantage with the event model. Each step we take in our application logic is a posted event -> handled event. And each posted event is calling the event handler with a setTimeout call. Every time we do a setTImeout we give the opportunity to other code in our QA to run.

    For the QA to receive button and slider clicks from the UI there need to be time for them to execute. If you run a busy loop

    Please login or register to see this code.

    Your QA will not get any UI actions. Other QAs can not be able to call your QA with fibaro.call(your_QA_id,<action>, ...) either.. (they can call but your code will not respond).

    So by "chunking up" our code into "blocks" chained together by posting events we will automatically give time to other systems and get a more responsive QA.

    The post function will do a setTimeout(handler,0) if we don't specify a time for the post command. 0 says that we want to run the handler as soon as possible. However, if there are other functions in the queue waiting to run they will be given a chance to run first (ex. there may be a UI click that waiting to call your button function).

     

    It may also be that you want to give time to your own code... because you may be running 2 or more "event chains in parallell:


     

    Please login or register to see this code.

     

    Here we start 2 ping "ping-pong event chains". Bob and Ann, each pinging and ponging with different intervals (3s and 2s) - running in "parallell".

    The ping event handler just carries out the job the event specifies (logging ping and user) and then it post an event to the other handler with the data and the delay specified in the event.

     

    We can of course easily do this with just two setTimeout loops but the code structure easily becomes messy - here we abstract the logic (the handlers) from the processes driving the logic.

     

     

    Edited by jgab
    • 0
    Posted (edited)

    When we have the 'cancel' function in our event toolbox we can easily code the use case

    "turn on light when sensor is breached and turn off light when sensor been safe for x seconds"

    It can handle multiple sensors and starts the countdown for turning off the light after all sensors have turned safe.

     

    Please login or register to see this code.

     

    Here we run a second loop to poll the sensors and post their state if they have changed value - it may not be that efficient. There is a much more effective way to receive and post device changes into our program using the /refreshStates api but that requires a bit more code. But we can later change out the poll loop with the /refreshStates api logic without having to change the rest of our code....

    Edited by jgab
    • 0
    Posted (edited)

    So the previous example controls a light with a set of sensors. Example in a room.

    Assume that we want to generalise this to handle all sensor and lights in a house. We then need to set up a table which sensor controls which lights. What we do here is that we setup what sensor control what room, and then a table with rooms and their lights.

    Please login or register to see this code.

     

    The main logic from the previous example is still there - the difference is that instead of working with individual lights we work with rooms. We turn on/off rooms, and rooms contains light IDs, delays, and eventual timer.

    Edited by jgab
    • Thanks 1
    • 0
    Posted (edited)

    If we add some time functions to our event toolbox it becomes even more useful.

    Please login or register to see this code.

     

    midnight() returns the time of the last midnight - subtracted from the current time we get how many seconds we are into the current day.

    hm2sec() returns a time in string format to the number of seconds. ex. "00:01" is 60. It also supports that we specify the time as "sunset" or "sunset+10" where the 10 is minutes. If we add hm2sec("15:40")+midnight() we get the absolute time in second (epoch) the way that os.time() return time.

    toTime() allow us to prefix the time with "+/", "t/","n/" to get epoch time as plus current time, today, or next matching time.

    Ex.

    toTime("10:00")     -> 10*3600+0*60 secs
    toTime("10:00:05")  -> 10*3600+0*60+5*1 secs
    toTime("t/10:00")    -> (t)oday at 10:00. midnight+10*3600+0*60 secs
    toTime("n/10:00")    -> (n)ext time. today at 10.00AM if called before (or at) 10.00AM else 10:00AM next day
    toTime("+/10:00")    -> Plus time. os.time() + 10 hours
    toTime("+/00:01:22") -> Plus time. os.time() + 1min and 22sec
    toTime("sunset")     -> todays sunset in relative secs since midnight, E.g. sunset="05:10", =>toTime("05:10")
    toTime("sunrise")    -> todays sunrise
    toTime("sunset+10")  -> todays sunset + 10min. E.g. sunset="05:10", =>toTime("05:10")+10*60
    toTime("sunrise-5")  -> todays sunrise - 5min
    toTime("t/sunset+10")-> (t)oday at sunset in 'absolute' time. E.g. midnight+toTime("sunset+10")

     

    We can then use this to enhance our post function.

    Please login or register to see this code.

     

    We can still do 

    Please login or register to see this code.

    to post an event with a delay of 100 seconds. But we can also do

    Please login or register to see this code.

    to post the event 10 minutes after sunset today. Or we can do

    Please login or register to see this code.

    to post at 15:00 today.

     

    Please login or register to see this code.

    to post at 15:00 today if we post before 15:00. If we post after 15:00 it will be delayed to 15:00 the next day. This is a useful prefix for doing daily loops. We will see that in the 'schedule' event in the next example.

     

     

    This way we have the basic for a small task scheduler.

     

    Please login or register to see this code.

     

    We define events for day actions, like morning, lunch, watering the flavours, going to bed etc. 

    and then we have a 'schedule' event that run every midnight and post the events at the time we have defined for the upcoming day.

    This scheduler can of course be more advanced with more types of events. We could have 2 morning events; 'morningWorkday' and 'morningFreeday, and in the morningWorkday event handler we could check a global variable if it's vacation and then instead post an immediate {type='morningFreeday'} event to handle the day as a free day. etc etc.

     

    Edited by jgab
    • 0
    Posted (edited)

    The last enhancement to our toolbox requires a change in how we declare event handlers.

    The problem today is that we can only define one handler for each event type. This is because we use the event type as a key in the EVENT table that holds our handlers - and a Lua table must have unique keys in a key-value table.

     

    Now we can define 

    Please login or register to see this code.

    We have one handler for 'device' events but we need to handle what device id should do what inside the handler.

     

    It would be nice if we could declare one handler for id=88 and another for id=89. Like this

    Please login or register to see this code.

     

    The change we need to do in our event toolbox is

    Please login or register to see this code.

     

    We still register event handlers with the event type so we can quickly look up handlers for a given event type (we can make it even more efficient but this is ok for the moment). However we can have many handlers for a given type so we then check if the event matches the event we registered for a specific handler and if so call the handler.

    This also means that it's important in what order we declare our event handlers and we will match them in the order they are declared and when we have a match we run that handler but don't run other matching handlers declared afterwards.

     

    Note also that

    EVENT({type='device'},function(event) .... end)

    will match all events with the type 'device', e.g. the id can be any id. This is useful as we may want to have general handlers that can trigger on many specific events.

     

     

        

    Edited by jgab
    • 0
  • Inquirer
  • Posted

    wow a lot of data to process, am ever so slowly trying to get my head around it but defintily like the style and feel it could be very useful to adapt . will be in touch once i have had a chance to spend some time on playing with it. keep getting side tracked .

    Join the conversation

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

    Guest
    Answer this question...

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