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