Appdaemon/Python newbie - is this possible?

@ReneTode,

Thanks for clarifying that.

@PianSom,

Well I am kind of a newbie myself, but like making things look a bit simpler (when I can). How I would do it is thus. When I have my apps.yaml file as I have it, I look up the self.args['dash_buttons'] in my self.initialize. Now it will report a list of dictionaries like the example above will give [{'dash_button1':'Gate'}, {'dash_button2':'Heating'}].

I convert that from a list to dictionary using the following code. Now there might be an easier way to do this, but this is what I know for now:

self.dash_buttons = dict()
for button in self.args['dash_buttons']:
    dash_buttons[list(button.keys())[0]] = list(button.values())[0]

This will now give me self.dash_buttons as {'dash_button1':'Gate','dash_button2':'Heating'}. In HA, since you have the dash buttons sending over mqtt (well I haven’t used one before), you can subscribe to the topic in HA, which creates an event, and sends it to Appdaemon. Look up example here.

Now depending on what your payload is, if its on or off, you can carry out the function based on what the button is allocated to in the dictionary.

This also makes it easier to lets say, use HA input select, to allocate different things to different functions. As all you have to do, is update the dictionary.

In all these, I am assuming that the names of the buttons, are part of the mqtt payload

thats really not needed.

self.args[“dash_buttons”] in itselve is already a list with dicts as you declared it.
so this:

self.dash_buttons = self.args['dash_buttons']

would be enough but also not needed.

you can call

self.args["dash_buttons"]["dash_button1"]

anywhere in your code and it will give “Gate” as a result.

1 Like

Very good point, for some reasons never figured out that :thinking:

Though the reason why sometimes I put them in their own dictionary are thus:

I have a habit of ensuring that the button or what ever it is (users in my code), are allowed in it. So I do something like

if button in self.dash_buttons:
    <do stuff>
else:
   <do another stuff and log>

it makes it easier for me to do that.

Also I could dynamically update the dictionary within the code, Though it will revert to the original if the module is reloaded.

Just to complete the example you linked to about sending MQTT messages as events, this is my code for receiving the events

    def initialize(self):
        self.listen_event(self.remote2_buttonC,
                "Four33Button", payload="13071009")

    def remote2_buttonC(self, event_name, data, kwargs):
        self.log("remote2_buttonC", level="DEBUG")
        self.turn_on("switch.computer_peripherals")

There are of course, different callback for the four buttons on each of 3 remotes.

If I want to change the operation of remote2, button c, I just change the code and it is loaded immediately.

Thanks @gpbenton,

But in this situation, it won’t be needed to define the payload, so the same function can be used to process different buttons. So instead it might be something like this

 def initialize(self):
        self.listen_event(self.dash_buttons_function, "DASH_BUTTONS")

 def dash_buttons_function(self, event_name, data, kwargs):
     self.log("__function__", level="DEBUG") #I like those lines
     if data.get("payload") in self.dash_buttons:
         if self.dash_buttons[data.get("payload")] == 'Gate':
             <gate function>
         elif self.dash_buttons[data.get("payload")] == 'Heating':
             <heating function>

if you want to change the dict then you need to get it into a var else it isnt needed.

this would work too:

if data.get("payload") in self.args["dash_buttons"]:
         if self.args["dash_buttons"][data.get("payload")] == 'Gate':
             <gate function>

for button in self.args["dash_buttons"]:
    <do stuff>

1 Like

Wow, that escalated quickly …

OK, so I have tried to follow the discussion above and put a test together.

I have a yaml

dash_app:
  module: dash
  class: simple_dash
  dash_buttons: 
    - switch.dash_brabantia: study_light_on
    - switch.dash_gillette: study_light_off

And so I need code which
1- initialzes by running a self_listen_state on every item in the list for an on action
2 - calls via getattr the second item on a line when one is found

Here is my attempt based on the discussion above. (I am struggling at the limits of my Python knowledge at this point, so please be gentle!)

import appdaemon.plugins.hass.hassapi as hass

class simple_dash(hass.Hass):

  def initialize(self):
    self.log("Hello from simple_dash")
    for control in self.args['dash_buttons']:
      self.listen_state(self.run_action(control), control[0], new="on")

  def run_action(self, entity, attribute, old, new, kwargs):
    self.log("Got an ON from {}, About to run {}".format(self.args["control"][0], self.args["control"][1] ))
    function = getattr(self, self.args["control"][1])
    function()


  def study_light_on(self):
    self.log("Running study_light_on")
    self.turn_on("light.study__ian")

  def study_light_off(self):
    self.log("Running study_light_off")
    self.turn_off("light.study__ian")

I am getting

  File "/home/pi/appdaemon/apps/dash/dash.py", line 7, in initialize
    for control in self.args['dash_buttons']:
