Implement Liquid Leak Detector

Implement liquid leak detector based on design
https://gerrit.openbmc.org/c/openbmc/docs/+/73152.

Related PRs:
https://gerrit.openbmc.org/c/openbmc/phosphor-dbus-interfaces/+/73151
https://gerrit.openbmc.org/c/openbmc/phosphor-dbus-interfaces/+/73707
https://gerrit.openbmc.org/c/openbmc/sdbusplus/+/75461
https://gerrit.openbmc.org/c/openbmc/phosphor-dbus-interfaces/+/75999

Tested:
```
>> busctl tree xyz.openbmc_project.LeakDetector
└─ /xyz
  └─ /xyz/openbmc_project
    └─ /xyz/openbmc_project/state
      └─ /xyz/openbmc_project/state/leak
        └─ /xyz/openbmc_project/state/leak/detector
          ├─ /xyz/openbmc_project/state/leak/detector/TrayDetector1
          └─ /xyz/openbmc_project/state/leak/detector/TrayDetector2

>> echo pull-up > /sys/devices/platform/gpio-sim.0/gpiochip2/sim_gpio0/pull

>> curl -k -H "X-Auth-Token: $token" -X GET https://${bmc}/redfish/v1/Systems/system/LogServices/EventLog/Entries
{
  "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries",
  "@odata.type": "#LogEntryCollection.LogEntryCollection",
  "Description": "Collection of System Event Log Entries",
  "Members": [
  	...
    {
      "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/2",
      "@odata.type": "#LogEntry.v1_9_0.LogEntry",
      "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/2/attachment",
      "Created": "2024-05-10T06:40:30.423+00:00",
      "EntryType": "Event",
      "Id": "2",
      "Message": "xyz.openbmc_project.State.Leak.Detector.LeakDetectedWarning",
      "Modified": "2024-05-10T06:40:30.423+00:00",
      "Name": "System Event Log Entry",
      "Resolved": false,
      "Severity": "Warning"
    }
  ],
  "Members@odata.count": 2,
  "Name": "System Event Log Entries"
}

>> echo pull-down > /sys/devices/platform/gpio-sim.0/gpiochip2/sim_gpio0/pull

>> curl -k -H "X-Auth-Token: $token" -X GET https://${bmc}/redfish/v1/Systems/system/LogServices/EventLog/Entries
{
  "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries",
  "@odata.type": "#LogEntryCollection.LogEntryCollection",
  "Description": "Collection of System Event Log Entries",
  "Members": [
	...
    {
      "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/2",
      "@odata.type": "#LogEntry.v1_9_0.LogEntry",
      "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/2/attachment",
      "Created": "2024-05-10T06:40:30.423+00:00",
      "EntryType": "Event",
      "Id": "2",
      "Message": "xyz.openbmc_project.State.Leak.Detector.LeakDetectedWarning",
      "Modified": "2024-05-10T06:42:23.989+00:00",
      "Name": "System Event Log Entry",
      "Resolved": true,
      "Severity": "Warning"
    },
    {
      "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/3",
      "@odata.type": "#LogEntry.v1_9_0.LogEntry",
      "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/3/attachment",
      "Created": "2024-05-10T06:42:24.024+00:00",
      "EntryType": "Event",
      "Id": "3",
      "Message": "xyz.openbmc_project.State.Leak.Detector.LeakDetectedNormal",
      "Modified": "2024-05-10T06:42:24.024+00:00",
      "Name": "System Event Log Entry",
      "Resolved": false,
      "Severity": "OK"
    }
  ],
  "Members@odata.count": 3,
  "Name": "System Event Log Entries"
}
```

Change-Id: Id6981deb314ca6852c5e31b932b28e601c2f3976
Signed-off-by: Jagpal Singh Gill <paligill@gmail.com>
diff --git a/meson.options b/meson.options
index de733c9..de8fed4 100644
--- a/meson.options
+++ b/meson.options
@@ -83,3 +83,9 @@
     value: 'disabled',
     description: 'Enables Sensor override feature without any check.',
 )
+option(
+    'leakdetector',
+    type: 'feature',
+    value: 'enabled',
+    description: 'Enable Liquid Leak Detector.',
+)
diff --git a/service_files/meson.build b/service_files/meson.build
index c3b23e0..294fc5b 100644
--- a/service_files/meson.build
+++ b/service_files/meson.build
@@ -17,6 +17,7 @@
     ['nvme', 'xyz.openbmc_project.nvmesensor.service'],
     ['psu', 'xyz.openbmc_project.psusensor.service'],
     ['external', 'xyz.openbmc_project.externalsensor.service'],
+    ['leakdetector', 'xyz.openbmc_project.leakdetector.service'],
 ]
 
 fs = import('fs')
