virtual-sensor: Add ability to read a config from dbus

If we match on the right interface we attempt to add a virtual sensor
from a configuration on dbus.

As we do not want to take arbitrary expressions from dbus, we only match
on a pre-determined set of calculations that can be an exprtk expression
or a function. One is added in a later commit.

Signed-off-by: Rashmica Gupta <rashmica.g@gmail.com>
Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I228f60fa0f484cd1e7be1aca3d097494c5168936
diff --git a/virtualSensor.cpp b/virtualSensor.cpp
index 53325ff..12adad2 100644
--- a/virtualSensor.cpp
+++ b/virtualSensor.cpp
@@ -13,6 +13,10 @@
 static constexpr bool DEBUG = false;
 static constexpr auto busName = "xyz.openbmc_project.VirtualSensor";
 static constexpr auto sensorDbusPath = "/xyz/openbmc_project/sensors/";
+static constexpr auto entityManagerBusName =
+    "xyz.openbmc_project.EntityManager";
+static constexpr auto vsThresholdsIfaceSuffix = ".Thresholds";
+static constexpr std::array<const char*, 0> calculationIfaces = {};
 
 using namespace phosphor::logging;
 
@@ -90,6 +94,135 @@
     return assocs;
 }
 
+template <typename U>
+struct VariantToNumber
+{
+    template <typename T>
+    U operator()(const T& t) const
+    {
+        if constexpr (std::is_convertible<T, U>::value)
+        {
+            return static_cast<U>(t);
+        }
+        throw std::invalid_argument("Invalid number type in config\n");
+    }
+};
+
+template <typename U>
+U getNumberFromConfig(const PropertyMap& map, const std::string& name,
+                      bool required)
+{
+    if (auto itr = map.find(name); itr != map.end())
+    {
+        return std::visit(VariantToNumber<U>(), itr->second);
+    }
+    else if (required)
+    {
+        log<level::ERR>("Required field missing in config",
+                        entry("NAME=%s", name.c_str()));
+        throw std::invalid_argument("Required field missing in config");
+    }
+    return std::numeric_limits<U>::quiet_NaN();
+}
+
+bool isCalculationType(const std::string& interface)
+{
+    auto itr = std::find(calculationIfaces.begin(), calculationIfaces.end(),
+                         interface);
+    if (itr != calculationIfaces.end())
+    {
+        return true;
+    }
+    return false;
+}
+
+const std::string getThresholdType(const std::string& direction,
+                                   uint64_t severity)
+{
+    std::string threshold;
+    std::string suffix;
+    static const std::array thresholdTypes{"Warning", "Critical",
+                                           "PerformanceLoss", "SoftShutdown",
+                                           "HardShutdown"};
+
+    if (severity >= thresholdTypes.size())
+    {
+        throw std::invalid_argument(
+            "Invalid threshold severity specified in entity manager");
+    }
+    threshold = thresholdTypes[severity];
+
+    if (direction == "less than")
+    {
+        suffix = "Low";
+    }
+    else if (direction == "greater than")
+    {
+        suffix = "High";
+    }
+    else
+    {
+        throw std::invalid_argument(
+            "Invalid threshold direction specified in entity manager");
+    }
+    return threshold + suffix;
+}
+
+void parseThresholds(Json& thresholds, const PropertyMap& propertyMap)
+{
+    std::string direction;
+
+    auto severity =
+        getNumberFromConfig<uint64_t>(propertyMap, "Severity", true);
+    auto value = getNumberFromConfig<double>(propertyMap, "Value", true);
+
+    auto itr = propertyMap.find("Direction");
+    if (itr != propertyMap.end())
+    {
+        direction = std::get<std::string>(itr->second);
+    }
+
+    auto threshold = getThresholdType(direction, severity);
+    thresholds[threshold] = value;
+}
+
+void VirtualSensor::parseConfigInterface(const PropertyMap& propertyMap,
+                                         const std::string& sensorType,
+                                         const std::string& interface)
+{
+    /* Parse sensors / DBus params */
+    if (auto itr = propertyMap.find("Sensors"); itr != propertyMap.end())
+    {
+        auto sensors = std::get<std::vector<std::string>>(itr->second);
+        for (auto sensor : sensors)
+        {
+            std::replace(sensor.begin(), sensor.end(), ' ', '_');
+            auto sensorObjPath = sensorDbusPath + sensorType + "/" + sensor;
+
+            auto paramPtr =
+                std::make_unique<SensorParam>(bus, sensorObjPath, this);
+            symbols.create_variable(sensor);
+            paramMap.emplace(std::move(sensor), std::move(paramPtr));
+        }
+    }
+    /* Get expression string */
+    if (!isCalculationType(interface))
+    {
+        throw std::invalid_argument("Invalid expression in interface");
+    }
+    exprStr = interface;
+
+    /* Get optional min and max input and output values */
+    ValueIface::maxValue(
+        getNumberFromConfig<double>(propertyMap, "MaxValue", false));
+    ValueIface::minValue(
+        getNumberFromConfig<double>(propertyMap, "MinValue", false));
+    maxValidInput =
+        getNumberFromConfig<double>(propertyMap, "MaxValidInput", false);
+    minValidInput =
+        getNumberFromConfig<double>(propertyMap, "MinValidInput", false);
+}
+
 void VirtualSensor::initVirtualSensor(const Json& sensorConfig,
                                       const std::string& objPath)
 {
@@ -211,12 +344,53 @@
         printParams(paramMap);
 }
 
+void VirtualSensor::initVirtualSensor(const InterfaceMap& interfaceMap,
+                                      const std::string& objPath,
+                                      const std::string& sensorType,
+                                      const std::string& calculationIface)
+{
+    Json thresholds;
+    const std::string vsThresholdsIntf =
+        calculationIface + vsThresholdsIfaceSuffix;
+
+    for (const auto& [interface, propertyMap] : interfaceMap)
+    {
+        /* Each threshold is on it's own interface with a number as a suffix
+         * eg xyz.openbmc_project.Configuration.ModifiedMedian.Thresholds1 */
+        if (interface.find(vsThresholdsIntf) != std::string::npos)
+        {
+            parseThresholds(thresholds, propertyMap);
+        }
+        else if (interface == calculationIface)
+        {
+            parseConfigInterface(propertyMap, sensorType, interface);
+        }
+    }
+
+    createThresholds(thresholds, objPath);
+    symbols.add_constants();
+    symbols.add_package(vecopsPackage);
+    expression.register_symbol_table(symbols);
+
+    /* Print all parameters for debug purpose only */
+    if (DEBUG)
+    {
+        printParams(paramMap);
+    }
+}
+
 void VirtualSensor::setSensorValue(double value)
 {
     value = std::clamp(value, ValueIface::minValue(), ValueIface::maxValue());
     ValueIface::value(value);
 }
 
+double VirtualSensor::calculateValue()
+{
+    // Placeholder until calculation types are added
+    return std::numeric_limits<double>::quiet_NaN();
+}
+
 void VirtualSensor::updateVirtualSensor()
 {
     for (auto& param : paramMap)
@@ -233,13 +407,18 @@
             throw std::invalid_argument("ParamName not found in symbols");
         }
     }
-    double val = expression.value();
+    auto itr =
+        std::find(calculationIfaces.begin(), calculationIfaces.end(), exprStr);
+    auto val = (itr == calculationIfaces.end()) ? expression.value()
+                                                : calculateValue();
 
     /* Set sensor value to dbus interface */
     setSensorValue(val);
 
     if (DEBUG)
+    {
         std::cout << "Sensor value is " << val << "\n";
+    }
 
     /* Check sensor thresholds and log required message */
     checkThresholds(val, perfLossIface);
@@ -317,6 +496,49 @@
     }
 }
 
