cfm: allow setting of max pwm

When a change from xyz.openbmc_project.Control.CFMLimit
interface is detected set the max pwm in pid control configs
to limit max fan speed.

This also puts the max CFM on d-bus for other commands to
use.

Tested-by: Used settingsd to control fan speed

Change-Id: Ic4ca37747690f6476cfa12bf3c19a44d7ee086c8
Signed-off-by: James Feist <james.feist@linux.intel.com>
diff --git a/include/ExitAirTempSensor.hpp b/include/ExitAirTempSensor.hpp
index 24f87db..1e774cf 100644
--- a/include/ExitAirTempSensor.hpp
+++ b/include/ExitAirTempSensor.hpp
@@ -27,7 +27,9 @@
 
     bool calculate(double&);
     void updateReading(void);
+    void createMaxCFMIface(void);
     void checkThresholds(void) override;
+    uint64_t getMaxRpm(uint64_t cfmMax);
 
   private:
     std::vector<sdbusplus::bus::match::match> matches;
@@ -35,6 +37,8 @@
     boost::container::flat_map<std::string, std::pair<double, double>>
         tachRanges;
     std::shared_ptr<sdbusplus::asio::connection> dbusConnection;
+    std::shared_ptr<sdbusplus::asio::dbus_interface> pwmLimitIface;
+    std::shared_ptr<sdbusplus::asio::dbus_interface> cfmLimitIface;
     sdbusplus::asio::object_server& objServer;
     void addTachRanges(const std::string& serviceName, const std::string& path);
 };
diff --git a/service_files/xyz.openbmc_project.exitairsensor.service b/service_files/xyz.openbmc_project.exitairsensor.service
index ca7dbb0..8049445 100644
--- a/service_files/xyz.openbmc_project.exitairsensor.service
+++ b/service_files/xyz.openbmc_project.exitairsensor.service
@@ -1,6 +1,7 @@
 [Unit]
 Description=Exit Air Temp Sensor
 StopWhenUnneeded=false
+After=xyz.openbmc_project.Settings.service
 
 [Service]
 Restart=always
diff --git a/src/ExitAirTempSensor.cpp b/src/ExitAirTempSensor.cpp
index f4e59ee..6befd35 100644
--- a/src/ExitAirTempSensor.cpp
+++ b/src/ExitAirTempSensor.cpp
@@ -38,12 +38,19 @@
 
 // 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;
+
 static void setupSensorMatch(
     std::vector<sdbusplus::bus::match::match>& matches,
     sdbusplus::bus::bus& connection, const std::string& type,
@@ -81,6 +88,65 @@
                          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](const boost::system::error_code ec,
+                           const std::variant<std::string>& classType) {
+                        if (ec)
+                        {
+                            std::cerr << "Error getting pid class\n";
+                            return;
+                        }
+                        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");
+            }
+        },
+        "xyz.openbmc_project.ObjectMapper",
+        "/xyz/openbmc_project/object_mapper",
+        "xyz.openbmc_project.ObjectMapper", "GetSubTree", "/", 0,
+        std::array<std::string, 1>{pidConfigurationType});
+}
+
 CFMSensor::CFMSensor(std::shared_ptr<sdbusplus::asio::connection>& conn,
                      const std::string& sensorName,
                      const std::string& sensorConfiguration,
@@ -130,6 +196,65 @@
                     updateReading();
                 }
             }));
+    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");
+
+    conn->async_method_call(
+        [this, conn](const boost::system::error_code ec,
+                     const std::variant<double> cfmVariant) {
+            uint64_t maxRpm = 100;
+            if (!ec)
+            {
+
+                auto cfm = std::get_if<double>(&cfmVariant);
+                if (cfm != nullptr || *cfm >= minSystemCfm)
+                {
+                    maxRpm = getMaxRpm(*cfm);
+                }
+            }
+            pwmLimitIface->register_property("Limit", maxRpm);
+            pwmLimitIface->initialize();
+            setMaxPWM(conn, maxRpm);
+        },
+        settingsDaemon, cfmSettingPath, "org.freedesktop.DBus.Properties",
+        "Get", cfmSettingIface, "Limit");
+
+    matches.emplace_back(
+        *conn,
+        "type='signal',"
+        "member='PropertiesChanged',interface='org."
+        "freedesktop.DBus.Properties',path='" +
+            std::string(cfmSettingPath) + "',arg0='" +
+            std::string(cfmSettingIface) + "'",
+        [this, conn](sdbusplus::message::message& message) {
+            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;
+            }
+            const auto 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 = getMaxRpm(*reading);
+            pwmLimitIface->set_property("Limit", maxRpm);
+            setMaxPWM(conn, maxRpm);
+        });
 }
 
 CFMSensor::~CFMSensor()
@@ -138,6 +263,15 @@
     objServer.remove_interface(thresholdInterfaceCritical);
     objServer.remove_interface(sensorInterface);
     objServer.remove_interface(association);
+    objServer.remove_interface(cfmLimitIface);
+    objServer.remove_interface(pwmLimitIface);
+}
+
+void CFMSensor::createMaxCFMIface(void)
+{
+    cfmLimitIface->register_property("Limit", static_cast<double>(c2) * maxCFM *
+                                                  tachs.size());
+    cfmLimitIface->initialize();
 }
 
 void CFMSensor::addTachRanges(const std::string& serviceName,
@@ -184,6 +318,52 @@
     }
 }
 
+uint64_t CFMSensor::getMaxRpm(uint64_t cfmMaxSetting)
+{
+    uint64_t pwmPercent = 100;
+    double totalCFM = std::numeric_limits<double>::max();
+    if (cfmMaxSetting == 0)
+    {
+        return pwmPercent;
+    }
+
+    while (totalCFM > cfmMaxSetting)
+    {
+        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;
+
+        pwmPercent--;
+        if (pwmPercent <= 0)
+        {
+            break;
+        }
+    }
+    return pwmPercent;
+}
+
 bool CFMSensor::calculate(double& value)
 {
     double totalCFM = 0;
@@ -614,6 +794,7 @@
                             loadVariant<double>(entry.second,
                                                 "TachMaxPercent") /
                             100;
+                        sensor->createMaxCFMIface();
 
                         cfmSensors.emplace_back(std::move(sensor));
                     }