Hi everybody!
Recently I gave a user some feedback who was looking to convert their YAML Automation file to the app-based Python structure of AppDaemon. It dawned on me that a lot of users who are using AppDaemon are seeking the extra flexibility that Python can afford to offer, but may not necessarily be familiar with how to write Python in the first place! Some may have a CS background, while others may not. So I decided to start a tutorial series of sorts wherein I tackle a problem and show an easy, specific way to write an AppDaemon app, as well a more complex, flexible way to do the same thing.
In the easy version, I will try to break the solution down in simple terms so that someone who knows absolutely nothing about Python can understand them. The more complex example will also have a brief description explaining general logic, but will be targeted towards those of you who have a better grasp of the fundamentals.
I hope you enjoy!
#Tutorial #1 - Tracker-Notifer
â simple â
door_notifications.py
import appdaemon.appapi as appapi
#
# App to send notification when a door left open for too long
#
# Args: (set these in appdaemon.cfg)
# ttl = # of seconds to wait until notified
#
#
# EXAMPLE appdaemon.cfg entry below
#
# # Apps
#
# [door_notifications]
# module = door_notifications
# class = DoorMonitor
# ttl = 15
#
class DoorMonitor(appapi.AppDaemon):
def initialize(self):
self.door_entities = ['binary_sensor.door_sensor1', 'binary_sensor.door_sensor2',
'binary_sensor.door_sensor3', 'binary_sensor.door_sensor4']
self.door_timer_library = {}
for door in self.door_entities:
self.listen_state(self.tracker, entity=door)
def tracker(self, entity, attribute, old, new, kwargs):
try:
self.cancel_timer(self.door_timer_library[entity])
except KeyError:
self.log('Tried to cancel a timer for {}, but none existed!'.format(entity),
level='DEBUG')
if new == 'on':
self.door_timer_library[entity] = self.run_in(self.notifier,
seconds=int(self.args['ttl']),
entity_name=entity)
def notifier(self, kwargs):
friendly_name = self.get_state(kwargs['entity_name'], attribute='friendly_name')
title = "Message from HASS!"
message = "{} has been open for more than {} seconds.".format(friendly_name,
self.args['ttl'])
self.call_service('notify/notify', title=title, message=message)
In all AppDaemon apps, the first thing you always want to do is import the AppDaemon api! This is core to AppDaemon and allows us to access values within HomeAssistant. Immediately following the import
section, youâll find a short bit of documentation on what this app does. In our case, we are telling our users that there should be a single argument that weâd like for them to set in the appdaemon.cfg
file, and thatâs the ttl
or time to live
value. This is the number of second to wait until being notified of the state of our door. Itâs directly followed by an example of how it should be placed in the appdaemon.cfg
file, which I always think is a nice thing to do.
We then get into building the app itself. Weâll call our app DoorMonitor
, as thatâs what this app will be doing: monitoring the state of our doors, and if they are open for too long (ie, longer than that ttl
value), then weâll notify the user of this occurrence. In my mind, the app 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!
def initialize
We first define a variable called door_entities
in what we call a list. Simply put, this is a way to hold our data in memory much like you would hold grocery items for a shopping list on paper. It keeps a number of things, in our case various entity_id
s, in an easily-accessible area. Youâll want to fill this list with the entity ids of all your door sensors.
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â which is associated with a âvalueâ. These are called key-value pairs, because the key references a value. To give you another real world example⌠you can simply look at the function of a spoken-language dictionary. Each word has a definition, and these wordsâ functions are unique. Thereâs more nuance here, but this metaphor does the job well enough.
Weâll then call listen_state
for-each entity_id
in the door_entities
list that we created earlier, so that whenever their state changes, our tracker function will fire!
def tracker
We start off this function by doing a bit of Python Magic thatâs a little bit out of scope for our âeasyâ lesson, but weâll definitely come back to it in just a minute.
On line 41 we are evaluating to see if the new
value of our entity is on
, which in our case can mean open
. On the next line, youâll see a long stretch of code - donât be intimidated! Letâs break it down.
-
Goal: Run the
notifier
function after some number of seconds (defined byttl
) -
Optionally: Provide the
entity_id
of door that has been open for too long - So it can be said after the door is trigger as
on
we wantself.notifier
torun_in
the number ofseconds=<ttl_value>
Youâll also see this long line of code actually wraps to the next line⌠weâll do a little bit more Python magic and supply notifier
a keyword argument (hey, remember dictionaries?) called entity
with the entity id of the door that is in the on
position.
Thatâs actually 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. Itâs helpful to know that our good friend @aimc has given us a few tricks with timers in AppDaemon. Timers will return a handle with which you can use to remove it from the global scheduler. What weâre doing here is putting the timerâs handle in the door_timer_library
, with its key being the entity id.
So now we have a way to act on if the door is open, or âonâ, but what happens when the door is opened, and closed immediately? We donât want to notify ourselves if the door is closed! Well if you jump back up to line 36, youâll see this is exactly what weâre doing. Whenever the state of the door changes, this tracker function will run and try
to cancel the timer associated with that entity id. If it runs into anything it doesnât expect, weâll fallback and throw a message in our logfile
.
Whew! Now that weâve got the hard part out of the way⌠weâll want to build a function that will notify ourselves of the event that happened.
def notifier
The very first thing we do is pull out the friendly_name
of the door entity that has been open for longer than the time-to-live value. We then simply set a few variable for title
and message
.
-
title
is a static string, âMessage from HASS!â -
message
is actually a framework where the first value is thefriendly_name
of the door, and second value is the number of seconds the user set as thettl
argument.
Finally weâll call the notify service with our defined values.
And thatâs it! Pretty simple, huh?
â complex â
monitor_notifier.py
import appdaemon.appapi as appapi
import re
from datetime import datetime, timedelta
#
# App to send notification when door opened or closed
#
# Args:
# ttl = number of seconds to wait until notified
# sensortypes_to_monitor = general name of sensor to montior
#
# EXAMPLE appdaemon.cfg entry below
#
# # Apps
#
# [monitor_notifier]
# module = monitor_notifier
# class = MonitorNotifier
# ttl = 15
# sensors_to_monitor = binary_sensor.door1
# sensortypes_to_monitor = door,garage,window
#
class MonitorNotifier(appapi.AppDaemon):
def initialize(self):
self.timer_library = {}
entities_to_monitor = []
if 'sensortypes_to_monitor' in self.args:
entities_to_monitor.extend(self.args['sensortypes_to_monitor'].split(','))
elif 'sensors_to_monitor' in self.args:
entities_to_monitor.extend(self.args['sensors_to_monitor'].split(','))
for entity_type in entities_to_monitor:
self._add_tracker(entity_name=entity_type)
self.listen_event(self.add_tracker, 'monitor_add_tracker')
def add_tracker(self, event_name, data, kwargs):
try:
entity_name = data['entity_name']
except KeyError:
self.error("You must supply a valid entity_name!")
return
if 'ttl' in data:
ttl = data['ttl']
else:
ttl = None
self._add_tracker(entity_name=entity_name, ttl=ttl)
def _add_tracker(self, entity_name, ttl=None):
""" Add a tracker for entity_id, optionally setting a ttl """
try:
self._check_entity(entity_name)
entities = [entity_name]
except ValueError:
try:
re_entity = re.compile(".*({}).*".format(entity_name))
entities = [entity for entity in self.get_state() if re_entity.match(entity)
if 'group' not in entity]
except re.error:
self.error("{} is not a valid entity name or RegEx string.".format(entity_name))
return
if not ttl:
ttl = self.args['ttl']
for entity in entities:
self.listen_state(self.tracker, entity=entity, ttl=ttl)
def tracker(self, entity, attribute, old, new, kwargs):
try:
self.cancel_timer(self.timer_library[entity])
except KeyError:
self.log('Tried to cancel a timer for {}, but none existed!'.format(entity),
level='DEBUG')
if new in ['on', 'open']:
self.timer_library[entity] = self.run_in(self.notifier,
seconds=int(kwargs['ttl']),
entity_name=entity,
ttl=kwargs['ttl'],
entity_state=new)
def notifier(self, kwargs):
friendly_name = self.get_state(kwargs['entity_name'], attribute='friendly_name')
last_seen = datetime.now() - timedelta(seconds=int(kwargs['ttl']))
title = "Message from HASS".format(friendly_name)
message = ("[{}] {} has been {} for more than {} seconds."
.format(last_seen.strftime('%H:%M:%S'), friendly_name,
kwargs['entity_state'], kwargs['ttl']))
self.call_service('notify/notify', title=title, message=message)
Iâm going to go a bit quicker in this section, otherwise this would be an incredibly long post.
Weâll want to import re
and datetime
and timedelta
from the datetime
library. RegEx will help us be more flexible with which types of entities we are looking to monitor, while datetime gives us a bit of flexibility in working with our ttl
values.
Youâll notice we can define two optional arguments in our config file: sensors_to_monitor
and sensortypes_to_monitor
. This allows us include specific entity ids and general entities to monitor. Two examples of these would be a given binary_sensor
and all sensors with the name door
or garage
in their entity id. In both cases, this should be a comma separated list.
def initalize
Weâll create a timer library, and a short-lived data container for all the entities weâd like to register from our config file. Weâll then split up both our args
and add them to the short-lived container, and immediately call _add_tracker
on each entity.
Add Tracker functions
We have an internal _add_tracker
function and an external one. The external function listens to events sent to HomeAssistant and will call the internal function if successful. Itâs important to know here that in order to make this app act closer to a service with interaction, simply logging an error here would be unacceptable. On failure, youâd want to call a notification service of your choice, and on success, youâd also want to give some sort of user feedback. This has been left out for brevity.
_add_tracker
utlizes some code that is located directly in @aimcâs api. We first check to see that the entity id is valid. If it is not, _check_entity
raises a ValueError
. This is not necessarily a bad thing! Since we can supply _add_tracker
a sensortype
, which is obviously not a valid entity id, weâll then construct a RegEx object and compare that to all the entities in your personal HomeAssistant API, discarding any groups. Again, we log an error if something is unexpected here.
The rest is fairly simple. We can provide _add_tracker
a ttl
value or not, it does not matter much. We also want to supply the given ttl
value to the callback function tracker
.
def tracker
This function is much the same as it is in the easy version. We account for two different types of new
values, and we supply both the ttl
and new
values to notifier
since they have the possibility to be variable.
def notifier
Notifier is also very similar as it is to the easy version. Since the ttl
value is variable, we have an extra line to calculate the last time the sensor was seen in the acceptable position and supply it to the message
. The message is also slightly more variable here as well. Hereâs a little example of the output!
Iâve gone back and forth on what medium to use, including various blogging platforms as well as YouTube. I might experiment with solving a problem âliveâ and thinking my way through the problem out loud as well.
Feedback is important! This series will only be as successful as you all make it to be! Let me know your thoughts and what you all would like to see, and Iâll consider all my options.
Happy Automating!
- SN