psutils: Add --get-model option

Add a --get-model command line option to the psutils tool.  This option
obtains the PSU model using information in sysfs.

Supports both methods of obtaining information about a PSU:
* psu.json file
* D-Bus

Tested:
* Verified --get-version still works
  * With psu.json file
  * Without psu.json file
* Verified new --get-model property works
  * With psu.json file
  * Without psu.json file
  * Tested all error paths
* See the following gist for the complete test plan:
  https://gist.github.com/smccarney/859ffaaa94ce12992af1b24e6c899962

Change-Id: If1be01f4b70ad9d80ce01402c57730990fd2c2ea
Signed-off-by: Shawn McCarney <shawnmm@us.ibm.com>
diff --git a/tools/power-utils/main.cpp b/tools/power-utils/main.cpp
index 8fae3a1..8ca7c04 100644
--- a/tools/power-utils/main.cpp
+++ b/tools/power-utils/main.cpp
@@ -15,6 +15,7 @@
  */
 #include "config.h"
 
+#include "model.hpp"
 #include "updater.hpp"
 #include "utils.hpp"
 #include "version.hpp"
@@ -30,15 +31,17 @@
 
 int main(int argc, char** argv)
 {
-    std::string psuPath;
+    std::string psuPathVersion, psuPathModel;
     std::vector<std::string> versions;
     bool rawOutput = false;
     std::vector<std::string> updateArguments;
 
     CLI::App app{"PSU utils app for OpenBMC"};
     auto action = app.add_option_group("Action");
-    action->add_option("-g,--get-version", psuPath,
+    action->add_option("-g,--get-version", psuPathVersion,
                        "Get PSU version from inventory path");
+    action->add_option("-m,--get-model", psuPathModel,
+                       "Get PSU model from inventory path");
     action->add_option("-c,--compare", versions,
                        "Compare and get the latest version");
     action
@@ -54,17 +57,21 @@
 
     bool useJsonFile = utils::checkFileExists(PSU_JSON_PATH);
     auto bus = sdbusplus::bus::new_default();
-    if (!psuPath.empty())
+    if (!psuPathVersion.empty())
     {
         if (!useJsonFile)
         {
-            ret = version::getVersion(bus, psuPath);
+            ret = version::getVersion(bus, psuPathVersion);
         }
         else
         {
-            ret = version::getVersion(psuPath);
+            ret = version::getVersion(psuPathVersion);
         }
     }
+    if (!psuPathModel.empty())
+    {
+        ret = model::getModel(bus, psuPathModel);
+    }
     if (!versions.empty())
     {
         ret = version::getLatest(versions);
diff --git a/tools/power-utils/meson.build b/tools/power-utils/meson.build
index 41149da..542eadb 100644
--- a/tools/power-utils/meson.build
+++ b/tools/power-utils/meson.build
@@ -3,6 +3,7 @@
     'version.cpp',
     'updater.cpp',
     'utils.cpp',
+    'model.cpp',
     'main.cpp',
     dependencies: [
         cli11_dep,
diff --git a/tools/power-utils/model.cpp b/tools/power-utils/model.cpp
new file mode 100644
index 0000000..6677eac
--- /dev/null
+++ b/tools/power-utils/model.cpp
@@ -0,0 +1,162 @@
+/**
+ * Copyright © 2024 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "config.h"
+
+#include "model.hpp"
+
+#include "pmbus.hpp"
+#include "utility.hpp"
+#include "utils.hpp"
+
+#include <nlohmann/json.hpp>
+#include <phosphor-logging/log.hpp>
+
+#include <exception>
+#include <format>
+#include <fstream>
+#include <stdexcept>
+
+using json = nlohmann::json;
+
+using namespace utils;
+using namespace phosphor::logging;
+using namespace phosphor::power::util;
+
+namespace model
+{
+
+namespace internal
+{
+
+/**
+ * @brief Get the PSU model from sysfs.
+ *
+ * Obtain PSU information from the PSU JSON file.
+ *
+ * Throws an exception if an error occurs.
+ *
+ * @param[in] psuInventoryPath - PSU D-Bus inventory path
+ *
+ * @return PSU model
+ */
+std::string getModelJson(const std::string& psuInventoryPath)
+{
+    // Parse PSU JSON file
+    std::ifstream file{PSU_JSON_PATH};
+    json data = json::parse(file);
+
+    // Get PSU device path from JSON
+    auto it = data.find("psuDevices");
+    if (it == data.end())
+    {
+        throw std::runtime_error{"Unable to find psuDevices"};
+    }
+    auto device = it->find(psuInventoryPath);
+    if (device == it->end())
+    {
+        throw std::runtime_error{std::format(
+            "Unable to find device path for PSU {}", psuInventoryPath)};
+    }
+    std::string devicePath = *device;
+    if (devicePath.empty())
+    {
+        throw std::runtime_error{
+            std::format("Empty device path for PSU {}", psuInventoryPath)};
+    }
+
+    // Get sysfs filename from JSON for Model information
+    it = data.find("fruConfigs");
+    if (it == data.end())
+    {
+        throw std::runtime_error{"Unable to find fruConfigs"};
+    }
+    std::string fileName;
+    for (const auto& fru : *it)
+    {
+        if (fru.contains("propertyName") && (fru["propertyName"] == "Model") &&
+            fru.contains("fileName"))
+        {
+            fileName = fru["fileName"];
+            break;
+        }
+    }
+    if (fileName.empty())
+    {
+        throw std::runtime_error{"Unable to find file name for Model"};
+    }
+
+    // Get PMBus access type from JSON
+    phosphor::pmbus::Type type = getPMBusAccessType(data);
+
+    // Read model from sysfs file
+    phosphor::pmbus::PMBus pmbus(devicePath);
+    std::string model = pmbus.readString(fileName, type);
+    return model;
+}
+
+/**
+ * @brief Get the PSU model from sysfs.
+ *
+ * Obtain PSU information from D-Bus.
+ *
+ * Throws an exception if an error occurs.
+ *
+ * @param[in] bus - D-Bus connection
+ * @param[in] psuInventoryPath - PSU D-Bus inventory path
+ *
+ * @return PSU model
+ */
+std::string getModelDbus(sdbusplus::bus_t& bus,
+                         const std::string& psuInventoryPath)
+{
+    // Get PSU I2C bus/address and create PMBus interface
+    const auto [i2cBus, i2cAddr] = getPsuI2c(bus, psuInventoryPath);
+    auto pmbus = getPmbusIntf(i2cBus, i2cAddr);
+
+    // Read model from sysfs file
+    std::string fileName = "ccin";
+    auto type = phosphor::pmbus::Type::HwmonDeviceDebug;
+    std::string model = pmbus->readString(fileName, type);
+    return model;
+}
+
+} // namespace internal
+
+std::string getModel(sdbusplus::bus_t& bus, const std::string& psuInventoryPath)
+{
+    std::string model;
+    try
+    {
+        // If PSU JSON file exists
+        if (checkFileExists(PSU_JSON_PATH))
+        {
+            // Obtain PSU information from JSON file
+            model = internal::getModelJson(psuInventoryPath);
+        }
+        else
+        {
+            // Obtain PSU information from D-Bus
+            model = internal::getModelDbus(bus, psuInventoryPath);
+        }
+    }
+    catch (const std::exception& e)
+    {
+        log<level::ERR>(std::format("Error: {}", e.what()).c_str());
+    }
+    return model;
+}
+
+} // namespace model
diff --git a/tools/power-utils/model.hpp b/tools/power-utils/model.hpp
new file mode 100644
index 0000000..413e28e
--- /dev/null
+++ b/tools/power-utils/model.hpp
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2024 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <sdbusplus/bus.hpp>
+
+#include <string>
+
+namespace model
+{
+
+/**
+ * @brief Get the PSU model from sysfs.
+ *
+ * @param[in] bus - D-Bus connection
+ * @param[in] psuInventoryPath - PSU D-Bus inventory path
+ *
+ * @return PSU model, or "" if model could not be found
+ */
+std::string getModel(sdbusplus::bus_t& bus,
+                     const std::string& psuInventoryPath);
+
+} // namespace model