Skip to content

Instantly share code, notes, and snippets.

@gretel
Last active May 27, 2025 22:41
Show Gist options
  • Save gretel/477e054e7044be67c8161c4e57118b76 to your computer and use it in GitHub Desktop.
Save gretel/477e054e7044be67c8161c4e57118b76 to your computer and use it in GitHub Desktop.
ESPHome configuration for a 'Seeed MR60BHA2' presence radar that visualizes detected heart rate with an adaptive brightness LED heartbeat effect
---
substitutions:
devicename: 'humanscanner'
upper_devicename: 'HumanScanner'
logger_level: 'INFO'
esphome:
min_version: 2025.5.0
name: "${devicename}"
comment: 'https://esphome.io/components/seeed_mr60bha2.html'
project:
name: 'gretel.crs'
version: '0.25.5.28'
on_boot:
priority: 600
then:
- lambda: |-
// Initialize heartbeat state
id(heartbeat_state) = false;
id(last_beat_time) = 0;
// If no presence on boot, turn off LED
if (!id(presence_sensor).state) {
auto call = id(pixel).turn_off();
call.perform();
}
platformio_options:
board_upload.maximum_size: 4194304
# should be merged soon?
external_components:
- source: github://pr#8209
components: [ seeed_mr60bha2 ]
refresh: 0s
esp32:
board: esp32-c6-devkitc-1
variant: esp32c6
flash_size: 4MB
framework:
type: esp-idf
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_pass
domain: !secret domain
fast_connect: on
ota:
- platform: esphome
password: !secret ota_key
logger:
level: ${logger_level}
api:
reboot_timeout: 0s # disabled
encryption:
key: !secret humanscanner_key # yours, not mine :)
on_client_connected:
- logger.log:
format: "Client %s connected to API with IP %s"
args: ["client_info.c_str()", "client_address.c_str()"]
- light.turn_on:
id: pixel
brightness: 5%
red: 0%
green: 100%
blue: 0%
on_client_disconnected:
- logger.log: "API client disconnected!"
- light.turn_off:
id: pixel
time:
- platform: homeassistant
id: homeassistant_time
i2c:
sda: GPIO22
scl: GPIO23
#scan: true
id: bus_a
uart:
id: uart_bus
baud_rate: 115200
rx_pin: 17
tx_pin: 16
parity: NONE
stop_bits: 1
seeed_mr60bha2:
id: mmwave
globals:
- id: heart_rate_value
type: float
restore_value: no
initial_value: "60.0"
- id: last_beat_time
type: uint32_t
restore_value: no
initial_value: "0"
- id: heartbeat_state
type: bool
restore_value: no
initial_value: "false"
- id: max_brightness
type: float
restore_value: no
initial_value: "0.5" # Starting with 50% max brightness
text_sensor:
- platform: version
name: "Version"
icon: "mdi:exponent-box"
- platform: wifi_info
ip_address:
icon: "mdi:counter"
name: "IP Address"
ssid:
name: "SSID"
# Virtual switch to toggle heartbeat feature
switch:
- platform: template
name: "Heartbeat Visualization"
id: heartbeat_enabled
icon: "mdi:heart-pulse"
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
binary_sensor:
- platform: status
name: "Status"
- platform: seeed_mr60bha2
has_target:
id: presence_sensor
name: "Presence"
on_state:
then:
- if:
condition:
lambda: "return !x;" # When presence is lost
then:
- light.turn_off: pixel
sensor:
- platform: uptime
name: "Uptime"
update_interval: 60s
icon: "mdi:clock-start"
- platform: wifi_signal
name: "WiFi Signal"
update_interval: 60s
icon: "mdi:wifi"
id: wifi_sig
- platform: bh1750
name: "Illuminance"
address: 0x23
update_interval: 5s
id: light_sensor
unit_of_measurement: "lx"
filters:
- median:
on_value:
then:
- lambda: |-
// Adjust max brightness based on illuminance
// Scale UP brightness as light level increases
// Get the current illuminance in lux
float lux = x;
// Define thresholds for scaling - adjusted for indoor lighting
const float MIN_LUX = 0.1; // Very dark room
const float MAX_LUX = 200.0; // Typical indoor lighting
// Define brightness limits - higher minimum for always visible
const float MIN_BRIGHTNESS = 0.25; // Minimum brightness (25%)
const float MAX_BRIGHTNESS = 0.5; // Maximum brightness (50%)
// Handle edge case where lux is extremely low or zero
if (lux < 0.01) {
// Force a default visible brightness for near-zero readings
id(max_brightness) = 0.3; // 30% brightness - always clearly visible
// Log for debugging
// ESP_LOGD("LIGHT", "Near-zero lux detected (%.3f), using default visibility brightness: %.2f", lux, id(max_brightness));
return;
}
// Calculate adaptive brightness
if (lux <= MIN_LUX) {
id(max_brightness) = MIN_BRIGHTNESS; // Dark room -> still visible LED
} else if (lux >= MAX_LUX) {
id(max_brightness) = MAX_BRIGHTNESS; // Bright room -> bright LED
} else {
// Use logarithmic scaling for better human perception
float log_min = log10(MIN_LUX);
float log_max = log10(MAX_LUX);
float log_current = log10(lux);
// Calculate ratio on logarithmic scale
float ratio = (log_current - log_min) / (log_max - log_min);
id(max_brightness) = MIN_BRIGHTNESS + (ratio * (MAX_BRIGHTNESS - MIN_BRIGHTNESS));
}
// Ensure brightness is always within valid range
id(max_brightness) = std::max(MIN_BRIGHTNESS, std::min(MAX_BRIGHTNESS, id(max_brightness)));
// Log brightness level for debugging
// ESP_LOGD("LIGHT", "Lux: %.2f, Brightness: %.2f", lux, id(max_brightness));
- platform: seeed_mr60bha2
breath_rate:
name: "Respiratory rate"
filters:
- median:
heart_rate:
id: heart_rate_sensor
name: "Heart rate"
on_value:
then:
- lambda: |-
// Store heart rate value in global variable
if (x > 20 && x < 200) { // Validate heart rate range
id(heart_rate_value) = x;
}
distance:
name: "Distance to detection"
num_targets:
name: "Target Count"
button:
- platform: restart
name: "Restart"
icon: "mdi:restart"
light:
- platform: esp32_rmt_led_strip
id: pixel
name: "Pixel"
pin: GPIO1
num_leds: 1
rgb_order: GRB
chipset: ws2812
default_transition_length: 0ms
max_refresh_rate: 16ms
internal: true # Hide from Home Assistant
# Heartbeat effect
interval:
- interval: 200ms
then:
- if:
condition:
and:
- binary_sensor.is_on: presence_sensor
- switch.is_on: heartbeat_enabled
then:
- lambda: |-
// Calculate beat interval based on heart rate
float heart_rate = id(heart_rate_value);
uint32_t beat_interval_ms = static_cast<uint32_t>(60000.0 / heart_rate);
uint32_t now = millis();
// Check if it's time for a new heartbeat
if (now - id(last_beat_time) >= beat_interval_ms) {
// Start a new heartbeat
id(last_beat_time) = now;
id(heartbeat_state) = true;
// Pulse the LED bright red with adaptive brightness
auto call = id(pixel).turn_on();
call.set_brightness(id(max_brightness));
call.set_red(255);
call.set_green(0);
call.set_blue(0);
call.perform();
}
// Handle the fading part of the heartbeat
if (id(heartbeat_state)) {
// Calculate time since last beat
uint32_t time_since_beat = now - id(last_beat_time);
// After 20% of the beat interval, start fading
if (time_since_beat >= (beat_interval_ms * 0.2)) {
id(heartbeat_state) = false;
// Fade to dim - 10% of the max brightness
auto fade_call = id(pixel).turn_on();
fade_call.set_brightness(id(max_brightness) * 0.1);
fade_call.set_red(255);
fade_call.set_green(0);
fade_call.set_blue(0);
fade_call.set_transition_length(350);
fade_call.perform();
}
}
else:
- if:
condition:
and:
- binary_sensor.is_on: presence_sensor
- not:
switch.is_on: heartbeat_enabled
then:
# Steady dim red when presence detected but heartbeat disabled
- light.turn_on:
id: pixel
red: 100%
green: 0%
blue: 0%
brightness: 5%
else:
# No presence, LED off
- light.turn_off: pixel
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment