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