[App] Motion controlled switch with light threshold

This is what started as a simple App that if the light level was below a given threshold, it turned on my light switch for a specific time when motion was detected…

Then I found out that I needed to take PIR time (the time of no motion before the motion detector will be ready to activate again) into consideration as well.
Then @crondk showed off input sliders which I thought was a great way to interactively change the light threshold from my phone.

A special thanks to @aimc and @ReneTode for their excellent support in the forums!

Comments are welcome! :slight_smile:

motionLightSensor.py
import appdaemon.appapi as appapi


class MotionLightSensor(appapi.AppDaemon):
    """Manage a switch using motion and light

    Features:
       - Turns on switch when motion is detected and light level is not above
         configured threshold.
       - Turns off switch after configured delay period
       - If the motion sensor is still active after delay period, keep light on
         and start a new delay period.
       - Optionally, use input_slider to set light threshold and switch adjusts
         directly.

    Arguments:
        light_level:  Above this level, the switch is always off.
        light_slider (optional): Slider to interactively set light_level with.
            The light_level above is used as the starting value.
        delay:  Seconds to keep switch on after binary_sensor has triggered.
        switch:  Switch to control.
        binary_sensor: Motion sensor.
        light_sensor: Sensor used to meassure luminance.

    Example configuration
    In appdaemon.yaml:
        motion_constrained_sensor:
          module: motionLightSensor
          class: MotionLightSensor
          light_level: '10'
          light_slider: input_slider.light_threshold
          delay: '300'
          switch: switch.fibaro_wall_plug_switch
          binary_sensor: binary_sensor.hallway_sensor
          light_sensor: sensor.aeotec_zw100_multisensor_6_luminance

    In configuration.yaml:
        # Define input_slider
        input_slider:
          light_threshold:
            name: Light level treshold
            min: 0
            max: 20
            step: 1
    """

    def initialize(self):
        self.log("Initializing MotionLightSensor")
        self.handle = None
        # Access all arguments in order to trigger error due to missing
        # arguments during initialization
        self.arg_switch = self.args["switch"]
        self.arg_light_sensor = self.args["light_sensor"]
        self.arg_binary_sensor = self.args["binary_sensor"]
        self.light_threshold = float(self.args["light_level"])
        self.log("Light threshold is {}".format(self.light_threshold))
        # If a slider has been defined, it is used to interactively set light
        # threshold
        if "light_slider" in self.args:
            self.log("Using {} to set light threshold".format(
                self.args["light_slider"]))
            self.set_state(self.args["light_slider"],
                           state=self.light_threshold)
            self.listen_state(self.light_slider_change,
                              self.args["light_slider"])
        self.arg_delay = self.args["delay"]
        # Subscribe to all "on" events from the binary sensor
        self.listen_state(self.motion_event, self.arg_binary_sensor, new="on")
        # Subscribe to all events from the light sensor
        self.listen_state(self.light_event, self.arg_light_sensor)
        # Report light sensors current value
        light_level = self.light_level()
        self.log("Light level {}".format(light_level))

    def light_slider_change(self, entity, attribute, old, new, kwargs):
        self.log("Light threshold changed from {} to {}".format(old, new))
        self.light_threshold = float(new)
        light_level = self.light_level()
        if (
            light_level > float(new) and
            light_level <= float(old)
        ):
            self.turn_switch_off()
        elif (
            light_level <= float(new) and
            light_level > float(old) and
            self.get_state(self.arg_binary_sensor, "state") == "on"
        ):
            self.turn_switch_on()

    def light_level(self):
        light_level = float(self.get_state(self.arg_light_sensor, "state"))
        return light_level

    def motion_event(self, entity, attribute, old, new, kwargs):
        self.log("Motion detected")
        if self.light_level() <= self.light_threshold:
            self.log("It is dark enough")
            self.turn_switch_on()

    def light_event(self, entity, attribute, old, new, kwargs):
        self.log("Light level changed from {} to {}".format(old, new))
        if (
            float(new) > self.light_threshold and
            float(old) <= self.light_threshold
        ):
            self.turn_switch_off()
        elif (
            float(new) <= self.light_threshold and
            float(old) > self.light_threshold and
            self.get_state(self.arg_binary_sensor, "state") == "on"
        ):
            self.turn_switch_on()

    def start_delay(self):
        self.log("Starting delay timer")
        self.cancel_timer(self.handle)
        self.handle = self.run_in(self.delay_done, self.arg_delay)

    def delay_done(self, kwargs):
        self.log("Delay passed")
        if (
            self.get_state(self.arg_binary_sensor, "state") == "on" and
            self.light_level() <= self.light_threshold
        ):
            self.log("Motion sensor still active and it is dark enough,"
                     " starting new delay")
            self.start_delay()
        else:
            self.log("Sensor inactive or not dark enough")
            self.turn_switch_off()

    def turn_switch_on(self):
        self.log("Turning on {}".format(self.arg_switch))
        self.turn_on(self.arg_switch)
        self.start_delay()

    def turn_switch_off(self, kwargs=None):
        self.log("Turning off {}".format(self.arg_switch))
        self.turn_off(self.arg_switch)
