xref: /openbmc/bmcweb/redfish-core/include/utils/sensor_utils.hpp (revision 433c9193b0d086f009d53a5860f1ee586cf45792)
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         if constexpr (BMCWEB_REDFISH_ALLOW_ROTATIONAL_FANS)
203         {
204             return "RPM";
205         }
206         else
207         {
208             return "%";
209         }
210     }
211     if (sensorType == "temperature")
212     {
213         return "Cel";
214     }
215     if (sensorType == "fan_pwm" || sensorType == "utilization" ||
216         sensorType == "humidity")
217     {
218         return "%";
219     }
220     if (sensorType == "altitude")
221     {
222         return "m";
223     }
224     if (sensorType == "airflow")
225     {
226         return "cft_i/min";
227     }
228     if (sensorType == "energy")
229     {
230         return "J";
231     }
232     if (sensorType == "liquidflow")
233     {
234         return "L/min";
235     }
236     if (sensorType == "pressure")
237     {
238         return "Pa";
239     }
240     return "";
241 }
242 
toReadingType(std::string_view sensorType)243 inline sensor::ReadingType toReadingType(std::string_view sensorType)
244 {
245     if (sensorType == "voltage")
246     {
247         return sensor::ReadingType::Voltage;
248     }
249     if (sensorType == "power")
250     {
251         return sensor::ReadingType::Power;
252     }
253     if (sensorType == "current")
254     {
255         return sensor::ReadingType::Current;
256     }
257     if (sensorType == "fan_tach")
258     {
259         if constexpr (BMCWEB_REDFISH_ALLOW_ROTATIONAL_FANS)
260         {
261             return sensor::ReadingType::Rotational;
262         }
263         else
264         {
265             return sensor::ReadingType::Percent;
266         }
267     }
268     if (sensorType == "temperature")
269     {
270         return sensor::ReadingType::Temperature;
271     }
272     if (sensorType == "fan_pwm" || sensorType == "utilization")
273     {
274         return sensor::ReadingType::Percent;
275     }
276     if (sensorType == "humidity")
277     {
278         return sensor::ReadingType::Humidity;
279     }
280     if (sensorType == "altitude")
281     {
282         return sensor::ReadingType::Altitude;
283     }
284     if (sensorType == "airflow")
285     {
286         return sensor::ReadingType::AirFlow;
287     }
288     if (sensorType == "energy")
289     {
290         return sensor::ReadingType::EnergyJoules;
291     }
292     if (sensorType == "liquidflow")
293     {
294         return sensor::ReadingType::LiquidFlowLPM;
295     }
296     if (sensorType == "pressure")
297     {
298         return sensor::ReadingType::PressurePa;
299     }
300     return sensor::ReadingType::Invalid;
301 }
302 
303 } // namespace sensors
304 
305 // represents metric Id, metadata, reading value and timestamp of single
306 // reading update in milliseconds
307 using Reading = std::tuple<std::string, std::string, double, uint64_t>;
308 // represents multiple independent readings
309 using Readings = std::vector<Reading>;
310 // represents a timestamp and multiple independent readings
311 using Statistics = std::tuple<uint64_t, Readings>;
312 // represents sensor's path, its metadata
313 using SensorPaths =
314     std::vector<std::tuple<sdbusplus::message::object_path, std::string>>;
315 // represents reading parameters for statistics readings
316 using ReadingParameters =
317     std::vector<std::tuple<SensorPaths, std::string, std::string, uint64_t>>;
318 
updateSensorStatistics(nlohmann::json & sensorJson,const std::optional<Statistics> & statistics,const std::optional<ReadingParameters> & readingParameters)319 inline void updateSensorStatistics(
320     nlohmann::json& sensorJson, const std::optional<Statistics>& statistics,
321     const std::optional<ReadingParameters>& readingParameters)
322 {
323     if (statistics.has_value() && readingParameters.has_value())
324     {
325         Readings metrics = std::get<1>(*statistics);
326         for (const auto& [sensorPaths, operationType, metricId, duration] :
327              *readingParameters)
328         {
329             if (operationType ==
330                 "xyz.openbmc_project.Telemetry.Report.OperationType.Maximum")
331             {
332                 if (metrics.size() == 1)
333                 {
334                     const auto& [id, metadata, value, timestamp] = metrics[0];
335                     sensorJson["PeakReading"] = value;
336                     if (timestamp != 0)
337                     {
338                         sensorJson["PeakReadingTime"] = timestamp;
339                     }
340                 }
341             }
342         }
343     }
344 }
345 
346 /**
347  * @brief Returns the Redfish State value for the specified inventory item.
348  * @param inventoryItem D-Bus inventory item associated with a sensor.
349  * @param sensorAvailable Boolean representing if D-Bus sensor is marked as
350  * available.
351  * @return State value for inventory item.
352  */
getState(const InventoryItem * inventoryItem,const bool sensorAvailable)353 inline resource::State getState(const InventoryItem* inventoryItem,
354                                 const bool sensorAvailable)
355 {
356     if ((inventoryItem != nullptr) && !(inventoryItem->isPresent))
357     {
358         return resource::State::Absent;
359     }
360 
361     if (!sensorAvailable)
362     {
363         return resource::State::UnavailableOffline;
364     }
365 
366     return resource::State::Enabled;
367 }
368 
369 /**
370  * @brief Returns the Redfish Health value for the specified sensor.
371  * @param sensorJson Sensor JSON object.
372  * @param valuesDict Map of all sensor DBus values.
373  * @param inventoryItem D-Bus inventory item associated with the sensor.  Will
374  * be nullptr if no associated inventory item was found.
375  * @return Health value for sensor.
376  */
getHealth(nlohmann::json & sensorJson,const dbus::utility::DBusPropertiesMap & valuesDict,const InventoryItem * inventoryItem)377 inline std::string getHealth(nlohmann::json& sensorJson,
378                              const dbus::utility::DBusPropertiesMap& valuesDict,
379                              const InventoryItem* inventoryItem)
380 {
381     // Get current health value (if any) in the sensor JSON object.  Some JSON
382     // objects contain multiple sensors (such as PowerSupplies).  We want to set
383     // the overall health to be the most severe of any of the sensors.
384     std::string currentHealth;
385     auto statusIt = sensorJson.find("Status");
386     if (statusIt != sensorJson.end())
387     {
388         auto healthIt = statusIt->find("Health");
389         if (healthIt != statusIt->end())
390         {
391             std::string* health = healthIt->get_ptr<std::string*>();
392             if (health != nullptr)
393             {
394                 currentHealth = *health;
395             }
396         }
397     }
398 
399     // If current health in JSON object is already Critical, return that.  This
400     // should override the sensor health, which might be less severe.
401     if (currentHealth == "Critical")
402     {
403         return "Critical";
404     }
405 
406     const bool* criticalAlarmHigh = nullptr;
407     const bool* criticalAlarmLow = nullptr;
408     const bool* warningAlarmHigh = nullptr;
409     const bool* warningAlarmLow = nullptr;
410 
411     const bool success = sdbusplus::unpackPropertiesNoThrow(
412         dbus_utils::UnpackErrorPrinter(), valuesDict, "CriticalAlarmHigh",
413         criticalAlarmHigh, "CriticalAlarmLow", criticalAlarmLow,
414         "WarningAlarmHigh", warningAlarmHigh, "WarningAlarmLow",
415         warningAlarmLow);
416 
417     if (success)
418     {
419         // Check if sensor has critical threshold alarm
420         if ((criticalAlarmHigh != nullptr && *criticalAlarmHigh) ||
421             (criticalAlarmLow != nullptr && *criticalAlarmLow))
422         {
423             return "Critical";
424         }
425     }
426 
427     // Check if associated inventory item is not functional
428     if ((inventoryItem != nullptr) && !(inventoryItem->isFunctional))
429     {
430         return "Critical";
431     }
432 
433     // If current health in JSON object is already Warning, return that. This
434     // should override the sensor status, which might be less severe.
435     if (currentHealth == "Warning")
436     {
437         return "Warning";
438     }
439 
440     if (success)
441     {
442         // Check if sensor has warning threshold alarm
443         if ((warningAlarmHigh != nullptr && *warningAlarmHigh) ||
444             (warningAlarmLow != nullptr && *warningAlarmLow))
445         {
446             return "Warning";
447         }
448     }
449 
450     return "OK";
451 }
452 
setLedState(nlohmann::json & sensorJson,const InventoryItem * inventoryItem)453 inline void setLedState(nlohmann::json& sensorJson,
454                         const InventoryItem* inventoryItem)
455 {
456     if (inventoryItem != nullptr && !inventoryItem->ledObjectPath.empty())
457     {
458         switch (inventoryItem->ledState)
459         {
460             case LedState::OFF:
461                 sensorJson["IndicatorLED"] = resource::IndicatorLED::Off;
462                 break;
463             case LedState::ON:
464                 sensorJson["IndicatorLED"] = resource::IndicatorLED::Lit;
465                 break;
466             case LedState::BLINK:
467                 sensorJson["IndicatorLED"] = resource::IndicatorLED::Blinking;
468                 break;
469             default:
470                 break;
471         }
472     }
473 }
474 
dBusSensorReadingBasisToRedfish(const std::string & readingBasis)475 inline sensor::ReadingBasisType dBusSensorReadingBasisToRedfish(
476     const std::string& readingBasis)
477 {
478     if (readingBasis ==
479         "xyz.openbmc_project.Sensor.Type.ReadingBasisType.Headroom")
480     {
481         return sensor::ReadingBasisType::Headroom;
482     }
483     if (readingBasis ==
484         "xyz.openbmc_project.Sensor.Type.ReadingBasisType.Delta")
485     {
486         return sensor::ReadingBasisType::Delta;
487     }
488     if (readingBasis == "xyz.openbmc_project.Sensor.Type.ReadingBasisType.Zero")
489     {
490         return sensor::ReadingBasisType::Zero;
491     }
492 
493     return sensor::ReadingBasisType::Invalid;
494 }
495 
496 /**
497  * @brief Computes percent value for Rotational Fan (fan_tach)
498  * @param[in] sensorName  The name of the fan_tach sensor
499  * @param[in] maxValue The MaxValue D-Bus value
500  * @param[in] minValue The MinValue D-Bus value
501  * @param[in] value The Value D-Bus value
502  * @return Computed percent returned here if sanity checks pass
503  */
getFanPercent(std::string_view sensorName,std::optional<double> maxValue,std::optional<double> minValue,std::optional<double> value)504 inline std::optional<long> getFanPercent(
505     std::string_view sensorName, std::optional<double> maxValue,
506     std::optional<double> minValue, std::optional<double> value)
507 {
508     /* Sanity check values to be used to compute the percent and guard against
509      * divide by zero.
510      */
511     if (!value.has_value() || !std::isfinite(*value))
512     {
513         BMCWEB_LOG_DEBUG("No Value for {}", sensorName);
514         return std::nullopt;
515     }
516 
517     if (!maxValue.has_value() || !std::isfinite(*maxValue))
518     {
519         BMCWEB_LOG_DEBUG("maxValue not available for {}", sensorName);
520         return std::nullopt;
521     }
522 
523     if (!minValue.has_value() || !std::isfinite(*minValue))
524     {
525         BMCWEB_LOG_DEBUG("minValue not available for {}", sensorName);
526         return std::nullopt;
527     }
528 
529     double range = *maxValue - *minValue;
530     if (range <= 0)
531     {
532         BMCWEB_LOG_ERROR("Bad min/max for {}", sensorName);
533         return std::nullopt;
534     }
535 
536     // Compute fan's RPM Percent value
537     return std::lround(((*value - *minValue) * 100) / range);
538 }
539 
convertToFanPercent(nlohmann::json & sensorJson,std::string_view sensorName,nlohmann::json::json_pointer & unit,std::optional<double> maxValue,std::optional<double> minValue,std::optional<double> value,bool isExcerpt)540 inline void convertToFanPercent(
541     nlohmann::json& sensorJson, std::string_view sensorName,
542     nlohmann::json::json_pointer& unit, std::optional<double> maxValue,
543     std::optional<double> minValue, std::optional<double> value, bool isExcerpt)
544 {
545     // RPM value reported under SpeedRPM only
546     unit = "/SpeedRPM"_json_pointer;
547 
548     if (!isExcerpt)
549     {
550         // Convert max/min to percent range to match Reading
551         sensorJson["ReadingRangeMax"] = 100;
552         sensorJson["ReadingRangeMin"] = 0;
553     }
554 
555     std::optional<long> percentValue =
556         getFanPercent(sensorName, maxValue, minValue, value);
557     if (percentValue.has_value())
558     {
559         sensorJson["Reading"] = percentValue;
560     }
561     else
562     {
563         // Indication that percent couldn't be computed
564         sensorJson["Reading"] = nullptr;
565     }
566 }
567 
dBusSensorImplementationToRedfish(const std::string & implementation)568 inline sensor::ImplementationType dBusSensorImplementationToRedfish(
569     const std::string& implementation)
570 {
571     if (implementation ==
572         "xyz.openbmc_project.Sensor.Type.ImplementationType.Physical")
573     {
574         return sensor::ImplementationType::PhysicalSensor;
575     }
576     if (implementation ==
577         "xyz.openbmc_project.Sensor.Type.ImplementationType.Synthesized")
578     {
579         return sensor::ImplementationType::Synthesized;
580     }
581     if (implementation ==
582         "xyz.openbmc_project.Sensor.Type.ImplementationType.Reported")
583     {
584         return sensor::ImplementationType::Reported;
585     }
586 
587     return sensor::ImplementationType::Invalid;
588 }
589 
fillSensorStatus(const dbus::utility::DBusPropertiesMap & propertiesDict,nlohmann::json & sensorJson,InventoryItem * inventoryItem)590 inline void fillSensorStatus(
591     const dbus::utility::DBusPropertiesMap& propertiesDict,
592     nlohmann::json& sensorJson, InventoryItem* inventoryItem)
593 {
594     const bool* checkAvailable = nullptr;
595     bool available = true;
596 
597     const bool success = sdbusplus::unpackPropertiesNoThrow(
598         dbus_utils::UnpackErrorPrinter(), propertiesDict, "Available",
599         checkAvailable);
600     if (!success)
601     {
602         messages::internalError();
603     }
604     if (checkAvailable != nullptr)
605     {
606         available = *checkAvailable;
607     }
608 
609     sensorJson["Status"]["State"] = getState(inventoryItem, available);
610     sensorJson["Status"]["Health"] =
611         getHealth(sensorJson, propertiesDict, inventoryItem);
612 }
613 
fillSensorIdentity(std::string_view sensorName,std::string_view sensorType,const dbus::utility::DBusPropertiesMap & propertiesDict,nlohmann::json & sensorJson,bool isExcerpt,nlohmann::json::json_pointer & unit)614 inline bool fillSensorIdentity(
615     std::string_view sensorName, std::string_view sensorType,
616     const dbus::utility::DBusPropertiesMap& propertiesDict,
617     nlohmann::json& sensorJson, bool isExcerpt,
618     nlohmann::json::json_pointer& unit)
619 {
620     std::optional<std::string> readingBasis;
621     std::optional<std::string> implementation;
622     std::optional<Statistics> statistics;
623     std::optional<ReadingParameters> readingParameters;
624     std::optional<double> maxValue;
625     std::optional<double> minValue;
626     std::optional<double> value;
627 
628     const bool success = sdbusplus::unpackPropertiesNoThrow(
629         dbus_utils::UnpackErrorPrinter(), propertiesDict, "ReadingBasis",
630         readingBasis, "Implementation", implementation, "Readings", statistics,
631         "ReadingParameters", readingParameters, "MaxValue", maxValue,
632         "MinValue", minValue, "Value", value);
633 
634     if (!success)
635     {
636         BMCWEB_LOG_ERROR("Failed to unpack properties");
637         messages::internalError();
638         return false;
639     }
640 
641     /* Sensor excerpts use different keys to reference the sensor. These are
642      * built by the caller.
643      * Additionally they don't include these additional properties.
644      */
645     if (!isExcerpt)
646     {
647         std::string subNodeEscaped = getSensorId(sensorName, sensorType);
648         // For sensors in SensorCollection we set Id instead of MemberId,
649         // including power sensors.
650         sensorJson["Id"] = std::move(subNodeEscaped);
651 
652         std::string sensorNameEs(sensorName);
653         std::ranges::replace(sensorNameEs, '_', ' ');
654         sensorJson["Name"] = std::move(sensorNameEs);
655         sensorJson["@odata.type"] = "#Sensor.v1_11_1.Sensor";
656 
657         sensor::ReadingType readingType = sensors::toReadingType(sensorType);
658         if (readingType == sensor::ReadingType::Invalid)
659         {
660             BMCWEB_LOG_ERROR("Redfish cannot map reading type for {}",
661                              sensorType);
662         }
663         else
664         {
665             sensorJson["ReadingType"] = readingType;
666         }
667 
668         std::string_view readingUnits = sensors::toReadingUnits(sensorType);
669         if (readingUnits.empty())
670         {
671             BMCWEB_LOG_ERROR("Redfish cannot map reading unit for {}",
672                              sensorType);
673         }
674         else
675         {
676             sensorJson["ReadingUnits"] = readingUnits;
677         }
678 
679         if (readingBasis.has_value())
680         {
681             sensor::ReadingBasisType readingBasisOpt =
682                 dBusSensorReadingBasisToRedfish(*readingBasis);
683             if (readingBasisOpt != sensor::ReadingBasisType::Invalid)
684             {
685                 sensorJson["ReadingBasis"] = readingBasisOpt;
686             }
687         }
688 
689         if (implementation.has_value())
690         {
691             sensor::ImplementationType implementationOpt =
692                 dBusSensorImplementationToRedfish(*implementation);
693             if (implementationOpt != sensor::ImplementationType::Invalid)
694             {
695                 sensorJson["Implementation"] = implementationOpt;
696             }
697         }
698 
699         updateSensorStatistics(sensorJson, statistics, readingParameters);
700     }
701 
702     if (sensorType == "fan_tach")
703     {
704         if constexpr (BMCWEB_REDFISH_ALLOW_ROTATIONAL_FANS)
705         {
706             if (value.has_value() && std::isfinite(*value))
707             {
708                 sensorJson["SpeedRPM"] = *value;
709             }
710         }
711         else
712         {
713             // Convert fan's RPM value to Percent value for Reading
714             convertToFanPercent(sensorJson, sensorName, unit, maxValue,
715                                 minValue, value, isExcerpt);
716         }
717     }
718 
719     return true;
720 }
721 
fillPowerThermalIdentity(std::string_view sensorName,std::string_view sensorType,nlohmann::json & sensorJson,InventoryItem * inventoryItem,nlohmann::json::json_pointer & unit,bool & forceToInt)722 inline bool fillPowerThermalIdentity(
723     std::string_view sensorName, std::string_view sensorType,
724     nlohmann::json& sensorJson, InventoryItem* inventoryItem,
725     nlohmann::json::json_pointer& unit, bool& forceToInt)
726 {
727     if (sensorType != "power")
728     {
729         // Set MemberId and Name for non-power sensors.  For PowerSupplies
730         // and PowerControl, those properties have more general values
731         // because multiple sensors can be stored in the same JSON object.
732         std::string sensorNameEs(sensorName);
733         std::ranges::replace(sensorNameEs, '_', ' ');
734         sensorJson["Name"] = std::move(sensorNameEs);
735     }
736 
737     if (sensorType == "temperature")
738     {
739         unit = "/ReadingCelsius"_json_pointer;
740         sensorJson["@odata.type"] = "#Thermal.v1_3_0.Temperature";
741         // TODO(ed) Documentation says that path should be type fan_tach,
742         // implementation seems to implement fan
743         return true;
744     }
745 
746     if (sensorType == "fan" || sensorType == "fan_tach")
747     {
748         unit = "/Reading"_json_pointer;
749         sensorJson["ReadingUnits"] = thermal::ReadingUnits::RPM;
750         sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
751         if constexpr (BMCWEB_REDFISH_ALLOW_DEPRECATED_INDICATORLED)
752         {
753             setLedState(sensorJson, inventoryItem);
754         }
755         forceToInt = true;
756         return true;
757     }
758 
759     if (sensorType == "fan_pwm")
760     {
761         unit = "/Reading"_json_pointer;
762         sensorJson["ReadingUnits"] = thermal::ReadingUnits::Percent;
763         sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
764         if constexpr (BMCWEB_REDFISH_ALLOW_DEPRECATED_INDICATORLED)
765         {
766             setLedState(sensorJson, inventoryItem);
767         }
768         forceToInt = true;
769         return true;
770     }
771 
772     if (sensorType == "voltage")
773     {
774         unit = "/ReadingVolts"_json_pointer;
775         sensorJson["@odata.type"] = "#Power.v1_0_0.Voltage";
776         return true;
777     }
778 
779     if (sensorType == "power")
780     {
781         std::string lower;
782         std::ranges::transform(sensorName, std::back_inserter(lower),
783                                bmcweb::asciiToLower);
784         if (lower == "total_power")
785         {
786             sensorJson["@odata.type"] = "#Power.v1_0_0.PowerControl";
787             // Put multiple "sensors" into a single PowerControl, so have
788             // generic names for MemberId and Name. Follows Redfish mockup.
789             sensorJson["MemberId"] = "0";
790             sensorJson["Name"] = "Chassis Power Control";
791             unit = "/PowerConsumedWatts"_json_pointer;
792         }
793         else if (lower.find("input") != std::string::npos)
794         {
795             unit = "/PowerInputWatts"_json_pointer;
796         }
797         else
798         {
799             unit = "/PowerOutputWatts"_json_pointer;
800         }
801         return true;
802     }
803 
804     BMCWEB_LOG_ERROR("Redfish cannot map object type for {}", sensorName);
805     return false;
806 }
807 
808 // Map of dbus interface name, dbus property name and redfish property_name
809 using SensorPropertyMap = std::tuple<std::string_view, std::string_view,
810                                      nlohmann::json::json_pointer>;
811 using SensorPropertyList = std::vector<SensorPropertyMap>;
812 
mapPropertiesBySubnode(std::string_view sensorType,ChassisSubNode chassisSubNode,SensorPropertyList & properties,nlohmann::json::json_pointer & unit,bool isExcerpt)813 inline void mapPropertiesBySubnode(
814     std::string_view sensorType, ChassisSubNode chassisSubNode,
815     SensorPropertyList& properties, nlohmann::json::json_pointer& unit,
816     bool isExcerpt)
817 {
818     // unit contains the redfish property_name based on the sensor type/node
819     properties.emplace_back("xyz.openbmc_project.Sensor.Value", "Value", unit);
820 
821     if (isExcerpt)
822     {
823         // Excerpts don't have any of these extended properties
824         return;
825     }
826 
827     if (chassisSubNode == ChassisSubNode::sensorsNode)
828     {
829         properties.emplace_back(
830             "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningHigh",
831             "/Thresholds/UpperCaution/Reading"_json_pointer);
832         properties.emplace_back(
833             "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningLow",
834             "/Thresholds/LowerCaution/Reading"_json_pointer);
835         properties.emplace_back(
836             "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalHigh",
837             "/Thresholds/UpperCritical/Reading"_json_pointer);
838         properties.emplace_back(
839             "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalLow",
840             "/Thresholds/LowerCritical/Reading"_json_pointer);
841         properties.emplace_back(
842             "xyz.openbmc_project.Sensor.Threshold.HardShutdown",
843             "HardShutdownHigh", "/Thresholds/UpperFatal/Reading"_json_pointer);
844         properties.emplace_back(
845             "xyz.openbmc_project.Sensor.Threshold.HardShutdown",
846             "HardShutdownLow", "/Thresholds/LowerFatal/Reading"_json_pointer);
847     }
848     else if (sensorType != "power")
849     {
850         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Warning",
851                                 "WarningHigh",
852                                 "/UpperThresholdNonCritical"_json_pointer);
853         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Warning",
854                                 "WarningLow",
855                                 "/LowerThresholdNonCritical"_json_pointer);
856         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Critical",
857                                 "CriticalHigh",
858                                 "/UpperThresholdCritical"_json_pointer);
859         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Critical",
860                                 "CriticalLow",
861                                 "/LowerThresholdCritical"_json_pointer);
862     }
863 
864     // TODO Need to get UpperThresholdFatal and LowerThresholdFatal
865 
866     if (chassisSubNode == ChassisSubNode::sensorsNode)
867     {
868         if constexpr (BMCWEB_REDFISH_ALLOW_ROTATIONAL_FANS)
869         {
870             properties.emplace_back("xyz.openbmc_project.Sensor.Value",
871                                     "MinValue",
872                                     "/ReadingRangeMin"_json_pointer);
873             properties.emplace_back("xyz.openbmc_project.Sensor.Value",
874                                     "MaxValue",
875                                     "/ReadingRangeMax"_json_pointer);
876         }
877         else
878         {
879             /* fan_tach sensors are converted to percent by
880              * convertToFanPercent() called from fillSensorIdentity().
881              * ReadingRangeMin and ReadingRangeMax converted to percent range in
882              * that function as well.
883              */
884             if (sensorType != "fan_tach")
885             {
886                 properties.emplace_back("xyz.openbmc_project.Sensor.Value",
887                                         "MinValue",
888                                         "/ReadingRangeMin"_json_pointer);
889                 properties.emplace_back("xyz.openbmc_project.Sensor.Value",
890                                         "MaxValue",
891                                         "/ReadingRangeMax"_json_pointer);
892             }
893         }
894         properties.emplace_back("xyz.openbmc_project.Sensor.Accuracy",
895                                 "Accuracy", "/Accuracy"_json_pointer);
896     }
897     else if (sensorType == "temperature")
898     {
899         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
900                                 "/MinReadingRangeTemp"_json_pointer);
901         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
902                                 "/MaxReadingRangeTemp"_json_pointer);
903     }
904     else if (sensorType != "power")
905     {
906         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
907                                 "/MinReadingRange"_json_pointer);
908         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
909                                 "/MaxReadingRange"_json_pointer);
910     }
911 }
912 
913 /**
914  * @brief Builds a json sensor representation of a sensor.
915  * @param sensorName  The name of the sensor to be built
916  * @param sensorType  The type (temperature, fan_tach, etc) of the sensor to
917  * build
918  * @param chassisSubNode The subnode (thermal, sensor, etc) of the sensor
919  * @param propertiesDict A dictionary of the properties to build the sensor
920  * from.
921  * @param sensorJson  The json object to fill
922  * @param inventoryItem D-Bus inventory item associated with the sensor.  Will
923  * be nullptr if no associated inventory item was found.
924  */
objectPropertiesToJson(std::string_view sensorName,std::string_view sensorType,ChassisSubNode chassisSubNode,const dbus::utility::DBusPropertiesMap & propertiesDict,nlohmann::json & sensorJson,InventoryItem * inventoryItem)925 inline void objectPropertiesToJson(
926     std::string_view sensorName, std::string_view sensorType,
927     ChassisSubNode chassisSubNode,
928     const dbus::utility::DBusPropertiesMap& propertiesDict,
929     nlohmann::json& sensorJson, InventoryItem* inventoryItem)
930 {
931     // Parameter to set to override the type we get from dbus, and force it to
932     // int, regardless of what is available.  This is used for schemas like fan,
933     // that require integers, not floats.
934     bool forceToInt = false;
935 
936     nlohmann::json::json_pointer unit("/Reading");
937 
938     // This ChassisSubNode builds sensor excerpts
939     bool isExcerpt = isExcerptNode(chassisSubNode);
940     bool filledOk = false;
941 
942     if (chassisSubNode == ChassisSubNode::sensorsNode || isExcerpt)
943     {
944         filledOk = fillSensorIdentity(sensorName, sensorType, propertiesDict,
945                                       sensorJson, isExcerpt, unit);
946     }
947     else
948     {
949         filledOk =
950             fillPowerThermalIdentity(sensorName, sensorType, sensorJson,
951                                      inventoryItem, unit, std::ref(forceToInt));
952     }
953     if (!filledOk)
954     {
955         return;
956     }
957 
958     if (!isExcerpt)
959     {
960         fillSensorStatus(propertiesDict, sensorJson, inventoryItem);
961     }
962 
963     // Map of dbus interface name, dbus property name and redfish property_name
964     SensorPropertyList properties;
965 
966     // Add additional property mappings based on the sensor type/node
967     mapPropertiesBySubnode(sensorType, chassisSubNode, properties, unit,
968                            isExcerpt);
969 
970     for (const SensorPropertyMap& p : properties)
971     {
972         for (const auto& [valueName, valueVariant] : propertiesDict)
973         {
974             if (valueName != std::get<1>(p))
975             {
976                 continue;
977             }
978 
979             // The property we want to set may be nested json, so use
980             // a json_pointer for easy indexing into the json structure.
981             const nlohmann::json::json_pointer& key = std::get<2>(p);
982 
983             const double* doubleValue = std::get_if<double>(&valueVariant);
984             if (doubleValue == nullptr)
985             {
986                 BMCWEB_LOG_ERROR("Got value interface that wasn't double");
987                 continue;
988             }
989             if (!std::isfinite(*doubleValue))
990             {
991                 if (valueName == "Value")
992                 {
993                     // Readings are allowed to be NAN for unavailable;  coerce
994                     // them to null in the json response.
995                     sensorJson[key] = nullptr;
996                     continue;
997                 }
998                 BMCWEB_LOG_WARNING("Sensor value for {} was unexpectedly {}",
999                                    valueName, *doubleValue);
1000                 continue;
1001             }
1002             if (forceToInt)
1003             {
1004                 sensorJson[key] = static_cast<int64_t>(*doubleValue);
1005             }
1006             else
1007             {
1008                 sensorJson[key] = *doubleValue;
1009             }
1010         }
1011     }
1012 }
1013 
1014 /**
1015  * @brief Builds a json sensor excerpt representation of a sensor.
1016  *
1017  * @details This is a wrapper function to provide consistent setting of
1018  * "DataSourceUri" for sensor excerpts and filling of properties. Since sensor
1019  * excerpts usually have just the D-Bus path for the sensor that is accepted
1020  * and used to build "DataSourceUri".
1021 
1022  * @param path The D-Bus path to the sensor to be built
1023  * @param chassisId The Chassis Id for the sensor
1024  * @param chassisSubNode The subnode (e.g. ThermalMetrics) of the sensor
1025  * @param sensorTypeExpected The expected type of the sensor
1026  * @param propertiesDict A dictionary of the properties to build the sensor
1027  * from.
1028  * @param sensorJson  The json object to fill
1029  * @returns True if sensorJson object filled. False on any error.
1030  * Caller is responsible for handling error.
1031  */
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)1032 inline bool objectExcerptToJson(
1033     const std::string& path, const std::string_view chassisId,
1034     ChassisSubNode chassisSubNode,
1035     const std::optional<std::string>& sensorTypeExpected,
1036     const dbus::utility::DBusPropertiesMap& propertiesDict,
1037     nlohmann::json& sensorJson)
1038 {
1039     if (!isExcerptNode(chassisSubNode))
1040     {
1041         BMCWEB_LOG_DEBUG("{} is not a sensor excerpt",
1042                          chassisSubNodeToString(chassisSubNode));
1043         return false;
1044     }
1045 
1046     sdbusplus::message::object_path sensorPath(path);
1047     std::string sensorName = sensorPath.filename();
1048     std::string sensorType = sensorPath.parent_path().filename();
1049     if (sensorName.empty() || sensorType.empty())
1050     {
1051         BMCWEB_LOG_DEBUG("Invalid sensor path {}", path);
1052         return false;
1053     }
1054 
1055     if (sensorTypeExpected && (sensorType != *sensorTypeExpected))
1056     {
1057         BMCWEB_LOG_DEBUG("{} is not expected type {}", path,
1058                          *sensorTypeExpected);
1059         return false;
1060     }
1061 
1062     // Sensor excerpts use DataSourceUri to reference full sensor Redfish path
1063     sensorJson["DataSourceUri"] =
1064         boost::urls::format("/redfish/v1/Chassis/{}/Sensors/{}", chassisId,
1065                             getSensorId(sensorName, sensorType));
1066 
1067     // Fill in sensor excerpt properties
1068     objectPropertiesToJson(sensorName, sensorType, chassisSubNode,
1069                            propertiesDict, sensorJson, nullptr);
1070 
1071     return true;
1072 }
1073 
1074 // Maps D-Bus: Service, SensorPath
1075 using SensorServicePathMap = std::pair<std::string, std::string>;
1076 using SensorServicePathList = std::vector<SensorServicePathMap>;
1077 
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)1078 inline void getAllSensorObjects(
1079     const std::string& associatedPath, const std::string& path,
1080     std::span<const std::string_view> interfaces, const int32_t depth,
1081     std::function<void(const boost::system::error_code& ec,
1082                        SensorServicePathList&)>&& callback)
1083 {
1084     sdbusplus::message::object_path endpointPath{associatedPath};
1085     endpointPath /= "all_sensors";
1086 
1087     dbus::utility::getAssociatedSubTree(
1088         endpointPath, sdbusplus::message::object_path(path), depth, interfaces,
1089         [callback = std::move(callback)](
1090             const boost::system::error_code& ec,
1091             const dbus::utility::MapperGetSubTreeResponse& subtree) {
1092             SensorServicePathList sensorsServiceAndPath;
1093 
1094             if (ec)
1095             {
1096                 callback(ec, sensorsServiceAndPath);
1097                 return;
1098             }
1099 
1100             for (const auto& [sensorPath, serviceMaps] : subtree)
1101             {
1102                 for (const auto& [service, mapInterfaces] : serviceMaps)
1103                 {
1104                     sensorsServiceAndPath.emplace_back(service, sensorPath);
1105                 }
1106             }
1107 
1108             callback(ec, sensorsServiceAndPath);
1109         });
1110 }
1111 
1112 enum class SensorPurpose
1113 {
1114     totalPower,
1115     unknownPurpose,
1116 };
1117 
sensorPurposeToString(SensorPurpose subNode)1118 constexpr std::string_view sensorPurposeToString(SensorPurpose subNode)
1119 {
1120     if (subNode == SensorPurpose::totalPower)
1121     {
1122         return "xyz.openbmc_project.Sensor.Purpose.SensorPurpose.TotalPower";
1123     }
1124 
1125     return "Unknown Purpose";
1126 }
1127 
sensorPurposeFromString(const std::string & subNodeStr)1128 inline SensorPurpose sensorPurposeFromString(const std::string& subNodeStr)
1129 {
1130     // If none match unknownNode is returned
1131     SensorPurpose subNode = SensorPurpose::unknownPurpose;
1132 
1133     if (subNodeStr ==
1134         "xyz.openbmc_project.Sensor.Purpose.SensorPurpose.TotalPower")
1135     {
1136         subNode = SensorPurpose::totalPower;
1137     }
1138 
1139     return subNode;
1140 }
1141 
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)1142 inline void checkSensorPurpose(
1143     const std::string& serviceName, const std::string& sensorPath,
1144     const SensorPurpose sensorPurpose,
1145     const std::shared_ptr<SensorServicePathList>& sensorMatches,
1146     const std::shared_ptr<boost::system::error_code>& asyncErrors,
1147     const boost::system::error_code& ec,
1148     const std::vector<std::string>& purposeList)
1149 {
1150     // If not found this sensor will be skipped but allow to
1151     // continue processing remaining sensors
1152     if (ec && (ec != boost::system::errc::io_error) && (ec.value() != EBADR))
1153     {
1154         BMCWEB_LOG_DEBUG("D-Bus response error : {}", ec);
1155         *asyncErrors = ec;
1156     }
1157 
1158     if (!ec)
1159     {
1160         const std::string_view checkPurposeStr =
1161             sensorPurposeToString(sensorPurpose);
1162 
1163         BMCWEB_LOG_DEBUG("checkSensorPurpose: {}", checkPurposeStr);
1164 
1165         for (const std::string& purposeStr : purposeList)
1166         {
1167             if (purposeStr == checkPurposeStr)
1168             {
1169                 // Add to list
1170                 BMCWEB_LOG_DEBUG("checkSensorPurpose adding {} {}", serviceName,
1171                                  sensorPath);
1172                 sensorMatches->emplace_back(serviceName, sensorPath);
1173                 return;
1174             }
1175         }
1176     }
1177 }
1178 
1179 /**
1180  * @brief Gets sensors from list with specified purpose
1181  *
1182  * Checks the <sensorListIn> sensors for any which implement the specified
1183  * <sensorPurpose> in interface xyz.openbmc_project.Sensor.Purpose. Adds any
1184  * matches to the <sensorMatches> list. After checking all sensors in
1185  * <sensorListIn> the <callback> is called with just the list of matching
1186  * sensors, which could be an empty list. Additionally the <callback> is called
1187  * on error and error handling is left to the callback.
1188  *
1189  * @param asyncResp Response data
1190  * @param[in] sensorListIn List of sensors to check
1191  * @param[in] sensorPurpose Looks for sensors matching this purpose
1192  * @param[out] sensorMatches Sensors from sensorListIn with sensorPurpose
1193  * @param[in] callback Callback to handle list of matching sensors
1194  */
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)1195 inline void getSensorsByPurpose(
1196     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1197     const SensorServicePathList& sensorListIn,
1198     const SensorPurpose sensorPurpose,
1199     const std::shared_ptr<SensorServicePathList>& sensorMatches,
1200     const std::function<void(
1201         const boost::system::error_code& ec,
1202         const std::shared_ptr<SensorServicePathList>& sensorMatches)>& callback)
1203 {
1204     /* Keeps track of number of asynchronous calls made. Once all handlers have
1205      * been called the callback function is called with the results.
1206      */
1207     std::shared_ptr<int> remainingSensorsToVist =
1208         std::make_shared<int>(sensorListIn.size());
1209 
1210     /* Holds last unrecoverable error returned by any of the asynchronous
1211      * handlers. Once all handlers have been called the callback will be sent
1212      * the error to handle.
1213      */
1214     std::shared_ptr<boost::system::error_code> asyncErrors =
1215         std::make_shared<boost::system::error_code>();
1216 
1217     BMCWEB_LOG_DEBUG("getSensorsByPurpose enter {}", *remainingSensorsToVist);
1218 
1219     for (const auto& [serviceName, sensorPath] : sensorListIn)
1220     {
1221         dbus::utility::getProperty<std::vector<std::string>>(
1222             serviceName, sensorPath, "xyz.openbmc_project.Sensor.Purpose",
1223             "Purpose",
1224             [asyncResp, serviceName, sensorPath, sensorPurpose, sensorMatches,
1225              callback, remainingSensorsToVist,
1226              asyncErrors](const boost::system::error_code& ec,
1227                           const std::vector<std::string>& purposeList) {
1228                 // Keep track of sensor visited
1229                 (*remainingSensorsToVist)--;
1230                 BMCWEB_LOG_DEBUG("Visited {}. Remaining sensors {}", sensorPath,
1231                                  *remainingSensorsToVist);
1232 
1233                 checkSensorPurpose(serviceName, sensorPath, sensorPurpose,
1234                                    sensorMatches, asyncErrors, ec, purposeList);
1235 
1236                 // All sensors have been visited
1237                 if (*remainingSensorsToVist == 0)
1238                 {
1239                     BMCWEB_LOG_DEBUG(
1240                         "getSensorsByPurpose, exit found {} matches",
1241                         sensorMatches->size());
1242                     callback(*asyncErrors, sensorMatches);
1243                     return;
1244                 }
1245             });
1246     }
1247 }
1248 
1249 } // namespace sensor_utils
1250 } // namespace redfish
1251