Break CFM out into its own sensor

Break CFM out into its own sensor to be the System Airflow
sensor. Also change CFM calculation to use rpm instead of pwm
as it is more accurate.

Change-Id: Ic152465f8c0cd9256176b6a673f121562cff8580
Signed-off-by: James Feist <james.feist@linux.intel.com>
diff --git a/src/ExitAirTempSensor.cpp b/src/ExitAirTempSensor.cpp
index c9066ad..9f97c5d 100644
--- a/src/ExitAirTempSensor.cpp
+++ b/src/ExitAirTempSensor.cpp
@@ -38,20 +38,246 @@
 
 // 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;
 
+static constexpr double cfmMaxReading = 255;
+static constexpr double cfmMinReading = 0;
+
+static void setupSensorMatch(
+    std::vector<sdbusplus::bus::match::match>& matches,
+    sdbusplus::bus::bus& connection, const std::string& type,
+    std::function<void(const double&, sdbusplus::message::message&)>&& callback)
+{
+
+    std::function<void(sdbusplus::message::message & message)> eventHandler =
+        [callback{std::move(callback)}](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);
+            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));
+}
+
+template <typename T>
+static T loadVariant(
+    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");
+    }
+    if constexpr (std::is_same_v<T, double>)
+    {
+        return sdbusplus::message::variant_ns::visit(VariantToDoubleVisitor(),
+                                                     it->second);
+    }
+    else if constexpr (std::is_same_v<T, std::string>)
+    {
+        return sdbusplus::message::variant_ns::visit(VariantToStringVisitor(),
+                                                     it->second);
+    }
+    else
+    {
+        static_assert("Type Not Implemented");
+    }
+}
+
+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>&& thresholds,
+                     std::shared_ptr<ExitAirTempSensor>& parent) :
+    Sensor(boost::replace_all_copy(sensorName, " ", "_"),
+           "" /* todo: remove arg from base*/, std::move(thresholds),
+           sensorConfiguration, "xyz.openbmc_project.Configuration.ExitAirTemp",
+           cfmMaxReading, cfmMinReading),
+    dbusConnection(conn), parent(parent)
+{
+    sensorInterface =
+        objectServer.add_interface("/xyz/openbmc_project/sensors/cfm/" + name,
+                                   "xyz.openbmc_project.Sensor.Value");
+
+    if (thresholds::hasWarningInterface(thresholds))
+    {
+        thresholdInterfaceWarning = objectServer.add_interface(
+            "/xyz/openbmc_project/sensors/cfm/" + name,
+            "xyz.openbmc_project.Sensor.Threshold.Warning");
+    }
+    if (thresholds::hasCriticalInterface(thresholds))
+    {
+        thresholdInterfaceCritical = objectServer.add_interface(
+            "/xyz/openbmc_project/sensors/cfm/" + name,
+            "xyz.openbmc_project.Sensor.Threshold.Critical");
+    }
+    setInitialProperties(conn);
+    setupSensorMatch(
+        matches, *dbusConnection, "fan_tach",
+        std::move(
+            [this](const double& value, sdbusplus::message::message& message) {
+                tachReadings[message.get_path()] = value;
+                if (tachRanges.find(message.get_path()) == tachRanges.end())
+                {
+                    // calls update reading after updating ranges
+                    addTachRanges(message.get_sender(), message.get_path());
+                }
+                else
+                {
+                    updateReading();
+                }
+            }));
+}
+
+void CFMSensor::addTachRanges(const std::string& serviceName,
+                              const std::string& path)
+{
+    dbusConnection->async_method_call(
+        [this, path](const boost::system::error_code ec,
+                     const boost::container::flat_map<std::string,
+                                                      BasicVariantType>& data) {
+            if (ec)
+            {
+                std::cerr << "Error getting properties from " << path << "\n";
+            }
+
+            double max = loadVariant<double>(data, "MaxValue");
+            double min = loadVariant<double>(data, "MinValue");
+            tachRanges[path] = std::make_pair(min, max);
+            updateReading();
+        },
+        serviceName, path, "org.freedesktop.DBus.Properties", "GetAll",
+        "xyz.openbmc_project.Sensor.Value");
+}
+
+void CFMSensor::checkThresholds(void)
+{
+    thresholds::checkThresholds(this);
+}
+
+void CFMSensor::updateReading(void)
+{
+    double val = 0.0;
+    if (calculate(val))
+    {
+        if (value != val && parent)
+        {
+            parent->updateReading();
+        }
+        updateValue(val);
+    }
+    else
+    {
+        updateValue(std::numeric_limits<double>::quiet_NaN());
+    }
+}
+
+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 boost::ends_with(item.first, tachName);
+            });
+        auto findRange = std::find_if(
+            tachRanges.begin(), tachRanges.end(), [&](const auto& item) {
+                return boost::ends_with(item.first, tachName);
+            });
+        if (findReading == tachReadings.end())
+        {
+            std::cerr << "Can't find " << tachName << "in readings\n";
+            return false; // 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;
+    }
+
+    // divide by 100 since rpm is in percent
+    value = totalCFM / 100;
+}
+
+static constexpr double exitAirMaxReading = 127;
+static constexpr double exitAirMinReading = -128;
 ExitAirTempSensor::ExitAirTempSensor(
     std::shared_ptr<sdbusplus::asio::connection>& conn,
-    const std::string& sensorConfiguration,
+    const std::string& sensorName, 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),
+    Sensor(boost::replace_all_copy(sensorName, " ", "_"),
+           "" /* todo: remove arg from base*/, std::move(thresholds),
+           sensorConfiguration, "xyz.openbmc_project.Configuration.ExitAirTemp",
+           exitAirMaxReading, exitAirMinReading),
     dbusConnection(conn)
 {
     sensorInterface = objectServer.add_interface(
@@ -82,47 +308,24 @@
 void ExitAirTempSensor::setupMatches(void)
 {
 
-    constexpr const std::array<const char*, 3> matchTypes = {
-        "power", "fan_pwm", inletTemperatureSensor};
+    constexpr const std::array<const char*, 2> matchTypes = {
+        "power", inletTemperatureSensor};
 
-    for (const auto& type : matchTypes)
+    for (const std::string& 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));
+        setupSensorMatch(matches, *dbusConnection, type,
+                         [this, type](const double& value,
+                                      sdbusplus::message::message& message) {
+                             if (type == "power")
+                             {
+                                 powerReadings[message.get_path()] = value;
+                             }
+                             else if (type == inletTemperatureSensor)
+                             {
+                                 inletTemp = value;
+                             }
+                             updateReading();
+                         });
     }
 }
 
