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 MappedFloor::MappedFloor(const json& jsonObj,
35                          const std::vector<Group>& groups) :
36     ActionBase(jsonObj, groups)
37 {
38     setKeyGroup(jsonObj);
39     setFloorTable(jsonObj);
40 }
41 
42 const Group* MappedFloor::getGroup(const std::string& name)
43 {
44     auto groupIt =
45         find_if(_groups.begin(), _groups.end(),
46                 [name](const auto& group) { return name == group.getName(); });
47 
48     if (groupIt == _groups.end())
49     {
50         throw ActionParseError{
51             ActionBase::getName(),
52             fmt::format("Group name {} is not a valid group", name)};
53     }
54 
55     return &(*groupIt);
56 }
57 
58 void MappedFloor::setKeyGroup(const json& jsonObj)
59 {
60     if (!jsonObj.contains("key_group"))
61     {
62         throw ActionParseError{ActionBase::getName(),
63                                "Missing required 'key_group' entry"};
64     }
65     _keyGroup = getGroup(jsonObj["key_group"].get<std::string>());
66 }
67 
68 void MappedFloor::setFloorTable(const json& jsonObj)
69 {
70     if (!jsonObj.contains("fan_floors"))
71     {
72         throw ActionParseError{ActionBase::getName(),
73                                "Missing fan_floors JSON entry"};
74     }
75 
76     const auto& fanFloors = jsonObj.at("fan_floors");
77 
78     for (const auto& floors : fanFloors)
79     {
80         if (!floors.contains("key") || !floors.contains("floors"))
81         {
82             throw ActionParseError{
83                 ActionBase::getName(),
84                 "Missing key or floors entries in actions/fan_floors JSON"};
85         }
86 
87         FanFloors ff;
88         ff.keyValue = getJsonValue(floors["key"]);
89 
90         for (const auto& groupEntry : floors["floors"])
91         {
92             if ((!groupEntry.contains("group") &&
93                  !groupEntry.contains("parameter")) ||
94                 !groupEntry.contains("floors"))
95             {
96                 throw ActionParseError{
97                     ActionBase::getName(),
98                     "Missing group, parameter, or floors entries in "
99                     "actions/fan_floors/floors JSON"};
100             }
101 
102             FloorGroup fg;
103             if (groupEntry.contains("group"))
104             {
105                 fg.groupOrParameter =
106                     getGroup(groupEntry["group"].get<std::string>());
107             }
108             else
109             {
110                 fg.groupOrParameter =
111                     groupEntry["parameter"].get<std::string>();
112             }
113 
114             for (const auto& floorEntry : groupEntry["floors"])
115             {
116                 if (!floorEntry.contains("value") ||
117                     !floorEntry.contains("floor"))
118                 {
119 
120                     throw ActionParseError{
121                         ActionBase::getName(),
122                         "Missing value or floor entries in "
123                         "actions/fan_floors/floors/floors JSON"};
124                 }
125 
126                 auto value = getJsonValue(floorEntry["value"]);
127                 auto floor = floorEntry["floor"].get<uint64_t>();
128 
129                 fg.floorEntries.emplace_back(std::move(value),
130                                              std::move(floor));
131             }
132 
133             ff.floorGroups.push_back(std::move(fg));
134         }
135 
136         _fanFloors.push_back(std::move(ff));
137     }
138 }
139 
140 /**
141  * @brief Converts the variant to a double if it's a
142  *        int32_t or int64_t.
143  */
144 void tryConvertToDouble(PropertyVariantType& value)
145 {
146     std::visit(
147         [&value](auto&& val) {
148             using V = std::decay_t<decltype(val)>;
149             if constexpr (std::is_same_v<int32_t, V> ||
150                           std::is_same_v<int64_t, V>)
151             {
152                 value = static_cast<double>(val);
153             }
154         },
155         value);
156 }
157 
158 std::optional<PropertyVariantType>
159     MappedFloor::getMaxGroupValue(const Group& group, const Manager& manager)
160 {
161     std::optional<PropertyVariantType> max;
162     bool checked = false;
163 
164     for (const auto& member : group.getMembers())
165     {
166         try
167         {
168             auto value = Manager::getObjValueVariant(
169                 member, group.getInterface(), group.getProperty());
170 
171             // Only allow a group to have multiple members if it's numeric.
172             // Unlike std::is_arithmetic, bools are not considered numeric here.
173             if (!checked && (group.getMembers().size() > 1))
174             {
175                 std::visit(
176                     [&group, this](auto&& val) {
177                         using V = std::decay_t<decltype(val)>;
178                         if constexpr (!std::is_same_v<double, V> &&
179                                       !std::is_same_v<int32_t, V> &&
180                                       !std::is_same_v<int64_t, V>)
181                         {
182                             throw std::runtime_error{fmt::format(
183                                 "{}: Group {} has more than one member but "
184                                 "isn't numeric",
185                                 ActionBase::getName(), group.getName())};
186                         }
187                     },
188                     value);
189                 checked = true;
190             }
191 
192             if (max && (value > max))
193             {
194                 max = value;
195             }
196             else if (!max)
197             {
198                 max = value;
199             }
200         }
201         catch (const std::out_of_range& e)
202         {
203             // Property not there, continue on
204         }
205     }
206 
207     if (max)
208     {
209         tryConvertToDouble(*max);
210     }
211 
212     return max;
213 }
214 
215 void MappedFloor::run(Zone& zone)
216 {
217     std::optional<uint64_t> newFloor;
218     bool missingGroupProperty = false;
219     auto& manager = *zone.getManager();
220 
221     auto keyValue = getMaxGroupValue(*_keyGroup, manager);
222     if (!keyValue)
223     {
224         zone.setFloor(zone.getDefaultFloor());
225         return;
226     }
227 
228     for (const auto& floorTable : _fanFloors)
229     {
230         // First, find the floorTable entry to use based on the key value.
231         auto tableKeyValue = floorTable.keyValue;
232 
233         // Convert numeric values from the JSON to doubles so they can
234         // be compared to values coming from D-Bus.
235         tryConvertToDouble(tableKeyValue);
236 
237         // The key value from D-Bus must be less than the value
238         // in the table for this entry to be valid.
239         if (*keyValue >= tableKeyValue)
240         {
241             continue;
242         }
243 
244         // Now check each group in the tables
245         for (const auto& [groupOrParameter, floorGroups] :
246              floorTable.floorGroups)
247         {
248             std::optional<PropertyVariantType> propertyValue;
249 
250             if (std::holds_alternative<std::string>(groupOrParameter))
251             {
252                 propertyValue = Manager::getParameter(
253                     std::get<std::string>(groupOrParameter));
254                 if (propertyValue)
255                 {
256                     tryConvertToDouble(*propertyValue);
257                 }
258                 else
259                 {
260                     // If the parameter isn't there, then don't use
261                     // this floor table
262                     log<level::DEBUG>(
263                         fmt::format("{}: Parameter {} specified in the JSON "
264                                     "could not be found",
265                                     ActionBase::getName(),
266                                     std::get<std::string>(groupOrParameter))
267                             .c_str());
268                     continue;
269                 }
270             }
271             else
272             {
273                 propertyValue = getMaxGroupValue(
274                     *std::get<const Group*>(groupOrParameter), manager);
275             }
276 
277             if (!propertyValue)
278             {
279                 // Couldn't successfully get a value.  Results in default floor.
280                 missingGroupProperty = true;
281                 break;
282             }
283 
284             // Do either a <= or an == check depending on the data type to get
285             // the floor value based on this group.
286             std::optional<uint64_t> floor;
287             for (const auto& [tableValue, tableFloor] : floorGroups)
288             {
289                 PropertyVariantType value{tableValue};
290                 tryConvertToDouble(value);
291 
292                 if (std::holds_alternative<double>(*propertyValue))
293                 {
294                     if (*propertyValue <= value)
295                     {
296                         floor = tableFloor;
297                         break;
298                     }
299                 }
300                 else if (*propertyValue == value)
301                 {
302                     floor = tableFloor;
303                     break;
304                 }
305             }
306 
307             // Keep track of the highest floor value found across all
308             // entries/groups
309             if (floor)
310             {
311                 if ((newFloor && (floor > *newFloor)) || !newFloor)
312                 {
313                     newFloor = floor;
314                 }
315             }
316             else
317             {
318                 // No match found in this group's table.
319                 // Results in default floor.
320                 missingGroupProperty = true;
321             }
322         }
323 
324         // Valid key value for this entry, so done
325         break;
326     }
327 
328     if (newFloor && !missingGroupProperty)
329     {
330         zone.setFloorHold(getUniqueName(), *newFloor, true);
331     }
332     else
333     {
334         zone.setFloorHold(getUniqueName(), zone.getDefaultFloor(), true);
335     }
336 }
337 
338 } // namespace phosphor::fan::control::json
339