blob: c9066adf51c0a0a8ad6dd95578b970623de28022 [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";
41static constexpr double maxReading = 127;
42static constexpr double minReading = -128;
43
44static constexpr bool DEBUG = false;
45
46ExitAirTempSensor::ExitAirTempSensor(
47 std::shared_ptr<sdbusplus::asio::connection>& conn,
48 const std::string& sensorConfiguration,
49 sdbusplus::asio::object_server& objectServer,
50 std::vector<thresholds::Threshold>&& thresholds) :
51 Sensor("Exit_Air_Temperature", "" /* todo: remove arg from base*/,
52 std::move(thresholds), sensorConfiguration,
53 "xyz.openbmc_project.Configuration.ExitAirTemp", maxReading,
54 minReading),
55 dbusConnection(conn)
56{
57 sensorInterface = objectServer.add_interface(
58 "/xyz/openbmc_project/sensors/temperature/" + name,
59 "xyz.openbmc_project.Sensor.Value");
60
61 if (thresholds::hasWarningInterface(thresholds))
62 {
63 thresholdInterfaceWarning = objectServer.add_interface(
64 "/xyz/openbmc_project/sensors/temperature/" + name,
65 "xyz.openbmc_project.Sensor.Threshold.Warning");
66 }
67 if (thresholds::hasCriticalInterface(thresholds))
68 {
69 thresholdInterfaceCritical = objectServer.add_interface(
70 "/xyz/openbmc_project/sensors/temperature/" + name,
71 "xyz.openbmc_project.Sensor.Threshold.Critical");
72 }
73 setInitialProperties(conn);
74 setupMatches();
75}
76
77ExitAirTempSensor::~ExitAirTempSensor()
78{
79 // this sensor currently isn't destroyed so we don't care
80}
81
82void ExitAirTempSensor::setupMatches(void)
83{
84
85 constexpr const std::array<const char*, 3> matchTypes = {
86 "power", "fan_pwm", inletTemperatureSensor};
87
88 for (const auto& type : matchTypes)
89 {
90 std::function<void(sdbusplus::message::message & message)>
91 eventHandler = [this, type](sdbusplus::message::message& message) {
92 std::string objectName;
93 boost::container::flat_map<
94 std::string, sdbusplus::message::variant<double, int64_t>>
95 values;
96 message.read(objectName, values);
97 auto findValue = values.find("Value");
98 if (findValue == values.end())
99 {
100 return;
101 }
102 double value = sdbusplus::message::variant_ns::visit(
103 VariantToDoubleVisitor(), findValue->second);
104 if (type == "power")
105 {
106 powerReadings[message.get_path()] = value;
107 }
108 else if (type == inletTemperatureSensor)
109 {
110 inletTemp = value;
111 }
112 else if (type == "fan_pwm")
113 {
114 pwmReadings[message.get_path()] = value;
115 }
116 updateReading();
117 };
118 matches.emplace_back(static_cast<sdbusplus::bus::bus&>(*dbusConnection),
119 "type='signal',"
120 "member='PropertiesChanged',interface='org."
121 "freedesktop.DBus.Properties',path_"
122 "namespace='/xyz/openbmc_project/sensors/" +
123 std::string(type) +
124 "',arg0='xyz.openbmc_project.Sensor.Value'",
125 std::move(eventHandler));
126 }
127}
128
129void ExitAirTempSensor::updateReading(void)
130{
131
132 double val = 0.0;
133 if (calculate(val))
134 {
135 updateValue(val);
136 }
137 else
138 {
139 updateValue(std::numeric_limits<double>::quiet_NaN());
140 }
141}
142
143// todo: break this out into it's own sensor
144int32_t ExitAirTempSensor::getTotalCFM(void)
145{
146 int32_t totalCFM = 0;
147 // todo: rpm instead of pwm
148 for (const auto& zone : cfmData)
149 {
150 if (zone.pwm.empty())
151 {
152 std::cerr << "CFM without PWM";
153 return -1;
154 }
155
156 const std::string& firstName = zone.pwm[0];
157
158 auto findPwm = std::find_if(
159 pwmReadings.begin(), pwmReadings.end(), [&](const auto& item) {
160 return boost::ends_with(item.first, firstName);
161 });
162 if (findPwm == pwmReadings.end())
163 {
164 std::cerr << "Can't find " << firstName << "in readings\n";
165 return -1; // haven't gotten a reading
166 }
167
168 double pwm = findPwm->second;
169 if constexpr (DEBUG)
170 {
171 std::cout << "Pwm " << firstName << "at " << pwm << "\n";
172 }
173
174 // Do a linear interpolation to get Ci
175 // Ci = C1 + (C2 - C1)/(PWM2 - PWM1) * (PWMi - PWM1)
176
177 int32_t ci = 0;
178 if (pwm == 0)
179 {
180 ci = 0;
181 }
182 else if (pwm < zone.pwmMin)
183 {
184 ci = zone.c1;
185 }
186 else if (pwm > zone.pwmMax)
187 {
188 ci = zone.c2;
189 }
190 else
191 {
192 ci = zone.c1 +
193 (((zone.c2 - zone.c1) * (pwm - (int32_t)zone.pwmMin)) /
194 ((int32_t)zone.pwmMax - (int32_t)zone.pwmMin));
195 }
196
197 // Now calculate the CFM for this domain
198 // CFMi = Ci * QTYi * Qmaxi * PWMi
199 totalCFM += ci * zone.pwm.size() * zone.maxCFM * pwm;
200 }
201 if constexpr (DEBUG)
202 {
203 std::cout << totalCFM / 100 << " CFM\n";
204 }
205 return totalCFM /= 100; // divide by 100 since PWM is in percent
206}
207
208bool ExitAirTempSensor::calculate(double& val)
209{
210 static bool firstRead = false;
211 double cfm = getTotalCFM();
212 if (cfm <= 0)
213 {
214 std::cerr << "Error getting cfm\n";
215 return false;
216 }
217
218 // if there is an error getting inlet temp, return error
219 if (std::isnan(inletTemp))
220 {
221 std::cerr << "Cannot get inlet temp\n";
222 val = 0;
223 return false;
224 }
225
226 // if fans are off, just make the exit temp equal to inlet
227 if (!isPowerOn(dbusConnection))
228 {
229 val = inletTemp;
230 return true;
231 }
232
233 double totalPower = 0;
234 for (const auto& reading : powerReadings)
235 {
236 if (std::isnan(reading.second))
237 {
238 continue;
239 }
240 totalPower += reading.second;
241 }
242
243 // Calculate power correction factor
244 // Ci = CL + (CH - CL)/(QMax - QMin) * (CFM - QMin)
245 float powerFactor = 0.0;
246 if (cfm <= qMin)
247 {
248 powerFactor = powerFactorMin;
249 }
250 else if (cfm >= qMax)
251 {
252 powerFactor = powerFactorMax;
253 }
254 else
255 {
256 powerFactor = powerFactorMin + ((powerFactorMax - powerFactorMin) /
257 (qMax - qMin) * (cfm - qMin));
258 }
259
260 totalPower *= powerFactor;
261 totalPower += pOffset;
262
263 if (totalPower == 0)
264 {
265 std::cerr << "total power 0\n";
266 val = 0;
267 return false;
268 }
269
270 if constexpr (DEBUG)
271 {
272 std::cout << "Power Factor " << powerFactor << "\n";
273 std::cout << "Inlet Temp " << inletTemp << "\n";
274 std::cout << "Total Power" << totalPower << "\n";
275 }
276
277 // Calculate the exit air temp
278 // Texit = Tfp + (1.76 * TotalPower / CFM * Faltitude)
279 double reading = 1.76 * totalPower * altitudeFactor;
280 reading /= cfm;
281 reading += inletTemp;
282
283 if constexpr (DEBUG)
284 {
285 std::cout << "Reading 1: " << reading << "\n";
286 }
287
288 // Now perform the exponential average
289 // Calculate alpha based on SDR values and CFM
290 // Ai = As + (Af - As)/(QMax - QMin) * (CFM - QMin)
291
292 double alpha = 0.0;
293 if (cfm < qMin)
294 {
295 alpha = alphaS;
296 }
297 else if (cfm >= qMax)
298 {
299 alpha = alphaF;
300 }
301 else
302 {
303 alpha = alphaS + ((alphaF - alphaS) * (cfm - qMin) / (qMax - qMin));
304 }
305
306 auto time = std::chrono::system_clock::now();
307 if (!firstRead)
308 {
309 firstRead = true;
310 lastTime = time;
311 lastReading = reading;
312 }
313 double alphaDT =
314 std::chrono::duration_cast<std::chrono::seconds>(time - lastTime)
315 .count() *
316 alpha;
317
318 // cap at 1.0 or the below fails
319 if (alphaDT > 1.0)
320 {
321 alphaDT = 1.0;
322 }
323
324 if constexpr (DEBUG)
325 {
326 std::cout << "AlphaDT: " << alphaDT << "\n";
327 }
328
329 reading = ((reading * alphaDT) + (lastReading * (1.0 - alphaDT)));
330
331 if constexpr (DEBUG)
332 {
333 std::cout << "Reading 2: " << reading << "\n";
334 }
335
336 val = reading;
337 lastReading = reading;
338 lastTime = time;
339 return true;
340}
341
342void ExitAirTempSensor::checkThresholds(void)
343{
344 thresholds::checkThresholds(this);
345}
346
347static double loadVariantDouble(
348 const boost::container::flat_map<std::string, BasicVariantType>& data,
349 const std::string& key)
350{
351 auto it = data.find(key);
352 if (it == data.end())
353 {
354 std::cerr << "Configuration missing " << key << "\n";
355 throw std::invalid_argument("Key Missing");
356 }
357 return sdbusplus::message::variant_ns::visit(VariantToDoubleVisitor(),
358 it->second);
359}
360
361static void loadVariantPathArray(
362 const boost::container::flat_map<std::string, BasicVariantType>& data,
363 const std::string& key, std::vector<std::string>& resp)
364{
365 auto it = data.find(key);
366 if (it == data.end())
367 {
368 std::cerr << "Configuration missing " << key << "\n";
369 throw std::invalid_argument("Key Missing");
370 }
371 BasicVariantType copy = it->second;
372 std::vector<std::string> config =
373 sdbusplus::message::variant_ns::get<std::vector<std::string>>(copy);
374 for (auto& str : config)
375 {
376 boost::replace_all(str, " ", "_");
377 }
378 resp = std::move(config);
379}
380
381void createSensor(sdbusplus::asio::object_server& objectServer,
382 std::shared_ptr<ExitAirTempSensor>& sensor,
383 std::shared_ptr<sdbusplus::asio::connection>& dbusConnection)
384{
385 if (!dbusConnection)
386 {
387 std::cerr << "Connection not created\n";
388 return;
389 }
390 dbusConnection->async_method_call(
391 [&](boost::system::error_code ec, const ManagedObjectType& resp) {
392 if (ec)
393 {
394 std::cerr << "Error contacting entity manager\n";
395 return;
396 }
397
398 std::vector<CFMInfo> cfmData;
399 bool foundExitAir = false;
400 for (const auto& pathPair : resp)
401 {
402 for (const auto& entry : pathPair.second)
403 {
404 if (entry.first == exitAirIface)
405 {
406 if (foundExitAir)
407 {
408 // Something is very wrong
409 std::cerr << "More than one exit air configuration "
410 "found\n";
411 std::exit(EXIT_FAILURE);
412 }
413 foundExitAir = true;
414
415 // thresholds should be under the same path
416 std::vector<thresholds::Threshold> sensorThresholds;
417 parseThresholdsFromConfig(pathPair.second,
418 sensorThresholds);
419 if (!sensor)
420 {
421 sensor = std::make_shared<ExitAirTempSensor>(
422 dbusConnection, pathPair.first.str,
423 objectServer, std::move(sensorThresholds));
424 }
425 else
426 {
427 sensor->thresholds = sensorThresholds;
428 }
429
430 sensor->powerFactorMin =
431 loadVariantDouble(entry.second, "PowerFactorMin");
432 sensor->powerFactorMax =
433 loadVariantDouble(entry.second, "PowerFactorMax");
434 sensor->qMin = loadVariantDouble(entry.second, "QMin");
435 sensor->qMax = loadVariantDouble(entry.second, "QMax");
436 sensor->alphaS =
437 loadVariantDouble(entry.second, "AlphaS");
438 sensor->alphaF =
439 loadVariantDouble(entry.second, "AlphaF");
440 }
441 else if (entry.first == cfmIface)
442
443 {
444
445 CFMInfo cfm;
446 loadVariantPathArray(entry.second, "Pwms", cfm.pwm);
447 cfm.maxCFM = loadVariantDouble(entry.second, "MaxCFM");
448
449 // change these into percent upon getting the data
450 cfm.c1 = loadVariantDouble(entry.second, "C1") / 100;
451 cfm.c2 = loadVariantDouble(entry.second, "C2") / 100;
452 cfm.pwmMin =
453 loadVariantDouble(entry.second, "PwmMinPercent") /
454 100;
455 cfm.pwmMax =
456 loadVariantDouble(entry.second, "PwmMaxPercent") /
457 100;
458
459 cfmData.emplace_back(cfm);
460 }
461 }
462 }
463 if (sensor)
464 {
465 sensor->cfmData = std::move(cfmData);
466
467 // todo: when power sensors are done delete this fake reading
468 sensor->powerReadings["foo"] = 144.0;
469
470 sensor->updateReading();
471 }
472 },
473 entityManagerName, "/", "org.freedesktop.DBus.ObjectManager",
474 "GetManagedObjects");
475}
476
477int main(int argc, char** argv)
478{
479
480 boost::asio::io_service io;
481 auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
482 systemBus->request_name("xyz.openbmc_project.ExitAirTempSensor");
483 sdbusplus::asio::object_server objectServer(systemBus);
484 std::shared_ptr<ExitAirTempSensor> sensor =
485 nullptr; // wait until we find the config
486 std::vector<std::unique_ptr<sdbusplus::bus::match::match>> matches;
487
488 io.post([&]() { createSensor(objectServer, sensor, systemBus); });
489
490 boost::asio::deadline_timer configTimer(io);
491
492 std::function<void(sdbusplus::message::message&)> eventHandler =
493 [&](sdbusplus::message::message& message) {
494 configTimer.expires_from_now(boost::posix_time::seconds(1));
495 // create a timer because normally multiple properties change
496 configTimer.async_wait([&](const boost::system::error_code& ec) {
497 if (ec == boost::asio::error::operation_aborted)
498 {
499 return; // we're being canceled
500 }
501 createSensor(objectServer, sensor, systemBus);
502 if (!sensor)
503 {
504 std::cout << "Configuration not detected\n";
505 }
506 });
507 };
508 constexpr const std::array<const char*, 2> monitorIfaces = {exitAirIface,
509 cfmIface};
510 for (const char* type : monitorIfaces)
511 {
512 auto match = std::make_unique<sdbusplus::bus::match::match>(
513 static_cast<sdbusplus::bus::bus&>(*systemBus),
514 "type='signal',member='PropertiesChanged',path_namespace='" +
515 std::string(inventoryPath) + "',arg0namespace='" + type + "'",
516 eventHandler);
517 matches.emplace_back(std::move(match));
518 }
519
520 io.run();
521}