Add unifi_console widget, API endpoint
This commit is contained in:
parent
adf601c572
commit
b9b9449cd3
6 changed files with 259 additions and 0 deletions
|
@ -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",
|
||||
|
|
101
pnpm-lock.yaml
generated
101
pnpm-lock.yaml
generated
|
@ -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'}
|
||||
|
|
|
@ -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",
|
||||
|
|
96
src/components/widgets/unifi_console/unifi_console.jsx
Normal file
96
src/components/widgets/unifi_console/unifi_console.jsx
Normal file
|
@ -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 (
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center first:ml-0 ml-4">
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex flex-col items-center">
|
||||
<SiUbiquiti className="w-5 h-5 text-theme-800 dark:text-theme-200" />
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("unifi.wait")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row ml-3">
|
||||
<SiUbiquiti className="text-theme-800 dark:text-theme-200 w-3 h-3 mr-1" />
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs font-bold flex flex-row justify-between">
|
||||
{data.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row ml-3 text-[10px] justify-between">
|
||||
<div className="flex flex-row">
|
||||
<div className="pr-0.5 text-theme-800 dark:text-theme-200">
|
||||
{t("common.number", {
|
||||
value: data.uptime / 86400,
|
||||
maximumFractionDigits: 1,
|
||||
})}
|
||||
</div>
|
||||
<div className="pr-1 text-theme-800 dark:text-theme-200">{t("unifi.days")}</div>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<div className="pr-1 text-theme-800 dark:text-theme-200">{t("unifi.wan")}</div>
|
||||
<BiCheckCircle className="text-theme-800 dark:text-theme-200 h-4 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row ml-3 py-0.5">
|
||||
<BiWifi className="text-theme-800 dark:text-theme-200 w-4 h-4 mr-1" />
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between" title={t("unifi.users")}>
|
||||
<div className="pr-0.5">
|
||||
{t("common.number", {
|
||||
value: data.wlan.users,
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row ml-3 pb-0.5">
|
||||
<MdSettingsEthernet className="text-theme-800 dark:text-theme-200 w-4 h-4 mr-1" />
|
||||
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between" title={t("unifi.users")}>
|
||||
<div className="pr-0.5">
|
||||
{t("common.number", {
|
||||
value: data.lan.users,
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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 }) {
|
||||
|
|
53
src/pages/api/widgets/unifi.js
Normal file
53
src/pages/api/widgets/unifi.js
Normal file
|
@ -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}`
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue