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