Add a modified median calculation

This is for virtual sensors added via D-Bus.

We use the median value if there is 3 or more valid values. If there
are only two valid values we used the biggest and if there is only one
valid value we use that.

Tested:
- Modified temperature values of sensors like this:

busctl set-property xyz.openbmc_project.HwmonTempSensor
/xyz/openbmc_project/sensors/temperature/Ambient_0_Temp
xyz.openbmc_project.Sensor.Value Value d 30

and expected changes were observed in the value of the Ambient Virtual
Temp value.

busctl get-property xyz.openbmc_project.VirtualSensor
/xyz/openbmc_project/sensors/temperature/Ambient_Virtual_Temp
xyz.openbmc_project.Sensor.Value Value

- Threshold alarms were asserted and deasserted when expected.

- Temperature values were not used when out of range.

Change-Id: I58ff7dcc17c6f87209434a754f91a99f483140aa
Signed-off-by: Rashmica Gupta <rashmica.g@gmail.com>
diff --git a/virtualSensor.cpp b/virtualSensor.cpp
index be9d78a..4e9f50a 100644
--- a/virtualSensor.cpp
+++ b/virtualSensor.cpp
@@ -16,7 +16,8 @@
 static constexpr auto entityManagerBusName =
     "xyz.openbmc_project.EntityManager";
 static constexpr auto vsThresholdsIfaceSuffix = ".Thresholds";
-static constexpr std::array<const char*, 0> calculationIfaces = {};
+static constexpr std::array<const char*, 1> calculationIfaces = {
+    "xyz.openbmc_project.Configuration.ModifiedMedian"};
 
 using namespace phosphor::logging;
 
@@ -384,12 +385,31 @@
     ValueIface::value(value);
 }
 
-double VirtualSensor::calculateValue()
+double VirtualSensor::calculateValue(const std::string& calculation,
+                                     const VirtualSensor::ParamMap& paramMap)
 {
-    // Placeholder until calculation types are added
+    auto itr = std::find(calculationIfaces.begin(), calculationIfaces.end(),
+                         calculation);
+    if (itr == calculationIfaces.end())
+    {
+        return std::numeric_limits<double>::quiet_NaN();
+    }
+    else if (calculation == "xyz.openbmc_project.Configuration.ModifiedMedian")
+    {
+        return calculateModifiedMedianValue(paramMap);
+    }
     return std::numeric_limits<double>::quiet_NaN();
 }
 
+bool VirtualSensor::sensorInRange(double value)
+{
+    if (value <= this->maxValidInput && value >= this->minValidInput)
+    {
+        return true;
+    }
+    return false;
+}
+
 void VirtualSensor::updateVirtualSensor()
 {
     for (auto& param : paramMap)
@@ -408,8 +428,9 @@
     }
     auto itr =
         std::find(calculationIfaces.begin(), calculationIfaces.end(), exprStr);
-    auto val = (itr == calculationIfaces.end()) ? expression.value()
-                                                : calculateValue();
+    auto val = (itr == calculationIfaces.end())
+                   ? expression.value()
+                   : calculateValue(exprStr, paramMap);
 
     /* Set sensor value to dbus interface */
     setSensorValue(val);
@@ -427,6 +448,47 @@
     checkThresholds(val, hardShutdownIface);
 }
 
+double VirtualSensor::calculateModifiedMedianValue(
+    const VirtualSensor::ParamMap& paramMap)
+{
+    std::vector<double> values;
+
+    for (auto& param : paramMap)
+    {
+        auto& name = param.first;
+        if (auto var = symbols.get_variable(name))
+        {
+            if (!sensorInRange(var->ref()))
+            {
+                continue;
+            }
+            values.push_back(var->ref());
+        }
+    }
+
+    size_t size = values.size();
+    std::sort(values.begin(), values.end());
+    switch (size)
+    {
+        case 2:
+            /* Choose biggest value */
+            return values.at(1);
+        case 0:
+            return std::numeric_limits<double>::quiet_NaN();
+        default:
+            /* Choose median value */
+            if (size % 2 == 0)
+            {
+                // Average of the two middle values
+                return (values.at(size / 2) + values.at(size / 2 - 1)) / 2;
+            }
+            else
+            {
+                return values.at((size - 1) / 2);
+            }
+    }
+}
+
 void VirtualSensor::createThresholds(const Json& threshold,
                                      const std::string& objPath)
 {
diff --git a/virtualSensor.hpp b/virtualSensor.hpp
index ada20d9..b4fd815 100644
--- a/virtualSensor.hpp
+++ b/virtualSensor.hpp
@@ -122,6 +122,8 @@
     void setSensorValue(double value);
     /** @brief Update sensor at regular intrval */
     void updateVirtualSensor();
+    /** @brief Check if sensor value is in valid range */
+    bool sensorInRange(double value);
 
     /** @brief Map of list of parameters */
     using ParamMap =
@@ -175,7 +177,11 @@
                            const std::string& calculationType);
 
     /** @brief Returns which calculation function or expression to use */
-    double calculateValue();
+    double calculateValue(const std::string& sensortype,
+                          const VirtualSensor::ParamMap& paramMap);
+    /** @brief Calculate median value from sensors */
+    double
+        calculateModifiedMedianValue(const VirtualSensor::ParamMap& paramMap);
     /** @brief create threshold objects from json config */
     void createThresholds(const Json& threshold, const std::string& objPath);
     /** @brief parse config from entity manager **/