1 /** 2 * Copyright © 2022 IBM Corporation 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 #include "zone.hpp" 17 18 #include "../utils/flight_recorder.hpp" 19 #include "dbus_zone.hpp" 20 #include "fan.hpp" 21 #include "sdbusplus.hpp" 22 23 #include <nlohmann/json.hpp> 24 #include <phosphor-logging/log.hpp> 25 #include <sdeventplus/event.hpp> 26 27 #include <algorithm> 28 #include <chrono> 29 #include <iterator> 30 #include <map> 31 #include <memory> 32 #include <numeric> 33 #include <utility> 34 #include <vector> 35 36 namespace phosphor::fan::control::json 37 { 38 39 using json = nlohmann::json; 40 using namespace phosphor::logging; 41 42 const std::map< 43 std::string, 44 std::map<std::string, std::function<std::function<void(DBusZone&, Zone&)>( 45 const json&, bool)>>> 46 Zone::_intfPropHandlers = { 47 {DBusZone::thermalModeIntf, 48 {{DBusZone::supportedProp, zone::property::supported}, 49 {DBusZone::currentProp, zone::property::current}}}}; 50 51 Zone::Zone(const json& jsonObj, const sdeventplus::Event& event, Manager* mgr) : 52 ConfigBase(jsonObj), _dbusZone{}, _manager(mgr), _defaultFloor(0), 53 _incDelay(0), _decInterval(0), _floor(0), _target(0), _incDelta(0), 54 _decDelta(0), _requestTargetBase(0), _isActive(true), 55 _incTimer(event, std::bind(&Zone::incTimerExpired, this)), 56 _decTimer(event, std::bind(&Zone::decTimerExpired, this)) 57 { 58 // Increase delay is optional, defaults to 0 59 if (jsonObj.contains("increase_delay")) 60 { 61 _incDelay = 62 std::chrono::seconds(jsonObj["increase_delay"].get<uint64_t>()); 63 } 64 65 // Poweron target is required 66 setPowerOnTarget(jsonObj); 67 68 // Default ceiling is optional, defaults to poweron target 69 _defaultCeiling = _poweronTarget; 70 if (jsonObj.contains("default_ceiling")) 71 { 72 _defaultCeiling = jsonObj["default_ceiling"].get<uint64_t>(); 73 } 74 // Start with the current ceiling set as the default ceiling 75 _ceiling = _defaultCeiling; 76 77 // Default floor is optional, defaults to 0 78 if (jsonObj.contains("default_floor")) 79 { 80 _defaultFloor = jsonObj["default_floor"].get<uint64_t>(); 81 if (_defaultFloor > _ceiling) 82 { 83 log<level::ERR>( 84 fmt::format("Configured default_floor({}) above ceiling({}), " 85 "setting default floor to ceiling", 86 _defaultFloor, _ceiling) 87 .c_str()); 88 _defaultFloor = _ceiling; 89 } 90 // Start with the current floor set as the default 91 _floor = _defaultFloor; 92 } 93 94 // Decrease interval is optional, defaults to 0 95 // A decrease interval of 0sec disables the decrease timer 96 if (jsonObj.contains("decrease_interval")) 97 { 98 _decInterval = 99 std::chrono::seconds(jsonObj["decrease_interval"].get<uint64_t>()); 100 } 101 102 // Setting properties on interfaces to be served are optional 103 if (jsonObj.contains("interfaces")) 104 { 105 setInterfaces(jsonObj); 106 } 107 } 108 109 void Zone::enable() 110 { 111 // Create thermal control dbus object 112 _dbusZone = std::make_unique<DBusZone>(*this); 113 114 // Init all configured dbus interfaces' property states 115 for (const auto& func : _propInitFunctions) 116 { 117 // Only call non-null init property functions 118 if (func) 119 { 120 func(*_dbusZone, *this); 121 } 122 } 123 124 // TODO - Restore any persisted properties in init function 125 // Restore thermal control current mode state, if exists 126 _dbusZone->restoreCurrentMode(); 127 128 // Emit object added for this zone's associated dbus object 129 _dbusZone->emit_object_added(); 130 131 // A decrease interval of 0sec disables the decrease timer 132 if (_decInterval != std::chrono::seconds::zero()) 133 { 134 // Start timer for fan target decreases 135 _decTimer.restart(_decInterval); 136 } 137 } 138 139 void Zone::addFan(std::unique_ptr<Fan> fan) 140 { 141 _fans.emplace_back(std::move(fan)); 142 } 143 144 void Zone::setTarget(uint64_t target) 145 { 146 if (_isActive) 147 { 148 if (_target != target) 149 { 150 FlightRecorder::instance().log( 151 "zone-set-target" + getName(), 152 fmt::format("Set target {} (from {})", target, _target)); 153 } 154 _target = target; 155 for (auto& fan : _fans) 156 { 157 fan->setTarget(_target); 158 } 159 } 160 } 161 162 void Zone::lockFanTarget(const std::string& fname, uint64_t target) 163 { 164 auto fanItr = std::find_if(_fans.begin(), _fans.end(), 165 [&fname](const auto& fan) { 166 return fan->getName() == fname; 167 }); 168 169 if (_fans.end() != fanItr) 170 { 171 (*fanItr)->lockTarget(target); 172 } 173 else 174 { 175 log<level::DEBUG>( 176 fmt::format("Configured fan {} not found in zone {} to lock target", 177 fname, getName()) 178 .c_str()); 179 } 180 } 181 182 void Zone::unlockFanTarget(const std::string& fname, uint64_t target) 183 { 184 auto fanItr = std::find_if(_fans.begin(), _fans.end(), 185 [&fname](const auto& fan) { 186 return fan->getName() == fname; 187 }); 188 189 if (_fans.end() != fanItr) 190 { 191 (*fanItr)->unlockTarget(target); 192 193 // attempt to resume Zone target on fan 194 (*fanItr)->setTarget(getTarget()); 195 } 196 else 197 { 198 log<level::DEBUG>( 199 fmt::format( 200 "Configured fan {} not found in zone {} to unlock target", 201 fname, getName()) 202 .c_str()); 203 } 204 } 205 206 void Zone::setTargetHold(const std::string& ident, uint64_t target, bool hold) 207 { 208 using namespace std::string_literals; 209 210 if (!hold) 211 { 212 size_t removed = _targetHolds.erase(ident); 213 if (removed) 214 { 215 FlightRecorder::instance().log( 216 "zone-target"s + getName(), 217 fmt::format("{} is removing target hold", ident)); 218 } 219 } 220 else 221 { 222 if (!((_targetHolds.find(ident) != _targetHolds.end()) && 223 (_targetHolds[ident] == target))) 224 { 225 FlightRecorder::instance().log( 226 "zone-target"s + getName(), 227 fmt::format("{} is setting target hold to {}", ident, target)); 228 } 229 _targetHolds[ident] = target; 230 _isActive = false; 231 } 232 233 auto itHoldMax = std::max_element(_targetHolds.begin(), _targetHolds.end(), 234 [](const auto& aHold, const auto& bHold) { 235 return aHold.second < bHold.second; 236 }); 237 if (itHoldMax == _targetHolds.end()) 238 { 239 _isActive = true; 240 } 241 else 242 { 243 if (_target != itHoldMax->second) 244 { 245 FlightRecorder::instance().log( 246 "zone-target"s + getName(), 247 fmt::format("Settings fans to target hold of {}", 248 itHoldMax->second)); 249 } 250 251 _target = itHoldMax->second; 252 for (auto& fan : _fans) 253 { 254 fan->setTarget(_target); 255 } 256 } 257 } 258 259 void Zone::setFloorHold(const std::string& ident, uint64_t target, bool hold) 260 { 261 using namespace std::string_literals; 262 263 if (target > _ceiling) 264 { 265 target = _ceiling; 266 } 267 268 if (!hold) 269 { 270 size_t removed = _floorHolds.erase(ident); 271 if (removed) 272 { 273 FlightRecorder::instance().log( 274 "zone-floor"s + getName(), 275 fmt::format("{} is removing floor hold", ident)); 276 } 277 } 278 else 279 { 280 if (!((_floorHolds.find(ident) != _floorHolds.end()) && 281 (_floorHolds[ident] == target))) 282 { 283 FlightRecorder::instance().log( 284 "zone-floor"s + getName(), 285 fmt::format("{} is setting floor hold to {}", ident, target)); 286 } 287 _floorHolds[ident] = target; 288 } 289 290 if (!std::all_of(_floorChange.begin(), _floorChange.end(), 291 [](const auto& entry) { return entry.second; })) 292 { 293 return; 294 } 295 296 auto itHoldMax = std::max_element(_floorHolds.begin(), _floorHolds.end(), 297 [](const auto& aHold, const auto& bHold) { 298 return aHold.second < bHold.second; 299 }); 300 if (itHoldMax == _floorHolds.end()) 301 { 302 if (_floor != _defaultFloor) 303 { 304 FlightRecorder::instance().log( 305 "zone-floor"s + getName(), 306 fmt::format("No set floor exists, using default floor", 307 _defaultFloor)); 308 } 309 _floor = _defaultFloor; 310 } 311 else 312 { 313 if (_floor != itHoldMax->second) 314 { 315 FlightRecorder::instance().log( 316 "zone-floor"s + getName(), 317 fmt::format("Setting new floor to {}", itHoldMax->second)); 318 } 319 _floor = itHoldMax->second; 320 } 321 322 // Floor above target, update target to floor 323 if (_target < _floor) 324 { 325 requestIncrease(_floor - _target); 326 } 327 } 328 329 void Zone::setFloor(uint64_t target) 330 { 331 // Check all entries are set to allow floor to be set 332 auto pred = [](const auto& entry) { return entry.second; }; 333 if (std::all_of(_floorChange.begin(), _floorChange.end(), pred)) 334 { 335 _floor = (target > _ceiling) ? _ceiling : target; 336 // Floor above target, update target to floor 337 if (_target < _floor) 338 { 339 requestIncrease(_floor - _target); 340 } 341 } 342 } 343 344 void Zone::requestIncrease(uint64_t targetDelta) 345 { 346 // Only increase when delta is higher than the current increase delta for 347 // the zone and currently under ceiling 348 if (targetDelta > _incDelta && _target < _ceiling) 349 { 350 auto requestTarget = getRequestTargetBase(); 351 requestTarget = (targetDelta - _incDelta) + requestTarget; 352 _incDelta = targetDelta; 353 // Target can not go above a current ceiling 354 if (requestTarget > _ceiling) 355 { 356 requestTarget = _ceiling; 357 } 358 setTarget(requestTarget); 359 // Restart timer countdown for fan target increase 360 _incTimer.restartOnce(_incDelay); 361 } 362 } 363 364 void Zone::incTimerExpired() 365 { 366 // Clear increase delta when timer expires allowing additional target 367 // increase requests or target decreases to occur 368 _incDelta = 0; 369 } 370 371 void Zone::requestDecrease(uint64_t targetDelta) 372 { 373 // Only decrease the lowest target delta requested 374 if (_decDelta == 0 || targetDelta < _decDelta) 375 { 376 _decDelta = targetDelta; 377 } 378 } 379 380 void Zone::decTimerExpired() 381 { 382 // Check all entries are set to allow a decrease 383 auto pred = [](const auto& entry) { return entry.second; }; 384 auto decAllowed = std::all_of(_decAllowed.begin(), _decAllowed.end(), pred); 385 386 // Only decrease targets when allowed, a requested decrease target delta 387 // exists, where no requested increases exist and the increase timer is not 388 // running (i.e. not in the middle of increasing) 389 if (decAllowed && _decDelta != 0 && _incDelta == 0 && 390 !_incTimer.isEnabled()) 391 { 392 auto requestTarget = getRequestTargetBase(); 393 // Request target should not start above ceiling 394 if (requestTarget > _ceiling) 395 { 396 requestTarget = _ceiling; 397 } 398 // Target can not go below the defined floor 399 if ((requestTarget < _decDelta) || (requestTarget - _decDelta < _floor)) 400 { 401 requestTarget = _floor; 402 } 403 else 404 { 405 requestTarget = requestTarget - _decDelta; 406 } 407 setTarget(requestTarget); 408 } 409 // Clear decrease delta when timer expires 410 _decDelta = 0; 411 // Decrease timer is restarted since its repeating 412 } 413 414 void Zone::setPersisted(const std::string& intf, const std::string& prop) 415 { 416 if (std::find_if(_propsPersisted[intf].begin(), _propsPersisted[intf].end(), 417 [&prop](const auto& p) { return prop == p; }) == 418 _propsPersisted[intf].end()) 419 { 420 _propsPersisted[intf].emplace_back(prop); 421 } 422 } 423 424 bool Zone::isPersisted(const std::string& intf, const std::string& prop) const 425 { 426 auto it = _propsPersisted.find(intf); 427 if (it == _propsPersisted.end()) 428 { 429 return false; 430 } 431 432 return std::any_of(it->second.begin(), it->second.end(), 433 [&prop](const auto& p) { return prop == p; }); 434 } 435 436 void Zone::setPowerOnTarget(const json& jsonObj) 437 { 438 if (!jsonObj.contains("poweron_target")) 439 { 440 auto msg = "Missing required zone's poweron target"; 441 log<level::ERR>(msg, entry("JSON=%s", jsonObj.dump().c_str())); 442 throw std::runtime_error(msg); 443 } 444 _poweronTarget = jsonObj["poweron_target"].get<uint64_t>(); 445 } 446 447 void Zone::setInterfaces(const json& jsonObj) 448 { 449 for (const auto& interface : jsonObj["interfaces"]) 450 { 451 if (!interface.contains("name") || !interface.contains("properties")) 452 { 453 log<level::ERR>("Missing required zone interface attributes", 454 entry("JSON=%s", interface.dump().c_str())); 455 throw std::runtime_error( 456 "Missing required zone interface attributes"); 457 } 458 auto propFuncs = 459 _intfPropHandlers.find(interface["name"].get<std::string>()); 460 if (propFuncs == _intfPropHandlers.end()) 461 { 462 // Construct list of available configurable interfaces 463 auto intfs = std::accumulate(std::next(_intfPropHandlers.begin()), 464 _intfPropHandlers.end(), 465 _intfPropHandlers.begin()->first, 466 [](auto list, auto intf) { 467 return std::move(list) + ", " + intf.first; 468 }); 469 log<level::ERR>("Configured interface not available", 470 entry("JSON=%s", interface.dump().c_str()), 471 entry("AVAILABLE_INTFS=%s", intfs.c_str())); 472 throw std::runtime_error("Configured interface not available"); 473 } 474 475 for (const auto& property : interface["properties"]) 476 { 477 if (!property.contains("name")) 478 { 479 log<level::ERR>( 480 "Missing required interface property attributes", 481 entry("JSON=%s", property.dump().c_str())); 482 throw std::runtime_error( 483 "Missing required interface property attributes"); 484 } 485 // Attribute "persist" is optional, defaults to `false` 486 auto persist = false; 487 if (property.contains("persist")) 488 { 489 persist = property["persist"].get<bool>(); 490 } 491 // Property name from JSON must exactly match supported 492 // index names to functions in property namespace 493 auto propFunc = 494 propFuncs->second.find(property["name"].get<std::string>()); 495 if (propFunc == propFuncs->second.end()) 496 { 497 // Construct list of available configurable properties 498 auto props = std::accumulate( 499 std::next(propFuncs->second.begin()), 500 propFuncs->second.end(), propFuncs->second.begin()->first, 501 [](auto list, auto prop) { 502 return std::move(list) + ", " + prop.first; 503 }); 504 log<level::ERR>("Configured property not available", 505 entry("JSON=%s", property.dump().c_str()), 506 entry("AVAILABLE_PROPS=%s", props.c_str())); 507 throw std::runtime_error( 508 "Configured property function not available"); 509 } 510 511 _propInitFunctions.emplace_back( 512 propFunc->second(property, persist)); 513 } 514 } 515 } 516 517 json Zone::dump() const 518 { 519 json output; 520 521 output["active"] = _isActive; 522 output["floor"] = _floor; 523 output["ceiling"] = _ceiling; 524 output["target"] = _target; 525 output["increase_delta"] = _incDelta; 526 output["decrease_delta"] = _decDelta; 527 output["power_on_target"] = _poweronTarget; 528 output["default_ceiling"] = _defaultCeiling; 529 output["default_floor"] = _defaultFloor; 530 output["increase_delay"] = _incDelay.count(); 531 output["decrease_interval"] = _decInterval.count(); 532 output["requested_target_base"] = _requestTargetBase; 533 output["floor_change"] = _floorChange; 534 output["decrease_allowed"] = _decAllowed; 535 output["persisted_props"] = _propsPersisted; 536 output["target_holds"] = _targetHolds; 537 output["floor_holds"] = _floorHolds; 538 539 std::map<std::string, std::vector<uint64_t>> lockedTargets; 540 for (const auto& fan : _fans) 541 { 542 const auto& locks = fan->getLockedTargets(); 543 if (!locks.empty()) 544 { 545 lockedTargets[fan->getName()] = locks; 546 } 547 } 548 output["target_locks"] = lockedTargets; 549 550 return output; 551 } 552 553 /** 554 * Properties of interfaces supported by the zone configuration that return 555 * a handler function that sets the zone's property value(s) and persist 556 * state. 557 */ 558 namespace zone::property 559 { 560 // Get a set property handler function for the configured values of the 561 // "Supported" property 562 std::function<void(DBusZone&, Zone&)> supported(const json& jsonObj, 563 bool persist) 564 { 565 std::vector<std::string> values; 566 if (!jsonObj.contains("values")) 567 { 568 log<level::ERR>("No 'values' found for \"Supported\" property, " 569 "using an empty list", 570 entry("JSON=%s", jsonObj.dump().c_str())); 571 } 572 else 573 { 574 for (const auto& value : jsonObj["values"]) 575 { 576 if (!value.contains("value")) 577 { 578 log<level::ERR>("No 'value' found for \"Supported\" property " 579 "entry, skipping", 580 entry("JSON=%s", value.dump().c_str())); 581 } 582 else 583 { 584 values.emplace_back(value["value"].get<std::string>()); 585 } 586 } 587 } 588 589 return Zone::setProperty<std::vector<std::string>>( 590 DBusZone::thermalModeIntf, DBusZone::supportedProp, 591 &DBusZone::supported, std::move(values), persist); 592 } 593 594 // Get a set property handler function for a configured value of the 595 // "Current" property 596 std::function<void(DBusZone&, Zone&)> current(const json& jsonObj, bool persist) 597 { 598 // Use default value for "Current" property if no "value" entry given 599 if (!jsonObj.contains("value")) 600 { 601 log<level::INFO>("No 'value' found for \"Current\" property, " 602 "using default", 603 entry("JSON=%s", jsonObj.dump().c_str())); 604 // Set persist state of property 605 return Zone::setPropertyPersist(DBusZone::thermalModeIntf, 606 DBusZone::currentProp, persist); 607 } 608 609 return Zone::setProperty<std::string>( 610 DBusZone::thermalModeIntf, DBusZone::currentProp, &DBusZone::current, 611 jsonObj["value"].get<std::string>(), persist); 612 } 613 614 } // namespace zone::property 615 616 } // namespace phosphor::fan::control::json 617