Add timer expiration feature to ExternalSensor

ExternalSensor now functions as intended, wholly within dbus-sensors,
without requiring any modification to the IPMI or Redfish servers,
to provide the feature of timeout expiration of external data, so that
stale/lost external connections can be properly indicated as such.

A "Timeout" parameter is added, in decimal seconds, providing a
watchdog for the arrival of external data. The expectation is that the
external source will provide D-Bus updates, to the sensor Value
property, at regular intervals, repeating indefinitely.
If this external source stops doing this, the watchdog barks, and
the Value of this Sensor will become set to "NaN". This provides
an indication to consumers of this Sensor, to realize that the Value
of this sensor has became stale/disconnected.

A practical application of this is fan control. Upon loss of external
temperature notification, the fans could be thrown into failsafe
mode, instead of risking the system overheating by wrongly continuing
to believe an old temperature value that has become stale.

Tested: Works for me. I started an external data source, data arrived
into the Value of the sensor. I stopped that external data source,
after the Timeout period, the Value became "NaN". I started the
external source again, the Value became correct again, as soon as
external data started to arrive again. I repeated this stop and start
procedure a few times, verifying that it operated as intended.

Signed-off-by: Josh Lehan <krellan@google.com>
Change-Id: I53b9ff4c0aa771aff4aaf3449fcab23c07afa296
diff --git a/src/ExternalSensor.cpp b/src/ExternalSensor.cpp
index c6db174..13082e3 100644
--- a/src/ExternalSensor.cpp
+++ b/src/ExternalSensor.cpp
@@ -10,6 +10,7 @@
 #include <sdbusplus/asio/connection.hpp>
 #include <sdbusplus/asio/object_server.hpp>
 
+#include <chrono>
 #include <iostream>
 #include <istream>
 #include <limits>
@@ -17,13 +18,17 @@
 #include <string>
 #include <vector>
 
+static constexpr bool debug = false;
+
 ExternalSensor::ExternalSensor(
     const std::string& objectType, sdbusplus::asio::object_server& objectServer,
     std::shared_ptr<sdbusplus::asio::connection>& conn,
     const std::string& sensorName, const std::string& sensorUnits,
     std::vector<thresholds::Threshold>&& thresholdsIn,
-    const std::string& sensorConfiguration, const double& maxReading,
-    const double& minReading, const PowerState& powerState) :
+    const std::string& sensorConfiguration, double maxReading,
+    double minReading, double timeoutSecs, const PowerState& powerState,
+    std::function<void(std::chrono::steady_clock::time_point now)>&&
+        writeHookIn) :
     // TODO(): When the Mutable feature is integrated,
     // make sure all ExternalSensor instances are mutable,
     // because that is the entire point of ExternalSensor,
@@ -31,7 +36,13 @@
     Sensor(boost::replace_all_copy(sensorName, " ", "_"),
            std::move(thresholdsIn), sensorConfiguration, objectType, maxReading,
            minReading, conn, powerState),
-    std::enable_shared_from_this<ExternalSensor>(), objServer(objectServer)
+    std::enable_shared_from_this<ExternalSensor>(), objServer(objectServer),
+    writeLast(std::chrono::steady_clock::now()),
+    writeTimeout(
+        std::chrono::duration_cast<std::chrono::steady_clock::duration>(
+            std::chrono::duration<double>(timeoutSecs))),
+    writeAlive(false), writePerishable(timeoutSecs > 0.0),
+    writeHook(std::move(writeHookIn))
 {
     // The caller must specify what physical characteristic
     // an external sensor is expected to be measuring, such as temperature,
@@ -63,17 +74,121 @@
     association =
         objectServer.add_interface(objectPath, association::interface);
     setInitialProperties(conn);
+
+    externalSetHook = [weakThis = weak_from_this()]() {
+        auto lockThis = weakThis.lock();
+        if (lockThis)
+        {
+            lockThis->externalSetTrigger();
+        }
+    };
+
+    if constexpr (debug)
+    {
+        std::cerr << "ExternalSensor " << name << " constructed: path "
+                  << configurationPath << ", type " << objectType << ", min "
+                  << minReading << ", max " << maxReading << ", timeout "
+                  << std::chrono::duration_cast<std::chrono::microseconds>(
+                         writeTimeout)
+                         .count()
+                  << " us\n";
+    }
 }
 
 ExternalSensor::~ExternalSensor()
 {
+    // Make sure the write hook does not reference this object anymore
+    externalSetHook = nullptr;
+
     objServer.remove_interface(association);
     objServer.remove_interface(thresholdInterfaceCritical);
     objServer.remove_interface(thresholdInterfaceWarning);
     objServer.remove_interface(sensorInterface);
+
+    if constexpr (debug)
+    {
+        std::cerr << "ExternalSensor " << name << " destructed\n";
+    }
 }
 
 void ExternalSensor::checkThresholds(void)
 {
     thresholds::checkThresholds(this);
 }
+
+bool ExternalSensor::isAliveAndPerishable(void) const
+{
+    return (writeAlive && writePerishable);
+}
+
+bool ExternalSensor::isAliveAndFresh(
+    const std::chrono::steady_clock::time_point& now) const
+{
+    // Must be alive and perishable, to have possibility of being fresh
+    if (!isAliveAndPerishable())
+    {
+        return false;
+    }
+
+    // If age, as of now, is less than timeout, it is deemed fresh
+    return (ageElapsed(now) < writeTimeout);
+}
+
+void ExternalSensor::writeBegin(
+    const std::chrono::steady_clock::time_point& now)
+{
+    if (!writeAlive)
+    {
+        std::cerr << "ExternalSensor " << name
+                  << " online, receiving first value " << value << "\n";
+    }
+
+    writeLast = now;
+    writeAlive = true;
+}
+
+void ExternalSensor::writeInvalidate(void)
+{
+    writeAlive = false;
+
+    std::cerr << "ExternalSensor " << name << " offline, timed out\n";
+
+    // Take back control of this sensor from the external override,
+    // as the external source has timed out.
+    // This allows sensor::updateValue() to work normally,
+    // as it would do for internal sensors with values from hardware.
+    overriddenState = false;
+
+    // Invalidate the existing Value, similar to what internal sensors do,
+    // when they encounter errors trying to read from hardware.
+    updateValue(std::numeric_limits<double>::quiet_NaN());
+}
+
+std::chrono::steady_clock::duration ExternalSensor::ageElapsed(
+    const std::chrono::steady_clock::time_point& now) const
+{
+    // Comparing 2 time_point will return duration
+    return (now - writeLast);
+}
+
+std::chrono::steady_clock::duration ExternalSensor::ageRemaining(
+    const std::chrono::steady_clock::time_point& now) const
+{
+    // Comparing duration will return another duration
+    return (writeTimeout - ageElapsed(now));
+}
+
+void ExternalSensor::externalSetTrigger(void)
+{
+    if constexpr (debug)
+    {
+        std::cerr << "ExternalSensor " << name << " received " << value << "\n";
+    }
+
+    auto now = std::chrono::steady_clock::now();
+
+    writeBegin(now);
+
+    // Tell the owner to recalculate the expiration timer
+    writeHook(now);
+}