A complete Guide to Motion Lighting in Smart Homes – Introducing AppDaemon

I have previously proposed a motion lighting implementation and discussed some of the problems Home Assistant’s YAML based configuration creates for people who are used to the flexibility of fully fledged, battle-tested, object-oriented programming languages. Some automation concepts actually require object instances per automation entity to track automation-internal state for different physical devices.

Introducing AppDaemon – a Python-based way to define automations and interact with your smart home in Home Assistant. AppDaemon allows you to define Python “apps” that can be reused with different parameters.

Motion Activated Lighting

Consider the surprisingly complex example of motion activated lights in a smart home environment. It’s a simple concept to understand, however, it can cause a number of subtle problems that are difficult and messy to solve without object-oriented programming ideas and reusability of code. What we are doing is creating a virtual motion light that uses physical devices as inputs and outputs. I have attempted to solve these issues before and it was less than pretty. To refresh our memory, motion lights have the following requirements (R):

  1. turn on when motion is detected
  2. turn off when no motion is detected after some timeout
  3. Do not interfere with manually activated lights (tricky and less than obvious)

That last one can be broken into the following two requirements:

  • 3.1 A light that is already on should not be affected by time outs.
  • 3.2 A light that is switched on within the time-out period should have its timer cancelled, and therefore stay on.

That last point is less obvious, but very important to handle as the following scenario should demonstrate:

In other words, we do not want motion lighting to override manually controlled lights. If the light is already on, then appropriate lighting has already been taken care of and is not a problem we need to solve in automations!

We can achieve this by implementing this simple logic:

The motion light comes back into action when the light is turned off by something else (manual or another automation).

The previous implementation attempt was messy because I did not know of the existence of AppDaemon. It allows automations to be defined as Python objects, which essentially allows us to create virtual devices defined by Python classes. Each motion sensor would get its own MotionLight object which contains some internal state, tracked individually for each motion sensor.

This allows us to implement very specific automations. The only way to achieve this in HA’s YAML configuration is by duplicating the automation for different motion sensors.1

Python code follows:

import appdaemon.plugins.hass.hassapi as hass

#
# App to turn lights on when motion detected then off again after a delay
#
# Use with constrints to activate only for the hours of darkness
#
# Args:
#
# sensor: binary sensor to use as trigger
# entity_on : entity to turn on when detecting motion, can be a light, script, scene or anything else that can be turned on
# entity_off : entity to turn off when detecting motion, can be a light, script or anything else that can be turned off. Can also be a scene which will be turned on
# delay: amount of time after turning on to turn off again. If not specified defaults to 60 seconds.
#


class MotionLights(hass.Hass):
  handle = None
  isOn = False
  delay = 60# default delay
  def initialize(self):

    self.handle = None

    if "delay" in self.args:
      self.delay = self.args["delay"]
    # Check some Params

    # Subscribe to sensors - change this to passed in RF code and MQTT topic subscription.
    if "sensor" in self.args:
      self.listen_state(self.motion, self.args["sensor"])
    else:
      self.log("No sensor specified, doing nothing")



  def motion(self, entity, attribute, old, new, kwargs):
    if new == "on":
        state = self.get_state(self.args["entity_on"])
        self.log("state is {}".format(state))
        if state == "off":
          if "entity_on" in self.args:
            self.log("Motion detected: turning {} on and starting timer for {} seconds".format(self.args["entity_on"], self.args["delay"]))
            self.turn_on(self.args["entity_on"])
            self.isOn = True

          self.cancel_timer(self.handle)
          self.handle = self.run_in(self.light_off, self.delay)
        else:
            self.log("Entity is already switched on. Motion trigger ignored.")

  def light_off(self, kwargs):
    self.log("isOn {}".format(self.isOn))
    if "entity_off" in self.args:
        if self.isOn:
            self.log("Turning {} off".format(self.args["entity_off"]))
            self.turn_off(self.args["entity_off"])
            self.isOn = False


  def cancel(self):
    self.cancel_timer(self.handle)

And this is how you configure it inside AppDaemon’s config file:

motion_lights:
  module: motion_lights
  class: MotionLights
  sensor: input_boolean.sense_motion
  entity_on: light.living_room_floor_lamp
  entity_off: light.living_room_floor_lamp
  delay: 5

Or implementing complex templates… not worth it, given a python script will be way more flexible and maintainable. 

Related posts

Troubleshooting Intermittent WiFi Issues: Solving “Host Unreachable, No IP Route” Error on Android and NUC Devices

How to Optimize Docker Builds with Nexus OSS for Apt, Maven, Docker and NPM Dependencies

Troubleshooting Asus Xonar U7: Blinking LED and Connectivity Issues

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Read More