Feature: Add APC UPS widget

This commit is contained in:
Nicu Pavel 2024-12-15 20:32:25 +00:00
parent a35c60f973
commit e43166697c
9 changed files with 189 additions and 0 deletions

View 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
host: IP address for apcupsd host
port: 3551
```

View File

@ -8,6 +8,7 @@ search:
You can also find a list of all available service widgets in the sidebar navigation.
- [Adguard Home](adguard-home.md)
- [APC UPS](apcups.md)
- [ArgoCD](argocd.md)
- [Atsumeru](atsumeru.md)
- [Audiobookshelf](audiobookshelf.md)

View File

@ -31,6 +31,7 @@ nav:
- "Service Widgets":
- widgets/services/index.md
- widgets/services/adguard-home.md
- widgets/services/apcups.md
- widgets/services/argocd.md
- widgets/services/atsumeru.md
- widgets/services/audiobookshelf.md

View File

@ -1007,5 +1007,11 @@
"issues": "Issues",
"merges": "Merge Requests",
"projects": "Projects"
},
"apcups": {
"status": "Status",
"load": "Load",
"bcharge":"Battery Charge",
"timeleft":"Time Left"
}
}

View File

@ -0,0 +1,32 @@
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, "status");
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>
);
}

122
src/widgets/apcups/proxy.js Normal file
View File

@ -0,0 +1,122 @@
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");
const DEBUG = false;
const APC_COMMANDS = {
status: 'status',
events: 'events',
};
const dumpBuffer = (buffer) => {
logger.debug(buffer.toString('hex').match(/../g).join(' '))
}
const 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);
if (DEBUG) logger.debug(ptr, lineLen, asciiData);
output.push(asciiData);
ptr += 2 + lineLen;
}
return output;
}
const statusAsJSON = (statusOutput) => 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;
}, {})
const getStatus = async (_host, _port) => new Promise((resolve, reject) => {
const host = _host ?? '127.0.0.1';
const port = _port ?? 3551;
const socket = new net.Socket();
socket.setTimeout(5000);
socket.connect({ host, port });
const fullResponse = [];
socket.on('connect', () => {
logger.debug(`Connecting to ${host}:${port}`);
const buffer = Buffer.alloc(APC_COMMANDS.status.length + 2);
buffer.writeUInt16BE(APC_COMMANDS.status.length, 0);
buffer.write(APC_COMMANDS.status, 2);
socket.write(buffer);
});
socket.on('data', (data) => {
fullResponse.push(data);
if (data.readUInt16BE(data.length - 2) === 0) {
try {
const buffer = Buffer.concat(fullResponse);
if (DEBUG) dumpBuffer(buffer);
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 data = {};
try {
const statusData = await getStatus(widget.host, widget.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);
}

View File

@ -0,0 +1,8 @@
import apcupsProxyHandler from "./proxy";
const widget = {
proxyHandler: apcupsProxyHandler,
allowedEndpoints: /status/,
};
export default widget;

View File

@ -2,6 +2,7 @@ import dynamic from "next/dynamic";
const components = {
adguard: dynamic(() => import("./adguard/component")),
apcups: dynamic(() => import("./apcups/component")),
argocd: dynamic(() => import("./argocd/component")),
atsumeru: dynamic(() => import("./atsumeru/component")),
audiobookshelf: dynamic(() => import("./audiobookshelf/component")),

View File

@ -1,4 +1,5 @@
import adguard from "./adguard/widget";
import apcups from "./apcups/widget";
import argocd from "./argocd/widget";
import atsumeru from "./atsumeru/widget";
import audiobookshelf from "./audiobookshelf/widget";
@ -133,6 +134,7 @@ import zabbix from "./zabbix/widget";
const widgets = {
adguard,
apcups,
argocd,
atsumeru,
audiobookshelf,