+ManagedObjectType VirtualSensors::getObjectsFromDBus()
+{
+    ManagedObjectType objects;
+
+    try
+    {
+        auto method = bus.new_method_call(entityManagerBusName, "/",
+                                          "org.freedesktop.DBus.ObjectManager",
+                                          "GetManagedObjects");
+        auto reply = bus.call(method);
+        reply.read(objects);
+    }
+    catch (const sdbusplus::exception::SdBusError& ex)
+    {
+        // If entity manager isn't running yet, keep going.
+        if (std::string("org.freedesktop.DBus.Error.ServiceUnknown") !=
+            ex.name())
+        {
+            throw ex.name();
+        }
+    }
+
+    return objects;
+}
+
+void VirtualSensors::propertiesChanged(sdbusplus::message::message& msg)
+{
+    std::string path;
+    PropertyMap properties;
+
+    msg.read(path, properties);
+
+    /* We get multiple callbacks for one sensor. 'Type' is a required field and
+     * is a unique label so use to to only proceed once per sensor */
+    if (properties.contains("Type"))
+    {
+        if (isCalculationType(path))
+        {
+            createVirtualSensorsFromDBus(path);
+        }
+    }
+}
+
 /** @brief Parsing Virtual Sensor config JSON file  */
 Json VirtualSensors::parseConfigFile(const std::string configFile)
 {
@@ -325,7 +547,7 @@
     {
         log<level::ERR>("config JSON file not found",
                         entry("FILENAME=%s", configFile.c_str()));
-        throw std::exception{};
+        return {};
     }
 
     auto data = Json::parse(jsonFile, nullptr, false);
@@ -351,14 +573,168 @@
     {"airflow", ValueIface::Unit::CFM},
     {"pressure", ValueIface::Unit::Pascals}};
 
