Variable names in reusable functions

I have an app that I use to turn off lights after a certain period of inactivity on my motion sensor. The code assigns a handle to the run_in function - like this:

self.x = self.run_in(self.light_off, 10)

I use the variable self.x to cancel the run_in if there is subsequent motion at some point - like this:

self.cancel_timer(self.x)

Here’s my question - since I use this function for several different lights and there can be multiple instances running - is the variable self.x unique in each instance where it is called? I don’t want another light that I am monitoring to be affected from another instance of the app. Hope this makes sense.

Thanks!

Tom

Hi Tom!

That’s a good question! There’s a few key things to remember here - and forgive me if I’m being overly simple. There are many here that are new to Python in general.

  • self.run_in is a function
  • self.run_in(self.light_off, 10) will call the function and return a result
  • self is just a silly name we use to say “this instance”, you could honestly say replace it with any other valid python variable name. :slight_smile:

So, what’s really going on when you call self.x = self.run_in(self.light_off, 10)?

Python goes out and executes the code under run_in with the params you gave it and returns an id, which you can cancel the event from. This is what self.x is. If you were put log self.x in the line right after, you’d see exactly that, it’s just an id code! If you were to do the same line twice in a row…

self.x = self.run_in(self.light_off, 10)
self.x = self.run_in(self.light_off, 11)

then all you’re going to end up doing is overwriting self.x with the second id. x is just a name/reference to some string (the handle id) that’s returned by the call to run_in. Since it’s just a name, you can redefine it as many times as you want… but you’ll lose the old id!

Now, remember what I said about self? It’s a name that just means “this instance”. So if you’re going to only have 1 of these per [instance of that] app, then you’ll be totally fine calling it self.x wherever you go.

Toy examples below!


Bad. :frowning:

# hellov1.py
class HelloWorld:

    def initialize(self):
        self.x = self.run_in(self.light_off, 10)
        self.log(self.x)  # == '1a88576f-be12-473b-8dfa-3caf124ed526'
        self.run_in(self.do_stuff, 3)

    def do_stuff(self, kwargs):
        self.x = self.run_in(self.light_off, 7)
        self.log(self.x)  # == '691d2f9c-119c-49fe-84ca-bcc6bfe51ca9'

    def light_off(self, kwargs):
        ...

Good. :slight_smile:

# hellov2.py
class HelloWorld:
    def initialize(self):
        self.x = self.run_in(self.light_off, 10)
        self.log(self.x)  # == '1a88576f-be12-473b-8dfa-3caf124ed526'

    def do_stuff(self, kwargs):
        # do cool stuff here
        self.log(self.x)  # == '1a88576f-be12-473b-8dfa-3caf124ed526'

    def light_off(self, kwargs):
        ...

class GoodbyeWorld:
    def initialize(self):
        self.x = self.run_in(self.light_off, 10)
        self.hello_world_app = self.get_app('hello_world')
        self.run_in(self.do_other_stuff, 3)

    def do_other_stuff(self, kwargs):
        self.log(self.x)                  # == '29859334-140f-4003-8e10-87a041c47d9c'
        self.log(self.hello_world_app.x)  # == '1a88576f-be12-473b-8dfa-3caf124ed526'

    def light_off(self, kwargs):
        ...

Hope this helps!

- SN

1 Like

SN - Thank you for taking the time to post such a thorough answer to my question!

So using your example (modified) from above - like this:

# hellov2.py
class HelloWorld:
    def initialize(self):
        self.x = self.run_in(self.light_off, 10)
        self.log(self.x)  # == '1a88576f-be12-473b-8dfa-3caf124ed526'
        self.listen_event(self.cancel_timer, some_event)  <<== if this event happens in the future, I want to cancel the run_in above

    def cancel_timer(self, kwargs):  
        # do cool stuff here
        self.cancel_timer(self.x)   <<<=== Is self.x going to work even though HelloWorld has been called by others?

    def light_off(self, kwargs):
        ...

What if I have two different apps defined in my appdaemon.yaml file - like this:

Hello1:
  module: hellov2
  class: HelloWorld
Hello2:
  module: hellov2
  class: HelloWorld

When “some_event” fires at some point in the future, will self.x still be the correct handle for the cancel_timer function? Or will it get overwritten by subsequent calls of the hellov2 module?

