chassis-psu: Add Chassis class for PSU management

This commit introduces the `Chassis` class within the
`phosphor::power::chassis` namespace. This class is responsible for
managing and monitoring power supply units (PSUs) associated with a
chassis.
Key functionalities include:
- Discovering individual PSUs and retrieving their properties and
  configuration from D-Bus.
- Monitoring PSU presence changes and system power state.
- Retrieving overall system power properties.
- Implementing a validation timer for PSU configurations.
- Use an `sdeventplus` timer to delay configuration validation
  until all EM interfaces are available.
- Maintain internal state flags (`powerOn`, `powerFaultOccurring`,
  `brownoutLogged`,`runValidateConfig`) and data structures:
  - `supportedConfigs` map of model names to `sys_properties`.
  - Vector of `PowerSupply` instances.
- Building and populating PSU driver names.

Tested:
  Simulated a multi-chassis setup, with each chassis containing a
  couple of PSUs. Executed a simulated application to invoke the Chassis
  object.
  - Verified each Chassis object listed only PSUs belong to the chassis,
    by verifying the PSU path has the correct chassis ID.

Note: Removed implementation of getSupportedConfiguration until design
      issue resolved.

Change-Id: Ic4bda5ab0bd6173a67284ed27a0bc883ed04f92f
Signed-off-by: Faisal Awada <faisal@us.ibm.com>
diff --git a/phosphor-power-supply/chassis.cpp b/phosphor-power-supply/chassis.cpp
new file mode 100644
index 0000000..505a955
--- /dev/null
+++ b/phosphor-power-supply/chassis.cpp
@@ -0,0 +1,319 @@
+#include "config.h"
+
+#include "chassis.hpp"
+
+#include <filesystem>
+#include <iostream>
+
+using namespace phosphor::logging;
+using namespace phosphor::power::util;
+namespace phosphor::power::chassis
+{
+
+constexpr auto IBMCFFPSInterface =
+    "xyz.openbmc_project.Configuration.IBMCFFPSConnector";
+constexpr auto chassisIdProp = "SlotNumber";
+constexpr auto i2cBusProp = "I2CBus";
+constexpr auto i2cAddressProp = "I2CAddress";
+constexpr auto psuNameProp = "Name";
+constexpr auto presLineName = "NamedPresenceGpio";
+constexpr auto supportedConfIntf =
+    "xyz.openbmc_project.Configuration.SupportedConfiguration";
+const auto deviceDirPath = "/sys/bus/i2c/devices/";
+const auto driverDirName = "/driver";
+
+const auto entityMgrService = "xyz.openbmc_project.EntityManager";
+const auto decoratorChassisId = "xyz.openbmc_project.Inventory.Decorator.Slot";
+
+Chassis::Chassis(sdbusplus::bus_t& bus, const std::string& chassisPath) :
+    bus(bus), chassisPath(chassisPath),
+    chassisPathUniqueId(getChassisPathUniqueId())
+{
+    getPSUConfiguration();
+}
+
+void Chassis::getPSUConfiguration()
+{
+    namespace fs = std::filesystem;
+    auto depth = 0;
+
+    try
+    {
+        if (chassisPathUniqueId == invalidObjectPathUniqueId)
+        {
+            lg2::error("Chassis does not have chassis ID: {CHASSISPATH}",
+                       "CHASSISPATH", chassisPath);
+            return;
+        }
+        auto connectorsSubTree = getSubTree(bus, "/", IBMCFFPSInterface, depth);
+        for (const auto& [path, services] : connectorsSubTree)
+        {
+            uint64_t id;
+            fs::path fspath(path);
+            getProperty(decoratorChassisId, chassisIdProp, fspath.parent_path(),
+                        entityMgrService, bus, id);
+            if (id == static_cast<uint64_t>(chassisPathUniqueId))
+
+            {
+                // For each object in the array of objects, I want
+                // to get properties from the service, path, and
+                // interface.
+                auto properties = getAllProperties(bus, path, IBMCFFPSInterface,
+                                                   entityMgrService);
+                getPSUProperties(properties);
+            }
+        }
+    }
+    catch (const sdbusplus::exception_t& e)
+    {
+        lg2::error("Failed while getting configuration - exception: {ERROR}",
+                   "ERROR", e);
+    }
+
+    if (psus.empty())
+    {
+        // Interface or properties not found. Let the Interfaces Added callback
+        // process the information once the interfaces are added to D-Bus.
+        lg2::info("No power supplies to monitor");
+    }
+}
+
+void Chassis::getPSUProperties(util::DbusPropertyMap& properties)
+{
+    std::string basePSUInvPath = chassisPath + "/motherboard/powersupply";
+
+    // From passed in properties, I want to get: I2CBus, I2CAddress,
+    // and Name. Create a power supply object, using Name to build the inventory
+    // path.
+
+    uint64_t* i2cbus = nullptr;
+    uint64_t* i2caddr = nullptr;
+    std::string* psuname = nullptr;
+    std::string* preslineptr = nullptr;
+
+    for (const auto& property : properties)
+    {
+        try
+        {
+            if (property.first == i2cBusProp)
+            {
+                i2cbus = std::get_if<uint64_t>(&properties[i2cBusProp]);
+            }
+            else if (property.first == i2cAddressProp)
+            {
+                i2caddr = std::get_if<uint64_t>(&properties[i2cAddressProp]);
+            }
+            else if (property.first == psuNameProp)
+            {
+                psuname = std::get_if<std::string>(&properties[psuNameProp]);
+            }
+            else if (property.first == presLineName)
+            {
+                preslineptr =
+                    std::get_if<std::string>(&properties[presLineName]);
+            }
+        }
+        catch (const std::exception& e)
+        {}
+    }
+
+    if (i2cbus && i2caddr && psuname && !psuname->empty())
+    {
+        std::string invpath = basePSUInvPath;
+        invpath.push_back(psuname->back());
+        std::string presline = "";
+
+        lg2::debug("Inventory Path: {INVPATH}", "INVPATH", invpath);
+
+        if (nullptr != preslineptr)
+        {
+            presline = *preslineptr;
+        }
+
+        auto invMatch =
+            std::find_if(psus.begin(), psus.end(), [&invpath](auto& psu) {
+                return psu->getInventoryPath() == invpath;
+            });
+        if (invMatch != psus.end())
+        {
+            // This power supply has the same inventory path as the one with
+            // information just added to D-Bus.
+            // Changes to GPIO line name unlikely, so skip checking.
+            // Changes to the I2C bus and address unlikely, as that would
+            // require corresponding device tree updates.
+            // Return out to avoid duplicate object creation.
+            return;
+        }
+
+        buildDriverName(*i2cbus, *i2caddr);
+        lg2::debug(
+            "make PowerSupply bus: {I2CBUS} addr: {I2CADDR} presline: {PRESLINE}",
+            "I2CBUS", *i2cbus, "I2CADDR", *i2caddr, "PRESLINE", presline);
+        auto psu = std::make_unique<PowerSupply>(
+            bus, invpath, *i2cbus, *i2caddr, driverName, presline,
+            std::bind(&Chassis::isPowerOn, this), chassisPath);
+        psus.emplace_back(std::move(psu));
+
+        // Subscribe to power supply presence changes
+        auto presenceMatch = std::make_unique<sdbusplus::bus::match_t>(
+            bus,
+            sdbusplus::bus::match::rules::propertiesChanged(invpath,
+                                                            INVENTORY_IFACE),
+            [this](auto& msg) { this->psuPresenceChanged(msg); });
+        presenceMatches.emplace_back(std::move(presenceMatch));
+    }
+    if (psus.empty())
+    {
+        lg2::info("No power supplies to monitor");
+    }
+    else
+    {
+        populateDriverName();
+    }
+}
+
+void Chassis::getSupportedConfiguration()
+{
+    // TBD
+}
+
+void Chassis::populateSupportedConfiguration(
+    const util::DbusPropertyMap& properties)
+{
+    try
+    {
+        auto propIt = properties.find("SupportedType");
+        if (propIt == properties.end())
+        {
+            return;
+        }
+        const std::string* type = std::get_if<std::string>(&(propIt->second));
+        if ((type == nullptr) || (*type != "PowerSupply"))
+        {
+            return;
+        }
+
+        propIt = properties.find("SupportedModel");
+        if (propIt == properties.end())
+        {
+            return;
+        }
+        const std::string* model = std::get_if<std::string>(&(propIt->second));
+        if (model == nullptr)
+        {
+            return;
+        }
+
+        SupportedPsuConfiguration supportedPsuConfig;
+        propIt = properties.find("RedundantCount");
+        if (propIt != properties.end())
+        {
+            const uint64_t* count = std::get_if<uint64_t>(&(propIt->second));
+            if (count != nullptr)
+            {
+                supportedPsuConfig.powerSupplyCount = *count;
+            }
+        }
+        propIt = properties.find("InputVoltage");
+        if (propIt != properties.end())
+        {
+            const std::vector<uint64_t>* voltage =
+                std::get_if<std::vector<uint64_t>>(&(propIt->second));
+            if (voltage != nullptr)
+            {
+                supportedPsuConfig.inputVoltage = *voltage;
+            }
+        }
+
+        // The PowerConfigFullLoad is an optional property, default it to false
+        // since that's the default value of the power-config-full-load GPIO.
+        supportedPsuConfig.powerConfigFullLoad = false;
+        propIt = properties.find("PowerConfigFullLoad");
+        if (propIt != properties.end())
+        {
+            const bool* fullLoad = std::get_if<bool>(&(propIt->second));
+            if (fullLoad != nullptr)
+            {
+                supportedPsuConfig.powerConfigFullLoad = *fullLoad;
+            }
+        }
+
+        supportedConfigs.emplace(*model, supportedPsuConfig);
+    }
+    catch (const std::exception& e)
+    {
+        lg2::info("populateSupportedConfiguration error {ERR}", "ERR", e);
+    }
+}
+
+void Chassis::psuPresenceChanged(sdbusplus::message_t& msg)
+{
+    std::string msgSensor;
+    std::map<std::string, std::variant<uint32_t, bool>> msgData;
+    msg.read(msgSensor, msgData);
+
+    // Check if it was the Present property that changed.
+    auto valPropMap = msgData.find(PRESENT_PROP);
+    if (valPropMap != msgData.end())
+    {
+        if (std::get<bool>(valPropMap->second))
+        {
+            // A PSU became present, force the PSU validation to run.
+            runValidateConfig = true;
+            validationTimer->restartOnce(validationTimeout);
+        }
+    }
+}
+
+void Chassis::buildDriverName(uint64_t i2cbus, uint64_t i2caddr)
+{
+    namespace fs = std::filesystem;
+    std::stringstream ss;
+    ss << std::hex << std::setw(4) << std::setfill('0') << i2caddr;
+    std::string symLinkPath =
+        deviceDirPath + std::to_string(i2cbus) + "-" + ss.str() + driverDirName;
+    try
+    {
+        fs::path linkStrPath = fs::read_symlink(symLinkPath);
+        driverName = linkStrPath.filename();
+    }
+    catch (const std::exception& e)
+    {
+        lg2::error(
+            "Failed to find device driver {SYM_LINK_PATH}, error {ERROR_STR}",
+            "SYM_LINK_PATH", symLinkPath, "ERROR_STR", e);
+    }
+}
+
+void Chassis::populateDriverName()
+{
+    std::string driverName;
+    // Search in PSUs for driver name
+    std::for_each(psus.begin(), psus.end(), [&driverName](auto& psu) {
+        if (!psu->getDriverName().empty())
+        {
+            driverName = psu->getDriverName();
+        }
+    });
+    // Assign driver name to all PSUs
+    std::for_each(psus.begin(), psus.end(),
+                  [&driverName](auto& psu) { psu->setDriverName(driverName); });
+}
+
+uint32_t Chassis::getChassisPathUniqueId()
+{
+    uint32_t chassisId = invalidObjectPathUniqueId;
+    try
+    {
+        getProperty(decoratorChassisId, chassisIdProp, chassisPath,
+                    INVENTORY_MGR_IFACE, bus, chassisId);
+    }
+    catch (const sdbusplus::exception_t& e)
+    {
+        lg2::error("Failed to find chassis ID  - exception: {ERROR}", "ERROR",
+                   e);
+    }
+    return chassisId;
+}
+
+} // namespace phosphor::power::chassis