control:actions: Add timer based actions

The timer based actions action starts and stops a timer that runs a list
of actions whenever the timer expires. The configured timer is set to
callback the list of actions against the given zones and any configured
groups.

Where any group does not have a configured value to be compared against,
the groups' service owned state is used to start/stop the timer. When
any service providing a group member is not owned, the timer is started
and if all memebers' services are owned, the timer is stopped.

Where all groups have a configured value to compare against, that will
be compared against all memebers within each group to start/stop the
timer. When all group members have a given value and it matches what's
in the cache, the timer is started and if any do not match, the timer is
stopped.

Basic event config using this action:
.
.
"actions": [
  {
  "name": "call_actions_based_on_timer",
  "timer": {
    "interval": 5000000,
    "type": "repeating"
  },
  "actions": [
    {
    "name": "test"
    }
  ]
  }
]

Change-Id: Ibb80af3218b3cda8b1a7f5bc4dd31046ea6b2bea
Signed-off-by: Matthew Barth <msbarth@us.ibm.com>
diff --git a/control/Makefile.am b/control/Makefile.am
index 7510e1e..1114b8f 100644
--- a/control/Makefile.am
+++ b/control/Makefile.am
@@ -53,7 +53,8 @@
 	json/actions/missing_owner_target.cpp \
 	json/actions/count_state_target.cpp \
 	json/actions/net_target_increase.cpp \
-	json/actions/net_target_decrease.cpp
+	json/actions/net_target_decrease.cpp \
+	json/actions/timer_based_actions.cpp
 else
 phosphor_fan_control_SOURCES += \
 	argument.cpp \
