xref: /openbmc/bmcweb/redfish-core/include/utils/sensor_utils.hpp (revision 378f1d6138791d1d936ae3bc2114ddcafe124ed0)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #pragma once
4 
5 #include "bmcweb_config.h"
6 
7 #include "async_resp.hpp"
8 #include "dbus_utility.hpp"
9 #include "error_messages.hpp"
10 #include "generated/enums/resource.hpp"
11 #include "generated/enums/sensor.hpp"
12 #include "generated/enums/thermal.hpp"
13 #include "logging.hpp"
14 #include "str_utility.hpp"
15 #include "utils/dbus_utils.hpp"
16 
17 #include <asm-generic/errno.h>
18 
19 #include <boost/url/format.hpp>
20 #include <nlohmann/json.hpp>
21 #include <sdbusplus/message/native_types.hpp>
22 #include <sdbusplus/unpack_properties.hpp>
23 
24 #include <algorithm>
25 #include <cmath>
26 #include <cstddef>
27 #include <cstdint>
28 #include <format>
29 #include <functional>
30 #include <iterator>
31 #include <memory>
32 #include <optional>
33 #include <ranges>
34 #include <set>
35 #include <span>
36 #include <string>
37 #include <string_view>
38 #include <tuple>
39 #include <utility>
40 #include <variant>
41 #include <vector>
42 
43 namespace redfish
44 {
45 namespace sensor_utils
46 {
47 
48 enum class ChassisSubNode
49 {
50     environmentMetricsNode,
51     powerNode,
52     sensorsNode,
53     thermalNode,
54     thermalMetricsNode,
55     unknownNode,
56 };
57 
chassisSubNodeToString(ChassisSubNode subNode)58 constexpr std::string_view chassisSubNodeToString(ChassisSubNode subNode)
59 {
60     switch (subNode)
61     {
62         case ChassisSubNode::environmentMetricsNode:
63             return "EnvironmentMetrics";
64         case ChassisSubNode::powerNode:
65             return "Power";
66         case ChassisSubNode::sensorsNode:
67             return "Sensors";
68         case ChassisSubNode::thermalNode:
69             return "Thermal";
70         case ChassisSubNode::thermalMetricsNode:
71             return "ThermalMetrics";
72         case ChassisSubNode::unknownNode:
73         default:
74             return "";
75     }
76 }
77 
chassisSubNodeFromString(const std::string & subNodeStr)78 inline ChassisSubNode chassisSubNodeFromString(const std::string& subNodeStr)
79 {
80     // If none match unknownNode is returned
81     ChassisSubNode subNode = ChassisSubNode::unknownNode;
82 
83     if (subNodeStr == "EnvironmentMetrics")
84     {
85         subNode = ChassisSubNode::environmentMetricsNode;
86     }
87     else if (subNodeStr == "Power")
88     {
89         subNode = ChassisSubNode::powerNode;
90     }
91     else if (subNodeStr == "Sensors")
92     {
93         subNode = ChassisSubNode::sensorsNode;
94     }
95     else if (subNodeStr == "Thermal")
96     {
97         subNode = ChassisSubNode::thermalNode;
98     }
99     else if (subNodeStr == "ThermalMetrics")
100     {
101         subNode = ChassisSubNode::thermalMetricsNode;
102     }
103 
104     return subNode;
105 }
106 
isExcerptNode(const ChassisSubNode subNode)107 inline bool isExcerptNode(const ChassisSubNode subNode)
108 {
109     return ((subNode == ChassisSubNode::thermalMetricsNode) ||
110             (subNode == ChassisSubNode::environmentMetricsNode));
111 }
112 
113 /**
114  * Possible states for physical inventory leds
115  */
116 enum class LedState
117 {
118     OFF,
119     ON,
120     BLINK,
121     UNKNOWN
122 };
123 
124 /**
125  * D-Bus inventory item associated with one or more sensors.
126  */
127 class InventoryItem
128 {
129   public:
InventoryItem(const std::string & objPath)130     explicit InventoryItem(const std::string& objPath) : objectPath(objPath)
131     {
132         // Set inventory item name to last node of object path
133         sdbusplus::message::object_path path(objectPath);
134         name = path.filename();
135         if (name.empty())
136         {
137             BMCWEB_LOG_ERROR("Failed to find '/' in {}", objectPath);
138         }
139     }
140 
141     std::string objectPath;
142     std::string name;
143     bool isPresent = true;
144     bool isFunctional = true;
145     bool isPowerSupply = false;
146     int powerSupplyEfficiencyPercent = -1;
147     std::string manufacturer;
148     std::string model;
149     std::string partNumber;
150     std::string serialNumber;
151     std::set<std::string> sensors;
152     std::string ledObjectPath;
153     LedState ledState = LedState::UNKNOWN;
154 };
155 
getSensorId(std::string_view sensorName,std::string_view sensorType)156 inline std::string getSensorId(std::string_view sensorName,
157                                std::string_view sensorType)
158 {
159     std::string normalizedType(sensorType);
160     auto remove = std::ranges::remove(normalizedType, '_');
161     normalizedType.erase(std::ranges::begin(remove), normalizedType.end());
162 
163     return std::format("{}_{}", normalizedType, sensorName);
164 }
165 
splitSensorNameAndType(std::string_view sensorId)166 inline std::pair<std::string, std::string> splitSensorNameAndType(
167     std::string_view sensorId)
168 {
169     size_t index = sensorId.find('_');
170     if (index == std::string::npos)
171     {
172         return std::make_pair<std::string, std::string>("", "");
173     }
174     std::string sensorType{sensorId.substr(0, index)};
175     std::string sensorName{sensorId.substr(index + 1)};
176     // fan_pwm and fan_tach need special handling
177     if (sensorType == "fantach" || sensorType == "fanpwm")
178     {
179         sensorType.insert(3, 1, '_');
180     }
181     return std::make_pair(sensorType, sensorName);
182 }
183 
184 namespace sensors
185 {
toReadingUnits(std::string_view sensorType)186 inline std::string_view toReadingUnits(std::string_view sensorType)
187 {
188     if (sensorType == "voltage")
189     {
190         return "V";
191     }
192     if (sensorType == "power")
193     {
194         return "W";
195     }
196     if (sensorType == "current")
197     {
198         return "A";
199     }
200     if (sensorType == "fan_tach")
201     {
202         return "RPM";
203     }
204     if (sensorType == "temperature")
205     {
206         return "Cel";
207     }
208     if (sensorType == "fan_pwm" || sensorType == "utilization" ||
209         sensorType == "humidity")
210     {
211         return "%";
212     }
213     if (sensorType == "altitude")
214     {
215         return "m";
216     }
217     if (sensorType == "airflow")
218     {
219         return "cft_i/min";
220     }
221     if (sensorType == "energy")
222     {
223         return "J";
224     }
225     if (sensorType == "liquidflow")
226     {
227         return "L/min";
228     }
229     if (sensorType == "pressure")
230     {
231         return "Pa";
232     }
233     return "";
234 }
235 
toReadingType(std::string_view sensorType)236 inline sensor::ReadingType toReadingType(std::string_view sensorType)
237 {
238     if (sensorType == "voltage")
239     {
240         return sensor::ReadingType::Voltage;
241     }
242     if (sensorType == "power")
243     {
244         return sensor::ReadingType::Power;
245     }
246     if (sensorType == "current")
247     {
248         return sensor::ReadingType::Current;
249     }
250     if (sensorType == "fan_tach")
251     {
252         return sensor::ReadingType::Rotational;
253     }
254     if (sensorType == "temperature")
255     {
256         return sensor::ReadingType::Temperature;
257     }
258     if (sensorType == "fan_pwm" || sensorType == "utilization")
259     {
260         return sensor::ReadingType::Percent;
261     }
262     if (sensorType == "humidity")
263     {
264         return sensor::ReadingType::Humidity;
265     }
266     if (sensorType == "altitude")
267     {
268         return sensor::ReadingType::Altitude;
269     }
270     if (sensorType == "airflow")
271     {
272         return sensor::ReadingType::AirFlow;
273     }
274     if (sensorType == "energy")
275     {
276         return sensor::ReadingType::EnergyJoules;
277     }
278     if (sensorType == "liquidflow")
279     {
280         return sensor::ReadingType::LiquidFlowLPM;
281     }
282     if (sensorType == "pressure")
283     {
284         return sensor::ReadingType::PressurePa;
285     }
286     return sensor::ReadingType::Invalid;
287 }
288 
289 } // namespace sensors
290 
291 /**
292  * @brief Returns the Redfish State value for the specified inventory item.
293  * @param inventoryItem D-Bus inventory item associated with a sensor.
294  * @param sensorAvailable Boolean representing if D-Bus sensor is marked as
295  * available.
296  * @return State value for inventory item.
297  */
getState(const InventoryItem * inventoryItem,const bool sensorAvailable)298 inline resource::State getState(const InventoryItem* inventoryItem,
299                                 const bool sensorAvailable)
300 {
301     if ((inventoryItem != nullptr) && !(inventoryItem->isPresent))
302     {
303         return resource::State::Absent;
304     }
305 
306     if (!sensorAvailable)
307     {
308         return resource::State::UnavailableOffline;
309     }
310 
311     return resource::State::Enabled;
312 }
313 
314 /**
315  * @brief Returns the Redfish Health value for the specified sensor.
316  * @param sensorJson Sensor JSON object.
317  * @param valuesDict Map of all sensor DBus values.
318  * @param inventoryItem D-Bus inventory item associated with the sensor.  Will
319  * be nullptr if no associated inventory item was found.
320  * @return Health value for sensor.
321  */
getHealth(nlohmann::json & sensorJson,const dbus::utility::DBusPropertiesMap & valuesDict,const InventoryItem * inventoryItem)322 inline std::string getHealth(nlohmann::json& sensorJson,
323                              const dbus::utility::DBusPropertiesMap& valuesDict,
324                              const InventoryItem* inventoryItem)
325 {
326     // Get current health value (if any) in the sensor JSON object.  Some JSON
327     // objects contain multiple sensors (such as PowerSupplies).  We want to set
328     // the overall health to be the most severe of any of the sensors.
329     std::string currentHealth;
330     auto statusIt = sensorJson.find("Status");
331     if (statusIt != sensorJson.end())
332     {
333         auto healthIt = statusIt->find("Health");
334         if (healthIt != statusIt->end())
335         {
336             std::string* health = healthIt->get_ptr<std::string*>();
337             if (health != nullptr)
338             {
339                 currentHealth = *health;
340             }
341         }
342     }
343 
344     // If current health in JSON object is already Critical, return that.  This
345     // should override the sensor health, which might be less severe.
346     if (currentHealth == "Critical")
347     {
348         return "Critical";
349     }
350 
351     const bool* criticalAlarmHigh = nullptr;
352     const bool* criticalAlarmLow = nullptr;
353     const bool* warningAlarmHigh = nullptr;
354     const bool* warningAlarmLow = nullptr;
355 
356     const bool success = sdbusplus::unpackPropertiesNoThrow(
357         dbus_utils::UnpackErrorPrinter(), valuesDict, "CriticalAlarmHigh",
358         criticalAlarmHigh, "CriticalAlarmLow", criticalAlarmLow,
359         "WarningAlarmHigh", warningAlarmHigh, "WarningAlarmLow",
360         warningAlarmLow);
361 
362     if (success)
363     {
364         // Check if sensor has critical threshold alarm
365         if ((criticalAlarmHigh != nullptr && *criticalAlarmHigh) ||
366             (criticalAlarmLow != nullptr && *criticalAlarmLow))
367         {
368             return "Critical";
369         }
370     }
371 
372     // Check if associated inventory item is not functional
373     if ((inventoryItem != nullptr) && !(inventoryItem->isFunctional))
374     {
375         return "Critical";
376     }
377 
378     // If current health in JSON object is already Warning, return that. This
379     // should override the sensor status, which might be less severe.
380     if (currentHealth == "Warning")
381     {
382         return "Warning";
383     }
384 
385     if (success)
386     {
387         // Check if sensor has warning threshold alarm
388         if ((warningAlarmHigh != nullptr && *warningAlarmHigh) ||
389             (warningAlarmLow != nullptr && *warningAlarmLow))
390         {
391             return "Warning";
392         }
393     }
394 
395     return "OK";
396 }
397 
setLedState(nlohmann::json & sensorJson,const InventoryItem * inventoryItem)398 inline void setLedState(nlohmann::json& sensorJson,
399                         const InventoryItem* inventoryItem)
400 {
401     if (inventoryItem != nullptr && !inventoryItem->ledObjectPath.empty())
402     {
403         switch (inventoryItem->ledState)
404         {
405             case LedState::OFF:
406                 sensorJson["IndicatorLED"] = resource::IndicatorLED::Off;
407                 break;
408             case LedState::ON:
409                 sensorJson["IndicatorLED"] = resource::IndicatorLED::Lit;
410                 break;
411             case LedState::BLINK:
412                 sensorJson["IndicatorLED"] = resource::IndicatorLED::Blinking;
413                 break;
414             default:
415                 break;
416         }
417     }
418 }
419 
420 /**
421  * @brief Builds a json sensor representation of a sensor.
422  * @param sensorName  The name of the sensor to be built
423  * @param sensorType  The type (temperature, fan_tach, etc) of the sensor to
424  * build
425  * @param chassisSubNode The subnode (thermal, sensor, etc) of the sensor
426  * @param propertiesDict A dictionary of the properties to build the sensor
427  * from.
428  * @param sensorJson  The json object to fill
429  * @param inventoryItem D-Bus inventory item associated with the sensor.  Will
430  * be nullptr if no associated inventory item was found.
431  */
objectPropertiesToJson(std::string_view sensorName,std::string_view sensorType,ChassisSubNode chassisSubNode,const dbus::utility::DBusPropertiesMap & propertiesDict,nlohmann::json & sensorJson,InventoryItem * inventoryItem)432 inline void objectPropertiesToJson(
433     std::string_view sensorName, std::string_view sensorType,
434     ChassisSubNode chassisSubNode,
435     const dbus::utility::DBusPropertiesMap& propertiesDict,
436     nlohmann::json& sensorJson, InventoryItem* inventoryItem)
437 {
438     // Parameter to set to override the type we get from dbus, and force it to
439     // int, regardless of what is available.  This is used for schemas like fan,
440     // that require integers, not floats.
441     bool forceToInt = false;
442 
443     nlohmann::json::json_pointer unit("/Reading");
444 
445     // This ChassisSubNode builds sensor excerpts
446     bool isExcerpt = isExcerptNode(chassisSubNode);
447 
448     /* Sensor excerpts use different keys to reference the sensor. These are
449      * built by the caller.
450      * Additionally they don't include these additional properties.
451      */
452     if (!isExcerpt)
453     {
454         if (chassisSubNode == ChassisSubNode::sensorsNode)
455         {
456             std::string subNodeEscaped = getSensorId(sensorName, sensorType);
457             // For sensors in SensorCollection we set Id instead of MemberId,
458             // including power sensors.
459             sensorJson["Id"] = std::move(subNodeEscaped);
460 
461             std::string sensorNameEs(sensorName);
462             std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
463             sensorJson["Name"] = std::move(sensorNameEs);
464         }
465         else if (sensorType != "power")
466         {
467             // Set MemberId and Name for non-power sensors.  For PowerSupplies
468             // and PowerControl, those properties have more general values
469             // because multiple sensors can be stored in the same JSON object.
470             std::string sensorNameEs(sensorName);
471             std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
472             sensorJson["Name"] = std::move(sensorNameEs);
473         }
474 
475         const bool* checkAvailable = nullptr;
476         bool available = true;
477         const bool success = sdbusplus::unpackPropertiesNoThrow(
478             dbus_utils::UnpackErrorPrinter(), propertiesDict, "Available",
479             checkAvailable);
480         if (!success)
481         {
482             messages::internalError();
483         }
484         if (checkAvailable != nullptr)
485         {
486             available = *checkAvailable;
487         }
488 
489         sensorJson["Status"]["State"] = getState(inventoryItem, available);
490         sensorJson["Status"]["Health"] =
491             getHealth(sensorJson, propertiesDict, inventoryItem);
492 
493         if (chassisSubNode == ChassisSubNode::sensorsNode)
494         {
495             sensorJson["@odata.type"] = "#Sensor.v1_2_0.Sensor";
496 
497             sensor::ReadingType readingType =
498                 sensors::toReadingType(sensorType);
499             if (readingType == sensor::ReadingType::Invalid)
500             {
501                 BMCWEB_LOG_ERROR("Redfish cannot map reading type for {}",
502                                  sensorType);
503             }
504             else
505             {
506                 sensorJson["ReadingType"] = readingType;
507             }
508 
509             std::string_view readingUnits = sensors::toReadingUnits(sensorType);
510             if (readingUnits.empty())
511             {
512                 BMCWEB_LOG_ERROR("Redfish cannot map reading unit for {}",
513                                  sensorType);
514             }
515             else
516             {
517                 sensorJson["ReadingUnits"] = readingUnits;
518             }
519         }
520         else if (sensorType == "temperature")
521         {
522             unit = "/ReadingCelsius"_json_pointer;
523             sensorJson["@odata.type"] = "#Thermal.v1_3_0.Temperature";
524             // TODO(ed) Documentation says that path should be type fan_tach,
525             // implementation seems to implement fan
526         }
527         else if (sensorType == "fan" || sensorType == "fan_tach")
528         {
529             unit = "/Reading"_json_pointer;
530             sensorJson["ReadingUnits"] = thermal::ReadingUnits::RPM;
531             sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
532             if constexpr (BMCWEB_REDFISH_ALLOW_DEPRECATED_INDICATORLED)
533             {
534                 setLedState(sensorJson, inventoryItem);
535             }
536             forceToInt = true;
537         }
538         else if (sensorType == "fan_pwm")
539         {
540             unit = "/Reading"_json_pointer;
541             sensorJson["ReadingUnits"] = thermal::ReadingUnits::Percent;
542             sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
543             if constexpr (BMCWEB_REDFISH_ALLOW_DEPRECATED_INDICATORLED)
544             {
545                 setLedState(sensorJson, inventoryItem);
546             }
547             forceToInt = true;
548         }
549         else if (sensorType == "voltage")
550         {
551             unit = "/ReadingVolts"_json_pointer;
552             sensorJson["@odata.type"] = "#Power.v1_0_0.Voltage";
553         }
554         else if (sensorType == "power")
555         {
556             std::string lower;
557             std::ranges::transform(sensorName, std::back_inserter(lower),
558                                    bmcweb::asciiToLower);
559             if (lower == "total_power")
560             {
561                 sensorJson["@odata.type"] = "#Power.v1_0_0.PowerControl";
562                 // Put multiple "sensors" into a single PowerControl, so have
563                 // generic names for MemberId and Name. Follows Redfish mockup.
564                 sensorJson["MemberId"] = "0";
565                 sensorJson["Name"] = "Chassis Power Control";
566                 unit = "/PowerConsumedWatts"_json_pointer;
567             }
568             else if (lower.find("input") != std::string::npos)
569             {
570                 unit = "/PowerInputWatts"_json_pointer;
571             }
572             else
573             {
574                 unit = "/PowerOutputWatts"_json_pointer;
575             }
576         }
577         else
578         {
579             BMCWEB_LOG_ERROR("Redfish cannot map object type for {}",
580                              sensorName);
581             return;
582         }
583     }
584 
585     // Map of dbus interface name, dbus property name and redfish property_name
586     std::vector<
587         std::tuple<const char*, const char*, nlohmann::json::json_pointer>>
588         properties;
589 
590     properties.emplace_back("xyz.openbmc_project.Sensor.Value", "Value", unit);
591 
592     if (!isExcerpt)
593     {
594         if (chassisSubNode == ChassisSubNode::sensorsNode)
595         {
596             properties.emplace_back(
597                 "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningHigh",
598                 "/Thresholds/UpperCaution/Reading"_json_pointer);
599             properties.emplace_back(
600                 "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningLow",
601                 "/Thresholds/LowerCaution/Reading"_json_pointer);
602             properties.emplace_back(
603                 "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalHigh",
604                 "/Thresholds/UpperCritical/Reading"_json_pointer);
605             properties.emplace_back(
606                 "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalLow",
607                 "/Thresholds/LowerCritical/Reading"_json_pointer);
608             properties.emplace_back(
609                 "xyz.openbmc_project.Sensor.Threshold.HardShutdown",
610                 "HardShutdownHigh",
611                 "/Thresholds/UpperFatal/Reading"_json_pointer);
612             properties.emplace_back(
613                 "xyz.openbmc_project.Sensor.Threshold.HardShutdown",
614                 "HardShutdownLow",
615                 "/Thresholds/LowerFatal/Reading"_json_pointer);
616 
617             /* Add additional properties specific to sensorType */
618             if (sensorType == "fan_tach")
619             {
620                 properties.emplace_back("xyz.openbmc_project.Sensor.Value",
621                                         "Value", "/SpeedRPM"_json_pointer);
622             }
623         }
624         else if (sensorType != "power")
625         {
626             properties.emplace_back(
627                 "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningHigh",
628                 "/UpperThresholdNonCritical"_json_pointer);
629             properties.emplace_back(
630                 "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningLow",
631                 "/LowerThresholdNonCritical"_json_pointer);
632             properties.emplace_back(
633                 "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalHigh",
634                 "/UpperThresholdCritical"_json_pointer);
635             properties.emplace_back(
636                 "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalLow",
637                 "/LowerThresholdCritical"_json_pointer);
638         }
639 
640         // TODO Need to get UpperThresholdFatal and LowerThresholdFatal
641 
642         if (chassisSubNode == ChassisSubNode::sensorsNode)
643         {
644             properties.emplace_back("xyz.openbmc_project.Sensor.Value",
645                                     "MinValue",
646                                     "/ReadingRangeMin"_json_pointer);
647             properties.emplace_back("xyz.openbmc_project.Sensor.Value",
648                                     "MaxValue",
649                                     "/ReadingRangeMax"_json_pointer);
650             properties.emplace_back("xyz.openbmc_project.Sensor.Accuracy",
651                                     "Accuracy", "/Accuracy"_json_pointer);
652         }
653         else if (sensorType == "temperature")
654         {
655             properties.emplace_back("xyz.openbmc_project.Sensor.Value",
656                                     "MinValue",
657                                     "/MinReadingRangeTemp"_json_pointer);
658             properties.emplace_back("xyz.openbmc_project.Sensor.Value",
659                                     "MaxValue",
660                                     "/MaxReadingRangeTemp"_json_pointer);
661         }
662         else if (sensorType != "power")
663         {
664             properties.emplace_back("xyz.openbmc_project.Sensor.Value",
665                                     "MinValue",
666                                     "/MinReadingRange"_json_pointer);
667             properties.emplace_back("xyz.openbmc_project.Sensor.Value",
668                                     "MaxValue",
669                                     "/MaxReadingRange"_json_pointer);
670         }
671     }
672 
673     for (const std::tuple<const char*, const char*,
674                           nlohmann::json::json_pointer>& p : properties)
675     {
676         for (const auto& [valueName, valueVariant] : propertiesDict)
677         {
678             if (valueName != std::get<1>(p))
679             {
680                 continue;
681             }
682 
683             // The property we want to set may be nested json, so use
684             // a json_pointer for easy indexing into the json structure.
685             const nlohmann::json::json_pointer& key = std::get<2>(p);
686 
687             const double* doubleValue = std::get_if<double>(&valueVariant);
688             if (doubleValue == nullptr)
689             {
690                 BMCWEB_LOG_ERROR("Got value interface that wasn't double");
691                 continue;
692             }
693             if (!std::isfinite(*doubleValue))
694             {
695                 if (valueName == "Value")
696                 {
697                     // Readings are allowed to be NAN for unavailable;  coerce
698                     // them to null in the json response.
699                     sensorJson[key] = nullptr;
700                     continue;
701                 }
702                 BMCWEB_LOG_WARNING("Sensor value for {} was unexpectedly {}",
703                                    valueName, *doubleValue);
704                 continue;
705             }
706             if (forceToInt)
707             {
708                 sensorJson[key] = static_cast<int64_t>(*doubleValue);
709             }
710             else
711             {
712                 sensorJson[key] = *doubleValue;
713             }
714         }
715     }
716 }
717 
718 /**
719  * @brief Builds a json sensor excerpt representation of a sensor.
720  *
721  * @details This is a wrapper function to provide consistent setting of
722  * "DataSourceUri" for sensor excerpts and filling of properties. Since sensor
723  * excerpts usually have just the D-Bus path for the sensor that is accepted
724  * and used to build "DataSourceUri".
725 
726  * @param path The D-Bus path to the sensor to be built
727  * @param chassisId The Chassis Id for the sensor
728  * @param chassisSubNode The subnode (e.g. ThermalMetrics) of the sensor
729  * @param sensorTypeExpected The expected type of the sensor
730  * @param propertiesDict A dictionary of the properties to build the sensor
731  * from.
732  * @param sensorJson  The json object to fill
733  * @returns True if sensorJson object filled. False on any error.
734  * Caller is responsible for handling error.
735  */
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)736 inline bool objectExcerptToJson(
737     const std::string& path, const std::string_view chassisId,
738     ChassisSubNode chassisSubNode,
739     const std::optional<std::string>& sensorTypeExpected,
740     const dbus::utility::DBusPropertiesMap& propertiesDict,
741     nlohmann::json& sensorJson)
742 {
743     if (!isExcerptNode(chassisSubNode))
744     {
745         BMCWEB_LOG_DEBUG("{} is not a sensor excerpt",
746                          chassisSubNodeToString(chassisSubNode));
747         return false;
748     }
749 
750     sdbusplus::message::object_path sensorPath(path);
751     std::string sensorName = sensorPath.filename();
752     std::string sensorType = sensorPath.parent_path().filename();
753     if (sensorName.empty() || sensorType.empty())
754     {
755         BMCWEB_LOG_DEBUG("Invalid sensor path {}", path);
756         return false;
757     }
758 
759     if (sensorTypeExpected && (sensorType != *sensorTypeExpected))
760     {
761         BMCWEB_LOG_DEBUG("{} is not expected type {}", path,
762                          *sensorTypeExpected);
763         return false;
764     }
765 
766     // Sensor excerpts use DataSourceUri to reference full sensor Redfish path
767     sensorJson["DataSourceUri"] =
768         boost::urls::format("/redfish/v1/Chassis/{}/Sensors/{}", chassisId,
769                             getSensorId(sensorName, sensorType));
770 
771     // Fill in sensor excerpt properties
772     objectPropertiesToJson(sensorName, sensorType, chassisSubNode,
773                            propertiesDict, sensorJson, nullptr);
774 
775     return true;
776 }
777 
778 // Maps D-Bus: Service, SensorPath
779 using SensorServicePathMap = std::pair<std::string, std::string>;
780 using SensorServicePathList = std::vector<SensorServicePathMap>;
781 
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)782 inline void getAllSensorObjects(
783     const std::string& associatedPath, const std::string& path,
784     std::span<const std::string_view> interfaces, const int32_t depth,
785     std::function<void(const boost::system::error_code& ec,
786                        SensorServicePathList&)>&& callback)
787 {
788     sdbusplus::message::object_path endpointPath{associatedPath};
789     endpointPath /= "all_sensors";
790 
791     dbus::utility::getAssociatedSubTree(
792         endpointPath, sdbusplus::message::object_path(path), depth, interfaces,
793         [callback = std::move(callback)](
794             const boost::system::error_code& ec,
795             const dbus::utility::MapperGetSubTreeResponse& subtree) {
796             SensorServicePathList sensorsServiceAndPath;
797 
798             if (ec)
799             {
800                 callback(ec, sensorsServiceAndPath);
801                 return;
802             }
803 
804             for (const auto& [sensorPath, serviceMaps] : subtree)
805             {
806                 for (const auto& [service, mapInterfaces] : serviceMaps)
807                 {
808                     sensorsServiceAndPath.emplace_back(service, sensorPath);
809                 }
810             }
811 
812             callback(ec, sensorsServiceAndPath);
813         });
814 }
815 
816 enum class SensorPurpose
817 {
818     totalPower,
819     unknownPurpose,
820 };
821 
sensorPurposeToString(SensorPurpose subNode)822 constexpr std::string_view sensorPurposeToString(SensorPurpose subNode)
823 {
824     if (subNode == SensorPurpose::totalPower)
825     {
826         return "xyz.openbmc_project.Sensor.Purpose.SensorPurpose.TotalPower";
827     }
828 
829     return "Unknown Purpose";
830 }
831 
sensorPurposeFromString(const std::string & subNodeStr)832 inline SensorPurpose sensorPurposeFromString(const std::string& subNodeStr)
833 {
834     // If none match unknownNode is returned
835     SensorPurpose subNode = SensorPurpose::unknownPurpose;
836 
837     if (subNodeStr ==
838         "xyz.openbmc_project.Sensor.Purpose.SensorPurpose.TotalPower")
839     {
840         subNode = SensorPurpose::totalPower;
841     }
842 
843     return subNode;
844 }
845 
checkSensorPurpose(const std::string & serviceName,const std::string & sensorPath,const SensorPurpose sensorPurpose,const std::shared_ptr<SensorServicePathList> & sensorMatches,const std::shared_ptr<boost::system::error_code> & asyncErrors,const boost::system::error_code & ec,const std::vector<std::string> & purposeList)846 inline void checkSensorPurpose(
847     const std::string& serviceName, const std::string& sensorPath,
848     const SensorPurpose sensorPurpose,
849     const std::shared_ptr<SensorServicePathList>& sensorMatches,
850     const std::shared_ptr<boost::system::error_code>& asyncErrors,
851     const boost::system::error_code& ec,
852     const std::vector<std::string>& purposeList)
853 {
854     // If not found this sensor will be skipped but allow to
855     // continue processing remaining sensors
856     if (ec && (ec != boost::system::errc::io_error) && (ec.value() != EBADR))
857     {
858         BMCWEB_LOG_DEBUG("D-Bus response error : {}", ec);
859         *asyncErrors = ec;
860     }
861 
862     if (!ec)
863     {
864         const std::string_view checkPurposeStr =
865             sensorPurposeToString(sensorPurpose);
866 
867         BMCWEB_LOG_DEBUG("checkSensorPurpose: {}", checkPurposeStr);
868 
869         for (const std::string& purposeStr : purposeList)
870         {
871             if (purposeStr == checkPurposeStr)
872             {
873                 // Add to list
874                 BMCWEB_LOG_DEBUG("checkSensorPurpose adding {} {}", serviceName,
875                                  sensorPath);
876                 sensorMatches->emplace_back(serviceName, sensorPath);
877                 return;
878             }
879         }
880     }
881 }
882 
883 /**
884  * @brief Gets sensors from list with specified purpose
885  *
886  * Checks the <sensorListIn> sensors for any which implement the specified
887  * <sensorPurpose> in interface xyz.openbmc_project.Sensor.Purpose. Adds any
888  * matches to the <sensorMatches> list. After checking all sensors in
889  * <sensorListIn> the <callback> is called with just the list of matching
890  * sensors, which could be an empty list. Additionally the <callback> is called
891  * on error and error handling is left to the callback.
892  *
893  * @param asyncResp Response data
894  * @param[in] sensorListIn List of sensors to check
895  * @param[in] sensorPurpose Looks for sensors matching this purpose
896  * @param[out] sensorMatches Sensors from sensorListIn with sensorPurpose
897  * @param[in] callback Callback to handle list of matching sensors
898  */
getSensorsByPurpose(const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const SensorServicePathList & sensorListIn,const SensorPurpose sensorPurpose,const std::shared_ptr<SensorServicePathList> & sensorMatches,const std::function<void (const boost::system::error_code & ec,const std::shared_ptr<SensorServicePathList> & sensorMatches)> & callback)899 inline void getSensorsByPurpose(
900     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
901     const SensorServicePathList& sensorListIn,
902     const SensorPurpose sensorPurpose,
903     const std::shared_ptr<SensorServicePathList>& sensorMatches,
904     const std::function<void(
905         const boost::system::error_code& ec,
906         const std::shared_ptr<SensorServicePathList>& sensorMatches)>& callback)
907 {
908     /* Keeps track of number of asynchronous calls made. Once all handlers have
909      * been called the callback function is called with the results.
910      */
911     std::shared_ptr<int> remainingSensorsToVist =
912         std::make_shared<int>(sensorListIn.size());
913 
914     /* Holds last unrecoverable error returned by any of the asynchronous
915      * handlers. Once all handlers have been called the callback will be sent
916      * the error to handle.
917      */
918     std::shared_ptr<boost::system::error_code> asyncErrors =
919         std::make_shared<boost::system::error_code>();
920 
921     BMCWEB_LOG_DEBUG("getSensorsByPurpose enter {}", *remainingSensorsToVist);
922 
923     for (const auto& [serviceName, sensorPath] : sensorListIn)
924     {
925         dbus::utility::getProperty<std::vector<std::string>>(
926             serviceName, sensorPath, "xyz.openbmc_project.Sensor.Purpose",
927             "Purpose",
928             [asyncResp, serviceName, sensorPath, sensorPurpose, sensorMatches,
929              callback, remainingSensorsToVist,
930              asyncErrors](const boost::system::error_code& ec,
931                           const std::vector<std::string>& purposeList) {
932                 // Keep track of sensor visited
933                 (*remainingSensorsToVist)--;
934                 BMCWEB_LOG_DEBUG("Visited {}. Remaining sensors {}", sensorPath,
935                                  *remainingSensorsToVist);
936 
937                 checkSensorPurpose(serviceName, sensorPath, sensorPurpose,
938                                    sensorMatches, asyncErrors, ec, purposeList);
939 
940                 // All sensors have been visited
941                 if (*remainingSensorsToVist == 0)
942                 {
943                     BMCWEB_LOG_DEBUG(
944                         "getSensorsByPurpose, exit found {} matches",
945                         sensorMatches->size());
946                     callback(*asyncErrors, sensorMatches);
947                     return;
948                 }
949             });
950     }
951 }
952 
953 } // namespace sensor_utils
954 } // namespace redfish
955