mirror of
https://github.com/karl0ss/homepage.git
synced 2025-04-29 12:03:41 +01:00
Feature: Add APC UPS widget (#4840)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
parent
9b8dd94aae
commit
fdf405fe0a
16
docs/widgets/services/apcups.md
Normal file
16
docs/widgets/services/apcups.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: APC UPS Monitoring
|
||||||
|
description: Lightweight monitoring widget for APC UPSs using apcupsd daemon
|
||||||
|
---
|
||||||
|
|
||||||
|
This widget extracts UPS information from an apcupsd daemon.
|
||||||
|
Only works for [APC/Schneider](https://www.se.com/us/en/product-range/61915-smartups/#products) UPS products.
|
||||||
|
|
||||||
|
[!NOTE]
|
||||||
|
By default apcupsd daemon is bound to 127.0.0.1. Edit `/etc/apcupsd.conf` and change `NISIP` to an IP accessible from your homepage docker (usually your internal LAN interface).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: apcups
|
||||||
|
url: tcp://your.acpupsd.host:3551
|
||||||
|
```
|
@ -8,6 +8,7 @@ search:
|
|||||||
You can also find a list of all available service widgets in the sidebar navigation.
|
You can also find a list of all available service widgets in the sidebar navigation.
|
||||||
|
|
||||||
- [Adguard Home](adguard-home.md)
|
- [Adguard Home](adguard-home.md)
|
||||||
|
- [APC UPS](apcups.md)
|
||||||
- [ArgoCD](argocd.md)
|
- [ArgoCD](argocd.md)
|
||||||
- [Atsumeru](atsumeru.md)
|
- [Atsumeru](atsumeru.md)
|
||||||
- [Audiobookshelf](audiobookshelf.md)
|
- [Audiobookshelf](audiobookshelf.md)
|
||||||
|
@ -31,6 +31,7 @@ nav:
|
|||||||
- "Service Widgets":
|
- "Service Widgets":
|
||||||
- widgets/services/index.md
|
- widgets/services/index.md
|
||||||
- widgets/services/adguard-home.md
|
- widgets/services/adguard-home.md
|
||||||
|
- widgets/services/apcups.md
|
||||||
- widgets/services/argocd.md
|
- widgets/services/argocd.md
|
||||||
- widgets/services/atsumeru.md
|
- widgets/services/atsumeru.md
|
||||||
- widgets/services/audiobookshelf.md
|
- widgets/services/audiobookshelf.md
|
||||||
|
@ -1016,5 +1016,11 @@
|
|||||||
"issues": "Issues",
|
"issues": "Issues",
|
||||||
"merges": "Merge Requests",
|
"merges": "Merge Requests",
|
||||||
"projects": "Projects"
|
"projects": "Projects"
|
||||||
|
},
|
||||||
|
"apcups": {
|
||||||
|
"status": "Status",
|
||||||
|
"load": "Load",
|
||||||
|
"bcharge":"Battery Charge",
|
||||||
|
"timeleft":"Time Left"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
33
src/widgets/apcups/component.jsx
Normal file
33
src/widgets/apcups/component.jsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import Container from "components/services/widget/container";
|
||||||
|
import Block from "components/services/widget/block";
|
||||||
|
|
||||||
|
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||||
|
|
||||||
|
export default function Component({ service }) {
|
||||||
|
const { widget } = service;
|
||||||
|
const { data, error } = useWidgetAPI(widget);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Container service={service} error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="apcups.status" />
|
||||||
|
<Block label="apcups.load" />
|
||||||
|
<Block label="apcups.bcharge" />
|
||||||
|
<Block label="apcups.timeleft" />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="apcups.status" value={data.status} />
|
||||||
|
<Block label="apcups.load" value={data.load} />
|
||||||
|
<Block label="apcups.bcharge" value={data.bcharge} />
|
||||||
|
<Block label="apcups.timeleft" value={data.timeleft} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
112
src/widgets/apcups/proxy.js
Normal file
112
src/widgets/apcups/proxy.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import net from "node:net";
|
||||||
|
import { Buffer } from "node:buffer";
|
||||||
|
|
||||||
|
import getServiceWidget from "utils/config/service-helpers";
|
||||||
|
import createLogger from "utils/logger";
|
||||||
|
|
||||||
|
const logger = createLogger("apcupsProxyHandler");
|
||||||
|
|
||||||
|
function parseResponse(buffer) {
|
||||||
|
let ptr = 0;
|
||||||
|
const output = [];
|
||||||
|
while (ptr < buffer.length) {
|
||||||
|
const lineLen = buffer.readUInt16BE(ptr);
|
||||||
|
const asciiData = buffer.toString("ascii", ptr + 2, lineLen + ptr + 2);
|
||||||
|
output.push(asciiData);
|
||||||
|
ptr += 2 + lineLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusAsJSON(statusOutput) {
|
||||||
|
return statusOutput?.reduce((output, line) => {
|
||||||
|
if (!line || line.startsWith("END APC")) return output;
|
||||||
|
const [key, value] = line.trim().split(":");
|
||||||
|
const newOutput = { ...output };
|
||||||
|
newOutput[key.trim()] = value?.trim();
|
||||||
|
return newOutput;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStatus(host = "127.0.0.1", port = 3551) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const socket = new net.Socket();
|
||||||
|
socket.setTimeout(5000);
|
||||||
|
socket.connect({ host, port });
|
||||||
|
|
||||||
|
const response = [];
|
||||||
|
|
||||||
|
socket.on("connect", () => {
|
||||||
|
const CMD = "status";
|
||||||
|
logger.debug(`Connecting to ${host}:${port}`);
|
||||||
|
const buffer = Buffer.alloc(CMD.length + 2);
|
||||||
|
buffer.writeUInt16BE(CMD.length, 0);
|
||||||
|
buffer.write(CMD, 2);
|
||||||
|
socket.write(buffer);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("data", (data) => {
|
||||||
|
response.push(data);
|
||||||
|
|
||||||
|
if (data.readUInt16BE(data.length - 2) === 0) {
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.concat(response);
|
||||||
|
const output = parseResponse(buffer);
|
||||||
|
resolve(output);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
socket.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("error", (err) => {
|
||||||
|
socket.destroy();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
socket.on("timeout", () => {
|
||||||
|
socket.destroy();
|
||||||
|
reject(new Error("socket timeout"));
|
||||||
|
});
|
||||||
|
socket.on("end", () => {
|
||||||
|
logger.debug("socket end");
|
||||||
|
});
|
||||||
|
socket.on("close", () => {
|
||||||
|
logger.debug("socket closed");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function apcupsProxyHandler(req, res) {
|
||||||
|
const { group, service, index } = req.query;
|
||||||
|
|
||||||
|
if (!group || !service) {
|
||||||
|
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||||
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = await getServiceWidget(group, service, index);
|
||||||
|
if (!widget) {
|
||||||
|
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||||
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(widget.url);
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const statusData = await getStatus(url.hostname, url.port);
|
||||||
|
const jsonData = statusAsJSON(statusData);
|
||||||
|
|
||||||
|
data.status = jsonData.STATUS;
|
||||||
|
data.load = jsonData.LOADPCT;
|
||||||
|
data.bcharge = jsonData.BCHARGE;
|
||||||
|
data.timeleft = jsonData.TIMELEFT;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
|
return res.status(500).json({ error: e.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send(data);
|
||||||
|
}
|
7
src/widgets/apcups/widget.js
Normal file
7
src/widgets/apcups/widget.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import apcupsProxyHandler from "./proxy";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
proxyHandler: apcupsProxyHandler,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
@ -2,6 +2,7 @@ import dynamic from "next/dynamic";
|
|||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
adguard: dynamic(() => import("./adguard/component")),
|
adguard: dynamic(() => import("./adguard/component")),
|
||||||
|
apcups: dynamic(() => import("./apcups/component")),
|
||||||
argocd: dynamic(() => import("./argocd/component")),
|
argocd: dynamic(() => import("./argocd/component")),
|
||||||
atsumeru: dynamic(() => import("./atsumeru/component")),
|
atsumeru: dynamic(() => import("./atsumeru/component")),
|
||||||
audiobookshelf: dynamic(() => import("./audiobookshelf/component")),
|
audiobookshelf: dynamic(() => import("./audiobookshelf/component")),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import adguard from "./adguard/widget";
|
import adguard from "./adguard/widget";
|
||||||
|
import apcups from "./apcups/widget";
|
||||||
import argocd from "./argocd/widget";
|
import argocd from "./argocd/widget";
|
||||||
import atsumeru from "./atsumeru/widget";
|
import atsumeru from "./atsumeru/widget";
|
||||||
import audiobookshelf from "./audiobookshelf/widget";
|
import audiobookshelf from "./audiobookshelf/widget";
|
||||||
@ -134,6 +135,7 @@ import zabbix from "./zabbix/widget";
|
|||||||
|
|
||||||
const widgets = {
|
const widgets = {
|
||||||
adguard,
|
adguard,
|
||||||
|
apcups,
|
||||||
argocd,
|
argocd,
|
||||||
atsumeru,
|
atsumeru,
|
||||||
audiobookshelf,
|
audiobookshelf,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user