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