Comprehensive Room Presence Detection: Linking it all up!

I’ve been testing various methods of room presence detection and while many do have limitations, they can give an accurate description of room presence in a home when they are all linked up and considered together. I wrote this guide on how I’ve been implementing my version of comprehensive room presence detection. I’ve made sure that all the processes are modular and scalable so it’s simple to add new users/sensors or tweak criteria for changes.

This guide is for implementing per-person home and room presence detection in Home Assistant, where the logic is processed via the Node-RED add-on.

Showing how room presence can be displayed nicely in the frontend
Once implemented room presence can be displayed nicely in the frontend too. (This example was created using lovelace in Home Assistant).

Download – You can download a copy of all the flows below:

(note – I used a scrubber program to hopefully make it easier to import the flow. However, when I try to import it, I get an error message on all the Home Assistant nodes which are cleared when I double click on each one. If I figure out how to stop this, I’ll update the file.)

My flows use a combination of GPS tracking of phones, Bluetooth Low Energy (BLE) room presence, motion sensors and bed presence sensors to try to determine house and room presence up to the best level possible at any given time. I use the combined results of these methods to trigger many automations (lights, sleep, alarms etc.). Having reliable presence detection makes triggering all the other flows much more intuitive since you can drastically reduce the number of other conditions required for an automation to run.


Hardware:

In my setup, presence is tracked for two people across four rooms (the Bedroom, Living Room, Bathroom and Kitchen).

  • Motion sensors: 2x Aqara Motion sensors above the door in the Bedroom and Living Room. These are implemented via Zigbee2MQTT and a CC2531 dongle.
  • BLE Room Presence: 4x ESP32s flashed with ESP32-mqtt-room (1 in each room). These track BLE beacons on android phones generated by the Beacon Simulator app.
  • Bed presence: 2x HC-SR04 ultrasonic sensors on the underside of bed slats on either side (not the best solution but worked more reliably than pressure sensors for me).
  • Humidity Sensor: 1x Aqara temperature pressure humidity sensor tracks the humidity in the Bathroom to detect shower usage.

And that’s it. I do also use the HA app on the two android phones along with the Google Maps integration for home/away presence (discussed in the section below).

All the methods below can work fine with different methods of BLE room presence detection or brands of motion sensors. So long as the output of the entities is consistent, they can be swapped out easily. It’s also easy to start tracking further rooms and people by adding a motion sensor or even just an ESP32 (the Bathroom and Kitchen don’t have motion sensors in this example).


In Home Assistant:

Each person has a person.*person* entity (set automatically by the Google Maps integration or the device tracker component of the HA android app using the Person integration). In the flows below, I have just used an example person (Person 1). In this guide, I often abbreviate them to p1 in entity naming – so the entity for Person 1 would be person.p1. For actual people, I would suggest using initials for brevity so it helps if people in the home don’t share the same initials!

I use input_selects as a way to set all house/room presence and the individual locations of people:

  • input_select.house_presence – keeps track of who is home
  • input_select.*room*_presence – keeps track of individual room presence
  • input_select.*person*_location – keeps track of where people are (and is more detailed than the home/away person.*person* entity)

Two groups are also defined, containing a list of all the input_select.*person*_location and person.*person* entities. This is used in Node-RED function nodes to search through the whereabouts and home/away status of each person effectively.

This covers all the possible types of entities that keep a track of home and room presence. With the methods described, presence detection is easily extended to cover new rooms with a few hardware additions.

Example config for input_select.house_presence:

input_select:  
  house_presence:
    name: House Presence
    initial: Unknown #Default until actual state is known after restart
    options:
      - Unknown
      - Empty
      - Someone
      - Guests
      - Extended Away
      - P1
      - P2
      - P1 + P2 # +more for extra people

Example config for input_select.bedroom_presence:

input_select:  
  bedroom_presence:
    name: Bedroom Presence
    initial: Unknown #Default until actual state is known after restart
    options:
      - Unknown
      - Empty
      - Someone
      - Guests
      - P1
      - P2
      - P1 + P2
      - Extended Away
      - Going to Sleep
      - Sleeping
      - Waking Up
      - Woken Up

Example config for input_select.living_room_presence:

input_select: 
  living_room_presence:
    name: Living Room Presence
    initial: Unknown #Default until actual state is known after restart
    options:
      - Unknown
      - Empty
      - Someone
      - Guests
      - Extended Away
      - P1
      - P2
      - P1 + P2 #Expand below for more people

Example config for input_select.bathroom_presence:

