AppDaemon Q&A

I run under Bash like this:

cd …/…/mnt/c/Users/myusername/appdaemon
appdaemon -c conf/appdaemon.cfg

My appdaemon installation was using Bash command

If you use the pip installation, that command will just call the installed appdaemon not the one in the current directory, Try the command I pasted.

I did that and it game me error:

With my command under bash it runs the correct apps listed under my conf/apps folder and if I make changes to app it reloads means I am at right folder but appdaemon.py is coming from somewhere else.
Sorry, not to mean take much your time. I am investigating more to see what is wrong.

You need to be one level up from the actual appdaemon.py file.

Here are the results and results are positive! and that’s good news!

Adding timeout parameter to the call of SSEClient works, at the given timeout seconds (I tested with 10) it comes back from the blocking call with the exception called “requests.exceptions.Timeout” and we can ignore it and connect again. Though I really cannot simulate where AppDaemon gets in the state where SSEClient thinks connected to HA but no data. But even though that happens that timeout should reconnect in timely manner to HA again.

My added exception handling code which has log info that didn’t show up on console log not sure why? Also I don’t have logs file(s) as I have config setup to use STDOUT and STDERR and not the actual file but I attached console log from HA showing reconnecting AppDaemon.

About the issue of not executing my modified appdaemon.py was caused by Windows 10 Bash. Lesson learned even though I was in correct folder, bash was executing the cached version of that file from the location
C:\Users\myusername\AppData\Local\lxss\rootfs\usr\local\lib\python3.4\dist-packages\appdaemon
Once I modified that file I see my changes getting executed with debug messages.

Here is the console log screen capture from HA showing no reconnect before timeout parameter added (before green line) and reconnecting (after green line) after timeout added at every 10-15 seconds when there is no activity after adding timeout parameter.

Great news - I’ll add a hard coded timeout of 10s to the next version - thanks for your work in figuring this out!

Cool, do you think it can come from config file rather than fixed 10 seconds in case if any one wants to reduce down time in case of failure?

1 Like

Yep, I can do that and have it default to 10 instead.

1 Like

Perfect, thanks

Sorry if that already came up - this thread is really long :wink:

I’m trying to optimize my heating automation.
I’m writing an app that reads a “crontab-like” file, that specifies day, time, a room and a temperature per line.
The app should then schedule the necessary service calls to HA to change temperatures.

One thing that irritates me:
From the initialize() method I’m doing something like:

self.run_daily(self.run_daily_c(entity, temperature), runtime, constrain_days=days)

with

  def run_daily_c(self, entity, temperature):
    self.log_notify("Setting {} to {} degree Celsius".format(entity, temperature))

As I understood the documentation, self.run_daily() schedules to run self.run_daily_c() for the time specified by runtime.
But looking at the log messages I see that self.run_daily_c() is already called during the initializaton.
I would have expected the log messages to appear only at the scheduled time and not instantly.
What am I missing here?

A second thing:
Is there a way to “watch” a user-specified file for changes?
I’d like to reinitialize the app when my schedule table file changes.

TIA,
Sebastian

You are passing in a function call as the parameter - rather than use the function as a callback, it will execute the function and try to use the return value (if any) as the callback which will not work. If you want tp pass parameters to your callback you can;t do it that way, you need to specify the callback with no parameters, give keyword values to the main function then use the args dictionary to retrieve them in the callback itself.

Ah, I see, thanks! Could you give me an example?

I now tried it like this:

self.run_daily(self.run_daily_c, runtime, constrain_days=days, entity="kitchen", temperature="20")

and:

  def run_daily_c(self, kwargs):
    self.log_notify("Setting {} to {} Celsius".format(self.args['entity'], self.args['temperature']))

This kept the method from being run by initialize() but made appdaemon throw a lot of errors at the scheduled time:

2016-10-21 13:26:08.003587 WARNING Traceback (most recent call last):
  File "/usr/local/lib/python3.4/site-packages/appdaemon/appdaemon.py", line 381, in do_every_second
    exec_schedule(name, entry, conf.schedule[name][entry])
  File "/usr/local/lib/python3.4/site-packages/appdaemon/appdaemon.py", line 281, in exec_schedule
    dispatch_worker(name, {"name": name, "id": conf.objects[name]["id"], "type": "attr", "function": args["callback"], "attribute": args["kwargs"]["attribute"], "entity": args["kwargs"]["entity"], "new_state": args["kwargs"]["new_state"], "old_state": args["kwargs"]["old_state"], "kwargs": args["kwargs"]})
KeyError: 'attribute'

your args are set in your configfile and not in your app.

after that your line would be:

self.run_daily(self.run_daily_c, runtime, constrain_days=days)

I need to be able to pass arguments from the app, because the app reads the values from a file.
It’s not practical to do this from the config file.

So, looking at the API doc again, the footprint for a scheduler callback function is
def my_callback(self, **kwargs):

So I changed my function to:
def run_daily_c(self, **kwargs):

There’s also an example given:
self.handle = self.run_in(self.run_in_c, title = "run_in5")

So

self.run_daily(self.run_daily_c, runtime, constrain_days=days, entity="kitchen", temperature="20")

should be possible.
And this should work too:

  def run_daily_c(self, **kwargs):
    self.log_notify("Setting {} to {} Celsius".format(kwargs['entity'], kwargs['temperature']))

I have to admit, that I’m still quite unfamiliar with the **kwargs concept of Python, but to my understanding this is how it should work.
Except it doesn’t :wink:

Sebastian

use kwargs instead of args - sorry. my bad.

ah oke. i got confused because you did set entity in de init and then called for self.args[“entity”] in the callback.
i guess that if you get this working you would like to change it to entity=ENTITY_VAR?

