Hey!
Inspired by all the other Bed Occupancy sensor projects here, I made my own.
Summary
- CNBTR YZC-161B 50 kg load cell, one under each leg of the bed, fitted in 3D printed holder: https://www.thingiverse.com/thing:3563099
- The four load cells are wired as a Wheatstone bridge and connected to a hx711 load cell amplifier
- hx711 connected (via SPI) to WeMos d1 mini (esp8266) with esphome
- Device publish weight once per second to MQTT server running on digitalocean, together with InfluxDB and Grafana.
- Home Assistant subscribe to the MQTT topic for weight, and perform various automations based on that (e.q. turning of light when going to bed, calculating total sleep time, pushing stats each morning via Telegram to phone).
- Grafana instance pushes the nights sleep graph to phone each morning.
3D printed holder
Designed in FreeCAD and ordered online via Treatstock/Thingiverse/West print.
Printed to fit IKEA SULTAN bed legs.
Wiring
Can be improved . The white cable is a 4 wire telephone cable. The cells are connected in a Wheatstone bridge directly, so the cells are connected to the other cells as part of the bridge, and the resulting output/input is just four wires that is connected to hx711.
Hardware
Test setup with load cells and the Wheatstone bridge (green breadboard). Also in photo: WeMos d1 mini, an DHT11 temperature and humidity sensor, the hx11 amplifier.
“Mounted” on bed table next to the bed. White cable is the connection to the bridge/load cells. Will design PCB at one point (I usually use EagleCad + OSHPARK).
esphome yaml:
esphome:
name: bed
platform: ESP8266
board: d1_mini
wifi:
ssid: -
password: -
sensor:
- platform: dht
model: DHT11
pin: D2
temperature:
name: "Bedroom Temperature"
humidity:
name: "Bedroom Humidity"
update_interval: 60s
- platform: hx711
name: "Bed weight"
dout_pin: D0
clk_pin: D1
gain: 128
update_interval: 1s
filters:
- lambda: |-
auto first_mass = 0.0;
auto first_value = 21047304;
auto second_mass = 76;
auto second_value = 18826926;
return map(x, first_value, second_value, first_mass, second_mass);
unit_of_measurement: kg
mqtt:
broker: -
port: 8883
username: -
password: -
ssl_fingerprints:
- <>
# Enable logging
logger:
ota:
Grafana + MQTT + InfluxDB
For visualizing “sleep patterns” as well as for alerting. Once the average weight during a two minutes window goes from > 40 kg to < 40 kg, it will alert and send the graph via Telegram to phone.
Compared to a normal human scale, these load cells have a constant pressure, which make the weight to creep during night, which is something I haven’t adjusted for. But once can see and identify the deep sleep parts and movements and not focus so much on absolute weight at this point.
Also, the dht11 sensor is noisy.
Content of
docker-compose.yaml
running on digitalocean (togheter with a bunch of other things). The reason for running this on digitalocean and not on the raspberry pi where I run Home Assistant is because I can reuse the ssl certificates and I already have a setup with reverse proxy. I also get a more reliable setup where I will still log data even if the raspberry PI is down.
services:
mosquitto:
image: eclipse-mosquitto
ports:
- "8883:8883"
restart: unless-stopped
volumes:
- ./mosquitto.conf:/mosquitto/config/mosquitto.conf
- ./passwd:/mosquitto/config/passwd
- ./cert.pem:/mosquitto/config/cert.pem
- ./chain.pem:/mosquitto/config/chain.pem
- ./privkey.pem:/mosquitto/config/privkey.pem
influxdb:
image: influxdb:1.7
ports:
- 8086:8086
restart: unless-stopped
volumes:
- /root/data/influxdb:/var/lib/influxdb
grafana:
image: grafana/grafana:5.4.3
depends_on:
- influxdb
ports:
- 3000:3000
restart: unless-stopped
volumes:
- /root/data/grafana:/var/lib/grafana
environment:
VIRTUAL_PORT: 3000
VIRTUAL_HOST: -
LETSENCRYPT_HOST: -
LETSENCRYPT_EMAIL: -
mqttbridge:
build: ./mqttbridge
image: nilhcem/mqttbridge
depends_on:
- mosquitto
- influxdb
restart: unless-stopped
volumes:
- ./bridge_config.json:/app/config.json
Sensors
Given the raw weight received every second on MQTT, I convert them into convenient sensors and binary sensors for use in automations.
sensors.yaml
# Forcing update on every new value, even if value is the same as previous value.
# Required to be able to calculate correct variance
- platform: mqtt
name: "forced_updated_bed_weight"
state_topic: "bed/sensor/bed_weight/state"
force_update: true
unit_of_measurement: "kg"
icon: "mdi:scale"
- platform: template
sensors:
bed_weight_cleaned:
friendly_name: "Bed weight cleaned"
value_template: >-
{% if states('sensor.forced_updated_bed_weight')|int >= 0 %}
{{ states('sensor.forced_updated_bed_weight')|int }}
{% else %}
0
{% endif %}
- platform: template
sensors:
persons_in_bed:
friendly_name: "Persons in bed"
entity_id: sensor.bed_weight_cleaned
value_template: >-
{% if states('sensor.bed_weight_cleaned')|int > 110 %}
2
{% elif states('sensor.bed_weight_cleaned')|int > 40 %}
1
{% else %}
0
{% endif %}
- platform: statistics
name: bed_weight_stats
entity_id: sensor.forced_updated_bed_weight
sampling_size: 600
precision: 6
max_age:
minutes: 5
binary_sensors.yaml
- platform: template
sensors:
in_bed:
friendly_name: "Is in bed"
entity_id: sensor.persons_in_bed
value_template: >-
{{ states('sensor.persons_in_bed')|int > 0 }}
# Sensor to determine if I have been in bed for more than one minute.
- platform: template
sensors:
is_steady_in_bed:
friendly_name: "Is steady in bed"
entity_id: binary_sensor.in_bed
delay_on:
minutes: 1
delay_off:
minutes: 1
value_template: >-
{{ is_state('binary_sensor.in_bed', 'on') }}
# Sensor determine if I have been laying still. This is derived from the statistics sensor
# where we determine that if the variance is less than 20/300 during the last 5 minutes (which is the max number of time bed_weight_stats_mean is keeping samples).
# This is something to tweak to identify actual deep sleep patterns.
- platform: template
sensors:
is_bed_weight_change_stable:
friendly_name: "Is bed weight stable"
entity_id: sensor.forced_updated_bed_weight
value_template: >-
{{ state_attr('sensor.bed_weight_stats_mean', 'variance') <= 20/300 }}
Automations
- Turn off lights when going to bed on days before workday.
- Track bed time, sleep time, push notification on going up.
A bit messy to read, enjoy
automations.yaml
#
# Bed weight automations
#
- id: '1555343389279'
alias: Turn all off when in bed on days before work
trigger:
platform: state
entity_id: binary_sensor.is_steady_in_bed
to: 'on'
condition:
- condition: time
after: '19:30'
- condition: time
weekday:
- sun
- mon
- tue
- wed
- thu
action:
- data:
entity_id: scene.all_off
service: scene.turn_on
- data:
entity_id: media_player.spotify
service: media_player.media_pause
- id: '1555343389280'
alias: Store time for being in bed.
trigger:
platform: state
entity_id: binary_sensor.is_steady_in_bed
to: 'on'
action:
- data_template:
entity_id: input_number.being_in_bed_since
value: '{{ as_timestamp(now()) | int }}'
service: input_number.set_value
- id: '1555343389285'
alias: Store time when sleeping in bed.
trigger:
platform: state
entity_id: binary_sensor.is_bed_weight_change_stable
to: 'on'
condition:
- condition: state
entity_id: binary_sensor.is_steady_in_bed
state: 'on'
- condition: template
value_template: "{{ states('input_number.sleeping_in_bed_since')|int == 0 }}"
action:
- data_template:
entity_id: input_number.sleeping_in_bed_since
value: '{{ as_timestamp(now()) | int }}'
service: input_number.set_value
- id: '1555343389286'
alias: Store Accumulated Sleeping Time (chunks)
trigger:
platform: state
entity_id: binary_sensor.is_bed_weight_change_stable
to: 'on'
condition:
- condition: state
entity_id: binary_sensor.is_steady_in_bed
state: 'on'
action:
- data_template:
entity_id: input_number.accumulated_sleeping_time
value: "{{ states('input_number.accumulated_sleeping_time')|int + 5*60 }}"
service: input_number.set_value
- data_template:
entity_id: input_number.accumulated_sleeping_time_on_since
value: '{{ as_timestamp(now()) | int }}'
service: input_number.set_value
- id: '1555343389287'
alias: Store Accumulated Sleeping Time (since last on when going off)
trigger:
platform: state
entity_id: binary_sensor.is_bed_weight_change_stable
to: 'off'
condition:
- condition: state
entity_id: binary_sensor.is_steady_in_bed
state: 'on'
- condition: template
value_template: "{{ states('input_number.accumulated_sleeping_time_on_since')|int > 0 }}"
action:
- data_template:
entity_id: input_number.accumulated_sleeping_time
value: "{{ states('input_number.accumulated_sleeping_time')|int + as_timestamp(now()) - states('input_number.accumulated_sleeping_time_on_since')|int }}"
service: input_number.set_value
- data:
entity_id: input_number.accumulated_sleeping_time_on_since
value: 0
service: input_number.set_value
- id: '1555343389290'
alias: Notify after disengaging the bed.
trigger:
platform: state
entity_id: binary_sensor.is_steady_in_bed
to: 'off'
action:
- service: notify.telegram
data:
title: 'Good morning!'
data_template:
message: "Bed time: {% set bed_since_diff = (as_timestamp(now()) - states('input_number.being_in_bed_since') | int) | int %}{% if bed_since_diff < 60 %}{{ bed_since_diff }} seconds{%- elif bed_since_diff < 3600 %}{{ (bed_since_diff/60)|round(1) }} minutes{%- else %}{{ (bed_since_diff/3600)|round(1) }} hours{%- endif %}\nSleep time: {% set val = states('input_number.sleeping_in_bed_since')|int %}{% if val > 0 %}{% set sleep_since_diff = (as_timestamp(now()) - val) | int %}{% if sleep_since_diff < 60 %}{{ sleep_since_diff }} seconds{%- elif sleep_since_diff < 3600 %}{{ (sleep_since_diff/60)|round(1) }} minutes{%- else %}{{ (sleep_since_diff/3600)|round(1) }} hours{%- endif %}.{%- else %}<no sleep time>{%- endif %}\nAccumulated Sleep time: {% set val = states('input_number.accumulated_sleeping_time')|int %}{% if val > 0 %}{% if val < 60 %}{{ val }} seconds{%- elif val < 3600 %}{{ (val/60)|round(1) }} minutes{%- else %}{{ (val/3600)|round(1) }} hours{%- endif %}.{%- else %}<no accumulated sleep time>{%- endif %}"
- service: input_number.set_value
data:
entity_id: input_number.being_in_bed_since
value: 0
- service: input_number.set_value
data:
entity_id: input_number.sleeping_in_bed_since
value: 0
- service: input_number.set_value
data:
entity_id: input_number.accumulated_sleeping_time
value: 0
- service: input_number.set_value
data:
entity_id: input_number.accumulated_sleeping_time_on_since
value: 0