control: mapped_floor: Add conditions

Add the concept of a condition to the mapped_floor action such that it
will only run if the condition is met.  If the condition isn't met, the
run() function will just exit immediately.

A condition is created by placing the following in the JSON:

* condition_group
  - The name of the group that has the property the condition
    will use.  For now, it must be a single member group.
* condition_value
  - The value the property has to be to meet the condition.
* condition_op
  - Either 'equal' or 'not_equal', where the property has to
    either be equal to or not equal to the condition value.

For example, the following says the single member of the 'cpu 0' group
must have its Model property be equal to "1234" for the action to run:

{
    "groups": [{
        "name": "cpu 0",
        "interface": "xyz.openbmc_project.Inventory.Decorator.Asset",
        "property": { "name": "Model" }
      }
      ...
    ],
    ...
    "name": "mapped_floor",
    "key_group": "ambient temp",
    "condition_group": "cpu 0",
    "condition_value": "1234",
    "condition_op": "equal",
    ...
}

If a condition is present but isn't met, the action will remove its
floor hold if it has one to support the case of the condition property
changing values.

Change-Id: I3ede20efd334e2c5292a441c089534420959c7bc
Signed-off-by: Matt Spinler <spinler@us.ibm.com>
diff --git a/control/json/actions/mapped_floor.cpp b/control/json/actions/mapped_floor.cpp
index 55414ac..906559b 100644
--- a/control/json/actions/mapped_floor.cpp
+++ b/control/json/actions/mapped_floor.cpp
@@ -59,6 +59,7 @@
     setKeyGroup(jsonObj);
     setFloorTable(jsonObj);
     setDefaultFloor(jsonObj);
+    setCondition(jsonObj);
 }
 
 const Group* MappedFloor::getGroup(const std::string& name)
@@ -178,6 +179,53 @@
     }
 }
 
+void MappedFloor::setCondition(const json& jsonObj)
+{
+    // condition_group, condition_value, and condition_op
+    // are optional, though they must show up together.
+    // Assume if condition_group is present then they all
+    // must be.
+    if (!jsonObj.contains("condition_group"))
+    {
+        return;
+    }
+
+    _conditionGroup = getGroup(jsonObj["condition_group"].get<std::string>());
+
+    if (_conditionGroup->getMembers().size() != 1)
+    {
+        throw ActionParseError{
+            ActionBase::getName(),
+            fmt::format("condition_group {} must only have 1 member",
+                        _conditionGroup->getName())};
+    }
+
+    if (!jsonObj.contains("condition_value"))
+    {
+        throw ActionParseError{ActionBase::getName(),
+                               "Missing required 'condition_value' entry in "
+                               "mapped_floor action"};
+    }
+
+    _conditionValue = getJsonValue(jsonObj["condition_value"]);
+
+    if (!jsonObj.contains("condition_op"))
+    {
+        throw ActionParseError{ActionBase::getName(),
+                               "Missing required 'condition_op' entry in "
+                               "mapped_floor action"};
+    }
+
+    _conditionOp = jsonObj["condition_op"].get<std::string>();
+
+    if ((_conditionOp != "equal") && (_conditionOp != "not_equal"))
+    {
+        throw ActionParseError{ActionBase::getName(),
+                               "Invalid 'condition_op' value in "
+                               "mapped_floor action"};
+    }
+}
+
 /**
  * @brief Converts the variant to a double if it's a
  *        int32_t or int64_t.
@@ -254,8 +302,65 @@
     return max;
 }
 
+bool MappedFloor::meetsCondition()
+{
+    if (!_conditionGroup)
+    {
+        return true;
+    }
+
+    bool meets = false;
+
+    // setCondition() also checks these
+    assert(_conditionGroup->getMembers().size() == 1);
+    assert((_conditionOp == "equal") || (_conditionOp == "not_equal"));
+
+    const auto& member = _conditionGroup->getMembers()[0];
+
+    try
+    {
+        auto value =
+            Manager::getObjValueVariant(member, _conditionGroup->getInterface(),
+                                        _conditionGroup->getProperty());
+
+        if ((_conditionOp == "equal") && (value == _conditionValue))
+        {
+            meets = true;
+        }
+        else if ((_conditionOp == "not_equal") && (value != _conditionValue))
+        {
+            meets = true;
+        }
+    }
+    catch (const std::out_of_range& e)
+    {
+        // Property not there, so consider it failing the 'equal'
+        // condition and passing the 'not_equal' condition.
+        if (_conditionOp == "equal")
+        {
+            meets = false;
+        }
+        else // not_equal
+        {
+            meets = true;
+        }
+    }
+
+    return meets;
+}
+
 void MappedFloor::run(Zone& zone)
 {
+    if (!meetsCondition())
+    {
+        // Make sure this no longer has a floor hold
+        if (zone.hasFloorHold(getUniqueName()))
+        {
+            zone.setFloorHold(getUniqueName(), 0, false);
+        }
+        return;
+    }
+
     std::optional<uint64_t> newFloor;
 
     auto keyValue = getMaxGroupValue(*_keyGroup);
diff --git a/control/json/actions/mapped_floor.hpp b/control/json/actions/mapped_floor.hpp
index 9c8a937..75cc7b2 100644
--- a/control/json/actions/mapped_floor.hpp
+++ b/control/json/actions/mapped_floor.hpp
@@ -101,6 +101,17 @@
  *   3. The default floor in the zone config
  *     - Chosen when when 2. would be used, but it wasn't supplied
  *
+ * This action can also have a condition specified where a group property
+ * must either match or not match a given value to determine if the
+ * action should run or not.  This requires the following in the JSON:
+ *    "condition_group": The group name
+ *       - As of now, group must just have a single member.
+ *    "condition_op": Either "equal" or "not_equal"
+ *    "condition_value": The value to check against
+ *
+ * This allows completely separate mapped_floor actions to run based on
+ * the value of a D-bus property - i.e. it allows multiple floor tables.
+ *
  * Other notes:
  *  - If a group has multiple members, they must be numeric or else
  *    the code will throw an exception.
@@ -113,7 +124,6 @@
  *    it can be:
  *            "parameter": "some_parameter"
  */
