Add support for OEM Power Modes

- Allow mode to be set via PassThrough interface
- Allow non-customer OEM power modes to be persisted
- Persist any OEM power mode settings
- moved mode related code from Status to PowerMode object
- merged PowerIPS into PowerMode object

Tested on Everest and Rainier.
Setting mode through PassThrough/ce-login:
  busctl call org.open_power.OCC.Control /org/open_power/control/occ0 org.open_power.OCC.PassThrough SetMode yq 11 3600
Trace (via PassThrough interface)
  openpower-occ-control[4440]: PassThrough::setMode() Setting Power Mode 11 (data: 3600)
  openpower-occ-control[4440]: PowerMode::sendModeChange: SET_MODE(11,3600) command to OCC0 (9 bytes)
Trace (setting mode via GUI/Redfish):
  openpower-occ-control[4440]: Power Mode Change Requested: xyz.openbmc_project.Control.Power.Mode.PowerMode.MaximumPerformance
  openpower-occ-control[4440]: PowerMode::sendModeChange: SET_MODE(12,0) command to OCC0 (9 bytes)
Verified when system in any OEM mode that Redfish also reports OEM
Verified all modes are persisted across PM Complex resets and reboots

Change-Id: Idd0be05cb6fd74dbd0776145f212c49addd1c365
Signed-off-by: Chris Cain <cjcain@us.ibm.com>
diff --git a/powermode.cpp b/powermode.cpp
index e87b921..04a9787 100644
--- a/powermode.cpp
+++ b/powermode.cpp
@@ -1,10 +1,13 @@
+#include "powermode.hpp"
+
 #include <fmt/core.h>
 
+#include <com/ibm/Host/Target/server.hpp>
 #include <phosphor-logging/log.hpp>
-#include <powermode.hpp>
 #include <xyz/openbmc_project/Control/Power/Mode/server.hpp>
 
 #include <cassert>
+#include <fstream>
 #include <regex>
 
 namespace open_power
@@ -17,13 +20,10 @@
 using namespace phosphor::logging;
 using Mode = sdbusplus::xyz::openbmc_project::Control::Power::server::Mode;
 
+// Called when DBus power mode gets changed
 void PowerMode::modeChanged(sdbusplus::message::message& msg)
 {
-    if (!occStatus.occActive())
-    {
-        // Nothing to do
-        return;
-    }
+    SysPwrMode newMode = SysPwrMode::NO_CHANGE;
 
     std::map<std::string, std::variant<std::string>> properties{};
     std::string interface;
@@ -34,22 +34,51 @@
     {
         auto modeEntryValue = modeEntry->second;
         propVal = std::get<std::string>(modeEntryValue);
-
-        if (convertStringToMode(propVal) != SysPwrMode::NO_CHANGE)
+        newMode = convertStringToMode(propVal);
+        if (newMode != SysPwrMode::NO_CHANGE)
         {
+            // DBus mode changed, get rid of any OEM mode if set
+            persistedData.purge();
+
             log<level::INFO>(
                 fmt::format("Power Mode Change Requested: {}", propVal)
                     .c_str());
 
-            // Trigger mode change to OCC
-            occStatus.sendModeChange();
+            // Send mode change to OCC
+            sendModeChange();
+        }
+    }
+}
+
+// Called from OCC PassThrough interface (via CE login / BMC command line)
+bool PowerMode::setMode(const SysPwrMode newMode, const uint16_t modeData)
+{
+    if (updateDbusMode(newMode) == false)
+    {
+        // Unsupported mode
+        return false;
+    }
+
+    // If new mode is valid customer mode, the DBus update will trigger the mode
+    // change request to OCC.  For OEM modes, the request will be sent here.
+    if (VALID_OEM_POWER_MODE_SETTING(newMode))
+    {
+        // Save OEM mode
+        persistedData.writeModeFile(newMode, modeData);
+
+        // Send mode change to OCC
+        if (sendModeChange() != CmdStatus::SUCCESS)
+        {
+            // Mode change failed
+            return false;
         }
     }
 
-    return;
+    return true;
 }
 
 // Convert PowerMode string to OCC SysPwrMode
