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