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