Resolving Home Assistant upgrade errors in custom Python Components

Resolving Home Assistant upgrade errors in custom Python Components

Upgrading to Home Assistant v0.69.0 broke some of my custom components that use the MQTT Python module. The reason for this is a small change in the module API. This post explains how to fix your custom components to work with the latest version of Home Assistant!

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:

Say you switched on your lights manually because you are reading a book in the lounge. You get up to grab a drink and your motion sensor is triggered. This — in turn — starts the sequence of events which ultimately ends in the lights turning off (due to motion sensor timeout), leaving you sitting in the dark. Very awkward. This happened even though you switched them on manually before the motion sensor was activated.

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:

When motion is detected, turn light on if it is off (R1) and only turn it off (R2) if it was turned on by the motion automation (R3.1) (and nothing else). If the light was controlled by another entity during the time out period (R3.2), then cancel the timer and do not send any further commands to the light—as to not alter its state. (R3.1). Next time motion is triggered, the light is already on and is therefore ignored.

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**.

In essence, a MotionLight is a virtual device that takes a set of motion sensors as input, and a set of lights as output. The MotionLight itself is not a physical entity and only exists within the Python environment.

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

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

Pagination