diff --git a/control/json/actions/timer_based_actions.cpp b/control/json/actions/timer_based_actions.cpp
new file mode 100644
index 0000000..52f35a2
--- /dev/null
+++ b/control/json/actions/timer_based_actions.cpp
@@ -0,0 +1,221 @@
+/**
+ * Copyright © 2021 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "timer_based_actions.hpp"
+
+#include "../manager.hpp"
+#include "action.hpp"
+#include "event.hpp"
+#include "group.hpp"
+#include "sdbusplus.hpp"
+#include "sdeventplus.hpp"
+#include "zone.hpp"
+
+#include <fmt/format.h>
+
+#include <nlohmann/json.hpp>
+
+#include <algorithm>
+#include <chrono>
+
+namespace phosphor::fan::control::json
+{
+
+using json = nlohmann::json;
+
+TimerBasedActions::TimerBasedActions(const json& jsonObj,
+                                     const std::vector<Group>& groups) :
+    ActionBase(jsonObj, groups),
+    _timer(util::SDEventPlus::getEvent(),
+           std::bind(&TimerBasedActions::timerExpired, this))
+{
+    // If any of groups' value == nullopt(i.e. not configured), action is
+    // driven by the service owned state of the group members
+    _byOwner =
+        std::any_of(_groups.begin(), _groups.end(), [](const auto& group) {
+            return group.getValue() == std::nullopt;
+        });
+
+    setTimerConf(jsonObj);
+    setActions(jsonObj);
+}
+
+void TimerBasedActions::run(Zone& zone)
+{
+    if (_byOwner)
+    {
+        // If any service providing a group member is not owned, start
+        // timer and if all members' services are owned, stop timer.
+        if (std::any_of(_groups.begin(), _groups.end(), [](const auto& group) {
+                const auto& members = group.getMembers();
+                return std::any_of(members.begin(), members.end(),
+                                   [&group](const auto& member) {
+                                       return !Manager::hasOwner(
+                                           member, group.getInterface());
+                                   });
+            }))
+        {
+            startTimer();
+        }
+        else
+        {
+            stopTimer();
+        }
+    }
+    else
+    {
+        auto* mgr = zone.getManager();
+        // If all group members have a given value and it matches what's
+        // in the cache, start timer and if any do not match, stop
+        // timer.
+        if (std::all_of(
+                _groups.begin(), _groups.end(), [&mgr](const auto& group) {
+                    const auto& members = group.getMembers();
+                    return std::all_of(members.begin(), members.end(),
+                                       [&mgr, &group](const auto& member) {
+                                           return group.getValue() ==
+                                                  mgr->getProperty(
+                                                      member,
+                                                      group.getInterface(),
+                                                      group.getProperty());
+                                       });
+                }))
+        {
+            // Timer will be started(and never stopped) when _groups is empty
+            startTimer();
+        }
+        else
+        {
+            stopTimer();
+        }
+    }
+}
+
+void TimerBasedActions::startTimer()
+{
+    if (!_timer.isEnabled())
+    {
+        if (_type == TimerType::repeating)
+        {
+            _timer.restart(_interval);
+        }
+        else if (_type == TimerType::oneshot)
+        {
+            _timer.restartOnce(_interval);
+        }
+    }
+}
+
+void TimerBasedActions::stopTimer()
+{
+    if (_timer.isEnabled())
+    {
+        _timer.setEnabled(false);
+    }
+}
+
+void TimerBasedActions::timerExpired()
+{
+    // Perform the actions
+    std::for_each(_actions.begin(), _actions.end(),
+                  [](auto& action) { action->run(); });
+}
+
+void TimerBasedActions::setZones(
+    std::vector<std::reference_wrapper<Zone>>& zones)
+{
+    for (auto& zone : zones)
+    {
+        this->addZone(zone);
+        // Add zone to _actions
+        std::for_each(_actions.begin(), _actions.end(),
+                      [&zone](std::unique_ptr<ActionBase>& action) {
+                          action->addZone(zone);
+                      });
+    }
+}
+
+void TimerBasedActions::setTimerConf(const json& jsonObj)
+{
+    if (!jsonObj.contains("timer"))
+    {
+        throw ActionParseError{getName(), "Missing required timer entry"};
+    }
+    auto jsonTimer = jsonObj["timer"];
+    if (!jsonTimer.contains("interval") || !jsonTimer.contains("type"))
+    {
+        throw ActionParseError{
+            getName(), "Missing required timer parameters {interval, type}"};
+    }
+
+    // Interval provided in microseconds
+    _interval = static_cast<std::chrono::microseconds>(
+        jsonTimer["interval"].get<uint64_t>());
+
+    // Retrieve type of timer
+    auto type = jsonTimer["type"].get<std::string>();
+    if (type == "oneshot")
+    {
+        _type = TimerType::oneshot;
+    }
+    else if (type == "repeating")
+    {
+        _type = TimerType::repeating;
+    }
+    else
+    {
+        throw ActionParseError{
+            getName(), fmt::format("Timer type '{}' is not supported", type)};
+    }
+}
+
+void TimerBasedActions::setActions(const json& jsonObj)
+{
+    if (!jsonObj.contains("actions"))
+    {
+        throw ActionParseError{getName(), "Missing required actions entry"};
+    }
+    for (const auto& jsonAct : jsonObj["actions"])
+    {
+        if (!jsonAct.contains("name"))
+        {
+            throw ActionParseError{getName(), "Missing required action name"};
+        }
+
+        // Get any configured profile restrictions on the action
+        std::vector<std::string> profiles;
+        if (jsonAct.contains("profiles"))
+        {
+            for (const auto& profile : jsonAct["profiles"])
+            {
+                profiles.emplace_back(profile.get<std::string>());
+            }
+        }
+
+        // Set the groups configured for each action run when the timer expires
+        std::vector<Group> groups;
+        Event::setGroups(jsonAct, profiles, groups);
+
+        // List of zones is set on these actions by overriden setZones()
+        auto actObj = ActionFactory::getAction(
+            jsonAct["name"].get<std::string>(), jsonAct, std::move(groups), {});
+        if (actObj)
+        {
+            _actions.emplace_back(std::move(actObj));
+        }
+    }
+}
+
+} // namespace phosphor::fan::control::json
diff --git a/control/json/actions/timer_based_actions.hpp b/control/json/actions/timer_based_actions.hpp
new file mode 100644
index 0000000..c36ecf6
--- /dev/null
+++ b/control/json/actions/timer_based_actions.hpp
@@ -0,0 +1,152 @@
+/**
+ * Copyright © 2021 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include "../manager.hpp"
+#include "action.hpp"
+#include "group.hpp"
+#include "zone.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include <chrono>
+#include <functional>
+#include <memory>
+#include <vector>
+
+namespace phosphor::fan::control::json
+{
+
+using json = nlohmann::json;
+
+/**
+ * @class TimerBasedActions - Action that wraps a list of actions with a timer
+ *
+ * Sets up a list of actions to be invoked when the defined timer expires.
+ * Once for a `oneshot` timer or at each expiration of a `repeating` timer.
+ */
+class TimerBasedActions :
+    public ActionBase,
+    public ActionRegister<TimerBasedActions>
+{
+  public:
+    /* Name of this action */
+    static constexpr auto name = "call_actions_based_on_timer";
+
+    TimerBasedActions() = delete;
+    TimerBasedActions(const TimerBasedActions&) = delete;
+    TimerBasedActions(TimerBasedActions&&) = delete;
+    TimerBasedActions& operator=(const TimerBasedActions&) = delete;
+    TimerBasedActions& operator=(TimerBasedActions&&) = delete;
+    ~TimerBasedActions() = default;
+
+    /**
+     * @brief Call actions when timer expires
+     *
+     * @param[in] jsonObj - JSON configuration of this action
+     * @param[in] groups - Groups of dbus objects the action uses
+     */
+    TimerBasedActions(const json& jsonObj, const std::vector<Group>& groups);
+
+    /**
+     * @brief Run the action
+     *
+     * Starts or stops a timer that runs a list of actions whenever the
+     * timer expires. The configured timer is set to callback the list of
+     * actions against the given zones and configured groups.
+     *
+     * Where any group does not have a configured value to be compared against,
+     * the groups' service owned state is used to start/stop the timer. When any
+     * service providing a group member is not owned, the timer is started and
+     * if all members' services are owned, the timer is stopped.
+     *
+     * Where all groups have a configured value to compare against, that will be
+     * compared against all members within each group to start/stop the timer.
+     * When all group members have a given value and it matches what's in the
+     * cache, the timer is started and if any do not match, the timer is
+     * stopped.
+     *
+     * @param[in] zone - Zone to run the action on
+     */
+    void run(Zone& zone) override;
+
+    /**
+     * @brief Start the timer
+     *
+     * Starts the configured timer of this action if not already running
+     */
+    void startTimer();
+
+    /**
+     * @brief Stop the timer
+     *
+     * Stops the configured timer of this action if running
+     */
+    void stopTimer();
+
+    /**
+     * @brief Timer expire's callback
+     *
+     * Called each time the timer expires, running the configured actions
+     */
+    void timerExpired();
+
+    /**
+     * @brief Set the zones on the action and the timer's actions
+     *
+     * @param[in] zones - Zones for the action and timer's actions
+     *
+     * Sets the zones on this action and the timer's actions to run against
+     */
+    virtual void
+        setZones(std::vector<std::reference_wrapper<Zone>>& zones) override;
+
+  private:
+    /* The timer for this action */
+    Timer _timer;
+
+    /* Whether timer triggered by groups' owner or property value states */
+    bool _byOwner;
+
+    /* Timer interval for this action's timer */
+    std::chrono::microseconds _interval;
+
+    /* Timer type for this action's timer */
+    TimerType _type;
+
+    /* List of actions to be called when the timer expires */
+    std::vector<std::unique_ptr<ActionBase>> _actions;
+
+    /**
+     * @brief Parse and set the timer configuration
+     *
+     * @param[in] jsonObj - JSON object for the action
+     *
+     * Sets the timer configuration used to run the list of actions
+     */
+    void setTimerConf(const json& jsonObj);
+
+    /**
+     * @brief Parse and set the list of actions
+     *
+     * @param[in] jsonObj - JSON object for the action
+     *
+     * Sets the list of actions that is run when the timer expires
+     */
+    void setActions(const json& jsonObj);
+};
+
+} // namespace phosphor::fan::control::json