diff --git a/service_files/xyz.openbmc_project.leakdetector.service b/service_files/xyz.openbmc_project.leakdetector.service
new file mode 100644
index 0000000..a478504
--- /dev/null
+++ b/service_files/xyz.openbmc_project.leakdetector.service
@@ -0,0 +1,13 @@
+
+[Unit]
+Description=Leak Detector for Liquid Cooling
+Requires=xyz.openbmc_project.EntityManager.service
+After=xyz.openbmc_project.EntityManager.service
+
+[Service]
+Restart=always
+RestartSec=5
+ExecStart=/usr/libexec/dbus-sensors/leakdetector
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/EntityManagerInterface.cpp b/src/EntityManagerInterface.cpp
new file mode 100644
index 0000000..581cd1b
--- /dev/null
+++ b/src/EntityManagerInterface.cpp
@@ -0,0 +1,124 @@
+#include "EntityManagerInterface.hpp"
+
+#include "Utils.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/message/native_types.hpp>
+#include <xyz/openbmc_project/Inventory/Item/client.hpp>
+
+#include <algorithm>
+#include <utility>
+
+namespace entity_manager
+{
+
+PHOSPHOR_LOG2_USING;
+
+namespace rules_intf = sdbusplus::bus::match::rules;
+
+EntityManagerInterface::EntityManagerInterface(
+    sdbusplus::async::context& ctx, const interface_list_t& interfaceNames,
+    Callback_t addedCallback, Callback_t removedCallback) :
+    ctx(ctx), interfaceNames(interfaceNames),
+    addedCallback(std::move(addedCallback)),
+    removedCallback(std::move(removedCallback))
+{
+    ctx.spawn(handleInventoryAdded());
+    ctx.spawn(handleInventoryRemoved());
+}
+
+auto EntityManagerInterface::handleInventoryGet() -> sdbusplus::async::task<>
+{
+    if (!addedCallback)
+    {
+        error("addedCallback is not set");
+        co_return;
+    }
+
+    using InventoryIntf =
+        sdbusplus::client::xyz::openbmc_project::inventory::Item<>;
+
+    constexpr auto entityManager =
+        sdbusplus::async::proxy()
+            .service(serviceName)
+            .path(InventoryIntf::namespace_path)
+            .interface("org.freedesktop.DBus.ObjectManager");
+
+    for (const auto& [objectPath, detectorConfig] :
+         co_await entityManager.call<ManagedObjectType>(ctx,
+                                                        "GetManagedObjects"))
+    {
+        for (const auto& interfaceName : interfaceNames)
+        {
+            if (detectorConfig.contains(interfaceName))
+            {
+                addedCallback(objectPath, interfaceName);
+            }
+        }
+    }
+
+    co_return;
+}
+
+auto EntityManagerInterface::handleInventoryAdded() -> sdbusplus::async::task<>
+{
+    if (!addedCallback)
+    {
+        error("addedCallback is not set");
+        co_return;
+    }
+
+    auto addedMatch = sdbusplus::async::match(
+        ctx, rules_intf::interfacesAdded() + rules_intf::sender(serviceName));
+
+    while (!ctx.stop_requested())
+    {
+        auto [objectPath, inventoryData] =
+            co_await addedMatch
+                .next<sdbusplus::message::object_path, SensorData>();
+
+        for (const auto& interfaceName : interfaceNames)
+        {
+            if (inventoryData.contains(interfaceName))
+            {
+                addedCallback(objectPath, interfaceName);
+            }
+        }
+    }
+
+    co_return;
+}
+
+auto EntityManagerInterface::handleInventoryRemoved()
+    -> sdbusplus::async::task<>
+{
+    if (!removedCallback)
+    {
+        error("removedCallback is not set");
+        co_return;
+    }
+
+    auto removedMatch = sdbusplus::async::match(
+        ctx, rules_intf::interfacesRemoved() + rules_intf::sender(serviceName));
+
+    while (!ctx.stop_requested())
+    {
+        auto [objectPath, interfaces] =
+            co_await removedMatch
+                .next<sdbusplus::message::object_path, interface_list_t>();
+
+        for (const auto& interfaceName : interfaceNames)
+        {
+            if (std::ranges::find(interfaces, interfaceName) !=
+                interfaces.end())
+            {
+                removedCallback(objectPath, interfaceName);
+            }
+        }
+    }
+
+    co_return;
+}
+
+} // namespace entity_manager
diff --git a/src/EntityManagerInterface.hpp b/src/EntityManagerInterface.hpp
new file mode 100644
index 0000000..7c73998
--- /dev/null
+++ b/src/EntityManagerInterface.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/message/native_types.hpp>
+
+#include <functional>
+#include <string>
+#include <vector>
+
+namespace entity_manager
+{
+
+class EntityManagerInterface
+{
+  public:
+    using Callback_t = std::function<void(
+        const sdbusplus::message::object_path&, const std::string&)>;
+    using interface_list_t = std::vector<std::string>;
+    static constexpr auto serviceName = "xyz.openbmc_project.EntityManager";
+
+    EntityManagerInterface() = delete;
+
+    EntityManagerInterface(sdbusplus::async::context& ctx,
+                           const interface_list_t& interfaceNames,
+                           Callback_t addedCallback,
+                           Callback_t removedCallback);
+
+    /** Get the inventory info from Entity Manager */
+    auto handleInventoryGet() -> sdbusplus::async::task<>;
+
+  private:
+    /** @brief Handle async inventory add from Entity Manager */
+    auto handleInventoryAdded() -> sdbusplus::async::task<>;
+
+    /** @brief Handle async inventory remove from Entity Manager */
+    auto handleInventoryRemoved() -> sdbusplus::async::task<>;
+
+    sdbusplus::async::context& ctx;
+    interface_list_t interfaceNames;
+    Callback_t addedCallback;
+    Callback_t removedCallback;
+};
+
+} // namespace entity_manager
diff --git a/src/GPIOInterface.cpp b/src/GPIOInterface.cpp
new file mode 100644
index 0000000..0bd0f4c
--- /dev/null
+++ b/src/GPIOInterface.cpp
@@ -0,0 +1,94 @@
+#include "GPIOInterface.hpp"
+
+#include <gpiod.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+
+#include <exception>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <utility>
+
+namespace gpio
+{
+
+PHOSPHOR_LOG2_USING;
+
+GPIOInterface::GPIOInterface(sdbusplus::async::context& ctx,
+                             const std::string& consumerName,
+                             const std::string& pinName, bool activeLow,
+                             Callback_t updateStateCallback) :
+    ctx(ctx), pinName(pinName),
+    updateStateCallback(std::move(updateStateCallback))
+{
+    if (!this->updateStateCallback)
+    {
+        throw std::runtime_error("updateStateCallback is not set");
+    }
+    line = gpiod::find_line(pinName);
+    if (!line)
+    {
+        throw std::runtime_error("Failed to find GPIO line for " + pinName);
+    }
+    try
+    {
+        line.request({consumerName, gpiod::line_request::EVENT_BOTH_EDGES,
+                      activeLow ? gpiod::line_request::FLAG_ACTIVE_LOW : 0});
+    }
+    catch (std::exception& e)
+    {
+        throw std::runtime_error("Failed to request line for " + pinName +
+                                 " with error " + e.what());
+    }
+
+    int lineFd = line.event_get_fd();
+    if (lineFd < 0)
+    {
+        throw std::runtime_error(
+            "Failed to get event fd for GPIO line " + pinName);
+    }
+
+    fdioInstance = std::make_unique<sdbusplus::async::fdio>(ctx, lineFd);
+}
+
+auto GPIOInterface::start() -> sdbusplus::async::task<>
+{
+    // Start the async read for the GPIO line
+    ctx.spawn(readGPIOAsyncEvent());
+
+    // Read the initial GPIO value
+    co_await readGPIOAsync();
+}
+
+auto GPIOInterface::readGPIOAsync() -> sdbusplus::async::task<>
+{
+    auto lineValue = line.get_value();
+    if (lineValue < 0)
+    {
+        error("Failed to read GPIO line {LINENAME}", "LINENAME", pinName);
+        co_return;
+    }
+    co_await updateStateCallback(lineValue == gpiod::line_event::RISING_EDGE);
+
+    co_return;
+}
+
+auto GPIOInterface::readGPIOAsyncEvent() -> sdbusplus::async::task<>
+{
+    while (!ctx.stop_requested())
+    {
+        // Wait for the fd event for the line to change
+        co_await fdioInstance->next();
+
+        line.event_read();
+        auto lineValue = line.get_value();
+
+        co_await updateStateCallback(
+            lineValue == gpiod::line_event::RISING_EDGE);
+    }
+
+    co_return;
+}
+
+} // namespace gpio
diff --git a/src/GPIOInterface.hpp b/src/GPIOInterface.hpp
new file mode 100644
index 0000000..b860551
--- /dev/null
+++ b/src/GPIOInterface.hpp
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <gpiod.hpp>
+#include <sdbusplus/async.hpp>
+
+#include <functional>
+#include <memory>
+#include <string>
+
+namespace gpio
+{
+
+class GPIOInterface
+{
+  public:
+    using Callback_t = std::function<sdbusplus::async::task<>(bool)>;
+
+    GPIOInterface() = delete;
+
+    GPIOInterface(sdbusplus::async::context& ctx,
+                  const std::string& consumerName, const std::string& pinName,
+                  bool activeLow, Callback_t updateStateCallback);
+
+    /** @brief Start the GPIO Interface */
+    auto start() -> sdbusplus::async::task<>;
+
+  private:
+    /** @brief Read the gpio state asynchronously */
+    auto readGPIOAsync() -> sdbusplus::async::task<>;
+
+    /** @brief Read the gpio state asynchronously based on gpio event */
+    auto readGPIOAsyncEvent() -> sdbusplus::async::task<>;
+
+    sdbusplus::async::context& ctx;
+    const std::string& pinName;
+    Callback_t updateStateCallback;
+    gpiod::line line;
+    /** File descriptor based async event handler */
+    std::unique_ptr<sdbusplus::async::fdio> fdioInstance;
+};
+
+} // namespace gpio
diff --git a/src/SystemdInterface.cpp b/src/SystemdInterface.cpp
new file mode 100644
index 0000000..fd41506
--- /dev/null
+++ b/src/SystemdInterface.cpp
@@ -0,0 +1,52 @@
+#include "SystemdInterface.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/message/native_types.hpp>
+
+#include <exception>
+#include <string>
+#include <variant>
+
+PHOSPHOR_LOG2_USING;
+
+namespace systemd
+{
+
+auto SystemdInterface::startUnit(sdbusplus::async::context& ctx,
+                                 std::string sysdUnit)
+    -> sdbusplus::async::task<>
+{
+    if (sysdUnit.empty())
+    {
+        error("sysdUnit is empty");
+        co_return;
+    }
+
+    try
+    {
+        constexpr auto systemd =
+            sdbusplus::async::proxy()
+                .service("org.freedesktop.systemd1")
+                .path("/org/freedesktop/systemd1")
+                .interface("org.freedesktop.systemd1.Manager");
+
+        std::variant<sdbusplus::message::object_path> jobObjectPath =
+            co_await systemd
+                .call<std::variant<sdbusplus::message::object_path>>(
+                    ctx, "StartUnit", sysdUnit, "replace");
+
+        debug("Started {UNIT} with {JOBID}", "UNIT", sysdUnit, "JOBID",
+              std::get<sdbusplus::message::object_path>(jobObjectPath));
+    }
+    catch (const std::exception& e)
+    {
+        warning("Failed to start {UNIT}: {ERROR}", "UNIT", sysdUnit, "ERROR",
+                e);
+        co_return;
+    }
+
+    co_return;
+}
+
+} // namespace systemd
diff --git a/src/SystemdInterface.hpp b/src/SystemdInterface.hpp
new file mode 100644
index 0000000..027d4cb
--- /dev/null
+++ b/src/SystemdInterface.hpp
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <sdbusplus/async.hpp>
+
+#include <string>
+
+namespace systemd
+{
+
+class SystemdInterface
+{
+  public:
+    /** @brief Start a systemd unit.
+     * Returns true on success, otherwise false.
+     */
+    static auto startUnit(sdbusplus::async::context& ctx, std::string sysdUnit)
+        -> sdbusplus::async::task<>;
+};
+
+} // namespace systemd
diff --git a/src/leakdetector/LeakDetectionManager.cpp b/src/leakdetector/LeakDetectionManager.cpp
new file mode 100644
index 0000000..5348f5f
--- /dev/null
+++ b/src/leakdetector/LeakDetectionManager.cpp
@@ -0,0 +1,158 @@
+#include "LeakDetectionManager.hpp"
+
+#include "LeakGPIODetector.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/message/native_types.hpp>
+#include <sdbusplus/server/manager.hpp>
+
+#include <exception>
+#include <functional>
+#include <memory>
+#include <optional>
+#include <string>
+
+PHOSPHOR_LOG2_USING;
+
+namespace leak
+{
+
+DetectionManager::DetectionManager(sdbusplus::async::context& ctx) :
+    ctx(ctx), leakEvents(ctx),
+    entityManager(
+        ctx, {GPIODetectorConfigIntf::interface},
+        std::bind_front(&DetectionManager::processInventoryAdded, this),
+        std::bind_front(&DetectionManager::processInventoryRemoved, this))
+{
+    ctx.spawn(entityManager.handleInventoryGet());
+}
+
+auto DetectionManager::processInventoryAdded(
+    const sdbusplus::message::object_path& objectPath,
+    const std::string& /*unused*/) -> void
+{
+    ctx.spawn(processConfigAddedAsync(objectPath));
+}
+
+auto DetectionManager::processInventoryRemoved(
+    const sdbusplus::message::object_path& objectPath,
+    const std::string& /*unused*/) -> void
+{
+    if (!detectors.contains(objectPath.str))
+    {
+        return;
+    }
+    debug("Removed detector {DETECTOR}", "DETECTOR", objectPath);
+    detectors.erase(objectPath.str);
+}
+
+auto DetectionManager::processConfigAddedAsync(
+    sdbusplus::message::object_path objectPath) -> sdbusplus::async::task<>
+{
+    auto res = co_await getDetectorConfig(objectPath);
+    if (!res)
+    {
+        co_return;
+    }
+    auto config = res.value();
+
+    if (detectors.contains(objectPath.str))
+    {
+        warning("Detector {DETECTOR} already exist", "DETECTOR", config.name);
+        co_return;
+    }
+
+    try
+    {
+        detectors[objectPath.str] =
+            std::make_unique<GPIODetector>(ctx, leakEvents, config);
+    }
+    catch (std::exception& e)
+    {
+        error("Failed to create detector {DETECTOR}: {ERROR}", "DETECTOR",
+              config.name, "ERROR", e.what());
+    }
+
+    co_return;
+}
+
+auto DetectionManager::getDetectorConfig(
+    sdbusplus::message::object_path objectPath)
+    -> sdbusplus::async::task<std::optional<config::DetectorConfig>>
+{
+    config::DetectorConfig config = {};
+
+    auto properties =
+        co_await GPIODetectorConfigIntf(ctx)
+            .service(entity_manager::EntityManagerInterface::serviceName)
+            .path(objectPath.str)
+            .properties();
+
+    config.name = properties.name;
+
+    for (const auto& [key, value] : config::validDetectorTypes)
+    {
+        if (properties.type == key)
+        {
+            config.type = value;
+            break;
+        }
+    }
+
+    config.pinName = properties.pin_name;
+
+    for (const auto& [key, value] : config::validPinPolarity)
+    {
+        if (properties.polarity == key)
+        {
+            config.polarity = value;
+            break;
+        }
+    }
+    if (config.polarity == config::PinPolarity::unknown)
+    {
+        error("Invalid polarity {POLARITY} for {NAME}", "POLARITY",
+              properties.polarity, "NAME", config.name);
+        co_return std::nullopt;
+    }
+
+    for (const auto& [key, value] : config::validDetectorLevel)
+    {
+        if (properties.level == key)
+        {
+            config.level = value;
+            break;
+        }
+    }
+    if (config.level == config::DetectorLevel::unknown)
+    {
+        error("Invalid level {LEVEL} for {NAME}", "LEVEL", properties.level,
+              "NAME", config.name);
+        co_return std::nullopt;
+    }
+
+    debug("Detector config: {NAME} {PIN_NAME} {POLARITY} {LEVEL}", "NAME",
+          config.name, "PIN_NAME", config.pinName, "POLARITY", config.polarity,
+          "LEVEL", config.level);
+
+    co_return config;
+}
+
+} // namespace leak
+
+int main()
+{
+    constexpr auto path = leak::DetectorIntf::namespace_path::value;
+    constexpr auto serviceName = "xyz.openbmc_project.leakdetector";
+    sdbusplus::async::context ctx;
+    sdbusplus::server::manager_t manager{ctx, path};
+
+    info("Creating leak detection manager at {PATH}", "PATH", path);
+    leak::DetectionManager leakDetectionManager{ctx};
+
+    ctx.request_name(serviceName);
+
+    ctx.run();
+    return 0;
+}
diff --git a/src/leakdetector/LeakDetectionManager.hpp b/src/leakdetector/LeakDetectionManager.hpp
new file mode 100644
index 0000000..34111b9
--- /dev/null
+++ b/src/leakdetector/LeakDetectionManager.hpp
@@ -0,0 +1,57 @@
+#include "EntityManagerInterface.hpp"
+#include "LeakEvents.hpp"
+#include "LeakGPIODetector.hpp"
+
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/message/native_types.hpp>
+#include <xyz/openbmc_project/Configuration/GPIOLeakDetector/client.hpp>
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <unordered_map>
+
+namespace leak
+{
+
+class DetectionManager;
+
+using GPIODetectorConfigIntf =
+    sdbusplus::client::xyz::openbmc_project::configuration::GPIOLeakDetector<>;
+
+class DetectionManager
+{
+  public:
+    DetectionManager() = delete;
+
+    explicit DetectionManager(sdbusplus::async::context& ctx);
+
+  private:
+    using detector_map_t =
+        std::unordered_map<std::string, std::unique_ptr<GPIODetector>>;
+
+    /** @brief  Process new interfaces added to inventory */
+    auto processInventoryAdded(
+        const sdbusplus::message::object_path& objectPath,
+        const std::string& interfaceName) -> void;
+
+    /** @brief Process interfaces removed from inventory */
+    auto processInventoryRemoved(
+        const sdbusplus::message::object_path& objectPath,
+        const std::string& interfaceName) -> void;
+
+    /** @brief Process the config add asynchronously */
+    auto processConfigAddedAsync(sdbusplus::message::object_path objectPath)
+        -> sdbusplus::async::task<>;
+
+    /** @brief Get the detector configuration from the Entity Manager */
+    auto getDetectorConfig(sdbusplus::message::object_path objectPath)
+        -> sdbusplus::async::task<std::optional<config::DetectorConfig>>;
+
+    sdbusplus::async::context& ctx;
+    Events leakEvents;
+    entity_manager::EntityManagerInterface entityManager;
+    detector_map_t detectors;
+};
+
+} // namespace leak
diff --git a/src/leakdetector/LeakEvents.cpp b/src/leakdetector/LeakEvents.cpp
new file mode 100644
index 0000000..e3755a7
--- /dev/null
+++ b/src/leakdetector/LeakEvents.cpp
@@ -0,0 +1,64 @@
+#include "LeakEvents.hpp"
+
+#include "LeakGPIODetector.hpp"
+
+#include <phosphor-logging/commit.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/message/native_types.hpp>
+#include <xyz/openbmc_project/State/Leak/Detector/event.hpp>
+
+#include <tuple>
+
+PHOSPHOR_LOG2_USING;
+
+namespace leak
+{
+
+auto Events::generateLeakEvent(sdbusplus::message::object_path detectorPath,
+                               DetectorStateIntf::DetectorState state,
+                               config::DetectorLevel level)
+    -> sdbusplus::async::task<>
+{
+    auto eventName = std::make_tuple(detectorPath.str, level);
+
+    // NOLINTNEXTLINE(clang-analyzer-core.uninitialized.Branch)
+    if (state == DetectorStateIntf::DetectorState::Normal)
+    {
+        auto pendingEvent = pendingEvents.find(eventName);
+        if (pendingEvent != pendingEvents.end())
+        {
+            co_await lg2::resolve(ctx, pendingEvent->second);
+
+            using DetectorNormal = sdbusplus::event::xyz::openbmc_project::
+                state::leak::Detector::LeakDetectedNormal;
+            co_await lg2::commit(ctx,
+                                 DetectorNormal("DETECTOR_NAME", detectorPath));
+
+            pendingEvents.erase(eventName);
+        }
+        co_return;
+    }
+
+    namespace error_intf =
+        sdbusplus::error::xyz::openbmc_project::state::leak::Detector;
+    sdbusplus::message::object_path eventPath{};
+
+    if (level == config::DetectorLevel::critical)
+    {
+        eventPath = co_await lg2::commit(
+            ctx,
+            error_intf::LeakDetectedCritical("DETECTOR_NAME", detectorPath));
+        error("Critical leak detected for {PATH}", "PATH", detectorPath);
+    }
+    else
+    {
+        eventPath = co_await lg2::commit(
+            ctx,
+            error_intf::LeakDetectedWarning("DETECTOR_NAME", detectorPath));
+        warning("Warning leak detected for {PATH}", "PATH", detectorPath);
+    }
+    pendingEvents[eventName] = eventPath;
+}
+
+} // namespace leak
diff --git a/src/leakdetector/LeakEvents.hpp b/src/leakdetector/LeakEvents.hpp
new file mode 100644
index 0000000..ef63205
--- /dev/null
+++ b/src/leakdetector/LeakEvents.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/message/native_types.hpp>
+#include <xyz/openbmc_project/State/Leak/Detector/client.hpp>
+
+#include <map>
+#include <string>
+#include <tuple>
+
+namespace leak
+{
+
+namespace config
+{
+enum class DetectorLevel;
+}
+
+using DetectorStateIntf =
+    sdbusplus::client::xyz::openbmc_project::state::leak::Detector<>;
+
+class Events
+{
+  public:
+    Events() = delete;
+
+    explicit Events(sdbusplus::async::context& ctx) : ctx(ctx) {}
+
+    auto generateLeakEvent(sdbusplus::message::object_path detectorPath,
+                           DetectorStateIntf::DetectorState state,
+                           config::DetectorLevel level)
+        -> sdbusplus::async::task<>;
+
+  private:
+    /** @brief Map type for event name to log event object path */
+    using event_key_t = std::tuple<std::string, config::DetectorLevel>;
+    using event_map_t = std::map<event_key_t, sdbusplus::message::object_path>;
+
+    sdbusplus::async::context& ctx;
+    event_map_t pendingEvents;
+};
+
+} // namespace leak
diff --git a/src/leakdetector/LeakGPIODetector.cpp b/src/leakdetector/LeakGPIODetector.cpp
new file mode 100644
index 0000000..f21d1b5
--- /dev/null
+++ b/src/leakdetector/LeakGPIODetector.cpp
@@ -0,0 +1,89 @@
+#include "LeakGPIODetector.hpp"
+
+#include "LeakEvents.hpp"
+#include "SystemdInterface.hpp"
+
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/message/native_types.hpp>
+
+#include <array>
+#include <functional>
+#include <string>
+#include <string_view>
+#include <utility>
+
+namespace leak
+{
+
+namespace config
+{
+
+/** @brief Leak level to systemd target service map */
+static constexpr std::array<std::pair<config::DetectorLevel, std::string_view>,
+                            2>
+    leakActionTargets = {{{config::DetectorLevel::warning,
+                           "xyz.openbmc_project.leakdetector.warning@"},
+                          {config::DetectorLevel::critical,
+                           "xyz.openbmc_project.leakdetector.critical@"}}};
+
+} // namespace config
+
+static auto getObjectPath(const std::string& detectorName)
+    -> sdbusplus::message::object_path
+{
+    return (
+        sdbusplus::message::object_path(DetectorIntf::namespace_path::value) /
+        DetectorIntf::namespace_path::detector / detectorName);
+}
+
+GPIODetector::GPIODetector(sdbusplus::async::context& ctx, Events& leakEvents,
+                           const config::DetectorConfig& config) :
+    DetectorIntf(ctx, getObjectPath(config.name).str.c_str()), ctx(ctx),
+    leakEvents(leakEvents), config(config),
+    gpioInterface(ctx, config.name, config.pinName,
+                  (config.polarity == config::PinPolarity::activeLow),
+                  std::bind_front(&GPIODetector::updateGPIOStateAsync, this))
+{
+    pretty_name<false>(config.name);
+    type<false>(config.type);
+
+    ctx.spawn(gpioInterface.start());
+
+    debug("Created leak detector {NAME}", "NAME", config.name);
+}
+
+auto GPIODetector::updateGPIOStateAsync(bool gpioState)
+    -> sdbusplus::async::task<>
+{
+    auto newState = gpioState ? DetectorIntf::DetectorState::Abnormal
+                              : DetectorIntf::DetectorState::Normal;
+
+    debug("Updating detector {DETECTOR} state to {STATE}", "DETECTOR",
+          config.name, "STATE", newState);
+
+    if (newState != state_)
+    {
+        state(newState);
+
+        co_await leakEvents.generateLeakEvent(getObjectPath(config.name),
+                                              state_, config.level);
+        if (state_ != DetectorIntf::DetectorState::Normal)
+        {
+            for (const auto& [level, serviceSuffix] : config::leakActionTargets)
+            {
+                if (config.level == level)
+                {
+                    auto target = std::string(serviceSuffix) + config.name +
+                                  ".service";
+                    debug("Starting systemd target {TARGET}", "TARGET", target);
+                    co_await systemd::SystemdInterface::startUnit(ctx, target);
+                    break;
+                }
+            }
+        }
+    }
+
+    co_return;
+}
+
+} // namespace leak
diff --git a/src/leakdetector/LeakGPIODetector.hpp b/src/leakdetector/LeakGPIODetector.hpp
new file mode 100644
index 0000000..074e758
--- /dev/null
+++ b/src/leakdetector/LeakGPIODetector.hpp
@@ -0,0 +1,102 @@
+#pragma once
+
+#include "GPIOInterface.hpp"
+#include "LeakEvents.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/async/server.hpp>
+#include <xyz/openbmc_project/Association/Definitions/aserver.hpp>
+#include <xyz/openbmc_project/Configuration/GPIOLeakDetector/client.hpp>
+#include <xyz/openbmc_project/State/Leak/Detector/aserver.hpp>
+
+#include <array>
+#include <string>
+#include <string_view>
+#include <utility>
+
+PHOSPHOR_LOG2_USING;
+
+namespace leak
+{
+
+class GPIODetector;
+
+using DetectorConfigIntf =
+    sdbusplus::client::xyz::openbmc_project::configuration::GPIOLeakDetector<>;
+
+using DetectorIntf = sdbusplus::async::server_t<
+    GPIODetector,
+    sdbusplus::aserver::xyz::openbmc_project::association::Definitions,
+    sdbusplus::aserver::xyz::openbmc_project::state::leak::Detector>;
+
+namespace config
+{
+
+/** @brief Detector type to enum map */
+static constexpr std::array<
+    std::pair<std::string_view, DetectorIntf::DetectorType>, 2>
+    validDetectorTypes = {
+        {{"LeakSensingCable", DetectorIntf::DetectorType::LeakSensingCable},
+         {"Unknown", DetectorIntf::DetectorType::Unknown}}};
+
+/** @brief GPIO polarity */
+enum class PinPolarity
+{
+    activeLow,
+    activeHigh,
+    unknown
+};
+
+/** @brief Polarity name to enum map */
+static constexpr std::array<std::pair<std::string_view, PinPolarity>, 2>
+    validPinPolarity = {
+        {{"Low", PinPolarity::activeLow}, {"High", PinPolarity::activeHigh}}};
+
+/** @brief Detector level */
+enum class DetectorLevel
+{
+    critical,
+    warning,
+    unknown
+};
+
+/** @brief Leak detector level name to enum map */
+static constexpr std::array<std::pair<std::string_view, DetectorLevel>, 2>
+    validDetectorLevel = {{{"Warning", DetectorLevel::warning},
+                           {"Critical", DetectorLevel::critical}}};
+
+/** @brief Leak detector configuration */
+struct DetectorConfig
+{
+    std::string name = Defaults::name;
+    DetectorIntf::DetectorType type = DetectorIntf::DetectorType::Unknown;
+    std::string pinName = Defaults::pinName;
+    PinPolarity polarity = PinPolarity::unknown;
+    DetectorLevel level = DetectorLevel::unknown;
+
+    struct Defaults
+    {
+        static constexpr auto name = "unknown";
+        static constexpr auto pinName = "unknown";
+    };
+};
+
+}; // namespace config
+
+class GPIODetector : public DetectorIntf
+{
+  public:
+    explicit GPIODetector(sdbusplus::async::context& ctx, Events& leakEvents,
+                          const config::DetectorConfig& config);
+
+    auto updateGPIOStateAsync(bool gpioState) -> sdbusplus::async::task<>;
+
+  private:
+    sdbusplus::async::context& ctx;
+    Events& leakEvents;
+    config::DetectorConfig config;
+    gpio::GPIOInterface gpioInterface;
+};
+
+} // namespace leak
diff --git a/src/leakdetector/meson.build b/src/leakdetector/meson.build
new file mode 100644
index 0000000..aa953e4
--- /dev/null
+++ b/src/leakdetector/meson.build
@@ -0,0 +1,21 @@
+src_inc = include_directories('..')
+phosphor_dbus_interfaces_dep = dependency('phosphor-dbus-interfaces')
+
+executable(
+    'leakdetector',
+    'LeakDetectionManager.cpp',
+    'LeakGPIODetector.cpp',
+    'LeakEvents.cpp',
+    systemdinterface_src,
+    dependencies: [
+        default_deps,
+        utils_dep,
+        phosphor_dbus_interfaces_dep,
+        gpiodcxx,
+        gpiointerface_dep,
+        entitymanagerinterface_dep,
+    ],
+    include_directories: src_inc,
+    install: true,
+    install_dir: get_option('libexecdir') / 'dbus-sensors',
+)
diff --git a/src/meson.build b/src/meson.build
index ca995e5..5668bf4 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -61,6 +61,32 @@
     dependencies: [default_deps, thresholds_dep],
 )
 
