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 <nlohmann/json.hpp> 24 25 #include <algorithm> 26 #include <format> 27 28 namespace phosphor::fan::control::json 29 { 30 31 using json = nlohmann::json; 32 33 template <typename T> 34 uint64_t addFloorOffset(uint64_t floor, T offset, const std::string& actionName) 35 { 36 if constexpr (!std::is_arithmetic_v<T>) 37 { 38 throw std::runtime_error("Invalid variant type in addFloorOffset"); 39 } 40 41 auto newFloor = static_cast<T>(floor) + offset; 42 if (newFloor < 0) 43 { 44 log<level::ERR>( 45 std::format("{}: Floor offset of {} resulted in negative floor", 46 actionName, offset) 47 .c_str()); 48 return floor; 49 } 50 51 return static_cast<uint64_t>(newFloor); 52 } 53 54 MappedFloor::MappedFloor(const json& jsonObj, 55 const std::vector<Group>& groups) : 56 ActionBase(jsonObj, groups) 57 { 58 setKeyGroup(jsonObj); 59 setFloorTable(jsonObj); 60 setDefaultFloor(jsonObj); 61 setCondition(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 std::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 throw ActionParseError{ 161 ActionBase::getName(), 162 "Missing value or floor entries in " 163 "actions/fan_floors/floors/floors JSON"}; 164 } 165 166 auto value = getJsonValue(floorEntry["value"]); 167 auto floor = floorEntry["floor"].get<uint64_t>(); 168 169 fg.floorEntries.emplace_back(std::move(value), 170 std::move(floor)); 171 } 172 173 ff.floorGroups.push_back(std::move(fg)); 174 } 175 176 _fanFloors.push_back(std::move(ff)); 177 } 178 } 179 180 void MappedFloor::setCondition(const json& jsonObj) 181 { 182 // condition_group, condition_value, and condition_op 183 // are optional, though they must show up together. 184 // Assume if condition_group is present then they all 185 // must be. 186 if (!jsonObj.contains("condition_group")) 187 { 188 return; 189 } 190 191 _conditionGroup = getGroup(jsonObj["condition_group"].get<std::string>()); 192 193 if (_conditionGroup->getMembers().size() != 1) 194 { 195 throw ActionParseError{ 196 ActionBase::getName(), 197 std::format("condition_group {} must only have 1 member", 198 _conditionGroup->getName())}; 199 } 200 201 if (!jsonObj.contains("condition_value")) 202 { 203 throw ActionParseError{ActionBase::getName(), 204 "Missing required 'condition_value' entry in " 205 "mapped_floor action"}; 206 } 207 208 _conditionValue = getJsonValue(jsonObj["condition_value"]); 209 210 if (!jsonObj.contains("condition_op")) 211 { 212 throw ActionParseError{ActionBase::getName(), 213 "Missing required 'condition_op' entry in " 214 "mapped_floor action"}; 215 } 216 217 _conditionOp = jsonObj["condition_op"].get<std::string>(); 218 219 if ((_conditionOp != "equal") && (_conditionOp != "not_equal")) 220 { 221 throw ActionParseError{ActionBase::getName(), 222 "Invalid 'condition_op' value in " 223 "mapped_floor action"}; 224 } 225 } 226 227 /** 228 * @brief Converts the variant to a double if it's a 229 * int32_t or int64_t. 230 */ 231 void tryConvertToDouble(PropertyVariantType& value) 232 { 233 std::visit( 234 [&value](auto&& val) { 235 using V = std::decay_t<decltype(val)>; 236 if constexpr (std::is_same_v<int32_t, V> || 237 std::is_same_v<int64_t, V>) 238 { 239 value = static_cast<double>(val); 240 } 241 }, 242 value); 243 } 244 245 std::optional<PropertyVariantType> 246 MappedFloor::getMaxGroupValue(const Group& group) 247 { 248 std::optional<PropertyVariantType> max; 249 bool checked = false; 250 251 for (const auto& member : group.getMembers()) 252 { 253 try 254 { 255 auto value = Manager::getObjValueVariant( 256 member, group.getInterface(), group.getProperty()); 257 258 // Only allow a group to have multiple members if it's numeric. 259 // Unlike std::is_arithmetic, bools are not considered numeric 260 // here. 261 if (!checked && (group.getMembers().size() > 1)) 262 { 263 std::visit( 264 [&group, this](auto&& val) { 265 using V = std::decay_t<decltype(val)>; 266 if constexpr (!std::is_same_v<double, V> && 267 !std::is_same_v<int32_t, V> && 268 !std::is_same_v<int64_t, V>) 269 { 270 throw std::runtime_error{std::format( 271 "{}: Group {} has more than one member but " 272 "isn't numeric", 273 ActionBase::getName(), group.getName())}; 274 } 275 }, 276 value); 277 checked = true; 278 } 279 280 if (max && (value > max)) 281 { 282 max = value; 283 } 284 else if (!max) 285 { 286 max = value; 287 } 288 } 289 catch (const std::out_of_range& e) 290 { 291 // Property not there, continue on 292 } 293 } 294 295 if (max) 296 { 297 tryConvertToDouble(*max); 298 } 299 300 return max; 301 } 302 303 bool MappedFloor::meetsCondition() 304 { 305 if (!_conditionGroup) 306 { 307 return true; 308 } 309 310 bool meets = false; 311 312 // setCondition() also checks these 313 assert(_conditionGroup->getMembers().size() == 1); 314 assert((_conditionOp == "equal") || (_conditionOp == "not_equal")); 315 316 const auto& member = _conditionGroup->getMembers()[0]; 317 318 try 319 { 320 auto value = 321 Manager::getObjValueVariant(member, _conditionGroup->getInterface(), 322 _conditionGroup->getProperty()); 323 324 if ((_conditionOp == "equal") && (value == _conditionValue)) 325 { 326 meets = true; 327 } 328 else if ((_conditionOp == "not_equal") && (value != _conditionValue)) 329 { 330 meets = true; 331 } 332 } 333 catch (const std::out_of_range& e) 334 { 335 // Property not there, so consider it failing the 'equal' 336 // condition and passing the 'not_equal' condition. 337 if (_conditionOp == "equal") 338 { 339 meets = false; 340 } 341 else // not_equal 342 { 343 meets = true; 344 } 345 } 346 347 return meets; 348 } 349 350 void MappedFloor::run(Zone& zone) 351 { 352 if (!meetsCondition()) 353 { 354 // Make sure this no longer has a floor hold 355 if (zone.hasFloorHold(getUniqueName())) 356 { 357 zone.setFloorHold(getUniqueName(), 0, false); 358 } 359 return; 360 } 361 362 std::optional<uint64_t> newFloor; 363 364 auto keyValue = getMaxGroupValue(*_keyGroup); 365 if (!keyValue) 366 { 367 auto floor = _defaultFloor ? *_defaultFloor : zone.getDefaultFloor(); 368 zone.setFloorHold(getUniqueName(), floor, true); 369 return; 370 } 371 372 for (const auto& floorTable : _fanFloors) 373 { 374 // First, find the floorTable entry to use based on the key value. 375 auto tableKeyValue = floorTable.keyValue; 376 377 // Convert numeric values from the JSON to doubles so they can 378 // be compared to values coming from D-Bus. 379 tryConvertToDouble(tableKeyValue); 380 381 // The key value from D-Bus must be less than the value 382 // in the table for this entry to be valid. 383 if (*keyValue >= tableKeyValue) 384 { 385 continue; 386 } 387 388 // Now check each group in the tables 389 for (const auto& [groupOrParameter, floorGroups] : 390 floorTable.floorGroups) 391 { 392 std::optional<PropertyVariantType> propertyValue; 393 394 if (std::holds_alternative<std::string>(groupOrParameter)) 395 { 396 propertyValue = Manager::getParameter( 397 std::get<std::string>(groupOrParameter)); 398 if (propertyValue) 399 { 400 tryConvertToDouble(*propertyValue); 401 } 402 else 403 { 404 // If the parameter isn't there, then don't use 405 // this floor table 406 log<level::DEBUG>( 407 std::format("{}: Parameter {} specified in the JSON " 408 "could not be found", 409 ActionBase::getName(), 410 std::get<std::string>(groupOrParameter)) 411 .c_str()); 412 continue; 413 } 414 } 415 else 416 { 417 propertyValue = 418 getMaxGroupValue(*std::get<const Group*>(groupOrParameter)); 419 } 420 421 std::optional<uint64_t> floor; 422 if (propertyValue) 423 { 424 // Do either a <= or an == check depending on the data type 425 // to get the floor value based on this group. 426 for (const auto& [tableValue, tableFloor] : floorGroups) 427 { 428 PropertyVariantType value{tableValue}; 429 tryConvertToDouble(value); 430 431 if (std::holds_alternative<double>(*propertyValue)) 432 { 433 if (*propertyValue <= value) 434 { 435 floor = tableFloor; 436 break; 437 } 438 } 439 else if (*propertyValue == value) 440 { 441 floor = tableFloor; 442 break; 443 } 444 } 445 } 446 447 // No floor found in this group, use a default floor for now but 448 // let keep going in case it finds a higher one. 449 if (!floor) 450 { 451 if (floorTable.defaultFloor) 452 { 453 floor = *floorTable.defaultFloor; 454 } 455 else if (_defaultFloor) 456 { 457 floor = *_defaultFloor; 458 } 459 else 460 { 461 floor = zone.getDefaultFloor(); 462 } 463 } 464 465 // Keep track of the highest floor value found across all 466 // entries/groups 467 if ((newFloor && (floor > *newFloor)) || !newFloor) 468 { 469 newFloor = floor; 470 } 471 } 472 473 // if still no floor, use the default one from the floor table if 474 // there 475 if (!newFloor && floorTable.defaultFloor) 476 { 477 newFloor = floorTable.defaultFloor.value(); 478 } 479 480 if (newFloor) 481 { 482 *newFloor = applyFloorOffset(*newFloor, floorTable.offsetParameter); 483 } 484 485 // Valid key value for this entry, so done 486 break; 487 } 488 489 if (!newFloor) 490 { 491 newFloor = _defaultFloor ? *_defaultFloor : zone.getDefaultFloor(); 492 } 493 494 zone.setFloorHold(getUniqueName(), *newFloor, true); 495 } 496 497 uint64_t MappedFloor::applyFloorOffset(uint64_t floor, 498 const std::string& offsetParameter) const 499 { 500 if (!offsetParameter.empty()) 501 { 502 auto offset = Manager::getParameter(offsetParameter); 503 if (offset) 504 { 505 if (std::holds_alternative<int32_t>(*offset)) 506 { 507 return addFloorOffset(floor, std::get<int32_t>(*offset), 508 getUniqueName()); 509 } 510 else if (std::holds_alternative<int64_t>(*offset)) 511 { 512 return addFloorOffset(floor, std::get<int64_t>(*offset), 513 getUniqueName()); 514 } 515 else if (std::holds_alternative<double>(*offset)) 516 { 517 return addFloorOffset(floor, std::get<double>(*offset), 518 getUniqueName()); 519 } 520 else 521 { 522 throw std::runtime_error( 523 "Invalid data type in floor offset parameter "); 524 } 525 } 526 } 527 528 return floor; 529 } 530 531 } // namespace phosphor::fan::control::json 532