#:########################################################################################:#
# TITLE: STAIR OCCUPANCY AND UNDERHOUSE ENVIRONMENT
# zorruno.com layout v1.1 2026
#:########################################################################################:#
# REPO:
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-occupancystair.yaml
#:########################################################################################:#
# VERSIONS:
# V2.4 2026-07-26 Added status-aware ownership for the footer-light auto timer
# V2.3 2026-07-25 Standardized MQTT payloads: "ON"/"OFF" → "On"/"Off"
# V2.2 2026-02-25 Updated yaml to zorruno layout V1.1
# V2.1 2025-08-25 Added some MQTT to send commands to turn on remote lights
# V2.0 2025-06-05 YAML Tidyups
#:########################################################################################:#
# HARDWARE:
# D1 Mini ESP32 with:
# - HiLink LD2410 mmWave presence sensor (UART)
# - BME280 Temp/Humidity/Pressure sensor (I2C)
# - Generic PIR sensor (GPIO)
#
# References:
# - https://esphome.io/components/sensor/ld2410.html
# - https://www.simplysmart.house/blog/presence-detection-ld2410-home-assistant
# - https://github.com/patrick3399/Hi-Link_mmWave_Radar_ESPHome/tree/main
#:########################################################################################:#
# OPERATION NOTES:
# - Provides mmWave presence (moving/still) plus PIR motion.
# - Provides environment sensors (BME280).
# - When mmWave presence/moving target or PIR triggers, a script publishes MQTT commands to a remote
#   light controller (e.g. stair footer lights) for a configurable auto-on duration.
# - Auto control can be enabled/disabled with "Stair Footer Lights Auto".
# - Auto duration is controlled by "Stair Footer Lights Auto Time" (seconds).
# - LD2410 exposes configuration entities (numbers/selects/switches/buttons/text sensors).
#:########################################################################################:#
# MQTT COMMANDS:
# This device publishes MQTT commands to a remote device:
# - Topic: ${mqtt_remote_device1_command_topic}
# - Payloads: ${mqtt_remote_device_command_ON}, ${mqtt_remote_device_command_OFF}
#
# Example (as configured by substitutions):
# - ${mqtt_command_main_topic}/stair-footerlights  ->  On, Off
#:########################################################################################:#
# OFFLINE NOTES:
# a) Home Assistant OFFLINE, but Network and MQTT ONLINE
#    - mmWave/PIR and BME280 sensing continues locally.
#    - Remote light control via MQTT publish continues (auto script still publishes).
#    - HA entities are unavailable until HA returns.
# b) MQTT OFFLINE (but WiFi/Network and HA API ONLINE)
#    - Sensing continues and HA API entities remain available.
#    - Remote light control will NOT work (MQTT publishes fail while MQTT is down).
# c) Entire WiFi/Network OFFLINE
#    - Sensing continues locally, but no HA API and no MQTT remote control.
#    - SNTP is not needed (no timeclocks used).
#:########################################################################################:#

#:########################################################################################:#
# SUBSTITUTIONS: Specific device variable substitutions
# If NOT using a secrets file, just replace these with the passwords etc (in quotes)
#:########################################################################################:#
substitutions:
  # Device Naming
  device_name: "esp-occupancystair"
  friendly_name: "Stair Occupancy and Underhouse Environment"
  description_comment: "D1 Mini ESP32 with LD2410 mmWave for internal stairwell and environment sensors for under house (Layout V1.1)"
  device_area: "Underhouse" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.

  # Project Naming
  project_name: "Generic.ESP32"
  project_version: "v2.4" # Project V denotes release of yaml file, allowing checking of deployed vs latest version

  # Passwords & Secrets (unfortunately you can't use substitutions inside secrets names)
  api_key: !secret esp-api_key
  ota_pass: !secret esp-ota_pass
  static_ip_address: !secret esp-occupancystair_ip
  mqtt_command_main_topic: !secret mqtt_command_main_topic
  mqtt_status_main_topic: !secret mqtt_status_main_topic

  # If we are changing IP addresses, you must update the current IP address here, otherwise it remains
  # Don't forget to switch it back when changed.
  current_ip_address: ${static_ip_address}

  # Device Settings
  log_level: "INFO" # NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
  update_interval: "60s" # update time for general sensors etc

  # MQTT REMOTE Controls
  mqtt_remote_device1_name: "stair-footerlights"
  mqtt_remote_device1_command_topic: "${mqtt_command_main_topic}/${mqtt_remote_device1_name}"
  mqtt_remote_device1_status_topic: "${mqtt_status_main_topic}/${mqtt_remote_device1_name}"
  mqtt_remote_device_command_ON: "On"
  mqtt_remote_device_command_OFF: "Off"

