🚷 Automatic Room Occupancy

Tags: #<Tag:0x00007fc40bed9aa0> #<Tag:0x00007fc40bed99d8>

Automatic Room Occupancy

This blueprint will toggle an input_boolean that signifies if a room is occupied or not. Uses multiple inputs to determine this such as motion, media players and room presence sensors EG room-assistant.

This is very much a work in progress.
I have a to-do list (see below) and any help on this would be much appreciated. Also any suggestions are greatly appreciated :smile:

:goal_net: Goal

I (like many) have room level automations that rely on knowing if someone is in the room or not. Accuracy is the issue here and I have found that no one solution works well on its own, and it is best to combine multiple layers of logic.

:notebook_with_decorative_cover: Prerequisites

You need an input_boolean to signify if the room is occupied. You will need a minimum of one of the following for your room:

  • Motion Sensor(s) – create a group if multiple
  • Door Sensor(s) – create a group if multiple
  • Media Players
  • Presence Sensors – see room-assistant

This will work best with 2 or all of the above. If you only have a motion sensor this will be overkill and there may be better suited blueprints for you.

:timer_clock: Optional Timeout Helpers

These will all default to 10 minutes if not provided. For best results add these helpers so you can tweak things to your liking on your own front end.

  • input_number.motion_occupancy_timeout
  • input_number.media_occupancy_timeout
  • input_number.bluetooth_occupancy_timeout (NOT IN USE YET)

:computer: Logic

The room will be occupied when ANY of the following are met:

  • Motion (for any amount of time)
  • Door is shut (useful for rooms EG bathrooms)
  • Media is playing in the room on at least one device
  • Presence sensor(s) show at least 1 person in room

Optional: You can have it only trigger when someone is home or guest mode is on.

Note: You do not need all of these for this to work as all are optional. These can also individually be turned off. This is handy if you have an unreliable method for switching occupancy on, but still want it to be a condition of the off state.

The room will not be occupied when ALL of the following conditions are met:

  • No motion
  • No media playing in room
  • Presence sensors do not report anyone in room

Note: Each of these (BT still todo) have a timeout feature. See helpers section above. This will default to 10 minutes if helper does not exist.

To-Do / Wishlist (Help Needed) :spiral_notepad:

Use bluetooth timeout input

I want to use a timeout feature but I cant use last_updated or last_changed. As the state may have changed from one room to another that does not include this room. EG this room is bedroom but they have moved from lounge to kitchen within timeout period.

“Wasp in a box”

Have the room always occupied if the door is shut and there has been any motion since it was shut.

Changelog :spiral_notepad:

2020.12.23: Better error handling if motion or door not provided
2020.12.23: Added door sensor and ability to have room occupied if door is shut
2020.12.22: Trigger on media or presence state changes. Remove repeating trigger.
2020.12.21: Initial Version

Blueprint Code

Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.

