Feature: Fritz!Box Widget (#2387)

* Feature: Fritz!Box Widget

* Use i18n

* code style & formatting

---------

Co-authored-by: Thorben Grove <thorben.grove@tui.de>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
pull/2395/head
Thorben 1 year ago committed by GitHub
parent b3414fc35f
commit 4c45c6453f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,22 @@
---
title: FRITZ!Box
description: FRITZ!Box Widget Configuration
---
Application access & UPnP must be activated on your device:
```
Home Network > Network > Network Settings > Access Settings in the Home Network
[x] Allow access for applications
[x] Transmit status information over UPnP
```
You don't need to provide any credentials.
Allowed fields (limited to a max of 4): `["connectionStatus", "upTime", "maxDown", "maxUp", "down", "up", "received", "sent", "externalIPAddress"]`.
```yaml
widget:
type: fritzbox
url: https://192.168.178.1
```

@ -53,6 +53,7 @@ nav:
- widgets/services/fileflows.md - widgets/services/fileflows.md
- widgets/services/flood.md - widgets/services/flood.md
- widgets/services/freshrss.md - widgets/services/freshrss.md
- widgets/services/fritzbox.md
- widgets/services/gamedig.md - widgets/services/gamedig.md
- widgets/services/ghostfolio.md - widgets/services/ghostfolio.md
- widgets/services/glances.md - widgets/services/glances.md

@ -122,6 +122,24 @@
"subscriptions": "Abonnements", "subscriptions": "Abonnements",
"unread": "Ungelesen" "unread": "Ungelesen"
}, },
"fritzbox": {
"connectionStatus": "Status",
"connectionStatusUnconfigured": "Unkonfiguriert",
"connectionStatusConnecting": "Verbinde",
"connectionStatusAuthenticating": "Authenifiziere",
"connectionStatusPendingDisconnect": "Anstehende Trennung",
"connectionStatusDisconnecting": "Trenne",
"connectionStatusDisconnected": "Getrennt",
"connectionStatusConnected": "Verbunden",
"uptime": "Betriebszeit",
"maxDown": "Max. Down",
"maxUp": "Max. Up",
"down": "Down",
"up": "Up",
"received": "Empfangen",
"sent": "Gesendet",
"externalIPAddress": "Ext. IP"
},
"caddy": { "caddy": {
"upstreams": "Upstreams", "upstreams": "Upstreams",
"requests": "Aktuelle Anfragen", "requests": "Aktuelle Anfragen",

@ -122,6 +122,24 @@
"subscriptions": "Subscriptions", "subscriptions": "Subscriptions",
"unread": "Unread" "unread": "Unread"
}, },
"fritzbox": {
"connectionStatus": "Status",
"connectionStatusUnconfigured": "Unconfigured",
"connectionStatusConnecting": "Connecting",
"connectionStatusAuthenticating": "Authenticating",
"connectionStatusPendingDisconnect": "PendingDisconnect",
"connectionStatusDisconnecting": "Disconnecting",
"connectionStatusDisconnected": "Disconnected",
"connectionStatusConnected": "Connected",
"uptime": "Uptime",
"maxDown": "Max. Down",
"maxUp": "Max. Up",
"down": "Down",
"up": "Up",
"received": "Received",
"sent": "Sent",
"externalIPAddress": "Ext. IP"
},
"caddy": { "caddy": {
"upstreams": "Upstreams", "upstreams": "Upstreams",
"requests": "Current requests", "requests": "Current requests",

@ -27,6 +27,7 @@ const components = {
fileflows: dynamic(() => import("./fileflows/component")), fileflows: dynamic(() => import("./fileflows/component")),
flood: dynamic(() => import("./flood/component")), flood: dynamic(() => import("./flood/component")),
freshrss: dynamic(() => import("./freshrss/component")), freshrss: dynamic(() => import("./freshrss/component")),
fritzbox: dynamic(() => import("./fritzbox/component")),
gamedig: dynamic(() => import("./gamedig/component")), gamedig: dynamic(() => import("./gamedig/component")),
ghostfolio: dynamic(() => import("./ghostfolio/component")), ghostfolio: dynamic(() => import("./ghostfolio/component")),
glances: dynamic(() => import("./glances/component")), glances: dynamic(() => import("./glances/component")),

@ -0,0 +1,67 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";
const formatUptime = (timestamp) => {
const hours = Math.floor(timestamp / 3600);
const minutes = Math.floor((timestamp % 3600) / 60);
const seconds = timestamp % 60;
const hourDuration = hours > 0 ? `${hours}h` : "00h";
const minDuration = minutes > 0 ? `${minutes}m` : "00m";
const secDuration = seconds > 0 ? `${seconds}s` : "00s";
return hourDuration + minDuration + secDuration;
};
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data: fritzboxData, error: fritzboxError } = useWidgetAPI(widget, "status");
if (fritzboxError) {
return <Container service={service} error={fritzboxError} />;
}
// Default fields
if (!widget.fields?.length > 0) {
widget.fields = ["connectionStatus", "uptime", "maxDown", "maxUp"];
}
const MAX_ALLOWED_FIELDS = 4;
// Limits max number of displayed fields
if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
}
if (!fritzboxData) {
return (
<Container service={service}>
<Block label="fritzbox.connectionStatus" />
<Block label="fritzbox.uptime" />
<Block label="fritzbox.maxDown" />
<Block label="fritzbox.maxUp" />
<Block label="fritzbox.down" />
<Block label="fritzbox.up" />
<Block label="fritzbox.received" />
<Block label="fritzbox.sent" />
<Block label="fritzbox.externalIPAddress" />
</Container>
);
}
return (
<Container service={service}>
<Block label="fritzbox.connectionStatus" value={t(`fritzbox.connectionStatus${fritzboxData.connectionStatus}`)} />
<Block label="fritzbox.uptime" value={formatUptime(fritzboxData.uptime)} />
<Block label="fritzbox.maxDown" value={t("common.byterate", { value: fritzboxData.maxDown / 8, decimals: 1 })} />
<Block label="fritzbox.maxUp" value={t("common.byterate", { value: fritzboxData.maxUp / 8, decimals: 1 })} />
<Block label="fritzbox.down" value={t("common.byterate", { value: fritzboxData.down, decimals: 1 })} />
<Block label="fritzbox.up" value={t("common.byterate", { value: fritzboxData.up, decimals: 1 })} />
<Block label="fritzbox.received" value={t("common.bytes", { value: fritzboxData.received })} />
<Block label="fritzbox.sent" value={t("common.bytes", { value: fritzboxData.sent })} />
<Block label="fritzbox.externalIPAddress" value={fritzboxData.externalIPAddress} />
</Container>
);
}

