App #2: Smart Light

Hi guys!

Here is another automation using AppDaemon :grinning:

This automation is not final or complete, and probably there is a way to make more generic. My aim is to share examples that maybe can help new members starting with HA+AppDaemon.

Suggestions or recommendations to improve the implementation are welcome!

App #2: Smart Light
Turn on the light of the bathroom if a movement is detected and the ambient light is less than 40 lux.
Turn off light of the bathroom after two minutes without detecting any movement.

Entities

sensor.bathroom_ambient_light
sensor.bathroom_motion
light.bathroom_lamp

app.yaml

bathroom_light:
  module: bathroom_smart_light
  class: BathroomSmartLight  
  sensors:
    motion: binary_sensor.bathroom_motion
    light: sensor.bathroom_ambient_light
  lights:
    bathroom: light.bathroom_lamp

bathroom_smart_light.py

import appdaemon.plugins.hass.hassapi as hass

class BathroomSmartLight(hass.Hass):

    def initialize(self):
        self.log('initializing ...')
        self.timer = None
        self.listen_state(self.on_motion_detected, self.args["sensors"]["motion"], new="on") 

    def on_motion_detected(self, entity, attribute, old, new, kwargs):
        light_entity = self.args["lights"]["bathroom"]
        ambient_light = int(self.get_state(self.args["sensors"]["light"]))
        bathroom_light = self.get_state(light_entity)
        if ambient_light <= 35 and bathroom_light == "off":
            self.log('turning on the bathroom light')
            self.turn_on(light_entity)
            self.__set_timer()
        elif bathroom_light == "on":
            self.log('Light already ON ...') 
            self.__set_timer()   


    def __start_timer(self):
        self.timer = self.run_in(self.__turn_off_light, 10)

    def __set_timer(self): 
        if self.timer == None:
            self.log('starting timer ...')   
            self.__start_timer()
        else:
            self.log('rescheduling timer ...')  
            self.cancel_timer(self.timer)  
            self.__start_timer()  

    def __turn_off_light(self, kwargs):
        self.log('Timer expired, turning off the light ...')  
        self.turn_off(self.args["lights"]["bathroom"])                  

I hope this can be useful for someone :wink:

Happy coding!
Humberto

Edit #1 : Adopt a more pythonic naming convention and other suggestions received from @SupahNoob

Edit #2 Avoid hardcode entities names in the automation code. Thanks @swiftlyfalling @Burningstone

Previous post: App #1: Doorbell notification
Next post: App #3: Smart Radiator

3 Likes

Curious, does your motion sensor bathroom_motion register multiple ONs? ie, if motion happens consistently over a period of 10 seconds, will it keep sending ON, ON, ON over and over? I would probably approach this problem a bit differently instead of using ā€œprivateā€ methods and storing the ambient light (since you can grab the state of a given entity at will already!).

1 Like

This is a great start.

For me, when I write something like this, I know Iā€™ll use it again. And I also know, at some point, Home Assistant is going to break, or my sensor will break, or something will go wrong. So, I like to make the code as generic and as configurable as possible, and I like to validate everything to ensure everything is what I expect it to be. So, if I were writing this, Iā€™d change it a bit:

import appdaemon.plugins.hass.hassapi as hass

# app.yaml
# bathroom_motion_light:
#   module: smart_light
#   class: SmartLight
#   light_entity: light.bathroom
#   lux_entity: sensor.bathroom_ambient_light
#   motion_entity: sensor.bathroom_motion
#   lux_limit: 40
#   light_timeout: 5

class SmartLight(hass.Hass):

    def initialize(self):
        self.log('initializing')

        try:
            self._light_entity = self.args.get('light_entity', None)
            self._motion_entity = self.args.get('motion_entity', None)
            self._lux_entity = self.args.get('lux_entity', None)
            self._lux_limit = int(self.args.get('lux_limit', 40))
            self._light_timeout = int(self.args.get('light_timeout', 5))
        except (TypeError, ValueError):
            self.log("Invalid Configuration", level="ERROR")
            return

        if None in [
                self._light_entity,
                self._motion_entity,
                self._lux_entity]:

            self.log('Incomplete Configuration', level="ERROR")
            return
            
        self._timer = None
        self.listen_state(self.motion_detected, self._motion_entity, new="ON") 

    def motion_detected(self, entity, attribute, old, new, kwargs):
        try:
            ambient_light = int(self.get_state(self._lux_entity))
        except (ValueError, TypeError):
            self.log("Could not get Ambient Light Level", level="WARNING")
            return

        light_state = int(self.get_state(self._light_entity))
        if light_state not in ['OFF', 'ON']:
            self.log("Invalid Light State: {}".format(light_state),
                level="WARNING"))
            return
    
        if ambient_light <= self._lux_limit and light_state == "OFF":
            self.log('Turning ON Light')
            self.turn_on(self._light_entity)
            self.start_timer()
        elif light_state == "ON":
            self.log('Light already ON') 
            self.start_timer()   

    def start_timer(self):
        self.cancel_timer(self._timer)
        self._timer = self.run_in(self.turn_off_light, self._light_timeout)

    def turn_off_light(self, kwargs):
        self.log('Timer expired, turning OFF the light')
        self._timer = None
        self.turn_off(self._light_entity)

Untested code, so there may be typos.

Once it was nice and generic, and I started using it, Iā€™d probably quickly realize a few thingsā€¦

  1. I may want to turn on/off more than one light. So Iā€™d change light_entity to also accept a list.
  2. Perhaps some rooms have more than one motion sensor?
  3. If your lights have configurable levels, perhaps youā€™d like to add a constraint that checks if youā€™re sleeping (based on time, or some switch you flip or whatever). Add a ā€œbrightness_levelā€ option and have two versions of this app running: one for when youā€™re sleeping (with a brightness of 16, perhaps, and a constraint on your sleep mode being ā€œonā€) and one for when youā€™re awake (with a brightness of, 255, perhaps and a constraint on your sleep mode being ā€œoffā€).