Entity is just a reference to the room which radiator should be controlled.
I want to use something like “kitchen”, “livingroom” etc. in the schedule file and map this to the actual HA entities.
It’s probably not wise to call the variable “entity” at this point? So let’s call it “room” then:

def initialize(self):
[...]
  self.run_daily(self.run_daily_c, runtime, constrain_days=days, room="kitchen", temperature="20")

  def run_daily_c(self, **kwargs):
    self.log_notify("Setting {} to {} Celsius".format(kwargs['room'], kwargs['temperature']))

That gets me:

 WARNING Traceback (most recent call last):
  File "/usr/local/lib/python3.4/site-packages/appdaemon/appdaemon.py", line 418, in worker
    function(ha.sanitize_timer_kwargs(args["kwargs"]))
TypeError: run_daily_c() takes 1 positional argument but 2 were given

I don’t get the error message?!
What’s the second argument passed to the function?
The first one should be “self”, right?

Sebastian

Ah, I finally got it. I needed to lose the ** from kwargs:

def run_daily_c(self, kwargs):

Sebastian

1 Like

@sebk-666 i think you are trying 1 code for several rooms, but are those rooms connected in the code?

i have several heating controls around the house and i have it like this:
in the config:

 [heatinglivingroom]
module = heating
class = thermostat
thermometer = sensor.livingroom_temp
heating1 = switch.radiator_1
heating2 = switch.radiator_2
max_temp1 = input_slider.livingroom_thermostaata
max_temp2 = input_slider.livingroom_thermostaatb
automatic = input_boolean.livingroom_temp

 [heatingkitchen]
module = heating
class = thermostat
thermometer = sensor.kitchen_temp
heating1 = switch.radiator_3
heating2 = switch.radiator_4
max_temp1 = input_slider.kitchen_thermostaata
max_temp2 = input_slider.kitchen_thermostaatb
automatic = input_boolean.kitchen_temp

and then in the app:


class thermostat(appapi.AppDaemon):

  def initialize(self):
      self.listen_state(self.temp_change,self.args["thermometer"])
      self.listen_state(self.temp_change,self.args["max_temp1"])
      self.listen_state(self.temp_change,self.args["max_temp2"])
      self.listen_state(self.temp_change,self.args["automatic"])

off course i will add a self.run_daily to change the settings. (as soon as i have written down on which times i want which temp)
that part would then change the slider value to the desired temp at that particular time.

I’m not sure what you mean by “are those rooms connected in the code”.
I’ve got a working “proof-of-concept” now, so I can show you what I’m planning.

I want to specify days and times of when to set what temperature for which thermostat.
There’s currently no sensor for the actual room temperature, so I don’t need to act on sensor data.

The module config currently looks like this:

[Heating Schedule]
module = heating_schedule
class = HeatingSchedule
file = /conf/heatingschedule.txt
log = 1

In /conf/heatingschedule.txt everything is defined.
There’s a “mapping-section” at the top, where you can map room names (or whatever you want to put there) to the actual entities in HA.

#
# Part 1: entity mappings
#
# assign an alias to an entity. Format:
#
# alias: entity
#
kitchen:    climate.danfoss_z_thermostat_014g0013_heating_1_6
bathroom:   climate.danfoss_z_thermostat_014g0013_heating_1_5
livingroom: climate.danfoss_z_thermostat_014g0013_heating_1_4
bedroom:    climate.danfoss_z_thermostat_014g0013_heating_1_3
office:     climate.danfoss_z_thermostat_014g0013_heating_1_2

#
# Part 2: define a schedule
#
# One "control event" per line. Format:
#
# wd|we   hour  minute    room    temperature
#
# with
#  wd := weekday (mon-fri) and
#  we := weekend (sat,sun)
#

# testing
wd      17  13      office        23
wd      17  14      office        21

The app currently looks like this:

import appdaemon.appapi as appapi
from pathlib import Path
import datetime
import re

class HeatingSchedule(appapi.AppDaemon):

  def initialize(self):
    self.entities = {}

    if Path(self.args['file']).is_file():
      try:
        with open(self.args['file'], 'r') as f:
          content = f.readlines()
      except:
        raise

      for line in content:
        # weed out comments and empty lines
        if line.startswith('#'): continue
        if not line.strip(): continue

        # map names to entities in schedule file:
        # name: climate.this_is_an_entity_1_2
        match = re.match('([a-zA-Z0-9]+):\s+([a-z0-9_\.]+)', line)
        if match:
          self.entities[match.group(1)] = match.group(2)
          continue

        try:
          # wd|we  hour  minute  room  temperature
          schedule = line.split(None, 5)

          if schedule[0].lower() == "wd":
            days = "mon,tue,wed,thu,fri"
          if schedule[0].lower() == "we":
            days = "sat,sun"

          runtime = datetime.time(int(schedule[1]), int(schedule[2]), 0)

          self.run_daily(self.run_daily_c, runtime, constrain_days=days, room=schedule[3].lower(), temperature=schedule[4])
        except:
          raise

#     for key in self.entities:
#       self.log_notify("MAP: {} -> {}".format(key, self.entities[key]))

  def run_daily_c(self, kwargs):
    self.log_notify("Setting {} to {} degree Celsius. ({})".format(kwargs['room'], kwargs['temperature'], self.entities[kwargs['room']]))
    self.call_service("climate/set_temperature", entity_id = self.entities[kwargs['room']], temperature = kwargs['temperature'])

  def log_notify(self, message, level = "INFO"):
    if "log" in self.args:
      self.log(message)
    if "notify" in self.args:
      self.notify(message)

One thing that I’m still missing is a way to re-init the app when heatingschedule.txt is modified.

Sebastian