bmcweb: Add IndicatorLED property to sensors

Added support for the IndicatorLED property for physical leds
associated with Thermal and Power sensors.

Testing: Verified output on a witherspoon.
         No new errors in redfish validation.

Change-Id: I4e49b3c1769742e49f57c6c1b77a82511cdc8b99
Signed-off-by: Anthony Wilson <wilsonan@us.ibm.com>
diff --git a/redfish-core/lib/sensors.hpp b/redfish-core/lib/sensors.hpp
index fb3b25e..c900863 100644
--- a/redfish-core/lib/sensors.hpp
+++ b/redfish-core/lib/sensors.hpp
@@ -75,6 +75,17 @@
 };
 
 /**
+ * Possible states for physical inventory leds
+ */
+enum class LedState
+{
+    OFF,
+    ON,
+    BLINK,
+    UNKNOWN
+};
+
+/**
  * D-Bus inventory item associated with one or more sensors.
  */
 class InventoryItem
@@ -83,7 +94,8 @@
     InventoryItem(const std::string& objPath) :
         objectPath(objPath), name(), isPresent(true), isFunctional(true),
         isPowerSupply(false), manufacturer(), model(), partNumber(),
-        serialNumber(), sensors()
+        serialNumber(), sensors(), ledObjectPath(""),
+        ledState(LedState::UNKNOWN)
     {
         // Set inventory item name to last node of object path
         auto pos = objectPath.rfind('/');
@@ -103,6 +115,8 @@
     std::string partNumber;
     std::string serialNumber;
     std::set<std::string> sensors;
+    std::string ledObjectPath;
+    LedState ledState;
 };
 
 /**
@@ -596,6 +610,28 @@
     return "OK";
 }
 
+static void setLedState(nlohmann::json& sensorJson,
+                        const InventoryItem* inventoryItem)
+{
+    if (inventoryItem != nullptr && !inventoryItem->ledObjectPath.empty())
+    {
+        switch (inventoryItem->ledState)
+        {
+            case LedState::OFF:
+                sensorJson["IndicatorLED"] = "Off";
+                break;
+            case LedState::ON:
+                sensorJson["IndicatorLED"] = "Lit";
+                break;
+            case LedState::BLINK:
+                sensorJson["IndicatorLED"] = "Blinking";
+                break;
+            default:
+                break;
+        }
+    }
+}
+
 /**
  * @brief Builds a json sensor representation of a sensor.
  * @param sensorName  The name of the sensor to be built
@@ -690,6 +726,7 @@
         unit = "/Reading"_json_pointer;
         sensor_json["ReadingUnits"] = "RPM";
         sensor_json["@odata.type"] = "#Thermal.v1_3_0.Fan";
+        setLedState(sensor_json, inventoryItem);
         forceToInt = true;
     }
     else if (sensorType == "fan_pwm")
@@ -697,6 +734,7 @@
         unit = "/Reading"_json_pointer;
         sensor_json["ReadingUnits"] = "Percent";
         sensor_json["@odata.type"] = "#Thermal.v1_3_0.Fan";
+        setLedState(sensor_json, inventoryItem);
         forceToInt = true;
     }
     else if (sensorType == "voltage")
@@ -1132,6 +1170,26 @@
 }
 
 /**
+ * @brief Finds the inventory item associated with the specified led path.
+ * @param inventoryItems D-Bus inventory items associated with sensors.
+ * @param ledObjPath D-Bus object path of led.
+ * @return Inventory item within vector, or nullptr if no match found.
+ */
+inline InventoryItem*
+    findInventoryItemForLed(std::vector<InventoryItem>& inventoryItems,
+                            const std::string& ledObjPath)
+{
+    for (InventoryItem& inventoryItem : inventoryItems)
+    {
+        if (inventoryItem.ledObjectPath == ledObjPath)
+        {
+            return &inventoryItem;
+        }
+    }
+    return nullptr;
+}
+
+/**
  * @brief Adds inventory item and associated sensor to specified vector.
  *
  * Adds a new InventoryItem to the vector if necessary.  Searches for an
@@ -1290,7 +1348,7 @@
  *
  * The callback must have the following signature:
  *   @code
- *   callback(std::shared_ptr<std::vector<InventoryItem>> inventoryItems)
+ *   callback(void)
  *   @endcode
  *
  * This function is called recursively, obtaining data asynchronously from one
@@ -1320,7 +1378,7 @@
     // If no more connections left, call callback
     if (invConnectionsIndex >= invConnections->size())
     {
-        callback(inventoryItems);
+        callback();
         BMCWEB_LOG_DEBUG << "getInventoryItemsData exit";
         return;
     }
@@ -1459,6 +1517,7 @@
                 }
             }
         }
+
         callback(invConnections);
         BMCWEB_LOG_DEBUG << "getInventoryItemsConnections respHandler exit";
     };
@@ -1475,7 +1534,8 @@
  * @brief Gets associations from sensors to inventory items.
  *
  * Looks for ObjectMapper associations from the specified sensors to related
- * inventory items.
+ * inventory items. Then finds the associations from those inventory items to
+ * their LEDs, if any.
  *
  * Finds the inventory items asynchronously.  Invokes callback when information
  * has been obtained.
@@ -1564,6 +1624,49 @@
             }
         }
 
+        // Now loop through the returned object paths again, this time to
+        // find the leds associated with the inventory items we just found
+        std::string inventoryAssocPath;
+        inventoryAssocPath.reserve(128); // avoid memory allocations
+        for (const auto& objDictEntry : resp)
+        {
+            const std::string& objPath =
+                static_cast<const std::string&>(objDictEntry.first);
+            const boost::container::flat_map<
+                std::string, boost::container::flat_map<
+                                 std::string, dbus::utility::DbusVariantType>>&
+                interfacesDict = objDictEntry.second;
+
+            for (InventoryItem& inventoryItem : *inventoryItems)
+            {
+                inventoryAssocPath = inventoryItem.objectPath;
+                inventoryAssocPath += "/leds";
+                if (objPath == inventoryAssocPath)
+                {
+                    // Get Association interface for object path
+                    auto assocIt =
+                        interfacesDict.find("xyz.openbmc_project.Association");
+                    if (assocIt != interfacesDict.end())
+                    {
+                        // Get inventory item from end point
+                        auto endpointsIt = assocIt->second.find("endpoints");
+                        if (endpointsIt != assocIt->second.end())
+                        {
+                            const std::vector<std::string>* endpoints =
+                                std::get_if<std::vector<std::string>>(
+                                    &endpointsIt->second);
+                            if ((endpoints != nullptr) && !endpoints->empty())
+                            {
+                                // Store LED path in inventory item
+                                const std::string& ledPath = endpoints->front();
+                                inventoryItem.ledObjectPath = ledPath;
+                            }
+                        }
+                    }
+                    break;
+                }
+            }
+        }
         callback(inventoryItems);
         BMCWEB_LOG_DEBUG << "getInventoryItemAssociations respHandler exit";
     };
@@ -1585,6 +1688,209 @@
 }
 
 /**
+ * @brief Gets D-Bus data for inventory item leds associated with sensors.
+ *
+ * Uses the specified connections (services) to obtain D-Bus data for inventory
+ * item leds associated with sensors.  Stores the resulting data in the
+ * inventoryItems vector.
+ *
+ * This data is later used to provide sensor property values in the JSON
+ * response.
+ *
+ * Finds the inventory item led data asynchronously.  Invokes callback when data
+ * has been obtained.
+ *
+ * The callback must have the following signature:
+ *   @code
+ *   callback(std::shared_ptr<std::vector<InventoryItem>> inventoryItems)
+ *   @endcode
+ *
+ * This function is called recursively, obtaining data asynchronously from one
+ * connection in each call.  This ensures the callback is not invoked until the
+ * last asynchronous function has completed.
+ *
+ * @param sensorsAsyncResp Pointer to object holding response data.
+ * @param inventoryItems D-Bus inventory items associated with sensors.
+ * @param ledConnections Connections that provide data for the inventory leds.
+ * @param callback Callback to invoke when inventory data has been obtained.
+ * @param ledConnectionsIndex Current index in ledConnections.  Only specified
+ * in recursive calls to this function.
+ */
+template <typename Callback>
+void getInventoryLedData(
+    std::shared_ptr<SensorsAsyncResp> sensorsAsyncResp,
+    std::shared_ptr<std::vector<InventoryItem>> inventoryItems,
+    std::shared_ptr<boost::container::flat_map<std::string, std::string>>
+        ledConnections,
+    Callback&& callback, size_t ledConnectionsIndex = 0)
+{
+    BMCWEB_LOG_DEBUG << "getInventoryLedData enter";
+
+    // If no more connections left, call callback
+    if (ledConnectionsIndex >= ledConnections->size())
+    {
+        callback(inventoryItems);
+        BMCWEB_LOG_DEBUG << "getInventoryLedData exit";
+        return;
+    }
+
+    // Get inventory item data from current connection
+    auto it = ledConnections->nth(ledConnectionsIndex);
+    if (it != ledConnections->end())
+    {
+        const std::string& ledPath = (*it).first;
+        const std::string& ledConnection = (*it).second;
+        // Response handler for Get State property
+        auto respHandler =
+            [sensorsAsyncResp, inventoryItems, ledConnections, ledPath,
+             callback{std::move(callback)},
+             ledConnectionsIndex](const boost::system::error_code ec,
+                                  const std::variant<std::string>& ledState) {
+                BMCWEB_LOG_DEBUG << "getInventoryLedData respHandler enter";
+                if (ec)
+                {
+                    BMCWEB_LOG_ERROR
+                        << "getInventoryLedData respHandler DBus error " << ec;
+                    messages::internalError(sensorsAsyncResp->res);
+                    return;
+                }
+
+                const std::string* state = std::get_if<std::string>(&ledState);
+                if (state != nullptr)
+                {
+                    BMCWEB_LOG_DEBUG << "Led state: " << *state;
+                    // Find inventory item with this LED object path
+                    InventoryItem* inventoryItem =
+                        findInventoryItemForLed(*inventoryItems, ledPath);
+                    if (inventoryItem != nullptr)
+                    {
+                        // Store LED state in InventoryItem
+                        if (boost::ends_with(*state, "On"))
+                        {
+                            inventoryItem->ledState = LedState::ON;
+                        }
+                        else if (boost::ends_with(*state, "Blink"))
+                        {
+                            inventoryItem->ledState = LedState::BLINK;
+                        }
+                        else if (boost::ends_with(*state, "Off"))
+                        {
+                            inventoryItem->ledState = LedState::OFF;
+                        }
+                        else
+                        {
+                            inventoryItem->ledState = LedState::UNKNOWN;
+                        }
+                    }
+                }
+                else
+                {
+                    BMCWEB_LOG_DEBUG << "Failed to find State data for LED: "
+                                     << ledPath;
+                }
+
+                // Recurse to get LED data from next connection
+                getInventoryLedData(sensorsAsyncResp, inventoryItems,
+                                    ledConnections, std::move(callback),
+                                    ledConnectionsIndex + 1);
+
+                BMCWEB_LOG_DEBUG << "getInventoryLedData respHandler exit";
+            };
+
+        // Get the State property for the current LED
+        crow::connections::systemBus->async_method_call(
+            std::move(respHandler), ledConnection, ledPath,
+            "org.freedesktop.DBus.Properties", "Get",
+            "xyz.openbmc_project.Led.Physical", "State");
+    }
+
+    BMCWEB_LOG_DEBUG << "getInventoryLedData exit";
+}
+
+/**
+ * @brief Gets LED data for LEDs associated with given inventory items.
+ *
+ * Gets the D-Bus connections (services) that provide LED data for the LEDs
+ * associated with the specified inventory items.  Then gets the LED data from
+ * each connection and stores it in the inventory item.
+ *
+ * This data is later used to provide sensor property values in the JSON
+ * response.
+ *
+ * Finds the LED data asynchronously.  Invokes callback when information has
+ * been obtained.
+ *
+ * The callback must have the following signature:
+ *   @code
+ *   callback(std::shared_ptr<std::vector<InventoryItem>> inventoryItems)
+ *   @endcode
+ *
+ * @param sensorsAsyncResp Pointer to object holding response data.
+ * @param inventoryItems D-Bus inventory items associated with sensors.
+ * @param callback Callback to invoke when inventory items have been obtained.
+ */
+template <typename Callback>
+void getInventoryLeds(
+    std::shared_ptr<SensorsAsyncResp> sensorsAsyncResp,
+    std::shared_ptr<std::vector<InventoryItem>> inventoryItems,
+    Callback&& callback)
+{
+    BMCWEB_LOG_DEBUG << "getInventoryLeds enter";
+
+    const std::string path = "/xyz/openbmc_project";
+    const std::array<std::string, 1> interfaces = {
+        "xyz.openbmc_project.Led.Physical"};
+
+    // Response handler for parsing output from GetSubTree
+    auto respHandler = [callback{std::move(callback)}, sensorsAsyncResp,
+                        inventoryItems](const boost::system::error_code ec,
+                                        const GetSubTreeType& subtree) {
+        BMCWEB_LOG_DEBUG << "getInventoryLeds respHandler enter";
+        if (ec)
+        {
+            messages::internalError(sensorsAsyncResp->res);
+            BMCWEB_LOG_ERROR << "getInventoryLeds respHandler DBus error "
+                             << ec;
+            return;
+        }
+
+        // Build map of LED object paths to connections
+        std::shared_ptr<boost::container::flat_map<std::string, std::string>>
+            ledConnections = std::make_shared<
+                boost::container::flat_map<std::string, std::string>>();
+
+        // Loop through objects from GetSubTree
+        for (const std::pair<
+                 std::string,
+                 std::vector<std::pair<std::string, std::vector<std::string>>>>&
+                 object : subtree)
+        {
+            // Check if object path is LED for one of the specified inventory
+            // items
+            const std::string& ledPath = object.first;
+            if (findInventoryItemForLed(*inventoryItems, ledPath) != nullptr)
+            {
+                // Add mapping from ledPath to connection
+                const std::string& connection = object.second.begin()->first;
+                (*ledConnections)[ledPath] = connection;
+                BMCWEB_LOG_DEBUG << "Added mapping " << ledPath << " -> "
+                                 << connection;
+            }
+        }
+
+        getInventoryLedData(sensorsAsyncResp, inventoryItems, ledConnections,
+                            std::move(callback));
+        BMCWEB_LOG_DEBUG << "getInventoryLeds respHandler exit";
+    };
+    // Make call to ObjectMapper to find all inventory items
+    crow::connections::systemBus->async_method_call(
+        std::move(respHandler), "xyz.openbmc_project.ObjectMapper",
+        "/xyz/openbmc_project/object_mapper",
+        "xyz.openbmc_project.ObjectMapper", "GetSubTree", path, 0, interfaces);
+    BMCWEB_LOG_DEBUG << "getInventoryLeds exit";
+}
+
+/**
  * @brief Gets inventory items associated with sensors.
  *
  * Finds the inventory items that are associated with the specified sensors.
@@ -1626,12 +1932,20 @@
                     std::shared_ptr<boost::container::flat_set<std::string>>
                         invConnections) {
                     BMCWEB_LOG_DEBUG << "getInventoryItemsConnectionsCb enter";
+                    auto getInventoryItemsDataCb =
+                        [sensorsAsyncResp, inventoryItems,
+                         callback{std::move(callback)}]() {
+                            BMCWEB_LOG_DEBUG << "getInventoryItemsDataCb enter";
+                            // Find led connections and get the data
+                            getInventoryLeds(sensorsAsyncResp, inventoryItems,
+                                             std::move(callback));
+                            BMCWEB_LOG_DEBUG << "getInventoryItemsDataCb exit";
+                        };
 
                     // Get inventory item data from connections
                     getInventoryItemsData(sensorsAsyncResp, inventoryItems,
                                           invConnections, objectMgrPaths,
-                                          std::move(callback));
-
+                                          std::move(getInventoryItemsDataCb));
                     BMCWEB_LOG_DEBUG << "getInventoryItemsConnectionsCb exit";
                 };
 
@@ -1688,6 +2002,7 @@
     powerSupply["Model"] = inventoryItem.model;
     powerSupply["PartNumber"] = inventoryItem.partNumber;
     powerSupply["SerialNumber"] = inventoryItem.serialNumber;
+    setLedState(powerSupply, &inventoryItem);
     powerSupply["Status"]["State"] = getState(&inventoryItem);
 
     const char* health = inventoryItem.isFunctional ? "OK" : "Critical";