Added discrete threshold trigger support

Implemented discrete threshold logic
Discrete trigger with empty threshold array is handled as 'onChange'
Added unit tests coverage for discrete trigger

Supported scenarios:
-discrete threshold with value and dwell time
-discrete threshold with value, without dwell time
-discrete trigger without threshold ('onChange')

Tests:
-Unit tests passed

Change-Id: Id60a48f4113bd955d97e154888c00d1b6e5490af
Signed-off-by: Szymon Dompke <szymon.dompke@intel.com>
diff --git a/meson.build b/meson.build
index 68397c2..90f1799 100644
--- a/meson.build
+++ b/meson.build
@@ -71,9 +71,11 @@
 executable(
     'telemetry',
     [
+        'src/discrete_threshold.cpp',
         'src/main.cpp',
         'src/metric.cpp',
         'src/numeric_threshold.cpp',
+        'src/on_change_threshold.cpp',
         'src/persistent_json_storage.cpp',
         'src/report.cpp',
         'src/report_factory.cpp',
diff --git a/src/discrete_threshold.cpp b/src/discrete_threshold.cpp
new file mode 100644
index 0000000..a93f682
--- /dev/null
+++ b/src/discrete_threshold.cpp
@@ -0,0 +1,103 @@
+#include "discrete_threshold.hpp"
+
+#include <phosphor-logging/log.hpp>
+
+DiscreteThreshold::DiscreteThreshold(
+    boost::asio::io_context& ioc,
+    std::vector<std::shared_ptr<interfaces::Sensor>> sensorsIn,
+    std::vector<std::string> sensorNames,
+    std::vector<std::unique_ptr<interfaces::TriggerAction>> actionsIn,
+    std::chrono::milliseconds dwellTimeIn, double thresholdValueIn,
+    std::string name) :
+    ioc(ioc),
+    sensors(std::move(sensorsIn)), actions(std::move(actionsIn)),
+    dwellTime(dwellTimeIn), thresholdValue(thresholdValueIn), name(name)
+{
+    details.reserve(sensors.size());
+    for (size_t i = 0; i < sensors.size(); i++)
+    {
+        details.emplace_back(sensorNames[i], false, ioc);
+    }
+}
+
+void DiscreteThreshold::initialize()
+{
+    for (auto& sensor : sensors)
+    {
+        sensor->registerForUpdates(weak_from_this());
+    }
+}
+
+DiscreteThreshold::ThresholdDetail&
+    DiscreteThreshold::getDetails(interfaces::Sensor& sensor)
+{
+    auto it =
+        std::find_if(sensors.begin(), sensors.end(),
+                     [&sensor](const auto& x) { return &sensor == x.get(); });
+    auto index = std::distance(sensors.begin(), it);
+    return details.at(index);
+}
+
+void DiscreteThreshold::sensorUpdated(interfaces::Sensor& sensor,
+                                      uint64_t timestamp)
+{}
+
+void DiscreteThreshold::sensorUpdated(interfaces::Sensor& sensor,
+                                      uint64_t timestamp, double value)
+{
+    auto& [sensorName, dwell, timer] = getDetails(sensor);
+
+    if (thresholdValue)
+    {
+        if (dwell && value != thresholdValue)
+        {
+            timer.cancel();
+            dwell = false;
+        }
+        else if (value == thresholdValue)
+        {
+            startTimer(sensor, timestamp, value);
+        }
+    }
+}
+
+void DiscreteThreshold::startTimer(interfaces::Sensor& sensor,
+                                   uint64_t timestamp, double value)
+{
+    auto& [sensorName, dwell, timer] = getDetails(sensor);
+    if (dwellTime == std::chrono::milliseconds::zero())
+    {
+        commit(sensorName, timestamp, value);
+    }
+    else
+    {
+        dwell = true;
+        timer.expires_after(dwellTime);
+        timer.async_wait([this, &sensor, timestamp,
+                          value](const boost::system::error_code ec) {
+            if (ec)
+            {
+                phosphor::logging::log<phosphor::logging::level::DEBUG>(
+                    "Timer has been canceled");
+                return;
+            }
+            auto& [sensorName, dwell, timer] = getDetails(sensor);
+            commit(sensorName, timestamp, value);
+            dwell = false;
+        });
+    }
+}
+
+void DiscreteThreshold::commit(const std::string& sensorName,
+                               uint64_t timestamp, double value)
+{
+    for (const auto& action : actions)
+    {
+        action->commit(sensorName, timestamp, value);
+    }
+}
+
+const char* DiscreteThreshold::getName() const
+{
+    return name.c_str();
+}
diff --git a/src/discrete_threshold.hpp b/src/discrete_threshold.hpp
new file mode 100644
index 0000000..100b88d
--- /dev/null
+++ b/src/discrete_threshold.hpp
@@ -0,0 +1,63 @@
+#pragma once
+
+#include "interfaces/sensor.hpp"
+#include "interfaces/sensor_listener.hpp"
+#include "interfaces/threshold.hpp"
+#include "interfaces/trigger_action.hpp"
+#include "interfaces/trigger_types.hpp"
+
+#include <boost/asio/steady_timer.hpp>
+
+#include <chrono>
+#include <memory>
+#include <vector>
+
+class DiscreteThreshold :
+    public interfaces::Threshold,
+    public interfaces::SensorListener,
+    public std::enable_shared_from_this<DiscreteThreshold>
+{
+  public:
+    DiscreteThreshold(
+        boost::asio::io_context& ioc,
+        std::vector<std::shared_ptr<interfaces::Sensor>> sensors,
+        std::vector<std::string> sensorNames,
+        std::vector<std::unique_ptr<interfaces::TriggerAction>> actions,
+        std::chrono::milliseconds dwellTime, double thresholdValue,
+        std::string name);
+    DiscreteThreshold(const DiscreteThreshold&) = delete;
+    DiscreteThreshold(DiscreteThreshold&&) = delete;
+    ~DiscreteThreshold()
+    {}
+
+    void initialize() override;
+    void sensorUpdated(interfaces::Sensor&, uint64_t) override;
+    void sensorUpdated(interfaces::Sensor&, uint64_t, double) override;
+    const char* getName() const;
+
+  private:
+    boost::asio::io_context& ioc;
+    const std::vector<std::shared_ptr<interfaces::Sensor>> sensors;
+    const std::vector<std::unique_ptr<interfaces::TriggerAction>> actions;
+    const std::chrono::milliseconds dwellTime;
+    const double thresholdValue;
+    const std::string name;
+
+    struct ThresholdDetail
+    {
+        std::string sensorName;
+        bool dwell;
+        boost::asio::steady_timer timer;
+
+        ThresholdDetail(const std::string& name, bool dwell,
+                        boost::asio::io_context& ioc) :
+            sensorName(name),
+            dwell(dwell), timer(ioc)
+        {}
+    };
+    std::vector<ThresholdDetail> details;
+
+    void startTimer(interfaces::Sensor&, uint64_t, double);
+    void commit(const std::string&, uint64_t, double);
+    ThresholdDetail& getDetails(interfaces::Sensor& sensor);
+};
diff --git a/src/interfaces/trigger_types.hpp b/src/interfaces/trigger_types.hpp
index f99cd3c..90c673d 100644
--- a/src/interfaces/trigger_types.hpp
+++ b/src/interfaces/trigger_types.hpp
@@ -37,8 +37,7 @@
     return std::string(utils::enumToString(details::convDataSeverity, v));
 }
 
-using ThresholdParam =
-    std::tuple<std::string, std::string, std::variant<double>, uint64_t>;
+using ThresholdParam = std::tuple<std::string, std::string, uint64_t, double>;
 } // namespace discrete
 
 namespace numeric
