Bathroom Automation with Door and motion sensors

Hi, just today I started my journey with AppDaemon and Python and the first thing I wanted to try was automating the bathroom lights. Previously I tried with the YAML automation and was unable to achieve what I wanted.
Here is the entire automation code which does the following

  • Listen to door sensor states
  • If door opens, then based on the time, switch on the lights
  • If door is closed then, using motion sensor detect if bathroom is occupied or not
  • If occupied, then, don’t do anything, else, switch off the lights

The automation seems to be working for me ok, however since am new to AppDaemon and Python, wanted to share this code here and understand if this can be improved w.r.t code quality and maybe some other suggestions that you might have. This will really help me continue my journey with this amazing add-on

Edit: Code updated after adding contrain and other changes suggested
Edit 2: Implemented changes per @Burningstone’s suggestion
Edit 3: Implemented few more logics (which made sense to me currently :wink:), thanks @gpbenton @ReneTode
Edit 4: Re-done the entire logic with keep it simple philosophy, the code is much cleaner now

class BathroomAutomations(hass.Hass):
    def initialize(self):
        self.motion = self.args["motion_id"]
        self.light = self.args["light_id"]
        self.door = self.args["door_id"]
        # Always listen to motion detector and turn on the light in case it is off
        self.listen_state(self.motion_cb, entity=self.motion, new="on")

        # Listen to Door sensor
        self.listen_state(self.door_cb, entity=self.door)

    def motion_cb(self, entity, attribute, old, new, kwargs):
        self.log("Enter motion_cb")
        if self.get_state(self.light) == "off":
            self.turn_on(self.light)

    def door_cb(self, entity, attribute, old, new, kwargs):
        if new == "on":
            self.log("Door opened")
            if self.get_state(self.light) == "off":
                self.turn_on(self.light)
        elif new == "off":
            self.log("Door closed")
            # 20 seconds timeout started
            self.door_close_timeout_handle = self.run_in(
                self.door_close_timeout_cb, 20)
            self.door_close_motion_handle = self.listen_state(
                self.door_close_motion_cb, entity=self.motion, new="on")

    def door_close_timeout_cb(self, kwargs):
        # Cancel the door close motion detection because timeout expired
        self.cancel_timer(self.door_close_motion_handle)
        if self.get_state(self.light) == "on":
            self.turn_off(self.light)

    def door_close_motion_cb(self, entity, attribute, old, new, kwargs):
        # Cancel the timer as bathroom is occupied
        self.cancel_timer(self.door_close_timeout_handle)

Just a quick idea - this sort of check can be handled with a callback constraint to make the code look a little cleaner.

1 Like

But this is more important to get correct. You should not really sleep in callback functions, as this holds up a thread see the note on threading

I think, in this case, you could, in this case, call self.run_in() and then continue your wait by continuously calling that callback, but I think it would be a better design to set up another trigger to be called when the motion occurs.

1 Like

did you test this?
because the state returns "on"or “off”
and i dont think that
if not “off”: will result in True

This is where I was getting confused and could not understand the design that needs to be implemented. Basically, by logic, I want to monitor the motion sensor for the next 20 seconds and the moment it gets triggered, I want to stop everything and ensure that the light does not switch off.

Can you help me figure out how this needs to be coded?

can also be a callback constraint

1 Like

True, this did not work after trying because of the same problem that you mentioned but now I have put a contrain_input_boolean, so this line is gone.

But, thanks for pointing out the mistake :grinning:

Some suggestions:

  • define your entities in the initialize function like: self.light = self.args[‘light_id’] instead of doing it over and over again

  • read up on f string literals for python the code for the logs gets way more readable like this in my opinion e.g.:

    self.log("Turning off Lights: {}".format(self.args["light_id"]))

    becomes

    self.log(f"Turning off Lights: {self.args['light_id']}

1 Like

Thank you so much @Burningstone, this really helps. I have updated the code per your suggestions. Did not know about f string honestly. I went from %s to .format to f strings literally in a day :smile:.

Love the community support here.

instead of the sleep thing you could do something like this:

self.run_in(self.check_motion, 20)

def check_motion(self, *args: list):
    if self.get_state(self.motion) == 'on':
        # do the rest 

This will check if the motion is on 20 seconds after the door has been closed.

Thanks for the suggestion, however, my logic is a little different than this one. I want to monitor for the entire duration of 20 seconds and not after 20 seconds. Reason being, the motion sensor has been set up to turn off after 3 seconds of detecting motion; it is highly unlikely that this will be on after 20 seconds.

So, the idea was to see if the sensor gets triggered within 20 seconds of door getting closed, this gives me a very high probability of bathroom being occupied.

Ah I understand. You could create a handle and run the check for motion function every second with self.run_every.

In the check for motion function add a counter and each time it is called increase the counter by one. Also add a condition in the check for motion function,that once the counter hits 20 or motion has been detected, cancel the handle.

create a listen_state for the motion sensor and set a timestamp.
like:

def init(...):
  self.last_motion = datetime.datetime.now()
  self.listen_state(self.cb,self.args["motion_id"])
def cb(...):
  if new == "on":
    self.last_motion = datetime.datetime.now()

pseude code

now you can check in the run_in if there was motion in the last 20 seconds

handle? Sorry for being a novice here :slight_smile:. Any pseudo code you can help me with?

There are many ways to do this, and Rene’s will work fine. As an alternative, I would do

self cb_handle = self.listen_event(self.motion_detected_cb, .....)
self.timeout_handle = self.run_in(self.timeout_cb ....)

def motion_detected_cb( self,  ....):
  self.cancel_timeout(self.timeout_handle)
  # do motion detected code here

def timeout_cb(self, kwargs):
  self.cancel_listen_state(self.cb_handle)
  # do motion not detected code here
1 Like

pssst its cancel_timer and not cancel_timeout :wink:

1 Like

I have removed the time.sleep now and it just works wonders. Most of the situations are getting handled pretty easily.

Thanks a lot @ReneTode, @gpbenton, @Burningstone for all the help. You guys are awesome, cheers!

1 Like

Please post your final code, so others can learn from you as well :wink:

done, updated the final version which is currently running on my system :slight_smile: