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