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 setCondition(jsonObj); 63 } 64 65 const Group* MappedFloor::getGroup(const std::string& name) 66 { 67 auto groupIt = 68 find_if(_groups.begin(), _groups.end(), 69 [name](const auto& group) { return name == group.getName(); }); 70 71 if (groupIt == _groups.end()) 72 { 73 throw ActionParseError{ 74 ActionBase::getName(), 75 fmt::format("Group name {} is not a valid group", name)}; 76 } 77 78 return &(*groupIt); 79 } 80 81 void MappedFloor::setKeyGroup(const json& jsonObj) 82 { 83 if (!jsonObj.contains("key_group")) 84 { 85 throw ActionParseError{ActionBase::getName(), 86 "Missing required 'key_group' entry"}; 87 } 88 _keyGroup = getGroup(jsonObj["key_group"].get<std::string>()); 89 } 90 91 void MappedFloor::setDefaultFloor(const json& jsonObj) 92 { 93 if (jsonObj.contains("default_floor")) 94 { 95 _defaultFloor = jsonObj["default_floor"].get<uint64_t>(); 96 } 97 } 98 99 void MappedFloor::setFloorTable(const json& jsonObj) 100 { 101 if (!jsonObj.contains("fan_floors")) 102 { 103 throw ActionParseError{ActionBase::getName(), 104 "Missing fan_floors JSON entry"}; 105 } 106 107 const auto& fanFloors = jsonObj.at("fan_floors"); 108 109 for (const auto& floors : fanFloors) 110 { 111 if (!floors.contains("key") || !floors.contains("floors")) 112 { 113 throw ActionParseError{ 114 ActionBase::getName(), 115 "Missing key or floors entries in actions/fan_floors JSON"}; 116 } 117 118 FanFloors ff; 119 ff.keyValue = getJsonValue(floors["key"]); 120 121 if (floors.contains("floor_offset_parameter")) 122 { 123 ff.offsetParameter = 124 floors["floor_offset_parameter"].get<std::string>(); 125 } 126 127 if (floors.contains("default_floor")) 128 { 129 ff.defaultFloor = floors["default_floor"].get<uint64_t>(); 130 } 131 132 for (const auto& groupEntry : floors["floors"]) 133 { 134 if ((!groupEntry.contains("group") && 135 !groupEntry.contains("parameter")) || 136 !groupEntry.contains("floors")) 137 { 138 throw ActionParseError{ 139 ActionBase::getName(), 140 "Missing group, parameter, or floors entries in " 141 "actions/fan_floors/floors JSON"}; 142 } 143 144 FloorGroup fg; 145 if (groupEntry.contains("group")) 146 { 147 fg.groupOrParameter = 148 getGroup(groupEntry["group"].get<std::string>()); 149 } 150 else 151 { 152 fg.groupOrParameter = 153 groupEntry["parameter"].get<std::string>(); 154 } 155 156 for (const auto& floorEntry : groupEntry["floors"]) 157 { 158 if (!floorEntry.contains("value") || 159 !floorEntry.contains("floor")) 160 { 161 162 throw ActionParseError{ 163 ActionBase::getName(), 164 "Missing value or floor entries in " 165 "actions/fan_floors/floors/floors JSON"}; 166 } 167 168 auto value = getJsonValue(floorEntry["value"]); 169 auto floor = floorEntry["floor"].get<uint64_t>(); 170 171 fg.floorEntries.emplace_back(std::move(value), 172 std::move(floor)); 173 } 174 175 ff.floorGroups.push_back(std::move(fg)); 176 } 177 178 _fanFloors.push_back(std::move(ff)); 179 } 180 } 181 182 void MappedFloor::setCondition(const json& jsonObj) 183 { 184 // condition_group, condition_value, and condition_op 185 // are optional, though they must show up together. 186 // Assume if condition_group is present then they all 187 // must be. 188 if (!jsonObj.contains("condition_group")) 189 { 190 return; 191 } 192 193 _conditionGroup = getGroup(jsonObj["condition_group"].get<std::string>()); 194 195 if (_conditionGroup->getMembers().size() != 1) 196 { 197 throw ActionParseError{ 198 ActionBase::getName(), 199 fmt::format("condition_group {} must only have 1 member", 200 _conditionGroup->getName())}; 201 } 202 203 if (!jsonObj.contains("condition_value")) 204 { 205 throw ActionParseError{ActionBase::getName(), 206 "Missing required 'condition_value' entry in " 207 "mapped_floor action"}; 208 } 209 210 _conditionValue = getJsonValue(jsonObj["condition_value"]); 211 212 if (!jsonObj.contains("condition_op")) 213 { 214 throw ActionParseError{ActionBase::getName(), 215 "Missing required 'condition_op' entry in " 216 "mapped_floor action"}; 217 } 218 219 _conditionOp = jsonObj["condition_op"].get<std::string>(); 220 221 if ((_conditionOp != "equal") && (_conditionOp != "not_equal")) 222 { 223 throw ActionParseError{ActionBase::getName(), 224 "Invalid 'condition_op' value in " 225 "mapped_floor action"}; 226 } 227 } 228 229 /** 230 * @brief Converts the variant to a double if it's a 231 * int32_t or int64_t. 232 */ 233 void tryConvertToDouble(PropertyVariantType& value) 234 { 235 std::visit( 236 [&value](auto&& val) { 237 using V = std::decay_t<decltype(val)>; 238 if constexpr (std::is_same_v<int32_t, V> || 239 std::is_same_v<int64_t, V>) 240 { 241 value = static_cast<double>(val); 242 } 243 }, 244 value); 245 } 246 247 std::optional<PropertyVariantType> 248 MappedFloor::getMaxGroupValue(const Group& group) 249 { 250 std::optional<PropertyVariantType> max; 251 bool checked = false; 252 253 for (const auto& member : group.getMembers()) 254 { 255 try 256 { 257 auto value = Manager::getObjValueVariant( 258 member, group.getInterface(), group.getProperty()); 259 260 // Only allow a group to have multiple members if it's numeric. 261 // Unlike std::is_arithmetic, bools are not considered numeric 262 // here. 263 if (!checked && (group.getMembers().size() > 1)) 264 { 265 std::visit( 266 [&group, this](auto&& val) { 267 using V = std::decay_t<decltype(val)>; 268 if constexpr (!std::is_same_v<double, V> && 269 !std::is_same_v<int32_t, V> && 270 !std::is_same_v<int64_t, V>) 271 { 272 throw std::runtime_error{fmt::format( 273 "{}: Group {} has more than one member but " 274 "isn't numeric", 275 ActionBase::getName(), group.getName())}; 276 } 277 }, 278 value); 279 checked = true; 280 } 281 282 if (max && (value > max)) 283 { 284 max = value; 285 } 286 else if (!max) 287 { 288 max = value; 289 } 290 } 291 catch (const std::out_of_range& e) 292 { 293 // Property not there, continue on 294 } 295 } 296 297 if (max) 298 { 299 tryConvertToDouble(*max); 300 } 301 302 return max; 303 } 304 305 bool MappedFloor::meetsCondition() 306 { 307 if (!_conditionGroup) 308 { 309 return true; 310 } 311 312 bool meets = false; 313 314 // setCondition() also checks these 315 assert(_conditionGroup->getMembers().size() == 1); 316 assert((_conditionOp == "equal") || (_conditionOp == "not_equal")); 317 318 const auto& member = _conditionGroup->getMembers()[0]; 319 320 try 321 { 322 auto value = 323 Manager::getObjValueVariant(member, _conditionGroup->getInterface(), 324 _conditionGroup->getProperty()); 325 326 if ((_conditionOp == "equal") && (value == _conditionValue)) 327 { 328 meets = true; 329 } 330 else if ((_conditionOp == "not_equal") && (value != _conditionValue)) 331 { 332 meets = true; 333 } 334 } 335 catch (const std::out_of_range& e) 336 { 337 // Property not there, so consider it failing the 'equal' 338 // condition and passing the 'not_equal' condition. 339 if (_conditionOp == "equal") 340 { 341 meets = false; 342 } 343 else // not_equal 344 { 345 meets = true; 346 } 347 } 348 349 return meets; 350 } 351 352 void MappedFloor::run(Zone& zone) 353 { 354 if (!meetsCondition()) 355 { 356 // Make sure this no longer has a floor hold 357 if (zone.hasFloorHold(getUniqueName())) 358 { 359 zone.setFloorHold(getUniqueName(), 0, false); 360 } 361 return; 362 } 363 364 std::optional<uint64_t> newFloor; 365 366 auto keyValue = getMaxGroupValue(*_keyGroup); 367 if (!keyValue) 368 { 369 auto floor = _defaultFloor ? *_defaultFloor : zone.getDefaultFloor(); 370 zone.setFloorHold(getUniqueName(), floor, true); 371 return; 372 } 373 374 for (const auto& floorTable : _fanFloors) 375 { 376 // First, find the floorTable entry to use based on the key value. 377 auto tableKeyValue = floorTable.keyValue; 378 379 // Convert numeric values from the JSON to doubles so they can 380 // be compared to values coming from D-Bus. 381 tryConvertToDouble(tableKeyValue); 382 383 // The key value from D-Bus must be less than the value 384 // in the table for this entry to be valid. 385 if (*keyValue >= tableKeyValue) 386 { 387 continue; 388 } 389 390 // Now check each group in the tables 391 for (const auto& [groupOrParameter, floorGroups] : 392 floorTable.floorGroups) 393 { 394 std::optional<PropertyVariantType> propertyValue; 395 396 if (std::holds_alternative<std::string>(groupOrParameter)) 397 { 398 propertyValue = Manager::getParameter( 399 std::get<std::string>(groupOrParameter)); 400 if (propertyValue) 401 { 402 tryConvertToDouble(*propertyValue); 403 } 404 else 405 { 406 // If the parameter isn't there, then don't use 407 // this floor table 408 log<level::DEBUG>( 409 fmt::format("{}: Parameter {} specified in the JSON " 410 "could not be found", 411 ActionBase::getName(), 412 std::get<std::string>(groupOrParameter)) 413 .c_str()); 414 continue; 415 } 416 } 417 else 418 { 419 propertyValue = 420 getMaxGroupValue(*std::get<const Group*>(groupOrParameter)); 421 } 422 423 std::optional<uint64_t> floor; 424 if (propertyValue) 425 { 426 // Do either a <= or an == check depending on the data type 427 // to get the floor value based on this group. 428 for (const auto& [tableValue, tableFloor] : floorGroups) 429 { 430 PropertyVariantType value{tableValue}; 431 tryConvertToDouble(value); 432 433 if (std::holds_alternative<double>(*propertyValue)) 434 { 435 if (*propertyValue <= value) 436 { 437 floor = tableFloor; 438 break; 439 } 440 } 441 else if (*propertyValue == value) 442 { 443 floor = tableFloor; 444 break; 445 } 446 } 447 } 448 449 // No floor found in this group, use a default floor for now but 450 // let keep going in case it finds a higher one. 451 if (!floor) 452 { 453 if (floorTable.defaultFloor) 454 { 455 floor = *floorTable.defaultFloor; 456 } 457 else if (_defaultFloor) 458 { 459 floor = *_defaultFloor; 460 } 461 else 462 { 463 floor = zone.getDefaultFloor(); 464 } 465 } 466 467 // Keep track of the highest floor value found across all 468 // entries/groups 469 if ((newFloor && (floor > *newFloor)) || !newFloor) 470 { 471 newFloor = floor; 472 } 473 } 474 475 // if still no floor, use the default one from the floor table if 476 // there 477 if (!newFloor && floorTable.defaultFloor) 478 { 479 newFloor = floorTable.defaultFloor.value(); 480 } 481 482 if (newFloor) 483 { 484 *newFloor = applyFloorOffset(*newFloor, floorTable.offsetParameter); 485 } 486 487 // Valid key value for this entry, so done 488 break; 489 } 490 491 if (!newFloor) 492 { 493 newFloor = _defaultFloor ? *_defaultFloor : zone.getDefaultFloor(); 494 } 495 496 zone.setFloorHold(getUniqueName(), *newFloor, true); 497 } 498 499 uint64_t MappedFloor::applyFloorOffset(uint64_t floor, 500 const std::string& offsetParameter) const 501 { 502 if (!offsetParameter.empty()) 503 { 504 auto offset = Manager::getParameter(offsetParameter); 505 if (offset) 506 { 507 if (std::holds_alternative<int32_t>(*offset)) 508 { 509 return addFloorOffset(floor, std::get<int32_t>(*offset), 510 getUniqueName()); 511 } 512 else if (std::holds_alternative<int64_t>(*offset)) 513 { 514 return addFloorOffset(floor, std::get<int64_t>(*offset), 515 getUniqueName()); 516 } 517 else if (std::holds_alternative<double>(*offset)) 518 { 519 return addFloorOffset(floor, std::get<double>(*offset), 520 getUniqueName()); 521 } 522 else 523 { 524 throw std::runtime_error( 525 "Invalid data type in floor offset parameter "); 526 } 527 } 528 } 529 530 return floor; 531 } 532 533 } // namespace phosphor::fan::control::json 534