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 MappedFloor::MappedFloor(const json& jsonObj, 35 const std::vector<Group>& groups) : 36 ActionBase(jsonObj, groups) 37 { 38 setKeyGroup(jsonObj); 39 setFloorTable(jsonObj); 40 } 41 42 const Group* MappedFloor::getGroup(const std::string& name) 43 { 44 auto groupIt = 45 find_if(_groups.begin(), _groups.end(), 46 [name](const auto& group) { return name == group.getName(); }); 47 48 if (groupIt == _groups.end()) 49 { 50 throw ActionParseError{ 51 ActionBase::getName(), 52 fmt::format("Group name {} is not a valid group", name)}; 53 } 54 55 return &(*groupIt); 56 } 57 58 void MappedFloor::setKeyGroup(const json& jsonObj) 59 { 60 if (!jsonObj.contains("key_group")) 61 { 62 throw ActionParseError{ActionBase::getName(), 63 "Missing required 'key_group' entry"}; 64 } 65 _keyGroup = getGroup(jsonObj["key_group"].get<std::string>()); 66 } 67 68 void MappedFloor::setFloorTable(const json& jsonObj) 69 { 70 if (!jsonObj.contains("fan_floors")) 71 { 72 throw ActionParseError{ActionBase::getName(), 73 "Missing fan_floors JSON entry"}; 74 } 75 76 const auto& fanFloors = jsonObj.at("fan_floors"); 77 78 for (const auto& floors : fanFloors) 79 { 80 if (!floors.contains("key") || !floors.contains("floors")) 81 { 82 throw ActionParseError{ 83 ActionBase::getName(), 84 "Missing key or floors entries in actions/fan_floors JSON"}; 85 } 86 87 FanFloors ff; 88 ff.keyValue = getJsonValue(floors["key"]); 89 90 for (const auto& groupEntry : floors["floors"]) 91 { 92 if ((!groupEntry.contains("group") && 93 !groupEntry.contains("parameter")) || 94 !groupEntry.contains("floors")) 95 { 96 throw ActionParseError{ 97 ActionBase::getName(), 98 "Missing group, parameter, or floors entries in " 99 "actions/fan_floors/floors JSON"}; 100 } 101 102 FloorGroup fg; 103 if (groupEntry.contains("group")) 104 { 105 fg.groupOrParameter = 106 getGroup(groupEntry["group"].get<std::string>()); 107 } 108 else 109 { 110 fg.groupOrParameter = 111 groupEntry["parameter"].get<std::string>(); 112 } 113 114 for (const auto& floorEntry : groupEntry["floors"]) 115 { 116 if (!floorEntry.contains("value") || 117 !floorEntry.contains("floor")) 118 { 119 120 throw ActionParseError{ 121 ActionBase::getName(), 122 "Missing value or floor entries in " 123 "actions/fan_floors/floors/floors JSON"}; 124 } 125 126 auto value = getJsonValue(floorEntry["value"]); 127 auto floor = floorEntry["floor"].get<uint64_t>(); 128 129 fg.floorEntries.emplace_back(std::move(value), 130 std::move(floor)); 131 } 132 133 ff.floorGroups.push_back(std::move(fg)); 134 } 135 136 _fanFloors.push_back(std::move(ff)); 137 } 138 } 139 140 /** 141 * @brief Converts the variant to a double if it's a 142 * int32_t or int64_t. 143 */ 144 void tryConvertToDouble(PropertyVariantType& value) 145 { 146 std::visit( 147 [&value](auto&& val) { 148 using V = std::decay_t<decltype(val)>; 149 if constexpr (std::is_same_v<int32_t, V> || 150 std::is_same_v<int64_t, V>) 151 { 152 value = static_cast<double>(val); 153 } 154 }, 155 value); 156 } 157 158 std::optional<PropertyVariantType> 159 MappedFloor::getMaxGroupValue(const Group& group, const Manager& manager) 160 { 161 std::optional<PropertyVariantType> max; 162 bool checked = false; 163 164 for (const auto& member : group.getMembers()) 165 { 166 try 167 { 168 auto value = Manager::getObjValueVariant( 169 member, group.getInterface(), group.getProperty()); 170 171 // Only allow a group to have multiple members if it's numeric. 172 // Unlike std::is_arithmetic, bools are not considered numeric here. 173 if (!checked && (group.getMembers().size() > 1)) 174 { 175 std::visit( 176 [&group, this](auto&& val) { 177 using V = std::decay_t<decltype(val)>; 178 if constexpr (!std::is_same_v<double, V> && 179 !std::is_same_v<int32_t, V> && 180 !std::is_same_v<int64_t, V>) 181 { 182 throw std::runtime_error{fmt::format( 183 "{}: Group {} has more than one member but " 184 "isn't numeric", 185 ActionBase::getName(), group.getName())}; 186 } 187 }, 188 value); 189 checked = true; 190 } 191 192 if (max && (value > max)) 193 { 194 max = value; 195 } 196 else if (!max) 197 { 198 max = value; 199 } 200 } 201 catch (const std::out_of_range& e) 202 { 203 // Property not there, continue on 204 } 205 } 206 207 if (max) 208 { 209 tryConvertToDouble(*max); 210 } 211 212 return max; 213 } 214 215 void MappedFloor::run(Zone& zone) 216 { 217 std::optional<uint64_t> newFloor; 218 bool missingGroupProperty = false; 219 auto& manager = *zone.getManager(); 220 221 auto keyValue = getMaxGroupValue(*_keyGroup, manager); 222 if (!keyValue) 223 { 224 zone.setFloor(zone.getDefaultFloor()); 225 return; 226 } 227 228 for (const auto& floorTable : _fanFloors) 229 { 230 // First, find the floorTable entry to use based on the key value. 231 auto tableKeyValue = floorTable.keyValue; 232 233 // Convert numeric values from the JSON to doubles so they can 234 // be compared to values coming from D-Bus. 235 tryConvertToDouble(tableKeyValue); 236 237 // The key value from D-Bus must be less than the value 238 // in the table for this entry to be valid. 239 if (*keyValue >= tableKeyValue) 240 { 241 continue; 242 } 243 244 // Now check each group in the tables 245 for (const auto& [groupOrParameter, floorGroups] : 246 floorTable.floorGroups) 247 { 248 std::optional<PropertyVariantType> propertyValue; 249 250 if (std::holds_alternative<std::string>(groupOrParameter)) 251 { 252 propertyValue = Manager::getParameter( 253 std::get<std::string>(groupOrParameter)); 254 if (propertyValue) 255 { 256 tryConvertToDouble(*propertyValue); 257 } 258 else 259 { 260 log<level::ERR>( 261 fmt::format("{}: Parameter {} specified in the JSON " 262 "could not be found", 263 ActionBase::getName(), 264 std::get<std::string>(groupOrParameter)) 265 .c_str()); 266 } 267 } 268 else 269 { 270 propertyValue = getMaxGroupValue( 271 *std::get<const Group*>(groupOrParameter), manager); 272 } 273 274 if (!propertyValue) 275 { 276 // Couldn't successfully get a value. Results in default floor. 277 missingGroupProperty = true; 278 break; 279 } 280 281 // Do either a <= or an == check depending on the data type to get 282 // the floor value based on this group. 283 std::optional<uint64_t> floor; 284 for (const auto& [tableValue, tableFloor] : floorGroups) 285 { 286 PropertyVariantType value{tableValue}; 287 tryConvertToDouble(value); 288 289 if (std::holds_alternative<double>(*propertyValue)) 290 { 291 if (*propertyValue <= value) 292 { 293 floor = tableFloor; 294 break; 295 } 296 } 297 else if (*propertyValue == value) 298 { 299 floor = tableFloor; 300 break; 301 } 302 } 303 304 // Keep track of the highest floor value found across all 305 // entries/groups 306 if (floor) 307 { 308 if ((newFloor && (floor > *newFloor)) || !newFloor) 309 { 310 newFloor = floor; 311 } 312 } 313 else 314 { 315 // No match found in this group's table. 316 // Results in default floor. 317 missingGroupProperty = true; 318 } 319 } 320 321 // Valid key value for this entry, so done 322 break; 323 } 324 325 if (newFloor && !missingGroupProperty) 326 { 327 zone.setFloor(*newFloor); 328 } 329 else 330 { 331 zone.setFloor(zone.getDefaultFloor()); 332 } 333 } 334 335 } // namespace phosphor::fan::control::json 336