#:########################################################################################:#
# PACKAGES: Included Common Packages
# https://esphome.io/components/packages.html
#:########################################################################################:#
packages:
  #### WIFI, Network (Static/DHCP/IPV6 etc), Fallback AP, Safemode ####
  common_wifi: !include
    file: common/network_common.yaml
    vars:
      local_device_name: "${device_name}"
      local_static_ip_address: "${static_ip_address}"
      local_ota_pass: "${ota_pass}"
      local_current_ip_address: "${current_ip_address}"

  #### HOME ASSISTANT API (choose encryption or no encryption options) ####
  common_api: !include
    file: common/api_common.yaml
    #file: common/api_common_noencryption.yaml
    vars:
      local_api_key: "${api_key}"

  #### MQTT ####
  common_mqtt: !include
    file: common/mqtt_common.yaml
    vars:
      local_device_name: "${device_name}"

  #### WEB PORTAL ####
  #common_webportal: !include common/webportal_common.yaml

  #### SNTP (Only use if you want/need accurate timeclocks) ####
  #common_sntp: !include common/sntp_common.yaml

  #### DIAGNOSTICS Sensors ####
  diag_basic: !include common/include_basic_diag_sensors.yaml
  diag_more: !include common/include_more_diag_sensors.yaml
  #diag_debug: !include common/include_debug_diag_sensors.yaml
  #diag_resetcount: !include common/include_resetcount_diag_sensors.yaml

#:########################################################################################:#
# ESPHOME:
# https://esphome.io/components/esphome.html
#:########################################################################################:#
esphome:
  name: "${device_name}"
  friendly_name: "${friendly_name}"
  comment: "${description_comment}" # appears on the esphome page in HA
  area: "${device_area}"
  min_version: 2024.6.0

#:########################################################################################:#
# ESP PLATFORM AND FRAMEWORK:
# https://esphome.io/components/esp32.html
#:########################################################################################:#
esp32:
  board: esp32dev
  framework:
    type: esp-idf # "esp-idf" OR "arduino"
    version: recommended

#:########################################################################################:#
# GLOBAL VARIABLES:
# https://esphome.io/components/globals.html
#:########################################################################################:#
globals:
  - id: stair_footer_auto_default_s
    type: int
    restore_value: true
    initial_value: "20"

  - id: stair_footer_state_known
    type: bool
    restore_value: false
    initial_value: "false"

  - id: stair_footer_is_on
    type: bool
    restore_value: false
    initial_value: "false"

  - id: stair_footer_auto_owns_light
    type: bool
    restore_value: false
    initial_value: "false"

  - id: stair_footer_auto_on_pending
    type: bool
    restore_value: false
    initial_value: "false"

#:########################################################################################:#
# I2C BUS:
# https://esphome.io/components/i2c.html
#:########################################################################################:#
i2c:
  sda: GPIO19
  scl: GPIO21
  scan: true
  frequency: 100kHz

#:########################################################################################:#
# LOGGING:
# https://esphome.io/components/logger.html
#:########################################################################################:#
logger:
  level: "${log_level}"
  baud_rate: 0

#:########################################################################################:#
# MQTT STATUS:
# Tracks the authoritative footer-light state so auto control only turns off a
# light that it turned on itself.
#:########################################################################################:#
mqtt:
  on_message:
    - topic: "${mqtt_remote_device1_status_topic}"
      payload: "${mqtt_remote_device_command_ON}"
      then:
        - lambda: |-
            id(stair_footer_state_known) = true;
            id(stair_footer_is_on) = true;
            id(stair_footer_auto_on_pending) = false;

    - topic: "${mqtt_remote_device1_status_topic}"
      payload: "${mqtt_remote_device_command_OFF}"
      then:
        - lambda: |-
            id(stair_footer_state_known) = true;
            id(stair_footer_is_on) = false;
            if (!id(stair_footer_auto_on_pending)) {
              id(stair_footer_auto_owns_light) = false;
            }

