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