input_select: 
  bathroom_presence:
    name: Bathroom Presence
    initial: Unknown #Default until actual state is known after restart
    options:
      - Unknown
      - Empty
      - Someone
      - Guests
      - Extended Away
      - P1
      - P2
      - P1 + P2 #Expand below for more people
      - Shower

Example config for input_select.kitchen_presence:

input_select: 
  kitchen_presence:
    name: Kitchen Presence
    initial: Unknown #Default until actual state is known after restart
    options:
      - Unknown
      - Empty
      - Someone
      - Guests
      - Extended Away
      - P1
      - P2
      - P1 + P2 #Expand below for more people

input_select.living_room_presence and input_select.kitchen_presence have the fewest options of all the rooms. The input select for the bathroom includes options for showering while the bedroom input select also has several options for when the occupants are asleep.

Example config for input_select.p1_location:

input_select:
  p1_location:
    name: Person 1 Location
    initial: Unknown #Default until actual state is known after restart
    options:
      - Unknown
      - Just Arrived
      - Home
      - Just Left
      - Away
      - Extended Away
      - Living Room
      - Bedroom
      - Bathroom
      - Kitchen #Extra rooms can be added below

Example configs for group.person_locations and group.person_home_away:

group:  
  person_locations:
    name: Person Locations
    entities:
      - input_select.p1_location
      - input_select.p2_location #Add more defined input_selects below

  person_home_away:
    name: Person Locations
    entities:
      - person.p1
      - person.p2 #Add more people below


In Node-RED:

Next up is a summary of my workflow to set the best possible room presence with whatever level of info is available at any given point. In my experience, BLE room presence is fantastic when it’s working (ie. the beacon is picked up and the person has their phone with them) but the overall process needs a helping hand to stay on track at times.

Group Frequently Used Nodes

In my Node-RED flows, all entities that are updated by many flows (such as input_select.p1_location) are terminated at a single node rather than lots of individual call service nodes at the end of each flow. This makes it much easier to find each flow that can update an input select. Different options are set in a function node prior to the message getting sent to be set to a terminus (this also shows what option each flow is setting). All the call service nodes that set house/room presence and people locations are grouped nearby with their own link in nodes right at the start.

Showing how to group frequently used nodes to set various input_select variables. Each have a link in node so they are only set in one place
Image: Frequently used nodes to set various input_select variables. Each have a link in node so they are only set in one place

Step 1: Home/Away Presence

1.1 – Set Home/Away on Startup:

I have each person’s input_select.*person*_location set as either Home or Away on startup. This is just in case changes were missed while Node-RED was offline. More detailed presence info will just overwrite this Home/Away status when available anyway. I have a flow that sends a message only when Home Assistant starts up. I use a sensor that monitors uptime and triggers when Home Assistant has been online for a few minutes. This triggers many flows that initialise various parameters and update them or do simpler tasks like setting the Home Assistant theme after a restart.

Setting Home or Away can be done using current state nodes or in a single function node for each person like my example below. (Note this flow relies on this add on that allows you to access Home Assistant states via a global variable – see this link for another example).

Showing how to set Home/Away on startup
Image 1.1: Set Home/Away on startup

1.2 – Set Just Arrived/Home/Just Left/Away/Extended Away:

My house presence flow is largely derived from a write-up on the bonani.tech blog. The author does a great job of explaining their flow, I’d recommend giving it a read! I did make some adjustments to better suit the naming conventions that I use. This single flow sets each person’s input_select.*person*_location to Just Arrived, Home, Just Left, Away and even Extended Away based on changes to their person.*person* entity.

Since the flow cleverly only has one input trigger node for all persons, I added some extra steps to the change node that follows it. In the change node, I have stripped “person.” from the person.p1 and set this as the msg.topic for the flow. This is then lazily modified on a per person basis to switch the msg.topic to p1_location if it was Person 1 whose person entity changed and to p2_location if it was Person 2 etc. This is achieved by searching msg.topic for p1 and replacing with p1_location and the same for p2 in a second change module in the node.

I left the last set msg.payload option as it was in the original flow that I imported. These changes mean that the flow now sets input_select.p1_location if the flow is triggered by person.p1 and input_select.p2_location if triggered by person.p2. Since the msg.topic is used as a differentiator, this single flow can keep track of lots of people at the same time. The changes in the person entity (based on Google Maps/HA App tracking) sets each person’s input_select to be Just Arrived, Home, Just Left, Away and even Extended Away. This flow slightly goes against my plan to have all service calls for a person’s input_select terminate in one place but I think the msg.topic separation would get confused with the link node (perhaps worth a revisit in the future).

