Help Converting HA Automation

Trying to convert my existing automations to appdaemon, mainly because I want to learn the programming side of things.

I have this automation that essentially sends a notification if a given door is open for more than a minute.

- alias: 'Back door open'
  trigger:
    platform: state
    entity_id: binary_sensor.door1_sensor_5_0
    from: 'off'
    to: 'on'
    for:
      seconds: 60
  action:
    - service: notify.ios_v1ruexe
      data_template:
        title: "Back Door Still Open"
        message: "Back Door has been open for more than 60 seconds"
        data:
          push:
            badge: 1

I am having trouble wrapping my head around how this would work in appdaemon. I know I can listen to the state of something and I get the last_updated or last_changed state. But I am having trouble figuring out how to say this “sensor” has been in state “on” for x amount of time. I would also like to be able to look at all of the door sensors instead of having an automation for each sensor.

AppDaemon has the ability to add a for argument to,listen state that fires a callback when a given state has been true for that amount of time. I can’t check the docs right now but the info is there in the API dock under listen_state()

You are right, I am new to this and still trying to read api docs. It is listed as duration.

1 Like

@aimc I’d never seen this duration before, that’s super cool.

@smolz Did you get your automation working? I converted your specific automation up above into this app.

class BackDoor(appapi.AppDaemon):

    def initiatilize(self):
        self.listen_state(self.notifications, entity='binary_sensor.door1_sensor_5_0', new='open', duration=60)

    def notifications(self, entity, attribute, old, new, kwargs):
        title = "Back Door Still Open"
        message = "Back Door has been open for more than 60 seconds"
        data = {"push": {"badge": 1}}

        self.call_service('notify/ios_v1ruexe', title=title, message=message, data=data)

However I think you can build on this idea. You state “if a given door is open for more than a minute” which makes me assume you monitor more than just the back door this way? Especially since in your last sentence you say “all the door sensors”.

So this is my crack at it! :slight_smile: Try your best to understand it before reading my explanation below. I’ll try my best to break it down for you. I am going to assume you know absolutely nothing about Python or programming. If it’s easier, you can also view the syntax highlighted here.

import appdaemon.appapi as appapi

#
# App to send notification when door opened or closed
#
# Args: (set these in appdaemon.cfg)
# ttl = # of seconds to wait until notified
#

class DoorMonitor(appapi.AppDaemon):

    def initiatilize(self):
        self.door_entities = ['binary_sensor.door1_sensor_5_0', 'binary_sensor.door2_sensor_5_0', 
                              'binary_sensor.door3_sensor_5_0', 'binary_sensor.door4_sensor_5_0']

        self.door_timer_library = {}

        for door in self.door_entities:
            self.listen_state(self.tracker, entity=door)

    def tracker(entity, attribute, old, new, kwargs):
        friendly_name = self.get_state(entity, attribute='friendly_name')

        try:
            self.cancel_timer(self.door_timer_library[entity])
        except:
            self.log('Tried to cancel a timer for {}, but none existed!'.format(entity), level='INFO')

        if new == 'on':
            self.door_timer_library[entity] = self.run_in(self.notifier, seconds=int(self.args['ttl']), 
                                                          friendly_name=friendly_name, timer=self.args['ttl'])

    def notifier(self, kwargs):
        title = "{} Still Open".format(kwargs['friendly_name'])
        message = "This door has been open for more than {} seconds".format(kwargs['timer'])
        data = {"push": {"badge": 1}}

        self.call_service('notify/ios_v1ruexe', title=title, message=message, data=data)

Starting off at the very top, we’ll say you have 1 argument you specify, and that’s the ttl or “time to live” value. This is the number of seconds, or duration of time that should pass before you’re notified of a door’s state being on, or open!

I call your app “DoorMonitor” as that is essentially what we’re doing here. In my mind, it consists of two parts. A function that should track the state, and then another function that should handle notifying you of the occurrence of the specific door being open for longer than the ttl value. As such, you’ll see we have both tracker and notifier functions! That’s enough for a quick overview though. :slight_smile:


In the initialize section of your app…

  • We define a variable called door_entities and in what we call a “list”. This is a way to hold our data in memory much like you would hold data on a shopping list on paper. It simply keeps a number of things, in our case various entity_ids in an easily-accessible area.
  • We then define another variable called door_timer_library in what we call a “dictionary”. Dictionaries in Python are another way to contain our data, except each datapoint will have both a unique identifier called a “key” associated with a “value”. These are called key-value pairs, because the key references a value. To give you a real world example… you can simply look at the function of a spoke-language dictionary. Each word has a meaning, and these words’ functions are unique. There’s more nuance here, but this metaphor does the job well enough. :slight_smile:
  • We’ll then call listen_state for-each entity_id in the door_entities list we created earlier, so that whenever their state changes, our tracker function will fire.

