Files
zorruno-homeassistant/esphome/esp-occupancyoffice.yaml
T

572 lines
20 KiB
YAML

#:########################################################################################:#
# 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.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 timer commands to:
# - ${mqtt_remote_device1_command_topic}
# - ${mqtt_remote_device2_command_topic}
# - 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 remote timer commands still function if MQTT is online.
# - 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 remote 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.1"
# 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.
remote_timer_left_lights_payload: "10"
remote_timer_right_lights_payload: "360"
# 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_left_lights_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_right_lights_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_left_lights_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_right_lights_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