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