Implement TemperatureReadingsCelsius property for ThermalMetrics

The ThermalMetrics schema[1] provides for efficient thermal metric
gathering for thermal sensors. The schema allows retrieving just the
thermal metrics with one Redfish URI. This prevents the additional work
required when returning all the sensor data, or multiple Redfish URI
calls to retrieve the properties for all of the thermal sensors.

This commit implements the TemperatureReadingsCelsius property of
ThermalMetrics[1]. ThermalMetrics is a property of ThermalSubsystem[2].
TemperatureReadingsCelsius is a SensorArrayExcerpt[3].

[1] https://redfish.dmtf.org/schemas/v1/ThermalMetrics.v1_0_1.json
[2] https://redfish.dmtf.org/schemas/v1/ThermalSubsystem.v1_3_2.json
[3] http://redfish.dmtf.org/schemas/v1/Sensor.v1_9_0.json#/definitions/SensorArrayExcerpt

The temperature sensors are found by finding 'all_sensors' endpoints for
specific chassis of D-Bus service
/xyz/openbmc_project/sensors/temperature. An entry of SensorArrayExcerpt
is built for each temperature sensor retrieved.

Implementation Notes:
 - Common function sensor_utils::objectPropertiesToJson() is used to
   fill in sensor excerpt properties. Currently the only excerpt
   ChassisSubNode is ThermalMetrics. However there are others excerpts
   defined by Redfish. Right now mostly this is just skipping things,
   but I'm expecting when other sensor excerpts are implemented that
   some of the other properties may be added for excerpts as well. I'm
   expecting the combination of the chassisSubNode and the sensorType
   will be used to determine which properties are included for a
   particular call to build a sensor Json representation.
 - New sensor_utils::objectExcerptToJson() function created. This wraps
   sensor_utils::objectPropertiesToJson() and builds DataSourceUri for a
   sensor excerpt.
 - New sensor_utils::getAllSensorObjects() function created. This builds
   list of 'all_sensors' association endpoints for specified D-Bus path
   with specified D-Bus interfaces. Callback function is called with
   list for handling sensors.

Tested:
1. Redfish Service Validator passed.
2. doGet method:
```
curl -k -H "X-Auth-Token: ${token}" -X GET https://${bmc}/redfish/v1/Chassis/chassis/ThermalSubsystem/ThermalMetrics
{
  "@odata.id": "/redfish/v1/Chassis/chassis/ThermalSubsystem/ThermalMetrics",
  "@odata.type": "#ThermalMetrics.v1_0_1.ThermalMetrics",
  "Id": "ThermalMetrics",
  "Name": "Thermal Metrics",
  "TemperatureReadingsCelsius": [
    {
      "DataSourceUri": "/redfish/v1/Chassis/chassis/Sensors/temperature_ps0_temp0",
      "Reading": -131072000.0
    },
    {
      "DataSourceUri": "/redfish/v1/Chassis/chassis/Sensors/temperature_ps0_temp1",
      "Reading": -131072000.0
    },
    {
      "DataSourceUri": "/redfish/v1/Chassis/chassis/Sensors/temperature_ps0_temp2",
      "Reading": -131072000.0
    },
    {
      "DataSourceUri": "/redfish/v1/Chassis/chassis/Sensors/temperature_ps1_temp0",
      "Reading": -131072000.0
    },
    {
      "DataSourceUri": "/redfish/v1/Chassis/chassis/Sensors/temperature_ps1_temp1",
      "Reading": -131072000.0
    },
    {
      "DataSourceUri": "/redfish/v1/Chassis/chassis/Sensors/temperature_ps1_temp2",
      "Reading": -131072000.0
    }
  ],
  "TemperatureReadingsCelsius@odata.count": 6
}
```

3. Verification of DataSourceUri:
```
curl -k -H "X-Auth-Token: ${token}" -X GET https://${bmc}/redfish/v1/Chassis/chassis/Sensors/temperature_ps1_temp0
{
  "@odata.id": "/redfish/v1/Chassis/chassis/Sensors/temperature_ps1_temp0",
  "@odata.type": "#Sensor.v1_2_0.Sensor",
  "Id": "temperature_ps1_temp0",
  "Name": "ps1 temp0",
  "Reading": -131072000.0,
  "ReadingType": "Temperature",
  "ReadingUnits": "Cel",
  "Status": {
    "Health": "OK",
    "State": "Enabled"
  }
}
```

