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