1241 lines
35 KiB
YAML
1241 lines
35 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"
|
||
min_version: 2025.5.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);
|
||
- priority: -100
|
||
then:
|
||
- if:
|
||
condition:
|
||
- switch.is_on: enable_voice_support
|
||
then:
|
||
- micro_wake_word.start:
|
||
|
||
preferences:
|
||
flash_write_interval: 15sec
|
||
|
||
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"
|
||
|
||
- id: current_wake_word
|
||
type: std::string
|
||
restore_value: yes
|
||
initial_value: '"mww_computer"'
|
||
|
||
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);
|
||
}
|
||
|
||
interval:
|
||
# Regular MWW state check every 30s
|
||
- interval: 30s
|
||
then:
|
||
- if:
|
||
condition:
|
||
- switch.is_on: enable_voice_support
|
||
- not:
|
||
micro_wake_word.is_running
|
||
then:
|
||
- micro_wake_word.start:
|
||
|
||
# Regular occupancy state reporting to HASS every 30s
|
||
- 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: 300s
|
||
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();
|
||
}
|
||
|
||
logger:
|
||
level: DEBUG
|
||
baud_rate: 115200
|
||
|
||
api:
|
||
reboot_timeout: 15min
|
||
# on_client_connected:
|
||
# - logger.log:
|
||
# format: "Client %s (IP %s) connected to API"
|
||
# args: ["client_info.c_str()", "client_address.c_str()"]
|
||
# - script.execute: light_off
|
||
# - if:
|
||
# condition:
|
||
# lambda: |-
|
||
# return id(enable_voice_support).state &&
|
||
# !id(mww).is_running();
|
||
# then:
|
||
# - micro_wake_word.start:
|
||
# on_client_disconnected:
|
||
# - logger.log:
|
||
# format: "Client %s (IP %s) disconnected from API"
|
||
# args: ["client_info.c_str()", "client_address.c_str()"]
|
||
# - if:
|
||
# condition:
|
||
# lambda: |-
|
||
# return id(enable_voice_support).state &&
|
||
# id(mww).is_running();
|
||
# then:
|
||
# - micro_wake_word.stop:
|
||
# - light.turn_on:
|
||
# id: output_led
|
||
# effect: flash_white
|
||
|
||
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"
|
||
|
||
debug:
|
||
update_interval: 15s
|
||
|
||
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:
|
||
- id: i2s_input
|
||
i2s_lrclk_pin:
|
||
number: GPIO17 # WS
|
||
i2s_bclk_pin:
|
||
number: GPIO16 # SCK
|
||
|
||
microphone:
|
||
- platform: i2s_audio
|
||
id: mic
|
||
i2s_audio_id: i2s_input
|
||
i2s_din_pin: GPIO4 # SD
|
||
adc_type: external
|
||
pdm: false
|
||
channel: left
|
||
|
||
micro_wake_word:
|
||
id: mww
|
||
microphone:
|
||
microphone: mic
|
||
gain_factor: 8
|
||
stop_after_detection: false
|
||
models:
|
||
- model: github://genehand/Custom_V2_MicroWakeWords/models/computer/computer.json@update-json
|
||
id: mww_computer
|
||
- model: github://esphome/micro-wake-word-models/models/v2/hey_jarvis.json
|
||
id: mww_hey_jarvis
|
||
- model: github://esphome/micro-wake-word-models/models/v2/hey_mycroft.json
|
||
id: mww_hey_mycroft
|
||
- model: github://esphome/micro-wake-word-models/models/v2/okay_nabu.json
|
||
id: mww_okay_nabu
|
||
- model: github://esphome/micro-wake-word-models/models/v2/alexa.json
|
||
id: mww_alexa
|
||
vad:
|
||
on_wake_word_detected:
|
||
- logger.log: "A wake word was detected!"
|
||
- if:
|
||
condition:
|
||
voice_assistant.is_running:
|
||
then:
|
||
voice_assistant.stop:
|
||
- voice_assistant.start:
|
||
wake_word: !lambda return wake_word;
|
||
|
||
voice_assistant:
|
||
id: va
|
||
microphone: mic
|
||
micro_wake_word: mww
|
||
use_wake_word: false
|
||
noise_suppression_level: 3
|
||
auto_gain: 31 dbfs
|
||
volume_multiplier: 4
|
||
on_wake_word_detected:
|
||
- logger.log: "Wake word detected in VA pipeline"
|
||
- light.turn_on:
|
||
id: output_led
|
||
brightness: 100%
|
||
red: 0
|
||
green: 0
|
||
blue: 1
|
||
on_listening:
|
||
- logger.log: "Listening for commands"
|
||
- light.turn_on:
|
||
id: output_led
|
||
brightness: 100%
|
||
red: 0
|
||
green: 0
|
||
blue: 1
|
||
on_stt_vad_end:
|
||
- logger.log: "Processing STT result"
|
||
- light.turn_on:
|
||
id: output_led
|
||
brightness: 75%
|
||
red: 0
|
||
green: 1
|
||
blue: 1
|
||
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: 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 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
|
||
|
||
- platform: debug
|
||
free:
|
||
name: "Heap Free"
|
||
block:
|
||
name: "Heap Max Block"
|
||
loop_time:
|
||
name: "Loop Time"
|
||
cpu_frequency:
|
||
name: "CPU Frequency"
|
||
|
||
- platform: sgp4x
|
||
voc:
|
||
name: "SGP41 VOC Index"
|
||
id: sgp41_voc_index
|
||
accuracy_decimals: 0
|
||
icon: mdi:waves-arrow-up
|
||
filters:
|
||
- sliding_window_moving_average: # We take a reading every 15 seconds, but calculate the sliding
|
||
window_size: 12 # average over 12 readings i.e. 60 seconds/1 minute to normalize
|
||
send_every: 3 # brief spikes while still sending a value every 15 seconds.
|
||
nox:
|
||
name: "SGP41 NOx Index"
|
||
id: sgp41_nox_index
|
||
accuracy_decimals: 0
|
||
icon: mdi:waves-arrow-up
|
||
filters:
|
||
- sliding_window_moving_average:
|
||
window_size: 12
|
||
send_every: 3
|
||
compensation:
|
||
temperature_source: sht45_temperature
|
||
humidity_source: sht45_humidity
|
||
store_baseline: true
|
||
update_interval: 5s
|
||
|
||
- platform: template
|
||
name: "SGP41 TVOC (µg/m³)"
|
||
id: sgp41_tvoc_ugm3
|
||
icon: mdi:molecule
|
||
lambda: |-
|
||
float i = id(sgp41_voc_index).state;
|
||
if (i < 1) return NAN;
|
||
float tvoc = (log(501.0 - i) - 6.24) * -878.53;
|
||
return tvoc;
|
||
unit_of_measurement: "µg/m³"
|
||
accuracy_decimals: 0
|
||
|
||
- platform: template
|
||
name: "SGP41 TVOC (ppb)"
|
||
id: sgp41_tvoc_ppb
|
||
icon: mdi:molecule
|
||
lambda: |-
|
||
float tvoc_ugm3 = id(sgp41_tvoc_ugm3).state;
|
||
float tvoc_ppm = tvoc_ugm3 * 0.436; // ppb estimated using isobutylene MW (56.1 g/mol)
|
||
return tvoc_ppm;
|
||
unit_of_measurement: "ppb"
|
||
accuracy_decimals: 0
|
||
|
||
- platform: template
|
||
name: "SGP41 eCO2 (appr.)"
|
||
id: sgp41_eco2_appr
|
||
icon: mdi:molecule-co2
|
||
lambda: |-
|
||
float tvoc_ppb = id(sgp41_tvoc_ppb).state;
|
||
float eco2_ppm = 400.0 + 1.5 * tvoc_ppb;
|
||
if (eco2_ppm > 2000) eco2_ppm = 2000;
|
||
return eco2_ppm;
|
||
unit_of_measurement: "ppm"
|
||
accuracy_decimals: 0
|
||
|
||
- 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
|
||
|
||
- platform: template
|
||
name: "SHT45 Dew Point"
|
||
icon: mdi:thermometer-water
|
||
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
|
||
|
||
- platform: template
|
||
name: "Room Health Score"
|
||
id: room_health_score
|
||
unit_of_measurement: "%"
|
||
accuracy_decimals: 0
|
||
icon: mdi:home-heart
|
||
lambda: |-
|
||
float voc_index = id(sgp41_voc_index).state;
|
||
float temp = id(sht45_temperature).state;
|
||
float humidity = id(sht45_humidity).state;
|
||
|
||
// VOC Score (0–100)
|
||
float voc_score = 0;
|
||
if (voc_index <= 100) voc_score = 100;
|
||
else if (voc_index <= 200) voc_score = 80;
|
||
else if (voc_index <= 300) voc_score = 60;
|
||
else if (voc_index <= 400) voc_score = 40;
|
||
else if (voc_index <= 500) voc_score = 50;
|
||
else voc_score = 0;
|
||
|
||
// Temperature Score (0–100)
|
||
float temp_score = 100.0 - abs(temp - 23.0) * 10.0;
|
||
if (temp_score < 0) temp_score = 0;
|
||
|
||
// Humidity Score (0–100), ideal range 35–55%
|
||
float humidity_score = 100.0 - abs(humidity - 50.0) * 3.0;
|
||
if (humidity_score < 0) humidity_score = 0;
|
||
|
||
// Weighted average
|
||
float overall_score = (voc_score * 0.5 + temp_score * 0.25 + humidity_score * 0.25);
|
||
|
||
return round(overall_score);
|
||
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
|
||
|
||
binary_sensor:
|
||
- platform: template
|
||
name: "SuperSensor Occupancy"
|
||
id: supersensor_occupancy
|
||
device_class: occupancy
|
||
on_state:
|
||
then:
|
||
- script.execute: light_off
|
||
|
||
- platform: gpio
|
||
name: "PIR GPIO"
|
||
id: pir_gpio
|
||
pin:
|
||
number: GPIO32
|
||
mode: INPUT_PULLUP
|
||
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"
|
||
|
||
text_sensor:
|
||
- platform: version
|
||
name: "ESPHome Version"
|
||
entity_category: diagnostic
|
||
|
||
- platform: wifi_info
|
||
ip_address:
|
||
name: "WiFi IP Address"
|
||
ssid:
|
||
name: "WiFi SSID"
|
||
bssid:
|
||
name: "WiFi BSSID"
|
||
mac_address:
|
||
name: "WiFi MAC Address"
|
||
|
||
- platform: debug
|
||
device:
|
||
name: "Device Info"
|
||
reset_reason:
|
||
name: "Reset Reason"
|
||
|
||
- platform: ld2410
|
||
version:
|
||
name: "LD2410C Firmware Version"
|
||
mac_address:
|
||
name: "LD2410C MAC Address"
|
||
|
||
- platform: template
|
||
name: "Chemical Pollution"
|
||
id: sgp41_chemical_pollution
|
||
icon: mdi:molecule
|
||
lambda: |-
|
||
float voc_index = id(sgp41_voc_index).state;
|
||
if (voc_index < 1 || voc_index > 500) return {"Unknown"};
|
||
if (voc_index <= 100) return {"Excellent"};
|
||
else if (voc_index <= 200) return {"Good"};
|
||
else if (voc_index <= 300) return {"Moderate"};
|
||
else if (voc_index <= 400) return {"Unhealthy"};
|
||
else return {"Hazardous"};
|
||
update_interval: 15s
|
||
|
||
- platform: template
|
||
name: "Room Health"
|
||
id: room_health_text
|
||
icon: mdi:home-heart
|
||
lambda: |-
|
||
float score = id(room_health_score).state;
|
||
if (score < 0) return {"Unknown"};
|
||
else if (score >= 90.0) return {"Great"};
|
||
else if (score >= 80.0) return {"Good"};
|
||
else if (score >= 60.0) return {"Fair"};
|
||
else if (score >= 40.0) return {"Poor"};
|
||
else return {"Bad"};
|
||
update_interval: 15s
|
||
|
||
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:
|
||
- micro_wake_word.start:
|
||
on_turn_off:
|
||
- micro_wake_word.stop:
|
||
|
||
# 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 -30 to +5 for the temperature sensor
|
||
- platform: template
|
||
name: "Temperature Offset"
|
||
id: temperature_offset_setter
|
||
min_value: -30
|
||
max_value: 10
|
||
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 -20 to +20 for the humidity sensor
|
||
- platform: template
|
||
name: "Humidity Offset"
|
||
id: humidity_offset_setter
|
||
min_value: -20
|
||
max_value: 20
|
||
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"
|
||
|
||
- platform: template
|
||
name: "Wake Word Selector"
|
||
id: wake_word_selector
|
||
options:
|
||
- "Computer"
|
||
- "Hey Jarvis"
|
||
- "Hey Mycroft"
|
||
- "Okay Nabu"
|
||
- "Alexa"
|
||
initial_option: "Computer"
|
||
optimistic: true
|
||
restore_value: true
|
||
set_action:
|
||
# Disable models that aren't selected
|
||
- if:
|
||
condition:
|
||
lambda: 'return x != "Computer";'
|
||
then:
|
||
- micro_wake_word.disable_model: mww_computer
|
||
- if:
|
||
condition:
|
||
lambda: 'return x != "Hey Jarvis";'
|
||
then:
|
||
- micro_wake_word.disable_model: mww_hey_jarvis
|
||
- if:
|
||
condition:
|
||
lambda: 'return x != "Hey Mycroft";'
|
||
then:
|
||
- micro_wake_word.disable_model: mww_hey_mycroft
|
||
- if:
|
||
condition:
|
||
lambda: 'return x != "Okay Nabu";'
|
||
then:
|
||
- micro_wake_word.disable_model: mww_okay_nabu
|
||
- if:
|
||
condition:
|
||
lambda: 'return x != "Alexa";'
|
||
then:
|
||
- micro_wake_word.disable_model: mww_alexa
|
||
# Enable model we selected
|
||
- if:
|
||
condition:
|
||
lambda: 'return x == "Computer";'
|
||
then:
|
||
- micro_wake_word.enable_model: mww_computer
|
||
- if:
|
||
condition:
|
||
lambda: 'return x == "Hey Jarvis";'
|
||
then:
|
||
- micro_wake_word.enable_model: mww_hey_jarvis
|
||
- if:
|
||
condition:
|
||
lambda: 'return x == "Hey Mycroft";'
|
||
then:
|
||
- micro_wake_word.enable_model: mww_hey_mycroft
|
||
- if:
|
||
condition:
|
||
lambda: 'return x == "Okay Nabu";'
|
||
then:
|
||
- micro_wake_word.enable_model: mww_okay_nabu
|
||
- if:
|
||
condition:
|
||
lambda: 'return x == "Alexa";'
|
||
then:
|
||
- micro_wake_word.enable_model: mww_alexa
|
||
|
||
- platform: template
|
||
name: "Wake Word Sensitivity"
|
||
optimistic: true
|
||
initial_option: Default
|
||
restore_value: true
|
||
options:
|
||
- Default
|
||
- More sensitive
|
||
- Very sensitive
|
||
set_action:
|
||
# Sets specific wake word probabilities computed for each particular model
|
||
# Note probability cutoffs are set as a quantized uint8 value, each comment has the corresponding floating point cutoff
|
||
- lambda: |-
|
||
if (x == "Default") {
|
||
id(mww_computer).set_probability_cutoff(168); // 0.66 (default)
|
||
id(mww_hey_jarvis).set_probability_cutoff(247); // 0.97 (default)
|
||
id(mww_hey_mycroft).set_probability_cutoff(242); // 0.95 (default)
|
||
id(mww_okay_nabu).set_probability_cutoff(217); // 0.85 (default)
|
||
id(mww_alexa).set_probability_cutoff(217); // 0.85 (default)
|
||
} else if (x == "More sensitive") {
|
||
id(mww_computer).set_probability_cutoff(153); // 0.60
|
||
id(mww_hey_jarvis).set_probability_cutoff(235); // 0.92
|
||
id(mww_hey_mycroft).set_probability_cutoff(237); // 0.93
|
||
id(mww_okay_nabu).set_probability_cutoff(176); // 0.69
|
||
id(mww_alexa).set_probability_cutoff(176); // 0.69
|
||
} else if (x == "Very sensitive") {
|
||
id(mww_computer).set_probability_cutoff(138); // 0.54
|
||
id(mww_hey_jarvis).set_probability_cutoff(212); // 0.83
|
||
id(mww_hey_mycroft).set_probability_cutoff(230); // 0.90
|
||
id(mww_okay_nabu).set_probability_cutoff(143); // 0.56
|
||
id(mww_alexa).set_probability_cutoff(143); // 0.56
|
||
}
|