KeyError: 'dash_buttons'

EDIT - now I look at this again I can see that the run_action function is redundant - it would be better to reference the actual function to be run directly in self.listen_state by reference to the control[1] eg study_light_on. The second argument of self.listen_state should be the first item of the list element control[0] eg switch.dash_brabantia

So the line in initialize should be something like

self.listen_state(getattr(self, control[1])(), control[0], new="on")

but correctly syntax’ed!! :slight_smile:

I think you are following a poor example to start. This code will have the same effect

class simple_dash(hass.Hass):

  def initialize(self):
    self.log("Hello from simple_dash")
    self.listen_state(self.brabantia, "switch.dash_brabantia", new="on")
    self.listen_state(self.gillette, "switch.dash_gillette", new="on")

  def brabantia(self, entity, attribute, old, new, kwargs):
    self.turn_on("light.study_ian")

  def gillette(self, entity, attribute, old, new, kwargs):
    self.turn_off("light.study_ian")

Thanks @gpbenton

Yes, I know I can do it that way, but for the reasons set out above I am trying to give myself flexibility by allowing modification of the behaviour through changing the .yaml rather than the .py

I don’t see the advantage - it isn’t any more ‘flexible’. Remember that when it breaks in six months time you are going to have to fix this, and remembering the data structures involved is complex enough when you know python.

But its your app, so I’ll shut up now.

@PianSom,

Well based on all said and done, now this is how I would have done it. Though its a stubborn way to do it against what @gpbenton said about data structure, and the pros may frown at it. But I believe it should work and achieve what you want. I will borrow some clues from @ReneTode above (believe me I didn’t know there was a function called eval() until I saw this tread. Told you I am also a newbie kind of, so take my advise on your own peril). But I am just trying to help a fellow new starter.

To start with, I am assuming your apps.yaml looks thus

dash_app:
  module: dash
  class: simple_dash
  dash_buttons: 
    - brabantia: 'Study light on'
    - gillette: 'Study light off'

and al you buttons are switches and start with the strings “dash_”. Then you can run the following codes

def initialize(self):
    self.dash_buttons = dict()
    for button in self.args['dash_buttons']:
        dash_buttons["switch.dash_{}".format(list(button.keys())[0])] = list(button.values())[0].lower() #.lower() just in case capital lets are used in the yaml file 
        self.listen_state(self.run_action, "switch.dash_{}".format(list(button.keys())[0]), new="on")
    self.log("__function__, buttons = {}".format(self.dash_buttons), level = "INFO")#print out buttons


def run_action(self, entity, attribute, old, new, kwargs):
    function = "self.{}()".format(self.dash_buttons[entity].replace(" ", "_")) #replace spaces with under score
    self.log("Got an ON from {}, About to run {}".format(entity, function ))
    eval(function)
    return


def study_light_on(self):
   self.log("Running study_light_on")
   self.turn_on("light.study__ian")
   return

def study_light_off(self):
    self.log("Running study_light_off")
    self.turn_off("light.study__ian")
    return

With the above, I believe once you declare the buttons in the apps.yaml file and it reloads, it should just work. As long as you use the same string in the .yaml file as your function names.

One could still make the .yaml file simpler in the sense of of writing single words, but it will entail more variables which obviously its not recommenced.

Hope it helps.

Thanks @Odianosen25 ! I will try your code later.

As I hinted in my EDIT at the end of my last post, do you think it is possible to get rid of run_action altogether by using

self.listen_state( getattr( list(button.keys())[1] ) ),   YOUR CODE HERE

(with the necessary changes to the .yaml to make it as I originally suggested, with the actual function name e.g. study_light_on)?

It would be more elegant (:slight_smile: ) not to have an unnecessary function.

@PianSom,

Well it might be possible, and honestly I don’t know much about what getattr will return. (one of those new things in this tread I learnt :smirk:).

You have a good point though as you could potentially get rid of it, and ensure all the functions as you put it “correctly syntax’ed!! :slight_smile:” . So the functions might look something like this, if that works:

def study_light_on(self, entity, attribute, old, new, kwargs):
   self.log("Running study_light_on")
   self.turn_on("light.study__ian")
   return

def study_light_off(self, entity, attribute, old, new, kwargs):
    self.log("Running study_light_off")
    self.turn_off("light.study__ian")
    return

But honestly I have no clue on that. Besides if adding an extra function makes it easier to read, see no reason to remove it really. And you might want to potentially use that function somewhere else. And if you use a call back for a switch, it will make it more difficult to re-use it again (i think)

i read what you are doing and in your python code is so much wrong that it is clear that you have no clue what you are doing.
but thats no problem i will try to get some things clear for you:

ill take apart your app step by step.

first lets take a look at your yaml:

