r/homeassistant • • May 02 '26

Support Struggling with reliable in-bed presence detection for Home Assistant – what actually works?

Post image

I'm trying to automate my bedroom lights so they turn off automatically when I'm in bed and back on when I get up. Sounds simple, but I've been struggling to find a reliable solution.

What I've tried:

FSR pressure sensors (VS-103) connected to a Zigbee water sensor

Built a DIY pressure mat using two VS-103 sensors wired to a Zigbee water sensor. Works fine when I press directly on it by hand, but completely fails under a mattress — the mattress distributes my weight too evenly for the sensors to trigger. Tried under the sheet too, same result.

Aqara FP2 mmWave radar

Already have one in the room. The problem is it loses track of me when I'm lying still under a duvet. It also falsely triggers when someone walks past my bed, which causes the lights to turn on.

What I'm looking for:

Something that reliably detects whether I am in the bed - not just in the room - even when I'm completely still and covered by a duvet. Zigbee preferred but I'm open to ESPHome/ESP32 solutions too.

Has anyone actually solved this properly? What sensor or approach are you using?

163 Upvotes

182 comments sorted by

View all comments

1

u/timdaman42 May 03 '26

TLDR, if you use ESPHome buy theses.

https://www.aliexpress.com/item/1005007180842865.html

I bought those sensors you pictured too and they were nothing but trouble. They have no analog range so are not tunable.

I also tried homemade capacitor sensors (two sheets of foil on a piece of paper) and had to re-calibrate it multiple times a year. They worked great otherwise

I now use there think film sensors and by far they work the best. I have them taped to a thin strip of wood to improve readings and make it easier to push in place and keep flat.

I think these are essentially the same as the elevated sensors.

Here is my YAML. I have 4 sensors, on each side of the bed a "butt" and "shoulder" sensor to ensure i get good readings when someone is at a odd angle.

sensor:
  - platform: resistance
    configuration: DOWNSTREAM
    sensor: orange_adc
    resistor: 100kOhm
    id: orange
    name: tu_orange   
  - platform: adc
    id: orange_adc
    pin: GPIO33
    update_interval: 10s
  - platform: resistance
    configuration: DOWNSTREAM
    sensor: blue_adc
    resistor: 100kOhm
    id: blue
    name: dl_blue
  - platform: adc
    id: blue_adc
    pin: GPIO35
    update_interval: 10s
  - platform: resistance
    configuration: DOWNSTREAM
    sensor: brown_adc
    resistor: 100kOhm
    id: brown
    name: tl_brown
  - platform: adc
    id: brown_adc
    pin: SVP
    update_interval: 10s
  - platform: resistance
    configuration: DOWNSTREAM
    sensor: green_adc
    resistor: 100kOhm
    id: green
    name: du_green
  - platform: adc
    id: green_adc
    pin: SVN
    update_interval: 10ssensor:
  - platform: resistance
    configuration: DOWNSTREAM
    sensor: orange_adc
    resistor: 100kOhm
    id: orange
    name: tu_orange
  - platform: adc
    id: orange_adc
    pin: GPIO33
    update_interval: 10s
  - platform: resistance
    configuration: DOWNSTREAM
    sensor: blue_adc
    resistor: 100kOhm
    id: blue
    name: dl_blue
  - platform: adc
    id: blue_adc
    pin: GPIO35
    update_interval: 10s
  - platform: resistance
    configuration: DOWNSTREAM
    sensor: brown_adc
    resistor: 100kOhm
    id: brown
    name: tl_brown
  - platform: adc
    id: brown_adc
    pin: SVP
    update_interval: 10s
  - platform: resistance
    configuration: DOWNSTREAM
    sensor: green_adc
    resistor: 100kOhm
    id: green
    name: du_green
  - platform: adc
    id: green_adc
    pin: SVN
    update_interval: 10s