Showing how to set input_select.*person*_location to Just Arrived/Home/Just Left/Away/Extended Away
Image 1.2: Set input_select.*person*_location to Just Arrived/Home/Just Left/Away/Extended Away

The next two flows are to set the overall house presence and keep it updated as people leave and arrive.

1.3 – Set House Presence:

The first flow is triggered by any person’s person.*person* changing to either Home/Away. The node with the question mark icon shows a place where the flow checks that this specific type of automation is actually turned on (that the user wants it to run). I have a series of input_boolean.automations_*type* that can pause various types of automation. The node here checks that input_boolean.automations_presence is on before continuing the flow.

Next, there is also a separator if there are guests. Currently, I have guest mode set to effectively override much of the presence detection. With guests, input_select.house_presence is set the Guests and remains unchanged. Without guests, a function node returns a string with the people at home and sends the value to a node that sets input_select.house_presence.

The function node searches all the person.*person* entities listed in group.person_home_away. It compiles a ‘+’ separated list of all the person.*person* entities which are Home. It takes the friendly name of all the person.*person* entities which are handily each person’s initials. Extra formatting sets the returned msg.payload so that the correct input_select.house_presence is set. So, if only Person 1 is home the node would set input_select.house_presence to P1 whereas, in a house with four people, it would return any combination of the four people who are home, in the order that they are set in the group.

This could also be achieved by checking each other person manually as someone else’s input_select.*person*_location changed, but that would get cumbersome for any more than two tracked people. Instead, this method is easily adapted to more people.

Showing the process of setting and updating input_select.house_presence with current occupation
Image 1.3: Set and update input_select.house_presence with current occupation

1.4 – Set House Presence to Extended Away if everyone is Extended Away:

Another short flow is triggered by any one person’s location input select going to Extended Away. The flow then sets input_select.house_presence to Extended Away if everyone else is too. If everyone goes on holiday, input_select.house_presence will be changed as soon as the last person’s input_select.*person*_location goes to Extended Away.

Showing the process of setting input_select.house_presence to Extended Away if everyone is Extended Away
Image 1.4: Set input_select.house_presence to Extended Away if everyone is Extended Away

Step 2: Generic Room Presence Detection

The next step down from overall house presence are flows to set generic room presence with no reliance on any BLE tracking. Instead, motion sensors and other generic indications of room presence are used as triggers (eg. a humidity sensor in the Bathroom and bed presence in the Bedroom). My generic room presence flow determines whether a room is occupied and can set an input_select.*room*_presence to be either Someone or Empty (or a specific person if only they are home).

2.1 – Motion/Other Generice Room Presence Indicators:

This flow is triggered by generic indicators of room presence wherever they exist. Each room has two link nodes – the first is for detection of presence (such as a motion or bed presence sensor being activated) and the second for the indication of a room being empty (such as motion detectors going to off). Things like a bed presence sensor going to off are not necessarily indications of a room being empty (since the Bedroom can be occupied without bed presence being on) so they are not wired into the Bedroom Not Occupied link node.

In the Bathroom, the humidity rising to over 75% suggests occupation while it falling below 75% suggests that it is no longer occupied. Examples like this or TV usage in a room can trigger generic presence detection in the absence of motion sensors. Since the Kitchen has no generic indicator of room presence, it is simply not included in Generic Room Presence Detection. Adding a power monitored kettle etc. would be an example of how to add this to the Kitchen.

It is not a good idea to add a room to Generic Room Presence Detection without an indicator that suggests the room is Empty. Otherwise, in the absence of more precise BLE Room Presence Detection, the room can never be set to Empty by just Generic Room Presence Detection.

For each room, a boolean flow variable is also set. This keeps track of whether Generic Room Presence is detected in each room and is used later (in Step 4) when setting the presence in a room. The naming convention for these flow variables is important and is explained in the note in the flow.

Showing the process of setting up indicators of Generic Room Presence (motion sensors etc.)
Image 2.1: Indicators of Generic Room Presence (motion sensors etc.)

2.2 – Set rooms to “Someone”:

When there is an indication of presence in a room by this generic method, the input_select.*room*_presence for that room is set to a specific person (ie. P1) if only one person is home or to Someone, if there are multiple people home since the people cannot be distinguished (in this generic room presence flow).

