Add LastStateChangeTime to chassis manager

This property is set to the timestamp of the last time
the chassis power state changed.  It is persisted
so it survives reboots.

Resolves openbmc/openbmc#3300

Tested:  Various incantations of power cycling, reboots,
         and AC pulls.

Change-Id: I19f244e0490bc9b921454e393989a9cbd283e2dd
Signed-off-by: Matt Spinler <spinler@us.ibm.com>
diff --git a/chassis_state_manager.cpp b/chassis_state_manager.cpp
index a723d8f..a99aec9 100644
--- a/chassis_state_manager.cpp
+++ b/chassis_state_manager.cpp
@@ -95,6 +95,22 @@
             server::Chassis::requestedPowerTransition(Transition::On);
             return;
         }
+        else
+        {
+            // The system is off.  If we think it should be on then
+            // we probably lost AC while up, so set a new state
+            // change time.
+            uint64_t lastTime;
+            PowerState lastState;
+
+            if (deserializeStateChangeTime(lastTime, lastState))
+            {
+                if (lastState == PowerState::On)
+                {
+                    setStateChangeTime();
+                }
+            }
+        }
     }
     catch (const SdBusError& e)
     {
@@ -220,6 +236,7 @@
     {
         log<level::INFO>("Received signal that power OFF is complete");
         this->currentPowerState(server::Chassis::PowerState::Off);
+        this->setStateChangeTime();
     }
     else if ((newStateUnit == CHASSIS_STATE_POWERON_TGT) &&
              (newStateResult == "done") &&
@@ -227,6 +244,7 @@
     {
         log<level::INFO>("Received signal that power ON is complete");
         this->currentPowerState(server::Chassis::PowerState::On);
+        this->setStateChangeTime();
     }
 
     return 0;
@@ -367,6 +385,80 @@
     }
 }
 
+void Chassis::serializeStateChangeTime()
+{
+    fs::path path{CHASSIS_STATE_CHANGE_PERSIST_PATH};
+    std::ofstream os(path.c_str(), std::ios::binary);
+    cereal::JSONOutputArchive oarchive(os);
+
+    oarchive(ChassisInherit::lastStateChangeTime(),
+             ChassisInherit::currentPowerState());
+}
+
+bool Chassis::deserializeStateChangeTime(uint64_t& time, PowerState& state)
+{
+    fs::path path{CHASSIS_STATE_CHANGE_PERSIST_PATH};
+
+    try
+    {
+        if (fs::exists(path))
+        {
+            std::ifstream is(path.c_str(), std::ios::in | std::ios::binary);
+            cereal::JSONInputArchive iarchive(is);
+            iarchive(time, state);
+            return true;
+        }
+    }
+    catch (std::exception& e)
+    {
+        log<level::ERR>(e.what());
+        fs::remove(path);
+    }
+
+    return false;
+}
+
+void Chassis::restoreChassisStateChangeTime()
+{
+    uint64_t time;
+    PowerState state;
+
+    if (!deserializeStateChangeTime(time, state))
+    {
+        ChassisInherit::lastStateChangeTime(0);
+    }
+    else
+    {
+        ChassisInherit::lastStateChangeTime(time);
+    }
+}
+
+void Chassis::setStateChangeTime()
+{
+    using namespace std::chrono;
+    uint64_t lastTime;
+    PowerState lastState;
+
+    auto now =
+        duration_cast<milliseconds>(system_clock::now().time_since_epoch())
+            .count();
+
+    // If power is on when the BMC is rebooted, this function will get called
+    // because sysStateChange() runs.  Since the power state didn't change
+    // in this case, neither should the state change time, so check that
+    // the power state actually did change here.
+    if (deserializeStateChangeTime(lastTime, lastState))
+    {
+        if (lastState == ChassisInherit::currentPowerState())
+        {
+            return;
+        }
+    }
+
+    ChassisInherit::lastStateChangeTime(now);
+    serializeStateChangeTime();
+}
+
 } // namespace manager
 } // namespace state
 } // namepsace phosphor
diff --git a/chassis_state_manager.hpp b/chassis_state_manager.hpp
index 5532949..9a6b647 100644
--- a/chassis_state_manager.hpp
+++ b/chassis_state_manager.hpp
@@ -61,6 +61,8 @@
     {
         subscribeToSystemdSignals();
 
+        restoreChassisStateChangeTime();
+
         determineInitialState();
 
         restorePOHCounter(); // restore POHCounter from persisted file
@@ -157,6 +159,35 @@
      */
     bool deserializePOH(const fs::path& path, uint32_t& retCounter);
 
+    /** @brief Sets the LastStateChangeTime property and persists it. */
+    void setStateChangeTime();
+
+    /** @brief Serialize the last power state change time.
+     *
+     *  Save the time the state changed and the state itself.
+     *  The state needs to be saved as well so that during rediscovery
+     *  on reboots there's a way to know not to update the time again.
+     */
+    void serializeStateChangeTime();
+
+    /** @brief Deserialize the last power state change time.
+     *
+     *  @param[out] time - Deserialized time
+     *  @param[out] state - Deserialized power state
+     *
+     *  @return bool - true if successful, false otherwise.
+     */
+    bool deserializeStateChangeTime(uint64_t& time, PowerState& state);
+
+    /** @brief Restores the power state change time.
+     *
+     *  The time is loaded into the LastStateChangeTime D-Bus property.
+     *  On the very first start after this code has been applied but
+     *  before the state has changed, the LastStateChangeTime value
+     *  will be zero.
+     */
+    void restoreChassisStateChangeTime();
+
     /** @brief Timer */
     std::unique_ptr<phosphor::state::manager::Timer> timer;
 };
diff --git a/configure.ac b/configure.ac
index 838aaf2..f1d3a2f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -66,6 +66,12 @@
 AC_DEFINE_UNQUOTED([POH_COUNTER_PERSIST_PATH], ["$POH_COUNTER_PERSIST_PATH"], \
     [Path of file for storing POH counter.])
 
+AC_ARG_VAR(CHASSIS_STATE_CHANGE_PERSIST_PATH, [Path of file for storing the state change time.])
+AS_IF([test "x$CHASSIS_STATE_CHANGE_PERSIST_PATH" == "x"], \
+    [CHASSIS_STATE_CHANGE_PERSIST_PATH="/var/lib/phosphor-state-manager/chassisStateChangeTime"])
+AC_DEFINE_UNQUOTED([CHASSIS_STATE_CHANGE_PERSIST_PATH], ["$CHASSIS_STATE_CHANGE_PERSIST_PATH"], \
+    [Path of file for storing the state change time.])
+
 AC_ARG_VAR(BOOT_COUNT_MAX_ALLOWED, [The maximum allowed reboot count])
 AS_IF([test "x$BOOT_COUNT_MAX_ALLOWED" == "x"], [BOOT_COUNT_MAX_ALLOWED=3])
 AC_DEFINE_UNQUOTED([BOOT_COUNT_MAX_ALLOWED], [$BOOT_COUNT_MAX_ALLOWED], [The maximum allowed reboot count])