Thatā€™s the beautiful thing about AppDaemon. You can write what you wrote and it works and youā€™re good to go and thereā€™s nothing wrong with that. And you can just write code (even if some pieces look the same as others) for every light you might want to control like this. And everything is great. Or you can make it generic and reuse it. It works either way. And, while one is a bit more friendly to yourself and others if you have a lot of lights, neither is ā€œbetterā€ because they both get the job done.

4 Likes

Hello @SupahNoob

The motion sensor after detects movement it goes to sleep (e.g., 1 min), if when it wakes up it detects movement again you can receive multiples ON, ON. So, whenever the motion sensor emits ON and the timer is running I want to reschedule the timer.

Thanks for your recommendation :+1:

Hi @swiftlyfalling

Indeed, you are right this kind of automation should be more generic since you can apply the same behavior in multiple rooms. I forgot to put some note about that :slightly_frowning_face:.

My idea was to extend this automation in a future post to make it generic. But, many thanks for sharing your generic solution :+1:

I will try it later :+1: and I will let you know :slightly_smiling_face:

1 Like

Hey,
I used this code example as a base to add more functionality and also fixed a few things. To summarize all the features:

  • turn on/off light after motion is detected and timeout occurred
  • timeout can be specified (used AppDaemon internal timer) or a HA countdown entity can be used (easier to show remaining time in HA)
  • entity to measure illuminance and threshold can be specified to keep the lights off if there is already enough light in the room
  • support for multiple motion sensors
  • entity to disable smartlights can be passed
  • validation checks
  • re-start countdown at startup if lights are already on (can happen if a motion is detected, light turns on and then you restart HA before lights go off)

Here is the code:

import appdaemon.plugins.hass.hassapi as hass
import appdaemon.plugins.hass
import threading

"""
App to automatically turn lights on/off for a certain time depending on the input of one or multiple motions sensors.

Args:
    light_entity - the light/switch that shall be controlled
    motion_entities - a list of motion sensors
    lux_entity (optional) - the sensor that returns the current illuminance 
    lux_limit (optional) - the light will not be turned on if the lux_entity returns a value smaller than the lux_limit
    light_timeout (optional) - the time in seconds after which the lights shall be turned off. Each motion event restarts the timer
    countdown_entity (optional) - a timer entity that can be used for the timeout handling. If this entity is set, the light_timeout argument will be ignored, because the entity already has its own timeout
    disable_motion_sensor_entity - a switch entity that can enable/disable the automatic light on/off handling
"""

class SmartLight(hass.Hass):

    def initialize(self):
        self.__single_threading_lock = threading.Lock()

        try:
            self._light_entity = self.args.get('light_entity', None)
            self._motion_entities = self.args.get('motion_entities', None)
            self._lux_entity = self.args.get('lux_entity', None)
            self._lux_limit = int(self.args.get('lux_limit', 0))
            self._light_timeout = int(self.args.get('light_timeout', 0))
            self._countdown_entity = self.args.get('countdown_entity', None)
            self._disable_motion_sensor_entity = self.args.get('disable_motion_sensor_entity', None)
        except (TypeError, ValueError):
            self.log("Invalid Configuration", level="ERROR")
            return

        if type(self._motion_entities) is not list:
            self.log('motion_entities must be a list of entities', level="ERROR")

        if self._light_entity is None:
            self.log('light entity missing.', level="ERROR")
            return

        if self._lux_limit > 0 and self._lux_entity is None:
            self.log('lux_limit given but no lux_entity specified', level="ERROR")
            return

        if self._countdown_entity is not None and self._light_timeout > 0:
            self.log('light_timeout will be ignored, because a countdown_entity is given. The timeout of the entity '+self._countdown_entity+ " will be used instead.", level="WARNING")

        if self._countdown_entity is None and self._light_timeout == 0:
            self.log('No timeout given. 5s will be used by default.', level="WARNING")
            self._light_timeout = 5

        self._timer = None
        if self._countdown_entity is not None:
            self.listen_event(self.turn_off_light, "timer.finished", entity_id = self._countdown_entity)
        if self.get_state(self._light_entity) == "on":
            self.log('light ' + self._light_entity + " is already on.")
            self.restart_timer()
        for motion_entity in self._motion_entities:
            self.listen_state(self.motion_detected_callback, motion_entity, new="on")
        if self._disable_motion_sensor_entity is not None:
            self.listen_state(self.motion_sensor_disabled_callback, self._disable_motion_sensor_entity)
        self.log('initialized')

    def is_motion_sensor_disabled(self):
        if self._disable_motion_sensor_entity is not None:
            return self.get_state(self._disable_motion_sensor_entity) == "on"
        return False 

    def motion_sensor_disabled_callback(self, entity, attribute, old, new, kwargs):
        self.log("Smartlights disabled: {}".format(self.is_motion_sensor_disabled()))
        if self.is_motion_sensor_disabled():
            self.stop_timer()
        else:
            self.restart_timer()

    def motion_detected_callback(self, entity, attribute, old, new, kwargs):
        self.motion_detected()

    def motion_detected(self):
        with self.__single_threading_lock: # prevents code from parallel execution e.g. due to multiple consecutive events
            self.log("Motion detected")
            if self.is_motion_sensor_disabled():
                self.log("... but motion sensor disabled")
                return
            if self._lux_limit > 0:
                try:
                    ambient_light = int(self.get_state(self._lux_entity))
                    self.log("Ambient light received: {} (threshold: {})".format(ambient_light, self._lux_limit))
                except (ValueError, TypeError):
                    self.log("Could not get Ambient Light Level", level="WARNING")
                    return

            light_state = self.get_state(self._light_entity)
            if light_state not in ['off', 'on']:
                self.log("Invalid Light State: {}. Expecting either on or off.".format(light_state), level="WARNING")
                return
        
            if (self._lux_limit == 0 or ambient_light <= self._lux_limit) and light_state == "off":
                self.turn_on_light()
                self.restart_timer()
            elif light_state == "on":
                self.log('Light already ON') 
                self.restart_timer()

    def stop_timer(self):
        if self._countdown_entity is not None:
            self.call_service("timer/cancel", entity_id = self._countdown_entity)
        else:
            self.cancel_timer(self._timer)

    def start_timer(self):
        if self._countdown_entity is not None:
            self.call_service("timer/start", entity_id  = self._countdown_entity)
        else:
            self._timer = self.run_in(self.turn_off_light, self._light_timeout)

    def restart_timer(self):
        if self.is_motion_sensor_disabled():
            self.log('Timer not restarted because smartlight disabled')
            return
        self.log('Restart timer')
        self.stop_timer()
        self.start_timer()

    def turn_on_light(self):
        self.log('Turning light on')
        self.turn_on(self._light_entity)

    def turn_off_light(self, event_name = None, data = None, kwargs = None):
        self.log('Timer expired')
        self._timer = None
        self.turn_off(self._light_entity)

