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