pseq: Create PMBusDriverDevice class

Create the PMBusDriverDevice class in the phosphor-power-sequencer
application.

This class is used for power sequencer devices that are bound to a PMBus
device driver.  It obtains PMBus information from sysfs/hwmon files
created by the driver.

Tested:
* Performed all of the following tests on Rainier and Everest systems
* Correct sysfs/hwmon files and directories found
* GPIO values obtained successfully
* STATUS_WORD values obtained successfully
* STATUS_VOUT values obtained successfully
* READ_VOUT values obtained successfully
* VOUT_UV_FAULT_LIMIT values obtained successfully
* Mapping built from PMBus PAGE numbers to hwmon file numbers
* Verified error paths and exceptions thrown

Change-Id: I2efd3146fa08d3584857c94c2bbbf691b1e2ad7d
Signed-off-by: Shawn McCarney <shawnmm@us.ibm.com>
diff --git a/phosphor-power-sequencer/src/meson.build b/phosphor-power-sequencer/src/meson.build
index 2d14caf..ee6057e 100644
--- a/phosphor-power-sequencer/src/meson.build
+++ b/phosphor-power-sequencer/src/meson.build
@@ -6,6 +6,7 @@
 phosphor_power_sequencer_library = static_library(
     'phosphor-power-sequencer',
     'config_file_parser.cpp',
+    'pmbus_driver_device.cpp',
     'rail.cpp',
     'services.cpp',
     'standard_device.cpp',
diff --git a/phosphor-power-sequencer/src/pmbus_driver_device.cpp b/phosphor-power-sequencer/src/pmbus_driver_device.cpp
new file mode 100644
index 0000000..e8c0e04
--- /dev/null
+++ b/phosphor-power-sequencer/src/pmbus_driver_device.cpp
@@ -0,0 +1,253 @@
+/**
+ * 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 "pmbus_driver_device.hpp"
+
+#include <ctype.h> // for tolower()
+
+#include <algorithm>
+#include <exception>
+#include <filesystem>
+#include <format>
+#include <regex>
+#include <stdexcept>
+
+namespace phosphor::power::sequencer
+{
+
+using namespace pmbus;
+namespace fs = std::filesystem;
+
+std::vector<int> PMBusDriverDevice::getGPIOValues(Services& services)
+{
+    // Get lower case version of device name to use as chip label
+    std::string label{name};
+    std::transform(label.begin(), label.end(), label.begin(), ::tolower);
+
+    // Read the GPIO values by specifying the chip label
+    std::vector<int> values;
+    try
+    {
+        values = services.getGPIOValues(label);
+    }
+    catch (const std::exception& e)
+    {
+        throw std::runtime_error{std::format(
+            "Unable to read GPIO values from device {} using label {}: {}",
+            name, label, e.what())};
+    }
+    return values;
+}
+
+uint16_t PMBusDriverDevice::getStatusWord(uint8_t page)
+{
+    uint16_t value{0};
+    try
+    {
+        std::string fileName = std::format("status{:d}", page);
+        value = pmbusInterface->read(fileName, Type::Debug);
+    }
+    catch (const std::exception& e)
+    {
+        throw std::runtime_error{std::format(
+            "Unable to read STATUS_WORD for PAGE {:d} of device {}: {}", page,
+            name, e.what())};
+    }
+    return value;
+}
+
+uint8_t PMBusDriverDevice::getStatusVout(uint8_t page)
+{
+    uint8_t value{0};
+    try
+    {
+        std::string fileName = std::format("status{:d}_vout", page);
+        value = pmbusInterface->read(fileName, Type::Debug);
+    }
+    catch (const std::exception& e)
+    {
+        throw std::runtime_error{std::format(
+            "Unable to read STATUS_VOUT for PAGE {:d} of device {}: {}", page,
+            name, e.what())};
+    }
+    return value;
+}
+
+double PMBusDriverDevice::getReadVout(uint8_t page)
+{
+    double volts{0.0};
+    try
+    {
+        unsigned int fileNumber = getFileNumber(page);
+        std::string fileName = std::format("in{}_input", fileNumber);
+        std::string millivoltsStr = pmbusInterface->readString(fileName,
+                                                               Type::Hwmon);
+        unsigned long millivolts = std::stoul(millivoltsStr);
+        volts = millivolts / 1000.0;
+    }
+    catch (const std::exception& e)
+    {
+        throw std::runtime_error{std::format(
+            "Unable to read READ_VOUT for PAGE {:d} of device {}: {}", page,
+            name, e.what())};
+    }
+    return volts;
+}
+
+double PMBusDriverDevice::getVoutUVFaultLimit(uint8_t page)
+{
+    double volts{0.0};
+    try
+    {
+        unsigned int fileNumber = getFileNumber(page);
+        std::string fileName = std::format("in{}_lcrit", fileNumber);
+        std::string millivoltsStr = pmbusInterface->readString(fileName,
+                                                               Type::Hwmon);
+        unsigned long millivolts = std::stoul(millivoltsStr);
+        volts = millivolts / 1000.0;
+    }
+    catch (const std::exception& e)
+    {
+        throw std::runtime_error{std::format(
+            "Unable to read VOUT_UV_FAULT_LIMIT for PAGE {:d} of device {}: {}",
+            page, name, e.what())};
+    }
+    return volts;
+}
+
+unsigned int PMBusDriverDevice::getFileNumber(uint8_t page)
+{
+    if (pageToFileNumber.empty())
+    {
+        buildPageToFileNumberMap();
+    }
+
+    auto it = pageToFileNumber.find(page);
+    if (it == pageToFileNumber.end())
+    {
+        throw std::runtime_error{std::format(
+            "Unable to find hwmon file number for PAGE {:d} of device {}", page,
+            name)};
+    }
+
+    return it->second;
+}
+
+void PMBusDriverDevice::buildPageToFileNumberMap()
+{
+    // Clear any existing mappings
+    pageToFileNumber.clear();
+
+    // Build mappings using voltage label files in hwmon directory
+    try
+    {
+        fs::path hwmonDir = pmbusInterface->getPath(Type::Hwmon);
+        if (fs::is_directory(hwmonDir))
+        {
+            // Loop through all files in hwmon directory
+            std::string fileName;
+            unsigned int fileNumber;
+            std::optional<uint8_t> page;
+            for (const auto& f : fs::directory_iterator{hwmonDir})
+            {
+                // If this is a voltage label file
+                fileName = f.path().filename().string();
+                if (isLabelFile(fileName, fileNumber))
+                {
+                    // Read PMBus PAGE number from label file contents
+                    page = readPageFromLabelFile(fileName);
+                    if (page)
+                    {
+                        // Add mapping from PAGE number to file number
+                        pageToFileNumber.emplace(*page, fileNumber);
+                    }
+                }
+            }
+        }
+    }
+    catch (const std::exception& e)
+    {
+        throw std::runtime_error{
+            std::format("Unable to map PMBus PAGE numbers to hwmon file "
+                        "numbers for device {}: {}",
+                        name, e.what())};
+    }
+}
+
+bool PMBusDriverDevice::isLabelFile(const std::string& fileName,
+                                    unsigned int& fileNumber)
+{
+    bool isLabel{false};
+    try
+    {
+        // Check if file name has expected pattern for voltage label file
+        std::regex regex{"in(\\d+)_label"};
+        std::smatch results;
+        if (std::regex_match(fileName, results, regex))
+        {
+            // Verify 2 match results: entire match and one sub-match
+            if (results.size() == 2)
+            {
+                // Get sub-match that contains the file number
+                std::string fileNumberStr = results.str(1);
+                fileNumber = std::stoul(fileNumberStr);
+                isLabel = true;
+            }
+        }
+    }
+    catch (...)
+    {
+        // Ignore error.  If this file is needed for pgood fault detection, an
+        // error will occur later when the necessary mapping is missing.  Avoid
+        // logging unnecessary errors for files that may not be required.
+    }
+    return isLabel;
+}
+
+std::optional<uint8_t>
+    PMBusDriverDevice::readPageFromLabelFile(const std::string& fileName)
+{
+    std::optional<uint8_t> page;
+    try
+    {
+        // Read voltage label file contents
+        std::string contents = pmbusInterface->readString(fileName,
+                                                          Type::Hwmon);
+
+        // Check if file contents match the expected pattern
+        std::regex regex{"vout(\\d+)"};
+        std::smatch results;
+        if (std::regex_match(contents, results, regex))
+        {
+            // Verify 2 match results: entire match and one sub-match
+            if (results.size() == 2)
+            {
+                // Get sub-match that contains the page number + 1
+                std::string pageStr = results.str(1);
+                page = std::stoul(pageStr) - 1;
+            }
+        }
+    }
+    catch (...)
+    {
+        // Ignore error.  If this file is needed for pgood fault detection, an
+        // error will occur later when the necessary mapping is missing.  Avoid
+        // logging unnecessary errors for files that may not be required.
+    }
+    return page;
+}
+
+} // namespace phosphor::power::sequencer
diff --git a/phosphor-power-sequencer/src/pmbus_driver_device.hpp b/phosphor-power-sequencer/src/pmbus_driver_device.hpp
new file mode 100644
index 0000000..e7e85ac
--- /dev/null
+++ b/phosphor-power-sequencer/src/pmbus_driver_device.hpp
@@ -0,0 +1,258 @@
+/**
+ * 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 "pmbus.hpp"
+#include "rail.hpp"
+#include "services.hpp"
+#include "standard_device.hpp"
+
+#include <stddef.h> // for size_t
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace phosphor::power::sequencer
+{
+
+/**
+ * @class PMBusDriverDevice
+ *
+ * StandardDevice sub-class for power sequencer devices that are bound to a
+ * PMBus device driver.
+ */
+class PMBusDriverDevice : public StandardDevice
+{
+  public:
+    // Specify which compiler-generated methods we want
+    PMBusDriverDevice() = delete;
+    PMBusDriverDevice(const PMBusDriverDevice&) = delete;
+    PMBusDriverDevice(PMBusDriverDevice&&) = delete;
+    PMBusDriverDevice& operator=(const PMBusDriverDevice&) = delete;
+    PMBusDriverDevice& operator=(PMBusDriverDevice&&) = delete;
+    virtual ~PMBusDriverDevice() = default;
+
+    /**
+     * Constructor.
+     *
+     * @param name Device name
+     * @param rails Voltage rails that are enabled and monitored by this device
+     * @param services System services like hardware presence and the journal
+     * @param bus I2C bus for the device
+     * @param address I2C address for the device
+     * @param driverName Device driver name
+     * @param instance Chip instance number
+     */
+    explicit PMBusDriverDevice(const std::string& name,
+                               std::vector<std::unique_ptr<Rail>> rails,
+                               Services& services, uint8_t bus,
+                               uint16_t address,
+                               const std::string& driverName = "",
+                               size_t instance = 0) :
+        StandardDevice(name, std::move(rails)),
+        bus{bus}, address{address}, driverName{driverName}, instance{instance}
+    {
+        pmbusInterface = services.createPMBus(bus, address, driverName,
+                                              instance);
+    }
+
+    /**
+     * Returns the I2C bus for the device.
+     *
+     * @return I2C bus
+     */
+    uint8_t getBus() const
+    {
+        return bus;
+    }
+
+    /**
+     * Returns the I2C address for the device.
+     *
+     * @return I2C address
+     */
+    uint16_t getAddress() const
+    {
+        return address;
+    }
+
+    /**
+     * Returns the device driver name.
+     *
+     * @return driver name
+     */
+    const std::string& getDriverName() const
+    {
+        return driverName;
+    }
+
+    /**
+     * Returns the chip instance number.
+     *
+     * @return chip instance
+     */
+    size_t getInstance() const
+    {
+        return instance;
+    }
+
+    /**
+     * Returns interface to the PMBus information that is provided by the device
+     * driver in sysfs.
+     *
+     * @return PMBus interface object
+     */
+    pmbus::PMBusBase& getPMBusInterface()
+    {
+        return *pmbusInterface;
+    }
+
+    /** @copydoc PowerSequencerDevice::getGPIOValues() */
+    virtual std::vector<int> getGPIOValues(Services& services) override;
+
+    /** @copydoc PowerSequencerDevice::getStatusWord() */
+    virtual uint16_t getStatusWord(uint8_t page) override;
+
+    /** @copydoc PowerSequencerDevice::getStatusVout() */
+    virtual uint8_t getStatusVout(uint8_t page) override;
+
+    /** @copydoc PowerSequencerDevice::getReadVout() */
+    virtual double getReadVout(uint8_t page) override;
+
+    /** @copydoc PowerSequencerDevice::getVoutUVFaultLimit() */
+    virtual double getVoutUVFaultLimit(uint8_t page) override;
+
+    /**
+     * Returns map from PMBus PAGE numbers to sysfs hwmon file numbers.
+     *
+     * Throws an exception if an error occurs trying to build the map.
+     *
+     * @return page to file number map
+     */
+    const std::map<uint8_t, unsigned int>& getPageToFileNumberMap()
+    {
+        if (pageToFileNumber.empty())
+        {
+            buildPageToFileNumberMap();
+        }
+        return pageToFileNumber;
+    }
+
+    /**
+     * Returns the hwmon file number that corresponds to the specified PMBus
+     * PAGE number.
+     *
+     * Throws an exception if a file number was not found for the specified PAGE
+     * number.
+     *
+     * @param page PMBus PAGE number
+     * @return hwmon file number
+     */
+    unsigned int getFileNumber(uint8_t page);
+
+  protected:
+    /** @copydoc StandardDevice::prepareForPgoodFaultDetection() */
+    virtual void prepareForPgoodFaultDetection(Services& services) override
+    {
+        // Rebuild PMBus PAGE to hwmon file number map
+        buildPageToFileNumberMap();
+
+        // Call parent class method to do any actions defined there
+        StandardDevice::prepareForPgoodFaultDetection(services);
+    }
+
+    /**
+     * Build mapping from PMBus PAGE numbers to the hwmon file numbers in
+     * sysfs.
+     *
+     * hwmon file names have the format:
+     *   <type><number>_<item>
+     *
+     * The <number> is not the PMBus PAGE number.  The PMBus PAGE is determined
+     * by reading the contents of the <type><number>_label file.
+     *
+     * If the map is not empty, it is cleared and rebuilt.  This is necessary
+     * over time because power devices may have been added or removed.
+     *
+     * Throws an exception if an error occurs trying to build the map.
+     */
+    virtual void buildPageToFileNumberMap();
+
+    /**
+     * Returns whether the specified sysfs hwmon file is a voltage label file.
+     *
+     * If it is a label file, the hwmon file number is obtained from the file
+     * name and returned.
+     *
+     * @param fileName file within the sysfs hwmon directory
+     * @param fileNumber the hwmon file number is stored in this output
+     *                   parameter if this is a label file
+     * @return true if specified file is a voltage label file, false otherwise
+     */
+    virtual bool isLabelFile(const std::string& fileName,
+                             unsigned int& fileNumber);
+
+    /**
+     * Reads the specified voltage label file to obtain the associated PMBus
+     * PAGE number.
+     *
+     * The returned optional variable will have no value if the PMBus PAGE
+     * number could not be obtained due to an error.
+     *
+     * @param fileName voltage label file within the sysfs hwmon directory
+     * @return PMBus page number
+     */
+    virtual std::optional<uint8_t>
+        readPageFromLabelFile(const std::string& fileName);
+
+    /**
+     * I2C bus for the device.
+     */
+    uint8_t bus;
+
+    /**
+     * I2C address for the device.
+     */
+    uint16_t address;
+
+    /**
+     * Device driver name.
+     */
+    std::string driverName;
+
+    /**
+     * Chip instance number.
+     */
+    size_t instance;
+
+    /**
+     * Interface to the PMBus information that is provided by the device driver
+     * in sysfs.
+     */
+    std::unique_ptr<pmbus::PMBusBase> pmbusInterface;
+
+    /**
+     * Map from PMBus PAGE numbers to sysfs hwmon file numbers.
+     */
+    std::map<uint8_t, unsigned int> pageToFileNumber;
+};
+
+} // namespace phosphor::power::sequencer
diff --git a/phosphor-power-sequencer/test/meson.build b/phosphor-power-sequencer/test/meson.build
index 21d1529..2a15f0a 100644
--- a/phosphor-power-sequencer/test/meson.build
+++ b/phosphor-power-sequencer/test/meson.build
@@ -3,6 +3,7 @@
                 'config_file_parser_error_tests.cpp',
                 'config_file_parser_tests.cpp',
                 'format_utils_tests.cpp',
