presence: Track missing fans with timers

Fill in the ErrorReporter class to track fan presence status using Timer
objects.  The timers will be started if power is on when fans are
removed.  If a fan is replaced before the timer expires, or if the
system powers off before the timer expires, the timer will be stopped.

The function called when a timer expires is currently stubbed, but
eventually it will create an event log.

The class watches presence changes by watching the Present property for
the fans in the inventory.  Technically, it could watch an internal
status, but this method was chosen because
a) It makes testing easier, so presence changes can be forced using
   busctl as opposed to physically removing hardware.
b) There wasn't really a good place to hook in presence state watches.
c) The application would already crash anyway if the inventory service
   wasn't working for some reason.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: Ib0524b9b8335414de6b52ce159771eba95248441
diff --git a/presence/error_reporter.cpp b/presence/error_reporter.cpp
index 7ff71ae..a73d08b 100644
--- a/presence/error_reporter.cpp
+++ b/presence/error_reporter.cpp
@@ -15,6 +15,13 @@
  */
 #include "error_reporter.hpp"
 
+#include "logging.hpp"
+#include "psensor.hpp"
+#include "utility.hpp"
+
+#include <fmt/format.h>
+#include <unistd.h>
+
 #include <phosphor-logging/log.hpp>
 
 namespace phosphor::fan::presence
@@ -22,14 +29,57 @@
 
 using json = nlohmann::json;
 using namespace phosphor::logging;
+using namespace sdbusplus::bus::match;
+using namespace std::literals::string_literals;
+namespace fs = std::filesystem;
+
+const auto itemIface = "xyz.openbmc_project.Inventory.Item"s;
+const auto invPrefix = "/xyz/openbmc_project/inventory"s;
 
 ErrorReporter::ErrorReporter(
     sdbusplus::bus::bus& bus, const json& jsonConf,
     const std::vector<
         std::tuple<Fan, std::vector<std::unique_ptr<PresenceSensor>>>>& fans) :
-    _bus(bus)
+    _bus(bus),
+    _event(sdeventplus::Event::get_default())
 {
     loadConfig(jsonConf);
+
+    // If different methods to check the power state are needed across the
+    // various platforms, the method/class to use could be read out of JSON
+    // or set with a compilation flag.
+    _powerState = std::make_unique<PGoodState>(
+        bus, std::bind(std::mem_fn(&ErrorReporter::powerStateChanged), this,
+                       std::placeholders::_1));
+
+    for (const auto& fan : fans)
+    {
+        auto path = invPrefix + std::get<1>(std::get<0>(fan));
+
+        // Register for fan presence changes, get their initial states,
+        // and create the fan missing timers for each fan.
+
+        _matches.emplace_back(
+            _bus, rules::propertiesChanged(path, itemIface),
+            std::bind(std::mem_fn(&ErrorReporter::presenceChanged), this,
+                      std::placeholders::_1));
+
+        _fanStates.emplace(path, getPresence(std::get<0>(fan)));
+
+        auto timer = std::make_unique<
+            sdeventplus::utility::Timer<sdeventplus::ClockId::Monotonic>>(
+            _event,
+            std::bind(std::mem_fn(&ErrorReporter::fanMissingTimerExpired), this,
+                      path));
+
+        _fanMissingTimers.emplace(path, std::move(timer));
+    }
+
+    // If power is already on, check for currently missing fans.
+    if (_powerState->isPowerOn())
+    {
+        powerStateChanged(true);
+    }
 }
 
 void ErrorReporter::loadConfig(const json& jsonConf)
@@ -46,4 +96,77 @@
         jsonConf.at("fan_missing_error_time").get<std::size_t>()};
 }
 
+void ErrorReporter::presenceChanged(sdbusplus::message::message& msg)
+{
+    bool present;
+    auto fanPath = msg.get_path();
+    std::string interface;
+    std::map<std::string, std::variant<bool>> properties;
+
+    msg.read(interface, properties);
+
+    auto presentProp = properties.find("Present");
+    if (presentProp != properties.end())
+    {
+        present = std::get<bool>(presentProp->second);
+        if (_fanStates[fanPath] != present)
+        {
+            getLogger().log(fmt::format("Fan {} presence state change to {}",
+                                        fanPath, present));
+
+            _fanStates[fanPath] = present;
+            checkFan(fanPath);
+        }
+    }
+}
+
+void ErrorReporter::checkFan(const std::string& fanPath)
+{
+    if (!_fanStates[fanPath])
+    {
+        // Fan is missing. If power is on, start the timer.
+        // If power is off, stop a running timer.
+        if (_powerState->isPowerOn())
+        {
+            _fanMissingTimers[fanPath]->restartOnce(_fanMissingErrorTime);
+        }
+        else if (_fanMissingTimers[fanPath]->isEnabled())
+        {
+            _fanMissingTimers[fanPath]->setEnabled(false);
+        }
+    }
+    else
+    {
+        // Fan is present. Stop a running timer.
+        if (_fanMissingTimers[fanPath]->isEnabled())
+        {
+            _fanMissingTimers[fanPath]->setEnabled(false);
+        }
+    }
+}
+
+void ErrorReporter::fanMissingTimerExpired(const std::string& fanPath)
+{}
+
+void ErrorReporter::powerStateChanged(bool powerState)
+{
+    if (powerState)
+    {
+        // If there are fans already missing, log it.
+        auto missing = std::count_if(
+            _fanStates.begin(), _fanStates.end(),
+            [](const auto& fanState) { return fanState.second == false; });
+
+        if (missing)
+        {
+            getLogger().log(
+                fmt::format("At power on, there are {} missing fans", missing));
+        }
+    }
+
+    std::for_each(
+        _fanStates.begin(), _fanStates.end(),
+        [this](const auto& fanState) { this->checkFan(fanState.first); });
+}
+
 } // namespace phosphor::fan::presence