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
diff --git a/phosphor-power-supply/chassis.hpp b/phosphor-power-supply/chassis.hpp
new file mode 100644
index 0000000..527b0ff
--- /dev/null
+++ b/phosphor-power-supply/chassis.hpp
@@ -0,0 +1,189 @@
+#pragma once
+
+#include "power_supply.hpp"
+#include "types.hpp"
+#include "utility.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <sdbusplus/server/manager.hpp>
+#include <sdbusplus/server/object.hpp>
+#include <sdeventplus/event.hpp>
+#include <sdeventplus/utility/timer.hpp>
+#include <xyz/openbmc_project/State/Decorator/PowerSystemInputs/server.hpp>
+
+#include <map>
+#include <string>
+#include <vector>
+
+struct SupportedPsuConfiguration
+{
+    int powerSupplyCount;
+    std::vector<uint64_t> inputVoltage;
+    bool powerConfigFullLoad;
+};
+
+using namespace phosphor::power::psu;
+
+namespace phosphor::power::chassis
+{
+
+constexpr uint32_t invalidObjectPathUniqueId = 9999;
+using PowerSystemInputsInterface = sdbusplus::xyz::openbmc_project::State::
+    Decorator::server::PowerSystemInputs;
+using PowerSystemInputsObject =
+    sdbusplus::server::object_t<PowerSystemInputsInterface>;
+
+// Validation timeout. Allow 30s to detect if new EM interfaces show up in D-Bus
+// before performing the validation.
+// Previously the timer was set to 10 seconds was too short, it results in
+// incorrect errors being logged, but no real consequence of longer timeout.
+constexpr auto validationTimeout = std::chrono::seconds(30);
+
+/**
+ * @class Chassis
+ *
+ * @brief This class will create an object used to manage and monitor a list of
+ * power supply devices attached to the chassis.
+ */
+class Chassis
+{
+  public:
+    Chassis() = delete;
+    ~Chassis() = default;
+    Chassis(const Chassis&) = delete;
+    Chassis& operator=(const Chassis&) = delete;
+    Chassis(Chassis&&) = delete;
+    Chassis& operator=(Chassis&&) = delete;
+
+    /**
+     * @brief Constructor to read configuration from D-Bus.
+     *
+     * @param[in] bus - D-Bus bus object
+     * @param[in] chassisPath - Chassis path
+     */
+    Chassis(sdbusplus::bus_t& bus, const std::string& chassisPath);
+
+    /**
+     * @brief Get the status of Power on.
+     */
+    bool isPowerOn()
+    {
+        return powerOn;
+    }
+
+  private:
+    /**
+     * @brief The D-Bus object
+     */
+    sdbusplus::bus_t& bus;
+
+    /**
+     * @brief The timer that performs power supply validation as the entity
+     * manager interfaces show up in d-bus.
+     */
+    std::unique_ptr<
+        sdeventplus::utility::Timer<sdeventplus::ClockId::Monotonic>>
+        validationTimer;
+
+    /** @brief True if the power is on. */
+    bool powerOn = false;
+
+    /** @brief Used to subscribe to D-Bus power supply presence changes */
+    std::vector<std::unique_ptr<sdbusplus::bus::match_t>> presenceMatches;
+
+    /**
+     * @brief Flag to indicate if the validateConfig() function should be run.
+     * Set to false once the configuration has been validated to avoid
+     * running multiple times due to interfaces added signal. Set to
+     * true during power off to trigger the validation on power on.
+     */
+    bool runValidateConfig = true;
+
+    /**
+     * @brief Map of supported PSU configurations that include the model name
+     * and their properties.
+     */
+    std::map<std::string, SupportedPsuConfiguration> supportedConfigs;
+
+    /**
+     * @brief The vector for power supplies.
+     */
+    std::vector<std::unique_ptr<PowerSupply>> psus;
+
+    /**
+     * @brief The device driver name for all power supplies.
+     */
+    std::string driverName;
+
+    /**
+     * @brief Chassis D-Bus object path
+     */
+    std::string chassisPath;
+
+    /**
+     * @brief The Chassis path unique ID
+     */
+    uint32_t chassisPathUniqueId = invalidObjectPathUniqueId;
+
+    /**
+     * @brief Get PSU properties from D-Bus, use that to build a power supply
+     * object.
+     *
+     * @param[in] properties - A map of property names and values
+     */
+    void getPSUProperties(util::DbusPropertyMap& properties);
+
+    /**
+     * @brief Get PSU configuration from D-Bus
+     */
+    void getPSUConfiguration();
+
+    /**
+     * @brief Initialize the chassis's supported configuration from the
+     * Supported Configuration D-Bus object provided by the Entity
+     * Manager.
+     */
+    void getSupportedConfiguration();
+
+    /**
+     * @brief Callback for inventory property changes
+     *
+     * Process change of the Power Supply presence.
+     *
+     * @param[in]  msg - Data associated with the Present change signal
+     **/
+    void psuPresenceChanged(sdbusplus::message_t& msg);
+
+    /**
+     * @brief Helper function to populate the PSU supported configuration
+     *
+     * @param[in] properties - A map of property names and values
+     */
+    void populateSupportedConfiguration(
+        const util::DbusPropertyMap& properties);
+
+    /**
+     * @brief Build the device driver name for the power supply.
+     *
+     * @param[in] i2cbus - i2c bus
+     * @param[in] i2caddr - i2c bus address
+     */
+    void buildDriverName(uint64_t i2cbus, uint64_t i2caddr);
+
+    /**
+     * @brief Find PSU with device driver name, then populate the device
+     * driver name to all PSUs (including missing PSUs).
+     */
+    void populateDriverName();
+
+    /**
+     * @brief Get chassis path unique ID.
+     *
+     * @return uint32_t - Chassis path unique ID.
+     */
+    uint32_t getChassisPathUniqueId();
+};
+
+} // namespace phosphor::power::chassis
diff --git a/phosphor-power-supply/docs/MultiChassis.md b/phosphor-power-supply/docs/MultiChassis.md
index 106e614..cf3bea4 100644
--- a/phosphor-power-supply/docs/MultiChassis.md
+++ b/phosphor-power-supply/docs/MultiChassis.md
@@ -12,6 +12,7 @@
 
 ```text
 /xyz/openbmc_project/inventory/system/chassis/motherboard/powersupply0
+
 ```
 
 ## Goal: Support Multi-Chassis PSUs
@@ -46,7 +47,7 @@
   for D-BUS events to track chassis PGOOD Status.
 - Retrieve the PSU configurations from **entity manager**
 - Find all PSUs associated with chassis from **inventory manager** (using
-  **[getSubTree](https://github.com/openbmc/phosphor-power/blob/master/utility.hpp)**)
+  [`getSubTree`](https://github.com/openbmc/phosphor-power/blob/master/utility.hpp))
   and add them to vector in the chassis object. **getSubTree** is a function
   interacts with the D-Bus object mapper to retrieve all objects under a
   specified path that implement interface to certain depth.