changes to esphome devices and packages to start removing node red dependencies

This commit is contained in:
root
2026-04-25 21:05:32 +12:00
parent 51bff34d2e
commit 15f348570f
71 changed files with 1322 additions and 202 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 930 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 930 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+1 -1
View File
@@ -1 +1 @@
{"pid": 67, "version": 1, "ha_version": "2026.4.1", "start_ts": 1775275075.484253}
{"pid": 67, "version": 1, "ha_version": "2026.4.4", "start_ts": 1777063274.5674608}
+133 -36
View File
@@ -6,6 +6,11 @@
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-gasheater.yaml
#:########################################################################################:#
# VERSIONS:
# V1.11 2026-04-21 Reboot now enters temporary FULL OFF hold before returning to BYPASS;
# FULL OFF is remembered across reboots if it was already active
# V1.10 2026-04-21 Added FULL OFF mode and reboot-safe shutdown to force heater OFF
# after ESPHome restart; fixed stray setpoint typo
# V1.8 2026-04-21 Physical testing, swapped GPIO12 and 14 (had the buttons/relays inverted)
# V1.7 2026-03-26 Added Heater Operation switch; MQTT status now mirrors the Status Text
# entity; preserved Test Mode AUTO test temperature and visible 6-step calibration
# V1.6 2026-03-22 Added richer status text: At Temperature, Operating (Level X),
@@ -27,9 +32,9 @@
# - 3-relay daughterboard
# - Rinnai Neo gas heater
# - Relay outputs:
# - GPIO12 = ON/OFF button press
# - GPIO14 = ON/OFF button press
# - GPIO13 = Flame UP button press
# - GPIO14 = Flame DOWN button press
# - GPIO12 = Flame DOWN button press
# - LED inputs:
# - GPIO4 = Red standby LED
# - GPIO5 = Blue operating LED
@@ -59,7 +64,12 @@
# - Heater Operation is a high-level thermostat enable helper:
# - ON = turn Test Mode OFF (if needed) and set Mode to AUTO
# - OFF = turn heater Power OFF and set Mode to BYPASS (Remote Only)
# - On normal ESPHome startup, Mode defaults to BYPASS (Remote Only).
# - FULL OFF mode is a stronger safety state than BYPASS:
# - If either heater LED shows the heater is still on, ESPHome will pulse the power
# button after a short delay to force the heater OFF
# - On normal ESPHome startup, the controller first enters FULL OFF for a short hold time
# to verify the heater is really off, then returns to BYPASS (Remote Only).
# - If FULL OFF was already active before reboot, FULL OFF is restored and remembered.
# - On power ON in AUTO, the controller:
# - Applies a warmup lockout
# - Calibrates to level 1 using 6x DOWN
@@ -109,7 +119,7 @@ substitutions:
# Project Naming
project_name: "Sonoff Technologies.Sonoff DEV"
project_version: "v1.7"
project_version: "v1.11"
# Passwords & Secrets
api_key: !secret esp-api_key
@@ -135,6 +145,9 @@ substitutions:
temp_stale_timeout_min: "20"
fast_ramp_seconds: "240"
fast_ramp_step_s: "4"
safe_power_off_delay_s: "2"
full_off_check_interval_s: "5"
boot_full_off_hold_s: "10"
# Optional anti short-cycle (set to "0" to disable either one)
min_on_minutes: "1"
@@ -213,20 +226,33 @@ esphome:
id(last_temp_ms) = millis();
id(warmup_until_ms) = millis() + (${warmup_seconds} * 1000UL);
- delay: ${warmup_seconds}s
- if:
condition:
lambda: 'return id(powered_state).state;'
lambda: 'return id(full_off_persist);'
then:
- if:
condition:
lambda: 'return id(mode_select).state != "BYPASS (Remote Only)";'
then:
- logger.log: "Boot: heater already powered. Calibrating to level 1."
- script.execute: calibrate_to_min
- logger.log: "Boot: FULL OFF was active before reboot; restoring FULL OFF."
- select.set:
id: mode_select
option: "FULL OFF"
- script.execute: enforce_full_off
else:
- logger.log: "Boot: heater already powered. BYPASS (Remote Only) -> skipping calibration."
- logger.log: "Boot: entering temporary FULL OFF hold before BYPASS."
- select.set:
id: mode_select
option: "FULL OFF"
- script.execute: enforce_full_off
- delay: ${boot_full_off_hold_s}s
- script.execute: recompute_powered_state
- if:
condition:
lambda: 'return !id(powered_state).state;'
then:
- logger.log: "Boot: heater appears OFF after FULL OFF hold; switching to BYPASS."
- select.set:
id: mode_select
option: "BYPASS (Remote Only)"
else:
- logger.log: "Boot: heater still appears ON after FULL OFF hold; staying in FULL OFF."
##########################################################################################
# ESP PLATFORM
##########################################################################################
@@ -338,6 +364,13 @@ globals:
restore_value: no
initial_value: "false"
# Persist whether FULL OFF was the active mode before reboot.
# If true, reboot returns to FULL OFF instead of temporary FULL OFF then BYPASS.
- id: full_off_persist
type: bool
restore_value: yes
initial_value: "false"
##########################################################################################
# ESPHOME LOGGING ENABLE
##########################################################################################
@@ -363,6 +396,11 @@ binary_sensor:
entity_category: diagnostic
on_state:
- script.execute: recompute_powered_state
- if:
condition:
lambda: 'return !id(test_mode_enabled).state && id(mode_select).state == "FULL OFF" && (id(led_operating_blue).state || id(led_standby_red).state);'
then:
- script.execute: enforce_full_off
- platform: gpio
id: led_operating_blue
@@ -378,6 +416,11 @@ binary_sensor:
entity_category: diagnostic
on_state:
- script.execute: recompute_powered_state
- if:
condition:
lambda: 'return !id(test_mode_enabled).state && id(mode_select).state == "FULL OFF" && (id(led_operating_blue).state || id(led_standby_red).state);'
then:
- script.execute: enforce_full_off
- platform: template
id: powered_state
@@ -413,7 +456,7 @@ switch:
internal: true
restore_mode: ALWAYS_OFF
pin:
number: GPIO12
number: GPIO14
inverted: ${relay_inverted}
- platform: gpio
@@ -429,7 +472,7 @@ switch:
internal: true
restore_mode: ALWAYS_OFF
pin:
number: GPIO14
number: GPIO12
inverted: ${relay_inverted}
- platform: template
@@ -463,6 +506,11 @@ switch:
return id(mode_select).state == "AUTO";
turn_on_action:
- if:
condition:
switch.is_on: test_mode_enabled
then:
- switch.turn_off: test_mode_enabled
- select.set:
id: mode_select
option: "AUTO"
@@ -482,9 +530,9 @@ switch:
turn_on_action:
- if:
condition:
lambda: 'return id(mode_select).state == "BYPASS (Remote Only)" && !id(test_mode_enabled).state;'
lambda: 'return (id(mode_select).state == "BYPASS (Remote Only)" || id(mode_select).state == "FULL OFF") && !id(test_mode_enabled).state;'
then:
- logger.log: "BYPASS (Remote Only): ignoring Power ON."
- logger.log: "BYPASS/FULL OFF: ignoring Power ON."
else:
- if:
condition:
@@ -556,7 +604,8 @@ sensor:
if (v > vmax) v = vmax;
id(sp_suppress_mqtt) = true;
id(setpoint_c) = v;
const float step = ${setpoint_step_c};
id(setpoint_c) = roundf(v / step) * step;
- number.set:
id: setpoint_number
value: !lambda 'return id(setpoint_c);'
@@ -577,10 +626,6 @@ text_sensor:
const float at_temp_threshold = id(setpoint_c) + ${off_above_c};
const std::string mode = id(mode_select).state;
if (!use_test_temp && id(room_temp_lost_fault)) {
return std::string("Fault (Room Temperature Lost)");
}
if (id(calibrating)) {
if (use_test_temp) return std::string("Calibrating (Test)");
return std::string("Calibrating");
@@ -608,12 +653,22 @@ text_sensor:
return std::string("Off (Test)");
}
if (mode == "FULL OFF") {
if (id(led_operating_blue).state) return std::string("Full Off (Operating)");
if (id(led_standby_red).state) return std::string("Full Off (Standby)");
return std::string("Full Off (Off)");
}
if (mode == "BYPASS (Remote Only)") {
if (id(led_operating_blue).state) return std::string("Remote Only (Operating)");
if (id(led_standby_red).state) return std::string("Remote Only (Standby)");
return std::string("Remote Only (Off)");
}
if (!use_test_temp && id(room_temp_lost_fault)) {
return std::string("Fault (Room Temperature Lost)");
}
if (mode == "MANUAL") {
if (id(led_operating_blue).state) {
char buf[32];
@@ -660,14 +715,28 @@ text_sensor:
##########################################################################################
# SELECT COMPONENT
##########################################################################################
# Mode meanings:
# - AUTO = thermostat active
# - MANUAL = manual flame control allowed
# - BYPASS (Remote Only) = ESPHome control bypassed, heater may still be used externally
# - FULL OFF = ESPHome actively forces the heater OFF if it detects either LED on
select:
- platform: template
id: mode_select
name: "Mode"
options: ["AUTO","MANUAL","BYPASS (Remote Only)"]
options: ["AUTO","MANUAL","BYPASS (Remote Only)","FULL OFF"]
optimistic: true
initial_option: "BYPASS (Remote Only)"
restore_value: false
set_action:
- lambda: |-
id(full_off_persist) = (x == "FULL OFF");
if (x != "AUTO") id(room_temp_lost_fault) = false;
- if:
condition:
lambda: 'return x == "FULL OFF";'
then:
- script.execute: enforce_full_off
- platform: template
id: flame_level_select
@@ -686,7 +755,7 @@ select:
else:
- if:
condition:
lambda: 'return id(mode_select).state != "BYPASS (Remote Only)";'
lambda: 'return id(mode_select).state == "AUTO" || id(mode_select).state == "MANUAL";'
then:
- select.set:
id: mode_select
@@ -724,7 +793,7 @@ select:
else:
- script.execute: adjust_to_desired
else:
- logger.log: "BYPASS (Remote Only): ignoring Flame Level selection."
- logger.log: "BYPASS/FULL OFF: ignoring Flame Level selection."
##########################################################################################
# BUTTON COMPONENT
@@ -736,14 +805,14 @@ button:
on_press:
- if:
condition:
lambda: 'return id(mode_select).state != "BYPASS (Remote Only)";'
lambda: 'return id(mode_select).state == "AUTO" || id(mode_select).state == "MANUAL";'
then:
- select.set:
id: mode_select
option: "MANUAL"
- script.execute: step_up_once
else:
- logger.log: "BYPASS (Remote Only): ignoring Flame Up (step)."
- logger.log: "BYPASS/FULL OFF: ignoring Flame Up (step)."
- platform: template
id: btn_flame_down
@@ -751,14 +820,14 @@ button:
on_press:
- if:
condition:
lambda: 'return id(mode_select).state != "BYPASS (Remote Only)";'
lambda: 'return id(mode_select).state == "AUTO" || id(mode_select).state == "MANUAL";'
then:
- select.set:
id: mode_select
option: "MANUAL"
- script.execute: step_down_once
else:
- logger.log: "BYPASS (Remote Only): ignoring Flame Down (step)."
- logger.log: "BYPASS/FULL OFF: ignoring Flame Down (step)."
- platform: template
id: btn_calibrate_min
@@ -777,6 +846,8 @@ button:
float v = id(setpoint_c) + step;
const float vmax = ${setpoint_max_c};
if (v > vmax) v = vmax;
// Snap to the nearest configured step to avoid float drift
v = roundf(v / step) * step;
id(setpoint_c) = v;
- number.set:
id: setpoint_number
@@ -792,6 +863,8 @@ button:
float v = id(setpoint_c) - step;
const float vmin = ${setpoint_min_c};
if (v < vmin) v = vmin;
// Snap to the nearest configured step to avoid float drift
v = roundf(v / step) * step;
id(setpoint_c) = v;
- number.set:
id: setpoint_number
@@ -814,7 +887,8 @@ number:
return id(setpoint_c);
set_action:
- lambda: |-
id(setpoint_c) = x;
const float step = ${setpoint_step_c};
id(setpoint_c) = roundf(x / step) * step;
- if:
condition:
lambda: |-
@@ -856,6 +930,15 @@ interval:
then:
- script.execute: auto_control_tick
# In FULL OFF, keep checking that the heater is really off.
- interval: ${full_off_check_interval_s}s
then:
- if:
condition:
lambda: 'return !id(test_mode_enabled).state && id(mode_select).state == "FULL OFF" && id(powered_state).state;'
then:
- script.execute: enforce_full_off
##########################################################################################
# SCRIPT COMPONENT
##########################################################################################
@@ -883,6 +966,20 @@ script:
id(powered_state).publish_state(new_state);
- script.execute: publish_status
# FULL OFF enforcement helper.
# After a short delay, if the heater still looks on, pulse the power button once.
- id: enforce_full_off
mode: restart
then:
- delay: ${safe_power_off_delay_s}s
- script.execute: recompute_powered_state
- if:
condition:
lambda: 'return !id(test_mode_enabled).state && id(mode_select).state == "FULL OFF" && id(powered_state).state;'
then:
- logger.log: "FULL OFF: heater still appears ON, pulsing power to force OFF."
- script.execute: press_power
# Low-level relay click helper.
# Handles the button press pulse and enforces a minimum gap between clicks.
- id: press_with_gap
@@ -923,25 +1020,25 @@ script:
then:
- if:
condition:
lambda: 'return id(mode_select).state != "BYPASS (Remote Only)";'
lambda: 'return id(mode_select).state == "AUTO" || id(mode_select).state == "MANUAL";'
then:
- script.execute:
id: press_with_gap
which: "up"
else:
- logger.log: "BYPASS (Remote Only): ignoring UP press."
- logger.log: "BYPASS/FULL OFF: ignoring UP press."
- id: press_down
then:
- if:
condition:
lambda: 'return id(mode_select).state != "BYPASS (Remote Only)";'
lambda: 'return id(mode_select).state == "AUTO" || id(mode_select).state == "MANUAL";'
then:
- script.execute:
id: press_with_gap
which: "down"
else:
- logger.log: "BYPASS (Remote Only): ignoring DOWN press."
- logger.log: "BYPASS/FULL OFF: ignoring DOWN press."
# Step one flame level UP and only update the visible level after the relay click finishes.
- id: step_up_once
@@ -1058,7 +1155,7 @@ script:
then:
- if:
condition:
lambda: 'return id(mode_select).state != "BYPASS (Remote Only)";'
lambda: 'return id(mode_select).state == "AUTO" || id(mode_select).state == "MANUAL";'
then:
- if:
condition:
+69 -9
View File
@@ -6,6 +6,7 @@
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-laundryenv.yaml
#:########################################################################################:#
# VERSIONS:
# V1.2 2026-04-25 Added PIR MQTT light timer control, updated notes, added PIR debounce filters
# V1.1 2026-03-11 Updated yaml to zorruno layout V1.1
# V1.0 2025-03-28 Initial Version
#:########################################################################################:#
@@ -21,21 +22,36 @@
# - Provides laundry temperature and humidity monitoring
# - Provides a PIR motion binary sensor
# - Provides a smoke detector binary sensor
# - Designed as a simple environmental and safety monitoring node
# - No relay or timed control logic is used in this yaml
# - PIR Detected publishes an MQTT On command to the Laundry Lights device
# - PIR Clear publishes an MQTT timer payload to the Laundry Lights device
# - The Laundry Lights device performs the actual relay control and timer countdown
# - The remote timer payload is in seconds and should match the accepted range on the light switch
# - This device does not directly control a relay
#:########################################################################################:#
# MQTT COMMANDS:
# - Remote light command topic:
# - ${mqtt_remote_device1_command_topic}
# - PIR Detected sends:
# - ${mqtt_remote_device1_command1}
# - PIR Clear sends:
# - ${mqtt_remote_device1_command2}
# - The timer payload is in seconds
#:########################################################################################:#
# OFFLINE NOTES:
# a) Home Assistant offline (network and MQTT online):
# - Device continues to read and publish local sensor states
# - PIR, smoke, temperature, and humidity sensing continue locally
# - PIR MQTT light control continues if the MQTT broker and target device are online
#
# b) MQTT offline (or HA/MQTT offline):
# - Device continues sampling sensors locally
# - No MQTT-based status publishing will be available
# - PIR, smoke, temperature, and humidity sensing continue locally
# - PIR MQTT light control is unavailable
#
# c) Entire WiFi/Network offline:
# - Device continues sampling sensors locally
# - No HA or MQTT connectivity will be available
# - PIR MQTT light control is unavailable
# - Accurate time is not needed; SNTP is not required for operation
#:########################################################################################:#
@@ -53,20 +69,32 @@ substitutions:
# Project Naming
project_name: "Sonoff Technologies.Sonoff DEV"
project_version: "v1.1"
# Entity Naming
entity_prefix: "Laundry"
project_version: "v1.2"
# Passwords and Secrets
api_key: !secret esp-laundryenv_api_key
ota_pass: !secret esp-laundryenv_ota_pass
static_ip_address: !secret esp-laundryenv_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"
update_interval: "60s"
entity_prefix: "Laundry"
# Remote light timer payloads, in seconds.
remote_timer_lights_payload: "120"
# MQTT REMOTE Controls
mqtt_remote_device1_name: "laundry-lights"
mqtt_remote_device1_command_topic: "${mqtt_command_main_topic}/${mqtt_remote_device1_name}"
mqtt_remote_device1_command1: "On"
mqtt_remote_device1_command2: "${remote_timer_lights_payload}"
#:########################################################################################:#
# PACKAGES:
@@ -153,12 +181,37 @@ sensor:
name: "${entity_prefix} Humidity"
update_interval: "${update_interval}"
#:########################################################################################:#
# SCRIPT COMPONENT:
# MQTT commands sent to the remote Laundry Lights device
# 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
- 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
#:########################################################################################:#
# BINARY SENSORS:
# https://esphome.io/components/binary_sensor/
#:########################################################################################:#
binary_sensor:
- platform: gpio
id: laundry_pir
pin:
number: GPIO13
mode:
@@ -166,10 +219,16 @@ binary_sensor:
pullup: false
name: "${entity_prefix} PIR"
device_class: motion
#filters:
# - invert:
filters:
- delayed_on: 100ms
- delayed_off: 1s
on_press:
- script.execute: send_remote_light_on_commands
on_release:
- script.execute: send_remote_light_timer_commands
- platform: gpio
id: laundry_smoke_detector
pin:
number: GPIO5
mode:
@@ -177,3 +236,4 @@ binary_sensor:
pullup: true
inverted: true
name: "${entity_prefix} Smoke Detector"
device_class: smoke
+94 -94
View File
@@ -6,6 +6,7 @@
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-laundrylights.yaml
#:########################################################################################:#
# VERSIONS:
# V3.7 2026-04-24 Reviewed MQTT timer control, corrected notes, added local MQTT status publishing
# V3.6 2026-03-11 Updated yaml to zorruno layout V1.1
# V3.5 2025-07-24 YAML tidyups
# V3.0 2025-06-05 YAML tidyups
@@ -25,38 +26,43 @@
#:########################################################################################:#
# OPERATION NOTES:
# - Button 1 toggles Relay 1 for the laundry lights
# - Button 2 supports two actions:
# - Single click toggles Relay 2 locally
# - Hold for 2 seconds or more sends MQTT ON/OFF to the remote device
# - Button 2 single click toggles Relay 2 locally
# - Relay 2 is turned ON at boot
# - Remote MQTT status is subscribed to and normalised to ON/OFF/TRUE/FALSE/1/0
# - If the remote state is unknown, a hold on Button 2 defaults to sending ON
# - MQTT command topic supports external control of Relay 1, such as from an occupancy sensor
# - MQTT payload On turns Relay 1 ON and cancels any active timer
# - MQTT payload Off turns Relay 1 OFF and cancels any active timer
# - MQTT numeric payloads from 1 to 3600 turn Relay 1 ON for that many seconds
# - Any MQTT payload outside 1 to 3600 seconds is ignored
# - Relay 1 status is published to the local MQTT status topic as On or Off
# - This switch is used for:
# - Laundry Lights (1)
# - Laundry Power Enable (2)
#:########################################################################################:#
# MQTT COMMANDS:
# - Remote device command topic:
# - ${mqtt_remote1_command_topic}
# - Remote device status topic:
# - ${mqtt_remote1_status_topic}
# - Hold Button 2:
# - Sends OFF if remote state is known and currently ON
# - Sends ON otherwise
# - Local command topic:
# - ${mqtt_local_command1_topic}
# - Accepted command payloads:
# - On
# - Off
# - 1 to 3600 seconds
# - Local status topic:
# - ${mqtt_local_status_topic}
# - Published status payloads:
# - On
# - Off
#:########################################################################################:#
# OFFLINE NOTES:
# a) Home Assistant offline (network and MQTT online):
# - Local buttons and local relays continue to work
# - Remote MQTT control/status for Button 2 hold still works if the MQTT broker is online
# - MQTT control and MQTT timer commands continue to work if the MQTT broker is online
#
# b) MQTT offline (or HA/MQTT offline):
# - Local buttons and local relays continue to work
# - Button 2 single click still toggles Relay 2 locally
# - Button 2 hold cannot control the remote MQTT device
# - MQTT control, MQTT timer commands, and MQTT status publishing are unavailable
#
# c) Entire WiFi/Network offline:
# - Local buttons and local relays continue to work
# - Remote MQTT control/status is unavailable
# - MQTT control, MQTT timer commands, and MQTT status publishing are unavailable
# - Accurate time is not needed; SNTP is not required for operation
#:########################################################################################:#
@@ -74,35 +80,32 @@ substitutions:
# Project Naming
project_name: "Zemismart Technologies.KS-811 Double" # Project Details
project_version: "v3.6" # Project V denotes release of yaml file, allowing checking of deployed vs latest version
project_version: "v3.7" # Project V denotes release of yaml file, allowing checking of deployed vs latest version
# Passwords & Secrets
api_key: !secret esp-api_key # unfortunately you can't use substitutions inside secrets names
ota_pass: !secret esp-ota_pass # unfortunately you can't use substitutions inside secrets names
static_ip_address: !secret esp-laundrylights_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}
mqtt_command_main_topic: !secret mqtt_command_main_topic
mqtt_status_main_topic: !secret mqtt_status_main_topic
# Device Settings
#relay_icon: "mdi:lightbulb-group"
log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
update_interval: "60s" # update time for for general sensors etc
# MQTT REMOTE Controls
mqtt_remote1_device_name: "3dprinter-power"
mqtt_remote1_command_topic: "${mqtt_command_main_topic}/${mqtt_remote1_device_name}" # Topic we will use to command this locally without HA
mqtt_remote1_status_topic: "${mqtt_status_main_topic}/${mqtt_remote1_device_name}" # Topic we will use to view status locally without HA
# MQTT LOCAL Controls
mqtt_local_device1_name: "laundry-lights"
mqtt_local_command1_topic: "${mqtt_command_main_topic}/${mqtt_local_device1_name}" # Topic that remote things will use to command this locally without HA
mqtt_local_status_topic: "${mqtt_status_main_topic}/${mqtt_local_device1_name}" # Topic that remote things will use to view local Relay 1 status without HA
# Switch Naming
switch_1_name: "Laundry Lights"
switch_2_name: "Laundry Power Enable" # This is virtual only, no power connected to 3rd relay
#switch_3_name: "Fan 3 Relay"
#switch_4_name: "Fan 4 Relay"
switch_2_name: "Laundry Power Enable"
#:########################################################################################:#
# PACKAGES:
@@ -189,20 +192,6 @@ status_led:
number: GPIO2
inverted: true
#:########################################################################################:#
# GLOBAL VARIABLES:
#:########################################################################################:#
globals:
- id: remote1_state
type: bool
restore_value: false
initial_value: "false"
- id: remote1_known
type: bool
restore_value: false
initial_value: "false"
#:########################################################################################:#
# BINARY SENSORS:
# https://esphome.io/components/binary_sensor/
@@ -213,6 +202,7 @@ binary_sensor:
number: GPIO16
mode: INPUT
inverted: true
use_interrupt: false # This device GPIO pin doesn't support them
name: "Button 1: ${switch_1_name}"
on_press:
- switch.toggle: Relay_1
@@ -224,7 +214,7 @@ binary_sensor:
inverted: true
name: "Button 2: ${switch_2_name}"
on_multi_click:
# SINGLE CLICK: quick tap -> toggle Relay_2
# SINGLE CLICK: quick tap toggles Relay_2
- timing:
- ON for at most 0.5s
- OFF for at least 0.5s
@@ -238,64 +228,66 @@ binary_sensor:
else:
- switch.turn_on: Relay_2
# HOLD: pressed for 2s or more -> toggle remote; default to ON if unknown
- timing:
- ON for at least 2s
#:########################################################################################:#
# MQTT COMPONENT:
# Device-specific MQTT command triggers
# https://esphome.io/components/mqtt.html
#:########################################################################################:#
mqtt:
on_message:
- topic: "${mqtt_local_command1_topic}"
payload: "On"
then:
- if:
condition:
lambda: |-
return id(remote1_known) && id(remote1_state); // known AND currently ON
then:
- mqtt.publish:
topic: "${mqtt_remote1_command_topic}"
payload: "OFF"
retain: false
else:
- mqtt.publish:
topic: "${mqtt_remote1_command_topic}"
payload: "ON"
retain: false
- script.stop: relay_1_timer_script
- switch.turn_on: Relay_1
#:########################################################################################:#
# TEXT SENSORS:
#:########################################################################################:#
text_sensor:
- platform: mqtt_subscribe
id: remote1_status
internal: true
topic: "${mqtt_remote1_status_topic}"
on_value:
- topic: "${mqtt_local_command1_topic}"
payload: "Off"
then:
- script.stop: relay_1_timer_script
- switch.turn_off: Relay_1
- topic: "${mqtt_local_command1_topic}"
then:
- lambda: |-
// copy, trim whitespace, uppercase
std::string s = x;
// trim leading/trailing whitespace
auto notspace = [](int ch) { return !std::isspace(ch); };
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notspace));
s.erase(std::find_if(s.rbegin(), s.rend(), notspace).base(), s.end());
for (auto &c : s) c = (char) toupper(c);
bool recognized = false;
if (s == "ON" || s == "1" || s == "TRUE") {
id(remote1_state) = true;
id(remote1_known) = true;
recognized = true;
} else if (s == "OFF" || s == "0" || s == "FALSE") {
id(remote1_state) = false;
id(remote1_known) = true;
recognized = true;
} else {
// unknown payload (e.g. "ONLINE", "OFFLINE", JSON, etc.)
id(remote1_known) = false;
if (x.empty()) {
return;
}
ESP_LOGI("remote1_status", "Parsed status='%s' -> known=%s state=%s",
s.c_str(),
id(remote1_known) ? "true" : "false",
id(remote1_state) ? "ON" : "OFF");
bool numeric_payload = true;
for (char c : x) {
if (c < '0' || c > '9') {
numeric_payload = false;
break;
}
}
if (!numeric_payload) {
return;
}
const int timer_seconds = atoi(x.c_str());
if (timer_seconds < 1 || timer_seconds > 3600) {
return;
}
id(relay_1_timer_script)->execute(timer_seconds * 1000);
#:########################################################################################:#
# SCRIPT COMPONENT:
# Relay 1 timer script
# mode: restart means a new valid timer value restarts the timer from zero
# https://esphome.io/components/script.html
#:########################################################################################:#
script:
- id: relay_1_timer_script
mode: restart
parameters:
timer_duration_ms: int
then:
- switch.turn_on: Relay_1
- delay: !lambda "return timer_duration_ms;"
- switch.turn_off: Relay_1
#:########################################################################################:#
# SWITCH COMPONENT:
@@ -306,6 +298,14 @@ switch:
name: "Relay 1: ${switch_1_name}"
pin: GPIO13
id: Relay_1
on_turn_on:
- mqtt.publish:
topic: "${mqtt_local_status_topic}"
payload: "On"
on_turn_off:
- mqtt.publish:
topic: "${mqtt_local_status_topic}"
payload: "Off"
- platform: gpio
name: "Relay 2: ${switch_2_name}"
+1 -1
View File
@@ -85,7 +85,7 @@ substitutions:
# MQTT REMOTE Controls
mqtt_remote_device1_name: "masterbath-towelrail"
mqtt_remote_device1_command_topic: "${mqtt_command_main_topic}/${mqtt_remote_device1_name}"
mqtt_remote_device1_command_topic: "${mqtt_command_main_topic}/${mqtt_remote_device1_name}/operation"
mqtt_remote_device1_command1: "BOOST"
+99 -10
View File
@@ -6,6 +6,7 @@
# 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:
@@ -32,17 +33,21 @@
# - 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 still functions; HA entities will update again when HA returns.
# - 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 integrations/updates will not function until MQTT returns.
# - MQTT-based remote timer commands will not function until MQTT returns.
# c) Entire WiFi/Network OFFLINE
# - Device continues running locally, but HA/MQTT integration is unavailable.
# - 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.
#:########################################################################################:#
@@ -61,14 +66,16 @@ substitutions:
# Project Naming
project_name: "HiLink.LD1125H Office Occupancy"
project_version: "v1.0"
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
# 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}
@@ -76,6 +83,31 @@ substitutions:
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
@@ -97,12 +129,12 @@ packages:
local_device_name: "${device_name}"
#### SNTP (Only use if you want/need accurate timeclocks) ####
common_sntp: !include common/sntp_common.yaml
#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_debug: !include common/include_debug_diag_sensors.yaml
#diag_resetcount: !include common/include_resetcount_diag_sensors.yaml
#:########################################################################################:#
@@ -114,7 +146,7 @@ esphome:
friendly_name: "${friendly_name}"
comment: "${description_comment}"
area: "${device_area}"
min_version: 2024.6.0
min_version: 2026.4.1
project:
name: "${project_name}"
version: "${project_version}"
@@ -156,7 +188,6 @@ esphome:
esp32:
board: esp32dev
framework:
# type: arduino
type: esp-idf
version: recommended
@@ -185,6 +216,7 @@ logger:
api:
encryption:
key: "${api_key}"
reboot_timeout: 0s
on_client_connected:
- esp32_ble_tracker.start_scan:
continuous: true
@@ -355,7 +387,7 @@ number:
return std::vector<uint8_t>(rmaxst.begin(), rmaxst.end());
- platform: template
name: "Clearence Time (s)"
name: "Clearance Time (s)"
id: ld1125h_clear_time
icon: "mdi:cogs"
optimistic: true
@@ -466,6 +498,59 @@ text_sensor:
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/
@@ -475,6 +560,10 @@ binary_sensor:
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"
+146 -42
View File
@@ -6,6 +6,8 @@
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-officeduallights.yaml
#:########################################################################################:#
# VERSIONS:
# V2.7 2026-04-24 Updated MQTT local topics, retain false, and timed control using seconds
# V2.6 2026-04-23 Added independent MQTT timed ON control for relay1 and relay2
# V2.5 2026-03-11 Updated yaml to zorruno layout V1.1
# V2.4 2025-06-30 Tidied up MQTT direct relay control (esps can control each other via MQTT)
# V2.3 2025-06-18 Added MQTT direct relay control
@@ -22,21 +24,27 @@
# - Controls 2 office light circuits:
# - Overhead cool white lights
# - Right hand warm bunker light
# - Local button 1 toggles relay/light 1
# - Local button 2 toggles relay/light 2
# - Relay state changes are sent out via MQTT status topics
# - Local button 1 toggles relay/light 1 and cancels any active timer for relay 1
# - Local button 2 toggles relay/light 2 and cancels any active timer for relay 2
# - Relay state changes are sent out via MQTT status topics with retain false
# - MQTT can command each relay with:
# - On = turn on with no timer and cancel any active timer
# - Off = turn off immediately and cancel any active timer
# - 1 to 3600 = turn on for that many seconds, then turn off
# - If a new valid timed value is received while a timer is already active, the timer restarts from zero
# - Invalid MQTT timer values are ignored
# - Template binary lights drive the template relay switches
# - Sonoff Dual R1 requires UART serial packets to operate the physical relays
#
# MQTT COMMANDS:
# - Command relay 1:
# - ${mqtt_local_command_topic}/relay1/set ON or OFF
# - ${mqtt_local_command1_topic} On, Off, or 1 to 3600 seconds
# - Command relay 2:
# - ${mqtt_local_command_topic}/relay2/set ON or OFF
# - ${mqtt_local_command2_topic} On, Off, or 1 to 3600 seconds
# - Relay 1 status publishes to:
# - ${mqtt_local_status_topic}/relay1/state
# - ${mqtt_local_status1_topic} On or Off
# - Relay 2 status publishes to:
# - ${mqtt_local_status_topic}/relay2/state
# - ${mqtt_local_status2_topic} On or Off
#:########################################################################################:#
# OFFLINE NOTES:
# a) Home Assistant offline (network and MQTT still online):
@@ -68,25 +76,34 @@ substitutions:
# Project Naming
project_name: "Sonoff Technologies.Sonoff Dual R1"
project_version: "v2.5"
project_version: "v2.7"
# Passwords and Secrets
api_key: !secret esp-api_key
ota_pass: !secret esp-ota_pass
static_ip_address: !secret esp-officeduallights_ip
current_ip_address: ${static_ip_address}
mqtt_local_command_main_topic: !secret mqtt_command_main_topic
mqtt_local_status_main_topic: !secret mqtt_status_main_topic
mqtt_command_main_topic: !secret mqtt_command_main_topic
mqtt_status_main_topic: !secret mqtt_status_main_topic
# Device Settings
relay_icon: "mdi:lightbulb-group"
log_level: "NONE"
update_interval: "60s"
# MQTT Controls
mqtt_device_name: "office-dual-lights"
mqtt_local_command_topic: "${mqtt_local_command_main_topic}/${mqtt_device_name}"
mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_device_name}"
# MQTT LOCAL Controls
mqtt_local_device1_name: "office-dual-lights-right"
mqtt_local_command1_topic: "${mqtt_command_main_topic}/${mqtt_local_device1_name}"
mqtt_local_status1_topic: "${mqtt_status_main_topic}/${mqtt_local_device1_name}"
mqtt_local_device2_name: "office-dual-lights-left"
mqtt_local_command2_topic: "${mqtt_command_main_topic}/${mqtt_local_device2_name}"
mqtt_local_status2_topic: "${mqtt_status_main_topic}/${mqtt_local_device2_name}"
# MQTT REMOTE Controls
# mqtt_remote_device1_name: "masterbath-towelrail"
# mqtt_remote_device1_command_topic: "${mqtt_command_main_topic}/${mqtt_remote_device1_name}/operation"
# mqtt_remote_device1_command1: "BOOST"
# Switch Naming
switch_1_name: "Dual L1 Relay"
@@ -173,37 +190,115 @@ uart:
baud_rate: 19200
#:########################################################################################:#
# MQTT:
# MQTT COMPONENT:
# Device-specific MQTT command triggers
# https://esphome.io/components/mqtt.html
#:########################################################################################:#
mqtt:
on_message:
- topic: "${mqtt_local_command_topic}/relay1/set"
payload: "ON"
- topic: "${mqtt_local_command1_topic}"
payload: "On"
then:
- switch.turn_on: relay_1
- script.stop: relay_1_timer_script
- light.turn_on: light_1
- topic: "${mqtt_local_command_topic}/relay1/set"
payload: "OFF"
- topic: "${mqtt_local_command1_topic}"
payload: "Off"
then:
- switch.turn_off: relay_1
- script.stop: relay_1_timer_script
- light.turn_off: light_1
- topic: "${mqtt_local_command_topic}/relay2/set"
payload: "ON"
- topic: "${mqtt_local_command1_topic}"
then:
- switch.turn_on: relay_2
- lambda: |-
if (x.empty()) {
return;
}
bool numeric_payload = true;
for (char c : x) {
if (c < '0' || c > '9') {
numeric_payload = false;
break;
}
}
if (!numeric_payload) {
return;
}
const int timer_seconds = atoi(x.c_str());
if (timer_seconds < 1 || timer_seconds > 3600) {
return;
}
id(relay_1_timer_script)->execute(timer_seconds * 1000);
- topic: "${mqtt_local_command2_topic}"
payload: "On"
then:
- script.stop: relay_2_timer_script
- light.turn_on: light_2
- topic: "${mqtt_local_command_topic}/relay2/set"
payload: "OFF"
- topic: "${mqtt_local_command2_topic}"
payload: "Off"
then:
- switch.turn_off: relay_2
- script.stop: relay_2_timer_script
- light.turn_off: light_2
- topic: "${mqtt_local_command2_topic}"
then:
- lambda: |-
if (x.empty()) {
return;
}
bool numeric_payload = true;
for (char c : x) {
if (c < '0' || c > '9') {
numeric_payload = false;
break;
}
}
if (!numeric_payload) {
return;
}
const int timer_seconds = atoi(x.c_str());
if (timer_seconds < 1 || timer_seconds > 3600) {
return;
}
id(relay_2_timer_script)->execute(timer_seconds * 1000);
#:########################################################################################:#
# SCRIPT COMPONENT:
# Independent per-relay timer scripts
# mode: restart means a new valid timer value restarts the timer from zero
# https://esphome.io/components/script.html
#:########################################################################################:#
script:
- id: relay_1_timer_script
mode: restart
parameters:
timer_duration_ms: int
then:
- light.turn_on: light_1
- delay: !lambda "return timer_duration_ms;"
- light.turn_off: light_1
- id: relay_2_timer_script
mode: restart
parameters:
timer_duration_ms: int
then:
- light.turn_on: light_2
- delay: !lambda "return timer_duration_ms;"
- light.turn_off: light_2
#:########################################################################################:#
# STATUS LED:
# STATUS LED COMPONENT:
# Sonoff Dual LED is on GPIO13
# https://esphome.io/components/status_led.html
#:########################################################################################:#
@@ -213,8 +308,9 @@ status_led:
inverted: true
#:########################################################################################:#
# BINARY SENSORS:
# BINARY SENSOR COMPONENT:
# Sonoff Dual R1 buttons are GPIO04 and GPIO14
# Local button presses cancel any active timer for that relay before toggling
# https://esphome.io/components/binary_sensor/
#:########################################################################################:#
binary_sensor:
@@ -225,7 +321,8 @@ binary_sensor:
inverted: true
id: button_1
on_press:
- switch.toggle: relay_1
- script.stop: relay_1_timer_script
- light.toggle: light_1
- platform: gpio
pin:
@@ -234,11 +331,13 @@ binary_sensor:
inverted: true
id: button_2
on_press:
- switch.toggle: relay_2
- script.stop: relay_2_timer_script
- light.toggle: light_2
#:########################################################################################:#
# SWITCH:
# SWITCH COMPONENT:
# Sonoff Dual R1 requires serial data packets to switch the relays
# Relay state changes publish MQTT status updates with retain false
# https://esphome.io/components/switch/
#:########################################################################################:#
switch:
@@ -250,8 +349,9 @@ switch:
internal: true
turn_on_action:
- mqtt.publish:
topic: "${mqtt_local_status_topic}/relay1/state"
payload: "ON"
topic: "${mqtt_local_status1_topic}"
payload: "On"
retain: false
- if:
condition:
switch.is_off: relay_2
@@ -261,8 +361,9 @@ switch:
- uart.write: [0xA0, 0x04, 0x03, 0xA1]
turn_off_action:
- mqtt.publish:
topic: "${mqtt_local_status_topic}/relay1/state"
payload: "OFF"
topic: "${mqtt_local_status1_topic}"
payload: "Off"
retain: false
- if:
condition:
switch.is_off: relay_2
@@ -279,8 +380,9 @@ switch:
internal: true
turn_on_action:
- mqtt.publish:
topic: "${mqtt_local_status_topic}/relay2/state"
payload: "ON"
topic: "${mqtt_local_status2_topic}"
payload: "On"
retain: false
- if:
condition:
switch.is_off: relay_1
@@ -290,8 +392,9 @@ switch:
- uart.write: [0xA0, 0x04, 0x03, 0xA1]
turn_off_action:
- mqtt.publish:
topic: "${mqtt_local_status_topic}/relay2/state"
payload: "OFF"
topic: "${mqtt_local_status2_topic}"
payload: "Off"
retain: false
- if:
condition:
switch.is_off: relay_1
@@ -301,7 +404,7 @@ switch:
- uart.write: [0xA0, 0x04, 0x01, 0xA1]
#:########################################################################################:#
# OUTPUTS:
# OUTPUT COMPONENT:
# Template outputs drive the relay template switches when the light state changes
# https://esphome.io/components/output/
#:########################################################################################:#
@@ -329,7 +432,8 @@ output:
}
#:########################################################################################:#
# LIGHT:
# LIGHT COMPONENT:
# Main control entities for both local and MQTT control paths
# https://esphome.io/components/light/
#:########################################################################################:#
light:
+127 -7
View File
@@ -55,6 +55,8 @@ substitutions:
api_key: !secret esp-api_key
ota_pass: !secret esp-ota_pass
static_ip_address: !secret esp-officelights_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.
@@ -64,6 +66,16 @@ substitutions:
log_level: "INFO"
update_interval: "60s"
# MQTT LOCAL Controls
# Topic that remote things will use to command this locally without HA
mqtt_local_device1_name: "office-main-lights-left"
mqtt_local_command1_topic: "${mqtt_command_main_topic}/${mqtt_local_device1_name}"
mqtt_local_status1_topic: "${mqtt_status_main_topic}/${mqtt_local_device1_name}"
mqtt_local_device2_name: "office-main-lights-right"
mqtt_local_command2_topic: "${mqtt_command_main_topic}/${mqtt_local_device2_name}"
mqtt_local_status2_topic: "${mqtt_status_main_topic}/${mqtt_local_device2_name}"
# Switch Naming
switch_1_name: "Nighttime Lights" # Only one light (Bunker Light) actually connected to this relay
switch_2_name: "Daytime Lights" # Virtual only, no power connected to 2nd relay
@@ -99,7 +111,7 @@ packages:
#### 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_debug: !include common/include_debug_diag_sensors.yaml
#diag_resetcount: !include common/include_resetcount_diag_sensors.yaml
#:########################################################################################:#
@@ -160,7 +172,7 @@ binary_sensor:
use_interrupt: false
name: "Button 1: ${switch_1_name}"
on_press:
- switch.toggle: relay_1
- switch.toggle: Relay_1
# Button 2 (GPIO05)
- platform: gpio
@@ -170,7 +182,7 @@ binary_sensor:
inverted: true
name: "Button 2: ${switch_2_name}"
on_press:
- switch.toggle: relay_2
- switch.toggle: Relay_2
# Button 3 (GPIO4)
- platform: gpio
@@ -180,7 +192,115 @@ binary_sensor:
inverted: true
name: "Button 3: ${switch_3_name}"
on_press:
- switch.toggle: relay_3
- switch.toggle: Relay_3
#:########################################################################################:#
# MQTT COMPONENT:
# Device-specific MQTT command triggers
# https://esphome.io/components/mqtt.html
#:########################################################################################:#
mqtt:
on_message:
- topic: "${mqtt_local_command1_topic}"
payload: "On"
then:
- script.stop: relay_1_timer_script
- switch.turn_on: Relay_1
- topic: "${mqtt_local_command1_topic}"
payload: "Off"
then:
- script.stop: relay_1_timer_script
- switch.turn_off: Relay_1
- topic: "${mqtt_local_command1_topic}"
then:
- lambda: |-
if (x.empty()) {
return;
}
bool numeric_payload = true;
for (char c : x) {
if (c < '0' || c > '9') {
numeric_payload = false;
break;
}
}
if (!numeric_payload) {
return;
}
const int timer_seconds = atoi(x.c_str());
if (timer_seconds < 1 || timer_seconds > 3600) {
return;
}
id(relay_1_timer_script)->execute(timer_seconds * 1000);
- topic: "${mqtt_local_command2_topic}"
payload: "On"
then:
- script.stop: relay_2_timer_script
- switch.turn_on: Relay_2
- topic: "${mqtt_local_command2_topic}"
payload: "Off"
then:
- script.stop: relay_2_timer_script
- switch.turn_off: Relay_2
- topic: "${mqtt_local_command2_topic}"
then:
- lambda: |-
if (x.empty()) {
return;
}
bool numeric_payload = true;
for (char c : x) {
if (c < '0' || c > '9') {
numeric_payload = false;
break;
}
}
if (!numeric_payload) {
return;
}
const int timer_seconds = atoi(x.c_str());
if (timer_seconds < 1 || timer_seconds > 3600) {
return;
}
id(relay_2_timer_script)->execute(timer_seconds * 1000);
#:########################################################################################:#
# SCRIPT COMPONENT:
# Relay 1 timer script
# mode: restart means a new valid timer value restarts the timer from zero
# https://esphome.io/components/script.html
#:########################################################################################:#
script:
- id: relay_1_timer_script
mode: restart
parameters:
timer_duration_ms: int
then:
- switch.turn_on: Relay_1
- delay: !lambda "return timer_duration_ms;"
- switch.turn_off: Relay_1
- id: relay_2_timer_script
mode: restart
parameters:
timer_duration_ms: int
then:
- switch.turn_on: Relay_2
- delay: !lambda "return timer_duration_ms;"
- switch.turn_off: Relay_2
#:########################################################################################:#
# SWITCH
@@ -191,16 +311,16 @@ switch:
- platform: gpio
name: "Relay 1: ${switch_1_name}"
pin: GPIO13
id: relay_1
id: Relay_1
# Relay 2 (GPIO12)
- platform: gpio
name: "Relay 2: ${switch_2_name}"
pin: GPIO12
id: relay_2
id: Relay_2
# Relay 3 (GPIO14)
- platform: gpio
name: "Relay 3: ${switch_3_name}"
pin: GPIO14
id: relay_3
id: Relay_3
@@ -0,0 +1,127 @@
################################################################################
# PACKAGE: Laundry Lights Backup Occupancy Automation
################################################################################
#
# Operation:
# - Turns Laundry lights on when either of these sensors becomes active:
# - Laundry cupboard/wall PIR from ESPHome laundry environment device
# - Backup ceiling PIR from Zigbee
#
# - Turns Laundry lights off only after BOTH PIRs are clear for the configured
# delay period.
#
# Notes:
# - This is treated as a BACKUP automation.
# - The ESPHome devices should normally communicate directly via MQTT.
# - The wall PIR should still operate the lights with MQTT only.
# - This HA automation provides a fallback path using HA entity states.
#
################################################################################
################################################################################
# USER SETTINGS / PACKAGE VARIABLES
################################################################################
input_number:
laundry_lights_backup_clear_delay_seconds:
name: "Laundry Lights Backup Clear Delay Seconds"
min: 0
max: 3600
step: 1
mode: box
unit_of_measurement: s
icon: mdi:timer-outline
initial: 240
################################################################################
# AUTOMATIONS
################################################################################
automation:
- id: laundry_lights_backup_occupancy_control
alias: "Laundry Lights - Backup Occupancy Control"
description: >
Backup automation for Laundry lights. Turns lights on when either the
ESPHome wall/cupboard PIR or Zigbee ceiling PIR detects movement/occupancy.
Turns lights off after both PIRs are clear for the configured delay period.
mode: restart
triggers:
# Either PIR becoming active turns the Laundry lights on.
- trigger: state
entity_id: binary_sensor.esp_laundryenv_laundry_pir
to: "on"
id: laundry_wall_pir_detected
- trigger: state
entity_id: binary_sensor.laundry_ceiling_pir_x03ir_occupancy
to: "on"
id: laundry_ceiling_pir_detected
# Either PIR becoming clear causes a check. The action only continues
# to the off-delay when BOTH PIRs are clear.
- trigger: state
entity_id:
- binary_sensor.esp_laundryenv_laundry_pir
- binary_sensor.laundry_ceiling_pir_x03ir_occupancy
to: "off"
id: laundry_pir_clear_check
# On HA startup, re-evaluate the current state. This prevents lights
# staying on indefinitely if HA restarted while the room was already clear.
- trigger: homeassistant
event: start
id: ha_startup_check
actions:
- choose:
######################################################################
# ON LOGIC:
# If either PIR is active, turn the Laundry lights on immediately.
######################################################################
- alias: "Turn lights on if either Laundry PIR is active"
conditions:
- condition: template
value_template: >
{{
is_state('binary_sensor.esp_laundryenv_laundry_pir', 'on')
or is_state('binary_sensor.laundry_ceiling_pir_x03ir_occupancy', 'on')
}}
sequence:
- action: switch.turn_on
target:
entity_id: switch.esp_laundrylights_relay_1_laundry_lights
######################################################################
# OFF LOGIC:
# Only start the off-delay if BOTH PIRs are clear.
# After the delay, check both PIRs again before switching off.
######################################################################
- alias: "Wait, then turn lights off if both Laundry PIRs remain clear"
conditions:
- condition: template
value_template: >
{{
is_state('binary_sensor.esp_laundryenv_laundry_pir', 'off')
and is_state('binary_sensor.laundry_ceiling_pir_x03ir_occupancy', 'off')
}}
sequence:
- delay:
seconds: >
{{
states('input_number.laundry_lights_backup_clear_delay_seconds') | int(240)
}}
# Re-check after the delay in case either PIR became active again.
- condition: template
value_template: >
{{
is_state('binary_sensor.esp_laundryenv_laundry_pir', 'off')
and is_state('binary_sensor.laundry_ceiling_pir_x03ir_occupancy', 'off')
}}
# Only send OFF if the Laundry lights are currently ON.
- condition: state
entity_id: switch.esp_laundrylights_relay_1_laundry_lights
state: "on"
- action: switch.turn_off
target:
entity_id: switch.esp_laundrylights_relay_1_laundry_lights
@@ -0,0 +1,523 @@
################################################################################
# PACKAGE: Office Lights And Power Backup Occupancy Automation
################################################################################
#
# Version:
# - 1.0 - 2026-04-25
# - Initial package version.
# - Backup automation for Office lights and Office power.
# - Uses Office mmWave occupancy as the normal trigger.
# - Uses Office spare button / relay as a backup manual master.
#
# Operation:
# - Turns Office lights and power on when the Office mmWave occupancy sensor
# detects occupancy or movement.
#
# - Turns Office lights and power off after the Office mmWave occupancy sensor
# has been clear for the configured delay period.
#
# - Provides a backup manual control path using the Office spare button.
# This is useful if the occupancy sensor is offline.
#
# Notes:
# - This is treated as a BACKUP automation.
# - The standard method should still be the direct ESPHome/MQTT control path.
# - This HA automation provides a fallback path using HA entity states.
# - The physical spare button is read using:
# binary_sensor.esp_officelights_button_3_spare
# - The spare relay is used as the manual master/latch state:
# switch.esp_officelights_relay_3_spare
#
################################################################################
################################################################################
# USER SETTINGS / PACKAGE VARIABLES
################################################################################
input_number:
office_lights_backup_clear_delay_seconds:
name: "Office Lights Backup Clear Delay Seconds"
min: 0
max: 3600
step: 1
mode: box
unit_of_measurement: s
icon: mdi:timer-outline
initial: 600
################################################################################
# AUTOMATIONS
################################################################################
automation:
- id: office_lights_power_backup_occupancy_control
alias: "Office Lights And Power - Backup Occupancy Control"
description: >
Backup automation for Office lights and power. Turns required devices on
when Office mmWave occupancy or movement is detected. Turns them off after
the occupancy sensor is clear for the configured delay period. Also allows
manual backup toggling using the Office spare button and spare relay.
mode: restart
triggers:
##########################################################################
# Occupancy detected.
##########################################################################
- trigger: state
entity_id: binary_sensor.esp_occupancyoffice_mmwave_occupancy_or_movement
to: "on"
id: occupancy_detected
##########################################################################
# Occupancy clear.
##########################################################################
- trigger: state
entity_id: binary_sensor.esp_occupancyoffice_mmwave_occupancy_or_movement
to: "off"
id: occupancy_clear
##########################################################################
# Manual backup button press.
#
# The spare relay state is used as the master/latch:
# - If spare relay is OFF when the button is pressed, turn everything ON.
# - If spare relay is ON when the button is pressed, turn everything OFF.
##########################################################################
- trigger: state
entity_id: binary_sensor.esp_officelights_button_3_spare
from: "off"
to: "on"
id: backup_button_pressed
##########################################################################
# Startup check.
#
# If HA starts while the Office is already clear, this allows the normal
# off-delay logic to run.
##########################################################################
- trigger: homeassistant
event: start
id: ha_startup_check
actions:
- choose:
######################################################################
# ON LOGIC:
# Occupancy detected - turn everything on.
######################################################################
- alias: "Occupancy detected - turn Office lights and power on"
conditions:
- condition: trigger
id: occupancy_detected
sequence:
##############################################################
# Power first.
##############################################################
- alias: "Turn on Office ELV power supply if needed"
if:
- condition: state
entity_id: switch.esp_officeelvcontrol_office_elv_power_supply
state: "off"
then:
- action: switch.turn_on
target:
entity_id: switch.esp_officeelvcontrol_office_elv_power_supply
- alias: "Turn on Office right hand GPO if needed"
if:
- condition: state
entity_id: switch.esp_officedeskrightgpo_office_right_hand_gpo
state: "off"
then:
- action: switch.turn_on
target:
entity_id: switch.esp_officedeskrightgpo_office_right_hand_gpo
- alias: "Turn on Office USB Hub if needed"
if:
- condition: state
entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply
state: "off"
then:
- action: switch.turn_on
target:
entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply
##############################################################
# Lights.
##############################################################
- alias: "Turn on Office right hand bunker light if needed"
if:
- condition: state
entity_id: light.esp_officeduallights_light_1_right_hand_bunker_light
state: "off"
then:
- action: light.turn_on
target:
entity_id: light.esp_officeduallights_light_1_right_hand_bunker_light
- alias: "Turn on Office cool white overhead lights if needed"
if:
- condition: state
entity_id: light.esp_officeduallights_light_2_cool_white_overhead_lights
state: "off"
then:
- action: light.turn_on
target:
entity_id: light.esp_officeduallights_light_2_cool_white_overhead_lights
- alias: "Turn on Office nighttime lights relay if needed"
if:
- condition: state
entity_id: switch.esp_officelights_relay_1_nighttime_lights
state: "off"
then:
- action: switch.turn_on
target:
entity_id: switch.esp_officelights_relay_1_nighttime_lights
- alias: "Turn on Office daytime lights relay if needed"
if:
- condition: state
entity_id: switch.esp_officelights_relay_2_daytime_lights
state: "off"
then:
- action: switch.turn_on
target:
entity_id: switch.esp_officelights_relay_2_daytime_lights
##############################################################
# Manual master/latch relay.
##############################################################
- alias: "Turn on Office spare relay master if needed"
if:
- condition: state
entity_id: switch.esp_officelights_relay_3_spare
state: "off"
then:
- action: switch.turn_on
target:
entity_id: switch.esp_officelights_relay_3_spare
######################################################################
# OFF LOGIC:
# Occupancy clear - wait, re-check, then turn everything off.
######################################################################
- alias: "Occupancy clear - wait, re-check, then turn Office lights and power off"
conditions:
- condition: template
value_template: >
{{
trigger.id in [
'occupancy_clear',
'ha_startup_check'
]
}}
- condition: state
entity_id: binary_sensor.esp_occupancyoffice_mmwave_occupancy_or_movement
state: "off"
sequence:
- delay:
seconds: >
{{
states('input_number.office_lights_backup_clear_delay_seconds')
| int(600)
}}
# Re-check after the delay in case occupancy returned.
- condition: state
entity_id: binary_sensor.esp_occupancyoffice_mmwave_occupancy_or_movement
state: "off"
##############################################################
# Lights off first.
##############################################################
- alias: "Turn off Office right hand bunker light if needed"
if:
- condition: state
entity_id: light.esp_officeduallights_light_1_right_hand_bunker_light
state: "on"
then:
- action: light.turn_off
target:
entity_id: light.esp_officeduallights_light_1_right_hand_bunker_light
- alias: "Turn off Office cool white overhead lights if needed"
if:
- condition: state
entity_id: light.esp_officeduallights_light_2_cool_white_overhead_lights
state: "on"
then:
- action: light.turn_off
target:
entity_id: light.esp_officeduallights_light_2_cool_white_overhead_lights
- alias: "Turn off Office nighttime lights relay if needed"
if:
- condition: state
entity_id: switch.esp_officelights_relay_1_nighttime_lights
state: "on"
then:
- action: switch.turn_off
target:
entity_id: switch.esp_officelights_relay_1_nighttime_lights
- alias: "Turn off Office daytime lights relay if needed"
if:
- condition: state
entity_id: switch.esp_officelights_relay_2_daytime_lights
state: "on"
then:
- action: switch.turn_off
target:
entity_id: switch.esp_officelights_relay_2_daytime_lights
##############################################################
# Power off after lights.
##############################################################
- alias: "Turn off Office right hand GPO if needed"
if:
- condition: state
entity_id: switch.esp_officedeskrightgpo_office_right_hand_gpo
state: "on"
then:
- action: switch.turn_off
target:
entity_id: switch.esp_officedeskrightgpo_office_right_hand_gpo
- alias: "Turn off Office ELV power supply if needed"
if:
- condition: state
entity_id: switch.esp_officeelvcontrol_office_elv_power_supply
state: "on"
then:
- action: switch.turn_off
target:
entity_id: switch.esp_officeelvcontrol_office_elv_power_supply
- alias: "Turn off Office USB Hub power supply if needed"
if:
- condition: state
entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply
state: "on"
then:
- action: switch.turn_off
target:
entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply
##############################################################
# Manual master/latch relay off last.
##############################################################
- alias: "Turn off Office spare relay master if needed"
if:
- condition: state
entity_id: switch.esp_officelights_relay_3_spare
state: "on"
then:
- action: switch.turn_off
target:
entity_id: switch.esp_officelights_relay_3_spare
######################################################################
# MANUAL BACKUP LOGIC:
# Button pressed while spare relay master is ON - turn everything OFF.
######################################################################
- alias: "Backup button pressed - spare relay on, turn everything off"
conditions:
- condition: trigger
id: backup_button_pressed
- condition: state
entity_id: switch.esp_officelights_relay_3_spare
state: "on"
sequence:
##############################################################
# Lights off first.
##############################################################
- alias: "Turn off Office right hand bunker light if needed"
if:
- condition: state
entity_id: light.esp_officeduallights_light_1_right_hand_bunker_light
state: "on"
then:
- action: light.turn_off
target:
entity_id: light.esp_officeduallights_light_1_right_hand_bunker_light
- alias: "Turn off Office cool white overhead lights if needed"
if:
- condition: state
entity_id: light.esp_officeduallights_light_2_cool_white_overhead_lights
state: "on"
then:
- action: light.turn_off
target:
entity_id: light.esp_officeduallights_light_2_cool_white_overhead_lights
- alias: "Turn off Office nighttime lights relay if needed"
if:
- condition: state
entity_id: switch.esp_officelights_relay_1_nighttime_lights
state: "on"
then:
- action: switch.turn_off
target:
entity_id: switch.esp_officelights_relay_1_nighttime_lights
- alias: "Turn off Office daytime lights relay if needed"
if:
- condition: state
entity_id: switch.esp_officelights_relay_2_daytime_lights
state: "on"
then:
- action: switch.turn_off
target:
entity_id: switch.esp_officelights_relay_2_daytime_lights
##############################################################
# Power off after lights.
##############################################################
- alias: "Turn off Office right hand GPO if needed"
if:
- condition: state
entity_id: switch.esp_officedeskrightgpo_office_right_hand_gpo
state: "on"
then:
- action: switch.turn_off
target:
entity_id: switch.esp_officedeskrightgpo_office_right_hand_gpo
- alias: "Turn off Office ELV power supply if needed"
if:
- condition: state
entity_id: switch.esp_officeelvcontrol_office_elv_power_supply
state: "on"
then:
- action: switch.turn_off
target:
entity_id: switch.esp_officeelvcontrol_office_elv_power_supply
- alias: "Turn off Office USB Hub power supply if needed"
if:
- condition: state
entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply
state: "on"
then:
- action: switch.turn_off
target:
entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply
##############################################################
# Manual master/latch relay off last.
##############################################################
- alias: "Turn off Office spare relay master if needed"
if:
- condition: state
entity_id: switch.esp_officelights_relay_3_spare
state: "on"
then:
- action: switch.turn_off
target:
entity_id: switch.esp_officelights_relay_3_spare
######################################################################
# MANUAL BACKUP LOGIC:
# Button pressed while spare relay master is OFF - turn everything ON.
#
# No off timer is started from this manual press. If the occupancy
# sensor later changes to clear, the normal clear-delay logic will run.
######################################################################
- alias: "Backup button pressed - spare relay off, turn everything on"
conditions:
- condition: trigger
id: backup_button_pressed
- condition: state
entity_id: switch.esp_officelights_relay_3_spare
state: "off"
sequence:
##############################################################
# Power first.
##############################################################
- alias: "Turn on Office ELV power supply if needed"
if:
- condition: state
entity_id: switch.esp_officeelvcontrol_office_elv_power_supply
state: "off"
then:
- action: switch.turn_on
target:
entity_id: switch.esp_officeelvcontrol_office_elv_power_supply
- alias: "Turn on Office right hand GPO if needed"
if:
- condition: state
entity_id: switch.esp_officedeskrightgpo_office_right_hand_gpo
state: "off"
then:
- action: switch.turn_on
target:
entity_id: switch.esp_officedeskrightgpo_office_right_hand_gpo
- alias: "Turn on Office USB Hub power supply if needed"
if:
- condition: state
entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply
state: "off"
then:
- action: switch.turn_on
target:
entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply
##############################################################
# Lights.
##############################################################
- alias: "Turn on Office right hand bunker light if needed"
if:
- condition: state
entity_id: light.esp_officeduallights_light_1_right_hand_bunker_light
state: "off"
then:
- action: light.turn_on
target:
entity_id: light.esp_officeduallights_light_1_right_hand_bunker_light
- alias: "Turn on Office cool white overhead lights if needed"
if:
- condition: state
entity_id: light.esp_officeduallights_light_2_cool_white_overhead_lights
state: "off"
then:
- action: light.turn_on
target:
entity_id: light.esp_officeduallights_light_2_cool_white_overhead_lights
- alias: "Turn on Office nighttime lights relay if needed"
if:
- condition: state
entity_id: switch.esp_officelights_relay_1_nighttime_lights
state: "off"
then:
- action: switch.turn_on
target:
entity_id: switch.esp_officelights_relay_1_nighttime_lights
- alias: "Turn on Office daytime lights relay if needed"
if:
- condition: state
entity_id: switch.esp_officelights_relay_2_daytime_lights
state: "off"
then:
- action: switch.turn_on
target:
entity_id: switch.esp_officelights_relay_2_daytime_lights
##############################################################
# Manual master/latch relay.
##############################################################
- alias: "Turn on Office spare relay master if needed"
if:
- condition: state
entity_id: switch.esp_officelights_relay_3_spare
state: "off"
then:
- action: switch.turn_on
target:
entity_id: switch.esp_officelights_relay_3_spare