monitor: Add fan_frus_with_nonfunc_rotors cause

Create a new power off rule to power off when a specific number of fan
FRUs have nonfunctional rotors.  With this rule failing rotors can be
treated differently when they are spread across fans FRUS than when they
are within the same fan FRU.

For example, if both rotors of a 2 rotor fan fail the system can stay
up, but if 2 rotors in separate fans fail then the system could be made
to power off.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: Ia1d13596a9e8a6e3a361e5b631699a3c80e36fb8
diff --git a/docs/monitor/power_off_config.md b/docs/monitor/power_off_config.md
index 175eec9..7c7fef6 100644
--- a/docs/monitor/power_off_config.md
+++ b/docs/monitor/power_off_config.md
@@ -19,6 +19,8 @@
   - "missing_fan_frus" - Power off due to missing fan enclosures
   - "nonfunc_fan_rotors" - Power off due to nonfunctional fan
     rotors([`sensors`](sensors.md))
+  - "fan_frus_with_nonfunc_rotors" - Power off due to the number of fan FRUs
+    with at least one nonfunctional rotor.
 - `count` - integer
   - Number of the configured `cause` instances to begin the power off `type`
 
diff --git a/monitor/json_parser.cpp b/monitor/json_parser.cpp
index bf37307..20a3bcb 100644
--- a/monitor/json_parser.cpp
+++ b/monitor/json_parser.cpp
@@ -436,8 +436,12 @@
         causes{
             {"missing_fan_frus",
              [count]() { return std::make_unique<MissingFanFRUCause>(count); }},
-            {"nonfunc_fan_rotors", [count]() {
+            {"nonfunc_fan_rotors",
+             [count]() {
         return std::make_unique<NonfuncFanRotorCause>(count);
+    }},
+            {"fan_frus_with_nonfunc_rotors", [count]() {
+        return std::make_unique<FanFRUsWithNonfuncRotorsCause>(count);
     }}};
 
     auto it = causes.find(powerOffCause);
diff --git a/monitor/power_off_cause.hpp b/monitor/power_off_cause.hpp
index 63b0cfd..bc3704f 100644
--- a/monitor/power_off_cause.hpp
+++ b/monitor/power_off_cause.hpp
@@ -138,7 +138,7 @@
      * @brief Constructor
      *
      * @param[in] count - The minimum number of rotors that must be
-     *                    nonfunctional nonfunctional to need a power off.
+     *                    nonfunctional to need a power off.
      */
     explicit NonfuncFanRotorCause(size_t count) :
         PowerOffCause(count, "Nonfunctional Fan Rotors")
@@ -164,4 +164,53 @@
     }
 };
 
+/**
+ * @class FanFRUsWithNonfuncRotorsCause
+ *
+ * This class provides a satisfied() method that checks for
+ * fans with nonfunctional fan rotors in the fan health map.
+ */
+class FanFRUsWithNonfuncRotorsCause : public PowerOffCause
+{
+  public:
+    FanFRUsWithNonfuncRotorsCause() = delete;
+    ~FanFRUsWithNonfuncRotorsCause() = default;
+    FanFRUsWithNonfuncRotorsCause(const FanFRUsWithNonfuncRotorsCause&) =
+        delete;
+    FanFRUsWithNonfuncRotorsCause&
+        operator=(const FanFRUsWithNonfuncRotorsCause&) = delete;
+    FanFRUsWithNonfuncRotorsCause(FanFRUsWithNonfuncRotorsCause&&) = delete;
+    FanFRUsWithNonfuncRotorsCause&
+        operator=(FanFRUsWithNonfuncRotorsCause&&) = delete;
+
+    /**
+     * @brief Constructor
+     *
+     * @param[in] count - The minimum number of fan FRUs with
+     *                    nonfunctional rotors to need a power off.
+     */
+    explicit FanFRUsWithNonfuncRotorsCause(size_t count) :
+        PowerOffCause(count, "Fans with Nonfunctional Rotors")
+    {}
+
+    /**
+     * @brief Returns true if 'count' or more fan FRUs have
+     *        nonfunctional rotors.
+     *
+     * @param[in] fanHealth - The FanHealth map
+     */
+    bool satisfied(const FanHealth& fanHealth) override
+    {
+        size_t count = std::count_if(fanHealth.begin(), fanHealth.end(),
+                                     [](const auto& fan) {
+            const auto& tachs = std::get<sensorFuncHealthPos>(fan.second);
+
+            return std::any_of(tachs.begin(), tachs.end(),
+                               [](bool func) { return !func; });
+        });
+
+        return count >= _count;
+    }
+};
+
 } // namespace phosphor::fan::monitor
diff --git a/monitor/test/power_off_cause_test.cpp b/monitor/test/power_off_cause_test.cpp
index 1c057b2..9016ddb 100644
--- a/monitor/test/power_off_cause_test.cpp
+++ b/monitor/test/power_off_cause_test.cpp
@@ -53,3 +53,56 @@
     health["fan2"] = {false, {true, true}};
     EXPECT_FALSE(cause.satisfied(health));
 }
+
+TEST(PowerOffCauseTest, FansWithNonFuncRotorsTest)
+{
+    {
+        FanHealth health{{"fan0", {true, {true, true}}},
+                         {"fan1", {true, {true, true}}},
+                         {"fan2", {true, {true, true}}},
+                         {"fan3", {true, {true, true}}}};
+
+        FanFRUsWithNonfuncRotorsCause cause{2};
+        EXPECT_FALSE(cause.satisfied(health));
+    }
+
+    {
+        FanHealth health{{"fan0", {true, {true, true}}},
+                         {"fan1", {true, {true, true}}},
+                         {"fan2", {true, {false, true}}},
+                         {"fan3", {true, {true, true}}}};
+
+        FanFRUsWithNonfuncRotorsCause cause{2};
+        EXPECT_FALSE(cause.satisfied(health));
+    }
+
+    {
+        FanHealth health{{"fan0", {true, {true, true}}},
+                         {"fan1", {true, {false, false}}},
+                         {"fan2", {true, {true, true}}},
+                         {"fan3", {true, {true, true}}}};
+
+        FanFRUsWithNonfuncRotorsCause cause{2};
+        EXPECT_FALSE(cause.satisfied(health));
+    }
+
+    {
+        FanHealth health{{"fan0", {true, {true, true}}},
+                         {"fan1", {true, {false, false}}},
+                         {"fan2", {true, {true, true}}},
+                         {"fan3", {true, {true, false}}}};
+
+        FanFRUsWithNonfuncRotorsCause cause{2};
+        EXPECT_TRUE(cause.satisfied(health));
+    }
+
+    {
+        FanHealth health{{"fan0", {true, {false, true}}},
+                         {"fan1", {true, {true, true}}},
+                         {"fan2", {true, {true, false}}},
+                         {"fan3", {true, {true, true}}}};
+
+        FanFRUsWithNonfuncRotorsCause cause{2};
+        EXPECT_TRUE(cause.satisfied(health));
+    }
+}