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