In the tracker section of your app…

  • @aimc has given us some nice tools to work with here, so thank him already. :wink:
  • First off we’ll pull out the friendly name of the entity that we’re working work. Remember, since we technically called listen_state for each of our entities in door_entities, this code essentially turns into a skeleton of which all of those entities will fall into. Depending on which entity’s state changed, that entity will show up here in the reference entity.
  • We’ll then do a little bit of Python magic here (try/except is a bit out of scope for a first lesson!) We’ll come back to this in a second, just hold on!
  • Where the app says new == on, we are checking to see if the new value of entity is on, which in our case means open. What we want to do here is run the notifier function, in some number of seconds, of which is defined in our config file under the argument ttl. You’ll see this long line of code actually wraps to the next line… we’ll do a little bit more Python magic and supply notifier two keywords (hey, remember dictionaries?) called friendly_name and timer, with their respective values.
  • Buuuut that’s not all this line of code is doing! You’ll see at the very front of the line, we are putting something into our door_timer_library dictionary. This is exactly how you add to a Python dictionary. Reference the dictionary, create the key inside the dictionary, and then assign that key a value. In our case, our good friend @aimc gives us another tool to work with called a handle. Which is essentially a reference to the object we’ve put in the scheduler: "in <value of ttl> run self.notifier with keyword arguments of friendly_name and timer"

In the notifier section of your app…

  • This part is simple. We simply set a few variable for title, message, and data. I’ve made things a bit tidier, by putting a sort skeleton message for the title and message, where our keyword arguments, or kwargs, fill in the games. It’s kinda like a boring, practical game of madlibs. :slight_smile:
  • I didn’t really understand your data that you send, so I simply copied it. Sorry 'bout that! If you give me more info, you might be able to customize this variable too!
  • Finally we’ll call the notify service to your device ios_v1ruexe with the variables we defined earlier.

… but wait, there’s one last thing SupahNoob. We have a way to SET timers … but what happens when the door opens up, and then closes (set to off) before the we reach the ttl value?! You haven’t set anything in tracker for when new == 'off'! Well, remember our little bit of Python magic that’s out of the scope of the lesson? It’s all covered there. Whenever a door’s state is changed, tracker gets called to run. We’ll then try to run the cancel_timer accessing the handle we placed in the door_timer_libarary which would remove that timer from our scheduler. If we’ve not even set a timer for that entity just yet, then we’ll throw up a log in our log file with an information line that say it doesn’t yet exist. Great! So when the door is closed, we don’t need to be notified of the state. Case closed. But also, what happens if, somehow, for some reason, we call a door as open 2 times in a row? Well we then would simple “refresh” the timer on that door in the schedule. First, it would cancelled, and then it would get re-created as new == 'on'!


So that was a lot! But I hope you got to learn something if you didn’t know Python, and it’s totally possible that others here might have learned something too.

Otherwise, you just get to see my thought process. :wink:

  • SN

//edit

@bbrendon @aimc @rpitera @Bit-River @ReneTode

This seems to have gotten a positive reception! Shoot me a message and let me know if you’d like to see me tackle other problems and explain them in such a way. I think there are a lot of users here that are unfamiliar with Python, but can see the power and flexibility it provides. I wouldn’t even mind setting up a series wherein we tackle common problems!

6 Likes

In a similar vain, I wanted to setup alerts for devices generically. Here is my take:

import appdaemon.appapi as appapi

#
# EntityLeftnotifyState
#

