#:########################################################################################:# # 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.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) # - 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 still functions; HA entities will update again when HA returns. # b) MQTT OFFLINE (or broker unreachable) # - Device continues running locally and via HA API (if HA is reachable). # - MQTT-based integrations/updates will not function until MQTT returns. # c) Entire WiFi/Network OFFLINE # - Device continues running locally, 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.0" # 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 # 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 #:########################################################################################:# # 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: 2024.6.0 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(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(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(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(rmaxst.begin(), rmaxst.end()); #:########################################################################################:# # ESP PLATFORM AND FRAMEWORK # https://esphome.io/components/esp32.html #:########################################################################################:# esp32: board: esp32dev framework: # type: arduino 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}" 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(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(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(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(rmaxst.begin(), rmaxst.end()); - platform: template name: "Clearence 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" #:########################################################################################:# # BINARY SENSOR # https://esphome.io/components/binary_sensor/ #:########################################################################################:# binary_sensor: - platform: template name: "mmWave Occupancy or Movement" id: ld1125h_movocc_binary device_class: occupancy - platform: template name: "mmWave Movement" id: ld1125h_mov_binary device_class: motion