#:########################################################################################:#
# UART:
# https://esphome.io/components/uart.html
#:########################################################################################:#
uart:
  id: ld2410_uart
  rx_pin: GPIO16
  tx_pin: GPIO17
  baud_rate: 256000
  data_bits: 8
  stop_bits: 1
  parity: NONE

#:########################################################################################:#
# STATUS LED:
# https://esphome.io/components/status_led.html
#:########################################################################################:#
status_led:
  pin:
    number: GPIO2
    ignore_strapping_warning: true
    inverted: false

#:########################################################################################:#
# LD2410:
# https://esphome.io/components/sensor/ld2410.html
#:########################################################################################:#
ld2410:
  uart_id: ld2410_uart

#:########################################################################################:#
# NUMBER COMPONENT:
# https://esphome.io/components/number/
#:########################################################################################:#
number:
  - platform: ld2410
    timeout:
      name: "Timeout"
    light_threshold:
      name: "Light Threshold"
    max_move_distance_gate:
      name: "Max Move Distance Gate"
    max_still_distance_gate:
      name: "Max Still Distance Gate"
    g0:
      move_threshold:
        name: "g0 move threshold"
      still_threshold:
        name: "g0 still threshold"
    g1:
      move_threshold:
        name: "g1 move threshold"
      still_threshold:
        name: "g1 still threshold"
    g2:
      move_threshold:
        name: "g2 move threshold"
      still_threshold:
        name: "g2 still threshold"
    g3:
      move_threshold:
        name: "g3 move threshold"
      still_threshold:
        name: "g3 still threshold"
    g4:
      move_threshold:
        name: "g4 move threshold"
      still_threshold:
        name: "g4 still threshold"
    g5:
      move_threshold:
        name: "g5 move threshold"
      still_threshold:
        name: "g5 still threshold"
    g6:
      move_threshold:
        name: "g6 move threshold"
      still_threshold:
        name: "g6 still threshold"
    g7:
      move_threshold:
        name: "g7 move threshold"
      still_threshold:
        name: "g7 still threshold"
    g8:
      move_threshold:
        name: "g8 move threshold"
      still_threshold:
        name: "g8 still threshold"

  - platform: template
    id: stair_footer_auto_time_s
    name: "Stair Footer Lights Auto Time"
    unit_of_measurement: "s"
    min_value: 10
    max_value: 300
    step: 1
    optimistic: true
    restore_value: true
    initial_value: 20

#:########################################################################################:#
# SELECT COMPONENT:
# https://esphome.io/components/select/index.html
#:########################################################################################:#
select:
  - platform: ld2410
    distance_resolution:
      name: "${friendly_name} LD2140 Distance Resolution"
    baud_rate:
      name: "${friendly_name} LD2140 Baud Rate"
    light_function:
      name: "${friendly_name} LD2140 Light Function"
    out_pin_level:
      name: "${friendly_name} LD2140 Out Pin Level"

#:########################################################################################:#
# SENSOR COMPONENT:
# https://esphome.io/components/sensor/
#:########################################################################################:#
sensor:
  - platform: bme280_i2c
    temperature:
      name: "Temperature"
      accuracy_decimals: 1
      oversampling: 2x
    pressure:
      name: "Pressure"
      oversampling: 2x
    humidity:
      name: "Humidity"
      accuracy_decimals: 1
      oversampling: 2x
    address: 0x76
    update_interval: "${update_interval}"

  - platform: ld2410
    light:
      name: "Light"
    moving_distance:
      name: "Moving Distance"
    still_distance:
      name: "Still Distance"
    moving_energy:
      name: "Move Energy"
    still_energy:
      name: "Still Energy"
    detection_distance:
      name: "Detection Distance"
    g0:
      move_energy:
        name: "g0 move energy"
      still_energy:
        name: "g0 still energy"
    g1:
      move_energy:
        name: "g1 move energy"
      still_energy:
        name: "g1 still energy"
    g2:
      move_energy:
        name: "g2 move energy"
      still_energy:
        name: "g2 still energy"
    g3:
      move_energy:
        name: "g3 move energy"
      still_energy:
        name: "g3 still energy"
    g4:
      move_energy:
        name: "g4 move energy"
      still_energy:
        name: "g4 still energy"
    g5:
      move_energy:
        name: "g5 move energy"
      still_energy:
        name: "g5 still energy"
    g6:
      move_energy:
        name: "g6 move energy"
      still_energy:
        name: "g6 still energy"
    g7:
      move_energy:
        name: "g7 move energy"
      still_energy:
        name: "g7 still energy"
    g8:
      move_energy:
        name: "g8 move energy"
      still_energy:
        name: "g8 still energy"

