Struggling with appdaemon time scheduler [SOLVED]

Hi all,
I’ve made a test script, the goal is to turn on two switches, one 90 seconds after the other. The code is below. No matter what I do (using time, datetime or seconds), they both start at the same time. Can anyone explain what I am doing wrong?

thanks,
Sean

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


#
# Garden irrigation App
#
# Program by: Sean McGrath
# Created: 03-10-2018
#
# Args: valve
#



class irrigationValves(hass.Hass):
  def initialize(self):
    self.log("Starting {} with valve {}".format(self.name, self.args["valve"]))

    v4 = "switch.in4"
    v5 = "switch.in5"
    allValves = "group.irrigation_valves"
    soon = self.get_now() + datetime.timedelta(seconds=90)
    self.log("Soon = {}".format(soon))
    self.run_at(self.turn_on(v4), soon)
    self.turn_on(v5)

you need to use a callback in self.run_at and not a function.

    self.run_at(self.your_callback, soon)
  def your_callback(self.kwargs):
    self.turn_on("switch.in4")
1 Like

Thank you, Rene – that is very helpful. I’ll do that tonight when I get home. I didn’t realize that a callback was that specific a thing and now am re-reading the documentation.

It does bring another question, though. In the examples, it often has something like:
self.handle = self.run_in(callback, 120, random_start = -60, **kwargs)

where I am using something more like:
self.run_in(callback, 120, random_start = -60, **kwargs)

What does the self.handle do, and will I get into trouble if I don’t use it?

cheers,
Sean

1 Like

From the documentation for run_in

Returns

A handle that can be used to cancel the timer.

Like any language, you can ignore the return value if you don’t have any need to cancel the timer.

1 Like

Thanks for the quick reply, gp. I guess the issue is that I don’t know any programming languages (except one class in Pascal thirty years ago). :slight_smile:

I’ve tried googling it, also wrt Python, but haven’t yet found an explanation that resonates. Is handle a reserved word, or just a convention? I.e., sould I get the same result if I used:

self.handlegrip = self.run_in(callback, 120, random_start = -60, **kwargs)

Yes, the return code from a function is a value, and it is stored in a variable. It doesn’t matter what that variable is called - assuming the name complies with the python rules.

The other point is that the self in the variable name indicates that the variable is stored as part of the current object. Now I have written that sentence I realize it probably doesn’t really make sense to you - you will need to look up. I think the python tutorial might be a good place to start. At least it has a section on classes and objects.

You use the handle if you might want to, say, cancel the second valve turning on because of something else.
As an example I use it to cancel a callback that notifies me that my laundry is ready every 30 minutes when the magnet sensor on the washing machine tells it that I’ve already emptied it.

Thanks both of you, that is helpful. Funny enough, the objects part I do understand; classes and objects and such are clear.

Hi birds, can you give me an example of the code you use to cancel the callback when the sensor shows it’s empty? I think I will have a very similar use case.

cheers,
Sean