+const std::string getSensorTypeFromUnit(const std::string& unit)
+{
+    std::string unitPrefix = "xyz.openbmc_project.Sensor.Value.Unit.";
+    for (auto [type, unitObj] : unitMap)
+    {
+        auto unitPath = ValueIface::convertUnitToString(unitObj);
+        if (unitPath == (unitPrefix + unit))
+        {
+            return type;
+        }
+    }
+    return "";
+}
+
+void VirtualSensors::setupMatches()
+{
+    /* Already setup */
+    if (!this->matches.empty())
+    {
+        return;
+    }
+
+    /* Setup matches */
+    auto eventHandler = [this](sdbusplus::message::message& message) {
+        if (message.is_method_error())
+        {
+            log<level::ERR>("Callback method error");
+            return;
+        }
+        this->propertiesChanged(message);
+    };
+
+    for (const char* iface : calculationIfaces)
+    {
+        auto match = std::make_unique<sdbusplus::bus::match::match>(
+            bus,
+            sdbusplus::bus::match::rules::propertiesChangedNamespace(
+                "/xyz/openbmc_project/inventory", iface),
+            eventHandler);
+        this->matches.emplace_back(std::move(match));
+    }
+}
+
+void VirtualSensors::createVirtualSensorsFromDBus(
+    const std::string& calculationIface)
+{
+    if (calculationIface.empty())
+    {
+        log<level::ERR>("No calculation type supplied");
+        return;
+    }
+    auto objects = getObjectsFromDBus();
+
+    /* Get virtual sensors config data */
+    for (const auto& [path, interfaceMap] : objects)
+    {
+        auto objpath = static_cast<std::string>(path);
+        std::string name = path.filename();
+        std::string sensorType, sensorUnit;
+
+        /* Find Virtual Sensor interfaces */
+        if (!interfaceMap.contains(calculationIface))
+        {
+            continue;
+        }
+        if (name.empty())
+        {
+            log<level::ERR>(
+                "Virtual Sensor name not found in entity manager config");
+            continue;
+        }
+        if (virtualSensorsMap.contains(name))
+        {
+            log<level::ERR>("A virtual sensor with this name already exists",
+                            entry("NAME=%s", name.c_str()));
+            continue;
+        }
+
+        /* Extract the virtual sensor type as we need this to initialize the
+         * sensor */
+        for (const auto& [interface, propertyMap] : interfaceMap)
+        {
+            if (interface != calculationIface)
+            {
+                continue;
+            }
+            auto itr = propertyMap.find("Units");
+            if (itr != propertyMap.end())
+            {
+                sensorUnit = std::get<std::string>(itr->second);
+                break;
+            }
+        }
+        sensorType = getSensorTypeFromUnit(sensorUnit);
+        if (sensorType.empty())
+        {
+            log<level::ERR>("Sensor unit is not supported",
+                            entry("TYPE=%s", sensorUnit.c_str()));
+            continue;
+        }
+
+        try
+        {
+            auto virtObjPath = sensorDbusPath + sensorType + "/" + name;
+
+            auto virtualSensorPtr = std::make_unique<VirtualSensor>(
+                bus, virtObjPath.c_str(), interfaceMap, name, sensorType,
+                calculationIface);
+            log<level::INFO>("Added a new virtual sensor",
+                             entry("NAME=%s", name.c_str()));
+            virtualSensorPtr->updateVirtualSensor();
+
+            /* Initialize unit value for virtual sensor */
+            virtualSensorPtr->ValueIface::unit(unitMap[sensorType]);
+            virtualSensorPtr->emit_object_added();
+
+            virtualSensorsMap.emplace(name, std::move(virtualSensorPtr));
+
+            /* Setup match for interfaces removed */
+            auto intfRemoved = [this, objpath,
+                                name](sdbusplus::message::message& message) {
+                if (!virtualSensorsMap.contains(name))
+                {
+                    return;
+                }
+                sdbusplus::message::object_path path;
+                message.read(path);
+                if (static_cast<const std::string&>(path) == objpath)
+                {
+                    log<level::INFO>("Removed a virtual sensor",
+                                     entry("NAME=%s", name.c_str()));
+                    virtualSensorsMap.erase(name);
+                }
+            };
+            auto matchOnRemove = std::make_unique<sdbusplus::bus::match::match>(
+                bus,
+                sdbusplus::bus::match::rules::interfacesRemoved() +
+                    sdbusplus::bus::match::rules::argNpath(0, objpath),
+                intfRemoved);
+            /* TODO: slight race condition here. Check that the config still
+             * exists */
+            this->matches.emplace_back(std::move(matchOnRemove));
+        }
+        catch (std::invalid_argument& ia)
+        {
+            log<level::ERR>("Failed to set up virtual sensor",
+                            entry("Error=%s", ia.what()));
+        }
+    }
+}
+
 void VirtualSensors::createVirtualSensors()
 {
     static const Json empty{};
 
     auto data = parseConfigFile(VIRTUAL_SENSOR_CONFIG_FILE);
+
     // print values
     if (DEBUG)
+    {
         std::cout << "Config json data:\n" << data << "\n\n";
+    }
 
     /* Get virtual sensors  config data */
     for (const auto& j : data)
@@ -366,6 +742,29 @@
         auto desc = j.value("Desc", empty);
         if (!desc.empty())
         {
+            if (desc.value("Config", "") == "D-Bus")
+            {
+                /* Look on D-Bus for a virtual sensor config. Set up matches
+                 * first because the configs may not be on D-Bus yet and we
+                 * don't want to miss them */
+                setupMatches();
+
+                if (desc.contains("Type"))
+                {
+                    auto path = "xyz.openbmc_project.Configuration." +
+                                desc.value("Type", "");
+                    if (!isCalculationType(path))
+                    {
+                        log<level::ERR>(
+                            "Invalid calculation type supplied\n",
+                            entry("TYPE=%s", desc.value("Type", "").c_str()));
+                        continue;
+                    }
+                    createVirtualSensorsFromDBus(path);
+                }
+                continue;
+            }
+
             std::string sensorType = desc.value("SensorType", "");
             std::string name = desc.value("Name", "");
             std::replace(name.begin(), name.end(), ' ', '_');
diff --git a/virtualSensor.hpp b/virtualSensor.hpp
index c28e547..ada20d9 100644
--- a/virtualSensor.hpp
+++ b/virtualSensor.hpp
@@ -17,6 +17,17 @@
 namespace virtualSensor
 {
 
+using BasicVariantType =
+    std::variant<std::string, int64_t, uint64_t, double, int32_t, uint32_t,
+                 int16_t, uint16_t, uint8_t, bool, std::vector<std::string>>;
+
+using PropertyMap = std::map<std::string, BasicVariantType>;
+
+using InterfaceMap = std::map<std::string, PropertyMap>;
+
+using ManagedObjectType =
+    std::map<sdbusplus::message::object_path, InterfaceMap>;
+
 using Json = nlohmann::json;
 
 template <typename... T>
@@ -88,6 +99,25 @@
         initVirtualSensor(sensorConfig, objPath);
     }
 
+    /** @brief Constructs VirtualSensor
+     *
+     * @param[in] bus          - Handle to system dbus
+     * @param[in] objPath      - The Dbus path of sensor
+     * @param[in] ifacemap     - All the sensor information
+     * @param[in] name         - Virtual sensor name
+     * @param[in] type         - Virtual sensor type/unit
+     * @param[in] calcType     - Calculation used to calculate sensor value
+     *
+     */
+    VirtualSensor(sdbusplus::bus::bus& bus, const char* objPath,
+                  const InterfaceMap& ifacemap, const std::string& name,
+                  const std::string& type, const std::string& calculationType) :
+        ValueObject(bus, objPath, action::defer_emit),
+        bus(bus), name(name)
+    {
+        initVirtualSensor(ifacemap, objPath, type, calculationType);
+    }
+
     /** @brief Set sensor value */
     void setSensorValue(double value);
     /** @brief Update sensor at regular intrval */
@@ -111,6 +141,10 @@
     exprtk::expression<double> expression{};
     /** @brief The vecops package so the expression can use vectors */
     exprtk::rtl::vecops::package<double> vecopsPackage;
+    /** @brief The maximum valid value for an input sensor **/
+    double maxValidInput = std::numeric_limits<double>::infinity();
+    /** @brief The minimum valid value for an input sensor **/
+    double minValidInput = -std::numeric_limits<double>::infinity();
 
     /** @brief The critical threshold interface object */
     std::unique_ptr<Threshold<CriticalObject>> criticalIface;
@@ -132,8 +166,22 @@
     void initVirtualSensor(const Json& sensorConfig,
                            const std::string& objPath);
 
+    /** @brief Read config from interface map and initialize sensor data
+     * for each virtual sensor
+     */
+    void initVirtualSensor(const InterfaceMap& interfaceMap,
+                           const std::string& objPath,
+                           const std::string& sensorType,
+                           const std::string& calculationType);
+
+    /** @brief Returns which calculation function or expression to use */
+    double calculateValue();
     /** @brief create threshold objects from json config */
     void createThresholds(const Json& threshold, const std::string& objPath);
+    /** @brief parse config from entity manager **/
+    void parseConfigInterface(const PropertyMap& propertyMap,
+                              const std::string& sensorType,
+                              const std::string& interface);
 
     /** @brief Check Sensor threshold and update alarm and log */
     template <typename V, typename T>
@@ -201,19 +249,29 @@
     {
         createVirtualSensors();
     }
+    /** @brief Calls createVirtualSensor when interface added */
+    void propertiesChanged(sdbusplus::message::message& msg);
 
   private:
     /** @brief sdbusplus bus client connection. */
     sdbusplus::bus::bus& bus;
+    /** @brief Get virual sensor config from DBus**/
+    ManagedObjectType getObjectsFromDBus();
     /** @brief Parsing virtual sensor config JSON file  */
     Json parseConfigFile(const std::string configFile);
 
+    /** @brief Matches for virtual sensors */
+    std::vector<std::unique_ptr<sdbusplus::bus::match::match>> matches;
     /** @brief Map of the object VirtualSensor */
     std::unordered_map<std::string, std::unique_ptr<VirtualSensor>>
         virtualSensorsMap;
 
-    /** @brief Create list of virtual sensors */
+    /** @brief Create list of virtual sensors from JSON config*/
     void createVirtualSensors();
+    /** @brief Create list of virtual sensors from DBus config */
+    void createVirtualSensorsFromDBus(const std::string& calculationType);
+    /** @brief Setup matches for virtual sensors */
+    void setupMatches();
 };
 
 } // namespace virtualSensor