8 Likes

nice. also very nice documentation!

Also I like the use of the [App] tag in the subject. We should make that a suggestion in the next version announcement.

1 Like

Thanks @Phudeot for the recipe but not sure how to implement it correctly ! What should be saved in a python file and where should I put it in my HA system ? :wink:

The short answer: Copy the whole code snippet into the Python file app/motionLightSensor.py, where app is placed in the Appdaemon configuration directory.

If you are new to AppDaemon I recommend you read through the documentation and get the hello_world app to work as described on http://appdaemon.readthedocs.io/en/latest/INSTALL/#configuration through
http://appdaemon.readthedocs.io/en/latest/INSTALL/#configuring-a-test-app

Now that you have gotten your first app to work, take the code in the first post and place it in apps/MotionLightSensor.py, just beside hello.py.

I noticed that I had a misinformed module name in the included example configuration in the docstring of motionLightSensor.py (edit: fixed). I’ll repeat the correct one here for convenience. In appdaemon.yaml, just below your configuration of hello.py, you add the configuration for


# Apps
hello_world:
  module: hello
  class: HelloWorld

motion_constrained_sensor:
  module: motionLightSensor
  class: MotionLightSensor
  light_level: '10'
  light_slider: input_slider.light_threshold
  delay: '300'
  switch: switch.fibaro_wall_plug_switch
  binary_sensor: binary_sensor.hallway_sensor
  light_sensor: sensor.aeotec_zw100_multisensor_6_luminance

Naturally, you need to change the entity ids of the switch and sensors to some of your own switches/sensors. :wink:

And then somewhere in HA’s configuration.yaml you define the input_slider:

# Define input_slider
input_slider:
  light_threshold:
    name: Light level treshold
    min: 0
    max: 20
    step: 1
1 Like

@Phudeot Thanks for explanations :wink: I didn’t notice it was a recipe to use with AppDaemon so not for me :frowning: I gave up on setting up AppDaemon, too complicated ! Thanks anyways :slight_smile:

I was going to use this as my first AD app to work on, but I get this warning:

2018-04-01 09:02:56.043217 WARNING AppDaemon: ------------------------------------------------------------
2018-04-01 09:02:56.043396 WARNING AppDaemon: Traceback (most recent call last):
  File "/opt/AppDaemon/lib/python3.6/site-packages/appdaemon/appdaemon.py", line 1971, in check_app_updates
    self.init_object(app)
  File "/opt/AppDaemon/lib/python3.6/site-packages/appdaemon/appdaemon.py", line 1500, in init_object
    app_class = getattr(modname, app_args["class"])
AttributeError: module 'motionLightSensor' has no attribute 'MotionLightSensor'

I’m still on an old version of Home Assistant and AppDaemon so maybe my setup should be modified…

The error message indicates that there is something wrong with the line class MotionLightSensor(appapi.AppDaemon): in motionLightSensor.py, perhaps just a spelling issue, or capitilazation error…

