xref: /openbmc/dbus-sensors/src/exit-air/ExitAirTempSensor.cpp (revision 89be6147e5a7ffa86d88d8f3e27eba8eb2c3a9da)
1 /*
2 // Copyright (c) 2018 Intel Corporation
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 //      http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 */
16 
17 #include "ExitAirTempSensor.hpp"
18 
19 #include "SensorPaths.hpp"
20 #include "Thresholds.hpp"
21 #include "Utils.hpp"
22 #include "VariantVisitors.hpp"
23 #include "sensor.hpp"
24 
25 #include <boost/algorithm/string/replace.hpp>
26 #include <boost/asio/error.hpp>
27 #include <boost/asio/io_context.hpp>
28 #include <boost/asio/post.hpp>
29 #include <boost/asio/steady_timer.hpp>
30 #include <boost/container/flat_map.hpp>
31 #include <phosphor-logging/lg2.hpp>
32 #include <sdbusplus/asio/connection.hpp>
33 #include <sdbusplus/asio/object_server.hpp>
34 #include <sdbusplus/bus.hpp>
35 #include <sdbusplus/bus/match.hpp>
36 #include <sdbusplus/message.hpp>
37 
38 #include <algorithm>
39 #include <array>
40 #include <chrono>
41 #include <cmath>
42 #include <cstddef>
43 #include <cstdint>
44 #include <functional>
45 #include <limits>
46 #include <memory>
47 #include <stdexcept>
48 #include <string>
49 #include <utility>
50 #include <variant>
51 #include <vector>
52 
53 constexpr const double altitudeFactor = 1.14;
54 constexpr const char* exitAirType = "ExitAirTempSensor";
55 constexpr const char* cfmType = "CFMSensor";
56 
57 // todo: this *might* need to be configurable
58 constexpr const char* inletTemperatureSensor = "temperature/Front_Panel_Temp";
59 constexpr const char* pidConfigurationType =
60     "xyz.openbmc_project.Configuration.Pid";
61 constexpr const char* settingsDaemon = "xyz.openbmc_project.Settings";
62 constexpr const char* cfmSettingPath = "/xyz/openbmc_project/control/cfm_limit";
63 constexpr const char* cfmSettingIface = "xyz.openbmc_project.Control.CFMLimit";
64 
65 static constexpr double cfmMaxReading = 255;
66 static constexpr double cfmMinReading = 0;
67 
68 static constexpr size_t minSystemCfm = 50;
69 
70 constexpr const auto monitorTypes{
71     std::to_array<const char*>({exitAirType, cfmType})};
72 
73 static std::vector<std::shared_ptr<CFMSensor>> cfmSensors;
74 
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)75 static void setupSensorMatch(
76     std::vector<sdbusplus::bus::match_t>& matches, sdbusplus::bus_t& connection,
77     const std::string& type,
78     std::function<void(const double&, sdbusplus::message_t&)>&& callback)
79 {
80     std::function<void(sdbusplus::message_t & message)> eventHandler =
81         [callback{std::move(callback)}](sdbusplus::message_t& message) {
82             std::string objectName;
83             boost::container::flat_map<std::string,
84                                        std::variant<double, int64_t>>
85                 values;
86             message.read(objectName, values);
87             auto findValue = values.find("Value");
88             if (findValue == values.end())
89             {
90                 return;
91             }
92             double value =
93                 std::visit(VariantToDoubleVisitor(), findValue->second);
94             if (std::isnan(value))
95             {
96                 return;
97             }
98 
99             callback(value, message);
100         };
101     matches.emplace_back(
102         connection,
103         "type='signal',"
104         "member='PropertiesChanged',interface='org."
105         "freedesktop.DBus.Properties',path_"
106         "namespace='/xyz/openbmc_project/sensors/" +
107             std::string(type) + "',arg0='xyz.openbmc_project.Sensor.Value'",
108         std::move(eventHandler));
109 }
110 
setMaxPWM(const std::shared_ptr<sdbusplus::asio::connection> & conn,double value)111 static void setMaxPWM(const std::shared_ptr<sdbusplus::asio::connection>& conn,
112                       double value)
113 {
114     using GetSubTreeType = std::vector<std::pair<
115         std::string,
116         std::vector<std::pair<std::string, std::vector<std::string>>>>>;
117 
118     conn->async_method_call(
119         [conn,
120          value](const boost::system::error_code ec, const GetSubTreeType& ret) {
121             if (ec)
122             {
123                 lg2::error("Error calling mapper");
124                 return;
125             }
126             for (const auto& [path, objDict] : ret)
127             {
128                 if (objDict.empty())
129                 {
130                     return;
131                 }
132                 const std::string& owner = objDict.begin()->first;
133 
134                 conn->async_method_call(
135                     [conn, value, owner,
136                      path{path}](const boost::system::error_code ec,
137                                  const std::variant<std::string>& classType) {
138                         if (ec)
139                         {
140                             lg2::error("Error getting pid class");
141                             return;
142                         }
143                         const auto* classStr =
144                             std::get_if<std::string>(&classType);
145                         if (classStr == nullptr || *classStr != "fan")
146                         {
147                             return;
148                         }
149                         conn->async_method_call(
150                             [](boost::system::error_code& ec) {
151                                 if (ec)
152                                 {
153                                     lg2::error("Error setting pid class");
154                                     return;
155                                 }
156                             },
157                             owner, path, "org.freedesktop.DBus.Properties",
158                             "Set", pidConfigurationType, "OutLimitMax",
159                             std::variant<double>(value));
160                     },
161                     owner, path, "org.freedesktop.DBus.Properties", "Get",
162                     pidConfigurationType, "Class");
163             }
164         },
165         mapper::busName, mapper::path, mapper::interface, mapper::subtree, "/",
166         0, std::array<std::string, 1>{pidConfigurationType});
167 }
168 
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)169 CFMSensor::CFMSensor(std::shared_ptr<sdbusplus::asio::connection>& conn,
170                      const std::string& sensorName,
171                      const std::string& sensorConfiguration,
172                      sdbusplus::asio::object_server& objectServer,
173                      std::vector<thresholds::Threshold>&& thresholdData,
174                      std::shared_ptr<ExitAirTempSensor>& parent) :
175     Sensor(escapeName(sensorName), std::move(thresholdData),
176            sensorConfiguration, "CFMSensor", false, false, cfmMaxReading,
177            cfmMinReading, conn, PowerState::on),
178     parent(parent), objServer(objectServer)
179 {
180     sensorInterface = objectServer.add_interface(
181         "/xyz/openbmc_project/sensors/airflow/" + name,
182         "xyz.openbmc_project.Sensor.Value");
183 
184     for (const auto& threshold : thresholds)
185     {
186         std::string interface = thresholds::getInterface(threshold.level);
187         thresholdInterfaces[static_cast<size_t>(threshold.level)] =
188             objectServer.add_interface(
189                 "/xyz/openbmc_project/sensors/airflow/" + name, interface);
190     }
191 
192     association = objectServer.add_interface(
193         "/xyz/openbmc_project/sensors/airflow/" + name, association::interface);
194 
195     setInitialProperties(sensor_paths::unitCFM);
196 
197     pwmLimitIface =
198         objectServer.add_interface("/xyz/openbmc_project/control/pwm_limit",
199                                    "xyz.openbmc_project.Control.PWMLimit");
200     cfmLimitIface =
201         objectServer.add_interface("/xyz/openbmc_project/control/MaxCFM",
202                                    "xyz.openbmc_project.Control.CFMLimit");
203 }
204 
setupMatches()205 void CFMSensor::setupMatches()
206 {
207     std::weak_ptr<CFMSensor> weakRef = weak_from_this();
208     setupSensorMatch(
209         matches, *dbusConnection, "fan_tach",
210         [weakRef](const double& value, sdbusplus::message_t& message) {
211             auto self = weakRef.lock();
212             if (!self)
213             {
214                 return;
215             }
216             self->tachReadings[message.get_path()] = value;
217             if (self->tachRanges.find(message.get_path()) ==
218                 self->tachRanges.end())
219             {
220                 // calls update reading after updating ranges
221                 self->addTachRanges(message.get_sender(), message.get_path());
222             }
223             else
224             {
225                 self->updateReading();
226             }
227         });
228 
229     dbusConnection->async_method_call(
230         [weakRef](const boost::system::error_code ec,
231                   const std::variant<double> cfmVariant) {
232             auto self = weakRef.lock();
233             if (!self)
234             {
235                 return;
236             }
237 
238             uint64_t maxRpm = 100;
239             if (!ec)
240             {
241                 const auto* cfm = std::get_if<double>(&cfmVariant);
242                 if (cfm != nullptr && *cfm >= minSystemCfm)
243                 {
244                     maxRpm = self->getMaxRpm(*cfm);
245                 }
246             }
247             self->pwmLimitIface->register_property("Limit", maxRpm);
248             self->pwmLimitIface->initialize();
249             setMaxPWM(self->dbusConnection, maxRpm);
250         },
251         settingsDaemon, cfmSettingPath, "org.freedesktop.DBus.Properties",
252         "Get", cfmSettingIface, "Limit");
253 
254     matches.emplace_back(
255         *dbusConnection,
256         "type='signal',"
257         "member='PropertiesChanged',interface='org."
258         "freedesktop.DBus.Properties',path='" +
259             std::string(cfmSettingPath) + "',arg0='" +
260             std::string(cfmSettingIface) + "'",
261         [weakRef](sdbusplus::message_t& message) {
262             auto self = weakRef.lock();
263             if (!self)
264             {
265                 return;
266             }
267             boost::container::flat_map<std::string, std::variant<double>>
268                 values;
269             std::string objectName;
270             message.read(objectName, values);
271             const auto findValue = values.find("Limit");
272             if (findValue == values.end())
273             {
274                 return;
275             }
276             auto* const reading = std::get_if<double>(&(findValue->second));
277             if (reading == nullptr)
278             {
279                 lg2::error("Got CFM Limit of wrong type");
280                 return;
281             }
282             if (*reading < minSystemCfm && *reading != 0)
283             {
284                 lg2::error("Illegal CFM setting detected");
285                 return;
286             }
287             uint64_t maxRpm = self->getMaxRpm(*reading);
288             self->pwmLimitIface->set_property("Limit", maxRpm);
289             setMaxPWM(self->dbusConnection, maxRpm);
290         });
291 }
292 
~CFMSensor()293 CFMSensor::~CFMSensor()
294 {
295     for (const auto& iface : thresholdInterfaces)
296     {
297         objServer.remove_interface(iface);
298     }
299     objServer.remove_interface(sensorInterface);
300     objServer.remove_interface(association);
301     objServer.remove_interface(cfmLimitIface);
302     objServer.remove_interface(pwmLimitIface);
303 }
304 
createMaxCFMIface()305 void CFMSensor::createMaxCFMIface()
306 {
307     cfmLimitIface->register_property("Limit", c2 * maxCFM * tachs.size());
308     cfmLimitIface->initialize();
309 }
310 
addTachRanges(const std::string & serviceName,const std::string & path)311 void CFMSensor::addTachRanges(const std::string& serviceName,
312                               const std::string& path)
313 {
314     std::weak_ptr<CFMSensor> weakRef = weak_from_this();
315     dbusConnection->async_method_call(
316         [weakRef, path](const boost::system::error_code ec,
317                         const SensorBaseConfigMap& data) {
318             if (ec)
319             {
320                 lg2::error("Error getting properties from '{PATH}'", "PATH",
321                            path);
322                 return;
323             }
324             auto self = weakRef.lock();
325             if (!self)
326             {
327                 return;
328             }
329             double max = loadVariant<double>(data, "MaxValue");
330             double min = loadVariant<double>(data, "MinValue");
331             self->tachRanges[path] = std::make_pair(min, max);
332             self->updateReading();
333         },
334         serviceName, path, "org.freedesktop.DBus.Properties", "GetAll",
335         "xyz.openbmc_project.Sensor.Value");
336 }
337 
checkThresholds()338 void CFMSensor::checkThresholds()
339 {
340     thresholds::checkThresholds(this);
341 }
342 
updateReading()343 void CFMSensor::updateReading()
344 {
345     double val = 0.0;
346     if (calculate(val))
347     {
348         if (value != val && parent)
349         {
350             parent->updateReading();
351         }
352         updateValue(val);
353     }
354     else
355     {
356         updateValue(std::numeric_limits<double>::quiet_NaN());
357     }
358 }
359 
getMaxRpm(uint64_t cfmMaxSetting) const360 uint64_t CFMSensor::getMaxRpm(uint64_t cfmMaxSetting) const
361 {
362     uint64_t pwmPercent = 100;
363     double totalCFM = std::numeric_limits<double>::max();
364     if (cfmMaxSetting == 0)
365     {
366         return pwmPercent;
367     }
368 
369     bool firstLoop = true;
370     while (totalCFM > cfmMaxSetting)
371     {
372         if (firstLoop)
373         {
374             firstLoop = false;
375         }
376         else
377         {
378             pwmPercent--;
379         }
380 
381         double ci = 0;
382         if (pwmPercent == 0)
383         {
384             ci = 0;
385         }
386         else if (pwmPercent < tachMinPercent)
387         {
388             ci = c1;
389         }
390         else if (pwmPercent > tachMaxPercent)
391         {
392             ci = c2;
393         }
394         else
395         {
396             ci = c1 + (((c2 - c1) * (pwmPercent - tachMinPercent)) /
397                        (tachMaxPercent - tachMinPercent));
398         }
399 
400         // Now calculate the CFM for this tach
401         // CFMi = Ci * Qmaxi * TACHi
402         totalCFM = ci * maxCFM * pwmPercent;
403         totalCFM *= tachs.size();
404         // divide by 100 since pwm is in percent
405         totalCFM /= 100;
406 
407         if (pwmPercent <= 0)
408         {
409             break;
410         }
411     }
412 
413     return pwmPercent;
414 }
415 
calculate(double & value)416 bool CFMSensor::calculate(double& value)
417 {
418     double totalCFM = 0;
419     for (const std::string& tachName : tachs)
420     {
421         auto findReading = std::find_if(
422             tachReadings.begin(), tachReadings.end(),
423             [&](const auto& item) { return item.first.ends_with(tachName); });
424         auto findRange = std::find_if(
425             tachRanges.begin(), tachRanges.end(),
426             [&](const auto& item) { return item.first.ends_with(tachName); });
427         if (findReading == tachReadings.end())
428         {
429             lg2::debug("Can't find '{NAME}' in readings", "NAME", tachName);
430             continue; // haven't gotten a reading
431         }
432 
433         if (findRange == tachRanges.end())
434         {
435             lg2::error("Can't find '{NAME}' in ranges", "NAME", tachName);
436             return false; // haven't gotten a max / min
437         }
438 
439         // avoid divide by 0
440         if (findRange->second.second == 0)
441         {
442             lg2::error("Tach Max Set to 0, tachName: '{NAME}'", "NAME",
443                        tachName);
444             return false;
445         }
446 
447         double rpm = findReading->second;
448 
449         // for now assume the min for a fan is always 0, divide by max to get
450         // percent and mult by 100
451         rpm /= findRange->second.second;
452         rpm *= 100;
453 
454         lg2::debug("Tach '{NAME}' at '{RPM}'", "NAME", tachName, "RPM", rpm);
455 
456         // Do a linear interpolation to get Ci
457         // Ci = C1 + (C2 - C1)/(RPM2 - RPM1) * (TACHi - TACH1)
458 
459         double ci = 0;
460         if (rpm == 0)
461         {
462             ci = 0;
463         }
464         else if (rpm < tachMinPercent)
465         {
466             ci = c1;
467         }
468         else if (rpm > tachMaxPercent)
469         {
470             ci = c2;
471         }
472         else
473         {
474             ci = c1 + (((c2 - c1) * (rpm - tachMinPercent)) /
475                        (tachMaxPercent - tachMinPercent));
476         }
477 
478         // Now calculate the CFM for this tach
479         // CFMi = Ci * Qmaxi * TACHi
480         totalCFM += ci * maxCFM * rpm;
481         lg2::debug(
482             "totalCFM = {CFM}, Ci = {CI}, MaxCFM = {MAXCFM}, rpm = {RPM}, c1 = {C1}"
483             ", c2 = {C2}, max = {MAX}, min = {MIN}",
484             "CFM", totalCFM, "CI", ci, "MAXCFM", maxCFM, "RPM", rpm, "C1", c1,
485             "C2", c2, "MAX", tachMaxPercent, "MIN", tachMinPercent);
486     }
487 
488     // divide by 100 since rpm is in percent
489     value = totalCFM / 100;
490     lg2::debug("cfm value = {VALUE}", "VALUE", value);
491     return true;
492 }
493 
494 static constexpr double exitAirMaxReading = 127;
495 static constexpr double exitAirMinReading = -128;
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)496 ExitAirTempSensor::ExitAirTempSensor(
497     std::shared_ptr<sdbusplus::asio::connection>& conn,
498     const std::string& sensorName, const std::string& sensorConfiguration,
499     sdbusplus::asio::object_server& objectServer,
500     std::vector<thresholds::Threshold>&& thresholdData) :
501     Sensor(escapeName(sensorName), std::move(thresholdData),
502            sensorConfiguration, "ExitAirTemp", false, false, exitAirMaxReading,
503            exitAirMinReading, conn, PowerState::on),
504     objServer(objectServer)
505 {
506     sensorInterface = objectServer.add_interface(
507         "/xyz/openbmc_project/sensors/temperature/" + name,
508         "xyz.openbmc_project.Sensor.Value");
509 
510     for (const auto& threshold : thresholds)
511     {
512         std::string interface = thresholds::getInterface(threshold.level);
513         thresholdInterfaces[static_cast<size_t>(threshold.level)] =
514             objectServer.add_interface(
515                 "/xyz/openbmc_project/sensors/temperature/" + name, interface);
516     }
517     association = objectServer.add_interface(
518         "/xyz/openbmc_project/sensors/temperature/" + name,
519         association::interface);
520     setInitialProperties(sensor_paths::unitDegreesC);
521 }
522 
~ExitAirTempSensor()523 ExitAirTempSensor::~ExitAirTempSensor()
524 {
525     for (const auto& iface : thresholdInterfaces)
526     {
527         objServer.remove_interface(iface);
528     }
529     objServer.remove_interface(sensorInterface);
530     objServer.remove_interface(association);
531 }
532 
setupMatches()533 void ExitAirTempSensor::setupMatches()
534 {
535     constexpr const auto matchTypes{
536         std::to_array<const char*>({"power", inletTemperatureSensor})};
537 
538     std::weak_ptr<ExitAirTempSensor> weakRef = weak_from_this();
539     for (const std::string type : matchTypes)
540     {
541         setupSensorMatch(
542             matches, *dbusConnection, type,
543             [weakRef,
544              type](const double& value, sdbusplus::message_t& message) {
545                 auto self = weakRef.lock();
546                 if (!self)
547                 {
548                     return;
549                 }
550                 if (type == "power")
551                 {
552                     std::string path = message.get_path();
553                     if (path.find("PS") != std::string::npos &&
554                         path.ends_with("Input_Power"))
555                     {
556                         self->powerReadings[message.get_path()] = value;
557                     }
558                 }
559                 else if (type == inletTemperatureSensor)
560                 {
561                     self->inletTemp = value;
562                 }
563                 self->updateReading();
564             });
565     }
566     dbusConnection->async_method_call(
567         [weakRef](boost::system::error_code ec,
568                   const std::variant<double>& value) {
569             if (ec)
570             {
571                 // sensor not ready yet
572                 return;
573             }
574             auto self = weakRef.lock();
575             if (!self)
576             {
577                 return;
578             }
579             self->inletTemp = std::visit(VariantToDoubleVisitor(), value);
580         },
581         "xyz.openbmc_project.HwmonTempSensor",
582         std::string("/xyz/openbmc_project/sensors/") + inletTemperatureSensor,
583         properties::interface, properties::get, sensorValueInterface, "Value");
584     dbusConnection->async_method_call(
585         [weakRef](boost::system::error_code ec, const GetSubTreeType& subtree) {
586             if (ec)
587             {
588                 lg2::error("Error contacting mapper");
589                 return;
590             }
591             auto self = weakRef.lock();
592             if (!self)
593             {
594                 return;
595             }
596             for (const auto& [path, matches] : subtree)
597             {
598                 size_t lastSlash = path.rfind('/');
599                 if (lastSlash == std::string::npos ||
600                     lastSlash == path.size() || matches.empty())
601                 {
602                     continue;
603                 }
604                 std::string sensorName = path.substr(lastSlash + 1);
605                 if (sensorName.starts_with("PS") &&
606                     sensorName.ends_with("Input_Power"))
607                 {
608                     // lambda capture requires a proper variable (not a
609                     // structured binding)
610                     const std::string& cbPath = path;
611                     self->dbusConnection->async_method_call(
612                         [weakRef, cbPath](boost::system::error_code ec,
613                                           const std::variant<double>& value) {
614                             if (ec)
615                             {
616                                 lg2::error("Error getting value from '{PATH}'",
617                                            "PATH", cbPath);
618                             }
619                             auto self = weakRef.lock();
620                             if (!self)
621                             {
622                                 return;
623                             }
624                             double reading =
625                                 std::visit(VariantToDoubleVisitor(), value);
626                             lg2::debug("'{PATH}' reading '{VALUE}'", "PATH",
627                                        cbPath, "VALUE", reading);
628                             self->powerReadings[cbPath] = reading;
629                         },
630                         matches[0].first, cbPath, properties::interface,
631                         properties::get, sensorValueInterface, "Value");
632                 }
633             }
634         },
635         mapper::busName, mapper::path, mapper::interface, mapper::subtree,
636         "/xyz/openbmc_project/sensors/power", 0,
637         std::array<const char*, 1>{sensorValueInterface});
638 }
639 
updateReading()640 void ExitAirTempSensor::updateReading()
641 {
642     double val = 0.0;
643     if (calculate(val))
644     {
645         val = std::floor(val + 0.5);
646         updateValue(val);
647     }
648     else
649     {
650         updateValue(std::numeric_limits<double>::quiet_NaN());
651     }
652 }
653 
getTotalCFM()654 double ExitAirTempSensor::getTotalCFM()
655 {
656     double sum = 0;
657     for (auto& sensor : cfmSensors)
658     {
659         double reading = 0;
660         if (!sensor->calculate(reading))
661         {
662             return -1;
663         }
664         sum += reading;
665     }
666 
667     return sum;
668 }
669 
calculate(double & val)670 bool ExitAirTempSensor::calculate(double& val)
671 {
672     constexpr size_t maxErrorPrint = 5;
673     static bool firstRead = false;
674     static size_t errorPrint = maxErrorPrint;
675 
676     double cfm = getTotalCFM();
677     if (cfm <= 0)
678     {
679         lg2::error("Error getting cfm");
680         return false;
681     }
682 
683     // Though cfm is not expected to be less than qMin normally,
684     // it is not a hard limit for exit air temp calculation.
685     // 50% qMin is chosen as a generic limit between providing
686     // a valid derived exit air temp and reporting exit air temp not available.
687     constexpr const double cfmLimitFactor = 0.5;
688     if (cfm < (qMin * cfmLimitFactor))
689     {
690         if (errorPrint > 0)
691         {
692             errorPrint--;
693             lg2::error("cfm '{CFM}' is too low, expected qMin '{QMIN}'", "CFM",
694                        cfm, "QMIN", qMin);
695         }
696         val = 0;
697         return false;
698     }
699 
700     // if there is an error getting inlet temp, return error
701     if (std::isnan(inletTemp))
702     {
703         if (errorPrint > 0)
704         {
705             errorPrint--;
706             lg2::error("Cannot get inlet temp");
707         }
708         val = 0;
709         return false;
710     }
711 
712     // if fans are off, just make the exit temp equal to inlet
713     if (!isPowerOn())
714     {
715         val = inletTemp;
716         return true;
717     }
718 
719     double totalPower = 0;
720     for (const auto& [path, reading] : powerReadings)
721     {
722         if (std::isnan(reading))
723         {
724             continue;
725         }
726         totalPower += reading;
727     }
728 
729     // Calculate power correction factor
730     // Ci = CL + (CH - CL)/(QMax - QMin) * (CFM - QMin)
731     double powerFactor = 0.0;
732     if (cfm <= qMin)
733     {
734         powerFactor = powerFactorMin;
735     }
736     else if (cfm >= qMax)
737     {
738         powerFactor = powerFactorMax;
739     }
740     else
741     {
742         powerFactor = powerFactorMin + ((powerFactorMax - powerFactorMin) /
743                                         (qMax - qMin) * (cfm - qMin));
744     }
745 
746     totalPower *= powerFactor;
747     totalPower += pOffset;
748 
749     if (totalPower == 0)
750     {
751         if (errorPrint > 0)
752         {
753             errorPrint--;
754             lg2::error("total power 0");
755         }
756         val = 0;
757         return false;
758     }
759 
760     lg2::debug(
761         "Power Factor: {POWER_FACTOR}, Inlet Temp: {INLET_TEMP}, Total Power: {TOTAL_POWER}",
762         "POWER_FACTOR", powerFactor, "INLET_TEMP", inletTemp, "TOTAL_POWER",
763         totalPower);
764 
765     // Calculate the exit air temp
766     // Texit = Tfp + (1.76 * TotalPower / CFM * Faltitude)
767     double reading = 1.76 * totalPower * altitudeFactor;
768     reading /= cfm;
769     reading += inletTemp;
770 
771     lg2::debug("Reading 1: '{VALUE}'", "VALUE", reading);
772 
773     // Now perform the exponential average
774     // Calculate alpha based on SDR values and CFM
775     // Ai = As + (Af - As)/(QMax - QMin) * (CFM - QMin)
776 
777     double alpha = 0.0;
778     if (cfm < qMin)
779     {
780         alpha = alphaS;
781     }
782     else if (cfm >= qMax)
783     {
784         alpha = alphaF;
785     }
786     else
787     {
788         alpha = alphaS + ((alphaF - alphaS) * (cfm - qMin) / (qMax - qMin));
789     }
790 
791     auto time = std::chrono::steady_clock::now();
792     if (!firstRead)
793     {
794         firstRead = true;
795         lastTime = time;
796         lastReading = reading;
797     }
798     double alphaDT =
799         std::chrono::duration_cast<std::chrono::seconds>(time - lastTime)
800             .count() *
801         alpha;
802 
803     // cap at 1.0 or the below fails
804     if (alphaDT > 1.0)
805     {
806         alphaDT = 1.0;
807     }
808 
809     lg2::debug("AlphaDT: '{ADT}'", "ADT", alphaDT);
810 
811     reading = ((reading * alphaDT) + (lastReading * (1.0 - alphaDT)));
812 
813     lg2::debug("Reading 2: '{VALUE}'", "VALUE", reading);
814 
815     val = reading;
816     lastReading = reading;
817     lastTime = time;
818     errorPrint = maxErrorPrint;
819     return true;
820 }
821 
checkThresholds()822 void ExitAirTempSensor::checkThresholds()
823 {
824     thresholds::checkThresholds(this);
825 }
826 
loadVariantPathArray(const SensorBaseConfigMap & data,const std::string & key,std::vector<std::string> & resp)827 static void loadVariantPathArray(const SensorBaseConfigMap& data,
828                                  const std::string& key,
829                                  std::vector<std::string>& resp)
830 {
831     auto it = data.find(key);
832     if (it == data.end())
833     {
834         lg2::error("Configuration missing '{KEY}'", "KEY", key);
835         throw std::invalid_argument("Key Missing");
836     }
837     BasicVariantType copy = it->second;
838     std::vector<std::string> config = std::get<std::vector<std::string>>(copy);
839     for (auto& str : config)
840     {
841         boost::replace_all(str, " ", "_");
842     }
843     resp = std::move(config);
844 }
845 
createSensor(sdbusplus::asio::object_server & objectServer,std::shared_ptr<ExitAirTempSensor> & exitAirSensor,std::shared_ptr<sdbusplus::asio::connection> & dbusConnection)846 void createSensor(sdbusplus::asio::object_server& objectServer,
847                   std::shared_ptr<ExitAirTempSensor>& exitAirSensor,
848                   std::shared_ptr<sdbusplus::asio::connection>& dbusConnection)
849 {
850     if (!dbusConnection)
851     {
852         lg2::error("Connection not created");
853         return;
854     }
855     auto getter = std::make_shared<GetSensorConfiguration>(
856         dbusConnection, [&objectServer, &dbusConnection,
857                          &exitAirSensor](const ManagedObjectType& resp) {
858             cfmSensors.clear();
859             for (const auto& [path, interfaces] : resp)
860             {
861                 for (const auto& [intf, cfg] : interfaces)
862                 {
863                     if (intf == configInterfaceName(exitAirType))
864                     {
865                         // thresholds should be under the same path
866                         std::vector<thresholds::Threshold> sensorThresholds;
867                         parseThresholdsFromConfig(interfaces, sensorThresholds);
868 
869                         std::string name =
870                             loadVariant<std::string>(cfg, "Name");
871                         exitAirSensor = nullptr;
872                         exitAirSensor = std::make_shared<ExitAirTempSensor>(
873                             dbusConnection, name, path.str, objectServer,
874                             std::move(sensorThresholds));
875                         exitAirSensor->powerFactorMin =
876                             loadVariant<double>(cfg, "PowerFactorMin");
877                         exitAirSensor->powerFactorMax =
878                             loadVariant<double>(cfg, "PowerFactorMax");
879                         exitAirSensor->qMin = loadVariant<double>(cfg, "QMin");
880                         exitAirSensor->qMax = loadVariant<double>(cfg, "QMax");
881                         exitAirSensor->alphaS =
882                             loadVariant<double>(cfg, "AlphaS");
883                         exitAirSensor->alphaF =
884                             loadVariant<double>(cfg, "AlphaF");
885                     }
886                     else if (intf == configInterfaceName(cfmType))
887                     {
888                         // thresholds should be under the same path
889                         std::vector<thresholds::Threshold> sensorThresholds;
890                         parseThresholdsFromConfig(interfaces, sensorThresholds);
891                         std::string name =
892                             loadVariant<std::string>(cfg, "Name");
893                         auto sensor = std::make_shared<CFMSensor>(
894                             dbusConnection, name, path.str, objectServer,
895                             std::move(sensorThresholds), exitAirSensor);
896                         loadVariantPathArray(cfg, "Tachs", sensor->tachs);
897                         sensor->maxCFM = loadVariant<double>(cfg, "MaxCFM");
898 
899                         // change these into percent upon getting the data
900                         sensor->c1 = loadVariant<double>(cfg, "C1") / 100;
901                         sensor->c2 = loadVariant<double>(cfg, "C2") / 100;
902                         sensor->tachMinPercent =
903                             loadVariant<double>(cfg, "TachMinPercent");
904                         sensor->tachMaxPercent =
905                             loadVariant<double>(cfg, "TachMaxPercent");
906                         sensor->createMaxCFMIface();
907                         sensor->setupMatches();
908 
909                         cfmSensors.emplace_back(std::move(sensor));
910                     }
911                 }
912             }
913             if (exitAirSensor)
914             {
915                 exitAirSensor->setupMatches();
916                 exitAirSensor->updateReading();
917             }
918         });
919     getter->getConfiguration(
920         std::vector<std::string>(monitorTypes.begin(), monitorTypes.end()));
921 }
922 
main()923 int main()
924 {
925     boost::asio::io_context io;
926     auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
927     sdbusplus::asio::object_server objectServer(systemBus, true);
928     objectServer.add_manager("/xyz/openbmc_project/sensors");
929     systemBus->request_name("xyz.openbmc_project.ExitAirTempSensor");
930     std::shared_ptr<ExitAirTempSensor> sensor =
931         nullptr; // wait until we find the config
932 
933     boost::asio::post(io,
934                       [&]() { createSensor(objectServer, sensor, systemBus); });
935 
936     boost::asio::steady_timer configTimer(io);
937 
938     std::function<void(sdbusplus::message_t&)> eventHandler =
939         [&](sdbusplus::message_t&) {
940             configTimer.expires_after(std::chrono::seconds(1));
941             // create a timer because normally multiple properties change
942             configTimer.async_wait([&](const boost::system::error_code& ec) {
943                 if (ec == boost::asio::error::operation_aborted)
944                 {
945                     return; // we're being canceled
946                 }
947                 createSensor(objectServer, sensor, systemBus);
948                 if (!sensor)
949                 {
950                     lg2::info("Configuration not detected");
951                 }
952             });
953         };
954     std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches =
955         setupPropertiesChangedMatches(*systemBus, monitorTypes, eventHandler);
956 
957     setupManufacturingModeMatch(*systemBus);
958     io.run();
959     return 0;
960 }
961