Add median* condition

The median condition determines the median value from a configured group
of properties and checks that against a defined condition to determine
whether or not the callback is called.

*Note: When 2 properties are used with the median condition, the
property determined to be the max is used instead of the average of the
2 properties. This is for providing a "worst case" median value.

An example would be to create a group consisting of multiple ambient
sensors where the median value from these ambient sensors would be used
to shutdown the system if above a given temperature.

i.e.)

- name: median temps
  description: >
    'If this condition passes the median ambient temperature
    is too high(>= 45C). Shut the system down.'
  class: condition
  condition: median
  paths: ambient sensors
  properties: ambient temp
  callback: ambient log and shutdown
  op: '>='
  bound: 45000
  oneshot: true

Tested:
    A defined median condition is generated according to the
MedianCondition class
    The MedianCondition class produces a single median value from a
group of property values
    Median value used against the given operation to determine if
callback is called or not

Change-Id: Icd53e1a6e30a263b7706a935f040eea97dcc2414
Signed-off-by: Matthew Barth <msbarth@us.ibm.com>
diff --git a/src/Makefile.am b/src/Makefile.am
index 3838e05..bac980e 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -37,6 +37,7 @@
 	templates/callbackgroup.mako.cpp \
 	templates/conditional.mako.cpp \
 	templates/count.mako.cpp \
+	templates/median.mako.cpp \
 	templates/generated.mako.hpp \
 	templates/journal.mako.cpp \
 	templates/elog.mako.cpp \
diff --git a/src/median.hpp b/src/median.hpp
new file mode 100644
index 0000000..a3ffe67
--- /dev/null
+++ b/src/median.hpp
@@ -0,0 +1,123 @@
+#pragma once
+
+#include "callback.hpp"
+#include "data_types.hpp"
+
+#include <algorithm>
+#include <functional>
+
+namespace phosphor
+{
+namespace dbus
+{
+namespace monitoring
+{
+
+/** @class MedianCondition
+ *  @brief Determine a median from properties and apply a condition.
+ *
+ *  When invoked, a median class instance performs its condition
+ *  test against a median value that has been determined from a set
+ *  of configured properties.
+ *
+ *  Once the median value is determined, a C++ relational operator
+ *  is applied to it and a value provided by the configuration file,
+ *  which determines if the condition passes or not.
+ *
+ *  Where no property values configured are found to determine a median from,
+ *  the condition defaults to `true` and passes.
+ *
+ *  If the oneshot parameter is true, then this condition won't pass
+ *  again until it fails at least once.
+ */
+template <typename T>
+class MedianCondition : public IndexedConditional
+{
+  public:
+    MedianCondition() = delete;
+    MedianCondition(const MedianCondition&) = default;
+    MedianCondition(MedianCondition&&) = default;
+    MedianCondition& operator=(const MedianCondition&) = default;
+    MedianCondition& operator=(MedianCondition&&) = default;
+    ~MedianCondition() = default;
+
+    MedianCondition(const PropertyIndex& conditionIndex,
+                    const std::function<bool(T)>& _medianOp,
+                    bool oneshot = false) :
+        IndexedConditional(conditionIndex),
+        medianOp(_medianOp), oneshot(oneshot)
+    {
+    }
+
+    bool operator()() override
+    {
+        // Default the condition result to true
+        // if no property values are found to produce a median.
+        auto result = true;
+        std::vector<T> values;
+        for (const auto& item : index)
+        {
+            const auto& storage = std::get<storageIndex>(item.second);
+            // Don't count properties that don't exist.
+            if (std::get<valueIndex>(storage.get()).empty())
+            {
+                continue;
+            }
+            values.emplace_back(
+                any_ns::any_cast<T>(std::get<valueIndex>(storage.get())));
+        }
+
+        if (!values.empty())
+        {
+            auto median = values.front();
+            // Get the determined median value
+            if (values.size() == 2)
+            {
+                // For 2 values, use the highest instead of the average
+                // for a worst case median value
+                median = *std::max_element(values.begin(), values.end());
+            }
+            else if (values.size() > 2)
+            {
+                const auto oddIt = values.begin() + values.size() / 2;
+                std::nth_element(values.begin(), oddIt, values.end());
+                median = *oddIt;
+                // Determine median for even number of values
+                if (index.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;
+                }
+            }
+
+            // Now apply the condition to the median value.
+            result = medianOp(median);
+        }
+
+        // If this was a oneshot and the the condition has already
+        // passed, then don't let it pass again until the condition
+        // has gone back to false.
+        if (oneshot && result && lastResult)
+        {
+            return false;
+        }
+
+        lastResult = result;
+        return result;
+    }
+
+  private:
+    /** @brief The comparison to perform on the median value. */
+    std::function<bool(T)> medianOp;
+    /** @brief If the condition can be allowed to pass again
+               on subsequent checks that are also true. */
+    const bool oneshot;
+    /** @brief The result of the previous check. */
+    bool lastResult = false;
+};
+
+} // namespace monitoring
+} // namespace dbus
+} // namespace phosphor
diff --git a/src/pdmgen.py b/src/pdmgen.py
index c4184fc..963c7b3 100755
--- a/src/pdmgen.py
+++ b/src/pdmgen.py
@@ -768,6 +768,33 @@
             indent=indent)
 
 
+class MedianCondition(Condition, Renderer):
+    '''Handle the median condition config file directive.'''
+
+    def __init__(self, *a, **kw):
+        self.op = kw.pop('op')
+        self.bound = kw.pop('bound')
+        self.oneshot = TrivialArgument(
+            type='boolean',
+            value=kw.pop('oneshot', False))
+        super(MedianCondition, self).__init__(**kw)
+
+    def setup(self, objs):
+        '''Resolve type.'''
+
+        super(MedianCondition, self).setup(objs)
+        self.bound = TrivialArgument(
+            type=self.type,
+            value=self.bound)
+
+    def construct(self, loader, indent):
+        return self.render(
+            loader,
+            'median.mako.cpp',
+            c=self,
+            indent=indent)
+
+
 class Journal(Callback, Renderer):
     '''Handle the journal callback config file directive.'''
 
@@ -1110,6 +1137,7 @@
             },
             'condition': {
                 'count': CountCondition,
+                'median': MedianCondition,
             },
         }
 
diff --git a/src/templates/generated.mako.hpp b/src/templates/generated.mako.hpp
index d0a5ead..3436b49 100644
--- a/src/templates/generated.mako.hpp
+++ b/src/templates/generated.mako.hpp
@@ -6,6 +6,7 @@
 #include <chrono>
 #include <string>
 #include "count.hpp"
+#include "median.hpp"
 #include "data_types.hpp"
 #include "journal.hpp"
 #include "elog.hpp"
diff --git a/src/templates/median.mako.cpp b/src/templates/median.mako.cpp
new file mode 100644
index 0000000..7730483
--- /dev/null
+++ b/src/templates/median.mako.cpp
@@ -0,0 +1,4 @@
+std::make_unique<MedianCondition<${c.datatype}>>(
+${indent(1)}ConfigPropertyIndicies::get()[${c.instances}],
+${indent(1)}[](const auto& item){return item ${c.op} ${c.bound.argument(loader, indent=indent +1)};},
+${indent(1)}${c.oneshot.argument(loader, indent=indent +1)})\