dash_app:
  module: dash
  class: simple_dash
  dash_buttons: 
    - switch.dash_brabantia: study_light_on
    - switch.dash_gillette: study_light_off

YAML is nothing more then a fancy way to write a dictionairy.
and you can call that dictionairy from inside your app.
self.args is a dictionairy that contains this:

  module: dash
  class: simple_dash
  dash_buttons: 
    - switch.dash_brabantia: study_light_on
    - switch.dash_gillette: study_light_off

now we can start to split that up:
self.args[“module”] = simple_dash
self.args[“dash_buttons”] contains a list with dictionairies. the list is because you use -.
so in that list we find [{“switch.dash_brabantia”: “study_light_on”},{“switch.dash_gillette”: “study_light_off”}]
we can splitt that out a bit more
self.args[“dash_buttons”][0] = {“switch.dash_brabantia”: “study_light_on”}
self.args[“dash_buttons”][0][“switch.dash_brabantia”] = “study_light_on”

i hope that clears a bit more from the translation from yaml to self.args
now the code

    for control in self.args['dash_buttons']:
      self.listen_state(self.run_action(control), control[0], new="on")

you try to give args in a callback and that isnt gonna work.
the callback needs to be the name from a function, without any args. if you want to give on some args you can do that after the needed settings as kwargs.
i dont think you can use getattr or eval at that place or a string, but i am not sure about that, because i never tried and dont know how Andrew translates that part to a function.
when i look at your function you can do a few things:

  1. give control as a kwarg
  2. use the entity to get the action from the args.

the 2nd is the most easy because we already have the arg list.
so your listen_state wil become

    self.listen_state(self.run_action, control[0], new="on")

then the callback

  def run_action(self, entity, attribute, old, new, kwargs):
    self.log("Got an ON from {}, About to run {}".format(self.args["control"][0], self.args["control"][1] ))
    function = getattr(self, self.args["control"][1])
    function()

you try to get self.args[“control”] but as i explained you now know that that wont work.
control is nowhere in your yaml and even without quotes it wont work because you have no variable called control in your function also.
but we have the entity in the callback and we have your yaml.
so lets get the things you like from that.

for button in self.args["dash_buttons"]:
  if button[0] == entity:
    functionpart = button[1]
getattr(self, function)()

thats about the code you used above and why things cant work.
to get rid of the run_action function all together you could try if this will work:

    for control in self.args['dash_buttons']:
      self.listen_state(getattr(self,control[1]), control[0], new="on")

it can very well be that it will work but i never tried.

1 Like

Hi @ReneTode, @Odianosen25

Thanks so much for taking the time to write such detailed responses. (It may be that learning Python through the medium of Appdaemon implementation is not the most sensible idea - but “no clue” is slightly harsh :slight_smile:)

Following your suggestions here and a few more steps on my Python journey, I can confirm that the following code works as expected for the yaml given below.
:grinning:

dash_app:
  module: dash2
  class: simple_dash2
  dash_buttons:
    - switch.dash_brabantia: study_light_on
    - switch.dash_gillette: study_light_off
import appdaemon.plugins.hass.hassapi as hass

class simple_dash2(hass.Hass):

  def initialize(self):
    for control in self.args['dash_buttons']:    # for each item in the dash_buttons: list 
      [(listen_device, callback_name)] = control.items()  # extract the values of the device to be listened for, and the corresponding function to be run
      callback_function = getattr (self, callback_name)  #setup the callback function (which must exist below) from the value just found
      self.listen_state(callback_function, listen_device, new="on")  #setup the AD listen



  def study_light_on(self, entity, attribute, old, new, kwargs):
    self.turn_on("light.study__ian")

  def study_light_off(self, entity, attribute, old, new, kwargs):
    self.turn_off("light.study__ian")

Nice work. You just thought me an easier way to some do somethings. Well we learning and when we take up challenges like this, we can just get better at it. I have a mentality of “anything is possible”, its just a matter of figuring out the way to do it; even if its a messed up way initially :smirk:

I don’t mind learning python via Appdaemon, its been cool and fun :grin: I could do anything so far.

1 Like

you are right.
that didnt come out like i wanted :wink:
you already did a lot of things right.
i just ment that i got the feeling that there were some parts that you used, without understanding how they worked.

does it really work without errors?
then i guess the getattr is a nice addition to my knowledge :wink:
eval could not be used like that, so now i know the difference :wink:

That is completely fair - as the title says, I am just a newbie here :slight_smile: And it’s good that we all (with the possible exception of @aimc :slight_smile: ) have learned something.

Yes, the code works without errors or warnings.

Having written it this way now, it strikes me that it is actually quite a nice way of associating any HA event directly with a piece of Python code. I wonder if there may be other uses for it.

3 Likes