@@ -140,69 +343,20 @@
     }
 }
 
-// todo: break this out into it's own sensor
-int32_t ExitAirTempSensor::getTotalCFM(void)
+double ExitAirTempSensor::getTotalCFM(void)
 {
-    int32_t totalCFM = 0;
-    // todo: rpm instead of pwm
-    for (const auto& zone : cfmData)
+    double sum = 0;
+    for (auto& sensor : cfmSensors)
     {
-        if (zone.pwm.empty())
+        double reading = 0;
+        if (!sensor->calculate(reading))
         {
-            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;
+        sum += reading;
     }
-    if constexpr (DEBUG)
-    {
-        std::cout << totalCFM / 100 << " CFM\n";
-    }
-    return totalCFM /= 100; // divide by 100 since PWM is in percent
+
+    return sum;
 }
 
 bool ExitAirTempSensor::calculate(double& val)
@@ -344,20 +498,6 @@
     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)
@@ -379,7 +519,7 @@
 }
 
 void createSensor(sdbusplus::asio::object_server& objectServer,
-                  std::shared_ptr<ExitAirTempSensor>& sensor,
+                  std::shared_ptr<ExitAirTempSensor>& exitAirSensor,
                   std::shared_ptr<sdbusplus::asio::connection>& dbusConnection)
 {
     if (!dbusConnection)
@@ -394,80 +534,88 @@
                 std::cerr << "Error contacting entity manager\n";
                 return;
             }
