control: set_parameter_from_group_max action

This action will write a D-Bus property value to the Manager's parameter
cache.  The property value written is the maximum value of all the
configured groups. The value can be modified using the Modifier utility
class's expressions before storing it.

The use case for this is that this action would be configured to write a
parameter that could then be used by another action.

An example JSON config is:

  {
    "name": "set_parameter_from_group",
    "parameter_name": "proc_0_throttle_temp",
    "modifier": {
      "operator": "minus",
      "value": 4
    }
  }

This would read the maximum D-Bus property found in all the groups
configured for the action, subtract 4 from it, and then store it as a
Manager parameter under the name proc_0_throttle_temp.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I75d91110023e0de9908d694997676e4854917ea8
diff --git a/control/Makefile.am b/control/Makefile.am
index d78ec47..0af34d2 100644
--- a/control/Makefile.am
+++ b/control/Makefile.am
@@ -71,6 +71,7 @@
 	json/actions/net_target_decrease.cpp \
 	json/actions/timer_based_actions.cpp \
 	json/actions/mapped_floor.cpp \
+	json/actions/set_parameter_from_group_max.cpp \
 	json/utils/modifier.cpp
 else
 phosphor_fan_control_SOURCES += \
diff --git a/control/json/actions/set_parameter_from_group_max.cpp b/control/json/actions/set_parameter_from_group_max.cpp
new file mode 100644
index 0000000..622c20c
--- /dev/null
+++ b/control/json/actions/set_parameter_from_group_max.cpp
@@ -0,0 +1,144 @@
+/**
+ * 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 "set_parameter_from_group_max.hpp"
+
+#include "../manager.hpp"
+
+#include <fmt/format.h>
+
+namespace phosphor::fan::control::json
+{
+
+using json = nlohmann::json;
+using namespace phosphor::logging;
+
+SetParameterFromGroupMax::SetParameterFromGroupMax(
+    const json& jsonObj, const std::vector<Group>& groups) :
+    ActionBase(jsonObj, groups)
+{
+    setParameterName(jsonObj);
+    setModifier(jsonObj);
+}
+
+void SetParameterFromGroupMax::run(Zone& zone)
+{
+    std::optional<PropertyVariantType> max;
+
+    // Find the maximum value of all group member properties, possibly modify
+    // it, and then write it to the Manager as a parameter.
+
+    for (const auto& group : _groups)
+    {
+        const auto& members = group.getMembers();
+        for (const auto& member : members)
+        {
+            PropertyVariantType value;
+            try
+            {
+                value = Manager::getObjValueVariant(
+                    member, group.getInterface(), group.getProperty());
+            }
+            catch (const std::out_of_range&)
+            {
+                continue;
+            }
+
+            // Only allow a group to have multiple members if it's
+            // numeric. Unlike with std::is_arithmetic, bools are not
+            // considered numeric here.
+            if (members.size() > 1)
+            {
+                bool invalid = false;
+                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 {} has more "
+                                                        "than one member but "
+                                                        "isn't numeric",
+                                                        ActionBase::getName(),
+                                                        group.getName())
+                                                .c_str());
+                            invalid = true;
+                        }
+                    },
+                    value);
+                if (invalid)
+                {
+                    continue;
+                }
+            }
+
+            if (max && (value > max))
+            {
+                max = value;
+            }
+            else if (!max)
+            {
+                max = value;
+            }
+        }
+    }
+
+    if (_modifier)
+    {
+        try
+        {
+            *max = _modifier->doOp(*max);
+        }
+        catch (const std::exception& e)
+        {
+            log<level::ERR>(
+                fmt::format("{}: Could not perform modifier operation: {}",
+                            ActionBase::getName(), e.what())
+                    .c_str());
+            return;
+        }
+    }
+
+    Manager::setParameter(_name, *max);
+}
+
+void SetParameterFromGroupMax::setParameterName(const json& jsonObj)
+{
+    if (!jsonObj.contains("parameter_name"))
+    {
+        throw ActionParseError{ActionBase::getName(),
+                               "Missing required parameter_name value"};
+    }
+
+    _name = jsonObj["parameter_name"].get<std::string>();
+}
+
+void SetParameterFromGroupMax::setModifier(const json& jsonObj)
+{
+    if (jsonObj.contains("modifier"))
+    {
+        try
+        {
+            _modifier = std::make_unique<Modifier>(jsonObj.at("modifier"));
+        }
+        catch (const std::invalid_argument& e)
+        {
+            throw ActionParseError{ActionBase::getName(), e.what()};
+        }
+    }
+}
+
+}; // namespace phosphor::fan::control::json
diff --git a/control/json/actions/set_parameter_from_group_max.hpp b/control/json/actions/set_parameter_from_group_max.hpp
new file mode 100644
index 0000000..ae18eef4
--- /dev/null
+++ b/control/json/actions/set_parameter_from_group_max.hpp
@@ -0,0 +1,117 @@
+/**
+ * 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 "../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 SetParameterFromGroupMax - Action to store a Parameter based
+ * on the maximum property value of all configured groups.
+ *
+ * Sets a value in the Manager's parameter store based on the maximum
+ * group property value.  The property value can be modified before
+ * storing it if the JSON specifies a valid Modifier class expression.
+ *
+ * For example:
+ *
+ *  {
+ *    "name": "set_parameter_from_group",
+ *    "parameter_name": "proc_0_throttle_temp",
+ *    "modifier": {
+ *      "expression": "subtract",
+ *      "value": 4
+ *    }
+ *  }
+ *
+ * The above JSON will cause the action to read the property specified
+ * by the group, subtract 4 from it, and then write that value to the Manager
+ * using the proc_0_throttle_temp name.
+ *
+ * See the Modifier class documentation for valid expressions.
+ */
+class SetParameterFromGroupMax :
+    public ActionBase,
+    public ActionRegister<SetParameterFromGroupMax>
+{
+  public:
+    /* Name of this action */
+    static constexpr auto name = "set_parameter_from_group_max";
+
+    SetParameterFromGroupMax() = delete;
+    SetParameterFromGroupMax(const SetParameterFromGroupMax&) = delete;
+    SetParameterFromGroupMax(SetParameterFromGroupMax&&) = delete;
+    SetParameterFromGroupMax&
+        operator=(const SetParameterFromGroupMax&) = delete;
+    SetParameterFromGroupMax& operator=(SetParameterFromGroupMax&&) = delete;
+    ~SetParameterFromGroupMax() = default;
+
+    /**
+     * @brief Constructor
+     *
+     * @param[in] jsonObj - JSON configuration of this action
+     * @param[in] groups - Groups of dbus objects the action uses
+     */
+    SetParameterFromGroupMax(const json& jsonObj,
+                             const std::vector<Group>& groups);
+
+    /**
+     * @brief Reads a property value from the configured group,
+     *        modifies it if specified, and then store the value
+     *        in the Manager as a parameter.
+     *
+     * @param[in] zone - Zone to run the action on
+     */
+    void run(Zone& zone) override;
+
+  private:
+    /**
+     * @brief Read the parameter name from the JSON
+     *
+     * @param[in] jsonObj - JSON configuration of this action
+     */
+    void setParameterName(const json& jsonObj);
+
+    /**
+     * @brief Read the optional modifier from the JSON
+     *
+     * @param[in] jsonObj - JSON configuration of this action
+     */
+    void setModifier(const json& jsonObj);
+
+    /**
+     * @brief The parameter name
+     */
+    std::string _name;
+
+    /**
+     * @brief The class used to modify the value
+     *
+     * Only created if a modifier is specified in the JSON.
+     */
+    std::unique_ptr<Modifier> _modifier;
+};
+
+} // namespace phosphor::fan::control::json