bmcweb: Implement SensorCollection

Add collection of all power and current sensors.

Testing: Verified SensorCollection and Sensor output
         on a Witherspoon system.
         Verified no errors from RedfishServiceValidator.

Change-Id: Icfdc14d738bf037d5d599a3c6fc0be5ea0919929
Signed-off-by: Anthony Wilson <wilsonan@us.ibm.com>
diff --git a/redfish-core/lib/sensors.hpp b/redfish-core/lib/sensors.hpp
index 3fd86d6..eaf180d 100644
--- a/redfish-core/lib/sensors.hpp
+++ b/redfish-core/lib/sensors.hpp
@@ -15,6 +15,8 @@
 */
 #pragma once
 
+#include "node.hpp"
+
 #include <math.h>
 
 #include <boost/algorithm/string/predicate.hpp>
@@ -308,13 +310,30 @@
             sensorsAsyncResp->res.jsonValue["Temperatures"] =
                 nlohmann::json::array();
         }
+        else if (chassisSubNode == "Sensors")
+        {
+            sensorsAsyncResp->res.jsonValue["@odata.type"] =
+                "#SensorCollection.SensorCollection";
+            sensorsAsyncResp->res.jsonValue["@odata.context"] =
+                "/redfish/v1/$metadata#SensorCollection.SensorCollection";
+            sensorsAsyncResp->res.jsonValue["Description"] =
+                "Collection of Sensors for this Chassis";
+            sensorsAsyncResp->res.jsonValue["Members"] =
+                nlohmann::json::array();
+            sensorsAsyncResp->res.jsonValue["Members@odata.count"] = 0;
+        }
+
+        if (chassisSubNode != "Sensors")
+        {
+            sensorsAsyncResp->res.jsonValue["Id"] = chassisSubNode;
+            sensorsAsyncResp->res.jsonValue["@odata.context"] =
+                "/redfish/v1/$metadata#" + chassisSubNode + "." +
+                chassisSubNode;
+        }
+
         sensorsAsyncResp->res.jsonValue["@odata.id"] =
             "/redfish/v1/Chassis/" + sensorsAsyncResp->chassisId + "/" +
             chassisSubNode;
-
-        sensorsAsyncResp->res.jsonValue["@odata.context"] =
-            "/redfish/v1/$metadata#" + chassisSubNode + "." + chassisSubNode;
-        sensorsAsyncResp->res.jsonValue["Id"] = chassisSubNode;
         sensorsAsyncResp->res.jsonValue["Name"] = chassisSubNode;
 
         // Get the list of all sensors for this Chassis element
@@ -340,7 +359,9 @@
                         sensorsAsyncResp->res, sensorsAsyncResp->chassisSubNode,
                         sensorsAsyncResp->chassisSubNode == "Thermal"
                             ? "Temperatures"
-                            : "Voltages");
+                            : sensorsAsyncResp->chassisSubNode == "Power"
+                                  ? "Voltages"
+                                  : "Sensors");
                     return;
                 }
                 const std::shared_ptr<boost::container::flat_set<std::string>>
@@ -582,6 +603,8 @@
  * @param sensorName  The name of the sensor to be built
  * @param sensorType  The type (temperature, fan_tach, etc) of the sensor to
  * build
+ * @param sensorSchema  The schema (Power, Thermal, etc) being associated with
+ * the sensor to build
  * @param interfacesDict  A dictionary of the interfaces and properties of said
  * interfaces to be built from
  * @param sensor_json  The json object to fill
@@ -590,6 +613,7 @@
  */
 void objectInterfacesToJson(
     const std::string& sensorName, const std::string& sensorType,
+    const std::string& sensorSchema,
     const boost::container::flat_map<
         std::string, boost::container::flat_map<std::string, SensorVariant>>&
         interfacesDict,
@@ -617,11 +641,18 @@
         }
     }
 
