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 
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 
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:
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 
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>
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 {
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 
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  */
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  */
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 
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  */
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     properties.reserve(7);
513 
514     properties.emplace_back("xyz.openbmc_project.Sensor.Value", "Value", unit);
515 
516     if (chassisSubNode == ChassisSubNode::sensorsNode)
517     {
518         properties.emplace_back(
519             "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningHigh",
520             "/Thresholds/UpperCaution/Reading"_json_pointer);
521         properties.emplace_back(
522             "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningLow",
523             "/Thresholds/LowerCaution/Reading"_json_pointer);
524         properties.emplace_back(
525             "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalHigh",
526             "/Thresholds/UpperCritical/Reading"_json_pointer);
527         properties.emplace_back(
528             "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalLow",
529             "/Thresholds/LowerCritical/Reading"_json_pointer);
530     }
531     else if (sensorType != "power")
532     {
533         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Warning",
534                                 "WarningHigh",
535                                 "/UpperThresholdNonCritical"_json_pointer);
536         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Warning",
537                                 "WarningLow",
538                                 "/LowerThresholdNonCritical"_json_pointer);
539         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Critical",
540                                 "CriticalHigh",
541                                 "/UpperThresholdCritical"_json_pointer);
542         properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Critical",
543                                 "CriticalLow",
544                                 "/LowerThresholdCritical"_json_pointer);
545     }
546 
547     // TODO Need to get UpperThresholdFatal and LowerThresholdFatal
548 
549     if (chassisSubNode == ChassisSubNode::sensorsNode)
550     {
551         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
552                                 "/ReadingRangeMin"_json_pointer);
553         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
554                                 "/ReadingRangeMax"_json_pointer);
555         properties.emplace_back("xyz.openbmc_project.Sensor.Accuracy",
556                                 "Accuracy", "/Accuracy"_json_pointer);
557     }
558     else if (sensorType == "temperature")
559     {
560         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
561                                 "/MinReadingRangeTemp"_json_pointer);
562         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
563                                 "/MaxReadingRangeTemp"_json_pointer);
564     }
565     else if (sensorType != "power")
566     {
567         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
568                                 "/MinReadingRange"_json_pointer);
569         properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
570                                 "/MaxReadingRange"_json_pointer);
571     }
572 
573     for (const std::tuple<const char*, const char*,
574                           nlohmann::json::json_pointer>& p : properties)
575     {
576         for (const auto& [valueName, valueVariant] : propertiesDict)
577         {
578             if (valueName != std::get<1>(p))
579             {
580                 continue;
581             }
582 
583             // The property we want to set may be nested json, so use
584             // a json_pointer for easy indexing into the json structure.
585             const nlohmann::json::json_pointer& key = std::get<2>(p);
586 
587             const double* doubleValue = std::get_if<double>(&valueVariant);
588             if (doubleValue == nullptr)
589             {
590                 BMCWEB_LOG_ERROR("Got value interface that wasn't double");
591                 continue;
592             }
593             if (!std::isfinite(*doubleValue))
594             {
595                 if (valueName == "Value")
596                 {
597                     // Readings are allowed to be NAN for unavailable;  coerce
598                     // them to null in the json response.
599                     sensorJson[key] = nullptr;
600                     continue;
601                 }
602                 BMCWEB_LOG_WARNING("Sensor value for {} was unexpectedly {}",
603                                    valueName, *doubleValue);
604                 continue;
605             }
606             if (forceToInt)
607             {
608                 sensorJson[key] = static_cast<int64_t>(*doubleValue);
609             }
610             else
611             {
612                 sensorJson[key] = *doubleValue;
613             }
614         }
615     }
616 }
617 
618 } // namespace sensor_utils
619 } // namespace redfish
620