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