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