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