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
fillSensorStatus(const dbus::utility::DBusPropertiesMap & propertiesDict,nlohmann::json & sensorJson,InventoryItem * inventoryItem)504 inline void fillSensorStatus(
505 const dbus::utility::DBusPropertiesMap& propertiesDict,
506 nlohmann::json& sensorJson, InventoryItem* inventoryItem)
507 {
508 const bool* checkAvailable = nullptr;
509 bool available = true;
510
511 const bool success = sdbusplus::unpackPropertiesNoThrow(
512 dbus_utils::UnpackErrorPrinter(), propertiesDict, "Available",
513 checkAvailable);
514 if (!success)
515 {
516 messages::internalError();
517 }
518 if (checkAvailable != nullptr)
519 {
520 available = *checkAvailable;
521 }
522
523 sensorJson["Status"]["State"] = getState(inventoryItem, available);
524 sensorJson["Status"]["Health"] =
525 getHealth(sensorJson, propertiesDict, inventoryItem);
526 }
527
fillSensorIdentity(std::string_view sensorName,std::string_view sensorType,const dbus::utility::DBusPropertiesMap & propertiesDict,nlohmann::json & sensorJson)528 inline void fillSensorIdentity(
529 std::string_view sensorName, std::string_view sensorType,
530 const dbus::utility::DBusPropertiesMap& propertiesDict,
531 nlohmann::json& sensorJson)
532 {
533 std::string subNodeEscaped = getSensorId(sensorName, sensorType);
534 // For sensors in SensorCollection we set Id instead of MemberId,
535 // including power sensors.
536 sensorJson["Id"] = std::move(subNodeEscaped);
537
538 std::string sensorNameEs(sensorName);
539 std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
540 sensorJson["Name"] = std::move(sensorNameEs);
541 sensorJson["@odata.type"] = "#Sensor.v1_11_0.Sensor";
542
543 sensor::ReadingType readingType = sensors::toReadingType(sensorType);
544 if (readingType == sensor::ReadingType::Invalid)
545 {
546 BMCWEB_LOG_ERROR("Redfish cannot map reading type for {}", sensorType);
547 }
548 else
549 {
550 sensorJson["ReadingType"] = readingType;
551 }
552
553 std::string_view readingUnits = sensors::toReadingUnits(sensorType);
554 if (readingUnits.empty())
555 {
556 BMCWEB_LOG_ERROR("Redfish cannot map reading unit for {}", sensorType);
557 }
558 else
559 {
560 sensorJson["ReadingUnits"] = readingUnits;
561 }
562
563 std::optional<std::string> readingBasis;
564 std::optional<std::string> implementation;
565 std::optional<Statistics> statistics;
566 std::optional<ReadingParameters> readingParameters;
567
568 const bool success = sdbusplus::unpackPropertiesNoThrow(
569 dbus_utils::UnpackErrorPrinter(), propertiesDict, "ReadingBasis",
570 readingBasis, "Implementation", implementation, "Readings", statistics,
571 "ReadingParameters", readingParameters);
572 if (!success)
573 {
574 messages::internalError();
575 }
576
577 if (readingBasis.has_value())
578 {
579 sensor::ReadingBasisType readingBasisOpt =
580 dBusSensorReadingBasisToRedfish(*readingBasis);
581 if (readingBasisOpt != sensor::ReadingBasisType::Invalid)
582 {
583 sensorJson["ReadingBasis"] = readingBasisOpt;
584 }
585 }
586
587 if (implementation.has_value())
588 {
589 sensor::ImplementationType implementationOpt =
590 dBusSensorImplementationToRedfish(*implementation);
591 if (implementationOpt != sensor::ImplementationType::Invalid)
592 {
593 sensorJson["Implementation"] = implementationOpt;
594 }
595 }
596
597 updateSensorStatistics(sensorJson, statistics, readingParameters);
598 }
599
fillPowerThermalIdentity(std::string_view sensorName,std::string_view sensorType,nlohmann::json & sensorJson,InventoryItem * inventoryItem,nlohmann::json::json_pointer & unit,bool & forceToInt)600 inline bool fillPowerThermalIdentity(
601 std::string_view sensorName, std::string_view sensorType,
602 nlohmann::json& sensorJson, InventoryItem* inventoryItem,
603 nlohmann::json::json_pointer& unit, bool& forceToInt)
604 {
605 if (sensorType != "power")
606 {
607 // Set MemberId and Name for non-power sensors. For PowerSupplies
608 // and PowerControl, those properties have more general values
609 // because multiple sensors can be stored in the same JSON object.
610 std::string sensorNameEs(sensorName);
611 std::replace(sensorNameEs.begin(), sensorNameEs.end(), '_', ' ');
612 sensorJson["Name"] = std::move(sensorNameEs);
613 }
614
615 if (sensorType == "temperature")
616 {
617 unit = "/ReadingCelsius"_json_pointer;
618 sensorJson["@odata.type"] = "#Thermal.v1_3_0.Temperature";
619 // TODO(ed) Documentation says that path should be type fan_tach,
620 // implementation seems to implement fan
621 return true;
622 }
623
624 if (sensorType == "fan" || sensorType == "fan_tach")
625 {
626 unit = "/Reading"_json_pointer;
627 sensorJson["ReadingUnits"] = thermal::ReadingUnits::RPM;
628 sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
629 if constexpr (BMCWEB_REDFISH_ALLOW_DEPRECATED_INDICATORLED)
630 {
631 setLedState(sensorJson, inventoryItem);
632 }
633 forceToInt = true;
634 return true;
635 }
636
637 if (sensorType == "fan_pwm")
638 {
639 unit = "/Reading"_json_pointer;
640 sensorJson["ReadingUnits"] = thermal::ReadingUnits::Percent;
641 sensorJson["@odata.type"] = "#Thermal.v1_3_0.Fan";
642 if constexpr (BMCWEB_REDFISH_ALLOW_DEPRECATED_INDICATORLED)
643 {
644 setLedState(sensorJson, inventoryItem);
645 }
646 forceToInt = true;
647 return true;
648 }
649
650 if (sensorType == "voltage")
651 {
652 unit = "/ReadingVolts"_json_pointer;
653 sensorJson["@odata.type"] = "#Power.v1_0_0.Voltage";
654 return true;
655 }
656
657 if (sensorType == "power")
658 {
659 std::string lower;
660 std::ranges::transform(sensorName, std::back_inserter(lower),
661 bmcweb::asciiToLower);
662 if (lower == "total_power")
663 {
664 sensorJson["@odata.type"] = "#Power.v1_0_0.PowerControl";
665 // Put multiple "sensors" into a single PowerControl, so have
666 // generic names for MemberId and Name. Follows Redfish mockup.
667 sensorJson["MemberId"] = "0";
668 sensorJson["Name"] = "Chassis Power Control";
669 unit = "/PowerConsumedWatts"_json_pointer;
670 }
671 else if (lower.find("input") != std::string::npos)
672 {
673 unit = "/PowerInputWatts"_json_pointer;
674 }
675 else
676 {
677 unit = "/PowerOutputWatts"_json_pointer;
678 }
679 return true;
680 }
681
682 BMCWEB_LOG_ERROR("Redfish cannot map object type for {}", sensorName);
683 return false;
684 }
685
686 // Map of dbus interface name, dbus property name and redfish property_name
687 using SensorPropertyMap = std::tuple<std::string_view, std::string_view,
688 nlohmann::json::json_pointer>;
689 using SensorPropertyList = std::vector<SensorPropertyMap>;
690
mapPropertiesBySubnode(std::string_view sensorType,ChassisSubNode chassisSubNode,SensorPropertyList & properties,nlohmann::json::json_pointer & unit,bool isExcerpt)691 inline void mapPropertiesBySubnode(
692 std::string_view sensorType, ChassisSubNode chassisSubNode,
693 SensorPropertyList& properties, nlohmann::json::json_pointer& unit,
694 bool isExcerpt)
695 {
696 // unit contains the redfish property_name based on the sensor type/node
697 properties.emplace_back("xyz.openbmc_project.Sensor.Value", "Value", unit);
698
699 if (isExcerpt)
700 {
701 // Excerpts don't have any of these extended properties
702 return;
703 }
704
705 if (chassisSubNode == ChassisSubNode::sensorsNode)
706 {
707 properties.emplace_back(
708 "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningHigh",
709 "/Thresholds/UpperCaution/Reading"_json_pointer);
710 properties.emplace_back(
711 "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningLow",
712 "/Thresholds/LowerCaution/Reading"_json_pointer);
713 properties.emplace_back(
714 "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalHigh",
715 "/Thresholds/UpperCritical/Reading"_json_pointer);
716 properties.emplace_back(
717 "xyz.openbmc_project.Sensor.Threshold.Critical", "CriticalLow",
718 "/Thresholds/LowerCritical/Reading"_json_pointer);
719 properties.emplace_back(
720 "xyz.openbmc_project.Sensor.Threshold.HardShutdown",
721 "HardShutdownHigh", "/Thresholds/UpperFatal/Reading"_json_pointer);
722 properties.emplace_back(
723 "xyz.openbmc_project.Sensor.Threshold.HardShutdown",
724 "HardShutdownLow", "/Thresholds/LowerFatal/Reading"_json_pointer);
725
726 /* Add additional properties specific to sensorType */
727 if (sensorType == "fan_tach")
728 {
729 properties.emplace_back("xyz.openbmc_project.Sensor.Value", "Value",
730 "/SpeedRPM"_json_pointer);
731 }
732 }
733 else if (sensorType != "power")
734 {
735 properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Warning",
736 "WarningHigh",
737 "/UpperThresholdNonCritical"_json_pointer);
738 properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Warning",
739 "WarningLow",
740 "/LowerThresholdNonCritical"_json_pointer);
741 properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Critical",
742 "CriticalHigh",
743 "/UpperThresholdCritical"_json_pointer);
744 properties.emplace_back("xyz.openbmc_project.Sensor.Threshold.Critical",
745 "CriticalLow",
746 "/LowerThresholdCritical"_json_pointer);
747 }
748
749 // TODO Need to get UpperThresholdFatal and LowerThresholdFatal
750
751 if (chassisSubNode == ChassisSubNode::sensorsNode)
752 {
753 properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
754 "/ReadingRangeMin"_json_pointer);
755 properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
756 "/ReadingRangeMax"_json_pointer);
757 properties.emplace_back("xyz.openbmc_project.Sensor.Accuracy",
758 "Accuracy", "/Accuracy"_json_pointer);
759 }
760 else if (sensorType == "temperature")
761 {
762 properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
763 "/MinReadingRangeTemp"_json_pointer);
764 properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
765 "/MaxReadingRangeTemp"_json_pointer);
766 }
767 else if (sensorType != "power")
768 {
769 properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MinValue",
770 "/MinReadingRange"_json_pointer);
771 properties.emplace_back("xyz.openbmc_project.Sensor.Value", "MaxValue",
772 "/MaxReadingRange"_json_pointer);
773 }
774 }
775
776 /**
777 * @brief Builds a json sensor representation of a sensor.
778 * @param sensorName The name of the sensor to be built
779 * @param sensorType The type (temperature, fan_tach, etc) of the sensor to
780 * build
781 * @param chassisSubNode The subnode (thermal, sensor, etc) of the sensor
782 * @param propertiesDict A dictionary of the properties to build the sensor
783 * from.
784 * @param sensorJson The json object to fill
785 * @param inventoryItem D-Bus inventory item associated with the sensor. Will
786 * be nullptr if no associated inventory item was found.
787 */
objectPropertiesToJson(std::string_view sensorName,std::string_view sensorType,ChassisSubNode chassisSubNode,const dbus::utility::DBusPropertiesMap & propertiesDict,nlohmann::json & sensorJson,InventoryItem * inventoryItem)788 inline void objectPropertiesToJson(
789 std::string_view sensorName, std::string_view sensorType,
790 ChassisSubNode chassisSubNode,
791 const dbus::utility::DBusPropertiesMap& propertiesDict,
792 nlohmann::json& sensorJson, InventoryItem* inventoryItem)
793 {
794 // Parameter to set to override the type we get from dbus, and force it to
795 // int, regardless of what is available. This is used for schemas like fan,
796 // that require integers, not floats.
797 bool forceToInt = false;
798
799 nlohmann::json::json_pointer unit("/Reading");
800
801 // This ChassisSubNode builds sensor excerpts
802 bool isExcerpt = isExcerptNode(chassisSubNode);
803
804 /* Sensor excerpts use different keys to reference the sensor. These are
805 * built by the caller.
806 * Additionally they don't include these additional properties.
807 */
808 if (!isExcerpt)
809 {
810 if (chassisSubNode == ChassisSubNode::sensorsNode)
811 {
812 fillSensorIdentity(sensorName, sensorType, propertiesDict,
813 sensorJson);
814 }
815 else
816 {
817 bool filledOk = fillPowerThermalIdentity(
818 sensorName, sensorType, sensorJson, inventoryItem, unit,
819 std::ref(forceToInt));
820 if (!filledOk)
821 {
822 return;
823 }
824 }
825
826 fillSensorStatus(propertiesDict, sensorJson, inventoryItem);
827 }
828
829 // Map of dbus interface name, dbus property name and redfish property_name
830 SensorPropertyList properties;
831
832 // Add additional property mappings based on the sensor type/node
833 mapPropertiesBySubnode(sensorType, chassisSubNode, properties, unit,
834 isExcerpt);
835
836 for (const SensorPropertyMap& p : properties)
837 {
838 for (const auto& [valueName, valueVariant] : propertiesDict)
839 {
840 if (valueName != std::get<1>(p))
841 {
842 continue;
843 }
844
845 // The property we want to set may be nested json, so use
846 // a json_pointer for easy indexing into the json structure.
847 const nlohmann::json::json_pointer& key = std::get<2>(p);
848
849 const double* doubleValue = std::get_if<double>(&valueVariant);
850 if (doubleValue == nullptr)
851 {
852 BMCWEB_LOG_ERROR("Got value interface that wasn't double");
853 continue;
854 }
855 if (!std::isfinite(*doubleValue))
856 {
857 if (valueName == "Value")
858 {
859 // Readings are allowed to be NAN for unavailable; coerce
860 // them to null in the json response.
861 sensorJson[key] = nullptr;
862 continue;
863 }
864 BMCWEB_LOG_WARNING("Sensor value for {} was unexpectedly {}",
865 valueName, *doubleValue);
866 continue;
867 }
868 if (forceToInt)
869 {
870 sensorJson[key] = static_cast<int64_t>(*doubleValue);
871 }
872 else
873 {
874 sensorJson[key] = *doubleValue;
875 }
876 }
877 }
878 }
879
880 /**
881 * @brief Builds a json sensor excerpt representation of a sensor.
882 *
883 * @details This is a wrapper function to provide consistent setting of
884 * "DataSourceUri" for sensor excerpts and filling of properties. Since sensor
885 * excerpts usually have just the D-Bus path for the sensor that is accepted
886 * and used to build "DataSourceUri".
887
888 * @param path The D-Bus path to the sensor to be built
889 * @param chassisId The Chassis Id for the sensor
890 * @param chassisSubNode The subnode (e.g. ThermalMetrics) of the sensor
891 * @param sensorTypeExpected The expected type of the sensor
892 * @param propertiesDict A dictionary of the properties to build the sensor
893 * from.
894 * @param sensorJson The json object to fill
895 * @returns True if sensorJson object filled. False on any error.
896 * Caller is responsible for handling error.
897 */
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)898 inline bool objectExcerptToJson(
899 const std::string& path, const std::string_view chassisId,
900 ChassisSubNode chassisSubNode,
901 const std::optional<std::string>& sensorTypeExpected,
902 const dbus::utility::DBusPropertiesMap& propertiesDict,
903 nlohmann::json& sensorJson)
904 {
905 if (!isExcerptNode(chassisSubNode))
906 {
907 BMCWEB_LOG_DEBUG("{} is not a sensor excerpt",
908 chassisSubNodeToString(chassisSubNode));
909 return false;
910 }
911
912 sdbusplus::message::object_path sensorPath(path);
913 std::string sensorName = sensorPath.filename();
914 std::string sensorType = sensorPath.parent_path().filename();
915 if (sensorName.empty() || sensorType.empty())
916 {
917 BMCWEB_LOG_DEBUG("Invalid sensor path {}", path);
918 return false;
919 }
920
921 if (sensorTypeExpected && (sensorType != *sensorTypeExpected))
922 {
923 BMCWEB_LOG_DEBUG("{} is not expected type {}", path,
924 *sensorTypeExpected);
925 return false;
926 }
927
928 // Sensor excerpts use DataSourceUri to reference full sensor Redfish path
929 sensorJson["DataSourceUri"] =
930 boost::urls::format("/redfish/v1/Chassis/{}/Sensors/{}", chassisId,
931 getSensorId(sensorName, sensorType));
932
933 // Fill in sensor excerpt properties
934 objectPropertiesToJson(sensorName, sensorType, chassisSubNode,
935 propertiesDict, sensorJson, nullptr);
936
937 return true;
938 }
939
940 // Maps D-Bus: Service, SensorPath
941 using SensorServicePathMap = std::pair<std::string, std::string>;
942 using SensorServicePathList = std::vector<SensorServicePathMap>;
943
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)944 inline void getAllSensorObjects(
945 const std::string& associatedPath, const std::string& path,
946 std::span<const std::string_view> interfaces, const int32_t depth,
947 std::function<void(const boost::system::error_code& ec,
948 SensorServicePathList&)>&& callback)
949 {
950 sdbusplus::message::object_path endpointPath{associatedPath};
951 endpointPath /= "all_sensors";
952
953 dbus::utility::getAssociatedSubTree(
954 endpointPath, sdbusplus::message::object_path(path), depth, interfaces,
955 [callback = std::move(callback)](
956 const boost::system::error_code& ec,
957 const dbus::utility::MapperGetSubTreeResponse& subtree) {
958 SensorServicePathList sensorsServiceAndPath;
959
960 if (ec)
961 {
962 callback(ec, sensorsServiceAndPath);
963 return;
964 }
965
966 for (const auto& [sensorPath, serviceMaps] : subtree)
967 {
968 for (const auto& [service, mapInterfaces] : serviceMaps)
969 {
970 sensorsServiceAndPath.emplace_back(service, sensorPath);
971 }
972 }
973
974 callback(ec, sensorsServiceAndPath);
975 });
976 }
977
978 enum class SensorPurpose
979 {
980 totalPower,
981 unknownPurpose,
982 };
983
sensorPurposeToString(SensorPurpose subNode)984 constexpr std::string_view sensorPurposeToString(SensorPurpose subNode)
985 {
986 if (subNode == SensorPurpose::totalPower)
987 {
988 return "xyz.openbmc_project.Sensor.Purpose.SensorPurpose.TotalPower";
989 }
990
991 return "Unknown Purpose";
992 }
993
sensorPurposeFromString(const std::string & subNodeStr)994 inline SensorPurpose sensorPurposeFromString(const std::string& subNodeStr)
995 {
996 // If none match unknownNode is returned
997 SensorPurpose subNode = SensorPurpose::unknownPurpose;
998
999 if (subNodeStr ==
1000 "xyz.openbmc_project.Sensor.Purpose.SensorPurpose.TotalPower")
1001 {
1002 subNode = SensorPurpose::totalPower;
1003 }
1004
1005 return subNode;
1006 }
1007
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)1008 inline void checkSensorPurpose(
1009 const std::string& serviceName, const std::string& sensorPath,
1010 const SensorPurpose sensorPurpose,
1011 const std::shared_ptr<SensorServicePathList>& sensorMatches,
1012 const std::shared_ptr<boost::system::error_code>& asyncErrors,
1013 const boost::system::error_code& ec,
1014 const std::vector<std::string>& purposeList)
1015 {
1016 // If not found this sensor will be skipped but allow to
1017 // continue processing remaining sensors
1018 if (ec && (ec != boost::system::errc::io_error) && (ec.value() != EBADR))
1019 {
1020 BMCWEB_LOG_DEBUG("D-Bus response error : {}", ec);
1021 *asyncErrors = ec;
1022 }
1023
1024 if (!ec)
1025 {
1026 const std::string_view checkPurposeStr =
1027 sensorPurposeToString(sensorPurpose);
1028
1029 BMCWEB_LOG_DEBUG("checkSensorPurpose: {}", checkPurposeStr);
1030
1031 for (const std::string& purposeStr : purposeList)
1032 {
1033 if (purposeStr == checkPurposeStr)
1034 {
1035 // Add to list
1036 BMCWEB_LOG_DEBUG("checkSensorPurpose adding {} {}", serviceName,
1037 sensorPath);
1038 sensorMatches->emplace_back(serviceName, sensorPath);
1039 return;
1040 }
1041 }
1042 }
1043 }
1044
1045 /**
1046 * @brief Gets sensors from list with specified purpose
1047 *
1048 * Checks the <sensorListIn> sensors for any which implement the specified
1049 * <sensorPurpose> in interface xyz.openbmc_project.Sensor.Purpose. Adds any
1050 * matches to the <sensorMatches> list. After checking all sensors in
1051 * <sensorListIn> the <callback> is called with just the list of matching
1052 * sensors, which could be an empty list. Additionally the <callback> is called
1053 * on error and error handling is left to the callback.
1054 *
1055 * @param asyncResp Response data
1056 * @param[in] sensorListIn List of sensors to check
1057 * @param[in] sensorPurpose Looks for sensors matching this purpose
1058 * @param[out] sensorMatches Sensors from sensorListIn with sensorPurpose
1059 * @param[in] callback Callback to handle list of matching sensors
1060 */
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)1061 inline void getSensorsByPurpose(
1062 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1063 const SensorServicePathList& sensorListIn,
1064 const SensorPurpose sensorPurpose,
1065 const std::shared_ptr<SensorServicePathList>& sensorMatches,
1066 const std::function<void(
1067 const boost::system::error_code& ec,
1068 const std::shared_ptr<SensorServicePathList>& sensorMatches)>& callback)
1069 {
1070 /* Keeps track of number of asynchronous calls made. Once all handlers have
1071 * been called the callback function is called with the results.
1072 */
1073 std::shared_ptr<int> remainingSensorsToVist =
1074 std::make_shared<int>(sensorListIn.size());
1075
1076 /* Holds last unrecoverable error returned by any of the asynchronous
1077 * handlers. Once all handlers have been called the callback will be sent
1078 * the error to handle.
1079 */
1080 std::shared_ptr<boost::system::error_code> asyncErrors =
1081 std::make_shared<boost::system::error_code>();
1082
1083 BMCWEB_LOG_DEBUG("getSensorsByPurpose enter {}", *remainingSensorsToVist);
1084
1085 for (const auto& [serviceName, sensorPath] : sensorListIn)
1086 {
1087 dbus::utility::getProperty<std::vector<std::string>>(
1088 serviceName, sensorPath, "xyz.openbmc_project.Sensor.Purpose",
1089 "Purpose",
1090 [asyncResp, serviceName, sensorPath, sensorPurpose, sensorMatches,
1091 callback, remainingSensorsToVist,
1092 asyncErrors](const boost::system::error_code& ec,
1093 const std::vector<std::string>& purposeList) {
1094 // Keep track of sensor visited
1095 (*remainingSensorsToVist)--;
1096 BMCWEB_LOG_DEBUG("Visited {}. Remaining sensors {}", sensorPath,
1097 *remainingSensorsToVist);
1098
1099 checkSensorPurpose(serviceName, sensorPath, sensorPurpose,
1100 sensorMatches, asyncErrors, ec, purposeList);
1101
1102 // All sensors have been visited
1103 if (*remainingSensorsToVist == 0)
1104 {
1105 BMCWEB_LOG_DEBUG(
1106 "getSensorsByPurpose, exit found {} matches",
1107 sensorMatches->size());
1108 callback(*asyncErrors, sensorMatches);
1109 return;
1110 }
1111 });
1112 }
1113 }
1114
1115 } // namespace sensor_utils
1116 } // namespace redfish
1117