4. A bad chassis ID:
```
curl -k -H "X-Auth-Token: ${token}" -X GET https://${bmc}/redfish/v1/Chassis/chassisBAD/ThermalSubsystem/ThermalMetrics
{
  "error": {
    "@Message.ExtendedInfo": [
      {
        "@odata.type": "#Message.v1_1_1.Message",
        "Message": "The requested resource of type Chassis named 'chassisBAD' was not found.",
        "MessageArgs": [
          "Chassis",
          "chassisBAD"
        ],
        "MessageId": "Base.1.18.1.ResourceNotFound",
        "MessageSeverity": "Critical",
        "Resolution": "Provide a valid resource identifier and resubmit the request."
      }
    ],
    "code": "Base.1.18.1.ResourceNotFound",
    "message": "The requested resource of type Chassis named 'chassisBAD' was not found."
  }
}
```

Signed-off-by: George Liu <liuxiwei@ieisystem.com>
Change-Id: I6e4ed1f281fd5371c978983b6cc5666badd3752c
Signed-off-by: Janet Adkins <janeta@us.ibm.com>
diff --git a/Redfish.md b/Redfish.md
index 2ffaa5e..daf8e82 100644
--- a/Redfish.md
+++ b/Redfish.md
@@ -349,6 +349,10 @@
 
 ##### ThermalMetrics
 
+- TemperatureReadingsCelsius[]/DataSourceUri
+- TemperatureReadingsCelsius[]/Reading
+- TemperatureReadingsCelsius@odata.count
+
 #### /redfish/v1/Chassis/{ChassisId}/ThermalSubsystem/Fans
 
 ##### FansCollection
diff --git a/redfish-core/include/utils/sensor_utils.hpp b/redfish-core/include/utils/sensor_utils.hpp
index f3d1e56..64d1c8a 100644
--- a/redfish-core/include/utils/sensor_utils.hpp
+++ b/redfish-core/include/utils/sensor_utils.hpp
@@ -8,10 +8,13 @@
 #include "utils/dbus_utils.hpp"
 #include "utils/json_utils.hpp"
 
+#include <boost/url/format.hpp>
 #include <sdbusplus/unpack_properties.hpp>
 
 #include <algorithm>
 #include <format>
+#include <functional>
+#include <optional>
 #include <ranges>
 #include <string>
 #include <string_view>
@@ -29,6 +32,7 @@
     powerNode,
     sensorsNode,
     thermalNode,
+    thermalMetricsNode,
     unknownNode,
 };
 
@@ -42,6 +46,8 @@
             return "Sensors";
         case ChassisSubNode::thermalNode:
             return "Thermal";
+        case ChassisSubNode::thermalMetricsNode:
+            return "ThermalMetrics";
         case ChassisSubNode::unknownNode:
         default:
             return "";
@@ -65,10 +71,19 @@
     {
         subNode = ChassisSubNode::thermalNode;
     }
+    else if (subNodeStr == "ThermalMetrics")
+    {
+        subNode = ChassisSubNode::thermalMetricsNode;
+    }
 
     return subNode;
 }
 
+inline bool isExcerptNode(const ChassisSubNode subNode)
+{
+    return (subNode == ChassisSubNode::thermalMetricsNode);
+}
+
 /**
  * Possible states for physical inventory leds
  */
