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