blob: 9e493f0f9e18d9229e49ac2445ca5333b6cd2035 [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";
James Feist1ccdb5e2019-01-24 09:44:01 -0800135 return;
James Feistb2eb3f52018-12-04 16:17:50 -0800136 }
137
138 double max = loadVariant<double>(data, "MaxValue");
139 double min = loadVariant<double>(data, "MinValue");
140 tachRanges[path] = std::make_pair(min, max);
141 updateReading();
142 },
143 serviceName, path, "org.freedesktop.DBus.Properties", "GetAll",
144 "xyz.openbmc_project.Sensor.Value");
145}
146
147void CFMSensor::checkThresholds(void)
148{
149 thresholds::checkThresholds(this);
150}
151
152void CFMSensor::updateReading(void)
153{
154 double val = 0.0;
155 if (calculate(val))
156 {
157 if (value != val && parent)
158 {
159 parent->updateReading();
160 }
161 updateValue(val);
162 }
163 else
164 {
165 updateValue(std::numeric_limits<double>::quiet_NaN());
166 }
167}
168
169bool CFMSensor::calculate(double& value)
170{
171 double totalCFM = 0;
172 for (const std::string& tachName : tachs)
173 {
174 auto findReading = std::find_if(
175 tachReadings.begin(), tachReadings.end(), [&](const auto& item) {
176 return boost::ends_with(item.first, tachName);
177 });
178 auto findRange = std::find_if(
179 tachRanges.begin(), tachRanges.end(), [&](const auto& item) {
180 return boost::ends_with(item.first, tachName);
181 });
182 if (findReading == tachReadings.end())
183 {
184 std::cerr << "Can't find " << tachName << "in readings\n";
185 return false; // haven't gotten a reading
186 }
187
188 if (findRange == tachRanges.end())
189 {
190 std::cerr << "Can't find " << tachName << "in ranges\n";
191 return false; // haven't gotten a max / min
192 }
193
194 // avoid divide by 0
195 if (findRange->second.second == 0)
196 {
197 std::cerr << "Tach Max Set to 0 " << tachName << "\n";
198 return false;
199 }
200
201 double rpm = findReading->second;
202
203 // for now assume the min for a fan is always 0, divide by max to get
204 // percent and mult by 100
205 rpm /= findRange->second.second;
206 rpm *= 100;
207
208 if constexpr (DEBUG)
209 {
210 std::cout << "Tach " << tachName << "at " << rpm << "\n";
211 }
212
213 // Do a linear interpolation to get Ci
214 // Ci = C1 + (C2 - C1)/(RPM2 - RPM1) * (TACHi - TACH1)
215
216 double ci = 0;
217 if (rpm == 0)
218 {
219 ci = 0;
220 }
221 else if (rpm < tachMinPercent)
222 {
223 ci = c1;
224 }
225 else if (rpm > tachMaxPercent)
226 {
227 ci = c2;
228 }
229 else
230 {
231 ci = c1 + (((c2 - c1) * (rpm - tachMinPercent)) /
232 (tachMaxPercent - tachMinPercent));
233 }
234
235 // Now calculate the CFM for this tach
236 // CFMi = Ci * Qmaxi * TACHi
237 totalCFM += ci * maxCFM * rpm;
238 }
239
240 // divide by 100 since rpm is in percent
241 value = totalCFM / 100;
242}
243
244static constexpr double exitAirMaxReading = 127;
245static constexpr double exitAirMinReading = -128;
James Feistbc896df2018-11-26 16:28:17 -0800246ExitAirTempSensor::ExitAirTempSensor(
247 std::shared_ptr<sdbusplus::asio::connection>& conn,
James Feistb2eb3f52018-12-04 16:17:50 -0800248 const std::string& sensorName, const std::string& sensorConfiguration,
James Feistbc896df2018-11-26 16:28:17 -0800249 sdbusplus::asio::object_server& objectServer,
250 std::vector<thresholds::Threshold>&& thresholds) :
James Feistb2eb3f52018-12-04 16:17:50 -0800251 Sensor(boost::replace_all_copy(sensorName, " ", "_"),
252 "" /* todo: remove arg from base*/, std::move(thresholds),
253 sensorConfiguration, "xyz.openbmc_project.Configuration.ExitAirTemp",
254 exitAirMaxReading, exitAirMinReading),
James Feistbc896df2018-11-26 16:28:17 -0800255 dbusConnection(conn)
256{
257 sensorInterface = objectServer.add_interface(
258 "/xyz/openbmc_project/sensors/temperature/" + name,
259 "xyz.openbmc_project.Sensor.Value");
260
261 if (thresholds::hasWarningInterface(thresholds))
262 {
263 thresholdInterfaceWarning = objectServer.add_interface(
264 "/xyz/openbmc_project/sensors/temperature/" + name,
265 "xyz.openbmc_project.Sensor.Threshold.Warning");
266 }
267 if (thresholds::hasCriticalInterface(thresholds))
268 {
269 thresholdInterfaceCritical = objectServer.add_interface(
270 "/xyz/openbmc_project/sensors/temperature/" + name,
271 "xyz.openbmc_project.Sensor.Threshold.Critical");
272 }
273 setInitialProperties(conn);
274 setupMatches();
James Feist71d31b22019-01-02 16:57:54 -0800275 setupPowerMatch(conn);
James Feistbc896df2018-11-26 16:28:17 -0800276}
277
278ExitAirTempSensor::~ExitAirTempSensor()
279{
280 // this sensor currently isn't destroyed so we don't care
281}
282
283void ExitAirTempSensor::setupMatches(void)
284{
285
James Feistb2eb3f52018-12-04 16:17:50 -0800286 constexpr const std::array<const char*, 2> matchTypes = {
287 "power", inletTemperatureSensor};
James Feistbc896df2018-11-26 16:28:17 -0800288
James Feistb2eb3f52018-12-04 16:17:50 -0800289 for (const std::string& type : matchTypes)
James Feistbc896df2018-11-26 16:28:17 -0800290 {
James Feistb2eb3f52018-12-04 16:17:50 -0800291 setupSensorMatch(matches, *dbusConnection, type,
292 [this, type](const double& value,
293 sdbusplus::message::message& message) {
294 if (type == "power")
295 {
296 powerReadings[message.get_path()] = value;
297 }
298 else if (type == inletTemperatureSensor)
299 {
300 inletTemp = value;
301 }
302 updateReading();
303 });
James Feistbc896df2018-11-26 16:28:17 -0800304 }
305}
306
307void ExitAirTempSensor::updateReading(void)
308{
309
310 double val = 0.0;
311 if (calculate(val))
312 {
313 updateValue(val);
314 }
315 else
316 {
317 updateValue(std::numeric_limits<double>::quiet_NaN());
318 }
319}
320
James Feistb2eb3f52018-12-04 16:17:50 -0800321double ExitAirTempSensor::getTotalCFM(void)
James Feistbc896df2018-11-26 16:28:17 -0800322{
James Feistb2eb3f52018-12-04 16:17:50 -0800323 double sum = 0;
324 for (auto& sensor : cfmSensors)
James Feistbc896df2018-11-26 16:28:17 -0800325 {
James Feistb2eb3f52018-12-04 16:17:50 -0800326 double reading = 0;
327 if (!sensor->calculate(reading))
James Feistbc896df2018-11-26 16:28:17 -0800328 {
James Feistbc896df2018-11-26 16:28:17 -0800329 return -1;
330 }
James Feistb2eb3f52018-12-04 16:17:50 -0800331 sum += reading;
James Feistbc896df2018-11-26 16:28:17 -0800332 }
James Feistb2eb3f52018-12-04 16:17:50 -0800333
334 return sum;
James Feistbc896df2018-11-26 16:28:17 -0800335}
336
337bool ExitAirTempSensor::calculate(double& val)
338{
339 static bool firstRead = false;
340 double cfm = getTotalCFM();
341 if (cfm <= 0)
342 {
343 std::cerr << "Error getting cfm\n";
344 return false;
345 }
346
347 // if there is an error getting inlet temp, return error
348 if (std::isnan(inletTemp))
349 {
350 std::cerr << "Cannot get inlet temp\n";
351 val = 0;
352 return false;
353 }
354
355 // if fans are off, just make the exit temp equal to inlet
James Feist71d31b22019-01-02 16:57:54 -0800356 if (!isPowerOn())
James Feistbc896df2018-11-26 16:28:17 -0800357 {
358 val = inletTemp;
359 return true;
360 }
361
362 double totalPower = 0;
363 for (const auto& reading : powerReadings)
364 {
365 if (std::isnan(reading.second))
366 {
367 continue;
368 }
369 totalPower += reading.second;
370 }
371
372 // Calculate power correction factor
373 // Ci = CL + (CH - CL)/(QMax - QMin) * (CFM - QMin)
374 float powerFactor = 0.0;
375 if (cfm <= qMin)
376 {
377 powerFactor = powerFactorMin;
378 }
379 else if (cfm >= qMax)
380 {
381 powerFactor = powerFactorMax;
382 }
383 else
384 {
385 powerFactor = powerFactorMin + ((powerFactorMax - powerFactorMin) /
386 (qMax - qMin) * (cfm - qMin));
387 }
388
389 totalPower *= powerFactor;
390 totalPower += pOffset;
391
392 if (totalPower == 0)
393 {
394 std::cerr << "total power 0\n";
395 val = 0;
396 return false;
397 }
398
399 if constexpr (DEBUG)
400 {
401 std::cout << "Power Factor " << powerFactor << "\n";
402 std::cout << "Inlet Temp " << inletTemp << "\n";
403 std::cout << "Total Power" << totalPower << "\n";
404 }
405
406 // Calculate the exit air temp
407 // Texit = Tfp + (1.76 * TotalPower / CFM * Faltitude)
408 double reading = 1.76 * totalPower * altitudeFactor;
409 reading /= cfm;
410 reading += inletTemp;
411
412 if constexpr (DEBUG)
413 {
414 std::cout << "Reading 1: " << reading << "\n";
415 }
416
417 // Now perform the exponential average
418 // Calculate alpha based on SDR values and CFM
419 // Ai = As + (Af - As)/(QMax - QMin) * (CFM - QMin)
420
421 double alpha = 0.0;
422 if (cfm < qMin)
423 {
424 alpha = alphaS;
425 }
426 else if (cfm >= qMax)
427 {
428 alpha = alphaF;
429 }
430 else
431 {
432 alpha = alphaS + ((alphaF - alphaS) * (cfm - qMin) / (qMax - qMin));
433 }
434
435 auto time = std::chrono::system_clock::now();
436 if (!firstRead)
437 {
438 firstRead = true;
439 lastTime = time;
440 lastReading = reading;
441 }
442 double alphaDT =
443 std::chrono::duration_cast<std::chrono::seconds>(time - lastTime)
444 .count() *
445 alpha;
446
447 // cap at 1.0 or the below fails
448 if (alphaDT > 1.0)
449 {
450 alphaDT = 1.0;
451 }
452
453 if constexpr (DEBUG)
454 {
455 std::cout << "AlphaDT: " << alphaDT << "\n";
456 }
457
458 reading = ((reading * alphaDT) + (lastReading * (1.0 - alphaDT)));
459
460 if constexpr (DEBUG)
461 {
462 std::cout << "Reading 2: " << reading << "\n";
463 }
464
465 val = reading;
466 lastReading = reading;
467 lastTime = time;
468 return true;
469}
470
471void ExitAirTempSensor::checkThresholds(void)
472{
473 thresholds::checkThresholds(this);
474}
475
James Feistbc896df2018-11-26 16:28:17 -0800476static void loadVariantPathArray(
477 const boost::container::flat_map<std::string, BasicVariantType>& data,
478 const std::string& key, std::vector<std::string>& resp)
479{
480 auto it = data.find(key);
481 if (it == data.end())
482 {
483 std::cerr << "Configuration missing " << key << "\n";
484 throw std::invalid_argument("Key Missing");
485 }
486 BasicVariantType copy = it->second;
487 std::vector<std::string> config =
488 sdbusplus::message::variant_ns::get<std::vector<std::string>>(copy);
489 for (auto& str : config)
490 {
491 boost::replace_all(str, " ", "_");
492 }
493 resp = std::move(config);
494}
495
496void createSensor(sdbusplus::asio::object_server& objectServer,
James Feistb2eb3f52018-12-04 16:17:50 -0800497 std::shared_ptr<ExitAirTempSensor>& exitAirSensor,
James Feistbc896df2018-11-26 16:28:17 -0800498 std::shared_ptr<sdbusplus::asio::connection>& dbusConnection)
499{
500 if (!dbusConnection)
501 {
502 std::cerr << "Connection not created\n";
503 return;
504 }
505 dbusConnection->async_method_call(
506 [&](boost::system::error_code ec, const ManagedObjectType& resp) {
507 if (ec)
508 {
509 std::cerr << "Error contacting entity manager\n";
510 return;
511 }
James Feistb2eb3f52018-12-04 16:17:50 -0800512 std::vector<std::unique_ptr<CFMSensor>> cfmSensors;
James Feistbc896df2018-11-26 16:28:17 -0800513 for (const auto& pathPair : resp)
514 {
515 for (const auto& entry : pathPair.second)
516 {
517 if (entry.first == exitAirIface)
518 {
James Feistbc896df2018-11-26 16:28:17 -0800519 // thresholds should be under the same path
520 std::vector<thresholds::Threshold> sensorThresholds;
521 parseThresholdsFromConfig(pathPair.second,
522 sensorThresholds);
James Feistb2eb3f52018-12-04 16:17:50 -0800523 if (!exitAirSensor)
James Feistbc896df2018-11-26 16:28:17 -0800524 {
James Feistb2eb3f52018-12-04 16:17:50 -0800525 std::string name =
526 loadVariant<std::string>(entry.second, "Name");
527 exitAirSensor = std::make_shared<ExitAirTempSensor>(
528 dbusConnection, name, pathPair.first.str,
James Feistbc896df2018-11-26 16:28:17 -0800529 objectServer, std::move(sensorThresholds));
530 }
531 else
532 {
James Feistb2eb3f52018-12-04 16:17:50 -0800533 exitAirSensor->thresholds = sensorThresholds;
James Feistbc896df2018-11-26 16:28:17 -0800534 }
535
James Feistb2eb3f52018-12-04 16:17:50 -0800536 exitAirSensor->powerFactorMin =
537 loadVariant<double>(entry.second, "PowerFactorMin");
538 exitAirSensor->powerFactorMax =
539 loadVariant<double>(entry.second, "PowerFactorMax");
540 exitAirSensor->qMin =
541 loadVariant<double>(entry.second, "QMin");
542 exitAirSensor->qMax =
543 loadVariant<double>(entry.second, "QMax");
544 exitAirSensor->alphaS =
545 loadVariant<double>(entry.second, "AlphaS");
546 exitAirSensor->alphaF =
547 loadVariant<double>(entry.second, "AlphaF");
James Feistbc896df2018-11-26 16:28:17 -0800548 }
549 else if (entry.first == cfmIface)
550
551 {
James Feistb2eb3f52018-12-04 16:17:50 -0800552 // thresholds should be under the same path
553 std::vector<thresholds::Threshold> sensorThresholds;
554 parseThresholdsFromConfig(pathPair.second,
555 sensorThresholds);
556 std::string name =
557 loadVariant<std::string>(entry.second, "Name");
558 auto sensor = std::make_unique<CFMSensor>(
559 dbusConnection, name, pathPair.first.str,
560 objectServer, std::move(sensorThresholds),
561 exitAirSensor);
562 loadVariantPathArray(entry.second, "Tachs",
563 sensor->tachs);
564 sensor->maxCFM =
565 loadVariant<double>(entry.second, "MaxCFM");
James Feistbc896df2018-11-26 16:28:17 -0800566
567 // change these into percent upon getting the data
James Feistb2eb3f52018-12-04 16:17:50 -0800568 sensor->c1 =
569 loadVariant<double>(entry.second, "C1") / 100;
570 sensor->c2 =
571 loadVariant<double>(entry.second, "C2") / 100;
572 sensor->tachMinPercent =
573 loadVariant<double>(entry.second,
574 "TachMinPercent") /
James Feistbc896df2018-11-26 16:28:17 -0800575 100;
James Feistb2eb3f52018-12-04 16:17:50 -0800576 sensor->tachMaxPercent =
577 loadVariant<double>(entry.second,
578 "TachMaxPercent") /
James Feistbc896df2018-11-26 16:28:17 -0800579 100;
580
James Feistb2eb3f52018-12-04 16:17:50 -0800581 cfmSensors.emplace_back(std::move(sensor));
James Feistbc896df2018-11-26 16:28:17 -0800582 }
583 }
584 }
James Feistb2eb3f52018-12-04 16:17:50 -0800585 if (exitAirSensor)
James Feistbc896df2018-11-26 16:28:17 -0800586 {
James Feistb2eb3f52018-12-04 16:17:50 -0800587 exitAirSensor->cfmSensors = std::move(cfmSensors);
James Feistbc896df2018-11-26 16:28:17 -0800588
James Feistb2eb3f52018-12-04 16:17:50 -0800589 // todo: when power sensors are done delete this fake
590 // reading
591 exitAirSensor->powerReadings["foo"] = 144.0;
James Feistbc896df2018-11-26 16:28:17 -0800592
James Feistb2eb3f52018-12-04 16:17:50 -0800593 exitAirSensor->updateReading();
James Feistbc896df2018-11-26 16:28:17 -0800594 }
595 },
596 entityManagerName, "/", "org.freedesktop.DBus.ObjectManager",
597 "GetManagedObjects");
598}
599
600int main(int argc, char** argv)
601{
602
603 boost::asio::io_service io;
604 auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
605 systemBus->request_name("xyz.openbmc_project.ExitAirTempSensor");
606 sdbusplus::asio::object_server objectServer(systemBus);
607 std::shared_ptr<ExitAirTempSensor> sensor =
608 nullptr; // wait until we find the config
609 std::vector<std::unique_ptr<sdbusplus::bus::match::match>> matches;
610
611 io.post([&]() { createSensor(objectServer, sensor, systemBus); });
612
613 boost::asio::deadline_timer configTimer(io);
614
615 std::function<void(sdbusplus::message::message&)> eventHandler =
616 [&](sdbusplus::message::message& message) {
617 configTimer.expires_from_now(boost::posix_time::seconds(1));
618 // create a timer because normally multiple properties change
619 configTimer.async_wait([&](const boost::system::error_code& ec) {
620 if (ec == boost::asio::error::operation_aborted)
621 {
622 return; // we're being canceled
623 }
624 createSensor(objectServer, sensor, systemBus);
625 if (!sensor)
626 {
627 std::cout << "Configuration not detected\n";
628 }
629 });
630 };
631 constexpr const std::array<const char*, 2> monitorIfaces = {exitAirIface,
632 cfmIface};
633 for (const char* type : monitorIfaces)
634 {
635 auto match = std::make_unique<sdbusplus::bus::match::match>(
636 static_cast<sdbusplus::bus::bus&>(*systemBus),
637 "type='signal',member='PropertiesChanged',path_namespace='" +
638 std::string(inventoryPath) + "',arg0namespace='" + type + "'",
639 eventHandler);
640 matches.emplace_back(std::move(match));
641 }
642
643 io.run();
644}