@@ -378,133 +393,147 @@
     const dbus::utility::DBusPropertiesMap& propertiesDict,
     nlohmann::json& sensorJson, InventoryItem* inventoryItem)
 {
-    if (chassisSubNode == ChassisSubNode::sensorsNode)
-    {
-        std::string subNodeEscaped = getSensorId(sensorName, sensorType);
-        // For sensors in SensorCollection we set Id instead of MemberId,
-        // including power sensors.
-        sensorJson["Id"] = std::move(subNodeEscaped);
-
-        std::string sensorNameEs(sensorName);
-        std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
-        sensorJson["Name"] = std::move(sensorNameEs);
-    }
-    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.
-        std::string sensorNameEs(sensorName);
-        std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
-        sensorJson["Name"] = std::move(sensorNameEs);
-    }
-
-    const bool* checkAvailable = nullptr;
-    bool available = true;
-    const bool success = sdbusplus::unpackPropertiesNoThrow(
-        dbus_utils::UnpackErrorPrinter(), propertiesDict, "Available",
-        checkAvailable);
-    if (!success)
-    {
-        messages::internalError();
-    }
-    if (checkAvailable != nullptr)
-    {
-        available = *checkAvailable;
-    }
-
-    sensorJson["Status"]["State"] = getState(inventoryItem, available);
-    sensorJson["Status"]["Health"] =
-        getHealth(sensorJson, propertiesDict, inventoryItem);
-
     // Parameter to set to override the type we get from dbus, and force it to
     // int, regardless of what is available.  This is used for schemas like fan,
     // that require integers, not floats.
     bool forceToInt = false;
 
     nlohmann::json::json_pointer unit("/Reading");
