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