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