-    if (chassisSubNode == ChassisSubNode::sensorsNode)
-    {
-        sensorJson["@odata.type"] = "#Sensor.v1_2_0.Sensor";
 
-        sensor::ReadingType readingType = sensors::toReadingType(sensorType);
-        if (readingType == sensor::ReadingType::Invalid)
+    // This ChassisSubNode builds sensor excerpts
+    bool isExcerpt = isExcerptNode(chassisSubNode);
+
+    /* Sensor excerpts use different keys to reference the sensor. These are
+     * built by the caller.
+     * Additionally they don't include these additional properties.
+     */
+    if (!isExcerpt)
+    {
+        if (chassisSubNode == ChassisSubNode::sensorsNode)
         {
-            BMCWEB_LOG_ERROR("Redfish cannot map reading type for {}",
-                             sensorType);
+            std::string subNodeEscaped = getSensorId(sensorName, sensorType);
+            // For sensors in SensorCollection we set Id instead of MemberId,
+            // including power sensors.
+            sensorJson["Id"] = std::move(subNodeEscaped);
+
+            std::string sensorNameEs(sensorName);
+            std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
+            sensorJson["Name"] = std::move(sensorNameEs);
         }
-        else
+        else if (sensorType != "power")
         {
-            sensorJson["ReadingType"] = readingType;
+            // 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.
+            std::string sensorNameEs(sensorName);
+            std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
+            sensorJson["Name"] = std::move(sensorNameEs);
         }
 
-        std::string_view readingUnits = sensors::toReadingUnits(sensorType);
-        if (readingUnits.empty())
+        const bool* checkAvailable = nullptr;
+        bool available = true;
+        const bool success = sdbusplus::unpackPropertiesNoThrow(
+            dbus_utils::UnpackErrorPrinter(), propertiesDict, "Available",
+            checkAvailable);
+        if (!success)
         {
-            BMCWEB_LOG_ERROR("Redfish cannot map reading unit for {}",
-                             sensorType);
+            messages::internalError();
+        }
+        if (checkAvailable != nullptr)
+        {
+            available = *checkAvailable;
+        }
+
+        sensorJson["Status"]["State"] = getState(inventoryItem, available);
+        sensorJson["Status"]["Health"] =
+            getHealth(sensorJson, propertiesDict, inventoryItem);
+
+        if (chassisSubNode == ChassisSubNode::sensorsNode)
+        {
+            sensorJson["@odata.type"] = "#Sensor.v1_2_0.Sensor";
+
+            sensor::ReadingType readingType =
+                sensors::toReadingType(sensorType);
+            if (readingType == sensor::ReadingType::Invalid)
+            {
+                BMCWEB_LOG_ERROR("Redfish cannot map reading type for {}",
+                                 sensorType);
+            }
+            else
+            {
+                sensorJson["ReadingType"] = readingType;
+            }
+
+            std::string_view readingUnits = sensors::toReadingUnits(sensorType);
+            if (readingUnits.empty())
+            {
+                BMCWEB_LOG_ERROR("Redfish cannot map reading unit for {}",
+                                 sensorType);
+            }
+            else
+            {
+                sensorJson["ReadingUnits"] = readingUnits;
+            }
+        }
+        else if (sensorType == "temperature")
+        {
+            unit = "/ReadingCelsius"_json_pointer;
+            sensorJson["@odata.type"] = "#Thermal.v1_3_0.Temperature";
+            // TODO(ed) Documentation says that path should be type fan_tach,
+            // implementation seems to implement fan
+        }
+        else if (sensorType == "fan" || sensorType == "fan_tach")
+        {
+            unit = "/Reading"_json_pointer;
+            sensorJson["ReadingUnits"] = thermal::ReadingUnits::RPM;
+            sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
+            setLedState(sensorJson, inventoryItem);
+            forceToInt = true;
+        }
+        else if (sensorType == "fan_pwm")
+        {
+            unit = "/Reading"_json_pointer;
+            sensorJson["ReadingUnits"] = thermal::ReadingUnits::Percent;
+            sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
+            setLedState(sensorJson, inventoryItem);
+            forceToInt = true;
+        }
+        else if (sensorType == "voltage")
+        {
+            unit = "/ReadingVolts"_json_pointer;
+            sensorJson["@odata.type"] = "#Power.v1_0_0.Voltage";
+        }
+        else if (sensorType == "power")
+        {
+            std::string lower;
+            std::ranges::transform(sensorName, std::back_inserter(lower),
+                                   bmcweb::asciiToLower);
+            if (lower == "total_power")
+            {
+                sensorJson["@odata.type"] = "#Power.v1_0_0.PowerControl";
+                // Put multiple "sensors" into a single PowerControl, so have
+                // generic names for MemberId and Name. Follows Redfish mockup.
+                sensorJson["MemberId"] = "0";
+                sensorJson["Name"] = "Chassis Power Control";
+                unit = "/PowerConsumedWatts"_json_pointer;
+            }
+            else if (lower.find("input") != std::string::npos)
+            {
+                unit = "/PowerInputWatts"_json_pointer;
+            }
+            else
+            {
+                unit = "/PowerOutputWatts"_json_pointer;
+            }
         }
         else
         {
-            sensorJson["ReadingUnits"] = readingUnits;
+            BMCWEB_LOG_ERROR("Redfish cannot map object type for {}",
+                             sensorName);
+            return;
         }
     }
-    else if (sensorType == "temperature")
-    {
-        unit = "/ReadingCelsius"_json_pointer;
-        sensorJson["@odata.type"] = "#Thermal.v1_3_0.Temperature";
-        // TODO(ed) Documentation says that path should be type fan_tach,
-        // implementation seems to implement fan
-    }
-    else if (sensorType == "fan" || sensorType == "fan_tach")
-    {
-        unit = "/Reading"_json_pointer;
-        sensorJson["ReadingUnits"] = thermal::ReadingUnits::RPM;
-        sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
-        setLedState(sensorJson, inventoryItem);
-        forceToInt = true;
-    }
-    else if (sensorType == "fan_pwm")
-    {
-        unit = "/Reading"_json_pointer;
-        sensorJson["ReadingUnits"] = thermal::ReadingUnits::Percent;
-        sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
-        setLedState(sensorJson, inventoryItem);
-        forceToInt = true;
-    }
-    else if (sensorType == "voltage")
-    {
-        unit = "/ReadingVolts"_json_pointer;
-        sensorJson["@odata.type"] = "#Power.v1_0_0.Voltage";
-    }
-    else if (sensorType == "power")
-    {
-        std::string lower;
-        std::ranges::transform(sensorName, std::back_inserter(lower),
-                               bmcweb::asciiToLower);
-        if (lower == "total_power")
-        {
-            sensorJson["@odata.type"] = "#Power.v1_0_0.PowerControl";
-            // Put multiple "sensors" into a single PowerControl, so have
-            // generic names for MemberId and Name. Follows Redfish mockup.
-            sensorJson["MemberId"] = "0";
-            sensorJson["Name"] = "Chassis Power Control";
-            unit = "/PowerConsumedWatts"_json_pointer;
-        }
-        else if (lower.find("input") != std::string::npos)
-        {
-            unit = "/PowerInputWatts"_json_pointer;
-        }
-        else
-        {
-            unit = "/PowerOutputWatts"_json_pointer;
-        }
-    }
-    else
-    {
-        BMCWEB_LOG_ERROR("Redfish cannot map object type for {}", sensorName);
-        return;
-    }
+
     // Map of dbus interface name, dbus property name and redfish property_name
     std::vector<
         std::tuple<const char*, const char*, nlohmann::json::json_pointer>>
@@ -512,68 +541,77 @@
 
     properties.emplace_back("xyz.openbmc_project.Sensor.Value", "Value", unit);
 
-    if (chassisSubNode == ChassisSubNode::sensorsNode)
+    if (!isExcerpt)
     {
-        properties.emplace_back(
-            "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningHigh",
-            "/Thresholds/UpperCaution/Reading"_json_pointer);
-        properties.emplace_back(
-            "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningLow",
-            "/Thresholds/LowerCaution/Reading"_json_pointer);
-        properties.emplace_back(
-            "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalHigh",
-            "/Thresholds/UpperCritical/Reading"_json_pointer);
-        properties.emplace_back(
-            "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalLow",
-            "/Thresholds/LowerCritical/Reading"_json_pointer);
-
-        /* Add additional properties specific to sensorType */
-        if (sensorType == "fan_tach")
+        if (chassisSubNode == ChassisSubNode::sensorsNode)
         {
-            properties.emplace_back("xyz.openbmc_project.Sensor.Value", "Value",
-                                    "/SpeedRPM"_json_pointer);
+            properties.emplace_back(
+                "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningHigh",
+                "/Thresholds/UpperCaution/Reading"_json_pointer);
+            properties.emplace_back(
+                "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningLow",
+                "/Thresholds/LowerCaution/Reading"_json_pointer);
+            properties.emplace_back(
+                "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalHigh",
+                "/Thresholds/UpperCritical/Reading"_json_pointer);
+            properties.emplace_back(
+                "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalLow",
+                "/Thresholds/LowerCritical/Reading"_json_pointer);
+
+            /* Add additional properties specific to sensorType */
+            if (sensorType == "fan_tach")
+            {
+                properties.emplace_back("xyz.openbmc_project.Sensor.Value",
+                                        "Value", "/SpeedRPM"_json_pointer);
+            }
         }
-    }
-    else if (sensorType != "power")
-    {
-        properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Warning",
-                                "WarningHigh",
-                                "/UpperThresholdNonCritical"_json_pointer);
-        properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Warning",
-                                "WarningLow",
-                                "/LowerThresholdNonCritical"_json_pointer);
-        properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Critical",
-                                "CriticalHigh",
-                                "/UpperThresholdCritical"_json_pointer);
-        properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Critical",
-                                "CriticalLow",
-                                "/LowerThresholdCritical"_json_pointer);
-    }
+        else if (sensorType != "power")
+        {
+            properties.emplace_back(
+                "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningHigh",
+                "/UpperThresholdNonCritical"_json_pointer);
+            properties.emplace_back(
+                "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningLow",
+                "/LowerThresholdNonCritical"_json_pointer);
+            properties.emplace_back(
+                "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalHigh",
+                "/UpperThresholdCritical"_json_pointer);
+            properties.emplace_back(
+                "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalLow",
+                "/LowerThresholdCritical"_json_pointer);
+        }
 