better look at your own code.

    self.listen_state(self.another_callback,"sensor.motion")
    self.handle = self.run_at(self.your_callback, soon)
  def your_callback(self.kwargs):
    self.turn_on("switch.in4")
  def another_callback(self,entity, new,old,attributes,kwargs):
    self.cancel_timer(self.handle

now the light in4 wont be turned on if there is motion between now and soon

but i think you want to read appdaemon for beginner

Thanks, Rene. I found cancel_timer in the API doc, so everything is clear now. I hadn’t made the link to handles.

Yes, I did read your tutorials – they were quite helpful. Re-reading them is not a bad idea now that I understand a bit more. In any case, they don’t cover handles or how to use a handle to cancel a callback.

you are right.
i think its time to write a few more :wink:

2 Likes

For the record, this is indeed the correct code:

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


#
# Garden irrigation App
#
# Program by: Sean McGrath
# Created: 03-10-2018
#
# Args: valve
#



class irrigationValves(hass.Hass):
  def initialize(self):
    self.log("Starting {} with valve {}".format(self.name, self.args["valve"]))

    v5 = "switch.in5"
    allValves = "group.irrigation_valves"
    soon = self.get_now() + datetime.timedelta(seconds=90)
    self.log("Soon = {}".format(soon))
    self.run_at(self.switchValve, soon)
    self.turn_on(v5)

  def switchValve(self, kwargs):
    v4 = "switch.in4"
    self.turn_on(v4)

Thanks all for the help. I now have version 0.2 of my irrigation program working :slight_smile:

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

#
# Garden irrigation App
#
# Program by: Sean McGrath
# Created: 03-10-2018
#
# Args: totalValves, onHour, onMinute, valveDuration
# Contraints: constrain_days
#


class irrigationValves(hass.Hass):
  def initialize(self):
    self.log("Starting {}".format(self.name))


    #
    # Time
    #

    now = self.get_now()
    nowDate = now.date()
    nowTime = now.time()

    onceDT = now + datetime.timedelta(seconds=5) # Convert to time for testing with run_once

    dailyTime = datetime.time(self.args["onHour"], self.args["onMinute"], 0) # Use this for running with run_daily
    dailyDT = datetime.datetime.combine(nowDate, dailyTime)

#    initialDT = dailyDT # For running with run_daily
    initialDT = onceDT # For testing with run_once

    self.log("Check initial {} and daily {}".format(initialDT, dailyTime))

    self.handle = self.run_once(self.noRain, nowTime, initialDT = initialDT)

  def noRain(self, kwargs):

    valveDuration = int(self.args["valveDuration"])
    totalValves = int(self.args["totalValves"])
    precipitation=float(self.entities.sensor.buienradar_precipitation.state)
    initialDT = kwargs["initialDT"]

    self.log("We had {} mm of rain today".format(precipitation))

    if precipitation == 0:
      self.run_once(self.switchOff, initialDT.time())
      for j in range (1, 1 + totalValves, 1):
        valveDT = initialDT + datetime.timedelta(seconds=(j + (j-1) * valveDuration)) # Seconds for run_once, minutes for daily
        valveTime = valveDT.time()
        offDT = initialDT + datetime.timedelta(seconds=(j + j * valveDuration)) # Seconds for run_once, minutes for daily
        offTime = offDT.time()
        self.run_once(self.switchOn, valveTime, j = j)
        self.run_once(self.switchOff, offTime)


  def switchOn (self, kwargs):
    thisValve = "switch.in"+str(kwargs["j"])
    self.turn_on(thisValve)

  def switchOff (self, kwargs):
    allValves = "group.irrigation_valves"
    self.turn_off(allValves)

  def terminate(self):
    allValves = "group.irrigation_valves"
    self.log("Turning off {}".format(allValves))
    self.turn_off(allValves)
    self.log("Terminating {}".format(self.name))

Sean

why use run_once in initialise?
now the app needs to be restarted every time.

Yes, that is why it’s only v 0.2. It is much easier to test that way, but everything is set up to move to run_daily.

create an input_boolean and use it as constraint.
that way you can stop and start the app whenever you like

a few remarks:
run_once and run_daily need a time that is not in the past.
you get the time in a var, then do some stuff and after that you use that time.
that way the time is always in the past.
it will work in a lot of cases when everything is very fast, but 1 short moment of delay and you will get an error.

you calculate your initialDT in your initialise and use it in your calback.
but if you use run_daily that initialDT will never change after you start the app.
that results in that the second day the time will be in the past and the app wil stop working.

in your norain CB you calculate the time and use run_once.
way more easy it is to use run_in. you dont need the date, just the amount of secs

Thanks for the comments, I’m thinking through them.

I use run_once instead of run_in because I want all the calculations to work the same for run_daily.

you get the time in a var, then do some stuff and after that you use that time.
that way the time is always in the past.

Actually, I get the time and add five seconds to it (that is the original onceDT, which is the basis for all calculations). Do you think that is not long enough? I started with ten seconds, but so far five has seemed plenty. Also, when I move to run_daily, as far as I understand a time in the past is ok – worst case is that it starts a day later. Is that incorrect?

you calculate your initialDT in your initialise and use it in your calback.
but if you use run_daily that initialDT will never change after you start the app.
that results in that the second day the time will be in the past and the app wil stop working.

Oops. Thank you. I know I need to look into run_daily a bit more, as I vaguely remember there is a reset each day. I guess this brings the effect you mention. I’ll check it.

in your run_once you use nowtime and not onceDT

you dont want to use run_daily in your callback only in your initialise.
in a callback you can use run_in instead of run_once.

the callback will be triggered daily by the run_daily from the initialise

run_daily doesnt reset the app.
it just triggers the callback daily, so all values that you set in the initialise will stay the same untill you restart appdaemon or change something in the app.

just an example:

  def initialize(self):
    onceDT = datetime.time(19, 0, 0)
    self.run_daily(self.callback1, onceDT)
  def callback1(self, kwargs):
    self.run_in(self.callback2,60)
    self.run_in(self.callback3,120)
  def callback2(self,kwargs):
    self.turn_on("switch.switch1")
  def callback3(self,kwargs):
    self.turn_on("switch.switch2")

this will result in that switch1 wil be turned on every day at 19:01:00 and switch2 every day at 19:02:00

in your run_once you use nowtime and not onceDT

Oh, sorry. Yes, I see what you mean. Yes, that is on purpose and will be changed for run_daily. You are right. For the rest, I see your point – I’ll need to think through run_daily a bit more.

1 Like