class Notifications(appapi.AppDaemon):


  def initialize(self):

    ## Initialize
    self.notifyState = None
    self.stopState = None
    self.entityType = None
    self.stopState = None
    self.notifydelaymin = None
    self.all_entity_alert = None
    self.secureAction = None
    self.friendlyEntityType = None
    self.friendlyStopState = None
    self.friendlyNotifyState = None
    
    
    ##Get Config Values
    self.notifyState = self.args["notifyState"]
    self.stopState = self.args["stopState"]
    self.entityType = self.args["entityType"]
    self.stopState = self.args["stopState"]
    
    if "notifydelaymin" in self.args:
      self.notifydelaymin = self.args["notifydelaymin"]
    if "all_entity_alert" in self.args:
      self.all_entity_alert = self.args["all_entity_alert"]
    if "secureAction" in self.args:
      self.secureAction = self.args["secureAction"]
      
    if "friendlyEntityType" in self.args and self.args["friendlyEntityType"] != "":
      self.friendlyEntityType = self.args["friendlyEntityType"]
    else:
      self.friendlyEntityType = self.entityType
      
    if "friendlyStopState" in self.args and self.args["friendlyStopState"] != "":
      self.friendlyStopState = self.args["friendlyStopState"]
    else:
      self.friendlyStopState = self.stopState
      
    if "friendlyNotifyState" in self.args and self.args["friendlyNotifyState"] != "":
      self.friendlyNotifyState = self.args["friendlyNotifyState"]
    else:
      self.friendlyNotifyState = self.notifyState
      
      
    
    self.log("Generic Notification App - {}".format(self.friendlyEntityType))
    
    self.entity_timer = None
   
    if "entities" in self.args:
      for entity in self.split_device_list(self.args["entities"]):
        ## If Group - initialize all entities from the group
        if "group" in entity:
          self.log("Group provided.")
          groupitem = self.get_state(entity,"all")
          entity_list = groupitem['attributes']['entity_id']

          for member in entity_list:
            if self.entityType in member:
              self.log("Initializing entity: " + self.friendly_name(member))
              self.listen_state(self.state_changed, member, new=self.notifyState)
              self.listen_state(self.state_changed, member, new=self.stopState)  
        else:
          self.log("Single entity provided.")
          self.log("Initializing entity: " + self.friendly_name(entity))
          self.listen_state(self.state_changed, entity, new=self.notifyState)
          self.listen_state(self.state_changed, entity, new=selfstopState)               
    else:
      # This will monitory ALL of the entities in your house
      self.log("No entity provided in cfg, not doing anything.")
      # self.listen_state(self.state_changed, entityType, new=notifyState)
      # self.listen_state(self.state_changed, entityType, new=stopState)
   
  def state_changed(self, entity, attribute, old, new, kwargs):
    #notifyState = self.args["notifyState"]
  
    entitystate = new 
      
    if self.notifyState in entitystate:
      self.run_in(self.start_timer, 0, item=entity, friendlyState=self.friendlyNotifyState)
    else:
      self.run_in(self.stop_timer, 0, item=entity, friendlyState=self.friendlyStopState)     
      
  def send_notification(self, kwargs):
    notifyType = kwargs["notifyType"]
    entity = kwargs["item"]
    msg = kwargs["msg"]
    self.log(msg)
    
   # This sends a notifcation and then starts the timer for another XX minutes    
    if "alert" in notifyType and self.secureAction:
      actionData={"entityName":"{}".format(entity),"entityType":"{}".format(self.entityType), "secureAction":"{}".format(self.secureAction), "actions": [ {"action": "manual_action", "title": "Secure {}".format(self.friendlyEntityType).title() } ] } 
      self.call_service("notify/notifyall", message=msg, data=actionData)
    else:
      self.call_service("notify/notifyall", message=msg)
    
    # if "alert" in notifyType:
      # msg = msg + "To secure the {}, click here: https://home.kylerw.com/api/services/{}/{}?entity_id=".format(entityType,entityType,secureAction,entity)
    
    #self.call_service("notify/notifyall", message=msg)
  

  def start_timer(self, kwargs):
    entity = kwargs["item"]
    friendlyState = kwargs["friendlyState"]
    
    if "notifydelaymin" in self.args:
      cfgtimer = self.notifydelaymin
      notifydelay = int(cfgtimer) * 60
    else:
      notifydelay = 600
      
    msg = "{} is currently {}. Will alert again in {} seconds.".format(self.friendly_name(entity), friendlyState, notifydelay)    
    ## Send notification immediately    
    self.run_in(self.send_notification, 0, msg=msg, item=entity, notifyType="alert")     
    ### Reset timer after XXX seconds
    self.entity_timer = self.run_in(self.start_timer, notifydelay, item=entity, friendlyState=friendlyState)
      
  def stop_timer(self, kwargs):
    entity = kwargs["item"]
    friendlyState = kwargs["friendlyState"]
    
    msg = "{} is now {}. Cancelling timer.".format(self.friendly_name(entity), friendlyState)
    self.run_in(self.send_notification, 0, msg=msg, item=entity, notifyType="stop") 
    # This cancels the timer once the entity is in a stop state
    self.cancel_timer(self.entity_timer)
    
    self.run_in(self.all_entities_check, 0)
    
  def all_entities_check(self, kwargs):
    # notifyState = self.args["notifyState"]
    # stopState = self.args["stopState"]
    # entityType = self.args["entityType"]
    ## Will send alert if all entitys are in a stop state
    if "all_entity_alert" in self.args and self.all_entity_alert == "true":
      
      if "entities" in self.args:
        for entity in self.split_device_list(self.args["entities"]):
          allState = self.get_state(entity)
          if allState == self.stopState:
            break
      else:
        allState = get_state(entityType)
      
      if self.notifyState not in allState:
        msg = "All {}s are now {}.".format(self.friendlyEntityType, self.friendlyStopState)
        self.run_in(self.send_notification, 0, msg=msg, item=None, notifyType="stop")

