Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93d5f0cdd7 | ||
|
|
d6df234154 | ||
|
|
d856b42e92 | ||
|
|
d3f79ebe8a | ||
|
|
f576114db1 | ||
|
|
6a66a48a51 |
@@ -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 镜像保持原版。
|
||||
@@ -0,0 +1,5 @@
|
||||
# 网络卡片 v1.2.1
|
||||
|
||||
2026-10-14 已部署。网络主文字改为下载/上传速率,删除 ETH/有线网络文字;下方保留 eth0、1000 Mbps、IP 和状态,不重复显示速率。两处网络卡片使用一致布局。模拟及真实设备快照三尺寸、中英文浏览器检查通过,无溢出,桌面截图已查看。生产认证 API 确认两设备在线、树莓派实时流量与历史标准指标有效。Agent 保持 v3.8.0,未再次升级。
|
||||
|
||||
停服一致性数据备份 /volume1/homes/way/pigway-console-backup-network-rates-layout-20261014/data.tar.gz,目录 0700;旧插件在数据卷 adapter-backups/network-rates-layout-20261014。Console 镜像及 NAS 每日存储采集保持。
|
||||
@@ -0,0 +1,5 @@
|
||||
# 树莓派接入插件 v1.2.2
|
||||
|
||||
2026-10-14 网络卡片改为按 network.link 显示已连接/已断开,字段缺失显示状态未确认,与 NAS 的接口连接语义一致。页面不再把 network.internet 用作接口状态文案。Agent 的既有互联网探测和告警逻辑不修改。两项后端测试及全页三尺寸中英文主题浏览器验证通过。部署使用停服一致性数据备份 /volume1/homes/way/pigway-console-backup-synology-vpi-link-v1.2.2-20261014/data.tar.gz,目录 0700;旧树莓派插件保存在数据卷 adapter-backups/vpi-link-v1.2.2/pigway-agent。
|
||||
|
||||
生产安装并启用 1.2.2,实际 UI 已使用 network.link 且不存在“互联网正常”文案;认证 API 两设备在线,实时流量和历史指标保留。验证会话删除。Tea ZIP 与 SHA256 附件下载校验一致。
|
||||
@@ -24,3 +24,11 @@ This project targets Raspberry Pi. Other Linux platforms are not covered by its
|
||||
## v1.1.0 历史指标
|
||||
|
||||
将现有 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)。
|
||||
|
||||
当前网络卡片以实时上下行速率为主文字,详见 [v1.2.1 部署记录](DEPLOYMENT-v1.2.1-2026-10-14.md)。
|
||||
|
||||
网络连接文案统一为接口链路状态,见 [v1.2.2](DEPLOYMENT-v1.2.2-2026-10-14.md)。
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
3cfd361f7663a9a9c93b507d17bb1c7e98003d61316826f0a4ba4a93594f72d4 .gitignore
|
||||
2a476aea1b4fcaf43ace0a7f297e4f5c3cd5014b79992bb5bedf84faa194071f README.md
|
||||
1f960195978886b318953a6dbc7f13a551d9bb2bab44aa88b1599c69c8a9ced5 README.md
|
||||
d081c599d821149853368b1a8637c57a16995344406ff5ec9d7619b66e0479b3 package.py
|
||||
cf40d1302a8398497e4d6724ec0a22b24420e32ed2697ba3f4d039d193cf38ea plugin/manifest.json
|
||||
e5c04001dbd8ed8bba614f7b341237fc6487a9a8fe2f28ccb6f59278725b0a69 plugin/ui/config.js
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "pigway-agent",
|
||||
"name": "PIGWay Raspberry Pi Agent",
|
||||
"version": "1.1.0",
|
||||
"version": "1.3.0",
|
||||
"protocol_version": 1,
|
||||
"entrypoint": "worker.py",
|
||||
"runtime": "python3",
|
||||
@@ -38,5 +38,8 @@
|
||||
"contract": "console-ui-v1",
|
||||
"entrypoint": "ui/index.js"
|
||||
},
|
||||
"legacy_connection_fields": true
|
||||
"legacy_connection_fields": true,
|
||||
"collection": {
|
||||
"interval_seconds": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@ export function agentHostCard(entry) {
|
||||
memTotal = y.memory_total_bytes,
|
||||
diskUsed = y.disk_used_bytes,
|
||||
diskTotal = y.disk_total_bytes;
|
||||
return `<article class="panel host-panel"><div class="host-head"><div><div class="host-title"><h2>${esc(m.name || h.name || m.id)}</h2><span class="uptime-badge">${esc(formatUptime(s.uptime_seconds))}</span></div><div class="device-details"><span>ID ${esc(m.id || h.id)}</span><span>${appState.LANG === "zh" ? "主机名" : "Hostname"} ${esc(m.hostname || v.hostname || "--")}</span><span>${esc(v.manufacturer || "")} ${esc(v.model || "--")}</span><span>${appState.LANG === "zh" ? "序列号" : "Serial"} ${esc(v.serial || "--")}</span><span>${appState.LANG === "zh" ? "硬件版本" : "Revision"} ${esc(v.revision || "--")}</span><span>${esc(v.os || "--")}</span><span>${appState.LANG === "zh" ? "内核" : "Kernel"} ${esc(v.kernel || "--")}</span><span>${esc(v.architecture || "--")} · CPU ${esc(v.cpu_count || "--")}</span><span>${appState.LANG === "zh" ? "程序版本" : "Agent"} ${esc(s.version || "--")}</span></div></div><span class="badge good">${tr("在线")}</span></div><div class="host-grid"><div class="metric"><div class="muted">${tr("处理器负载")}</div><div class="value">${esc(y.cpu_percent)}%</div>${meter(y.cpu_percent)}<div class="muted">${appState.LANG === "zh" ? "频率" : "Frequency"} ${esc(y.cpu_freq_mhz)} MHz · ${esc(y.temperature_c)}°C${fanMetric(s.plugins || {})}</div></div><div class="metric"><div class="muted">${tr("内存占用")}</div><div class="value">${esc(y.memory_percent)}%</div>${meter(y.memory_percent)}<div class="muted">${bytes(memUsed)} / ${bytes(memTotal)} · ${appState.LANG === "zh" ? "可用" : "Available"} ${bytes(y.memory_available_bytes)}</div></div><div class="metric"><div class="muted">${tr("磁盘占用")}</div><div class="value">${esc(y.disk_percent)}%</div>${meter(y.disk_percent)}<div class="muted">${bytes(diskUsed)} / ${bytes(diskTotal)} · ${appState.LANG === "zh" ? "剩余" : "Free"} ${bytes(y.disk_free_bytes)}</div></div><div class="metric"><div class="muted">${tr("主网络")}</div><div class="value">${n.kind === "WIF" && signal !== null && signal !== undefined ? `${esc(signal)}%` : esc(n.kind || "--")}</div>${n.kind === "WIF" ? meter(signal) : ""}<div class="muted">${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"}</div></div><div class="metric"><div class="muted">${tr("显示状态")}</div><div class="value">${esc(ownerName(d.owner))}</div><div class="muted">${appState.LANG === "zh" ? "告警状态独立于硬件插件" : "Alert state is independent of hardware plugins"}</div></div><div class="metric"><div class="muted">${tr("当前告警")}</div><div class="value">${(d.alerts || []).length}</div><div class="muted">${(d.alerts || []).map((a) => esc(ownerName(a.id))).join(" · ") || tr("无告警")}</div></div></div>${pluginSummary(s.plugins || {}, h.id)}<div class="group"><h3>${tr("服务与进程监控")}</h3><div class="rows">${(s.watches || []).map((w) => `<div class="row"><span>${esc(w.name)} · ${esc(w.target)}</span><span class="badge ${w.up ? "good" : "bad"}">${tr(w.up ? "正常" : "停止")}</span></div>`).join("") || `<div class="muted">${tr("暂无数据")}</div>`}</div></div></article>`;
|
||||
return `<article class="panel host-panel"><div class="host-head"><div><div class="host-title"><h2>${esc(m.name || h.name || m.id)}</h2><span class="uptime-badge">${esc(formatUptime(s.uptime_seconds))}</span></div><div class="device-details"><span>ID ${esc(m.id || h.id)}</span><span>${appState.LANG === "zh" ? "主机名" : "Hostname"} ${esc(m.hostname || v.hostname || "--")}</span><span>${esc(v.manufacturer || "")} ${esc(v.model || "--")}</span><span>${appState.LANG === "zh" ? "序列号" : "Serial"} ${esc(v.serial || "--")}</span><span>${appState.LANG === "zh" ? "硬件版本" : "Revision"} ${esc(v.revision || "--")}</span><span>${esc(v.os || "--")}</span><span>${appState.LANG === "zh" ? "内核" : "Kernel"} ${esc(v.kernel || "--")}</span><span>${esc(v.architecture || "--")} · CPU ${esc(v.cpu_count || "--")}</span><span>${appState.LANG === "zh" ? "程序版本" : "Agent"} ${esc(s.version || "--")}</span></div></div><span class="badge good">${tr("在线")}</span></div><div class="host-grid"><div class="metric"><div class="muted">${tr("处理器负载")}</div><div class="value">${esc(y.cpu_percent)}%</div>${meter(y.cpu_percent)}<div class="muted">${appState.LANG === "zh" ? "频率" : "Frequency"} ${esc(y.cpu_freq_mhz)} MHz · ${esc(y.temperature_c)}°C${fanMetric(s.plugins || {})}</div></div><div class="metric"><div class="muted">${tr("内存占用")}</div><div class="value">${esc(y.memory_percent)}%</div>${meter(y.memory_percent)}<div class="muted">${bytes(memUsed)} / ${bytes(memTotal)} · ${appState.LANG === "zh" ? "可用" : "Available"} ${bytes(y.memory_available_bytes)}</div></div><div class="metric"><div class="muted">${tr("磁盘占用")}</div><div class="value">${esc(y.disk_percent)}%</div>${meter(y.disk_percent)}<div class="muted">${bytes(diskUsed)} / ${bytes(diskTotal)} · ${appState.LANG === "zh" ? "剩余" : "Free"} ${bytes(y.disk_free_bytes)}</div></div><div class="metric"><div class="muted">${tr("主网络")}</div><div class="value">↓ ${bytes(n.rx_bytes_per_second)}/s · ↑ ${bytes(n.tx_bytes_per_second)}/s</div><div class="muted">${esc(n.interface || "--")} · ${n.kind === "WIF" ? `${esc(n.metric)} dBm · ` : n.link_speed_mbps ? `${esc(n.link_speed_mbps)} Mbps · ` : ""}${esc(n.ip_label)} ${esc(n.ip)} · ${n.link === true ? (appState.LANG === "zh" ? "已连接" : "Connected") : n.link === false ? (appState.LANG === "zh" ? "已断开" : "Disconnected") : (appState.LANG === "zh" ? "状态未确认" : "Unconfirmed")}</div></div><div class="metric"><div class="muted">${tr("显示状态")}</div><div class="value">${esc(ownerName(d.owner))}</div><div class="muted">${appState.LANG === "zh" ? "告警状态独立于硬件插件" : "Alert state is independent of hardware plugins"}</div></div><div class="metric"><div class="muted">${tr("当前告警")}</div><div class="value">${(d.alerts || []).length}</div><div class="muted">${(d.alerts || []).map((a) => esc(ownerName(a.id))).join(" · ") || tr("无告警")}</div></div></div>${pluginSummary(s.plugins || {}, h.id)}<div class="group"><h3>${tr("服务与进程监控")}</h3><div class="rows">${(s.watches || []).map((w) => `<div class="row"><span>${esc(w.name)} · ${esc(w.target)}</span><span class="badge ${w.up ? "good" : "bad"}">${tr(w.up ? "正常" : "停止")}</span></div>`).join("") || `<div class="muted">${tr("暂无数据")}</div>`}</div></div></article>`;
|
||||
}
|
||||
|
||||
@@ -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,9 +75,20 @@ 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'})
|
||||
for metric in metrics:
|
||||
source = value.get('network', {}) if metric['key'].startswith('network_') else value.get('system', {})
|
||||
at = source.get('observed_at') if metric['key'] != 'uptime_seconds' else value.get('timestamp')
|
||||
if isinstance(at, (int, float)) and not isinstance(at, bool) and math.isfinite(at) and at > 0:
|
||||
metric['observed_at'] = at
|
||||
return {'schema_version': 1, 'observed_at': value.get('timestamp'),
|
||||
'identity': {'reported_name': value['machine'].get('name'), 'category': 'host',
|
||||
'details': value.get('device', {})},
|
||||
'collection': value.get('collection', MANIFEST['collection']),
|
||||
'capabilities': MANIFEST['capabilities'], 'metrics': metrics,
|
||||
'resources': [{'id': 'host', 'type': 'host'}],
|
||||
'alerts': value.get('display', {}).get('alerts', []),
|
||||
|
||||
@@ -11,18 +11,21 @@ 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': {'observed_at':120,'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.assertEqual(next(m['observed_at'] for m in result['metrics'] if m['key']=='network_tx_bytes_per_second'),120)
|
||||
self.assertEqual(result['collection']['interval_seconds'],1)
|
||||
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'],[])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user