1 /**
2  * Copyright © 2021 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 "mapped_floor.hpp"
17 
18 #include "../manager.hpp"
19 #include "../zone.hpp"
20 #include "group.hpp"
21 #include "sdeventplus.hpp"
22 
23 #include <nlohmann/json.hpp>
24 
25 #include <algorithm>
26 #include <format>
27 
28 namespace phosphor::fan::control::json
29 {
30 
31 using json = nlohmann::json;
32 
33 template <typename T>
34 uint64_t addFloorOffset(uint64_t floor, T offset, const std::string& actionName)
35 {
36     if constexpr (!std::is_arithmetic_v<T>)
37     {
38         throw std::runtime_error("Invalid variant type in addFloorOffset");
39     }
40 
41     auto newFloor = static_cast<T>(floor) + offset;
42     if (newFloor < 0)
43     {
44         log<level::ERR>(
45             std::format("{}: Floor offset of {} resulted in negative floor",
46                         actionName, offset)
47                 .c_str());
48         return floor;
49     }
50 
51     return static_cast<uint64_t>(newFloor);
52 }
53 
54 MappedFloor::MappedFloor(const json& jsonObj,
55                          const std::vector<Group>& groups) :
56     ActionBase(jsonObj, groups)
57 {
58     setKeyGroup(jsonObj);
59     setFloorTable(jsonObj);
60     setDefaultFloor(jsonObj);
61     setCondition(jsonObj);
62 }
63 
64 const Group* MappedFloor::getGroup(const std::string& name)
65 {
66     auto groupIt =
67         find_if(_groups.begin(), _groups.end(),
68                 [name](const auto& group) { return name == group.getName(); });
69 
70     if (groupIt == _groups.end())
71     {
72         throw ActionParseError{
73             ActionBase::getName(),
74             std::format("Group name {} is not a valid group", name)};
75     }
76 
77     return &(*groupIt);
78 }
79 
80 void MappedFloor::setKeyGroup(const json& jsonObj)
81 {
82     if (!jsonObj.contains("key_group"))
83     {
84         throw ActionParseError{ActionBase::getName(),
85                                "Missing required 'key_group' entry"};
86     }
87     _keyGroup = getGroup(jsonObj["key_group"].get<std::string>());
88 }
89 
90 void MappedFloor::setDefaultFloor(const json& jsonObj)
91 {
92     if (jsonObj.contains("default_floor"))
93     {
94         _defaultFloor = jsonObj["default_floor"].get<uint64_t>();
95     }
96 }
97 
98 void MappedFloor::setFloorTable(const json& jsonObj)
99 {
100     if (!jsonObj.contains("fan_floors"))
101     {
102         throw ActionParseError{ActionBase::getName(),
103                                "Missing fan_floors JSON entry"};
104     }
105 
106     const auto& fanFloors = jsonObj.at("fan_floors");
107 
108     for (const auto& floors : fanFloors)
109     {
110         if (!floors.contains("key") || !floors.contains("floors"))
111         {
112             throw ActionParseError{
113                 ActionBase::getName(),
114                 "Missing key or floors entries in actions/fan_floors JSON"};
115         }
116 
117         FanFloors ff;
118         ff.keyValue = getJsonValue(floors["key"]);
119 
120         if (floors.contains("floor_offset_parameter"))
121         {
122             ff.offsetParameter =
123                 floors["floor_offset_parameter"].get<std::string>();
124         }
125 
126         if (floors.contains("default_floor"))
127         {
128             ff.defaultFloor = floors["default_floor"].get<uint64_t>();
129         }
130 
131         for (const auto& groupEntry : floors["floors"])
132         {
133             if ((!groupEntry.contains("group") &&
134                  !groupEntry.contains("parameter")) ||
135                 !groupEntry.contains("floors"))
136             {
137                 throw ActionParseError{
138                     ActionBase::getName(),
139                     "Missing group, parameter, or floors entries in "
140                     "actions/fan_floors/floors JSON"};
141             }
142 
143             FloorGroup fg;
144             if (groupEntry.contains("group"))
145             {
146                 fg.groupOrParameter =
147                     getGroup(groupEntry["group"].get<std::string>());
148             }
149             else
150             {
151                 fg.groupOrParameter =
152                     groupEntry["parameter"].get<std::string>();
153             }
154 
155             for (const auto& floorEntry : groupEntry["floors"])
156             {
157                 if (!floorEntry.contains("value") ||
158                     !floorEntry.contains("floor"))
159                 {
160                     throw ActionParseError{
161                         ActionBase::getName(),
162                         "Missing value or floor entries in "
163                         "actions/fan_floors/floors/floors JSON"};
164                 }
165 
166                 auto value = getJsonValue(floorEntry["value"]);
167                 auto floor = floorEntry["floor"].get<uint64_t>();
168 
169                 fg.floorEntries.emplace_back(std::move(value),
170                                              std::move(floor));
171             }
172 
173             ff.floorGroups.push_back(std::move(fg));
174         }
175 
176         _fanFloors.push_back(std::move(ff));
177     }
178 }
179 
180 void MappedFloor::setCondition(const json& jsonObj)
181 {
182     // condition_group, condition_value, and condition_op
183     // are optional, though they must show up together.
184     // Assume if condition_group is present then they all
185     // must be.
186     if (!jsonObj.contains("condition_group"))
187     {
188         return;
189     }
190 
191     _conditionGroup = getGroup(jsonObj["condition_group"].get<std::string>());
192 
193     if (_conditionGroup->getMembers().size() != 1)
194     {
195         throw ActionParseError{
196             ActionBase::getName(),
197             std::format("condition_group {} must only have 1 member",
198                         _conditionGroup->getName())};
199     }
200 
201     if (!jsonObj.contains("condition_value"))
202     {
203         throw ActionParseError{ActionBase::getName(),
204                                "Missing required 'condition_value' entry in "
205                                "mapped_floor action"};
206     }
207 
208     _conditionValue = getJsonValue(jsonObj["condition_value"]);
209 
210     if (!jsonObj.contains("condition_op"))
211     {
212         throw ActionParseError{ActionBase::getName(),
213                                "Missing required 'condition_op' entry in "
214                                "mapped_floor action"};
215     }
216 
217     _conditionOp = jsonObj["condition_op"].get<std::string>();
218 
219     if ((_conditionOp != "equal") && (_conditionOp != "not_equal"))
220     {
221         throw ActionParseError{ActionBase::getName(),
222                                "Invalid 'condition_op' value in "
223                                "mapped_floor action"};
224     }
225 }
226 
227 /**
228  * @brief Converts the variant to a double if it's a
229  *        int32_t or int64_t.
230  */
231 void tryConvertToDouble(PropertyVariantType& value)
232 {
233     std::visit(
234         [&value](auto&& val) {
235         using V = std::decay_t<decltype(val)>;
236         if constexpr (std::is_same_v<int32_t, V> || std::is_same_v<int64_t, V>)
237         {
238             value = static_cast<double>(val);
239         }
240     },
241         value);
242 }
243 
244 std::optional<PropertyVariantType>
245     MappedFloor::getMaxGroupValue(const Group& group)
246 {
247     std::optional<PropertyVariantType> max;
248     bool checked = false;
249 
250     for (const auto& member : group.getMembers())
251     {
252         try
253         {
254             auto value = Manager::getObjValueVariant(
255                 member, group.getInterface(), group.getProperty());
256 
257             // Only allow a group to have multiple members if it's numeric.
258             // Unlike std::is_arithmetic, bools are not considered numeric
259             // here.
260             if (!checked && (group.getMembers().size() > 1))
261             {
262                 std::visit(
263                     [&group, this](auto&& val) {
264                     using V = std::decay_t<decltype(val)>;
265                     if constexpr (!std::is_same_v<double, V> &&
266                                   !std::is_same_v<int32_t, V> &&
267                                   !std::is_same_v<int64_t, V>)
268                     {
269                         throw std::runtime_error{std::format(
270                             "{}: Group {} has more than one member but "
271                             "isn't numeric",
272                             ActionBase::getName(), group.getName())};
273                     }
274                 },
275                     value);
276                 checked = true;
277             }
278 
279             if (max && (value > max))
280             {
281                 max = value;
282             }
283             else if (!max)
284             {
285                 max = value;
286             }
287         }
288         catch (const std::out_of_range& e)
289         {
290             // Property not there, continue on
291         }
292     }
293 
294     if (max)
295     {
296         tryConvertToDouble(*max);
297     }
298 
299     return max;
300 }
301 
302 bool MappedFloor::meetsCondition()
303 {
304     if (!_conditionGroup)
305     {
306         return true;
307     }
308 
309     bool meets = false;
310 
311     // setCondition() also checks these
312     assert(_conditionGroup->getMembers().size() == 1);
313     assert((_conditionOp == "equal") || (_conditionOp == "not_equal"));
314 
315     const auto& member = _conditionGroup->getMembers()[0];
316 
317     try
318     {
319         auto value =
320             Manager::getObjValueVariant(member, _conditionGroup->getInterface(),
321                                         _conditionGroup->getProperty());
322 
323         if ((_conditionOp == "equal") && (value == _conditionValue))
324         {
325             meets = true;
326         }
327         else if ((_conditionOp == "not_equal") && (value != _conditionValue))
328         {
329             meets = true;
330         }
331     }
332     catch (const std::out_of_range& e)
333     {
334         // Property not there, so consider it failing the 'equal'
335         // condition and passing the 'not_equal' condition.
336         if (_conditionOp == "equal")
337         {
338             meets = false;
339         }
340         else // not_equal
341         {
342             meets = true;
343         }
344     }
345 
346     return meets;
347 }
348 
349 void MappedFloor::run(Zone& zone)
350 {
351     if (!meetsCondition())
352     {
353         // Make sure this no longer has a floor hold
354         if (zone.hasFloorHold(getUniqueName()))
355         {
356             zone.setFloorHold(getUniqueName(), 0, false);
357         }
358         return;
359     }
360 
361     std::optional<uint64_t> newFloor;
362 
363     auto keyValue = getMaxGroupValue(*_keyGroup);
364     if (!keyValue)
365     {
366         auto floor = _defaultFloor ? *_defaultFloor : zone.getDefaultFloor();
367         zone.setFloorHold(getUniqueName(), floor, true);
368         return;
369     }
370 
371     for (const auto& floorTable : _fanFloors)
372     {
373         // First, find the floorTable entry to use based on the key value.
374         auto tableKeyValue = floorTable.keyValue;
375 
376         // Convert numeric values from the JSON to doubles so they can
377         // be compared to values coming from D-Bus.
378         tryConvertToDouble(tableKeyValue);
379 
380         // The key value from D-Bus must be less than the value
381         // in the table for this entry to be valid.
382         if (*keyValue >= tableKeyValue)
383         {
384             continue;
385         }
386 
387         // Now check each group in the tables
388         for (const auto& [groupOrParameter, floorGroups] :
389              floorTable.floorGroups)
390         {
391             std::optional<PropertyVariantType> propertyValue;
392 
393             if (std::holds_alternative<std::string>(groupOrParameter))
394             {
395                 propertyValue = Manager::getParameter(
396                     std::get<std::string>(groupOrParameter));
397                 if (propertyValue)
398                 {
399                     tryConvertToDouble(*propertyValue);
400                 }
401                 else
402                 {
403                     // If the parameter isn't there, then don't use
404                     // this floor table
405                     log<level::DEBUG>(
406                         std::format("{}: Parameter {} specified in the JSON "
407                                     "could not be found",
408                                     ActionBase::getName(),
409                                     std::get<std::string>(groupOrParameter))
410                             .c_str());
411                     continue;
412                 }
413             }
414             else
415             {
416                 propertyValue =
417                     getMaxGroupValue(*std::get<const Group*>(groupOrParameter));
418             }
419 
420             std::optional<uint64_t> floor;
421             if (propertyValue)
422             {
423                 // Do either a <= or an == check depending on the data type
424                 // to get the floor value based on this group.
425                 for (const auto& [tableValue, tableFloor] : floorGroups)
426                 {
427                     PropertyVariantType value{tableValue};
428                     tryConvertToDouble(value);
429 
430                     if (std::holds_alternative<double>(*propertyValue))
431                     {
432                         if (*propertyValue <= value)
433                         {
434                             floor = tableFloor;
435                             break;
436                         }
437                     }
438                     else if (*propertyValue == value)
439                     {
440                         floor = tableFloor;
441                         break;
442                     }
443                 }
444             }
445 
446             // No floor found in this group, use a default floor for now but
447             // let keep going in case it finds a higher one.
448             if (!floor)
449             {
450                 if (floorTable.defaultFloor)
451                 {
452                     floor = *floorTable.defaultFloor;
453                 }
454                 else if (_defaultFloor)
455                 {
456                     floor = *_defaultFloor;
457                 }
458                 else
459                 {
460                     floor = zone.getDefaultFloor();
461                 }
462             }
463 
464             // Keep track of the highest floor value found across all
465             // entries/groups
466             if ((newFloor && (floor > *newFloor)) || !newFloor)
467             {
468                 newFloor = floor;
469             }
470         }
471 
472         // if still no floor, use the default one from the floor table if
473         // there
474         if (!newFloor && floorTable.defaultFloor)
475         {
476             newFloor = floorTable.defaultFloor.value();
477         }
478 
479         if (newFloor)
480         {
481             *newFloor = applyFloorOffset(*newFloor, floorTable.offsetParameter);
482         }
483 
484         // Valid key value for this entry, so done
485         break;
486     }
487 
488     if (!newFloor)
489     {
490         newFloor = _defaultFloor ? *_defaultFloor : zone.getDefaultFloor();
491     }
492 
493     zone.setFloorHold(getUniqueName(), *newFloor, true);
494 }
495 
496 uint64_t MappedFloor::applyFloorOffset(uint64_t floor,
497                                        const std::string& offsetParameter) const
498 {
499     if (!offsetParameter.empty())
500     {
501         auto offset = Manager::getParameter(offsetParameter);
502         if (offset)
503         {
504             if (std::holds_alternative<int32_t>(*offset))
505             {
506                 return addFloorOffset(floor, std::get<int32_t>(*offset),
507                                       getUniqueName());
508             }
509             else if (std::holds_alternative<int64_t>(*offset))
510             {
511                 return addFloorOffset(floor, std::get<int64_t>(*offset),
512                                       getUniqueName());
513             }
514             else if (std::holds_alternative<double>(*offset))
515             {
516                 return addFloorOffset(floor, std::get<double>(*offset),
517                                       getUniqueName());
518             }
519             else
520             {
521                 throw std::runtime_error(
522                     "Invalid data type in floor offset parameter ");
523             }
524         }
525     }
526 
527     return floor;
528 }
529 
530 } // namespace phosphor::fan::control::json
531