xref: /openbmc/phosphor-pid-control/dbus/dbusconfiguration.cpp (revision a552fe2f014997573b7fe5716545e74b6d15a9d1)
1 /*
2 // Copyright (c) 2018 Intel Corporation
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 //      http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 */
16 #include "config.h"
17 
18 #include "dbusconfiguration.hpp"
19 
20 #include "conf.hpp"
21 #include "dbushelper.hpp"
22 #include "dbusutil.hpp"
23 #include "util.hpp"
24 
25 #include <boost/asio/steady_timer.hpp>
26 #include <sdbusplus/bus.hpp>
27 #include <sdbusplus/bus/match.hpp>
28 #include <sdbusplus/exception.hpp>
29 
30 #include <algorithm>
31 #include <chrono>
32 #include <functional>
33 #include <iostream>
34 #include <list>
35 #include <set>
36 #include <unordered_map>
37 #include <variant>
38 
39 namespace pid_control
40 {
41 
42 constexpr const char* pidConfigurationInterface =
43     "xyz.openbmc_project.Configuration.Pid";
44 constexpr const char* objectManagerInterface =
45     "org.freedesktop.DBus.ObjectManager";
46 constexpr const char* pidZoneConfigurationInterface =
47     "xyz.openbmc_project.Configuration.Pid.Zone";
48 constexpr const char* stepwiseConfigurationInterface =
49     "xyz.openbmc_project.Configuration.Stepwise";
50 constexpr const char* thermalControlIface =
51     "xyz.openbmc_project.Control.ThermalMode";
52 constexpr const char* sensorInterface = "xyz.openbmc_project.Sensor.Value";
53 constexpr const char* defaultPwmInterface =
54     "xyz.openbmc_project.Control.FanPwm";
55 
56 using Association = std::tuple<std::string, std::string, std::string>;
57 using Associations = std::vector<Association>;
58 
59 namespace thresholds
60 {
61 constexpr const char* warningInterface =
62     "xyz.openbmc_project.Sensor.Threshold.Warning";
63 constexpr const char* criticalInterface =
64     "xyz.openbmc_project.Sensor.Threshold.Critical";
65 const std::array<const char*, 4> types = {"CriticalLow", "CriticalHigh",
66                                           "WarningLow", "WarningHigh"};
67 
68 } // namespace thresholds
69 
70 namespace dbus_configuration
71 {
72 using SensorInterfaceType = std::pair<std::string, std::string>;
73 
getSensorNameFromPath(const std::string & dbusPath)74 inline std::string getSensorNameFromPath(const std::string& dbusPath)
75 {
76     return dbusPath.substr(dbusPath.find_last_of("/") + 1);
77 }
78 
sensorNameToDbusName(const std::string & sensorName)79 inline std::string sensorNameToDbusName(const std::string& sensorName)
80 {
81     std::string retString = sensorName;
82     std::replace(retString.begin(), retString.end(), ' ', '_');
83     return retString;
84 }
85 
getSelectedProfiles(sdbusplus::bus_t & bus)86 std::vector<std::string> getSelectedProfiles(sdbusplus::bus_t& bus)
87 {
88     std::vector<std::string> ret;
89     auto mapper =
90         bus.new_method_call("xyz.openbmc_project.ObjectMapper",
91                             "/xyz/openbmc_project/object_mapper",
92                             "xyz.openbmc_project.ObjectMapper", "GetSubTree");
93     mapper.append("/", 0, std::array<const char*, 1>{thermalControlIface});
94     std::unordered_map<
95         std::string, std::unordered_map<std::string, std::vector<std::string>>>
96         respData;
97 
98     try
99     {
100         auto resp = bus.call(mapper);
101         resp.read(respData);
102     }
103     catch (const sdbusplus::exception_t&)
104     {
105         // can't do anything without mapper call data
106         throw std::runtime_error("ObjectMapper Call Failure");
107     }
108     if (respData.empty())
109     {
110         // if the user has profiles but doesn't expose the interface to select
111         // one, just go ahead without using profiles
112         return ret;
113     }
114 
115     // assumption is that we should only have a small handful of selected
116     // profiles at a time (probably only 1), so calling each individually should
117     // not incur a large cost
118     for (const auto& objectPair : respData)
119     {
120         const std::string& path = objectPair.first;
121         for (const auto& ownerPair : objectPair.second)
122         {
123             const std::string& busName = ownerPair.first;
124             auto getProfile =
125                 bus.new_method_call(busName.c_str(), path.c_str(),
126                                     "org.freedesktop.DBus.Properties", "Get");
127             getProfile.append(thermalControlIface, "Current");
128             std::variant<std::string> variantResp;
129             try
130             {
131                 auto resp = bus.call(getProfile);
132                 resp.read(variantResp);
133             }
134             catch (const sdbusplus::exception_t&)
135             {
136                 throw std::runtime_error("Failure getting profile");
137             }
138             std::string mode = std::get<std::string>(variantResp);
139             ret.emplace_back(std::move(mode));
140         }
141     }
142     if constexpr (pid_control::conf::DEBUG)
143     {
144         std::cout << "Profiles selected: ";
145         for (const auto& profile : ret)
146         {
147             std::cout << profile << " ";
148         }
149         std::cout << "\n";
150     }
151     return ret;
152 }
153 
eventHandler(sd_bus_message * m,void * context,sd_bus_error *)154 int eventHandler(sd_bus_message* m, void* context, sd_bus_error*)
155 {
156     if (context == nullptr || m == nullptr)
157     {
158         throw std::runtime_error("Invalid match");
159     }
160 
161     // we skip associations because the mapper populates these, not the sensors
162     const std::array<const char*, 2> skipList = {
163         "xyz.openbmc_project.Association",
164         "xyz.openbmc_project.Association.Definitions"};
165 
166     sdbusplus::message_t message(m);
167     if (std::string(message.get_member()) == "InterfacesAdded")
168     {
169         sdbusplus::message::object_path path;
170         std::unordered_map<
171             std::string,
172             std::unordered_map<std::string, std::variant<Associations, bool>>>
173             data;
174 
175         message.read(path, data);
176 
177         for (const char* skip : skipList)
178         {
179             auto find = data.find(skip);
180             if (find != data.end())
181             {
182                 data.erase(find);
183                 if (data.empty())
184                 {
185                     return 1;
186                 }
187             }
188         }
189 
190         if constexpr (pid_control::conf::DEBUG)
191         {
192             std::cout << "New config detected: " << path.str << std::endl;
193             for (auto& d : data)
194             {
195                 std::cout << "\tdata is " << d.first << std::endl;
196                 for (auto& second : d.second)
197                 {
198                     std::cout << "\t\tdata is " << second.first << std::endl;
199                 }
200             }
201         }
202     }
203 
204     boost::asio::steady_timer* timer =
205         static_cast<boost::asio::steady_timer*>(context);
206 
207     // do a brief sleep as we tend to get a bunch of these events at
208     // once
209     timer->expires_after(std::chrono::seconds(2));
210     timer->async_wait([](const boost::system::error_code ec) {
211         if (ec == boost::asio::error::operation_aborted)
212         {
213             /* another timer started*/
214             return;
215         }
216 
217         std::cout << "New configuration detected, reloading\n.";
218         tryRestartControlLoops();
219     });
220 
221     return 1;
222 }
223 
createMatches(sdbusplus::bus_t & bus,boost::asio::steady_timer & timer)224 void createMatches(sdbusplus::bus_t& bus, boost::asio::steady_timer& timer)
225 {
226     // this is a list because the matches can't be moved
227     static std::list<sdbusplus::bus::match_t> matches;
228 
229     const std::array<std::string, 4> interfaces = {
230         thermalControlIface, pidConfigurationInterface,
231         pidZoneConfigurationInterface, stepwiseConfigurationInterface};
232 
233     // this list only needs to be created once
234     if (!matches.empty())
235     {
236         return;
237     }
238 
239     // we restart when the configuration changes or there are new sensors
240     for (const auto& interface : interfaces)
241     {
242         matches.emplace_back(
243             bus,
244             "type='signal',member='PropertiesChanged',arg0namespace='" +
245                 interface + "'",
246             eventHandler, &timer);
247     }
248     matches.emplace_back(
249         bus,
250         "type='signal',member='InterfacesAdded',arg0path='/xyz/openbmc_project/"
251         "sensors/'",
252         eventHandler, &timer);
253     matches.emplace_back(bus,
254                          "type='signal',member='InterfacesRemoved',arg0path='/"
255                          "xyz/openbmc_project/sensors/'",
256                          eventHandler, &timer);
257 }
258 
259 /**
260  * retrieve an attribute from the pid configuration map
261  * @param[in] base - the PID configuration map, keys are the attributes and
262  * value is the variant associated with that attribute.
263  * @param attributeName - the name of the attribute
264  * @return a variant holding the value associated with a key
265  * @throw runtime_error : attributeName is not in base
266  */
getPIDAttribute(const std::unordered_map<std::string,DbusVariantType> & base,const std::string & attributeName)267 inline DbusVariantType getPIDAttribute(
268     const std::unordered_map<std::string, DbusVariantType>& base,
269     const std::string& attributeName)
270 {
271     auto search = base.find(attributeName);
272     if (search == base.end())
273     {
274         throw std::runtime_error("missing attribute " + attributeName);
275     }
276     return search->second;
277 }
278 
getCycleTimeSetting(const std::unordered_map<std::string,DbusVariantType> & zone,const int zoneIndex,const std::string & attributeName,uint64_t & value)279 inline void getCycleTimeSetting(
280     const std::unordered_map<std::string, DbusVariantType>& zone,
281     const int zoneIndex, const std::string& attributeName, uint64_t& value)
282 {
283     auto findAttributeName = zone.find(attributeName);
284     if (findAttributeName != zone.end())
285     {
286         double tmpAttributeValue =
287             std::visit(VariantToDoubleVisitor(), zone.at(attributeName));
288         if (tmpAttributeValue >= 1.0)
289         {
290             value = static_cast<uint64_t>(tmpAttributeValue);
291         }
292         else
293         {
294             std::cerr << "Zone " << zoneIndex << ": " << attributeName
295                       << " is invalid. Use default " << value << " ms\n";
296         }
297     }
298     else
299     {
300         std::cerr << "Zone " << zoneIndex << ": " << attributeName
301                   << " cannot find setting. Use default " << value << " ms\n";
302     }
303 }
304 
populatePidInfo(sdbusplus::bus_t & bus,const std::unordered_map<std::string,DbusVariantType> & base,conf::ControllerInfo & info,const std::string * thresholdProperty,const std::map<std::string,conf::SensorConfig> & sensorConfig)305 void populatePidInfo(
306     [[maybe_unused]] sdbusplus::bus_t& bus,
307     const std::unordered_map<std::string, DbusVariantType>& base,
308     conf::ControllerInfo& info, const std::string* thresholdProperty,
309     const std::map<std::string, conf::SensorConfig>& sensorConfig)
310 {
311     info.type = std::get<std::string>(getPIDAttribute(base, "Class"));
312     if (info.type == "fan")
313     {
314         info.setpoint = 0;
315     }
316     else
317     {
318         info.setpoint = std::visit(VariantToDoubleVisitor(),
319                                    getPIDAttribute(base, "SetPoint"));
320     }
321 
322     int failsafepercent = 0;
323     auto findFailSafe = base.find("FailSafePercent");
324     if (findFailSafe != base.end())
325     {
326         failsafepercent = std::visit(VariantToDoubleVisitor(),
327                                      getPIDAttribute(base, "FailSafePercent"));
328     }
329     info.failSafePercent = failsafepercent;
330 
331     if (thresholdProperty != nullptr)
332     {
333         std::string interface;
334         if (*thresholdProperty == "WarningHigh" ||
335             *thresholdProperty == "WarningLow")
336         {
337             interface = thresholds::warningInterface;
338         }
339         else
340         {
341             interface = thresholds::criticalInterface;
342         }
343 
344         // Although this checks only the first vector element for the
345         // named threshold, it is OK, because the SetPointOffset parser
346         // splits up the input into individual vectors, each with only a
347         // single element, if it detects that SetPointOffset is in use.
348         const std::string& path =
349             sensorConfig.at(info.inputs.front().name).readPath;
350 
351         DbusHelper helper(sdbusplus::bus::new_system());
352         std::string service = helper.getService(interface, path);
353         double reading = 0;
354         try
355         {
356             helper.getProperty(service, path, interface, *thresholdProperty,
357                                reading);
358         }
359         catch (const sdbusplus::exception_t& ex)
360         {
361             // unsupported threshold, leaving reading at 0
362         }
363 
364         info.setpoint += reading;
365     }
366 
367     info.pidInfo.ts = 1.0; // currently unused
368     info.pidInfo.proportionalCoeff = std::visit(
369         VariantToDoubleVisitor(), getPIDAttribute(base, "PCoefficient"));
370     info.pidInfo.integralCoeff = std::visit(
371         VariantToDoubleVisitor(), getPIDAttribute(base, "ICoefficient"));
372     // DCoefficient is below, it is optional, same reason as in buildjson.cpp
373     info.pidInfo.feedFwdOffset = std::visit(
374         VariantToDoubleVisitor(), getPIDAttribute(base, "FFOffCoefficient"));
375     info.pidInfo.feedFwdGain = std::visit(
376         VariantToDoubleVisitor(), getPIDAttribute(base, "FFGainCoefficient"));
377     info.pidInfo.integralLimit.max = std::visit(
378         VariantToDoubleVisitor(), getPIDAttribute(base, "ILimitMax"));
379     info.pidInfo.integralLimit.min = std::visit(
380         VariantToDoubleVisitor(), getPIDAttribute(base, "ILimitMin"));
381     info.pidInfo.outLim.max = std::visit(VariantToDoubleVisitor(),
382                                          getPIDAttribute(base, "OutLimitMax"));
383     info.pidInfo.outLim.min = std::visit(VariantToDoubleVisitor(),
384                                          getPIDAttribute(base, "OutLimitMin"));
385     info.pidInfo.slewNeg =
386         std::visit(VariantToDoubleVisitor(), getPIDAttribute(base, "SlewNeg"));
387     info.pidInfo.slewPos =
388         std::visit(VariantToDoubleVisitor(), getPIDAttribute(base, "SlewPos"));
389 
390     bool checkHysterWithSetpt = false;
391     double negativeHysteresis = 0;
392     double positiveHysteresis = 0;
393     double derivativeCoeff = 0;
394 
395     auto findCheckHysterFlag = base.find("CheckHysteresisWithSetpoint");
396     auto findNeg = base.find("NegativeHysteresis");
397     auto findPos = base.find("PositiveHysteresis");
398     auto findDerivative = base.find("DCoefficient");
399 
400     if (findCheckHysterFlag != base.end())
401     {
402         checkHysterWithSetpt = std::get<bool>(findCheckHysterFlag->second);
403     }
404     if (findNeg != base.end())
405     {
406         negativeHysteresis =
407             std::visit(VariantToDoubleVisitor(), findNeg->second);
408     }
409     if (findPos != base.end())
410     {
411         positiveHysteresis =
412             std::visit(VariantToDoubleVisitor(), findPos->second);
413     }
414     if (findDerivative != base.end())
415     {
416         derivativeCoeff =
417             std::visit(VariantToDoubleVisitor(), findDerivative->second);
418     }
419 
420     info.pidInfo.checkHysterWithSetpt = checkHysterWithSetpt;
421     info.pidInfo.negativeHysteresis = negativeHysteresis;
422     info.pidInfo.positiveHysteresis = positiveHysteresis;
423     info.pidInfo.derivativeCoeff = derivativeCoeff;
424 }
425 
init(sdbusplus::bus_t & bus,boost::asio::steady_timer & timer,std::map<std::string,conf::SensorConfig> & sensorConfig,std::map<int64_t,conf::PIDConf> & zoneConfig,std::map<int64_t,conf::ZoneConfig> & zoneDetailsConfig)426 bool init(sdbusplus::bus_t& bus, boost::asio::steady_timer& timer,
427           std::map<std::string, conf::SensorConfig>& sensorConfig,
428           std::map<int64_t, conf::PIDConf>& zoneConfig,
429           std::map<int64_t, conf::ZoneConfig>& zoneDetailsConfig)
430 {
431     sensorConfig.clear();
432     zoneConfig.clear();
433     zoneDetailsConfig.clear();
434 
435     createMatches(bus, timer);
436 
437     auto mapper =
438         bus.new_method_call("xyz.openbmc_project.ObjectMapper",
439                             "/xyz/openbmc_project/object_mapper",
440                             "xyz.openbmc_project.ObjectMapper", "GetSubTree");
441     mapper.append(
442         "/", 0,
443         std::array<const char*, 6>{
444             objectManagerInterface, pidConfigurationInterface,
445             pidZoneConfigurationInterface, stepwiseConfigurationInterface,
446             sensorInterface, defaultPwmInterface});
447     std::unordered_map<
448         std::string, std::unordered_map<std::string, std::vector<std::string>>>
449         respData;
450     try
451     {
452         auto resp = bus.call(mapper);
453         resp.read(respData);
454     }
455     catch (const sdbusplus::exception_t&)
456     {
457         // can't do anything without mapper call data
458         throw std::runtime_error("ObjectMapper Call Failure");
459     }
460 
461     if (respData.empty())
462     {
463         // can't do anything without mapper call data
464         throw std::runtime_error("No configuration data available from Mapper");
465     }
466     // create a map of pair of <has pid configuration, ObjectManager path>
467     std::unordered_map<std::string, std::pair<bool, std::string>> owners;
468     // and a map of <path, interface> for sensors
469     std::unordered_map<std::string, std::string> sensors;
470     for (const auto& objectPair : respData)
471     {
472         for (const auto& ownerPair : objectPair.second)
473         {
474             auto& owner = owners[ownerPair.first];
475             for (const std::string& interface : ownerPair.second)
476             {
477                 if (interface == objectManagerInterface)
478                 {
479                     owner.second = objectPair.first;
480                 }
481                 if (interface == pidConfigurationInterface ||
482                     interface == pidZoneConfigurationInterface ||
483                     interface == stepwiseConfigurationInterface)
484                 {
485                     owner.first = true;
486                 }
487                 if (interface == sensorInterface ||
488                     interface == defaultPwmInterface)
489                 {
490                     // we're not interested in pwm sensors, just pwm control
491                     if (interface == sensorInterface &&
492                         objectPair.first.find("pwm") != std::string::npos)
493                     {
494                         continue;
495                     }
496                     sensors[objectPair.first] = interface;
497                 }
498             }
499         }
500     }
501     ManagedObjectType configurations;
502     for (const auto& owner : owners)
503     {
504         // skip if no pid configuration (means probably a sensor)
505         if (!owner.second.first)
506         {
507             continue;
508         }
509         auto endpoint = bus.new_method_call(
510             owner.first.c_str(), owner.second.second.c_str(),
511             "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
512         ManagedObjectType configuration;
513         try
514         {
515             auto response = bus.call(endpoint);
516             response.read(configuration);
517         }
518         catch (const sdbusplus::exception_t&)
519         {
520             // this shouldn't happen, probably means daemon crashed
521             throw std::runtime_error(
522                 "Error getting managed objects from " + owner.first);
523         }
524 
525         for (auto& pathPair : configuration)
526         {
527             if (pathPair.second.find(pidConfigurationInterface) !=
528                     pathPair.second.end() ||
529                 pathPair.second.find(pidZoneConfigurationInterface) !=
530                     pathPair.second.end() ||
531                 pathPair.second.find(stepwiseConfigurationInterface) !=
532                     pathPair.second.end())
533             {
534                 configurations.emplace(pathPair);
535             }
536         }
537     }
538 
539     // remove controllers from config that aren't in the current profile(s)
540     std::vector<std::string> selectedProfiles = getSelectedProfiles(bus);
541     if (selectedProfiles.size())
542     {
543         for (auto pathIt = configurations.begin();
544              pathIt != configurations.end();)
545         {
546             for (auto confIt = pathIt->second.begin();
547                  confIt != pathIt->second.end();)
548             {
549                 auto profilesFind = confIt->second.find("Profiles");
550                 if (profilesFind == confIt->second.end())
551                 {
552                     confIt++;
553                     continue; // if no profiles selected, apply always
554                 }
555                 auto profiles =
556                     std::get<std::vector<std::string>>(profilesFind->second);
557                 if (profiles.empty())
558                 {
559                     confIt++;
560                     continue;
561                 }
562 
563                 bool found = false;
564                 for (const std::string& profile : profiles)
565                 {
566                     if (std::find(selectedProfiles.begin(),
567                                   selectedProfiles.end(), profile) !=
568                         selectedProfiles.end())
569                     {
570                         found = true;
571                         break;
572                     }
573                 }
574                 if (found)
575                 {
576                     confIt++;
577                 }
578                 else
579                 {
580                     confIt = pathIt->second.erase(confIt);
581                 }
582             }
583             if (pathIt->second.empty())
584             {
585                 pathIt = configurations.erase(pathIt);
586             }
587             else
588             {
589                 pathIt++;
590             }
591         }
592     }
593 
594     // On D-Bus, although not necessary,
595     // having the "zoneID" field can still be useful,
596     // as it is used for diagnostic messages,
597     // logging file names, and so on.
598     // Accept optional "ZoneIndex" parameter to explicitly specify.
599     // If not present, or not unique, auto-assign index,
600     // using 0-based numbering, ensuring uniqueness.
601     std::map<std::string, int64_t> foundZones;
602     for (const auto& configuration : configurations)
603     {
604         auto findZone =
605             configuration.second.find(pidZoneConfigurationInterface);
606         if (findZone != configuration.second.end())
607         {
608             const auto& zone = findZone->second;
609 
610             const std::string& name = std::get<std::string>(zone.at("Name"));
611 
612             auto findZoneIndex = zone.find("ZoneIndex");
613             if (findZoneIndex == zone.end())
614             {
615                 continue;
616             }
617 
618             auto ptrZoneIndex = std::get_if<double>(&(findZoneIndex->second));
619             if (!ptrZoneIndex)
620             {
621                 continue;
622             }
623 
624             auto desiredIndex = static_cast<int64_t>(*ptrZoneIndex);
625             auto grantedIndex = setZoneIndex(name, foundZones, desiredIndex);
626             std::cout << "Zone " << name << " is at ZoneIndex " << grantedIndex
627                       << "\n";
628         }
629     }
630 
631     for (const auto& configuration : configurations)
632     {
633         auto findZone =
634             configuration.second.find(pidZoneConfigurationInterface);
635         if (findZone != configuration.second.end())
636         {
637             const auto& zone = findZone->second;
638 
639             const std::string& name = std::get<std::string>(zone.at("Name"));
640 
641             auto index = getZoneIndex(name, foundZones);
642 
643             auto& details = zoneDetailsConfig[index];
644 
645             details.minThermalOutput = std::visit(VariantToDoubleVisitor(),
646                                                   zone.at("MinThermalOutput"));
647 
648             int failsafepercent = 0;
649             auto findFailSafe = zone.find("FailSafePercent");
650             if (findFailSafe != zone.end())
651             {
652                 failsafepercent = std::visit(VariantToDoubleVisitor(),
653                                              zone.at("FailSafePercent"));
654             }
655             details.failsafePercent = failsafepercent;
656 
657             getCycleTimeSetting(zone, index, "CycleIntervalTimeMS",
658                                 details.cycleTime.cycleIntervalTimeMS);
659             getCycleTimeSetting(zone, index, "UpdateThermalsTimeMS",
660                                 details.cycleTime.updateThermalsTimeMS);
661 
662             bool accumulateSetPoint = false;
663             auto findAccSetPoint = zone.find("AccumulateSetPoint");
664             if (findAccSetPoint != zone.end())
665             {
666                 accumulateSetPoint = std::get<bool>(findAccSetPoint->second);
667             }
668             details.accumulateSetPoint = accumulateSetPoint;
669         }
670         auto findBase = configuration.second.find(pidConfigurationInterface);
671         // loop through all the PID configurations and fill out a sensor config
672         if (findBase != configuration.second.end())
673         {
674             const auto& base =
675                 configuration.second.at(pidConfigurationInterface);
676             const std::string pidName =
677                 sensorNameToDbusName(std::get<std::string>(base.at("Name")));
678             const std::string pidClass =
679                 std::get<std::string>(base.at("Class"));
680             const std::vector<std::string>& zones =
681                 std::get<std::vector<std::string>>(base.at("Zones"));
682             for (const std::string& zone : zones)
683             {
684                 auto index = getZoneIndex(zone, foundZones);
685 
686                 conf::PIDConf& conf = zoneConfig[index];
687                 std::vector<std::string> inputSensorNames(
688                     std::get<std::vector<std::string>>(base.at("Inputs")));
689                 std::vector<std::string> outputSensorNames;
690                 std::vector<std::string> missingAcceptableSensorNames;
691                 std::vector<std::string> archivedInputSensorNames;
692 
693                 auto findMissingAcceptable = base.find("MissingIsAcceptable");
694                 if (findMissingAcceptable != base.end())
695                 {
696                     missingAcceptableSensorNames =
697                         std::get<std::vector<std::string>>(
698                             findMissingAcceptable->second);
699                 }
700 
701                 // assumption: all fan pids must have at least one output
702                 if (pidClass == "fan")
703                 {
704                     outputSensorNames = std::get<std::vector<std::string>>(
705                         getPIDAttribute(base, "Outputs"));
706                 }
707 
708                 bool unavailableAsFailed = true;
709                 auto findUnavailableAsFailed =
710                     base.find("InputUnavailableAsFailed");
711                 if (findUnavailableAsFailed != base.end())
712                 {
713                     unavailableAsFailed =
714                         std::get<bool>(findUnavailableAsFailed->second);
715                 }
716 
717                 std::vector<SensorInterfaceType> inputSensorInterfaces;
718                 std::vector<SensorInterfaceType> outputSensorInterfaces;
719                 std::vector<SensorInterfaceType>
720                     missingAcceptableSensorInterfaces;
721 
722                 /* populate an interface list for different sensor direction
723                  * types (input,output)
724                  */
725                 /* take the Inputs from the configuration and generate
726                  * a list of dbus descriptors (path, interface).
727                  * Mapping can be many-to-one since an element of Inputs can be
728                  * a regex
729                  */
730                 for (const std::string& sensorName : inputSensorNames)
731                 {
732 #ifndef HANDLE_MISSING_OBJECT_PATHS
733                     findSensors(sensors, sensorNameToDbusName(sensorName),
734                                 inputSensorInterfaces);
735 #else
736                     std::vector<std::pair<std::string, std::string>>
737                         sensorPathIfacePairs;
738                     auto found =
739                         findSensors(sensors, sensorNameToDbusName(sensorName),
740                                     sensorPathIfacePairs);
741                     if (found)
742                     {
743                         inputSensorInterfaces.insert(
744                             inputSensorInterfaces.end(),
745                             sensorPathIfacePairs.begin(),
746                             sensorPathIfacePairs.end());
747                     }
748                     else if (pidClass != "fan")
749                     {
750                         if (std::find(missingAcceptableSensorNames.begin(),
751                                       missingAcceptableSensorNames.end(),
752                                       sensorName) ==
753                             missingAcceptableSensorNames.end())
754                         {
755                             std::cerr
756                                 << "Pid controller: Missing a missing-unacceptable sensor from D-Bus "
757                                 << sensorName << "\n";
758                             std::string inputSensorName =
759                                 sensorNameToDbusName(sensorName);
760                             auto& config = sensorConfig[inputSensorName];
761                             archivedInputSensorNames.push_back(inputSensorName);
762                             config.type = pidClass;
763                             config.readPath =
764                                 getSensorPath(config.type, inputSensorName);
765                             config.timeout = 0;
766                             config.ignoreDbusMinMax = true;
767                             config.unavailableAsFailed = unavailableAsFailed;
768                         }
769                         else
770                         {
771                             // When an input sensor is NOT on DBus, and it's in
772                             // the MissingIsAcceptable list. Ignore it and
773                             // continue with the next input sensor.
774                             std::cout
775                                 << "Pid controller: Missing a missing-acceptable sensor from D-Bus "
776                                 << sensorName << "\n";
777                             continue;
778                         }
779                     }
780 #endif
781                 }
782                 for (const std::string& sensorName : outputSensorNames)
783                 {
784                     findSensors(sensors, sensorNameToDbusName(sensorName),
785                                 outputSensorInterfaces);
786                 }
787                 for (const std::string& sensorName :
788                      missingAcceptableSensorNames)
789                 {
790                     findSensors(sensors, sensorNameToDbusName(sensorName),
791                                 missingAcceptableSensorInterfaces);
792                 }
793 
794                 for (const SensorInterfaceType& inputSensorInterface :
795                      inputSensorInterfaces)
796                 {
797                     const std::string& dbusInterface =
798                         inputSensorInterface.second;
799                     const std::string& inputSensorPath =
800                         inputSensorInterface.first;
801 
802                     // Setting timeout to 0 is intentional, as D-Bus passive
803                     // sensor updates are pushed in, not pulled by timer poll.
804                     // Setting ignoreDbusMinMax is intentional, as this
805                     // prevents normalization of values to [0.0, 1.0] range,
806                     // which would mess up the PID loop math.
807                     // All non-fan PID classes should be initialized this way.
808                     // As for why a fan should not use this code path, see
809                     // the ed1dafdf168def37c65bfb7a5efd18d9dbe04727 commit.
810                     if ((pidClass == "temp") || (pidClass == "margin") ||
811                         (pidClass == "power") || (pidClass == "powersum"))
812                     {
813                         std::string inputSensorName =
814                             getSensorNameFromPath(inputSensorPath);
815                         auto& config = sensorConfig[inputSensorName];
816                         archivedInputSensorNames.push_back(inputSensorName);
817                         config.type = pidClass;
818                         config.readPath = inputSensorInterface.first;
819                         config.timeout = 0;
820                         config.ignoreDbusMinMax = true;
821                         config.unavailableAsFailed = unavailableAsFailed;
822                     }
823 
824                     if (dbusInterface != sensorInterface)
825                     {
826                         /* all expected inputs in the configuration are expected
827                          * to be sensor interfaces
828                          */
829                         throw std::runtime_error(
830                             "sensor at dbus path [" + inputSensorPath +
831                             "] has an interface [" + dbusInterface +
832                             "] that does not match the expected interface of " +
833                             sensorInterface);
834                     }
835                 }
836 
837                 // MissingIsAcceptable same postprocessing as Inputs
838                 missingAcceptableSensorNames.clear();
839                 for (const SensorInterfaceType&
840                          missingAcceptableSensorInterface :
841                      missingAcceptableSensorInterfaces)
842                 {
843                     const std::string& dbusInterface =
844                         missingAcceptableSensorInterface.second;
845                     const std::string& missingAcceptableSensorPath =
846                         missingAcceptableSensorInterface.first;
847 
848                     std::string missingAcceptableSensorName =
849                         getSensorNameFromPath(missingAcceptableSensorPath);
850                     missingAcceptableSensorNames.push_back(
851                         missingAcceptableSensorName);
852 
853                     if (dbusInterface != sensorInterface)
854                     {
855                         /* MissingIsAcceptable same error checking as Inputs
856                          */
857                         throw std::runtime_error(
858                             "sensor at dbus path [" +
859                             missingAcceptableSensorPath +
860                             "] has an interface [" + dbusInterface +
861                             "] that does not match the expected interface of " +
862                             sensorInterface);
863                     }
864                 }
865 
866                 /* fan pids need to pair up tach sensors with their pwm
867                  * counterparts
868                  */
869                 if (pidClass == "fan")
870                 {
871                     /* If a PID is a fan there should be either
872                      * (1) one output(pwm) per input(tach)
873                      * OR
874                      * (2) one putput(pwm) for all inputs(tach)
875                      * everything else indicates a bad configuration.
876                      */
877                     bool singlePwm = false;
878                     if (outputSensorInterfaces.size() == 1)
879                     {
880                         /* one pwm, set write paths for all fan sensors to it */
881                         singlePwm = true;
882                     }
883                     else if (inputSensorInterfaces.size() ==
884                              outputSensorInterfaces.size())
885                     {
886                         /* one to one mapping, each fan sensor gets its own pwm
887                          * control */
888                         singlePwm = false;
889                     }
890                     else
891                     {
892                         throw std::runtime_error(
893                             "fan PID has invalid number of Outputs");
894                     }
895                     std::string fanSensorName;
896                     std::string pwmPath;
897                     std::string pwmInterface;
898                     std::string pwmSensorName;
899                     if (singlePwm)
900                     {
901                         /* if just a single output(pwm) is provided then use
902                          * that pwm control path for all the fan sensor write
903                          * path configs
904                          */
905                         pwmPath = outputSensorInterfaces.at(0).first;
906                         pwmInterface = outputSensorInterfaces.at(0).second;
907                     }
908                     for (uint32_t idx = 0; idx < inputSensorInterfaces.size();
909                          idx++)
910                     {
911                         if (!singlePwm)
912                         {
913                             pwmPath = outputSensorInterfaces.at(idx).first;
914                             pwmInterface =
915                                 outputSensorInterfaces.at(idx).second;
916                         }
917                         if (defaultPwmInterface != pwmInterface)
918                         {
919                             throw std::runtime_error(
920                                 "fan pwm control at dbus path [" + pwmPath +
921                                 "] has an interface [" + pwmInterface +
922                                 "] that does not match the expected interface "
923                                 "of " +
924                                 defaultPwmInterface);
925                         }
926                         const std::string& fanPath =
927                             inputSensorInterfaces.at(idx).first;
928                         fanSensorName = getSensorNameFromPath(fanPath);
929                         pwmSensorName = getSensorNameFromPath(pwmPath);
930                         std::string fanPwmIndex = fanSensorName + pwmSensorName;
931                         archivedInputSensorNames.push_back(fanPwmIndex);
932                         auto& fanConfig = sensorConfig[fanPwmIndex];
933                         fanConfig.type = pidClass;
934                         fanConfig.readPath = fanPath;
935                         fanConfig.writePath = pwmPath;
936                         // todo: un-hardcode this if there are fans with
937                         // different ranges
938                         fanConfig.max = 255;
939                         fanConfig.min = 0;
940                     }
941                 }
942                 // if the sensors aren't available in the current state, don't
943                 // add them to the configuration.
944                 if (archivedInputSensorNames.empty())
945                 {
946                     continue;
947                 }
948 
949                 std::string offsetType;
950 
951                 // SetPointOffset is a threshold value to pull from the sensor
952                 // to apply an offset. For upper thresholds this means the
953                 // setpoint is usually negative.
954                 auto findSetpointOffset = base.find("SetPointOffset");
955                 if (findSetpointOffset != base.end())
956                 {
957                     offsetType =
958                         std::get<std::string>(findSetpointOffset->second);
959                     if (std::find(thresholds::types.begin(),
960                                   thresholds::types.end(), offsetType) ==
961                         thresholds::types.end())
962                     {
963                         throw std::runtime_error(
964                             "Unsupported type: " + offsetType);
965                     }
966                 }
967 
968                 std::vector<double> inputTempToMargin;
969 
970                 auto findTempToMargin = base.find("TempToMargin");
971                 if (findTempToMargin != base.end())
972                 {
973                     inputTempToMargin =
974                         std::get<std::vector<double>>(findTempToMargin->second);
975                 }
976 
977                 std::vector<pid_control::conf::SensorInput> sensorInputs =
978                     spliceInputs(archivedInputSensorNames, inputTempToMargin,
979                                  missingAcceptableSensorNames);
980 
981                 if (offsetType.empty())
982                 {
983                     conf::ControllerInfo& info = conf[pidName];
984                     info.inputs = std::move(sensorInputs);
985                     populatePidInfo(bus, base, info, nullptr, sensorConfig);
986                 }
987                 else
988                 {
989                     // we have to split up the inputs, as in practice t-control
990                     // values will differ, making setpoints differ
991                     for (const pid_control::conf::SensorInput& input :
992                          sensorInputs)
993                     {
994                         conf::ControllerInfo& info = conf[input.name];
995                         info.inputs.emplace_back(input);
996                         populatePidInfo(bus, base, info, &offsetType,
997                                         sensorConfig);
998                     }
999                 }
1000             }
1001         }
1002         auto findStepwise =
1003             configuration.second.find(stepwiseConfigurationInterface);
1004         if (findStepwise != configuration.second.end())
1005         {
1006             const auto& base = findStepwise->second;
1007             const std::string pidName =
1008                 sensorNameToDbusName(std::get<std::string>(base.at("Name")));
1009             const std::vector<std::string>& zones =
1010                 std::get<std::vector<std::string>>(base.at("Zones"));
1011             for (const std::string& zone : zones)
1012             {
1013                 auto index = getZoneIndex(zone, foundZones);
1014 
1015                 conf::PIDConf& conf = zoneConfig[index];
1016 
1017                 std::vector<std::string> inputs;
1018                 std::vector<std::string> missingAcceptableSensors;
1019                 std::vector<std::string> missingAcceptableSensorNames;
1020                 std::vector<std::string> sensorNames =
1021                     std::get<std::vector<std::string>>(base.at("Inputs"));
1022 
1023                 auto findMissingAcceptable = base.find("MissingIsAcceptable");
1024                 if (findMissingAcceptable != base.end())
1025                 {
1026                     missingAcceptableSensorNames =
1027                         std::get<std::vector<std::string>>(
1028                             findMissingAcceptable->second);
1029                 }
1030 
1031                 bool unavailableAsFailed = true;
1032                 auto findUnavailableAsFailed =
1033                     base.find("InputUnavailableAsFailed");
1034                 if (findUnavailableAsFailed != base.end())
1035                 {
1036                     unavailableAsFailed =
1037                         std::get<bool>(findUnavailableAsFailed->second);
1038                 }
1039 
1040                 bool sensorFound = false;
1041                 for (const std::string& sensorName : sensorNames)
1042                 {
1043                     std::vector<std::pair<std::string, std::string>>
1044                         sensorPathIfacePairs;
1045                     if (!findSensors(sensors, sensorNameToDbusName(sensorName),
1046                                      sensorPathIfacePairs))
1047                     {
1048 #ifndef HANDLE_MISSING_OBJECT_PATHS
1049                         break;
1050 #else
1051                         if (std::find(missingAcceptableSensorNames.begin(),
1052                                       missingAcceptableSensorNames.end(),
1053                                       sensorName) ==
1054                             missingAcceptableSensorNames.end())
1055                         {
1056                             // When an input sensor is NOT on DBus, and it's NOT
1057                             // in the MissingIsAcceptable list. Build it as a
1058                             // failed sensor with default information (temp
1059                             // sensor path, temp type, ...)
1060                             std::cerr
1061                                 << "Stepwise controller: Missing a missing-unacceptable sensor from D-Bus "
1062                                 << sensorName << "\n";
1063                             std::string shortName =
1064                                 sensorNameToDbusName(sensorName);
1065 
1066                             inputs.push_back(shortName);
1067                             auto& config = sensorConfig[shortName];
1068                             config.type = "temp";
1069                             config.readPath =
1070                                 getSensorPath(config.type, shortName);
1071                             config.ignoreDbusMinMax = true;
1072                             config.unavailableAsFailed = unavailableAsFailed;
1073                             // todo: maybe un-hardcode this if we run into
1074                             // slower timeouts with sensors
1075 
1076                             config.timeout = 0;
1077                             sensorFound = true;
1078                         }
1079                         else
1080                         {
1081                             // When an input sensor is NOT on DBus, and it's in
1082                             // the MissingIsAcceptable list. Ignore it and
1083                             // continue with the next input sensor.
1084                             std::cout
1085                                 << "Stepwise controller: Missing a missing-acceptable sensor from D-Bus "
1086                                 << sensorName << "\n";
1087                             continue;
1088                         }
1089 #endif
1090                     }
1091                     else
1092                     {
1093                         for (const auto& sensorPathIfacePair :
1094                              sensorPathIfacePairs)
1095                         {
1096                             std::string shortName = getSensorNameFromPath(
1097                                 sensorPathIfacePair.first);
1098 
1099                             inputs.push_back(shortName);
1100                             auto& config = sensorConfig[shortName];
1101                             config.readPath = sensorPathIfacePair.first;
1102                             config.type = "temp";
1103                             config.ignoreDbusMinMax = true;
1104                             config.unavailableAsFailed = unavailableAsFailed;
1105                             // todo: maybe un-hardcode this if we run into
1106                             // slower timeouts with sensors
1107 
1108                             config.timeout = 0;
1109                             sensorFound = true;
1110                         }
1111                     }
1112                 }
1113                 if (!sensorFound)
1114                 {
1115                     continue;
1116                 }
1117 
1118                 // MissingIsAcceptable same postprocessing as Inputs
1119                 for (const std::string& missingAcceptableSensorName :
1120                      missingAcceptableSensorNames)
1121                 {
1122                     std::vector<std::pair<std::string, std::string>>
1123                         sensorPathIfacePairs;
1124                     if (!findSensors(
1125                             sensors,
1126                             sensorNameToDbusName(missingAcceptableSensorName),
1127                             sensorPathIfacePairs))
1128                     {
1129 #ifndef HANDLE_MISSING_OBJECT_PATHS
1130                         break;
1131 #else
1132                         // When a sensor in the MissingIsAcceptable list is NOT
1133                         // on DBus and it still reaches here, which contradicts
1134                         // to what we did in the Input sensor building step.
1135                         // Continue.
1136                         continue;
1137 #endif
1138                     }
1139 
1140                     for (const auto& sensorPathIfacePair : sensorPathIfacePairs)
1141                     {
1142                         std::string shortName =
1143                             getSensorNameFromPath(sensorPathIfacePair.first);
1144 
1145                         missingAcceptableSensors.push_back(shortName);
1146                     }
1147                 }
1148 
1149                 conf::ControllerInfo& info = conf[pidName];
1150 
1151                 std::vector<double> inputTempToMargin;
1152 
1153                 auto findTempToMargin = base.find("TempToMargin");
1154                 if (findTempToMargin != base.end())
1155                 {
1156                     inputTempToMargin =
1157                         std::get<std::vector<double>>(findTempToMargin->second);
1158                 }
1159 
1160                 info.inputs = spliceInputs(inputs, inputTempToMargin,
1161                                            missingAcceptableSensors);
1162 
1163                 info.type = "stepwise";
1164                 info.stepwiseInfo.ts = 1.0; // currently unused
1165                 info.stepwiseInfo.positiveHysteresis = 0.0;
1166                 info.stepwiseInfo.negativeHysteresis = 0.0;
1167                 std::string subtype = std::get<std::string>(base.at("Class"));
1168 
1169                 info.stepwiseInfo.isCeiling = (subtype == "Ceiling");
1170                 auto findPosHyst = base.find("PositiveHysteresis");
1171                 auto findNegHyst = base.find("NegativeHysteresis");
1172                 if (findPosHyst != base.end())
1173                 {
1174                     info.stepwiseInfo.positiveHysteresis = std::visit(
1175                         VariantToDoubleVisitor(), findPosHyst->second);
1176                 }
1177                 if (findNegHyst != base.end())
1178                 {
1179                     info.stepwiseInfo.negativeHysteresis = std::visit(
1180                         VariantToDoubleVisitor(), findNegHyst->second);
1181                 }
1182                 std::vector<double> readings =
1183                     std::get<std::vector<double>>(base.at("Reading"));
1184                 if (readings.size() > ec::maxStepwisePoints)
1185                 {
1186                     throw std::invalid_argument("Too many stepwise points.");
1187                 }
1188                 if (readings.empty())
1189                 {
1190                     throw std::invalid_argument(
1191                         "Must have one stepwise point.");
1192                 }
1193                 std::copy(readings.begin(), readings.end(),
1194                           info.stepwiseInfo.reading);
1195                 if (readings.size() < ec::maxStepwisePoints)
1196                 {
1197                     info.stepwiseInfo.reading[readings.size()] =
1198                         std::numeric_limits<double>::quiet_NaN();
1199                 }
1200                 std::vector<double> outputs =
1201                     std::get<std::vector<double>>(base.at("Output"));
1202                 if (readings.size() != outputs.size())
1203                 {
1204                     throw std::invalid_argument(
1205                         "Outputs size must match readings");
1206                 }
1207                 std::copy(outputs.begin(), outputs.end(),
1208                           info.stepwiseInfo.output);
1209                 if (outputs.size() < ec::maxStepwisePoints)
1210                 {
1211                     info.stepwiseInfo.output[outputs.size()] =
1212                         std::numeric_limits<double>::quiet_NaN();
1213                 }
1214             }
1215         }
1216     }
1217     if constexpr (pid_control::conf::DEBUG)
1218     {
1219         debugPrint(sensorConfig, zoneConfig, zoneDetailsConfig);
1220     }
1221     if (zoneConfig.empty() || zoneDetailsConfig.empty())
1222     {
1223         std::cerr
1224             << "No fan zones, application pausing until new configuration\n";
1225         return false;
1226     }
1227     return true;
1228 }
1229 
1230 } // namespace dbus_configuration
1231 } // namespace pid_control
1232