hardware: data readers can set up sensor thresholds

We are monitoring some hardware sensors. Some sensor values might be
considered dangerous (e.g. high temperatures). This commit introduces a
way of storing thresholds for data readers and validating measured
sensor values against those thresholds.

This is implemented in a way that each data reader must also implement a
method called thresholds() that returns a mapping of sensor XPath to
a Threshold object. These will be collected in the future by
IETFHardware and used to check obtained sensor values in order to raise
alarms.

Change-Id: I2d3854603f2ec04613906290633151f4d7b034f1
diff --git a/src/ietf-hardware/Factory.cpp b/src/ietf-hardware/Factory.cpp
index 572e320..860d6c4 100644
--- a/src/ietf-hardware/Factory.cpp
+++ b/src/ietf-hardware/Factory.cpp
@@ -48,16 +48,40 @@
                                                                  "psu2",
                                                                  std::make_shared<TransientI2C>(2, 0x59, "ym2151e"));
 
-    ietfHardware->registerDataReader([pduGroup = std::move(pduGroup), psu1, psu2]() {
-        auto psu1Reader = std::async(std::launch::async, [psu1] { return psu1->readValues(); });
-        auto psu2Reader = std::async(std::launch::async, [psu2] { return psu2->readValues(); });
-        auto pduReader = std::async(std::launch::async, [&pduGroup] { return pduGroup(); });
+    struct ParallelPDUReader {
+        Group pduGroup;
+        std::shared_ptr<velia::ietf_hardware::FspYhPsu> psu1;
+        std::shared_ptr<velia::ietf_hardware::FspYhPsu> psu2;
 
-        auto res = psu1Reader.get();
-        res.merge(psu2Reader.get());
-        res.merge(pduReader.get());
-        return res;
-    });
+        ParallelPDUReader(Group&& pduGroup, std::shared_ptr<velia::ietf_hardware::FspYhPsu> psu1, std::shared_ptr<velia::ietf_hardware::FspYhPsu> psu2)
+            : pduGroup(std::move(pduGroup))
+            , psu1(std::move(psu1))
+            , psu2(std::move(psu2))
+        {
+        }
+
+        DataTree operator()()
+        {
+            auto psu1Reader = std::async(std::launch::async, [&] { return psu1->readValues(); });
+            auto psu2Reader = std::async(std::launch::async, [&] { return psu2->readValues(); });
+            auto pduReader = std::async(std::launch::async, [&] { return pduGroup(); });
+
+            auto res = psu1Reader.get();
+            res.merge(psu2Reader.get());
+            res.merge(pduReader.get());
+            return res;
+        }
+
+        ThresholdsBySensorPath thresholds() const
+        {
+            auto res = psu1->thresholds();
+            res.merge(psu2->thresholds());
+            res.merge(pduGroup.thresholds());
+            return res;
+        }
+    };
+
+    ietfHardware->registerDataReader(ParallelPDUReader(std::move(pduGroup), psu1, psu2));
 }
 
 std::shared_ptr<IETFHardware> create(const std::string& applianceName)
diff --git a/src/ietf-hardware/FspYhPsu.cpp b/src/ietf-hardware/FspYhPsu.cpp
index f41aca5..1a5c315 100644
--- a/src/ietf-hardware/FspYhPsu.cpp
+++ b/src/ietf-hardware/FspYhPsu.cpp
@@ -124,17 +124,24 @@
     using velia::ietf_hardware::data_reader::SysfsValue;
     using velia::ietf_hardware::data_reader::Fans;
     using velia::ietf_hardware::data_reader::SensorType;
