Add unifi_console widget, API endpoint

pull/341/head
Michael Shamoon 2 years ago
parent adf601c572
commit b9b9449cd3

@ -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",

@ -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",

@ -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 }) {

@ -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…
Cancel
Save