PSUSensor: Individual customization of sensors

This is a large patch, but it adds a great new feature.

In the "Exposes" stanza of your Entity Manager JSON file,
you can now customize the properties of each individual sensor.

Each sensor, identified by one of the Labels strings,
now accepts 4 optional parameters. They are x_Name, x_Scale,
x_Min, x_Max, where x is the Labels string.

This lets you customize the PSUProperty dynamically at runtime.

If x_Name is given, it will take precedence, overriding
the prefix/suffix synthesis usually performed by PSUSensor.
If any of these parameters are not given,
the hardcoded PSUProperty table will still be used as defaults.

Example usage:
{
  "Address": "0x34",
  "Bus": 12,
  "Name": "MyDevice",
  "Labels": ["vin", "vout1", "iin", "iout1"],
  "vin_Name": "MyDevice_Vin",
  "vin_Scale": 500,
  "vin_Min": 10.0,
  "vin_Max": 90.0,
  "vout1_Name": "MyDevice_Vout",
  "vout1_Scale": 1000,
  "vout1_Min": 5.0,
  "vout1_Max": 45.0,
  ...
}

Signed-off-by: Josh Lehan <krellan@google.com>
Change-Id: I741b61d8192a3bff364461247f31ca20edda2198
diff --git a/src/PSUSensorMain.cpp b/src/PSUSensorMain.cpp
index e3007cd..b5bbf81 100644
--- a/src/PSUSensorMain.cpp
+++ b/src/PSUSensorMain.cpp
@@ -23,6 +23,7 @@
 #include <boost/algorithm/string/replace.hpp>
 #include <boost/container/flat_map.hpp>
 #include <boost/container/flat_set.hpp>
+#include <cmath>
 #include <filesystem>
 #include <fstream>
 #include <functional>
@@ -63,6 +64,8 @@
 static boost::container::flat_map<std::string, std::vector<std::string>>
     limitEventMatch;
 
+static std::vector<PSUProperty> psuProperties;
+
 // Function CheckEvent will check each attribute from eventMatch table in the
 // sysfs. If the attributes exists in sysfs, then store the complete path
 // of the attribute into eventPathList.
@@ -399,6 +402,12 @@
                 labelHead = label.substr(0, label.find(" "));
             }
 
+            if constexpr (DEBUG)
+            {
+                std::cerr << "Sensor type=\"" << sensorNameSubStr
+                          << "\" label=\"" << labelHead << "\"\n";
+            }
+
             checkPWMSensor(sensorPath, labelHead, *interfacePath, objectServer,
                            psuNames[0]);
 
@@ -414,68 +423,187 @@
                 }
             }
 
-            /* Find out sensor name index for this label */
-            std::regex rgx("[A-Za-z]+([0-9]+)");
-            int nameIndex{0};
-            if (std::regex_search(labelHead, matches, rgx))
-            {
-                nameIndex = std::stoi(matches[1]);
-
-                // Decrement to preserve alignment, because hwmon
-                // human-readable filenames and labels use 1-based numbering,
-                // but the "Name", "Name1", "Name2", etc. naming
-                // convention (the psuNames vector) uses 0-based numbering.
-                if (nameIndex > 0)
-                {
-                    --nameIndex;
-                }
-            }
-            else
-            {
-                nameIndex = 0;
-            }
-
-            if (psuNames.size() <= nameIndex)
-            {
-                std::cerr << "Could not pair " << labelHead
-                          << " with a Name field\n";
-                continue;
-            }
-
             auto findProperty = labelMatch.find(labelHead);
             if (findProperty == labelMatch.end())
             {
-                std::cerr << "Could not find " << labelHead << "\n";
+                if constexpr (DEBUG)
+                {
+                    std::cerr << "Could not find matching default property for "
+                              << labelHead << "\n";
+                }
                 continue;
             }
 
