Handle PSU inventory changes

Watch the PSU inventory property changes, so that when a PSU is plugged
out/in, the software object is removed or created.
If mutliple PSUs have the same software version, the related
associations are updated.

Tested: On Witherspoon, manually setting PSU's present property to false
        and true, when both PSUs have the same version.
        0. Before setting PSU's present false, verify the software
        object is associated with two PSUs;
        1. When one of the PSU is set false, verify the assocation is
        removed and only one PSU path is associated;
        2. When both of the PSUs are set false, verify the software
        object is removed;
        3. When a PSU is set to true, verify the software object is
        created and associated with the PSU;
        4. When the other PSU is set to true, verify the software object
        is associated with both PSUs.

Signed-off-by: Lei YU <mine260309@gmail.com>
Change-Id: I417a87bf57c01aa57f78f09b7abc4e948a4d1752
diff --git a/src/item_updater.cpp b/src/item_updater.cpp
index a257a6b..1f1af9d 100644
--- a/src/item_updater.cpp
+++ b/src/item_updater.cpp
@@ -199,6 +199,8 @@
     auto versionId = utils::getVersionId(psuVersion);
     auto path = std::string(SOFTWARE_OBJPATH) + "/" + versionId;
 
+    psuStatusMap[psuInventoryPath] = {true, psuVersion};
+
     auto it = activations.find(versionId);
     if (it != activations.end())
     {
@@ -208,6 +210,7 @@
                                                   ACTIVATION_REV_ASSOCIATION,
                                                   psuInventoryPath));
         it->second->associations(associations);
+        psuPathActivationMap.emplace(psuInventoryPath, it->second);
     }
     else
     {
@@ -222,6 +225,7 @@
         auto activation = createActivationObject(path, versionId, "",
                                                  activationState, associations);
         activations.emplace(versionId, std::move(activation));
+        psuPathActivationMap.emplace(psuInventoryPath, activations[versionId]);
 
         auto versionPtr = createVersionObject(path, versionId, psuVersion,
                                               VersionPurpose::PSU, "");
@@ -232,6 +236,43 @@
     }
 }
 
+void ItemUpdater::removePsuObject(const std::string& psuInventoryPath)
+{
+    psuStatusMap[psuInventoryPath] = {false, ""};
+    auto it = psuPathActivationMap.find(psuInventoryPath);
+    if (it == psuPathActivationMap.end())
+    {
+        log<level::ERR>("No Activation found for PSU",
+                        entry("PSUPATH=%s", psuInventoryPath.c_str()));
+        return;
+    }
+    const auto& activationPtr = it->second;
+    psuPathActivationMap.erase(psuInventoryPath);
+
+    auto associations = activationPtr->associations();
+    for (auto iter = associations.begin(); iter != associations.end();)
+    {
+        if ((std::get<2>(*iter)).compare(psuInventoryPath) == 0)
+        {
+            iter = associations.erase(iter);
+        }
+        else
+        {
+            ++iter;
+        }
+    }
+    if (associations.empty())
+    {
+        // Remove the activation
+        erase(activationPtr->versionId);
+    }
+    else
+    {
+        // Update association
+        activationPtr->associations(associations);
+    }
+}
+
 std::unique_ptr<Version> ItemUpdater::createVersionObject(
     const std::string& objPath, const std::string& versionId,
     const std::string& versionString,
@@ -245,9 +286,67 @@
     return version;
 }
 