This flow also checks that presence detection is wanted by the user under the node with the ‘?’ icon. When setting the Bedroom to Someone by this method, there is also a check that the no-one is currently sleeping in the room (ie that input_select.bedroom_presence is not Going to Sleep, Sleeping, Waking Up or Woken Up. In these cases, Bedroom presence should not be changed. This is achieved by the node with the ‘moon’ icon. This check is also performed commonly to make sure that nothing changes the presence in the Bedroom away from any type of sleeping.

Showing the process of setting an input_select.*room*_presence to Someone by Generic Room Presence Detection
Image 2.2: Setting an input_select.*room*_presence to Someone by Generic Room Presence Detection

2.3 – Set rooms to “Empty”:

After motion sensor timeout or another generic indication of a room no longer being occupied, if the input_select.*room*_presence for that room is still Someone, there is another timeout before the room is set back to Empty. This timeout is important when BLE room presence is also in the mix (discussed later).

This timeout can be cancelled (no longer setting the room to Empty) if during the timer, the input_select.*room*_presence for that room changes to something other than Someone (most likely due to the input select being set to a specific person when their BLE detects them in the room too) or if new motion is detected in that room (the link node that sets the input select to Someone is also wired in to cancel any possible timer setting the room to Empty). In both these cases, there is confirmed occupation in that room so any timer looking to set the room to empty should be cancelled and no message passed.

There is also an option to add other conditions that must be met before a room is set to Empty by this method. In this example, the Bedroom can only be set to Empty by Generic Room Presence Detection is there is no presence detected on either side of the bed. If bed presence is detected before the Bedroom is to be set to Empty, the timer keeps getting restarted until there is no more bed presence detected or the timer is cancelled by something else.

Showing the process of setting an input_select.*room*_presence to Empty by Generic Room Presence Detection
Image 2.3: Setting an input_select.*room*_presence to Empty by Generic Room Presence Detection

Step 3: BLE Room Presence Detection

Next up is the ‘holy grail’ of presence tracking – the elusive room presence detection which distinguishes exactly who is in which room. In my flows, this works on top of the generic room presence detection based on motion and other faster-acting sensors. Motion sensors will always be faster than BLE which is the most common type of person-specific room presence detection that I’ve encountered. So, I use a combination of the two.

Using a combination is helpful if a person goes into a room but then stays seated/still. First, when someone walks into a room – the generic flow (2.2) would set an input_select.*room*_presence to be Someone straight away (this is used for lighting etc.). Second, when BLE tracking catches up and detects that a person has moved to this room, the Someone option is overwritten with the new person (based on BLE tracking). Now, when the motion detector is clear but the person is still sat there, the input_select.*room*_presence would still show the person in there (via BLE) so the room would not be set to Empty.

3.1 – BLE Inputs and Checking other States:

The BLE sensor for each person is fed into a flow that also saves other info that is later checked before setting a person’s input_select.*person*_location.

Similarly to the Home/Away flows, the input entity id of the sensor is stripped and a msg.person is set (for Person 1, msg.person would be p1 if their BLE sensor was named sensor.p1_esp32_mqtt_room etc.). Next, msg.ble_room is set as the state of the sensor that triggered the flow.

Then, the flow only continues if input_boolean.automations_presence_ble is on (this is an easy way to stop BLE detection overwriting generic room presence detection if required). If allowed to continue, a series of other states are saved too (whether Bedroom or Living Room motion has been detected) and also the current state of the person’s input_select.*person*_location. Since the msg.person was set, the current state node can just check on the state of input_select.{{person}}_location (where {{person}} retrieves msg.person).

Following this, a function node checks various properties about bed presence. If input_select.automations_bed_presence is on, and the person’s bed (eg. binary_sensor.bed_presence_p1) is also on (occupied), then the node sets msg.bed_presence to on. Otherwise, msg.bed_presence is set to off/not automated. Adding each of these properties to a msg property makes checking then much easier later.

Showing the process of initialising BLE based Room Presence Detection
Image 3.1: Initialising BLE based Room Presence Detection

3.2 – Set Room Presence:

To limit erroneous BLE signals setting room occupation, I added a few conditions that must be fulfilled before the room is set via BLE. This combines the states of various sensors from all rooms being tracked (the ones saved in the above flow, 3.1).

Next, come the tests discussed previously that must be passed for BLE room presence flow to set a person’s input_select.*person*_location. These designations are done for everyone in a single flow (making it simple to expand for more users).

First, there is a check to see that the state of msg.ble_room is different from the current input_select.*person*_location – if they agree then there’s no need to change anything. Then, things differ for each of the three rooms being set.

The Bedroom

If input_select.*person*_location is to be set to the Bedroom via BLE then there is a check if the bedroom motion sensor has been triggered in the last 5 mins (this can be either to on or to off – either one suggest some sort of motion in the last 5 mins or so). If there has been motion, then input_select.*person*_location is set to Bedroom. If there hasn’t been motion, there is a check to see if the BLE state has been the same for over 2 mins. If so, it’s likely that the BLE incorrectly changed input_select.*person*_location to an incorrect room while the person has actually been sitting still in the bedroom and hasn’t triggered the motion detector. In this case, after 2 mins of the BLE state still being Bedroom, input_select.*person*_location is set to Bedroom.

The Living Room

If input_select.*person*_location is to be set to the Living Room via BLE then it first checks that the person is not in bed. If the person is not in bed the flow follows the same steps as setting Bedroom via BLE. Otherwise, if the person is detected as being in bed then their BLE room must continue to be the same (Living Room) for 2 mins before their input_select.*person*_location is set to the Living Room.

The Bathroom

If input_select.*person*_location is to be set to the Bathroom via BLE, then the first check is again that the person is not in bed. This is an example of a room without a motion sensor but since motion will likely be triggered in either the bedroom or the living room on the way to the bathroom, it checks both those motions sensors, if there has been motion in either recently then is does a final check that the BLE has been detecting the person in the Bathroom for over 10 seconds. With no motion in either the bedroom or the living room, there’s just a longer time that the BLE state has to be bathroom before input_select.*person*_location is set to Bathroom. This longer time of the BLE room being the Bathroom is also required in the case that the person who’s BLE is showing Bathroom is also detected as being in bed (in the Bedroom).

The Kitchen

The flow for the Kitchen follows the same process as described for the Bathroom above.

Showing the process of setting an input_select.*person*_location to a specific room based on BLE Presence Detection
Image 3.2: Setting an input_select.*person*_location to a specific room based on BLE Presence Detection

Step 4: Update Room Presence

This last section where the input_select.*room*_presence for each room is updated to reflect the states of the input_select.*person*_location for everyone. The flow is triggered by a change in the state of someone’s input_select.*person*_location. Then, the individual presence in each room is returned by a function node for each room. The Bedroom is not changed if someone is asleep. For each room that is occupied, the list of each person’s input_select.*person*_location (group.person_locations) is searched. For each room, a ‘+’ separated list is created (in the order that people are defined in the group) of the initials of everyone who is in there. If there is no-one in the room based on each input_select.*person*_location, the function node outputs that the room is Empty. However, in the case that there are Generic Room Presence Detection methods in the room, the flow variable that was set in Section 2.1 is first checked. If there is no BLE presence detected but the Generic Room Presence Detection still suggests the room is occupied, the function node outputs Someone instead of Empty. This effectively reverts the presence in the room back to being controlled by Generic Room Presence until BLE Presence is next detected.

The function nodes are configured so that only the room to be checked needs to be added (in the correct format, discussed in the node). Since the Bedroom, Living Room and Bathroom are all set up with Generic Room Presence Detection as well as BLE Room Presence, these room use this extra check while the Kitchen (with no Generic Room Presence Detection) does not.

Much like in Section 2.3 where rooms were set to Empty by Generic Room Presence Detection, setting a room to Empty is subject to a timer that can be reset by the function node no longer outputting Empty. This is in case someone’s input_select.*person*_location is set to an incorrect room momentarily by BLE Room Presence.

Delaying setting a room to Empty can sometimes lead to the input_select.*room*_presence of multiples rooms containing a person (ie input_select.bathroom_presence could be P1 and input_select.bathroom_presence could be P1 + P2 at the same time. This is acceptable in my use case since it’d more important for me to be cautious setting a room to Empty rather than removing someone from a room quickly. Setting a room to Empty resulted in some flashing lights when people incorrectly jumped between rooms.

Showing the process of setting input_select.*room*_presence based on updates to input_select.*person*_location
Image 4: Setting input_select.*room*_presence based on updates to input_select.*person*_location

Conclusion

Thanks for reading this tutorial on my implementation of comprehensive room presence detection! It’s my first writeup of my projects so feel free to get in touch with any comments or suggestions. I’ve certainly noticed that having to write it all down made me think more critically and helped to improve the flows in general.

If you want to try out this way of room presence detection, you can download a file with all the flows discussed at the top of the page (in the download section, just after the first image). So long as you set up you own input select variables and groups etc. that were described in the ‘In Home Assistant’ section, it’s just a matter of changing the various rooms and people to suit your setup.

I mentioned that I use this room presence as a way to automate various things around the house (lighting, plant lighting, various aspects of a morning alarm clock, vacuum schedules and when to activate displays etc.). Over time I’ll try to do shorter writeups of these projects too – they will be linked under the Home Automation section of this website.

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to top