From efb038e0f13c67aa7de724fef7e1779f54d57902 Mon Sep 17 00:00:00 2001 From: Brandon Harvey Date: Sun, 12 Apr 2026 12:35:11 -0500 Subject: [PATCH 1/2] feat: add configurable sensor debounce for water level sensors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a "Sensor Debounce Seconds" number entity (disabled by default, 0-30s, default 0) that lets users configure how many consecutive seconds a sensor must read wet/dry before the pump reacts. Default 0 preserves original behavior. Also fix typo in output sensor ID (fulid → fluid) and add debounce to auto-refill trigger. --- Integrations/ESPHome/Core.yaml | 76 ++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index a5e0790..0395016 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -76,6 +76,14 @@ globals: initial_value: "true" - id: testCycleCount type: int + - id: output_wet_count + type: int + restore_value: no + initial_value: '0' + - id: input_dry_count + type: int + restore_value: no + initial_value: '0' captive_portal: @@ -202,6 +210,8 @@ switch: - lambda: |- id(pump_start_time) = millis(); id(safety_alert_active) = false; + id(output_wet_count) = 0; + id(input_dry_count) = 0; - script.execute: pump_safety_check else: - logger.log: "Pump blocked - water conditions not met" @@ -270,9 +280,11 @@ binary_sensor: inverted: true on_release: then: + - delay: !lambda 'return (int)id(sensor_debounce_seconds).state * 1000;' - if: condition: and: + - binary_sensor.is_off: fluid_input_sensor - switch.is_on: auto_refill - switch.is_on: tank_refill_mode - switch.is_off: pump_control @@ -285,7 +297,7 @@ binary_sensor: - platform: gpio name: Fluid Output - id: fulid_output_sensor + id: fluid_output_sensor icon: mdi:water device_class: moisture pin: @@ -321,6 +333,20 @@ number: unit_of_measurement: "s" restore_value: true mode: box + - platform: template + name: "Sensor Debounce Seconds" + id: sensor_debounce_seconds + icon: mdi:timer-sand + entity_category: config + disabled_by_default: true + optimistic: true + min_value: 0 + max_value: 30 + step: 1 + initial_value: 0 + unit_of_measurement: "s" + restore_value: true + mode: box sensor: - platform: internal_temperature @@ -438,13 +464,37 @@ script: condition: - switch.is_on: pump_control then: + # Debounce counters for sensor readings + - lambda: |- + int debounce = (int)id(sensor_debounce_seconds).state; + // Output sensor wet counter (stop when full) + if (id(stop_pump_when_full).state && id(fluid_output_sensor).state) { + id(output_wet_count) += 1; + } else if (id(tank_refill_mode).state && !id(stop_pump_when_full).state && id(fluid_input_sensor).state) { + // Inverted mode: input sensor wet = destination full + id(output_wet_count) += 1; + } else { + id(output_wet_count) = 0; + } + // Input sensor dry counter (stop when dry) + if (id(stop_pump_when_dry).state && !id(tank_refill_mode).state && !id(fluid_input_sensor).state) { + id(input_dry_count) += 1; + } else { + id(input_dry_count) = 0; + } - if: condition: - - and: - - switch.is_on: stop_pump_when_full - - binary_sensor.is_on: fulid_output_sensor + - lambda: |- + int debounce = (int)id(sensor_debounce_seconds).state; + return id(output_wet_count) > 0 && (debounce == 0 || id(output_wet_count) >= debounce); then: - - logger.log: "Pump stopping - tank full" + - if: + condition: + - switch.is_on: stop_pump_when_full + then: + - logger.log: "Pump stopping - tank full" + else: + - logger.log: "Pump stopping - destination full (inverted mode)" - switch.turn_off: pump_control - if: condition: @@ -457,19 +507,9 @@ script: - switch.turn_off: pump_control - if: condition: - - and: - - switch.is_on: tank_refill_mode - - switch.is_off: stop_pump_when_full - - binary_sensor.is_on: fluid_input_sensor - then: - - logger.log: "Pump stopping - destination full (inverted mode)" - - switch.turn_off: pump_control - - if: - condition: - - and: - - switch.is_on: stop_pump_when_dry - - switch.is_off: tank_refill_mode - - binary_sensor.is_off: fluid_input_sensor + - lambda: |- + int debounce = (int)id(sensor_debounce_seconds).state; + return id(input_dry_count) > 0 && (debounce == 0 || id(input_dry_count) >= debounce); then: - logger.log: "Pump stopping - input dry" - switch.turn_off: pump_control From 52a6748c572fc0bec7522f71e945932039a909be Mon Sep 17 00:00:00 2001 From: Brandon Harvey Date: Sun, 12 Apr 2026 12:47:29 -0500 Subject: [PATCH 2/2] feat: add configurable sensor debounce with stop-then-verify Add a "Sensor Debounce Seconds" number entity (disabled by default, 0-30s, default 0) for water level sensors. Uses stop-then-verify approach: pump stops immediately when a sensor triggers, waits the configured seconds, then re-checks. If the reading was a false alarm (e.g. wave bounce), the pump restarts automatically. Safety timeout is preserved across debounce restarts. Also fix typo in output sensor ID (fulid -> fluid) and add debounce to auto-refill trigger. --- Integrations/ESPHome/Core.yaml | 151 +++++++++++++++++++++------------ 1 file changed, 95 insertions(+), 56 deletions(-) diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index 0395016..c9fac47 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -76,14 +76,10 @@ globals: initial_value: "true" - id: testCycleCount type: int - - id: output_wet_count - type: int - restore_value: no - initial_value: '0' - - id: input_dry_count - type: int + - id: debounce_restart + type: bool restore_value: no - initial_value: '0' + initial_value: 'false' captive_portal: @@ -191,31 +187,39 @@ switch: then: - if: condition: - or: - # Normal mode: input has water - - and: - - switch.is_off: tank_refill_mode - - binary_sensor.is_on: fluid_input_sensor - # Inverted mode: input is dry (destination is low, needs filling) - - and: - - switch.is_on: tank_refill_mode - - binary_sensor.is_off: fluid_input_sensor - # Bypass: dry protection not enabled - - switch.is_off: stop_pump_when_dry + - lambda: 'return id(debounce_restart);' then: - - logger.log: "Pump turning on - conditions met" - - text_sensor.template.publish: - id: last_pump_action - state: "Pump Started" - lambda: |- - id(pump_start_time) = millis(); + id(debounce_restart) = false; id(safety_alert_active) = false; - id(output_wet_count) = 0; - id(input_dry_count) = 0; + - logger.log: "Pump restarting after debounce verification" - script.execute: pump_safety_check else: - - logger.log: "Pump blocked - water conditions not met" - - switch.turn_off: pump_control + - if: + condition: + or: + # Normal mode: input has water + - and: + - switch.is_off: tank_refill_mode + - binary_sensor.is_on: fluid_input_sensor + # Inverted mode: input is dry (destination is low, needs filling) + - and: + - switch.is_on: tank_refill_mode + - binary_sensor.is_off: fluid_input_sensor + # Bypass: dry protection not enabled + - switch.is_off: stop_pump_when_dry + then: + - logger.log: "Pump turning on - conditions met" + - text_sensor.template.publish: + id: last_pump_action + state: "Pump Started" + - lambda: |- + id(pump_start_time) = millis(); + id(safety_alert_active) = false; + - script.execute: pump_safety_check + else: + - logger.log: "Pump blocked - water conditions not met" + - switch.turn_off: pump_control on_turn_off: then: - if: @@ -464,38 +468,31 @@ script: condition: - switch.is_on: pump_control then: - # Debounce counters for sensor readings - - lambda: |- - int debounce = (int)id(sensor_debounce_seconds).state; - // Output sensor wet counter (stop when full) - if (id(stop_pump_when_full).state && id(fluid_output_sensor).state) { - id(output_wet_count) += 1; - } else if (id(tank_refill_mode).state && !id(stop_pump_when_full).state && id(fluid_input_sensor).state) { - // Inverted mode: input sensor wet = destination full - id(output_wet_count) += 1; - } else { - id(output_wet_count) = 0; - } - // Input sensor dry counter (stop when dry) - if (id(stop_pump_when_dry).state && !id(tank_refill_mode).state && !id(fluid_input_sensor).state) { - id(input_dry_count) += 1; - } else { - id(input_dry_count) = 0; - } + # Stop when full (output wet) - stop first, verify after - if: condition: - - lambda: |- - int debounce = (int)id(sensor_debounce_seconds).state; - return id(output_wet_count) > 0 && (debounce == 0 || id(output_wet_count) >= debounce); + - and: + - switch.is_on: stop_pump_when_full + - binary_sensor.is_on: fluid_output_sensor then: + - switch.turn_off: pump_control - if: condition: - - switch.is_on: stop_pump_when_full + - lambda: 'return id(sensor_debounce_seconds).state > 0;' then: - - logger.log: "Pump stopping - tank full" + - delay: !lambda 'return (int)id(sensor_debounce_seconds).state * 1000;' + - if: + condition: + - binary_sensor.is_off: fluid_output_sensor + then: + - logger.log: "Debounce: tank full was false alarm, restarting pump" + - lambda: 'id(debounce_restart) = true;' + - switch.turn_on: pump_control + else: + - logger.log: "Pump stopped - tank full (confirmed)" else: - - logger.log: "Pump stopping - destination full (inverted mode)" - - switch.turn_off: pump_control + - logger.log: "Pump stopping - tank full" + # Safety timeout (unchanged) - if: condition: - lambda: |- @@ -505,14 +502,56 @@ script: then: - logger.log: "Pump stopping - safety" - switch.turn_off: pump_control + # Destination full in inverted mode (input wet) - if: condition: - - lambda: |- - int debounce = (int)id(sensor_debounce_seconds).state; - return id(input_dry_count) > 0 && (debounce == 0 || id(input_dry_count) >= debounce); + - and: + - switch.is_on: tank_refill_mode + - switch.is_off: stop_pump_when_full + - binary_sensor.is_on: fluid_input_sensor + then: + - switch.turn_off: pump_control + - if: + condition: + - lambda: 'return id(sensor_debounce_seconds).state > 0;' + then: + - delay: !lambda 'return (int)id(sensor_debounce_seconds).state * 1000;' + - if: + condition: + - binary_sensor.is_off: fluid_input_sensor + then: + - logger.log: "Debounce: destination full was false alarm, restarting pump" + - lambda: 'id(debounce_restart) = true;' + - switch.turn_on: pump_control + else: + - logger.log: "Pump stopped - destination full (confirmed)" + else: + - logger.log: "Pump stopping - destination full (inverted mode)" + # Stop when dry (input dry) + - if: + condition: + - and: + - switch.is_on: stop_pump_when_dry + - switch.is_off: tank_refill_mode + - binary_sensor.is_off: fluid_input_sensor then: - - logger.log: "Pump stopping - input dry" - switch.turn_off: pump_control + - if: + condition: + - lambda: 'return id(sensor_debounce_seconds).state > 0;' + then: + - delay: !lambda 'return (int)id(sensor_debounce_seconds).state * 1000;' + - if: + condition: + - binary_sensor.is_on: fluid_input_sensor + then: + - logger.log: "Debounce: input dry was false alarm, restarting pump" + - lambda: 'id(debounce_restart) = true;' + - switch.turn_on: pump_control + else: + - logger.log: "Pump stopped - input dry (confirmed)" + else: + - logger.log: "Pump stopping - input dry" - delay: 1s - id: testScript then: