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) 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 261 auto keyValue = getMaxGroupValue(*_keyGroup); 262 if (!keyValue) 263 { 264 auto floor = _defaultFloor ? *_defaultFloor : zone.getDefaultFloor(); 265 zone.setFloorHold(getUniqueName(), floor, true); 266 return; 267 } 268 269 for (const auto& floorTable : _fanFloors) 270 { 271 // First, find the floorTable entry to use based on the key value. 272 auto tableKeyValue = floorTable.keyValue; 273 274 // Convert numeric values from the JSON to doubles so they can 275 // be compared to values coming from D-Bus. 276 tryConvertToDouble(tableKeyValue); 277 278 // The key value from D-Bus must be less than the value 279 // in the table for this entry to be valid. 280 if (*keyValue >= tableKeyValue) 281 { 282 continue; 283 } 284 285 // Now check each group in the tables 286 for (const auto& [groupOrParameter, floorGroups] : 287 floorTable.floorGroups) 288 { 289 std::optional<PropertyVariantType> propertyValue; 290 291 if (std::holds_alternative<std::string>(groupOrParameter)) 292 { 293 propertyValue = Manager::getParameter( 294 std::get<std::string>(groupOrParameter)); 295 if (propertyValue) 296 { 297 tryConvertToDouble(*propertyValue); 298 } 299 else 300 { 301 // If the parameter isn't there, then don't use 302 // this floor table 303 log<level::DEBUG>( 304 fmt::format("{}: Parameter {} specified in the JSON " 305 "could not be found", 306 ActionBase::getName(), 307 std::get<std::string>(groupOrParameter)) 308 .c_str()); 309 continue; 310 } 311 } 312 else 313 { 314 propertyValue = 315 getMaxGroupValue(*std::get<const Group*>(groupOrParameter)); 316 } 317 318 std::optional<uint64_t> floor; 319 if (propertyValue) 320 { 321 // Do either a <= or an == check depending on the data type 322 // to get the floor value based on this group. 323 for (const auto& [tableValue, tableFloor] : floorGroups) 324 { 325 PropertyVariantType value{tableValue}; 326 tryConvertToDouble(value); 327 328 if (std::holds_alternative<double>(*propertyValue)) 329 { 330 if (*propertyValue <= value) 331 { 332 floor = tableFloor; 333 break; 334 } 335 } 336 else if (*propertyValue == value) 337 { 338 floor = tableFloor; 339 break; 340 } 341 } 342 } 343 344 // No floor found in this group, use a default floor for now but 345 // let keep going in case it finds a higher one. 346 if (!floor) 347 { 348 if (floorTable.defaultFloor) 349 { 350 floor = *floorTable.defaultFloor; 351 } 352 else if (_defaultFloor) 353 { 354 floor = *_defaultFloor; 355 } 356 else 357 { 358 floor = zone.getDefaultFloor(); 359 } 360 } 361 362 // Keep track of the highest floor value found across all 363 // entries/groups 364 if ((newFloor && (floor > *newFloor)) || !newFloor) 365 { 366 newFloor = floor; 367 } 368 } 369 370 // if still no floor, use the default one from the floor table if 371 // there 372 if (!newFloor && floorTable.defaultFloor) 373 { 374 newFloor = floorTable.defaultFloor.value(); 375 } 376 377 if (newFloor) 378 { 379 *newFloor = applyFloorOffset(*newFloor, floorTable.offsetParameter); 380 } 381 382 // Valid key value for this entry, so done 383 break; 384 } 385 386 if (!newFloor) 387 { 388 newFloor = _defaultFloor ? *_defaultFloor : zone.getDefaultFloor(); 389 } 390 391 zone.setFloorHold(getUniqueName(), *newFloor, true); 392 } 393 394 uint64_t MappedFloor::applyFloorOffset(uint64_t floor, 395 const std::string& offsetParameter) const 396 { 397 if (!offsetParameter.empty()) 398 { 399 auto offset = Manager::getParameter(offsetParameter); 400 if (offset) 401 { 402 if (std::holds_alternative<int32_t>(*offset)) 403 { 404 return addFloorOffset(floor, std::get<int32_t>(*offset), 405 getUniqueName()); 406 } 407 else if (std::holds_alternative<int64_t>(*offset)) 408 { 409 return addFloorOffset(floor, std::get<int64_t>(*offset), 410 getUniqueName()); 411 } 412 else if (std::holds_alternative<double>(*offset)) 413 { 414 return addFloorOffset(floor, std::get<double>(*offset), 415 getUniqueName()); 416 } 417 else 418 { 419 throw std::runtime_error( 420 "Invalid data type in floor offset parameter "); 421 } 422 } 423 } 424 425 return floor; 426 } 427 428 } // namespace phosphor::fan::control::json 429