Input configuration can be as simple as:

KitchenMotionLight:
  module: smartlight
  class: SmartLight
  light_entity: light.kitchen_lightstripe
  motion_entities:
    - binary_sensor.kitchen_movement_occupancy
  light_timeout: 300 #uses AppDaemon internal timer

or a bit more complex:

DiningRoomMotionLight:
  module: smartlight
  class: SmartLight
  light_entity: light.dining_room
  motion_entities: 
    - binary_sensor.dining_room_movement1_occupancy
    - binary_sensor.dining_room_movement2_occupancy
  lux_limit: 17000
  countdown_entity: timer.turn_off_dining_room_lights #uses HA countdown entity
  disable_motion_sensor_entity: input_boolean.disable_dining_room_motion
3 Likes

Thanks for this excellent sample. Iā€™ve added some more possibilities to this:
Short overview of the current possibilites:

  • turn on light on motion
  • have several disable switches for each thread
  • turn of all entities of last activated scene if timeout ocured
  • several motion sensors per thread
    have a look on the settings in the yaml and youā€™ll see whatā€™s possible - but itā€™s still in development
import appdaemon.plugins.hass.hassapi as hass
import appdaemon.plugins.hass
import threading

"""
https://community.home-assistant.io/t/app-2-smart-light/129011/6

App to automatically turn lights on/off for a certain time depending on the input of one or multiple motions sensors.

Args:
    light_entity - the light/switch that shall be controlled
    motion_entities - a list of motion sensors
    lux_entity (optional) - the sensor that returns the current illuminance 
    lux_limit (optional) - the light will not be turned on if the lux_entity returns a value smaller than the lux_limit
    light_timeout (optional) - the time in seconds after which the lights shall be turned off. Each motion event restarts the timer
    transition (optional) - the time in seconds to reach final brightness
    restart_timer (optional) - yes      - restart in any case!! 
                             - motion   - restart only if motion activated light!! - this is the default
                             - no       - no !!
    brightness_entity (optional) - entity for brightness setting
    brightness (optional) - brighntess
    countdown_entity (optional) - a timer entity that can be used for the timeout handling. If this entity is set, the light_timeout argument will be ignored, because the entity already has its own timeout
    disable_motion_sensor_entities (optional) - switch entities that can enable/disable the automatic light on/off handling
    somebody_is_home_entity (optional) - only switch lights when somebody is home    

    Scene Stuff -
    scene_on_activate_clear_timer  -  if on clears_timer of lights
    scene_timeout -  if scene is activated or called this is the scene timeout only in combination with scene_on_activate_clear_timer
    scene_entity  -  if on clears_timer of lights
    scene_listen_entities  -  listens for this scenes if activated

Changes from stoellchen:
    20201225:  
    	lux_limit changed from int to float
    	motion_entity changed to motion_entities and checking lights to switch on or off
    	disable_motion_sensor_entity switched to list  room-disable and global disable

# Example:
#  WohnzimmerMotionLight:
#    module: smartlight
#    class: SmartLight
#    light_entities: 
#       - light.wand
#       - light.sideboard_rechts
#       - light.sideboard_links
#    somebody_is_home_entity: group.family
#    motion_entities: 
#      - binary_sensor.ms1
#    lux_entity: sensor.ms1
#    lux_limit: 30
#    brightness_entity: sensor.template_motion_sensor_light_brightness
#    light_timeout: 300
#    disable_motion_sensor_entities: 
#      - input_boolean.disable_wohnzimmer_motion

"""