I think it should be:

import appdaemon.plugins.hass.hassapi as hass

class MotionLightSensor(hass.Hass):

Yes, if you’re on Hass (which I’m not) that sounds right.
I’m glad you found a fix!

Thanks so much for sharing, this is great, I was looking to do somthing similar but you have done it more nicer than I would have. I just need to add some extra logic so my lights can change depending on the time of the night :slight_smile:

1 Like

Just in case anyone is interested. I have modified the original code so I can I can specify time ranges (only whole hours) that include the brightness and color and also an option to specify a device tracker that should stop the lights going out while “home” in my case my TV.

If anyone is interested I can share my code. I will just share my app config for now to give you an idea of how it should work (still testing it)

motion_light_main_lounge:
  module: motion_lights_light
  class: MotionLightSensor
  light_level: '170'
  delay: '900'
  switch: light.lounge
  binary_sensor: binary_sensor.motion_sensor_158d00013fb2d7
  light_sensor: sensor.illumination_f0b429b3e568
  delay_switch_off: device_tracker.samsungtv
  variable_light:
    range1:
      brightness: 255
      start_hour: 5
      end_hour: 19
      light_color: [255,189,46]
    range2:
      brightness: 125
      start_hour: 20
      end_hour: 22
      light_color: [255,189,46]
    range3:
      brightness: 25
      start_hour: 23
      end_hour: 6
      light_color: [255,70,0]
1 Like

Cool addition!
Now I’d like to shift my light to RGB as well… :wink:
I do like to see your code so please post it.

Of course I will share my code some time this week. I am still testing it so it might be buggy and it might not be very pretty because I dont really know much Python.

1 Like

This is pretty cool! I set up a motion sensor light a while ago using a Raspberry Pi to publish an MQTT message to Home Assistant whenever it got a signal from a PIR sensor (hooked up to the GPIO pins), and then set up a few automations to deal with turning the lights on/off (and also checking the last time the sensor was activated to ensure the light didn’t turn off if someone was still in the room) - but your solution seems much more functional! :slight_smile:

1 Like

You can use a nodemcu (esp8266 or esp32) and ESPEasy firmware, micro-python or bens multi-sensor firmware to provide sensor information via MQTT to homeassistant and they are about $3 each.

1 Like

This is what I have so far… it might be buggy but seems to work for me :slight_smile:

import appdaemon.plugins.hass.hassapi as hass
import datetime as dt

