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 return "RPM";
203 }
204 if (sensorType == "temperature")
205 {
206 return "Cel";
207 }
208 if (sensorType == "fan_pwm" || sensorType == "utilization" ||
209 sensorType == "humidity")
210 {
211 return "%";
212 }
213 if (sensorType == "altitude")
214 {
215 return "m";
216 }
217 if (sensorType == "airflow")
218 {
219 return "cft_i/min";
220 }
221 if (sensorType == "energy")
222 {
223 return "J";
224 }
225 if (sensorType == "liquidflow")
226 {
227 return "L/min";
228 }
229 if (sensorType == "pressure")
230 {
231 return "Pa";
232 }
233 return "";
234 }
235
toReadingType(std::string_view sensorType)236 inline sensor::ReadingType toReadingType(std::string_view sensorType)
237 {
238 if (sensorType == "voltage")
239 {
240 return sensor::ReadingType::Voltage;
241 }
242 if (sensorType == "power")
243 {
244 return sensor::ReadingType::Power;
245 }
246 if (sensorType == "current")
247 {
248 return sensor::ReadingType::Current;
249 }
250 if (sensorType == "fan_tach")
251 {
252 return sensor::ReadingType::Rotational;
253 }
254 if (sensorType == "temperature")
255 {
256 return sensor::ReadingType::Temperature;
257 }
258 if (sensorType == "fan_pwm" || sensorType == "utilization")
259 {
260 return sensor::ReadingType::Percent;
261 }
262 if (sensorType == "humidity")
263 {
264 return sensor::ReadingType::Humidity;
265 }
266 if (sensorType == "altitude")
267 {
268 return sensor::ReadingType::Altitude;
269 }
270 if (sensorType == "airflow")
271 {
272 return sensor::ReadingType::AirFlow;
273 }
274 if (sensorType == "energy")
275 {
276 return sensor::ReadingType::EnergyJoules;
277 }
278 if (sensorType == "liquidflow")
279 {
280 return sensor::ReadingType::LiquidFlowLPM;
281 }
282 if (sensorType == "pressure")
283 {
284 return sensor::ReadingType::PressurePa;
285 }
286 return sensor::ReadingType::Invalid;
287 }
288
289 } // namespace sensors
290
291 // represents metric Id, metadata, reading value and timestamp of single
292 // reading update in milliseconds
293 using Reading = std::tuple<std::string, std::string, double, uint64_t>;
294 // represents multiple independent readings
295 using Readings = std::vector<Reading>;
296 // represents a timestamp and multiple independent readings
297 using Statistics = std::tuple<uint64_t, Readings>;
298 // represents sensor's path, its metadata
299 using SensorPaths =
300 std::vector<std::tuple<sdbusplus::message::object_path, std::string>>;
301 // represents reading parameters for statistics readings
302 using ReadingParameters =
303 std::vector<std::tuple<SensorPaths, std::string, std::string, uint64_t>>;
304
updateSensorStatistics(nlohmann::json & sensorJson,const std::optional<Statistics> & statistics,const std::optional<ReadingParameters> & readingParameters)305 inline void updateSensorStatistics(
306 nlohmann::json& sensorJson, const std::optional<Statistics>& statistics,
307 const std::optional<ReadingParameters>& readingParameters)
308 {
309 if (statistics.has_value() && readingParameters.has_value())
310 {
311 Readings metrics = std::get<1>(*statistics);
312 for (const auto& [sensorPaths, operationType, metricId, duration] :
313 *readingParameters)
314 {
315 if (operationType ==
316 "xyz.openbmc_project.Telemetry.Report.OperationType.Maximum")
317 {
318 if (metrics.size() == 1)
319 {
320 const auto& [id, metadata, value, timestamp] = metrics[0];
321 sensorJson["PeakReading"] = value;
322 if (timestamp != 0)
323 {
324 sensorJson["PeakReadingTime"] = timestamp;
325 }
326 }
327 }
328 }
329 }
330 }
331
332 /**
333 * @brief Returns the Redfish State value for the specified inventory item.
334 * @param inventoryItem D-Bus inventory item associated with a sensor.
335 * @param sensorAvailable Boolean representing if D-Bus sensor is marked as
336 * available.
337 * @return State value for inventory item.
338 */
getState(const InventoryItem * inventoryItem,const bool sensorAvailable)339 inline resource::State getState(const InventoryItem* inventoryItem,
340 const bool sensorAvailable)
341 {
342 if ((inventoryItem != nullptr) && !(inventoryItem->isPresent))
343 {
344 return resource::State::Absent;
345 }
346
347 if (!sensorAvailable)
348 {
349 return resource::State::UnavailableOffline;
350 }
351
352 return resource::State::Enabled;
353 }
354
355 /**
356 * @brief Returns the Redfish Health value for the specified sensor.
357 * @param sensorJson Sensor JSON object.
358 * @param valuesDict Map of all sensor DBus values.
359 * @param inventoryItem D-Bus inventory item associated with the sensor. Will
360 * be nullptr if no associated inventory item was found.
361 * @return Health value for sensor.
362 */
getHealth(nlohmann::json & sensorJson,const dbus::utility::DBusPropertiesMap & valuesDict,const InventoryItem * inventoryItem)363 inline std::string getHealth(nlohmann::json& sensorJson,
364 const dbus::utility::DBusPropertiesMap& valuesDict,
365 const InventoryItem* inventoryItem)
366 {
367 // Get current health value (if any) in the sensor JSON object. Some JSON
368 // objects contain multiple sensors (such as PowerSupplies). We want to set
369 // the overall health to be the most severe of any of the sensors.
370 std::string currentHealth;
371 auto statusIt = sensorJson.find("Status");
372 if (statusIt != sensorJson.end())
373 {
374 auto healthIt = statusIt->find("Health");
375 if (healthIt != statusIt->end())
376 {
377 std::string* health = healthIt->get_ptr<std::string*>();
378 if (health != nullptr)
379 {
380 currentHealth = *health;
381 }
382 }
383 }
384
385 // If current health in JSON object is already Critical, return that. This
386 // should override the sensor health, which might be less severe.
387 if (currentHealth == "Critical")
388 {
389 return "Critical";
390 }
391
392 const bool* criticalAlarmHigh = nullptr;
393 const bool* criticalAlarmLow = nullptr;
394 const bool* warningAlarmHigh = nullptr;
395 const bool* warningAlarmLow = nullptr;
396
397 const bool success = sdbusplus::unpackPropertiesNoThrow(
398 dbus_utils::UnpackErrorPrinter(), valuesDict, "CriticalAlarmHigh",
399 criticalAlarmHigh, "CriticalAlarmLow", criticalAlarmLow,
400 "WarningAlarmHigh", warningAlarmHigh, "WarningAlarmLow",
401 warningAlarmLow);
402
403 if (success)
404 {
405 // Check if sensor has critical threshold alarm
406 if ((criticalAlarmHigh != nullptr && *criticalAlarmHigh) ||
407 (criticalAlarmLow != nullptr && *criticalAlarmLow))
408 {
409 return "Critical";
410 }
411 }
412
413 // Check if associated inventory item is not functional
414 if ((inventoryItem != nullptr) && !(inventoryItem->isFunctional))
415 {
416 return "Critical";
417 }
418
419 // If current health in JSON object is already Warning, return that. This
420 // should override the sensor status, which might be less severe.
421 if (currentHealth == "Warning")
422 {
423 return "Warning";
424 }
425
426 if (success)
427 {
428 // Check if sensor has warning threshold alarm
429 if ((warningAlarmHigh != nullptr && *warningAlarmHigh) ||
430 (warningAlarmLow != nullptr && *warningAlarmLow))
431 {
432 return "Warning";
433 }
434 }
435
436 return "OK";
437 }
438
setLedState(nlohmann::json & sensorJson,const InventoryItem * inventoryItem)439 inline void setLedState(nlohmann::json& sensorJson,
440 const InventoryItem* inventoryItem)
441 {
442 if (inventoryItem != nullptr && !inventoryItem->ledObjectPath.empty())
443 {
444 switch (inventoryItem->ledState)
445 {
446 case LedState::OFF:
447 sensorJson["IndicatorLED"] = resource::IndicatorLED::Off;
448 break;
449 case LedState::ON:
450 sensorJson["IndicatorLED"] = resource::IndicatorLED::Lit;
451 break;
452 case LedState::BLINK:
453 sensorJson["IndicatorLED"] = resource::IndicatorLED::Blinking;
454 break;
455 default:
456 break;
457 }
458 }
459 }
460
dBusSensorReadingBasisToRedfish(const std::string & readingBasis)461 inline sensor::ReadingBasisType dBusSensorReadingBasisToRedfish(
462 const std::string& readingBasis)
463 {
464 if (readingBasis ==
465 "xyz.openbmc_project.Sensor.Type.ReadingBasisType.Headroom")
466 {
467 return sensor::ReadingBasisType::Headroom;
468 }
469 if (readingBasis ==
470 "xyz.openbmc_project.Sensor.Type.ReadingBasisType.Delta")
471 {
472 return sensor::ReadingBasisType::Delta;
473 }
474 if (readingBasis == "xyz.openbmc_project.Sensor.Type.ReadingBasisType.Zero")
475 {
476 return sensor::ReadingBasisType::Zero;
477 }
478
479 return sensor::ReadingBasisType::Invalid;
480 }
481
dBusSensorImplementationToRedfish(const std::string & implementation)482 inline sensor::ImplementationType dBusSensorImplementationToRedfish(
483 const std::string& implementation)
484 {
485 if (implementation ==
486 "xyz.openbmc_project.Sensor.Type.ImplementationType.Physical")
487 {
488 return sensor::ImplementationType::PhysicalSensor;
489 }
490 if (implementation ==
491 "xyz.openbmc_project.Sensor.Type.ImplementationType.Synthesized")
492 {
493 return sensor::ImplementationType::Synthesized;
494 }
495 if (implementation ==
496 "xyz.openbmc_project.Sensor.Type.ImplementationType.Reported")
497 {
498 return sensor::ImplementationType::Reported;
499 }
500
501 return sensor::ImplementationType::Invalid;
502 }
503
504 /**
505 * @brief Builds a json sensor representation of a sensor.
506 * @param sensorName The name of the sensor to be built
507 * @param sensorType The type (temperature, fan_tach, etc) of the sensor to
508 * build
509 * @param chassisSubNode The subnode (thermal, sensor, etc) of the sensor
510 * @param propertiesDict A dictionary of the properties to build the sensor
511 * from.
512 * @param sensorJson The json object to fill
513 * @param inventoryItem D-Bus inventory item associated with the sensor. Will
514 * be nullptr if no associated inventory item was found.
515 */
objectPropertiesToJson(std::string_view sensorName,std::string_view sensorType,ChassisSubNode chassisSubNode,const dbus::utility::DBusPropertiesMap & propertiesDict,nlohmann::json & sensorJson,InventoryItem * inventoryItem)516 inline void objectPropertiesToJson(
517 std::string_view sensorName, std::string_view sensorType,
518 ChassisSubNode chassisSubNode,
519 const dbus::utility::DBusPropertiesMap& propertiesDict,
520 nlohmann::json& sensorJson, InventoryItem* inventoryItem)
521 {
522 // Parameter to set to override the type we get from dbus, and force it to
523 // int, regardless of what is available. This is used for schemas like fan,
524 // that require integers, not floats.
525 bool forceToInt = false;
526
527 nlohmann::json::json_pointer unit("/Reading");
528
529 // This ChassisSubNode builds sensor excerpts
530 bool isExcerpt = isExcerptNode(chassisSubNode);
531
532 /* Sensor excerpts use different keys to reference the sensor. These are
533 * built by the caller.
534 * Additionally they don't include these additional properties.
535 */
536 if (!isExcerpt)
537 {
538 if (chassisSubNode == ChassisSubNode::sensorsNode)
539 {
540 std::string subNodeEscaped = getSensorId(sensorName, sensorType);
541 // For sensors in SensorCollection we set Id instead of MemberId,
542 // including power sensors.
543 sensorJson["Id"] = std::move(subNodeEscaped);
544
545 std::string sensorNameEs(sensorName);
546 std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
547 sensorJson["Name"] = std::move(sensorNameEs);
548 }
549 else if (sensorType != "power")
550 {
551 // Set MemberId and Name for non-power sensors. For PowerSupplies
552 // and PowerControl, those properties have more general values
553 // because multiple sensors can be stored in the same JSON object.
554 std::string sensorNameEs(sensorName);
555 std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
556 sensorJson["Name"] = std::move(sensorNameEs);
557 }
558
559 const bool* checkAvailable = nullptr;
560 bool available = true;
561 std::optional<std::string> readingBasis;
562 std::optional<std::string> implementation;
563 std::optional<Statistics> statistics;
564 std::optional<ReadingParameters> readingParameters;
565
566 const bool success = sdbusplus::unpackPropertiesNoThrow(
567 dbus_utils::UnpackErrorPrinter(), propertiesDict, "Available",
568 checkAvailable, "ReadingBasis", readingBasis, "Implementation",
569 implementation, "Readings", statistics, "ReadingParameters",
570 readingParameters);
571 if (!success)
572 {
573 messages::internalError();
574 }
575 if (checkAvailable != nullptr)
576 {
577 available = *checkAvailable;
578 }
579
580 sensorJson["Status"]["State"] = getState(inventoryItem, available);
581 sensorJson["Status"]["Health"] =
582 getHealth(sensorJson, propertiesDict, inventoryItem);
583
584 if (chassisSubNode == ChassisSubNode::sensorsNode)
585 {
586 sensorJson["@odata.type"] = "#Sensor.v1_11_0.Sensor";
587
588 sensor::ReadingType readingType =
589 sensors::toReadingType(sensorType);
590 if (readingType == sensor::ReadingType::Invalid)
591 {
592 BMCWEB_LOG_ERROR("Redfish cannot map reading type for {}",
593 sensorType);
594 }
595 else
596 {
597 sensorJson["ReadingType"] = readingType;
598 }
599
600 std::string_view readingUnits = sensors::toReadingUnits(sensorType);
601 if (readingUnits.empty())
602 {
603 BMCWEB_LOG_ERROR("Redfish cannot map reading unit for {}",
604 sensorType);
605 }
606 else
607 {
608 sensorJson["ReadingUnits"] = readingUnits;
609 }
610
611 if (readingBasis.has_value())
612 {
613 sensor::ReadingBasisType readingBasisOpt =
614 dBusSensorReadingBasisToRedfish(*readingBasis);
615 if (readingBasisOpt != sensor::ReadingBasisType::Invalid)
616 {
617 sensorJson["ReadingBasis"] = readingBasisOpt;
618 }
619 }
620
621 if (implementation.has_value())
622 {
623 sensor::ImplementationType implementationOpt =
624 dBusSensorImplementationToRedfish(*implementation);
625 if (implementationOpt != sensor::ImplementationType::Invalid)
626 {
627 sensorJson["Implementation"] = implementationOpt;
628 }
629 }
630
631 updateSensorStatistics(sensorJson, statistics, readingParameters);
632 }
633 else if (sensorType == "temperature")
634 {
635 unit = "/ReadingCelsius"_json_pointer;
636 sensorJson["@odata.type"] = "#Thermal.v1_3_0.Temperature";
637 // TODO(ed) Documentation says that path should be type fan_tach,
638 // implementation seems to implement fan
639 }
640 else if (sensorType == "fan" || sensorType == "fan_tach")
641 {
642 unit = "/Reading"_json_pointer;
643 sensorJson["ReadingUnits"] = thermal::ReadingUnits::RPM;
644 sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
645 if constexpr (BMCWEB_REDFISH_ALLOW_DEPRECATED_INDICATORLED)
646 {
647 setLedState(sensorJson, inventoryItem);
648 }
649 forceToInt = true;
650 }
651 else if (sensorType == "fan_pwm")
652 {
653 unit = "/Reading"_json_pointer;
654 sensorJson["ReadingUnits"] = thermal::ReadingUnits::Percent;
655 sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
656 if constexpr (BMCWEB_REDFISH_ALLOW_DEPRECATED_INDICATORLED)
657 {
658 setLedState(sensorJson, inventoryItem);
659 }
660 forceToInt = true;
661 }
662 else if (sensorType == "voltage")
663 {
664 unit = "/ReadingVolts"_json_pointer;
665 sensorJson["@odata.type"] = "#Power.v1_0_0.Voltage";
666 }
667 else if (sensorType == "power")
668 {
669 std::string lower;
670 std::ranges::transform(sensorName, std::back_inserter(lower),
671 bmcweb::asciiToLower);
672 if (lower == "total_power")
673 {
674 sensorJson["@odata.type"] = "#Power.v1_0_0.PowerControl";
675 // Put multiple "sensors" into a single PowerControl, so have
676 // generic names for MemberId and Name. Follows Redfish mockup.
677 sensorJson["MemberId"] = "0";
678 sensorJson["Name"] = "Chassis Power Control";
679 unit = "/PowerConsumedWatts"_json_pointer;
680 }
681 else if (lower.find("input") != std::string::npos)
682 {
683 unit = "/PowerInputWatts"_json_pointer;
684 }
685 else
686 {
687 unit = "/PowerOutputWatts"_json_pointer;
688 }
689 }
690 else
691 {
692 BMCWEB_LOG_ERROR("Redfish cannot map object type for {}",
693 sensorName);
694 return;
695 }
696 }
697
698 // Map of dbus interface name, dbus property name and redfish property_name
699 std::vector<
700 std::tuple<const char*, const char*, nlohmann::json::json_pointer>>
701 properties;
702
703 properties.emplace_back("xyz.openbmc_project.Sensor.Value", "Value", unit);
704
705 if (!isExcerpt)
706 {
707 if (chassisSubNode == ChassisSubNode::sensorsNode)
708 {
709 properties.emplace_back(
710 "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningHigh",
711 "/Thresholds/UpperCaution/Reading"_json_pointer);
712 properties.emplace_back(
713 "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningLow",
714 "/Thresholds/LowerCaution/Reading"_json_pointer);
715 properties.emplace_back(
716 "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalHigh",
717 "/Thresholds/UpperCritical/Reading"_json_pointer);
718 properties.emplace_back(
719 "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalLow",
720 "/Thresholds/LowerCritical/Reading"_json_pointer);
721 properties.emplace_back(
722 "xyz.openbmc_project.Sensor.Threshold.HardShutdown",
723 "HardShutdownHigh",
724 "/Thresholds/UpperFatal/Reading"_json_pointer);
725 properties.emplace_back(
726 "xyz.openbmc_project.Sensor.Threshold.HardShutdown",
727 "HardShutdownLow",
728 "/Thresholds/LowerFatal/Reading"_json_pointer);
729
730 /* Add additional properties specific to sensorType */
731 if (sensorType == "fan_tach")
732 {
733 properties.emplace_back("xyz.openbmc_project.Sensor.Value",
734 "Value", "/SpeedRPM"_json_pointer);
735 }
736 }
737 else if (sensorType != "power")
738 {
739 properties.emplace_back(
740 "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningHigh",
741 "/UpperThresholdNonCritical"_json_pointer);
742 properties.emplace_back(
743 "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningLow",
744 "/LowerThresholdNonCritical"_json_pointer);
745 properties.emplace_back(
746 "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalHigh",
747 "/UpperThresholdCritical"_json_pointer);
748 properties.emplace_back(
749 "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalLow",
750 "/LowerThresholdCritical"_json_pointer);
751 }
752
753 // TODO Need to get UpperThresholdFatal and LowerThresholdFatal
754
755 if (chassisSubNode == ChassisSubNode::sensorsNode)
756 {
757 properties.emplace_back("xyz.openbmc_project.Sensor.Value",
758 "MinValue",
759 "/ReadingRangeMin"_json_pointer);
760 properties.emplace_back("xyz.openbmc_project.Sensor.Value",
761 "MaxValue",
762 "/ReadingRangeMax"_json_pointer);
763 properties.emplace_back("xyz.openbmc_project.Sensor.Accuracy",
764 "Accuracy", "/Accuracy"_json_pointer);
765 }
766 else if (sensorType == "temperature")
767 {
768 properties.emplace_back("xyz.openbmc_project.Sensor.Value",
769 "MinValue",
770 "/MinReadingRangeTemp"_json_pointer);
771 properties.emplace_back("xyz.openbmc_project.Sensor.Value",
772 "MaxValue",
773 "/MaxReadingRangeTemp"_json_pointer);
774 }
775 else if (sensorType != "power")
776 {
777 properties.emplace_back("xyz.openbmc_project.Sensor.Value",
778 "MinValue",
779 "/MinReadingRange"_json_pointer);
780 properties.emplace_back("xyz.openbmc_project.Sensor.Value",
781 "MaxValue",
782 "/MaxReadingRange"_json_pointer);
783 }
784 }
785
786 for (const std::tuple<const char*, const char*,
787 nlohmann::json::json_pointer>& p : properties)
788 {
789 for (const auto& [valueName, valueVariant] : propertiesDict)
790 {
791 if (valueName != std::get<1>(p))
792 {
793 continue;
794 }
795
796 // The property we want to set may be nested json, so use
797 // a json_pointer for easy indexing into the json structure.
798 const nlohmann::json::json_pointer& key = std::get<2>(p);
799
800 const double* doubleValue = std::get_if<double>(&valueVariant);
801 if (doubleValue == nullptr)
802 {
803 BMCWEB_LOG_ERROR("Got value interface that wasn't double");
804 continue;
805 }
806 if (!std::isfinite(*doubleValue))
807 {
808 if (valueName == "Value")
809 {
810 // Readings are allowed to be NAN for unavailable; coerce
811 // them to null in the json response.
812 sensorJson[key] = nullptr;
813 continue;
814 }
815 BMCWEB_LOG_WARNING("Sensor value for {} was unexpectedly {}",
816 valueName, *doubleValue);
817 continue;
818 }
819 if (forceToInt)
820 {
821 sensorJson[key] = static_cast<int64_t>(*doubleValue);
822 }
823 else
824 {
825 sensorJson[key] = *doubleValue;
826 }
827 }
828 }
829 }
830
831 /**
832 * @brief Builds a json sensor excerpt representation of a sensor.
833 *
834 * @details This is a wrapper function to provide consistent setting of
835 * "DataSourceUri" for sensor excerpts and filling of properties. Since sensor
836 * excerpts usually have just the D-Bus path for the sensor that is accepted
837 * and used to build "DataSourceUri".
838
839 * @param path The D-Bus path to the sensor to be built
840 * @param chassisId The Chassis Id for the sensor
841 * @param chassisSubNode The subnode (e.g. ThermalMetrics) of the sensor
842 * @param sensorTypeExpected The expected type of the sensor
843 * @param propertiesDict A dictionary of the properties to build the sensor
844 * from.
845 * @param sensorJson The json object to fill
846 * @returns True if sensorJson object filled. False on any error.
847 * Caller is responsible for handling error.
848 */
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)849 inline bool objectExcerptToJson(
850 const std::string& path, const std::string_view chassisId,
851 ChassisSubNode chassisSubNode,
852 const std::optional<std::string>& sensorTypeExpected,
853 const dbus::utility::DBusPropertiesMap& propertiesDict,
854 nlohmann::json& sensorJson)
855 {
856 if (!isExcerptNode(chassisSubNode))
857 {
858 BMCWEB_LOG_DEBUG("{} is not a sensor excerpt",
859 chassisSubNodeToString(chassisSubNode));
860 return false;
861 }
862
863 sdbusplus::message::object_path sensorPath(path);
864 std::string sensorName = sensorPath.filename();
865 std::string sensorType = sensorPath.parent_path().filename();
866 if (sensorName.empty() || sensorType.empty())
867 {
868 BMCWEB_LOG_DEBUG("Invalid sensor path {}", path);
869 return false;
870 }
871
872 if (sensorTypeExpected && (sensorType != *sensorTypeExpected))
873 {
874 BMCWEB_LOG_DEBUG("{} is not expected type {}", path,
875 *sensorTypeExpected);
876 return false;
877 }
878
879 // Sensor excerpts use DataSourceUri to reference full sensor Redfish path
880 sensorJson["DataSourceUri"] =
881 boost::urls::format("/redfish/v1/Chassis/{}/Sensors/{}", chassisId,
882 getSensorId(sensorName, sensorType));
883
884 // Fill in sensor excerpt properties
885 objectPropertiesToJson(sensorName, sensorType, chassisSubNode,
886 propertiesDict, sensorJson, nullptr);
887
888 return true;
889 }
890
891 // Maps D-Bus: Service, SensorPath
892 using SensorServicePathMap = std::pair<std::string, std::string>;
893 using SensorServicePathList = std::vector<SensorServicePathMap>;
894
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)895 inline void getAllSensorObjects(
896 const std::string& associatedPath, const std::string& path,
897 std::span<const std::string_view> interfaces, const int32_t depth,
898 std::function<void(const boost::system::error_code& ec,
899 SensorServicePathList&)>&& callback)
900 {
901 sdbusplus::message::object_path endpointPath{associatedPath};
902 endpointPath /= "all_sensors";
903
904 dbus::utility::getAssociatedSubTree(
905 endpointPath, sdbusplus::message::object_path(path), depth, interfaces,
906 [callback = std::move(callback)](
907 const boost::system::error_code& ec,
908 const dbus::utility::MapperGetSubTreeResponse& subtree) {
909 SensorServicePathList sensorsServiceAndPath;
910
911 if (ec)
912 {
913 callback(ec, sensorsServiceAndPath);
914 return;
915 }
916
917 for (const auto& [sensorPath, serviceMaps] : subtree)
918 {
919 for (const auto& [service, mapInterfaces] : serviceMaps)
920 {
921 sensorsServiceAndPath.emplace_back(service, sensorPath);
922 }
923 }
924
925 callback(ec, sensorsServiceAndPath);
926 });
927 }
928
929 enum class SensorPurpose
930 {
931 totalPower,
932 unknownPurpose,
933 };
934
sensorPurposeToString(SensorPurpose subNode)935 constexpr std::string_view sensorPurposeToString(SensorPurpose subNode)
936 {
937 if (subNode == SensorPurpose::totalPower)
938 {
939 return "xyz.openbmc_project.Sensor.Purpose.SensorPurpose.TotalPower";
940 }
941
942 return "Unknown Purpose";
943 }
944
sensorPurposeFromString(const std::string & subNodeStr)945 inline SensorPurpose sensorPurposeFromString(const std::string& subNodeStr)
946 {
947 // If none match unknownNode is returned
948 SensorPurpose subNode = SensorPurpose::unknownPurpose;
949
950 if (subNodeStr ==
951 "xyz.openbmc_project.Sensor.Purpose.SensorPurpose.TotalPower")
952 {
953 subNode = SensorPurpose::totalPower;
954 }
955
956 return subNode;
957 }
958
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)959 inline void checkSensorPurpose(
960 const std::string& serviceName, const std::string& sensorPath,
961 const SensorPurpose sensorPurpose,
962 const std::shared_ptr<SensorServicePathList>& sensorMatches,
963 const std::shared_ptr<boost::system::error_code>& asyncErrors,
964 const boost::system::error_code& ec,
965 const std::vector<std::string>& purposeList)
966 {
967 // If not found this sensor will be skipped but allow to
968 // continue processing remaining sensors
969 if (ec && (ec != boost::system::errc::io_error) && (ec.value() != EBADR))
970 {
971 BMCWEB_LOG_DEBUG("D-Bus response error : {}", ec);
972 *asyncErrors = ec;
973 }
974
975 if (!ec)
976 {
977 const std::string_view checkPurposeStr =
978 sensorPurposeToString(sensorPurpose);
979
980 BMCWEB_LOG_DEBUG("checkSensorPurpose: {}", checkPurposeStr);
981
982 for (const std::string& purposeStr : purposeList)
983 {
984 if (purposeStr == checkPurposeStr)
985 {
986 // Add to list
987 BMCWEB_LOG_DEBUG("checkSensorPurpose adding {} {}", serviceName,
988 sensorPath);
989 sensorMatches->emplace_back(serviceName, sensorPath);
990 return;
991 }
992 }
993 }
994 }
995
996 /**
997 * @brief Gets sensors from list with specified purpose
998 *
999 * Checks the <sensorListIn> sensors for any which implement the specified
1000 * <sensorPurpose> in interface xyz.openbmc_project.Sensor.Purpose. Adds any
1001 * matches to the <sensorMatches> list. After checking all sensors in
1002 * <sensorListIn> the <callback> is called with just the list of matching
1003 * sensors, which could be an empty list. Additionally the <callback> is called
1004 * on error and error handling is left to the callback.
1005 *
1006 * @param asyncResp Response data
1007 * @param[in] sensorListIn List of sensors to check
1008 * @param[in] sensorPurpose Looks for sensors matching this purpose
1009 * @param[out] sensorMatches Sensors from sensorListIn with sensorPurpose
1010 * @param[in] callback Callback to handle list of matching sensors
1011 */
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)1012 inline void getSensorsByPurpose(
1013 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1014 const SensorServicePathList& sensorListIn,
1015 const SensorPurpose sensorPurpose,
1016 const std::shared_ptr<SensorServicePathList>& sensorMatches,
1017 const std::function<void(
1018 const boost::system::error_code& ec,
1019 const std::shared_ptr<SensorServicePathList>& sensorMatches)>& callback)
1020 {
1021 /* Keeps track of number of asynchronous calls made. Once all handlers have
1022 * been called the callback function is called with the results.
1023 */
1024 std::shared_ptr<int> remainingSensorsToVist =
1025 std::make_shared<int>(sensorListIn.size());
1026
1027 /* Holds last unrecoverable error returned by any of the asynchronous
1028 * handlers. Once all handlers have been called the callback will be sent
1029 * the error to handle.
1030 */
1031 std::shared_ptr<boost::system::error_code> asyncErrors =
1032 std::make_shared<boost::system::error_code>();
1033
1034 BMCWEB_LOG_DEBUG("getSensorsByPurpose enter {}", *remainingSensorsToVist);
1035
1036 for (const auto& [serviceName, sensorPath] : sensorListIn)
1037 {
1038 dbus::utility::getProperty<std::vector<std::string>>(
1039 serviceName, sensorPath, "xyz.openbmc_project.Sensor.Purpose",
1040 "Purpose",
1041 [asyncResp, serviceName, sensorPath, sensorPurpose, sensorMatches,
1042 callback, remainingSensorsToVist,
1043 asyncErrors](const boost::system::error_code& ec,
1044 const std::vector<std::string>& purposeList) {
1045 // Keep track of sensor visited
1046 (*remainingSensorsToVist)--;
1047 BMCWEB_LOG_DEBUG("Visited {}. Remaining sensors {}", sensorPath,
1048 *remainingSensorsToVist);
1049
1050 checkSensorPurpose(serviceName, sensorPath, sensorPurpose,
1051 sensorMatches, asyncErrors, ec, purposeList);
1052
1053 // All sensors have been visited
1054 if (*remainingSensorsToVist == 0)
1055 {
1056 BMCWEB_LOG_DEBUG(
1057 "getSensorsByPurpose, exit found {} matches",
1058 sensorMatches->size());
1059 callback(*asyncErrors, sensorMatches);
1060 return;
1061 }
1062 });
1063 }
1064 }
1065
1066 } // namespace sensor_utils
1067 } // namespace redfish
1068