control: Add count state floor action

This action is similar to the count_state_target action except it sets
the fan floor instead of fan target.

It sets the zone to a configured floor with a hold when a number of
members within the group are at a configured state. Once the number of
members at the given state falls below the configured count, the floor
hold is released.

Example JSON is:

{
    "name": "count_state_floor",
    "count": 1,
    "state": false,
    "floor": 5000
}

This will set the floor hold to 5000 when at least 1 member of the
configured group has a value of false.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: Ie67314ad0221264a7a3ee634728e8216cb0a16cd
diff --git a/control/Makefile.am b/control/Makefile.am
index 3b52e47..77a4006 100644
--- a/control/Makefile.am
+++ b/control/Makefile.am
@@ -85,6 +85,7 @@
 	json/actions/timer_based_actions.cpp \
 	json/actions/mapped_floor.cpp \
 	json/actions/set_parameter_from_group_max.cpp \
+	json/actions/count_state_floor.cpp \
 	json/utils/flight_recorder.cpp \
 	json/utils/modifier.cpp
 else
diff --git a/control/json/actions/count_state_floor.cpp b/control/json/actions/count_state_floor.cpp
new file mode 100644
index 0000000..da5bddf
--- /dev/null
+++ b/control/json/actions/count_state_floor.cpp
@@ -0,0 +1,99 @@
+/**
+ * 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 "count_state_floor.hpp"
+
+#include "../manager.hpp"
+#include "../zone.hpp"
+#include "action.hpp"
+#include "group.hpp"
+
+namespace phosphor::fan::control::json
+{
+
+CountStateFloor::CountStateFloor(const json& jsonObj,
+                                 const std::vector<Group>& groups) :
+    ActionBase(jsonObj, groups)
+{
+    setCount(jsonObj);
+    setState(jsonObj);
+    setFloor(jsonObj);
+}
+
+void CountStateFloor::run(Zone& zone)
+{
+    size_t numAtState = 0;
+    for (const auto& group : _groups)
+    {
+        for (const auto& member : group.getMembers())
+        {
+            try
+            {
+                if (Manager::getObjValueVariant(member, group.getInterface(),
+                                                group.getProperty()) == _state)
+                {
+                    numAtState++;
+                }
+            }
+            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));
+}
+
+void CountStateFloor::setCount(const json& jsonObj)
+{
+    if (!jsonObj.contains("count"))
+    {
+        throw ActionParseError{ActionBase::getName(),
+                               "Missing required count value"};
+    }
+    _count = jsonObj["count"].get<size_t>();
+}
+
+void CountStateFloor::setState(const json& jsonObj)
+{
+    if (!jsonObj.contains("state"))
+    {
+        throw ActionParseError{ActionBase::getName(),
+                               "Missing required state value"};
+    }
+    _state = getJsonValue(jsonObj["state"]);
+}
+
+void CountStateFloor::setFloor(const json& jsonObj)
+{
+    if (!jsonObj.contains("floor"))
+    {
+        throw ActionParseError{ActionBase::getName(),
+                               "Missing required floor value"};
+    }
+    _floor = jsonObj["floor"].get<uint64_t>();
+}
+
+} // namespace phosphor::fan::control::json
diff --git a/control/json/actions/count_state_floor.hpp b/control/json/actions/count_state_floor.hpp
new file mode 100644
index 0000000..99aa13d
--- /dev/null
+++ b/control/json/actions/count_state_floor.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 "../zone.hpp"
+#include "action.hpp"
+#include "group.hpp"
+
+#include <nlohmann/json.hpp>
+
+namespace phosphor::fan::control::json
+{
+
+using json = nlohmann::json;
+
+/**
+ * @class CountStateFloor - Action to set a floor when members are at a state
+ *
+ * Sets the fans to a configured floor when a number of members within the
+ * group are at a configured state. Once the number of members at the given
+ * state falls below the configured count, the floor hold is released.
+ *
+ * Example JSON:
+ *    {
+ *      "name": "count_state_floor",
+ *      "count": 1,
+ *      "state": false,
+ *      "floor": 5000
+ *    }
+ */
+class CountStateFloor :
+    public ActionBase,
+    public ActionRegister<CountStateFloor>
+{
+  public:
+    /* Name of this action */
+    static constexpr auto name = "count_state_floor";
+
+    CountStateFloor() = delete;
+    CountStateFloor(const CountStateFloor&) = delete;
+    CountStateFloor(CountStateFloor&&) = delete;
+    CountStateFloor& operator=(const CountStateFloor&) = delete;
+    CountStateFloor& operator=(CountStateFloor&&) = delete;
+    ~CountStateFloor() = default;
+
+    /**
+     * @brief Set floor when a number of members are at a given state
+     *
+     * @param[in] jsonObj - JSON configuration of this action
+     * @param[in] groups - Groups of dbus objects the action uses
+     */
+    CountStateFloor(const json& jsonObj, const std::vector<Group>& groups);
+
+    /**
+     * @brief Run the action
+     *
+     * Counts the number of members within a group are equal to a given state
+     * and when the number of members are at or above the given state, the
+     * zone is set to the configured floor, with a hold.  The hold is released
+     * when the number of members goes below the configured count.
+     *
+     * @param[in] zone - Zone to run the action on
+     */
+    void run(Zone& zone) override;
+
+  private:
+    /**
+     * @brief Parse and set the count
+     *
+     * @param[in] jsonObj - JSON object for the action
+     *
+     * Sets the number of members that must equal the state
+     */
+    void setCount(const json& jsonObj);
+
+    /**
+     * @brief Parse and set the state
+     *
+     * @param[in] jsonObj - JSON object for the action
+     *
+     * Sets the state for each member to equal to set the floor
+     */
+    void setState(const json& jsonObj);
+
+    /**
+     * @brief Parse and set the floor
+     *
+     * @param[in] jsonObj - JSON object for the action
+     *
+     * Sets the floor to use when running the action
+     */
+    void setFloor(const json& jsonObj);
+
+    /* Number of group members */
+    size_t _count;
+
+    /* State the members must be at to set the floor */
+    PropertyVariantType _state;
+
+    /* Floor for this action */
+    uint64_t _floor;
+};
+
+} // namespace phosphor::fan::control::json