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