Reload AppDaemon App from Another App

Good morning -

I have an AD app that does some complex logic based on time-of-day. I had been keeping those times in my app.yaml, but wanted to expose them via HA Input_Text entities so my wife could edit the time the AD app runs.

Got everything working before I realized: it was only working because I was saving the file frequently, and prompting the callbacks to be reloaded. Any changes to the Input_Text wouldn’t actually effect the AD App until the AD App was reloaded / re-initialized.

I looked thru the documentation, but didn’t see any service calls to prompt a reload. I was thinking of a second AD app that listened for changes to the Input_Text entity, then reloaded the first app on-change.

The “brute-force” alternative: I’m running everything in separate docker containers but on a single physical host, so the alternative is a cron job that re-saves that particular app file every 5 minutes or so. That should reload the AD App frequently enough to solve the problem in most cases.

Any thoughts / suggestions?

Can you post the code of your app that does things based on time-of-day? I’m almost sure there is a way to do this without saving the file every 5 minutes :slight_smile:

By the way why not use input_datetime instead of input_text if the entry is a time?

@Hiltonians as @Burningstone said, there has got to be a way that you wouldn’t need to be restarting the app, if you modified your code.

But presently in AD 4.0 as in beta now, it is actually possible to have another app reload or restart an app as needed. I will assume you are using <= 3.0.5, so wouldn’t be possible.

Kind regards