blueprint:
  name: "[Occupancy] Auto Room Occupancy"
  description: >
    Automatically turn on and off an ocupancy switch for a single room.

    ------LOGIC------
    Occupied when ANY of the following conditions are met (all are optional).
      - door is shut
      - any motoion
      - any media player playing
      - door is shut (for rooms eg bathrooms)
      - on room presence (eg room-assistant)

    Occupancy is clared under ALL of the following conditions being met (all are optional):
      - motion off for a set amount of time
      - no player in room played for a set amount of time
      - no one in room (TODO//: add a timer)

    ------HELPERS------
      Uses the following helpers to allow for front end control of timeouts.
      All default to 10 minutes if not provided.
        - input_number.motion_occupancy_timeout
        - input_number.media_occupancy_timeout
        - input_number.bluetooth_occupancy_timeout (NOT IN USE YET)

    ------TODO------
      - Use bluetooth timeout input (Help needed)


  domain: automation


  input:
    # SENSORS USED
    motion_sensor:
      name: Motion Sensor
      description: "A motion sensor or group of motion sensors in room. State must be either 'on' or 'off'."
      default: "binary_sensor.none"
      selector:
        entity:
    door_sensor:
      name: Door Sensor
      description: "A door sensor or group of door sensors in room. State must be either 'on' or 'off'."
      default: "binary_sensor.none"
      selector:
        entity:
    media_players:
      name: Media Players (Optional)
      description: "A comma separated list of media_players in room. Include domain and name IE media_player.lounge_echo."
    media_states:
      name: Media States to Match (optional)
      description: "A comma separated list of states a Media Player can be in when room is occupied. Default is 'playing'."
      default: "playing"
    presence_entities:
      name: Room Presence Entities (Optional)
      description: "A comma separated list of room presence entities. Include domain and name IE 'sensor.phone_1_room_presence, sensor.phone_2_room_presence'."
      default: "sensor.none"
    presence_states:
      name: Room Presence State (Optional)
      description: "A comma separated list of states that presence entity can match. EG 'lounge, lounge2'."
      default: "none"

    # ENABLE / DISABLE INDIVIDUAL FEATURES
    occupied_on_motion_enabled:
      name: Enable Motion Occupancy
      description: "Turn on/off occupancy being set to 'on' via motion."
      default: true
      selector:
        boolean:
    occupied_on_shut_door:
      name: Enable Door Occupancy
      description: "Turn on/off occupancy being set to 'on' when door is shut."
      default: false
      selector:
        boolean:
    occupied_on_media_enabled:
      name: Enable Media Occupancy
      description: "Turn on/off occupancy being set to 'on' via media."
      default: false
      selector:
        boolean:
    occupied_on_presence_enabled:
      name: Enable Presence Occupancy
      description: "Turn on/off occupancy being set to 'on' via presence."
      default: false
      selector:
        boolean:

    # GLOBAL DISABLE
    disabled_entity_id:
      name: Disable Mode (Optional)
      description: "An input_boolean to entirely disable this logic. Where 'on' disables."
      default: "none"
      selector:
        entity:
          domain: input_boolean

    # HOME SENSORS
    household_group:
      name: Household Group (Optional)
      description: "A grouped set of people in your house. On logic will only run if home."
      default: "none"
      selector:
        entity:
          domain: group
    guest_mode_switch:
      name: Guest Mode (Optional)
      description: "An input_boolean to allow logic to work when household is away."
      default: "none"
      selector:
        entity:
          domain: input_boolean
    
    # TARGET
    occupancy_switch:
      name: Occupancy Switch
      description: "An input_boolean that switches occupancy state in target room. On = occupied and Off = not_occupied."
      selector:
        entity:
          domain: input_boolean


variables:
  time_now: "{{ as_timestamp(now()) }}"
  
  media_players_str: !input media_players
  media_players: "{{ media_players_str.split(',') | map('trim') | list }}"
  media_states_str: !input media_states
  media_states: "{{ media_states_str.split(',') | map('trim') | list }}"
  media_timeout: "{{ states('input_number.media_occupancy_timeout') | int(10) }}"
  media_is_playing: "{{ expand(media_players) | selectattr('state','in',media_states) | list | count > 0 }}"

  presence_entities_str: !input presence_entities
  presence_entities: "{{ presence_entities_str.split(',') | map('trim') | list }}"
  presence_states_str: !input presence_states
  presence_states: "{{ presence_states_str.split(',') | map('trim') | list }}"
  presence_timeout: "{{ states('input_number.bluetooth_occupancy_timeout') | int(10) }}"
  in_room: "{{ expand(presence_entities) | selectattr('state','in',presence_states) | list | count > 0 }}"

  motion_sensor: !input motion_sensor
  motion_timeout: "{{ states('input_number.motion_occupancy_timeout') }}"
  motion_last_changed: "{{ states[motion_sensor].last_changed.timestamp() | float(0) }}"
  motion_is_timed_out: "{{ (time_now - motion_last_changed) > motion_timeout | float(10) }}"
  is_motion: "{{ is_state(motion_sensor, 'on') }}"

  door_sensor: !input door_sensor
  door_shut: "{{ is_state(door_sensor, 'off') }}"

  occupied_on_motion_enabled: !input occupied_on_motion_enabled
  occupied_on_shut_door_enabled: !input occupied_on_shut_door
  occupied_on_media_enabled: !input occupied_on_media_enabled
  occupied_on_presence_enabled: !input occupied_on_presence_enabled

  disabled_entity_id: !input disabled_entity_id
  blueprint_disabled: "{{ disabled_entity_id != 'none' and is_state(disabled_entity_id, 'on') }}"
  
  household_group: !input household_group
  guest_mode_switch: !input guest_mode_switch
  house_home: "{{ household_group == 'none' or is_state(household_group, 'home') }}"
  guests_home: "{{ guest_mode_switch == 'none' or is_state(guest_mode_switch, 'on') }}"
  at_home: "{{ house_home or guests_home }}"


mode: parallel


trigger:

  # DOOR
  - platform: state
    entity_id: !input door_sensor

  # MOTION
  - platform: state
    entity_id: !input motion_sensor
    to: 'on'
  - platform: state
    entity_id: !input motion_sensor
    to: 'off'
    for:
      minutes: "{{ states('input_number.motion_occupancy_timeout') | int(10) }}"

  # MEDIA
  - platform: state
    entity_id: !input media_players

  # PRESENCE
  - platform: state
    entity_id: !input presence_entities


condition:
  - "{{ not blueprint_disabled }}"


action:
  - choose:

      # OCCUPIED IF DOOR SHUT
      - conditions:
          - "{{ occupied_on_shut_door_enabled }}"
          - "{{ door_shut }}"
          - "{{ at_home }}"
        sequence:
          - service: input_boolean.turn_on
            data:
              entity_id: !input occupancy_switch

      # OCCUPIED ON MOTION
      - conditions:
          - "{{ occupied_on_motion_enabled }}"
          - "{{ is_motion }}"
          - "{{ at_home }}"
        sequence:
          - service: input_boolean.turn_on
            data:
              entity_id: !input occupancy_switch

      # OCCUPIED ON MEDIA PLAY
      - conditions:
          - "{{ occupied_on_media_enabled }}"
          - "{{ media_is_playing }}"
          - "{{ at_home }}"
        sequence:
          - service: input_boolean.turn_on
            data:
              entity_id: !input occupancy_switch

      # OCCUPIED ON PRESENCE
      - conditions:
          - "{{ occupied_on_presence_enabled }}"
          - "{{ in_room }}"
          - "{{ at_home }}"
        sequence:
          - service: input_boolean.turn_on
            data:
              entity_id: !input occupancy_switch

      # CLEAR OCCUPANCY
      - conditions:
          - "{{ not is_motion }}"
          - "{{ motion_is_timed_out }}"


          # NO MEDIA - none of the players has played in the last x minutes
          - >
            {% set t = (time_now - media_timeout) * 60 %}
            {% set ns = namespace(not_playing=[]) %}
            {% for player in media_players %}
              {% for state in media_states %}
                {% if states[player].last_changed is defined %}
                  {% set timed_out = states[player].last_changed.timestamp() < t %}
                {% else %}
                  {% set timed_out = true %}
                {% endif %}
                {% if states(player) != state and timed_out %}
                  {% set ns.not_playing = ns.not_playing + [ player.entity_id ] %}
                {% endif %}
              {% endfor %}
            {% endfor %}
            {{ ns.not_playing | length >= media_players | length }}


          # NO PRESENCE - none of the presence entities in room
          - "{{ not in_room }}"
          # TODO// Need to add timeout IE time since last in room.
          # But cant use last_updated or last_changed as may have changed from one room to another that does not include this room
          # EG this room is bedroom but they have moved from lounge to kitchen within timeout period.
          # HELP NEEDED! :)

        sequence:
          - service: input_boolean.turn_off
            data:
              entity_id: !input occupancy_switch
10 Likes

Cool blueprint! Minor typo correction:

Yeah, I really enjoy my media payers too. Whenever I walk into a room, they pay some people to play lots of music, and Home Assistant detects the deduction from my bank account.

On a more serious note, you forgot an L:
payers
players

1 Like

:laughing:
Thanks for the heads up, all sorted.

I was trying to overly complicate the triggering so this has been re-done.
Gone is the nasty time repeated work around.

Next on the list is to add door sensors
Handy for rooms that are always occupied when door is shut EG bathrooms.

Also to allow motion logic for if there is movement in a room since the door was shut.

The automation is working. but when there is no more occupancy my boolean is not going back to False

All characteristics of a room occupied by a person who is either asleep or sitting still, like when reading.

Completely reliable, passive occupancy detection is a challenging goal. You may want to consider employing the Bayesian Binary Sensor to add some infererence into the equation. However, configuring it involves a fair bit of experimentation.

Yes agreed.

I use room-assistant to try and combat this. Its not 100% accurate but I have it pretty dam close for my use case.

In this case when I’m reading (and my phone is next to me) it reports that I’m in the room through Bluetooth.

I’ll look at the Bayesian sensor, thank you!
Do you think that could just replace this blueprint entirely or compliment it?

Can you show me your automation config and I’ll take a look?

Thanks for the nice blueprint. Very helpful!

I spotted a small bug in your media player timeout logic:

{% set t = (time_now - media_timeout) * 60 %}

I’m pretty sure that your intention was to subtract the media_timeout value as minutes. The correct code would therefore be:

{% set t = time_now - (media_timeout * 60) %}

The parentheses are redundant in this case but they make it easier to read IMO :slightly_smiling_face:

That sounds like a proper solution for my very first smart home scenario problem (sitting in the office room on the desk, want to have lights and heating on where a motion sensor is not sufficient because of lack of movement). I think a motion sensor + door sensor + this blueprint are a powerful solution. Not sure if room assistant could add something to that.

Thanks for sharing and actively working on / improving it!

You can also leave out the parentheses because operator precedence will perform multiplication before subtraction.

{% set t = time_now - media_timeout * 60 %}

You can easily demonstrate it by pasting this into the Template Editor:

{{ 10 - 2 * 3 }}

The result will be 4 (not 24).

I have an idea: would you take into considerations that there might be a used device (Laptop for e.g. or printer) that is used in one room to mark it as occupied? It would be cool if that could be used.

Hi, I am trying to use your blueprint. It “creates” it, I can see in automations.yaml (multiple times) but it dos not show up in the UI… do you know why?

Screenshot?

There is nothing to screen shot because it is not created in the UI (automations), it is in yaml in automations.yaml:

- id: '1617804849577'
  alias: '[Occupancy] Auto Room Occupancy'
  description: ''
  use_blueprint:
    path: gdeboos/automatic-room-occupancy.yaml
    input:
      presence_states: none
      motion_sensor: binary_sensor.motion_sonoff_01
      occupancy_switch: input_boolean.occupancy_01
      media_players: none

(it is a test with 1 motion sensor now)

Reboot HA + clear cache?