AppDaemon Q&A

OK, so you are running into a classic problem with multi-threaded event driven concurrent programming :slight_smile: The problem is that the real world isn’t our friend and things happen randomly and all together

I think that the way to solve this is to have a common piece of code that controls access to all the spheech. When a program wants to say something, it tells the common code, and the common code maintains a queue and makes sure that only one thing is being said at once, It sounds complicated but Python has good tools to help with this and I can try and help you figure it out if you like. This is probably the only sensible way to solve the problem without crippling AppDaemon.

If we manage to figure it out it would make a great additional example App!

EDIT: It looks like you are part way there, you seem to have the speak and play all in a single App already?

With a little restructuring this could work fine:

  1. Modify speak and say to put the parameter info on a queue rather than do it directly
  2. Create a worker thread to read from the queue, do the speak or say, then maybe wait a second or two before grabbing the next one.

This is pretty easy as it is how AppDaemon works internally.

What do you think?

1 Like

that would be cool, you could also give items a priority in the queue, so critical things could go to the top ( things like “excuse me but the smoke alarm in the kitchen says you are burning dinner”). Those things could be prioritized (at the coders direction) above things like “excuse me, I’m detecting a methane buildup in the restroom, do you want me to turn on the exhaust fan”. I’m going to build a sensor for that last one.

LOL - the methane thing sounds cool :slight_smile: And Also, I have a hunch that this App would be useful for other situations - maybe we can make a universal notification App to avoid recoding stuff all over the place.

Or maybe another approach would be to allow a way to enqueue requests to AppDaemon’s worker threads since the Que and workers are alread in place. Let me think about that.

EDIT: That’ won;t work as there are multiple threads but maybe some kind of a singleton capability in the Apps …

what if we have the app listen for a custom event and manage it’s own queue.

you guys are the coolest.

how about this:

the speach functions are now in an empty app.
so lets run that with run_secondly.
put the names from the sounds to play (spoken tekst is translated to sound before play) in a list.
in the callback:
if list is empty do nothing, else play sound and wait 2 seconds.

making 5 lists to put the names in could give 5 priorities.

the only thing i need to find out is how to delete sound_priority1[0] from list sound_priority1, making sound_priority1[1] to sound_priority1[0]

I was going with a dictionary type of idea where the primary dictionary like below. Then just using a min value type of thing to get the smallest item out of the dictionary.

{“1”:{“1”:“Hi There”},{“2”:“This is the high priority queue”},
“2”:{“1”:“Hello world”},{“2”:“this is the medium priority queue”}}
and so on and so on.

yeah oke, a dict would make my code shorter with a for next loop.
but somehow i Always try to avoid dicts :stuck_out_tongue:

Would something like this work

while self.messages :
  pqueue=self.messages[min(self.messages)]
  while pqueue :
     message=pqueue[min(pqueue)]
     self.log("output message {}".format(message))
     pqueue[min(pqueue)].remove

thats why i dont like dicts.
i need to look at that 10 times :wink:
i guess it could work but the messagenumber keeps counting that way.

i thought more in this way:

