Last Motion Sensor for room presence

Homeassistant is missing a native function to create a last motion sensor. It should take a list of all motion sensors and extract the name of the last triggered entity changing to state on.

sensor:
  last_motion:
    entities:
      - binary_sensor.motion_1
      - binary_sensor.motion_2
      - binary_sensor.motion_3
      - binary_sensor.motion_4

That way it’s possible to determine in which room/area someone is currently in. This is very useful for every single person household e.g. you can use it to automatically turn off lights and media players in room where nobody is in.

Currently this is only possible with custom components that offer a variable like:

A full example of a last motion sensor workaround and usecase can be found in this thread:

I’d love to see this feature in hass.

Until someone creates a Last Motion Sensor, you can use a Template Sensor to produce the desired result.

  - platform: template
    sensors:
      last_motion:
        friendly_name: 'Last Motion'
        value_template: >
          {% set sensors = [states.binary_sensor.kitchen,
                            states.binary_sensor.living, 
                            states.binary_sensor.bathroom] %}
          {% set t = sensors | map(attribute='last_changed') | max %}
          {% set s = (sensors | selectattr('last_changed', 'eq', t) | list)[0] %}
          {{s.name}} {{s.last_changed.timestamp() | timestamp_local() }}

If you just want the sensor’s name and not the time it triggered, remove this from the last line: {{s.last_changed.timestamp() | timestamp_local() }}

If you already have a group of all motion sensors, you can write it like this:

  - platform: template
    sensors:
      last_motion:
        friendly_name: 'Last Motion'
        value_template: >
          {% set t = expand('group.all_motion') | map(attribute='last_changed') | max %}
          {% set s = (expand('group.all_motion') | selectattr('last_changed', 'eq', t) | list)[0] %}
          {{s.name}} {{s.last_changed.timestamp() | timestamp_local() }}

If you don’t wish to specify the motion sensors by name or including them in a group, they can be extracted by their device_class. Here are the two relevant lines in the template:

          {% set t = states.binary_sensor | selectattr('attributes.device_class', 'eq', 'motion') | map(attribute='last_changed') | max %}
          {% set s = (states.binary_sensor | selectattr('attributes.device_class', 'eq', 'motion') | selectattr('last_changed', 'eq', t) | list)[0] %}
3 Likes

But your solution would also track if a motion sensor’s state switches to off. Imagine walking around your home. If you do this fast multiple motion sensors will have the state on (triggered). So the first one that went to on will also be the first one that switches to off and that is not the information I need to determine in which room I am. Am I wrong?

No, you’re not wrong. Simply enhance the template to only consider sensors whose current state is on using stateattr('state', 'eq', 'on').

Thanks! Identify the motion sensors by their device class is a good idea. So the sensor would be more flexible if I add another sensor. Can you please provide a full example? Where do I have to add the part with stateattr('state', 'eq', 'on') that you mentioned?

Give it a try; it’s easy. It will be more satisfying to do it yourself than to have it done for you. I’ve already provided two full examples so you can use them for reference. Hint: just add it as an additional filter to each template. You can use the Template Editor to experiment with the template.

Let me know if you get stuck.

Hi @123

I know this is an old topic, but I have some troubles adding the filter (state -eq on) to this sensor.

I tried adding the filter to both lines as follows:

{% set t = sensors | selectattr('state','eq','on') | map(attribute='last_changed') | max %}
{% set s = (sensors | selectattr('state','eq','on') | selectattr('last_changed', 'eq', t) | list)[0] %}

This only works if any motion sensor has a state on. If all sensors are off, there will be errors:

ValueError: max() arg is an empty sequence 

Which is expected, as you cannot max() on something that is empty. Do you have any idea how I could solve this? Maybe I should add an if around these two lines somehow to check if there are any sensors with state on?

Many thanks for the help!

Yes, check if the list’s length is greater than zero before proceeding to use max.

Many thanks @123! I think I found a clean solution:

{% set t = sensors | map(attribute='last_changed') | max %}
{% set s = (sensors | selectattr('last_changed', 'eq', t) | list)[0] %}
{% if( s | selectattr('state', 'eq', 'on') ) -%}
{{s.name}} {{s.last_changed.timestamp() | timestamp_local() }}
{%- endif %}

I’m just not sure whether I should add an else that returns undefined or some other null value?

If the intention is to use it in a Template Sensor then it should return a value for all circumstances.

Here’s yet another way to do it:

{% set t = sensors | selectattr('state','eq','on') | list %}
{% if t | length > 0 %}
  {% set s = (t | selectattr('last_changed', 'eq', t | map(attribute='last_changed') | max) | list)[0] %}
  {{s.name}} {{s.last_changed.timestamp() | timestamp_local() }}
{% else %}
  none
{% endif %}
3 Likes

I have been trying to modify my LAST MOTION sensor to show only motion detected. ON not OFF. This solution is as close as I can get, however, I would like the LAST MOTION to remain listed. It looks like after 60 seconds of no PIR activity, “none” is triggered by ELSE statement.
Any guidance how I can keep the LAST MOTION identified, regardless of time passed?

Sorry, this topic is no longer fresh in my mind and I don’t have any guidance to offer. Hopefully someone else can help you with it.

1 Like

This should work. Requires creation of a group with a list of motion sensors in it. But you can easily swap this out to use the sensors variable in the OP.

{% set t = expand('group.all_motion') | selectattr('state', 'eq', 'on') | map(attribute='last_changed') | max %}
{% set s = (expand('group.all_motion') | selectattr('last_changed', 'eq', t) | list)[0] %}
{{s.name}} {{s.last_changed.timestamp() | timestamp_local() }}
{% set t = expand('group.all_pir_sensors') | selectattr('state', 'eq', 'on') | map(attribute='last_changed') | max %}
{% set s = (expand('group.all_pir_sensors') | selectattr('last_changed', 'eq', t) | list)[0] %}
{{s.name}}

if there are two sensors in the group BOTH ‘on’ at the same time, it flips between the two, rather than only showing the most recent one that changed to ‘on’