Thanks again for your help!

Tom

Hello1 and Hello2 are different Apps - which means they both get their own unique self, so self.x in hello1 is different to self.x in hello2 so no, it won’t get overwritten.

1 Like

Perfect! Thank you both for helping this noob!

BTW … I have just about completed moving all of my automations to Appdaemon - absolutely love it to the yaml way of doing things. Thank you for creating this new component!

2 Likes

Hi SN,

To give me peace of mind and help me narrow in to why timers aren’t getting cancelled.
If for some reason:
self.x = self.run_in(self.light_off, 10)
is being initiated multiple times (due to kids pressing a button multiple times…given up on trying to tell them not to)

There is only every one instance of self.x initiated, in addition any previous self.x timers gets overwritten. Only therefore need to execute single cancel command:
self.cancel_timer(self.x)

Have a got this right??

Thanks,

Rob.

That’s not right, no. Every callback will be added and they will all run according to schedule, all you are doing is overwriting your copy of the handle.

The correct way to handle this is to call the cancel_timer() command for self.x before each time you create a new callback.

Hi aimc,
i’ve also problems with cancel_timer()…
i have a light that i turn on and run in for 5 minute with motion sensor: i’d like to cancel timer if someone turn the light off (for example by the front end app or by the switch, it’s a ikea tradfri light).
i have an app that turn on the light and save the timer so when a movement is detected the timer i saved that instance.
but if i try to retrieve the id of that timer in another app i get error “AttributeError: ‘NoneType’ object has no attribute ‘tmr_ingresso’”.
i also tryed to cancel the timer in the same app where i create my timer (for example if i detect 2 movement i’d like to use only the second timer and cancel the first one) but i have the same error before.
this is my code of the same .py:
class movimento_pir_ingresso(appapi.AppDaemon):

  def initialize(self):
#    self.log("Rilevato movimento ingresso initialize")
    self.listen_state(self.motion, "binary_sensor.pir_ing_est", new = "on")

  def motion(self, entity, attribute, old, new, kwargs):
#    self.log(Rilevato movimento ingresso in motion")
    dalle_ore = int(self.get_state("input_datetime.inizio_monitoraggio_pir_ing_est")[:2])
    alle_ore = int(self.get_state("input_datetime.fine_monitoraggio_pir_ing_est")[:2])
    adesso = int(datetime.now().strftime("%H"))
#    self.log("luce:{}".format(self.get_state("light.luce_ingresso")))
      a_casa = False
    if (adesso >= dalle_ore) or (adesso <= alle_ore) or (a_casa == False):
      self.notify("Rilevato movimento ingresso", title = "PIR ingresso", name = "notify_hass")
    if (self.sun_down()): # and self.get_state("light.luce_ingresso")=='off'):
      self.movimento_pir_ingresso_app = self.get_app('movimento_pir_ingresso')  <-----i'm retrieving the previous timer
      self.log("timer_ingresso esecuzione precedente:{}".format(str(self.movimento_pir_ingresso_app.tmr_ingresso)))
      self.turn_on("light.luce_ingresso")
      self.tmr_ingresso = self.run_in(self.light_off, 300)
      self.turn_on("light.gateway_light_34ce00fa7117", rgb_color= [255,0,0])
      self.log("timer_ingresso: {}".format(str(self.tmr_ingresso)))
      self.flashcount = 0
      self.flashing_gateway = self.run_in(self.flash_warning, 1)
      self.log("timer_gateway: {}".format(str(self.flashing_gateway)))
      
  def light_off(self, kwargs):
    self.turn_off("light.luce_ingresso")
  
  def flash_warning(self, kwargs):
    self.toggle("light.gateway_light_34ce00fa7117")
    self.flashcount += 1
    if self.flashcount < 60:
      self.run_in(self.flash_warning, 1)
    self.turn_off("light.gateway_light_34ce00fa7117")

class azzera_timer(appapi.AppDaemon):
  def initialize(self):
    self.listen_state(self.azzera_tmr,"light.luce_ingresso", new = "off")
  def azzera_tmr(self, entity, attribute, old, new, kwargs):
    self.movimento_pir_ingresso_app = self.get_app('movimento_pir_ingresso')
    self.log("timer_ingresso prima:{}".format(str(self.movimento_pir_ingresso_app.tmr_ingresso)))
    self.cancel_timer(self.movimento_pir_ingresso_app)
    self.log("timer_ingresso dopo:{}".format(str(self.movimento_pir_ingresso_app.tmr_ingresso)))