diff --git a/src/on_change_threshold.cpp b/src/on_change_threshold.cpp
new file mode 100644
index 0000000..0365ef4
--- /dev/null
+++ b/src/on_change_threshold.cpp
@@ -0,0 +1,42 @@
+#include "on_change_threshold.hpp"
+
+#include <phosphor-logging/log.hpp>
+
+OnChangeThreshold::OnChangeThreshold(
+    std::vector<std::shared_ptr<interfaces::Sensor>> sensorsIn,
+    std::vector<std::string> sensorNamesIn,
+    std::vector<std::unique_ptr<interfaces::TriggerAction>> actionsIn) :
+    sensors(std::move(sensorsIn)),
+    sensorNames(std::move(sensorNamesIn)), actions(std::move(actionsIn))
+{}
+
+void OnChangeThreshold::initialize()
+{
+    for (auto& sensor : sensors)
+    {
+        sensor->registerForUpdates(weak_from_this());
+    }
+}
+
+void OnChangeThreshold::sensorUpdated(interfaces::Sensor& sensor,
+                                      uint64_t timestamp)
+{}
+
+void OnChangeThreshold::sensorUpdated(interfaces::Sensor& sensor,
+                                      uint64_t timestamp, double value)
+{
+    auto it =
+        std::find_if(sensors.begin(), sensors.end(),
+                     [&sensor](const auto& x) { return &sensor == x.get(); });
+    auto index = std::distance(sensors.begin(), it);
+    commit(sensorNames.at(index), timestamp, value);
+}
+
+void OnChangeThreshold::commit(const std::string& sensorName,
+                               uint64_t timestamp, double value)
+{
+    for (const auto& action : actions)
+    {
+        action->commit(sensorName, timestamp, value);
+    }
+}
diff --git a/src/on_change_threshold.hpp b/src/on_change_threshold.hpp
new file mode 100644
index 0000000..0d0f841
--- /dev/null
+++ b/src/on_change_threshold.hpp
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "interfaces/sensor.hpp"
+#include "interfaces/sensor_listener.hpp"
+#include "interfaces/threshold.hpp"
+#include "interfaces/trigger_action.hpp"
+#include "interfaces/trigger_types.hpp"
+
+#include <boost/asio/steady_timer.hpp>
+
+#include <chrono>
+#include <memory>
+#include <vector>
+
+class OnChangeThreshold :
+    public interfaces::Threshold,
+    public interfaces::SensorListener,
+    public std::enable_shared_from_this<OnChangeThreshold>
+{
+  public:
+    OnChangeThreshold(
+        std::vector<std::shared_ptr<interfaces::Sensor>> sensors,
+        std::vector<std::string> sensorNames,
+        std::vector<std::unique_ptr<interfaces::TriggerAction>> actions);
+    ~OnChangeThreshold()
+    {}
+
+    void initialize() override;
+    void sensorUpdated(interfaces::Sensor&, uint64_t) override;
+    void sensorUpdated(interfaces::Sensor&, uint64_t, double) override;
+
+  private:
+    const std::vector<std::shared_ptr<interfaces::Sensor>> sensors;
+    const std::vector<std::string> sensorNames;
+    const std::vector<std::unique_ptr<interfaces::TriggerAction>> actions;
+
+    void commit(const std::string&, uint64_t, double);
+};
diff --git a/src/trigger_actions.cpp b/src/trigger_actions.cpp
index 26fa703..8f2cf82 100644
--- a/src/trigger_actions.cpp
+++ b/src/trigger_actions.cpp
@@ -7,6 +7,25 @@
 namespace action
 {
 
+namespace
+{
+std::string timestampToString(uint64_t timestamp)
+{
+    std::time_t t = static_cast<time_t>(timestamp);
+    std::array<char, sizeof("YYYY-MM-DDThh:mm:ssZ")> buf = {};
+    size_t size =
+        std::strftime(buf.data(), buf.size(), "%FT%TZ", std::gmtime(&t));
+    if (size == 0)
+    {
+        throw std::runtime_error("Failed to parse timestamp to string");
+    }
+    return std::string(buf.data(), size);
+}
+} // namespace
+
+namespace numeric
+{
+
 static const char* getDirection(double value, double threshold)
 {
     if (value < threshold)
@@ -24,13 +43,13 @@
 {
     switch (type)
     {
-        case numeric::Type::upperCritical:
+        case ::numeric::Type::upperCritical:
             return "UpperCritical";
-        case numeric::Type::lowerCritical:
+        case ::numeric::Type::lowerCritical:
             return "LowerCritical";
-        case numeric::Type::upperWarning:
+        case ::numeric::Type::upperWarning:
             return "UpperWarning";
-        case numeric::Type::lowerWarning:
+        case ::numeric::Type::lowerWarning:
             return "LowerWarning";
     }
     throw std::runtime_error("Invalid type");
@@ -39,19 +58,10 @@
 void LogToJournal::commit(const std::string& sensorName, uint64_t timestamp,
                           double value)
 {
-    std::time_t t = static_cast<time_t>(timestamp);
-    std::array<char, sizeof("YYYY-MM-DDThh:mm:ssZ")> buf = {};
-    size_t size =
-        std::strftime(buf.data(), buf.size(), "%FT%TZ", std::gmtime(&t));
-    if (size == 0)
-    {
-        throw std::runtime_error("Failed to parse timestamp to string");
-    }
-
     std::string msg = std::string(getType()) +
                       " numeric threshold condition is met on sensor " +
                       sensorName + ", recorded value " + std::to_string(value) +
-                      ", timestamp " + std::string(buf.data(), size) +
+                      ", timestamp " + timestampToString(timestamp) +
                       ", direction " +
                       std::string(getDirection(value, threshold));
 
@@ -62,13 +72,13 @@
 {
     switch (type)
     {
-        case numeric::Type::upperCritical:
+        case ::numeric::Type::upperCritical:
             return "OpenBMC.0.1.0.NumericThresholdUpperCritical";
-        case numeric::Type::lowerCritical:
+        case ::numeric::Type::lowerCritical:
             return "OpenBMC.0.1.0.NumericThresholdLowerCritical";
-        case numeric::Type::upperWarning:
+        case ::numeric::Type::upperWarning:
             return "OpenBMC.0.1.0.NumericThresholdUpperWarning";
-        case numeric::Type::lowerWarning:
+        case ::numeric::Type::lowerWarning:
             return "OpenBMC.0.1.0.NumericThresholdLowerWarning";
     }
     throw std::runtime_error("Invalid type");
@@ -85,6 +95,84 @@
                                  getDirection(value, threshold)));
 }
 
+} // namespace numeric
+
+namespace discrete
+{
+const char* LogToJournal::getSeverity() const
+{
+    switch (severity)
+    {
+        case ::discrete::Severity::ok:
+            return "OK";
+        case ::discrete::Severity::warning:
+            return "Warning";
+        case ::discrete::Severity::critical:
+            return "Critical";
+    }
+    throw std::runtime_error("Invalid severity");
+}
+
+void LogToJournal::commit(const std::string& sensorName, uint64_t timestamp,
+                          double value)
+{
+    std::string msg = std::string(getSeverity()) +
+                      " discrete threshold condition is met on sensor " +
+                      sensorName + ", recorded value " + std::to_string(value) +
+                      ", timestamp " + timestampToString(timestamp);
+
+    phosphor::logging::log<phosphor::logging::level::INFO>(msg.c_str());
+}
+
+const char* LogToRedfish::getMessageId() const
+{
+    switch (severity)
+    {
+        case ::discrete::Severity::ok:
+            return "OpenBMC.0.1.0.DiscreteThresholdOk";
+        case ::discrete::Severity::warning:
+            return "OpenBMC.0.1.0.DiscreteThresholdWarning";
+        case ::discrete::Severity::critical:
+            return "OpenBMC.0.1.0.DiscreteThresholdCritical";
+    }
+    throw std::runtime_error("Invalid severity");
+}
+
+void LogToRedfish::commit(const std::string& sensorName, uint64_t timestamp,
+                          double value)
+{
+    phosphor::logging::log<phosphor::logging::level::INFO>(
+        "Discrete treshold condition is met",
+        phosphor::logging::entry("REDFISH_MESSAGE_ID=%s", getMessageId()),
+        phosphor::logging::entry("REDFISH_MESSAGE_ARGS=%s,%f,%llu",
+                                 sensorName.c_str(), value, timestamp));
+}
+
+namespace onChange
+{
+void LogToJournal::commit(const std::string& sensorName, uint64_t timestamp,
+                          double value)
+{
+    std::string msg = "Value changed on sensor " + sensorName +
+                      ", recorded value " + std::to_string(value) +
+                      ", timestamp " + timestampToString(timestamp);
+
+    phosphor::logging::log<phosphor::logging::level::INFO>(msg.c_str());
+}
+
+void LogToRedfish::commit(const std::string& sensorName, uint64_t timestamp,
+                          double value)
+{
+    const char* messageId = "OpenBMC.0.1.0.DiscreteThresholdOnChange";
+    phosphor::logging::log<phosphor::logging::level::INFO>(
+        "Uncondtional discrete threshold triggered",
+        phosphor::logging::entry("REDFISH_MESSAGE_ID=%s", messageId),
+        phosphor::logging::entry("REDFISH_MESSAGE_ARGS=%s,%f,%llu",
+                                 sensorName.c_str(), value, timestamp));
+}
+} // namespace onChange
+} // namespace discrete
+
 void UpdateReport::commit(const std::string&, uint64_t, double)
 {
     for (const auto& name : reportNames)
diff --git a/src/trigger_actions.hpp b/src/trigger_actions.hpp
index 2c8db69..408bcb5 100644
--- a/src/trigger_actions.hpp
+++ b/src/trigger_actions.hpp
@@ -7,17 +7,19 @@
 namespace action
 {
 
+namespace numeric
+{
 class LogToJournal : public interfaces::TriggerAction
 {
   public:
-    LogToJournal(numeric::Type type, double val) : type(type), threshold(val)
+    LogToJournal(::numeric::Type type, double val) : type(type), threshold(val)
     {}
 
     void commit(const std::string& id, uint64_t timestamp,
                 double value) override;
 
   private:
-    numeric::Type type;
+    ::numeric::Type type;
     double threshold;
 
     const char* getType() const;
@@ -26,18 +28,76 @@
 class LogToRedfish : public interfaces::TriggerAction
 {
   public:
-    LogToRedfish(numeric::Type type, double val) : type(type), threshold(val)
+    LogToRedfish(::numeric::Type type, double val) : type(type), threshold(val)
     {}
 
     void commit(const std::string& id, uint64_t timestamp,
                 double value) override;
 
   private:
-    numeric::Type type;
+    ::numeric::Type type;
     double threshold;
 
     const char* getMessageId() const;
 };
+} // namespace numeric
+
+namespace discrete
+{
+class LogToJournal : public interfaces::TriggerAction
+{
+  public:
+    LogToJournal(::discrete::Severity severity) : severity(severity)
+    {}
+
+    void commit(const std::string& id, uint64_t timestamp,
+                double value) override;
+
+  private:
+    ::discrete::Severity severity;
+
+    const char* getSeverity() const;
+};
+
+class LogToRedfish : public interfaces::TriggerAction
+{
+  public:
+    LogToRedfish(::discrete::Severity severity) : severity(severity)
+    {}
+
+    void commit(const std::string& id, uint64_t timestamp,
+                double value) override;
+
+  private:
+    ::discrete::Severity severity;
+
+    const char* getMessageId() const;
+};
+
+namespace onChange
+{
+class LogToJournal : public interfaces::TriggerAction
+{
+  public:
+    LogToJournal()
+    {}
+
+    void commit(const std::string& id, uint64_t timestamp,
+                double value) override;
+};
+
+class LogToRedfish : public interfaces::TriggerAction
+{
+  public:
+    LogToRedfish()
+    {}
+
+    void commit(const std::string& id, uint64_t timestamp,
+                double value) override;
+};
+} // namespace onChange
+
+} // namespace discrete
 
 class UpdateReport : public interfaces::TriggerAction
 {
diff --git a/src/trigger_factory.cpp b/src/trigger_factory.cpp
index 7953cd9..3114e9a 100644
--- a/src/trigger_factory.cpp
+++ b/src/trigger_factory.cpp
@@ -1,6 +1,8 @@
 #include "trigger_factory.hpp"
 
+#include "discrete_threshold.hpp"
 #include "numeric_threshold.hpp"
+#include "on_change_threshold.hpp"
 #include "sensor.hpp"
 #include "trigger.hpp"
 #include "trigger_actions.hpp"
@@ -24,40 +26,96 @@
     const TriggerThresholdParams& thresholdParams,
     interfaces::TriggerManager& triggerManager) const
 {
-    if (isDiscrete)
-    {
-        throw std::runtime_error("Not implemented!");
-    }
-
     auto [sensors, sensorNames] = getSensors(yield, sensorPaths);
     std::vector<std::shared_ptr<interfaces::Threshold>> thresholds;
 
-    const auto& params =
-        std::get<std::vector<numeric::ThresholdParam>>(thresholdParams);
-    for (const auto& [typeStr, dwellTime, directionStr, value] : params)
+    if (isDiscrete)
     {
-        numeric::Type type = numeric::stringToType(typeStr);
-        std::vector<std::unique_ptr<interfaces::TriggerAction>> actions;
-        if (logToJournal)
+        const auto& params =
+            std::get<std::vector<discrete::ThresholdParam>>(thresholdParams);
+        for (const auto& [thresholdName, severityStr, dwellTime,
+                          thresholdValue] : params)
         {
-            actions.emplace_back(
-                std::make_unique<action::LogToJournal>(type, value));
-        }
-        if (logToRedfish)
-        {
-            actions.emplace_back(
-                std::make_unique<action::LogToRedfish>(type, value));
-        }
-        if (updateReport)
-        {
-            actions.emplace_back(std::make_unique<action::UpdateReport>(
-                reportManager, reportNames));
-        }
+            discrete::Severity severity =
+                discrete::stringToSeverity(severityStr);
+            std::vector<std::unique_ptr<interfaces::TriggerAction>> actions;
+            if (logToJournal)
+            {
+                actions.emplace_back(
+                    std::make_unique<action::discrete::LogToJournal>(severity));
+            }
+            if (logToRedfish)
+            {
+                actions.emplace_back(
+                    std::make_unique<action::discrete::LogToRedfish>(severity));
+            }
+            if (updateReport)
+            {
+                actions.emplace_back(std::make_unique<action::UpdateReport>(
+                    reportManager, reportNames));
+            }
 
-        thresholds.emplace_back(std::make_shared<NumericThreshold>(
-            bus->get_io_context(), sensors, sensorNames, std::move(actions),
-            std::chrono::milliseconds(dwellTime),
-            numeric::stringToDirection(directionStr), value));
+            thresholds.emplace_back(std::make_shared<DiscreteThreshold>(
+                bus->get_io_context(), sensors, sensorNames, std::move(actions),
+                std::chrono::milliseconds(dwellTime), thresholdValue,
+                thresholdName));
+        }
+        if (params.empty())
+        {
+            std::vector<std::unique_ptr<interfaces::TriggerAction>> actions;
+            if (logToJournal)
+            {
+                actions.emplace_back(
+                    std::make_unique<
+                        action::discrete::onChange::LogToJournal>());
+            }
+            if (logToRedfish)
+            {
+                actions.emplace_back(
+                    std::make_unique<
+                        action::discrete::onChange::LogToRedfish>());
+            }
+            if (updateReport)
+            {
+                actions.emplace_back(std::make_unique<action::UpdateReport>(
+                    reportManager, reportNames));
+            }
+
+            thresholds.emplace_back(std::make_shared<OnChangeThreshold>(
+                sensors, sensorNames, std::move(actions)));
+        }
+    }
+    else
+    {
+        const auto& params =
+            std::get<std::vector<numeric::ThresholdParam>>(thresholdParams);
+        for (const auto& [typeStr, dwellTime, directionStr, value] : params)
+        {
+            numeric::Type type = numeric::stringToType(typeStr);
+            std::vector<std::unique_ptr<interfaces::TriggerAction>> actions;
+            if (logToJournal)
+            {
+                actions.emplace_back(
+                    std::make_unique<action::numeric::LogToJournal>(type,
+                                                                    value));
+            }
+            if (logToRedfish)
+            {
+                actions.emplace_back(
+                    std::make_unique<action::numeric::LogToRedfish>(type,
+                                                                    value));
+            }
+            if (updateReport)
+            {
+                actions.emplace_back(std::make_unique<action::UpdateReport>(
+                    reportManager, reportNames));
+            }
+
+            thresholds.emplace_back(std::make_shared<NumericThreshold>(
+                bus->get_io_context(), sensors, sensorNames, std::move(actions),
+                std::chrono::milliseconds(dwellTime),
+                numeric::stringToDirection(directionStr), value));
+        }
     }
 
     return std::make_unique<Trigger>(
diff --git a/src/trigger_manager.cpp b/src/trigger_manager.cpp
index 4191738..bf372c2 100644
--- a/src/trigger_manager.cpp
+++ b/src/trigger_manager.cpp
@@ -17,13 +17,6 @@
                                                 std::string>>& sensors,
                     const std::vector<std::string>& reportNames,
                     const TriggerThresholdParams& thresholds) {
-                    if (isDiscrete)
-                    {
-                        throw sdbusplus::exception::SdBusError(
-                            static_cast<int>(std::errc::not_supported),
-                            "Only numeric threshold is supported");
-                    }
-
                     if (triggers.size() >= maxTriggers)
                     {
                         throw sdbusplus::exception::SdBusError(
diff --git a/tests/meson.build b/tests/meson.build
index 5be01be..d2e5aac 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -11,8 +11,10 @@
     executable(
         'telemetry-ut',
         [
+            '../src/discrete_threshold.cpp',
             '../src/metric.cpp',
             '../src/numeric_threshold.cpp',
+            '../src/on_change_threshold.cpp',
             '../src/persistent_json_storage.cpp',
             '../src/report.cpp',
             '../src/report_factory.cpp',
@@ -28,8 +30,10 @@
             'src/stubs/dbus_sensor_object.cpp',
             'src/test_conversion.cpp',
             'src/test_detached_timer.cpp',
+            'src/test_discrete_threshold.cpp',
             'src/test_metric.cpp',
             'src/test_numeric_threshold.cpp',
+            'src/test_on_change_threshold.cpp',
             'src/test_persistent_json_storage.cpp',
             'src/test_report.cpp',
             'src/test_report_manager.cpp',
diff --git a/tests/src/dbus_environment.cpp b/tests/src/dbus_environment.cpp
index 4780cc5..9e025d7 100644
--- a/tests/src/dbus_environment.cpp
+++ b/tests/src/dbus_environment.cpp
@@ -72,6 +72,17 @@
     return waitForFuture(getFuture(name), timeout);
 }
 
+bool DbusEnvironment::waitForFutures(std::string_view name,
+                                     std::chrono::milliseconds timeout)
+{
+    auto& data = futures[std::string(name)];
+    auto ret = waitForFutures(
+        std::move(data), true, [](auto sum, auto val) { return sum && val; },
+        timeout);
+    data = std::vector<std::future<bool>>{};
+    return ret;
+}
+
 std::future<bool> DbusEnvironment::getFuture(std::string_view name)
 {
     auto& data = futures[std::string(name)];
diff --git a/tests/src/dbus_environment.hpp b/tests/src/dbus_environment.hpp
index 07c4368..8146bcb 100644
--- a/tests/src/dbus_environment.hpp
+++ b/tests/src/dbus_environment.hpp
@@ -1,3 +1,5 @@
+#pragma once
+
 #include <sdbusplus/asio/object_server.hpp>
 #include <sdbusplus/asio/property.hpp>
 
@@ -36,35 +38,61 @@
         synchronizeIoc();
     }
 
-    template <class T>
-    static T waitForFuture(
-        std::future<T> future,
+    template <class T, class F>
+    static T waitForFutures(
+        std::vector<std::future<T>> futures, T init, F&& accumulator,
         std::chrono::milliseconds timeout = std::chrono::seconds(10))
     {
         constexpr auto precission = std::chrono::milliseconds(10);
         auto elapsed = std::chrono::milliseconds(0);
 
-        while (future.valid() && elapsed < timeout)
+        auto sum = init;
+        for (auto& future : futures)
         {
-            synchronizeIoc();
+            while (future.valid() && elapsed < timeout)
+            {
+                synchronizeIoc();
 
-            if (future.wait_for(precission) == std::future_status::ready)
-            {
-                return future.get();
-            }
-            else
-            {
-                elapsed += precission;
+                if (future.wait_for(precission) == std::future_status::ready)
+                {
+                    sum = accumulator(sum, future.get());
+                }
+                else
+                {
+                    elapsed += precission;
+                }
             }
         }
 
-        throw std::runtime_error("Timed out while waiting for future");
+        if (elapsed >= timeout)
+        {
+            throw std::runtime_error("Timed out while waiting for future");
+        }
+
+        return sum;
+    }
+
+    template <class T>
+    static T waitForFuture(
+        std::future<T> future,
+        std::chrono::milliseconds timeout = std::chrono::seconds(10))
+    {
+        std::vector<std::future<T>> futures;
+        futures.emplace_back(std::move(future));
+
+        return waitForFutures(
+            std::move(futures), T{},
+            [](auto, const auto& value) { return value; }, timeout);
     }
 
     static bool waitForFuture(
         std::string_view name,
         std::chrono::milliseconds timeout = std::chrono::seconds(10));
 
+    static bool waitForFutures(
+        std::string_view name,
+        std::chrono::milliseconds timeout = std::chrono::seconds(10));
+
   private:
     static std::future<bool> getFuture(std::string_view name);
 
diff --git a/tests/src/params/trigger_params.hpp b/tests/src/params/trigger_params.hpp
index 6d34d3b..95b19d2 100644
--- a/tests/src/params/trigger_params.hpp
+++ b/tests/src/params/trigger_params.hpp
@@ -21,6 +21,12 @@
         return nameProperty;
     }
 
+    TriggerParams& isDiscrete(bool val)
+    {
+        discreteProperty = val;
+        return *this;
+    }
+
     bool isDiscrete() const
     {
         return discreteProperty;
@@ -52,6 +58,12 @@
         return reportNamesProperty;
     }
 
+    TriggerParams& thresholdParams(TriggerThresholdParams val)
+    {
+        thresholdsProperty = std::move(val);
+        return *this;
+    }
+
     const TriggerThresholdParams& thresholdParams() const
     {
         return thresholdsProperty;
diff --git a/tests/src/test_discrete_threshold.cpp b/tests/src/test_discrete_threshold.cpp
new file mode 100644
index 0000000..2b31767
--- /dev/null
+++ b/tests/src/test_discrete_threshold.cpp
@@ -0,0 +1,271 @@
+#include "dbus_environment.hpp"
+#include "discrete_threshold.hpp"
+#include "helpers.hpp"
+#include "mocks/sensor_mock.hpp"
+#include "mocks/trigger_action_mock.hpp"
+#include "utils/conv_container.hpp"
+
+#include <gmock/gmock.h>
+
+using namespace testing;
+using namespace std::chrono_literals;
+
+class TestDiscreteThreshold : public Test
+{
+  public:
+    std::vector<std::shared_ptr<SensorMock>> sensorMocks = {
+        std::make_shared<NiceMock<SensorMock>>(),
+        std::make_shared<NiceMock<SensorMock>>()};
+    std::vector<std::string> sensorNames = {"Sensor1", "Sensor2"};
+    std::unique_ptr<TriggerActionMock> actionMockPtr =
+        std::make_unique<StrictMock<TriggerActionMock>>();
+    TriggerActionMock& actionMock = *actionMockPtr;
+    std::shared_ptr<DiscreteThreshold> sut;
+
+    std::shared_ptr<DiscreteThreshold>
+        makeThreshold(std::chrono::milliseconds dwellTime,
+                      double thresholdValue)
+    {
+        std::vector<std::unique_ptr<interfaces::TriggerAction>> actions;
+        actions.push_back(std::move(actionMockPtr));
+
+        return std::make_shared<DiscreteThreshold>(
+            DbusEnvironment::getIoc(),
+            utils::convContainer<std::shared_ptr<interfaces::Sensor>>(
+                sensorMocks),
+            sensorNames, std::move(actions), dwellTime, thresholdValue,
+            "treshold_name");
+    }
+
+    void SetUp() override
+    {
+        sut = makeThreshold(0ms, 90.0);
+    }
+};
+
+TEST_F(TestDiscreteThreshold, initializeThresholdExpectAllSensorsAreRegistered)
+{
+    for (auto& sensor : sensorMocks)
+    {
+        EXPECT_CALL(*sensor,
+                    registerForUpdates(Truly([sut = sut.get()](const auto& x) {
+                        return x.lock().get() == sut;
+                    })));
+    }
+
+    sut->initialize();
+}
+
+TEST_F(TestDiscreteThreshold, thresholdIsNotInitializeExpectNoActionCommit)
+{
+    EXPECT_CALL(actionMock, commit(_, _, _)).Times(0);
+}
+
+struct DiscreteParams
+{
+    struct UpdateParams
+    {
+        size_t sensor;
+        uint64_t timestamp;
+        double value;
+        std::chrono::milliseconds sleepAfter;
+
+        UpdateParams(size_t sensor, uint64_t timestamp, double value,
+                     std::chrono::milliseconds sleepAfter = 0ms) :
+            sensor(sensor),
+            timestamp(timestamp), value(value), sleepAfter(sleepAfter)
+        {}
+    };
+
+    struct ExpectedParams
+    {
+        size_t sensor;
+        uint64_t timestamp;
+        double value;
+        std::chrono::milliseconds waitMin;
+
+        ExpectedParams(size_t sensor, uint64_t timestamp, double value,
+                       std::chrono::milliseconds waitMin = 0ms) :
+            sensor(sensor),
+            timestamp(timestamp), value(value), waitMin(waitMin)
+        {}
+    };
+
+    DiscreteParams& Updates(std::vector<UpdateParams> val)
+    {
+        updates = std::move(val);
+        return *this;
+    }
+
+    DiscreteParams& Expected(std::vector<ExpectedParams> val)
+    {
+        expected = std::move(val);
+        return *this;
+    }
+
+    DiscreteParams& ThresholdValue(double val)
+    {
+        thresholdValue = val;
+        return *this;
+    }
+
+    DiscreteParams& DwellTime(std::chrono::milliseconds val)
+    {
+        dwellTime = std::move(val);
+        return *this;
+    }
+
+    friend void PrintTo(const DiscreteParams& o, std::ostream* os)
+    {
+        *os << "{ DwellTime: " << o.dwellTime.count() << "ms ";
+        *os << ", ThresholdValue: " << o.thresholdValue;
+        *os << ", Updates: ";
+        for (const auto& [index, timestamp, value, sleepAfter] : o.updates)
+        {
+            *os << "{ SensorIndex: " << index << ", Timestamp: " << timestamp
+                << ", Value: " << value
+                << ", SleepAfter: " << sleepAfter.count() << "ms }, ";
+        }
+        *os << "Expected: ";
+        for (const auto& [index, timestamp, value, waitMin] : o.expected)
+        {
+            *os << "{ SensorIndex: " << index << ", Timestamp: " << timestamp
+                << ", Value: " << value << ", waitMin: " << waitMin.count()
+                << "ms }, ";
+        }
+        *os << " }";
+    }
+
+    std::vector<UpdateParams> updates;
+    std::vector<ExpectedParams> expected;
+    double thresholdValue = 0.0;
+    std::chrono::milliseconds dwellTime = 0ms;
+};
+
+class TestDiscreteThresholdCommon :
+    public TestDiscreteThreshold,
+    public WithParamInterface<DiscreteParams>
+{
+  public:
+    void sleep(std::chrono::milliseconds duration)
+    {
+        if (duration != 0ms)
+        {
+            DbusEnvironment::sleepFor(duration);
+        }
+    }
+
+    void testBodySensorIsUpdatedMultipleTimes()
+    {
+        std::vector<std::chrono::time_point<std::chrono::high_resolution_clock>>
+            timestamps(sensorMocks.size());
+
+        sut->initialize();
+
+        InSequence seq;
+
+        for (const auto& [index, timestamp, value, waitMin] :
+             GetParam().expected)
+        {
+            EXPECT_CALL(actionMock,
+                        commit(sensorNames[index], timestamp, value))
+                .WillOnce(DoAll(
+                    InvokeWithoutArgs([idx = index, &timestamps] {
+                        timestamps[idx] =
+                            std::chrono::high_resolution_clock::now();
+                    }),
+                    InvokeWithoutArgs(DbusEnvironment::setPromise("commit"))));
+        }
+
+        auto start = std::chrono::high_resolution_clock::now();
+
+        for (const auto& [index, timestamp, value, sleepAfter] :
+             GetParam().updates)
+        {
+            sut->sensorUpdated(*sensorMocks[index], timestamp, value);
+            sleep(sleepAfter);
+        }
+
+        EXPECT_THAT(DbusEnvironment::waitForFutures("commit"), true);
+        for (const auto& [index, timestamp, value, waitMin] :
+             GetParam().expected)
+        {
+            EXPECT_THAT(timestamps[index] - start, Ge(waitMin));
+        }
+    }
+};
+
+class TestDiscreteThresholdNoDwellTime : public TestDiscreteThresholdCommon
+{
+  public:
+    void SetUp() override
+    {
+        sut = makeThreshold(0ms, GetParam().thresholdValue);
+    }
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    _, TestDiscreteThresholdNoDwellTime,
+    Values(
+        DiscreteParams()
+            .ThresholdValue(90.0)
+            .Updates({{0, 1, 80.0}, {0, 2, 89.0}})
+            .Expected({}),
+        DiscreteParams()
+            .ThresholdValue(90.0)
+            .Updates({{0, 1, 80.0}, {0, 2, 90.0}, {0, 3, 80.0}, {0, 4, 90.0}})
+            .Expected({{0, 2, 90.0}, {0, 4, 90.0}}),
+        DiscreteParams()
+            .ThresholdValue(90.0)
+            .Updates({{0, 1, 90.0}, {0, 2, 99.0}, {1, 3, 100.0}, {1, 4, 90.0}})
+            .Expected({{0, 1, 90.0}, {1, 4, 90.0}})));
+
+TEST_P(TestDiscreteThresholdNoDwellTime, senorsIsUpdatedMultipleTimes)
+{
+    testBodySensorIsUpdatedMultipleTimes();
+}
+
+class TestDiscreteThresholdWithDwellTime : public TestDiscreteThresholdCommon
+{
+  public:
+    void SetUp() override
+    {
+        sut = makeThreshold(GetParam().dwellTime, GetParam().thresholdValue);
+    }
+};
+
+INSTANTIATE_TEST_SUITE_P(
+    _, TestDiscreteThresholdWithDwellTime,
+    Values(DiscreteParams()
+               .DwellTime(200ms)
+               .ThresholdValue(90.0)
+               .Updates({{0, 1, 90.0, 100ms}, {0, 2, 91.0}, {0, 3, 90.0}})
+               .Expected({{0, 3, 90.0, 300ms}}),
+           DiscreteParams()
+               .DwellTime(100ms)
+               .ThresholdValue(90.0)
+               .Updates({{0, 1, 90.0, 100ms}})
+               .Expected({{0, 1, 90.0, 100ms}}),
+           DiscreteParams()
+               .DwellTime(1000ms)
+               .ThresholdValue(90.0)
+               .Updates({{0, 1, 90.0, 700ms},
+                         {0, 1, 91.0, 100ms},
+                         {0, 1, 90.0, 300ms},
+                         {0, 1, 91.0, 100ms}})
+               .Expected({}),
+           DiscreteParams()
+               .DwellTime(200ms)
+               .ThresholdValue(90.0)
+               .Updates({{0, 1, 90.0},
+                         {1, 2, 89.0, 100ms},
+                         {1, 3, 90.0, 100ms},
+                         {1, 4, 89.0, 100ms},
+                         {1, 5, 90.0, 300ms},
+                         {1, 6, 89.0, 100ms}})
+               .Expected({{0, 1, 90, 200ms}, {1, 5, 90, 500ms}})));
+
+TEST_P(TestDiscreteThresholdWithDwellTime, senorsIsUpdatedMultipleTimes)
+{
+    testBodySensorIsUpdatedMultipleTimes();
+}
diff --git a/tests/src/test_on_change_threshold.cpp b/tests/src/test_on_change_threshold.cpp
new file mode 100644
index 0000000..20819c5
--- /dev/null
+++ b/tests/src/test_on_change_threshold.cpp
@@ -0,0 +1,129 @@
+#include "dbus_environment.hpp"
+#include "helpers.hpp"
+#include "mocks/sensor_mock.hpp"
+#include "mocks/trigger_action_mock.hpp"
+#include "on_change_threshold.hpp"
+#include "utils/conv_container.hpp"
+
+#include <gmock/gmock.h>
+
+using namespace testing;
+using namespace std::chrono_literals;
+
+class TestOnChangeThreshold : public Test
+{
+  public:
+    std::vector<std::shared_ptr<SensorMock>> sensorMocks = {
+        std::make_shared<NiceMock<SensorMock>>(),
+        std::make_shared<NiceMock<SensorMock>>()};
+    std::vector<std::string> sensorNames = {"Sensor1", "Sensor2"};
+    std::unique_ptr<TriggerActionMock> actionMockPtr =
+        std::make_unique<StrictMock<TriggerActionMock>>();
+    TriggerActionMock& actionMock = *actionMockPtr;
+    std::shared_ptr<OnChangeThreshold> sut;
+
+    void SetUp() override
+    {
+        std::vector<std::unique_ptr<interfaces::TriggerAction>> actions;
+        actions.push_back(std::move(actionMockPtr));
+
+        sut = std::make_shared<OnChangeThreshold>(
+            utils::convContainer<std::shared_ptr<interfaces::Sensor>>(
+                sensorMocks),
+            sensorNames, std::move(actions));
+    }
+};
+
+TEST_F(TestOnChangeThreshold, initializeThresholdExpectAllSensorsAreRegistered)
+{
+    for (auto& sensor : sensorMocks)
+    {
+        EXPECT_CALL(*sensor,
+                    registerForUpdates(Truly([sut = sut.get()](const auto& x) {
+                        return x.lock().get() == sut;
+                    })));
+    }
+
+    sut->initialize();
+}
+
+TEST_F(TestOnChangeThreshold, thresholdIsNotInitializeExpectNoActionCommit)
+{
+    EXPECT_CALL(actionMock, commit(_, _, _)).Times(0);
+}
+
+struct OnChangeParams
+{
+    using UpdateParams = std::tuple<size_t, uint64_t, double>;
+    using ExpectedParams = std::tuple<size_t, uint64_t, double>;
+
+    OnChangeParams& Updates(std::vector<UpdateParams> val)
+    {
+        updates = std::move(val);
+        return *this;
+    }
+
+    OnChangeParams& Expected(std::vector<ExpectedParams> val)
+    {
+        expected = std::move(val);
+        return *this;
+    }
+
+    friend void PrintTo(const OnChangeParams& o, std::ostream* os)
+    {
+        *os << "{ Updates: ";
+        for (const auto& [index, timestamp, value] : o.updates)
+        {
+            *os << "{ SensorIndex: " << index << ", Timestamp: " << timestamp
+                << ", Value: " << value << " }, ";
+        }
+        *os << "Expected: ";
+        for (const auto& [index, timestamp, value] : o.expected)
+        {
+            *os << "{ SensorIndex: " << index << ", Timestamp: " << timestamp
+                << ", Value: " << value << " }, ";
+        }
+        *os << " }";
+    }
+
+    std::vector<UpdateParams> updates;
+    std::vector<ExpectedParams> expected;
+};
+
+class TestOnChangeThresholdUpdates :
+    public TestOnChangeThreshold,
+    public WithParamInterface<OnChangeParams>
+{};
+
+INSTANTIATE_TEST_SUITE_P(
+    _, TestOnChangeThresholdUpdates,
+    Values(
+        OnChangeParams().Updates({{0, 1, 80.0}}).Expected({{0, 1, 80.0}}),
+        OnChangeParams()
+            .Updates({{0, 1, 80.0}, {1, 2, 81.0}})
+            .Expected({{0, 1, 80.0}, {1, 2, 81.0}}),
+        OnChangeParams()
+            .Updates({{0, 1, 80.0}, {0, 2, 90.0}})
+            .Expected({{0, 1, 80.0}, {0, 2, 90.0}}),
+        OnChangeParams()
+            .Updates({{0, 1, 80.0}, {1, 2, 90.0}, {0, 3, 90.0}})
+            .Expected({{0, 1, 80.0}, {1, 2, 90.0}, {0, 3, 90.0}}),
+        OnChangeParams()
+            .Updates({{0, 1, 80.0}, {1, 2, 80.0}, {1, 3, 90.0}, {0, 4, 90.0}})
+            .Expected(
+                {{0, 1, 80.0}, {1, 2, 80.0}, {1, 3, 90.0}, {0, 4, 90.0}})));
+
+TEST_P(TestOnChangeThresholdUpdates, senorsIsUpdatedMultipleTimes)
+{
+    InSequence seq;
+    for (const auto& [index, timestamp, value] : GetParam().expected)
+    {
+        EXPECT_CALL(actionMock, commit(sensorNames[index], timestamp, value));
+    }
+
+    sut->initialize();
+    for (const auto& [index, timestamp, value] : GetParam().updates)
+    {
+        sut->sensorUpdated(*sensorMocks[index], timestamp, value);
+    }
+}
diff --git a/tests/src/test_trigger_actions.cpp b/tests/src/test_trigger_actions.cpp
index 2fa220a..7a65fa2 100644
--- a/tests/src/test_trigger_actions.cpp
+++ b/tests/src/test_trigger_actions.cpp
@@ -7,85 +7,212 @@
 
 namespace action
 {
+namespace numeric
+{
+using LogParam = std::tuple<::numeric::Type, double, double>;
 
-using LogParam = std::pair<numeric::Type, double>;
+static auto getCorrectParams()
+{
+    return Values(std::make_tuple(::numeric::Type::upperCritical, 91.1, 90),
+                  std::make_tuple(::numeric::Type::lowerCritical, 91.2, 90),
+                  std::make_tuple(::numeric::Type::upperWarning, 88.5, 90),
+                  std::make_tuple(::numeric::Type::lowerWarning, 88.6, 90));
+}
 
-class TestLogToJournal : public Test, public WithParamInterface<LogParam>
+static auto getIncorrectParams()
+{
+    return Values(
+        std::make_tuple(::numeric::Type::upperCritical, 90.0, 90),
+        std::make_tuple(static_cast<::numeric::Type>(-1), 88.0, 90),
+        std::make_tuple(static_cast<::numeric::Type>(123), 123.0, 90));
+}
+
+class TestLogToJournalNumeric : public Test, public WithParamInterface<LogParam>
 {
   public:
     void SetUp() override
     {
-        auto [type, threshold] = GetParam();
-        sut = std::make_unique<LogToJournal>(type, threshold);
+        auto [type, threshold, value] = GetParam();
+        sut = std::make_unique<numeric::LogToJournal>(type, threshold);
+        commmitValue = value;
+    }
+
+    std::unique_ptr<numeric::LogToJournal> sut;
+    double commmitValue;
+};
+
+INSTANTIATE_TEST_SUITE_P(LogToJournalNumericParams, TestLogToJournalNumeric,
+                         getCorrectParams());
+
+TEST_P(TestLogToJournalNumeric, commitAnActionDoesNotThrow)
+{
+    EXPECT_NO_THROW(sut->commit("Test", 100'000, commmitValue));
+}
+
+class TestLogToJournalNumericThrow : public TestLogToJournalNumeric
+{};
+
+INSTANTIATE_TEST_SUITE_P(_, TestLogToJournalNumericThrow, getIncorrectParams());
+
+TEST_P(TestLogToJournalNumericThrow, commitAnActionExpectThrow)
+{
+    EXPECT_THROW(sut->commit("Test", 100'000, commmitValue),
+                 std::runtime_error);
+}
+
+class TestLogToRedfishNumeric : public Test, public WithParamInterface<LogParam>
+{
+  public:
+    void SetUp() override
+    {
+        auto [type, threshold, value] = GetParam();
+        sut = std::make_unique<LogToRedfish>(type, threshold);
+        commmitValue = value;
+    }
+
+    std::unique_ptr<LogToRedfish> sut;
+    double commmitValue;
+};
+
+INSTANTIATE_TEST_SUITE_P(LogToRedfishNumericParams, TestLogToRedfishNumeric,
+                         getCorrectParams());
+
+TEST_P(TestLogToRedfishNumeric, commitExpectNoThrow)
+{
+    EXPECT_NO_THROW(sut->commit("Test", 100'000, commmitValue));
+}
+
+class TestLogToRedfishNumericThrow : public TestLogToRedfishNumeric
+{};
+
+INSTANTIATE_TEST_SUITE_P(_, TestLogToRedfishNumericThrow, getIncorrectParams());
+
+TEST_P(TestLogToRedfishNumericThrow, commitExpectToThrow)
+{
+    EXPECT_THROW(sut->commit("Test", 100'000, commmitValue),
+                 std::runtime_error);
+}
+
+} // namespace numeric
+
+namespace discrete
+{
+using LogParam = ::discrete::Severity;
+
+static auto getCorrectParams()
+{
+    return Values(::discrete::Severity::critical, ::discrete::Severity::warning,
+                  ::discrete::Severity::ok);
+}
+
+static auto getIncorrectParams()
+{
+    return Values(static_cast<::discrete::Severity>(-1),
+                  static_cast<::discrete::Severity>(42));
+}
+
+class TestLogToJournalDiscrete :
+    public Test,
+    public WithParamInterface<LogParam>
+{
+  public:
+    void SetUp() override
+    {
+        auto severity = GetParam();
+        sut = std::make_unique<LogToJournal>(severity);
     }
 
     std::unique_ptr<LogToJournal> sut;
 };
 
-INSTANTIATE_TEST_SUITE_P(
-    LogToJournalParams, TestLogToJournal,
-    Values(std::make_pair(numeric::Type::upperCritical, 91.1),
-           std::make_pair(numeric::Type::lowerCritical, 91.2),
-           std::make_pair(numeric::Type::upperWarning, 88.5),
-           std::make_pair(numeric::Type::lowerWarning, 88.6)));
+INSTANTIATE_TEST_SUITE_P(LogToJournalDiscreteParams, TestLogToJournalDiscrete,
+                         getCorrectParams());
 
-TEST_P(TestLogToJournal, commitAnActionDoesNotThrow)
+TEST_P(TestLogToJournalDiscrete, commitAnActionDoesNotThrow)
 {
     EXPECT_NO_THROW(sut->commit("Test", 100'000, 90.0));
 }
 
-class TestLogToJournalThrow : public TestLogToJournal
+class TestLogToJournalDiscreteThrow : public TestLogToJournalDiscrete
 {};
 
-INSTANTIATE_TEST_SUITE_P(
-    _, TestLogToJournalThrow,
-    Values(std::make_pair(numeric::Type::upperCritical, 90.0),
-           std::make_pair(static_cast<numeric::Type>(-1), 88.0),
-           std::make_pair(static_cast<numeric::Type>(123), 123.0)));
+INSTANTIATE_TEST_SUITE_P(_, TestLogToJournalDiscreteThrow,
+                         getIncorrectParams());
 
-TEST_P(TestLogToJournalThrow, commitAnActionExpectThrow)
+TEST_P(TestLogToJournalDiscreteThrow, commitAnActionExpectThrow)
 {
     EXPECT_THROW(sut->commit("Test", 100'000, 90.0), std::runtime_error);
 }
 
-class TestLogToRedfish : public Test, public WithParamInterface<LogParam>
+class TestLogToRedfishDiscrete :
+    public Test,
+    public WithParamInterface<LogParam>
 {
   public:
     void SetUp() override
     {
-        auto [type, threshold] = GetParam();
-        sut = std::make_unique<LogToRedfish>(type, threshold);
+        auto severity = GetParam();
+        sut = std::make_unique<LogToRedfish>(severity);
     }
 
     std::unique_ptr<LogToRedfish> sut;
 };
 
-INSTANTIATE_TEST_SUITE_P(
-    LogToRedfishParams, TestLogToRedfish,
-    Values(std::make_pair(numeric::Type::upperCritical, 91.1),
-           std::make_pair(numeric::Type::lowerCritical, 91.4),
-           std::make_pair(numeric::Type::upperWarning, 88.6),
-           std::make_pair(numeric::Type::lowerWarning, 88.5)));
+INSTANTIATE_TEST_SUITE_P(LogToRedfishDiscreteParams, TestLogToRedfishDiscrete,
+                         getCorrectParams());
 
-TEST_P(TestLogToRedfish, commitExpectNoThrow)
+TEST_P(TestLogToRedfishDiscrete, commitExpectNoThrow)
 {
     EXPECT_NO_THROW(sut->commit("Test", 100'000, 90.0));
 }
 
-class TestLogToRedfishThrow : public TestLogToRedfish
+class TestLogToRedfishDiscreteThrow : public TestLogToRedfishDiscrete
 {};
 
-INSTANTIATE_TEST_SUITE_P(
-    _, TestLogToRedfishThrow,
-    Values(std::make_pair(numeric::Type::upperCritical, 90.0),
-           std::make_pair(static_cast<numeric::Type>(-1), 88.5),
-           std::make_pair(static_cast<numeric::Type>(123), 123.6)));
+INSTANTIATE_TEST_SUITE_P(_, TestLogToRedfishDiscreteThrow,
+                         getIncorrectParams());
 
-TEST_P(TestLogToRedfishThrow, commitExpectToThrow)
+TEST_P(TestLogToRedfishDiscreteThrow, commitExpectToThrow)
 {
     EXPECT_THROW(sut->commit("Test", 100'000, 90.0), std::runtime_error);
 }
 
+namespace onChange
+{
+class TestLogToJournalDiscreteOnChange : public Test
+{
+  public:
+    void SetUp() override
+    {
+        sut = std::make_unique<LogToJournal>();
+    }
+
+    std::unique_ptr<LogToJournal> sut;
+};
+
+TEST_F(TestLogToJournalDiscreteOnChange, commitExpectNoThrow)
+{
+    EXPECT_NO_THROW(sut->commit("Test", 100'000, 90.0));
+}
+
+class TestLogToRedfishDiscreteOnChange : public Test
+{
+  public:
+    void SetUp() override
+    {
+        sut = std::make_unique<LogToRedfish>();
+    }
+
+    std::unique_ptr<LogToRedfish> sut;
+};
+
+TEST_F(TestLogToRedfishDiscreteOnChange, commitExpectNoThrow)
+{
+    EXPECT_NO_THROW(sut->commit("Test", 100'000, 90.0));
+}
+} // namespace onChange
+} // namespace discrete
+
 class TestUpdateReport : public Test
 {
   public:
diff --git a/tests/src/test_trigger_manager.cpp b/tests/src/test_trigger_manager.cpp
index 95043e2..8754446 100644
--- a/tests/src/test_trigger_manager.cpp
+++ b/tests/src/test_trigger_manager.cpp
@@ -51,6 +51,36 @@
     EXPECT_THAT(path, Eq(triggerMock.getPath()));
 }
 
+TEST_F(TestTriggerManager, addTriggerWithDiscreteThresholds)
+{
+    TriggerParams triggerParamsDiscrete;
+    auto thresholds = std::vector<discrete::ThresholdParam>{
+        {"discrete_threshold1",
+         discrete::severityToString(discrete::Severity::ok), 10, 11.0},
+        {"discrete_threshold2",
+         discrete::severityToString(discrete::Severity::warning), 10, 12.0},
+        {"discrete_threshold3",
+         discrete::severityToString(discrete::Severity::critical), 10, 13.0}};
+
+    triggerParamsDiscrete.thresholdParams(thresholds).isDiscrete(true);
+
+    auto [ec, path] = addTrigger(triggerParamsDiscrete);
+    EXPECT_THAT(ec.value(), Eq(boost::system::errc::success));
+    EXPECT_THAT(path, Eq(triggerMock.getPath()));
+}
+
+TEST_F(TestTriggerManager, addDiscreteTriggerWithoutThresholds)
+{
+    TriggerParams triggerParamsDiscrete;
+    auto thresholds = std::vector<discrete::ThresholdParam>();
+
+    triggerParamsDiscrete.thresholdParams(thresholds).isDiscrete(true);
+
+    auto [ec, path] = addTrigger(triggerParamsDiscrete);
+    EXPECT_THAT(ec.value(), Eq(boost::system::errc::success));
+    EXPECT_THAT(path, Eq(triggerMock.getPath()));
+}
+
 TEST_F(TestTriggerManager, DISABLED_failToAddTriggerTwice)
 {
     triggerFactoryMock.expectMake(triggerParams, Ref(*sut))