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