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