Add filters option to property watch class

Each property changed signal calls a defined callback, an optional set
of filters can be defined to filter out property values not intended to
be used within the callback. These filters can be setup to eliminate
property value(s) that should be ignored per property group and its
configured callback.

ex.) Handle errant sensor values that could occur from a faulty device
so these values do not cause unwanted results from a callback function.

Example config entry to throw out an ambient temperature less than 0C or
greater than 100C:

- name: watch ambient temps
  description: >
    'Filter the ambient temps, discarding any value that
    does not pass, then trigger the callback logic.'
  class: watch
  watch: property
  paths: ambient sensors
  properties: ambient temp
  callback: median temps
  filters:
    - op: '>='
      bound: 0
    - op: '<='
      bound: 100000

Tested:
    Generate a single filter function against a group of properties
    Generate multiple filter functions against a group of properties
    Property values are cleared when it fails to pass a filter
    Property values that pass all filters are updated

Change-Id: Id5126263096b4787a40befdb14cf3514038df3ed
Signed-off-by: Matthew Barth <msbarth@us.ibm.com>
diff --git a/src/example/example.yaml b/src/example/example.yaml
index b011a26..c613d1e 100644
--- a/src/example/example.yaml
+++ b/src/example/example.yaml
@@ -50,12 +50,23 @@
     'A property watch instructs PDM to maintain a cache of the state
     of the specified properties on the specified D-Bus objects.
 
-    An optional callback can be triggered when property values change.'
+    An optional set of filters can be applied to the specified properties,
+    where each property's cache is cleared when it fails to pass
+    any one filter. The property's cache is cleared so it will not have an
+    affect on any optional callback that may be triggered.
+
+    An optional callback can be triggered when property values change and
+    those values pass all filters that may be defined.'
   class: watch
   watch: property
   paths: example path group
   properties: example property group
   callback: example count condition
+  filters:
+    - op: '>='
+      bound: 0
+    - op: '<='
+      bound: 100
 
 - name: example journal callback
   description: >
diff --git a/src/pdmgen.py b/src/pdmgen.py
index 963c7b3..dc1c9b7 100755
--- a/src/pdmgen.py
+++ b/src/pdmgen.py
@@ -196,6 +196,30 @@
         return a
 
 
+class OpArgument(Argument):
+    '''Operation type arguments.'''
+
+    def __init__(self, **kw):
+        self.op = kw.pop('op')
+        self.bound = kw.pop('bound')
+        self.decorators = kw.pop('decorators', [])
+        if kw.get('type', None):
+            self.decorators.insert(0, Literal(kw['type']))
+        if kw.get('type', None) == 'string':
+            self.decorators.insert(0, Quote())
+        if kw.get('type', None) == 'boolean':
+            self.decorators.insert(0, FixBool())
+
+        super(OpArgument, self).__init__(**kw)
+
+    def argument(self, loader, indent):
+        a = str(self.bound)
+        for d in self.decorators:
+            a = d(a)
+
+        return a
+
+
 class Indent(object):
     '''Help templates be depth agnostic.'''
 
@@ -616,6 +640,7 @@
     '''Handle the property watch config file directive.'''
 
     def __init__(self, *a, **kw):
+        self.filters = [OpArgument(**x) for x in kw.pop('filters', {})]
         self.callback = kw.pop('callback', None)
         super(PropertyWatch, self).__init__(**kw)
 
diff --git a/src/propertywatch.hpp b/src/propertywatch.hpp
index a94b99e..3168abe 100644
--- a/src/propertywatch.hpp
+++ b/src/propertywatch.hpp
@@ -116,12 +116,16 @@
     PropertyWatchOfType& operator=(const PropertyWatchOfType&) = delete;
     PropertyWatchOfType& operator=(PropertyWatchOfType&&) = default;
     ~PropertyWatchOfType() = default;
-    PropertyWatchOfType(const PropertyIndex& watchIndex, Callback& callback) :
-        PropertyWatch<DBusInterfaceType>(watchIndex, &callback)
+    PropertyWatchOfType(const std::vector<std::function<bool(T)>>& filterOps,
+                        const PropertyIndex& watchIndex, Callback& callback) :
+        PropertyWatch<DBusInterfaceType>(watchIndex, &callback),
+        filterOps(filterOps)
     {
     }
-    PropertyWatchOfType(const PropertyIndex& watchIndex) :
-        PropertyWatch<DBusInterfaceType>(watchIndex, nullptr)
+    PropertyWatchOfType(const std::vector<std::function<bool(T)>>& filterOps,
+                        const PropertyIndex& watchIndex) :
+        PropertyWatch<DBusInterfaceType>(watchIndex, nullptr),
+        filterOps(filterOps)
     {
     }
 
@@ -170,6 +174,10 @@
      */
     void interfacesAdded(const std::string& path,
                          const InterfacesAdded<T>& interfaces);
+
+  private:
+    /** @brief List of filter operations to perform on property changes. */
+    const std::vector<std::function<bool(T)>> filterOps;
 };
 
 } // namespace monitoring
diff --git a/src/propertywatchimpl.hpp b/src/propertywatchimpl.hpp
index 94d6fd2..c639210 100644
--- a/src/propertywatchimpl.hpp
+++ b/src/propertywatchimpl.hpp
@@ -154,11 +154,28 @@
             continue;
         }
 
-        std::get<valueIndex>(std::get<storageIndex>(item->second).get()) =
-            sdbusplus::message::variant_ns::get<T>(p.second);
-
-        // Invoke callback if present.
-        this->callback(Context::SIGNAL);
+        // Run property value thru filter operations
+        auto isFiltered = false;
+        const auto& storage = std::get<storageIndex>(item->second);
+        auto value = sdbusplus::message::variant_ns::get<T>(p.second);
+        for (auto& filterOp : filterOps)
+        {
+            if (!filterOp(value))
+            {
+                // Property value filtered, clear it from storage so
+                // callback functions do not use it
+                isFiltered = true;
+                std::get<valueIndex>(storage.get()).clear();
+                break;
+            }
+        }
+        if (!isFiltered)
+        {
+            // Property value not filtered out, update
+            std::get<valueIndex>(storage.get()) = value;
+            // Invoke callback if present.
+            this->callback(Context::SIGNAL);
+        }
     }
 }
 
diff --git a/src/templates/generated.mako.hpp b/src/templates/generated.mako.hpp
index 3436b49..a6c33bf 100644
--- a/src/templates/generated.mako.hpp
+++ b/src/templates/generated.mako.hpp
@@ -230,6 +230,17 @@
         {
 % for w in watches:
             std::make_unique<PropertyWatchOfType<${w.datatype}, SDBusPlus>>(
+    % if w.filters:
+                std::vector<std::function<bool(${w.datatype})>>{
+        % for f in w.filters:
+                    [](const auto& val){
+                        return val ${f.op} ${f.argument(loader, indent=indent +1)};
+                    },
+        % endfor
+                },
+    % else:
+                std::vector<std::function<bool(${w.datatype})>>{},
+    % endif
     % if w.callback is None:
                 ConfigPropertyIndicies::get()[${w.instances}]),
     % else:
diff --git a/src/test/propertywatchtest.cpp b/src/test/propertywatchtest.cpp
index 1c56658..5a0c121 100644
--- a/src/test/propertywatchtest.cpp
+++ b/src/test/propertywatchtest.cpp
@@ -123,7 +123,7 @@
     MockDBusInterface::instance(dbus);
 
     const std::vector<std::string> expectedMapperInterfaces;
-    PropertyWatchOfType<T, MockDBusInterface> watch(watchIndex);
+    PropertyWatchOfType<T, MockDBusInterface> watch({}, watchIndex);
 
     auto ndx = static_cast<size_t>(0);
     for (const auto& o : convert(watchIndex))