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