+// Returns NO_CHANGE if OEM or unsupported mode
 SysPwrMode convertStringToMode(const std::string& i_modeString)
 {
     SysPwrMode pmode = SysPwrMode::NO_CHANGE;
@@ -65,22 +94,230 @@
     }
     else if (mode == Mode::PowerMode::Static)
     {
-        pmode = SysPwrMode::DISABLE;
+        pmode = SysPwrMode::STATIC;
     }
     else
     {
-        log<level::ERR>(
-            fmt::format("convertStringToMode: Invalid Power Mode specified: {}",
-                        i_modeString)
-                .c_str());
+        if (mode != Mode::PowerMode::OEM)
+        {
+            log<level::ERR>(
+                fmt::format(
+                    "convertStringToMode: Invalid Power Mode specified: {}",
+                    i_modeString)
+                    .c_str());
+        }
     }
 
     return pmode;
 }
 
-void PowerIPS::ipsChanged(sdbusplus::message::message& msg)
+// Check if Hypervisor target is PowerVM
+bool isPowerVM()
 {
-    if (!occStatus.occActive())
+    using namespace open_power::occ::powermode;
+    namespace Hyper = sdbusplus::com::ibm::Host::server;
+    constexpr auto HYPE_PATH = "/com/ibm/host0/hypervisor";
+    constexpr auto HYPE_INTERFACE = "com.ibm.Host.Target";
+    constexpr auto HYPE_PROP = "Target";
+
+    bool powerVmTarget = false;
+
+    // This will throw exception on failure
+    auto& bus = utils::getBus();
+    auto service = utils::getService(HYPE_PATH, HYPE_INTERFACE);
+    auto method = bus.new_method_call(service.c_str(), HYPE_PATH,
+                                      "org.freedesktop.DBus.Properties", "Get");
+    method.append(HYPE_INTERFACE, HYPE_PROP);
+    auto reply = bus.call(method);
+
+    std::variant<std::string> hyperEntryValue;
+    reply.read(hyperEntryValue);
+    auto propVal = std::get<std::string>(hyperEntryValue);
+    if (Hyper::Target::convertHypervisorFromString(propVal) ==
+        Hyper::Target::Hypervisor::PowerVM)
+    {
+        powerVmTarget = true;
+    }
+
+    log<level::DEBUG>(
+        fmt::format("isPowerVM returning {}", powerVmTarget).c_str());
+
+    return powerVmTarget;
+}
+
+// Get the requested power mode from DBus
+SysPwrMode PowerMode::getDbusMode()
+{
+    using namespace open_power::occ::powermode;
+    SysPwrMode currentMode = SysPwrMode::NO_CHANGE;
+
+    // This will throw exception on failure
+    auto& bus = utils::getBus();
+    auto service = utils::getService(PMODE_PATH, PMODE_INTERFACE);
+    auto method = bus.new_method_call(service.c_str(), PMODE_PATH,
+                                      "org.freedesktop.DBus.Properties", "Get");
+    method.append(PMODE_INTERFACE, POWER_MODE_PROP);
+    auto reply = bus.call(method);
+
+    std::variant<std::string> stateEntryValue;
+    reply.read(stateEntryValue);
+    auto propVal = std::get<std::string>(stateEntryValue);
+
+    currentMode = powermode::convertStringToMode(propVal);
+    if (!VALID_POWER_MODE_SETTING(currentMode))
+    {
+        log<level::ERR>(
+            fmt::format(
+                "PowerMode::getDbusMode Invalid power mode found on DBus: {}",
+                currentMode)
+                .c_str());
+        currentMode = SysPwrMode::NO_CHANGE;
+    }
+
+    return currentMode;
+}
+
+// Set the power mode on DBus
+bool PowerMode::updateDbusMode(const SysPwrMode newMode)
+{
+    using namespace open_power::occ::powermode;
+    using namespace std::literals::string_literals;
+
+    if (!VALID_POWER_MODE_SETTING(newMode) &&
+        !VALID_OEM_POWER_MODE_SETTING(newMode))
+    {
+        log<level::ERR>(
+            fmt::format(
+                "PowerMode::updateDbusMode - Requested power mode not supported: {}",
+                newMode)
+                .c_str());
+        return false;
+    }
+
+    // Mode::PowerMode dBusMode;
+    std::string dBusMode;
+    switch (newMode)
+    {
+        case SysPwrMode::STATIC:
+            dBusMode = PMODE_INTERFACE + ".PowerMode.Static"s;
+            break;
+        case SysPwrMode::POWER_SAVING:
+            dBusMode = PMODE_INTERFACE + ".PowerMode.PowerSaving"s;
+            break;
+        case SysPwrMode::MAX_PERF:
+            dBusMode = PMODE_INTERFACE + ".PowerMode.MaximumPerformance"s;
+            break;
+        default:
+            dBusMode = PMODE_INTERFACE + ".PowerMode.OEM"s;
+    }
+
+    utils::setProperty(PMODE_PATH, PMODE_INTERFACE, POWER_MODE_PROP,
+                       std::move(dBusMode));
+
+    return true;
+}
+
+// Send mode change request to the master OCC
+CmdStatus PowerMode::sendModeChange()
+{
+    CmdStatus status = CmdStatus::FAILURE;
+
+    if (!masterActive)
+    {
+        // Nothing to do
+        log<level::DEBUG>("PowerMode::sendModeChange: MODE CHANGE not enabled");
+        return CmdStatus::SUCCESS;
+    }
+
+    if (!isPowerVM())
+    {
+        // Mode change is only supported on PowerVM systems
+        log<level::DEBUG>(
+            "PowerMode::sendModeChange: MODE CHANGE does not get sent on non-PowerVM systems");
+        return CmdStatus::SUCCESS;
+    }
+
+    // Use OEM power mode if it was set
+    SysPwrMode newMode = SysPwrMode::NO_CHANGE;
+    uint16_t modeData = 0;
+    if (persistedData.getOemMode(newMode, modeData) == false)
+    {
+        // Read customer power mode from Dbus
+        newMode = getDbusMode();
+    }
+
+    if (VALID_POWER_MODE_SETTING(newMode) ||
+        VALID_OEM_POWER_MODE_SETTING(newMode))
+    {
+        std::vector<std::uint8_t> cmd, rsp;
+        cmd.reserve(9);
+        cmd.push_back(uint8_t(CmdType::SET_MODE_AND_STATE));
+        cmd.push_back(0x00); // Data Length (2 bytes)
+        cmd.push_back(0x06);
+        cmd.push_back(0x30); // Data (Version)
+        cmd.push_back(uint8_t(OccState::NO_CHANGE));
+        cmd.push_back(uint8_t(newMode));
+        cmd.push_back(modeData >> 8);   // Mode Data (Freq Point)
+        cmd.push_back(modeData & 0xFF); //
+        cmd.push_back(0x00);            // reserved
+        log<level::INFO>(
+            fmt::format(
+                "PowerMode::sendModeChange: SET_MODE({},{}) command to OCC{} ({} bytes)",
+                newMode, modeData, occInstance, cmd.size())
+                .c_str());
+        status = occCmd.send(cmd, rsp);
+        if (status == CmdStatus::SUCCESS)
+        {
+            if (rsp.size() == 5)
+            {
+                if (RspStatus::SUCCESS != RspStatus(rsp[2]))
+                {
+                    log<level::ERR>(
+                        fmt::format(
+                            "PowerMode::sendModeChange: SET MODE failed with status 0x{:02X}",
+                            rsp[2])
+                            .c_str());
+                    dump_hex(rsp);
+                    status = CmdStatus::FAILURE;
+                }
+            }
+            else
+            {
+                log<level::ERR>(
+                    "PowerMode::sendModeChange: INVALID SET MODE response");
+                dump_hex(rsp);
+                status = CmdStatus::FAILURE;
+            }
+        }
+        else
+        {
+            if (status == CmdStatus::OPEN_FAILURE)
+            {
+                // OCC not active yet
+                status = CmdStatus::SUCCESS;
+            }
+            else
+            {
+                log<level::ERR>("PowerMode::sendModeChange: SET_MODE FAILED!");
+            }
+        }
+    }
+    else
+    {
+        log<level::ERR>(
+            fmt::format(
+                "PowerMode::sendModeChange: Unable to set power mode to {}",
+                newMode)
+                .c_str());
+        status = CmdStatus::FAILURE;
+    }
+
+    return status;
+}
+
+void PowerMode::ipsChanged(sdbusplus::message::message& msg)
+{
+    if (!masterActive)
     {
         // Nothing to do
         return;
@@ -145,12 +382,308 @@
     if (parmsChanged)
     {
         // Trigger mode change to OCC
-        occStatus.sendIpsData();
+        sendIpsData();
     }
 
     return;
 }
 
+/** @brief Get the Idle Power Saver properties
+ * @return true if IPS is enabled
+ */
+bool PowerMode::getIPSParms(uint8_t& enterUtil, uint16_t& enterTime,
+                            uint8_t& exitUtil, uint16_t& exitTime)
+{
+    using namespace open_power::occ::powermode;
+    // Defaults:
+    bool ipsEnabled = false; // Disabled
+    enterUtil = 8;           // Enter Utilization (8%)
+    enterTime = 240;         // Enter Delay Time (240s)
+    exitUtil = 12;           // Exit Utilization (12%)
+    exitTime = 10;           // Exit Delay Time (10s)
+
+    std::map<std::string, std::variant<bool, uint8_t, uint64_t>>
+        ipsProperties{};
+
+    // Get all IPS properties from DBus
+    try
+    {
+        auto& bus = utils::getBus();
+        auto service = utils::getService(PIPS_PATH, PIPS_INTERFACE);
+        auto method =
+            bus.new_method_call(service.c_str(), PIPS_PATH,
+                                "org.freedesktop.DBus.Properties", "GetAll");
+        method.append(PIPS_INTERFACE);
+        auto reply = bus.call(method);
+        reply.read(ipsProperties);
+    }
+    catch (const sdbusplus::exception::exception& e)
+    {
+        log<level::ERR>(
+            fmt::format(
+                "Unable to read Idle Power Saver parameters so it will be disabled: {}",
+                e.what())
+                .c_str());
+        return ipsEnabled;
+    }
+
+    auto ipsEntry = ipsProperties.find(IPS_ENABLED_PROP);
+    if (ipsEntry != ipsProperties.end())
+    {
+        ipsEnabled = std::get<bool>(ipsEntry->second);
+    }
+    else
+    {
+        log<level::ERR>(
+            fmt::format("PowerMode::getIPSParms could not find property: {}",
+                        IPS_ENABLED_PROP)
+                .c_str());
+    }
+
+    ipsEntry = ipsProperties.find(IPS_ENTER_UTIL);
+    if (ipsEntry != ipsProperties.end())
+    {
+        enterUtil = std::get<uint8_t>(ipsEntry->second);
+    }
+    else
+    {
+        log<level::ERR>(
+            fmt::format("PowerMode::getIPSParms could not find property: {}",
+                        IPS_ENTER_UTIL)
+                .c_str());
+    }
+
+    ipsEntry = ipsProperties.find(IPS_ENTER_TIME);
+    if (ipsEntry != ipsProperties.end())
+    {
+        std::chrono::milliseconds ms(std::get<uint64_t>(ipsEntry->second));
+        enterTime =
+            std::chrono::duration_cast<std::chrono::seconds>(ms).count();
+    }
+    else
+    {
+        log<level::ERR>(
+            fmt::format("PowerMode::getIPSParms could not find property: {}",
+                        IPS_ENTER_TIME)
+                .c_str());
+    }
+
+    ipsEntry = ipsProperties.find(IPS_EXIT_UTIL);
+    if (ipsEntry != ipsProperties.end())
+    {
+        exitUtil = std::get<uint8_t>(ipsEntry->second);
+    }
+    else
+    {
+        log<level::ERR>(
+            fmt::format("PowerMode::getIPSParms could not find property: {}",
+                        IPS_EXIT_UTIL)
+                .c_str());
+    }
+
+    ipsEntry = ipsProperties.find(IPS_EXIT_TIME);
+    if (ipsEntry != ipsProperties.end())
+    {
+        std::chrono::milliseconds ms(std::get<uint64_t>(ipsEntry->second));
+        exitTime = std::chrono::duration_cast<std::chrono::seconds>(ms).count();
+    }
+    else
+    {
+        log<level::ERR>(
+            fmt::format("PowerMode::getIPSParms could not find property: {}",
+                        IPS_EXIT_TIME)
+                .c_str());
+    }
+
+    if (enterUtil > exitUtil)
+    {
+        log<level::ERR>(
+            fmt::format(
+                "ERROR: Idle Power Saver Enter Utilization ({}%) is > Exit Utilization ({}%) - using Exit for both",
+                enterUtil, exitUtil)
+                .c_str());
+        enterUtil = exitUtil;
+    }
+
+    return ipsEnabled;
+}
+
+// Send Idle Power Saver config data to the master OCC
+CmdStatus PowerMode::sendIpsData()
+{
+    CmdStatus status = CmdStatus::FAILURE;
+
+    if (!masterActive)
+    {
+        // Nothing to do
+        return CmdStatus::SUCCESS;
+    }
+
+    if (!isPowerVM())
+    {
+        // Idle Power Saver data is only supported on PowerVM systems
+        log<level::DEBUG>(
+            "PowerMode::sendIpsData: SET_CFG_DATA[IPS] does not get sent on non-PowerVM systems");
+        return CmdStatus::SUCCESS;
+    }
+
+    uint8_t enterUtil, exitUtil;
+    uint16_t enterTime, exitTime;
+    const bool ipsEnabled =
+        getIPSParms(enterUtil, enterTime, exitUtil, exitTime);
+
+    log<level::INFO>(
+        fmt::format(
+            "Idle Power Saver Parameters: enabled:{}, enter:{}%/{}s, exit:{}%/{}s",
+            ipsEnabled, enterUtil, enterTime, exitUtil, exitTime)
+            .c_str());
+
+    std::vector<std::uint8_t> cmd, rsp;
+    cmd.reserve(12);
+    cmd.push_back(uint8_t(CmdType::SET_CONFIG_DATA));
+    cmd.push_back(0x00);               // Data Length (2 bytes)
+    cmd.push_back(0x09);               //
+    cmd.push_back(0x11);               // Config Format: IPS Settings
+    cmd.push_back(0x00);               // Version
+    cmd.push_back(ipsEnabled ? 1 : 0); // IPS Enable
+    cmd.push_back(enterTime >> 8);     // Enter Delay Time
+    cmd.push_back(enterTime & 0xFF);   //
+    cmd.push_back(enterUtil);          // Enter Utilization
+    cmd.push_back(exitTime >> 8);      // Exit Delay Time
+    cmd.push_back(exitTime & 0xFF);    //
+    cmd.push_back(exitUtil);           // Exit Utilization
+    log<level::INFO>(fmt::format("PowerMode::sendIpsData: SET_CFG_DATA[IPS] "
+                                 "command to OCC{} ({} bytes)",
+                                 occInstance, cmd.size())
+                         .c_str());
+    status = occCmd.send(cmd, rsp);
+    if (status == CmdStatus::SUCCESS)
+    {
+        if (rsp.size() == 5)
+        {
+            if (RspStatus::SUCCESS != RspStatus(rsp[2]))
+            {
+                log<level::ERR>(
+                    fmt::format(
+                        "PowerMode::sendIpsData: SET_CFG_DATA[IPS] failed with status 0x{:02X}",
+                        rsp[2])
+                        .c_str());
+                dump_hex(rsp);
+                status = CmdStatus::FAILURE;
+            }
+        }
+        else
+        {
+            log<level::ERR>(
+                "PowerMode::sendIpsData: INVALID SET_CFG_DATA[IPS] response");
+            dump_hex(rsp);
+            status = CmdStatus::FAILURE;
+        }
+    }
+    else
+    {
+        if (status == CmdStatus::OPEN_FAILURE)
+        {
+            // OCC not active yet
+            status = CmdStatus::SUCCESS;
+        }
+        else
+        {
+            log<level::ERR>(
+                "PowerMode::sendIpsData: SET_CFG_DATA[IPS] FAILED!");
+        }
+    }
+
+    return status;
+}
+
+inline void OccPersistData::print()
+{
+    log<level::DEBUG>(
+        fmt::format(
+            "OccPersistData: OEM Mode: 0x{:02X}, OEM Mode Freq: {} (0x{:04X})",
+            oemData.oemMode, oemData.oemModeFreq, oemData.oemModeFreq)
+            .c_str());
+}
+
+// Saves the OEM mode data in the filesystem using cereal.
+void OccPersistData::save()
+{
+    std::filesystem::path opath =
+        std::filesystem::path{OCC_CONTROL_PERSIST_PATH} / oemModeFilename;
+
+    if (!std::filesystem::exists(opath.parent_path()))
+    {
+        std::filesystem::create_directory(opath.parent_path());
+    }
+
+    log<level::DEBUG>(
+        fmt::format("OccPersistData::save: Writing OEM persisted data to {}",
+                    opath.c_str())
+            .c_str());
+    print();
+
+    std::ofstream stream{opath.c_str()};
+    cereal::JSONOutputArchive oarchive{stream};
+
+    oarchive(oemData);
+}
+
+// Loads the OEM mode data in the filesystem using cereal.
+void OccPersistData::load()
+{
+
+    std::filesystem::path ipath =
+        std::filesystem::path{OCC_CONTROL_PERSIST_PATH} / oemModeFilename;
+
+    if (!std::filesystem::exists(ipath))
+    {
+        return;
+    }
+
+    log<level::DEBUG>(
+        fmt::format("OccPersistData::load: Reading OEM persisted data from {}",
+                    ipath.c_str())
+            .c_str());
+    try
+    {
+        std::ifstream stream{ipath.c_str()};
+        cereal::JSONInputArchive iarchive(stream);
+        iarchive(oemData);
+
+        oemSet = true;
+    }
+    catch (const std::exception& e)
+    {
+        auto error = errno;
+        log<level::ERR>(
+            fmt::format("OccPersistData::load: failed to read {}, errno={}",
+                        ipath.c_str(), error)
+                .c_str());
+    }
+
+    print();
+}
+
+void OccPersistData::purge()
+{
+    std::filesystem::path opath =
+        std::filesystem::path{OCC_CONTROL_PERSIST_PATH} / oemModeFilename;
+
+    if (!std::filesystem::exists(opath))
+    {
+        return;
+    }
+
+    print();
+    log<level::DEBUG>("OccPersistData::purge() Removing OEM data");
+
+    oemSet = false;
+    oemData.oemMode = SysPwrMode::NO_CHANGE;
+    oemData.oemModeFreq = 0x0000;
+    remove(opath.c_str());
+}
+
 } // namespace powermode
 
 } // namespace occ