@ -0,0 +1,84 @@
import { xml2json } from "xml-js";
import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
const logger = createLogger("fritzboxProxyHandler");
async function requestEndpoint(apiBaseUrl, service, action) {
const servicePath = service === "WANIPConnection" ? "WANIPConn1" : "WANCommonIFC1";
const params = {
method: "POST",
headers: {
"Content-Type": "text/xml; charset='utf-8'",
SoapAction: `urn:schemas-upnp-org:service:${service}:1#${action}`,
},
body:
"<?xml version='1.0' encoding='utf-8'?>" +
"<s:Envelope s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/' xmlns:s='http://schemas.xmlsoap.org/soap/envelope/'>" +
"<s:Body>" +
`<u:${action} xmlns:u='urn:schemas-upnp-org:service:${service}:1' />` +
"</s:Body>" +
"</s:Envelope>",
};
const apiUrl = `${apiBaseUrl}/igdupnp/control/${servicePath}`;
const [status, , data] = await httpProxy(apiUrl, params);
if (status !== 200) {
logger.debug(`HTTP ${status} performing SoapRequest for ${service}->${action}`, data);
throw new Error(`Failed fetching '${action}'`);
}
const response = {};
try {
const jsonData = JSON.parse(xml2json(data));
const responseElements = jsonData?.elements[0]?.elements[0]?.elements[0]?.elements || [];
responseElements.forEach((element) => {
response[element.name] = element.elements[0]?.text || "";
});
} catch (e) {
logger.debug(`Failed parsing ${service}->${action} response:`, data);
throw new Error(`Failed parsing '${action}' response`);
}
return response;
}
export default async function fritzboxProxyHandler(req, res) {
const { group, service } = req.query;
const serviceWidget = await getServiceWidget(group, service);
if (!serviceWidget) {
res.status(500).json({ error: "Service widget not found" });
return;
}
if (!serviceWidget.url) {
res.status(500).json({ error: "Service widget url not configured" });
return;
}
const serviceWidgetUrl = new URL(serviceWidget.url);
const port = serviceWidgetUrl.protocol === "https:" ? 49443 : 49000;
const apiBaseUrl = `${serviceWidgetUrl.protocol}//${serviceWidgetUrl.hostname}:${port}`;
await Promise.all([
requestEndpoint(apiBaseUrl, "WANIPConnection", "GetStatusInfo"),
requestEndpoint(apiBaseUrl, "WANIPConnection", "GetExternalIPAddress"),
requestEndpoint(apiBaseUrl, "WANCommonInterfaceConfig", "GetCommonLinkProperties"),
requestEndpoint(apiBaseUrl, "WANCommonInterfaceConfig", "GetAddonInfos"),
])
.then(([statusInfo, externalIPAddress, linkProperties, addonInfos]) => {
res.status(200).json({
connectionStatus: statusInfo.NewConnectionStatus,
uptime: statusInfo.NewUptime,
maxDown: linkProperties.NewLayer1DownstreamMaxBitRate,
maxUp: linkProperties.NewLayer1UpstreamMaxBitRate,
down: addonInfos.NewByteReceiveRate,
up: addonInfos.NewByteSendRate,
received: addonInfos.NewX_AVM_DE_TotalBytesReceived64,
sent: addonInfos.NewX_AVM_DE_TotalBytesSent64,
externalIPAddress: externalIPAddress.NewExternalIPAddress,
});
})
.catch((error) => {
res.status(500).json({ error: error.message });
});
}

@ -0,0 +1,7 @@
import fritzboxProxyHandler from "./proxy";
const widget = {
proxyHandler: fritzboxProxyHandler,
};
export default widget;

@ -21,6 +21,7 @@ import evcc from "./evcc/widget";
import fileflows from "./fileflows/widget"; import fileflows from "./fileflows/widget";
import flood from "./flood/widget"; import flood from "./flood/widget";
import freshrss from "./freshrss/widget"; import freshrss from "./freshrss/widget";
import fritzbox from "./fritzbox/widget";
import gamedig from "./gamedig/widget"; import gamedig from "./gamedig/widget";
import ghostfolio from "./ghostfolio/widget"; import ghostfolio from "./ghostfolio/widget";
import glances from "./glances/widget"; import glances from "./glances/widget";
@ -122,6 +123,7 @@ const widgets = {
fileflows, fileflows,
flood, flood,
freshrss, freshrss,
fritzbox,
gamedig, gamedig,
ghostfolio, ghostfolio,
glances, glances,

Loading…
Cancel
Save