class MotionLightSensor(hass.Hass):
    """Manage a switch using motion and light

    Features:
       - Turns on switch when motion is detected and light level is not above
         configured threshold.
       - Turns off switch after configured delay period
       - If the motion sensor is still active after delay period, keep light on
         and start a new delay period.
       - Optionally, use input_slider to set light threshold and switch adjusts
         directly.

    Arguments:
        light_level:  Above this level, the switch is always off.
        delay:  Seconds to keep switch on after binary_sensor has triggered.
        switch:  Light to control (maybe should be renamed).
        binary_sensor: Motion sensor.
        light_sensor: Sensor used to meassure luminance.
        delay_switch_off device tracker that if "home" should stop lights going off
        variable_light config for specifying between which hours the light should be which brightness and color

    Example configuration
    In appdaemon.yaml:
    motion_light_leds_lounge:
      module: motion_lights_light
      class: MotionLightSensor
      light_level: '150'
      delay: '900'
      switch: light.addressableledstrip
      binary_sensor: binary_sensor.motion_sensor_158d00013fb2d7
      light_sensor: sensor.illumination_f0b429b3e568
      delay_switch_off: device_tracker.samsungtv
      variable_light:
        range1:
          brightness: 255
          start_hour: 5
          end_hour: 20
          light_color: [255,63,0]
        range2:
          brightness: 125
          start_hour: 20
          end_hour: 22
          light_color: [255,63,0]
        range3:
          brightness: 25
          start_hour: 22
          end_hour: 5
          light_color: [255,63,0]


    """

    def initialize(self):
        self.log("Initializing MotionLightSensor")
        self.handle = None
        # Access all arguments in order to trigger error due to missing
        # arguments during initialization
        self.arg_switch = self.args["switch"]
        self.arg_light_sensor = self.args["light_sensor"]
        self.arg_binary_sensor = self.args["binary_sensor"]
        self.light_threshold = float(self.args["light_level"])
        
        #Check if there is a device tracker that should stop lights going off
        #In my case its a TV, when watching TV I dont want the lights going off
        if "delay_switch_off" in self.args:        
            self.delay_switch_off =  self.args["delay_switch_off"]
        else:
            self.delay_switch_off = "none"

        self.log("Light threshold is {}".format(self.light_threshold))

        if "variable_light" in self.args:
            self.log("You have set variable light levels {}".format(
                self.args["variable_light"]))
            self.variable_light = self.args["variable_light"]
            for key in self.variable_light:
                self.log("Between {} and {} the light will be set to {}".format((self.variable_light.get(key).get("start_hour")),(self.variable_light.get(key).get("end_hour")),self.variable_light.get(key).get("brightness")))


        self.arg_delay = self.args["delay"]
        # Subscribe to all "on" events from the binary sensor
        self.listen_state(self.motion_event, self.arg_binary_sensor, new="on")
        # Report light sensors current value
        light_level = self.light_level()
        self.log("Light level {}".format(light_level))
    

    def light_brightness(self):
        self.log("Checking what level to set the light to")
        listofhours = []
        hour_now = (dt.datetime.now().hour)
        self.log("Current hour is {}".format(hour_now))
        start_hour = "none"
        end_hour = "none"
        light_level = "none"
        light_level_output = "none"
        light_color = "none"
        light_color_output = "none"
        
        #keys here are the ranges
        for key in self.variable_light:
            start_time = self.variable_light.get(key).get("start_hour")
            end_time = self.variable_light.get(key).get("end_hour")
            light_level = self.variable_light.get(key).get("brightness")
            
            self.log("Checking if light_color in variables")
            if self.variable_light.get(key).get("light_color"):
                light_color = self.variable_light.get(key).get("light_color")
                self.log("Color light configured")
            else:
                light_color = "none"
                self.log("Not a color light")
                self.log("Values in {} are : {}".format(key,self.variable_light.get(key).values()))     
                
            if start_hour > end_hour:
                listofhours = (list(range(start_time, 24, 1))) + (list(range(1, end_time, 1)))
            else:
                listofhours = (list(range(start_time, end_time +1, 1)))
            if hour_now in listofhours:
                self.log("light level {} for between {} and {}".format(light_level,start_time,end_time)) 
                light_level_output = light_level
                if light_color != "none":
                    self.log("color {} to be used for this time range".format(light_color))           
                    light_color_output = light_color
        
        if light_level_output != "none":
            self.log("light level {} and color {}".format(light_level_output,light_color_output)) 
            return light_level_output,light_color_output
        else:
            self.log("returning default light level {} color {}".format("200",light_color_output)) 
            return "200",light_color_output
               

    def light_level(self):
        light_level = float(self.get_state(self.arg_light_sensor))
        return light_level

    def motion_event(self, entity, attribute, old, new, kwargs):
        self.log("Motion detected light level is {}".format(self.light_level()))
        
        #self.get_state(self.delay_off_tracker)
        if self.light_level() <= self.light_threshold:
            self.log("It is dark enough")
            self.turn_switch_on()

    def start_delay(self):
        self.log("Starting delay timer")
        self.cancel_timer(self.handle)
        self.handle = self.run_in(self.delay_done, self.arg_delay)

    def delay_done(self, kwargs):
        self.log("Delay passed")
        if (
            self.get_state(self.arg_binary_sensor) == "on" and
            self.light_level() <= self.light_threshold
        ):
            self.log("Motion sensor still active and it is dark enough,"
                     " starting new delay")
            self.start_delay()
        else:
            self.log("Sensor inactive or not dark enough")
            self.turn_switch_off()

    def turn_switch_on(self):
       
        self.log("Turning on {}".format(self.arg_switch))
        self.brightness_color = self.light_brightness()
        self.log("Brightness level is {}".format(self.brightness_color[0]))
        if self.brightness_color[1] == "none":
            self.log("No color found turning on without color")
            self.turn_on(self.arg_switch, brightness=self.brightness_color[0])
        else:
            self.log("Found color trying turning on with color")
            self.turn_on(self.arg_switch, brightness=self.brightness_color[0], rgb_color=self.brightness_color[1])  
        self.start_delay()

    def turn_switch_off(self, kwargs=None):
        self.log("Turning off {}".format(self.arg_switch))
        if (self.delay_switch_off == "none") or (self.get_state(self.delay_switch_off) == "not_home"): 
            self.turn_off(self.arg_switch)
        else:
            #self.log("{} is {} so not switching off light".format(self.delay_switch_off,(self.get_state(delay_switch_off))))
            self.log("Not switching off light and tv on")
            #self.log("Light level changed from {} to {}".format(old, new))
            self.start_delay()