-
 class MappedFloor : public ActionBase, public ActionRegister<MappedFloor>
 {
   public:
@@ -165,6 +175,13 @@
     void setFloorTable(const json& jsonObj);
 
     /**
+     * @brief Parse and set the conditions
+     *
+     * @param jsonObj - JSON object for the action
+     */
+    void setCondition(const json& jsonObj);
+
+    /**
      * @brief Applies the offset in offsetParameter to the
      *        value passed in.
      *
@@ -207,9 +224,26 @@
      */
     const Group* getGroup(const std::string& name);
 
+    /**
+     * @brief Checks if the condition is met, if there is one.
+     *
+     * @return bool - False if there is a condition and it
+     *                isn't met, true otherwise.
+     */
+    bool meetsCondition();
+
     /* Key group pointer */
     const Group* _keyGroup;
 
+    /* condition group pointer */
+    const Group* _conditionGroup = nullptr;
+
+    /* Condition value */
+    PropertyVariantType _conditionValue;
+
+    /* Condition operation */
+    std::string _conditionOp;
+
     /* Optional default floor value for the action */
     std::optional<uint64_t> _defaultFloor;
 
diff --git a/docs/control/events.md b/docs/control/events.md
index b90a02d..8f14e9f 100644
--- a/docs/control/events.md
+++ b/docs/control/events.md
@@ -485,6 +485,35 @@
 
 At the end of the analysis, a floor hold will be set with the final floor value.
 
+This action can also have a condition specified where a group property must
+either match or not match a given value to determine if the action should run or
+not. This requires the following in the JSON:
+
+- "condition_group": The group name
+  - For now, this group must just have a single member.
+- "condition_op": Either "equal" or "not_equal"
+- "condition_value": The value to check against
+
+For example, the following says the single member of the 'cpu 0' group must have
+its Model property be equal to "1234" for the action to run:
+
+```
+    "groups": [{
+        "name": "cpu 0",
+        "interface": "xyz.openbmc_project.Inventory.Decorator.Asset",
+        "property": { "name": "Model" }
+      }
+      ...
+    ],
+    ...
+    "name": "mapped_floor",
+    "key_group": "ambient temp",
+    "condition_group": "cpu 0",
+    "condition_value": "1234",
+    "condition_op": "equal",
+    ...
+```
+
 ### set_target_on_missing_owner
 
 Sets the fans to a configured target when any service owner associated to the