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