+systemdinterface_src = files('SystemdInterface.cpp')
+
+gpiointerface_a = static_library(
+    'gpiointerface_a',
+    'GPIOInterface.cpp',
+    dependencies: default_deps,
+)
+
+gpiointerface_dep = declare_dependency(
+    include_directories: ['.'],
+    link_with: [gpiointerface_a],
+    dependencies: default_deps,
+)
+
+entitymanagerinterface_a = static_library(
+    'entitymanagerinterface_a',
+    'EntityManagerInterface.cpp',
+    dependencies: default_deps,
+)
+
+entitymanagerinterface_dep = declare_dependency(
+    include_directories: ['.'],
+    link_with: [entitymanagerinterface_a],
+    dependencies: default_deps,
+)
+
 peci_incdirs = []
 if not meson.get_compiler('cpp').has_header('linux/peci-ioctl.h')
     peci_incdirs = ['../include']
@@ -118,3 +144,7 @@
 if get_option('tests').allowed()
     subdir('tests')
 endif
+
+if get_option('leakdetector').allowed()
+    subdir('leakdetector')
+endif
diff --git a/subprojects/phosphor-dbus-interfaces.wrap b/subprojects/phosphor-dbus-interfaces.wrap
new file mode 100644
index 0000000..346aa0c
--- /dev/null
+++ b/subprojects/phosphor-dbus-interfaces.wrap
@@ -0,0 +1,6 @@
+[wrap-git]
+url = https://github.com/openbmc/phosphor-dbus-interfaces.git
+revision = HEAD
+
+[provide]
+phosphor-dbus-interfaces = phosphor_dbus_interfaces_dep