I found a bug which I fixed in the function “light_brightness” (just to do with variable names) which meant that ranges between before and after midnight were not working correctly. If anyone is still interested I will paste the updated code.

Yeah it would be great if you could, I’ve gotta update my motion light sensor app

You might want to comment out the logging, I didnt add a debug option in the config yet:

import appdaemon.plugins.hass.hassapi as hass
import datetime as dt

class MotionLightSensor(hass.Hass):
    """Manage a switch using motion and light

    Features:
       - Turns on switch when motion is detected and light level is not above
         configured threshold.
       - Turns off switch after configured delay period
       - If the motion sensor is still active after delay period, keep light on
         and start a new delay period.

    Arguments:
        light_level:  Above this level, the switch is always off.
        delay:  Seconds to keep switch on after binary_sensor has triggered.
        switch:  Light to control (maybe should be renamed).
        binary_sensor: Motion sensor.
        light_sensor: Sensor used to meassure luminance.
        delay_switch_off device tracker that if "home" should stop lights going off
        variable_light config for specifying between which hours the light should be which brightness and color

    Example configuration
    In appdaemon.yaml:
    motion_light_leds_lounge:
      module: motion_lights_light
      class: MotionLightSensor
      light_level: '150'
      delay: '900'
      switch: light.addressableledstrip
      binary_sensor: binary_sensor.motion_sensor_158d00013fb2d7
      light_sensor: sensor.illumination_f0b429b3e568
      delay_switch_off: device_tracker.samsungtv
      variable_light:
        range1:
          brightness: 255
          start_hour: 5
          end_hour: 20
          light_color: [255,63,0]
        range2:
          brightness: 125
          start_hour: 20
          end_hour: 22
          light_color: [255,63,0]
        range3:
          brightness: 25
          start_hour: 22
          end_hour: 5
          light_color: [255,63,0]


    """

    def initialize(self):
        self.log("Initializing MotionLightSensor")
        self.handle = None
        # Access all arguments in order to trigger error due to missing
        # arguments during initialization
        self.arg_switch = self.args["switch"]
        self.arg_light_sensor = self.args["light_sensor"]
        self.arg_binary_sensor = self.args["binary_sensor"]
        self.light_threshold = float(self.args["light_level"])
        
        #Check if there is a device tracker that should stop lights going off
        #In my case its a TV, when watching TV I dont want the lights going off
        if "delay_switch_off" in self.args:        
            self.delay_switch_off =  self.args["delay_switch_off"]
        else:
            self.delay_switch_off = "none"

        self.log("Light threshold is {}".format(self.light_threshold))

        if "variable_light" in self.args:
            self.log("You have set variable light levels {}".format(
                self.args["variable_light"]))
            self.variable_light = self.args["variable_light"]
            for key in self.variable_light:
                self.log("Between {} and {} the light will be set to {}".format((self.variable_light.get(key).get("start_hour")),(self.variable_light.get(key).get("end_hour")),self.variable_light.get(key).get("brightness")))


        self.arg_delay = self.args["delay"]
        # Subscribe to all "on" events from the binary sensor
        self.listen_state(self.motion_event, self.arg_binary_sensor, new="on")
        # Report light sensors current value
        light_level = self.light_level()
        self.log("Light level {}".format(light_level))
    

    def light_brightness(self):
        self.log("Checking what level to set the light to")
        listofhours = []
        hour_now = (dt.datetime.now().hour)
        self.log("Current hour is {}".format(hour_now))
        start_time = "none"
        end_time = "none"
        light_level = "none"
        light_level_output = "none"
        light_color = "none"
        light_color_output = "none"
        
        #keys here are the ranges
        for key in self.variable_light:
            start_time = self.variable_light.get(key).get("start_hour")
            end_time = self.variable_light.get(key).get("end_hour")
            light_level = self.variable_light.get(key).get("brightness")
            
            self.log("Checking if light_color in variables")
            if self.variable_light.get(key).get("light_color"):
                light_color = self.variable_light.get(key).get("light_color")
                self.log("Color light configured")
            else:
                light_color = "none"
                self.log("Not a color light")
                self.log("Values in {} are : {}".format(key,self.variable_light.get(key).values()))     
                
            if start_time > end_time:
                self.log("Start hour: {} is greater than end hour: {}".format(start_time,end_time))                
                listofhours = (list(range(start_time, 24, 1))) + (list(range(1, end_time, 1)))
                self.log("Range of hours is {}".format(listofhours))   
            else:
                listofhours = (list(range(start_time, end_time +1, 1)))
            if hour_now in listofhours:
                self.log("light level {} for between {} and {}".format(light_level,start_time,end_time)) 
                light_level_output = light_level
                if light_color != "none":
                    self.log("color {} to be used for this time range".format(light_color))           
                    light_color_output = light_color
                
        
        if light_level_output != "none":
            self.log("light level {} and color {}".format(light_level_output,light_color_output)) 
            return light_level_output,light_color_output
        else:
            self.log("returning default light level {} color {}".format("200",light_color_output)) 
            return "200",light_color_output
               

    def light_level(self):
        light_level = float(self.get_state(self.arg_light_sensor))
        return light_level

    def motion_event(self, entity, attribute, old, new, kwargs):
        self.log("Motion detected light level is {}".format(self.light_level()))
        
        #self.get_state(self.delay_off_tracker)
        if self.light_level() <= self.light_threshold:
            self.log("It is dark enough")
            self.turn_switch_on()

    def start_delay(self):
        self.log("Starting delay timer")
        self.cancel_timer(self.handle)
        self.handle = self.run_in(self.delay_done, self.arg_delay)

    def delay_done(self, kwargs):
        self.log("Delay passed")
        if (
            self.get_state(self.arg_binary_sensor) == "on" and
            self.light_level() <= self.light_threshold
        ):
            self.log("Motion sensor still active and it is dark enough,"
                     " starting new delay")
            self.start_delay()
        else:
            self.log("Sensor inactive or not dark enough")
            self.turn_switch_off()

    def turn_switch_on(self):
       
        self.log("Turning on {}".format(self.arg_switch))
        self.brightness_color = self.light_brightness()
        self.log("Brightness level is {}".format(self.brightness_color[0]))
        if self.brightness_color[1] == "none":
            self.log("No color found turning on without color")
            self.turn_on(self.arg_switch, brightness=self.brightness_color[0])
        else:
            self.log("Found color trying turning on with color")
            self.turn_on(self.arg_switch, brightness=self.brightness_color[0], rgb_color=self.brightness_color[1])  
        self.start_delay()

    def turn_switch_off(self, kwargs=None):
        self.log("Turning off {}".format(self.arg_switch))
        if (self.delay_switch_off == "none") or (self.get_state(self.delay_switch_off) == "not_home"): 
            self.turn_off(self.arg_switch)
        else:
            #self.log("{} is {} so not switching off light".format(self.delay_switch_off,(self.get_state(delay_switch_off))))
            self.log("Not switching off light and tv on")
            #self.log("Light level changed from {} to {}".format(old, new))
            self.start_delay()
1 Like