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                 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 
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                 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 
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 
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 
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 
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 
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 
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 
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 
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 
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 
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 
422 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 
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 
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 
514 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
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
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