-            if constexpr (DEBUG)
+            // Protect the hardcoded labelMatch list from changes,
+            // by making a copy and modifying that instead.
+            // Avoid bleedthrough of one device's customizations to
+            // the next device, as each should be independently customizable.
+            psuProperties.push_back(findProperty->second);
+            auto psuProperty = psuProperties.rbegin();
+
+            // Use label head as prefix for reading from config file,
+            // example if temp1: temp1_Name, temp1_Scale, temp1_Min, ...
+            std::string keyName = labelHead + "_Name";
+            std::string keyScale = labelHead + "_Scale";
+            std::string keyMin = labelHead + "_Min";
+            std::string keyMax = labelHead + "_Max";
+
+            bool customizedName = false;
+            auto findCustomName = baseConfig->second.find(keyName);
+            if (findCustomName != baseConfig->second.end())
             {
-                std::cerr << "Sensor label head " << labelHead
-                          << " paired with " << psuNames[nameIndex] << "\n";
+                try
+                {
+                    psuProperty->labelTypeName = std::visit(
+                        VariantToStringVisitor(), findCustomName->second);
+                }
+                catch (std::invalid_argument&)
+                {
+                    std::cerr << "Unable to parse " << keyName << "\n";
+                    continue;
+                }
+
+                // All strings are valid, including empty string
+                customizedName = true;
+            }
+
+            bool customizedScale = false;
+            auto findCustomScale = baseConfig->second.find(keyScale);
+            if (findCustomScale != baseConfig->second.end())
+            {
+                try
+                {
+                    psuProperty->sensorScaleFactor = std::visit(
+                        VariantToUnsignedIntVisitor(), findCustomScale->second);
+                }
+                catch (std::invalid_argument&)
+                {
+                    std::cerr << "Unable to parse " << keyScale << "\n";
+                    continue;
+                }
+
+                // Avoid later division by zero
+                if (psuProperty->sensorScaleFactor > 0)
+                {
+                    customizedScale = true;
+                }
+                else
+                {
+                    std::cerr << "Unable to accept " << keyScale << "\n";
+                    continue;
+                }
+            }
+
+            auto findCustomMin = baseConfig->second.find(keyMin);
+            if (findCustomMin != baseConfig->second.end())
+            {
+                try
+                {
+                    psuProperty->minReading = std::visit(
+                        VariantToDoubleVisitor(), findCustomMin->second);
+                }
+                catch (std::invalid_argument&)
+                {
+                    std::cerr << "Unable to parse " << keyMin << "\n";
+                    continue;
+                }
+            }
+
+            auto findCustomMax = baseConfig->second.find(keyMax);
+            if (findCustomMax != baseConfig->second.end())
+            {
+                try
+                {
+                    psuProperty->maxReading = std::visit(
+                        VariantToDoubleVisitor(), findCustomMax->second);
+                }
+                catch (std::invalid_argument&)
+                {
+                    std::cerr << "Unable to parse " << keyMax << "\n";
+                    continue;
+                }
+            }
+
+            if (!(psuProperty->minReading < psuProperty->maxReading))
+            {
+                std::cerr << "Min must be less than Max\n";
+                continue;
+            }
+
+            // If the sensor name is being customized by config file,
+            // then prefix/suffix composition becomes not necessary,
+            // and in fact not wanted, because it gets in the way.
+            std::string psuNameFromIndex;
+            if (!customizedName)
+            {
+                /* Find out sensor name index for this label */
+                std::regex rgx("[A-Za-z]+([0-9]+)");
+                int nameIndex{0};
+                if (std::regex_search(labelHead, matches, rgx))
+                {
+                    nameIndex = std::stoi(matches[1]);
+
+                    // Decrement to preserve alignment, because hwmon
+                    // human-readable filenames and labels use 1-based
+                    // numbering, but the "Name", "Name1", "Name2", etc. naming
+                    // convention (the psuNames vector) uses 0-based numbering.
+                    if (nameIndex > 0)
+                    {
+                        --nameIndex;
+                    }
+                }
+                else
+                {
+                    nameIndex = 0;
+                }
+
+                if (psuNames.size() <= nameIndex)
+                {
+                    std::cerr << "Could not pair " << labelHead
+                              << " with a Name field\n";
+                    continue;
+                }
+
+                psuNameFromIndex = psuNames[nameIndex];
+
+                if constexpr (DEBUG)
+                {
+                    std::cerr << "Sensor label head " << labelHead
+                              << " paired with " << psuNameFromIndex
+                              << " at index " << nameIndex << "\n";
+                }
             }
 
             checkEventLimits(sensorPathStr, limitEventMatch, eventPathList);
 
