Fan floor by median sensor value within a range

Add action to set the floor speed based on sensors from within a defined
valid range and using their median value. The floor speed is selected
from the first map key entry that the median value is less than where 3
or more sensor values are valid. In the case where less than 3 sensor
values are valid, use the highest valid value and default the floor
speed when 0 sensor values are valid.

Tested:
    Configured wspoon with this action & correct floor resulted:
        Sensor value invalidated outside of range
        Single valid ambient sensor
        Default floor with no valid sensors(kill ambient service)
        Highest value used w/ 2 valid sensors
        Middle value used w/ odd number of valid sensors(median)
        Average value of middle two valid sensors w/ even number(median)

Change-Id: Ia1599ff13e25dbd7caa7b02c9340cc3e1e9947c6
Signed-off-by: Matthew Barth <msbarth@us.ibm.com>
diff --git a/control/Makefile.am b/control/Makefile.am
index 43f7b69..2ac570a 100644
--- a/control/Makefile.am
+++ b/control/Makefile.am
@@ -9,6 +9,7 @@
 	fan.cpp \
 	main.cpp \
 	manager.cpp \
+	utility.cpp \
 	preconditions.cpp \
 	actions.cpp \
 	triggers.cpp \
diff --git a/control/actions.cpp b/control/actions.cpp
index 5bd9e3a..0388b5c 100644
--- a/control/actions.cpp
+++ b/control/actions.cpp
@@ -1,4 +1,5 @@
 #include "actions.hpp"
+#include "utility.hpp"
 
 namespace phosphor
 {
@@ -286,6 +287,75 @@
     };
 }
 
+Action set_floor_from_median_sensor_value(
+        int64_t lowerBound,
+        int64_t upperBound,
+        std::map<int64_t, uint64_t>&& valueToSpeed)
+{
+    return [lowerBound,
+            upperBound,
+            valueToSpeed = std::move(valueToSpeed)](control::Zone& zone,
+                                                    const Group& group)
+    {
+        auto speed = zone.getDefFloor();
+        if (group.size() != 0)
+        {
+            std::vector<int64_t> validValues;
+            for (auto const& member : group)
+            {
+                try
+                {
+                    auto value = zone.template getPropertyValue<int64_t>(
+                            std::get<pathPos>(member),
+                            std::get<intfPos>(member),
+                            std::get<propPos>(member));
+                    if (value == std::clamp(value, lowerBound, upperBound))
+                    {
+                        // Sensor value is valid
+                        validValues.emplace_back(value);
+                    }
+                }
+                catch (const std::out_of_range& oore)
+                {
+                    continue;
+                }
+            }
+
+            if (!validValues.empty())
+            {
+                auto median = validValues.front();
+                // Get the determined median value
+                if (validValues.size() == 2)
+                {
+                    // For 2 values, use the highest instead of the average
+                    // for a thermally safe floor
+                    median = *std::max_element(validValues.begin(),
+                                               validValues.end());
+                }
+                else if (validValues.size() > 2)
+                {
+                    median = utility::getMedian(validValues);
+                }
+
+                // Use determined median sensor value to find floor speed
+                auto it = std::find_if(
+                    valueToSpeed.begin(),
+                    valueToSpeed.end(),
+                    [&median](auto const& entry)
+                    {
+                        return median < entry.first;
+                    }
+                );
+                if (it != std::end(valueToSpeed))
+                {
+                    speed = (*it).second;
+                }
+            }
+        }
+        zone.setFloor(speed);
+    };
+}
+
 } // namespace action
 } // namespace control
 } // namespace fan
diff --git a/control/actions.hpp b/control/actions.hpp
index 9b9bbed..f7053cb 100644
--- a/control/actions.hpp
+++ b/control/actions.hpp
@@ -347,6 +347,28 @@
     };
 }
 
+/**
+ * @brief An action to set the floor speed on a zone
+ * @details Using sensor group values that are within a defined range, the
+ * floor speed is selected from the first map key entry that the median
+ * sensor value is less than where 3 or more sensor group values are valid.
+ * In the case where less than 3 sensor values are valid, use the highest
+ * sensor group value and default the floor speed when 0 sensor group values
+ * are valid.
+ *
+ * @param[in] lowerBound - Lowest allowed sensor value to be valid
+ * @param[in] upperBound - Highest allowed sensor value to be valid
+ * @param[in] valueToSpeed - Ordered map of sensor value-to-speed
+ *
+ * @return Action lambda function
+ *     An Action function to set the zone's floor speed from a resulting group
+ * of valid sensor values based on their highest value or median.
+ */
+Action set_floor_from_median_sensor_value(
+        int64_t lowerBound,
+        int64_t upperBound,
+        std::map<int64_t, uint64_t>&& valueToSpeed);
+
 } // namespace action
 } // namespace control
 } // namespace fan
diff --git a/control/utility.cpp b/control/utility.cpp
new file mode 100644
index 0000000..49a8a13
--- /dev/null
+++ b/control/utility.cpp
@@ -0,0 +1,39 @@
+#include <algorithm>
+#include <stdexcept>
+
+#include "utility.hpp"
+
+namespace phosphor
+{
+namespace fan
+{
+namespace control
+{
+namespace utility
+{
+
+int64_t getMedian(std::vector<int64_t>& values)
+{
+    if (values.empty())
+    {
+        throw std::out_of_range("getMedian(): Empty list of values");
+    }
+    const auto oddIt = values.begin() + values.size() / 2;
+    std::nth_element(values.begin(), oddIt, values.end());
+    auto median = *oddIt;
+    // Determine median for even number of values
+    if (values.size() % 2 == 0)
+    {
+        // Use average of middle 2 values for median
+        const auto evenIt = values.begin() + values.size() / 2 - 1;
+        std::nth_element(values.begin(), evenIt, values.end());
+        median = (median + *evenIt) / 2;
+    }
+
+    return median;
+}
+
+} // namespace utility
+} // namespace control
+} // namespace fan
+} // namespace phosphor
diff --git a/control/utility.hpp b/control/utility.hpp
new file mode 100644
index 0000000..3458180
--- /dev/null
+++ b/control/utility.hpp
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <vector>
+#include "types.hpp"
+
+namespace phosphor
+{
+namespace fan
+{
+namespace control
+{
+namespace utility
+{
+
+/**
+ * @brief A utility function to return a median value
+ * @details A median value is determined from a set of values where the middle
+ * value is returned from an odd set of values and an average of the middle
+ * two values for an even set of values.
+ *
+ * @param[in] values - Set of values to determine the median from
+ *
+ * @return A median value
+ *
+ * @throw std::out_of_range Empty list of values given
+ *
+ * Note: The set of values will be partially re-ordered
+ * https://en.cppreference.com/w/cpp/algorithm/nth_element
+ */
+int64_t getMedian(std::vector<int64_t>& values);
+
+} // namespace utility
+} // namespace control
+} // namespace fan
+} // namespace phosphor