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