-    // Set MemberId and Name for non-power sensors.  For PowerSupplies and
-    // PowerControl, those properties have more general values because multiple
-    // sensors can be stored in the same JSON object.
-    if (sensorType != "power")
+    if (sensorSchema == "Sensors")
     {
+        // For sensors in SensorCollection we set Id instead of MemberId,
+        // including power sensors.
+        sensor_json["Id"] = sensorName;
+        sensor_json["Name"] = boost::replace_all_copy(sensorName, "_", " ");
+    }
+    else if (sensorType != "power")
+    {
+        // Set MemberId and Name for non-power sensors.  For PowerSupplies and
+        // PowerControl, those properties have more general values because
+        // multiple sensors can be stored in the same JSON object.
         sensor_json["MemberId"] = sensorName;
         sensor_json["Name"] = boost::replace_all_copy(sensorName, "_", " ");
     }
@@ -636,7 +667,20 @@
     bool forceToInt = false;
 
     const char* unit = "Reading";
-    if (sensorType == "temperature")
+    if (sensorSchema == "Sensors")
+    {
+        sensor_json["@odata.type"] = "#Sensor.v1_0_0.Sensor";
+        sensor_json["@odata.context"] = "/redfish/v1/$metadata#Sensor.Sensor";
+        if (sensorType == "power")
+        {
+            sensor_json["ReadingUnits"] = "Watts";
+        }
+        else if (sensorType == "current")
+        {
+            sensor_json["ReadingUnits"] = "Amperes";
+        }
+    }
+    else if (sensorType == "temperature")
     {
         unit = "ReadingCelsius";
         sensor_json["@odata.type"] = "#Thermal.v1_3_0.Temperature";
@@ -696,7 +740,7 @@
 
     properties.emplace_back("xyz.openbmc_project.Sensor.Value", "Value", unit);
 
-    if (sensorType != "power")
+    if (sensorType != "power" && sensorSchema != "Sensors")
     {
         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Warning",
                                 "WarningHigh", "UpperThresholdNonCritical");
@@ -710,7 +754,14 @@
 
     // TODO Need to get UpperThresholdFatal and LowerThresholdFatal
 
-    if (sensorType == "temperature")
+    if (sensorSchema == "Sensors")
+    {
+        properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
+                                "ReadingRangeMin");
+        properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
+                                "ReadingRangeMax");
+    }
+    else if (sensorType == "temperature")
     {
         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
                                 "MinReadingRangeTemp");
@@ -1709,85 +1760,102 @@
                 InventoryItem* inventoryItem =
                     findInventoryItemForSensor(inventoryItems, objPath);
 