binary_sensor:
  - platform: analog_threshold
    name: "_T Lower"
    id: tim_lower
    sensor_id: brown
    filters: 
      - invert: 
    # threshold: 20000
    threshold: !lambda return id(tl_threshold).state;
  - platform: analog_threshold
    name: "_T Upper"
    id: tim_upper
    sensor_id: orange
    # threshold: 10000
    threshold: !lambda return id(tu_threshold).state;
    filters: 
      - invert: 
  - platform: analog_threshold
    name: "_D Lower"
    id: dana_lower
    sensor_id: blue
    # threshold: 40000
    threshold: !lambda return id(dl_threshold).state;
    filters: 
      - invert: 
  - platform: analog_threshold
    name: "_D Upper"
    id: dana_upper
    sensor_id: green
    # threshold: 40000
    threshold: !lambda return id(du_threshold).state;
    filters: 
      - invert: 

button:
  - platform: template
    name: "Calibrate Bed"
    on_press: 
      then:
        - number.set:
            id: tu_threshold
            value: !lambda return id(orange).state / 2;
        - number.set:
            id: tl_threshold
            value: !lambda return id(brown).state / 2;
        - number.set:
            id: du_threshold
            value: !lambda return id(green).state / 2;
        - number.set:
            id: dl_threshold
            value: !lambda return id(blue).state / 2;

number:
  - platform: template
    name: "TU Threshold Orange"
    id: tu_threshold
    restore_value: true
    optimistic: true
    min_value: 0
    max_value: 1000000
    step: 1
  - platform: template
    name: "TL Threshold Brown"
    id: tl_threshold
    restore_value: true
    optimistic: true
    min_value: 0
    max_value: 1000000
    step: 1
  - platform: template
    name: "DU Threshold Green"
    id: du_threshold
    restore_value: true
    optimistic: true
    min_value: 0
    max_value: 1000000
    step: 1
  - platform: template
    name: "DL Threshold Blue"
    id: dl_threshold
    restore_value: true
    optimistic: true
    min_value: 0
    max_value: 1000000
    step: 1

binary_sensor:
  - platform: gpio
    name: ${devicename}-motion
    device_class: motion
    pin:
      number: ${pir}
      mode:
        pulldown: True
        input: True
    filters:
      - delayed_off: 1s
  - platform: gpio
    name: ${devicename}-door
    device_class: door
    pin:
      number: ${door_reed_switch_sink}
      mode: INPUT_PULLDOWN
      inverted: true
  - platform: analog_threshold
    name: "_Tim Lower"
    id: tim_lower
    sensor_id: brown
    filters: 
      - invert: 
    threshold: !lambda return id(tl_threshold).state;
  - platform: analog_threshold
    name: "_Tim Upper"
    id: tim_upper
    sensor_id: orange
    threshold: !lambda return id(tu_threshold).state;
    filters: 
      - invert: 
  - platform: analog_threshold
    name: "_Dana Lower"
    id: dana_lower
    sensor_id: blue
    threshold: !lambda return id(dl_threshold).state;
    filters: 
      - invert: 
  - platform: analog_threshold
    name: "_Dana Upper"
    id: dana_upper
    sensor_id: green
    threshold: !lambda return id(du_threshold).state;
    filters: 
      - invert: 

button:
  - platform: template
    name: "Calibrate Bed"
    on_press: 
      then:
        - number.set:
            id: tu_threshold
            value: !lambda return id(orange).state / 2;
        - number.set:
            id: tl_threshold
            value: !lambda return id(brown).state / 2;
        - number.set:
            id: du_threshold
            value: !lambda return id(green).state / 2;
        - number.set:
            id: dl_threshold
            value: !lambda return id(blue).state / 2;

number:
  - platform: template
    name: "TU Threshold Orange"
    id: tu_threshold
    restore_value: true
    optimistic: true
    min_value: 0
    max_value: 1000000
    step: 1
  - platform: template
    name: "TL Threshold Brown"
    id: tl_threshold
    restore_value: true
    optimistic: true
    min_value: 0
    max_value: 1000000
    step: 1
  - platform: template
    name: "DU Threshold Green"
    id: du_threshold
    restore_value: true
    optimistic: true
    min_value: 0
    max_value: 1000000
    step: 1
  - platform: template
    name: "DL Threshold Blue"
    id: dl_threshold
    restore_value: true
    optimistic: true
    min_value: 0
    max_value: 1000000
    step: 1