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();
+}