class SmartLight(hass.Hass):

    def initialize(self):
        self.__single_threading_lock = threading.Lock()

        try:
            self._light_entities = self.args.get('light_entities', None)
            self._motion_entities = self.args.get('motion_entities', None)
            self._lux_entity = self.args.get('lux_entity', None)
            self._lux_limit = int(self.args.get('lux_limit', 0))
            self._brightness_entity = self.args.get('brightness_entity', None)
            self._brightness = int(self.args.get('brightness', 100))
            self._light_timeout = int(self.args.get('light_timeout', 0))
            self._transition = int(self.args.get('transition',0))
            self._countdown_entity = self.args.get('countdown_entity', None)
            self._restart_timer = self.args.get('restart_timer', 'motion')
            self._restart_timer_state = "-"
            self._disable_motion_sensor_entities = self.args.get('disable_motion_sensor_entities', None)
            self._somebody_is_home = self.args.get('somebody_is_home_entity', None)

            self._scene_on_activate_clear_timer = self.args.get('scene_on_activate_clear_timer', None)
            self._scene_entity = self.args.get('scene_entity', None)
            self._scene_timeout = int(self.args.get('scene_timeout', 18000))
            self._scene_listen_entities = self.args.get('scene_listen_entities', None)

        except (TypeError, ValueError):
            self.log("Invalid Configuration", level="ERROR")
            return

        if type(self._motion_entities) is not list:
            self.log('motion_entities must be a list of entities', level="ERROR")
        
        if type(self._light_entities) is not list:
            self.log('light entities must be a list of entities.', level="ERROR")
            return

        if self._lux_limit > 0 and self._lux_entity is None:
            self.log('lux_limit given but no lux_entity specified', level="ERROR")
            return
		
        if self._transition <= 0:
            self.log('_transiton <=0 0 will be used', level="INFO")
            self._transition = 0
        elif self._transition >= 120:
            self.log('transition >120 120 will be used', level="INFO")
            self._transition = 120
        else:
            self.log('transition {} will be used'.format( self._transition ), level="INFO")

        if self._brightness is None:
            self.log('brightness not given 100 will be used', level="INFO")
            self._brightness = 100
        else:
            if self._brightness <=0:
                self.log('brightness <=0 20 will be used', level="INFO")
                self._brightness = 20
            elif self._brightness >100:
                self.log('brightness >100 100 will be used', level="INFO")
                self._brightness = 100
            else:
                self.log('brightness {} will be used'.format( self._brightness ), level="INFO")
