Announcement: AppDaemon 3.0.5

Hi All, this release contains a fix for a security issue in a 3rd party library, no changes to anything else.

1 Like

Hi thanks for the update.
Do you have any roadmap for AppDaemon 4.0? :sweat_smile:

How do I determine which version of AppDaemon I am running? I am currently running in a docker container and the docker file is appended with the “latest” tag. Any ideas?

You can check the currently installed version at this location

/usr/local/lib/python3.6/site-packages inside your docker container.

I run an appdaemon app that runs daily and updates a sensor with the currently installed version. Find below if you are interested.

def set_sensor(self, kwargs: dict) -> None:
    """Set the sensor to the current installed version."""
    subdirs = os.listdir("/usr/local/lib/python3.6/site-packages")
    for subdirname in subdirs:
        if "appdaemon" in subdirname.lower():
            if "info" in subdirname.lower():
                version = subdirname.split("-")[1][:-5]
                self.set_state("sensor.appdaemon_installed",
                               state=version)

drwxr-sr-x 1 root staff 258 Dec 9 13:26 appdaemon

drwxr-sr-x 1 root staff 134 Dec 9 13:26 appdaemon-3.0.2.dist-info

does this indicate I am running version 3.0.2? If so where can I find info about benefits of upgrading to the latest? I ask because it seems that very rarely a time based automation fails to fire.

Nothing big changed since 3.0.2 you can find all the changes here

https://appdaemon.readthedocs.io/en/latest/HISTORY.html

Post your time based automation problem in a new topic and maybe someone might be able to identify an error in your code.

One final question, maybe. Is there a way to use multiple loggers to segment items into separate logger files? I’m hoping this will lead to an answer with the time based automatons. Also can you offer formatting suggestions (right now there is no entry separation) thanks.

I don’t know if there is a build in way for this, but you could simply write the log to a file instead of the log. Why do you need this? Do you have so many log items in your log?
What do you mean with formatting suggestions?

I am trying to use the log file in the place of a print statement, trying document code execution to determine exactly when a function is being executed, so that a can determine what programming errors I may have. Is there a way to create new line characters in the log “\n” or something like that?

Sorry, I don’t get it. Can you please show your code and show where you want to log?

I followed the docker example when I first got AppDaemon running in that example it demonstrated a file system containing a appdaemon/conf with error.log and appdaemon.log in the conf directory. So I have been using that all along (using self.log to put all the items in one long log). Below is a segment of code I have been using. I am very novice and am open to any suggestions you might have. Thank you

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

#
# App to control Pool Main Pump
#
# Args: input_datetime for start and stop times
#

