xref: /openbmc/bmcweb/redfish-core/include/utils/sensor_utils.hpp (revision 1aa94df4cb3b7ab864a4c5fb009538c259c2801e)
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 // represents metric Id, metadata, reading value and timestamp of single
292 // reading update in milliseconds
293 using Reading = std::tuple<std::string, std::string, double, uint64_t>;
294 // represents multiple independent readings
295 using Readings = std::vector<Reading>;
296 // represents a timestamp and multiple independent readings
297 using Statistics = std::tuple<uint64_t, Readings>;
298 // represents sensor's path, its metadata
299 using SensorPaths =
300     std::vector<std::tuple<sdbusplus::message::object_path, std::string>>;
301 // represents reading parameters for statistics readings
302 using ReadingParameters =
303     std::vector<std::tuple<SensorPaths, std::string, std::string, uint64_t>>;
304 
updateSensorStatistics(nlohmann::json & sensorJson,const std::optional<Statistics> & statistics,const std::optional<ReadingParameters> & readingParameters)305 inline void updateSensorStatistics(
306     nlohmann::json& sensorJson, const std::optional<Statistics>& statistics,
307     const std::optional<ReadingParameters>& readingParameters)
308 {
309     if (statistics.has_value() && readingParameters.has_value())
310     {
311         Readings metrics = std::get<1>(*statistics);
312         for (const auto& [sensorPaths, operationType, metricId, duration] :
313              *readingParameters)
314         {
315             if (operationType ==
316                 "xyz.openbmc_project.Telemetry.Report.OperationType.Maximum")
317             {
318                 if (metrics.size() == 1)
319                 {
320                     const auto& [id, metadata, value, timestamp] = metrics[0];
321                     sensorJson["PeakReading"] = value;
322                     if (timestamp != 0)
323                     {
324                         sensorJson["PeakReadingTime"] = timestamp;
325                     }
326                 }
327             }
328         }
329     }
330 }
331 
332 /**
333  * @brief Returns the Redfish State value for the specified inventory item.
334  * @param inventoryItem D-Bus inventory item associated with a sensor.
335  * @param sensorAvailable Boolean representing if D-Bus sensor is marked as
336  * available.
337  * @return State value for inventory item.
338  */
getState(const InventoryItem * inventoryItem,const bool sensorAvailable)339 inline resource::State getState(const InventoryItem* inventoryItem,
340                                 const bool sensorAvailable)
341 {
342     if ((inventoryItem != nullptr) && !(inventoryItem->isPresent))
343     {
344         return resource::State::Absent;
345     }
346 
347     if (!sensorAvailable)
348     {
349         return resource::State::UnavailableOffline;
350     }
351 
352     return resource::State::Enabled;
353 }
354 
355 /**
356  * @brief Returns the Redfish Health value for the specified sensor.
357  * @param sensorJson Sensor JSON object.
358  * @param valuesDict Map of all sensor DBus values.
359  * @param inventoryItem D-Bus inventory item associated with the sensor.  Will
360  * be nullptr if no associated inventory item was found.
361  * @return Health value for sensor.
362  */
getHealth(nlohmann::json & sensorJson,const dbus::utility::DBusPropertiesMap & valuesDict,const InventoryItem * inventoryItem)363 inline std::string getHealth(nlohmann::json& sensorJson,
364                              const dbus::utility::DBusPropertiesMap& valuesDict,
365                              const InventoryItem* inventoryItem)
366 {
367     // Get current health value (if any) in the sensor JSON object.  Some JSON
368     // objects contain multiple sensors (such as PowerSupplies).  We want to set
369     // the overall health to be the most severe of any of the sensors.
370     std::string currentHealth;
371     auto statusIt = sensorJson.find("Status");
372     if (statusIt != sensorJson.end())
373     {
374         auto healthIt = statusIt->find("Health");
375         if (healthIt != statusIt->end())
376         {
377             std::string* health = healthIt->get_ptr<std::string*>();
378             if (health != nullptr)
379             {
380                 currentHealth = *health;
381             }
382         }
383     }
384 
385     // If current health in JSON object is already Critical, return that.  This
386     // should override the sensor health, which might be less severe.
387     if (currentHealth == "Critical")
388     {
389         return "Critical";
390     }
391 
392     const bool* criticalAlarmHigh = nullptr;
393     const bool* criticalAlarmLow = nullptr;
394     const bool* warningAlarmHigh = nullptr;
395     const bool* warningAlarmLow = nullptr;
396 
397     const bool success = sdbusplus::unpackPropertiesNoThrow(
398         dbus_utils::UnpackErrorPrinter(), valuesDict, "CriticalAlarmHigh",
399         criticalAlarmHigh, "CriticalAlarmLow", criticalAlarmLow,
400         "WarningAlarmHigh", warningAlarmHigh, "WarningAlarmLow",
401         warningAlarmLow);
402 
403     if (success)
404     {
405         // Check if sensor has critical threshold alarm
406         if ((criticalAlarmHigh != nullptr && *criticalAlarmHigh) ||
407             (criticalAlarmLow != nullptr && *criticalAlarmLow))
408         {
409             return "Critical";
410         }
411     }
412 
413     // Check if associated inventory item is not functional
414     if ((inventoryItem != nullptr) && !(inventoryItem->isFunctional))
415     {
416         return "Critical";
417     }
418 
419     // If current health in JSON object is already Warning, return that. This
420     // should override the sensor status, which might be less severe.
421     if (currentHealth == "Warning")
422     {
423         return "Warning";
424     }
425 
426     if (success)
427     {
428         // Check if sensor has warning threshold alarm
429         if ((warningAlarmHigh != nullptr && *warningAlarmHigh) ||
430             (warningAlarmLow != nullptr && *warningAlarmLow))
431         {
432             return "Warning";
433         }
434     }
435 
436     return "OK";
437 }
438 
setLedState(nlohmann::json & sensorJson,const InventoryItem * inventoryItem)439 inline void setLedState(nlohmann::json& sensorJson,
440                         const InventoryItem* inventoryItem)
441 {
442     if (inventoryItem != nullptr && !inventoryItem->ledObjectPath.empty())
443     {
444         switch (inventoryItem->ledState)
445         {
446             case LedState::OFF:
447                 sensorJson["IndicatorLED"] = resource::IndicatorLED::Off;
448                 break;
449             case LedState::ON:
450                 sensorJson["IndicatorLED"] = resource::IndicatorLED::Lit;
451                 break;
452             case LedState::BLINK:
453                 sensorJson["IndicatorLED"] = resource::IndicatorLED::Blinking;
454                 break;
455             default:
456                 break;
457         }
458     }
459 }
460 
dBusSensorReadingBasisToRedfish(const std::string & readingBasis)461 inline sensor::ReadingBasisType dBusSensorReadingBasisToRedfish(
462     const std::string& readingBasis)
463 {
464     if (readingBasis ==
465         "xyz.openbmc_project.Sensor.Type.ReadingBasisType.Headroom")
466     {
467         return sensor::ReadingBasisType::Headroom;
468     }
469     if (readingBasis ==
470         "xyz.openbmc_project.Sensor.Type.ReadingBasisType.Delta")
471     {
472         return sensor::ReadingBasisType::Delta;
473     }
474     if (readingBasis == "xyz.openbmc_project.Sensor.Type.ReadingBasisType.Zero")
475     {
476         return sensor::ReadingBasisType::Zero;
477     }
478 
479     return sensor::ReadingBasisType::Invalid;
480 }
481 
dBusSensorImplementationToRedfish(const std::string & implementation)482 inline sensor::ImplementationType dBusSensorImplementationToRedfish(
483     const std::string& implementation)
484 {
485     if (implementation ==
486         "xyz.openbmc_project.Sensor.Type.ImplementationType.Physical")
487     {
488         return sensor::ImplementationType::PhysicalSensor;
489     }
490     if (implementation ==
491         "xyz.openbmc_project.Sensor.Type.ImplementationType.Synthesized")
492     {
493         return sensor::ImplementationType::Synthesized;
494     }
495     if (implementation ==
496         "xyz.openbmc_project.Sensor.Type.ImplementationType.Reported")
497     {
498         return sensor::ImplementationType::Reported;
499     }
500 
501     return sensor::ImplementationType::Invalid;
502 }
503 
504 /**
505  * @brief Builds a json sensor representation of a sensor.
506  * @param sensorName  The name of the sensor to be built
507  * @param sensorType  The type (temperature, fan_tach, etc) of the sensor to
508  * build
509  * @param chassisSubNode The subnode (thermal, sensor, etc) of the sensor
510  * @param propertiesDict A dictionary of the properties to build the sensor
511  * from.
512  * @param sensorJson  The json object to fill
513  * @param inventoryItem D-Bus inventory item associated with the sensor.  Will
514  * be nullptr if no associated inventory item was found.
515  */
objectPropertiesToJson(std::string_view sensorName,std::string_view sensorType,ChassisSubNode chassisSubNode,const dbus::utility::DBusPropertiesMap & propertiesDict,nlohmann::json & sensorJson,InventoryItem * inventoryItem)516 inline void objectPropertiesToJson(
517     std::string_view sensorName, std::string_view sensorType,
518     ChassisSubNode chassisSubNode,
519     const dbus::utility::DBusPropertiesMap& propertiesDict,
520     nlohmann::json& sensorJson, InventoryItem* inventoryItem)
521 {
522     // Parameter to set to override the type we get from dbus, and force it to
523     // int, regardless of what is available.  This is used for schemas like fan,
524     // that require integers, not floats.
525     bool forceToInt = false;
526 
527     nlohmann::json::json_pointer unit("/Reading");
528 
529     // This ChassisSubNode builds sensor excerpts
530     bool isExcerpt = isExcerptNode(chassisSubNode);
531 
532     /* Sensor excerpts use different keys to reference the sensor. These are
533      * built by the caller.
534      * Additionally they don't include these additional properties.
535      */
536     if (!isExcerpt)
537     {
538         if (chassisSubNode == ChassisSubNode::sensorsNode)
539         {
540             std::string subNodeEscaped = getSensorId(sensorName, sensorType);
541             // For sensors in SensorCollection we set Id instead of MemberId,
542             // including power sensors.
543             sensorJson["Id"] = std::move(subNodeEscaped);
544 
545             std::string sensorNameEs(sensorName);
546             std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
547             sensorJson["Name"] = std::move(sensorNameEs);
548         }
549         else if (sensorType != "power")
550         {
551             // Set MemberId and Name for non-power sensors.  For PowerSupplies
552             // and PowerControl, those properties have more general values
553             // because multiple sensors can be stored in the same JSON object.
554             std::string sensorNameEs(sensorName);
555             std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
556             sensorJson["Name"] = std::move(sensorNameEs);
557         }
558 
559         const bool* checkAvailable = nullptr;
560         bool available = true;
561         std::optional<std::string> readingBasis;
562         std::optional<std::string> implementation;
563         std::optional<Statistics> statistics;
564         std::optional<ReadingParameters> readingParameters;
565 
566         const bool success = sdbusplus::unpackPropertiesNoThrow(
567             dbus_utils::UnpackErrorPrinter(), propertiesDict, "Available",
568             checkAvailable, "ReadingBasis", readingBasis, "Implementation",
569             implementation, "Readings", statistics, "ReadingParameters",
570             readingParameters);
571         if (!success)
572         {
573             messages::internalError();
574         }
575         if (checkAvailable != nullptr)
576         {
577             available = *checkAvailable;
578         }
579 
580         sensorJson["Status"]["State"] = getState(inventoryItem, available);
581         sensorJson["Status"]["Health"] =
582             getHealth(sensorJson, propertiesDict, inventoryItem);
583 
584         if (chassisSubNode == ChassisSubNode::sensorsNode)
585         {
586             sensorJson["@odata.type"] = "#Sensor.v1_11_0.Sensor";
587 
588             sensor::ReadingType readingType =
589                 sensors::toReadingType(sensorType);
590             if (readingType == sensor::ReadingType::Invalid)
591             {
592                 BMCWEB_LOG_ERROR("Redfish cannot map reading type for {}",
593                                  sensorType);
594             }
595             else
596             {
597                 sensorJson["ReadingType"] = readingType;
598             }
599 
600             std::string_view readingUnits = sensors::toReadingUnits(sensorType);
601             if (readingUnits.empty())
602             {
603                 BMCWEB_LOG_ERROR("Redfish cannot map reading unit for {}",
604                                  sensorType);
605             }
606             else
607             {
608                 sensorJson["ReadingUnits"] = readingUnits;
609             }
610 
611             if (readingBasis.has_value())
612             {
613                 sensor::ReadingBasisType readingBasisOpt =
614                     dBusSensorReadingBasisToRedfish(*readingBasis);
615                 if (readingBasisOpt != sensor::ReadingBasisType::Invalid)
616                 {
617                     sensorJson["ReadingBasis"] = readingBasisOpt;
618                 }
619             }
620 
621             if (implementation.has_value())
622             {
623                 sensor::ImplementationType implementationOpt =
624                     dBusSensorImplementationToRedfish(*implementation);
625                 if (implementationOpt != sensor::ImplementationType::Invalid)
626                 {
627                     sensorJson["Implementation"] = implementationOpt;
628                 }
629             }
630 
631             updateSensorStatistics(sensorJson, statistics, readingParameters);
632         }
633         else if (sensorType == "temperature")
634         {
635             unit = "/ReadingCelsius"_json_pointer;
636             sensorJson["@odata.type"] = "#Thermal.v1_3_0.Temperature";
637             // TODO(ed) Documentation says that path should be type fan_tach,
638             // implementation seems to implement fan
639         }
640         else if (sensorType == "fan" || sensorType == "fan_tach")
641         {
642             unit = "/Reading"_json_pointer;
643             sensorJson["ReadingUnits"] = thermal::ReadingUnits::RPM;
644             sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
645             if constexpr (BMCWEB_REDFISH_ALLOW_DEPRECATED_INDICATORLED)
646             {
647                 setLedState(sensorJson, inventoryItem);
648             }
649             forceToInt = true;
650         }
651         else if (sensorType == "fan_pwm")
652         {
653             unit = "/Reading"_json_pointer;
654             sensorJson["ReadingUnits"] = thermal::ReadingUnits::Percent;
655             sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
656             if constexpr (BMCWEB_REDFISH_ALLOW_DEPRECATED_INDICATORLED)
657             {
658                 setLedState(sensorJson, inventoryItem);
659             }
660             forceToInt = true;
661         }
662         else if (sensorType == "voltage")
663         {
664             unit = "/ReadingVolts"_json_pointer;
665             sensorJson["@odata.type"] = "#Power.v1_0_0.Voltage";
666         }
667         else if (sensorType == "power")
668         {
669             std::string lower;
670             std::ranges::transform(sensorName, std::back_inserter(lower),
671                                    bmcweb::asciiToLower);
672             if (lower == "total_power")
673             {
674                 sensorJson["@odata.type"] = "#Power.v1_0_0.PowerControl";
675                 // Put multiple "sensors" into a single PowerControl, so have
676                 // generic names for MemberId and Name. Follows Redfish mockup.
677                 sensorJson["MemberId"] = "0";
678                 sensorJson["Name"] = "Chassis Power Control";
679                 unit = "/PowerConsumedWatts"_json_pointer;
680             }
681             else if (lower.find("input") != std::string::npos)
682             {
683                 unit = "/PowerInputWatts"_json_pointer;
684             }
685             else
686             {
687                 unit = "/PowerOutputWatts"_json_pointer;
688             }
689         }
690         else
691         {
692             BMCWEB_LOG_ERROR("Redfish cannot map object type for {}",
693                              sensorName);
694             return;
695         }
696     }
697 
698     // Map of dbus interface name, dbus property name and redfish property_name
699     std::vector<
700         std::tuple<const char*, const char*, nlohmann::json::json_pointer>>
701         properties;
702 
703     properties.emplace_back("xyz.openbmc_project.Sensor.Value", "Value", unit);
704 
705     if (!isExcerpt)
706     {
707         if (chassisSubNode == ChassisSubNode::sensorsNode)
708         {
709             properties.emplace_back(
710                 "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningHigh",
711                 "/Thresholds/UpperCaution/Reading"_json_pointer);
712             properties.emplace_back(
713                 "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningLow",
714                 "/Thresholds/LowerCaution/Reading"_json_pointer);
715             properties.emplace_back(
716                 "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalHigh",
717                 "/Thresholds/UpperCritical/Reading"_json_pointer);
718             properties.emplace_back(
719                 "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalLow",
720                 "/Thresholds/LowerCritical/Reading"_json_pointer);
721             properties.emplace_back(
722                 "xyz.openbmc_project.Sensor.Threshold.HardShutdown",
723                 "HardShutdownHigh",
724                 "/Thresholds/UpperFatal/Reading"_json_pointer);
725             properties.emplace_back(
726                 "xyz.openbmc_project.Sensor.Threshold.HardShutdown",
727                 "HardShutdownLow",
728                 "/Thresholds/LowerFatal/Reading"_json_pointer);
729 
730             /* Add additional properties specific to sensorType */
731             if (sensorType == "fan_tach")
732             {
733                 properties.emplace_back("xyz.openbmc_project.Sensor.Value",
734                                         "Value", "/SpeedRPM"_json_pointer);
735             }
736         }
737         else if (sensorType != "power")
738         {
739             properties.emplace_back(
740                 "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningHigh",
741                 "/UpperThresholdNonCritical"_json_pointer);
742             properties.emplace_back(
743                 "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningLow",
744                 "/LowerThresholdNonCritical"_json_pointer);
745             properties.emplace_back(
746                 "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalHigh",
747                 "/UpperThresholdCritical"_json_pointer);
748             properties.emplace_back(
749                 "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalLow",
750                 "/LowerThresholdCritical"_json_pointer);
751         }
752 
753         // TODO Need to get UpperThresholdFatal and LowerThresholdFatal
754 
755         if (chassisSubNode == ChassisSubNode::sensorsNode)
756         {
757             properties.emplace_back("xyz.openbmc_project.Sensor.Value",
758                                     "MinValue",
759                                     "/ReadingRangeMin"_json_pointer);
760             properties.emplace_back("xyz.openbmc_project.Sensor.Value",
761                                     "MaxValue",
762                                     "/ReadingRangeMax"_json_pointer);
763             properties.emplace_back("xyz.openbmc_project.Sensor.Accuracy",
764                                     "Accuracy", "/Accuracy"_json_pointer);
765         }
766         else if (sensorType == "temperature")
767         {
768             properties.emplace_back("xyz.openbmc_project.Sensor.Value",
769                                     "MinValue",
770                                     "/MinReadingRangeTemp"_json_pointer);
771             properties.emplace_back("xyz.openbmc_project.Sensor.Value",
772                                     "MaxValue",
773                                     "/MaxReadingRangeTemp"_json_pointer);
774         }
775         else if (sensorType != "power")
776         {
777             properties.emplace_back("xyz.openbmc_project.Sensor.Value",
778                                     "MinValue",
779                                     "/MinReadingRange"_json_pointer);
780             properties.emplace_back("xyz.openbmc_project.Sensor.Value",
781                                     "MaxValue",
782                                     "/MaxReadingRange"_json_pointer);
783         }
784     }
785 
786     for (const std::tuple<const char*, const char*,
787                           nlohmann::json::json_pointer>& p : properties)
788     {
789         for (const auto& [valueName, valueVariant] : propertiesDict)
790         {
791             if (valueName != std::get<1>(p))
792             {
793                 continue;
794             }
795 
796             // The property we want to set may be nested json, so use
797             // a json_pointer for easy indexing into the json structure.
798             const nlohmann::json::json_pointer& key = std::get<2>(p);
799 
800             const double* doubleValue = std::get_if<double>(&valueVariant);
801             if (doubleValue == nullptr)
802             {
803                 BMCWEB_LOG_ERROR("Got value interface that wasn't double");
804                 continue;
805             }
806             if (!std::isfinite(*doubleValue))
807             {
808                 if (valueName == "Value")
809                 {
810                     // Readings are allowed to be NAN for unavailable;  coerce
811                     // them to null in the json response.
812                     sensorJson[key] = nullptr;
813                     continue;
814                 }
815                 BMCWEB_LOG_WARNING("Sensor value for {} was unexpectedly {}",
816                                    valueName, *doubleValue);
817                 continue;
818             }
819             if (forceToInt)
820             {
821                 sensorJson[key] = static_cast<int64_t>(*doubleValue);
822             }
823             else
824             {
825                 sensorJson[key] = *doubleValue;
826             }
827         }
828     }
829 }
830 
831 /**
832  * @brief Builds a json sensor excerpt representation of a sensor.
833  *
834  * @details This is a wrapper function to provide consistent setting of
835  * "DataSourceUri" for sensor excerpts and filling of properties. Since sensor
836  * excerpts usually have just the D-Bus path for the sensor that is accepted
837  * and used to build "DataSourceUri".
838 
839  * @param path The D-Bus path to the sensor to be built
840  * @param chassisId The Chassis Id for the sensor
841  * @param chassisSubNode The subnode (e.g. ThermalMetrics) of the sensor
842  * @param sensorTypeExpected The expected type of the sensor
843  * @param propertiesDict A dictionary of the properties to build the sensor
844  * from.
845  * @param sensorJson  The json object to fill
846  * @returns True if sensorJson object filled. False on any error.
847  * Caller is responsible for handling error.
848  */
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)849 inline bool objectExcerptToJson(
850     const std::string& path, const std::string_view chassisId,
851     ChassisSubNode chassisSubNode,
852     const std::optional<std::string>& sensorTypeExpected,
853     const dbus::utility::DBusPropertiesMap& propertiesDict,
854     nlohmann::json& sensorJson)
855 {
856     if (!isExcerptNode(chassisSubNode))
857     {
858         BMCWEB_LOG_DEBUG("{} is not a sensor excerpt",
859                          chassisSubNodeToString(chassisSubNode));
860         return false;
861     }
862 
863     sdbusplus::message::object_path sensorPath(path);
864     std::string sensorName = sensorPath.filename();
865     std::string sensorType = sensorPath.parent_path().filename();
866     if (sensorName.empty() || sensorType.empty())
867     {
868         BMCWEB_LOG_DEBUG("Invalid sensor path {}", path);
869         return false;
870     }
871 
872     if (sensorTypeExpected && (sensorType != *sensorTypeExpected))
873     {
874         BMCWEB_LOG_DEBUG("{} is not expected type {}", path,
875                          *sensorTypeExpected);
876         return false;
877     }
878 
879     // Sensor excerpts use DataSourceUri to reference full sensor Redfish path
880     sensorJson["DataSourceUri"] =
881         boost::urls::format("/redfish/v1/Chassis/{}/Sensors/{}", chassisId,
882                             getSensorId(sensorName, sensorType));
883 
884     // Fill in sensor excerpt properties
885     objectPropertiesToJson(sensorName, sensorType, chassisSubNode,
886                            propertiesDict, sensorJson, nullptr);
887 
888     return true;
889 }
890 
891 // Maps D-Bus: Service, SensorPath
892 using SensorServicePathMap = std::pair<std::string, std::string>;
893 using SensorServicePathList = std::vector<SensorServicePathMap>;
894 
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)895 inline void getAllSensorObjects(
896     const std::string& associatedPath, const std::string& path,
897     std::span<const std::string_view> interfaces, const int32_t depth,
898     std::function<void(const boost::system::error_code& ec,
899                        SensorServicePathList&)>&& callback)
900 {
901     sdbusplus::message::object_path endpointPath{associatedPath};
902     endpointPath /= "all_sensors";
903 
904     dbus::utility::getAssociatedSubTree(
905         endpointPath, sdbusplus::message::object_path(path), depth, interfaces,
906         [callback = std::move(callback)](
907             const boost::system::error_code& ec,
908             const dbus::utility::MapperGetSubTreeResponse& subtree) {
909             SensorServicePathList sensorsServiceAndPath;
910 
911             if (ec)
912             {
913                 callback(ec, sensorsServiceAndPath);
914                 return;
915             }
916 
917             for (const auto& [sensorPath, serviceMaps] : subtree)
918             {
919                 for (const auto& [service, mapInterfaces] : serviceMaps)
920                 {
921                     sensorsServiceAndPath.emplace_back(service, sensorPath);
922                 }
923             }
924 
925             callback(ec, sensorsServiceAndPath);
926         });
927 }
928 
929 enum class SensorPurpose
930 {
931     totalPower,
932     unknownPurpose,
933 };
934 
sensorPurposeToString(SensorPurpose subNode)935 constexpr std::string_view sensorPurposeToString(SensorPurpose subNode)
936 {
937     if (subNode == SensorPurpose::totalPower)
938     {
939         return "xyz.openbmc_project.Sensor.Purpose.SensorPurpose.TotalPower";
940     }
941 
942     return "Unknown Purpose";
943 }
944 
sensorPurposeFromString(const std::string & subNodeStr)945 inline SensorPurpose sensorPurposeFromString(const std::string& subNodeStr)
946 {
947     // If none match unknownNode is returned
948     SensorPurpose subNode = SensorPurpose::unknownPurpose;
949 
950     if (subNodeStr ==
951         "xyz.openbmc_project.Sensor.Purpose.SensorPurpose.TotalPower")
952     {
953         subNode = SensorPurpose::totalPower;
954     }
955 
956     return subNode;
957 }
958 
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)959 inline void checkSensorPurpose(
960     const std::string& serviceName, const std::string& sensorPath,
961     const SensorPurpose sensorPurpose,
962     const std::shared_ptr<SensorServicePathList>& sensorMatches,
963     const std::shared_ptr<boost::system::error_code>& asyncErrors,
964     const boost::system::error_code& ec,
965     const std::vector<std::string>& purposeList)
966 {
967     // If not found this sensor will be skipped but allow to
968     // continue processing remaining sensors
969     if (ec && (ec != boost::system::errc::io_error) && (ec.value() != EBADR))
970     {
971         BMCWEB_LOG_DEBUG("D-Bus response error : {}", ec);
972         *asyncErrors = ec;
973     }
974 
975     if (!ec)
976     {
977         const std::string_view checkPurposeStr =
978             sensorPurposeToString(sensorPurpose);
979 
980         BMCWEB_LOG_DEBUG("checkSensorPurpose: {}", checkPurposeStr);
981 
982         for (const std::string& purposeStr : purposeList)
983         {
984             if (purposeStr == checkPurposeStr)
985             {
986                 // Add to list
987                 BMCWEB_LOG_DEBUG("checkSensorPurpose adding {} {}", serviceName,
988                                  sensorPath);
989                 sensorMatches->emplace_back(serviceName, sensorPath);
990                 return;
991             }
992         }
993     }
994 }
995 
996 /**
997  * @brief Gets sensors from list with specified purpose
998  *
999  * Checks the <sensorListIn> sensors for any which implement the specified
1000  * <sensorPurpose> in interface xyz.openbmc_project.Sensor.Purpose. Adds any
1001  * matches to the <sensorMatches> list. After checking all sensors in
1002  * <sensorListIn> the <callback> is called with just the list of matching
1003  * sensors, which could be an empty list. Additionally the <callback> is called
1004  * on error and error handling is left to the callback.
1005  *
1006  * @param asyncResp Response data
1007  * @param[in] sensorListIn List of sensors to check
1008  * @param[in] sensorPurpose Looks for sensors matching this purpose
1009  * @param[out] sensorMatches Sensors from sensorListIn with sensorPurpose
1010  * @param[in] callback Callback to handle list of matching sensors
1011  */
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)1012 inline void getSensorsByPurpose(
1013     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1014     const SensorServicePathList& sensorListIn,
1015     const SensorPurpose sensorPurpose,
1016     const std::shared_ptr<SensorServicePathList>& sensorMatches,
1017     const std::function<void(
1018         const boost::system::error_code& ec,
1019         const std::shared_ptr<SensorServicePathList>& sensorMatches)>& callback)
1020 {
1021     /* Keeps track of number of asynchronous calls made. Once all handlers have
1022      * been called the callback function is called with the results.
1023      */
1024     std::shared_ptr<int> remainingSensorsToVist =
1025         std::make_shared<int>(sensorListIn.size());
1026 
1027     /* Holds last unrecoverable error returned by any of the asynchronous
1028      * handlers. Once all handlers have been called the callback will be sent
1029      * the error to handle.
1030      */
1031     std::shared_ptr<boost::system::error_code> asyncErrors =
1032         std::make_shared<boost::system::error_code>();
1033 
1034     BMCWEB_LOG_DEBUG("getSensorsByPurpose enter {}", *remainingSensorsToVist);
1035 
1036     for (const auto& [serviceName, sensorPath] : sensorListIn)
1037     {
1038         dbus::utility::getProperty<std::vector<std::string>>(
1039             serviceName, sensorPath, "xyz.openbmc_project.Sensor.Purpose",
1040             "Purpose",
1041             [asyncResp, serviceName, sensorPath, sensorPurpose, sensorMatches,
1042              callback, remainingSensorsToVist,
1043              asyncErrors](const boost::system::error_code& ec,
1044                           const std::vector<std::string>& purposeList) {
1045                 // Keep track of sensor visited
1046                 (*remainingSensorsToVist)--;
1047                 BMCWEB_LOG_DEBUG("Visited {}. Remaining sensors {}", sensorPath,
1048                                  *remainingSensorsToVist);
1049 
1050                 checkSensorPurpose(serviceName, sensorPath, sensorPurpose,
1051                                    sensorMatches, asyncErrors, ec, purposeList);
1052 
1053                 // All sensors have been visited
1054                 if (*remainingSensorsToVist == 0)
1055                 {
1056                     BMCWEB_LOG_DEBUG(
1057                         "getSensorsByPurpose, exit found {} matches",
1058                         sensorMatches->size());
1059                     callback(*asyncErrors, sensorMatches);
1060                     return;
1061                 }
1062             });
1063     }
1064 }
1065 
1066 } // namespace sensor_utils
1067 } // namespace redfish
1068