#                self._brightness = 100
        if self._brightness_entity is None:
            self.log("brighntes_sensor is None - brightness {} will be used".format( self._brightness ), level="INFO")
        else:
            self.log('brightness from {} will be used current {}' .format(self._brightness_entity,self.get_state( self._brightness_entity )) , level="INFO")


        if self._countdown_entity is not None and self._light_timeout > 0:
            self.log('light_timeout will be ignored, because a countdown_entity is given. The timeout of the entity '+self._countdown_entity+ " will be used instead.", level="WARNING")

        if self._somebody_is_home is None:
            self.log("Somebody_is_home doesn't matter", level="WARNING")
            return

        if self._countdown_entity is None and self._light_timeout == 0:
            self.log('No timeout given. 5s will be used by default.', level="WARNING")
            self._light_timeout = 5

        if self._restart_timer == 'motion':
            self.log('restart timer when motion activated')
        elif self._restart_timer == 'no':
            self.log('Do NOT restart timer even when motion is detected')
        elif self._restart_timer == 'yes':
            self.log('Restart timer even when motion is detected')
        else:
            self._restart_timer = 'motion'
            self.log('Restart timer default: motion activeded' ,level="WARNING")

        if self._disable_motion_sensor_entities is None:
            self.log('disable motion sensor(s) not defined .', level="WARNING")
        else:
            if type(self._disable_motion_sensor_entities) is not list:
                self._disable_motion_sensor_entities = None
                self.log('disable motion sensor(s) must be a list of entities .', level="WARNING")

        if self._scene_on_activate_clear_timer is None:
           self._scene_on_activate_clear_timer ="off"
        elif self._scene_on_activate_clear_timer != "clear":
           self._scene_on_activate_clear_timer ="off"
        self.log('Scene - cancel timer:{}'. format(self._scene_on_activate_clear_timer) , level="INFO")            
                    
        if self._scene_entity is not None:
            try:
                if self.get_state(self._scene_entity) == "scening":
                    self.log('Scene {} instead of lights.'.format(self._scene_entity), level="INFO")
            except (ValueError, TypeError):
                self.log('Scene {} does not exist.'.format(self._scene_entity), level="WARNING")
                self._scene_entity = None   
        
        if self._scene_timeout is None:
            self.log('scene timout not given 18000 will be used', level="INFO")
            self._scene_timeout = 18000
        else:
            if self._scene_timeout <=0:
                self.log('Scene timeout <=20 20 will be used', level="INFO")
                self._scene_timeout = 20
            elif self._scene_timeout >43200:
                self.log('Scene timeout >43200 43200 will be used', level="INFO")
                self._scene_timeout = 43200
            else:
                self.log('Scene timeout {} will be used'.format( self._scene_timeout ), level="INFO")
        self._last_scene_activated='-'
        if type(self._scene_listen_entities) is list:
            self.log('Scene to listen {} will be used'.format( self._scene_listen_entities ), level="INFO")

        self._timer = None
        if self._countdown_entity is not None:
            self.listen_event(self.turn_off_light, "timer.finished", entity_id = self._countdown_entity)
        
        for light_entity in self._light_entities:
            if self.get_state(light_entity) == "on":
                self.log('light ' + light_entity + " is already on.")
                self.restart_timer('startup')

        if type(self._scene_listen_entities) is list:
            tempStatus='-'
            for lscene_entity in self._scene_listen_entities:
                if self.get_state( lscene_entity ) == "scening":
                    tempStatus=lscene_entity
            if tempStatus!='-':
                # filter on entity does not work - we get all scenes - looking later for the correct scene
                self.listen_event(self.scene_detected_callback, event='call_service', domain='scene', service='turn_on', entity_id = tempStatus  ,new="also")
                             
        for motion_entity in self._motion_entities:
            self.listen_state(self.motion_detected_callback, motion_entity, new="on")
            
        if self._disable_motion_sensor_entities is not None:
            for disable_entity in self._disable_motion_sensor_entities:
                self.listen_state(self.motion_sensor_disabled_callback, disable_entity )

        try:
            ambient_light = float(self.get_state(self._lux_entity) )
            self.log("INITIALIZED: Ambient: {} ({}) for {}".format(ambient_light, self._lux_limit , self._lux_entity ))
        except (ValueError, TypeError):
            self.log("INITIALIZED: Could not get Ambient Light Level for " + self._lux_entity)

    def is_motion_sensor_disabled(self):
        if self._disable_motion_sensor_entities is not None:
            for disable_entity in self._disable_motion_sensor_entities:
                return self.get_state( disable_entity ) == "on"
        return False 

    def motion_sensor_disabled_callback(self, entity, attribute, old, new, kwargs):
        self.log("Smartlights disabled: {}".format(self.is_motion_sensor_disabled()))
        if self.is_motion_sensor_disabled():
            self.stop_timer('Smartlight now disabled')
        else:
            self.restart_timer('Smartlight now enabled')

    def get_my_brightness(self):
        if self._brightness_entity is None:
            return self._brightness
        else:
            return self.get_state( self._brightness_entity )

    # def scene_detected_callback(self,entity_id,attribute,old,aa):
    def scene_detected_callback(self,entity,attribute,kwargs):
        self.scene_detected(self,attribute,kwargs)
        
    def motion_detected_callback(self, entity, attribute, old, new, kwargs):
        self.motion_detected()

    def scene_detected(self ,entity,attribute,kwargs):
        with self.__single_threading_lock: # prevents code from parallel execution e.g. due to multiple consecutive events
            self.log("scene detected  {}" . format( attribute['service_data']['entity_id'] ))
            if attribute['service_data']['entity_id'] in self._scene_listen_entities:
                self.stop_timer('motion scene')
                self.log("timers stopped because of scene:{}" . format( attribute['service_data']['entity_id'] ))
                self._last_scene_activated=attribute['service_data']['entity_id']
                self.start_timer('motion scene')
            else:
                self.log("Do nothing we are not listening on this scene")
                self._last_scene_activated=''
                
    def motion_detected(self):
        with self.__single_threading_lock: # prevents code from parallel execution e.g. due to multiple consecutive events
            self.log("Motion detected " )
            if self.is_motion_sensor_disabled():
                self.log("... but motion sensor disabled")
                return

            if self._somebody_is_home is not None:
                if self.get_state(self._somebody_is_home) != "home":
                    self.log("... but nobody is at home {}" .format( self.get_state(self._somebody_is_home) ) )
                    return
            
            if self._lux_limit > 0:
                try:
                    ambient_light = float(self.get_state(self._lux_entity) )
                    if ambient_light >= self._lux_limit:
                        self.log("Ambient light:{} (lmt:{}) - do nothing - stop".format(ambient_light, self._lux_limit))
                        return
                    else:
                        self.log("Ambient light:{} (lmt:{}) - go on".format(ambient_light, self._lux_limit))        
                except (ValueError, TypeError):
                    self.log("Could not get Ambient Light Level for " + self._lux_entity , level="WARNING")
                    return

            # we start a scene and not lights!!!!
            if self._scene_entity is not None:
                if self.get_state(self._scene_entity) == "scening":
                    # self.turn_on( self._scene_entity )
                    # self.call_service( 'scene/turn_on', entity_id = self._scene_entity )
                    self.turn_on( self._scene_entity )
                    self.log("Scene {} turned on.".format( self._scene_entity ), level="INFO")
                    self._restart_timer_state ="motion scene"
                    self.restart_timer( self._restart_timer_state )
                 
            else:
                tempStatus ='no'
                for light_entity in self._light_entities:
                    light_state = self.get_state( light_entity )
                    if light_state not in ['off', 'on']:
                        self.log("Invalid light state for {}: {}. Expecting either on or off.".format(light_entity,light_state), level="WARNING")
                        return
        
                    if (self._lux_limit == 0 or ambient_light <= self._lux_limit) and light_state == "off" and self._restart_timer_state !="motion scene":
                        self.turn_on_light( light_entity )
                        if tempStatus == 'no':
                            tempStatus ='motion light'
                        self._restart_timer_state ="motion light"
                    elif light_state == "on":
                        self.log('Light already ON ({}) - {}'.format(light_entity,self._restart_timer_state) )
                        if self._restart_timer_state == "motion light" or self._restart_timer == "yes":
                            self.log('Light already ON - restart state:{} - type:{}'.format( self._restart_timer_state , self._restart_timer ))
                            if tempStatus == 'no' or tempStatus !='motion light':
                                tempStatus ='just restart'
                if tempStatus !='no':
                    self.restart_timer(self._restart_timer_state)                    

    def stop_timer(self,param):
        if self._countdown_entity is not None:
            self.call_service("timer/cancel", entity_id = self._countdown_entity)
        else:
            self.cancel_timer(self._timer)
        self._restart_timer_state ="-"
        
    def start_timer(self,param):
        if self._countdown_entity is not None:
            self.call_service("timer/start", entity_id  = self._countdown_entity)
            self.log("start_timer {}".format(entity_id) )
        else:
            if param == "motion scene":
                self._timer = self.run_in(self.turn_off_light, self._scene_timeout)
                self.log("start_timer scene timeout:{} - {}".format( self._scene_timeout ,self._restart_timer_state) )
                self._restart_timer_state=param
            else:
                self._timer = self.run_in(self.turn_off_light, self._light_timeout)
                self.log("start_timer light timeout:{} - {}".format( self._light_timeout,self._restart_timer_state ) )
                self._restart_timer_state=param

    def restart_timer(self , param ):
        if self.is_motion_sensor_disabled():
            self.log('Timer not restarted because smartlight disabled')
            return
        self.log('Restart timer {}'.format(param) )
        self.stop_timer(param)
        self.start_timer(param)

    def turn_on_light(self,light_entity):
        light_entity_domain = light_entity.split(".")
        if light_entity_domain[0] == "light":
            self.turn_on( light_entity , brightness_pct = self.get_my_brightness()  )
            # self.turn_on( light_entity , brightness_pct = self.get_my_brightness() , transition = self._transition )
            # self.call_service( 'light/turn_on', entity_id = light_entity , brightness_pct = self.get_my_brightness() , transition = self._transition)
            self.log('Turning {} on - {} - brightness:{}% transition:{}' . format(light_entity_domain[0],light_entity,self.get_my_brightness() ,self._transition ))
        else:
            self.turn_on(light_entity)
            self.log('Turning {} on - {}' . format(light_entity_domain[0],light_entity))
    

    def turn_off_light(self, event_name = None, data = None, kwargs = None):
        self.log('Timer expired')
        self._timer = None
        self._timer_restart_state = "-"
        for light_entity in self._light_entities:
            self.log('Turning light off -' + light_entity)
            if light_entity.split(".")[0] =="light":
                self.turn_off(light_entity,transition = self._transition)
            else:
                self.turn_off(light_entity )
        if self._last_scene_activated !='' and self._restart_timer_state=='motion scene':
            try:
                entities_of_scene = self.get_state( self._last_scene_activated ,attribute = "all" )
                self.log('shutting down all entities from scene {} timed out!!'.format(self._last_scene_activated))
                for entity_id in entities_of_scene['attributes']['entity_id']:
                    if entity_id.split(".")[0] =="light":
                        self.log('turn off LIGHT: {}'.format(entity_id))
                        self.turn_off( entity_id )
                    elif entity_id.split(".")[0] =="switch":
                        self.log('turn off SWITCH: {}'.format(entity_id))
                        self.turn_off( entity_id )
            except (ValueError, TypeError):
                self.log('ERROR in scene shutdown !!' ,level="WARNING")
            
        