-void ItemUpdater::onPsuInventoryChanged(sdbusplus::message::message&)
+void ItemUpdater::onPsuInventoryChanged(sdbusplus::message::message& msg)
 {
-    // TODO
+    using Interface = std::string;
+    using Property = std::string;
+    using Properties =
+        std::map<Property, sdbusplus::message::variant<bool, std::string>>;
+
+    Interface interface;
+    Properties properties;
+    std::optional<bool> present;
+    std::optional<std::string> version;
+    std::string psuPath = msg.get_path();
+
+    msg.read(interface, properties);
+
+    // The code was expecting to get callback on mutliple properties changed.
+    // But in practice, the callback is received one-by-one for each property.
+    // So it has to handle Present and Version property separately.
+    auto p = properties.find("Present");
+    if (p != properties.end())
+    {
+        present = sdbusplus::message::variant_ns::get<bool>(p->second);
+        psuStatusMap[psuPath].present = *present;
+    }
+    p = properties.find("Version");
+    if (p != properties.end())
+    {
+        version = sdbusplus::message::variant_ns::get<std::string>(p->second);
+        psuStatusMap[psuPath].version = *version;
+    }
+
+    // If present or version is not changed, ignore
+    if (!present.has_value() && !version.has_value())
+    {
+        return;
+    }
+
+    if (psuStatusMap[psuPath].present)
+    {
+        // If version is not updated, let's wait for it
+        if (psuStatusMap[psuPath].version.empty())
+        {
+            log<level::DEBUG>("Waiting for version to be updated");
+            return;
+        }
+        // Create object or association based on the version and psu inventory
+        // path
+        createPsuObject(psuPath, psuStatusMap[psuPath].version);
+    }
+    else
+    {
+        if (!present.has_value())
+        {
+            // If a PSU is plugged out, version property is update to empty as
+            // well, and we get callback here, but ignore that because it is
+            // handled by "Present" callback.
+            return;
+        }
+        // Remove object or association
+        removePsuObject(psuPath);
+    }
 }
 
 void ItemUpdater::processPSUImage()
@@ -265,11 +364,14 @@
         {
             createPsuObject(p, version);
         }
-        // Add matches for PSU present changes
-        psuMatches.emplace_back(bus,
-                                MatchRules::propertiesChanged(p, ITEM_IFACE),
-                                std::bind(&ItemUpdater::onPsuInventoryChanged,
-                                          this, std::placeholders::_1));
+        // Add matches for PSU Inventory's property changes
+        psuMatches.emplace_back(
+            bus,
+            MatchRules::type::signal() + MatchRules::path(p) +
+                MatchRules::member("PropertiesChanged") +
+                MatchRules::interface("org.freedesktop.DBus.Properties"),
+            std::bind(&ItemUpdater::onPsuInventoryChanged, this,
+                      std::placeholders::_1));
     }
 }
 
diff --git a/src/item_updater.hpp b/src/item_updater.hpp
index 6df7624..7bd9f66 100644
--- a/src/item_updater.hpp
+++ b/src/item_updater.hpp
@@ -111,10 +111,21 @@
                                 Version::VersionPurpose versionPurpose,
                             const std::string& filePath);
 
-    /** @brief Create Activation and Version object for PSU inventory */
+    /** @brief Create Activation and Version object for PSU inventory
+     *  @details If the same version exists for multiple PSUs, just add
+     *           related association, instead of creating new objects.
+     * */
     void createPsuObject(const std::string& psuInventoryPath,
                          const std::string& psuVersion);
 
+    /** @brief Remove Activation and Version object for PSU inventory
+     *  @details If the same version exists for mutliple PSUs, just remove
+     *           related association.
+     *           If the version has no association, the Activation and
+     *           Version object will be removed
+     */
+    void removePsuObject(const std::string& psuInventoryPath);
+
     /**
      * @brief Create and populate the active PSU Version.
      */
@@ -131,6 +142,21 @@
      * version id */
     std::map<std::string, std::unique_ptr<Version>> versions;
 
+    /** @brief The reference map of PSU Inventory objects and the
+     * Activation*/
+    std::map<std::string, const std::unique_ptr<Activation>&>
+        psuPathActivationMap;
+
+    /** @brief A struct to hold the PSU present status and version */
+    struct psuStatus
+    {
+        bool present;
+        std::string version;
+    };
+
+    /** @brief The map of PSU inventory path and the psuStatus */
+    std::map<std::string, psuStatus> psuStatusMap;
+
     /** @brief sdbusplus signal match for PSU Software*/
     sdbusplus::bus::match_t versionMatch;