Cleanup and prepping for bed presence

This commit is contained in:
CeeWeasel
2025-06-29 22:39:39 -04:00
parent 790850f397
commit 20322b2251
14 changed files with 2450 additions and 572 deletions
+80
View File
@@ -0,0 +1,80 @@
substitutions:
sensor_gpio_left: GPIO10
sensor_gpio_right: GPIO20
packages:
diagnostics: !include
file: ../esphome-common/diagnostics.yaml
vars:
boot_gpio: GPIO9
uptime_id: bed_presence_uptime
restart_id: btn_restart
status_id: bed_presence_status
left_bed_sensor: !include
file: sensor.yaml
vars:
sensor_name: Left
sensor_id: left
sensor_gpio: ${sensor_gpio_left}
right_bed_sensor: !include
file: sensor.yaml
vars:
sensor_name: Right
sensor_id: right
sensor_gpio: ${sensor_gpio_right}
binary_sensor:
- platform: template
name: Bed Occupied Both (Fast)
id: bed_occupied_both_fast
device_class: occupancy
icon: mdi:bunk-bed
disabled_by_default: true
lambda: return id(bed_occupied_left_fast).state && id(bed_occupied_right_fast).state;
- platform: template
name: Bed Occupied Either (Fast)
id: bed_occupied_either_fast
device_class: occupancy
icon: mdi:bunk-bed
disabled_by_default: true
lambda: return id(bed_occupied_left_fast).state || id(bed_occupied_right_fast).state;
- platform: template
name: Bed Occupied Both
id: bed_occupied_both
device_class: occupancy
icon: mdi:bunk-bed
lambda: return id(bed_occupied_left).state && id(bed_occupied_right).state;
- platform: template
name: Bed Occupied Either
id: bed_occupied_either
device_class: occupancy
icon: mdi:bunk-bed
lambda: return id(bed_occupied_left).state || id(bed_occupied_right).state;
switch:
- platform: template
name: Full Range
id: full_range
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
entity_category: config
icon: mdi:arrow-expand
select:
- platform: template
name: "Response Speed"
id: response_speed
icon: mdi:speedometer
optimistic: true
restore_value: true
options:
- "Fast"
- "Normal"
- "Slow"
initial_option: "Normal"
entity_category: config
+15
View File
@@ -0,0 +1,15 @@
api:
on_client_connected:
- esp32_ble_tracker.start_scan:
continuous: true
on_client_disconnected:
- esp32_ble_tracker.stop_scan:
bluetooth_proxy:
active: true
esp32_ble_tracker:
id: ble_tracker
scan_parameters:
active: true
continuous: false
+59
View File
@@ -0,0 +1,59 @@
substitutions:
boot_gpio: GPIO9
boot_id: boot_button
status_id: status_sensor
factory_reset_id: factory_reset_button
restart_id: restart_button
wifi_db_id: wifi_signal_db
wifi_percent_id: wifi_signal_percent
uptime_id: uptime_sensor
binary_sensor:
- platform: gpio
pin:
number: ${boot_gpio}
mode: INPUT_PULLUP
inverted: true
id: ${boot_id}
internal: true
name: Boot Button
filters:
- delayed_on: 10s
on_press:
then:
- button.press: ${factory_reset_id}
- platform: status
id: ${status_id}
name: Status
button:
- platform: factory_reset
id: ${factory_reset_id}
internal: true
- platform: restart
name: Restart
id: ${restart_id}
entity_category: diagnostic
sensor:
- platform: wifi_signal # Reports the WiFi signal strength in dB
name: WiFi Signal dB
id: ${wifi_db_id}
update_interval: 60s
- platform: copy # Reports the WiFi signal strength in %
source_id: ${wifi_db_id}
name: WiFi Signal Percent
id: ${wifi_percent_id}
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: '%'
entity_category: diagnostic
device_class: ""
- platform: uptime
type: seconds
id: ${uptime_id}
name: Uptime
+240
View File
@@ -0,0 +1,240 @@
substitutions:
# Calibrate Trigger Percentile
# This controls how the trigger threshold is set during calibration. During calibration, the unoccupied and occupied
# pressure values are determined. This is the percentage of the difference between those values required to trigger
# the "occupied" state.
# - 0.75 Requires 75% of the occupied pressure to trigger (DEFAULT)
# - 0.50 Set trigger exactly in the middle of unoccupied and occupied pressures
# - 0.25 More sensitive, likely to be triggered by partner
trigger_percentile: '0.75'
# Window Averaging Samples
# Add a sliding window moving average to the incoming sensor samples. This specifies the number of samples to average
# over. Each sample is 1/2 second.
# - 1 Don't perform averaging
# - 5 Average over 5 samples (2.5 seconds) (DEFAULT)
# - 10 Average over 10 samples (5 seconds)
averaging_window_samples: '5'
# Fast Sensor Delay
# This controls how long the "Fast" sensor must register "unoccupied" or "occupied" before reporting. Averaging or
# delay is encouraged to prevent frequent false negatives as you shift in bed.
# - 2s Same response as standard sensor
# - 500ms Good starting point for getting speed without too many false negatives
# - 0ms No delay (DEFAULT)
fast_delayed_on: '0ms'
fast_delayed_off: '0ms'
# Standard Sensor Delay
# This controls how long the standard sensor must register "unoccupied" or "occupied" before reporting. Averaging or
# delay is enocuraged to ensure stability.
standard_delayed_on: '0s'
standard_delayed_off: '1s'
# Slow Sensor Delay
# This controls how long the "Slow" sensor must register "unoccupied" or "occupied" before reporting. Averaging or
# delay is encouraged to ensure stability.
slow_delayed_on: '2s'
slow_delayed_off: '5s'
# Reporting Delta
# The sensor must change by this amount before an update is sent (or the reporting_interval_max has been hit)
# - 10.0 Only report very large changes
# - 1.0 Relatively sensitive with much less reporting (DEFAULT)
# - 0.1 Very sensitive, will report frequently
reporting_delta: '1.0'
# Reporting Interval (Max)
# The max amount of time between sensor reports (even if the value hasn't changed)
reporting_interval_max: '180s'
# Linear Calibration Points
# Change the linear calibration points for the FSR sensor. These values correspond with the raw pulses per minute from
# the pulse_counter. Only mess with this if you know what you're doing. In standard mode (Full Range = False), the
# sensor is linearly calibrated between calibrate_0 (0%) and calibreate_100 (100%). When Full Range = True,
# calibrate_0 is set to 0.
calibrate_100: '408000'
calibrate_0: '276000'
binary_sensor:
- platform: template
name: Bed Occupied ${sensor_name} (Fast)
id: bed_occupied_${sensor_id}_fast
device_class: occupancy
icon: mdi:speedometer
disabled_by_default: true
filters:
- delayed_on: ${fast_delayed_on}
- delayed_off: ${fast_delayed_off}
lambda: return id(bed_sensor_${sensor_id}).state > id(val_trigger_${sensor_id}).state;
- platform: template
name: Bed Occupied ${sensor_name} (Normal)
id: bed_occupied_${sensor_id}_normal
device_class: occupancy
icon: mdi:speedometer-medium
disabled_by_default: true
filters:
- delayed_on: ${standard_delayed_on}
- delayed_off: ${standard_delayed_off}
lambda: return id(bed_sensor_${sensor_id}).state > id(val_trigger_${sensor_id}).state;
- platform: template
id: bed_occupied_${sensor_id}_slow
name: Bed Occupied ${sensor_name} (Slow)
device_class: occupancy
icon: mdi:speedometer-slow
disabled_by_default: true
filters:
- delayed_on: ${slow_delayed_on}
- delayed_off: ${slow_delayed_off}
lambda: return id(bed_sensor_${sensor_id}).state > id(val_trigger_${sensor_id}).state;
- platform: template
name: Bed Occupied ${sensor_name}
id: bed_occupied_${sensor_id}
device_class: occupancy
icon: mdi:bed
lambda: |- # Sensor response is user tunable via "Response Speed"
if (id(response_speed).state == "Fast") {
return id(bed_occupied_${sensor_id}_fast).state;
} else if (id(response_speed).state == "Slow") {
return id(bed_occupied_${sensor_id}_slow).state;
} else {
return id(bed_occupied_${sensor_id}_normal).state;
}
sensor:
- platform: pulse_counter
pin: ${sensor_gpio}
name: ${sensor_name} Pressure
id: bed_sensor_${sensor_id}
update_interval: 0.5s
unit_of_measurement: '%'
icon: mdi:gauge
filters:
- lambda: |- # scale pulses/minute to 0%->100%
float cal_0 = id(full_range).state ? 0 : ${calibrate_0};
float val = (x - cal_0) * 100;
float range = ${calibrate_100} - cal_0;
return val/range;
- clamp:
min_value: 0
- sliding_window_moving_average:
window_size: ${averaging_window_samples}
send_every: 1
- or:
- delta: ${reporting_delta} # only send if sensor changes by `reporting_delta` (eliminate sensor noise)
- throttle: ${reporting_interval_max} # but still update every `reporting_interval_max`
- platform: copy
source_id: bed_sensor_${sensor_id}
name: Calibrated ${sensor_name} Pressure
id: bed_sensor_${sensor_id}_calibrated
icon: mdi:scale-balance
disabled_by_default: true
filters:
- lambda: |- # scale between val_unoccupied and val_occupied
float cal_0 = id(val_unoccupied_${sensor_id}).state;
float cal_100 = id(val_occupied_${sensor_id}).state;
float val = (x - cal_0) * 100;
float range = cal_100 - cal_0;
return val/range;
- clamp:
max_value: 100
min_value: 0
number:
- platform: template
name: ${sensor_name} Unoccupied Pressure
id: val_unoccupied_${sensor_id}
icon: mdi:gauge-empty
unit_of_measurement: '%'
entity_category: diagnostic
optimistic: true
restore_value: true
initial_value: 0
min_value: 0
max_value: 120
step: 1.0
mode: box
on_value:
then:
- lambda: |- # Update trigger
id(update_trigger_${sensor_id})->execute();
- platform: template
name: ${sensor_name} Occupied Pressure
id: val_occupied_${sensor_id}
icon: mdi:gauge-full
unit_of_measurement: '%'
entity_category: diagnostic
optimistic: true
restore_value: true
initial_value: 100
min_value: 0
max_value: 120
step: 1.0
mode: box
on_value:
then:
- lambda: |- # Update trigger
id(update_trigger_${sensor_id})->execute();
- platform: template
name: ${sensor_name} Trigger Pressure
id: val_trigger_${sensor_id}
optimistic: true
restore_value: true
initial_value: 50
min_value: 0
max_value: 120
step: 1.0
mode: box
icon: mdi:gauge
unit_of_measurement: '%'
entity_category: config
button:
- platform: template
name: Calibrate ${sensor_name} Unoccupied
id: calibration_${sensor_id}_set_unoccupied
icon: mdi:bed-empty
entity_category: config
on_press:
then:
- number.set:
id: val_unoccupied_${sensor_id}
# round to 2 decimal places
value: !lambda return round(id(bed_sensor_${sensor_id}).state * 100)/100.0;
- platform: template
name: Calibrate ${sensor_name} Occupied
id: calibration_${sensor_id}_set_occupied
icon: mdi:bed
entity_category: config
on_press:
then:
- number.set:
id: val_occupied_${sensor_id}
# round to 2 decimal places
value: !lambda return round(id(bed_sensor_${sensor_id}).state * 100)/100.0;
script:
- id: update_trigger_${sensor_id}
mode: queued
then:
- lambda: |- # Set trigger to x% of difference between min/max
float unoccupied_pressure = id(val_unoccupied_${sensor_id}).state;
float occupied_pressure = id(val_occupied_${sensor_id}).state;
// calculate new trigger value
float trigger_pressure = unoccupied_pressure + ((occupied_pressure - unoccupied_pressure) * float(${trigger_percentile}));
// round to 2 decimal places
trigger_pressure = round(trigger_pressure * 100)/100.0;
// set value
auto call = id(val_trigger_${sensor_id}).make_call();
call.set_value(trigger_pressure);
call.perform();