-    // TODO Need to get UpperThresholdFatal and LowerThresholdFatal
+        // TODO Need to get UpperThresholdFatal and LowerThresholdFatal
 
-    if (chassisSubNode == ChassisSubNode::sensorsNode)
-    {
-        properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
-                                "/ReadingRangeMin"_json_pointer);
-        properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
-                                "/ReadingRangeMax"_json_pointer);
-        properties.emplace_back("xyz.openbmc_project.Sensor.Accuracy",
-                                "Accuracy", "/Accuracy"_json_pointer);
-    }
-    else if (sensorType == "temperature")
-    {
-        properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
-                                "/MinReadingRangeTemp"_json_pointer);
-        properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
-                                "/MaxReadingRangeTemp"_json_pointer);
-    }
-    else if (sensorType != "power")
-    {
-        properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
-                                "/MinReadingRange"_json_pointer);
-        properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
-                                "/MaxReadingRange"_json_pointer);
+        if (chassisSubNode == ChassisSubNode::sensorsNode)
+        {
+            properties.emplace_back("xyz.openbmc_project.Sensor.Value",
+                                    "MinValue",
+                                    "/ReadingRangeMin"_json_pointer);
+            properties.emplace_back("xyz.openbmc_project.Sensor.Value",
+                                    "MaxValue",
+                                    "/ReadingRangeMax"_json_pointer);
+            properties.emplace_back("xyz.openbmc_project.Sensor.Accuracy",
+                                    "Accuracy", "/Accuracy"_json_pointer);
+        }
+        else if (sensorType == "temperature")
+        {
+            properties.emplace_back("xyz.openbmc_project.Sensor.Value",
+                                    "MinValue",
+                                    "/MinReadingRangeTemp"_json_pointer);
+            properties.emplace_back("xyz.openbmc_project.Sensor.Value",
+                                    "MaxValue",
+                                    "/MaxReadingRangeTemp"_json_pointer);
+        }
+        else if (sensorType != "power")
+        {
+            properties.emplace_back("xyz.openbmc_project.Sensor.Value",
+                                    "MinValue",
+                                    "/MinReadingRange"_json_pointer);
+            properties.emplace_back("xyz.openbmc_project.Sensor.Value",
+                                    "MaxValue",
+                                    "/MaxReadingRange"_json_pointer);
+        }
     }
 
     for (const std::tuple<const char*, const char*,
@@ -621,5 +659,103 @@
     }
 }
 