Maybe I didnt understood, but why you want to restart the app? Why not just listen to the state of the input_datetime and re-schedule the app schedule timers? If you look at the code of AlarmClock (ex1: AppDaemon Alarm Clock, ex2: https://github.com/eifinger/appdaemon-scripts/blob/4892fecaa95a32945834a9eb07492e0816883c9d/alarmClock/alarmClock.py) you may see this happen.

Thanks for the help – I agree, it feels like there is a better approach than bludgeoning the file with mock-changes. I don’t quite have enough time to devote to a beta test right now, but happy to see that AD4.0 might have an easy solution.

To answer why i’m doing this at all for @clyra and @Burningstone: I’m chasing children around with echoes. Imagine an Entity Card in HA containing:

  • Entity Card (in HA)
    • Tell Kids to get ready for bed at: 7:30pm
    • Tell Kids to take showers at: 7:40pm
    • Tell Kids to get into bed and read at: 8:00pm
    • Tell Kids lights out at: 8:30pm

and so on… one AD app controls the entire bedtime routine, but with different callbacks/functions, of which I will include only one for brevity… (they’re all different flavors of the same). The App also contains a few helper functions, only one is relevant here:

get_time() == looks up the corresponding input_text.entity, does a little light cleaning, and returns a python time object (the input_text also has regex pattern validation, but it doesn’t seem strictly enforced, so I check again anyway).

I have two kids with different lights-out times, hence the kwarg. Names were changed to protect the innocent.

class kids_bedtime(hass.Hass):

    def initialize(self):
        if self.args['skip_announcements'] == False:
            self.prefix = "" 
            self.prefix = "Attention Ernie and Bert! "*2
            self.run_daily(self.both_go_upstairs,   self.get_time("both_go_upstairs")  , kid="both" )
            self.run_daily(self.both_get_ready,     self.get_time("both_get_ready")    , kid="both" )
            self.run_daily(self.both_ready_for_bed, self.get_time("both_ready_for_bed"), kid="both" )
            self.run_daily(self.both_read_books,    self.get_time("both_read_books")   , kid="both" )
            self.run_daily(self.both_lights_out_1, self.subtractSecs(tm=self.get_time("ernie_lights_out"), secs=180),  kid="ernie" )
            self.run_daily(self.both_lights_out_1, self.subtractSecs(tm=self.get_time("bert_lights_out"),  secs=180),  kid="bert"  )
            self.run_daily(self.both_lights_out_2, self.subtractSecs(tm=self.get_time("ernie_lights_out"), secs=20) ,  kid="ernie" )
            self.run_daily(self.both_lights_out_2, self.subtractSecs(tm=self.get_time("bert_lights_out") , secs=20) ,  kid="bert"  )
            self.run_daily(self.both_lights_out_3, self.subtractSecs(tm=self.get_time("ernie_lights_out"), secs=0)  ,  kid="ernie" )
            self.run_daily(self.both_lights_out_3, self.subtractSecs(tm=self.get_time("bert_lights_out") , secs=0)  ,  kid="bert"  )


    def both_go_upstairs (self, kwargs):
        msg = self.prefix + "Time to get ready for bed.  Say goodnight to Oompa and head upstairs to your rooms."
        echoes = self.play_where(defname='go_upstairs')
        self.log('Bedtime announcement on Echoes: %s \n Message: %s' %(echoes, msg))
        self.call_service("python_script/alexa_say", where=echoes, msg=msg)
        #turn on stair light, so kids can see their way up
        self.turn_on("light.zooz_zen22_dimmer_v2_level_2") # stair lights



    def get_time(self, argname):
        import datetime
        self.log("BEDTIME PROCESS: %s" %argname)

        txt = self.get_state("input_text.bedtime_%s_time" %argname)
        self.log("--TIME RAW: %s" %txt)

        if txt[-2:] == "am": ampm = 'am'
        elif txt[-2:] == "pm": ampm = 'pm'
        else: ampm = ''

        txt = txt[:len(txt)-len(ampm)]
        hr = int(txt.split(":")[0])
        min = int(txt.split(":")[1])
        if ampm=="pm" and hr<12: hr=hr+12
        rtn = datetime.time(hr, min, 0)

        self.log("--TIME Processed: hour: %i  min: %i  rtn: %s" %(hr, min, rtn))
        return rtn

So… the use case I’m looking for: My wife decides to move back the “lights out” time, she can go into the Entity Card and simply update the appropriate input_text. Another automation can listen for the input_text entity change, and some how magically reload the kids_bedtime app.

Its that last part that I’m struggling with. Simply changing the input_text entity without that last bit of magic does nothing, until the app is reloaded.

As an aside - I highly recommend this approach. The kids argue and wheedle with my human nature over bedtime, but the unemotional and regimented predictability of Alexa’s instructions and associated lighting cues… it’s revolutionized bedtime. No arguing, everyone knows the drill. Score one for delegated parenting.

Thanks!

Good news, this is doable without reloading the app or saving your file every 5 minutes :slight_smile:

I will just show you an idea how you could achieve this. I leave it to you to figure out a clean way to loop through all the activities etc.

First I would change input_text to input_datetime.

input_datetime:
  both_go_upstairs_time:
    name: Both go upstairs time
    has_date: false
    has_time: true

Then in the initialize method I would add this:

both_go_upstairs_input_datetime = self.args.get("both_go_upstairs_input_datetime")
both_go_upstairs_time = self.get_state(both_go_upstairs_input_datetime)

self.both_go_upstairs_handle = self.run_daily(self.both_go_upstairs, both_go_upstairs_time, kid = "both")

self.listen_state(self.input_datetime_changed, self.both_go_upstairs_input_datetime)

Then I would add the following method to catch changes in the input_datetime:

def input_datetime_changed(self, entity, attributes, old, new, kwargs):
    if new is not None and new != old:
        self.cancel_timer(self.both_go_upstairs_handle)
        self.both_go_upstairs_handle = self.run_daily(self.both_go_upstairs, new, kid = "both")
1 Like

Commenting to add support to @Burningstone 's described solution. This is exactly the way I handle similar situations in AppDaemon.

By saving the handle for the run_daily() call, you are able to cancel and recreate the run_daily() call any time the text/date inputs in HA change. While this doesn’t “restart” the app, it “restarts” the parts that need to be restarted (the run_daily() call). You can use this same approach no matter where the input comes from. It could be a Home Assistant entity, a Google Calendar, some Alarm Clock like app that you run on your phone. With some extra effort you could even make this a “Hey Google, The kids should go to bed at 7:45pm” situation.

One rather complex AppDaemon app I have lets me set my daughter’s bed time by adding an alarm to the Alexa in her room. When I add the alarm, the App detects that, knows that’s when she’s supposed to wake up the next morning, and it then performs other tasks based on the time of that alarm.

There will be some situations where you want a thing to happen at the same time, every time, no matter what. And, if that time ever changes, you don’t mind a quick YAML edit to reflect that time. In those cases, a call to run_in/run_every/run_daily without any need for the handle is appropriate. But, at least for me, in the vast majority of cases, things automate based on other things happening. And this is the pattern to use when you want that kind of functionality. Want to give your kids only 45 minutes of TV time? Detect when the TV turns on and use a run_in() to turn it back off. Want a TTS notification 20 minutes before it’s time to leave for school but only on school days? Use a google calendar with the school schedule in it and a run_daily() that cancels and creates itself depending on if school is in session or not.

2 Likes

Thanks @Burningstone / @swiftlyfalling ! I’ll give this a try in the next couple days. I knew the handles were available, but I’ve never used them.

Also, good to hear this - it’s on my to-do list:

thanks!

Just to close the loop: Followed the instructions per @Burningstone, worked great. I ended up re-writing all the core logic to ingest a list of control dictionaries, but it’s much cleaner now, and more easily extensible. I’ll paste the full code below (names changed).

Now I have a fully functioning control panel for my bedtime automation, which immediately updates AD upon changes in HA.

I added a few other bells-and-whistles, like input_booleans to toggle on/off particular kids (if, for instance, one is sick and I don’t want the bedtime routine to bother them) and a master-automation kill-switch. I think the only other FYI is that I have my AlexaSay() procedure as a service within HomeAssistant’s Python Script framework, so I can call it aaS from where-ever. I also renamed my input_datetime and input_select entities with consistent names, so I could easily drive them with the c[‘name’] control record.

Thanks again @Burningstone for the point in the right direction!

import appdaemon.plugins.hass.hassapi as hass
import datetime as dt

class kids_bedtime(hass.Hass):

    def initialize(self):
        self.prefix = "Attention bert and ernie! "*2

        self.ctl = [  {'name':'bedtime1_go_upstairs',      'phase':'0', 'subsec':'0',   'kid':'both',  'time':'', 'echo':'', 'def':'bedtime1_go_upstairs'   , 'handle':'' }
                     ,{'name':'bedtime2_get_ready',        'phase':'0', 'subsec':'0',   'kid':'both',  'time':'', 'echo':'', 'def':'bedtime2_get_ready'     , 'handle':'' }
                     ,{'name':'bedtime3_ready_for_bed',    'phase':'0', 'subsec':'0',   'kid':'both',  'time':'', 'echo':'', 'def':'bedtime3_ready_for_bed' , 'handle':'' }
                     ,{'name':'bedtime4_read_books',       'phase':'0', 'subsec':'0',   'kid':'both',  'time':'', 'echo':'', 'def':'bedtime4_read_books'    , 'handle':'' }
                     ,{'name':'bedtime5_lights_out_bert',  'phase':'1', 'subsec':'180', 'kid':'bert',  'time':'', 'echo':'', 'def':'bedtime5_lights_out'    , 'handle':'' }
                     ,{'name':'bedtime5_lights_out_ernie', 'phase':'1', 'subsec':'180', 'kid':'ernie', 'time':'', 'echo':'', 'def':'bedtime5_lights_out'    , 'handle':'' }
                     ,{'name':'bedtime5_lights_out_bert',  'phase':'2', 'subsec':'20',  'kid':'bert',  'time':'', 'echo':'', 'def':'bedtime5_lights_out'    , 'handle':'' }
                     ,{'name':'bedtime5_lights_out_ernie', 'phase':'2', 'subsec':'20',  'kid':'ernie', 'time':'', 'echo':'', 'def':'bedtime5_lights_out'    , 'handle':'' }
                     ,{'name':'bedtime5_lights_out_bert',  'phase':'3', 'subsec':'0',   'kid':'bert',  'time':'', 'echo':'', 'def':'bedtime5_lights_out'    , 'handle':'' }
                     ,{'name':'bedtime5_lights_out_ernie', 'phase':'3', 'subsec':'0',   'kid':'ernie', 'time':'', 'echo':'', 'def':'bedtime5_lights_out'    , 'handle':'' }
                     ]

        for c in self.ctl:
            self.log("-INITIALIZING:  %s for %s (phase %s)" %(c['name'], c['kid'], c['phase']))
            self.load_callback(c)

            # listeners for changes in input_select and input_datetime controls:
            self.listen_state(self.change_detected, 'input_select.%s_echo' %c['name'], ctl=c)
            self.listen_state(self.change_detected, 'input_datetime.%s_time' %c['name'], ctl=c)


    # ----------------------------------------------
    # HELPER FUNCTIONS
    # ----------------------------------------------
    def change_detected(self, entity, attributes, old, new, kwargs):
        if new is not None and new is not old:
            self.log("-CHANGE DETECTED:  %s" %entity)
            c = kwargs['ctl']
            self.cancel_timer(c['handle'])
            self.load_callback(c)

    def load_callback (self, ctl):
        c = ctl
        # pull time and echo, and update the control record
        tm = self.get_state("input_datetime.%s_time" %c['name'] )
        c['time'] = dt.datetime.strptime(tm, "%H:%M:%S").time()
        c['echo'] = self.get_state("input_select.%s_echo" %c['name'] )[:1].lower()

        self.log("---Loading control dictionary: %s" %c)
        c['handle'] = self.run_daily(getattr(self,c['def']), self.subtractSecs(c['time'],c['subsec']), ctl=c )
        self.log("---Initialized with Handler:   %s" %c)

    def subtractSecs(self, tm, secs=0):
        import datetime as dt
        if secs==0:
            return tm
        else:
            fulldate = dt.datetime(100, 1, 1, tm.hour, tm.minute, tm.second)
            fulldate = fulldate - dt.timedelta(seconds=int(secs))
            return fulldate.time()

    def get_skip(self):
        skip = ''
        if self.get_state("input_boolean.bedtime_active_bert")=='off': skip = skip + 'b'
        if self.get_state("input_boolean.bedtime_active_ernie")=='off': skip = skip + 'e'
        return skip

    def automations_active(self):
        return self.get_state("input_boolean.automations_active")=='on'


    # ----------------------------------------------
    # ANNOUNCEMENTS AND OTHER LIGHT AUTOMATIONS
    # ----------------------------------------------
    def bedtime1_go_upstairs (self, kwargs):
        if self.automations_active():
            msg = self.prefix + "Time to get ready for bed.  Say goodnight to Oompa and head upstairs to your rooms."
            c = kwargs['ctl']
            self.log("--Announcing:  %s \n---%s" %(msg,c))
            self.call_service("python_script/alexa_say", where=c['echo'], msg=msg, skip=self.get_skip())
            self.turn_on("light.zooz_zen22_dimmer_v2_level_2", brightness=70) # stairs light

    def bedtime2_get_ready (self, kwargs):
        if self.automations_active():
            msg = self.prefix + "You should be in your rooms, getting ready for bed.  bert, please take a shower. ernie, shower if you need, otherwise please get your pajamas on, comb hair and brush teeth."
            c = kwargs['ctl']
            skip = self.get_skip()
            self.log("--Announcing:  %s \n---%s" %(msg,c))
            self.call_service("python_script/alexa_say", where=c['echo'], msg=msg, skip=skip)
            if 'b' not in skip:
                self.turn_on("switch.berts_light")
            if 'e' not in skip:
                self.turn_on("switch.ernies_light")

    def bedtime3_ready_for_bed (self, kwargs):
        if self.automations_active():
            msg = self.prefix + "You should be ready for bed now, with your pajamas on and teeth brushed.  Make sure you have your eyemask, grab a book and start heading to bed.  Reading time starts in a few minutes."
            c = kwargs['ctl']
            skip = self.get_skip()
            self.log("--Announcing:  %s \n---%s" %(msg,c))
            self.call_service("python_script/alexa_say", where=c['echo'], msg=msg, skip=skip)
            if 'b' not in skip:
                self.turn_on("switch.4336784084f3eb5d2039") # bert's  bed light
                self.turn_on("switch.4336784084f3eb5d6bae") # bert's sounds
            if 'e' not in skip:
                self.turn_on("switch.4336784084f3eb5d5bef") # ernie's sounds
                self.turn_on("switch.4336784084f3eb5d14de") # ernie's bed light
            self.turn_on("light.zooz_zen22_dimmer_v2_level_2", brightness=30) # stairs light

    def bedtime4_read_books (self, kwargs):
        if self.automations_active():
            msg = self.prefix + "Time to read books.  Snuggle into bed and get comfortable. Mom and Dad love you very much."
            c = kwargs['ctl']
            skip = self.get_skip()
            self.log("--Announcing:  %s \n---%s" %(msg,c))
            self.call_service("python_script/alexa_say", where=c['echo'], msg=msg, skip=skip)
            if 'b' not in skip:
                self.turn_off("switch.berts_light")
                self.turn_off("switch.berts_overhead_light")
            if 'e' not in skip:
                self.turn_off("switch.ernies_light")
                self.turn_off("switch.ernies_overhead_light")

    def bedtime5_lights_out (self, kwargs):
        if self.automations_active():
            c = kwargs['ctl']
            skip = self.get_skip()
            if c['phase'] == '1':
                msg = "Lights out in 3 minutes.  Find your bookmark and a good stopping point for the night."
                self.log("--Announcing:  %s \n---%s" %(msg,c))
                self.call_service("python_script/alexa_say", where=c['echo'], msg=msg, skip=skip)
                if 'b' not in skip:
                    self.turn_off("switch.berts_shower_light")
                if 'e' not in skip:
                    self.turn_off("switch.ernies_shower_light")
            if c['phase'] == '2':
                msg = "Dear %s. Lights out in a few seconds.  Place your bookmark and set down your book." %c['kid']
                self.log("--Announcing:  %s \n---%s" %(msg,c))
                self.call_service("python_script/alexa_say", where=c['echo'], msg=msg, skip=skip)
            if c['phase'] == '3':
                msg = "Sweet %s. Time for lights out.  Mom and Dad love you very much!" %c['kid']
                self.log("--Announcing:  %s \n---%s" %(msg,c))
                self.call_service("python_script/alexa_say", where=c['echo'], msg=msg, skip=skip)
                if c['kid'] == "bert" and 'b' not in skip:
                    self.turn_off("switch.4336784084f3eb5d2039") # bert's  bed light
                if c['kid'] == "ernie" and 'e' not in skip:
                    self.turn_off("switch.4336784084f3eb5d5bef") # ernie's bed light