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