blob: 510e59565bad1c465c8de7ff55d5db08087b868e [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
James Feistb2eb3f52018-12-04 16:17:50 -080079CFMSensor::CFMSensor(std::shared_ptr<sdbusplus::asio::connection>& conn,
80 const std::string& sensorName,
81 const std::string& sensorConfiguration,
82 sdbusplus::asio::object_server& objectServer,
83 std::vector<thresholds::Threshold>&& thresholds,
84 std::shared_ptr<ExitAirTempSensor>& parent) :
85 Sensor(boost::replace_all_copy(sensorName, " ", "_"),
86 "" /* todo: remove arg from base*/, std::move(thresholds),
87 sensorConfiguration, "xyz.openbmc_project.Configuration.ExitAirTemp",
88 cfmMaxReading, cfmMinReading),
89 dbusConnection(conn), parent(parent)
90{
91 sensorInterface =
92 objectServer.add_interface("/xyz/openbmc_project/sensors/cfm/" + name,
93 "xyz.openbmc_project.Sensor.Value");
94
95 if (thresholds::hasWarningInterface(thresholds))
96 {
97 thresholdInterfaceWarning = objectServer.add_interface(
98 "/xyz/openbmc_project/sensors/cfm/" + name,
99 "xyz.openbmc_project.Sensor.Threshold.Warning");
100 }
101 if (thresholds::hasCriticalInterface(thresholds))
102 {
103 thresholdInterfaceCritical = objectServer.add_interface(
104 "/xyz/openbmc_project/sensors/cfm/" + name,
105 "xyz.openbmc_project.Sensor.Threshold.Critical");
106 }
107 setInitialProperties(conn);
108 setupSensorMatch(
109 matches, *dbusConnection, "fan_tach",
110 std::move(
111 [this](const double& value, sdbusplus::message::message& message) {
112 tachReadings[message.get_path()] = value;
113 if (tachRanges.find(message.get_path()) == tachRanges.end())
114 {
115 // calls update reading after updating ranges
116 addTachRanges(message.get_sender(), message.get_path());
117 }
118 else
119 {
120 updateReading();
121 }
122 }));
123}
124
125void CFMSensor::addTachRanges(const std::string& serviceName,
126 const std::string& path)
127{
128 dbusConnection->async_method_call(
129 [this, path](const boost::system::error_code ec,
130 const boost::container::flat_map<std::string,
131 BasicVariantType>& data) {
132 if (ec)
133 {
134 std::cerr << "Error getting properties from " << path << "\n";
135 }
136
137 double max = loadVariant<double>(data, "MaxValue");
138 double min = loadVariant<double>(data, "MinValue");
139 tachRanges[path] = std::make_pair(min, max);
140 updateReading();
141 },
142 serviceName, path, "org.freedesktop.DBus.Properties", "GetAll",
143 "xyz.openbmc_project.Sensor.Value");
144}
145
146void CFMSensor::checkThresholds(void)
147{
148 thresholds::checkThresholds(this);
149}
150
151void CFMSensor::updateReading(void)
152{
153 double val = 0.0;
154 if (calculate(val))
155 {
156 if (value != val && parent)
157 {
158 parent->updateReading();
159 }
160 updateValue(val);
161 }
162 else
163 {
164 updateValue(std::numeric_limits<double>::quiet_NaN());
165 }
166}
167
168bool CFMSensor::calculate(double& value)
169{
170 double totalCFM = 0;
171 for (const std::string& tachName : tachs)
172 {
173 auto findReading = std::find_if(
174 tachReadings.begin(), tachReadings.end(), [&](const auto& item) {
175 return boost::ends_with(item.first, tachName);
176 });
177 auto findRange = std::find_if(
178 tachRanges.begin(), tachRanges.end(), [&](const auto& item) {
179 return boost::ends_with(item.first, tachName);
180 });
181 if (findReading == tachReadings.end())
182 {
183 std::cerr << "Can't find " << tachName << "in readings\n";
184 return false; // haven't gotten a reading
185 }
186
187 if (findRange == tachRanges.end())
188 {
189 std::cerr << "Can't find " << tachName << "in ranges\n";
190 return false; // haven't gotten a max / min
191 }
192
193 // avoid divide by 0
194 if (findRange->second.second == 0)
195 {
196 std::cerr << "Tach Max Set to 0 " << tachName << "\n";
197 return false;
198 }
199
200 double rpm = findReading->second;
201
202 // for now assume the min for a fan is always 0, divide by max to get
203 // percent and mult by 100
204 rpm /= findRange->second.second;
205 rpm *= 100;
206
207 if constexpr (DEBUG)
208 {
209 std::cout << "Tach " << tachName << "at " << rpm << "\n";
210 }
211
212 // Do a linear interpolation to get Ci
213 // Ci = C1 + (C2 - C1)/(RPM2 - RPM1) * (TACHi - TACH1)
214
215 double ci = 0;
216 if (rpm == 0)
217 {
218 ci = 0;
219 }
220 else if (rpm < tachMinPercent)
221 {
222 ci = c1;
223 }
224 else if (rpm > tachMaxPercent)
225 {
226 ci = c2;
227 }
228 else
229 {
230 ci = c1 + (((c2 - c1) * (rpm - tachMinPercent)) /
231 (tachMaxPercent - tachMinPercent));
232 }
233
234 // Now calculate the CFM for this tach
235 // CFMi = Ci * Qmaxi * TACHi
236 totalCFM += ci * maxCFM * rpm;
237 }
238
239 // divide by 100 since rpm is in percent
240 value = totalCFM / 100;
241}
242
243static constexpr double exitAirMaxReading = 127;
244static constexpr double exitAirMinReading = -128;
James Feistbc896df2018-11-26 16:28:17 -0800245ExitAirTempSensor::ExitAirTempSensor(
246 std::shared_ptr<sdbusplus::asio::connection>& conn,
James Feistb2eb3f52018-12-04 16:17:50 -0800247 const std::string& sensorName, const std::string& sensorConfiguration,
James Feistbc896df2018-11-26 16:28:17 -0800248 sdbusplus::asio::object_server& objectServer,
249 std::vector<thresholds::Threshold>&& thresholds) :
James Feistb2eb3f52018-12-04 16:17:50 -0800250 Sensor(boost::replace_all_copy(sensorName, " ", "_"),
251 "" /* todo: remove arg from base*/, std::move(thresholds),
252 sensorConfiguration, "xyz.openbmc_project.Configuration.ExitAirTemp",
253 exitAirMaxReading, exitAirMinReading),
James Feistbc896df2018-11-26 16:28:17 -0800254 dbusConnection(conn)
255{
256 sensorInterface = objectServer.add_interface(
257 "/xyz/openbmc_project/sensors/temperature/" + name,
258 "xyz.openbmc_project.Sensor.Value");
259
260 if (thresholds::hasWarningInterface(thresholds))
261 {
262 thresholdInterfaceWarning = objectServer.add_interface(
263 "/xyz/openbmc_project/sensors/temperature/" + name,
264 "xyz.openbmc_project.Sensor.Threshold.Warning");
265 }
266 if (thresholds::hasCriticalInterface(thresholds))
267 {
268 thresholdInterfaceCritical = objectServer.add_interface(
269 "/xyz/openbmc_project/sensors/temperature/" + name,
270 "xyz.openbmc_project.Sensor.Threshold.Critical");
271 }
272 setInitialProperties(conn);
273 setupMatches();
274}
275
276ExitAirTempSensor::~ExitAirTempSensor()
277{
278 // this sensor currently isn't destroyed so we don't care
279}
280
281void ExitAirTempSensor::setupMatches(void)
282{
283
James Feistb2eb3f52018-12-04 16:17:50 -0800284 constexpr const std::array<const char*, 2> matchTypes = {
285 "power", inletTemperatureSensor};
James Feistbc896df2018-11-26 16:28:17 -0800286
James Feistb2eb3f52018-12-04 16:17:50 -0800287 for (const std::string& type : matchTypes)
James Feistbc896df2018-11-26 16:28:17 -0800288 {
James Feistb2eb3f52018-12-04 16:17:50 -0800289 setupSensorMatch(matches, *dbusConnection, type,
290 [this, type](const double& value,
291 sdbusplus::message::message& message) {
292 if (type == "power")
293 {
294 powerReadings[message.get_path()] = value;
295 }
296 else if (type == inletTemperatureSensor)
297 {
298 inletTemp = value;
299 }
300 updateReading();
301 });
James Feistbc896df2018-11-26 16:28:17 -0800302 }
303}
304
305void ExitAirTempSensor::updateReading(void)
306{
307
308 double val = 0.0;
309 if (calculate(val))
310 {
311 updateValue(val);
312 }
313 else
314 {
315 updateValue(std::numeric_limits<double>::quiet_NaN());
316 }
317}
318
James Feistb2eb3f52018-12-04 16:17:50 -0800319double ExitAirTempSensor::getTotalCFM(void)
James Feistbc896df2018-11-26 16:28:17 -0800320{
James Feistb2eb3f52018-12-04 16:17:50 -0800321 double sum = 0;
322 for (auto& sensor : cfmSensors)
James Feistbc896df2018-11-26 16:28:17 -0800323 {
James Feistb2eb3f52018-12-04 16:17:50 -0800324 double reading = 0;
325 if (!sensor->calculate(reading))
James Feistbc896df2018-11-26 16:28:17 -0800326 {
James Feistbc896df2018-11-26 16:28:17 -0800327 return -1;
328 }
James Feistb2eb3f52018-12-04 16:17:50 -0800329 sum += reading;
James Feistbc896df2018-11-26 16:28:17 -0800330 }
James Feistb2eb3f52018-12-04 16:17:50 -0800331
332 return sum;
James Feistbc896df2018-11-26 16:28:17 -0800333}
334
335bool ExitAirTempSensor::calculate(double& val)
336{
337 static bool firstRead = false;
338 double cfm = getTotalCFM();
339 if (cfm <= 0)
340 {
341 std::cerr << "Error getting cfm\n";
342 return false;
343 }
344
345 // if there is an error getting inlet temp, return error
346 if (std::isnan(inletTemp))
347 {
348 std::cerr << "Cannot get inlet temp\n";
349 val = 0;
350 return false;
351 }
352
353 // if fans are off, just make the exit temp equal to inlet
354 if (!isPowerOn(dbusConnection))
355 {
356 val = inletTemp;
357 return true;
358 }
359
360 double totalPower = 0;
361 for (const auto& reading : powerReadings)
362 {
363 if (std::isnan(reading.second))
364 {
365 continue;
366 }
367 totalPower += reading.second;
368 }
369
370 // Calculate power correction factor
371 // Ci = CL + (CH - CL)/(QMax - QMin) * (CFM - QMin)
372 float powerFactor = 0.0;
373 if (cfm <= qMin)
374 {
375 powerFactor = powerFactorMin;
376 }
377 else if (cfm >= qMax)
378 {
379 powerFactor = powerFactorMax;
380 }
381 else
382 {
383 powerFactor = powerFactorMin + ((powerFactorMax - powerFactorMin) /
384 (qMax - qMin) * (cfm - qMin));
385 }
386
387 totalPower *= powerFactor;
388 totalPower += pOffset;
389
390 if (totalPower == 0)
391 {
392 std::cerr << "total power 0\n";
393 val = 0;
394 return false;
395 }
396
397 if constexpr (DEBUG)
398 {
399 std::cout << "Power Factor " << powerFactor << "\n";
400 std::cout << "Inlet Temp " << inletTemp << "\n";
401 std::cout << "Total Power" << totalPower << "\n";
402 }
403
404 // Calculate the exit air temp
405 // Texit = Tfp + (1.76 * TotalPower / CFM * Faltitude)
406 double reading = 1.76 * totalPower * altitudeFactor;
407 reading /= cfm;
408 reading += inletTemp;
409
410 if constexpr (DEBUG)
411 {
412 std::cout << "Reading 1: " << reading << "\n";
413 }
414
415 // Now perform the exponential average
416 // Calculate alpha based on SDR values and CFM
417 // Ai = As + (Af - As)/(QMax - QMin) * (CFM - QMin)
418
419 double alpha = 0.0;
420 if (cfm < qMin)
421 {
422 alpha = alphaS;
423 }
424 else if (cfm >= qMax)
425 {
426 alpha = alphaF;
427 }
428 else
429 {
430 alpha = alphaS + ((alphaF - alphaS) * (cfm - qMin) / (qMax - qMin));
431 }
432
433 auto time = std::chrono::system_clock::now();
434 if (!firstRead)
435 {
436 firstRead = true;
437 lastTime = time;
438 lastReading = reading;
439 }
440 double alphaDT =
441 std::chrono::duration_cast<std::chrono::seconds>(time - lastTime)
442 .count() *
443 alpha;
444
445 // cap at 1.0 or the below fails
446 if (alphaDT > 1.0)
447 {
448 alphaDT = 1.0;
449 }
450
451 if constexpr (DEBUG)
452 {
453 std::cout << "AlphaDT: " << alphaDT << "\n";
454 }
455
456 reading = ((reading * alphaDT) + (lastReading * (1.0 - alphaDT)));
457
458 if constexpr (DEBUG)
459 {
460 std::cout << "Reading 2: " << reading << "\n";
461 }
462
463 val = reading;
464 lastReading = reading;
465 lastTime = time;
466 return true;
467}
468
469void ExitAirTempSensor::checkThresholds(void)
470{
471 thresholds::checkThresholds(this);
472}
473
James Feistbc896df2018-11-26 16:28:17 -0800474static void loadVariantPathArray(
475 const boost::container::flat_map<std::string, BasicVariantType>& data,
476 const std::string& key, std::vector<std::string>& resp)
477{
478 auto it = data.find(key);
479 if (it == data.end())
480 {
481 std::cerr << "Configuration missing " << key << "\n";
482 throw std::invalid_argument("Key Missing");
483 }
484 BasicVariantType copy = it->second;
485 std::vector<std::string> config =
486 sdbusplus::message::variant_ns::get<std::vector<std::string>>(copy);
487 for (auto& str : config)
488 {
489 boost::replace_all(str, " ", "_");
490 }
491 resp = std::move(config);
492}
493
494void createSensor(sdbusplus::asio::object_server& objectServer,
James Feistb2eb3f52018-12-04 16:17:50 -0800495 std::shared_ptr<ExitAirTempSensor>& exitAirSensor,
James Feistbc896df2018-11-26 16:28:17 -0800496 std::shared_ptr<sdbusplus::asio::connection>& dbusConnection)
497{
498 if (!dbusConnection)
499 {
500 std::cerr << "Connection not created\n";
501 return;
502 }
503 dbusConnection->async_method_call(
504 [&](boost::system::error_code ec, const ManagedObjectType& resp) {
505 if (ec)
506 {
507 std::cerr << "Error contacting entity manager\n";
508 return;
509 }
James Feistb2eb3f52018-12-04 16:17:50 -0800510 std::vector<std::unique_ptr<CFMSensor>> cfmSensors;
James Feistbc896df2018-11-26 16:28:17 -0800511 for (const auto& pathPair : resp)
512 {
513 for (const auto& entry : pathPair.second)
514 {
515 if (entry.first == exitAirIface)
516 {
James Feistbc896df2018-11-26 16:28:17 -0800517 // thresholds should be under the same path
518 std::vector<thresholds::Threshold> sensorThresholds;
519 parseThresholdsFromConfig(pathPair.second,
520 sensorThresholds);
James Feistb2eb3f52018-12-04 16:17:50 -0800521 if (!exitAirSensor)
James Feistbc896df2018-11-26 16:28:17 -0800522 {
James Feistb2eb3f52018-12-04 16:17:50 -0800523 std::string name =
524 loadVariant<std::string>(entry.second, "Name");
525 exitAirSensor = std::make_shared<ExitAirTempSensor>(
526 dbusConnection, name, pathPair.first.str,
James Feistbc896df2018-11-26 16:28:17 -0800527 objectServer, std::move(sensorThresholds));
528 }
529 else
530 {
James Feistb2eb3f52018-12-04 16:17:50 -0800531 exitAirSensor->thresholds = sensorThresholds;
James Feistbc896df2018-11-26 16:28:17 -0800532 }
533
James Feistb2eb3f52018-12-04 16:17:50 -0800534 exitAirSensor->powerFactorMin =
535 loadVariant<double>(entry.second, "PowerFactorMin");
536 exitAirSensor->powerFactorMax =
537 loadVariant<double>(entry.second, "PowerFactorMax");
538 exitAirSensor->qMin =
539 loadVariant<double>(entry.second, "QMin");
540 exitAirSensor->qMax =
541 loadVariant<double>(entry.second, "QMax");
542 exitAirSensor->alphaS =
543 loadVariant<double>(entry.second, "AlphaS");
544 exitAirSensor->alphaF =
545 loadVariant<double>(entry.second, "AlphaF");
James Feistbc896df2018-11-26 16:28:17 -0800546 }
547 else if (entry.first == cfmIface)
548
549 {
James Feistb2eb3f52018-12-04 16:17:50 -0800550 // thresholds should be under the same path
551 std::vector<thresholds::Threshold> sensorThresholds;
552 parseThresholdsFromConfig(pathPair.second,
553 sensorThresholds);
554 std::string name =
555 loadVariant<std::string>(entry.second, "Name");
556 auto sensor = std::make_unique<CFMSensor>(
557 dbusConnection, name, pathPair.first.str,
558 objectServer, std::move(sensorThresholds),
559 exitAirSensor);
560 loadVariantPathArray(entry.second, "Tachs",
561 sensor->tachs);
562 sensor->maxCFM =
563 loadVariant<double>(entry.second, "MaxCFM");
James Feistbc896df2018-11-26 16:28:17 -0800564
565 // change these into percent upon getting the data
James Feistb2eb3f52018-12-04 16:17:50 -0800566 sensor->c1 =
567 loadVariant<double>(entry.second, "C1") / 100;
568 sensor->c2 =
569 loadVariant<double>(entry.second, "C2") / 100;
570 sensor->tachMinPercent =
571 loadVariant<double>(entry.second,
572 "TachMinPercent") /
James Feistbc896df2018-11-26 16:28:17 -0800573 100;
James Feistb2eb3f52018-12-04 16:17:50 -0800574 sensor->tachMaxPercent =
575 loadVariant<double>(entry.second,
576 "TachMaxPercent") /
James Feistbc896df2018-11-26 16:28:17 -0800577 100;
578
James Feistb2eb3f52018-12-04 16:17:50 -0800579 cfmSensors.emplace_back(std::move(sensor));
James Feistbc896df2018-11-26 16:28:17 -0800580 }
581 }
582 }
James Feistb2eb3f52018-12-04 16:17:50 -0800583 if (exitAirSensor)
James Feistbc896df2018-11-26 16:28:17 -0800584 {
James Feistb2eb3f52018-12-04 16:17:50 -0800585 exitAirSensor->cfmSensors = std::move(cfmSensors);
James Feistbc896df2018-11-26 16:28:17 -0800586
James Feistb2eb3f52018-12-04 16:17:50 -0800587 // todo: when power sensors are done delete this fake
588 // reading
589 exitAirSensor->powerReadings["foo"] = 144.0;
James Feistbc896df2018-11-26 16:28:17 -0800590
James Feistb2eb3f52018-12-04 16:17:50 -0800591 exitAirSensor->updateReading();
James Feistbc896df2018-11-26 16:28:17 -0800592 }
593 },
594 entityManagerName, "/", "org.freedesktop.DBus.ObjectManager",
595 "GetManagedObjects");
596}
597
598int main(int argc, char** argv)
599{
600
601 boost::asio::io_service io;
602 auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
603 systemBus->request_name("xyz.openbmc_project.ExitAirTempSensor");
604 sdbusplus::asio::object_server objectServer(systemBus);
605 std::shared_ptr<ExitAirTempSensor> sensor =
606 nullptr; // wait until we find the config
607 std::vector<std::unique_ptr<sdbusplus::bus::match::match>> matches;
608
609 io.post([&]() { createSensor(objectServer, sensor, systemBus); });
610
611 boost::asio::deadline_timer configTimer(io);
612
613 std::function<void(sdbusplus::message::message&)> eventHandler =
614 [&](sdbusplus::message::message& message) {
615 configTimer.expires_from_now(boost::posix_time::seconds(1));
616 // create a timer because normally multiple properties change
617 configTimer.async_wait([&](const boost::system::error_code& ec) {
618 if (ec == boost::asio::error::operation_aborted)
619 {
620 return; // we're being canceled
621 }
622 createSensor(objectServer, sensor, systemBus);
623 if (!sensor)
624 {
625 std::cout << "Configuration not detected\n";
626 }
627 });
628 };
629 constexpr const std::array<const char*, 2> monitorIfaces = {exitAirIface,
630 cfmIface};
631 for (const char* type : monitorIfaces)
632 {
633 auto match = std::make_unique<sdbusplus::bus::match::match>(
634 static_cast<sdbusplus::bus::bus&>(*systemBus),
635 "type='signal',member='PropertiesChanged',path_namespace='" +
636 std::string(inventoryPath) + "',arg0namespace='" + type + "'",
637 eventHandler);
638 matches.emplace_back(std::move(match));
639 }
640
641 io.run();
642}