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