From b9b9449cd33f42f86bed269babe6ead24b5f5cb9 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 5 Oct 2022 21:36:43 -0700 Subject: [PATCH] Add unifi_console widget, API endpoint --- package.json | 1 + pnpm-lock.yaml | 101 ++++++++++++++++++ public/locales/en/common.json | 7 ++ .../widgets/unifi_console/unifi_console.jsx | 96 +++++++++++++++++ src/components/widgets/widget.jsx | 1 + src/pages/api/widgets/unifi.js | 53 +++++++++ 6 files changed, 259 insertions(+) create mode 100644 src/components/widgets/unifi_console/unifi_console.jsx create mode 100644 src/pages/api/widgets/unifi.js diff --git a/package.json b/package.json index 3dd2ed0d1..0ee4cf2b4 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "next": "^12.3.1", "next-i18next": "^12.0.1", "node-os-utils": "^1.3.7", + "node-unifi": "^2.1.0", "pretty-bytes": "^6.0.0", "raw-body": "^2.5.1", "react": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49394c8ea..c6cbeb989 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,7 @@ specifiers: next: ^12.3.1 next-i18next: ^12.0.1 node-os-utils: ^1.3.7 + node-unifi: ^2.1.0 postcss: ^8.4.16 prettier: ^2.7.1 pretty-bytes: ^6.0.0 @@ -54,6 +55,7 @@ dependencies: next: 12.3.1_biqbaboplfbrettd7655fr4n2y next-i18next: 12.0.1_azq6kxkn3od7qdylwkyksrwopy node-os-utils: 1.3.7 + node-unifi: 2.1.0 pretty-bytes: 6.0.0 raw-body: 2.5.1 react: 18.2.0 @@ -458,6 +460,15 @@ packages: hasBin: true dev: true + /agent-base/6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /ajv/6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: @@ -577,6 +588,15 @@ packages: engines: {node: '>=4'} dev: true + /axios/0.27.2: + resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + dependencies: + follow-redirects: 1.15.2 + form-data: 4.0.0 + transitivePeerDependencies: + - debug + dev: false + /axobject-query/2.2.0: resolution: {integrity: sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==} dev: true @@ -1394,6 +1414,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /eventemitter2/6.4.9: + resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} + dev: false + /fast-deep-equal/3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true @@ -1488,6 +1512,15 @@ packages: mime-types: 2.1.35 dev: false + /form-data/4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /fraction.js/4.2.0: resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} dev: true @@ -1647,6 +1680,25 @@ packages: void-elements: 3.1.0 dev: false + /http-cookie-agent/4.0.2_tough-cookie@4.1.2: + resolution: {integrity: sha512-noTmxdH5CuytTnLj/Qv3Z84e/YFq8yLXAw3pqIYZ25Edhb9pQErIAC+ednw40Cic6Le/h9ryph5/TqsvkOaUCw==} + engines: {node: '>=14.18.0 <15.0.0 || >=16.0.0'} + peerDependencies: + deasync: ^0.1.26 + tough-cookie: ^4.0.0 + undici: ^5.1.1 + peerDependenciesMeta: + deasync: + optional: true + undici: + optional: true + dependencies: + agent-base: 6.0.2 + tough-cookie: 4.1.2 + transitivePeerDependencies: + - supports-color + dev: false + /http-errors/2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -2090,6 +2142,25 @@ packages: resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==} dev: true + /node-unifi/2.1.0: + resolution: {integrity: sha512-vawHGIFEc5XgCXo2I98h72pykVklemI5eE1d50oRZOLpfnYLVDNWF2RfdhvaRSHtVpPjFRshqJP2zuOSWnq4+A==} + engines: {node: '>=14.18.0 <15.0.0 || >=16.0.0'} + dependencies: + axios: 0.27.2 + eventemitter2: 6.4.9 + http-cookie-agent: 4.0.2_tough-cookie@4.1.2 + tough-cookie: 4.1.2 + url: 0.11.0 + ws: 8.9.0 + transitivePeerDependencies: + - bufferutil + - deasync + - debug + - supports-color + - undici + - utf-8-validate + dev: false + /normalize-path/3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -2365,10 +2436,20 @@ packages: once: 1.4.0 dev: false + /punycode/1.3.2: + resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} + dev: false + /punycode/2.1.1: resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} engines: {node: '>=6'} + /querystring/0.2.0: + resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} + engines: {node: '>=0.4.x'} + deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + dev: false + /querystringify/2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} dev: false @@ -2921,6 +3002,13 @@ packages: requires-port: 1.0.0 dev: false + /url/0.11.0: + resolution: {integrity: sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==} + dependencies: + punycode: 1.3.2 + querystring: 0.2.0 + dev: false + /use-sync-external-store/1.2.0_react@18.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: @@ -3000,6 +3088,19 @@ packages: /wrappy/1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + /ws/8.9.0: + resolution: {integrity: sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /xtend/4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} diff --git a/public/locales/en/common.json b/public/locales/en/common.json index dfc1a5ba6..f19120087 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -31,6 +31,13 @@ "used": "Used", "load": "Load" }, + "unifi": { + "users": "Users", + "status": "Status", + "days": "Days", + "wan": "WAN", + "wait": "Please wait" + }, "docker": { "rx": "RX", "tx": "TX", diff --git a/src/components/widgets/unifi_console/unifi_console.jsx b/src/components/widgets/unifi_console/unifi_console.jsx new file mode 100644 index 000000000..7e0ff7fc0 --- /dev/null +++ b/src/components/widgets/unifi_console/unifi_console.jsx @@ -0,0 +1,96 @@ +import useSWR from "swr"; +import { BiError, BiWifi, BiCheckCircle } from "react-icons/bi"; +import { MdSettingsEthernet } from "react-icons/md"; +import { useTranslation } from "next-i18next"; +import { SiUbiquiti } from "react-icons/si"; + +export default function Widget({ options }) { + const { t, i18n } = useTranslation(); + + const { data, error } = useSWR( + `/api/widgets/unifi?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}` + ); + + if (error || data?.error) { + return ( +
+
+
+ +
+ {t("widget.api_error")} + - +
+
+
+
+ ); + } + + if (!data) { + return ( +
+
+
+ +
+
+ {t("unifi.wait")} +
+
+
+ ); + } + + return ( +
+
+
+ +
+ {data.name} +
+
+
+
+
+ {t("common.number", { + value: data.uptime / 86400, + maximumFractionDigits: 1, + })} +
+
{t("unifi.days")}
+
+
+
{t("unifi.wan")}
+ +
+
+
+
+
+ +
+
+ {t("common.number", { + value: data.wlan.users, + maximumFractionDigits: 0, + })} +
+
+
+
+ +
+
+ {t("common.number", { + value: data.lan.users, + maximumFractionDigits: 0, + })} +
+
+
+
+
+ ); +} diff --git a/src/components/widgets/widget.jsx b/src/components/widgets/widget.jsx index de3f7a352..8daa87246 100644 --- a/src/components/widgets/widget.jsx +++ b/src/components/widgets/widget.jsx @@ -9,6 +9,7 @@ const widgetMappings = { search: dynamic(() => import("components/widgets/search/search")), greeting: dynamic(() => import("components/widgets/greeting/greeting")), datetime: dynamic(() => import("components/widgets/datetime/datetime")), + unifi_console: dynamic(() => import("components/widgets/unifi_console/unifi_console")), }; export default function Widget({ widget }) { diff --git a/src/pages/api/widgets/unifi.js b/src/pages/api/widgets/unifi.js new file mode 100644 index 000000000..f8cbcc1e6 --- /dev/null +++ b/src/pages/api/widgets/unifi.js @@ -0,0 +1,53 @@ +import { Controller } from "node-unifi"; + +export default async function handler(req, res) { + const { host, port, username, password } = req.query; + + if (!host) { + return res.status(400).json({ error: "Missing host" }); + } + + if (!username) { + return res.status(400).json({ error: "Missing username" }); + } + + if (!password) { + return res.status(400).json({ error: "Missing password" }); + } + + const controller = new Controller({ + host: host, + port: port, + sslverify: false + }); + + try { + //login to the controller + await controller.login(username, password); + + //retrieve sites + const sites = await controller.getSitesStats(); + const default_site = sites.find(s => s.name == "default"); + const wan = default_site.health.find(h => h.subsystem == "wan"); + const lan = default_site.health.find(h => h.subsystem == "lan"); + const wlan = default_site.health.find(h => h.subsystem == "wlan"); + + return res.status(200).json({ + name: wan.gw_name, + uptime: wan['gw_system-stats']['uptime'], + up: wan.status == 'ok', + wlan: { + users: wlan.num_user, + status: wlan.status + }, + lan: { + users: lan.num_user, + status: lan.status + } + }); + } catch (e) { + return res.status(400).json({ + error: `Error communicating with UniFi Console: ${e.message}` + }) + } +}