diff --git a/.cache/brands/integrations/astroweather/dark_icon.png b/.cache/brands/integrations/astroweather/dark_icon.png index 33f5493..f5b3b28 100644 Binary files a/.cache/brands/integrations/astroweather/dark_icon.png and b/.cache/brands/integrations/astroweather/dark_icon.png differ diff --git a/.cache/brands/integrations/astroweather/icon.png b/.cache/brands/integrations/astroweather/icon.png index 478ea3c..192cef1 100644 Binary files a/.cache/brands/integrations/astroweather/icon.png and b/.cache/brands/integrations/astroweather/icon.png differ diff --git a/.cache/brands/integrations/backup/dark_icon.png b/.cache/brands/integrations/backup/dark_icon.png index a60580f..199cec4 100644 Binary files a/.cache/brands/integrations/backup/dark_icon.png and b/.cache/brands/integrations/backup/dark_icon.png differ diff --git a/.cache/brands/integrations/backup/icon.png b/.cache/brands/integrations/backup/icon.png index a60580f..199cec4 100644 Binary files a/.cache/brands/integrations/backup/icon.png and b/.cache/brands/integrations/backup/icon.png differ diff --git a/.cache/brands/integrations/cloud/icon.png b/.cache/brands/integrations/cloud/icon.png new file mode 100644 index 0000000..874cd91 Binary files /dev/null and b/.cache/brands/integrations/cloud/icon.png differ diff --git a/.cache/brands/integrations/cloudflare/dark_icon.png b/.cache/brands/integrations/cloudflare/dark_icon.png new file mode 100644 index 0000000..f92db49 Binary files /dev/null and b/.cache/brands/integrations/cloudflare/dark_icon.png differ diff --git a/.cache/brands/integrations/eafm/dark_icon.png b/.cache/brands/integrations/eafm/dark_icon.png new file mode 100644 index 0000000..5531f56 Binary files /dev/null and b/.cache/brands/integrations/eafm/dark_icon.png differ diff --git a/.cache/brands/integrations/edgeos/dark_icon.png b/.cache/brands/integrations/edgeos/dark_icon.png index 58c4c81..906861f 100644 Binary files a/.cache/brands/integrations/edgeos/dark_icon.png and b/.cache/brands/integrations/edgeos/dark_icon.png differ diff --git a/.cache/brands/integrations/edgeos/icon.png b/.cache/brands/integrations/edgeos/icon.png index 787ea81..357b54a 100644 Binary files a/.cache/brands/integrations/edgeos/icon.png and b/.cache/brands/integrations/edgeos/icon.png differ diff --git a/.cache/brands/integrations/esphome/dark_icon@2x.png b/.cache/brands/integrations/esphome/dark_icon@2x.png new file mode 100644 index 0000000..94a787e Binary files /dev/null and b/.cache/brands/integrations/esphome/dark_icon@2x.png differ diff --git a/.cache/brands/integrations/everlights/dark_icon.png b/.cache/brands/integrations/everlights/dark_icon.png new file mode 100644 index 0000000..8c4a78b Binary files /dev/null and b/.cache/brands/integrations/everlights/dark_icon.png differ diff --git a/.cache/brands/integrations/fastdotcom/dark_icon.png b/.cache/brands/integrations/fastdotcom/dark_icon.png new file mode 100644 index 0000000..62a6540 Binary files /dev/null and b/.cache/brands/integrations/fastdotcom/dark_icon.png differ diff --git a/.cache/brands/integrations/feedreader/dark_icon.png b/.cache/brands/integrations/feedreader/dark_icon.png new file mode 100644 index 0000000..425a6e5 Binary files /dev/null and b/.cache/brands/integrations/feedreader/dark_icon.png differ diff --git a/.cache/brands/integrations/ffmpeg/dark_icon.png b/.cache/brands/integrations/ffmpeg/dark_icon.png new file mode 100644 index 0000000..96c2ba6 Binary files /dev/null and b/.cache/brands/integrations/ffmpeg/dark_icon.png differ diff --git a/.cache/brands/integrations/fibaro/dark_icon.png b/.cache/brands/integrations/fibaro/dark_icon.png new file mode 100644 index 0000000..c07321d Binary files /dev/null and b/.cache/brands/integrations/fibaro/dark_icon.png differ diff --git a/.cache/brands/integrations/file/dark_icon.png b/.cache/brands/integrations/file/dark_icon.png new file mode 100644 index 0000000..3dcfb26 Binary files /dev/null and b/.cache/brands/integrations/file/dark_icon.png differ diff --git a/.cache/brands/integrations/filesize/dark_icon.png b/.cache/brands/integrations/filesize/dark_icon.png new file mode 100644 index 0000000..bf671fb Binary files /dev/null and b/.cache/brands/integrations/filesize/dark_icon.png differ diff --git a/.cache/brands/integrations/fing/dark_icon.png b/.cache/brands/integrations/fing/dark_icon.png new file mode 100644 index 0000000..dee9e4c Binary files /dev/null and b/.cache/brands/integrations/fing/dark_icon.png differ diff --git a/.cache/brands/integrations/firefly_iii/dark_icon.png b/.cache/brands/integrations/firefly_iii/dark_icon.png new file mode 100644 index 0000000..aed9e87 Binary files /dev/null and b/.cache/brands/integrations/firefly_iii/dark_icon.png differ diff --git a/.cache/brands/integrations/fitbit/dark_icon.png b/.cache/brands/integrations/fitbit/dark_icon.png new file mode 100644 index 0000000..db5a0d2 Binary files /dev/null and b/.cache/brands/integrations/fitbit/dark_icon.png differ diff --git a/.cache/brands/integrations/fivem/dark_icon.png b/.cache/brands/integrations/fivem/dark_icon.png new file mode 100644 index 0000000..9674046 Binary files /dev/null and b/.cache/brands/integrations/fivem/dark_icon.png differ diff --git a/.cache/brands/integrations/fjaraskupan/dark_icon.png b/.cache/brands/integrations/fjaraskupan/dark_icon.png new file mode 100644 index 0000000..1f6d083 Binary files /dev/null and b/.cache/brands/integrations/fjaraskupan/dark_icon.png differ diff --git a/.cache/brands/integrations/flexit/dark_icon.png b/.cache/brands/integrations/flexit/dark_icon.png new file mode 100644 index 0000000..7b7f71b Binary files /dev/null and b/.cache/brands/integrations/flexit/dark_icon.png differ diff --git a/.cache/brands/integrations/flexom/dark_icon.png b/.cache/brands/integrations/flexom/dark_icon.png new file mode 100644 index 0000000..02d7b58 Binary files /dev/null and b/.cache/brands/integrations/flexom/dark_icon.png differ diff --git a/.cache/brands/integrations/flic/dark_icon.png b/.cache/brands/integrations/flic/dark_icon.png new file mode 100644 index 0000000..9af92b2 Binary files /dev/null and b/.cache/brands/integrations/flic/dark_icon.png differ diff --git a/.cache/brands/integrations/flightradar24/dark_icon.png b/.cache/brands/integrations/flightradar24/dark_icon.png new file mode 100644 index 0000000..10d6a11 Binary files /dev/null and b/.cache/brands/integrations/flightradar24/dark_icon.png differ diff --git a/.cache/brands/integrations/flightradar24/dark_logo.png b/.cache/brands/integrations/flightradar24/dark_logo.png new file mode 100644 index 0000000..cd9814a Binary files /dev/null and b/.cache/brands/integrations/flightradar24/dark_logo.png differ diff --git a/.cache/brands/integrations/flightradar24/icon.png b/.cache/brands/integrations/flightradar24/icon.png new file mode 100644 index 0000000..10d6a11 Binary files /dev/null and b/.cache/brands/integrations/flightradar24/icon.png differ diff --git a/.cache/brands/integrations/flipr/dark_icon.png b/.cache/brands/integrations/flipr/dark_icon.png new file mode 100644 index 0000000..3bc9055 Binary files /dev/null and b/.cache/brands/integrations/flipr/dark_icon.png differ diff --git a/.cache/brands/integrations/flo/dark_icon.png b/.cache/brands/integrations/flo/dark_icon.png new file mode 100644 index 0000000..50b476b Binary files /dev/null and b/.cache/brands/integrations/flo/dark_icon.png differ diff --git a/.cache/brands/integrations/flume/dark_icon.png b/.cache/brands/integrations/flume/dark_icon.png new file mode 100644 index 0000000..1536be6 Binary files /dev/null and b/.cache/brands/integrations/flume/dark_icon.png differ diff --git a/.cache/brands/integrations/fluss/dark_icon.png b/.cache/brands/integrations/fluss/dark_icon.png new file mode 100644 index 0000000..6c8b5e2 Binary files /dev/null and b/.cache/brands/integrations/fluss/dark_icon.png differ diff --git a/.cache/brands/integrations/flux_led/dark_icon.png b/.cache/brands/integrations/flux_led/dark_icon.png new file mode 100644 index 0000000..27d9e3f Binary files /dev/null and b/.cache/brands/integrations/flux_led/dark_icon.png differ diff --git a/.cache/brands/integrations/freshr/dark_icon.png b/.cache/brands/integrations/freshr/dark_icon.png new file mode 100644 index 0000000..337936b Binary files /dev/null and b/.cache/brands/integrations/freshr/dark_icon.png differ diff --git a/.cache/brands/integrations/fritzbox/dark_icon.png b/.cache/brands/integrations/fritzbox/dark_icon.png new file mode 100644 index 0000000..7542397 Binary files /dev/null and b/.cache/brands/integrations/fritzbox/dark_icon.png differ diff --git a/.cache/brands/integrations/fujitsu/dark_icon.png b/.cache/brands/integrations/fujitsu/dark_icon.png new file mode 100644 index 0000000..e57085e Binary files /dev/null and b/.cache/brands/integrations/fujitsu/dark_icon.png differ diff --git a/.cache/brands/integrations/google/dark_icon.png b/.cache/brands/integrations/google/dark_icon.png index 563b6c2..70fd449 100644 Binary files a/.cache/brands/integrations/google/dark_icon.png and b/.cache/brands/integrations/google/dark_icon.png differ diff --git a/.cache/brands/integrations/google/icon.png b/.cache/brands/integrations/google/icon.png index 563b6c2..70fd449 100644 Binary files a/.cache/brands/integrations/google/icon.png and b/.cache/brands/integrations/google/icon.png differ diff --git a/.cache/brands/integrations/gree/dark_icon.png b/.cache/brands/integrations/gree/dark_icon.png index 88c4f5d..8ac37c7 100644 Binary files a/.cache/brands/integrations/gree/dark_icon.png and b/.cache/brands/integrations/gree/dark_icon.png differ diff --git a/.cache/brands/integrations/huisbaasje/dark_icon.png b/.cache/brands/integrations/huisbaasje/dark_icon.png new file mode 100644 index 0000000..89258ae Binary files /dev/null and b/.cache/brands/integrations/huisbaasje/dark_icon.png differ diff --git a/.cache/brands/integrations/influxdb/dark_icon.png b/.cache/brands/integrations/influxdb/dark_icon.png new file mode 100644 index 0000000..e33df40 Binary files /dev/null and b/.cache/brands/integrations/influxdb/dark_icon.png differ diff --git a/.cache/brands/integrations/lightwave/dark_icon.png b/.cache/brands/integrations/lightwave/dark_icon.png new file mode 100644 index 0000000..1481f41 Binary files /dev/null and b/.cache/brands/integrations/lightwave/dark_icon.png differ diff --git a/.cache/brands/integrations/luxaflex/dark_icon.png b/.cache/brands/integrations/luxaflex/dark_icon.png new file mode 100644 index 0000000..99cf730 Binary files /dev/null and b/.cache/brands/integrations/luxaflex/dark_icon.png differ diff --git a/.cache/brands/integrations/openmediavault/dark_icon.png b/.cache/brands/integrations/openmediavault/dark_icon.png index 45dad3e..c2616f9 100644 Binary files a/.cache/brands/integrations/openmediavault/dark_icon.png and b/.cache/brands/integrations/openmediavault/dark_icon.png differ diff --git a/.cache/brands/integrations/openmediavault/icon.png b/.cache/brands/integrations/openmediavault/icon.png index 45dad3e..c2616f9 100644 Binary files a/.cache/brands/integrations/openmediavault/icon.png and b/.cache/brands/integrations/openmediavault/icon.png differ diff --git a/.cache/brands/integrations/pcs_lighting/dark_icon.png b/.cache/brands/integrations/pcs_lighting/dark_icon.png new file mode 100644 index 0000000..67176b5 Binary files /dev/null and b/.cache/brands/integrations/pcs_lighting/dark_icon.png differ diff --git a/.cache/brands/integrations/pilight/dark_icon.png b/.cache/brands/integrations/pilight/dark_icon.png new file mode 100644 index 0000000..e3e76b0 Binary files /dev/null and b/.cache/brands/integrations/pilight/dark_icon.png differ diff --git a/.cache/brands/integrations/rflink/dark_icon.png b/.cache/brands/integrations/rflink/dark_icon.png new file mode 100644 index 0000000..cfada63 Binary files /dev/null and b/.cache/brands/integrations/rflink/dark_icon.png differ diff --git a/.cache/brands/integrations/rhasspy/dark_icon.png b/.cache/brands/integrations/rhasspy/dark_icon.png index e1c5a2f..b8eec30 100644 Binary files a/.cache/brands/integrations/rhasspy/dark_icon.png and b/.cache/brands/integrations/rhasspy/dark_icon.png differ diff --git a/.cache/brands/integrations/tesla/dark_icon.png b/.cache/brands/integrations/tesla/dark_icon.png new file mode 100644 index 0000000..88cca8c Binary files /dev/null and b/.cache/brands/integrations/tesla/dark_icon.png differ diff --git a/.cache/brands/integrations/tplink_deco/dark_icon.png b/.cache/brands/integrations/tplink_deco/dark_icon.png new file mode 100644 index 0000000..db6d3e2 Binary files /dev/null and b/.cache/brands/integrations/tplink_deco/dark_icon.png differ diff --git a/.cache/brands/integrations/tplink_deco/icon.png b/.cache/brands/integrations/tplink_deco/icon.png new file mode 100644 index 0000000..db6d3e2 Binary files /dev/null and b/.cache/brands/integrations/tplink_deco/icon.png differ diff --git a/.cache/brands/integrations/waste_collection_schedule/icon.png b/.cache/brands/integrations/waste_collection_schedule/icon.png new file mode 100644 index 0000000..4db5580 Binary files /dev/null and b/.cache/brands/integrations/waste_collection_schedule/icon.png differ diff --git a/.cache/brands/integrations/weatherflow/dark_icon.png b/.cache/brands/integrations/weatherflow/dark_icon.png new file mode 100644 index 0000000..410705e Binary files /dev/null and b/.cache/brands/integrations/weatherflow/dark_icon.png differ diff --git a/.cache/brands/integrations/wilight/dark_icon.png b/.cache/brands/integrations/wilight/dark_icon.png new file mode 100644 index 0000000..b7d57c3 Binary files /dev/null and b/.cache/brands/integrations/wilight/dark_icon.png differ diff --git a/.cache/brands/integrations/wolflink/dark_icon.png b/.cache/brands/integrations/wolflink/dark_icon.png new file mode 100644 index 0000000..9d443be Binary files /dev/null and b/.cache/brands/integrations/wolflink/dark_icon.png differ diff --git a/.cache/brands/integrations/xiaomi_ble/logo.png b/.cache/brands/integrations/xiaomi_ble/logo.png new file mode 100644 index 0000000..fe40070 Binary files /dev/null and b/.cache/brands/integrations/xiaomi_ble/logo.png differ diff --git a/.cache/brands/integrations/yeelight/dark_icon.png b/.cache/brands/integrations/yeelight/dark_icon.png new file mode 100644 index 0000000..e92beb0 Binary files /dev/null and b/.cache/brands/integrations/yeelight/dark_icon.png differ diff --git a/.cache/brands/integrations/zha/dark_logo.png b/.cache/brands/integrations/zha/dark_logo.png new file mode 100644 index 0000000..fb65e96 Binary files /dev/null and b/.cache/brands/integrations/zha/dark_logo.png differ diff --git a/.cache/brands/integrations/zha/icon@2x.png b/.cache/brands/integrations/zha/icon@2x.png new file mode 100644 index 0000000..f9a4a9e Binary files /dev/null and b/.cache/brands/integrations/zha/icon@2x.png differ diff --git a/.cache/brands/integrations/zha/logo.png b/.cache/brands/integrations/zha/logo.png new file mode 100644 index 0000000..1ef0f5c Binary files /dev/null and b/.cache/brands/integrations/zha/logo.png differ diff --git a/.ha_run.lock b/.ha_run.lock index 91305ea..53e1e76 100644 --- a/.ha_run.lock +++ b/.ha_run.lock @@ -1 +1 @@ -{"pid": 67, "version": 1, "ha_version": "2026.4.1", "start_ts": 1775275075.484253} \ No newline at end of file +{"pid": 67, "version": 1, "ha_version": "2026.4.4", "start_ts": 1777063274.5674608} \ No newline at end of file diff --git a/esphome/esp-gasheater.yaml b/esphome/esp-gasheater.yaml index 61ee0e3..112848c 100644 --- a/esphome/esp-gasheater.yaml +++ b/esphome/esp-gasheater.yaml @@ -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: + - 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: 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(mode_select).state != "BYPASS (Remote Only)";' + lambda: 'return !id(powered_state).state;' then: - - logger.log: "Boot: heater already powered. Calibrating to level 1." - - script.execute: calibrate_to_min + - 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 already powered. BYPASS (Remote Only) -> skipping calibration." - + - 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: diff --git a/esphome/esp-laundryenv.yaml b/esphome/esp-laundryenv.yaml index 47cc503..324a82c 100644 --- a/esphome/esp-laundryenv.yaml +++ b/esphome/esp-laundryenv.yaml @@ -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,14 +219,21 @@ 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: input: true pullup: true inverted: true - name: "${entity_prefix} Smoke Detector" \ No newline at end of file + name: "${entity_prefix} Smoke Detector" + device_class: smoke \ No newline at end of file diff --git a/esphome/esp-laundrylights.yaml b/esphome/esp-laundrylights.yaml index 467247a..273fda8 100644 --- a/esphome/esp-laundrylights.yaml +++ b/esphome/esp-laundrylights.yaml @@ -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: @@ -175,8 +178,8 @@ esp8266: # https://esphome.io/components/logger.html #:########################################################################################:# logger: - level: "${log_level}" #INFO Level suggested, or DEBUG for testing - #baud_rate: 0 #set to 0 for no logging via UART, needed if you are using it for other serial things (eg PZEM) + level: "${log_level}" # INFO Level suggested, or DEBUG for testing + #baud_rate: 0 # set to 0 for no logging via UART, needed if you are using it for other serial things (eg PZEM) #esp8266_store_log_strings_in_flash: false #tx_buffer_size: 64 @@ -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 - 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 +#:########################################################################################:# +# 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 -#:########################################################################################:# -# 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}" diff --git a/esphome/esp-mainbathfancombo.yaml b/esphome/esp-mainbathfancombo.yaml index 1f40431..a647c76 100644 --- a/esphome/esp-mainbathfancombo.yaml +++ b/esphome/esp-mainbathfancombo.yaml @@ -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" diff --git a/esphome/esp-occupancyoffice.yaml b/esphome/esp-occupancyoffice.yaml index fae09da..e4352af 100644 --- a/esphome/esp-occupancyoffice.yaml +++ b/esphome/esp-occupancyoffice.yaml @@ -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(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,8 +560,12 @@ 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" id: ld1125h_mov_binary - device_class: motion \ No newline at end of file + device_class: motion diff --git a/esphome/esp-officeduallights.yaml b/esphome/esp-officeduallights.yaml index 15d1146..ea456aa 100644 --- a/esphome/esp-officeduallights.yaml +++ b/esphome/esp-officeduallights.yaml @@ -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); + #:########################################################################################:# -# STATUS LED: +# 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 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: diff --git a/esphome/esp-officelights.yaml b/esphome/esp-officelights.yaml index e50911b..b500b55 100644 --- a/esphome/esp-officelights.yaml +++ b/esphome/esp-officelights.yaml @@ -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 \ No newline at end of file + id: Relay_3 \ No newline at end of file diff --git a/packages/laundry_lights_backup_occupancy.yaml b/packages/laundry_lights_backup_occupancy.yaml new file mode 100644 index 0000000..a06e219 --- /dev/null +++ b/packages/laundry_lights_backup_occupancy.yaml @@ -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 diff --git a/packages/office_lights_power_backup_occupancy.yaml b/packages/office_lights_power_backup_occupancy.yaml new file mode 100644 index 0000000..93e7ee1 --- /dev/null +++ b/packages/office_lights_power_backup_occupancy.yaml @@ -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