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