[solved] How to combine timer and time constraints for a motion light?

I’m wondering how to turn off light after a delay, at the same time as using the constrain_xxx_time?

Let me take the MotionLights sample and configure it like this:

Downstairs Night Light:
  module: motion_lights
  class: MotionLights
  constrain_start_time: '20:00:00'
  constrain_end_time: '04:29:59'
  sensor: binary_sensor.downstairs_sensor_26_0
  delay: '300'
  entity_off: scene.downstairs_off
  entity_on: scene.downstairs_dim

If we have motion at 21:00:00, the light will be turned on and the light_off callback will be scheduled to run 300 seconds later, everything is great.

The relevant motion function in the MotionLights class looks like this:

def motion(self, entity, attribute, old, new, kwargs):
  if new == "on":
    if "entity_on" in self.args:
      self.log("Motion detected: turning {} on".format(self.args["entity_on"]))
      self.turn_on(self.args["entity_on"])
    if "delay" in self.args:
      delay = self.args["delay"]
    else:
      delay = 60
    self.cancel_timer(self.handle)
    self.handle = self.run_in(self.light_off, delay)

But what about if we have motion 200 seconds before the constrain_end_time? If I understood the documentation correctly, the light will be turned on as before but the scheduled light_off callback will never be executed since it is after the constrain_end_time and the light will stay on…

I could scheduled a light_off one second before the constrain_end_time:

constrain_end = datetime.datetime.strptime(self.args["constrain_end_time"], '%H:%M:%S').time()
off_time = datetime.datetime.combine(datetime.date.today(), constrain_end)
# We need to run before constrain_end_time so let us decrement the time a bit
off_time -= datetime.timedelta(seconds=1)
try:
  self.run_at(self.pond_off, off_time)
except ValueError:
  # We get a ValueError if start time is in the past, so add a day
  off_time += datetime.timedelta(days=1)
  self.run_at(self.pond_off, off_time)

But, do I really need to add the above code to all my Apps that use both timer and constrain_xxx_time? And I guess I still have a fraction of a second window left where it could fail?
Any suggestions are welcome…

You could look at using callback level constraints instead - have a constraint for the turn on but don’t set one for the turn off - that’s mainly why I added callback level constraints.

1 Like

Thanks, callback level constraints seems like a better approach. I’ll play around with those.

1 Like

Sweet, callback level constraints seems to be the way to go. Thanks @aimc!

    class MotionConstrainedSensor(appapi.AppDaemon):
      """Manage a time constrained motion controlled switch

      Example configuration:
        motion_constrained_sensor:
          module: motionConstrainedSensor
          class: MotionConstrainedSensor
          activate_time: '20:00:00'
          deactivate_time: sunrise + 01:00:00
          delay: '300'
          switch: switch.fibaro_wall_plug_switch
          binary_sensor: binary_sensor.hallway_sensor

      Arguments:
        activate_time:  Time when the binary_sensor starts to be active
        deactivate_time:  Time when the binary_sensor stops to be active
        delay:  Seconds to keep switch on after binary_sensor has triggered
        switch:  Switch to control
        binary_sensor: Sensor to trigger switch to turn on
      """

      def initialize(self):
        self.log("Initializing")
        self.handle = None
        # Access all arguments now in order to trigger any errors due to missing
        # arguments during initialization
        self.arg_switch = self.args["switch"]
        self.arg_delay = self.args["delay"]
        self.listen_state(self.motion,
                          self.args["binary_sensor"], new="on",
                          constrain_start_time=self.args["activate_time"],
                          constrain_end_time=self.args["deactivate_time"])

      def motion(self, entity, attribute, old, new, kwargs):
        self.log("Motion detected, turning on {}".format(self.arg_switch))
        self.turn_on(self.arg_switch)
        self.cancel_timer(self.handle)
        self.handle = self.run_in(self.light_off, self.arg_delay)

      def light_off(self, kwargs):
        self.log("Turning off {}".format(self.arg_switch))
        self.turn_off(self.arg_switch)
4 Likes

This is more interesting than I originally thought. :wink:
I need to take PIR time (the time of no motion before the motion detector will be ready to activate again) into consideration as well, both at end of delay as well as at constrain_start_time…

1 Like

This is great example - thanks for sharing this - it really helped me!

1 Like