The Config then looks like so:

[Lock Notifications]
module = notifications_state-timer
class = Notifications
entityType = lock
entities = group.all_locks ## single, group, or comma delimited
notifyState = unlocked    ## notify when an entity is in this state
stopState = locked         ## stop once an entity is in this state
########### Optional ###########
notifydelaymin = 10
### alert when all have been locked/closed/etc
all_entity_alert = true
## allows for custom HTML5 alerts that give you an action button to lock the door
secureAction = lock
### these help make the alerts meaningful
friendlyEntityType = Lock
friendlyStopState = Locked
friendlyNotifyState = Unlocked

or

[Door Sensor Notifications]
module = notifications_state-timer
class = Notifications
entityType = binary_sensor
entities = group.opensensors
notifyState = on
stopState = off
### Optional ###
notifydelaymin = 2
all_entity_alert = true
friendlyEntityType = Door
friendlyStopState = Closed
friendlyNotifyState = Open

This was my first pass at it and much of the original came from appdaemon examples and other code shared. The config should be able to be simplified as many of the input values can probably be extrapolated from each entity directly but I haven’t spent the time to find out.

can i use this part as a part from the appdaemon tutorial?

or even better, can you make an MD file from this and make a pull request here: https://github.com/ReneTode/My-AppDaemon
??

i think that if we make more of these files we would have a good tutorial in a short while.

2 Likes

@SupahNoob So great! Reading through the explanation for the 10th time to make sure I understand what is going on. I for one would love to see more examples, I am guessing that may hold a lot of people back from using Appdaemon but your explanation was amazeballs!

@SupahNoob

Getting this error?

2017-02-16 09:01:23.335857 WARNING ------------------------------------------------------------
2017-02-16 09:01:27.808027 WARNING ------------------------------------------------------------
2017-02-16 09:01:27.809248 WARNING Unexpected error in worker for App Door Notification:
2017-02-16 09:01:27.810721 WARNING Worker Ags: {'type': 'attr', 'entity': 'binary_sensor.door1_sensor_5_0', 'kwargs': {}, 'id': UUID('d7b81de8-8be8-44e2-b685-cf5b514f1fee'), 'old_state': 'on', 'name': 'Door Notification', 'function': <bound method DoorMonitor.tracker of <door_notifier.DoorMonitor object at 0x70651050>>, 'attribute': 'state', 'new_state': 'off'}
2017-02-16 09:01:27.811674 WARNING ------------------------------------------------------------
2017-02-16 09:01:27.813175 WARNING Traceback (most recent call last):
  File "/usr/local/lib/python3.5/site-packages/appdaemon/appdaemon.py", line 609, in worker
    ha.sanitize_state_kwargs(args["kwargs"]))
TypeError: tracker() takes 5 positional arguments but 6 were given

The definition for tracker should include self as the first parameter :

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

This is correct. That’s what I get for going fast!

Picked up a small typo in your DoorMonitor class above: you’ve written “initiatilize” instead of ‘initialize’ :slight_smile:

Found this thread when trying to do something similar. It helped me get most of the way, but I got stuck on how to add actions to the HTML5 notification. Sharing for anyone else that stumbles onto this thread.

If you’re trying to add an action to an HTML5 notification, the “actions” parameter takes an array of dicts. (new to Python - did I word that correctly?)

data = {"actions": [{"action": "action_for_your_automation_trigger", "title": "text that displays on HTML5 button"}]}

self.call_service("notify/html5_notify", title="title", message = "message", data = data)

not quite.

[a,b,c] is a list
{a:b, c,:d} is a dict
{a: {b: c}} is a nested dict
so in this case it is a dict, with a list of dicts.

1 Like