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