blob: 9f97c5d76b39f4b5023fbc42797b772ec83498a8 [file] [log] [blame]
James Feistbc896df2018-11-26 16:28:17 -08001/*
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 "Utils.hpp"
20#include "VariantVisitors.hpp"
21
22#include <math.h>
23
24#include <boost/algorithm/string/predicate.hpp>
25#include <boost/algorithm/string/replace.hpp>
26#include <chrono>
27#include <iostream>
28#include <limits>
29#include <numeric>
30#include <sdbusplus/asio/connection.hpp>
31#include <sdbusplus/asio/object_server.hpp>
32#include <vector>
33
34constexpr const float altitudeFactor = 1.14;
35constexpr const char* exitAirIface =
36 "xyz.openbmc_project.Configuration.ExitAirTempSensor";
37constexpr const char* cfmIface = "xyz.openbmc_project.Configuration.CFMSensor";
38
39// todo: this *might* need to be configurable
40constexpr const char* inletTemperatureSensor = "temperature/Front_Panel_Temp";
James Feistbc896df2018-11-26 16:28:17 -080041
42static constexpr bool DEBUG = false;
43
James Feistb2eb3f52018-12-04 16:17:50 -080044static constexpr double cfmMaxReading = 255;
45static constexpr double cfmMinReading = 0;
46
47static void setupSensorMatch(
48 std::vector<sdbusplus::bus::match::match>& matches,
49 sdbusplus::bus::bus& connection, const std::string& type,
50 std::function<void(const double&, sdbusplus::message::message&)>&& callback)
51{
52
53 std::function<void(sdbusplus::message::message & message)> eventHandler =
54 [callback{std::move(callback)}](sdbusplus::message::message& message) {
55 std::string objectName;
56 boost::container::flat_map<
57 std::string, sdbusplus::message::variant<double, int64_t>>
58 values;
59 message.read(objectName, values);
60 auto findValue = values.find("Value");
61 if (findValue == values.end())
62 {
63 return;
64 }
65 double value = sdbusplus::message::variant_ns::visit(
66 VariantToDoubleVisitor(), findValue->second);
67 callback(value, message);
68 };
69 matches.emplace_back(connection,
70 "type='signal',"
71 "member='PropertiesChanged',interface='org."
72 "freedesktop.DBus.Properties',path_"
73 "namespace='/xyz/openbmc_project/sensors/" +
74 std::string(type) +
75 "',arg0='xyz.openbmc_project.Sensor.Value'",
76 std::move(eventHandler));
77}
78
79template <typename T>
80static T loadVariant(
81 const boost::container::flat_map<std::string, BasicVariantType>& data,
82 const std::string& key)
83{
84 auto it = data.find(key);
85 if (it == data.end())
86 {
87 std::cerr << "Configuration missing " << key << "\n";
88 throw std::invalid_argument("Key Missing");
89 }
90 if constexpr (std::is_same_v<T, double>)
91 {
92 return sdbusplus::message::variant_ns::visit(VariantToDoubleVisitor(),
93 it->second);
94 }
95 else if constexpr (std::is_same_v<T, std::string>)
96 {
97 return sdbusplus::message::variant_ns::visit(VariantToStringVisitor(),
98 it->second);
99 }
100 else
101 {
102 static_assert("Type Not Implemented");
103 }
104}
105
106CFMSensor::CFMSensor(std::shared_ptr<sdbusplus::asio::connection>& conn,
107 const std::string& sensorName,
108 const std::string& sensorConfiguration,
109 sdbusplus::asio::object_server& objectServer,
110 std::vector<thresholds::Threshold>&& thresholds,
111 std::shared_ptr<ExitAirTempSensor>& parent) :
112 Sensor(boost::replace_all_copy(sensorName, " ", "_"),
113 "" /* todo: remove arg from base*/, std::move(thresholds),
114 sensorConfiguration, "xyz.openbmc_project.Configuration.ExitAirTemp",
115 cfmMaxReading, cfmMinReading),
116 dbusConnection(conn), parent(parent)
117{
118 sensorInterface =
119 objectServer.add_interface("/xyz/openbmc_project/sensors/cfm/" + name,
120 "xyz.openbmc_project.Sensor.Value");
121
122 if (thresholds::hasWarningInterface(thresholds))
123 {
124 thresholdInterfaceWarning = objectServer.add_interface(
125 "/xyz/openbmc_project/sensors/cfm/" + name,
126 "xyz.openbmc_project.Sensor.Threshold.Warning");
127 }
128 if (thresholds::hasCriticalInterface(thresholds))
129 {
130 thresholdInterfaceCritical = objectServer.add_interface(
131 "/xyz/openbmc_project/sensors/cfm/" + name,
132 "xyz.openbmc_project.Sensor.Threshold.Critical");
133 }
134 setInitialProperties(conn);
135 setupSensorMatch(
136 matches, *dbusConnection, "fan_tach",
137 std::move(
138 [this](const double& value, sdbusplus::message::message& message) {
139 tachReadings[message.get_path()] = value;
140 if (tachRanges.find(message.get_path()) == tachRanges.end())
141 {
142 // calls update reading after updating ranges
143 addTachRanges(message.get_sender(), message.get_path());
144 }
145 else
146 {
147 updateReading();
148 }
149 }));
150}
151
152void CFMSensor::addTachRanges(const std::string& serviceName,
153 const std::string& path)
154{
155 dbusConnection->async_method_call(
156 [this, path](const boost::system::error_code ec,
157 const boost::container::flat_map<std::string,
158 BasicVariantType>& data) {
159 if (ec)
160 {
161 std::cerr << "Error getting properties from " << path << "\n";
162 }
163
164 double max = loadVariant<double>(data, "MaxValue");
165 double min = loadVariant<double>(data, "MinValue");
166 tachRanges[path] = std::make_pair(min, max);
167 updateReading();
168 },
169 serviceName, path, "org.freedesktop.DBus.Properties", "GetAll",
170 "xyz.openbmc_project.Sensor.Value");
171}
172
173void CFMSensor::checkThresholds(void)
174{
175 thresholds::checkThresholds(this);
176}
177
178void CFMSensor::updateReading(void)
179{
180 double val = 0.0;
181 if (calculate(val))
182 {
183 if (value != val && parent)
184 {
185 parent->updateReading();
186 }
187 updateValue(val);
188 }
189 else
190 {
191 updateValue(std::numeric_limits<double>::quiet_NaN());
192 }
193}
194
195bool CFMSensor::calculate(double& value)
196{
197 double totalCFM = 0;
198 for (const std::string& tachName : tachs)
199 {
200 auto findReading = std::find_if(
201 tachReadings.begin(), tachReadings.end(), [&](const auto& item) {
202 return boost::ends_with(item.first, tachName);
203 });
204 auto findRange = std::find_if(
205 tachRanges.begin(), tachRanges.end(), [&](const auto& item) {
206 return boost::ends_with(item.first, tachName);
207 });
208 if (findReading == tachReadings.end())
209 {
210 std::cerr << "Can't find " << tachName << "in readings\n";
211 return false; // haven't gotten a reading
212 }
213
214 if (findRange == tachRanges.end())
215 {
216 std::cerr << "Can't find " << tachName << "in ranges\n";
217 return false; // haven't gotten a max / min
218 }
219
220 // avoid divide by 0
221 if (findRange->second.second == 0)
222 {
223 std::cerr << "Tach Max Set to 0 " << tachName << "\n";
224 return false;
225 }
226
227 double rpm = findReading->second;
228
229 // for now assume the min for a fan is always 0, divide by max to get
230 // percent and mult by 100
231 rpm /= findRange->second.second;
232 rpm *= 100;
233
234 if constexpr (DEBUG)
235 {
236 std::cout << "Tach " << tachName << "at " << rpm << "\n";
237 }
238
239 // Do a linear interpolation to get Ci
240 // Ci = C1 + (C2 - C1)/(RPM2 - RPM1) * (TACHi - TACH1)
241
242 double ci = 0;
243 if (rpm == 0)
244 {
245 ci = 0;
246 }
247 else if (rpm < tachMinPercent)
248 {
249 ci = c1;
250 }
251 else if (rpm > tachMaxPercent)
252 {
253 ci = c2;
254 }
255 else
256 {
257 ci = c1 + (((c2 - c1) * (rpm - tachMinPercent)) /
258 (tachMaxPercent - tachMinPercent));
259 }
260
261 // Now calculate the CFM for this tach
262 // CFMi = Ci * Qmaxi * TACHi
263 totalCFM += ci * maxCFM * rpm;
264 }
265
266 // divide by 100 since rpm is in percent
267 value = totalCFM / 100;
268}
269
270static constexpr double exitAirMaxReading = 127;
271static constexpr double exitAirMinReading = -128;
James Feistbc896df2018-11-26 16:28:17 -0800272ExitAirTempSensor::ExitAirTempSensor(
273 std::shared_ptr<sdbusplus::asio::connection>& conn,
James Feistb2eb3f52018-12-04 16:17:50 -0800274 const std::string& sensorName, const std::string& sensorConfiguration,
James Feistbc896df2018-11-26 16:28:17 -0800275 sdbusplus::asio::object_server& objectServer,
276 std::vector<thresholds::Threshold>&& thresholds) :
James Feistb2eb3f52018-12-04 16:17:50 -0800277 Sensor(boost::replace_all_copy(sensorName, " ", "_"),
278 "" /* todo: remove arg from base*/, std::move(thresholds),
279 sensorConfiguration, "xyz.openbmc_project.Configuration.ExitAirTemp",
280 exitAirMaxReading, exitAirMinReading),
James Feistbc896df2018-11-26 16:28:17 -0800281 dbusConnection(conn)
282{
283 sensorInterface = objectServer.add_interface(
284 "/xyz/openbmc_project/sensors/temperature/" + name,
285 "xyz.openbmc_project.Sensor.Value");
286
287 if (thresholds::hasWarningInterface(thresholds))
288 {
289 thresholdInterfaceWarning = objectServer.add_interface(
290 "/xyz/openbmc_project/sensors/temperature/" + name,
291 "xyz.openbmc_project.Sensor.Threshold.Warning");
292 }
293 if (thresholds::hasCriticalInterface(thresholds))
294 {
295 thresholdInterfaceCritical = objectServer.add_interface(
296 "/xyz/openbmc_project/sensors/temperature/" + name,
297 "xyz.openbmc_project.Sensor.Threshold.Critical");
298 }
299 setInitialProperties(conn);
300 setupMatches();
301}
302
303ExitAirTempSensor::~ExitAirTempSensor()
304{
305 // this sensor currently isn't destroyed so we don't care
306}
307
308void ExitAirTempSensor::setupMatches(void)
309{
310
James Feistb2eb3f52018-12-04 16:17:50 -0800311 constexpr const std::array<const char*, 2> matchTypes = {
312 "power", inletTemperatureSensor};
James Feistbc896df2018-11-26 16:28:17 -0800313
James Feistb2eb3f52018-12-04 16:17:50 -0800314 for (const std::string& type : matchTypes)
James Feistbc896df2018-11-26 16:28:17 -0800315 {
James Feistb2eb3f52018-12-04 16:17:50 -0800316 setupSensorMatch(matches, *dbusConnection, type,
317 [this, type](const double& value,
318 sdbusplus::message::message& message) {
319 if (type == "power")
320 {
321 powerReadings[message.get_path()] = value;
322 }
323 else if (type == inletTemperatureSensor)
324 {
325 inletTemp = value;
326 }
327 updateReading();
328 });
James Feistbc896df2018-11-26 16:28:17 -0800329 }
330}
331
332void ExitAirTempSensor::updateReading(void)
333{
334
335 double val = 0.0;
336 if (calculate(val))
337 {
338 updateValue(val);
339 }
340 else
341 {
342 updateValue(std::numeric_limits<double>::quiet_NaN());
343 }
344}
345
James Feistb2eb3f52018-12-04 16:17:50 -0800346double ExitAirTempSensor::getTotalCFM(void)
James Feistbc896df2018-11-26 16:28:17 -0800347{
James Feistb2eb3f52018-12-04 16:17:50 -0800348 double sum = 0;
349 for (auto& sensor : cfmSensors)
James Feistbc896df2018-11-26 16:28:17 -0800350 {
James Feistb2eb3f52018-12-04 16:17:50 -0800351 double reading = 0;
352 if (!sensor->calculate(reading))
James Feistbc896df2018-11-26 16:28:17 -0800353 {
James Feistbc896df2018-11-26 16:28:17 -0800354 return -1;
355 }
James Feistb2eb3f52018-12-04 16:17:50 -0800356 sum += reading;
James Feistbc896df2018-11-26 16:28:17 -0800357 }
James Feistb2eb3f52018-12-04 16:17:50 -0800358
359 return sum;
James Feistbc896df2018-11-26 16:28:17 -0800360}
361
362bool ExitAirTempSensor::calculate(double& val)
363{
364 static bool firstRead = false;
365 double cfm = getTotalCFM();
366 if (cfm <= 0)
367 {
368 std::cerr << "Error getting cfm\n";
369 return false;
370 }
371
372 // if there is an error getting inlet temp, return error
373 if (std::isnan(inletTemp))
374 {
375 std::cerr << "Cannot get inlet temp\n";
376 val = 0;
377 return false;
378 }
379
380 // if fans are off, just make the exit temp equal to inlet
381 if (!isPowerOn(dbusConnection))
382 {
383 val = inletTemp;
384 return true;
385 }
386
387 double totalPower = 0;
388 for (const auto& reading : powerReadings)
389 {
390 if (std::isnan(reading.second))
391 {
392 continue;
393 }
394 totalPower += reading.second;
395 }
396
397 // Calculate power correction factor
398 // Ci = CL + (CH - CL)/(QMax - QMin) * (CFM - QMin)
399 float powerFactor = 0.0;
400 if (cfm <= qMin)
401 {
402 powerFactor = powerFactorMin;
403 }
404 else if (cfm >= qMax)
405 {
406 powerFactor = powerFactorMax;
407 }
408 else
409 {
410 powerFactor = powerFactorMin + ((powerFactorMax - powerFactorMin) /
411 (qMax - qMin) * (cfm - qMin));
412 }
413
414 totalPower *= powerFactor;
415 totalPower += pOffset;
416
417 if (totalPower == 0)
418 {
419 std::cerr << "total power 0\n";
420 val = 0;
421 return false;
422 }
423
424 if constexpr (DEBUG)
425 {
426 std::cout << "Power Factor " << powerFactor << "\n";
427 std::cout << "Inlet Temp " << inletTemp << "\n";
428 std::cout << "Total Power" << totalPower << "\n";
429 }
430
431 // Calculate the exit air temp
432 // Texit = Tfp + (1.76 * TotalPower / CFM * Faltitude)
433 double reading = 1.76 * totalPower * altitudeFactor;
434 reading /= cfm;
435 reading += inletTemp;
436
437 if constexpr (DEBUG)
438 {
439 std::cout << "Reading 1: " << reading << "\n";
440 }
441
442 // Now perform the exponential average
443 // Calculate alpha based on SDR values and CFM
444 // Ai = As + (Af - As)/(QMax - QMin) * (CFM - QMin)
445
446 double alpha = 0.0;
447 if (cfm < qMin)
448 {
449 alpha = alphaS;
450 }
451 else if (cfm >= qMax)
452 {
453 alpha = alphaF;
454 }
455 else
456 {
457 alpha = alphaS + ((alphaF - alphaS) * (cfm - qMin) / (qMax - qMin));
458 }
459
460 auto time = std::chrono::system_clock::now();
461 if (!firstRead)
462 {
463 firstRead = true;
464 lastTime = time;
465 lastReading = reading;
466 }
467 double alphaDT =
468 std::chrono::duration_cast<std::chrono::seconds>(time - lastTime)
469 .count() *
470 alpha;
471
472 // cap at 1.0 or the below fails
473 if (alphaDT > 1.0)
474 {
475 alphaDT = 1.0;
476 }
477
478 if constexpr (DEBUG)
479 {
480 std::cout << "AlphaDT: " << alphaDT << "\n";
481 }
482
483 reading = ((reading * alphaDT) + (lastReading * (1.0 - alphaDT)));
484
485 if constexpr (DEBUG)
486 {
487 std::cout << "Reading 2: " << reading << "\n";
488 }
489
490 val = reading;
491 lastReading = reading;
492 lastTime = time;
493 return true;
494}
495
496void ExitAirTempSensor::checkThresholds(void)
497{
498 thresholds::checkThresholds(this);
499}
500
James Feistbc896df2018-11-26 16:28:17 -0800501static void loadVariantPathArray(
502 const boost::container::flat_map<std::string, BasicVariantType>& data,
503 const std::string& key, std::vector<std::string>& resp)
504{
505 auto it = data.find(key);
506 if (it == data.end())
507 {
508 std::cerr << "Configuration missing " << key << "\n";
509 throw std::invalid_argument("Key Missing");
510 }
511 BasicVariantType copy = it->second;
512 std::vector<std::string> config =
513 sdbusplus::message::variant_ns::get<std::vector<std::string>>(copy);
514 for (auto& str : config)
515 {
516 boost::replace_all(str, " ", "_");
517 }
518 resp = std::move(config);
519}
520
521void createSensor(sdbusplus::asio::object_server& objectServer,
James Feistb2eb3f52018-12-04 16:17:50 -0800522 std::shared_ptr<ExitAirTempSensor>& exitAirSensor,
James Feistbc896df2018-11-26 16:28:17 -0800523 std::shared_ptr<sdbusplus::asio::connection>& dbusConnection)
524{
525 if (!dbusConnection)
526 {
527 std::cerr << "Connection not created\n";
528 return;
529 }
530 dbusConnection->async_method_call(
531 [&](boost::system::error_code ec, const ManagedObjectType& resp) {
532 if (ec)
533 {
534 std::cerr << "Error contacting entity manager\n";
535 return;
536 }
James Feistb2eb3f52018-12-04 16:17:50 -0800537 std::vector<std::unique_ptr<CFMSensor>> cfmSensors;
James Feistbc896df2018-11-26 16:28:17 -0800538 for (const auto& pathPair : resp)
539 {
540 for (const auto& entry : pathPair.second)
541 {
542 if (entry.first == exitAirIface)
543 {
James Feistbc896df2018-11-26 16:28:17 -0800544 // thresholds should be under the same path
545 std::vector<thresholds::Threshold> sensorThresholds;
546 parseThresholdsFromConfig(pathPair.second,
547 sensorThresholds);
James Feistb2eb3f52018-12-04 16:17:50 -0800548 if (!exitAirSensor)
James Feistbc896df2018-11-26 16:28:17 -0800549 {
James Feistb2eb3f52018-12-04 16:17:50 -0800550 std::string name =
551 loadVariant<std::string>(entry.second, "Name");
552 exitAirSensor = std::make_shared<ExitAirTempSensor>(
553 dbusConnection, name, pathPair.first.str,
James Feistbc896df2018-11-26 16:28:17 -0800554 objectServer, std::move(sensorThresholds));
555 }
556 else
557 {
James Feistb2eb3f52018-12-04 16:17:50 -0800558 exitAirSensor->thresholds = sensorThresholds;
James Feistbc896df2018-11-26 16:28:17 -0800559 }
560
James Feistb2eb3f52018-12-04 16:17:50 -0800561 exitAirSensor->powerFactorMin =
562 loadVariant<double>(entry.second, "PowerFactorMin");
563 exitAirSensor->powerFactorMax =
564 loadVariant<double>(entry.second, "PowerFactorMax");
565 exitAirSensor->qMin =
566 loadVariant<double>(entry.second, "QMin");
567 exitAirSensor->qMax =
568 loadVariant<double>(entry.second, "QMax");
569 exitAirSensor->alphaS =
570 loadVariant<double>(entry.second, "AlphaS");
571 exitAirSensor->alphaF =
572 loadVariant<double>(entry.second, "AlphaF");
James Feistbc896df2018-11-26 16:28:17 -0800573 }
574 else if (entry.first == cfmIface)
575
576 {
James Feistb2eb3f52018-12-04 16:17:50 -0800577 // thresholds should be under the same path
578 std::vector<thresholds::Threshold> sensorThresholds;
579 parseThresholdsFromConfig(pathPair.second,
580 sensorThresholds);
581 std::string name =
582 loadVariant<std::string>(entry.second, "Name");
583 auto sensor = std::make_unique<CFMSensor>(
584 dbusConnection, name, pathPair.first.str,
585 objectServer, std::move(sensorThresholds),
586 exitAirSensor);
587 loadVariantPathArray(entry.second, "Tachs",
588 sensor->tachs);
589 sensor->maxCFM =
590 loadVariant<double>(entry.second, "MaxCFM");
James Feistbc896df2018-11-26 16:28:17 -0800591
592 // change these into percent upon getting the data
James Feistb2eb3f52018-12-04 16:17:50 -0800593 sensor->c1 =
594 loadVariant<double>(entry.second, "C1") / 100;
595 sensor->c2 =
596 loadVariant<double>(entry.second, "C2") / 100;
597 sensor->tachMinPercent =
598 loadVariant<double>(entry.second,
599 "TachMinPercent") /
James Feistbc896df2018-11-26 16:28:17 -0800600 100;
James Feistb2eb3f52018-12-04 16:17:50 -0800601 sensor->tachMaxPercent =
602 loadVariant<double>(entry.second,
603 "TachMaxPercent") /
James Feistbc896df2018-11-26 16:28:17 -0800604 100;
605
James Feistb2eb3f52018-12-04 16:17:50 -0800606 cfmSensors.emplace_back(std::move(sensor));
James Feistbc896df2018-11-26 16:28:17 -0800607 }
608 }
609 }
James Feistb2eb3f52018-12-04 16:17:50 -0800610 if (exitAirSensor)
James Feistbc896df2018-11-26 16:28:17 -0800611 {
James Feistb2eb3f52018-12-04 16:17:50 -0800612 exitAirSensor->cfmSensors = std::move(cfmSensors);
James Feistbc896df2018-11-26 16:28:17 -0800613
James Feistb2eb3f52018-12-04 16:17:50 -0800614 // todo: when power sensors are done delete this fake
615 // reading
616 exitAirSensor->powerReadings["foo"] = 144.0;
James Feistbc896df2018-11-26 16:28:17 -0800617
James Feistb2eb3f52018-12-04 16:17:50 -0800618 exitAirSensor->updateReading();
James Feistbc896df2018-11-26 16:28:17 -0800619 }
620 },
621 entityManagerName, "/", "org.freedesktop.DBus.ObjectManager",
622 "GetManagedObjects");
623}
624
625int main(int argc, char** argv)
626{
627
628 boost::asio::io_service io;
629 auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
630 systemBus->request_name("xyz.openbmc_project.ExitAirTempSensor");
631 sdbusplus::asio::object_server objectServer(systemBus);
632 std::shared_ptr<ExitAirTempSensor> sensor =
633 nullptr; // wait until we find the config
634 std::vector<std::unique_ptr<sdbusplus::bus::match::match>> matches;
635
636 io.post([&]() { createSensor(objectServer, sensor, systemBus); });
637
638 boost::asio::deadline_timer configTimer(io);
639
640 std::function<void(sdbusplus::message::message&)> eventHandler =
641 [&](sdbusplus::message::message& message) {
642 configTimer.expires_from_now(boost::posix_time::seconds(1));
643 // create a timer because normally multiple properties change
644 configTimer.async_wait([&](const boost::system::error_code& ec) {
645 if (ec == boost::asio::error::operation_aborted)
646 {
647 return; // we're being canceled
648 }
649 createSensor(objectServer, sensor, systemBus);
650 if (!sensor)
651 {
652 std::cout << "Configuration not detected\n";
653 }
654 });
655 };
656 constexpr const std::array<const char*, 2> monitorIfaces = {exitAirIface,
657 cfmIface};
658 for (const char* type : monitorIfaces)
659 {
660 auto match = std::make_unique<sdbusplus::bus::match::match>(
661 static_cast<sdbusplus::bus::bus&>(*systemBus),
662 "type='signal',member='PropertiesChanged',path_namespace='" +
663 std::string(inventoryPath) + "',arg0namespace='" + type + "'",
664 eventHandler);
665 matches.emplace_back(std::move(match));
666 }
667
668 io.run();
669}