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