control: Add optional delay to count_state_floor

Add an optional delay field to the count_state_floor action that takes a
value in seconds. When set to a nonzero value, the action will then
require the count condition to be satisfied for that amount of time
before setting the floor.  This can be used to prevent the floor from
jumping when there is just short change to a property value, for example
if a sensor set to not functional for a short amount of time.

For example:

{
    "name": "count_state_floor",
    "count": 1,
    "state": false,
    "delay": 5,
    "floor": 18000
}

This says that the floor won't be set to 18000 until at least 1 group
member has its property value set to false for a continuous 5 seconds.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I67409ce651d9592b1cabb3f7cb36ba998c3ef545
diff --git a/control/json/actions/count_state_floor.cpp b/control/json/actions/count_state_floor.cpp
index da5bddf..25cb3e6 100644
--- a/control/json/actions/count_state_floor.cpp
+++ b/control/json/actions/count_state_floor.cpp
@@ -19,6 +19,7 @@
 #include "../zone.hpp"
 #include "action.hpp"
 #include "group.hpp"
+#include "sdeventplus.hpp"
 
 namespace phosphor::fan::control::json
 {
@@ -30,11 +31,63 @@
     setCount(jsonObj);
     setState(jsonObj);
     setFloor(jsonObj);
+    setDelayTime(jsonObj);
 }
 
 void CountStateFloor::run(Zone& zone)
 {
+    auto countReached = doCount();
+
+    if (_delayTime == std::chrono::seconds::zero())
+    {
+        // If no delay time configured, can immediately update the hold.
+        zone.setFloorHold(getUniqueName(), _floor, countReached);
+        return;
+    }
+
+    if (!countReached)
+    {
+        if (_timer && _timer->isEnabled())
+        {
+            record("Stopping delay timer");
+            _timer->setEnabled(false);
+        }
+
+        zone.setFloorHold(getUniqueName(), _floor, countReached);
+        return;
+    }
+
+    // The count has been reached and a delay is configured, so either:
+    // 1. This hold has already been set, so don't need to do anything else.
+    // 2. The timer hasn't been started yet, so start it (May need to create
+    //    it first).
+    // 3. The timer is already running, don't need to do anything else.
+    // When the timer expires, then count again and set the hold.
+
+    if (zone.hasFloorHold(getUniqueName()))
+    {
+        return;
+    }
+
+    if (!_timer)
+    {
+        _timer = std::make_unique<Timer>(util::SDEventPlus::getEvent(),
+                                         [&zone, this](Timer&) {
+            zone.setFloorHold(getUniqueName(), _floor, doCount());
+        });
+    }
+
+    if (!_timer->isEnabled())
+    {
+        record("Starting delay timer");
+        _timer->restartOnce(_delayTime);
+    }
+}
+
+bool CountStateFloor::doCount()
+{
     size_t numAtState = 0;
+
     for (const auto& group : _groups)
     {
         for (const auto& member : group.getMembers())
@@ -45,25 +98,20 @@
                                                 group.getProperty()) == _state)
                 {
                     numAtState++;
+                    if (numAtState >= _count)
+                    {
+                        return true;
+                    }
                 }
             }
             catch (const std::out_of_range& oore)
             {
                 // Default to property not equal when not found
             }
-            if (numAtState >= _count)
-            {
-                break;
-            }
-        }
-        if (numAtState >= _count)
-        {
-            break;
         }
     }
 
-    // Update zone's floor hold based on action results
-    zone.setFloorHold(getUniqueName(), _floor, (numAtState >= _count));
+    return false;
 }
 
 void CountStateFloor::setCount(const json& jsonObj)
@@ -96,4 +144,12 @@
     _floor = jsonObj["floor"].get<uint64_t>();
 }
 
+void CountStateFloor::setDelayTime(const json& jsonObj)
+{
+    if (jsonObj.contains("delay"))
+    {
+        _delayTime = std::chrono::seconds(jsonObj["delay"].get<size_t>());
+    }
+}
+
 } // namespace phosphor::fan::control::json
diff --git a/control/json/actions/count_state_floor.hpp b/control/json/actions/count_state_floor.hpp
index 99aa13d..35d0cf2 100644
--- a/control/json/actions/count_state_floor.hpp
+++ b/control/json/actions/count_state_floor.hpp
@@ -40,6 +40,9 @@
  *      "state": false,
  *      "floor": 5000
  *    }
+ *
+ * There is an optional 'delay' field that that prevents the floor from
+ * being changed until the count has been met for that amount of time.
  */
 class CountStateFloor :
     public ActionBase,
@@ -104,6 +107,20 @@
      */
     void setFloor(const json& jsonObj);
 
+    /**
+     * @brief Parse and set the delay
+     *
+     * @param[in] jsonObj - JSON object for the action
+     */
+    void setDelayTime(const json& jsonObj);
+
+    /**
+     * @brief Counts the number of members that are equal to the state.
+     *
+     * @return bool - If the count is reached or not.
+     */
+    bool doCount();
+
     /* Number of group members */
     size_t _count;
 
@@ -112,6 +129,12 @@
 
     /* Floor for this action */
     uint64_t _floor;
+
+    /* Amount of time the count has to be met */
+    std::chrono::seconds _delayTime{0};
+
+    /* Timer used for checking the delay */
+    std::unique_ptr<Timer> _timer;
 };
 
 } // namespace phosphor::fan::control::json
diff --git a/docs/control/events.md b/docs/control/events.md
index 8f14e9f..2f359c8 100644
--- a/docs/control/events.md
+++ b/docs/control/events.md
@@ -396,14 +396,15 @@
     "name": "count_state_floor",
     "count": 2,
     "state": false,
-    "floor": 18000
+    "floor": 18000,
+    "delay": 3
 }
 ```
 
 The above config reads the configured D-Bus property on each group member
-configured for the action. If two or more members have a property value of
-false, a floor hold will be requested with a value of 18000. Otherwise, the
-floor hold will be released (if it was previously requested).
+configured for the action. If two or more members have a property value of false
+for 3 seconds, a floor hold will be requested with a value of 18000. Otherwise,
+the floor hold will be released (if it was previously requested).
 
 ### count_state_before_target