class poolPumpMain(hass.Hass):

  #required function -- must initialize variables
  def initialize(self):

  	#this var is to detect wheather or not a timer / scheduler is active
    self.pool_main_pump_on_timer = None
    self.pool_main_pump_off_timer = None

    startTime = self.get_state(self.args["startTime"])
    startTimeHour = int(startTime[0:2])
    startTimeMinute = int(startTime[3:5])

    stopTime = self.get_state(self.args["stopTime"])
    stopTimeHour = int(stopTime[0:2])
    stopTimeMinute = int(stopTime[3:5])

    on_time = datetime.time(startTimeHour, startTimeMinute, 0)
    off_time = datetime.time(stopTimeHour, stopTimeMinute, 0)

    self.run_daily(self.on_time_cb, on_time)
    self.run_daily(self.off_time_cb, off_time)
    
    #this detects if there has been a change to entity in Home Assistant, and calls the callback when a change has been detected
    self.handle = self.listen_state(self.startTimeChangedCallBack, "input_datetime.pool_pump_start_time")
    self.handle = self.listen_state(self.stopTimeChangedCallBack, "input_datetime.pool_pump_stop_time")

    self.handle = self.listen_state(self.pump_on_cb_via_24_hour_run, "input_boolean.pool_pump_main_24_hour_run", new = "on")
    self.handle = self.listen_state(self.pump_on_cb_via_man_start, "input_boolean.pool_pump_main_start", new = "on")
    self.handle = self.listen_state(self.pump_on_cb_via_time, "input_number.pool_pump_main_run_daily_int", new = "1")
    self.handle = self.listen_state(self.pump_on_cb_low_temperature, "input_boolean.low_outside_temperature", new = "on")

    self.handle = self.listen_state(self.pump_off_via_man_stop_cb, "input_boolean.pool_pump_main_stop", new = "on")
    self.handle = self.listen_state(self.pump_off_via_time_cb, "input_number.pool_pump_main_run_daily_int", new = "0")
    
    
  #this is the main callback to start the pool pump
  #the input_number.pool_pump_main_run_daily_int is set to "2" at the end of the function to ensure there is a change
  #when the on_time_cb or  the off_time_cb is called.  the on_time_cb and off_time_cb are the function which starts 
  #and stops the pump based on time. "0" stops the pump and "1" starts the pump
  def pump_on_cb_via_24_hour_run(self, entity, attribute, old, new, kwargs):
    self.log("Entering pump_on_cb via 24 hour run")
    self.turn_on("switch.pool_main_pump")
    self.log("MQTT command sent to turn on Main Pool Pump")
    self.set_state("input_number.pool_pump_main_run_daily_int", state = 2)

  def pump_on_cb_via_man_start(self, entity, attribute, old, new, kwargs):
    self.log("Entering pump_on_cb via manual start")
    self.turn_on("switch.pool_main_pump")
    self.log("MQTT command sent to turn on Main Pool Pump")
    self.set_state("input_number.pool_pump_main_run_daily_int", state = 2)
  
  def pump_on_cb_via_time(self, entity, attribute, old, new, kwargs):
    self.log("Entering pump_on_cb via time")
    self.turn_on("switch.pool_main_pump")
    self.log("MQTT command sent to turn on Main Pool Pump")
    self.set_state("input_number.pool_pump_main_run_daily_int", state = 2)

  def pump_on_cb_low_temperature(self, entity, attribute, old, new, kwargs):
    self.log("Entering pump_on_cb via low temperature")
    self.turn_on("switch.pool_main_pump")
    self.log("MQTT command sent to turn on Main Pool Pump")
    self.set_state("input_number.pool_pump_main_run_daily_int", state = 2)

  #this is the main callback to stop the pool pump
  #the input_number.pool_pump_main_run_daily_int is set to "2" at the end of the function to ensure there is a change
  #when the on_time_cb or  the off_time_cb is called.  the on_time_cb and off_time_cb are the function which starts 
  #and stops the pump based on time. "0" stops the pump and "1" starts the pump
  def pump_off_via_man_stop_cb(self, entity, attribute, old, new, kwargs):
    self.log("Entering pump_off_cb")
    lowTemp = self.get_state("input_boolean.low_outside_temperature")                   #check to see if there is a danger of freezing
    poolRun24Hour = self.get_state("input_boolean.pool_pump_main_24_hour_run")          #check to see if the 24hour run command has been issued
    self.set_state("input_number.pool_pump_main_run_daily_int", state = 2)              #reset sentinal vaule
    if ((lowTemp == "off") and (poolRun24Hour == "off")):                             #if there is no danger of freezing and the 24hour run
        self.turn_off("switch.pool_main_pump")                                        #command are both off allow the pump to be stopped
        self.log("MQTT command sent to turn off Main Pool Pump via the manual stop command")

  def pump_off_via_time_cb(self, entity, attribute, old, new, kwargs):
    self.log("Entering pump_off_cb")
    lowTemp = self.get_state("input_boolean.low_outside_temperature")                   #check to see if there is a danger of freezing
    poolRun24Hour = self.get_state("input_boolean.pool_pump_main_24_hour_run")          #check to see if the 24hour run command has been issued
    self.set_state("input_number.pool_pump_main_run_daily_int", state = 2)              #reset sentinal vaule
    if ((lowTemp == "off") and (poolRun24Hour == "off")):                             #if there is no danger of freezing and the 24hour run
        self.turn_off("switch.pool_main_pump")                                        #command are both off allow the pump to be stopped
        self.log("MQTT command sent to turn off Main Pool Pump via time")

  def on_time_cb(self, kwargs):
    # function to set a value to call the pump_on_cb
    self.set_state("input_number.pool_pump_main_run_daily_int", state = 1)
    self.log("on_time_cb called")
  
  def off_time_cb(self, kwargs):                                                       # function to set a value to call the pump_off_cb
    self.set_state("input_number.pool_pump_main_run_daily_int", state = 0)             #this function simply change the sentinal value to
    self.log("off time callback called")                                              #to 0 causing the pump to stop


  def startTimeChangedCallBack(self, entity, attribute, old, new, kwargs):
    #Slice the hour and minute out of the start time "the new variable"
    startTimeHour = int(new[0:2])
    startTimeMinute = int(new[3:5])

    #set the on_time variable, to be used for starting the pump based on schedule
    on_time = datetime.time(startTimeHour, startTimeMinute, 0)
    self.log("on time = {}".format(on_time))
    if self.pool_main_pump_on_timer != None:
    	self.cancel_timer(self.pool_main_pump_on_timer)
    self.pool_main_pump_on_timer = self.run_daily(self.on_time_cb, on_time)
    self.log("run daily on time callback")


  def stopTimeChangedCallBack(self, entity, attribute, old, new, kwargs):

    #Slice the hour and minute out of the stop time "the new variable"
    stopTimeHour = int(new[0:2])
    stopTimeMinute = int(new[3:5])
    
    #set the off_time variable, to be used for stopping the pump based on schedule
    off_time = datetime.time(stopTimeHour, stopTimeMinute, 0) 
    self.log("off time = {}".format(off_time))
    if self.pool_main_pump_off_timer != None:
    	self.cancel_timer(self.pool_main_pump_off_timer)
    self.pool_main_pump_off_timer = self.run_daily(self.off_time_cb, off_time)
    self.log("run daily off time callback")

And where do you get a problem? I found an error, the two callbacks you run daily

self.run_daily(self.on_time_cb, on_time)
self.run_daily(self.off_time_cb, off_time)

get never cancelled when the pool start/end time changes. You need to have them like this:

self.pool_main_pump_on_timer = self.run_daily(self.on_time_cb, on_time)
self.pool_main_pump_off_timer = self.run_daily(self.off_time_cb, off_time)

this way the will get cancelled in your startTimeChangedCallBack and stopTimeChangedCallBack callback.

I have some suggestions for your coding:

  • try to limit lines to 79 characters, makes code easier to read
  • you do some slicing and int conversion and datetime just to convert a simple date, use the built in self.parse_time this way you can have your time in the yaml file like this “11:30:00”
  • you don’t need and use all the handles you define for the state listeners, just write self.listen_state instead of self.handle = self.listen_state
  • you have lots of redundant code in your callbacks, better have one callback function that distinguishes the way it has been entered or create an additional function that turns on the pool pump, logs the MQTT thing and sets the input number
  • the same redundancy in the pump off callbacks, you can put the check if low_temp == off and poolRun24Hour == off into a separate function
  • this if self.pool_main_pump_on_timer != None: should be if self.pool_main_pump_on_timer is not None: or if self.pool_main_pump_on_timer:
  • function names should be lower case: startTimeChangedCallBack and stopTimeChangedCallBack

I suggest to use an IDE for developing apps like PyCharm, which will autmaticall show some of the suggestions I made above.

thank you for your suggestions. However I’m not sure I understand your first two comments, where you found the error. I have the

self.pool_main_pump_on_timer = self.run_daily(self.on_time_cb, on_time)
self.pool_main_pump_off_timer = self.run_daily(self.off_time_cb, off_time)

near the end of the file in separate callbacks. Have I done this incorrectly?

Also the reason I ended up with all the redundant callbacks was due to my lack of understanding based on variable being passed into the various functions, especially when monitoring for changes from HomeAssistant. I will look into this further, thank you.

Yes your way is incorrect, the way you are doing it you are have two callbacks that run daily which you define in the beginning and then another callback when you change the start/stop time. The cancel_timer in your callbacks doesn’t cancel the two callbacks you defined at the beginning and they will continue to run daily.

OK. Thank you