-                const char* fieldName = nullptr;
-                if (sensorType == "temperature")
+                const std::string& sensorSchema =
+                    SensorsAsyncResp->chassisSubNode;
+
+                nlohmann::json* sensorJson = nullptr;
+
+                if (sensorSchema == "Sensors")
                 {
-                    fieldName = "Temperatures";
-                }
-                else if (sensorType == "fan" || sensorType == "fan_tach" ||
-                         sensorType == "fan_pwm")
-                {
-                    fieldName = "Fans";
-                }
-                else if (sensorType == "voltage")
-                {
-                    fieldName = "Voltages";
-                }
-                else if (sensorType == "power")
-                {
-                    if (!sensorName.compare("total_power"))
-                    {
-                        fieldName = "PowerControl";
-                    }
-                    else if ((inventoryItem != nullptr) &&
-                             (inventoryItem->isPowerSupply))
-                    {
-                        fieldName = "PowerSupplies";
-                    }
-                    else
-                    {
-                        // Other power sensors are in SensorCollection
-                        continue;
-                    }
+                    SensorsAsyncResp->res.jsonValue["@odata.id"] =
+                        "/redfish/v1/Chassis/" + SensorsAsyncResp->chassisId +
+                        "/" + SensorsAsyncResp->chassisSubNode + "/" +
+                        sensorName;
+                    sensorJson = &(SensorsAsyncResp->res.jsonValue);
                 }
                 else
                 {
-                    BMCWEB_LOG_ERROR << "Unsure how to handle sensorType "
-                                     << sensorType;
-                    continue;
-                }
-
-                nlohmann::json& tempArray =
-                    SensorsAsyncResp->res.jsonValue[fieldName];
-                nlohmann::json* sensorJson = nullptr;
-
-                if (fieldName == "PowerControl")
-                {
-                    if (tempArray.empty())
+                    const char* fieldName = nullptr;
+                    if (sensorType == "temperature")
                     {
-                        // Put multiple "sensors" into a single PowerControl.
-                        // Follows MemberId naming and naming in power.hpp.
+                        fieldName = "Temperatures";
+                    }
+                    else if (sensorType == "fan" || sensorType == "fan_tach" ||
+                             sensorType == "fan_pwm")
+                    {
+                        fieldName = "Fans";
+                    }
+                    else if (sensorType == "voltage")
+                    {
+                        fieldName = "Voltages";
+                    }
+                    else if (sensorType == "power")
+                    {
+                        if (!sensorName.compare("total_power"))
+                        {
+                            fieldName = "PowerControl";
+                        }
+                        else if ((inventoryItem != nullptr) &&
+                                 (inventoryItem->isPowerSupply))
+                        {
+                            fieldName = "PowerSupplies";
+                        }
+                        else
+                        {
+                            // Other power sensors are in SensorCollection
+                            continue;
+                        }
+                    }
+                    else
+                    {
+                        BMCWEB_LOG_ERROR << "Unsure how to handle sensorType "
+                                         << sensorType;
+                        continue;
+                    }
+
+                    nlohmann::json& tempArray =
+                        SensorsAsyncResp->res.jsonValue[fieldName];
+                    if (fieldName == "PowerControl")
+                    {
+                        if (tempArray.empty())
+                        {
+                            // Put multiple "sensors" into a single
+                            // PowerControl. Follows MemberId naming and
+                            // naming in power.hpp.
+                            tempArray.push_back(
+                                {{"@odata.id",
+                                  "/redfish/v1/Chassis/" +
+                                      SensorsAsyncResp->chassisId + "/" +
+                                      SensorsAsyncResp->chassisSubNode + "#/" +
+                                      fieldName + "/0"}});
+                        }
+                        sensorJson = &(tempArray.back());
+                    }
+                    else if (fieldName == "PowerSupplies")
+                    {
+                        if (inventoryItem != nullptr)
+                        {
+                            sensorJson =
+                                &(getPowerSupply(tempArray, *inventoryItem,
+                                                 SensorsAsyncResp->chassisId));
+                        }
+                    }
+                    else
+                    {
                         tempArray.push_back(
                             {{"@odata.id",
                               "/redfish/v1/Chassis/" +
                                   SensorsAsyncResp->chassisId + "/" +
                                   SensorsAsyncResp->chassisSubNode + "#/" +
-                                  fieldName + "/0"}});
+                                  fieldName + "/"}});
+                        sensorJson = &(tempArray.back());
                     }
-                    sensorJson = &(tempArray.back());
-                }
-                else if (fieldName == "PowerSupplies")
-                {
-                    if (inventoryItem != nullptr)
-                    {
-                        sensorJson =
-                            &(getPowerSupply(tempArray, *inventoryItem,
-                                             SensorsAsyncResp->chassisId));
-                    }
-                }
-                else
-                {
-                    tempArray.push_back(
-                        {{"@odata.id", "/redfish/v1/Chassis/" +
-                                           SensorsAsyncResp->chassisId + "/" +
-                                           SensorsAsyncResp->chassisSubNode +
-                                           "#/" + fieldName + "/"}});
-                    sensorJson = &(tempArray.back());
                 }
 
                 if (sensorJson != nullptr)
                 {
                     objectInterfacesToJson(sensorName, sensorType,
+                                           SensorsAsyncResp->chassisSubNode,
                                            objDictEntry.second, *sensorJson,
                                            inventoryItem);
                 }
