A $25 bed occupancy sensor for Home Assistant
I wanted Home Assistant to know whether the bed was occupied, and by whom, so it could drive lighting, HVAC setbacks, and a few "is anyone home" automations. The commercial option (a Withings Sleep Mat) is around $130 and depends on a cloud account I don't control. The DIY option is four load cells and an ESP32 for about $25, exposes the weight reading directly, and the data never leaves the LAN.
What was happening
Existing presence detection ran on phone Wi-Fi and motion sensors. Phones go to sleep. Motion sensors don't fire when you're already in bed. The "bedroom occupied" automation kept un-tripping in the middle of the night and turning the HVAC back to daytime setpoints.
I wanted a sensor that answered the actual question: is weight on the mattress, and roughly how much.
What I found
Four 50kg half-bridge load cells, one under each leg of the bed, wired through an HX711 24-bit ADC module to an ESP32 DevKit V1. Each load cell is a strain-gauge half-bridge; pairs of opposing cells combine into a full Wheatstone bridge through the HX711. Total bill of materials, sourced cheap:
- 4× 50kg half-bridge load cells: ~$6
- 1× HX711 ADC module: ~$2
- 1× ESP32 DevKit V1: ~$7
- 8× small steel plates (~3"×3"×¼") drilled for M5: scrap bin
- Bolts, wire, heat-shrink: ~$3
Mechanical install is a sandwich at each leg: floor → bottom plate → load cell (arrow stamp pointing down for compression) → top plate → bed leg. Raises the bed about 1.5 inches.
Firmware is ESPHome, which has a first-class hx711 component. The YAML is most of the work:
sensor:
- platform: hx711
name: "Bed Raw"
dout_pin: GPIO16
clk_pin: GPIO17
gain: 128
update_interval: 2s
filters:
- calibrate_linear:
- 0.0 -> 0.0
- 1840000.0 -> 18.14 # 40 lb reference weight
- sliding_window_moving_average:
window_size: 5
send_every: 5
unit_of_measurement: "kg"
binary_sensor:
- platform: template
name: "Bed Occupied"
lambda: |-
return id(bed_raw).state > 5.0;
Calibration: strip the bed completely, note the raw reading, drop a known reference weight (a 40 lb bag of salt is 18.14 kg and behaves well as a single point), note the new reading, plug both into calibrate_linear. Two-point linear fit is enough for ±0.2 kg accuracy.
Per-person classification is a Home Assistant template sensor on top:
template:
- sensor:
- name: "Bed Occupancy State"
state: >
{% set w = states('sensor.bed_weight') | float(0) %}
{% if w < 5 %} empty
{% elif 60 <= w <= 80 %} person_a
{% elif 85 <= w <= 105 %} person_b
{% elif 140 <= w <= 180 %} both
{% else %} unknown
{% endif %}
Each person stands on the empty bed for 30 seconds, you note the steady reading, and those become the band centers with a ±10 kg tolerance. The unknown bucket catches a cat (small), a guest (out of bands), or slow drift.
What I'd do differently
Cheap HX711 boards drift with temperature. The bedroom ranges about 8°C across the year and the zero-point wanders by a few kg as a result. Two fixes I should have built from the start:
- Auto-tare at a known empty moment. A nightly 4am cron that, if presence is independently confirmed elsewhere, re-zeroes the sensor.
- Platform-bed caveat. Beds with a center support pillar carry weight through that pillar, not the legs. You either need a fifth cell under the pillar or a higher-current full-bridge HX711E with all four cells in a true single-bridge configuration. Mine is leg-only and the calibration is slightly off when one of us sits at the foot of the bed.
The pleasant surprise: the sensor caught a bedframe failure I would otherwise have noticed weeks later. One leg's reading went steadily negative over a few days, which turned out to be the floor flexing under that corner. Cheap diagnostics from a cheap sensor.