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                     log<level::ERR>(
261                         fmt::format("{}: Parameter {} specified in the JSON "
262                                     "could not be found",
263                                     ActionBase::getName(),
264                                     std::get<std::string>(groupOrParameter))
265                             .c_str());
266                 }
267             }
268             else
269             {
270                 propertyValue = getMaxGroupValue(
271                     *std::get<const Group*>(groupOrParameter), manager);
272             }
273 
274             if (!propertyValue)
275             {
276                 // Couldn't successfully get a value.  Results in default floor.
277                 missingGroupProperty = true;
278                 break;
279             }
280 
281             // Do either a <= or an == check depending on the data type to get
282             // the floor value based on this group.
283             std::optional<uint64_t> floor;
284             for (const auto& [tableValue, tableFloor] : floorGroups)
285             {
286                 PropertyVariantType value{tableValue};
287                 tryConvertToDouble(value);
288 
289                 if (std::holds_alternative<double>(*propertyValue))
290                 {
291                     if (*propertyValue <= value)
292                     {
293                         floor = tableFloor;
294                         break;
295                     }
296                 }
297                 else if (*propertyValue == value)
298                 {
299                     floor = tableFloor;
300                     break;
301                 }
302             }
303 
304             // Keep track of the highest floor value found across all
305             // entries/groups
306             if (floor)
307             {
308                 if ((newFloor && (floor > *newFloor)) || !newFloor)
309                 {
310                     newFloor = floor;
311                 }
312             }
313             else
314             {
315                 // No match found in this group's table.
316                 // Results in default floor.
317                 missingGroupProperty = true;
318             }
319         }
320 
321         // Valid key value for this entry, so done
322         break;
323     }
324 
325     if (newFloor && !missingGroupProperty)
326     {
327         zone.setFloor(*newFloor);
328     }
329     else
330     {
331         zone.setFloor(zone.getDefaultFloor());
332     }
333 }
334 
335 } // namespace phosphor::fan::control::json
336