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