+/**
+ * @brief Builds a json sensor excerpt representation of a sensor.
+ *
+ * @details This is a wrapper function to provide consistent setting of
+ * "DataSourceUri" for sensor excerpts and filling of properties. Since sensor
+ * excerpts usually have just the D-Bus path for the sensor that is accepted
+ * and used to build "DataSourceUri".
+
+ * @param path The D-Bus path to the sensor to be built
+ * @param chassisId The Chassis Id for the sensor
+ * @param chassisSubNode The subnode (e.g. ThermalMetrics) of the sensor
+ * @param sensorTypeExpected The expected type of the sensor
+ * @param propertiesDict A dictionary of the properties to build the sensor
+ * from.
+ * @param sensorJson  The json object to fill
+ * @returns True if sensorJson object filled. False on any error.
+ * Caller is responsible for handling error.
+ */
+inline bool objectExcerptToJson(
+    const std::string& path, const std::string_view chassisId,
+    ChassisSubNode chassisSubNode,
+    const std::optional<std::string>& sensorTypeExpected,
+    const dbus::utility::DBusPropertiesMap& propertiesDict,
+    nlohmann::json& sensorJson)
+{
+    if (!isExcerptNode(chassisSubNode))
+    {
+        BMCWEB_LOG_DEBUG("{} is not a sensor excerpt",
+                         chassisSubNodeToString(chassisSubNode));
+        return false;
+    }
+
+    sdbusplus::message::object_path sensorPath(path);
+    std::string sensorName = sensorPath.filename();
+    std::string sensorType = sensorPath.parent_path().filename();
+    if (sensorName.empty() || sensorType.empty())
+    {
+        BMCWEB_LOG_DEBUG("Invalid sensor path {}", path);
+        return false;
+    }
+
+    if (sensorTypeExpected && (sensorType != *sensorTypeExpected))
+    {
+        BMCWEB_LOG_DEBUG("{} is not expected type {}", path,
+                         *sensorTypeExpected);
+        return false;
+    }
+
+    // Sensor excerpts use DataSourceUri to reference full sensor Redfish path
+    sensorJson["DataSourceUri"] =
+        boost::urls::format("/redfish/v1/Chassis/{}/Sensors/{}", chassisId,
+                            getSensorId(sensorName, sensorType));
+
+    // Fill in sensor excerpt properties
+    objectPropertiesToJson(sensorName, sensorType, chassisSubNode,
+                           propertiesDict, sensorJson, nullptr);
+
+    return true;
+}
+
+// Maps D-Bus: Service, SensorPath
+using SensorServicePathMap = std::pair<std::string, std::string>;
+using SensorServicePathList = std::vector<SensorServicePathMap>;
+
+inline void getAllSensorObjects(
+    const std::string& associatedPath, const std::string& path,
+    std::span<const std::string_view> interfaces, const int32_t depth,
+    std::function<void(const boost::system::error_code& ec,
+                       SensorServicePathList&)>&& callback)
+{
+    sdbusplus::message::object_path endpointPath{associatedPath};
+    endpointPath /= "all_sensors";
+
+    dbus::utility::getAssociatedSubTree(
+        endpointPath, sdbusplus::message::object_path(path), depth, interfaces,
+        [callback = std::move(callback)](
+            const boost::system::error_code& ec,
+            const dbus::utility::MapperGetSubTreeResponse& subtree) {
+            SensorServicePathList sensorsServiceAndPath;
+
+            if (ec)
+            {
+                callback(ec, sensorsServiceAndPath);
+                return;
+            }
+
+            for (const auto& [sensorPath, serviceMaps] : subtree)
+            {
+                for (const auto& [service, mapInterfaces] : serviceMaps)
+                {
+                    sensorsServiceAndPath.emplace_back(service, sensorPath);
+                }
+            }
+
+            callback(ec, sensorsServiceAndPath);
+        });
+}
+
 } // namespace sensor_utils
 } // namespace redfish