-            unsigned int factor =
-                std::pow(10, findProperty->second.sensorScaleFactor);
-
-            /* Change first char of substring to uppercase */
-            char firstChar = sensorNameSubStr[0] - 0x20;
-            std::string strScaleFactor =
-                firstChar + sensorNameSubStr.substr(1) + "ScaleFactor";
-
-            auto findScaleFactor = baseConfig->second.find(strScaleFactor);
-            if (findScaleFactor != baseConfig->second.end())
+            // Similarly, if sensor scaling factor is being customized,
+            // then the below power-of-10 constraint becomes unnecessary,
+            // as config should be able to specify an arbitrary divisor.
+            unsigned int factor = psuProperty->sensorScaleFactor;
+            if (!customizedScale)
             {
-                factor =
-                    std::visit(VariantToIntVisitor(), findScaleFactor->second);
-            }
+                // Preserve existing usage of hardcoded labelMatch table below
+                factor = std::pow(10.0, factor);
 
-            if constexpr (DEBUG)
-            {
-                std::cerr << "Sensor scaling factor " << factor << " string "
-                          << strScaleFactor << "\n";
+                /* Change first char of substring to uppercase */
+                char firstChar = sensorNameSubStr[0] - 0x20;
+                std::string strScaleFactor =
+                    firstChar + sensorNameSubStr.substr(1) + "ScaleFactor";
+
+                // Preserve existing configs by accepting earlier syntax,
+                // example CurrScaleFactor, PowerScaleFactor, ...
+                auto findScaleFactor = baseConfig->second.find(strScaleFactor);
+                if (findScaleFactor != baseConfig->second.end())
+                {
+                    factor = std::visit(VariantToIntVisitor(),
+                                        findScaleFactor->second);
+                }
+
+                if constexpr (DEBUG)
+                {
+                    std::cerr << "Sensor scaling factor " << factor
+                              << " string " << strScaleFactor << "\n";
+                }
             }
 
             std::vector<thresholds::Threshold> sensorThresholds;
@@ -494,21 +622,52 @@
                 continue;
             }
 
-            std::string sensorName =
-                psuNames[nameIndex] + " " + findProperty->second.labelTypeName;
-
-            ++numCreated;
             if constexpr (DEBUG)
             {
-                std::cerr << "Created " << numCreated
-                          << " sensors so far: " << sensorName << "\n";
+                std::cerr << "Sensor properties: Name \""
+                          << psuProperty->labelTypeName << "\" Scale "
+                          << psuProperty->sensorScaleFactor << " Min "
+                          << psuProperty->minReading << " Max "
+                          << psuProperty->maxReading << "\n";
+            }
+
+            std::string sensorName = psuProperty->labelTypeName;
+            if (customizedName)
+            {
+                if (sensorName.empty())
+                {
+                    // Allow selective disabling of an individual sensor,
+                    // by customizing its name to an empty string.
+                    std::cerr << "Sensor disabled, empty string\n";
+                    continue;
+                }
+            }
+            else
+            {
+                // Sensor name not customized, do prefix/suffix composition,
+                // preserving default behavior by using psuNameFromIndex.
+                sensorName =
+                    psuNameFromIndex + " " + psuProperty->labelTypeName;
+            }
+
+            if constexpr (DEBUG)
+            {
+                std::cerr << "Sensor name \"" << sensorName << "\" path \""
+                          << sensorPathStr << "\" type \"" << sensorType
+                          << "\"\n";
             }
 
             sensors[sensorName] = std::make_unique<PSUSensor>(
                 sensorPathStr, sensorType, objectServer, dbusConnection, io,
                 sensorName, std::move(sensorThresholds), *interfacePath,
-                findSensorType->second, factor, findProperty->second.maxReading,
-                findProperty->second.minReading);
+                findSensorType->second, factor, psuProperty->maxReading,
+                psuProperty->minReading);
+
+            ++numCreated;
+            if constexpr (DEBUG)
+            {
+                std::cerr << "Created " << numCreated << " sensors so far\n";
+            }
         }
 
         // OperationalStatus event