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 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 void MappedFloor::setCondition(const json& jsonObj) 182 { 183 // condition_group, condition_value, and condition_op 184 // are optional, though they must show up together. 185 // Assume if condition_group is present then they all 186 // must be. 187 if (!jsonObj.contains("condition_group")) 188 { 189 return; 190 } 191 192 _conditionGroup = getGroup(jsonObj["condition_group"].get<std::string>()); 193 194 if (_conditionGroup->getMembers().size() != 1) 195 { 196 throw ActionParseError{ 197 ActionBase::getName(), 198 fmt::format("condition_group {} must only have 1 member", 199 _conditionGroup->getName())}; 200 } 201 202 if (!jsonObj.contains("condition_value")) 203 { 204 throw ActionParseError{ActionBase::getName(), 205 "Missing required 'condition_value' entry in " 206 "mapped_floor action"}; 207 } 208 209 _conditionValue = getJsonValue(jsonObj["condition_value"]); 210 211 if (!jsonObj.contains("condition_op")) 212 { 213 throw ActionParseError{ActionBase::getName(), 214 "Missing required 'condition_op' entry in " 215 "mapped_floor action"}; 216 } 217 218 _conditionOp = jsonObj["condition_op"].get<std::string>(); 219 220 if ((_conditionOp != "equal") && (_conditionOp != "not_equal")) 221 { 222 throw ActionParseError{ActionBase::getName(), 223 "Invalid 'condition_op' value in " 224 "mapped_floor action"}; 225 } 226 } 227 228 /** 229 * @brief Converts the variant to a double if it's a 230 * int32_t or int64_t. 231 */ 232 void tryConvertToDouble(PropertyVariantType& value) 233 { 234 std::visit( 235 [&value](auto&& val) { 236 using V = std::decay_t<decltype(val)>; 237 if constexpr (std::is_same_v<int32_t, V> || 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{fmt::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 fmt::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