diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 53e76bd9..dc4fcd00 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -701,5 +701,13 @@
         "errored": "Errors",
         "noRecent": "Out of Date",
         "totalUsed": "Used Storage"
+    },
+    "openmediavault": {
+        "downloading": "Downloading",
+        "total": "Total",
+        "running": "Running",
+        "stopped": "Stopped",
+        "passed": "Passed",
+        "failed": "Failed"
     }
 }
diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js
index 83b4b07b..4488277a 100644
--- a/src/utils/config/service-helpers.js
+++ b/src/utils/config/service-helpers.js
@@ -298,6 +298,7 @@ export function cleanServiceGroups(groups) {
           metric, // glances
           stream, // mjpeg
           fit,
+          method, // openmediavault widget
         } = cleanedService.widget;
 
         let fieldsList = fields;
@@ -368,6 +369,9 @@ export function cleanServiceGroups(groups) {
           if (stream) cleanedService.widget.stream = stream;
           if (fit) cleanedService.widget.fit = fit;
         }
+        if (type === "openmediavault") {
+          if (method) cleanedService.widget.method = method;
+        }
       }
 
       return cleanedService;
diff --git a/src/widgets/components.js b/src/widgets/components.js
index 0db4878c..d6785490 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -59,6 +59,7 @@ const components = {
   ombi: dynamic(() => import("./ombi/component")),
   opnsense: dynamic(() => import("./opnsense/component")),
   overseerr: dynamic(() => import("./overseerr/component")),
+  openmediavault: dynamic(() => import("./openmediavault/component")),
   paperlessngx: dynamic(() => import("./paperlessngx/component")),
   pfsense: dynamic(() => import("./pfsense/component")),
   photoprism: dynamic(() => import("./photoprism/component")),
diff --git a/src/widgets/openmediavault/component.jsx b/src/widgets/openmediavault/component.jsx
new file mode 100644
index 00000000..bd34a750
--- /dev/null
+++ b/src/widgets/openmediavault/component.jsx
@@ -0,0 +1,16 @@
+import ServicesGetStatus from "./methods/services_get_status";
+import SmartGetList from "./methods/smart_get_list";
+import DownloaderGetDownloadList from "./methods/downloader_get_downloadlist";
+
+export default function Component({ service }) {
+  switch (service.widget.method) {
+    case "services.getStatus":
+      return <ServicesGetStatus service={service} />;
+    case "smart.getListBg":
+      return <SmartGetList service={service} />;
+    case "downloader.getDownloadList":
+      return <DownloaderGetDownloadList service={service} />;
+    default:
+      return null;
+  }
+}
diff --git a/src/widgets/openmediavault/methods/downloader_get_downloadlist.jsx b/src/widgets/openmediavault/methods/downloader_get_downloadlist.jsx
new file mode 100644
index 00000000..ed776db0
--- /dev/null
+++ b/src/widgets/openmediavault/methods/downloader_get_downloadlist.jsx
@@ -0,0 +1,36 @@
+import useWidgetAPI from "utils/proxy/use-widget-api";
+import Container from "components/services/widget/container";
+import Block from "components/services/widget/block";
+
+const downloadReduce = (acc, e) => {
+  if (e.downloading) {
+    return acc + 1;
+  }
+  return acc;
+};
+
+const items = [
+  { label: "openmediavault.downloading", getNumber: (data) => (!data ? null : data.reduce(downloadReduce, 0)) },
+  { label: "openmediavault.total", getNumber: (data) => (!data ? null : data?.length) },
+];
+
+export default function Component({ service }) {
+  const { data, error } = useWidgetAPI(service.widget);
+
+  if (error) {
+    return <Container service={service} error={error} />;
+  }
+
+  const itemsWithData = items.map((item) => ({
+    ...item,
+    number: item.getNumber(data?.response?.data),
+  }));
+
+  return (
+    <Container service={service}>
+      {itemsWithData.map((e) => (
+        <Block key={e.label} label={e.label} value={e.number} />
+      ))}
+    </Container>
+  );
+}
diff --git a/src/widgets/openmediavault/methods/services_get_status.jsx b/src/widgets/openmediavault/methods/services_get_status.jsx
new file mode 100644
index 00000000..3ec66a45
--- /dev/null
+++ b/src/widgets/openmediavault/methods/services_get_status.jsx
@@ -0,0 +1,43 @@
+import useWidgetAPI from "utils/proxy/use-widget-api";
+import Container from "components/services/widget/container";
+import Block from "components/services/widget/block";
+
+const isRunningReduce = (acc, e) => {
+  if (e.running) {
+    return acc + 1;
+  }
+  return acc;
+};
+const notRunningReduce = (acc, e) => {
+  if (!e.running) {
+    return acc + 1;
+  }
+  return acc;
+};
+
+const items = [
+  { label: "openmediavault.running", getNumber: (data) => (!data ? null : data.reduce(isRunningReduce, 0)) },
+  { label: "openmediavault.stopped", getNumber: (data) => (!data ? null : data.reduce(notRunningReduce, 0)) },
+  { label: "openmediavault.total", getNumber: (data) => (!data ? null : data?.length) },
+];
+
+export default function Component({ service }) {
+  const { data, error } = useWidgetAPI(service.widget);
+
+  if (error) {
+    return <Container service={service} error={error} />;
+  }
+
+  const itemsWithData = items.map((item) => ({
+    ...item,
+    number: item.getNumber(data?.response?.data),
+  }));
+
+  return (
+    <Container service={service}>
+      {itemsWithData.map((e) => (
+        <Block key={e.label} label={e.label} value={e.number} />
+      ))}
+    </Container>
+  );
+}
diff --git a/src/widgets/openmediavault/methods/smart_get_list.jsx b/src/widgets/openmediavault/methods/smart_get_list.jsx
new file mode 100644
index 00000000..55a76db6
--- /dev/null
+++ b/src/widgets/openmediavault/methods/smart_get_list.jsx
@@ -0,0 +1,42 @@
+import useWidgetAPI from "utils/proxy/use-widget-api";
+import Container from "components/services/widget/container";
+import Block from "components/services/widget/block";
+
+const passedReduce = (acc, e) => {
+  if (e.overallstatus === "GOOD") {
+    return acc + 1;
+  }
+  return acc;
+};
+const failedReduce = (acc, e) => {
+  if (e.overallstatus !== "GOOD") {
+    return acc + 1;
+  }
+  return acc;
+};
+
+const items = [
+  { label: "openmediavault.passed", getNumber: (data) => (!data ? null : data.reduce(passedReduce, 0)) },
+  { label: "openmediavault.failed", getNumber: (data) => (!data ? null : data.reduce(failedReduce, 0)) },
+];
+
+export default function Component({ service }) {
+  const { data, error } = useWidgetAPI(service.widget);
+
+  if (error) {
+    return <Container service={service} error={error} />;
+  }
+
+  const itemsWithData = items.map((item) => ({
+    ...item,
+    number: item.getNumber(JSON.parse(data?.response?.output || "{}")?.data),
+  }));
+
+  return (
+    <Container service={service}>
+      {itemsWithData.map((e) => (
+        <Block key={e.label} label={e.label} value={e.number} />
+      ))}
+    </Container>
+  );
+}
diff --git a/src/widgets/openmediavault/proxy.js b/src/widgets/openmediavault/proxy.js
new file mode 100644
index 00000000..a9099d24
--- /dev/null
+++ b/src/widgets/openmediavault/proxy.js
@@ -0,0 +1,151 @@
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
+import getServiceWidget from "utils/config/service-helpers";
+import { addCookieToJar, setCookieHeader } from "utils/proxy/cookie-jar";
+import createLogger from "utils/logger";
+import widgets from "widgets/widgets";
+
+const PROXY_NAME = "OMVProxyHandler";
+const BG_MAX_RETRIES = 50;
+const BG_POLL_PERIOD = 500;
+
+const logger = createLogger(PROXY_NAME);
+
+async function getWidget(req) {
+  const { group, service } = req.query;
+
+  if (!group || !service) {
+    logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
+    return null;
+  }
+
+  const widget = await getServiceWidget(group, service);
+
+  if (!widget) {
+    logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
+    return null;
+  }
+
+  return widget;
+}
+
+async function rpc(url, request) {
+  const params = {
+    method: "POST",
+    headers: { "Content-Type": "application/json" },
+    body: JSON.stringify(request),
+  };
+  setCookieHeader(url, params);
+  const [status, contentType, data, headers] = await httpProxy(url, params);
+
+  return { status, contentType, data, headers };
+}
+
+async function poll(attemptsLeft, makeReqByPos, pos = 0) {
+  if (attemptsLeft <= 0) {
+    return null;
+  }
+
+  const resp = await makeReqByPos(pos);
+
+  const data = JSON.parse(resp.data.toString()).response;
+  if (data.running === true || data.outputPending) {
+    await new Promise((resolve) => {
+      setTimeout(resolve, BG_POLL_PERIOD);
+    });
+    return poll(attemptsLeft - 1, makeReqByPos, data.pos);
+  }
+  return resp;
+}
+
+async function tryLogin(widget) {
+  const url = new URL(formatApiCall(widgets?.[widget.type]?.api, { ...widget }));
+  const { username, password } = widget;
+  const resp = await rpc(url, {
+    method: "login",
+    service: "session",
+    params: { username, password },
+  });
+
+  if (resp.status !== 200) {
+    logger.error("HTTP %d logging in to OpenMediaVault. Data: %s", resp.status, resp.data);
+    return [false, resp];
+  }
+
+  const json = JSON.parse(resp.data.toString());
+  if (json.response.authenticated !== true) {
+    logger.error("Login error in OpenMediaVault. Data: %s", resp.data);
+    resp.status = 401;
+    return [false, resp];
+  }
+
+  return [true, resp];
+}
+async function processBg(url, filename) {
+  const resp = await poll(BG_MAX_RETRIES, (pos) =>
+    rpc(url, {
+      service: "exec",
+      method: "getOutput",
+      params: { pos, filename },
+    })
+  );
+
+  if (resp == null) {
+    const errText = "The maximum number of attempts to receive a response from Bg data has been exceeded.";
+    logger.error(errText);
+    return errText;
+  }
+  if (resp.status !== 200) {
+    logger.error("HTTP %d getting Bg data from OpenMediaVault RPC. Data: %s", resp.status, resp.data);
+  }
+  return resp;
+}
+
+export default async function proxyHandler(req, res) {
+  const widget = await getWidget(req);
+  if (!widget) {
+    return res.status(400).json({ error: "Invalid proxy service type" });
+  }
+
+  const api = widgets?.[widget.type]?.api;
+  if (!api) {
+    return res.status(403).json({ error: "Service does not support RPC calls" });
+  }
+
+  const url = new URL(formatApiCall(api, { ...widget }));
+  const [service, method] = widget.method.split(".");
+  const rpcReq = { params: { limit: -1, start: 0 }, service, method };
+
+  let resp = await rpc(url, rpcReq);
+
+  if (resp.status === 401) {
+    logger.debug("Session not authenticated.");
+    const [success, lResp] = await tryLogin(widget);
+
+    if (success) {
+      addCookieToJar(url, lResp.headers);
+    } else {
+      res.status(lResp.status).json({ error: { message: `HTTP Error ${lResp.status}`, url, data: lResp.data } });
+    }
+
+    logger.debug("Retrying OpenMediaVault request after login.");
+    resp = await rpc(url, rpcReq);
+  }
+
+  if (resp.status !== 200) {
+    logger.error("HTTP %d getting data from OpenMediaVault RPC. Data: %s", resp.status, resp.data);
+    return res.status(resp.status).json({ error: { message: `HTTP Error ${resp.status}`, url, data: resp.data } });
+  }
+
+  if (method.endsWith("Bg")) {
+    const json = JSON.parse(resp.data.toString());
+    const bgResp = await processBg(url, json.response);
+
+    if (typeof bgResp === "string") {
+      return res.status(400).json({ error: bgResp });
+    }
+    return res.status(bgResp.status).send(bgResp.data);
+  }
+
+  return res.status(resp.status).send(resp.data);
+}
diff --git a/src/widgets/openmediavault/widget.js b/src/widgets/openmediavault/widget.js
new file mode 100644
index 00000000..3678ebe8
--- /dev/null
+++ b/src/widgets/openmediavault/widget.js
@@ -0,0 +1,8 @@
+import proxyHandler from "./proxy";
+
+const widget = {
+  api: "{url}/rpc.php",
+  proxyHandler,
+};
+
+export default widget;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index d28f3b38..aaf555ef 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -53,6 +53,7 @@ import omada from "./omada/widget";
 import ombi from "./ombi/widget";
 import opnsense from "./opnsense/widget";
 import overseerr from "./overseerr/widget";
+import openmediavault from "./openmediavault/widget";
 import paperlessngx from "./paperlessngx/widget";
 import pfsense from "./pfsense/widget";
 import photoprism from "./photoprism/widget";
@@ -148,6 +149,7 @@ const widgets = {
   ombi,
   opnsense,
   overseerr,
+  openmediavault,
   paperlessngx,
   pfsense,
   photoprism,