-    m_properties.emplace_back(SysfsValue<SensorType::Temperature>(m_namePrefix + ":temperature-1", m_namePrefix, m_hwmon, 1));
-    m_properties.emplace_back(SysfsValue<SensorType::Temperature>(m_namePrefix + ":temperature-2", m_namePrefix, m_hwmon, 2));
-    m_properties.emplace_back(SysfsValue<SensorType::Current>(m_namePrefix + ":current-in", m_namePrefix, m_hwmon, 1));
-    m_properties.emplace_back(SysfsValue<SensorType::Current>(m_namePrefix + ":current-12V", m_namePrefix, m_hwmon, 2));
-    m_properties.emplace_back(SysfsValue<SensorType::VoltageAC>(m_namePrefix + ":voltage-in", m_namePrefix, m_hwmon, 1));
-    m_properties.emplace_back(SysfsValue<SensorType::VoltageDC>(m_namePrefix + ":voltage-12V", m_namePrefix, m_hwmon, 2));
-    m_properties.emplace_back(SysfsValue<SensorType::Power>(m_namePrefix + ":power-in", m_namePrefix, m_hwmon, 1));
-    m_properties.emplace_back(SysfsValue<SensorType::Power>(m_namePrefix + ":power-out", m_namePrefix, m_hwmon, 2));
-    m_properties.emplace_back(Fans(m_namePrefix + ":fan", m_namePrefix, m_hwmon, 1));
-    m_properties.emplace_back(SysfsValue<SensorType::Current>(m_namePrefix + ":current-5Vsb", m_namePrefix, m_hwmon, 3));
-    m_properties.emplace_back(SysfsValue<SensorType::VoltageDC>(m_namePrefix + ":voltage-5Vsb", m_namePrefix, m_hwmon, 3));
+
+
+    auto registerReader = [&]<typename DataReaderType>(DataReaderType&& reader) {
+        m_thresholds.merge(reader.thresholds());
+        m_properties.emplace_back(reader);
+    };
+
+    registerReader(SysfsValue<SensorType::Temperature>(m_namePrefix + ":temperature-1", m_namePrefix, m_hwmon, 1));
+    registerReader(SysfsValue<SensorType::Temperature>(m_namePrefix + ":temperature-2", m_namePrefix, m_hwmon, 2));
+    registerReader(SysfsValue<SensorType::Current>(m_namePrefix + ":current-in", m_namePrefix, m_hwmon, 1));
+    registerReader(SysfsValue<SensorType::Current>(m_namePrefix + ":current-12V", m_namePrefix, m_hwmon, 2));
+    registerReader(SysfsValue<SensorType::VoltageAC>(m_namePrefix + ":voltage-in", m_namePrefix, m_hwmon, 1));
+    registerReader(SysfsValue<SensorType::VoltageDC>(m_namePrefix + ":voltage-12V", m_namePrefix, m_hwmon, 2));
+    registerReader(SysfsValue<SensorType::Power>(m_namePrefix + ":power-in", m_namePrefix, m_hwmon, 1));
+    registerReader(SysfsValue<SensorType::Power>(m_namePrefix + ":power-out", m_namePrefix, m_hwmon, 2));
+    registerReader(Fans(m_namePrefix + ":fan", m_namePrefix, m_hwmon, 1));
+    registerReader(SysfsValue<SensorType::Current>(m_namePrefix + ":current-5Vsb", m_namePrefix, m_hwmon, 3));
+    registerReader(SysfsValue<SensorType::VoltageDC>(m_namePrefix + ":voltage-5Vsb", m_namePrefix, m_hwmon, 3));
 }
 
 DataTree FspYhPsu::readValues()
@@ -170,4 +177,9 @@
 
     return res;
 }
+
+ThresholdsBySensorPath FspYhPsu::thresholds() const
+{
+    return m_thresholds;
+}
 }
diff --git a/src/ietf-hardware/FspYhPsu.h b/src/ietf-hardware/FspYhPsu.h
index 27b9952..04004ee 100644
--- a/src/ietf-hardware/FspYhPsu.h
+++ b/src/ietf-hardware/FspYhPsu.h
@@ -32,6 +32,8 @@
     FspYhPsu(const std::filesystem::path& hwmonDir, const std::string& psuName, std::shared_ptr<TransientI2C> i2c);
     ~FspYhPsu();
     velia::ietf_hardware::DataTree readValues();
+    ThresholdsBySensorPath thresholds() const;
+
 private:
     std::mutex m_mtx;
     std::mutex m_condMtx;
@@ -47,7 +49,9 @@
 
     std::string m_namePrefix;
     velia::ietf_hardware::DataTree m_staticData;
+
     std::vector<std::function<velia::ietf_hardware::DataTree()>> m_properties;
+    ThresholdsBySensorPath m_thresholds;
 
     void createPower();
 };
diff --git a/src/ietf-hardware/IETFHardware.cpp b/src/ietf-hardware/IETFHardware.cpp
index 0e7d7ad..95ceb3d 100644
--- a/src/ietf-hardware/IETFHardware.cpp
+++ b/src/ietf-hardware/IETFHardware.cpp
@@ -64,11 +64,6 @@
     return res;
 }
 
-void IETFHardware::registerDataReader(const DataReader& callable)
-{
-    m_callbacks.push_back(callable);
-}
-
 /** @brief A namespace containing predefined data readers for IETFHardware class.
  * @see IETFHardware for more information
  */
