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") || !groupEntry.contains("floors"))
93             {
94                 throw ActionParseError{ActionBase::getName(),
95                                        "Missing group or floors entries in "
96                                        "actions/fan_floors/floors JSON"};
97             }
98 
99             FloorGroup fg;
100             fg.group = getGroup(groupEntry["group"].get<std::string>());
101 
102             for (const auto& floorEntry : groupEntry["floors"])
103             {
104                 if (!floorEntry.contains("value") ||
105                     !floorEntry.contains("floor"))
106                 {
107 
108                     throw ActionParseError{
109                         ActionBase::getName(),
110                         "Missing value or floor entries in "
111                         "actions/fan_floors/floors/floors JSON"};
112                 }
113 
114                 auto value = getJsonValue(floorEntry["value"]);
115                 auto floor = floorEntry["floor"].get<uint64_t>();
116 
117                 fg.floorEntries.emplace_back(std::move(value),
118                                              std::move(floor));
119             }
120 
121             ff.floorGroups.push_back(std::move(fg));
122         }
123 
124         _fanFloors.push_back(std::move(ff));
125     }
126 }
127 
128 /**
129  * @brief Converts the variant to a double if it's a
130  *        int32_t or int64_t.
131  */
132 void tryConvertToDouble(PropertyVariantType& value)
133 {
134     std::visit(
135         [&value](auto&& val) {
136             using V = std::decay_t<decltype(val)>;
137             if constexpr (std::is_same_v<int32_t, V> ||
138                           std::is_same_v<int64_t, V>)
139             {
140                 value = static_cast<double>(val);
141             }
142         },
143         value);
144 }
145 
146 std::optional<PropertyVariantType>
147     MappedFloor::getMaxGroupValue(const Group& group, const Manager& manager)
148 {
149     std::optional<PropertyVariantType> max;
150     bool checked = false;
151 
152     for (const auto& member : group.getMembers())
153     {
154         try
155         {
156             auto value = Manager::getObjValueVariant(
157                 member, group.getInterface(), group.getProperty());
158 
159             // Only allow a group to have multiple members if it's numeric.
160             // Unlike std::is_arithmetic, bools are not considered numeric here.
161             if (!checked && (group.getMembers().size() > 1))
162             {
163                 std::visit(
164                     [&group, this](auto&& val) {
165                         using V = std::decay_t<decltype(val)>;
166                         if constexpr (!std::is_same_v<double, V> &&
167                                       !std::is_same_v<int32_t, V> &&
168                                       !std::is_same_v<int64_t, V>)
169                         {
170                             throw std::runtime_error{fmt::format(
171                                 "{}: Group {} has more than one member but "
172                                 "isn't numeric",
173                                 ActionBase::getName(), group.getName())};
174                         }
175                     },
176                     value);
177                 checked = true;
178             }
179 
180             if (max && (value > max))
181             {
182                 max = value;
183             }
184             else if (!max)
185             {
186                 max = value;
187             }
188         }
189         catch (const std::out_of_range& e)
190         {
191             // Property not there, continue on
192         }
193     }
194 
195     if (max)
196     {
197         tryConvertToDouble(*max);
198     }
199 
200     return max;
201 }
202 
203 void MappedFloor::run(Zone& zone)
204 {
205     std::optional<uint64_t> newFloor;
206     bool missingGroupProperty = false;
207     auto& manager = *zone.getManager();
208 
209     auto keyValue = getMaxGroupValue(*_keyGroup, manager);
210     if (!keyValue)
211     {
212         zone.setFloor(zone.getDefaultFloor());
213         return;
214     }
215 
216     for (const auto& floorTable : _fanFloors)
217     {
218         // First, find the floorTable entry to use based on the key value.
219         auto tableKeyValue = floorTable.keyValue;
220 
221         // Convert numeric values from the JSON to doubles so they can
222         // be compared to values coming from D-Bus.
223         tryConvertToDouble(tableKeyValue);
224 
225         // The key value from D-Bus must be less than the value
226         // in the table for this entry to be valid.
227         if (*keyValue >= tableKeyValue)
228         {
229             continue;
230         }
231 
232         // Now check each group in the tables
233         for (const auto& [group, floorGroups] : floorTable.floorGroups)
234         {
235             auto propertyValue = getMaxGroupValue(*group, manager);
236             if (!propertyValue)
237             {
238                 // Couldn't successfully get a value.  Results in default floor.
239                 missingGroupProperty = true;
240                 break;
241             }
242 
243             // Do either a <= or an == check depending on the data type to get
244             // the floor value based on this group.
245             std::optional<uint64_t> floor;
246             for (const auto& [tableValue, tableFloor] : floorGroups)
247             {
248                 PropertyVariantType value{tableValue};
249                 tryConvertToDouble(value);
250 
251                 if (std::holds_alternative<double>(*propertyValue))
252                 {
253                     if (*propertyValue <= value)
254                     {
255                         floor = tableFloor;
256                         break;
257                     }
258                 }
259                 else if (*propertyValue == value)
260                 {
261                     floor = tableFloor;
262                     break;
263                 }
264             }
265 
266             // Keep track of the highest floor value found across all
267             // entries/groups
268             if (floor)
269             {
270                 if ((newFloor && (floor > *newFloor)) || !newFloor)
271                 {
272                     newFloor = floor;
273                 }
274             }
275             else
276             {
277                 // No match found in this group's table.
278                 // Results in default floor.
279                 missingGroupProperty = true;
280             }
281         }
282 
283         // Valid key value for this entry, so done
284         break;
285     }
286 
287     if (newFloor && !missingGroupProperty)
288     {
289         zone.setFloor(*newFloor);
290     }
291     else
292     {
293         zone.setFloor(zone.getDefaultFloor());
294     }
295 }
296 
297 } // namespace phosphor::fan::control::json
298