control: Json action to set zone's target from group

This commit adds an action to fan control json format, supporting
setting target of Zone to a value corresponding to the maximum
value from group member properties. The mapping is according to
the map. If there are more than one group using this action, the
maximum speed derived from the mapping of all groups will be set
to target.

For example:
{
   "name": "target_from_group_max",
   "groups": [
        {
           "name": "zone0_ambient",
           "interface": "xyz.openbmc_project.Sensor.Value",
           "property": { "name": "Value" }
        }
   ],
   "neg_hysteresis": 1,
   "pos_hysteresis": 0,
   "map": [
        { "value": 10.0, "target": 38.0 },
        ...
   ]
}

The above JSON will cause the action to read the property specified
in the group "zone0_ambient" from all members of the group. The
change in the group's members value will be checked against
"neg_hysteresis" and "pos_hysteresis" to decide if it is worth
taking action. "neg_hysteresis" is for the increasing case and
"pos_hysteresis" is for the decreasing case. The maximum property
value of the group will be mapped to the "map" to get the output
"target". The updated "target" value of each group will be stored
in a static map with a key. The maximum value from the static map
will be used to set to the Zone's target.

Signed-off-by: Chau Ly <chaul@amperecomputing.com>
Change-Id: I7b99a6e82ab2faaf40d290ab6f9cb27781e12952
diff --git a/control/Makefile.am b/control/Makefile.am
index 5d7d1e6..f65a1e9 100644
--- a/control/Makefile.am
+++ b/control/Makefile.am
@@ -90,6 +90,7 @@
 	json/actions/count_state_floor.cpp \
 	json/actions/get_managed_objects.cpp \
 	json/actions/pcie_card_floors.cpp \
+	json/actions/target_from_group_max.cpp \
 	json/utils/flight_recorder.cpp \
 	json/utils/modifier.cpp \
 	json/utils/pcie_card_metadata.cpp
diff --git a/control/json/actions/target_from_group_max.cpp b/control/json/actions/target_from_group_max.cpp
new file mode 100644
index 0000000..72c9ab0
--- /dev/null
+++ b/control/json/actions/target_from_group_max.cpp
@@ -0,0 +1,283 @@
+/**
+ * Copyright © 2022 Ampere Computing
+ *
+ * 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 "target_from_group_max.hpp"
+
+#include "../manager.hpp"
+
+#include <fmt/format.h>
+
+#include <iostream>
+
+namespace phosphor::fan::control::json
+{
+
+std::map<size_t, uint64_t> TargetFromGroupMax::_speedFromGroupsMap;
+size_t TargetFromGroupMax::_groupIndexCounter = 0;
+
+using json = nlohmann::json;
+using namespace phosphor::logging;
+
+TargetFromGroupMax::TargetFromGroupMax(const json& jsonObj,
+                                       const std::vector<Group>& groups) :
+    ActionBase(jsonObj, groups)
+{
+    setHysteresis(jsonObj);
+    setMap(jsonObj);
+    setIndex();
+}
+
+void TargetFromGroupMax::run(Zone& zone)
+{
+    // Holds the max property value of groups
+    auto maxGroup = processGroups();
+
+    // Group with non-numeric property value will be skipped from processing
+    if (maxGroup)
+    {
+        /*The maximum property value from the group*/
+        uint64_t groupValue =
+            static_cast<uint64_t>(std::get<double>(maxGroup.value()));
+
+        // Only check if previous and new values differ
+        if (groupValue != _prevGroupValue)
+        {
+            /*The speed derived from mapping*/
+            uint64_t groupSpeed = _speedFromGroupsMap[_groupIndex];
+
+            // Value is decreasing from previous  && greater than positive
+            // hysteresis
+            if ((groupValue < _prevGroupValue) &&
+                (_prevGroupValue - groupValue > _posHysteresis))
+            {
+                for (auto it = _valueToSpeedMap.rbegin();
+                     it != _valueToSpeedMap.rend(); ++it)
+                {
+                    // Value is at/above last map key, set speed to the last map
+                    // key's value
+                    if (it == _valueToSpeedMap.rbegin() &&
+                        groupValue >= it->first)
+                    {
+                        groupSpeed = it->second;
+                        break;
+                    }
+                    // Value is at/below first map key, set speed to the first
+                    // map key's value
+                    else if (std::next(it, 1) == _valueToSpeedMap.rend() &&
+                             groupValue <= it->first)
+                    {
+                        groupSpeed = it->second;
+                        break;
+                    }
+                    // Value decreased & transitioned across a map key, update
+                    // speed to this map key's value when new value is at or
+                    // below map's key and the key is at/below the previous
+                    // value
+                    if (groupValue <= it->first && it->first <= _prevGroupValue)
+                    {
+                        groupSpeed = it->second;
+                    }
+                }
+                _prevGroupValue = groupValue;
+                _speedFromGroupsMap[_groupIndex] = groupSpeed;
+
+                // Get the maximum speed derived from all groups, and set target
+                // for the Zone
+                auto maxSpeedFromGroupsIter = std::max_element(
+                    _speedFromGroupsMap.begin(), _speedFromGroupsMap.end(),
+                    [](const auto& x, const auto& y) {
+                        return x.second < y.second;
+                    });
+
+                zone.setTarget(maxSpeedFromGroupsIter->second);
+            }
+            // Value is increasing from previous && greater than negative
+            // hysteresis
+            else
+            {
+                if (groupValue - _prevGroupValue > _negHysteresis)
+                {
+                    for (auto it = _valueToSpeedMap.begin();
+                         it != _valueToSpeedMap.end(); ++it)
+                    {
+                        // Value is at/below the first map key, set speed to the
+                        // first map key's value
+                        if (it == _valueToSpeedMap.begin() &&
+                            groupValue <= it->first)
+                        {
+                            groupSpeed = it->second;
+                            break;
+                        }
+                        // Value is at/above last map key, set speed to the last
+                        // map key's value
+                        else if (std::next(it, 1) == _valueToSpeedMap.end() &&
+                                 groupValue >= it->first)
+                        {
+                            groupSpeed = it->second;
+                            break;
+                        }
+                        // Value increased & transitioned across a map key,
+                        // update speed to the next map key's value when new
+                        // value is above map's key and the key is at/above the
+                        // previous value
+                        if (groupValue > it->first &&
+                            it->first >= _prevGroupValue)
+                        {
+                            groupSpeed = std::next(it, 1)->second;
+                        }
+                        // Value increased & transitioned across a map key,
+                        // update speed to the map key's value when new value is
+                        // at the map's key and the key is above the previous
+                        // value
+                        else if (groupValue == it->first &&
+                                 it->first > _prevGroupValue)
+                        {
+                            groupSpeed = it->second;
+                        }
+                    }
+                }
+                _prevGroupValue = groupValue;
+                _speedFromGroupsMap[_groupIndex] = groupSpeed;
+
+                // Get the maximum speed derived from all groups, and set target
+                // for the Zone
+                auto maxSpeedFromGroupsIter = std::max_element(
+                    _speedFromGroupsMap.begin(), _speedFromGroupsMap.end(),
+                    [](const auto& x, const auto& y) {
+                        return x.second < y.second;
+                    });
+
+                zone.setTarget(maxSpeedFromGroupsIter->second);
+            }
+        }
+    }
+    else
+    {
+        std::cerr << "Failed to process groups for " << ActionBase::getName()
+                  << ": Further processing will be skipped \n";
+    }
+}
+
+void TargetFromGroupMax::setHysteresis(const json& jsonObj)
+{
+    if (!jsonObj.contains("neg_hysteresis") ||
+        !jsonObj.contains("pos_hysteresis"))
+    {
+        throw ActionParseError{
+            ActionBase::getName(),
+            "Missing required neg_hysteresis or pos_hysteresis value"};
+    }
+    _negHysteresis = jsonObj["neg_hysteresis"].get<uint64_t>();
+    _posHysteresis = jsonObj["pos_hysteresis"].get<uint64_t>();
+}
+
+void TargetFromGroupMax::setIndex()
+{
+    _groupIndex = _groupIndexCounter;
+    // Initialize the map of each group and their max values
+    _speedFromGroupsMap[_groupIndex] = 0;
+
+    // Increase the index counter by one to specify the next group key
+    _groupIndexCounter += 1;
+}
+
+void TargetFromGroupMax::setMap(const json& jsonObj)
+{
+    if (jsonObj.contains("map"))
+    {
+        for (const auto& map : jsonObj.at("map"))
+        {
+
+            if (!map.contains("value") || !map.contains("target"))
+            {
+                throw ActionParseError{ActionBase::getName(),
+                                       "Missing value or target in map"};
+            }
+            else
+            {
+                uint64_t val = map["value"].get<uint64_t>();
+                uint64_t target = map["target"].get<uint64_t>();
+                _valueToSpeedMap.insert(
+                    std::pair<uint64_t, uint64_t>(val, target));
+            }
+        }
+    }
+
+    else
+    {
+        throw ActionParseError{ActionBase::getName(), "Missing required map"};
+    }
+}
+
+std::optional<PropertyVariantType> TargetFromGroupMax::processGroups()
+{
+    // Holds the max property value of groups
+    std::optional<PropertyVariantType> max;
+
+    for (const auto& group : _groups)
+    {
+        const auto& members = group.getMembers();
+        for (const auto& member : members)
+        {
+            PropertyVariantType value;
+            bool invalid = false;
+            try
+            {
+                value = Manager::getObjValueVariant(
+                    member, group.getInterface(), group.getProperty());
+            }
+            catch (const std::out_of_range&)
+            {
+                continue;
+            }
+
+            // Only allow a group members to be
+            // numeric. Unlike with std::is_arithmetic, bools are not
+            // considered numeric here.
+            std::visit(
+                [&group, &invalid, this](auto&& val) {
+                    using V = std::decay_t<decltype(val)>;
+                    if constexpr (!std::is_same_v<double, V> &&
+                                  !std::is_same_v<int32_t, V> &&
+                                  !std::is_same_v<int64_t, V>)
+                    {
+                        log<level::ERR>(fmt::format("{}: Group {}'s member "
+                                                    "isn't numeric",
+                                                    ActionBase::getName(),
+                                                    group.getName())
+                                            .c_str());
+                        invalid = true;
+                    }
+                },
+                value);
+            if (invalid)
+            {
+                break;
+            }
+
+            if (max && (value > max))
+            {
+                max = value;
+            }
+            else if (!max)
+            {
+                max = value;
+            }
+        }
+    }
+    return max;
+}
+
+} // namespace phosphor::fan::control::json
diff --git a/control/json/actions/target_from_group_max.hpp b/control/json/actions/target_from_group_max.hpp
new file mode 100644
index 0000000..04bffe6
--- /dev/null
+++ b/control/json/actions/target_from_group_max.hpp
@@ -0,0 +1,150 @@
+/**
+ * Copyright © 2022 Ampere Computing
+ *
+ * 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 "../utils/modifier.hpp"
+#include "../zone.hpp"
+#include "action.hpp"
+#include "group.hpp"
+
+#include <nlohmann/json.hpp>
+
+namespace phosphor::fan::control::json
+{
+
+using json = nlohmann::json;
+
+/**
+ * @class class TargetFromGroupMax : - Action to set target of Zone
+ * to a value corresponding to the maximum value from group's member
+ * properties. The mapping is according to the configurable map.
+ *
+ * If there are more than one group using this action, the maximum
+ * speed derived from the mapping of all groups will be set to target.
+ *
+ * For example:
+ *
+ *  {
+      "name": "target_from_group_max",
+      "groups": [
+            {
+              "name": "zone0_ambient",
+              "interface": "xyz.openbmc_project.Sensor.Value",
+              "property": { "name": "Value" }
+            }
+          ],
+      "neg_hysteresis": 1,
+      "pos_hysteresis": 0,
+      "map": [
+            { "value": 10.0, "target": 38.0 }
+      ]
+    }
+
+ *
+ * The above JSON will cause the action to read the property specified
+ * in the group "zone0_ambient" from all members of the group, the change
+ * in the group's members value will be checked against "neg_hysteresis"
+ * and "pos_hysteresis" to decide if it is worth taking action.
+ * "neg_hysteresis" is for the increasing case and "pos_hysteresis" is
+ * for the decreasing case. The maximum property value in a group will be
+ * mapped to the "map" to get the output "target". The updated "target"
+ * value of each group will be stored in a static map with a key. The
+ * maximum value from the static map will be used to set to the Zone's target.
+ *
+ */
+class TargetFromGroupMax :
+    public ActionBase,
+    public ActionRegister<TargetFromGroupMax>
+{
+  public:
+    /* Name of this action */
+    static constexpr auto name = "target_from_group_max";
+
+    TargetFromGroupMax() = delete;
+    TargetFromGroupMax(const TargetFromGroupMax&) = delete;
+    TargetFromGroupMax(TargetFromGroupMax&&) = delete;
+    TargetFromGroupMax& operator=(const TargetFromGroupMax&) = delete;
+    TargetFromGroupMax& operator=(TargetFromGroupMax&&) = delete;
+    ~TargetFromGroupMax() = default;
+
+    /**
+     * @brief Constructor
+     *
+     * @param[in] jsonObj - JSON configuration of this action
+     * @param[in] groups - Groups of dbus objects the action uses
+     */
+    TargetFromGroupMax(const json& jsonObj, const std::vector<Group>& groups);
+
+    /**
+     * @brief Reads a property value from the configured group,
+     *        get the max, do mapping and get the target.
+     *
+     * @param[in] zone - Zone to run the action on
+     */
+    void run(Zone& zone) override;
+
+  private:
+    /*The previous maximum property value from group used for checking against
+     * hysteresis*/
+    uint64_t _prevGroupValue = 0;
+
+    /*The table of maximum speed derived from each group using this action*/
+    static std::map<size_t, uint64_t> _speedFromGroupsMap;
+
+    /*The group index counter*/
+    static size_t _groupIndexCounter;
+
+    /*The Hysteresis parameters from config*/
+    uint64_t _negHysteresis = 0;
+    uint64_t _posHysteresis = 0;
+
+    /*The group index from config*/
+    size_t _groupIndex = 0;
+
+    /*The mapping table from config*/
+    std::map<uint64_t, uint64_t> _valueToSpeedMap;
+
+    /**
+     * @brief Read the hysteresis parameters from the JSON
+     *
+     * @param[in] jsonObj - JSON configuration of this action
+     */
+    void setHysteresis(const json& jsonObj);
+
+    /**
+     * @brief Set index for the group
+     *
+     * @param[in] jsonObj - JSON configuration of this action
+     */
+    void setIndex();
+
+    /**
+     * @brief Read the map from the JSON
+     *
+     * @param[in] jsonObj - JSON configuration of this action
+     */
+    void setMap(const json& jsonObj);
+
+    /**
+     * @brief Process through all groups of the event and return the maximum
+     * property value
+     *
+     * @param[in] jsonObj - JSON configuration of this action
+     */
+    std::optional<PropertyVariantType> processGroups();
+};
+
+} // namespace phosphor::fan::control::json
diff --git a/control/meson.build b/control/meson.build
index 5225b0c..e5a6231 100644
--- a/control/meson.build
+++ b/control/meson.build
@@ -40,6 +40,7 @@
         'json/actions/pcie_card_floors.cpp',
         'json/actions/request_target_base.cpp',
         'json/actions/set_parameter_from_group_max.cpp',
