AppDaemon cancel_timer() confusion

Hi,

I have some trouble wrapping my head around the cancel_timer().

This works:

"""
Warn me if the espresso machine is left "on" for more than 45 minutes.
Notify every 15 minutes.
After 1.5 hours, turn off and notify.
Set times is app config (apps.yaml)
"""

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

class ApplianceStatus(hass.Hass):


    def initialize(self):
        """
        Initialize the timers and set listen_state
        """

        self.starttime = datetime.datetime.now()
        self.timer = None
        self.listen_state(self.StartTimer,"switch.switch")


    def StartTimer(self, entity, attribute, old, new, kwargs):
        """
        Cancel timer if entity is turned offself.
        Otherwise note the time, and start the loop (SendNotification)
        """

        if new == "off":
            self.cancel_timer(self.timer)

        else:

            self.starttime = datetime.datetime.now()
            self.timer = self.run_in(self.SendNotification,self.args["start_after"])


    def SendNotification(self, kwargs):
        """
        Cancel timer if entity is turned offself.
        Otherwise notify me about leaving the switch on every "time_between_notifications" secondsself.
        After "end_after" seconds, automatically turn off, and notify me.
        """

        delta = datetime.datetime.now() - self.starttime
        seconds = int(datetime.timedelta.total_seconds(delta))
        minutes = round(seconds/60)

        self.log(str(minutes) + " " + str(seconds)) # for troubleshooting

        if self.get_state('switch.switch') == 'off':
            self.cancel_timer(self.timer)

        elif seconds < self.args["end_after"]:

            self.call_service("notify/home_aephir_bot", message="Espresso machine has been on for " + str(minutes) + " minutes", data={"inline_keyboard":"Turn Off:/espresso_off, I Know:/removekeyboard"})
            self.log("Espresso machine has been on for " + str(minutes) + " minutes")
            self.cancel_timer(self.timer)
            self.run_in(self.SendNotification,self.args["time_between_notifications"])

        else:

            if self.args["switch_off"]:
                self.turn_off(self.args["entity"])
                switchofftext = ", i turned it off."
                self.turn_off('switch.switch')
            else:
                switchofftext = "."
            self.call_service("notify/home_aephir_bot", message="Espresso machine has been on for " + str(minutes) + " minutes" + switchofftext, data={"inline_keyboard":"Turn back on:/espresso_on, OK, thanks!:/removekeyboard"})
            self.log("Espresso machine has been on for " + str(minutes) + " minutes" + switchoffftext,)
            self.cancel_timer(self.timer)

But this does not (only change being what is commented out in the SendNotification).

"""
Warn me if the espresso machine is left "on" for more than 45 minutes.
Notify every 15 minutes.
After 1.5 hours, turn off and notify.
Set times is app config (apps.yaml)
"""

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

class ApplianceStatus(hass.Hass):


    def initialize(self):
        """
        Initialize the timers and set listen_state
        """

        self.starttime = datetime.datetime.now()
        self.timer = None
        self.listen_state(self.StartTimer,"switch.switch")


    def StartTimer(self, entity, attribute, old, new, kwargs):
        """
        Cancel timer if entity is turned offself.
        Otherwise note the time, and start the loop (SendNotification)
        """

        if new == "off":
            self.cancel_timer(self.timer)

        else:

            self.starttime = datetime.datetime.now()
            self.timer = self.run_in(self.SendNotification,self.args["start_after"])


    def SendNotification(self, kwargs):
        """
        Cancel timer if entity is turned offself.
        Otherwise notify me about leaving the switch on every "time_between_notifications" secondsself.
        After "end_after" seconds, automatically turn off, and notify me.
        """

        delta = datetime.datetime.now() - self.starttime
        seconds = int(datetime.timedelta.total_seconds(delta))
        minutes = round(seconds/60)

        self.log(str(minutes) + " " + str(seconds)) # for troubleshooting

        # if self.get_state('switch.switch') == 'off':
        #     self.cancel_timer(self.timer)

        elif seconds < self.args["end_after"]:

            self.call_service("notify/home_aephir_bot", message="Espresso machine has been on for " + str(minutes) + " minutes", data={"inline_keyboard":"Turn Off:/espresso_off, I Know:/removekeyboard"})
            self.log("Espresso machine has been on for " + str(minutes) + " minutes")
            self.cancel_timer(self.timer)
            self.run_in(self.SendNotification,self.args["time_between_notifications"])

        else:

            if self.args["switch_off"]:
                self.turn_off(self.args["entity"])
                switchofftext = ", i turned it off."
                self.turn_off('switch.switch')
            else:
                switchofftext = "."
            self.call_service("notify/home_aephir_bot", message="Espresso machine has been on for " + str(minutes) + " minutes" + switchofftext, data={"inline_keyboard":"Turn back on:/espresso_on, OK, thanks!:/removekeyboard"})
            self.log("Espresso machine has been on for " + str(minutes) + " minutes" + switchoffftext,)
            self.cancel_timer(self.timer)

The latter keeps sending notifications even after the switch is turned off. Could anyone explain the cancel_timer() a bit more in detail than in the documentation? Why does the cancel_timer() in the StartTimer not cancel the timer when the switch turns off?

1 Like

Hello,

Not that it doesn’t work, but you most likely running multiple timers without knowing. It is possible the callback is called different times, as this could happen and for that reason, you have different times being executed but you have access to only the last one.

Normally I wouldn’t advise you do this

if new == "off":
     self.cancel_timer(self.timer)
else:
    self.starttime = datetime.datetime.now()
    self.timer = self.run_in(self.SendNotification,self.args["start_after"])

Instead have it as

if new == "off":
     self.cancel_timer(self.timer)
     self.timer = None
else:
    if self.timer != None: #meaning a timer is definitely not running
         self.cancel_timer(self.timer)
    self.starttime = datetime.datetime.now()
    self.timer = self.run_in(self.SendNotification,self.args["start_after"])

This will ensure you definitely have but one timer running at every point in time for the switch. Also you will need to add the following

def SendNotification(self, kwargs):
    self.timer = None

And lastly the reason why the first one might have worked is that since you have over written the handle for the possible first timer in self.timer as explained above, it is actually the first one that is executed and so

if self.get_state('switch.switch') == 'off':
      self.cancel_timer(self.timer)

Is cancelling the second one and that’s why you thought it was working.

Hope this helps?

Regards

1 Like

pretty well explained, but changing the sendnotification is not really needed if you only call it from that 1 callback.
this should be enough

if new == "off":
     self.cancel_timer(self.timer)
     #self.timer = None # no need for this cancel timer should set self.timer to None anyway
else:
    if self.timer != None: #meaning a timer is definitely not running
         self.cancel_timer(self.timer)
    self.starttime = datetime.datetime.now()
    self.timer = self.run_in(self.SendNotification,self.args["start_after"])

the original code should work if switch.switch is exactly 1 times switched to on.
as soon as it is switched to on twice there are 2 timers stored in the same handler.
another option would be:

if new == "off" and old == "on":
     self.cancel_timer(self.timer)
else:
    self.starttime = datetime.datetime.now()
    self.timer = self.run_in(self.SendNotification,self.args["start_after"])
self.log("the switch was already on i didnt reset the timer")

That helps, but then a few new questions pop up :slight_smile:

So I can have several timers running under the same handle (self.timer)? Am I then correct to assume that I should cancel the timer (immediately?) before doing anything that starts with self.timer = self.run_in to avoid having multiple timers running on the same handle (self.timer)?

I don’t completely understand the difference between flipping the switch once, and turning it on multiple times. Wouldn’t this part of StartTimer:

        if new == "off":
            self.cancel_timer(self.timer)

make sure the timer is canceled, and essentially “reset” and ready to start completely over from scratch when it is turned on again?

And not for a practical reason, just to understand better, is there any reason to have

else:
    if self.timer != None: #meaning a timer is definitely not running
         self.cancel_timer(self.timer)

instead of just

else:
    self.cancel_timer(self.timer)

if you say that self.timer === None if the timer isn’t running?

no it wont.
every time you turn the switch on (without turning it off in between) a new timer is started
you can turn a switch on, when it is already on. thats the problem.
so clicking on it somewhere could start the service twice for instance.
thats why you need to check if it was on before and off now when you want to use that.
or just check if a timer is running before you do the code
like:

if self.time != None:
  # a timer is running so we can cancel it
  self.cancel_timer(self.timer)
  if new == "off":
    # do other stuff
  else:
    self.timer = self.run_in(...)

you use

else:
    if self.timer != None: #meaning a timer is definitely not running
         self.cancel_timer(self.timer)

because you cant cancel a timer that doesnt exist. that would cause an error.
so you first check if a timer is running.