self.cancel_timer is to canncel a time based function like run_in or run_every.
you cant stop an app with that, because an app isnt an timer.

if you want to stop a timer, you first need to set a timer (function)

with get_app you get the variables and functions from the app.
so if you did set a variable like “self.this_is_anything” in app “something” you could use

something = self.get_app(“something”)
some_value = something.this_is_anything

and even if you have a function “do_something” in that same app, you could use

something = self.get_app(“something”)
something.do_something()

so the get_app function can be compared to the import function.

Hi ReneTode, i understand.
In my app with the above instruction i set a 5 minute timer when motion is detected. the problem is that if i detect a new motion after 4 minutes, another thread start, turn on my light and set a new 5 minutes timer but the light will turn off after 1 minute because the previous timer in the previous thread turn it off.
A “bad” way to solve it is to test before setting timer if light is already on, in this case i will not have 2 timers but my timer will be the first one so the light will turn off after 5 minutes af the first motion detected and not the last.
The “best” (for me) way is to cancel the previous timer and set a new one; the problem is that in my new thread (when new motion is detected) i’m not able to get the previous timer, i tryed with
self.movimento_pir_ingresso_app = self.get_app('movimento_pir_ingresso')
to get the previous app info, than i tryed to print the timer in my log with
self.log("timer_ingresso esecuzione precedente:{}".format(str(self.movimento_pir_ingresso_app.tmr_ingresso)))
before calling the cancel_timer() that isn’t in my app because i’m trying to log it before cancel.
I hope you can understand what i need or would like to do…

you could take a look at the motion app in examples :wink:
its exactly doing what you want.

when new motion is detected there is NO new thread.
your app will stay active as long as its initialized.

so a new motion wont start a new thread, and so you can use the cancel timer in that case.

something like:

init:
  listen_state(do_something,sensor.motion, state=on)

def do_something(...):
  self.reset_timer()

def reset_timer():
  if self.timer_handle != none:
    self.cancel_timer(self.timer_handle)
  self.timer_handle = self.run_in(self.do_something_else,300)

def do_something_else(...):
  the stuff you want to do

from another app:

test = self.get_app(the first)
test.reset_timer()

ok thanks, i didn’t know that new state “on” of my binary sensor don’t set a new thread but call always the same one until when the first one isn’t released, this evening i will try with your example/help.

i could mistake something but i have again problems…
if self.sun_down():
if self.timer_ingresso == none:
with this error:
AttributeError: ‘movimento_pir_ingresso’ object has no attribute ‘timer_ingresso’
so if there’s no timer there is a problem with the attribute object…
i’ve solved in this way, before check if timer is set i use “try”, if the timer exists there no error so i cancel timer and turn on, if have AttributeError i turn on without che cancel timer
try:
esiste_timer_ingresso = self.timer_ingresso
self.log(“timer_ingresso prima di aver acceso la luce, il vecchio: {}”.format(str(self.timer_ingresso)))
self.cancel_timer(self.timer_ingresso)
self.turn_on(“light.luce_ingresso”)
self.timer_ingresso = self.run_in(self.light_off, 300)
self.log(“timer_ingresso dopo aver acceso la luce, creato il nuovo: {}”.format(str(self.timer_ingresso)))
except AttributeError:
self.turn_on(“light.luce_ingresso”)
self.timer_ingresso = self.run_in(self.light_off, 300)
self.log(“timer_ingresso dopo aver acceso la luce, creato il nuovo: {}”.format(str(self.timer_ingresso)))

set the timer in the app intialize to None
like this:

init():
  self.timer_ingresso = None

in that case the timer variable always exists.
by the way it should be None and not none (sorry for my typo) :wink:

but you still have to check if the timer exists before you cancel it, thats why i did put these lines in

  if self.timer_handle != None:
    self.cancel_timer(self.timer_handle)

with try it will also work, but when there is another error, you wont know it and the function wont work.

1 Like

i tryed with the initialization of the timer and it’s ok, i also think then with “TRY” instruction it will intercept errors because the except is only for AttributeError, btw i used your solution because it seems me better.
many thanks

1 Like