+        'json/actions/target_from_group_max.cpp',
         'json/actions/timer_based_actions.cpp',
         'json/utils/flight_recorder.cpp',
         'json/utils/modifier.cpp',
diff --git a/docs/control/events.md b/docs/control/events.md
index 724ae99..0e0af32 100644
--- a/docs/control/events.md
+++ b/docs/control/events.md
@@ -243,6 +243,7 @@
 - [pcie_card_floors](#pcie_card_floors)
 - [set_request_target_base_with_max](#set_request_target_base_with_max)
 - [set_parameter_from_group_max](#set_parameter_from_group_max)
+- [target_from_group_max](#target_from_group_max)
 - [call_actions_based_on_timer](#call_actions_based_on_timer)
 - [get_managed_objects](#get_managed_objects)
 
@@ -575,6 +576,44 @@
 subtract 4, and then store the resulting value in the `proc_0_throttle_temp`
 parameter.
 
+### target_from_group_max
+
+The action sets target of Zone to a value corresponding to the maximum
+value from maximum group property value. The mapping is based on a provided
+table. If there are more than one event using this action, the maximum speed
+derived from the mapping of all groups will be set to the zone's target.
+
+...
+{
+    "name": "target_from_group_max",
+    "groups": [
+        {
+          "name": "zone0_ambient",
+          "interface": "xyz.openbmc_project.Sensor.Value",
+          "property": { "name": "Value" }
+        }
+    ],
+    "neg_hysteresis": 1,
+    "pos_hysteresis": 0,
+    "map": [
+        { "value": 10.0, "target": 38.0 },
+        ...
+    ]
+}
+
+The above JSON will cause the action to read the property specified in the
+group "zone0_ambient" from all members of the group. The change in the group's
+members value will be checked against "neg_hysteresis" and "pos_hysteresis"
+to decide if it is worth taking action. "neg_hysteresis" is for the increasing
+case and "pos_hysteresis" is for the decreasing case. The maximum property value
+in the group will be mapped to the "map" to get the output "target".
+Each configured event using this action will be provided with a key in a static map
+to store its mapping result. The static map will be shared across the events of this
+action. Therefore, the updated "target" value derived from "zone0_ambient" will be
+stored in that static map with its own key. Each time it calls this action running
+for each event, after the new value is updated to the static map, the maximum value
+from it will be used to set to the Zone's target.
+
 ### call_actions_based_on_timer
 This action starts and stops a timer that runs a list of actions whenever the
 timer expires.  A timer can be either `oneshot` or `repeating`.