xref: /openbmc/phosphor-fan-presence/control/json/actions/mapped_floor.cpp (revision 1f4098752ec278febc0a0cf661013a573540e05a)
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 }
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             fmt::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 
161                     throw ActionParseError{
162                         ActionBase::getName(),
163                         "Missing value or floor entries in "
164                         "actions/fan_floors/floors/floors JSON"};
165                 }
166 
167                 auto value = getJsonValue(floorEntry["value"]);
168                 auto floor = floorEntry["floor"].get<uint64_t>();
169 
170                 fg.floorEntries.emplace_back(std::move(value),
171                                              std::move(floor));
172             }
173 
174             ff.floorGroups.push_back(std::move(fg));
175         }
176 
177         _fanFloors.push_back(std::move(ff));
178     }
179 }
180 
181 /**
182  * @brief Converts the variant to a double if it's a
183  *        int32_t or int64_t.
184  */
185 void tryConvertToDouble(PropertyVariantType& value)
186 {
187     std::visit(
188         [&value](auto&& val) {
189             using V = std::decay_t<decltype(val)>;
190             if constexpr (std::is_same_v<int32_t, V> ||
191                           std::is_same_v<int64_t, V>)
192             {
193                 value = static_cast<double>(val);
194             }
195         },
196         value);
197 }
198 
199 std::optional<PropertyVariantType>
200     MappedFloor::getMaxGroupValue(const Group& group, const Manager& manager)
201 {
202     std::optional<PropertyVariantType> max;
203     bool checked = false;
204 
205     for (const auto& member : group.getMembers())
206     {
207         try
208         {
209             auto value = Manager::getObjValueVariant(
210                 member, group.getInterface(), group.getProperty());
211 
212             // Only allow a group to have multiple members if it's numeric.
213             // Unlike std::is_arithmetic, bools are not considered numeric
214             // here.
215             if (!checked && (group.getMembers().size() > 1))
216             {
217                 std::visit(
218                     [&group, this](auto&& val) {
219                         using V = std::decay_t<decltype(val)>;
220                         if constexpr (!std::is_same_v<double, V> &&
221                                       !std::is_same_v<int32_t, V> &&
222                                       !std::is_same_v<int64_t, V>)
223                         {
224                             throw std::runtime_error{fmt::format(
225                                 "{}: Group {} has more than one member but "
226                                 "isn't numeric",
227                                 ActionBase::getName(), group.getName())};
228                         }
229                     },
230                     value);
231                 checked = true;
232             }
233 
234             if (max && (value > max))
235             {
236                 max = value;
237             }
238             else if (!max)
239             {
240                 max = value;
241             }
242         }
243         catch (const std::out_of_range& e)
244         {
245             // Property not there, continue on
246         }
247     }
248 
249     if (max)
250     {
251         tryConvertToDouble(*max);
252     }
253 
254     return max;
255 }
256 
257 void MappedFloor::run(Zone& zone)
258 {
259     std::optional<uint64_t> newFloor;
260     auto& manager = *zone.getManager();
261 
262     auto keyValue = getMaxGroupValue(*_keyGroup, manager);
263     if (!keyValue)
264     {
265         auto floor = _defaultFloor ? *_defaultFloor : zone.getDefaultFloor();
266         zone.setFloorHold(getUniqueName(), floor, true);
267         return;
268     }
269 
270     for (const auto& floorTable : _fanFloors)
271     {
272         // First, find the floorTable entry to use based on the key value.
273         auto tableKeyValue = floorTable.keyValue;
274 
275         // Convert numeric values from the JSON to doubles so they can
276         // be compared to values coming from D-Bus.
277         tryConvertToDouble(tableKeyValue);
278 
279         // The key value from D-Bus must be less than the value
280         // in the table for this entry to be valid.
281         if (*keyValue >= tableKeyValue)
282         {
283             continue;
284         }
285 
286         // Now check each group in the tables
287         for (const auto& [groupOrParameter, floorGroups] :
288              floorTable.floorGroups)
289         {
290             std::optional<PropertyVariantType> propertyValue;
291 
292             if (std::holds_alternative<std::string>(groupOrParameter))
293             {
294                 propertyValue = Manager::getParameter(
295                     std::get<std::string>(groupOrParameter));
296                 if (propertyValue)
297                 {
298                     tryConvertToDouble(*propertyValue);
299                 }
300                 else
301                 {
302                     // If the parameter isn't there, then don't use
303                     // this floor table
304                     log<level::DEBUG>(
305                         fmt::format("{}: Parameter {} specified in the JSON "
306                                     "could not be found",
307                                     ActionBase::getName(),
308                                     std::get<std::string>(groupOrParameter))
309                             .c_str());
310                     continue;
311                 }
312             }
313             else
314             {
315                 propertyValue = getMaxGroupValue(
316                     *std::get<const Group*>(groupOrParameter), manager);
317             }
318 
319             std::optional<uint64_t> floor;
320             if (propertyValue)
321             {
322                 // Do either a <= or an == check depending on the data type
323                 // to get the floor value based on this group.
324                 for (const auto& [tableValue, tableFloor] : floorGroups)
325                 {
326                     PropertyVariantType value{tableValue};
327                     tryConvertToDouble(value);
328 
329                     if (std::holds_alternative<double>(*propertyValue))
330                     {
331                         if (*propertyValue <= value)
332                         {
333                             floor = tableFloor;
334                             break;
335                         }
336                     }
337                     else if (*propertyValue == value)
338                     {
339                         floor = tableFloor;
340                         break;
341                     }
342                 }
343             }
344 
345             // No floor found in this group, use a default floor for now but
346             // let keep going in case it finds a higher one.
347             if (!floor)
348             {
349                 if (floorTable.defaultFloor)
350                 {
351                     floor = *floorTable.defaultFloor;
352                 }
353                 else if (_defaultFloor)
354                 {
355                     floor = *_defaultFloor;
356                 }
357                 else
358                 {
359                     floor = zone.getDefaultFloor();
360                 }
361             }
362 
363             // Keep track of the highest floor value found across all
364             // entries/groups
365             if ((newFloor && (floor > *newFloor)) || !newFloor)
366             {
367                 newFloor = floor;
368             }
369         }
370 
371         // if still no floor, use the default one from the floor table if
372         // there
373         if (!newFloor && floorTable.defaultFloor)
374         {
375             newFloor = floorTable.defaultFloor.value();
376         }
377 
378         if (newFloor)
379         {
380             *newFloor = applyFloorOffset(*newFloor, floorTable.offsetParameter);
381         }
382 
383         // Valid key value for this entry, so done
384         break;
385     }
386 
387     if (!newFloor)
388     {
389         newFloor = _defaultFloor ? *_defaultFloor : zone.getDefaultFloor();
390     }
391 
392     zone.setFloorHold(getUniqueName(), *newFloor, true);
393 }
394 
395 uint64_t MappedFloor::applyFloorOffset(uint64_t floor,
396                                        const std::string& offsetParameter) const
397 {
398     if (!offsetParameter.empty())
399     {
400         auto offset = Manager::getParameter(offsetParameter);
401         if (offset)
402         {
403             if (std::holds_alternative<int32_t>(*offset))
404             {
405                 return addFloorOffset(floor, std::get<int32_t>(*offset),
406                                       getUniqueName());
407             }
408             else if (std::holds_alternative<int64_t>(*offset))
409             {
410                 return addFloorOffset(floor, std::get<int64_t>(*offset),
411                                       getUniqueName());
412             }
413             else if (std::holds_alternative<double>(*offset))
414             {
415                 return addFloorOffset(floor, std::get<double>(*offset),
416                                       getUniqueName());
417             }
418             else
419             {
420                 throw std::runtime_error(
421                     "Invalid data type in floor offset parameter ");
422             }
423         }
424     }
425 
426     return floor;
427 }
428 
429 } // namespace phosphor::fan::control::json
430