@@ -95,11 +90,13 @@
 }
 
 DataTree StaticData::operator()() const { return m_staticData; }
+ThresholdsBySensorPath StaticData::thresholds() const { return {}; }
 
-Fans::Fans(std::string componentName, std::optional<std::string> parent, std::shared_ptr<sysfs::HWMon> hwmon, unsigned fanChannelsCount)
+Fans::Fans(std::string componentName, std::optional<std::string> parent, std::shared_ptr<sysfs::HWMon> hwmon, unsigned fanChannelsCount, Thresholds<int64_t> thresholds)
     : DataReader(std::move(componentName), std::move(parent))
     , m_hwmon(std::move(hwmon))
     , m_fanChannelsCount(fanChannelsCount)
+    , m_thresholds(std::move(thresholds))
 {
     // fans
     addComponent(m_staticData,
@@ -146,6 +143,17 @@
     return res;
 }
 
+ThresholdsBySensorPath Fans::thresholds() const
+{
+    ThresholdsBySensorPath res;
+
+    for (unsigned i = 1; i <= m_fanChannelsCount; i++) {
+        res.emplace(xpathForComponent(m_componentName + ":fan" + std::to_string(i) + ":rpm") + "sensor-data/value", m_thresholds);
+    }
+
+    return res;
+}
+
 std::string getSysfsFilename(const SensorType type, int sysfsChannelNr)
 {
     switch (type) {
@@ -205,10 +213,11 @@
     {"sensor-data/oper-status", "ok"}};
 
 template <SensorType TYPE>
-SysfsValue<TYPE>::SysfsValue(std::string componentName, std::optional<std::string> parent, std::shared_ptr<sysfs::HWMon> hwmon, int sysfsChannelNr)
+SysfsValue<TYPE>::SysfsValue(std::string componentName, std::optional<std::string> parent, std::shared_ptr<sysfs::HWMon> hwmon, int sysfsChannelNr, Thresholds<int64_t> thresholds)
     : DataReader(std::move(componentName), std::move(parent))
     , m_hwmon(std::move(hwmon))
     , m_sysfsFile(getSysfsFilename(TYPE, sysfsChannelNr))
+    , m_thresholds(std::move(thresholds))
 {
     addComponent(m_staticData,
                  m_componentName,
@@ -227,15 +236,22 @@
     return res;
 }
 
+template <SensorType TYPE>
+ThresholdsBySensorPath SysfsValue<TYPE>::thresholds() const
+{
+    return {{xpathForComponent(m_componentName) + "sensor-data/value", m_thresholds}};
+}
+
 template struct SysfsValue<SensorType::Current>;
 template struct SysfsValue<SensorType::Power>;
 template struct SysfsValue<SensorType::Temperature>;
 template struct SysfsValue<SensorType::VoltageAC>;
 template struct SysfsValue<SensorType::VoltageDC>;
 
-EMMC::EMMC(std::string componentName, std::optional<std::string> parent, std::shared_ptr<sysfs::EMMC> emmc)
+EMMC::EMMC(std::string componentName, std::optional<std::string> parent, std::shared_ptr<sysfs::EMMC> emmc, Thresholds<int64_t> thresholds)
     : DataReader(std::move(componentName), std::move(parent))
     , m_emmc(std::move(emmc))
+    , m_thresholds(std::move(thresholds))
 {
     auto emmcAttrs = m_emmc->attributes();
 
@@ -280,9 +296,9 @@
     return res;
 }
 
-void Group::registerDataReader(const IETFHardware::DataReader& callable)
+ThresholdsBySensorPath EMMC::thresholds() const
 {
-    m_readers.emplace_back(callable);
+    return {{xpathForComponent(m_componentName + ":lifetime") + "sensor-data/value", m_thresholds}};
 }
 
 DataTree Group::operator()() const
@@ -294,5 +310,9 @@
     return res;
 }
 
+ThresholdsBySensorPath Group::thresholds() const
+{
+    return m_thresholds;
+}
 }
 }
diff --git a/src/ietf-hardware/IETFHardware.h b/src/ietf-hardware/IETFHardware.h
index d807fe8..c1265b3 100644
--- a/src/ietf-hardware/IETFHardware.h
+++ b/src/ietf-hardware/IETFHardware.h
@@ -13,6 +13,7 @@
 #include <utility>
 #include "ietf-hardware/sysfs/EMMC.h"
 #include "ietf-hardware/sysfs/HWMon.h"
+#include "ietf-hardware/thresholds.h"
 #include "utils/log-fwd.h"
 
 using namespace std::literals;
