Move source files into application-specific sub-directories

Currently, dbus-sensors implement multiple applications:
 - psusensor
 - adcsensor
 - intelcpusensor
 - hwmontempsensor
 - ipmbsensor
 - nvmesensor
 - externalsensor
 - mcutempsensor
 - intrusionsensor
 - fansensor
 - exitairtempsensor

This commit is to create separate directories for each application so
that things can be separated more easily and the files are smaller,
instead of creating one huge file for the sensor implementation.

There was some discussion in discord on this. [1][2]

[1]: https://discord.com/channels/775381525260664832/1187158775438778408/1284106093756289067
[2]: https://discord.com/channels/775381525260664832/867820390406422538/1303217796821553214

Signed-off-by: George Liu <liuxiwei@ieisystem.com>
Change-Id: I258fc2ee7d8f939c7b83a07350395e78775b2b8d
diff --git a/src/exit-air/ExitAirTempSensor.cpp b/src/exit-air/ExitAirTempSensor.cpp
new file mode 100644
index 0000000..bd8960b
--- /dev/null
+++ b/src/exit-air/ExitAirTempSensor.cpp
@@ -0,0 +1,986 @@
+/*
+// 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 "SensorPaths.hpp"
+#include "Thresholds.hpp"
+#include "Utils.hpp"
+#include "VariantVisitors.hpp"
+#include "sensor.hpp"
+
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/asio/error.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/post.hpp>
+#include <boost/asio/steady_timer.hpp>
+#include <boost/container/flat_map.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <sdbusplus/message.hpp>
+
+#include <algorithm>
+#include <array>
+#include <chrono>
+#include <cmath>
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+#include <iostream>
+#include <limits>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <utility>
+#include <variant>
+#include <vector>
+
+constexpr const double altitudeFactor = 1.14;
+constexpr const char* exitAirType = "ExitAirTempSensor";
+constexpr const char* cfmType = "CFMSensor";
+
+// todo: this *might* need to be configurable
+constexpr const char* inletTemperatureSensor = "temperature/Front_Panel_Temp";
+constexpr const char* pidConfigurationType =
+    "xyz.openbmc_project.Configuration.Pid";
+constexpr const char* settingsDaemon = "xyz.openbmc_project.Settings";
+constexpr const char* cfmSettingPath = "/xyz/openbmc_project/control/cfm_limit";
+constexpr const char* cfmSettingIface = "xyz.openbmc_project.Control.CFMLimit";
+
+static constexpr bool debug = false;
+
+static constexpr double cfmMaxReading = 255;
+static constexpr double cfmMinReading = 0;
+
+static constexpr size_t minSystemCfm = 50;
+
+constexpr const auto monitorTypes{
+    std::to_array<const char*>({exitAirType, cfmType})};
+
+static std::vector<std::shared_ptr<CFMSensor>> cfmSensors;
+
+static void setupSensorMatch(
+    std::vector<sdbusplus::bus::match_t>& matches, sdbusplus::bus_t& connection,
+    const std::string& type,
+    std::function<void(const double&, sdbusplus::message_t&)>&& callback)
+{
+    std::function<void(sdbusplus::message_t & message)> eventHandler =
+        [callback{std::move(callback)}](sdbusplus::message_t& message) {
+            std::string objectName;
+            boost::container::flat_map<std::string,
+                                       std::variant<double, int64_t>>
+                values;
+            message.read(objectName, values);
+            auto findValue = values.find("Value");
+            if (findValue == values.end())
+            {
+                return;
+            }
+            double value =
+                std::visit(VariantToDoubleVisitor(), findValue->second);
+            if (std::isnan(value))
+            {
+                return;
+            }
+
+            callback(value, message);
+        };
+    matches.emplace_back(
+        connection,
+        "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));
+}
+
+static void setMaxPWM(const std::shared_ptr<sdbusplus::asio::connection>& conn,
+                      double value)
+{
+    using GetSubTreeType = std::vector<std::pair<
+        std::string,
+        std::vector<std::pair<std::string, std::vector<std::string>>>>>;
+
+    conn->async_method_call(
+        [conn,
+         value](const boost::system::error_code ec, const GetSubTreeType& ret) {
+            if (ec)
+            {
+                std::cerr << "Error calling mapper\n";
+                return;
+            }
+            for (const auto& [path, objDict] : ret)
+            {
+                if (objDict.empty())
+                {
+                    return;
+                }
+                const std::string& owner = objDict.begin()->first;
+
+                conn->async_method_call(
+                    [conn, value, owner,
+                     path{path}](const boost::system::error_code ec,
+                                 const std::variant<std::string>& classType) {
+                        if (ec)
+                        {
+                            std::cerr << "Error getting pid class\n";
+                            return;
+                        }
+                        const auto* classStr =
+                            std::get_if<std::string>(&classType);
+                        if (classStr == nullptr || *classStr != "fan")
+                        {
+                            return;
+                        }
+                        conn->async_method_call(
+                            [](boost::system::error_code& ec) {
+                                if (ec)
+                                {
+                                    std::cerr << "Error setting pid class\n";
+                                    return;
+                                }
+                            },
+                            owner, path, "org.freedesktop.DBus.Properties",
+                            "Set", pidConfigurationType, "OutLimitMax",
+                            std::variant<double>(value));
+                    },
+                    owner, path, "org.freedesktop.DBus.Properties", "Get",
+                    pidConfigurationType, "Class");
+            }
+        },
+        mapper::busName, mapper::path, mapper::interface, mapper::subtree, "/",
+        0, std::array<std::string, 1>{pidConfigurationType});
+}
+
+CFMSensor::CFMSensor(std::shared_ptr<sdbusplus::asio::connection>& conn,
+                     const std::string& sensorName,
+                     const std::string& sensorConfiguration,
+                     sdbusplus::asio::object_server& objectServer,
+                     std::vector<thresholds::Threshold>&& thresholdData,
+                     std::shared_ptr<ExitAirTempSensor>& parent) :
+    Sensor(escapeName(sensorName), std::move(thresholdData),
+           sensorConfiguration, "CFMSensor", false, false, cfmMaxReading,
+           cfmMinReading, conn, PowerState::on),
+    parent(parent), objServer(objectServer)
+{
+    sensorInterface = objectServer.add_interface(
+        "/xyz/openbmc_project/sensors/airflow/" + name,
+        "xyz.openbmc_project.Sensor.Value");
+
+    for (const auto& threshold : thresholds)
+    {
+        std::string interface = thresholds::getInterface(threshold.level);
+        thresholdInterfaces[static_cast<size_t>(threshold.level)] =
+            objectServer.add_interface(
+                "/xyz/openbmc_project/sensors/airflow/" + name, interface);
+    }
+
+    association = objectServer.add_interface(
+        "/xyz/openbmc_project/sensors/airflow/" + name, association::interface);
+
+    setInitialProperties(sensor_paths::unitCFM);
+
+    pwmLimitIface =
+        objectServer.add_interface("/xyz/openbmc_project/control/pwm_limit",
+                                   "xyz.openbmc_project.Control.PWMLimit");
+    cfmLimitIface =
+        objectServer.add_interface("/xyz/openbmc_project/control/MaxCFM",
+                                   "xyz.openbmc_project.Control.CFMLimit");
+}
+
+void CFMSensor::setupMatches()
+{
+    std::weak_ptr<CFMSensor> weakRef = weak_from_this();
+    setupSensorMatch(
+        matches, *dbusConnection, "fan_tach",
+        [weakRef](const double& value, sdbusplus::message_t& message) {
+            auto self = weakRef.lock();
+            if (!self)
+            {
+                return;
+            }
+            self->tachReadings[message.get_path()] = value;
+            if (self->tachRanges.find(message.get_path()) ==
+                self->tachRanges.end())
+            {
+                // calls update reading after updating ranges
+                self->addTachRanges(message.get_sender(), message.get_path());
+            }
+            else
+            {
+                self->updateReading();
+            }
+        });
+
+    dbusConnection->async_method_call(
+        [weakRef](const boost::system::error_code ec,
+                  const std::variant<double> cfmVariant) {
+            auto self = weakRef.lock();
+            if (!self)
+            {
+                return;
+            }
+
+            uint64_t maxRpm = 100;
+            if (!ec)
+            {
+                const auto* cfm = std::get_if<double>(&cfmVariant);
+                if (cfm != nullptr && *cfm >= minSystemCfm)
+                {
+                    maxRpm = self->getMaxRpm(*cfm);
+                }
+            }
+            self->pwmLimitIface->register_property("Limit", maxRpm);
+            self->pwmLimitIface->initialize();
+            setMaxPWM(self->dbusConnection, maxRpm);
+        },
+        settingsDaemon, cfmSettingPath, "org.freedesktop.DBus.Properties",
+        "Get", cfmSettingIface, "Limit");
+
+    matches.emplace_back(
+        *dbusConnection,
+        "type='signal',"
+        "member='PropertiesChanged',interface='org."
+        "freedesktop.DBus.Properties',path='" +
+            std::string(cfmSettingPath) + "',arg0='" +
+            std::string(cfmSettingIface) + "'",
+        [weakRef](sdbusplus::message_t& message) {
+            auto self = weakRef.lock();
+            if (!self)
+            {
+                return;
+            }
+            boost::container::flat_map<std::string, std::variant<double>>
+                values;
+            std::string objectName;
+            message.read(objectName, values);
+            const auto findValue = values.find("Limit");
+            if (findValue == values.end())
+            {
+                return;
+            }
+            auto* const reading = std::get_if<double>(&(findValue->second));
+            if (reading == nullptr)
+            {
+                std::cerr << "Got CFM Limit of wrong type\n";
+                return;
+            }
+            if (*reading < minSystemCfm && *reading != 0)
+            {
+                std::cerr << "Illegal CFM setting detected\n";
+                return;
+            }
+            uint64_t maxRpm = self->getMaxRpm(*reading);
+            self->pwmLimitIface->set_property("Limit", maxRpm);
+            setMaxPWM(self->dbusConnection, maxRpm);
+        });
+}
+
+CFMSensor::~CFMSensor()
+{
+    for (const auto& iface : thresholdInterfaces)
+    {
+        objServer.remove_interface(iface);
+    }
+    objServer.remove_interface(sensorInterface);
+    objServer.remove_interface(association);
+    objServer.remove_interface(cfmLimitIface);
+    objServer.remove_interface(pwmLimitIface);
+}
+
+void CFMSensor::createMaxCFMIface()
+{
+    cfmLimitIface->register_property("Limit", c2 * maxCFM * tachs.size());
+    cfmLimitIface->initialize();
+}
+
+void CFMSensor::addTachRanges(const std::string& serviceName,
+                              const std::string& path)
+{
+    std::weak_ptr<CFMSensor> weakRef = weak_from_this();
+    dbusConnection->async_method_call(
+        [weakRef, path](const boost::system::error_code ec,
+                        const SensorBaseConfigMap& data) {
+            if (ec)
+            {
+                std::cerr << "Error getting properties from " << path << "\n";
+                return;
+            }
+            auto self = weakRef.lock();
+            if (!self)
+            {
+                return;
+            }
+            double max = loadVariant<double>(data, "MaxValue");
+            double min = loadVariant<double>(data, "MinValue");
+            self->tachRanges[path] = std::make_pair(min, max);
+            self->updateReading();
+        },
+        serviceName, path, "org.freedesktop.DBus.Properties", "GetAll",
+        "xyz.openbmc_project.Sensor.Value");
+}
+
+void CFMSensor::checkThresholds()
+{
+    thresholds::checkThresholds(this);
+}
+
+void CFMSensor::updateReading()
+{
+    double val = 0.0;
+    if (calculate(val))
+    {
+        if (value != val && parent)
+        {
+            parent->updateReading();
+        }
+        updateValue(val);
+    }
+    else
+    {
+        updateValue(std::numeric_limits<double>::quiet_NaN());
+    }
+}
+
+uint64_t CFMSensor::getMaxRpm(uint64_t cfmMaxSetting) const
+{
+    uint64_t pwmPercent = 100;
+    double totalCFM = std::numeric_limits<double>::max();
+    if (cfmMaxSetting == 0)
+    {
+        return pwmPercent;
+    }
+
+    bool firstLoop = true;
+    while (totalCFM > cfmMaxSetting)
+    {
+        if (firstLoop)
+        {
+            firstLoop = false;
+        }
+        else
+        {
+            pwmPercent--;
+        }
+
+        double ci = 0;
+        if (pwmPercent == 0)
+        {
+            ci = 0;
+        }
+        else if (pwmPercent < tachMinPercent)
+        {
+            ci = c1;
+        }
+        else if (pwmPercent > tachMaxPercent)
+        {
+            ci = c2;
+        }
+        else
+        {
+            ci = c1 + (((c2 - c1) * (pwmPercent - tachMinPercent)) /
+                       (tachMaxPercent - tachMinPercent));
+        }
+
+        // Now calculate the CFM for this tach
+        // CFMi = Ci * Qmaxi * TACHi
+        totalCFM = ci * maxCFM * pwmPercent;
+        totalCFM *= tachs.size();
+        // divide by 100 since pwm is in percent
+        totalCFM /= 100;
+
+        if (pwmPercent <= 0)
+        {
+            break;
+        }
+    }
+
+    return pwmPercent;
+}
+
+bool CFMSensor::calculate(double& value)
+{
+    double totalCFM = 0;
+    for (const std::string& tachName : tachs)
+    {
+        auto findReading = std::find_if(
+            tachReadings.begin(), tachReadings.end(),
+            [&](const auto& item) { return item.first.ends_with(tachName); });
+        auto findRange = std::find_if(
+            tachRanges.begin(), tachRanges.end(),
+            [&](const auto& item) { return item.first.ends_with(tachName); });
+        if (findReading == tachReadings.end())
+        {
+            if constexpr (debug)
+            {
+                std::cerr << "Can't find " << tachName << "in readings\n";
+            }
+            continue; // haven't gotten a reading
+        }
+
+        if (findRange == tachRanges.end())
+        {
+            std::cerr << "Can't find " << tachName << " in ranges\n";
+            return false; // haven't gotten a max / min
+        }
+
+        // avoid divide by 0
+        if (findRange->second.second == 0)
+        {
+            std::cerr << "Tach Max Set to 0 " << tachName << "\n";
+            return false;
+        }
+
+        double rpm = findReading->second;
+
+        // for now assume the min for a fan is always 0, divide by max to get
+        // percent and mult by 100
+        rpm /= findRange->second.second;
+        rpm *= 100;
+
+        if constexpr (debug)
+        {
+            std::cout << "Tach " << tachName << "at " << rpm << "\n";
+        }
+
+        // Do a linear interpolation to get Ci
+        // Ci = C1 + (C2 - C1)/(RPM2 - RPM1) * (TACHi - TACH1)
+
+        double ci = 0;
+        if (rpm == 0)
+        {
+            ci = 0;
+        }
+        else if (rpm < tachMinPercent)
+        {
+            ci = c1;
+        }
+        else if (rpm > tachMaxPercent)
+        {
+            ci = c2;
+        }
+        else
+        {
+            ci = c1 + (((c2 - c1) * (rpm - tachMinPercent)) /
+                       (tachMaxPercent - tachMinPercent));
+        }
+
+        // Now calculate the CFM for this tach
+        // CFMi = Ci * Qmaxi * TACHi
+        totalCFM += ci * maxCFM * rpm;
+        if constexpr (debug)
+        {
+            std::cerr << "totalCFM = " << totalCFM << "\n";
+            std::cerr << "Ci " << ci << " MaxCFM " << maxCFM << " rpm " << rpm
+                      << "\n";
+            std::cerr << "c1 " << c1 << " c2 " << c2 << " max "
+                      << tachMaxPercent << " min " << tachMinPercent << "\n";
+        }
+    }
+
+    // divide by 100 since rpm is in percent
+    value = totalCFM / 100;
+    if constexpr (debug)
+    {
+        std::cerr << "cfm value = " << value << "\n";
+    }
+    return true;
+}
+
+static constexpr double exitAirMaxReading = 127;
+static constexpr double exitAirMinReading = -128;
+ExitAirTempSensor::ExitAirTempSensor(
+    std::shared_ptr<sdbusplus::asio::connection>& conn,
+    const std::string& sensorName, const std::string& sensorConfiguration,
+    sdbusplus::asio::object_server& objectServer,
+    std::vector<thresholds::Threshold>&& thresholdData) :
+    Sensor(escapeName(sensorName), std::move(thresholdData),
+           sensorConfiguration, "ExitAirTemp", false, false, exitAirMaxReading,
+           exitAirMinReading, conn, PowerState::on),
+    objServer(objectServer)
+{
+    sensorInterface = objectServer.add_interface(
+        "/xyz/openbmc_project/sensors/temperature/" + name,
+        "xyz.openbmc_project.Sensor.Value");
+
+    for (const auto& threshold : thresholds)
+    {
+        std::string interface = thresholds::getInterface(threshold.level);
+        thresholdInterfaces[static_cast<size_t>(threshold.level)] =
+            objectServer.add_interface(
+                "/xyz/openbmc_project/sensors/temperature/" + name, interface);
+    }
+    association = objectServer.add_interface(
+        "/xyz/openbmc_project/sensors/temperature/" + name,
+        association::interface);
+    setInitialProperties(sensor_paths::unitDegreesC);
+}
+
+ExitAirTempSensor::~ExitAirTempSensor()
+{
+    for (const auto& iface : thresholdInterfaces)
+    {
+        objServer.remove_interface(iface);
+    }
+    objServer.remove_interface(sensorInterface);
+    objServer.remove_interface(association);
+}
+
+void ExitAirTempSensor::setupMatches()
+{
+    constexpr const auto matchTypes{
+        std::to_array<const char*>({"power", inletTemperatureSensor})};
+
+    std::weak_ptr<ExitAirTempSensor> weakRef = weak_from_this();
+    for (const std::string type : matchTypes)
+    {
+        setupSensorMatch(
+            matches, *dbusConnection, type,
+            [weakRef,
+             type](const double& value, sdbusplus::message_t& message) {
+                auto self = weakRef.lock();
+                if (!self)
+                {
+                    return;
+                }
+                if (type == "power")
+                {
+                    std::string path = message.get_path();
+                    if (path.find("PS") != std::string::npos &&
+                        path.ends_with("Input_Power"))
+                    {
+                        self->powerReadings[message.get_path()] = value;
+                    }
+                }
+                else if (type == inletTemperatureSensor)
+                {
+                    self->inletTemp = value;
+                }
+                self->updateReading();
+            });
+    }
+    dbusConnection->async_method_call(
+        [weakRef](boost::system::error_code ec,
+                  const std::variant<double>& value) {
+            if (ec)
+            {
+                // sensor not ready yet
+                return;
+            }
+            auto self = weakRef.lock();
+            if (!self)
+            {
+                return;
+            }
+            self->inletTemp = std::visit(VariantToDoubleVisitor(), value);
+        },
+        "xyz.openbmc_project.HwmonTempSensor",
+        std::string("/xyz/openbmc_project/sensors/") + inletTemperatureSensor,
+        properties::interface, properties::get, sensorValueInterface, "Value");
+    dbusConnection->async_method_call(
+        [weakRef](boost::system::error_code ec, const GetSubTreeType& subtree) {
+            if (ec)
+            {
+                std::cerr << "Error contacting mapper\n";
+                return;
+            }
+            auto self = weakRef.lock();
+            if (!self)
+            {
+                return;
+            }
+            for (const auto& [path, matches] : subtree)
+            {
+                size_t lastSlash = path.rfind('/');
+                if (lastSlash == std::string::npos ||
+                    lastSlash == path.size() || matches.empty())
+                {
+                    continue;
+                }
+                std::string sensorName = path.substr(lastSlash + 1);
+                if (sensorName.starts_with("PS") &&
+                    sensorName.ends_with("Input_Power"))
+                {
+                    // lambda capture requires a proper variable (not a
+                    // structured binding)
+                    const std::string& cbPath = path;
+                    self->dbusConnection->async_method_call(
+                        [weakRef, cbPath](boost::system::error_code ec,
+                                          const std::variant<double>& value) {
+                            if (ec)
+                            {
+                                std::cerr << "Error getting value from "
+                                          << cbPath << "\n";
+                            }
+                            auto self = weakRef.lock();
+                            if (!self)
+                            {
+                                return;
+                            }
+                            double reading =
+                                std::visit(VariantToDoubleVisitor(), value);
+                            if constexpr (debug)
+                            {
+                                std::cerr
+                                    << cbPath << "Reading " << reading << "\n";
+                            }
+                            self->powerReadings[cbPath] = reading;
+                        },
+                        matches[0].first, cbPath, properties::interface,
+                        properties::get, sensorValueInterface, "Value");
+                }
+            }
+        },
+        mapper::busName, mapper::path, mapper::interface, mapper::subtree,
+        "/xyz/openbmc_project/sensors/power", 0,
+        std::array<const char*, 1>{sensorValueInterface});
+}
+
+void ExitAirTempSensor::updateReading()
+{
+    double val = 0.0;
+    if (calculate(val))
+    {
+        val = std::floor(val + 0.5);
+        updateValue(val);
+    }
+    else
+    {
+        updateValue(std::numeric_limits<double>::quiet_NaN());
+    }
+}
+
+double ExitAirTempSensor::getTotalCFM()
+{
+    double sum = 0;
+    for (auto& sensor : cfmSensors)
+    {
+        double reading = 0;
+        if (!sensor->calculate(reading))
+        {
+            return -1;
+        }
+        sum += reading;
+    }
+
+    return sum;
+}
+
+bool ExitAirTempSensor::calculate(double& val)
+{
+    constexpr size_t maxErrorPrint = 5;
+    static bool firstRead = false;
+    static size_t errorPrint = maxErrorPrint;
+
+    double cfm = getTotalCFM();
+    if (cfm <= 0)
+    {
+        std::cerr << "Error getting cfm\n";
+        return false;
+    }
+
+    // Though cfm is not expected to be less than qMin normally,
+    // it is not a hard limit for exit air temp calculation.
+    // 50% qMin is chosen as a generic limit between providing
+    // a valid derived exit air temp and reporting exit air temp not available.
+    constexpr const double cfmLimitFactor = 0.5;
+    if (cfm < (qMin * cfmLimitFactor))
+    {
+        if (errorPrint > 0)
+        {
+            errorPrint--;
+            std::cerr << "cfm " << cfm << " is too low, expected qMin " << qMin
+                      << "\n";
+        }
+        val = 0;
+        return false;
+    }
+
+    // if there is an error getting inlet temp, return error
+    if (std::isnan(inletTemp))
+    {
+        if (errorPrint > 0)
+        {
+            errorPrint--;
+            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())
+    {
+        val = inletTemp;
+        return true;
+    }
+
+    double totalPower = 0;
+    for (const auto& [path, reading] : powerReadings)
+    {
+        if (std::isnan(reading))
+        {
+            continue;
+        }
+        totalPower += reading;
+    }
+
+    // Calculate power correction factor
+    // Ci = CL + (CH - CL)/(QMax - QMin) * (CFM - QMin)
+    double 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)
+    {
+        if (errorPrint > 0)
+        {
+            errorPrint--;
+            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::steady_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;
+    errorPrint = maxErrorPrint;
+    return true;
+}
+
+void ExitAirTempSensor::checkThresholds()
+{
+    thresholds::checkThresholds(this);
+}
+
+static void loadVariantPathArray(const SensorBaseConfigMap& 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 = std::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>& exitAirSensor,
+                  std::shared_ptr<sdbusplus::asio::connection>& dbusConnection)
+{
+    if (!dbusConnection)
+    {
+        std::cerr << "Connection not created\n";
+        return;
+    }
+    auto getter = std::make_shared<GetSensorConfiguration>(
+        dbusConnection, [&objectServer, &dbusConnection,
+                         &exitAirSensor](const ManagedObjectType& resp) {
+            cfmSensors.clear();
+            for (const auto& [path, interfaces] : resp)
+            {
+                for (const auto& [intf, cfg] : interfaces)
+                {
+                    if (intf == configInterfaceName(exitAirType))
+                    {
+                        // thresholds should be under the same path
+                        std::vector<thresholds::Threshold> sensorThresholds;
+                        parseThresholdsFromConfig(interfaces, sensorThresholds);
+
+                        std::string name =
+                            loadVariant<std::string>(cfg, "Name");
+                        exitAirSensor = nullptr;
+                        exitAirSensor = std::make_shared<ExitAirTempSensor>(
+                            dbusConnection, name, path.str, objectServer,
+                            std::move(sensorThresholds));
+                        exitAirSensor->powerFactorMin =
+                            loadVariant<double>(cfg, "PowerFactorMin");
+                        exitAirSensor->powerFactorMax =
+                            loadVariant<double>(cfg, "PowerFactorMax");
+                        exitAirSensor->qMin = loadVariant<double>(cfg, "QMin");
+                        exitAirSensor->qMax = loadVariant<double>(cfg, "QMax");
+                        exitAirSensor->alphaS =
+                            loadVariant<double>(cfg, "AlphaS");
+                        exitAirSensor->alphaF =
+                            loadVariant<double>(cfg, "AlphaF");
+                    }
+                    else if (intf == configInterfaceName(cfmType))
+                    {
+                        // thresholds should be under the same path
+                        std::vector<thresholds::Threshold> sensorThresholds;
+                        parseThresholdsFromConfig(interfaces, sensorThresholds);
+                        std::string name =
+                            loadVariant<std::string>(cfg, "Name");
+                        auto sensor = std::make_shared<CFMSensor>(
+                            dbusConnection, name, path.str, objectServer,
+                            std::move(sensorThresholds), exitAirSensor);
+                        loadVariantPathArray(cfg, "Tachs", sensor->tachs);
+                        sensor->maxCFM = loadVariant<double>(cfg, "MaxCFM");
+
+                        // change these into percent upon getting the data
+                        sensor->c1 = loadVariant<double>(cfg, "C1") / 100;
+                        sensor->c2 = loadVariant<double>(cfg, "C2") / 100;
+                        sensor->tachMinPercent =
+                            loadVariant<double>(cfg, "TachMinPercent");
+                        sensor->tachMaxPercent =
+                            loadVariant<double>(cfg, "TachMaxPercent");
+                        sensor->createMaxCFMIface();
+                        sensor->setupMatches();
+
+                        cfmSensors.emplace_back(std::move(sensor));
+                    }
+                }
+            }
+            if (exitAirSensor)
+            {
+                exitAirSensor->setupMatches();
+                exitAirSensor->updateReading();
+            }
+        });
+    getter->getConfiguration(
+        std::vector<std::string>(monitorTypes.begin(), monitorTypes.end()));
+}
+
+int main()
+{
+    boost::asio::io_context io;
+    auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
+    sdbusplus::asio::object_server objectServer(systemBus, true);
+    objectServer.add_manager("/xyz/openbmc_project/sensors");
+    systemBus->request_name("xyz.openbmc_project.ExitAirTempSensor");
+    std::shared_ptr<ExitAirTempSensor> sensor =
+        nullptr; // wait until we find the config
+
+    boost::asio::post(io,
+                      [&]() { createSensor(objectServer, sensor, systemBus); });
+
+    boost::asio::steady_timer configTimer(io);
+
+    std::function<void(sdbusplus::message_t&)> eventHandler =
+        [&](sdbusplus::message_t&) {
+            configTimer.expires_after(std::chrono::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";
+                }
+            });
+        };
+    std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches =
+        setupPropertiesChangedMatches(*systemBus, monitorTypes, eventHandler);
+
+    setupManufacturingModeMatch(*systemBus);
+    io.run();
+    return 0;
+}
diff --git a/src/exit-air/ExitAirTempSensor.hpp b/src/exit-air/ExitAirTempSensor.hpp
new file mode 100644
index 0000000..68952ab
--- /dev/null
+++ b/src/exit-air/ExitAirTempSensor.hpp
@@ -0,0 +1,83 @@
+#pragma once
+#include <boost/container/flat_map.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <sensor.hpp>
+
+#include <chrono>
+#include <limits>
+#include <memory>
+#include <string>
+#include <vector>
+
+struct ExitAirTempSensor;
+struct CFMSensor : public Sensor, std::enable_shared_from_this<CFMSensor>
+{
+    std::vector<std::string> tachs;
+    double c1 = 0.0;
+    double c2 = 0.0;
+    double maxCFM = 0.0;
+    double tachMinPercent = 0.0;
+    double tachMaxPercent = 0.0;
+
+    std::shared_ptr<ExitAirTempSensor> parent;
+
+    CFMSensor(std::shared_ptr<sdbusplus::asio::connection>& conn,
+              const std::string& name, const std::string& sensorConfiguration,
+              sdbusplus::asio::object_server& objectServer,
+              std::vector<thresholds::Threshold>&& thresholdData,
+              std::shared_ptr<ExitAirTempSensor>& parent);
+    ~CFMSensor() override;
+
+    bool calculate(double& /*value*/);
+    void updateReading();
+    void setupMatches();
+    void createMaxCFMIface();
+    void addTachRanges(const std::string& serviceName, const std::string& path);
+    void checkThresholds() override;
+    uint64_t getMaxRpm(uint64_t cfmMax) const;
+
+  private:
+    std::vector<sdbusplus::bus::match_t> matches;
+    boost::container::flat_map<std::string, double> tachReadings;
+    boost::container::flat_map<std::string, std::pair<double, double>>
+        tachRanges;
+    std::shared_ptr<sdbusplus::asio::dbus_interface> pwmLimitIface;
+    std::shared_ptr<sdbusplus::asio::dbus_interface> cfmLimitIface;
+    sdbusplus::asio::object_server& objServer;
+};
+
+struct ExitAirTempSensor :
+    public Sensor,
+    std::enable_shared_from_this<ExitAirTempSensor>
+{
+    double powerFactorMin = 0.0;
+    double powerFactorMax = 0.0;
+    double qMin = 0.0;
+    double qMax = 0.0;
+    double alphaS = 0.0;
+    double alphaF = 0.0;
+    double pOffset = 0.0;
+
+    ExitAirTempSensor(std::shared_ptr<sdbusplus::asio::connection>& conn,
+                      const std::string& name,
+                      const std::string& sensorConfiguration,
+                      sdbusplus::asio::object_server& objectServer,
+                      std::vector<thresholds::Threshold>&& thresholdData);
+    ~ExitAirTempSensor() override;
+
+    void checkThresholds() override;
+    void updateReading();
+    void setupMatches();
+
+  private:
+    double lastReading = 0.0;
+
+    std::vector<sdbusplus::bus::match_t> matches;
+    double inletTemp = std::numeric_limits<double>::quiet_NaN();
+    boost::container::flat_map<std::string, double> powerReadings;
+
+    sdbusplus::asio::object_server& objServer;
+    std::chrono::time_point<std::chrono::steady_clock> lastTime;
+    static double getTotalCFM();
+    bool calculate(double& val);
+};
diff --git a/src/exit-air/meson.build b/src/exit-air/meson.build
new file mode 100644
index 0000000..70119af
--- /dev/null
+++ b/src/exit-air/meson.build
@@ -0,0 +1,13 @@
+src_inc = include_directories('..')
+
+executable(
+    'exitairtempsensor',
+    'ExitAirTempSensor.cpp',
+    dependencies: [
+        default_deps,
+        thresholds_dep,
+        utils_dep,
+    ],
+    include_directories: src_inc,
+    install: true,
+)
\ No newline at end of file