Add exit air temp sensor
Exit air temperature is calculated based on system
power and CFM. CFM sensor will be broken out into own
sensor in follow on commit.
Change-Id: I01b68c4de9a17e5a8d623bbbd7e7089f8f9d15d5
Signed-off-by: James Feist <james.feist@linux.intel.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 94395d2..06ff993 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -29,6 +29,8 @@
set (ADC_SRC_FILES src/Utils.cpp src/ADCSensor.cpp src/Thresholds.cpp)
+set (EXIT_AIR_SRC_FILES src/Utils.cpp src/Thresholds.cpp)
+
set (EXTERNAL_PACKAGES Boost sdbusplus-project nlohmann-json)
set (SENSOR_LINK_LIBS -lsystemd stdc++fs sdbusplus)
@@ -119,11 +121,17 @@
add_dependencies (adcsensor sdbusplus)
target_link_libraries (adcsensor ${SENSOR_LINK_LIBS})
+add_executable (exitairtempsensor src/ExitAirTempSensor.cpp
+ ${EXIT_AIR_SRC_FILES})
+add_dependencies (exitairtempsensor sdbusplus)
+target_link_libraries (exitairtempsensor ${SENSOR_LINK_LIBS})
+
if (NOT YOCTO)
add_dependencies (fansensor ${EXTERNAL_PACKAGES})
add_dependencies (hwmontempsensor ${EXTERNAL_PACKAGES})
add_dependencies (adcsensor ${EXTERNAL_PACKAGES})
add_dependencies (cpusensor ${EXTERNAL_PACKAGES})
+ add_dependencies (exitairtempsensor ${EXTERNAL_PACKAGES})
endif ()
set (
@@ -133,5 +141,7 @@
${PROJECT_SOURCE_DIR}/service_files/xyz.openbmc_project.fansensor.service
${PROJECT_SOURCE_DIR}/service_files/xyz.openbmc_project.hwmontempsensor.service
)
-install (TARGETS fansensor hwmontempsensor cpusensor adcsensor DESTINATION sbin)
+
+install (TARGETS fansensor hwmontempsensor cpusensor adcsensor
+ exitairtempsensor DESTINATION sbin)
install (FILES ${SERVICE_FILES} DESTINATION /lib/systemd/system/)
diff --git a/include/ExitAirTempSensor.hpp b/include/ExitAirTempSensor.hpp
new file mode 100644
index 0000000..44c736c
--- /dev/null
+++ b/include/ExitAirTempSensor.hpp
@@ -0,0 +1,53 @@
+#pragma once
+#include "sensor.hpp"
+
+#include <boost/container/flat_map.hpp>
+#include <chrono>
+#include <limits>
+#include <vector>
+
+struct CFMInfo
+{
+ std::vector<std::string> pwm;
+ int32_t c1;
+ int32_t c2;
+ int32_t maxCFM;
+ double pwmMin;
+ double pwmMax;
+};
+
+struct ExitAirTempSensor : public Sensor
+{
+ boost::container::flat_map<std::string, double> pwmReadings;
+ boost::container::flat_map<std::string, double> powerReadings;
+ std::vector<sdbusplus::bus::match::match> matches;
+
+ double inletTemp = std::numeric_limits<double>::quiet_NaN();
+
+ double powerFactorMin;
+ double powerFactorMax;
+ double qMin;
+ double qMax;
+ double alphaS;
+ double alphaF;
+ double pOffset = 0;
+
+ std::vector<CFMInfo> cfmData;
+ ExitAirTempSensor(std::shared_ptr<sdbusplus::asio::connection> &conn,
+ const std::string &sensorConfiguration,
+ sdbusplus::asio::object_server &objectServer,
+ std::vector<thresholds::Threshold> &&thresholds);
+ ~ExitAirTempSensor();
+
+ void checkThresholds(void) override;
+ void updateReading(void);
+
+ private:
+ double lastReading;
+
+ std::shared_ptr<sdbusplus::asio::connection> dbusConnection;
+ std::chrono::time_point<std::chrono::system_clock> lastTime;
+ int32_t getTotalCFM(void);
+ bool calculate(double &val);
+ void setupMatches(void);
+};
\ No newline at end of file
diff --git a/include/Utils.hpp b/include/Utils.hpp
index 8d9eac5..fd4dde2 100644
--- a/include/Utils.hpp
+++ b/include/Utils.hpp
@@ -12,8 +12,9 @@
const std::regex illegalDbusRegex("[^A-Za-z0-9_]");
using BasicVariantType =
- sdbusplus::message::variant<std::string, int64_t, uint64_t, double, int32_t,
- uint32_t, int16_t, uint16_t, uint8_t, bool>;
+ sdbusplus::message::variant<std::vector<std::string>, std::string, int64_t,
+ uint64_t, double, int32_t, uint32_t, int16_t,
+ uint16_t, uint8_t, bool>;
using ManagedObjectType = boost::container::flat_map<
sdbusplus::message::object_path,
diff --git a/src/ExitAirTempSensor.cpp b/src/ExitAirTempSensor.cpp
new file mode 100644
index 0000000..c9066ad
--- /dev/null
+++ b/src/ExitAirTempSensor.cpp
@@ -0,0 +1,521 @@
+/*
+// Copyright (c) 2018 Intel 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 "ExitAirTempSensor.hpp"
+
+#include "Utils.hpp"
+#include "VariantVisitors.hpp"
+
+#include <math.h>
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <chrono>
+#include <iostream>
+#include <limits>
+#include <numeric>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <vector>
+
+constexpr const float altitudeFactor = 1.14;
+constexpr const char* exitAirIface =
+ "xyz.openbmc_project.Configuration.ExitAirTempSensor";
+constexpr const char* cfmIface = "xyz.openbmc_project.Configuration.CFMSensor";
+
+// todo: this *might* need to be configurable
+constexpr const char* inletTemperatureSensor = "temperature/Front_Panel_Temp";
+static constexpr double maxReading = 127;
+static constexpr double minReading = -128;
+
+static constexpr bool DEBUG = false;
+
+ExitAirTempSensor::ExitAirTempSensor(
+ std::shared_ptr<sdbusplus::asio::connection>& conn,
+ const std::string& sensorConfiguration,
+ sdbusplus::asio::object_server& objectServer,
+ std::vector<thresholds::Threshold>&& thresholds) :
+ Sensor("Exit_Air_Temperature", "" /* todo: remove arg from base*/,
+ std::move(thresholds), sensorConfiguration,
+ "xyz.openbmc_project.Configuration.ExitAirTemp", maxReading,
+ minReading),
+ dbusConnection(conn)
+{
+ sensorInterface = objectServer.add_interface(
+ "/xyz/openbmc_project/sensors/temperature/" + name,
+ "xyz.openbmc_project.Sensor.Value");
+
+ if (thresholds::hasWarningInterface(thresholds))
+ {
+ thresholdInterfaceWarning = objectServer.add_interface(
+ "/xyz/openbmc_project/sensors/temperature/" + name,
+ "xyz.openbmc_project.Sensor.Threshold.Warning");
+ }
+ if (thresholds::hasCriticalInterface(thresholds))
+ {
+ thresholdInterfaceCritical = objectServer.add_interface(
+ "/xyz/openbmc_project/sensors/temperature/" + name,
+ "xyz.openbmc_project.Sensor.Threshold.Critical");
+ }
+ setInitialProperties(conn);
+ setupMatches();
+}
+
+ExitAirTempSensor::~ExitAirTempSensor()
+{
+ // this sensor currently isn't destroyed so we don't care
+}
+
+void ExitAirTempSensor::setupMatches(void)
+{
+
+ constexpr const std::array<const char*, 3> matchTypes = {
+ "power", "fan_pwm", inletTemperatureSensor};
+
+ for (const auto& type : matchTypes)
+ {
+ std::function<void(sdbusplus::message::message & message)>
+ eventHandler = [this, type](sdbusplus::message::message& message) {
+ std::string objectName;
+ boost::container::flat_map<
+ std::string, sdbusplus::message::variant<double, int64_t>>
+ values;
+ message.read(objectName, values);
+ auto findValue = values.find("Value");
+ if (findValue == values.end())
+ {
+ return;
+ }
+ double value = sdbusplus::message::variant_ns::visit(
+ VariantToDoubleVisitor(), findValue->second);
+ if (type == "power")
+ {
+ powerReadings[message.get_path()] = value;
+ }
+ else if (type == inletTemperatureSensor)
+ {
+ inletTemp = value;
+ }
+ else if (type == "fan_pwm")
+ {
+ pwmReadings[message.get_path()] = value;
+ }
+ updateReading();
+ };
+ matches.emplace_back(static_cast<sdbusplus::bus::bus&>(*dbusConnection),
+ "type='signal',"
+ "member='PropertiesChanged',interface='org."
+ "freedesktop.DBus.Properties',path_"
+ "namespace='/xyz/openbmc_project/sensors/" +
+ std::string(type) +
+ "',arg0='xyz.openbmc_project.Sensor.Value'",
+ std::move(eventHandler));
+ }
+}
+
+void ExitAirTempSensor::updateReading(void)
+{
+
+ double val = 0.0;
+ if (calculate(val))
+ {
+ updateValue(val);
+ }
+ else
+ {
+ updateValue(std::numeric_limits<double>::quiet_NaN());
+ }
+}
+
+// todo: break this out into it's own sensor
+int32_t ExitAirTempSensor::getTotalCFM(void)
+{
+ int32_t totalCFM = 0;
+ // todo: rpm instead of pwm
+ for (const auto& zone : cfmData)
+ {
+ if (zone.pwm.empty())
+ {
+ std::cerr << "CFM without PWM";
+ return -1;
+ }
+
+ const std::string& firstName = zone.pwm[0];
+
+ auto findPwm = std::find_if(
+ pwmReadings.begin(), pwmReadings.end(), [&](const auto& item) {
+ return boost::ends_with(item.first, firstName);
+ });
+ if (findPwm == pwmReadings.end())
+ {
+ std::cerr << "Can't find " << firstName << "in readings\n";
+ return -1; // haven't gotten a reading
+ }
+
+ double pwm = findPwm->second;
+ if constexpr (DEBUG)
+ {
+ std::cout << "Pwm " << firstName << "at " << pwm << "\n";
+ }
+
+ // Do a linear interpolation to get Ci
+ // Ci = C1 + (C2 - C1)/(PWM2 - PWM1) * (PWMi - PWM1)
+
+ int32_t ci = 0;
+ if (pwm == 0)
+ {
+ ci = 0;
+ }
+ else if (pwm < zone.pwmMin)
+ {
+ ci = zone.c1;
+ }
+ else if (pwm > zone.pwmMax)
+ {
+ ci = zone.c2;
+ }
+ else
+ {
+ ci = zone.c1 +
+ (((zone.c2 - zone.c1) * (pwm - (int32_t)zone.pwmMin)) /
+ ((int32_t)zone.pwmMax - (int32_t)zone.pwmMin));
+ }
+
+ // Now calculate the CFM for this domain
+ // CFMi = Ci * QTYi * Qmaxi * PWMi
+ totalCFM += ci * zone.pwm.size() * zone.maxCFM * pwm;
+ }
+ if constexpr (DEBUG)
+ {
+ std::cout << totalCFM / 100 << " CFM\n";
+ }
+ return totalCFM /= 100; // divide by 100 since PWM is in percent
+}
+
+bool ExitAirTempSensor::calculate(double& val)
+{
+ static bool firstRead = false;
+ double cfm = getTotalCFM();
+ if (cfm <= 0)
+ {
+ std::cerr << "Error getting cfm\n";
+ return false;
+ }
+
+ // if there is an error getting inlet temp, return error
+ if (std::isnan(inletTemp))
+ {
+ std::cerr << "Cannot get inlet temp\n";
+ val = 0;
+ return false;
+ }
+
+ // if fans are off, just make the exit temp equal to inlet
+ if (!isPowerOn(dbusConnection))
+ {
+ val = inletTemp;
+ return true;
+ }
+
+ double totalPower = 0;
+ for (const auto& reading : powerReadings)
+ {
+ if (std::isnan(reading.second))
+ {
+ continue;
+ }
+ totalPower += reading.second;
+ }
+
+ // Calculate power correction factor
+ // Ci = CL + (CH - CL)/(QMax - QMin) * (CFM - QMin)
+ float powerFactor = 0.0;
+ if (cfm <= qMin)
+ {
+ powerFactor = powerFactorMin;
+ }
+ else if (cfm >= qMax)
+ {
+ powerFactor = powerFactorMax;
+ }
+ else
+ {
+ powerFactor = powerFactorMin + ((powerFactorMax - powerFactorMin) /
+ (qMax - qMin) * (cfm - qMin));
+ }
+
+ totalPower *= powerFactor;
+ totalPower += pOffset;
+
+ if (totalPower == 0)
+ {
+ std::cerr << "total power 0\n";
+ val = 0;
+ return false;
+ }
+
+ if constexpr (DEBUG)
+ {
+ std::cout << "Power Factor " << powerFactor << "\n";
+ std::cout << "Inlet Temp " << inletTemp << "\n";
+ std::cout << "Total Power" << totalPower << "\n";
+ }
+
+ // Calculate the exit air temp
+ // Texit = Tfp + (1.76 * TotalPower / CFM * Faltitude)
+ double reading = 1.76 * totalPower * altitudeFactor;
+ reading /= cfm;
+ reading += inletTemp;
+
+ if constexpr (DEBUG)
+ {
+ std::cout << "Reading 1: " << reading << "\n";
+ }
+
+ // Now perform the exponential average
+ // Calculate alpha based on SDR values and CFM
+ // Ai = As + (Af - As)/(QMax - QMin) * (CFM - QMin)
+
+ double alpha = 0.0;
+ if (cfm < qMin)
+ {
+ alpha = alphaS;
+ }
+ else if (cfm >= qMax)
+ {
+ alpha = alphaF;
+ }
+ else
+ {
+ alpha = alphaS + ((alphaF - alphaS) * (cfm - qMin) / (qMax - qMin));
+ }
+
+ auto time = std::chrono::system_clock::now();
+ if (!firstRead)
+ {
+ firstRead = true;
+ lastTime = time;
+ lastReading = reading;
+ }
+ double alphaDT =
+ std::chrono::duration_cast<std::chrono::seconds>(time - lastTime)
+ .count() *
+ alpha;
+
+ // cap at 1.0 or the below fails
+ if (alphaDT > 1.0)
+ {
+ alphaDT = 1.0;
+ }
+
+ if constexpr (DEBUG)
+ {
+ std::cout << "AlphaDT: " << alphaDT << "\n";
+ }
+
+ reading = ((reading * alphaDT) + (lastReading * (1.0 - alphaDT)));
+
+ if constexpr (DEBUG)
+ {
+ std::cout << "Reading 2: " << reading << "\n";
+ }
+
+ val = reading;
+ lastReading = reading;
+ lastTime = time;
+ return true;
+}
+
+void ExitAirTempSensor::checkThresholds(void)
+{
+ thresholds::checkThresholds(this);
+}
+
+static double loadVariantDouble(
+ const boost::container::flat_map<std::string, BasicVariantType>& data,
+ const std::string& key)
+{
+ auto it = data.find(key);
+ if (it == data.end())
+ {
+ std::cerr << "Configuration missing " << key << "\n";
+ throw std::invalid_argument("Key Missing");
+ }
+ return sdbusplus::message::variant_ns::visit(VariantToDoubleVisitor(),
+ it->second);
+}
+
+static void loadVariantPathArray(
+ const boost::container::flat_map<std::string, BasicVariantType>& data,
+ const std::string& key, std::vector<std::string>& resp)
+{
+ auto it = data.find(key);
+ if (it == data.end())
+ {
+ std::cerr << "Configuration missing " << key << "\n";
+ throw std::invalid_argument("Key Missing");
+ }
+ BasicVariantType copy = it->second;
+ std::vector<std::string> config =
+ sdbusplus::message::variant_ns::get<std::vector<std::string>>(copy);
+ for (auto& str : config)
+ {
+ boost::replace_all(str, " ", "_");
+ }
+ resp = std::move(config);
+}
+
+void createSensor(sdbusplus::asio::object_server& objectServer,
+ std::shared_ptr<ExitAirTempSensor>& sensor,
+ std::shared_ptr<sdbusplus::asio::connection>& dbusConnection)
+{
+ if (!dbusConnection)
+ {
+ std::cerr << "Connection not created\n";
+ return;
+ }
+ dbusConnection->async_method_call(
+ [&](boost::system::error_code ec, const ManagedObjectType& resp) {
+ if (ec)
+ {
+ std::cerr << "Error contacting entity manager\n";
+ return;
+ }
+
+ std::vector<CFMInfo> cfmData;
+ bool foundExitAir = false;
+ for (const auto& pathPair : resp)
+ {
+ for (const auto& entry : pathPair.second)
+ {
+ if (entry.first == exitAirIface)
+ {
+ if (foundExitAir)
+ {
+ // Something is very wrong
+ std::cerr << "More than one exit air configuration "
+ "found\n";
+ std::exit(EXIT_FAILURE);
+ }
+ foundExitAir = true;
+
+ // thresholds should be under the same path
+ std::vector<thresholds::Threshold> sensorThresholds;
+ parseThresholdsFromConfig(pathPair.second,
+ sensorThresholds);
+ if (!sensor)
+ {
+ sensor = std::make_shared<ExitAirTempSensor>(
+ dbusConnection, pathPair.first.str,
+ objectServer, std::move(sensorThresholds));
+ }
+ else
+ {
+ sensor->thresholds = sensorThresholds;
+ }
+
+ sensor->powerFactorMin =
+ loadVariantDouble(entry.second, "PowerFactorMin");
+ sensor->powerFactorMax =
+ loadVariantDouble(entry.second, "PowerFactorMax");
+ sensor->qMin = loadVariantDouble(entry.second, "QMin");
+ sensor->qMax = loadVariantDouble(entry.second, "QMax");
+ sensor->alphaS =
+ loadVariantDouble(entry.second, "AlphaS");
+ sensor->alphaF =
+ loadVariantDouble(entry.second, "AlphaF");
+ }
+ else if (entry.first == cfmIface)
+
+ {
+
+ CFMInfo cfm;
+ loadVariantPathArray(entry.second, "Pwms", cfm.pwm);
+ cfm.maxCFM = loadVariantDouble(entry.second, "MaxCFM");
+
+ // change these into percent upon getting the data
+ cfm.c1 = loadVariantDouble(entry.second, "C1") / 100;
+ cfm.c2 = loadVariantDouble(entry.second, "C2") / 100;
+ cfm.pwmMin =
+ loadVariantDouble(entry.second, "PwmMinPercent") /
+ 100;
+ cfm.pwmMax =
+ loadVariantDouble(entry.second, "PwmMaxPercent") /
+ 100;
+
+ cfmData.emplace_back(cfm);
+ }
+ }
+ }
+ if (sensor)
+ {
+ sensor->cfmData = std::move(cfmData);
+
+ // todo: when power sensors are done delete this fake reading
+ sensor->powerReadings["foo"] = 144.0;
+
+ sensor->updateReading();
+ }
+ },
+ entityManagerName, "/", "org.freedesktop.DBus.ObjectManager",
+ "GetManagedObjects");
+}
+
+int main(int argc, char** argv)
+{
+
+ boost::asio::io_service io;
+ auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
+ systemBus->request_name("xyz.openbmc_project.ExitAirTempSensor");
+ sdbusplus::asio::object_server objectServer(systemBus);
+ std::shared_ptr<ExitAirTempSensor> sensor =
+ nullptr; // wait until we find the config
+ std::vector<std::unique_ptr<sdbusplus::bus::match::match>> matches;
+
+ io.post([&]() { createSensor(objectServer, sensor, systemBus); });
+
+ boost::asio::deadline_timer configTimer(io);
+
+ std::function<void(sdbusplus::message::message&)> eventHandler =
+ [&](sdbusplus::message::message& message) {
+ configTimer.expires_from_now(boost::posix_time::seconds(1));
+ // create a timer because normally multiple properties change
+ configTimer.async_wait([&](const boost::system::error_code& ec) {
+ if (ec == boost::asio::error::operation_aborted)
+ {
+ return; // we're being canceled
+ }
+ createSensor(objectServer, sensor, systemBus);
+ if (!sensor)
+ {
+ std::cout << "Configuration not detected\n";
+ }
+ });
+ };
+ constexpr const std::array<const char*, 2> monitorIfaces = {exitAirIface,
+ cfmIface};
+ for (const char* type : monitorIfaces)
+ {
+ auto match = std::make_unique<sdbusplus::bus::match::match>(
+ static_cast<sdbusplus::bus::bus&>(*systemBus),
+ "type='signal',member='PropertiesChanged',path_namespace='" +
+ std::string(inventoryPath) + "',arg0namespace='" + type + "'",
+ eventHandler);
+ matches.emplace_back(std::move(match));
+ }
+
+ io.run();
+}