From a33b8abce8ab0ec7b3bb81e62c58ab213c44193c Mon Sep 17 00:00:00 2001
From: De Cock Xavier <xdecock@gmail.com>
Date: Mon, 2 Oct 2023 22:25:13 +0200
Subject: [PATCH] Feat/component poller suspend (#5423)

---
 esphome/automation.py          | 38 ++++++++++++++++++++++++++++++++++
 esphome/core/base_automation.h | 34 ++++++++++++++++++++++++++++++
 esphome/core/component.cpp     | 10 +++++++++
 esphome/core/component.h       |  6 ++++++
 tests/test1.yaml               | 16 ++++++++++++++
 5 files changed, 104 insertions(+)

diff --git a/esphome/automation.py b/esphome/automation.py
index 0c4bda09d1..d90a9cb99a 100644
--- a/esphome/automation.py
+++ b/esphome/automation.py
@@ -11,6 +11,7 @@ from esphome.const import (
     CONF_TRIGGER_ID,
     CONF_TYPE_ID,
     CONF_TIME,
+    CONF_UPDATE_INTERVAL,
 )
 from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
 from esphome.util import Registry
@@ -69,6 +70,8 @@ WhileAction = cg.esphome_ns.class_("WhileAction", Action)
 RepeatAction = cg.esphome_ns.class_("RepeatAction", Action)
 WaitUntilAction = cg.esphome_ns.class_("WaitUntilAction", Action, cg.Component)
 UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action)
+SuspendComponentAction = cg.esphome_ns.class_("SuspendComponentAction", Action)
+ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
 Automation = cg.esphome_ns.class_("Automation")
 
 LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
@@ -303,6 +306,41 @@ async def component_update_action_to_code(config, action_id, template_arg, args)
     return cg.new_Pvariable(action_id, template_arg, comp)
 
 
+@register_action(
+    "component.suspend",
+    SuspendComponentAction,
+    maybe_simple_id(
+        {
+            cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
+        }
+    ),
+)
+async def component_suspend_action_to_code(config, action_id, template_arg, args):
+    comp = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, comp)
+
+
+@register_action(
+    "component.resume",
+    ResumeComponentAction,
+    maybe_simple_id(
+        {
+            cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
+            cv.Optional(CONF_UPDATE_INTERVAL): cv.templatable(
+                cv.positive_time_period_milliseconds
+            ),
+        }
+    ),
+)
+async def component_resume_action_to_code(config, action_id, template_arg, args):
+    comp = await cg.get_variable(config[CONF_ID])
+    var = cg.new_Pvariable(action_id, template_arg, comp)
+    if CONF_UPDATE_INTERVAL in config:
+        template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, int)
+        cg.add(var.set_update_interval(template_))
+    return var
+
+
 async def build_action(full_config, template_arg, args):
     registry_entry, config = cg.extract_registry_entry_config(
         ACTION_REGISTRY, full_config
diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h
index a17b6a6f85..af618af99a 100644
--- a/esphome/core/base_automation.h
+++ b/esphome/core/base_automation.h
@@ -330,4 +330,38 @@ template<typename... Ts> class UpdateComponentAction : public Action<Ts...> {
   PollingComponent *component_;
 };
 
+template<typename... Ts> class SuspendComponentAction : public Action<Ts...> {
+ public:
+  SuspendComponentAction(PollingComponent *component) : component_(component) {}
+
+  void play(Ts... x) override {
+    if (!this->component_->is_ready())
+      return;
+    this->component_->stop_poller();
+  }
+
+ protected:
+  PollingComponent *component_;
+};
+
+template<typename... Ts> class ResumeComponentAction : public Action<Ts...> {
+ public:
+  ResumeComponentAction(PollingComponent *component) : component_(component) {}
+  TEMPLATABLE_VALUE(uint32_t, update_interval)
+
+  void play(Ts... x) override {
+    if (!this->component_->is_ready()) {
+      return;
+    }
+    optional<uint32_t> update_interval = this->update_interval_.optional_value(x...);
+    if (update_interval.has_value()) {
+      this->component_->set_update_interval(update_interval.value());
+    }
+    this->component_->start_poller();
+  }
+
+ protected:
+  PollingComponent *component_;
+};
+
 }  // namespace esphome
diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp
index ae85d55498..e2f27f9828 100644
--- a/esphome/core/component.cpp
+++ b/esphome/core/component.cpp
@@ -188,10 +188,20 @@ void PollingComponent::call_setup() {
   // Let the polling component subclass setup their HW.
   this->setup();
 
+  // init the poller
+  this->start_poller();
+}
+
+void PollingComponent::start_poller() {
   // Register interval.
   this->set_interval("update", this->get_update_interval(), [this]() { this->update(); });
 }
 
+void PollingComponent::stop_poller() {
+  // Clear the interval to suspend component
+  this->cancel_interval("update");
+}
+
 uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; }
 void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
 
diff --git a/esphome/core/component.h b/esphome/core/component.h
index 7382f1c617..51a6296811 100644
--- a/esphome/core/component.h
+++ b/esphome/core/component.h
@@ -308,6 +308,12 @@ class PollingComponent : public Component {
   /// Get the update interval in ms of this sensor
   virtual uint32_t get_update_interval() const;
 
+  // Start the poller, used for component.suspend
+  void start_poller();
+
+  // Stop the poller, used for component.suspend
+  void stop_poller();
+
  protected:
   uint32_t update_interval_;
 };
diff --git a/tests/test1.yaml b/tests/test1.yaml
index 96dda707b6..b84aa21439 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -3566,6 +3566,22 @@ button:
     name: Midea Power Inverse
     on_press:
       midea_ac.power_toggle:
+  - platform: template
+    name: Poller component suspend test
+    on_press:
+      - component.suspend: myteleinfo
+      - delay: 20s
+      - component.update: myteleinfo
+      - delay: 20s
+      - component.resume: myteleinfo
+      - delay: 20s
+      - component.resume:
+          id: myteleinfo
+          update_interval: 2s
+      - delay: 20s
+      - component.resume:
+          id: myteleinfo
+          update_interval: !lambda return 2500;
   - platform: ld2410
     factory_reset:
       name: "factory reset"