-
-            std::vector<CFMInfo> cfmData;
-            bool foundExitAir = false;
+            std::vector<std::unique_ptr<CFMSensor>> cfmSensors;
             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)
+                        if (!exitAirSensor)
                         {
-                            sensor = std::make_shared<ExitAirTempSensor>(
-                                dbusConnection, pathPair.first.str,
+                            std::string name =
+                                loadVariant<std::string>(entry.second, "Name");
+                            exitAirSensor = std::make_shared<ExitAirTempSensor>(
+                                dbusConnection, name, pathPair.first.str,
                                 objectServer, std::move(sensorThresholds));
                         }
                         else
                         {
-                            sensor->thresholds = sensorThresholds;
+                            exitAirSensor->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");
+                        exitAirSensor->powerFactorMin =
+                            loadVariant<double>(entry.second, "PowerFactorMin");
+                        exitAirSensor->powerFactorMax =
+                            loadVariant<double>(entry.second, "PowerFactorMax");
+                        exitAirSensor->qMin =
+                            loadVariant<double>(entry.second, "QMin");
+                        exitAirSensor->qMax =
+                            loadVariant<double>(entry.second, "QMax");
+                        exitAirSensor->alphaS =
+                            loadVariant<double>(entry.second, "AlphaS");
+                        exitAirSensor->alphaF =
+                            loadVariant<double>(entry.second, "AlphaF");
                     }
                     else if (entry.first == cfmIface)
 
                     {
-
-                        CFMInfo cfm;
-                        loadVariantPathArray(entry.second, "Pwms", cfm.pwm);
-                        cfm.maxCFM = loadVariantDouble(entry.second, "MaxCFM");
+                        // thresholds should be under the same path
+                        std::vector<thresholds::Threshold> sensorThresholds;
+                        parseThresholdsFromConfig(pathPair.second,
+                                                  sensorThresholds);
+                        std::string name =
+                            loadVariant<std::string>(entry.second, "Name");
+                        auto sensor = std::make_unique<CFMSensor>(
+                            dbusConnection, name, pathPair.first.str,
+                            objectServer, std::move(sensorThresholds),
+                            exitAirSensor);
+                        loadVariantPathArray(entry.second, "Tachs",
+                                             sensor->tachs);
+                        sensor->maxCFM =
+                            loadVariant<double>(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") /
+                        sensor->c1 =
+                            loadVariant<double>(entry.second, "C1") / 100;
+                        sensor->c2 =
+                            loadVariant<double>(entry.second, "C2") / 100;
+                        sensor->tachMinPercent =
+                            loadVariant<double>(entry.second,
+                                                "TachMinPercent") /
                             100;
-                        cfm.pwmMax =
-                            loadVariantDouble(entry.second, "PwmMaxPercent") /
+                        sensor->tachMaxPercent =
+                            loadVariant<double>(entry.second,
+                                                "TachMaxPercent") /
                             100;
 
-                        cfmData.emplace_back(cfm);
+                        cfmSensors.emplace_back(std::move(sensor));
                     }
                 }
             }
-            if (sensor)
+            if (exitAirSensor)
             {
-                sensor->cfmData = std::move(cfmData);
+                exitAirSensor->cfmSensors = std::move(cfmSensors);
 
-                // todo: when power sensors are done delete this fake reading
-                sensor->powerReadings["foo"] = 144.0;
+                // todo: when power sensors are done delete this fake
+                // reading
+                exitAirSensor->powerReadings["foo"] = 144.0;
 
-                sensor->updateReading();
+                exitAirSensor->updateReading();
             }
         },
         entityManagerName, "/", "org.freedesktop.DBus.ObjectManager",