#:########################################################################################:#
# SWITCH COMPONENT:
# https://esphome.io/components/switch/
#:########################################################################################:#
switch:
  - platform: ld2410
    engineering_mode:
      name: "${friendly_name} LD2140 Engineering Mode"
    #bluetooth:
    #  name: "${friendly_name} LD2140 Control Bluetooth"

  - platform: template
    id: stair_footer_auto_enabled
    name: "Stair Footer Lights Auto"
    optimistic: true
    restore_mode: RESTORE_DEFAULT_ON

#:########################################################################################:#
# BINARY SENSORS:
# https://esphome.io/components/binary_sensor/
#:########################################################################################:#
binary_sensor:
  - platform: ld2410
    has_target:
      name: "mmWave Presence"
      on_press:
        - if:
            condition:
              switch.is_on: stair_footer_auto_enabled
            then:
              - script.execute: stair_footer_auto_script
    has_moving_target:
      name: "mmWave Moving Target"
      on_press:
        - if:
            condition:
              switch.is_on: stair_footer_auto_enabled
            then:
              - script.execute: stair_footer_auto_script
    has_still_target:
      name: "mmWave Still Target"
    out_pin_presence_status:
      name: "LD2140 Out Pin Presence Status"
      entity_category: diagnostic

  # Generic PIR Sensor (GPIO13)
  - platform: gpio
    pin:
      number: GPIO13
      mode:
        input: true
        pullup: true
      inverted: true
    name: "PIR Sensor"
    device_class: motion
    filters:
      - delayed_on: 50ms
    on_press:
      - if:
          condition:
            switch.is_on: stair_footer_auto_enabled
          then:
            - script.execute: stair_footer_auto_script

#:########################################################################################:#
# BUTTON COMPONENT:
# https://esphome.io/components/button/index.html
#:########################################################################################:#
button:
  - platform: ld2410
    factory_reset:
      name: "${friendly_name} LD2140 Factory reset"
    restart:
      name: "${friendly_name} LD2140 Restart"
    query_params:
      name: "Query Parameters"

#:########################################################################################:#
# TEXT SENSOR COMPONENT:
# https://esphome.io/components/text_sensor/
#:########################################################################################:#
text_sensor:
  - platform: ld2410
    version:
      name: "${friendly_name} LD2140 Firmware Version"
    #mac_address:
    #  name: "${friendly_name} LD2140 BT MAC Address"

#:########################################################################################:#
# SCRIPT COMPONENT:
# https://esphome.io/components/script.html
#:########################################################################################:#
script:
  - id: stair_footer_auto_script
    mode: restart
    then:
      - if:
          condition:
            lambda: |-
              return id(stair_footer_auto_owns_light)
                || !id(stair_footer_state_known)
                || !id(stair_footer_is_on);
          then:
            # Acquire ownership only when this automation needs to turn the light on.
            - if:
                condition:
                  lambda: "return !id(stair_footer_auto_owns_light);"
                then:
                  - lambda: |-
                      id(stair_footer_auto_owns_light) = true;
                      id(stair_footer_auto_on_pending) = true;
                  - mqtt.publish:
                      topic: "${mqtt_remote_device1_command_topic}"
                      payload: "${mqtt_remote_device_command_ON}"
                      retain: false

            # Repeated occupancy restarts this delay while retaining ownership.
            - delay: !lambda "return (uint32_t)(id(stair_footer_auto_time_s).state) * 1000;"

            # An external OFF status releases ownership and suppresses this command.
            - if:
                condition:
                  lambda: "return id(stair_footer_auto_owns_light);"
                then:
                  - lambda: |-
                      id(stair_footer_auto_owns_light) = false;
                      id(stair_footer_auto_on_pending) = false;
                  - mqtt.publish:
                      topic: "${mqtt_remote_device1_command_topic}"
                      payload: "${mqtt_remote_device_command_OFF}"
                      retain: false
