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