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