and my app.yaml for this looks:


Wohnzimmer:
  module: smartlight
  class: SmartLight
  somebody_is_home_entity: group.family
  light_entities: 
     - light.wand
     - light.sideboard_rechts
     - light.sideboard_links
  scene_on_activate_clear_timer: clear
  scene_entity_NOTWORKING: scene.sideboard_bild_turkis
  scene_timeout: 3600
  scene_listen_entities:
    - scene.hell
    - scene.tv_blau
    - scene.tromso_mit_licht_unten_blau_rot_dunkel
    - scene.tromso_mit_licht_unten
    - scene.sideboard_bild_turkis
    - scene.tracey_lights
  motion_entities: 
    - binary_sensor.ms1
    - binary_sensor.ms3
  lux_entity: sensor.ms1
  lux_limit: 10
  brightness_entity: sensor.template_motion_sensor_light_brightness
  transition: 10
  light_timeout: 300
  restart_timer: motion
  disable_motion_sensor_entities: 
    - input_boolean.disable_wohnzimmer_motion

Esszimmer:
  module: smartlight
  class: SmartLight
  somebody_is_home_entity: group.family
  light_entities: 
    - light.esszimmer_1
    - light.kerze
  motion_entities: 
    - binary_sensor.ms2
  lux_entity: sensor.ms2
  lux_limit: 40
  light_timeout: 300
  restart_timer: motion
  brightness_entity: sensor.template_motion_sensor_light_brightness
  transition: 11
  disable_motion_sensor_entities: 
    - input_boolean.disable_esszimmer_motion

SchlafzimmerLinks:
  module: smartlight
  class: SmartLight
  somebody_is_home_entity: group.family
  light_entities: 
    - light.bett_boden_links
    - light.bett_boden
  motion_entities: 
    - binary_sensor.ms8
  lux_entity: sensor.ms8
  lux_limit: 5
  brightness_entity: sensor.template_motion_sensor_light_brightness
  light_timeout: 120
  transition: 3
  restart_timer: motion
  disable_motion_sensor_entities: 
    - input_boolean.disable_schlafzimmer_motion

SchlafzimmerRechts:
  module: smartlight
  class: SmartLight
  somebody_is_home_entity: group.family
  light_entities: 
    - light.bett_boden
    - light.bett_boden_links
  motion_entities: 
    - binary_sensor.ms5
  lux_entity: sensor.ms5
  lux_limit: 10
  brightness_entity: sensor.template_motion_sensor_light_brightness
  light_timeout: 30
  transition: 3
  restart_timer: motion
  disable_motion_sensor_entities: 
    - input_boolean.disable_schlafzimmer_motion

Storage:
  module: smartlight
  class: SmartLight
  somebody_is_home_entity: group.family
  light_entities: 
    - switch.smart_plug_2
  motion_entities: 
    - binary_sensor.ms6
  lux_entity: sensor.ms6
  lux_limit: 60
  brightness: 100
  light_timeout: 120
  transition: 0
  restart_timer: motion
  disable_motion_sensor_entities: 
    - input_boolean.disable_storagezimmer_motion


and the brightness sensor I use:

sensor:

  - platform: template
    sensors:
      template_motion_sensor_light_brightness:
        friendly_name: Motion Sensor Brightness pct
        value_template: >-
          {% if states.input_select.time_of_day.state == 'Afternoon' %}
            100
          {% elif states.input_select.time_of_day.state == 'Evening' %}
            50
          {% elif states.input_select.time_of_day.state == 'Night' %}
            1
          {% elif states.input_select.time_of_day.state == 'Morning' %}
            {% if now().hour <= 05 %}
              5
            {% elif now().hour <= 06 %}
              10
            {% elif now().hour <= 07 %}
              30
            {% elif now().hour <= 08 %}
              60
            {% elif now().hour <= 09 %}
              70
            {% else %}
              100
            {% endif %} 
          {% else %}
            5
          {% endif %}

Iā€™m new to HA - Iā€™ll need to have a look in summer to get the values better for all seasons

and here the log-file to see it in action:

2020-12-26 10:28:41.429979 INFO SchlafzimmerLinks: restart timer when motion activated
2020-12-26 10:28:41.433818 INFO SchlafzimmerLinks: Scene - cancel timer:off
2020-12-26 10:28:41.437165 INFO SchlafzimmerLinks: Scene timeout 18000 will be used
2020-12-26 10:28:41.450299 INFO SchlafzimmerLinks: INITIALIZED: Ambient: 1.0 (5) for sensor.ms8
2020-12-26 10:28:41.460329 INFO SchlafzimmerRechts: transition 3 will be used
2020-12-26 10:28:41.464320 INFO SchlafzimmerRechts: brightness 100 will be used
2020-12-26 10:28:41.470326 INFO SchlafzimmerRechts: brightness from sensor.template_motion_sensor_light_brightness will be used current 100
2020-12-26 10:28:41.474082 INFO SchlafzimmerRechts: restart timer when motion activated
2020-12-26 10:28:41.477622 INFO SchlafzimmerRechts: Scene - cancel timer:off
2020-12-26 10:28:41.480557 INFO SchlafzimmerRechts: Scene timeout 18000 will be used
2020-12-26 10:28:41.492487 INFO SchlafzimmerRechts: INITIALIZED: Ambient: 28.0 (10) for sensor.ms5
2020-12-26 10:28:41.501328 INFO Storage: _transiton <=0 0 will be used
2020-12-26 10:28:41.505582 INFO Storage: brightness 100 will be used
2020-12-26 10:28:41.510087 INFO Storage: brighntes_sensor is None - brightness 100 will be used
2020-12-26 10:28:41.514357 INFO Storage: restart timer when motion activated
2020-12-26 10:28:41.518261 INFO Storage: Scene - cancel timer:off
2020-12-26 10:28:41.521899 INFO Storage: Scene timeout 18000 will be used
2020-12-26 10:28:41.536490 INFO Storage: INITIALIZED: Ambient: 98.0 (60) for sensor.ms6
2020-12-26 10:28:44.287312 INFO Wohnzimmer: scene detected  scene.tracey_lights
2020-12-26 10:28:44.302957 INFO Wohnzimmer: timers stopped because of scene:scene.tracey_lights
2020-12-26 10:28:44.313652 INFO Wohnzimmer: start_timer scene timeout:10 - -
2020-12-26 10:28:54.019732 INFO Wohnzimmer: Timer expired
2020-12-26 10:28:54.023000 INFO Wohnzimmer: Turning light off -light.wand
2020-12-26 10:28:54.168672 INFO Wohnzimmer: Turning light off -light.sideboard_rechts
2020-12-26 10:28:56.239794 INFO Wohnzimmer: Turning light off -light.sideboard_links
2020-12-26 10:28:57.126921 INFO Wohnzimmer: shutting down all entities from scene scene.tracey_lights timed out!!
2020-12-26 10:28:57.129983 INFO Wohnzimmer: turn off LIGHT: light.wand
2020-12-26 10:28:57.219168 INFO Wohnzimmer: turn off LIGHT: light.kerze
2020-12-26 10:28:57.323642 INFO Wohnzimmer: turn off LIGHT: light.bett_boden_links
2020-12-26 10:28:57.407021 INFO Wohnzimmer: turn off LIGHT: light.bett_kissen
2020-12-26 10:28:57.486676 INFO Wohnzimmer: turn off LIGHT: light.wohnzimmer_licht_3
2020-12-26 10:29:06.302668 INFO Wohnzimmer: scene detected  scene.tracey_lights
2020-12-26 10:29:06.313172 INFO Wohnzimmer: timers stopped because of scene:scene.tracey_lights
2020-12-26 10:29:06.330411 INFO Wohnzimmer: start_timer scene timeout:10 - -
2020-12-26 10:29:16.021533 INFO Wohnzimmer: Timer expired
2020-12-26 10:29:16.025217 INFO Wohnzimmer: Turning light off -light.wand
2020-12-26 10:29:16.187455 INFO Wohnzimmer: Turning light off -light.sideboard_rechts
2020-12-26 10:29:16.972153 INFO Wohnzimmer: Turning light off -light.sideboard_links
2020-12-26 10:29:17.771524 INFO Wohnzimmer: shutting down all entities from scene scene.tracey_lights timed out!!
2020-12-26 10:29:17.775501 INFO Wohnzimmer: turn off LIGHT: light.wand
2020-12-26 10:29:17.859150 INFO Wohnzimmer: turn off LIGHT: light.kerze
2020-12-26 10:29:17.958482 INFO Wohnzimmer: turn off LIGHT: light.bett_boden_links
2020-12-26 10:29:18.055152 INFO Wohnzimmer: turn off LIGHT: light.bett_kissen
2020-12-26 10:29:18.224490 INFO Wohnzimmer: turn off LIGHT: light.wohnzimmer_licht_3
2020-12-26 10:30:09.067309 INFO AppDaemon: Reading config
2020-12-26 10:30:09.155533 INFO AppDaemon: /config/appdaemon/apps/smartlight.yaml added or modified
2020-12-26 10:30:09.156587 INFO AppDaemon: App 'Wohnzimmer' changed
2020-12-26 10:30:09.157525 INFO AppDaemon: Found 7 total apps
2020-12-26 10:30:09.167732 INFO AppDaemon: Terminating Wohnzimmer
2020-12-26 10:30:09.171239 INFO AppDaemon: Initializing app Wohnzimmer using class SmartLight from module smartlight
2020-12-26 10:30:09.186741 INFO Wohnzimmer: transition 10 will be used
2020-12-26 10:30:09.190259 INFO Wohnzimmer: brightness 100 will be used
2020-12-26 10:30:09.195932 INFO Wohnzimmer: brightness from sensor.template_motion_sensor_light_brightness will be used current 100
2020-12-26 10:30:09.199411 INFO Wohnzimmer: restart timer when motion activated
2020-12-26 10:30:09.202517 INFO Wohnzimmer: Scene - cancel timer:clear
2020-12-26 10:30:09.205785 INFO Wohnzimmer: Scene timeout 3600 will be used
2020-12-26 10:30:09.208971 INFO Wohnzimmer: Scene to listen ['scene.hell', 'scene.tv_blau', 'scene.tromso_mit_licht_unten_blau_rot_dunkel', 'scene.tromso_mit_licht_unten', 'scene.sideboard_bild_turkis', 'scene.tracey_lights'] will be used
2020-12-26 10:30:09.228520 INFO Wohnzimmer: INITIALIZED: Ambient: 53.0 (10) for sensor.ms1
2020-12-26 10:31:31.963852 INFO Wohnzimmer: Motion detected 
2020-12-26 10:31:31.970296 INFO Wohnzimmer: Ambient light:92.0 (lmt:10) - do nothing - stop
2020-12-26 10:34:44.309356 INFO Wohnzimmer: Motion detected 
2020-12-26 10:34:44.315887 INFO Wohnzimmer: Ambient light:107.0 (lmt:10) - do nothing - stop
2020-12-26 10:34:44.753082 INFO Wohnzimmer: Motion detected 
2020-12-26 10:34:44.759512 INFO Wohnzimmer: Ambient light:107.0 (lmt:10) - do nothing - stop
2020-12-26 10:34:59.850919 INFO Esszimmer: Motion detected 
2020-12-26 10:34:59.858330 INFO Esszimmer: Ambient light:167.0 (lmt:40) - do nothing - stop
2020-12-26 10:36:48.279365 INFO SchlafzimmerLinks: Motion detected 
2020-12-26 10:36:48.286547 INFO SchlafzimmerLinks: Ambient light:1.0 (lmt:5) - go on
2020-12-26 10:36:48.475730 INFO SchlafzimmerLinks: Turning light on - light.bett_boden_links - brightness:100% transition:3
2020-12-26 10:36:48.568407 INFO SchlafzimmerLinks: Turning light on - light.bett_boden - brightness:100% transition:3
2020-12-26 10:36:48.573957 INFO SchlafzimmerLinks: Restart timer motion light
2020-12-26 10:36:48.583668 INFO SchlafzimmerLinks: start_timer light timeout:120 - -
2020-12-26 10:36:56.547986 INFO SchlafzimmerRechts: Motion detected 
2020-12-26 10:36:56.553965 INFO SchlafzimmerRechts: Ambient light:206.0 (lmt:10) - do nothing - stop
2020-12-26 10:38:22.022158 INFO Wohnzimmer: Motion detected 
2020-12-26 10:38:22.029354 INFO Wohnzimmer: Ambient light:139.0 (lmt:10) - do nothing - stop
2020-12-26 10:38:22.521705 INFO Wohnzimmer: Motion detected 
2020-12-26 10:38:22.528512 INFO Wohnzimmer: Ambient light:139.0 (lmt:10) - do nothing - stop
2020-12-26 10:38:24.448508 INFO Esszimmer: Motion detected 
2020-12-26 10:38:24.454796 INFO Esszimmer: Ambient light:205.0 (lmt:40) - do nothing - stop
2020-12-26 10:38:49.180576 INFO SchlafzimmerLinks: Timer expired
2020-12-26 10:38:49.296331 INFO SchlafzimmerLinks: Turning light off -light.bett_boden_links
2020-12-26 10:38:49.664582 INFO SchlafzimmerLinks: Turning light off -light.bett_boden
2020-12-26 10:42:01.506832 INFO Wohnzimmer: Motion detected 
2020-12-26 10:42:01.513152 INFO Wohnzimmer: Ambient light:136.0 (lmt:10) - do nothing - stop
2 Likes

@rhumbertgz, I think your example code is exactly what is needed as a minimal starting point.

Abstractions are useful but would initially just cause more question marks in a newbie developerā€™s head ā€“ they can come once one is comfortable with the initial steps.

  1. remove return in self._somebody_is_home check
  2. hmmm
        try:
               ambient_light = float(self.get_state(self._lux_entity) )
               self.log("INITIALIZED: Ambient: {} ({}) for {}".format(ambient_light, self._lux_limit , self._lux_entity ))
        except (ValueError, TypeError):
            self.log("INITIALIZED: Could not get Ambient Light Level for " + self._lux_entity)

do not spam log on the start

        try:
            if self._lux_entity is not None:
               ambient_light = float(self.get_state(self._lux_entity) )
               self.log("INITIALIZED: Ambient: {} ({}) for {}".format(ambient_light, self._lux_limit , self._lux_entity ))
        except (ValueError, TypeError):
            self.log("INITIALIZED: Could not get Ambient Light Level for " + self._lux_entity)