diff --git a/DEPLOYMENT-v1.2.0-2026-10-14.md b/DEPLOYMENT-v1.2.0-2026-10-14.md new file mode 100644 index 0000000..b0029cc --- /dev/null +++ b/DEPLOYMENT-v1.2.0-2026-10-14.md @@ -0,0 +1,5 @@ +# 树莓派接入插件 v1.2.0 + +2026-10-14 已部署。主网络卡片主文字改为“有线网络 · 链路速率”;接口/IP/状态与上下行速率独立两行,与群晖 v1.4.0 一致。Wi-Fi 信号与告警语义保留。Agent v3.8.0 提供 rx_bytes_per_second/tx_bytes_per_second,映射到标准历史指标;旧 Agent 缺少数值时不伪造速率,非数值、布尔和非有限值不写入历史。两项指标回归测试通过,核心全页三尺寸、中英文主题浏览器回归通过;真实生产快照额外验证两处网络卡片三尺寸与两语言,桌面截图已查看。 + +生产实际插件 1.2.0、Agent 3.8.0,两设备在线,树莓派上下行采集与标准历史指标通过认证 API 核验,临时会话删除。停服一致性备份 `/volume1/homes/way/pigway-console-backup-network-align-20261014/data.tar.gz`,目录 0700;旧插件在数据卷 adapter-backups/network-align-20261014。Console 镜像保持原版。 diff --git a/README.md b/README.md index 9b7b283..5f10207 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,5 @@ This project targets Raspberry Pi. Other Linux platforms are not covered by its 将现有 Agent API 的内存/磁盘字节数、开机时长与链路速度补充到标准 metrics,Console v2.1.0 可直接统一记录历史。不需要更新或新增树莓派进程。现有设备端没有上传下载流量计数,因此本版没有实际流量曲线;链路速度仅代表协商速率。 v1.1.0 已与 Console v2.1.0 部署,真实树莓派在线并开始记录历史;设备端 Agent 保持原版本。[部署验收记录](https://tea.pigway.com/way/pigway-device-console/src/branch/main/docs/DEPLOYMENT-v2.1.0-2026-10-11.md)。 + +生产已升级至 v1.2.0,网络卡片与上下行历史指标见 [部署记录](DEPLOYMENT-v1.2.0-2026-10-14.md)。 diff --git a/plugin/manifest.json b/plugin/manifest.json index 3cb84f3..ad9c71c 100644 --- a/plugin/manifest.json +++ b/plugin/manifest.json @@ -1,7 +1,7 @@ { "id": "pigway-agent", "name": "PIGWay Raspberry Pi Agent", - "version": "1.1.0", + "version": "1.2.0", "protocol_version": 1, "entrypoint": "worker.py", "runtime": "python3", diff --git a/plugin/ui/overview.js b/plugin/ui/overview.js index 4314ee3..afe5182 100644 --- a/plugin/ui/overview.js +++ b/plugin/ui/overview.js @@ -20,5 +20,5 @@ export function agentHostCard(entry) { memTotal = y.memory_total_bytes, diskUsed = y.disk_used_bytes, diskTotal = y.disk_total_bytes; - return `

${esc(m.name || h.name || m.id)}

${esc(formatUptime(s.uptime_seconds))}
ID ${esc(m.id || h.id)}${appState.LANG === "zh" ? "主机名" : "Hostname"} ${esc(m.hostname || v.hostname || "--")}${esc(v.manufacturer || "")} ${esc(v.model || "--")}${appState.LANG === "zh" ? "序列号" : "Serial"} ${esc(v.serial || "--")}${appState.LANG === "zh" ? "硬件版本" : "Revision"} ${esc(v.revision || "--")}${esc(v.os || "--")}${appState.LANG === "zh" ? "内核" : "Kernel"} ${esc(v.kernel || "--")}${esc(v.architecture || "--")} · CPU ${esc(v.cpu_count || "--")}${appState.LANG === "zh" ? "程序版本" : "Agent"} ${esc(s.version || "--")}
${tr("在线")}
${tr("处理器负载")}
${esc(y.cpu_percent)}%
${meter(y.cpu_percent)}
${appState.LANG === "zh" ? "频率" : "Frequency"} ${esc(y.cpu_freq_mhz)} MHz · ${esc(y.temperature_c)}°C${fanMetric(s.plugins || {})}
${tr("内存占用")}
${esc(y.memory_percent)}%
${meter(y.memory_percent)}
${bytes(memUsed)} / ${bytes(memTotal)} · ${appState.LANG === "zh" ? "可用" : "Available"} ${bytes(y.memory_available_bytes)}
${tr("磁盘占用")}
${esc(y.disk_percent)}%
${meter(y.disk_percent)}
${bytes(diskUsed)} / ${bytes(diskTotal)} · ${appState.LANG === "zh" ? "剩余" : "Free"} ${bytes(y.disk_free_bytes)}
${tr("主网络")}
${n.kind === "WIF" && signal !== null && signal !== undefined ? `${esc(signal)}%` : esc(n.kind || "--")}
${n.kind === "WIF" ? meter(signal) : ""}
${esc(n.interface || "--")} · ${n.kind === "WIF" ? `${esc(n.metric)} dBm · ` : n.kind === "ETH" && n.link_speed_mbps ? `${esc(n.link_speed_mbps)} Mbps · ` : ""}${esc(n.ip_label)} ${esc(n.ip)} · ${n.internet ? (appState.LANG === "zh" ? "互联网正常" : "Internet online") : appState.LANG === "zh" ? "互联网断开" : "Internet down"}
${tr("显示状态")}
${esc(ownerName(d.owner))}
${appState.LANG === "zh" ? "告警状态独立于硬件插件" : "Alert state is independent of hardware plugins"}
${tr("当前告警")}
${(d.alerts || []).length}
${(d.alerts || []).map((a) => esc(ownerName(a.id))).join(" · ") || tr("无告警")}
${pluginSummary(s.plugins || {}, h.id)}

${tr("服务与进程监控")}

${(s.watches || []).map((w) => `
${esc(w.name)} · ${esc(w.target)}${tr(w.up ? "正常" : "停止")}
`).join("") || `
${tr("暂无数据")}
`}
`; + return `

${esc(m.name || h.name || m.id)}

${esc(formatUptime(s.uptime_seconds))}
ID ${esc(m.id || h.id)}${appState.LANG === "zh" ? "主机名" : "Hostname"} ${esc(m.hostname || v.hostname || "--")}${esc(v.manufacturer || "")} ${esc(v.model || "--")}${appState.LANG === "zh" ? "序列号" : "Serial"} ${esc(v.serial || "--")}${appState.LANG === "zh" ? "硬件版本" : "Revision"} ${esc(v.revision || "--")}${esc(v.os || "--")}${appState.LANG === "zh" ? "内核" : "Kernel"} ${esc(v.kernel || "--")}${esc(v.architecture || "--")} · CPU ${esc(v.cpu_count || "--")}${appState.LANG === "zh" ? "程序版本" : "Agent"} ${esc(s.version || "--")}
${tr("在线")}
${tr("处理器负载")}
${esc(y.cpu_percent)}%
${meter(y.cpu_percent)}
${appState.LANG === "zh" ? "频率" : "Frequency"} ${esc(y.cpu_freq_mhz)} MHz · ${esc(y.temperature_c)}°C${fanMetric(s.plugins || {})}
${tr("内存占用")}
${esc(y.memory_percent)}%
${meter(y.memory_percent)}
${bytes(memUsed)} / ${bytes(memTotal)} · ${appState.LANG === "zh" ? "可用" : "Available"} ${bytes(y.memory_available_bytes)}
${tr("磁盘占用")}
${esc(y.disk_percent)}%
${meter(y.disk_percent)}
${bytes(diskUsed)} / ${bytes(diskTotal)} · ${appState.LANG === "zh" ? "剩余" : "Free"} ${bytes(y.disk_free_bytes)}
${tr("主网络")}
${n.kind === "ETH" ? `${appState.LANG === "zh" ? "有线网络" : "Wired network"}${n.link_speed_mbps ? ` · ${esc(n.link_speed_mbps)} Mbps` : ""}` : n.kind === "WIF" && signal !== null && signal !== undefined ? `${esc(signal)}%` : esc(n.kind || "--")}
${n.kind === "WIF" ? meter(signal) : ""}
${esc(n.interface || "--")} · ${n.kind === "WIF" ? `${esc(n.metric)} dBm · ` : ""}${esc(n.ip_label)} ${esc(n.ip)} · ${n.internet ? (appState.LANG === "zh" ? "互联网正常" : "Internet online") : appState.LANG === "zh" ? "互联网断开" : "Internet down"}
↓ ${bytes(n.rx_bytes_per_second)}/s · ↑ ${bytes(n.tx_bytes_per_second)}/s
${tr("显示状态")}
${esc(ownerName(d.owner))}
${appState.LANG === "zh" ? "告警状态独立于硬件插件" : "Alert state is independent of hardware plugins"}
${tr("当前告警")}
${(d.alerts || []).length}
${(d.alerts || []).map((a) => esc(ownerName(a.id))).join(" · ") || tr("无告警")}
${pluginSummary(s.plugins || {}, h.id)}

${tr("服务与进程监控")}

${(s.watches || []).map((w) => `
${esc(w.name)} · ${esc(w.target)}${tr(w.up ? "正常" : "停止")}
`).join("") || `
${tr("暂无数据")}
`}
`; } diff --git a/plugin/worker.py b/plugin/worker.py index 2f1d195..dd6a788 100644 --- a/plugin/worker.py +++ b/plugin/worker.py @@ -2,6 +2,7 @@ """Official Raspberry Pi adapter. This process alone knows Agent endpoints/authentication.""" import concurrent.futures import json +import math import sys import threading from pathlib import Path @@ -74,6 +75,11 @@ def collect(device): speed = value.get('network', {}).get('link_speed_mbps') if isinstance(speed, (int, float)) and not isinstance(speed, bool): metrics.append({'resource_id': 'host', 'key': 'network_link_speed_mbps', 'value': speed, 'unit': 'megabits_per_second'}) + for field, key in (('rx_bytes_per_second', 'network_rx_bytes_per_second'), + ('tx_bytes_per_second', 'network_tx_bytes_per_second')): + rate = value.get('network', {}).get(field) + if isinstance(rate, (int, float)) and not isinstance(rate, bool) and math.isfinite(rate) and rate >= 0: + metrics.append({'resource_id': 'host', 'key': key, 'value': rate, 'unit': 'bytes_per_second'}) return {'schema_version': 1, 'observed_at': value.get('timestamp'), 'identity': {'reported_name': value['machine'].get('name'), 'category': 'host', 'details': value.get('device', {})}, diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 6f756a0..7347544 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -11,18 +11,19 @@ class MetricsTests(unittest.TestCase): value = {'timestamp': 123, 'machine': {'name':'Pi'}, 'system': { 'cpu_percent': 10, 'memory_total_bytes': 8000, 'memory_used_bytes': 4000, 'disk_total_bytes': 16000, 'disk_used_bytes': 5000}, - 'uptime_seconds': 1000, 'network': {'link_speed_mbps':1000}, + 'uptime_seconds': 1000, 'network': {'link_speed_mbps':1000,'rx_bytes_per_second':0,'tx_bytes_per_second':1234}, 'display': {'alerts': []}} with patch.object(worker,'request',return_value=value): result=worker.collect({}) values = {m['key']:m['value'] for m in result['metrics']} self.assertEqual(values['memory_used_bytes'],4000) self.assertEqual(values['uptime_seconds'],1000) self.assertEqual(values['network_link_speed_mbps'],1000) - self.assertNotIn('network_rx_bytes_per_second',values) + self.assertEqual(values['network_rx_bytes_per_second'],0) + self.assertEqual(values['network_tx_bytes_per_second'],1234) self.assertTrue(result['alerts_complete']) def test_absent_or_boolean_fields_not_invented(self): - value={'system': {'cpu_percent': True}, 'machine':{}, 'network': {'link_speed_mbps': None}} + value={'system': {'cpu_percent': True}, 'machine':{}, 'network': {'link_speed_mbps': None, 'rx_bytes_per_second':float('nan'), 'tx_bytes_per_second':True}} with patch.object(worker,'request',return_value=value): result=worker.collect({}) self.assertEqual(result['metrics'],[])