YAOTU INSIGHTS

Homepage 集成 FRITZ!Box 路由器:UPnP 状态 Widget 配置与实现原理详解

Homepage 集成 FRITZ!Box 路由器:UPnP 状态 Widget 配置与实现原理详解
Homepage 集成 FRITZ!Box 路由器UPnP 状态 Widget 配置与实现原理详解【免费下载链接】homepageA highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations.项目地址: https://gitcode.com/GitHub_Trending/ho/homepage本文以开源项目 Homepage 的 FRITZ!Box Widget 官方文档 为主体结合仓库中src/widgets/fritzbox/的源码与测试完整讲解如何在 Homepage 仪表盘中接入 FRITZ!Box 路由器并展示其连接状态、带宽与 IP 信息从路由器端 UPnP 开启、配置文件编写、可用字段清单到 Widget 底层通过 SOAP/UPnP 协议获取数据的完整链路。读完本文你将能够独立配置并排障一个 FRITZ!Box 服务 Widget并理解其按需请求、字段上限与端口选择等实现细节。一、功能概述无需凭据的本地状态监控Homepage 的 FRITZ!Box Widget 是服务类 Widgetservice widget用于将 AVM FRITZ!Box 路由器的状态直接呈现在个人起始页上。它最显著的特点是不需要任何用户名或密码——数据通过路由器内置的UPnP通用即插即用接口读取只要在路由器设置中开启允许应用访问即可。从源码注册关系看该 Widget 在 src/widgets/widgets.js 中通过import fritzbox from ./fritzbox/widget注册其核心定义位于 src/widgets/fritzbox/widget.jsimport fritzboxProxyHandler from ./proxy; const widget { proxyHandler: fritzboxProxyHandler, allowedEndpoints: /status/, }; export default widget;其中proxyHandler指向proxy.js中的后端处理器负责向路由器发起 UPnP 请求allowedEndpoints: /status/声明前端只允许访问status这一个数据端点——这也是整个 Widget 唯一的数据通道。二、前置条件在 FRITZ!Box 上开启 UPnP 访问在配置 Homepage 之前必须先登录 FRITZ!Box 的 Web 管理界面开启 UPnP 应用访问权限。官方文档给出的路径为Home Network Network Network Settings Access Settings in the Home Network需要勾选两个选项[x] Allow access for applications允许应用访问[x] Transmit status information over UPnP通过 UPnP 传输状态信息只有同时开启这两项Homepage 才能通过 UPnP 读取到连接状态、带宽和 IP 等数据。若此步骤遗漏Widget 会因拿不到数据而报错如Failed fetching GetStatusInfo详见下文排障章节。三、基础配置示例在 Homepage 的widgets.yaml配置文件中新增一个服务 Widgettype固定为fritzbox并给出路由器的内网地址widget: type: fritzbox url: http://192.168.178.1这是文档给出的最小可用配置。其中type必须为fritzboxurlFRITZ!Box 的访问地址通常为网关地址192.168.178.1FRITZ!Box 默认网段或主机名fritz.box。关于协议选择文档特别提示由于无需凭据建议使用http而非https因为明文 HTTP 请求显著更快。这一建议在源码中有直接体现——src/widgets/fritzbox/proxy.js 会根据 URL 的协议自动选择 UPnP 端口const serviceWidgetUrl new URL(serviceWidget.url); const port serviceWidgetUrl.protocol https: ? 49443 : 49000; const apiBaseUrl ${serviceWidgetUrl.protocol}//${serviceWidgetUrl.hostname}:${port};即http协议走49000 端口https协议走49443 端口。因此示例中的http://192.168.178.1实际请求的是http://192.168.178.1:49000/igdupnp/control/...。若你配置了https请确保 FRITZ!Box 已开启 HTTPS 访问且证书可被接受。四、可用字段与默认值Widget 共支持 11 个字段文档明确指出最多只能显示 4 个Allowed fields (limited to a max of 4)[connectionStatus, uptime, maxDown, maxUp, down, up, received, sent, externalIPAddress, externalIPv6Address, externalIPv6Prefix]各字段的语义、展示格式及对应数据来源整理如下字段含义展示格式来源 UPnP 服务 / ActionconnectionStatus当前连接状态本地化文本见第五节WANIPConnection/GetStatusInfouptime在线时长common.duration时长格式WANIPConnection/GetStatusInfomaxDown最大下行带宽字节速率bit/s ÷ 8WANCommonInterfaceConfig/GetCommonLinkPropertiesmaxUp最大上行带宽字节速率bit/s ÷ 8WANCommonInterfaceConfig/GetCommonLinkPropertiesdown实时下行速率字节速率WANCommonInterfaceConfig/GetAddonInfosup实时上行速率字节速率WANCommonInterfaceConfig/GetAddonInfosreceived累计接收字节字节容量WANCommonInterfaceConfig/GetAddonInfossent累计发送字节字节容量WANCommonInterfaceConfig/GetAddonInfosexternalIPAddress公网 IPv4 地址纯文本WANIPConnection/GetExternalIPAddressexternalIPv6Address公网 IPv6 地址纯文本WANIPConnection/X_AVM_DE_GetExternalIPv6AddressexternalIPv6PrefixIPv6 前缀含掩码前缀/长度文本WANIPConnection/X_AVM_DE_GetIPv6Prefix默认字段为前 4 个定义在 src/widgets/fritzbox/component.jsxexport const fritzboxDefaultFields [connectionStatus, uptime, maxDown, maxUp];即不写fields时前端展示状态 / 在线时长 / 最大下行 / 最大上行四项。若需要自定义展示项可在配置中通过fields指定例如widget: type: fritzbox url: http://192.168.178.1 fields: - down - up - externalIPAddress - externalIPv6Address字段上限的强制截断fields最多 4 项的限制并非仅靠文档约束而是由前端组件强制执行的。component.jsx 中// Default fields if (!widget.fields?.length 0) { widget.fields fritzboxDefaultFields; } 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); }如果你在fields中列出了超过 4 个字段多余部分会被slice(0, 4)直接截断只保留前 4 个。这一行为在 src/widgets/fritzbox/component.test.jsx 中有明确的测试用例传入 6 个字段后断言结果被裁剪为[down, up, received, sent]且页面只渲染 4 个service-block。注意截断顺序由于前端按数组顺序slice若你的 4 个字段中包含externalIPv6Prefix等排序靠后的字段请务必把它放在前 4 位内否则会被丢弃。五、连接状态的本地化展示connectionStatus字段在路由器的 UPnP 响应中是一个数字枚举值Homepage 通过翻译文件将其映射为可读文本。public/locales/en/common.json 中定义了完整的映射枚举值键名显示文本0connectionStatusUnconfiguredUnconfigured1connectionStatusConnectingConnecting2connectionStatusAuthenticatingAuthenticating3connectionStatusPendingDisconnectPending Disconnect4connectionStatusDisconnectingDisconnecting5connectionStatusDisconnectedDisconnected6connectionStatusConnectedConnected前端渲染逻辑位于 component.jsxBlock labelfritzbox.connectionStatus value{t(fritzbox.connectionStatus${fritzboxData.connectionStatus})} /它把fritzbox.connectionStatus与后端返回的数值拼接成翻译键如connectionStatus6→ Connected。同时当后端未取到状态时proxy.js 会返回兜底值Unconfigured对应前端fritzbox.connectionStatusUnconfigured的翻译避免出现空值。六、数据来源与格式换算从 bit 到 byteWidget 展示的大多数数据并非直接来自路由器而是经过单位换算。换算逻辑集中在前端 component.jsxmaxDown/maxUp路由器返回的是NewLayer1DownstreamMaxBitRate/NewLayer1UpstreamMaxBitRate比特率bit/s前端先除以 8 再以字节速率显示Block labelfritzbox.maxDown value{t(common.byterate, { value: fritzboxData.maxDown / 8, decimals: 1 })} highlightValue{fritzboxData.maxDown / 8} /down/up来自GetAddonInfos的NewByteReceiveRate/NewByteSendRate已是字节速率直接以common.byterate格式化received/sent来自NewX_AVM_DE_TotalBytesReceived64/NewX_AVM_DE_TotalBytesSent64累计字节数以common.bytes格式化uptime以common.duration格式化为可读时长。上述换算均有测试佐证。component.test.jsx 中传入maxDown: 8000、maxUp: 16000后断言页面显示1000与2000即8000/8、16000/8与换算逻辑完全一致。七、底层原理后端如何通过 SOAP/UPnP 取数前端通过useWidgetAPI(widget, status)请求/status端点component.jsx真正与路由器通信的是后端代理 src/widgets/fritzbox/proxy.js。7.1 按需请求只取需要的字段代理不会一次性拉取全部数据而是根据配置的fields决定调用哪些 UPnP Actionproxy.jsif (!serviceWidget.fields?.length 0) { serviceWidget.fields fritzboxDefaultFields; } const requestStatusInfo [connectionStatus, uptime].some((field) serviceWidget.fields.includes(field)); const requestLinkProperties [maxDown, maxUp].some((field) serviceWidget.fields.includes(field)); const requestAddonInfos [down, up, received, sent].some((field) serviceWidget.fields.includes(field)); const requestExternalIPAddress [externalIPAddress].some((field) serviceWidget.fields.includes(field)); const requestExternalIPv6Address [externalIPv6Address].some((field) serviceWidget.fields.includes(field)); const requestExternalIPv6Prefix [externalIPv6Prefix].some((field) serviceWidget.fields.includes(field));随后通过Promise.all并行发起实际需要的请求proxy.jsawait Promise.all([ requestStatusInfo ? requestEndpoint(apiBaseUrl, WANIPConnection, GetStatusInfo) : null, requestLinkProperties ? requestEndpoint(apiBaseUrl, WANCommonInterfaceConfig, GetCommonLinkProperties) : null, requestAddonInfos ? requestEndpoint(apiBaseUrl, WANCommonInterfaceConfig, GetAddonInfos) : null, requestExternalIPAddress ? requestEndpoint(apiBaseUrl, WANIPConnection, GetExternalIPAddress) : null, requestExternalIPv6Address ? requestEndpoint(apiBaseUrl, WANIPConnection, X_AVM_DE_GetExternalIPv6Address) : null, requestExternalIPv6Prefix ? requestEndpoint(apiBaseUrl, WANIPConnection, X_AVM_DE_GetIPv6Prefix) : null, ])默认 4 个字段connectionStatus、uptime、maxDown、maxUp实际只触发 2 次 UPnP 请求GetStatusInfo与GetCommonLinkProperties效率较高。7.2 SOAP 信封与端点路径每个请求都是标准 UPnP SOAP 调用proxy.js。请求端点为{apiBaseUrl}/igdupnp/control/{servicePath}其中 servicePath 由 UPnP 服务类型决定const servicePath service WANIPConnection ? WANIPConn1 : WANCommonIFC1;即WANIPConnection服务 → 端点/igdupnp/control/WANIPConn1WANCommonInterfaceConfig服务 → 端点/igdupnp/control/WANCommonIFC1。SOAP 请求体示例GetStatusInfo?xml version1.0 encodingutf-8? s:Envelope s:encodingStylehttp://schemas.xmlsoap.org/soap/encoding/ xmlns:shttp://schemas.xmlsoap.org/soap/envelope/ s:Body u:GetStatusInfo xmlns:uurn:schemas-upnp-org:service:WANIPConnection:1 / /s:Body /s:Envelope同时通过SoapAction请求头声明动作urn:schemas-upnp-org:service:WANIPConnection:1#GetStatusInfo所有请求经由项目通用的 httpProxy 工具函数发出并复用代理层的超时、错误处理与 IPv6 禁用HOMEPAGE_PROXY_DISABLE_IPV6等能力。7.3 XML 响应解析与字段映射UPnP 返回的是 XML代理使用xml-js库将其转为 JSON 后逐元素提取proxy.jsconst 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 || ; });最后将各个响应对象的字段汇总为前端可消费的 JSONproxy.js。值得注意的两处细节IPv6 前缀拼接X_AVM_DE_GetIPv6Prefix返回前缀与长度两个字段代理将其拼为前缀/长度形式const ipv6Prefix externalIPv6Prefix?.NewIPv6Prefix; const ipv6Len externalIPv6Prefix?.NewPrefixLength; ... externalIPv6Prefix: ipv6Prefix ipv6Len ! null ? ${ipv6Prefix}/${ipv6Len} : (ipv6Prefix ?? null),字段命名统一FRITZ!Box 返回NewConnectionStatus、NewUptime、NewLayer1DownstreamMaxBitRate等New*前缀字段代理将其剥离为connectionStatus、uptime、maxDown等简洁键名与前端字段一一对应。7.4 配置读取链路代理通过getServiceWidget(group, service, index)来自 src/utils/config/service-helpers.js读取服务配置其底层由 src/utils/config/widget-helpers.js 从widgets.yaml加载并解析 YAML。若服务未找到或未配置url代理会分别返回Service widget not found与Service widget url not configured错误proxy.js。八、常见问题与排障Widget 报错Failed fetching GetStatusInfo通常是路由器端 UPnP 未开启或两项勾选不全。回到家庭网络 → 网络 → 网络设置核对Allow access for applications与Transmit status information over UPnP是否均已勾选并确认url指向的是 FRITZ!Box 本身网关地址而非其他设备。请求失败但 UPnP 已开启检查协议与端口组合http走 49000https走 49443。若配置了https而路由器未启用 HTTPS 管理请求会失败此时改用http即可文档也推荐这样做以获得更快响应。展示的字段不是预期的 4 个fields超出 4 项会被前端slice(0, 4)截断。请确认目标字段排在数组前 4 位且没有把externalIPv6Prefix等排在后位。数据为 0 或显示 Unconfigured当某个 UPnP Action 未配置字段而未被请求时对应值会以0或null兜底connectionStatus取不到时显示 Unconfigured。可先使用默认 4 字段验证基本连通性再逐步自定义fields。希望只显示特定数据善用fields按需声明例如只关注实时速率与公网 IPwidget: type: fritzbox url: http://192.168.178.1 fields: - down - up - received - sent九、小结Homepage 的 FRITZ!Box Widget 是一个典型的零配置凭据服务集成路由器端开启 UPnP、Homepage 端声明type: fritzbox与url即完成接入。其实现优雅地利用了 FRITZ!Box 标准 UPnP 接口WANIPConnection/WANCommonInterfaceConfig通过 SOAP 请求按需拉取数据、并行聚合再由前端完成单位换算与本地化展示并以最多 4 字段的强制约束保持仪表盘整洁。本文所涉配置示例可直接放入widgets.yaml使用相关源码与测试位于 src/widgets/fritzbox/ 目录可进一步深入研读。【免费下载链接】homepageA highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations.项目地址: https://gitcode.com/GitHub_Trending/ho/homepage创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考