diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index d8bfb039..9f5637bf 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -88,6 +88,13 @@
"bitrate": "Bitrate",
"no_active": "No Active Streams"
},
+ "omada": {
+ "connectedAp": "Connected APs",
+ "activeUser": "Active devices",
+ "alerts": "Alerts",
+ "connectedGateway": "Connected gateways",
+ "connectedSwitches": "Connected switches"
+ },
"nzbget": {
"rate": "Rate",
"remaining": "Remaining",
diff --git a/src/widgets/components.js b/src/widgets/components.js
index eb7c686f..9f3011b3 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -26,6 +26,7 @@ const components = {
nextdns: dynamic(() => import("./nextdns/component")),
npm: dynamic(() => import("./npm/component")),
nzbget: dynamic(() => import("./nzbget/component")),
+ omada: dynamic(() => import("./omada/component")),
ombi: dynamic(() => import("./ombi/component")),
overseerr: dynamic(() => import("./overseerr/component")),
paperlessngx: dynamic(() => import("./paperlessngx/component")),
diff --git a/src/widgets/omada/component.jsx b/src/widgets/omada/component.jsx
new file mode 100644
index 00000000..dee60842
--- /dev/null
+++ b/src/widgets/omada/component.jsx
@@ -0,0 +1,39 @@
+import { useTranslation } from "next-i18next";
+
+import useWidgetAPI from "../../utils/proxy/use-widget-api";
+import Container from "../../components/services/widget/container";
+import Block from "../../components/services/widget/block";
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+
+ const { widget } = service;
+
+ const { data: omadaData, error: omadaAPIError } = useWidgetAPI(widget, {
+ refreshInterval: 5000,
+ });
+
+ if (omadaAPIError) {
+ return ;
+ }
+
+ if (!omadaData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ { omadaData.connectedGateways > 0 && }
+ { omadaData.connectedSwitches > 0 && }
+
+ );
+}
diff --git a/src/widgets/omada/proxy.js b/src/widgets/omada/proxy.js
new file mode 100644
index 00000000..e89ad81d
--- /dev/null
+++ b/src/widgets/omada/proxy.js
@@ -0,0 +1,252 @@
+
+import { httpProxy } from "utils/proxy/http";
+import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
+
+const proxyName = "omadaProxyHandler";
+
+const logger = createLogger(proxyName);
+
+async function login(loginUrl, username, password, controllerVersionMajor) {
+ const params = {
+ username,
+ password
+ }
+
+ if (controllerVersionMajor === 3) {
+ params.method = "login";
+ params.params = {
+ name: username,
+ password
+ };
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ const [status, contentType, data] = await httpProxy(loginUrl, {
+ method: "POST",
+ body: JSON.stringify(params),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ return [status, JSON.parse(data.toString())];
+}
+
+
+export default async function omadaProxyHandler(req, res) {
+ const { group, service } = req.query;
+
+ if (group && service) {
+ const widget = await getServiceWidget(group, service);
+
+ if (widget) {
+
+ const {url} = widget;
+
+ const controllerInfoURL = `${widget.url}/api/info`;
+
+ let [status, contentType, data] = await httpProxy(controllerInfoURL, {
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (status !== 200) {
+ logger.error("Unable to retrieve Omada controller info");
+ return res.status(status).json({error: {message: `HTTP Error ${status}`, url: controllerInfoURL, data}});
+ }
+
+ let cId;
+ let controllerVersion;
+
+ try {
+ cId = JSON.parse(data).result.omadacId;
+ controllerVersion = JSON.parse(data).result.controllerVer;
+ } catch (e) {
+ controllerVersion = "3.2.x"
+ }
+
+ const controllerVersionMajor = parseInt(controllerVersion.split('.')[0], 10)
+
+ if (![3,4,5].includes(controllerVersionMajor)) {
+ return res.status(500).json({error: {message: "Error determining controller version", data}});
+ }
+
+ let loginUrl;
+
+ switch (controllerVersionMajor) {
+ case 3:
+ loginUrl = `${widget.url}/api/user/login?ajax`;
+ break;
+ case 4:
+ loginUrl = `${widget.url}/api/v2/login`;
+ break;
+ case 5:
+ loginUrl = `${widget.url}/${cId}/api/v2/login`;
+ break;
+ default:
+ break;
+ }
+
+ const [loginStatus, loginResponseData] = await login(loginUrl, widget.username, widget.password, controllerVersionMajor);
+
+ if (loginStatus !== 200 || loginResponseData.errorCode > 0) {
+ return res.status(status).json({error: {message: "Error logging in to Oamda controller", url: loginUrl, data: loginResponseData}});
+ }
+
+ const { token } = loginResponseData.result;
+
+ let sitesUrl;
+ let body = {};
+ let params = { token };
+ let headers = { "Csrf-Token": token };
+ let method = "GET";
+
+ switch (controllerVersionMajor) {
+ case 3:
+ sitesUrl = `${widget.url}/web/v1/controller?ajax=&token=${token}`;
+ body = {
+ "method": "getUserSites",
+ "params": {
+ "userName": widget.username
+ }
+ };
+ method = "POST";
+ break;
+ case 4:
+ sitesUrl = `${widget.url}/api/v2/sites?token=${token}¤tPage=1¤tPageSize=1000`;
+ break;
+ case 5:
+ sitesUrl = `${widget.url}/${cId}/api/v2/sites?token=${token}¤tPage=1¤tPageSize=1000`;
+ break;
+ default:
+ break;
+ }
+
+ [status, contentType, data] = await httpProxy(sitesUrl, {
+ method,
+ params,
+ body: JSON.stringify(body),
+ headers,
+ });
+
+ const sitesResponseData = JSON.parse(data);
+
+ if (sitesResponseData.errorCode > 0) {
+ logger.debug(`HTTTP ${status} getting sites list: ${sitesResponseData.msg}`);
+ return res.status(status).json({error: {message: "Error getting sites list", url, data: sitesResponseData}});
+ }
+
+ const site = (controllerVersionMajor === 3) ?
+ sitesResponseData.result.siteList.find(s => s.name === widget.site):
+ sitesResponseData.result.data.find(s => s.name === widget.site);
+
+ if (!site) {
+ return res.status(status).json({error: {message: `Site ${widget.site} is not found`, url, data}});
+ }
+
+ let siteResponseData;
+
+ let connectedAp;
+ let activeUser;
+ let connectedSwitches;
+ let connectedGateways;
+ let alerts;
+
+ if (controllerVersionMajor === 3) {
+ // Omada v3 controller requires switching site
+ const switchUrl = `${widget.url}/web/v1/controller?ajax=&token=${token}`;
+ method = "POST";
+ body = {
+ method: "switchSite",
+ params: {
+ siteName: site.siteName,
+ userName: widget.username
+ }
+ };
+ headers = { "Content-Type": "application/json" };
+ params = { token };
+
+ [status, contentType, data] = await httpProxy(switchUrl, {
+ method,
+ params,
+ body: JSON.stringify(body),
+ headers,
+ });
+
+ const switchResponseData = JSON.parse(data);
+ if (status !== 200 || switchResponseData.errorCode > 0) {
+ logger.error(`HTTP ${status} getting sites list: ${data}`);
+ return res.status(status).json({error: {message: "Error switching site", url: switchUrl, data}});
+ }
+
+ const statsUrl = `${widget.url}/web/v1/controller?getGlobalStat=&token=${token}`;
+ [status, contentType, data] = await httpProxy(statsUrl, {
+ method,
+ params,
+ body: JSON.stringify({
+ "method": "getGlobalStat",
+ }),
+ headers
+ });
+
+ siteResponseData = JSON.parse(data);
+
+ if (status !== 200 || siteResponseData.errorCode > 0) {
+ return res.status(status).json({error: {message: "Error getting stats", url: statsUrl, data}});
+ }
+
+ connectedAp = siteResponseData.result.connectedAp;
+ activeUser = siteResponseData.result.activeUser;
+ alerts = siteResponseData.result.alerts;
+ } else if (controllerVersionMajor === 4 || controllerVersionMajor === 5) {
+ const siteName = (controllerVersionMajor === 5) ? site.id : site.key;
+ const siteStatsUrl = (controllerVersionMajor === 4) ?
+ `${url}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}¤tPage=1¤tPageSize=1000` :
+ `${url}/${cId}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}¤tPage=1¤tPageSize=1000`;
+
+ [status, contentType, data] = await httpProxy(siteStatsUrl, {
+ headers: {
+ "Csrf-Token": token,
+ },
+ });
+
+ siteResponseData = JSON.parse(data);
+
+ if (status !== 200 || siteResponseData.errorCode > 0) {
+ logger.debug(`HTTP ${status} getting stats for site ${widget.site} with message ${siteResponseData.msg}`);
+ return res.status(500).send(data);
+ }
+
+ const alertUrl = (controllerVersionMajor === 4) ?
+ `${url}/api/v2/sites/${siteName}/alerts/num?token=${token}¤tPage=1¤tPageSize=1000` :
+ `${url}/${cId}/api/v2/sites/${siteName}/alerts/num?token=${token}¤tPage=1¤tPageSize=1000`;
+
+ // eslint-disable-next-line no-unused-vars
+ [status, contentType, data] = await httpProxy(alertUrl, {
+ headers: {
+ "Csrf-Token": token,
+ },
+ });
+ const alertResponseData = JSON.parse(data);
+
+ activeUser = siteResponseData.result.totalClientNum;
+ connectedAp = siteResponseData.result.connectedApNum;
+ connectedGateways = siteResponseData.result.connectedGatewayNum;
+ connectedSwitches = siteResponseData.result.connectedSwitchNum;
+ alerts = alertResponseData.result.alertNum;
+ }
+
+ return res.send(JSON.stringify({
+ connectedAp,
+ activeUser,
+ alerts,
+ connectedGateways,
+ connectedSwitches,
+ }));
+ }
+ }
+
+ return res.status(400).json({ error: "Invalid proxy service type" });
+}
diff --git a/src/widgets/omada/widget.js b/src/widgets/omada/widget.js
new file mode 100644
index 00000000..5e32edad
--- /dev/null
+++ b/src/widgets/omada/widget.js
@@ -0,0 +1,7 @@
+import omadaProxyHandler from "./proxy";
+
+const widget = {
+ proxyHandler: omadaProxyHandler,
+};
+
+export default widget;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 2b45e55a..c68dfe3e 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -21,6 +21,7 @@ import navidrome from "./navidrome/widget";
import nextdns from "./nextdns/widget";
import npm from "./npm/widget";
import nzbget from "./nzbget/widget";
+import omada from "./omada/widget";
import ombi from "./ombi/widget";
import overseerr from "./overseerr/widget";
import paperlessngx from "./paperlessngx/widget";
@@ -73,6 +74,7 @@ const widgets = {
nextdns,
npm,
nzbget,
+ omada,
ombi,
overseerr,
paperlessngx,