1136 lines
31 KiB
YAML
1136 lines
31 KiB
YAML
---
|
|
|
|
###############################################################################
|
|
# SuperSensor v2.0 ESPHome configuration
|
|
###############################################################################
|
|
#
|
|
# Copyright (C) 2025 Joshua M. Boniface <joshua@boniface.me>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, version 3.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
#
|
|
###############################################################################
|
|
|
|
esphome:
|
|
name: supersensor
|
|
name_add_mac_suffix: true
|
|
friendly_name: "Supersensor"
|
|
project:
|
|
name: joshuaboniface.supersensor
|
|
version: "2.0"
|
|
on_boot:
|
|
- priority: 600
|
|
then:
|
|
- lambda: |-
|
|
id(supersensor_occupancy).publish_state(false);
|
|
id(pir_presence).publish_state(false);
|
|
id(light_presence).publish_state(false);
|
|
id(radar_presence).publish_state(false);
|
|
- light.turn_on:
|
|
id: output_led
|
|
effect: flash_white
|
|
- priority: -600
|
|
then:
|
|
- wait_until:
|
|
api.connected:
|
|
- delay: 5s
|
|
- if:
|
|
condition:
|
|
switch.is_on: enable_voice_support
|
|
then:
|
|
- logger.log: "Initializing voice assistant on boot"
|
|
- switch.turn_off: voice_support_active
|
|
- delay: 2s
|
|
- switch.turn_on: voice_support_active
|
|
|
|
dashboard_import:
|
|
package_import_url: github://joshuaboniface/supersensor2/supersensor.yaml
|
|
|
|
esp32:
|
|
board: esp32dev
|
|
framework:
|
|
type: esp-idf
|
|
sdkconfig_options:
|
|
CONFIG_ESP32_DEFAULT_CPU_FREQ_240: "y"
|
|
CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ: "240"
|
|
CONFIG_ESP32_DATA_CACHE_64KB: "y"
|
|
CONFIG_ESP32_DATA_CACHE_LINE_64B: "y"
|
|
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
|
|
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ: "240"
|
|
CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
|
|
CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
|
|
CONFIG_SPIRAM_CACHE_WORKAROUND: "y"
|
|
CONFIG_OPTIMIZATION_LEVEL_RELEASE: "y"
|
|
|
|
globals:
|
|
# Defaults to -5 due to heating from the ESP
|
|
- id: temperature_offset
|
|
type: float
|
|
restore_value: true
|
|
initial_value: "-5.0"
|
|
|
|
- id: humidity_offset
|
|
type: float
|
|
restore_value: true
|
|
initial_value: "0.0"
|
|
|
|
- id: pir_hold_time
|
|
type: int
|
|
restore_value: true
|
|
initial_value: "15"
|
|
|
|
- id: light_presence_threshold
|
|
type: int
|
|
restore_value: true
|
|
initial_value: "30"
|
|
|
|
- id: occupancy_detect_mode
|
|
type: int
|
|
restore_value: true
|
|
initial_value: "0"
|
|
|
|
- id: occupancy_clear_mode
|
|
type: int
|
|
restore_value: true
|
|
initial_value: "0"
|
|
|
|
- id: last_api_connected_time
|
|
type: uint32_t
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
script:
|
|
- id: light_off
|
|
then:
|
|
if:
|
|
condition:
|
|
- binary_sensor.is_on: supersensor_occupancy
|
|
- switch.is_on: enable_presence_led
|
|
then:
|
|
- light.turn_on:
|
|
id: output_led
|
|
brightness: 15%
|
|
red: 1
|
|
green: 1
|
|
blue: 1
|
|
transition_length: 1s
|
|
else:
|
|
- light.turn_off:
|
|
id: output_led
|
|
transition_length: 1s
|
|
|
|
- id: pir_handler
|
|
then:
|
|
- lambda: |-
|
|
id(pir_presence).publish_state(true);
|
|
- while:
|
|
condition:
|
|
binary_sensor.is_on: pir_gpio
|
|
then:
|
|
- delay: !lambda 'return(id(pir_hold_time) * 1000);'
|
|
- lambda: |-
|
|
id(pir_presence).publish_state(false);
|
|
|
|
- id: light_handler
|
|
then:
|
|
- lambda: |-
|
|
if (id(tsl2591_lux).state >= id(light_presence_threshold)) {
|
|
id(light_presence).publish_state(true);
|
|
} else {
|
|
id(light_presence).publish_state(false);
|
|
}
|
|
|
|
- id: occupancy_detect_handler
|
|
then:
|
|
- lambda: |-
|
|
ESP_LOGD("occupancy_detect_handler", "Occupancy detect handler triggered");
|
|
|
|
// Get the current values of our presence sensors
|
|
bool pir = id(pir_presence).state;
|
|
bool radar = id(radar_presence).state;
|
|
bool light = id(light_presence).state;
|
|
|
|
// Determine if PIR counts (2nd bit of presence_type)
|
|
int pir_counts = (id(occupancy_detect_mode) & ( 1 << 2 )) >> 2;
|
|
|
|
// Determine if Radar counts (1st bit of presence_type)
|
|
int radar_counts = (id(occupancy_detect_mode) & ( 1 << 1 )) >> 1;
|
|
|
|
// Determine if Light counts (0th bit of presence_type)
|
|
int light_counts = (id(occupancy_detect_mode) & ( 1 << 0 )) >> 0;
|
|
|
|
// Determine our results
|
|
bool new_state = false;
|
|
if (pir_counts & radar_counts & light_counts) {
|
|
// Logical AND of pir & radar & light
|
|
new_state = pir & radar & light;
|
|
} else if (pir_counts & radar_counts) {
|
|
// Logical AND of pir & radar
|
|
new_state = pir & radar;
|
|
} else if (pir_counts & light_counts) {
|
|
// Logical AND of pir & light
|
|
new_state = pir & light;
|
|
} else if (radar_counts & light_counts) {
|
|
// Logical AND of radar & light
|
|
new_state = radar & light;
|
|
} else if (pir_counts) {
|
|
// Only pir
|
|
new_state = pir;
|
|
} else if (radar_counts) {
|
|
// Only radar
|
|
new_state = radar;
|
|
} else if (light_counts) {
|
|
// Only light
|
|
new_state = light;
|
|
}
|
|
|
|
ESP_LOGD("occupancy_detect_handler", "New state: %s", new_state ? "true" : "false");
|
|
|
|
// Force update even if state hasn't changed
|
|
id(supersensor_occupancy).publish_state(new_state);
|
|
|
|
// Add a delayed re-publish to ensure state propagation
|
|
if (new_state) {
|
|
id(supersensor_occupancy).publish_state(new_state);
|
|
}
|
|
|
|
- id: occupancy_clear_handler
|
|
then:
|
|
- lambda: |-
|
|
ESP_LOGD("occupancy_clear_handler", "Occupancy clear handler triggered");
|
|
|
|
// Get the current values of our presence sensors
|
|
bool pir = id(pir_presence).state;
|
|
bool radar = id(radar_presence).state;
|
|
bool light = id(light_presence).state;
|
|
|
|
// Determine if PIR counts (2nd bit of presence_type)
|
|
int pir_counts = (id(occupancy_clear_mode) & ( 1 << 2 )) >> 2;
|
|
|
|
// Determine if Radar counts (1st bit of presence_type)
|
|
int radar_counts = (id(occupancy_clear_mode) & ( 1 << 1 )) >> 1;
|
|
|
|
// Determine if Light counts (0th bit of presence_type)
|
|
int light_counts = (id(occupancy_clear_mode) & ( 1 << 0 )) >> 0;
|
|
|
|
// Determine our results
|
|
bool new_state = false;
|
|
if (pir_counts & radar_counts & light_counts) {
|
|
// Logical AND of pir & radar & light
|
|
new_state = pir & radar & light;
|
|
} else if (pir_counts & radar_counts) {
|
|
// Logical AND of pir & radar
|
|
new_state = pir & radar;
|
|
} else if (pir_counts & light_counts) {
|
|
// Logical AND of pir & light
|
|
new_state = pir & light;
|
|
} else if (radar_counts & light_counts) {
|
|
// Logical AND of radar & light
|
|
new_state = radar & light;
|
|
} else if (pir_counts) {
|
|
// Only pir
|
|
new_state = pir;
|
|
} else if (radar_counts) {
|
|
// Only radar
|
|
new_state = radar;
|
|
} else if (light_counts) {
|
|
// Only light
|
|
new_state = light;
|
|
}
|
|
|
|
ESP_LOGD("occupancy_clear_handler", "New state: %s", new_state ? "true" : "false");
|
|
|
|
// Force update even if state hasn't changed
|
|
id(supersensor_occupancy).publish_state(new_state);
|
|
|
|
// Add a delayed re-publish to ensure state propagation
|
|
if (!new_state) {
|
|
id(supersensor_occupancy).publish_state(new_state);
|
|
}
|
|
|
|
preferences:
|
|
flash_write_interval: 15sec
|
|
|
|
logger:
|
|
level: INFO
|
|
baud_rate: 115200
|
|
|
|
api:
|
|
reboot_timeout: 15min
|
|
services:
|
|
- service: restart_voice_assistant
|
|
then:
|
|
- logger.log: "Manually restarting voice assistant"
|
|
- voice_assistant.stop:
|
|
- delay: 2s
|
|
- if:
|
|
condition:
|
|
switch.is_on: enable_voice_support
|
|
then:
|
|
- switch.turn_off: voice_support_active
|
|
- delay: 1s
|
|
- switch.turn_on: voice_support_active
|
|
|
|
ota:
|
|
platform: esphome
|
|
|
|
web_server:
|
|
port: 80
|
|
|
|
captive_portal:
|
|
|
|
mdns:
|
|
disabled: false
|
|
|
|
wifi:
|
|
ap: {}
|
|
domain: ""
|
|
output_power: 8.5dB
|
|
reboot_timeout: 15min
|
|
power_save_mode: none
|
|
|
|
time:
|
|
- platform: homeassistant
|
|
id: homeassistant_time
|
|
on_time_sync:
|
|
then:
|
|
- logger.log: "Time synchronized with Home Assistant"
|
|
|
|
uart:
|
|
id: ld2410_uart
|
|
rx_pin: GPIO19
|
|
tx_pin: GPIO18
|
|
baud_rate: 256000
|
|
data_bits: 8
|
|
stop_bits: 1
|
|
parity: NONE
|
|
|
|
i2c:
|
|
sda: GPIO27
|
|
scl: GPIO26
|
|
scan: true
|
|
|
|
i2s_audio:
|
|
i2s_lrclk_pin: GPIO17 # WS
|
|
i2s_bclk_pin: GPIO16 # SCK
|
|
|
|
microphone:
|
|
- platform: i2s_audio
|
|
id: mic
|
|
adc_type: external
|
|
i2s_din_pin: GPIO4 # SD
|
|
pdm: false
|
|
|
|
interval:
|
|
- interval: 5s
|
|
then:
|
|
- if:
|
|
condition:
|
|
and:
|
|
- switch.is_on: enable_voice_support
|
|
- switch.is_on: voice_support_active
|
|
- not: voice_assistant.is_running
|
|
then:
|
|
- logger.log: "Voice assistant not running but should be; restarting"
|
|
- voice_assistant.start_continuous:
|
|
|
|
# Regular state reporting to HASS
|
|
- interval: 30s
|
|
then:
|
|
- lambda: |-
|
|
bool current_state = id(supersensor_occupancy).state;
|
|
ESP_LOGD("state_reporter", "Republishing occupancy state: %s", current_state ? "true" : "false");
|
|
id(supersensor_occupancy).publish_state(current_state);
|
|
|
|
# API watchdog every 5 minutes
|
|
- interval: 60s
|
|
then:
|
|
- lambda: |-
|
|
if (api::global_api_server->is_connected()) {
|
|
id(last_api_connected_time) = millis();
|
|
} else if (millis() - id(last_api_connected_time) > 300000) {
|
|
ESP_LOGE("api_watchdog", "API disconnected for too long, rebooting...");
|
|
App.safe_reboot();
|
|
}
|
|
|
|
micro_wake_word:
|
|
models:
|
|
- model: hey_jarvis
|
|
on_wake_word_detected:
|
|
then:
|
|
- voice_assistant.start:
|
|
wake_word: !lambda return wake_word;
|
|
|
|
voice_assistant:
|
|
microphone: mic
|
|
use_wake_word: false
|
|
noise_suppression_level: 3
|
|
auto_gain: 31dBFS
|
|
volume_multiplier: 8.0
|
|
id: assist
|
|
on_error:
|
|
- logger.log: "voice error"
|
|
- if:
|
|
condition:
|
|
and:
|
|
- switch.is_on: voice_support_active
|
|
- not: voice_assistant.is_running
|
|
then:
|
|
- voice_assistant.start_continuous:
|
|
on_end:
|
|
- logger.log: "voice ended"
|
|
- if:
|
|
condition:
|
|
and:
|
|
- switch.is_on: voice_support_active
|
|
- not: voice_assistant.is_running
|
|
then:
|
|
- voice_assistant.start_continuous:
|
|
on_client_connected:
|
|
- light.turn_off:
|
|
id: output_led
|
|
transition_length: 2s
|
|
- script.execute: light_off
|
|
- lambda: |-
|
|
id(voice_support_active).publish_state(true);
|
|
on_client_disconnected:
|
|
- light.turn_on:
|
|
id: output_led
|
|
effect: flash_white
|
|
on_wake_word_detected:
|
|
- light.turn_on:
|
|
id: output_led
|
|
brightness: 100%
|
|
red: 0
|
|
green: 0
|
|
blue: 1
|
|
on_listening:
|
|
- light.turn_on:
|
|
id: output_led
|
|
brightness: 100%
|
|
red: 0
|
|
green: 0
|
|
blue: 1
|
|
on_stt_vad_end:
|
|
- light.turn_on:
|
|
id: output_led
|
|
brightness: 75%
|
|
red: 0
|
|
green: 1
|
|
blue: 1
|
|
on_stt_end:
|
|
- script.execute: light_off
|
|
on_tts_start:
|
|
- if:
|
|
condition:
|
|
- lambda: |-
|
|
ESP_LOGI("tts_response", "%s", x.c_str());
|
|
return x.rfind("Sorry", 0) == 0;
|
|
then:
|
|
- logger.log: "Command failed!"
|
|
- light.turn_on:
|
|
id: output_led
|
|
effect: hold
|
|
brightness: 100%
|
|
red: 1
|
|
green: 0
|
|
blue: 0
|
|
else:
|
|
- logger.log: "Command successful!"
|
|
- light.turn_on:
|
|
id: output_led
|
|
effect: hold
|
|
brightness: 100%
|
|
red: 0
|
|
green: 1
|
|
blue: 0
|
|
|
|
light:
|
|
- platform: rgb
|
|
id: output_led
|
|
red: rgb_r
|
|
green: rgb_g
|
|
blue: rgb_b
|
|
default_transition_length: 0.15s
|
|
flash_transition_length: 0.15s
|
|
effects:
|
|
- strobe:
|
|
name: flash_white
|
|
colors:
|
|
- state: true
|
|
brightness: 15%
|
|
red: 100%
|
|
green: 90%
|
|
blue: 90%
|
|
duration: 0.5s
|
|
- state: false
|
|
duration: 0.5s
|
|
- automation:
|
|
name: hold
|
|
sequence:
|
|
- delay: 5s
|
|
- script.execute: light_off
|
|
|
|
output:
|
|
- platform: ledc
|
|
id: rgb_r
|
|
pin: GPIO23
|
|
- platform: ledc
|
|
id: rgb_g
|
|
pin: GPIO22
|
|
- platform: ledc
|
|
id: rgb_b
|
|
pin: GPIO21
|
|
|
|
ld2410:
|
|
id: ld2410_radar
|
|
uart_id: ld2410_uart
|
|
|
|
sensor:
|
|
- platform: sgp30
|
|
eco2:
|
|
name: "SGP30 eCO2"
|
|
id: sgp30_eco2
|
|
accuracy_decimals: 1
|
|
filters:
|
|
- sliding_window_moving_average:
|
|
window_size: 20
|
|
send_every: 1
|
|
tvoc:
|
|
name: "SGP30 TVOC"
|
|
id: sgp30_tvoc
|
|
accuracy_decimals: 1
|
|
filters:
|
|
- sliding_window_moving_average:
|
|
window_size: 20
|
|
send_every: 1
|
|
eco2_baseline:
|
|
name: "SGP30 Baseline eCO2"
|
|
id: sgp30_baseline_ec02
|
|
tvoc_baseline:
|
|
name: "SGP30 Baseline TVOC"
|
|
id: sgp30_baseline_tvoc
|
|
compensation:
|
|
temperature_source: sht45_temperature
|
|
humidity_source: sht45_humidity
|
|
store_baseline: yes
|
|
update_interval: 15s
|
|
|
|
- platform: sht4x
|
|
temperature:
|
|
name: "SHT45 Temperature"
|
|
id: sht45_temperature
|
|
accuracy_decimals: 1
|
|
filters:
|
|
- offset: !lambda return id(temperature_offset);
|
|
- sliding_window_moving_average:
|
|
window_size: 20
|
|
send_every: 1
|
|
humidity:
|
|
name: "SHT45 Relative Humidity"
|
|
id: sht45_humidity
|
|
accuracy_decimals: 1
|
|
filters:
|
|
- offset: !lambda return id(humidity_offset);
|
|
- sliding_window_moving_average:
|
|
window_size: 20
|
|
send_every: 1
|
|
heater_max_duty: 0.0
|
|
update_interval: 15s
|
|
|
|
- platform: absolute_humidity
|
|
name: "SHT45 Absolute Humidity"
|
|
temperature: sht45_temperature
|
|
humidity: sht45_humidity
|
|
id: sht45_absolute_humidity
|
|
|
|
# Dew Point
|
|
- platform: template
|
|
name: "SHT45 Dew Point"
|
|
id: sht45_dew_point
|
|
unit_of_measurement: "°C"
|
|
lambda: |-
|
|
float temp = id(sht45_temperature).state;
|
|
float rh = id(sht45_humidity).state;
|
|
if (isnan(temp) || isnan(rh)) return NAN;
|
|
float a = 17.27, b = 237.7;
|
|
float alpha = ((a * temp) / (b + temp)) + log(rh / 100.0);
|
|
return (b * alpha) / (a - alpha);
|
|
update_interval: 15s
|
|
|
|
# IAQ Index (1-5, 5=Excellent)
|
|
- platform: template
|
|
name: "IAQ Index"
|
|
id: iaq_index
|
|
lambda: |-
|
|
int tvoc = id(sgp30_tvoc).state;
|
|
int eco2 = id(sgp30_eco2).state;
|
|
if (tvoc > 2200 || eco2 > 2000) return 1; // Unhealthy
|
|
if (tvoc > 660 || eco2 > 1200) return 2; // Poor
|
|
if (tvoc > 220 || eco2 > 800) return 3; // Moderate
|
|
if (tvoc > 65 || eco2 > 500) return 4; // Good
|
|
return 5; // Excellent
|
|
update_interval: 15s
|
|
|
|
# Room Health Score (1-4, 4=Optimal)
|
|
- platform: template
|
|
name: "Room Health Score"
|
|
id: room_health
|
|
lambda: |-
|
|
float temp = id(sht45_temperature).state;
|
|
float rh = id(sht45_humidity).state;
|
|
int iaq = id(iaq_index).state;
|
|
|
|
bool temp_ok = (temp >= 18 && temp <= 24);
|
|
bool hum_ok = (rh >= 40 && rh <= 60);
|
|
bool iaq_ok = (iaq >= 4);
|
|
|
|
int conditions_met = 0;
|
|
if (temp_ok) conditions_met++;
|
|
if (hum_ok) conditions_met++;
|
|
if (iaq_ok) conditions_met++;
|
|
|
|
if (iaq_ok && temp_ok && hum_ok) {
|
|
return 4; // Optimal: All conditions met and IAQ is excellent/good
|
|
} else if (iaq >= 3 && conditions_met >= 2) {
|
|
return 3; // Fair: IAQ is moderate and at least 2 conditions met
|
|
} else if (iaq >= 2 && conditions_met >= 1) {
|
|
return 2; // Poor: IAQ is poor and at least 1 condition met
|
|
} else {
|
|
return 1; // Bad: All conditions failed or IAQ is unhealthy
|
|
}
|
|
update_interval: 15s
|
|
|
|
- platform: tsl2591
|
|
address: 0x29
|
|
update_interval: 1s
|
|
integration_time: 200ms
|
|
power_save_mode: no
|
|
gain: auto
|
|
device_factor: 53
|
|
glass_attenuation_factor: 7.7
|
|
visible:
|
|
name: "TSL2591 Raw Visible"
|
|
infrared:
|
|
name: "TSL2591 Raw Infrared"
|
|
full_spectrum:
|
|
name: "TSL2591 Raw Full Spectrum"
|
|
calculated_lux:
|
|
id: tsl2591_lux
|
|
name: "TSL2591 Illumination"
|
|
unit_of_measurement: lx
|
|
accuracy_decimals: 1
|
|
on_value:
|
|
- script.execute: light_handler
|
|
actual_gain:
|
|
id: "actual_gain"
|
|
name: "TSL2591 Gain"
|
|
|
|
- platform: ld2410
|
|
ld2410_id: ld2410_radar
|
|
moving_distance:
|
|
name: "LD2410C Moving Distance"
|
|
id: moving_distance
|
|
icon: mdi:signal-distance-variant
|
|
still_distance:
|
|
name: "LD2410C Still Distance"
|
|
id: still_distance
|
|
icon: mdi:signal-distance-variant
|
|
moving_energy:
|
|
name: "LD2410C Move Energy"
|
|
icon: mdi:flash
|
|
still_energy:
|
|
name: "LD2410C Still Energy"
|
|
icon: mdi:flash
|
|
detection_distance:
|
|
name: "LD2410C Presence Distance"
|
|
icon: mdi:signal-distance-variant
|
|
|
|
- platform: uptime
|
|
name: "ESP32 Uptime"
|
|
icon: mdi:clock-alert
|
|
update_interval: 5s
|
|
entity_category: diagnostic
|
|
|
|
- platform: wifi_signal
|
|
name: "WiFi RSSI"
|
|
icon: mdi:wifi-strength-2
|
|
update_interval: 5s
|
|
entity_category: diagnostic
|
|
|
|
- platform: internal_temperature
|
|
name: "ESP32 Temperature"
|
|
icon: mdi:thermometer
|
|
unit_of_measurement: °C
|
|
device_class: TEMPERATURE
|
|
update_interval: 5s
|
|
entity_category: diagnostic
|
|
|
|
- platform: template
|
|
name: "ESP32 CPU Frequency"
|
|
icon: mdi:cpu-32-bit
|
|
accuracy_decimals: 1
|
|
unit_of_measurement: MHz
|
|
update_interval: 5s
|
|
lambda: |-
|
|
return ets_get_cpu_frequency();
|
|
entity_category: diagnostic
|
|
|
|
- platform: template
|
|
name: "ESP32 Free Memory"
|
|
icon: mdi:memory
|
|
unit_of_measurement: 'kB'
|
|
state_class: measurement
|
|
update_interval: 5s
|
|
lambda: |-
|
|
return heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024;
|
|
entity_category: diagnostic
|
|
|
|
binary_sensor:
|
|
- platform: template
|
|
name: "SuperSensor Occupancy"
|
|
id: supersensor_occupancy
|
|
device_class: occupancy
|
|
on_press:
|
|
- script.execute: light_off
|
|
on_release:
|
|
- script.execute: light_off
|
|
|
|
- platform: gpio
|
|
name: "PIR GPIO"
|
|
id: pir_gpio
|
|
pin: GPIO32
|
|
internal: false
|
|
device_class: motion
|
|
on_press:
|
|
- script.stop: pir_handler
|
|
- script.execute: pir_handler
|
|
|
|
- platform: template
|
|
name: "PIR Presence"
|
|
id: pir_presence
|
|
device_class: motion
|
|
on_press:
|
|
- script.execute: occupancy_detect_handler
|
|
on_release:
|
|
- script.execute: occupancy_clear_handler
|
|
|
|
- platform: template
|
|
name: "Light Presence"
|
|
id: light_presence
|
|
device_class: motion
|
|
on_press:
|
|
- script.execute: occupancy_detect_handler
|
|
on_release:
|
|
- script.execute: occupancy_clear_handler
|
|
|
|
- platform: ld2410
|
|
ld2410_id: ld2410_radar
|
|
has_target:
|
|
name: "LD2410C Presence"
|
|
id: radar_presence
|
|
icon: mdi:motion-sensor
|
|
device_class: motion
|
|
on_press:
|
|
- script.execute: occupancy_detect_handler
|
|
on_release:
|
|
- script.execute: occupancy_clear_handler
|
|
has_moving_target:
|
|
name: "LD2410C Moving Target"
|
|
has_still_target:
|
|
name: "LD2410C Still Target"
|
|
|
|
button:
|
|
- platform: ld2410
|
|
restart:
|
|
name: "LD2410C Restart"
|
|
icon: mdi:power-cycle
|
|
entity_category: diagnostic
|
|
factory_reset:
|
|
name: "LD2410C Factory Reset"
|
|
icon: mdi:restart-alert
|
|
entity_category: diagnostic
|
|
|
|
- platform: restart
|
|
name: "ESP32 Restart"
|
|
icon: mdi:power-cycle
|
|
entity_category: diagnostic
|
|
|
|
- platform: factory_reset
|
|
name: "ESP32 Factory Reset"
|
|
icon: mdi:restart-alert
|
|
entity_category: diagnostic
|
|
|
|
switch:
|
|
# Global enable/disable for voice support
|
|
- platform: template
|
|
name: "Enable Voice Support"
|
|
icon: mdi:account-voice
|
|
id: enable_voice_support
|
|
optimistic: true
|
|
restore_mode: RESTORE_DEFAULT_OFF
|
|
on_turn_on:
|
|
- switch.turn_on: voice_support_active
|
|
on_turn_off:
|
|
- switch.turn_off: voice_support_active
|
|
|
|
# Active voice support flag/switch
|
|
- platform: template
|
|
name: "Voice Support Active"
|
|
icon: mdi:account-voice
|
|
id: voice_support_active
|
|
optimistic: true
|
|
restore_mode: ALWAYS_OFF
|
|
entity_category: config
|
|
on_turn_on:
|
|
- lambda: id(assist).set_use_wake_word(true);
|
|
- voice_assistant.stop:
|
|
- delay: 1s
|
|
- voice_assistant.start_continuous:
|
|
on_turn_off:
|
|
- voice_assistant.stop:
|
|
- lambda: id(assist).set_use_wake_word(false);
|
|
|
|
# Global enable/disable for presence LED
|
|
- platform: template
|
|
name: "Enable Presence LED"
|
|
icon: mdi:lightbulb-alert
|
|
id: enable_presence_led
|
|
optimistic: true
|
|
restore_mode: RESTORE_DEFAULT_ON
|
|
on_turn_on:
|
|
- script.execute: light_off
|
|
on_turn_off:
|
|
- script.execute: light_off
|
|
|
|
- platform: ld2410
|
|
engineering_mode:
|
|
name: "LD2410C Engineering Mode"
|
|
entity_category: diagnostic
|
|
bluetooth:
|
|
name: "LD2410C Bluetooth"
|
|
entity_category: diagnostic
|
|
|
|
number:
|
|
# Temperature offset:
|
|
# A calibration from -10 to +5 for the temperature sensor
|
|
- platform: template
|
|
name: "Temperature Offset"
|
|
id: temperature_offset_setter
|
|
min_value: -10
|
|
max_value: 5
|
|
step: 0.1
|
|
lambda: |-
|
|
return id(temperature_offset);
|
|
set_action:
|
|
then:
|
|
- globals.set:
|
|
id: temperature_offset
|
|
value: !lambda 'return float(x);'
|
|
|
|
# Humidity offset:
|
|
# A calibration from -10 to +10 for the humidity sensor
|
|
- platform: template
|
|
name: "Humidity Offset"
|
|
id: humidity_offset_setter
|
|
min_value: -10
|
|
max_value: 10
|
|
step: 0.1
|
|
lambda: |-
|
|
return id(humidity_offset);
|
|
set_action:
|
|
then:
|
|
- globals.set:
|
|
id: humidity_offset
|
|
value: !lambda 'return float(x);'
|
|
|
|
# PIR Hold Time:
|
|
# The number of seconds after motion detection for the PIR sensor to remain held on
|
|
- platform: template
|
|
name: "PIR Hold Time"
|
|
id: pir_hold_time_setter
|
|
min_value: 0
|
|
max_value: 60
|
|
step: 5
|
|
lambda: |-
|
|
return id(pir_hold_time);
|
|
set_action:
|
|
then:
|
|
- globals.set:
|
|
id: pir_hold_time
|
|
value: !lambda 'return int(x);'
|
|
|
|
# Light Presence Threshold
|
|
# The minimum Lux value to consider presence based on the ambient light level
|
|
- platform: template
|
|
name: "Light Presence Threshold"
|
|
id: light_presence_threshold_setter
|
|
min_value: 0
|
|
max_value: 200
|
|
step: 5
|
|
lambda: |-
|
|
return id(light_presence_threshold);
|
|
set_action:
|
|
then:
|
|
- globals.set:
|
|
id: light_presence_threshold
|
|
value: !lambda 'return int(x);'
|
|
|
|
- platform: ld2410
|
|
timeout:
|
|
name: "LD2410C Timeout"
|
|
light_threshold:
|
|
name: "LD2410C Light Threshold"
|
|
max_move_distance_gate:
|
|
name: "LD2410C Max Move Distance Gate"
|
|
max_still_distance_gate:
|
|
name: "LD2410C Max Still Distance Gate"
|
|
g0:
|
|
move_threshold:
|
|
name: "LD2410C Gate0 Move Threshold"
|
|
still_threshold:
|
|
name: "LD2410C Gate0 Still Threshold"
|
|
g1:
|
|
move_threshold:
|
|
name: "LD2410C Gate1 Move Threshold"
|
|
still_threshold:
|
|
name: "LD2410C Gate1 Still Threshold"
|
|
g2:
|
|
move_threshold:
|
|
name: "LD2410C Gate2 Move Threshold"
|
|
still_threshold:
|
|
name: "LD2410C Gate2 Still Threshold"
|
|
g3:
|
|
move_threshold:
|
|
name: "LD2410C Gate3 Move Threshold"
|
|
still_threshold:
|
|
name: "LD2410C Gate3 Still Threshold"
|
|
g4:
|
|
move_threshold:
|
|
name: "LD2410C Gate4 Move Threshold"
|
|
still_threshold:
|
|
name: "LD2410C Gate4 Still Threshold"
|
|
g5:
|
|
move_threshold:
|
|
name: "LD2410C Gate5 Move Threshold"
|
|
still_threshold:
|
|
name: "LD2410C Gate5 Still Threshold"
|
|
g6:
|
|
move_threshold:
|
|
name: "LD2410C Gate6 Move Threshold"
|
|
still_threshold:
|
|
name: "LD2410C Gate6 Still Threshold"
|
|
g7:
|
|
move_threshold:
|
|
name: "LD2410C Gate7 Move Threshold"
|
|
still_threshold:
|
|
name: "LD2410C Gate7 Still Threshold"
|
|
g8:
|
|
move_threshold:
|
|
name: "LD2410C Gate8 Move Threshold"
|
|
still_threshold:
|
|
name: "LD2410C Gate8 Still Threshold"
|
|
|
|
select:
|
|
|
|
# Occupancy Detect Mode:
|
|
# This selector defines the detection mode for the integrated occupancy sensor. Depending on the
|
|
# selected option, only the given sensor(s) will be used to judge when occupancy begins (i.e.
|
|
# when the given sensor(s) detect, occupancy detects).
|
|
# * PIR + Radar + Light:
|
|
# All 3 sensors reporting detection simultaneously will begin occupancy
|
|
# * PIR + Radar
|
|
# Both PIR and Radar sensors reporting detection simultaneously will begin occupancy
|
|
# * PIR + Light
|
|
# Both PIR and Light sensors reporting detection simultaneously will begin occupancy
|
|
# * Radar + Light
|
|
# Both Radar and Light sensors reporting detection simultaneously will begin occupancy
|
|
# * PIR Only
|
|
# PIR sensor reporting detection will begin occupancy
|
|
# * Radar Only
|
|
# Radar sensor reporting detection will begin occupancy
|
|
# * Light Only
|
|
# Light sensor reporting detection will begin occupancy
|
|
# * None
|
|
# No sensors will begin occupancy and the integrated occupancy functionality is disabled
|
|
# Values are reported as integers using bitwise logic:
|
|
# Bit 2: PIR
|
|
# Bit 1: Radar
|
|
# Bit 0: Light
|
|
- platform: template
|
|
name: "Occupancy Detect Mode"
|
|
id: occupancy_detect_mode_setter
|
|
options:
|
|
- "PIR + Radar + Light" # 111 = 7
|
|
- "PIR + Radar" # 110 = 6
|
|
- "PIR + Light" # 101 = 5
|
|
- "Radar + Light" # 011 = 3
|
|
- "PIR Only" # 100 = 4
|
|
- "Radar Only" # 010 = 2
|
|
- "Light Only" # 001 = 1
|
|
- "None" # 000 = 0
|
|
initial_option: "None"
|
|
optimistic: true
|
|
restore_value: true
|
|
set_action:
|
|
- globals.set:
|
|
id: occupancy_detect_mode
|
|
value: !lambda |-
|
|
ESP_LOGD("occupancy_detect_mode_setter", x.c_str());
|
|
if (x == "PIR + Radar + Light") {
|
|
return 7;
|
|
} else if (x == "PIR + Radar") {
|
|
return 6;
|
|
} else if (x == "PIR + Light") {
|
|
return 5;
|
|
} else if (x == "Radar + Light") {
|
|
return 3;
|
|
} else if (x == "PIR Only") {
|
|
return 4;
|
|
} else if (x == "Radar Only") {
|
|
return 2;
|
|
} else if (x == "Light Only") {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
# Occupancy Clear Mode:
|
|
# This selector defines the clear mode for the integrated occupancy sensor. Depending on the
|
|
# selected option, only the given sensor(s) will be used to judge when occupancy ends (i.e.
|
|
# when the given sensor(s) clear, occupancy clears).
|
|
# * PIR + Radar + Light:
|
|
# Any of the 3 sensors clearing will end occupancy
|
|
# * PIR + Radar:
|
|
# Either of the PIR or Radar sensors clearing will end occupancy
|
|
# * PIR + Light:
|
|
# Either of the PIR or Light sensors clearing will end occupancy
|
|
# * Radar + Light:
|
|
# Either of the Radar or Light sensors clearing will end occupancy
|
|
# * PIR Only
|
|
# PIR sensor clearing will end occupancy
|
|
# * Radar Only
|
|
# Radar sensor clearing will end occupancy
|
|
# * Light Only
|
|
# Light sensor clearing will end occupancy
|
|
# * None
|
|
# No sensors will end occupancy; state will persist indefinitely once triggered
|
|
# Values are reported as integers using bitwise logic:
|
|
# Bit 0: PIR
|
|
# Bit 1: Radar
|
|
# Bit 2: Light
|
|
- platform: template
|
|
name: "Occupancy Clear Mode"
|
|
id: occupancy_clear_mode_setter
|
|
options:
|
|
- "PIR + Radar + Light" # 111 = 7
|
|
- "PIR + Radar" # 110 = 6
|
|
- "PIR + Light" # 101 = 5
|
|
- "Radar + Light" # 011 = 3
|
|
- "PIR Only" # 100 = 4
|
|
- "Radar Only" # 010 = 2
|
|
- "Light Only" # 001 = 1
|
|
- "None" # 000 = 0
|
|
initial_option: "None"
|
|
optimistic: true
|
|
restore_value: true
|
|
set_action:
|
|
- globals.set:
|
|
id: occupancy_clear_mode
|
|
value: !lambda |-
|
|
ESP_LOGD("occupancy_detect_mode_setter", x.c_str());
|
|
if (x == "PIR + Radar + Light") {
|
|
return 7;
|
|
} else if (x == "PIR + Radar") {
|
|
return 6;
|
|
} else if (x == "PIR + Light") {
|
|
return 5;
|
|
} else if (x == "Radar + Light") {
|
|
return 3;
|
|
} else if (x == "PIR Only") {
|
|
return 4;
|
|
} else if (x == "Radar Only") {
|
|
return 2;
|
|
} else if (x == "Light Only") {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
- platform: ld2410
|
|
distance_resolution:
|
|
name: "LD2410C Distance Resolution"
|
|
|
|
text_sensor:
|
|
- platform: wifi_info
|
|
ip_address:
|
|
name: "WiFi IP Address"
|
|
ssid:
|
|
name: "WiFi SSID"
|
|
bssid:
|
|
name: "WiFi BSSID"
|
|
mac_address:
|
|
name: "WiFi MAC Address"
|
|
|
|
- platform: ld2410
|
|
version:
|
|
name: "LD2410C Firmware Version"
|
|
mac_address:
|
|
name: "LD2410C MAC Address"
|
|
|
|
# VOC Level
|
|
- platform: template
|
|
name: "VOC Level"
|
|
lambda: |-
|
|
int tvoc = id(sgp30_tvoc).state;
|
|
if (tvoc < 65) return {"Excellent"};
|
|
if (tvoc < 220) return {"Good"};
|
|
if (tvoc < 660) return {"Moderate"};
|
|
if (tvoc < 2200) return {"Poor"};
|
|
return {"Unhealthy"};
|
|
update_interval: 15s
|
|
|
|
# CO2 Level
|
|
- platform: template
|
|
name: "CO2 Level"
|
|
lambda: |-
|
|
int eco2 = id(sgp30_eco2).state;
|
|
if (eco2 < 500) return {"Excellent"};
|
|
if (eco2 < 800) return {"Good"};
|
|
if (eco2 < 1200) return {"Moderate"};
|
|
if (eco2 < 2000) return {"Poor"};
|
|
return {"Unhealthy"};
|
|
update_interval: 15s
|
|
|
|
# IAQ Classification
|
|
- platform: template
|
|
name: "IAQ Classification"
|
|
lambda: |-
|
|
int iaq = id(iaq_index).state;
|
|
if (iaq == 5) return {"Excellent"};
|
|
if (iaq == 4) return {"Good"};
|
|
if (iaq == 3) return {"Moderate"};
|
|
if (iaq == 2) return {"Poor"};
|
|
return {"Unhealthy"};
|
|
update_interval: 15s
|
|
|
|
# Room Health
|
|
- platform: template
|
|
name: "Room Health"
|
|
lambda: |-
|
|
int score = id(room_health).state;
|
|
if (score == 4) return {"Optimal"};
|
|
if (score == 3) return {"Fair"};
|
|
if (score == 2) return {"Poor"};
|
|
return {"Bad"};
|
|
update_interval: 15s
|