control: Create MappedFloor action
This action can be used to set a floor value based on 2 or more groups
having values within certain ranges, where the key group chooses the set
of tables in which to check the remaining group values.
For example, with the following JSON:
{
"name": "mapped_floor",
"key_group": "ambient_temp",
"fan_floors": [
{
"key": 27,
"floors": [
{
"group": "altitude",
"floors": [
{
"value": 5000,
"floor": 2000
},
{
"value": 7000,
"floor": 6000
}
]
},
{
"group": "power_mode",
"floors": [
{
"value": "PowerSave",
"floor": 3000
},
{
"value": "MaximumPerformance",
"floor": 5000
}
]
}
]
}
]
}
If the ambient_temp group has a value less than 27, then it looks up the
values for the altitude and power_mode groups, where for altitude, since
it's numeric, it will use a <= operator, and for power_mode, since it's
a string, it will use an == operator when comparing to the values in the
JSON. It will then choose the largest floor value between the altitude
and power_mode results.
There are several scenarios that result in a default floor being set.
Full action documentation is in the class header file.
Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I8e0ff3a97ff73dd20018473c1993b2e043276099
diff --git a/control/Makefile.am b/control/Makefile.am
index 1114b8f..9ae9b47 100644
--- a/control/Makefile.am
+++ b/control/Makefile.am
@@ -54,7 +54,8 @@
json/actions/count_state_target.cpp \
json/actions/net_target_increase.cpp \
json/actions/net_target_decrease.cpp \
- json/actions/timer_based_actions.cpp
+ json/actions/timer_based_actions.cpp \
+ json/actions/mapped_floor.cpp
else
phosphor_fan_control_SOURCES += \
argument.cpp \
diff --git a/control/json/actions/mapped_floor.cpp b/control/json/actions/mapped_floor.cpp
new file mode 100644
index 0000000..bca3bf5
--- /dev/null
+++ b/control/json/actions/mapped_floor.cpp
@@ -0,0 +1,297 @@
+/**
+ * Copyright © 2021 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "mapped_floor.hpp"
+
+#include "../manager.hpp"
+#include "../zone.hpp"
+#include "group.hpp"
+#include "sdeventplus.hpp"
+
+#include <fmt/format.h>
+
+#include <nlohmann/json.hpp>
+
+#include <algorithm>
+
+namespace phosphor::fan::control::json
+{
+
+using json = nlohmann::json;
+
+MappedFloor::MappedFloor(const json& jsonObj,
+ const std::vector<Group>& groups) :
+ ActionBase(jsonObj, groups)
+{
+ setKeyGroup(jsonObj);
+ setFloorTable(jsonObj);
+}
+
+const Group* MappedFloor::getGroup(const std::string& name)
+{
+ auto groupIt =
+ find_if(_groups.begin(), _groups.end(),
+ [name](const auto& group) { return name == group.getName(); });
+
+ if (groupIt == _groups.end())
+ {
+ throw ActionParseError{
+ ActionBase::getName(),
+ fmt::format("Group name {} is not a valid group", name)};
+ }
+
+ return &(*groupIt);
+}
+
+void MappedFloor::setKeyGroup(const json& jsonObj)
+{
+ if (!jsonObj.contains("key_group"))
+ {
+ throw ActionParseError{ActionBase::getName(),
+ "Missing required 'key_group' entry"};
+ }
+ _keyGroup = getGroup(jsonObj["key_group"].get<std::string>());
+}
+
+void MappedFloor::setFloorTable(const json& jsonObj)
+{
+ if (!jsonObj.contains("fan_floors"))
+ {
+ throw ActionParseError{ActionBase::getName(),
+ "Missing fan_floors JSON entry"};
+ }
+
+ const auto& fanFloors = jsonObj.at("fan_floors");
+
+ for (const auto& floors : fanFloors)
+ {
+ if (!floors.contains("key") || !floors.contains("floors"))
+ {
+ throw ActionParseError{
+ ActionBase::getName(),
+ "Missing key or floors entries in actions/fan_floors JSON"};
+ }
+
+ FanFloors ff;
+ ff.keyValue = getJsonValue(floors["key"]);
+
+ for (const auto& groupEntry : floors["floors"])
+ {
+ if (!groupEntry.contains("group") || !groupEntry.contains("floors"))
+ {
+ throw ActionParseError{ActionBase::getName(),
+ "Missing group or floors entries in "
+ "actions/fan_floors/floors JSON"};
+ }
+
+ FloorGroup fg;
+ fg.group = getGroup(groupEntry["group"].get<std::string>());
+
+ for (const auto& floorEntry : groupEntry["floors"])
+ {
+ if (!floorEntry.contains("value") ||
+ !floorEntry.contains("floor"))
+ {
+
+ throw ActionParseError{
+ ActionBase::getName(),
+ "Missing value or floor entries in "
+ "actions/fan_floors/floors/floors JSON"};
+ }
+
+ auto value = getJsonValue(floorEntry["value"]);
+ auto floor = floorEntry["floor"].get<uint64_t>();
+
+ fg.floorEntries.emplace_back(std::move(value),
+ std::move(floor));
+ }
+
+ ff.floorGroups.push_back(std::move(fg));
+ }
+
+ _fanFloors.push_back(std::move(ff));
+ }
+}
+
+/**
+ * @brief Converts the variant to a double if it's a
+ * int32_t or int64_t.
+ */
+void tryConvertToDouble(PropertyVariantType& value)
+{
+ std::visit(
+ [&value](auto&& val) {
+ using V = std::decay_t<decltype(val)>;
+ if constexpr (std::is_same_v<int32_t, V> ||
+ std::is_same_v<int64_t, V>)
+ {
+ value = static_cast<double>(val);
+ }
+ },
+ value);
+}
+
+std::optional<PropertyVariantType>
+ MappedFloor::getMaxGroupValue(const Group& group, const Manager& manager)
+{
+ std::optional<PropertyVariantType> max;
+ bool checked = false;
+
+ for (const auto& member : group.getMembers())
+ {
+ try
+ {
+ auto value = Manager::getObjValueVariant(
+ member, group.getInterface(), group.getProperty());
+
+ // Only allow a group to have multiple members if it's numeric.
+ // Unlike std::is_arithmetic, bools are not considered numeric here.
+ if (!checked && (group.getMembers().size() > 1))
+ {
+ std::visit(
+ [&group, this](auto&& val) {
+ using V = std::decay_t<decltype(val)>;
+ if constexpr (!std::is_same_v<double, V> &&
+ !std::is_same_v<int32_t, V> &&
+ !std::is_same_v<int64_t, V>)
+ {
+ throw std::runtime_error{fmt::format(
+ "{}: Group {} has more than one member but "
+ "isn't numeric",
+ ActionBase::getName(), group.getName())};
+ }
+ },
+ value);
+ checked = true;
+ }
+
+ if (max && (value > max))
+ {
+ max = value;
+ }
+ else if (!max)
+ {
+ max = value;
+ }
+ }
+ catch (const std::out_of_range& e)
+ {
+ // Property not there, continue on
+ }
+ }
+
+ if (max)
+ {
+ tryConvertToDouble(*max);
+ }
+
+ return max;
+}
+
+void MappedFloor::run(Zone& zone)
+{
+ std::optional<uint64_t> newFloor;
+ bool missingGroupProperty = false;
+ auto& manager = *zone.getManager();
+
+ auto keyValue = getMaxGroupValue(*_keyGroup, manager);
+ if (!keyValue)
+ {
+ zone.setFloor(zone.getDefaultFloor());
+ return;
+ }
+
+ for (const auto& floorTable : _fanFloors)
+ {
+ // First, find the floorTable entry to use based on the key value.
+ auto tableKeyValue = floorTable.keyValue;
+
+ // Convert numeric values from the JSON to doubles so they can
+ // be compared to values coming from D-Bus.
+ tryConvertToDouble(tableKeyValue);
+
+ // The key value from D-Bus must be less than the value
+ // in the table for this entry to be valid.
+ if (*keyValue >= tableKeyValue)
+ {
+ continue;
+ }
+
+ // Now check each group in the tables
+ for (const auto& [group, floorGroups] : floorTable.floorGroups)
+ {
+ auto propertyValue = getMaxGroupValue(*group, manager);
+ if (!propertyValue)
+ {
+ // Couldn't successfully get a value. Results in default floor.
+ missingGroupProperty = true;
+ break;
+ }
+
+ // Do either a <= or an == check depending on the data type to get
+ // the floor value based on this group.
+ std::optional<uint64_t> floor;
+ for (const auto& [tableValue, tableFloor] : floorGroups)
+ {
+ PropertyVariantType value{tableValue};
+ tryConvertToDouble(value);
+
+ if (std::holds_alternative<double>(*propertyValue))
+ {
+ if (*propertyValue <= value)
+ {
+ floor = tableFloor;
+ break;
+ }
+ }
+ else if (*propertyValue == value)
+ {
+ floor = tableFloor;
+ break;
+ }
+ }
+
+ // Keep track of the highest floor value found across all
+ // entries/groups
+ if (floor)
+ {
+ if ((newFloor && (floor > *newFloor)) || !newFloor)
+ {
+ newFloor = floor;
+ }
+ }
+ else
+ {
+ // No match found in this group's table.
+ // Results in default floor.
+ missingGroupProperty = true;
+ }
+ }
+
+ // Valid key value for this entry, so done
+ break;
+ }
+
+ if (newFloor && !missingGroupProperty)
+ {
+ zone.setFloor(*newFloor);
+ }
+ else
+ {
+ zone.setFloor(zone.getDefaultFloor());
+ }
+}
+
+} // namespace phosphor::fan::control::json
diff --git a/control/json/actions/mapped_floor.hpp b/control/json/actions/mapped_floor.hpp
new file mode 100644
index 0000000..baf2701
--- /dev/null
+++ b/control/json/actions/mapped_floor.hpp
@@ -0,0 +1,194 @@
+/**
+ * Copyright © 2021 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include "../zone.hpp"
+#include "action.hpp"
+#include "group.hpp"
+
+#include <nlohmann/json.hpp>
+
+namespace phosphor::fan::control::json
+{
+
+using json = nlohmann::json;
+
+/**
+ * @class MappedFloor - Action to set a fan floor based on ranges of
+ * multiple sensor values.
+ * For example, consider the following config:
+ *
+ * {
+ * "name": "mapped_floor",
+ * "key_group": "ambient_temp",
+ * "fan_floors": [
+ * {
+ * "key": 27,
+ * "floors": [
+ * {
+ * "group": "altitude",
+ * "floors": [
+ * {
+ * "value": 5000,
+ * "floor": 4500
+ * }
+ * ]
+ * },
+ * {
+ * "group": "power_mode",
+ * "floors": [
+ * {
+ * "value": "MaximumPerformance",
+ * "floor": 5000
+ * }
+ * ]
+ * }
+ * ]
+ * }
+ * ]
+ * }
+ *
+ * When it runs, it will:
+ *
+ * 1. Evaluate the key_group
+ * - Find the max D-Bus property value (if numeric) of the member properties
+ * in this group.
+ * - Check it against each 'key' value in the fan_floor entries until
+ * the key_group property value < key value, so:
+ * max ambient temp < 27.
+ * - If the above check passes, the rest of that entry will be evaluated
+ * and then the action will be done.
+ *
+ * 2. Evaluate the group values in each floors array entry for this key value.
+ * - Find the max D-Bus property value (if numeric) of the member properties
+ * of this group - in this case 'altitude'.
+ * - Depending on the data type of that group, compare to the 'value' entry:
+ * - If numeric, check if the group's value is <= the 'value' one
+ * - Otherwise, check if it is ==
+ * - If that passes, save the value from the 'floor' entry and continue to
+ * the next entry in the floors array, which, if present, would specify
+ * another group to check. In this case, 'power_mode'. Repeat the above
+ * step.
+ * - After all the group compares are done, choose the largest floor value
+ * to set the fan floor to. If any group check results doesn't end in
+ * a match being found, then the default floor will be set.
+ *
+ * Cases where the default floor will be set:
+ * - A table entry can't be found based on a key group's value.
+ * - A table entry can't be found based on a group's value.
+ * - A value can't be obtained for the 'key_group' D-Bus property group.
+ * - A value can't be obtained for any of the 'group' property groups.
+ * - A value is NaN, as no <, <=, or == checks would succeed.
+ *
+ * Other notes:
+ * - If a group has multiple members, they must be numeric or else
+ * the code will throw an exception.
+ */
+
+class MappedFloor : public ActionBase, public ActionRegister<MappedFloor>
+{
+ public:
+ /* Name of this action */
+ static constexpr auto name = "mapped_floor";
+
+ MappedFloor() = delete;
+ MappedFloor(const MappedFloor&) = delete;
+ MappedFloor(MappedFloor&&) = delete;
+ MappedFloor& operator=(const MappedFloor&) = delete;
+ MappedFloor& operator=(MappedFloor&&) = delete;
+ ~MappedFloor() = default;
+
+ /**
+ * @brief Parse the JSON to set the members
+ *
+ * @param[in] jsonObj - JSON configuration of this action
+ * @param[in] groups - Groups of dbus objects the action uses
+ */
+ MappedFloor(const json& jsonObj, const std::vector<Group>& groups);
+
+ /**
+ * @brief Run the action. See description above.
+ *
+ * @param[in] zone - Zone to run the action on
+ */
+ void run(Zone& zone) override;
+
+ private:
+ /**
+ * @brief Parse and set the key group
+ *
+ * @param[in] jsonObj - JSON object for the action
+ */
+ void setKeyGroup(const json& jsonObj);
+
+ /**
+ * @brief Parses and sets the floor group data members
+ *
+ * @param[in] jsonObj - JSON object for the action
+ */
+ void setFloorTable(const json& jsonObj);
+
+ /**
+ * @brief Determines the maximum value of the property specified
+ * for the group of all members in the group.
+ *
+ * If not numeric, and more than one member, will throw an exception.
+ * Converts numeric values to doubles so they can be compared later.
+ *
+ * If cannot get at least one valid value, returns std::nullopt.
+ *
+ * @param[in] group - The group to get the max value of
+ *
+ * @param[in] manager - The Manager object
+ *
+ * @return optional<PropertyVariantType> - The value, or std::nullopt
+ */
+ std::optional<PropertyVariantType> getMaxGroupValue(const Group& group,
+ const Manager& manager);
+
+ /**
+ * @brief Returns a pointer to the group object specified
+ *
+ * Throws ActionParseError if no group found
+ *
+ * @param[in] name - The group name
+ *
+ * @return const Group* - Pointer to the group
+ */
+ const Group* getGroup(const std::string& name);
+
+ /* Key group pointer */
+ const Group* _keyGroup;
+
+ using FloorEntry = std::tuple<PropertyVariantType, uint64_t>;
+
+ struct FloorGroup
+ {
+ const Group* group;
+ std::vector<FloorEntry> floorEntries;
+ };
+
+ struct FanFloors
+ {
+ PropertyVariantType keyValue;
+ std::vector<FloorGroup> floorGroups;
+ };
+
+ /* The fan floors action data, loaded from JSON */
+ std::vector<FanFloors> _fanFloors;
+};
+
+} // namespace phosphor::fan::control::json