xref: /openbmc/bmcweb/redfish-core/include/utils/sensor_utils.hpp (revision e32edd47e374ceb93d89ac1d2c3158c39ff6e5cf)
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 
fillSensorStatus(const dbus::utility::DBusPropertiesMap & propertiesDict,nlohmann::json & sensorJson,InventoryItem * inventoryItem)504 inline void fillSensorStatus(
505     const dbus::utility::DBusPropertiesMap& propertiesDict,
506     nlohmann::json& sensorJson, InventoryItem* inventoryItem)
507 {
508     const bool* checkAvailable = nullptr;
509     bool available = true;
510 
511     const bool success = sdbusplus::unpackPropertiesNoThrow(
512         dbus_utils::UnpackErrorPrinter(), propertiesDict, "Available",
513         checkAvailable);
514     if (!success)
515     {
516         messages::internalError();
517     }
518     if (checkAvailable != nullptr)
519     {
520         available = *checkAvailable;
521     }
522 
523     sensorJson["Status"]["State"] = getState(inventoryItem, available);
524     sensorJson["Status"]["Health"] =
525         getHealth(sensorJson, propertiesDict, inventoryItem);
526 }
527 
fillSensorIdentity(std::string_view sensorName,std::string_view sensorType,const dbus::utility::DBusPropertiesMap & propertiesDict,nlohmann::json & sensorJson)528 inline void fillSensorIdentity(
529     std::string_view sensorName, std::string_view sensorType,
530     const dbus::utility::DBusPropertiesMap& propertiesDict,
531     nlohmann::json& sensorJson)
532 {
533     std::string subNodeEscaped = getSensorId(sensorName, sensorType);
534     // For sensors in SensorCollection we set Id instead of MemberId,
535     // including power sensors.
536     sensorJson["Id"] = std::move(subNodeEscaped);
537 
538     std::string sensorNameEs(sensorName);
539     std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
540     sensorJson["Name"] = std::move(sensorNameEs);
541     sensorJson["@odata.type"] = "#Sensor.v1_11_0.Sensor";
542 
543     sensor::ReadingType readingType = sensors::toReadingType(sensorType);
544     if (readingType == sensor::ReadingType::Invalid)
545     {
546         BMCWEB_LOG_ERROR("Redfish cannot map reading type for {}", sensorType);
547     }
548     else
549     {
550         sensorJson["ReadingType"] = readingType;
551     }
552 
553     std::string_view readingUnits = sensors::toReadingUnits(sensorType);
554     if (readingUnits.empty())
555     {
556         BMCWEB_LOG_ERROR("Redfish cannot map reading unit for {}", sensorType);
557     }
558     else
559     {
560         sensorJson["ReadingUnits"] = readingUnits;
561     }
562 
563     std::optional<std::string> readingBasis;
564     std::optional<std::string> implementation;
565     std::optional<Statistics> statistics;
566     std::optional<ReadingParameters> readingParameters;
567 
568     const bool success = sdbusplus::unpackPropertiesNoThrow(
569         dbus_utils::UnpackErrorPrinter(), propertiesDict, "ReadingBasis",
570         readingBasis, "Implementation", implementation, "Readings", statistics,
571         "ReadingParameters", readingParameters);
572     if (!success)
573     {
574         messages::internalError();
575     }
576 
577     if (readingBasis.has_value())
578     {
579         sensor::ReadingBasisType readingBasisOpt =
580             dBusSensorReadingBasisToRedfish(*readingBasis);
581         if (readingBasisOpt != sensor::ReadingBasisType::Invalid)
582         {
583             sensorJson["ReadingBasis"] = readingBasisOpt;
584         }
585     }
586 
587     if (implementation.has_value())
588     {
589         sensor::ImplementationType implementationOpt =
590             dBusSensorImplementationToRedfish(*implementation);
591         if (implementationOpt != sensor::ImplementationType::Invalid)
592         {
593             sensorJson["Implementation"] = implementationOpt;
594         }
595     }
596 
597     updateSensorStatistics(sensorJson, statistics, readingParameters);
598 }
599 
fillPowerThermalIdentity(std::string_view sensorName,std::string_view sensorType,nlohmann::json & sensorJson,InventoryItem * inventoryItem,nlohmann::json::json_pointer & unit,bool & forceToInt)600 inline bool fillPowerThermalIdentity(
601     std::string_view sensorName, std::string_view sensorType,
602     nlohmann::json& sensorJson, InventoryItem* inventoryItem,
603     nlohmann::json::json_pointer& unit, bool& forceToInt)
604 {
605     if (sensorType != "power")
606     {
607         // Set MemberId and Name for non-power sensors.  For PowerSupplies
608         // and PowerControl, those properties have more general values
609         // because multiple sensors can be stored in the same JSON object.
610         std::string sensorNameEs(sensorName);
611         std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
612         sensorJson["Name"] = std::move(sensorNameEs);
613     }
614 
615     if (sensorType == "temperature")
616     {
617         unit = "/ReadingCelsius"_json_pointer;
618         sensorJson["@odata.type"] = "#Thermal.v1_3_0.Temperature";
619         // TODO(ed) Documentation says that path should be type fan_tach,
620         // implementation seems to implement fan
621         return true;
622     }
623 
624     if (sensorType == "fan" || sensorType == "fan_tach")
625     {
626         unit = "/Reading"_json_pointer;
627         sensorJson["ReadingUnits"] = thermal::ReadingUnits::RPM;
628         sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
629         if constexpr (BMCWEB_REDFISH_ALLOW_DEPRECATED_INDICATORLED)
630         {
631             setLedState(sensorJson, inventoryItem);
632         }
633         forceToInt = true;
634         return true;
635     }
636 
637     if (sensorType == "fan_pwm")
638     {
639         unit = "/Reading"_json_pointer;
640         sensorJson["ReadingUnits"] = thermal::ReadingUnits::Percent;
641         sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
642         if constexpr (BMCWEB_REDFISH_ALLOW_DEPRECATED_INDICATORLED)
643         {
644             setLedState(sensorJson, inventoryItem);
645         }
646         forceToInt = true;
647         return true;
648     }
649 
650     if (sensorType == "voltage")
651     {
652         unit = "/ReadingVolts"_json_pointer;
653         sensorJson["@odata.type"] = "#Power.v1_0_0.Voltage";
654         return true;
655     }
656 
657     if (sensorType == "power")
658     {
659         std::string lower;
660         std::ranges::transform(sensorName, std::back_inserter(lower),
661                                bmcweb::asciiToLower);
662         if (lower == "total_power")
663         {
664             sensorJson["@odata.type"] = "#Power.v1_0_0.PowerControl";
665             // Put multiple "sensors" into a single PowerControl, so have
666             // generic names for MemberId and Name. Follows Redfish mockup.
667             sensorJson["MemberId"] = "0";
668             sensorJson["Name"] = "Chassis Power Control";
669             unit = "/PowerConsumedWatts"_json_pointer;
670         }
671         else if (lower.find("input") != std::string::npos)
672         {
673             unit = "/PowerInputWatts"_json_pointer;
674         }
675         else
676         {
677             unit = "/PowerOutputWatts"_json_pointer;
678         }
679         return true;
680     }
681 
682     BMCWEB_LOG_ERROR("Redfish cannot map object type for {}", sensorName);
683     return false;
684 }
685 
686 // Map of dbus interface name, dbus property name and redfish property_name
687 using SensorPropertyMap = std::tuple<std::string_view, std::string_view,
688                                      nlohmann::json::json_pointer>;
689 using SensorPropertyList = std::vector<SensorPropertyMap>;
690 
mapPropertiesBySubnode(std::string_view sensorType,ChassisSubNode chassisSubNode,SensorPropertyList & properties,nlohmann::json::json_pointer & unit,bool isExcerpt)691 inline void mapPropertiesBySubnode(
692     std::string_view sensorType, ChassisSubNode chassisSubNode,
693     SensorPropertyList& properties, nlohmann::json::json_pointer& unit,
694     bool isExcerpt)
695 {
696     // unit contains the redfish property_name based on the sensor type/node
697     properties.emplace_back("xyz.openbmc_project.Sensor.Value", "Value", unit);
698 
699     if (isExcerpt)
700     {
701         // Excerpts don't have any of these extended properties
702         return;
703     }
704 
705     if (chassisSubNode == ChassisSubNode::sensorsNode)
706     {
707         properties.emplace_back(
708             "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningHigh",
709             "/Thresholds/UpperCaution/Reading"_json_pointer);
710         properties.emplace_back(
711             "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningLow",
712             "/Thresholds/LowerCaution/Reading"_json_pointer);
713         properties.emplace_back(
714             "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalHigh",
715             "/Thresholds/UpperCritical/Reading"_json_pointer);
716         properties.emplace_back(
717             "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalLow",
718             "/Thresholds/LowerCritical/Reading"_json_pointer);
719         properties.emplace_back(
720             "xyz.openbmc_project.Sensor.Threshold.HardShutdown",
721             "HardShutdownHigh", "/Thresholds/UpperFatal/Reading"_json_pointer);
722         properties.emplace_back(
723             "xyz.openbmc_project.Sensor.Threshold.HardShutdown",
724             "HardShutdownLow", "/Thresholds/LowerFatal/Reading"_json_pointer);
725 
726         /* Add additional properties specific to sensorType */
727         if (sensorType == "fan_tach")
728         {
729             properties.emplace_back("xyz.openbmc_project.Sensor.Value", "Value",
730                                     "/SpeedRPM"_json_pointer);
731         }
732     }
733     else if (sensorType != "power")
734     {
735         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Warning",
736                                 "WarningHigh",
737                                 "/UpperThresholdNonCritical"_json_pointer);
738         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Warning",
739                                 "WarningLow",
740                                 "/LowerThresholdNonCritical"_json_pointer);
741         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Critical",
742                                 "CriticalHigh",
743                                 "/UpperThresholdCritical"_json_pointer);
744         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Critical",
745                                 "CriticalLow",
746                                 "/LowerThresholdCritical"_json_pointer);
747     }
748 
749     // TODO Need to get UpperThresholdFatal and LowerThresholdFatal
750 
751     if (chassisSubNode == ChassisSubNode::sensorsNode)
752     {
753         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
754                                 "/ReadingRangeMin"_json_pointer);
755         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
756                                 "/ReadingRangeMax"_json_pointer);
757         properties.emplace_back("xyz.openbmc_project.Sensor.Accuracy",
758                                 "Accuracy", "/Accuracy"_json_pointer);
759     }
760     else if (sensorType == "temperature")
761     {
762         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
763                                 "/MinReadingRangeTemp"_json_pointer);
764         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
765                                 "/MaxReadingRangeTemp"_json_pointer);
766     }
767     else if (sensorType != "power")
768     {
769         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
770                                 "/MinReadingRange"_json_pointer);
771         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
772                                 "/MaxReadingRange"_json_pointer);
773     }
774 }
775 
776 /**
777  * @brief Builds a json sensor representation of a sensor.
778  * @param sensorName  The name of the sensor to be built
779  * @param sensorType  The type (temperature, fan_tach, etc) of the sensor to
780  * build
781  * @param chassisSubNode The subnode (thermal, sensor, etc) of the sensor
782  * @param propertiesDict A dictionary of the properties to build the sensor
783  * from.
784  * @param sensorJson  The json object to fill
785  * @param inventoryItem D-Bus inventory item associated with the sensor.  Will
786  * be nullptr if no associated inventory item was found.
787  */
objectPropertiesToJson(std::string_view sensorName,std::string_view sensorType,ChassisSubNode chassisSubNode,const dbus::utility::DBusPropertiesMap & propertiesDict,nlohmann::json & sensorJson,InventoryItem * inventoryItem)788 inline void objectPropertiesToJson(
789     std::string_view sensorName, std::string_view sensorType,
790     ChassisSubNode chassisSubNode,
791     const dbus::utility::DBusPropertiesMap& propertiesDict,
792     nlohmann::json& sensorJson, InventoryItem* inventoryItem)
793 {
794     // Parameter to set to override the type we get from dbus, and force it to
795     // int, regardless of what is available.  This is used for schemas like fan,
796     // that require integers, not floats.
797     bool forceToInt = false;
798 
799     nlohmann::json::json_pointer unit("/Reading");
800 
801     // This ChassisSubNode builds sensor excerpts
802     bool isExcerpt = isExcerptNode(chassisSubNode);
803 
804     /* Sensor excerpts use different keys to reference the sensor. These are
805      * built by the caller.
806      * Additionally they don't include these additional properties.
807      */
808     if (!isExcerpt)
809     {
810         if (chassisSubNode == ChassisSubNode::sensorsNode)
811         {
812             fillSensorIdentity(sensorName, sensorType, propertiesDict,
813                                sensorJson);
814         }
815         else
816         {
817             bool filledOk = fillPowerThermalIdentity(
818                 sensorName, sensorType, sensorJson, inventoryItem, unit,
819                 std::ref(forceToInt));
820             if (!filledOk)
821             {
822                 return;
823             }
824         }
825 
826         fillSensorStatus(propertiesDict, sensorJson, inventoryItem);
827     }
828 
829     // Map of dbus interface name, dbus property name and redfish property_name
830     SensorPropertyList properties;
831 
832     // Add additional property mappings based on the sensor type/node
833     mapPropertiesBySubnode(sensorType, chassisSubNode, properties, unit,
834                            isExcerpt);
835 
836     for (const SensorPropertyMap& p : properties)
837     {
838         for (const auto& [valueName, valueVariant] : propertiesDict)
839         {
840             if (valueName != std::get<1>(p))
841             {
842                 continue;
843             }
844 
845             // The property we want to set may be nested json, so use
846             // a json_pointer for easy indexing into the json structure.
847             const nlohmann::json::json_pointer& key = std::get<2>(p);
848 
849             const double* doubleValue = std::get_if<double>(&valueVariant);
850             if (doubleValue == nullptr)
851             {
852                 BMCWEB_LOG_ERROR("Got value interface that wasn't double");
853                 continue;
854             }
855             if (!std::isfinite(*doubleValue))
856             {
857                 if (valueName == "Value")
858                 {
859                     // Readings are allowed to be NAN for unavailable;  coerce
860                     // them to null in the json response.
861                     sensorJson[key] = nullptr;
862                     continue;
863                 }
864                 BMCWEB_LOG_WARNING("Sensor value for {} was unexpectedly {}",
865                                    valueName, *doubleValue);
866                 continue;
867             }
868             if (forceToInt)
869             {
870                 sensorJson[key] = static_cast<int64_t>(*doubleValue);
871             }
872             else
873             {
874                 sensorJson[key] = *doubleValue;
875             }
876         }
877     }
878 }
879 
880 /**
881  * @brief Builds a json sensor excerpt representation of a sensor.
882  *
883  * @details This is a wrapper function to provide consistent setting of
884  * "DataSourceUri" for sensor excerpts and filling of properties. Since sensor
885  * excerpts usually have just the D-Bus path for the sensor that is accepted
886  * and used to build "DataSourceUri".
887 
888  * @param path The D-Bus path to the sensor to be built
889  * @param chassisId The Chassis Id for the sensor
890  * @param chassisSubNode The subnode (e.g. ThermalMetrics) of the sensor
891  * @param sensorTypeExpected The expected type of the sensor
892  * @param propertiesDict A dictionary of the properties to build the sensor
893  * from.
894  * @param sensorJson  The json object to fill
895  * @returns True if sensorJson object filled. False on any error.
896  * Caller is responsible for handling error.
897  */
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)898 inline bool objectExcerptToJson(
899     const std::string& path, const std::string_view chassisId,
900     ChassisSubNode chassisSubNode,
901     const std::optional<std::string>& sensorTypeExpected,
902     const dbus::utility::DBusPropertiesMap& propertiesDict,
903     nlohmann::json& sensorJson)
904 {
905     if (!isExcerptNode(chassisSubNode))
906     {
907         BMCWEB_LOG_DEBUG("{} is not a sensor excerpt",
908                          chassisSubNodeToString(chassisSubNode));
909         return false;
910     }
911 
912     sdbusplus::message::object_path sensorPath(path);
913     std::string sensorName = sensorPath.filename();
914     std::string sensorType = sensorPath.parent_path().filename();
915     if (sensorName.empty() || sensorType.empty())
916     {
917         BMCWEB_LOG_DEBUG("Invalid sensor path {}", path);
918         return false;
919     }
920 
921     if (sensorTypeExpected && (sensorType != *sensorTypeExpected))
922     {
923         BMCWEB_LOG_DEBUG("{} is not expected type {}", path,
924                          *sensorTypeExpected);
925         return false;
926     }
927 
928     // Sensor excerpts use DataSourceUri to reference full sensor Redfish path
929     sensorJson["DataSourceUri"] =
930         boost::urls::format("/redfish/v1/Chassis/{}/Sensors/{}", chassisId,
931                             getSensorId(sensorName, sensorType));
932 
933     // Fill in sensor excerpt properties
934     objectPropertiesToJson(sensorName, sensorType, chassisSubNode,
935                            propertiesDict, sensorJson, nullptr);
936 
937     return true;
938 }
939 
940 // Maps D-Bus: Service, SensorPath
941 using SensorServicePathMap = std::pair<std::string, std::string>;
942 using SensorServicePathList = std::vector<SensorServicePathMap>;
943 
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)944 inline void getAllSensorObjects(
945     const std::string& associatedPath, const std::string& path,
946     std::span<const std::string_view> interfaces, const int32_t depth,
947     std::function<void(const boost::system::error_code& ec,
948                        SensorServicePathList&)>&& callback)
949 {
950     sdbusplus::message::object_path endpointPath{associatedPath};
951     endpointPath /= "all_sensors";
952 
953     dbus::utility::getAssociatedSubTree(
954         endpointPath, sdbusplus::message::object_path(path), depth, interfaces,
955         [callback = std::move(callback)](
956             const boost::system::error_code& ec,
957             const dbus::utility::MapperGetSubTreeResponse& subtree) {
958             SensorServicePathList sensorsServiceAndPath;
959 
960             if (ec)
961             {
962                 callback(ec, sensorsServiceAndPath);
963                 return;
964             }
965 
966             for (const auto& [sensorPath, serviceMaps] : subtree)
967             {
968                 for (const auto& [service, mapInterfaces] : serviceMaps)
969                 {
970                     sensorsServiceAndPath.emplace_back(service, sensorPath);
971                 }
972             }
973 
974             callback(ec, sensorsServiceAndPath);
975         });
976 }
977 
978 enum class SensorPurpose
979 {
980     totalPower,
981     unknownPurpose,
982 };
983 
sensorPurposeToString(SensorPurpose subNode)984 constexpr std::string_view sensorPurposeToString(SensorPurpose subNode)
985 {
986     if (subNode == SensorPurpose::totalPower)
987     {
988         return "xyz.openbmc_project.Sensor.Purpose.SensorPurpose.TotalPower";
989     }
990 
991     return "Unknown Purpose";
992 }
993 
sensorPurposeFromString(const std::string & subNodeStr)994 inline SensorPurpose sensorPurposeFromString(const std::string& subNodeStr)
995 {
996     // If none match unknownNode is returned
997     SensorPurpose subNode = SensorPurpose::unknownPurpose;
998 
999     if (subNodeStr ==
1000         "xyz.openbmc_project.Sensor.Purpose.SensorPurpose.TotalPower")
1001     {
1002         subNode = SensorPurpose::totalPower;
1003     }
1004 
1005     return subNode;
1006 }
1007 
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)1008 inline void checkSensorPurpose(
1009     const std::string& serviceName, const std::string& sensorPath,
1010     const SensorPurpose sensorPurpose,
1011     const std::shared_ptr<SensorServicePathList>& sensorMatches,
1012     const std::shared_ptr<boost::system::error_code>& asyncErrors,
1013     const boost::system::error_code& ec,
1014     const std::vector<std::string>& purposeList)
1015 {
1016     // If not found this sensor will be skipped but allow to
1017     // continue processing remaining sensors
1018     if (ec && (ec != boost::system::errc::io_error) && (ec.value() != EBADR))
1019     {
1020         BMCWEB_LOG_DEBUG("D-Bus response error : {}", ec);
1021         *asyncErrors = ec;
1022     }
1023 
1024     if (!ec)
1025     {
1026         const std::string_view checkPurposeStr =
1027             sensorPurposeToString(sensorPurpose);
1028 
1029         BMCWEB_LOG_DEBUG("checkSensorPurpose: {}", checkPurposeStr);
1030 
1031         for (const std::string& purposeStr : purposeList)
1032         {
1033             if (purposeStr == checkPurposeStr)
1034             {
1035                 // Add to list
1036                 BMCWEB_LOG_DEBUG("checkSensorPurpose adding {} {}", serviceName,
1037                                  sensorPath);
1038                 sensorMatches->emplace_back(serviceName, sensorPath);
1039                 return;
1040             }
1041         }
1042     }
1043 }
1044 
1045 /**
1046  * @brief Gets sensors from list with specified purpose
1047  *
1048  * Checks the <sensorListIn> sensors for any which implement the specified
1049  * <sensorPurpose> in interface xyz.openbmc_project.Sensor.Purpose. Adds any
1050  * matches to the <sensorMatches> list. After checking all sensors in
1051  * <sensorListIn> the <callback> is called with just the list of matching
1052  * sensors, which could be an empty list. Additionally the <callback> is called
1053  * on error and error handling is left to the callback.
1054  *
1055  * @param asyncResp Response data
1056  * @param[in] sensorListIn List of sensors to check
1057  * @param[in] sensorPurpose Looks for sensors matching this purpose
1058  * @param[out] sensorMatches Sensors from sensorListIn with sensorPurpose
1059  * @param[in] callback Callback to handle list of matching sensors
1060  */
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)1061 inline void getSensorsByPurpose(
1062     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1063     const SensorServicePathList& sensorListIn,
1064     const SensorPurpose sensorPurpose,
1065     const std::shared_ptr<SensorServicePathList>& sensorMatches,
1066     const std::function<void(
1067         const boost::system::error_code& ec,
1068         const std::shared_ptr<SensorServicePathList>& sensorMatches)>& callback)
1069 {
1070     /* Keeps track of number of asynchronous calls made. Once all handlers have
1071      * been called the callback function is called with the results.
1072      */
1073     std::shared_ptr<int> remainingSensorsToVist =
1074         std::make_shared<int>(sensorListIn.size());
1075 
1076     /* Holds last unrecoverable error returned by any of the asynchronous
1077      * handlers. Once all handlers have been called the callback will be sent
1078      * the error to handle.
1079      */
1080     std::shared_ptr<boost::system::error_code> asyncErrors =
1081         std::make_shared<boost::system::error_code>();
1082 
1083     BMCWEB_LOG_DEBUG("getSensorsByPurpose enter {}", *remainingSensorsToVist);
1084 
1085     for (const auto& [serviceName, sensorPath] : sensorListIn)
1086     {
1087         dbus::utility::getProperty<std::vector<std::string>>(
1088             serviceName, sensorPath, "xyz.openbmc_project.Sensor.Purpose",
1089             "Purpose",
1090             [asyncResp, serviceName, sensorPath, sensorPurpose, sensorMatches,
1091              callback, remainingSensorsToVist,
1092              asyncErrors](const boost::system::error_code& ec,
1093                           const std::vector<std::string>& purposeList) {
1094                 // Keep track of sensor visited
1095                 (*remainingSensorsToVist)--;
1096                 BMCWEB_LOG_DEBUG("Visited {}. Remaining sensors {}", sensorPath,
1097                                  *remainingSensorsToVist);
1098 
1099                 checkSensorPurpose(serviceName, sensorPath, sensorPurpose,
1100                                    sensorMatches, asyncErrors, ec, purposeList);
1101 
1102                 // All sensors have been visited
1103                 if (*remainingSensorsToVist == 0)
1104                 {
1105                     BMCWEB_LOG_DEBUG(
1106                         "getSensorsByPurpose, exit found {} matches",
1107                         sensorMatches->size());
1108                     callback(*asyncErrors, sensorMatches);
1109                     return;
1110                 }
1111             });
1112     }
1113 }
1114 
1115 } // namespace sensor_utils
1116 } // namespace redfish
1117