From 5b2176562bf3b1e86fee1cdc274d4f1ba7d8f89e Mon Sep 17 00:00:00 2001
From: Sergey Dudanov <sergey.dudanov@gmail.com>
Date: Tue, 4 Jul 2023 04:25:48 +0400
Subject: [PATCH] binary_sensor filters templatable delays (#5029)

---
 esphome/components/binary_sensor/__init__.py | 76 ++++++++++++++------
 esphome/components/binary_sensor/filter.cpp  | 11 ++-
 esphome/components/binary_sensor/filter.h    | 21 +++---
 tests/test1.yaml                             |  7 ++
 4 files changed, 79 insertions(+), 36 deletions(-)

diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py
index f4a5c95b12..41b4c5a0d7 100644
--- a/esphome/components/binary_sensor/__init__.py
+++ b/esphome/components/binary_sensor/__init__.py
@@ -95,6 +95,14 @@ DEVICE_CLASSES = [
 
 IS_PLATFORM_COMPONENT = True
 
+CONF_TIME_OFF = "time_off"
+CONF_TIME_ON = "time_on"
+
+DEFAULT_DELAY = "1s"
+DEFAULT_TIME_OFF = "100ms"
+DEFAULT_TIME_ON = "900ms"
+
+
 binary_sensor_ns = cg.esphome_ns.namespace("binary_sensor")
 BinarySensor = binary_sensor_ns.class_("BinarySensor", cg.EntityBase)
 BinarySensorInitiallyOff = binary_sensor_ns.class_(
@@ -138,47 +146,75 @@ FILTER_REGISTRY = Registry()
 validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
 
 
-@FILTER_REGISTRY.register("invert", InvertFilter, {})
+def register_filter(name, filter_type, schema):
+    return FILTER_REGISTRY.register(name, filter_type, schema)
+
+
+@register_filter("invert", InvertFilter, {})
 async def invert_filter_to_code(config, filter_id):
     return cg.new_Pvariable(filter_id)
 
 
-@FILTER_REGISTRY.register(
-    "delayed_on_off", DelayedOnOffFilter, cv.positive_time_period_milliseconds
+@register_filter(
+    "delayed_on_off",
+    DelayedOnOffFilter,
+    cv.Any(
+        cv.templatable(cv.positive_time_period_milliseconds),
+        cv.Schema(
+            {
+                cv.Required(CONF_TIME_ON): cv.templatable(
+                    cv.positive_time_period_milliseconds
+                ),
+                cv.Required(CONF_TIME_OFF): cv.templatable(
+                    cv.positive_time_period_milliseconds
+                ),
+            }
+        ),
+        msg="'delayed_on_off' filter requires either a delay time to be used for both "
+        "turn-on and turn-off delays, or two parameters 'time_on' and 'time_off' if "
+        "different delay times are required.",
+    ),
 )
 async def delayed_on_off_filter_to_code(config, filter_id):
-    var = cg.new_Pvariable(filter_id, config)
+    var = cg.new_Pvariable(filter_id)
     await cg.register_component(var, {})
+    if isinstance(config, dict):
+        template_ = await cg.templatable(config[CONF_TIME_ON], [], cg.uint32)
+        cg.add(var.set_on_delay(template_))
+        template_ = await cg.templatable(config[CONF_TIME_OFF], [], cg.uint32)
+        cg.add(var.set_off_delay(template_))
+    else:
+        template_ = await cg.templatable(config, [], cg.uint32)
+        cg.add(var.set_on_delay(template_))
+        cg.add(var.set_off_delay(template_))
     return var
 
 
-@FILTER_REGISTRY.register(
-    "delayed_on", DelayedOnFilter, cv.positive_time_period_milliseconds
+@register_filter(
+    "delayed_on", DelayedOnFilter, cv.templatable(cv.positive_time_period_milliseconds)
 )
 async def delayed_on_filter_to_code(config, filter_id):
-    var = cg.new_Pvariable(filter_id, config)
+    var = cg.new_Pvariable(filter_id)
     await cg.register_component(var, {})
+    template_ = await cg.templatable(config, [], cg.uint32)
+    cg.add(var.set_delay(template_))
     return var
 
 
-@FILTER_REGISTRY.register(
-    "delayed_off", DelayedOffFilter, cv.positive_time_period_milliseconds
+@register_filter(
+    "delayed_off",
+    DelayedOffFilter,
+    cv.templatable(cv.positive_time_period_milliseconds),
 )
 async def delayed_off_filter_to_code(config, filter_id):
-    var = cg.new_Pvariable(filter_id, config)
+    var = cg.new_Pvariable(filter_id)
     await cg.register_component(var, {})
+    template_ = await cg.templatable(config, [], cg.uint32)
+    cg.add(var.set_delay(template_))
     return var
 
 
-CONF_TIME_OFF = "time_off"
-CONF_TIME_ON = "time_on"
-
-DEFAULT_DELAY = "1s"
-DEFAULT_TIME_OFF = "100ms"
-DEFAULT_TIME_ON = "900ms"
-
-
-@FILTER_REGISTRY.register(
+@register_filter(
     "autorepeat",
     AutorepeatFilter,
     cv.All(
@@ -215,7 +251,7 @@ async def autorepeat_filter_to_code(config, filter_id):
     return var
 
 
-@FILTER_REGISTRY.register("lambda", LambdaFilter, cv.returning_lambda)
+@register_filter("lambda", LambdaFilter, cv.returning_lambda)
 async def lambda_filter_to_code(config, filter_id):
     lambda_ = await cg.process_lambda(
         config, [(bool, "x")], return_type=cg.optional.template(bool)
diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp
index 836c341574..46957383c3 100644
--- a/esphome/components/binary_sensor/filter.cpp
+++ b/esphome/components/binary_sensor/filter.cpp
@@ -26,22 +26,20 @@ void Filter::input(bool value, bool is_initial) {
   }
 }
 
-DelayedOnOffFilter::DelayedOnOffFilter(uint32_t delay) : delay_(delay) {}
 optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) {
   if (value) {
-    this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(true, is_initial); });
+    this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
   } else {
-    this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); });
+    this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
   }
   return {};
 }
 
 float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
 
-DelayedOnFilter::DelayedOnFilter(uint32_t delay) : delay_(delay) {}
 optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
   if (value) {
-    this->set_timeout("ON", this->delay_, [this, is_initial]() { this->output(true, is_initial); });
+    this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
     return {};
   } else {
     this->cancel_timeout("ON");
@@ -51,10 +49,9 @@ optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
 
 float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
 
-DelayedOffFilter::DelayedOffFilter(uint32_t delay) : delay_(delay) {}
 optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
   if (!value) {
-    this->set_timeout("OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); });
+    this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
     return {};
   } else {
     this->cancel_timeout("OFF");
diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h
index 0f0ab6875f..9514cb3fe2 100644
--- a/esphome/components/binary_sensor/filter.h
+++ b/esphome/components/binary_sensor/filter.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include "esphome/core/automation.h"
 #include "esphome/core/component.h"
 #include "esphome/core/helpers.h"
 
@@ -29,38 +30,40 @@ class Filter {
 
 class DelayedOnOffFilter : public Filter, public Component {
  public:
-  explicit DelayedOnOffFilter(uint32_t delay);
-
   optional<bool> new_value(bool value, bool is_initial) override;
 
   float get_setup_priority() const override;
 
+  template<typename T> void set_on_delay(T delay) { this->on_delay_ = delay; }
+  template<typename T> void set_off_delay(T delay) { this->off_delay_ = delay; }
+
  protected:
-  uint32_t delay_;
+  TemplatableValue<uint32_t> on_delay_{};
+  TemplatableValue<uint32_t> off_delay_{};
 };
 
 class DelayedOnFilter : public Filter, public Component {
  public:
-  explicit DelayedOnFilter(uint32_t delay);
-
   optional<bool> new_value(bool value, bool is_initial) override;
 
   float get_setup_priority() const override;
 
+  template<typename T> void set_delay(T delay) { this->delay_ = delay; }
+
  protected:
-  uint32_t delay_;
+  TemplatableValue<uint32_t> delay_{};
 };
 
 class DelayedOffFilter : public Filter, public Component {
  public:
-  explicit DelayedOffFilter(uint32_t delay);
-
   optional<bool> new_value(bool value, bool is_initial) override;
 
   float get_setup_priority() const override;
 
+  template<typename T> void set_delay(T delay) { this->delay_ = delay; }
+
  protected:
-  uint32_t delay_;
+  TemplatableValue<uint32_t> delay_{};
 };
 
 class InvertFilter : public Filter {
diff --git a/tests/test1.yaml b/tests/test1.yaml
index f8928430f4..05ba07d1d8 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -1355,8 +1355,15 @@ binary_sensor:
     device_class: window
     filters:
       - invert:
+      - delayed_on_off: 40ms
+      - delayed_on_off:
+          time_on: 10s
+          time_off: !lambda "return 1000;"
       - delayed_on: 40ms
       - delayed_off: 40ms
+      - delayed_on_off: !lambda "return 10;"
+      - delayed_on: !lambda "return 1000;"
+      - delayed_off: !lambda "return 0;"
     on_press:
       then:
         - lambda: >-