@@ -20,6 +21,7 @@
 namespace velia::ietf_hardware {
 
 using DataTree = std::map<std::string, std::string>;
+using ThresholdsBySensorPath = std::map<std::string, Thresholds<int64_t>>;
 
 /**
  * @brief Readout of hardware-state related data according to RFC 8348 (App. A)
@@ -49,7 +51,12 @@
 
     IETFHardware();
     ~IETFHardware();
-    void registerDataReader(const DataReader& callable);
+
+    template <class DataReaderType>
+    void registerDataReader(const DataReaderType& callable)
+    {
+        m_callbacks.push_back(callable);
+    }
 
     DataTree process();
 
@@ -89,6 +96,7 @@
 struct StaticData : private DataReader {
     StaticData(std::string propertyPrefix, std::optional<std::string> parent, DataTree tree);
     DataTree operator()() const;
+    ThresholdsBySensorPath thresholds() const;
 };
 
 /** @brief Manages fans component. Data is provided by a sysfs::HWMon object. */
@@ -96,10 +104,12 @@
 private:
     std::shared_ptr<sysfs::HWMon> m_hwmon;
     unsigned m_fanChannelsCount;
+    Thresholds<int64_t> m_thresholds;
 
 public:
-    Fans(std::string propertyPrefix, std::optional<std::string> parent, std::shared_ptr<sysfs::HWMon> hwmon, unsigned fanChannelsCount);
+    Fans(std::string propertyPrefix, std::optional<std::string> parent, std::shared_ptr<sysfs::HWMon> hwmon, unsigned fanChannelsCount, Thresholds<int64_t> thresholds = {});
     DataTree operator()() const;
+    ThresholdsBySensorPath thresholds() const;
 };
 
 enum class SensorType {
@@ -117,20 +127,24 @@
 private:
     std::shared_ptr<sysfs::HWMon> m_hwmon;
     std::string m_sysfsFile;
+    Thresholds<int64_t> m_thresholds;
 
 public:
-    SysfsValue(std::string propertyPrefix, std::optional<std::string> parent, std::shared_ptr<sysfs::HWMon> hwmon, int sysfsChannelNr);
+    SysfsValue(std::string propertyPrefix, std::optional<std::string> parent, std::shared_ptr<sysfs::HWMon> hwmon, int sysfsChannelNr, Thresholds<int64_t> thresholds = {});
     DataTree operator()() const;
+    ThresholdsBySensorPath thresholds() const;
 };
 
 /** @brief Manages a single eMMC block device hardware component. Data is provided by a sysfs::EMMC object. */
 struct EMMC : private DataReader {
 private:
     std::shared_ptr<sysfs::EMMC> m_emmc;
+    Thresholds<int64_t> m_thresholds;
 
 public:
-    EMMC(std::string propertyPrefix, std::optional<std::string> parent, std::shared_ptr<sysfs::EMMC> emmc);
+    EMMC(std::string propertyPrefix, std::optional<std::string> parent, std::shared_ptr<sysfs::EMMC> emmc, Thresholds<int64_t> thresholds = {});
     DataTree operator()() const;
+    ThresholdsBySensorPath thresholds() const;
 };
 
 /** @brief Use this when you want to wrap reading several properties in one go and still use it as a single DataReader instance            (e.g. in on thread)
@@ -138,10 +152,18 @@
 struct Group {
 private:
     std::vector<IETFHardware::DataReader> m_readers;
+    ThresholdsBySensorPath m_thresholds;
 
 public:
-    void registerDataReader(const IETFHardware::DataReader& callable);
     DataTree operator()() const;
+    ThresholdsBySensorPath thresholds() const;
+
+    template <class DataReaderType>
+    void registerDataReader(const DataReaderType& callable)
+    {
+        m_readers.push_back(callable);
+        m_thresholds.merge(callable.thresholds());
+    }
 };
 
 }
diff --git a/tests/sysrepo_ietf-hardware.cpp b/tests/sysrepo_ietf-hardware.cpp
index 605729d..e97aade 100644
--- a/tests/sysrepo_ietf-hardware.cpp
+++ b/tests/sysrepo_ietf-hardware.cpp
@@ -119,6 +119,11 @@
 
             return res;
         }
+
+        velia::ietf_hardware::ThresholdsBySensorPath thresholds() const
+        {
+            return {{"/ietf-hardware:hardware/component[name='ne:psu:child']/sensor-data/value", {}}};
+        }
     };
     ietfHardware->registerDataReader(PsuDataReader{psuActive});