and indeed when you use a single var like self.timer you can only store 1 timer name in there.
to make it more visual this line:

self.timer = self.run_in(...)

means:

start a run_in timer and store the name from the timer inside the global variable with the name timer

you could also use:

my_own_timer_name = self.run_in(...)
self.log("the name from the timer that is running now is: {}".format(my_own_timer_name))
1 Like

OK, that makes sense. It probably won’t be applicable with multiple “on” without “off” in between in my setup, but good to know for other applications!

So, let’s see if I’m starting to understand it all. This seems to work:

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

class ApplianceStatus(hass.Hass):


    def initialize(self):
        """
        Initialize the timers and set listen_state.
        """

        self.starttime = datetime.datetime.now()
        self.timer = None
        self.listen_state(self.StartTimer,"switch.switch")


    def StartTimer(self, entity, attribute, old, new, kwargs):
        """
        Cancel timer if entity is turned off.
        Otherwise note the time, and start the loop (SendNotification)
        """

        if new == 'off':
            self.cancel_timer(self.timer) # Cancel for any state change of 'switch.switch'. If I call 'SendNotification' from elsewhere, make this is cancele

        else:
            if self.timer != None:
                self.cancel_timer(self.timer)
            self.starttime = datetime.datetime.now()
            self.timer = self.run_in(self.SendNotification,self.args["start_after"])
        # self.log("the switch was already on i didnt reset the timer")


    def SendNotification(self, kwargs):
        """
        Notify me about leaving the switch on. Repeat every "time_between_notifications" seconds.
        After "end_after" seconds, automatically turn off, and notify me.
        Remember to cancel timer before each recursive callback, as well as after last action ending the loop.
        """

        delta = datetime.datetime.now() - self.starttime
        seconds = int(datetime.timedelta.total_seconds(delta))
        minutes = round(seconds/60)

        self.log("Espresso on for " + str(minutes) + " minutes and " + str(seconds) + " seconds.") # for troubleshooting

        if seconds < self.args["end_after"]:

            self.call_service("notify/home_aephir_bot", message="Espresso machine has been on for " + str(minutes) + " minutes", data={"inline_keyboard":"Turn Off:/espresso_off, I Know:/removekeyboard"})
            self.log("Espresso machine has been on for " + str(minutes) + " minutes")
            if self.timer != None:
                self.cancel_timer(self.timer)
            self.timer = self.run_in(self.SendNotification,self.args["time_between_notifications"])

        else:

            if self.args["switch_off"]:
                self.turn_off(self.args["entity"])
                switchofftext = ", i turned it off."
                self.turn_off('switch.switch')
            else:
                switchofftext = "."
            self.call_service("notify/home_aephir_bot", message="Espresso machine has been on for " + str(minutes) + " minutes" + switchofftext, data={"inline_keyboard":"Turn back on:/espresso_on, Thanks!:/removekeyboard"})
            self.log("Espresso machine has been on for " + str(minutes) + " minutes" + switchoffftext,)
            if self.timer != None:
                self.cancel_timer(self.timer)

It seems to me like it:

  1. Cancels the timer at various points (too many? Are some redundant? I might have been overly cautious now :smile: )
  2. StartTimer runs the SendNotification if the switch is turned from off to on.
  3. StartTimer does not run SendNotification if the switch is turned to on form anything not off. The already running loop continues instead.
  4. SendNotification loops, sends expected notifications (and turns off as expected).

So unless the experts can tell me some problem that hasn’t materialized itself yet… (like the issues I didn’t notice in the initial app)?

Is it “just” because I forgot to use self.timer = run_in(), and simply used run_in() in my initial app that it creates several timers? Or are there several instances of the app is running in “parallel”? And each instance has a timer associated with the variable (self.timer)?

inside the sendnotification they are redundant.
that will only be started when the timer is ended, so its None at that point anyway.

it starts the timer if it is turned to on indeed.

indeed it resets the loop. the loop doest continue, but it starts over

no that caused that the timer couldnt be cancelled. you need to know the name from the timer to cancel

only if you have the pycode called from the yaml several times, but they dont interfere with each other

again: run_in starts a timer and gives it a name, and only with that name you can cancel it with cancel_timer.

Ahhh, good point! OK, I’ll delete those.

Again, thanks for the help! I think I’m finally understanding enough about the timers to re-create some other apps I had put on hold :relieved:

1 Like