1 #pragma once
2 
3 #include "dbus_utility.hpp"
4 #include "generated/enums/resource.hpp"
5 #include "generated/enums/sensor.hpp"
6 #include "generated/enums/thermal.hpp"
7 #include "str_utility.hpp"
8 #include "utils/dbus_utils.hpp"
9 #include "utils/json_utils.hpp"
10 
11 #include <sdbusplus/unpack_properties.hpp>
12 
13 #include <algorithm>
14 #include <format>
15 #include <ranges>
16 #include <string>
17 #include <string_view>
18 #include <tuple>
19 #include <utility>
20 #include <vector>
21 
22 namespace redfish
23 {
24 namespace sensor_utils
25 {
26 
27 enum class ChassisSubNode
28 {
29     powerNode,
30     sensorsNode,
31     thermalNode,
32     unknownNode,
33 };
34 
chassisSubNodeToString(ChassisSubNode subNode)35 constexpr std::string_view chassisSubNodeToString(ChassisSubNode subNode)
36 {
37     switch (subNode)
38     {
39         case ChassisSubNode::powerNode:
40             return "Power";
41         case ChassisSubNode::sensorsNode:
42             return "Sensors";
43         case ChassisSubNode::thermalNode:
44             return "Thermal";
45         case ChassisSubNode::unknownNode:
46         default:
47             return "";
48     }
49 }
50 
chassisSubNodeFromString(const std::string & subNodeStr)51 inline ChassisSubNode chassisSubNodeFromString(const std::string& subNodeStr)
52 {
53     // If none match unknownNode is returned
54     ChassisSubNode subNode = ChassisSubNode::unknownNode;
55 
56     if (subNodeStr == "Power")
57     {
58         subNode = ChassisSubNode::powerNode;
59     }
60     else if (subNodeStr == "Sensors")
61     {
62         subNode = ChassisSubNode::sensorsNode;
63     }
64     else if (subNodeStr == "Thermal")
65     {
66         subNode = ChassisSubNode::thermalNode;
67     }
68 
69     return subNode;
70 }
71 
72 /**
73  * Possible states for physical inventory leds
74  */
75 enum class LedState
76 {
77     OFF,
78     ON,
79     BLINK,
80     UNKNOWN
81 };
82 
83 /**
84  * D-Bus inventory item associated with one or more sensors.
85  */
86 class InventoryItem
87 {
88   public:
InventoryItem(const std::string & objPath)89     explicit InventoryItem(const std::string& objPath) : objectPath(objPath)
90     {
91         // Set inventory item name to last node of object path
92         sdbusplus::message::object_path path(objectPath);
93         name = path.filename();
94         if (name.empty())
95         {
96             BMCWEB_LOG_ERROR("Failed to find '/' in {}", objectPath);
97         }
98     }
99 
100     std::string objectPath;
101     std::string name;
102     bool isPresent = true;
103     bool isFunctional = true;
104     bool isPowerSupply = false;
105     int powerSupplyEfficiencyPercent = -1;
106     std::string manufacturer;
107     std::string model;
108     std::string partNumber;
109     std::string serialNumber;
110     std::set<std::string> sensors;
111     std::string ledObjectPath;
112     LedState ledState = LedState::UNKNOWN;
113 };
114 
getSensorId(std::string_view sensorName,std::string_view sensorType)115 inline std::string getSensorId(std::string_view sensorName,
116                                std::string_view sensorType)
117 {
118     std::string normalizedType(sensorType);
119     auto remove = std::ranges::remove(normalizedType, '_');
120     normalizedType.erase(std::ranges::begin(remove), normalizedType.end());
121 
122     return std::format("{}_{}", normalizedType, sensorName);
123 }
124 
125 inline std::pair<std::string, std::string>
splitSensorNameAndType(std::string_view sensorId)126     splitSensorNameAndType(std::string_view sensorId)
127 {
128     size_t index = sensorId.find('_');
129     if (index == std::string::npos)
130     {
131         return std::make_pair<std::string, std::string>("", "");
132     }
133     std::string sensorType{sensorId.substr(0, index)};
134     std::string sensorName{sensorId.substr(index + 1)};
135     // fan_pwm and fan_tach need special handling
136     if (sensorType == "fantach" || sensorType == "fanpwm")
137     {
138         sensorType.insert(3, 1, '_');
139     }
140     return std::make_pair(sensorType, sensorName);
141 }
142 
143 namespace sensors
144 {
toReadingUnits(std::string_view sensorType)145 inline std::string_view toReadingUnits(std::string_view sensorType)
146 {
147     if (sensorType == "voltage")
148     {
149         return "V";
150     }
151     if (sensorType == "power")
152     {
153         return "W";
154     }
155     if (sensorType == "current")
156     {
157         return "A";
158     }
159     if (sensorType == "fan_tach")
160     {
161         return "RPM";
162     }
163     if (sensorType == "temperature")
164     {
165         return "Cel";
166     }
167     if (sensorType == "fan_pwm" || sensorType == "utilization" ||
168         sensorType == "humidity")
169     {
170         return "%";
171     }
172     if (sensorType == "altitude")
173     {
174         return "m";
175     }
176     if (sensorType == "airflow")
177     {
178         return "cft_i/min";
179     }
180     if (sensorType == "energy")
181     {
182         return "J";
183     }
184     return "";
185 }
186 
toReadingType(std::string_view sensorType)187 inline sensor::ReadingType toReadingType(std::string_view sensorType)
188 {
189     if (sensorType == "voltage")
190     {
191         return sensor::ReadingType::Voltage;
192     }
193     if (sensorType == "power")
194     {
195         return sensor::ReadingType::Power;
196     }
197     if (sensorType == "current")
198     {
199         return sensor::ReadingType::Current;
200     }
201     if (sensorType == "fan_tach")
202     {
203         return sensor::ReadingType::Rotational;
204     }
205     if (sensorType == "temperature")
206     {
207         return sensor::ReadingType::Temperature;
208     }
209     if (sensorType == "fan_pwm" || sensorType == "utilization")
210     {
211         return sensor::ReadingType::Percent;
212     }
213     if (sensorType == "humidity")
214     {
215         return sensor::ReadingType::Humidity;
216     }
217     if (sensorType == "altitude")
218     {
219         return sensor::ReadingType::Altitude;
220     }
221     if (sensorType == "airflow")
222     {
223         return sensor::ReadingType::AirFlow;
224     }
225     if (sensorType == "energy")
226     {
227         return sensor::ReadingType::EnergyJoules;
228     }
229     return sensor::ReadingType::Invalid;
230 }
231 
232 } // namespace sensors
233 
234 /**
235  * @brief Returns the Redfish State value for the specified inventory item.
236  * @param inventoryItem D-Bus inventory item associated with a sensor.
237  * @param sensorAvailable Boolean representing if D-Bus sensor is marked as
238  * available.
239  * @return State value for inventory item.
240  */
getState(const InventoryItem * inventoryItem,const bool sensorAvailable)241 inline resource::State getState(const InventoryItem* inventoryItem,
242                                 const bool sensorAvailable)
243 {
244     if ((inventoryItem != nullptr) && !(inventoryItem->isPresent))
245     {
246         return resource::State::Absent;
247     }
248 
249     if (!sensorAvailable)
250     {
251         return resource::State::UnavailableOffline;
252     }
253 
254     return resource::State::Enabled;
255 }
256 
257 /**
258  * @brief Returns the Redfish Health value for the specified sensor.
259  * @param sensorJson Sensor JSON object.
260  * @param valuesDict Map of all sensor DBus values.
261  * @param inventoryItem D-Bus inventory item associated with the sensor.  Will
262  * be nullptr if no associated inventory item was found.
263  * @return Health value for sensor.
264  */
getHealth(nlohmann::json & sensorJson,const dbus::utility::DBusPropertiesMap & valuesDict,const InventoryItem * inventoryItem)265 inline std::string getHealth(nlohmann::json& sensorJson,
266                              const dbus::utility::DBusPropertiesMap& valuesDict,
267                              const InventoryItem* inventoryItem)
268 {
269     // Get current health value (if any) in the sensor JSON object.  Some JSON
270     // objects contain multiple sensors (such as PowerSupplies).  We want to set
271     // the overall health to be the most severe of any of the sensors.
272     std::string currentHealth;
273     auto statusIt = sensorJson.find("Status");
274     if (statusIt != sensorJson.end())
275     {
276         auto healthIt = statusIt->find("Health");
277         if (healthIt != statusIt->end())
278         {
279             std::string* health = healthIt->get_ptr<std::string*>();
280             if (health != nullptr)
281             {
282                 currentHealth = *health;
283             }
284         }
285     }
286 
287     // If current health in JSON object is already Critical, return that.  This
288     // should override the sensor health, which might be less severe.
289     if (currentHealth == "Critical")
290     {
291         return "Critical";
292     }
293 
294     const bool* criticalAlarmHigh = nullptr;
295     const bool* criticalAlarmLow = nullptr;
296     const bool* warningAlarmHigh = nullptr;
297     const bool* warningAlarmLow = nullptr;
298 
299     const bool success = sdbusplus::unpackPropertiesNoThrow(
300         dbus_utils::UnpackErrorPrinter(), valuesDict, "CriticalAlarmHigh",
301         criticalAlarmHigh, "CriticalAlarmLow", criticalAlarmLow,
302         "WarningAlarmHigh", warningAlarmHigh, "WarningAlarmLow",
303         warningAlarmLow);
304 
305     if (success)
306     {
307         // Check if sensor has critical threshold alarm
308         if ((criticalAlarmHigh != nullptr && *criticalAlarmHigh) ||
309             (criticalAlarmLow != nullptr && *criticalAlarmLow))
310         {
311             return "Critical";
312         }
313     }
314 
315     // Check if associated inventory item is not functional
316     if ((inventoryItem != nullptr) && !(inventoryItem->isFunctional))
317     {
318         return "Critical";
319     }
320 
321     // If current health in JSON object is already Warning, return that. This
322     // should override the sensor status, which might be less severe.
323     if (currentHealth == "Warning")
324     {
325         return "Warning";
326     }
327 
328     if (success)
329     {
330         // Check if sensor has warning threshold alarm
331         if ((warningAlarmHigh != nullptr && *warningAlarmHigh) ||
332             (warningAlarmLow != nullptr && *warningAlarmLow))
333         {
334             return "Warning";
335         }
336     }
337 
338     return "OK";
339 }
340 
setLedState(nlohmann::json & sensorJson,const InventoryItem * inventoryItem)341 inline void setLedState(nlohmann::json& sensorJson,
342                         const InventoryItem* inventoryItem)
343 {
344     if (inventoryItem != nullptr && !inventoryItem->ledObjectPath.empty())
345     {
346         switch (inventoryItem->ledState)
347         {
348             case LedState::OFF:
349                 sensorJson["IndicatorLED"] = resource::IndicatorLED::Off;
350                 break;
351             case LedState::ON:
352                 sensorJson["IndicatorLED"] = resource::IndicatorLED::Lit;
353                 break;
354             case LedState::BLINK:
355                 sensorJson["IndicatorLED"] = resource::IndicatorLED::Blinking;
356                 break;
357             default:
358                 break;
359         }
360     }
361 }
362 
363 /**
364  * @brief Builds a json sensor representation of a sensor.
365  * @param sensorName  The name of the sensor to be built
366  * @param sensorType  The type (temperature, fan_tach, etc) of the sensor to
367  * build
368  * @param chassisSubNode The subnode (thermal, sensor, etc) of the sensor
369  * @param propertiesDict A dictionary of the properties to build the sensor
370  * from.
371  * @param sensorJson  The json object to fill
372  * @param inventoryItem D-Bus inventory item associated with the sensor.  Will
373  * be nullptr if no associated inventory item was found.
374  */
objectPropertiesToJson(std::string_view sensorName,std::string_view sensorType,ChassisSubNode chassisSubNode,const dbus::utility::DBusPropertiesMap & propertiesDict,nlohmann::json & sensorJson,InventoryItem * inventoryItem)375 inline void objectPropertiesToJson(
376     std::string_view sensorName, std::string_view sensorType,
377     ChassisSubNode chassisSubNode,
378     const dbus::utility::DBusPropertiesMap& propertiesDict,
379     nlohmann::json& sensorJson, InventoryItem* inventoryItem)
380 {
381     if (chassisSubNode == ChassisSubNode::sensorsNode)
382     {
383         std::string subNodeEscaped = getSensorId(sensorName, sensorType);
384         // For sensors in SensorCollection we set Id instead of MemberId,
385         // including power sensors.
386         sensorJson["Id"] = std::move(subNodeEscaped);
387 
388         std::string sensorNameEs(sensorName);
389         std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
390         sensorJson["Name"] = std::move(sensorNameEs);
391     }
392     else if (sensorType != "power")
393     {
394         // Set MemberId and Name for non-power sensors.  For PowerSupplies and
395         // PowerControl, those properties have more general values because
396         // multiple sensors can be stored in the same JSON object.
397         std::string sensorNameEs(sensorName);
398         std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
399         sensorJson["Name"] = std::move(sensorNameEs);
400     }
401 
402     const bool* checkAvailable = nullptr;
403     bool available = true;
404     const bool success = sdbusplus::unpackPropertiesNoThrow(
405         dbus_utils::UnpackErrorPrinter(), propertiesDict, "Available",
406         checkAvailable);
407     if (!success)
408     {
409         messages::internalError();
410     }
411     if (checkAvailable != nullptr)
412     {
413         available = *checkAvailable;
414     }
415 
416     sensorJson["Status"]["State"] = getState(inventoryItem, available);
417     sensorJson["Status"]["Health"] =
418         getHealth(sensorJson, propertiesDict, inventoryItem);
419 
420     // Parameter to set to override the type we get from dbus, and force it to
421     // int, regardless of what is available.  This is used for schemas like fan,
422     // that require integers, not floats.
423     bool forceToInt = false;
424 
425     nlohmann::json::json_pointer unit("/Reading");
426     if (chassisSubNode == ChassisSubNode::sensorsNode)
427     {
428         sensorJson["@odata.type"] = "#Sensor.v1_2_0.Sensor";
429 
430         sensor::ReadingType readingType = sensors::toReadingType(sensorType);
431         if (readingType == sensor::ReadingType::Invalid)
432         {
433             BMCWEB_LOG_ERROR("Redfish cannot map reading type for {}",
434                              sensorType);
435         }
436         else
437         {
438             sensorJson["ReadingType"] = readingType;
439         }
440 
441         std::string_view readingUnits = sensors::toReadingUnits(sensorType);
442         if (readingUnits.empty())
443         {
444             BMCWEB_LOG_ERROR("Redfish cannot map reading unit for {}",
445                              sensorType);
446         }
447         else
448         {
449             sensorJson["ReadingUnits"] = readingUnits;
450         }
451     }
452     else if (sensorType == "temperature")
453     {
454         unit = "/ReadingCelsius"_json_pointer;
455         sensorJson["@odata.type"] = "#Thermal.v1_3_0.Temperature";
456         // TODO(ed) Documentation says that path should be type fan_tach,
457         // implementation seems to implement fan
458     }
459     else if (sensorType == "fan" || sensorType == "fan_tach")
460     {
461         unit = "/Reading"_json_pointer;
462         sensorJson["ReadingUnits"] = thermal::ReadingUnits::RPM;
463         sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
464         setLedState(sensorJson, inventoryItem);
465         forceToInt = true;
466     }
467     else if (sensorType == "fan_pwm")
468     {
469         unit = "/Reading"_json_pointer;
470         sensorJson["ReadingUnits"] = thermal::ReadingUnits::Percent;
471         sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
472         setLedState(sensorJson, inventoryItem);
473         forceToInt = true;
474     }
475     else if (sensorType == "voltage")
476     {
477         unit = "/ReadingVolts"_json_pointer;
478         sensorJson["@odata.type"] = "#Power.v1_0_0.Voltage";
479     }
480     else if (sensorType == "power")
481     {
482         std::string lower;
483         std::ranges::transform(sensorName, std::back_inserter(lower),
484                                bmcweb::asciiToLower);
485         if (lower == "total_power")
486         {
487             sensorJson["@odata.type"] = "#Power.v1_0_0.PowerControl";
488             // Put multiple "sensors" into a single PowerControl, so have
489             // generic names for MemberId and Name. Follows Redfish mockup.
490             sensorJson["MemberId"] = "0";
491             sensorJson["Name"] = "Chassis Power Control";
492             unit = "/PowerConsumedWatts"_json_pointer;
493         }
494         else if (lower.find("input") != std::string::npos)
495         {
496             unit = "/PowerInputWatts"_json_pointer;
497         }
498         else
499         {
500             unit = "/PowerOutputWatts"_json_pointer;
501         }
502     }
503     else
504     {
505         BMCWEB_LOG_ERROR("Redfish cannot map object type for {}", sensorName);
506         return;
507     }
508     // Map of dbus interface name, dbus property name and redfish property_name
509     std::vector<
510         std::tuple<const char*, const char*, nlohmann::json::json_pointer>>
511         properties;
512 
513     properties.emplace_back("xyz.openbmc_project.Sensor.Value", "Value", unit);
514 
515     if (chassisSubNode == ChassisSubNode::sensorsNode)
516     {
517         properties.emplace_back(
518             "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningHigh",
519             "/Thresholds/UpperCaution/Reading"_json_pointer);
520         properties.emplace_back(
521             "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningLow",
522             "/Thresholds/LowerCaution/Reading"_json_pointer);
523         properties.emplace_back(
524             "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalHigh",
525             "/Thresholds/UpperCritical/Reading"_json_pointer);
526         properties.emplace_back(
527             "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalLow",
528             "/Thresholds/LowerCritical/Reading"_json_pointer);
529 
530         /* Add additional properties specific to sensorType */
531         if (sensorType == "fan_tach")
532         {
533             properties.emplace_back("xyz.openbmc_project.Sensor.Value", "Value",
534                                     "/SpeedRPM"_json_pointer);
535         }
536     }
537     else if (sensorType != "power")
538     {
539         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Warning",
540                                 "WarningHigh",
541                                 "/UpperThresholdNonCritical"_json_pointer);
542         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Warning",
543                                 "WarningLow",
544                                 "/LowerThresholdNonCritical"_json_pointer);
545         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Critical",
546                                 "CriticalHigh",
547                                 "/UpperThresholdCritical"_json_pointer);
548         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Critical",
549                                 "CriticalLow",
550                                 "/LowerThresholdCritical"_json_pointer);
551     }
552 
553     // TODO Need to get UpperThresholdFatal and LowerThresholdFatal
554 
555     if (chassisSubNode == ChassisSubNode::sensorsNode)
556     {
557         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
558                                 "/ReadingRangeMin"_json_pointer);
559         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
560                                 "/ReadingRangeMax"_json_pointer);
561         properties.emplace_back("xyz.openbmc_project.Sensor.Accuracy",
562                                 "Accuracy", "/Accuracy"_json_pointer);
563     }
564     else if (sensorType == "temperature")
565     {
566         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
567                                 "/MinReadingRangeTemp"_json_pointer);
568         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
569                                 "/MaxReadingRangeTemp"_json_pointer);
570     }
571     else if (sensorType != "power")
572     {
573         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
574                                 "/MinReadingRange"_json_pointer);
575         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
576                                 "/MaxReadingRange"_json_pointer);
577     }
578 
579     for (const std::tuple<const char*, const char*,
580                           nlohmann::json::json_pointer>& p : properties)
581     {
582         for (const auto& [valueName, valueVariant] : propertiesDict)
583         {
584             if (valueName != std::get<1>(p))
585             {
586                 continue;
587             }
588 
589             // The property we want to set may be nested json, so use
590             // a json_pointer for easy indexing into the json structure.
591             const nlohmann::json::json_pointer& key = std::get<2>(p);
592 
593             const double* doubleValue = std::get_if<double>(&valueVariant);
594             if (doubleValue == nullptr)
595             {
596                 BMCWEB_LOG_ERROR("Got value interface that wasn't double");
597                 continue;
598             }
599             if (!std::isfinite(*doubleValue))
600             {
601                 if (valueName == "Value")
602                 {
603                     // Readings are allowed to be NAN for unavailable;  coerce
604                     // them to null in the json response.
605                     sensorJson[key] = nullptr;
606                     continue;
607                 }
608                 BMCWEB_LOG_WARNING("Sensor value for {} was unexpectedly {}",
609                                    valueName, *doubleValue);
610                 continue;
611             }
612             if (forceToInt)
613             {
614                 sensorJson[key] = static_cast<int64_t>(*doubleValue);
615             }
616             else
617             {
618                 sensorJson[key] = *doubleValue;
619             }
620         }
621     }
622 }
623 
624 } // namespace sensor_utils
625 } // namespace redfish
626