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> || std::is_same_v<int64_t, V>) 237 { 238 value = static_cast<double>(val); 239 } 240 }, 241 value); 242 } 243 244 std::optional<PropertyVariantType> 245 MappedFloor::getMaxGroupValue(const Group& group) 246 { 247 std::optional<PropertyVariantType> max; 248 bool checked = false; 249 250 for (const auto& member : group.getMembers()) 251 { 252 try 253 { 254 auto value = Manager::getObjValueVariant( 255 member, group.getInterface(), group.getProperty()); 256 257 // Only allow a group to have multiple members if it's numeric. 258 // Unlike std::is_arithmetic, bools are not considered numeric 259 // here. 260 if (!checked && (group.getMembers().size() > 1)) 261 { 262 std::visit( 263 [&group, this](auto&& val) { 264 using V = std::decay_t<decltype(val)>; 265 if constexpr (!std::is_same_v<double, V> && 266 !std::is_same_v<int32_t, V> && 267 !std::is_same_v<int64_t, V>) 268 { 269 throw std::runtime_error{std::format( 270 "{}: Group {} has more than one member but " 271 "isn't numeric", 272 ActionBase::getName(), group.getName())}; 273 } 274 }, 275 value); 276 checked = true; 277 } 278 279 if (max && (value > max)) 280 { 281 max = value; 282 } 283 else if (!max) 284 { 285 max = value; 286 } 287 } 288 catch (const std::out_of_range& e) 289 { 290 // Property not there, continue on 291 } 292 } 293 294 if (max) 295 { 296 tryConvertToDouble(*max); 297 } 298 299 return max; 300 } 301 302 bool MappedFloor::meetsCondition() 303 { 304 if (!_conditionGroup) 305 { 306 return true; 307 } 308 309 bool meets = false; 310 311 // setCondition() also checks these 312 assert(_conditionGroup->getMembers().size() == 1); 313 assert((_conditionOp == "equal") || (_conditionOp == "not_equal")); 314 315 const auto& member = _conditionGroup->getMembers()[0]; 316 317 try 318 { 319 auto value = 320 Manager::getObjValueVariant(member, _conditionGroup->getInterface(), 321 _conditionGroup->getProperty()); 322 323 if ((_conditionOp == "equal") && (value == _conditionValue)) 324 { 325 meets = true; 326 } 327 else if ((_conditionOp == "not_equal") && (value != _conditionValue)) 328 { 329 meets = true; 330 } 331 } 332 catch (const std::out_of_range& e) 333 { 334 // Property not there, so consider it failing the 'equal' 335 // condition and passing the 'not_equal' condition. 336 if (_conditionOp == "equal") 337 { 338 meets = false; 339 } 340 else // not_equal 341 { 342 meets = true; 343 } 344 } 345 346 return meets; 347 } 348 349 void MappedFloor::run(Zone& zone) 350 { 351 if (!meetsCondition()) 352 { 353 // Make sure this no longer has a floor hold 354 if (zone.hasFloorHold(getUniqueName())) 355 { 356 zone.setFloorHold(getUniqueName(), 0, false); 357 } 358 return; 359 } 360 361 std::optional<uint64_t> newFloor; 362 363 auto keyValue = getMaxGroupValue(*_keyGroup); 364 if (!keyValue) 365 { 366 auto floor = _defaultFloor ? *_defaultFloor : zone.getDefaultFloor(); 367 zone.setFloorHold(getUniqueName(), floor, true); 368 return; 369 } 370 371 for (const auto& floorTable : _fanFloors) 372 { 373 // First, find the floorTable entry to use based on the key value. 374 auto tableKeyValue = floorTable.keyValue; 375 376 // Convert numeric values from the JSON to doubles so they can 377 // be compared to values coming from D-Bus. 378 tryConvertToDouble(tableKeyValue); 379 380 // The key value from D-Bus must be less than the value 381 // in the table for this entry to be valid. 382 if (*keyValue >= tableKeyValue) 383 { 384 continue; 385 } 386 387 // Now check each group in the tables 388 for (const auto& [groupOrParameter, floorGroups] : 389 floorTable.floorGroups) 390 { 391 std::optional<PropertyVariantType> propertyValue; 392 393 if (std::holds_alternative<std::string>(groupOrParameter)) 394 { 395 propertyValue = Manager::getParameter( 396 std::get<std::string>(groupOrParameter)); 397 if (propertyValue) 398 { 399 tryConvertToDouble(*propertyValue); 400 } 401 else 402 { 403 // If the parameter isn't there, then don't use 404 // this floor table 405 log<level::DEBUG>( 406 std::format("{}: Parameter {} specified in the JSON " 407 "could not be found", 408 ActionBase::getName(), 409 std::get<std::string>(groupOrParameter)) 410 .c_str()); 411 continue; 412 } 413 } 414 else 415 { 416 propertyValue = 417 getMaxGroupValue(*std::get<const Group*>(groupOrParameter)); 418 } 419 420 std::optional<uint64_t> floor; 421 if (propertyValue) 422 { 423 // Do either a <= or an == check depending on the data type 424 // to get the floor value based on this group. 425 for (const auto& [tableValue, tableFloor] : floorGroups) 426 { 427 PropertyVariantType value{tableValue}; 428 tryConvertToDouble(value); 429 430 if (std::holds_alternative<double>(*propertyValue)) 431 { 432 if (*propertyValue <= value) 433 { 434 floor = tableFloor; 435 break; 436 } 437 } 438 else if (*propertyValue == value) 439 { 440 floor = tableFloor; 441 break; 442 } 443 } 444 } 445 446 // No floor found in this group, use a default floor for now but 447 // let keep going in case it finds a higher one. 448 if (!floor) 449 { 450 if (floorTable.defaultFloor) 451 { 452 floor = *floorTable.defaultFloor; 453 } 454 else if (_defaultFloor) 455 { 456 floor = *_defaultFloor; 457 } 458 else 459 { 460 floor = zone.getDefaultFloor(); 461 } 462 } 463 464 // Keep track of the highest floor value found across all 465 // entries/groups 466 if ((newFloor && (floor > *newFloor)) || !newFloor) 467 { 468 newFloor = floor; 469 } 470 } 471 472 // if still no floor, use the default one from the floor table if 473 // there 474 if (!newFloor && floorTable.defaultFloor) 475 { 476 newFloor = floorTable.defaultFloor.value(); 477 } 478 479 if (newFloor) 480 { 481 *newFloor = applyFloorOffset(*newFloor, floorTable.offsetParameter); 482 } 483 484 // Valid key value for this entry, so done 485 break; 486 } 487 488 if (!newFloor) 489 { 490 newFloor = _defaultFloor ? *_defaultFloor : zone.getDefaultFloor(); 491 } 492 493 zone.setFloorHold(getUniqueName(), *newFloor, true); 494 } 495 496 uint64_t MappedFloor::applyFloorOffset(uint64_t floor, 497 const std::string& offsetParameter) const 498 { 499 if (!offsetParameter.empty()) 500 { 501 auto offset = Manager::getParameter(offsetParameter); 502 if (offset) 503 { 504 if (std::holds_alternative<int32_t>(*offset)) 505 { 506 return addFloorOffset(floor, std::get<int32_t>(*offset), 507 getUniqueName()); 508 } 509 else if (std::holds_alternative<int64_t>(*offset)) 510 { 511 return addFloorOffset(floor, std::get<int64_t>(*offset), 512 getUniqueName()); 513 } 514 else if (std::holds_alternative<double>(*offset)) 515 { 516 return addFloorOffset(floor, std::get<double>(*offset), 517 getUniqueName()); 518 } 519 else 520 { 521 throw std::runtime_error( 522 "Invalid data type in floor offset parameter "); 523 } 524 } 525 } 526 527 return floor; 528 } 529 530 } // namespace phosphor::fan::control::json 531