+                'pmbus_driver_device_tests.cpp',
                 'rail_tests.cpp',
                 'standard_device_tests.cpp',
                 dependencies: [
diff --git a/phosphor-power-sequencer/test/pmbus_driver_device_tests.cpp b/phosphor-power-sequencer/test/pmbus_driver_device_tests.cpp
new file mode 100644
index 0000000..322499e
--- /dev/null
+++ b/phosphor-power-sequencer/test/pmbus_driver_device_tests.cpp
@@ -0,0 +1,835 @@
+/**
+ * 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 "mock_pmbus.hpp"
+#include "mock_services.hpp"
+#include "pmbus.hpp"
+#include "pmbus_driver_device.hpp"
+#include "rail.hpp"
+#include "services.hpp"
+
+#include <errno.h>
+#include <stdlib.h> // for mkdtemp()
+
+#include <cstdint>
+#include <exception>
+#include <filesystem>
+#include <format>
+#include <fstream>
+#include <map>
+#include <memory>
+#include <optional>
+#include <stdexcept>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using namespace phosphor::power::sequencer;
+using namespace phosphor::pmbus;
+
+using ::testing::Return;
+using ::testing::Throw;
+
+class PMBusDriverDeviceTests : public ::testing::Test
+{
+  protected:
+    /**
+     * Constructor.
+     *
+     * Creates a temporary directory to use in simulating sysfs files.
+     */
+    PMBusDriverDeviceTests() : ::testing::Test{}
+    {
+        char pathTemplate[] = "/tmp/pmbus_driver_device_testsXXXXXX";
+        char* retVal = mkdtemp(pathTemplate);
+        if (retVal == nullptr)
+        {
+            throw std::runtime_error{std::format(
+                "Unable to create temporary directory: errno={}", errno)};
+        }
+        tempDir = fs::path{pathTemplate};
+    }
+
+    /**
+     * Destructor.
+     *
+     * Deletes the temporary directory created in the constructor.
+     */
+    virtual ~PMBusDriverDeviceTests()
+    {
+        try
+        {
+            if (!tempDir.empty() && fs::exists(tempDir))
+            {
+                fs::remove_all(tempDir);
+            }
+        }
+        catch (...)
+        {
+            // Destructors must not throw exceptions
+        }
+    }
+
+    /**
+     * Creates a Rail object that checks for a pgood fault using STATUS_VOUT.
+     *
+     * @param name Unique name for the rail
+     * @param pageNum PMBus PAGE number of the rail
+     * @return Rail object
+     */
+    std::unique_ptr<Rail> createRail(const std::string& name, uint8_t pageNum)
+    {
+        std::optional<std::string> presence{};
+        std::optional<uint8_t> page{pageNum};
+        bool isPowerSupplyRail{false};
+        bool checkStatusVout{true};
+        bool compareVoltageToLimit{false};
+        std::optional<GPIO> gpio{};
+        return std::make_unique<Rail>(name, presence, page, isPowerSupplyRail,
+                                      checkStatusVout, compareVoltageToLimit,
+                                      gpio);
+    }
+
+    /**
+     * Creates a file with the specified contents within the temporary
+     * directory.
+     *
+     * @param name File name
+     * @param contents File contents
+     */
+    void createFile(const std::string& name, const std::string& contents = "")
+    {
+        fs::path path{tempDir / name};
+        std::ofstream out{path};
+        out << contents;
+        out.close();
+    }
+
+    /**
+     * Temporary directory that is used to create simulated sysfs / hmmon files.
+     */
+    fs::path tempDir{};
+};
+
+TEST_F(PMBusDriverDeviceTests, Constructor)
+{
+    // Test where works; optional parameters not specified
+    {
+        MockServices services;
+
+        std::string name{"XYZ_PSEQ"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        rails.emplace_back(createRail("VDD", 5));
+        rails.emplace_back(createRail("VIO", 7));
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        EXPECT_EQ(device.getName(), name);
+        EXPECT_EQ(device.getRails().size(), 2);
+        EXPECT_EQ(device.getRails()[0]->getName(), "VDD");
+        EXPECT_EQ(device.getRails()[1]->getName(), "VIO");
+        EXPECT_EQ(device.getBus(), bus);
+        EXPECT_EQ(device.getAddress(), address);
+        EXPECT_EQ(device.getDriverName(), "");
+        EXPECT_EQ(device.getInstance(), 0);
+        EXPECT_NE(&(device.getPMBusInterface()), nullptr);
+    }
+
+    // Test where works; optional parameters specified
+    {
+        MockServices services;
+
+        std::string name{"XYZ_PSEQ"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        rails.emplace_back(createRail("VDD", 5));
+        rails.emplace_back(createRail("VIO", 7));
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        std::string driverName{"xyzdev"};
+        size_t instance{3};
+        PMBusDriverDevice device{name,    std::move(rails), services, bus,
+                                 address, driverName,       instance};
+
+        EXPECT_EQ(device.getName(), name);
+        EXPECT_EQ(device.getRails().size(), 2);
+        EXPECT_EQ(device.getRails()[0]->getName(), "VDD");
+        EXPECT_EQ(device.getRails()[1]->getName(), "VIO");
+        EXPECT_EQ(device.getBus(), bus);
+        EXPECT_EQ(device.getAddress(), address);
+        EXPECT_EQ(device.getDriverName(), driverName);
+        EXPECT_EQ(device.getInstance(), instance);
+        EXPECT_NE(&(device.getPMBusInterface()), nullptr);
+    }
+}
+
+TEST_F(PMBusDriverDeviceTests, GetBus)
+{
+    MockServices services;
+
+    std::string name{"XYZ_PSEQ"};
+    std::vector<std::unique_ptr<Rail>> rails;
+    uint8_t bus{4};
+    uint16_t address{0x72};
+    PMBusDriverDevice device{name, std::move(rails), services, bus, address};
+
+    EXPECT_EQ(device.getBus(), bus);
+}
+
+TEST_F(PMBusDriverDeviceTests, GetAddress)
+{
+    MockServices services;
+
+    std::string name{"XYZ_PSEQ"};
+    std::vector<std::unique_ptr<Rail>> rails;
+    uint8_t bus{3};
+    uint16_t address{0xab};
+    PMBusDriverDevice device{name, std::move(rails), services, bus, address};
+
+    EXPECT_EQ(device.getAddress(), address);
+}
+
+TEST_F(PMBusDriverDeviceTests, GetDriverName)
+{
+    MockServices services;
+
+    std::string name{"XYZ_PSEQ"};
+    std::vector<std::unique_ptr<Rail>> rails;
+    uint8_t bus{3};
+    uint16_t address{0x72};
+    std::string driverName{"xyzdev"};
+    PMBusDriverDevice device{name, std::move(rails), services,
+                             bus,  address,          driverName};
+
+    EXPECT_EQ(device.getDriverName(), driverName);
+}
+
+TEST_F(PMBusDriverDeviceTests, GetInstance)
+{
+    MockServices services;
+
+    std::string name{"XYZ_PSEQ"};
+    std::vector<std::unique_ptr<Rail>> rails;
+    uint8_t bus{3};
+    uint16_t address{0x72};
+    std::string driverName{"xyzdev"};
+    size_t instance{3};
+    PMBusDriverDevice device{name,    std::move(rails), services, bus,
+                             address, driverName,       instance};
+
+    EXPECT_EQ(device.getInstance(), instance);
+}
+
+TEST_F(PMBusDriverDeviceTests, GetPMBusInterface)
+{
+    MockServices services;
+
+    std::string name{"XYZ_PSEQ"};
+    std::vector<std::unique_ptr<Rail>> rails;
+    uint8_t bus{3};
+    uint16_t address{0x72};
+    PMBusDriverDevice device{name, std::move(rails), services, bus, address};
+
+    EXPECT_NE(&(device.getPMBusInterface()), nullptr);
+}
+
+TEST_F(PMBusDriverDeviceTests, GetGPIOValues)
+{
+    // Test where works
+    {
+        MockServices services;
+        std::vector<int> gpioValues{1, 1, 1};
+        EXPECT_CALL(services, getGPIOValues("abc_382%#, zy"))
+            .Times(1)
+            .WillOnce(Return(gpioValues));
+
+        std::string name{"ABC_382%#, ZY"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        EXPECT_TRUE(device.getGPIOValues(services) == gpioValues);
+    }
+
+    // Test where fails with exception
+    {
+        MockServices services;
+        EXPECT_CALL(services, getGPIOValues("xyz_pseq"))
+            .Times(1)
+            .WillOnce(
+                Throw(std::runtime_error{"libgpiod: Unable to open chip"}));
+
+        std::string name{"XYZ_PSEQ"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        try
+        {
+            device.getGPIOValues(services);
+            ADD_FAILURE() << "Should not have reached this line.";
+        }
+        catch (const std::exception& e)
+        {
+            EXPECT_STREQ(e.what(),
+                         "Unable to read GPIO values from device XYZ_PSEQ "
+                         "using label xyz_pseq: "
+                         "libgpiod: Unable to open chip");
+        }
+    }
+}
+
+TEST_F(PMBusDriverDeviceTests, GetStatusWord)
+{
+    // Test where works
+    {
+        MockServices services;
+
+        std::string name{"xyz_pseq"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+        EXPECT_CALL(pmbus, read("status13", Type::Debug, true))
+            .Times(1)
+            .WillOnce(Return(0x1234));
+
+        uint8_t page{13};
+        EXPECT_EQ(device.getStatusWord(page), 0x1234);
+    }
+
+    // Test where fails with exception
+    {
+        MockServices services;
+
+        std::string name{"xyz_pseq"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+        EXPECT_CALL(pmbus, read("status0", Type::Debug, true))
+            .Times(1)
+            .WillOnce(Throw(std::runtime_error{"File does not exist"}));
+
+        try
+        {
+            uint8_t page{0};
+            device.getStatusWord(page);
+            ADD_FAILURE() << "Should not have reached this line.";
+        }
+        catch (const std::exception& e)
+        {
+            EXPECT_STREQ(
+                e.what(),
+                "Unable to read STATUS_WORD for PAGE 0 of device xyz_pseq: "
+                "File does not exist");
+        }
+    }
+}
+
+TEST_F(PMBusDriverDeviceTests, GetStatusVout)
+{
+    // Test where works
+    {
+        MockServices services;
+
+        std::string name{"xyz_pseq"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+        EXPECT_CALL(pmbus, read("status13_vout", Type::Debug, true))
+            .Times(1)
+            .WillOnce(Return(0xde));
+
+        uint8_t page{13};
+        EXPECT_EQ(device.getStatusVout(page), 0xde);
+    }
+
+    // Test where fails with exception
+    {
+        MockServices services;
+
+        std::string name{"xyz_pseq"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+        EXPECT_CALL(pmbus, read("status0_vout", Type::Debug, true))
+            .Times(1)
+            .WillOnce(Throw(std::runtime_error{"File does not exist"}));
+
+        try
+        {
+            uint8_t page{0};
+            device.getStatusVout(page);
+            ADD_FAILURE() << "Should not have reached this line.";
+        }
+        catch (const std::exception& e)
+        {
+            EXPECT_STREQ(
+                e.what(),
+                "Unable to read STATUS_VOUT for PAGE 0 of device xyz_pseq: "
+                "File does not exist");
+        }
+    }
+}
+
+TEST_F(PMBusDriverDeviceTests, GetReadVout)
+{
+    // Test where works
+    {
+        // Create simulated hwmon voltage label file
+        createFile("in13_label"); // PAGE 9 -> file number 13
+
+        MockServices services;
+
+        std::string name{"xyz_pseq"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+        EXPECT_CALL(pmbus, getPath(Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return(tempDir));
+        EXPECT_CALL(pmbus, readString("in13_label", Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return("vout10")); // PAGE number 9 + 1
+        EXPECT_CALL(pmbus, readString("in13_input", Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return("851"));
+
+        uint8_t page{9};
+        EXPECT_EQ(device.getReadVout(page), 0.851);
+    }
+
+    // Test where fails
+    {
+        // Create simulated hwmon voltage label file
+        createFile("in13_label"); // PAGE 8 -> file number 13
+
+        MockServices services;
+
+        std::string name{"xyz_pseq"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+        EXPECT_CALL(pmbus, getPath(Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return(tempDir));
+        EXPECT_CALL(pmbus, readString("in13_label", Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return("vout9")); // PAGE number 8 + 1
+
+        try
+        {
+            uint8_t page{9};
+            device.getReadVout(page);
+            ADD_FAILURE() << "Should not have reached this line.";
+        }
+        catch (const std::exception& e)
+        {
+            EXPECT_STREQ(
+                e.what(),
+                "Unable to read READ_VOUT for PAGE 9 of device xyz_pseq: "
+                "Unable to find hwmon file number for PAGE 9 of device xyz_pseq");
+        }
+    }
+}
+
+TEST_F(PMBusDriverDeviceTests, GetVoutUVFaultLimit)
+{
+    // Test where works
+    {
+        // Create simulated hwmon voltage label file
+        createFile("in1_label"); // PAGE 6 -> file number 1
+
+        MockServices services;
+
+        std::string name{"xyz_pseq"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+        EXPECT_CALL(pmbus, getPath(Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return(tempDir));
+        EXPECT_CALL(pmbus, readString("in1_label", Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return("vout7")); // PAGE number 6 + 1
+        EXPECT_CALL(pmbus, readString("in1_lcrit", Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return("1329"));
+
+        uint8_t page{6};
+        EXPECT_EQ(device.getVoutUVFaultLimit(page), 1.329);
+    }
+
+    // Test where fails
+    {
+        // Create simulated hwmon voltage label file
+        createFile("in1_label"); // PAGE 7 -> file number 1
+
+        MockServices services;
+
+        std::string name{"xyz_pseq"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+        EXPECT_CALL(pmbus, getPath(Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return(tempDir));
+        EXPECT_CALL(pmbus, readString("in1_label", Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return("vout8")); // PAGE number 7 + 1
+
+        try
+        {
+            uint8_t page{6};
+            device.getVoutUVFaultLimit(page);
+            ADD_FAILURE() << "Should not have reached this line.";
+        }
+        catch (const std::exception& e)
+        {
+            EXPECT_STREQ(
+                e.what(),
+                "Unable to read VOUT_UV_FAULT_LIMIT for PAGE 6 of device xyz_pseq: "
+                "Unable to find hwmon file number for PAGE 6 of device xyz_pseq");
+        }
+    }
+}
+
+TEST_F(PMBusDriverDeviceTests, GetPageToFileNumberMap)
+{
+    // Test where works: No voltage label files/mappings found
+    {
+        // Create simulated hwmon files.  None are valid voltage label files.
+        createFile("in1_input");   // Not a label file
+        createFile("in9_lcrit");   // Not a label file
+        createFile("in_label");    // Invalid voltage label file name
+        createFile("in9a_label");  // Invalid voltage label file name
+        createFile("fan3_label");  // Not a voltage label file
+        createFile("temp8_label"); // Not a voltage label file
+
+        MockServices services;
+
+        std::string name{"xyz_pseq"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+        EXPECT_CALL(pmbus, getPath(Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return(tempDir));
+        EXPECT_CALL(pmbus, readString).Times(0);
+
+        const std::map<uint8_t, unsigned int>& map =
+            device.getPageToFileNumberMap();
+        EXPECT_TRUE(map.empty());
+    }
+
+    // Test where works: Multiple voltage label files/mappings found
+    {
+        // Create simulated hwmon files
+        createFile("in9_label");   // PAGE 3 -> file number 9
+        createFile("in13_label");  // PAGE 7 -> file number 13
+        createFile("in0_label");   // PAGE 12 -> file number 0
+        createFile("in11_label");  // No mapping; invalid contents
+        createFile("in12_label");  // No mapping; invalid contents
+        createFile("in1_input");   // Not a label file
+        createFile("in7_lcrit");   // Not a label file
+        createFile("fan3_label");  // Not a voltage label file
+        createFile("temp8_label"); // Not a voltage label file
+
+        MockServices services;
+
+        std::string name{"xyz_pseq"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+        EXPECT_CALL(pmbus, getPath(Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return(tempDir));
+        EXPECT_CALL(pmbus, readString("in9_label", Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return("vout4")); // PAGE number 3 + 1
+        EXPECT_CALL(pmbus, readString("in13_label", Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return("vout8")); // PAGE number 7 + 1
+        EXPECT_CALL(pmbus, readString("in0_label", Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return("vout13")); // PAGE number 12 + 1
+        EXPECT_CALL(pmbus, readString("in11_label", Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return("vout")); // Invalid format
+        EXPECT_CALL(pmbus, readString("in12_label", Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return("vout13a")); // Invalid format
+
+        const std::map<uint8_t, unsigned int>& map =
+            device.getPageToFileNumberMap();
+        EXPECT_EQ(map.size(), 3);
+        EXPECT_EQ(map.at(uint8_t{3}), 9);
+        EXPECT_EQ(map.at(uint8_t{7}), 13);
+        EXPECT_EQ(map.at(uint8_t{12}), 0);
+    }
+
+    // Test where fails: hwmon directory path is actually a file
+    {
+        // Create file that will be returned as the hwmon directory path
+        createFile("in9_label");
+
+        MockServices services;
+
+        std::string name{"xyz_pseq"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+        EXPECT_CALL(pmbus, getPath(Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return(tempDir / "in9_label"));
+        EXPECT_CALL(pmbus, readString).Times(0);
+
+        const std::map<uint8_t, unsigned int>& map =
+            device.getPageToFileNumberMap();
+        EXPECT_TRUE(map.empty());
+    }
+
+    // Test where fails: hwmon directory path does not exist
+    {
+        MockServices services;
+
+        std::string name{"xyz_pseq"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+        EXPECT_CALL(pmbus, getPath(Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return(tempDir / "does_not_exist"));
+        EXPECT_CALL(pmbus, readString).Times(0);
+
+        const std::map<uint8_t, unsigned int>& map =
+            device.getPageToFileNumberMap();
+        EXPECT_TRUE(map.empty());
+    }
+
+    // Test where fails: hwmon directory path is not readable
+    {
+        // Create simulated hwmon files
+        createFile("in9_label");
+        createFile("in13_label");
+        createFile("in0_label");
+
+        // Change temporary directory to be unreadable
+        fs::permissions(tempDir, fs::perms::none);
+
+        MockServices services;
+
+        std::string name{"xyz_pseq"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+        EXPECT_CALL(pmbus, getPath(Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return(tempDir));
+        EXPECT_CALL(pmbus, readString).Times(0);
+
+        try
+        {
+            device.getPageToFileNumberMap();
+            ADD_FAILURE() << "Should not have reached this line.";
+        }
+        catch (const std::exception& e)
+        {
+            // Error message varies
+        }
+
+        // Change temporary directory to be readable/writable
+        fs::permissions(tempDir, fs::perms::owner_all);
+    }
+}
+
+TEST_F(PMBusDriverDeviceTests, GetFileNumber)
+{
+    // Test where works
+    {
+        // Create simulated hwmon voltage label files
+        createFile("in0_label");  // PAGE 6 -> file number 0
+        createFile("in13_label"); // PAGE 9 -> file number 13
+
+        MockServices services;
+
+        std::string name{"xyz_pseq"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+        EXPECT_CALL(pmbus, getPath(Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return(tempDir));
+        EXPECT_CALL(pmbus, readString("in0_label", Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return("vout7")); // PAGE number 6 + 1
+        EXPECT_CALL(pmbus, readString("in13_label", Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return("vout10")); // PAGE number 9 + 1
+
+        // Map was empty and needs to be built
+        uint8_t page{6};
+        EXPECT_EQ(device.getFileNumber(page), 0);
+
+        // Map had already been built
+        page = 9;
+        EXPECT_EQ(device.getFileNumber(page), 13);
+    }
+
+    // Test where fails: No mapping for specified PMBus PAGE
+    {
+        // Create simulated hwmon voltage label files
+        createFile("in0_label");  // PAGE 6 -> file number 0
+        createFile("in13_label"); // PAGE 9 -> file number 13
+
+        MockServices services;
+
+        std::string name{"xyz_pseq"};
+        std::vector<std::unique_ptr<Rail>> rails;
+        uint8_t bus{3};
+        uint16_t address{0x72};
+        PMBusDriverDevice device{name, std::move(rails), services, bus,
+                                 address};
+
+        MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+        EXPECT_CALL(pmbus, getPath(Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return(tempDir));
+        EXPECT_CALL(pmbus, readString("in0_label", Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return("vout7")); // PAGE number 6 + 1
+        EXPECT_CALL(pmbus, readString("in13_label", Type::Hwmon))
+            .Times(1)
+            .WillOnce(Return("vout10")); // PAGE number 9 + 1
+
+        try
+        {
+            uint8_t page{13};
+            device.getFileNumber(page);
+            ADD_FAILURE() << "Should not have reached this line.";
+        }
+        catch (const std::exception& e)
+        {
+            EXPECT_STREQ(
+                e.what(),
+                "Unable to find hwmon file number for PAGE 13 of device xyz_pseq");
+        }
+    }
+}
+
+TEST_F(PMBusDriverDeviceTests, PrepareForPgoodFaultDetection)
+{
+    // This is a protected method and cannot be called directly from a gtest.
+    // Call findPgoodFault() which calls prepareForPgoodFaultDetection().
+
+    // Create simulated hwmon voltage label file
+    createFile("in1_label"); // PAGE 6 -> file number 1
+
+    MockServices services;
+
+    std::string name{"xyz_pseq"};
+    std::vector<std::unique_ptr<Rail>> rails;
+    uint8_t bus{3};
+    uint16_t address{0x72};
+    PMBusDriverDevice device{name, std::move(rails), services, bus, address};
+
+    // Methods that get hwmon file info should be called twice
+    MockPMBus& pmbus = static_cast<MockPMBus&>(device.getPMBusInterface());
+    EXPECT_CALL(pmbus, getPath(Type::Hwmon))
+        .Times(2)
+        .WillRepeatedly(Return(tempDir));
+    EXPECT_CALL(pmbus, readString("in1_label", Type::Hwmon))
+        .Times(2)
+        .WillRepeatedly(Return("vout7")); // PAGE number 6 + 1
+
+    // Map was empty and needs to be built
+    uint8_t page{6};
+    EXPECT_EQ(device.getFileNumber(page), 1);
+
+    // Call findPgoodFault() which calls prepareForPgoodFaultDetection() which
+    // rebuilds the map.
+    std::string powerSupplyError{};
+    std::map<std::string, std::string> additionalData{};
+    std::string error = device.findPgoodFault(services, powerSupplyError,
+                                              additionalData);
+    EXPECT_TRUE(error.empty());
+    EXPECT_EQ(additionalData.size(), 0);
+
+    EXPECT_EQ(device.getFileNumber(page), 1);
+}