filelist={"1":["file1","file2",...],"2":["file1","file2,...]}

for prioritylist in filelist:
  for file in prioritylist:
    self.play(file)
    prioritylist.remove(file)
    return

that way i can just use

filelist["1"].append(file)

to add new files with priority 1

or do i see something wrong?

Does self.play just play the file out to whatever the output speaker setup is on the RPI?

How are you going to get the messages from the event? I never could get that to work?

this:

cmd = ['omxplayer','-o','local',filename]

is a commandline function.
it tells the rpi to play filename with omxplayer to the local soundport (headphoneplug, lineout)

in any app i can use:

     priority="1"
     speaktext = "i am alive"
     sound = self.get_app("speak")
     sound.say(speaktext,"nl",priority)

say now creates a file and sets the filename in the list.
that part is going right.

i am just a bit struggling with the loop which checks and plays the sounds.

dont know why but it wont work.
the app keeps running for a while but at a random point it stops after a few loops.
and filling the list isnt working, because i never find anything in it in the loop.

what i tried:

import appdaemon.appapi as appapi
import datetime
import tempfile
import subprocess
import os
from gtts import gTTS

class speaknow(appapi.AppDaemon):

  def initialize(self):
    self.filelist = {"1":["empty"],"2":["empty"],"3":["empty"],"4":["empty"],"5":["empty"]}
    self.run_in(self.check_soundlist,2)

  def check_soundlist(self, kwargs):
    self.log("runsound is running")
    for priorityfilelist in self.filelist.values():
      for file in priorityfilelist:
        if file != "empty":
          self.log("file gevonden: " + file)
          self.play(file)
          priorityfilelist.remove(file)
          os.remove(fname)
          self.run_in(self.check_soundlist,2)        
          return
        self.log("priorityfilelist was empty")
    self.log("list was empty, restart check")
    self.run_in(self.check_soundlist,2)
    
  def say(self,text,lang,priority):
    with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as f:
        fname = f.name
    tts = gTTS(text=text, lang=lang)
    tts.save(fname)
    self.filelist[priority].append(fname)
  
  def playsound(self,file,priority):
    self.filelist[priority].append(file)

  def play(self,filename):
    cmd = ['omxplayer','-o','local',filename]
    with tempfile.TemporaryFile() as f:
        subprocess.call(cmd, stdout=f, stderr=f)
        f.seek(0)
        output = f.read()

check_soundlist actually does the talking based on what is in the filelist
say gets called and populates filelist. Right?

does filelist look right at the end of say if you do a self.log(“filelist ={}”.format(self.filelist))
does the list look right at the beginning of check_soundlist?

Can you trace it and narrow it down to where it’s going haywire with some debug log statements?

it seems like that self.get_app is buggy.

i used [soundfunctions] in the config to call the app
and i get this error all the time:

2016-12-21 03:34:25.253985 WARNING ------------------------------------------------------------
2016-12-21 03:34:25.257205 WARNING Unexpected error:
2016-12-21 03:34:25.260370 WARNING ------------------------------------------------------------
2016-12-21 03:34:25.295073 WARNING Traceback (most recent call last):
  File "/usr/local/lib/python3.4/dist-packages/appdaemon/appdaemon.py", line 407, in worker
    function()
  File "/home/pi/.homeassistant/apps/heatingnew.py", line 16, in initialize
    self.sound = self.get_app("soundfunctions")
  File "/usr/local/lib/python3.4/dist-packages/appdaemon/appapi.py", line 53, in get_app
    return conf.objects[name]["object"]
KeyError: 'soundfunctions'

2016-12-21 03:34:25.300193 WARNING ------------------------------------------------------------

where did soundfunctions come from? The code you sent was using “speak”. I made a couple of changes and it seems to work.

import appdaemon.appapi as appapi
import datetime
import tempfile
import subprocess
import os
from gtts import gTTS

class speaknow(appapi.AppDaemon):

  def initialize(self):
    self.filelist = {"1":["empty"],"2":["empty"],"3":["empty"],"4":["empty"],"5":["empty"]}
    self.run_in(self.check_soundlist,2)

  def check_soundlist(self, kwargs):
    self.log("runsound is running")
    for priorityfilelist in self.filelist.values():
      for file in priorityfilelist:
        if file != "empty":
          self.log("file gevonden: " + file)
          self.play(file)
          priorityfilelist.remove(file)
          os.remove(file)
          self.run_in(self.check_soundlist,2)        
          return
        self.log("priorityfilelist was empty")
    self.log("list was empty, restart check")
    self.run_in(self.check_soundlist,2)
    
  def say(self,text,lang,priority):
    with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as f:
        fname = f.name
    tts = gTTS(text=text, lang=lang)
    tts.save(fname)
    self.filelist[priority].append(fname)
  
  def playsound(self,file,priority):
    self.filelist[priority].append(file)

  def play(self,filename):
    cmd = ['omxplayer','-o','local',filename]
    with tempfile.TemporaryFile() as f:
        subprocess.call(cmd, stdout=f, stderr=f)
        f.seek(0)
        output = f.read()

(hass) hass@hass:~/appdaemon/conf/apps$ cat test.py
import appdaemon.appapi as appapi

class test(appapi.AppDaemon):

  def initialize(self):
    self.log("about to fire SPEAK event")
    self.run_in(self.say_something,2)

  def say_something(self, kwargs):
    priority="1"
    speaktext = "i am alive"
    sound = self.get_app("speak")
    sound.say(speaktext,"nl",priority)
(hass) hass@hass:~/appdaemon/conf/apps$

i renamed it to soundfunctions because i got the same error over and over with key error speak.
and because i used speak at more places i needed to be sure.

i dont know what it is.
i tried abandoning the dict and made lists in list, didnt work.
i tried writing filenames to a dir. didnt work.
restarted appdaemon many times.

i guess tommorow is another day (its 4:00 now) :wink:

Ok, but it’s working over here, or at least it seems to be.

ok, here is what I see…

  1. I’m running HA and AD from a virtual environment. I’m not hearing anything over my TV speakers.
  2. If I copy the MP3 files to a hold directory and play them individually (had to use sudo to do this), they chop off the end. I get “I am A{click}” most of the time.
  3. Other than the fact it isn’t saying anything, I have the I am alive thing in a loop inserting that text at random intervals between 5 and 10 seconds. Now that’s not enough to put any type of stress on this, it’s not even enough to get more than one message in at a time. But the files are being generated and cleaned up as expected.

virtual enviroment shouldnt effect anything.
if your sound comes over the hdmi then LOCAL should be changed to HDMI or the part
,’-o’,‘local’
can be deleted completely.

i had a chop off for some short sentences too.
but only for some.
by the way you should use “en” instead off “nl” for english instead of dutch.