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