#:########################################################################################:#
# TITLE: OFFICE OCCUPANCY AND ENVIRONMENT
# zorruno.com layout v1.1 2026
#:########################################################################################:#
# REPO:
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-occupancyoffice.yaml
#:########################################################################################:#
# VERSIONS:
# v1.3 2026-07-25 Restored MQTT light timer commands. Light 2-way sync is handled directly
#                 between esp-officelights and esp-officeduallights via MQTT. This device
#                 sends On/timer commands to all 4 light topics for occupancy-based control.
# v1.2 2026-07-25 Removed MQTT light control commands (reverted)
# v1.1 2026-04-24 Added MQTT remote timer command script triggered by mmWave occupancy or movement
# v1.0 2026-02-25 Updated yaml to zorruno layout V1.1
#:########################################################################################:#
# HARDWARE:
# - D1 Mini ESP32 (ESP32)
# - HiLink LD1125H mmWave sensor (UART @ 115200)
# - BME280 Temp/Humidity/Pressure sensor (I2C, addr 0x76)
# - Optional: Bluetooth Proxy / BLE tracker enabled
#
# References:
# - https://zorruno.com/2024/mmwave-occupancy-with-esp32-ld1125h/
# - https://github.com/patrick3399/Hi-Link_mmWave_Radar_ESPHome/tree/main
# - https://github.com/patrick3399/Hi-Link_mmWave_Radar_ESPHome/blob/main/LD1125H/ESP32-LD1125H-Complete.yaml
#:########################################################################################:#
# OPERATION NOTES:
# - LD1125H tuning parameters:
#   - mth1: 0 to 2.8m sensitive
#   - mth2: 2.8 to 8m sensitive
#   - mth3: above 8m sensitive
#   - rmax: max distance
#   - Clearance Time: Mov/Occ to Clearance waiting time
#   - Movement Time: Mov to Occ waiting time
# - On boot, this device pushes mth1/mth2/mth3/rmax values to the LD1125H via UART.
# - Uses the ssieb "serial" external component to read UART lines and parse "mov/occ" lines.
# - Publishes a text occupancy state (Movement/Occupancy/Clearance) and binary sensors for:
#   - mmWave Occupancy or Movement (occupancy)
#   - mmWave Movement (motion)
# - When mmWave Occupancy or Movement turns ON, this device publishes MQTT On commands to
#   all 4 office light topics. When it turns OFF, it publishes timer commands (10s or 360s)
#   so lights stay on briefly after the room is vacated.
# - Light 2-way sync between esp-officelights and esp-officeduallights ensures both switches
#   stay in sync regardless of which device initiates the change.
# - BLE scanning is started/stopped based on HA API client connection (reduces BLE load when unused).
#:########################################################################################:#
# OFFLINE NOTES:
# a) Home Assistant OFFLINE, but Network and MQTT ONLINE
#    - Device continues running mmWave logic and environment sensors.
#    - MQTT light timer commands still function if MQTT is online.
#    - HA entities will update again when HA returns.
#    - Physical buttons on esp-officelights and esp-officeduallights still work and cancel
#      any active timer if pressed.
# b) MQTT OFFLINE (or broker unreachable)
#    - Device continues running locally and via HA API (if HA is reachable).
#    - MQTT-based light timer commands will not function until MQTT returns.
# c) Entire WiFi/Network OFFLINE
#    - Device continues running local mmWave/environment logic, but HA/MQTT integration is unavailable.
#    - Accurate time is NOT needed (SNTP not needed): logic uses relative time (time since boot),
#      but timing resets on reboot.
#:########################################################################################:#

#:########################################################################################:#
# 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-occupancyoffice"
  friendly_name: "Office Occupancy & Environment"
  description_comment: "D1 Mini ESP32 with LD1125H mmWave and environment sensors for downstairs office (Layout V1.1)"
  device_area: "Office"

  # Project Naming
  project_name: "HiLink.LD1125H Office Occupancy"
  project_version: "v1.3"

  # Passwords & Secrets (unfortunately you can't use substitutions inside secrets names)
  api_key: !secret esp-occupancyoffice_api_key
  ota_pass: !secret esp-occupancyoffice_ota_pass
  static_ip_address: !secret esp-occupancyoffice_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" # general sensor update interval

  # Remote light timer payloads, in seconds.
  # Dual R1: left=overhead(cool white)=10s, right=bunker=360s
  # Main lightswitch: left=bunker=360s, right=overhead(virtual mirror)=10s
  # Note: "left"/"right" mean opposite things on each device due to physical wiring.
  remote_timer_dual_left_payload: "10"
  remote_timer_dual_right_payload: "360"
  remote_timer_main_left_payload: "360"
  remote_timer_main_right_payload: "10"

  # MQTT REMOTE Controls
  mqtt_remote_device1_name: "office-dual-lights-left"
  mqtt_remote_device1_command_topic: "${mqtt_command_main_topic}/${mqtt_remote_device1_name}"
  mqtt_remote_device1_command1: "On"
  mqtt_remote_device1_command2: "${remote_timer_dual_left_payload}"

  mqtt_remote_device2_name: "office-dual-lights-right"
  mqtt_remote_device2_command_topic: "${mqtt_command_main_topic}/${mqtt_remote_device2_name}"
  mqtt_remote_device2_command1: "On"
  mqtt_remote_device2_command2: "${remote_timer_dual_right_payload}"

  mqtt_remote_device3_name: "office-main-lights-left"
  mqtt_remote_device3_command_topic: "${mqtt_command_main_topic}/${mqtt_remote_device3_name}"
  mqtt_remote_device3_command1: "On"
  mqtt_remote_device3_command2: "${remote_timer_main_left_payload}"

  mqtt_remote_device4_name: "office-main-lights-right"
  mqtt_remote_device4_command_topic: "${mqtt_command_main_topic}/${mqtt_remote_device4_name}"
  mqtt_remote_device4_command1: "On"
  mqtt_remote_device4_command2: "${remote_timer_main_right_payload}"

#:########################################################################################:#
# 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}"

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

  #### 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}"
  area: "${device_area}"
  min_version: 2026.4.1
  project:
    name: "${project_name}"
    version: "${project_version}"
  platformio_options:
    build_flags:
      - "-Os"
      - "-Wl,--gc-sections"
      - "-fno-exceptions"
      # - "-fno-rtti"

  on_boot:
    - priority: -200
      then:
        - uart.write:
            id: ld1125h_uart_bus
            data: !lambda |-
              std::string th1st = "mth1=" + str_sprintf("%.0f", id(ld1125h_mth1).state) + "\r\n";
              return std::vector<uint8_t>(th1st.begin(), th1st.end());
        - uart.write:
            id: ld1125h_uart_bus
            data: !lambda |-
              std::string th2st = "mth2=" + str_sprintf("%.0f", id(ld1125h_mth2).state) + "\r\n";
              return std::vector<uint8_t>(th2st.begin(), th2st.end());
        - uart.write:
            id: ld1125h_uart_bus
            data: !lambda |-
              std::string th3st = "mth3=" + str_sprintf("%.0f", id(ld1125h_mth3).state) + "\r\n";
              return std::vector<uint8_t>(th3st.begin(), th3st.end());
        - uart.write:
            id: ld1125h_uart_bus
            data: !lambda |-
              std::string rmaxst = "rmax=" + str_sprintf("%.1f", id(ld1125h_rmax).state) + "\r\n";
              return std::vector<uint8_t>(rmaxst.begin(), rmaxst.end());

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

#:########################################################################################:#
# EXTERNAL COMPONENTS
# https://esphome.io/components/external_components.html
#:########################################################################################:#
external_components:
  - source:
      type: git
      url: https://github.com/ssieb/custom_components
    components: [serial]

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

#:########################################################################################:#
# API (Home Assistant)
# https://esphome.io/components/api.html
#:########################################################################################:#
api:
  encryption:
    key: "${api_key}"
  reboot_timeout: 0s
  on_client_connected:
    - esp32_ble_tracker.start_scan:
        continuous: true
  on_client_disconnected:
    - esp32_ble_tracker.stop_scan:

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

#:########################################################################################:#
# UART (LD1125H)
# https://esphome.io/components/uart.html
#:########################################################################################:#
uart:
  id: ld1125h_uart_bus
  rx_pin: GPIO16
  tx_pin: GPIO17
  baud_rate: 115200
  data_bits: 8
  stop_bits: 1
  parity: NONE

#:########################################################################################:#
# BLUETOOTH PROXY / BLE TRACKER
# https://esphome.io/components/bluetooth_proxy.html
# https://esphome.io/components/esp32_ble_tracker.html
#:########################################################################################:#
bluetooth_proxy:
  active: true
  cache_services: true

esp32_ble_tracker:
  scan_parameters:
    continuous: false

#:########################################################################################:#
# GLOBALS
# https://esphome.io/components/globals.html
#:########################################################################################:#
globals:
  - id: ld1125h_last_time
    type: time_t
    restore_value: false
    initial_value: "0"

  - id: ld1125h_last_mov_time
    type: time_t
    restore_value: false
    initial_value: "0"

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

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

#:########################################################################################:#
# INTERVAL (Clearance scan)
# https://esphome.io/components/interval.html
#:########################################################################################:#
interval:
  - interval: 1s
    setup_priority: -200
    then:
      - lambda: |-
          if ((::time(nullptr) - id(ld1125h_last_time)) > id(ld1125h_clear_time).state) {
            if ((id(ld1125h_clearance_status) == false) ||
                (id(ld1125h_occupancy).state != "Clearance")) {
              id(ld1125h_occupancy).publish_state("Clearance");
              id(ld1125h_clearance_status) = true;
            }
            if (id(ld1125h_movocc_binary).state == true) {
              id(ld1125h_movocc_binary).publish_state(false);
            }
            if (id(ld1125h_mov_binary).state == true) {
              id(ld1125h_mov_binary).publish_state(false);
            }
            id(ld1125h_last_time) = ::time(nullptr);
          }

#:########################################################################################:#
# NUMBER (LD1125H controls)
# https://esphome.io/components/number/
#:########################################################################################:#
number:
  - platform: template
    name: "0-2.8m Sensitivity"
    id: ld1125h_mth1
    icon: "mdi:cogs"
    optimistic: true
    restore_value: true
    initial_value: "60.0"
    min_value: 10.0
    max_value: 600.0
    step: 5.0
    set_action:
      - uart.write:
          id: ld1125h_uart_bus
          data: !lambda |-
            std::string th1st = "mth1=" + str_sprintf("%.0f", x) + "\r\n";
            return std::vector<uint8_t>(th1st.begin(), th1st.end());

  - platform: template
    name: "2.8-8m Sensitivity"
    id: ld1125h_mth2
    icon: "mdi:cogs"
    optimistic: true
    restore_value: true
    initial_value: "30"
    min_value: 5
    max_value: 300
    step: 5
    set_action:
      - uart.write:
          id: ld1125h_uart_bus
          data: !lambda |-
            std::string th2st = "mth2=" + str_sprintf("%.0f", x) + "\r\n";
            return std::vector<uint8_t>(th2st.begin(), th2st.end());

  - platform: template
    name: "8m+ Sensitivity"
    id: ld1125h_mth3
    icon: "mdi:cogs"
    optimistic: true
    restore_value: true
    initial_value: "20"
    min_value: 5
    max_value: 200
    step: 5
    set_action:
      - uart.write:
          id: ld1125h_uart_bus
          data: !lambda |-
            std::string th3st = "mth3=" + str_sprintf("%.0f", x) + "\r\n";
            return std::vector<uint8_t>(th3st.begin(), th3st.end());

  - platform: template
    name: "Max Detection (m)"
    id: ld1125h_rmax
    icon: "mdi:cogs"
    optimistic: true
    restore_value: true
    initial_value: "8"
    min_value: 0.4
    max_value: 12
    step: 0.1
    set_action:
      - uart.write:
          id: ld1125h_uart_bus
          data: !lambda |-
            std::string rmaxst = "rmax=" + str_sprintf("%.1f", x) + "\r\n";
            return std::vector<uint8_t>(rmaxst.begin(), rmaxst.end());

  - platform: template
    name: "Clearance Time (s)"
    id: ld1125h_clear_time
    icon: "mdi:cogs"
    optimistic: true
    restore_value: true
    initial_value: "5"
    min_value: 0.5
    max_value: 20
    step: 0.5

  - platform: template
    name: "Movement Time (s)"
    id: ld1125h_mov_time
    icon: "mdi:cogs"
    optimistic: true
    restore_value: true
    initial_value: "1"
    min_value: 0.5
    max_value: 10
    step: 0.5

#:########################################################################################:#
# SENSOR
# 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: template
    name: "mmWave Distance"
    id: ld1125h_distance
    icon: "mdi:signal-distance-variant"
    unit_of_measurement: "m"
    accuracy_decimals: 2
    filters:
      - sliding_window_moving_average:
          window_size: 8
          send_every: 2
      - heartbeat: 0.2s

#:########################################################################################:#
# TEXT SENSOR
# https://esphome.io/components/text_sensor/
#:########################################################################################:#
text_sensor:
  - platform: serial
    uart_id: ld1125h_uart_bus
    name: "${friendly_name} LD1125H UART Text"
    id: ld1125h_uart_text
    icon: "mdi:format-text"
    internal: true
    on_value:
      lambda: |-
        if (id(ld1125h_uart_text).state.substr(0, 3) == "occ") {
          id(ld1125h_distance).publish_state(
            atof(id(ld1125h_uart_text).state.substr(9).c_str())
          );
          if ((::time(nullptr) - id(ld1125h_last_mov_time)) >
              id(ld1125h_mov_time).state) {
            id(ld1125h_occupancy).publish_state("Occupancy");
            if (id(ld1125h_movocc_binary).state == false) {
              id(ld1125h_movocc_binary).publish_state(true);
            }
            if (id(ld1125h_mov_binary).state == true) {
              id(ld1125h_mov_binary).publish_state(false);
            }
          }
          if (id(ld1125h_movocc_binary).state == false) {
            id(ld1125h_movocc_binary).publish_state(true);
          }
          id(ld1125h_last_time) = ::time(nullptr);
          if (id(ld1125h_clearance_status) == true) {
            id(ld1125h_clearance_status) = false;
          }
        }
        else if (id(ld1125h_uart_text).state.substr(0, 3) == "mov") {
          id(ld1125h_distance).publish_state(
            atof(id(ld1125h_uart_text).state.substr(9).c_str())
          );
          id(ld1125h_occupancy).publish_state("Movement");
          if (id(ld1125h_movocc_binary).state == false) {
            id(ld1125h_movocc_binary).publish_state(true);
          }
          if (id(ld1125h_mov_binary).state == false) {
            id(ld1125h_mov_binary).publish_state(true);
          }
          id(ld1125h_last_mov_time) = ::time(nullptr);
          id(ld1125h_last_time)     = ::time(nullptr);
          if (id(ld1125h_clearance_status) == true) {
            id(ld1125h_clearance_status) = false;
          }
        }

  - platform: template
    name: "mmWave Occupancy"
    id: ld1125h_occupancy
    icon: "mdi:motion-sensor"

#:########################################################################################:#
# SCRIPT
# https://esphome.io/components/script.html
#:########################################################################################:#
script:
  - id: send_remote_light_on_commands
    mode: restart
    then:
      - mqtt.publish:
          topic: "${mqtt_remote_device1_command_topic}"
          payload: "${mqtt_remote_device1_command1}"
          qos: 0
          retain: false
      - mqtt.publish:
          topic: "${mqtt_remote_device2_command_topic}"
          payload: "${mqtt_remote_device2_command1}"
          qos: 0
          retain: false
      - mqtt.publish:
          topic: "${mqtt_remote_device3_command_topic}"
          payload: "${mqtt_remote_device3_command1}"
          qos: 0
          retain: false
      - mqtt.publish:
          topic: "${mqtt_remote_device4_command_topic}"
          payload: "${mqtt_remote_device4_command1}"
          qos: 0
          retain: false

  - id: send_remote_light_timer_commands
    mode: restart
    then:
      - mqtt.publish:
          topic: "${mqtt_remote_device1_command_topic}"
          payload: "${mqtt_remote_device1_command2}"
          qos: 0
          retain: false
      - mqtt.publish:
          topic: "${mqtt_remote_device2_command_topic}"
          payload: "${mqtt_remote_device2_command2}"
          qos: 0
          retain: false
      - mqtt.publish:
          topic: "${mqtt_remote_device3_command_topic}"
          payload: "${mqtt_remote_device3_command2}"
          qos: 0
          retain: false
      - mqtt.publish:
          topic: "${mqtt_remote_device4_command_topic}"
          payload: "${mqtt_remote_device4_command2}"
          qos: 0
          retain: false

#:########################################################################################:#
# BINARY SENSOR
# https://esphome.io/components/binary_sensor/
#:########################################################################################:#
binary_sensor:
  - platform: template
    name: "mmWave Occupancy or Movement"
    id: ld1125h_movocc_binary
    device_class: occupancy
    on_press:
      - script.execute: send_remote_light_on_commands
    on_release:
      - script.execute: send_remote_light_timer_commands

  - platform: template
    name: "mmWave Movement"
    id: ld1125h_mov_binary
    device_class: motion
