AppDaemon light schedule with sensor override

This is my first try to do a automation in AppDaemon that uses schedules to turn on and off lights with the optional behaviour of having a sensor that when active delays the turning off the lights.

This is a work in progress, any feedback is welcome. I need to add support for multiple sensors, and sensor functionality is mostly written with media players in mind right now.

import appdaemon.appapi as appapi

#
# Appdaemon light schedules App
#
# Schedules lights for on and off switching, also allows off switch to be overridden by sensor
#
# Args:
#
# time: ', ' separated list with one or more time and action
#   example: time = 05:00:00 on, sunrise off
# light: ', ' separated list with one or more light entity_id
#   example: light = light.lamp1, light.lamp2
# group: ', ' separated list with one or more group containing light
#   example: group = group.lightgroup1, group.lightgroup2
# sensor: single sensor that can delay schedule when turning off 
#
# Note: takes inspiration from AppDaemon motion detectionlights

class lighting(appapi.AppDaemon):

  def initialize(self):
    self.lights = []
    self.schedules = {}
    self.sensors = None
    self.sensor_active = False
    self.sensor_activity_handle = None
    self.schedule_active = False
    self.populate_lights()
    self.populate_schedules()
    self.populate_sensors()
    self.schedule_callback()
    self.log("Initialized")


  def run_daily_callback(self, kwargs):
    action = kwargs["action"] if "action" in kwargs else False
    trigger = kwargs["trigger"] if "trigger" in kwargs else False
    if self.sensors and not self.sensor(action, trigger): return
    for light in self.lights:
      self.call_service("light/turn_"+action, entity_id = light)
    self.log("Turned {} {}".format(action, self.lights))



#########################################################################################
#
# Sensor functions
#

  # Sensor is set up to listen when action is on
  # Sensor will stop listen on two different situations
  #   1. Sensor is low before schedule has triggered
  #   2. Sensor is low after 
  def sensor(self, action, trigger):
    if action == "on":
      self.sensor_activity_handle = self.listen_state(self.sensor_activity, self.sensors)
      self.log("Listening on sensor {}".format(self.sensors))
      self.schedule_active = True
    elif action == "off":
      # Schedule hasn't triggered yet, sensor is not allowed to turn off light
      if trigger == "sensor" and self.schedule_active:
        self.sensor_active = False
        self.log("Sensor tried to turn off lights but it is too early")
        return False
      # Schedule has triggered and sensor is active, sensor is now responsible for turning off light
      elif trigger == "schedule" and self.sensor_active:
        self.schedule_active = False
        self.log("Sensor is now allowed to turn off lights")
        return False
      # Everything is ready to turn off
      else:
        self.cancel_listen_state(self.sensor_activity_handle)
        #self.allow_sensor_override = False #self.schedule_active = False
        self.schedule_active = False
        self.sensor_active = False
    return True


  # Triggers on sensor activity
  def sensor_activity(self, entity, attribute, old, new, kwargs):
    self.log("Sensor {} changed state to {}".format(entity, new))
    if self.sensor_active:
      self.cancel_timer(self.sensor_active)
    if new == "idle":
      self.sensor_active = self.run_in(self.run_daily_callback, 450, action = "off", trigger = "sensor")
    else:
      self.sensor_active = True



#########################################################################################
#
# Initalization functions
#

  # Splits configured list "light" into a global python list "self.lights"
  # Resolves lights from configured list "group" into a global python list "self.lights"
  def populate_lights(self):
    if "light" in self.args:
      self.lights = self.args["light"].split(", ")
    if "group" in self.args:
      for group in self.args["group"].split(", "):
        for light in self.resolve_group(group):
          if not light in self.lights:
            self.lights.append(light)
    self.lights.sort()
    self.log("Lights for schedules {}".format(self.lights))


  # Splits a configured list of schedules with actions into a dict "self.schedules"
  def populate_schedules(self):
    if "time" in self.args:
      for schedule in self.args["time"].split(", "):
        time, action = schedule.split(" ")
        self.schedules[time] = action
    self.log("Schedules and actions {}".format(self.schedules))


  def populate_sensors(self):
    if "sensor" in self.args:
      self.sensors = self.args["sensor"]
      #self.sensors = self.args["sensor"].split(", ")
    self.log("Sensors that can override schedule {}".format(self.sensors))


  def schedule_callback(self):
    for time, action in self.schedules.items():
      time = self.parse_time(time)
      self.run_daily(self.run_daily_callback, time, action = action, trigger = "schedule")



#########################################################################################
#
# Support functions
#

  # Returns a list of entity_ids from the group
  def resolve_group(self, group):
    group = self.get_state(group, attribute = "all")
    return group["attributes"]["entity_id"]
1 Like

i must say i dont realy see the advantage from this.
if you want to turn off a lot off lights on different times on or off you would still get a lot of entries in your cfg file.
and i guess the only point of making it complicated like this is to get a simple overview from your schedule.

Advantage? We are two monkeys that have written two different versions of one of Shakespeares home automation systems :smile:

I don’t have more than two rooms + outside that I have controllable lighting in yet so yes the configuration is quite slim right now. That is definitely not an issue right now. I try to avoid as much configurations in AppDaemon as possible keep that in HASS.

/R

if you configure the right groups in hass it could be like this in your cfg:

group = groupid
time = “put in time”
action = “on/off”
overridesensor = sensorid

for every action.

then your code could be less the 10 lines. :wink:

i created my schedule because the list of lights to set on and off is growing and growing
and they all need different schedules.
in the end i will have 60 or more lights that are automated.
maybe you see why i have a need to have a decent overview :wink:

Of course I could =D, but getting it the way I want it to will mean that some things don’t fit into being defined in HASS. AppDaemon really allows me to be more dynamic, which solves a lot of hard situations for me. At least how I view stuff right now, and things might change. I still only have a few smart lights and not many sensors either, so maybe I will need a wholly different approach when I reach the same amount of smart peripherals as you.

/R

1 Like

I think I’m missing some of the code above can you share the updated code! :slight_smile:
@Robban

I don’t think that I have done anything with this automation. It should be complete, might be that Appdaemon and Hass have changed since then but I think its still the same code running on my pi.

# Splits a configured list of schedules with actions into a dict "self.schedules"
  def populate_schedules(self):
    if "time" in self.args:
      for schedule in self.args["time"].split(", "):
        time, action = schedule.split(" ")
        self.schedules[time] = action
    self.log("Schedules and actions {}".format(self.schedules))

It seems to be this part it’s not liking the split?
maybe even this line …

 time, action = schedule.split(" ")

tried adding a comma but not sure what I’m doing here. :slightly_smiling_face:

For schedules you specify a time and action with a space as separator. ‘05:00:00 on’

Cool got it working like that! Just didn’t know what I was doing now also made it work with switches! Now can the sensor work with an input_boolean?

Probably will. Maybe you need to do small alterations