This is my first non-trivial AppDaemon app, and also my first non-trivial Python program. I am interested in two things. First thoughs? Second constructive thoughts around my Python programming. Where have I done things the hard way? There are many places where I probably could have made it cleaner, used functions better, named functions better, etc. I’m looking more for better data structures and other features of python, AppDaemon and HA that I didn’t use as well as I could.
import appdaemon.appapi as appapi
import os
import json
import datetime
class alarmclock(appapi.AppDaemon):
# Created an initialization file just to mimic AD it's called from __init__
def initialize(self):
self.log("in initialize")
# initialize variables
self.filename=self.config["AppDaemon"]["app_dir"] + "/" + "alarmclock.cfg"
self.log("filename= {}".format(self.filename))
self.alarms={}
self.alarmhandles={}
self.done=False
self.loadalarms() # load alarms from disk
self.displayalarms() # display the alarms to the log just for grins
# setup listeners
self.listen_state(self.handle_input_slider, "input_slider")
self.listen_state(self.handle_input_boolean, "input_boolean")
self.listen_event(self.restartHA,"ha_started")
# setup initial values in HA based on saved alarm settings
self.updateHA()
# handle HA restart
def restartHA(self,event_name,data,kwargs):
self.log("HA event {}".format(event_name))
self.updateHA()
# update initial values in HA after HA restart or AD restart
def updateHA(self):
for room in self.alarms:
# set input sliders for hour and minutes
self.select_value("input_slider.{}alarmhour".format(room),str(self.parse_time(self.alarms[room]["time"]+":00").hour))
self.select_value("input_slider.{}alarmminutes".format(room),str(self.parse_time(self.alarms[room]["time"]+":00").minute))
# set input boolean for alarm status
if self.alarms[room]["active"] == "on" :
self.turn_on("input_boolean.{}alarm".format(room))
else:
self.turn_off("input_boolean.{}alarm".format(room))
# since this is being done as the result of either HA or AD restarting lets make sure all alarms schedules are in place
self.schedulealarm(room)
# Save alarms file to a json file
def savealarms(self):
self.log("in savealarms")
fout=open(self.filename,"wt")
self.displayalarms()
json.dump(self.alarms,fout)
fout.close()
# Load alarms from the json file
def loadalarms(self):
self.log("checking on file {}".format(self.filename))
if os.path.exists(self.filename) :
# file exists so open and load data
fout=open(self.filename,"rt")
self.alarms=json.load(fout)
fout.close()
# Set values on input sliders and activation switch in HA
else:
# file does not exist so initialize alarms
self.alarms={}
# add alarm to dictionary
def addalarm(self,room,timeincrement,value):
#initialize values for alarm
self.attributes={"time":"0:0","active":"False"}
self.alarms[room]=self.attributes
# now just run update alarm with the new values
self.updatealarm(room,timeincrement,value)
# update existing alarm in dictionary
def updatealarm(self,room,timeincrement,value):
timevalue=self.alarms[room]["time"]
savehour=timevalue[:timevalue.find(":")]
saveminute=timevalue[timevalue.find(":")+1:]
if timeincrement=="hour" : # handle hour input slider
savehour=str(value)
else : # else it has to be the minute input slider so handle it
saveminute=str(value)
timevalue=savehour + ":" + saveminute
self.alarms[room]["time"]=timevalue
self.displayalarm(room)
# input boolean for turning the alarm on or off
def handle_input_boolean(self, entity, attribute, old, new, kwargs):
self.log("in handle_input_boolean")
room=entity[entity.find(".")+1:entity.find("alarm")].lower()
if room in ['sam','charlie','master','guest']:
self.alarms[room]["active"]=new
self.log("room {} active set to {}".format(room,new))
self.schedulealarm(room)
self.savealarms()
# This would be the callback function when an input_slider is changed
def handle_input_slider(self, entity, attribute, old, new, kwargs):
self.log("in handle_input_slider")
room=entity[entity.find(".")+1:entity.find("alarm")].lower()
timeincrement=entity[entity.find("alarm")+5:].lower()
# the input slider keeps returning a float but we need an integer so convert string to float and float to integer.
x=int(float(new))
# manage variable range
if timeincrement=="hour":
maxvalue=23
else :
maxvalue=59
if (x>=0) and (x <= maxvalue) :
self.log("value good")
if room in self.alarms :
self.updatealarm(room,timeincrement,x)
else :
self.addalarm(room,timeincrement,x)
# schedule and save the alarms
self.schedulealarm(room)
self.savealarms()
def schedulealarm(self,room):
self.log("In schedulealarm - {}".format(room))
# if the alarm is active then schedule it
if self.alarms[room]["active"] == "on":
# make a valid time string
timestr=self.alarms[room]["time"]+":00"
alarmtime=self.parse_time(timestr)
# if there isn't a current alarmhandle the just schedule the alarm, else cancel the current alarmhandle and create a new one
if self.alarmhandles.get(room,"")=="":
self.log("handle was empty")
self.alarmhandles[room]=self.run_daily(self.alarm_lights,alarmtime,arg1=room)
else:
# an alarm handle already existed so delete it and create a new one with the corrected time.
self.log("Handle already existed {}".format(self.alarmhandles[room]))
self.cancel_timer(self.alarmhandles[room])
handle=self.run_daily(self.alarm_lights,alarmtime,arg1=room)
self.alarmhandles[room]=handle
else:
self.log("alarm for room {} is in state {}".format(room,self.alarms[room]["active"]))
# the alarm is not on in this room, so if there is a current schedule for it, remove it.
if room in self.alarmhandles :
self.log("removing existing alarm from schedule")
self.cancel_timer(self.alarmhandles[room])
# right now, we only have one light in each room to turn on, and they are named consistently
# in the future, there should be a list of devices to turn on in response to an alarm
# also provide method of selecting days to run alarm possibly tied into calendar...
def alarm_lights(self,kwargs):
if datetime.datetime.today().weekday() < 5:
self.turn_on("switch.{}_light_switch".format(kwargs["arg1"]))
self.log("Lights should have been turned on switch.{}_light_switch".format(kwargs["arg1"]))
else:
self.log("Lights not turned on because its the weekend")
# Display single alarm data
def displayalarm(self,room):
self.log("Room={}".format(room))
self.log("Attribut Value")
for alarmattribute,value in self.alarms[room].items():
self.log("{}{}".format(alarmattribute.ljust(10),value))
self.log(" ")
# Display all alarms by looping through all rooms and calling displayalarm above.
def displayalarms(self):
self.log("Displaying all alarms")
for room,alarmdict in self.alarms.items() :
self.displayalarm(room)