diff --git a/docs/widgets/services/fritzbox.md b/docs/widgets/services/fritzbox.md new file mode 100644 index 000000000..de63d77fc --- /dev/null +++ b/docs/widgets/services/fritzbox.md @@ -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 +``` diff --git a/mkdocs.yml b/mkdocs.yml index 97951650f..706a4a5b2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -53,6 +53,7 @@ nav: - widgets/services/fileflows.md - widgets/services/flood.md - widgets/services/freshrss.md + - widgets/services/fritzbox.md - widgets/services/gamedig.md - widgets/services/ghostfolio.md - widgets/services/glances.md diff --git a/public/locales/de/common.json b/public/locales/de/common.json index 42c08537e..fcd45f399 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -122,6 +122,24 @@ "subscriptions": "Abonnements", "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": { "upstreams": "Upstreams", "requests": "Aktuelle Anfragen", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 23b7ec1da..ad94e3aa4 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -122,6 +122,24 @@ "subscriptions": "Subscriptions", "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": { "upstreams": "Upstreams", "requests": "Current requests", diff --git a/src/widgets/components.js b/src/widgets/components.js index 99da81ea5..df9a75308 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -27,6 +27,7 @@ const components = { fileflows: dynamic(() => import("./fileflows/component")), flood: dynamic(() => import("./flood/component")), freshrss: dynamic(() => import("./freshrss/component")), + fritzbox: dynamic(() => import("./fritzbox/component")), gamedig: dynamic(() => import("./gamedig/component")), ghostfolio: dynamic(() => import("./ghostfolio/component")), glances: dynamic(() => import("./glances/component")), diff --git a/src/widgets/fritzbox/component.jsx b/src/widgets/fritzbox/component.jsx new file mode 100644 index 000000000..6e8bf11f1 --- /dev/null +++ b/src/widgets/fritzbox/component.jsx @@ -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 ; + } + + // 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 ( + + + + + + + + + + + + ); + } + + return ( + + + + + + + + + + + + ); +} diff --git a/src/widgets/fritzbox/proxy.js b/src/widgets/fritzbox/proxy.js new file mode 100644 index 000000000..e4f0fefdb --- /dev/null +++ b/src/widgets/fritzbox/proxy.js @@ -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: + "" + + "" + + "" + + `` + + "" + + "", + }; + 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 }); + }); +} diff --git a/src/widgets/fritzbox/widget.js b/src/widgets/fritzbox/widget.js new file mode 100644 index 000000000..131938210 --- /dev/null +++ b/src/widgets/fritzbox/widget.js @@ -0,0 +1,7 @@ +import fritzboxProxyHandler from "./proxy"; + +const widget = { + proxyHandler: fritzboxProxyHandler, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 0a2d24ed0..a41d93062 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -21,6 +21,7 @@ import evcc from "./evcc/widget"; import fileflows from "./fileflows/widget"; import flood from "./flood/widget"; import freshrss from "./freshrss/widget"; +import fritzbox from "./fritzbox/widget"; import gamedig from "./gamedig/widget"; import ghostfolio from "./ghostfolio/widget"; import glances from "./glances/widget"; @@ -122,6 +123,7 @@ const widgets = { fileflows, flood, freshrss, + fritzbox, gamedig, ghostfolio, glances,