@@ -1818,6 +1886,52 @@
     BMCWEB_LOG_DEBUG << "getSensorData exit";
 }
 
+void processSensorList(
+    std::shared_ptr<SensorsAsyncResp> SensorsAsyncResp,
+    std::shared_ptr<boost::container::flat_set<std::string>> sensorNames)
+{
+    auto getConnectionCb =
+        [SensorsAsyncResp, sensorNames](
+            const boost::container::flat_set<std::string>& connections) {
+            BMCWEB_LOG_DEBUG << "getConnectionCb enter";
+            auto getObjectManagerPathsCb =
+                [SensorsAsyncResp, sensorNames, connections](
+                    std::shared_ptr<
+                        boost::container::flat_map<std::string, std::string>>
+                        objectMgrPaths) {
+                    BMCWEB_LOG_DEBUG << "getObjectManagerPathsCb enter";
+                    auto getInventoryItemsCb =
+                        [SensorsAsyncResp, sensorNames, connections,
+                         objectMgrPaths](
+                            std::shared_ptr<std::vector<InventoryItem>>
+                                inventoryItems) {
+                            BMCWEB_LOG_DEBUG << "getInventoryItemsCb enter";
+                            // Get sensor data and store results in JSON
+                            getSensorData(SensorsAsyncResp, sensorNames,
+                                          connections, objectMgrPaths,
+                                          inventoryItems);
+                            BMCWEB_LOG_DEBUG << "getInventoryItemsCb exit";
+                        };
+
+                    // Get inventory items associated with sensors
+                    getInventoryItems(SensorsAsyncResp, sensorNames,
+                                      objectMgrPaths,
+                                      std::move(getInventoryItemsCb));
+
+                    BMCWEB_LOG_DEBUG << "getObjectManagerPathsCb exit";
+                };
+
+            // Get mapping from connection names to the DBus object
+            // paths that implement the ObjectManager interface
+            getObjectManagerPaths(SensorsAsyncResp,
+                                  std::move(getObjectManagerPathsCb));
+            BMCWEB_LOG_DEBUG << "getConnectionCb exit";
+        };
+
+    // Get set of connections that provide sensor values
+    getConnections(SensorsAsyncResp, sensorNames, std::move(getConnectionCb));
+}
+
 /**
  * @brief Entry point for retrieving sensors data related to requested
  *        chassis.
@@ -1831,47 +1945,7 @@
             std::shared_ptr<boost::container::flat_set<std::string>>
                 sensorNames) {
             BMCWEB_LOG_DEBUG << "getChassisCb enter";
-            auto getConnectionCb = [SensorsAsyncResp, sensorNames](
-                                       const boost::container::flat_set<
-                                           std::string>& connections) {
-                BMCWEB_LOG_DEBUG << "getConnectionCb enter";
-                auto getObjectManagerPathsCb =
-                    [SensorsAsyncResp, sensorNames, connections](
-                        std::shared_ptr<boost::container::flat_map<std::string,
-                                                                   std::string>>
-                            objectMgrPaths) {
-                        BMCWEB_LOG_DEBUG << "getObjectManagerPathsCb enter";
-                        auto getInventoryItemsCb =
-                            [SensorsAsyncResp, sensorNames, connections,
-                             objectMgrPaths](
-                                std::shared_ptr<std::vector<InventoryItem>>
-                                    inventoryItems) {
-                                BMCWEB_LOG_DEBUG << "getInventoryItemsCb enter";
-                                // Get sensor data and store results in JSON
-                                getSensorData(SensorsAsyncResp, sensorNames,
-                                              connections, objectMgrPaths,
-                                              inventoryItems);
-                                BMCWEB_LOG_DEBUG << "getInventoryItemsCb exit";
-                            };
-
-                        // Get inventory items associated with sensors
-                        getInventoryItems(SensorsAsyncResp, sensorNames,
-                                          objectMgrPaths,
-                                          std::move(getInventoryItemsCb));
-
-                        BMCWEB_LOG_DEBUG << "getObjectManagerPathsCb exit";
-                    };
-
-                // Get mapping from connection names to the DBus object
-                // paths that implement the ObjectManager interface
-                getObjectManagerPaths(SensorsAsyncResp,
-                                      std::move(getObjectManagerPathsCb));
-                BMCWEB_LOG_DEBUG << "getConnectionCb exit";
-            };
-
-            // Get set of connections that provide sensor values
-            getConnections(SensorsAsyncResp, sensorNames,
-                           std::move(getConnectionCb));
+            processSensorList(SensorsAsyncResp, sensorNames);
             BMCWEB_LOG_DEBUG << "getChassisCb exit";
         };
     SensorsAsyncResp->res.jsonValue["Redundancy"] = nlohmann::json::array();
@@ -2102,4 +2176,177 @@
     getChassis(sensorAsyncResp, std::move(getChassisSensorListCb));
 }
 
+class SensorCollection : public Node
+{
+  public:
+    SensorCollection(CrowApp& app) :
+        Node(app, "/redfish/v1/Chassis/<str>/Sensors", std::string())
+    {
+        entityPrivileges = {
+            {boost::beast::http::verb::get, {{"Login"}}},
+            {boost::beast::http::verb::head, {{"Login"}}},
+            {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
+            {boost::beast::http::verb::put, {{"ConfigureManager"}}},
+            {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
+            {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
+    }
+
+  private:
+    std::vector<const char*> typeList = {
+        "/xyz/openbmc_project/sensors/power",
+        "/xyz/openbmc_project/sensors/current"};
+    void doGet(crow::Response& res, const crow::Request& req,
+               const std::vector<std::string>& params) override
+    {
+        BMCWEB_LOG_DEBUG << "SensorCollection doGet enter";
+        if (params.size() != 1)
+        {
+            BMCWEB_LOG_DEBUG << "SensorCollection doGet param size < 1";
+            messages::internalError(res);
+            res.end();
+            return;
+        }
+
+        const std::string& chassisId = params[0];
+        std::shared_ptr<SensorsAsyncResp> asyncResp =
+            std::make_shared<SensorsAsyncResp>(res, chassisId, typeList,
+                                               "Sensors");
+
+        auto getChassisCb =
+            [asyncResp](std::shared_ptr<boost::container::flat_set<std::string>>
+                            sensorNames) {
+                BMCWEB_LOG_DEBUG << "getChassisCb enter";
+
+                nlohmann::json& entriesArray =
+                    asyncResp->res.jsonValue["Members"];
+                for (auto& sensor : *sensorNames)
+                {
+                    BMCWEB_LOG_DEBUG << "Adding sensor: " << sensor;
+
+                    std::size_t lastPos = sensor.rfind("/");
+                    if (lastPos == std::string::npos ||
+                        lastPos + 1 >= sensor.size())
+                    {
+                        BMCWEB_LOG_ERROR << "Invalid sensor path: " << sensor;
+                        messages::internalError(asyncResp->res);
+                        return;
+                    }
+                    std::string sensorName = sensor.substr(lastPos + 1);
+                    entriesArray.push_back(
+                        {{"@odata.id",
+                          "/redfish/v1/Chassis/" + asyncResp->chassisId + "/" +
+                              asyncResp->chassisSubNode + "/" + sensorName}});
+                }
+
+                asyncResp->res.jsonValue["Members@odata.count"] =
+                    entriesArray.size();
+                BMCWEB_LOG_DEBUG << "getChassisCb exit";
+            };
+
+        // Get set of sensors in chassis
+        getChassis(asyncResp, std::move(getChassisCb));
+        BMCWEB_LOG_DEBUG << "SensorCollection doGet exit";
+    }
+};
+
+class Sensor : public Node
+{
+  public:
+    Sensor(CrowApp& app) :
+        Node(app, "/redfish/v1/Chassis/<str>/Sensors/<str>/", std::string(),
+             std::string())
+    {
+        entityPrivileges = {
+            {boost::beast::http::verb::get, {{"Login"}}},
+            {boost::beast::http::verb::head, {{"Login"}}},
+            {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
+            {boost::beast::http::verb::put, {{"ConfigureManager"}}},
+            {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
+            {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
+    }
+
+  private:
+    void doGet(crow::Response& res, const crow::Request& req,
+               const std::vector<std::string>& params) override
+    {
+        BMCWEB_LOG_DEBUG << "Sensor doGet enter";
+        if (params.size() != 2)
+        {
+            BMCWEB_LOG_DEBUG << "Sensor doGet param size < 2";
+            messages::internalError(res);
+            res.end();
+            return;
+        }
+        const std::string& chassisId = params[0];
+        std::shared_ptr<SensorsAsyncResp> asyncResp =
+            std::make_shared<SensorsAsyncResp>(
+                res, chassisId, std::vector<const char*>(), "Sensors");
+
+        const std::string& sensorName = params[1];
+        const std::array<const char*, 1> interfaces = {
+            "xyz.openbmc_project.Sensor.Value"};
+
+        // Get a list of all of the sensors that implement Sensor.Value
+        // and get the path and service name associated with the sensor
+        crow::connections::systemBus->async_method_call(
+            [asyncResp, sensorName](const boost::system::error_code ec,
+                                    const GetSubTreeType& subtree) {
+                BMCWEB_LOG_DEBUG << "respHandler1 enter";
+                if (ec)
+                {
+                    messages::internalError(asyncResp->res);
+                    BMCWEB_LOG_ERROR << "Sensor getSensorPaths resp_handler: "
+                                     << "Dbus error " << ec;
+                    return;
+                }
+
+                GetSubTreeType::const_iterator it = std::find_if(
+                    subtree.begin(), subtree.end(),
+                    [sensorName](
+                        const std::pair<
+                            std::string,
+                            std::vector<std::pair<std::string,
+                                                  std::vector<std::string>>>>&
+                            object) {
+                        std::string_view sensor = object.first;
+                        std::size_t lastPos = sensor.rfind("/");
+                        if (lastPos == std::string::npos ||
+                            lastPos + 1 >= sensor.size())
+                        {
+                            BMCWEB_LOG_ERROR << "Invalid sensor path: "
+                                             << sensor;
+                            return false;
+                        }
+                        std::string_view name = sensor.substr(lastPos + 1);
+
+                        return name == sensorName;
+                    });
+
+                if (it == subtree.end())
+                {
+                    BMCWEB_LOG_ERROR << "Could not find path for sensor: "
+                                     << sensorName;
+                    messages::resourceNotFound(asyncResp->res, "Sensor",
+                                               sensorName);
+                    return;
+                }
+                std::string_view sensorPath = (*it).first;
+                BMCWEB_LOG_DEBUG << "Found sensor path for sensor '"
+                                 << sensorName << "': " << sensorPath;
+
+                const std::shared_ptr<boost::container::flat_set<std::string>>
+                    sensorList = std::make_shared<
+                        boost::container::flat_set<std::string>>();
+
+                sensorList->emplace(sensorPath);
+                processSensorList(asyncResp, sensorList);
+                BMCWEB_LOG_DEBUG << "respHandler1 exit";
+            },
+            "xyz.openbmc_project.ObjectMapper",
+            "/xyz/openbmc_project/object_mapper",
+            "xyz.openbmc_project.ObjectMapper", "GetSubTree",
+            "/xyz/openbmc_project/sensors", 2, interfaces);
+    }
+};
+
 } // namespace redfish