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 "conf.hpp"
18 #include "util.hpp"
19 
20 #include <algorithm>
21 #include <boost/asio/steady_timer.hpp>
22 #include <chrono>
23 #include <functional>
24 #include <iostream>
25 #include <list>
26 #include <regex>
27 #include <sdbusplus/bus.hpp>
28 #include <sdbusplus/bus/match.hpp>
29 #include <sdbusplus/exception.hpp>
30 #include <set>
31 #include <unordered_map>
32 #include <variant>
33 
34 static constexpr bool DEBUG = false; // enable to print found configuration
35 
36 extern std::map<std::string, struct conf::SensorConfig> sensorConfig;
37 extern std::map<int64_t, conf::PIDConf> zoneConfig;
38 extern std::map<int64_t, struct conf::ZoneConfig> zoneDetailsConfig;
39 
40 constexpr const char* pidConfigurationInterface =
41     "xyz.openbmc_project.Configuration.Pid";
42 constexpr const char* objectManagerInterface =
43     "org.freedesktop.DBus.ObjectManager";
44 constexpr const char* pidZoneConfigurationInterface =
45     "xyz.openbmc_project.Configuration.Pid.Zone";
46 constexpr const char* stepwiseConfigurationInterface =
47     "xyz.openbmc_project.Configuration.Stepwise";
48 constexpr const char* thermalControlIface =
49     "xyz.openbmc_project.Control.ThermalMode";
50 constexpr const char* sensorInterface = "xyz.openbmc_project.Sensor.Value";
51 constexpr const char* pwmInterface = "xyz.openbmc_project.Control.FanPwm";
52 
53 namespace dbus_configuration
54 {
55 
56 bool findSensors(const std::unordered_map<std::string, std::string>& sensors,
57                  const std::string& search,
58                  std::vector<std::pair<std::string, std::string>>& matches)
59 {
60     std::smatch match;
61     std::regex reg(search);
62     for (const auto& sensor : sensors)
63     {
64         if (std::regex_search(sensor.first, match, reg))
65         {
66             matches.push_back(sensor);
67         }
68     }
69 
70     return matches.size() > 0;
71 }
72 
73 // this function prints the configuration into a form similar to the cpp
74 // generated code to help in verification, should be turned off during normal
75 // use
76 void debugPrint(void)
77 {
78     // print sensor config
79     std::cout << "sensor config:\n";
80     std::cout << "{\n";
81     for (const auto& pair : sensorConfig)
82     {
83 
84         std::cout << "\t{" << pair.first << ",\n\t\t{";
85         std::cout << pair.second.type << ", ";
86         std::cout << pair.second.readPath << ", ";
87         std::cout << pair.second.writePath << ", ";
88         std::cout << pair.second.min << ", ";
89         std::cout << pair.second.max << ", ";
90         std::cout << pair.second.timeout << "},\n\t},\n";
91     }
92     std::cout << "}\n\n";
93     std::cout << "ZoneDetailsConfig\n";
94     std::cout << "{\n";
95     for (const auto& zone : zoneDetailsConfig)
96     {
97         std::cout << "\t{" << zone.first << ",\n";
98         std::cout << "\t\t{" << zone.second.minThermalOutput << ", ";
99         std::cout << zone.second.failsafePercent << "}\n\t},\n";
100     }
101     std::cout << "}\n\n";
102     std::cout << "ZoneConfig\n";
103     std::cout << "{\n";
104     for (const auto& zone : zoneConfig)
105     {
106         std::cout << "\t{" << zone.first << "\n";
107         for (const auto& pidconf : zone.second)
108         {
109             std::cout << "\t\t{" << pidconf.first << ",\n";
110             std::cout << "\t\t\t{" << pidconf.second.type << ",\n";
111             std::cout << "\t\t\t{";
112             for (const auto& input : pidconf.second.inputs)
113             {
114                 std::cout << "\n\t\t\t" << input << ",\n";
115             }
116             std::cout << "\t\t\t}\n";
117             std::cout << "\t\t\t" << pidconf.second.setpoint << ",\n";
118             std::cout << "\t\t\t{" << pidconf.second.pidInfo.ts << ",\n";
119             std::cout << "\t\t\t" << pidconf.second.pidInfo.proportionalCoeff
120                       << ",\n";
121             std::cout << "\t\t\t" << pidconf.second.pidInfo.integralCoeff
122                       << ",\n";
123             std::cout << "\t\t\t" << pidconf.second.pidInfo.feedFwdOffset
124                       << ",\n";
125             std::cout << "\t\t\t" << pidconf.second.pidInfo.feedFwdGain
126                       << ",\n";
127             std::cout << "\t\t\t{" << pidconf.second.pidInfo.integralLimit.min
128                       << "," << pidconf.second.pidInfo.integralLimit.max
129                       << "},\n";
130             std::cout << "\t\t\t{" << pidconf.second.pidInfo.outLim.min << ","
131                       << pidconf.second.pidInfo.outLim.max << "},\n";
132             std::cout << "\t\t\t" << pidconf.second.pidInfo.slewNeg << ",\n";
133             std::cout << "\t\t\t" << pidconf.second.pidInfo.slewPos << ",\n";
134             std::cout << "\t\t\t}\n\t\t}\n";
135         }
136         std::cout << "\t},\n";
137     }
138     std::cout << "}\n\n";
139 }
140 
141 size_t getZoneIndex(const std::string& name, std::vector<std::string>& zones)
142 {
143     auto it = std::find(zones.begin(), zones.end(), name);
144     if (it == zones.end())
145     {
146         zones.emplace_back(name);
147         it = zones.end() - 1;
148     }
149 
150     return it - zones.begin();
151 }
152 
153 std::vector<std::string> getSelectedProfiles(sdbusplus::bus::bus& bus)
154 {
155     std::vector<std::string> ret;
156     auto mapper =
157         bus.new_method_call("xyz.openbmc_project.ObjectMapper",
158                             "/xyz/openbmc_project/object_mapper",
159                             "xyz.openbmc_project.ObjectMapper", "GetSubTree");
160     mapper.append("/", 0, std::array<const char*, 1>{thermalControlIface});
161     std::unordered_map<
162         std::string, std::unordered_map<std::string, std::vector<std::string>>>
163         respData;
164 
165     try
166     {
167         auto resp = bus.call(mapper);
168         resp.read(respData);
169     }
170     catch (sdbusplus::exception_t&)
171     {
172         // can't do anything without mapper call data
173         throw std::runtime_error("ObjectMapper Call Failure");
174     }
175     if (respData.empty())
176     {
177         // if the user has profiles but doesn't expose the interface to select
178         // one, just go ahead without using profiles
179         return ret;
180     }
181 
182     // assumption is that we should only have a small handful of selected
183     // profiles at a time (probably only 1), so calling each individually should
184     // not incur a large cost
185     for (const auto& objectPair : respData)
186     {
187         const std::string& path = objectPair.first;
188         for (const auto& ownerPair : objectPair.second)
189         {
190             const std::string& busName = ownerPair.first;
191             auto getProfile =
192                 bus.new_method_call(busName.c_str(), path.c_str(),
193                                     "org.freedesktop.DBus.Properties", "Get");
194             getProfile.append(thermalControlIface, "Current");
195             std::variant<std::string> variantResp;
196             try
197             {
198                 auto resp = bus.call(getProfile);
199                 resp.read(variantResp);
200             }
201             catch (sdbusplus::exception_t&)
202             {
203                 throw std::runtime_error("Failure getting profile");
204             }
205             std::string mode = std::get<std::string>(variantResp);
206             ret.emplace_back(std::move(mode));
207         }
208     }
209     if constexpr (DEBUG)
210     {
211         std::cout << "Profiles selected: ";
212         for (const auto& profile : ret)
213         {
214             std::cout << profile << " ";
215         }
216         std::cout << "\n";
217     }
218     return ret;
219 }
220 
221 int eventHandler(sd_bus_message*, void* context, sd_bus_error*)
222 {
223 
224     if (context == nullptr)
225     {
226         throw std::runtime_error("Invalid match");
227     }
228     boost::asio::steady_timer* timer =
229         static_cast<boost::asio::steady_timer*>(context);
230 
231     // do a brief sleep as we tend to get a bunch of these events at
232     // once
233     timer->expires_after(std::chrono::seconds(2));
234     timer->async_wait([](const boost::system::error_code ec) {
235         if (ec == boost::asio::error::operation_aborted)
236         {
237             /* another timer started*/
238             return;
239         }
240 
241         std::cout << "New configuration detected, reloading\n.";
242         restartControlLoops();
243     });
244 
245     return 1;
246 }
247 
248 void createMatches(sdbusplus::bus::bus& bus, boost::asio::steady_timer& timer)
249 {
250     // this is a list because the matches can't be moved
251     static std::list<sdbusplus::bus::match::match> matches;
252 
253     const std::array<std::string, 4> interfaces = {
254         thermalControlIface, pidConfigurationInterface,
255         pidZoneConfigurationInterface, stepwiseConfigurationInterface};
256 
257     // this list only needs to be created once
258     if (!matches.empty())
259     {
260         return;
261     }
262 
263     // we restart when the configuration changes or there are new sensors
264     for (const auto& interface : interfaces)
265     {
266         matches.emplace_back(
267             bus,
268             "type='signal',member='PropertiesChanged',arg0namespace='" +
269                 interface + "'",
270             eventHandler, &timer);
271     }
272     matches.emplace_back(
273         bus,
274         "type='signal',member='InterfacesAdded',arg0path='/xyz/openbmc_project/"
275         "sensors/'",
276         eventHandler, &timer);
277 }
278 
279 bool init(sdbusplus::bus::bus& bus, boost::asio::steady_timer& timer)
280 {
281 
282     sensorConfig.clear();
283     zoneConfig.clear();
284     zoneDetailsConfig.clear();
285 
286     createMatches(bus, timer);
287 
288     using DbusVariantType =
289         std::variant<uint64_t, int64_t, double, std::string,
290                      std::vector<std::string>, std::vector<double>>;
291 
292     using ManagedObjectType = std::unordered_map<
293         sdbusplus::message::object_path,
294         std::unordered_map<std::string,
295                            std::unordered_map<std::string, DbusVariantType>>>;
296 
297     auto mapper =
298         bus.new_method_call("xyz.openbmc_project.ObjectMapper",
299                             "/xyz/openbmc_project/object_mapper",
300                             "xyz.openbmc_project.ObjectMapper", "GetSubTree");
301     mapper.append("/", 0,
302                   std::array<const char*, 6>{objectManagerInterface,
303                                              pidConfigurationInterface,
304                                              pidZoneConfigurationInterface,
305                                              stepwiseConfigurationInterface,
306                                              sensorInterface, pwmInterface});
307     std::unordered_map<
308         std::string, std::unordered_map<std::string, std::vector<std::string>>>
309         respData;
310     try
311     {
312         auto resp = bus.call(mapper);
313         resp.read(respData);
314     }
315     catch (sdbusplus::exception_t&)
316     {
317         // can't do anything without mapper call data
318         throw std::runtime_error("ObjectMapper Call Failure");
319     }
320 
321     if (respData.empty())
322     {
323         // can't do anything without mapper call data
324         throw std::runtime_error("No configuration data available from Mapper");
325     }
326     // create a map of pair of <has pid configuration, ObjectManager path>
327     std::unordered_map<std::string, std::pair<bool, std::string>> owners;
328     // and a map of <path, interface> for sensors
329     std::unordered_map<std::string, std::string> sensors;
330     for (const auto& objectPair : respData)
331     {
332         for (const auto& ownerPair : objectPair.second)
333         {
334             auto& owner = owners[ownerPair.first];
335             for (const std::string& interface : ownerPair.second)
336             {
337 
338                 if (interface == objectManagerInterface)
339                 {
340                     owner.second = objectPair.first;
341                 }
342                 if (interface == pidConfigurationInterface ||
343                     interface == pidZoneConfigurationInterface ||
344                     interface == stepwiseConfigurationInterface)
345                 {
346                     owner.first = true;
347                 }
348                 if (interface == sensorInterface || interface == pwmInterface)
349                 {
350                     // we're not interested in pwm sensors, just pwm control
351                     if (interface == sensorInterface &&
352                         objectPair.first.find("pwm") != std::string::npos)
353                     {
354                         continue;
355                     }
356                     sensors[objectPair.first] = interface;
357                 }
358             }
359         }
360     }
361     ManagedObjectType configurations;
362     for (const auto& owner : owners)
363     {
364         // skip if no pid configuration (means probably a sensor)
365         if (!owner.second.first)
366         {
367             continue;
368         }
369         auto endpoint = bus.new_method_call(
370             owner.first.c_str(), owner.second.second.c_str(),
371             "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
372         ManagedObjectType configuration;
373         try
374         {
375             auto responce = bus.call(endpoint);
376             responce.read(configuration);
377         }
378         catch (sdbusplus::exception_t&)
379         {
380             // this shouldn't happen, probably means daemon crashed
381             throw std::runtime_error("Error getting managed objects from " +
382                                      owner.first);
383         }
384 
385         for (auto& pathPair : configuration)
386         {
387             if (pathPair.second.find(pidConfigurationInterface) !=
388                     pathPair.second.end() ||
389                 pathPair.second.find(pidZoneConfigurationInterface) !=
390                     pathPair.second.end() ||
391                 pathPair.second.find(stepwiseConfigurationInterface) !=
392                     pathPair.second.end())
393             {
394                 configurations.emplace(pathPair);
395             }
396         }
397     }
398 
399     // remove controllers from config that aren't in the current profile(s)
400     std::vector<std::string> selectedProfiles = getSelectedProfiles(bus);
401     if (selectedProfiles.size())
402     {
403         for (auto pathIt = configurations.begin();
404              pathIt != configurations.end();)
405         {
406             for (auto confIt = pathIt->second.begin();
407                  confIt != pathIt->second.end();)
408             {
409                 auto profilesFind = confIt->second.find("Profiles");
410                 if (profilesFind == confIt->second.end())
411                 {
412                     confIt++;
413                     continue; // if no profiles selected, apply always
414                 }
415                 auto profiles =
416                     std::get<std::vector<std::string>>(profilesFind->second);
417                 if (profiles.empty())
418                 {
419                     confIt++;
420                     continue;
421                 }
422 
423                 bool found = false;
424                 for (const std::string& profile : profiles)
425                 {
426                     if (std::find(selectedProfiles.begin(),
427                                   selectedProfiles.end(),
428                                   profile) != selectedProfiles.end())
429                     {
430                         found = true;
431                         break;
432                     }
433                 }
434                 if (found)
435                 {
436                     confIt++;
437                 }
438                 else
439                 {
440                     confIt = pathIt->second.erase(confIt);
441                 }
442             }
443             if (pathIt->second.empty())
444             {
445                 pathIt = configurations.erase(pathIt);
446             }
447             else
448             {
449                 pathIt++;
450             }
451         }
452     }
453 
454     // on dbus having an index field is a bit strange, so randomly
455     // assign index based on name property
456     std::vector<std::string> foundZones;
457     for (const auto& configuration : configurations)
458     {
459         auto findZone =
460             configuration.second.find(pidZoneConfigurationInterface);
461         if (findZone != configuration.second.end())
462         {
463             const auto& zone = findZone->second;
464 
465             const std::string& name = std::get<std::string>(zone.at("Name"));
466             size_t index = getZoneIndex(name, foundZones);
467 
468             auto& details = zoneDetailsConfig[index];
469             details.minThermalOutput = std::visit(VariantToDoubleVisitor(),
470                                                   zone.at("MinThermalOutput"));
471             details.failsafePercent = std::visit(VariantToDoubleVisitor(),
472                                                  zone.at("FailSafePercent"));
473         }
474         auto findBase = configuration.second.find(pidConfigurationInterface);
475         if (findBase != configuration.second.end())
476         {
477 
478             const auto& base =
479                 configuration.second.at(pidConfigurationInterface);
480             const std::vector<std::string>& zones =
481                 std::get<std::vector<std::string>>(base.at("Zones"));
482             for (const std::string& zone : zones)
483             {
484                 size_t index = getZoneIndex(zone, foundZones);
485                 conf::PIDConf& conf = zoneConfig[index];
486 
487                 std::vector<std::string> sensorNames =
488                     std::get<std::vector<std::string>>(base.at("Inputs"));
489                 auto findOutputs =
490                     base.find("Outputs"); // currently only fans have outputs
491                 if (findOutputs != base.end())
492                 {
493                     std::vector<std::string> outputs =
494                         std::get<std::vector<std::string>>(findOutputs->second);
495                     sensorNames.insert(sensorNames.end(), outputs.begin(),
496                                        outputs.end());
497                 }
498 
499                 std::vector<std::string> inputs;
500                 std::vector<std::pair<std::string, std::string>>
501                     sensorInterfaces;
502                 for (const std::string& sensorName : sensorNames)
503                 {
504                     std::string name = sensorName;
505                     // replace spaces with underscores to be legal on dbus
506                     std::replace(name.begin(), name.end(), ' ', '_');
507                     findSensors(sensors, name, sensorInterfaces);
508                 }
509 
510                 // if the sensors aren't available in the current state, don't
511                 // add them to the configuration.
512                 if (sensorInterfaces.empty())
513                 {
514                     continue;
515                 }
516                 for (const auto& sensorPathIfacePair : sensorInterfaces)
517                 {
518 
519                     if (sensorPathIfacePair.second == sensorInterface)
520                     {
521                         size_t idx =
522                             sensorPathIfacePair.first.find_last_of("/") + 1;
523                         std::string shortName =
524                             sensorPathIfacePair.first.substr(idx);
525 
526                         inputs.push_back(shortName);
527                         auto& config = sensorConfig[shortName];
528                         config.type = std::get<std::string>(base.at("Class"));
529                         config.readPath = sensorPathIfacePair.first;
530                         // todo: maybe un-hardcode this if we run into slower
531                         // timeouts with sensors
532                         if (config.type == "temp")
533                         {
534                             config.timeout = 0;
535                         }
536                         else if (config.type == "fan")
537                         {
538                             config.max = conf::inheritValueFromDbus;
539                             config.min = conf::inheritValueFromDbus;
540                         }
541                     }
542                     else if (sensorPathIfacePair.second == pwmInterface)
543                     {
544                         // copy so we can modify it
545                         for (std::string otherSensor : sensorNames)
546                         {
547                             std::replace(otherSensor.begin(), otherSensor.end(),
548                                          ' ', '_');
549                             if (sensorPathIfacePair.first.find(otherSensor) !=
550                                 std::string::npos)
551                             {
552                                 continue;
553                             }
554 
555                             auto& config = sensorConfig[otherSensor];
556                             config.writePath = sensorPathIfacePair.first;
557                             // todo: un-hardcode this if there are fans with
558                             // different ranges
559                             config.max = 255;
560                             config.min = 0;
561                         }
562                     }
563                 }
564 
565                 struct conf::ControllerInfo& info =
566                     conf[std::get<std::string>(base.at("Name"))];
567                 info.inputs = std::move(inputs);
568 
569                 info.type = std::get<std::string>(base.at("Class"));
570                 // todo: auto generation yaml -> c script seems to discard this
571                 // value for fans, verify this is okay
572                 if (info.type == "fan")
573                 {
574                     info.setpoint = 0;
575                 }
576                 else
577                 {
578                     info.setpoint = std::visit(VariantToDoubleVisitor(),
579                                                base.at("SetPoint"));
580                 }
581                 info.pidInfo.ts = 1.0; // currently unused
582                 info.pidInfo.proportionalCoeff = std::visit(
583                     VariantToDoubleVisitor(), base.at("PCoefficient"));
584                 info.pidInfo.integralCoeff = std::visit(
585                     VariantToDoubleVisitor(), base.at("ICoefficient"));
586                 info.pidInfo.feedFwdOffset = std::visit(
587                     VariantToDoubleVisitor(), base.at("FFOffCoefficient"));
588                 info.pidInfo.feedFwdGain = std::visit(
589                     VariantToDoubleVisitor(), base.at("FFGainCoefficient"));
590                 info.pidInfo.integralLimit.max =
591                     std::visit(VariantToDoubleVisitor(), base.at("ILimitMax"));
592                 info.pidInfo.integralLimit.min =
593                     std::visit(VariantToDoubleVisitor(), base.at("ILimitMin"));
594                 info.pidInfo.outLim.max = std::visit(VariantToDoubleVisitor(),
595                                                      base.at("OutLimitMax"));
596                 info.pidInfo.outLim.min = std::visit(VariantToDoubleVisitor(),
597                                                      base.at("OutLimitMin"));
598                 info.pidInfo.slewNeg =
599                     std::visit(VariantToDoubleVisitor(), base.at("SlewNeg"));
600                 info.pidInfo.slewPos =
601                     std::visit(VariantToDoubleVisitor(), base.at("SlewPos"));
602                 double negativeHysteresis = 0;
603                 double positiveHysteresis = 0;
604 
605                 auto findNeg = base.find("NegativeHysteresis");
606                 auto findPos = base.find("PositiveHysteresis");
607                 if (findNeg != base.end())
608                 {
609                     negativeHysteresis =
610                         std::visit(VariantToDoubleVisitor(), findNeg->second);
611                 }
612 
613                 if (findPos != base.end())
614                 {
615                     positiveHysteresis =
616                         std::visit(VariantToDoubleVisitor(), findPos->second);
617                 }
618                 info.pidInfo.negativeHysteresis = negativeHysteresis;
619                 info.pidInfo.positiveHysteresis = positiveHysteresis;
620             }
621         }
622         auto findStepwise =
623             configuration.second.find(stepwiseConfigurationInterface);
624         if (findStepwise != configuration.second.end())
625         {
626             const auto& base = findStepwise->second;
627             const std::vector<std::string>& zones =
628                 std::get<std::vector<std::string>>(base.at("Zones"));
629             for (const std::string& zone : zones)
630             {
631                 size_t index = getZoneIndex(zone, foundZones);
632                 conf::PIDConf& conf = zoneConfig[index];
633 
634                 std::vector<std::string> inputs;
635                 std::vector<std::string> sensorNames =
636                     std::get<std::vector<std::string>>(base.at("Inputs"));
637 
638                 bool sensorFound = false;
639                 for (const std::string& sensorName : sensorNames)
640                 {
641                     std::string name = sensorName;
642                     // replace spaces with underscores to be legal on dbus
643                     std::replace(name.begin(), name.end(), ' ', '_');
644                     std::vector<std::pair<std::string, std::string>>
645                         sensorPathIfacePairs;
646 
647                     if (!findSensors(sensors, name, sensorPathIfacePairs))
648                     {
649                         break;
650                     }
651 
652                     for (const auto& sensorPathIfacePair : sensorPathIfacePairs)
653                     {
654                         size_t idx =
655                             sensorPathIfacePair.first.find_last_of("/") + 1;
656                         std::string shortName =
657                             sensorPathIfacePair.first.substr(idx);
658 
659                         inputs.push_back(shortName);
660                         auto& config = sensorConfig[shortName];
661                         config.readPath = sensorPathIfacePair.first;
662                         config.type = "temp";
663                         // todo: maybe un-hardcode this if we run into slower
664                         // timeouts with sensors
665 
666                         config.timeout = 0;
667                         sensorFound = true;
668                     }
669                 }
670                 if (!sensorFound)
671                 {
672                     continue;
673                 }
674                 struct conf::ControllerInfo& info =
675                     conf[std::get<std::string>(base.at("Name"))];
676                 info.inputs = std::move(inputs);
677 
678                 info.type = "stepwise";
679                 info.stepwiseInfo.ts = 1.0; // currently unused
680                 info.stepwiseInfo.positiveHysteresis = 0.0;
681                 info.stepwiseInfo.negativeHysteresis = 0.0;
682                 std::string subtype = std::get<std::string>(base.at("Class"));
683 
684                 info.stepwiseInfo.isCeiling = (subtype == "Ceiling");
685                 auto findPosHyst = base.find("PositiveHysteresis");
686                 auto findNegHyst = base.find("NegativeHysteresis");
687                 if (findPosHyst != base.end())
688                 {
689                     info.stepwiseInfo.positiveHysteresis = std::visit(
690                         VariantToDoubleVisitor(), findPosHyst->second);
691                 }
692                 if (findNegHyst != base.end())
693                 {
694                     info.stepwiseInfo.negativeHysteresis = std::visit(
695                         VariantToDoubleVisitor(), findNegHyst->second);
696                 }
697                 std::vector<double> readings =
698                     std::get<std::vector<double>>(base.at("Reading"));
699                 if (readings.size() > ec::maxStepwisePoints)
700                 {
701                     throw std::invalid_argument("Too many stepwise points.");
702                 }
703                 if (readings.empty())
704                 {
705                     throw std::invalid_argument(
706                         "Must have one stepwise point.");
707                 }
708                 std::copy(readings.begin(), readings.end(),
709                           info.stepwiseInfo.reading);
710                 if (readings.size() < ec::maxStepwisePoints)
711                 {
712                     info.stepwiseInfo.reading[readings.size()] =
713                         std::numeric_limits<double>::quiet_NaN();
714                 }
715                 std::vector<double> outputs =
716                     std::get<std::vector<double>>(base.at("Output"));
717                 if (readings.size() != outputs.size())
718                 {
719                     throw std::invalid_argument(
720                         "Outputs size must match readings");
721                 }
722                 std::copy(outputs.begin(), outputs.end(),
723                           info.stepwiseInfo.output);
724                 if (outputs.size() < ec::maxStepwisePoints)
725                 {
726                     info.stepwiseInfo.output[outputs.size()] =
727                         std::numeric_limits<double>::quiet_NaN();
728                 }
729             }
730         }
731     }
732     if constexpr (DEBUG)
733     {
734         debugPrint();
735     }
736     if (zoneConfig.empty() || zoneDetailsConfig.empty())
737     {
738         std::cerr
739             << "No fan zones, application pausing until new configuration\n";
740         return false;
741     }
742     return true;
743 }
744 } // namespace dbus_configuration
745