diff --git a/redfish-core/lib/thermal_metrics.hpp b/redfish-core/lib/thermal_metrics.hpp
index da4da01..abf296c 100644
--- a/redfish-core/lib/thermal_metrics.hpp
+++ b/redfish-core/lib/thermal_metrics.hpp
@@ -1,17 +1,117 @@
 #pragma once
 
 #include "app.hpp"
+#include "dbus_utility.hpp"
 #include "query.hpp"
 #include "registries/privilege_registry.hpp"
 #include "utils/chassis_utils.hpp"
+#include "utils/json_utils.hpp"
+#include "utils/sensor_utils.hpp"
 
+#include <boost/system/error_code.hpp>
+
+#include <array>
 #include <functional>
 #include <memory>
 #include <optional>
 #include <string>
+#include <string_view>
 
 namespace redfish
 {
+inline void afterGetTemperatureValue(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& chassisId, const std::string& path,
+    const boost::system::error_code& ec,
+    const dbus::utility::DBusPropertiesMap& valuesDict)
+{
+    if (ec)
+    {
+        if (ec.value() != EBADR)
+        {
+            BMCWEB_LOG_ERROR("DBUS response error for getAllProperties {}",
+                             ec.value());
+            messages::internalError(asyncResp->res);
+        }
+        return;
+    }
+
+    nlohmann::json item = nlohmann::json::object();
+
+    /* Don't return an error for a failure to fill in properties from any of
+     * the sensors in the list. Just skip it.
+     */
+    if (sensor_utils::objectExcerptToJson(
+            path, chassisId, sensor_utils::ChassisSubNode::thermalMetricsNode,
+            "temperature", valuesDict, item))
+    {
+        nlohmann::json& temperatureReadings =
+            asyncResp->res.jsonValue["TemperatureReadingsCelsius"];
+        nlohmann::json::array_t* temperatureArray =
+            temperatureReadings.get_ptr<nlohmann::json::array_t*>();
+        if (temperatureArray == nullptr)
+        {
+            BMCWEB_LOG_ERROR("Missing TemperatureReadingsCelsius Json array");
+            messages::internalError(asyncResp->res);
+            return;
+        }
+
+        temperatureArray->emplace_back(std::move(item));
+        asyncResp->res.jsonValue["TemperatureReadingsCelsius@odata.count"] =
+            temperatureArray->size();
+
+        json_util::sortJsonArrayByKey(*temperatureArray, "DataSourceUri");
+    }
+}
+
+inline void handleTemperatureReadingsCelsius(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& chassisId, const boost::system::error_code& ec,
+    const sensor_utils::SensorServicePathList& sensorsServiceAndPath)
+{
+    if (ec)
+    {
+        if (ec.value() != EBADR)
+        {
+            BMCWEB_LOG_ERROR("DBUS response error for getAssociatedSubTree {}",
+                             ec.value());
+            messages::internalError(asyncResp->res);
+        }
+        return;
+    }
+
+    asyncResp->res.jsonValue["TemperatureReadingsCelsius"] =
+        nlohmann::json::array_t();
+    asyncResp->res.jsonValue["TemperatureReadingsCelsius@odata.count"] = 0;
+
+    for (const auto& [service, sensorPath] : sensorsServiceAndPath)
+    {
+        sdbusplus::asio::getAllProperties(
+            *crow::connections::systemBus, service, sensorPath,
+            "xyz.openbmc_project.Sensor.Value",
+            [asyncResp, chassisId,
+             sensorPath](const boost::system::error_code& ec1,
+                         const dbus::utility::DBusPropertiesMap& properties) {
+                afterGetTemperatureValue(asyncResp, chassisId, sensorPath, ec1,
+                                         properties);
+            });
+    }
+}
+
+inline void getTemperatureReadingsCelsius(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& validChassisPath, const std::string& chassisId)
+{
+    constexpr std::array<std::string_view, 1> interfaces = {
+        "xyz.openbmc_project.Sensor.Value"};
+
+    sensor_utils::getAllSensorObjects(
+        validChassisPath, "/xyz/openbmc_project/sensors/temperature",
+        interfaces, 1,
+        std::bind_front(handleTemperatureReadingsCelsius, asyncResp,
+                        chassisId));
+}
+
 inline void
     doThermalMetrics(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
                      const std::string& chassisId,
@@ -32,6 +132,8 @@
         "/redfish/v1/Chassis/{}/ThermalSubsystem/ThermalMetrics", chassisId);
     asyncResp->res.jsonValue["Id"] = "ThermalMetrics";
     asyncResp->res.jsonValue["Name"] = "Thermal Metrics";
+
+    getTemperatureReadingsCelsius(asyncResp, *validChassisPath, chassisId);
 }
 
 inline void handleThermalMetricsHead(
diff --git a/test/redfish-core/include/utils/sensor_utils_test.cpp b/test/redfish-core/include/utils/sensor_utils_test.cpp
index df944f2..e435fdf 100644
--- a/test/redfish-core/include/utils/sensor_utils_test.cpp
+++ b/test/redfish-core/include/utils/sensor_utils_test.cpp
@@ -60,6 +60,9 @@
     subNodeStr = chassisSubNodeToString(ChassisSubNode::thermalNode);
     EXPECT_EQ(subNodeStr, "Thermal");
 
+    subNodeStr = chassisSubNodeToString(ChassisSubNode::thermalMetricsNode);
+    EXPECT_EQ(subNodeStr, "ThermalMetrics");
+
     subNodeStr = chassisSubNodeToString(ChassisSubNode::unknownNode);
     EXPECT_EQ(subNodeStr, "");
 }
@@ -77,6 +80,9 @@
     subNode = chassisSubNodeFromString("Thermal");
     EXPECT_EQ(subNode, ChassisSubNode::thermalNode);
 
+    subNode = chassisSubNodeFromString("ThermalMetrics");
+    EXPECT_EQ(subNode, ChassisSubNode::thermalMetricsNode);
+
     subNode = chassisSubNodeFromString("BadNode");
     EXPECT_EQ(subNode, ChassisSubNode::unknownNode);
 
@@ -84,5 +90,18 @@
     EXPECT_EQ(subNode, ChassisSubNode::unknownNode);
 }
 
+TEST(IsExcerptNode, True)
+{
+    EXPECT_TRUE(isExcerptNode(ChassisSubNode::thermalMetricsNode));
+}
+
+TEST(IsExcerptNode, False)
+{
+    EXPECT_FALSE(isExcerptNode(ChassisSubNode::sensorsNode));
+    EXPECT_FALSE(isExcerptNode(ChassisSubNode::powerNode));
+    EXPECT_FALSE(isExcerptNode(ChassisSubNode::thermalNode));
+    EXPECT_FALSE(isExcerptNode(ChassisSubNode::unknownNode));
+}
+
 } // namespace
 } // namespace redfish::sensor_utils