---

###############################################################################
# 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:
        - light.turn_on:
            id: output_led
        - delay: 0.25s
        - light.turn_off:
            id: output_led
        - delay: 0.25s
        - light.turn_on:
            id: output_led
        - delay: 0.25s
        - light.turn_off:
            id: output_led
        - 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: 25%
              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

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
  # Dummy I2S audio pipeline for "speaker"
  - id: i2s_dummy
    i2s_lrclk_pin:
      number: GPIO15 # WS
      ignore_strapping_warning: true  # This isn't connected to anything anyways
    i2s_bclk_pin:
      number: GPIO14  # SCK

microphone:
  - platform: i2s_audio
    id: mic
    i2s_audio_id: i2s_input
    i2s_din_pin: GPIO4 # SD
    adc_type: external
    pdm: false
    channel: left

speaker:
  # Dummy speaker to fix home-assistant/core#142363
  - platform: i2s_audio
    id: dummy_speaker
    i2s_audio_id: i2s_dummy
    i2s_dout_pin: GPIO25
    dac_type: external
    bits_per_sample: 16bit
    sample_rate: 16000
    channel: mono #mono will have bad performance, however, we are not using the on device speaker
    buffer_duration: 60ms

micro_wake_word:
  id: mww
  microphone:
    microphone: mic
    gain_factor: 64
  stop_after_detection: false
  models:
    - model: github://joshuaboniface/Custom_V2_MicroWakeWords/models/computer/computer.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
  speaker: dummy_speaker
  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: 25%
              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: "ESP32 Heap Free"
    block:
      name: "ESP32 Heap Max Block"
    loop_time:
      name: "ESP32 Loop Time"
    cpu_frequency:
      name: "ESP32 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
    update_interval: 15s

  - 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
    update_interval: 15s

  - 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
    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

  - 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: "%"
    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 (int) 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: true
        pulldown: true
    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
          }
