네트워크 사용량 이력 차트 추가
This commit is contained in:
@@ -15,8 +15,8 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
|||||||
- 서울 HA의 휴대폰 device tracker로 외출/귀가를 감지해 상태 변화 시 1회만 팬 모드 자동 전환
|
- 서울 HA의 휴대폰 device tracker로 외출/귀가를 감지해 상태 변화 시 1회만 팬 모드 자동 전환
|
||||||
- 오버레이 설정 모달에서 보안 정책, 팬 자동곡선, 이벤트 판정, 팬 이상감지 파라미터 조정
|
- 오버레이 설정 모달에서 보안 정책, 팬 자동곡선, 이벤트 판정, 팬 이상감지 파라미터 조정
|
||||||
- 설정 모달에서 라즈베리파이 정품 7인치 DSI 터치 디스플레이 상태 확인, 밝기/백라이트 즉시 적용, 콘솔 회전, 터치 좌표 보정, LCD/터치 비활성화 부팅 설정 조정
|
- 설정 모달에서 라즈베리파이 정품 7인치 DSI 터치 디스플레이 상태 확인, 밝기/백라이트 즉시 적용, 콘솔 회전, 터치 좌표 보정, LCD/터치 비활성화 부팅 설정 조정
|
||||||
- CPU 온도, 팬 RPM 핵심 차트
|
- CPU 온도, RP1 온도, CPU 전력, 팬 RPM 센서 이력 차트
|
||||||
- RP1 온도, 팬 효율, CPU 전력 상세 차트는 접힘 영역에서 확인
|
- DHCP client 목록 아래에서 WAN 기준 다운로드/업로드 속도와 RX/TX 패킷 이력 차트 표시
|
||||||
- UPS/배터리 잔여 시간 기능은 장치 제거 후 비활성 상태로 두고, 배터리 센서 조회와 장기 학습 계산은 돌리지 않음
|
- UPS/배터리 잔여 시간 기능은 장치 제거 후 비활성 상태로 두고, 배터리 센서 조회와 장기 학습 계산은 돌리지 않음
|
||||||
- 라즈베리파이 저전압/스로틀링 현재 상태, 복구 중/반복 발생 판정, 부팅 후 이력, 최근 감지 시각, 지속시간, 최근 10분 통계 표시
|
- 라즈베리파이 저전압/스로틀링 현재 상태, 복구 중/반복 발생 판정, 부팅 후 이력, 최근 감지 시각, 지속시간, 최근 10분 통계 표시
|
||||||
- WiFi/LAN client 목록과 2.4G/5G/LAN client 수 표시
|
- WiFi/LAN client 목록과 2.4G/5G/LAN client 수 표시
|
||||||
|
|||||||
+23
-1
@@ -947,6 +947,11 @@ function os_info(): array
|
|||||||
function network_info(): array
|
function network_info(): array
|
||||||
{
|
{
|
||||||
$rows = [];
|
$rows = [];
|
||||||
|
$defaultInterface = '';
|
||||||
|
$route = trim((string)sh(['/usr/sbin/ip', 'route', 'show', 'default'], false, 3)['out']);
|
||||||
|
if (preg_match('/\bdev\s+([^\s]+)/', $route, $matches)) {
|
||||||
|
$defaultInterface = (string)$matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
foreach (glob('/sys/class/net/*') ?: [] as $dir) {
|
foreach (glob('/sys/class/net/*') ?: [] as $dir) {
|
||||||
$name = basename($dir);
|
$name = basename($dir);
|
||||||
@@ -957,22 +962,38 @@ function network_info(): array
|
|||||||
|
|
||||||
$rx = (int)first_readable([$dir . '/statistics/rx_bytes']);
|
$rx = (int)first_readable([$dir . '/statistics/rx_bytes']);
|
||||||
$tx = (int)first_readable([$dir . '/statistics/tx_bytes']);
|
$tx = (int)first_readable([$dir . '/statistics/tx_bytes']);
|
||||||
|
$rxPackets = (int)first_readable([$dir . '/statistics/rx_packets']);
|
||||||
|
$txPackets = (int)first_readable([$dir . '/statistics/tx_packets']);
|
||||||
|
$rxErrors = (int)first_readable([$dir . '/statistics/rx_errors']);
|
||||||
|
$txErrors = (int)first_readable([$dir . '/statistics/tx_errors']);
|
||||||
|
$rxDropped = (int)first_readable([$dir . '/statistics/rx_dropped']);
|
||||||
|
$txDropped = (int)first_readable([$dir . '/statistics/tx_dropped']);
|
||||||
|
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
|
'default' => $name === $defaultInterface,
|
||||||
'state' => first_readable([$dir . '/operstate']) ?: 'unknown',
|
'state' => first_readable([$dir . '/operstate']) ?: 'unknown',
|
||||||
'carrier' => first_readable([$dir . '/carrier']) === '1' ? 'up' : 'down',
|
'carrier' => first_readable([$dir . '/carrier']) === '1' ? 'up' : 'down',
|
||||||
'mac' => first_readable([$dir . '/address']) ?: 'N/A',
|
'mac' => first_readable([$dir . '/address']) ?: 'N/A',
|
||||||
'mtu' => first_readable([$dir . '/mtu']) ?: 'N/A',
|
'mtu' => first_readable([$dir . '/mtu']) ?: 'N/A',
|
||||||
'rx_bytes' => $rx,
|
'rx_bytes' => $rx,
|
||||||
'tx_bytes' => $tx,
|
'tx_bytes' => $tx,
|
||||||
|
'rx_packets' => $rxPackets,
|
||||||
|
'tx_packets' => $txPackets,
|
||||||
|
'rx_errors' => $rxErrors,
|
||||||
|
'tx_errors' => $txErrors,
|
||||||
|
'rx_dropped' => $rxDropped,
|
||||||
|
'tx_dropped' => $txDropped,
|
||||||
'rx_human' => bytes_human($rx),
|
'rx_human' => bytes_human($rx),
|
||||||
'tx_human' => bytes_human($tx),
|
'tx_human' => bytes_human($tx),
|
||||||
'ipv4' => trim(sh(['/usr/sbin/ip', '-4', '-o', 'addr', 'show', 'dev', $name], false, 3)['out']) ?: 'N/A',
|
'ipv4' => trim(sh(['/usr/sbin/ip', '-4', '-o', 'addr', 'show', 'dev', $name], false, 3)['out']) ?: 'N/A',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $rows;
|
return [
|
||||||
|
'default_interface' => $defaultInterface,
|
||||||
|
'interfaces' => $rows,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function latest_sensor(): array
|
function latest_sensor(): array
|
||||||
@@ -4381,6 +4402,7 @@ function collect_snapshot(bool $applyFan = true): array
|
|||||||
'battery' => $battery,
|
'battery' => $battery,
|
||||||
|
|
||||||
'wifi' => wifi_data(),
|
'wifi' => wifi_data(),
|
||||||
|
'network' => network_info(),
|
||||||
'history' => $history,
|
'history' => $history,
|
||||||
'processes' => $processes,
|
'processes' => $processes,
|
||||||
'custom_services' => custom_systemd_services(),
|
'custom_services' => custom_systemd_services(),
|
||||||
|
|||||||
@@ -105,6 +105,8 @@
|
|||||||
settingsOpen: false,
|
settingsOpen: false,
|
||||||
settingsDirty: false,
|
settingsDirty: false,
|
||||||
customServiceScrollHoldUntil: 0,
|
customServiceScrollHoldUntil: 0,
|
||||||
|
networkLast: null,
|
||||||
|
networkHistory: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const storageKeys = {
|
const storageKeys = {
|
||||||
@@ -160,6 +162,11 @@
|
|||||||
temperature: 'CPU Temperature',
|
temperature: 'CPU Temperature',
|
||||||
rp1Temp: 'RP1 Temp',
|
rp1Temp: 'RP1 Temp',
|
||||||
cpuWatt: 'CPU Watt',
|
cpuWatt: 'CPU Watt',
|
||||||
|
networkHistory: 'Network History',
|
||||||
|
downloadPerSecond: 'Download / s',
|
||||||
|
uploadPerSecond: 'Upload / s',
|
||||||
|
rxPacketsPerSecond: 'RX Packets / s',
|
||||||
|
txPacketsPerSecond: 'TX Packets / s',
|
||||||
remaining: 'Remaining',
|
remaining: 'Remaining',
|
||||||
batteryVoltage: 'Battery Voltage',
|
batteryVoltage: 'Battery Voltage',
|
||||||
wifiClients: 'DHCP Clients',
|
wifiClients: 'DHCP Clients',
|
||||||
@@ -355,6 +362,11 @@
|
|||||||
temperature: 'CPU 온도',
|
temperature: 'CPU 온도',
|
||||||
rp1Temp: 'RP1 온도',
|
rp1Temp: 'RP1 온도',
|
||||||
cpuWatt: 'CPU 전력',
|
cpuWatt: 'CPU 전력',
|
||||||
|
networkHistory: '네트워크 이력',
|
||||||
|
downloadPerSecond: '초당 다운로드 속도',
|
||||||
|
uploadPerSecond: '초당 업로드 속도',
|
||||||
|
rxPacketsPerSecond: '초당 RX 패킷',
|
||||||
|
txPacketsPerSecond: '초당 TX 패킷',
|
||||||
remaining: '잔여 시간',
|
remaining: '잔여 시간',
|
||||||
batteryVoltage: '배터리 전압',
|
batteryVoltage: '배터리 전압',
|
||||||
wifiClients: 'DHCP 클라이언트',
|
wifiClients: 'DHCP 클라이언트',
|
||||||
@@ -1823,6 +1835,74 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function primaryNetworkInterface(network) {
|
||||||
|
const rows = Array.isArray(network?.interfaces) ? network.interfaces : [];
|
||||||
|
const preferred = String(network?.default_interface || 'eth0');
|
||||||
|
return rows.find(row => row.name === preferred)
|
||||||
|
|| rows.find(row => row.name === 'eth0')
|
||||||
|
|| rows.find(row => row.carrier === 'up' && row.name !== 'br0')
|
||||||
|
|| rows[0]
|
||||||
|
|| null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function networkCounter(row, key) {
|
||||||
|
const value = Number(row?.[key]);
|
||||||
|
return Number.isFinite(value) && value >= 0 ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushNetworkSample(data) {
|
||||||
|
const iface = primaryNetworkInterface(data.network);
|
||||||
|
if (!iface) return;
|
||||||
|
|
||||||
|
const nowMs = Number(data.time_ms || Date.now());
|
||||||
|
const sample = {
|
||||||
|
iface: String(iface.name || ''),
|
||||||
|
time_ms: nowMs,
|
||||||
|
rx_bytes: networkCounter(iface, 'rx_bytes'),
|
||||||
|
tx_bytes: networkCounter(iface, 'tx_bytes'),
|
||||||
|
rx_packets: networkCounter(iface, 'rx_packets'),
|
||||||
|
tx_packets: networkCounter(iface, 'tx_packets'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!sample.iface || sample.rx_bytes === null || sample.tx_bytes === null || sample.rx_packets === null || sample.tx_packets === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previous = state.networkLast;
|
||||||
|
state.networkLast = sample;
|
||||||
|
|
||||||
|
if (!previous || previous.iface !== sample.iface || sample.time_ms <= previous.time_ms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seconds = (sample.time_ms - previous.time_ms) / 1000;
|
||||||
|
if (seconds <= 0 || seconds > 30) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = key => sample[key] - previous[key];
|
||||||
|
const rxBytes = delta('rx_bytes');
|
||||||
|
const txBytes = delta('tx_bytes');
|
||||||
|
const rxPackets = delta('rx_packets');
|
||||||
|
const txPackets = delta('tx_packets');
|
||||||
|
|
||||||
|
if (rxBytes < 0 || txBytes < 0 || rxPackets < 0 || txPackets < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.networkHistory.push({
|
||||||
|
time: String(data.generated_at || '').replace('T', ' ') || new Date(sample.time_ms).toISOString().slice(0, 19).replace('T', ' '),
|
||||||
|
rx_mbps: (rxBytes * 8) / seconds / 1000000,
|
||||||
|
tx_mbps: (txBytes * 8) / seconds / 1000000,
|
||||||
|
rx_pps: rxPackets / seconds,
|
||||||
|
tx_pps: txPackets / seconds,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (state.networkHistory.length > 180) {
|
||||||
|
state.networkHistory.splice(0, state.networkHistory.length - 180);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderSystemStatus(data) {
|
function renderSystemStatus(data) {
|
||||||
const system = data.system || {};
|
const system = data.system || {};
|
||||||
const disk = system.disk || {};
|
const disk = system.disk || {};
|
||||||
@@ -2476,6 +2556,12 @@
|
|||||||
if (document.getElementById('batteryVoltageChart')) {
|
if (document.getElementById('batteryVoltageChart')) {
|
||||||
chart('batteryVoltageChart', 'BATTERYV', rows, 'battery_voltage', '#14b8a6', 'V', dynamicScale(rows, 'battery_voltage', { minSpan: 0.2, padding: 0.06 }));
|
chart('batteryVoltageChart', 'BATTERYV', rows, 'battery_voltage', '#14b8a6', 'V', dynamicScale(rows, 'battery_voltage', { minSpan: 0.2, padding: 0.06 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const networkRows = state.networkHistory.slice(-180);
|
||||||
|
chart('netRxChart', 'NETRX', networkRows, 'rx_mbps', '#0ea5e9', ' Mbit/s', dynamicScale(networkRows, 'rx_mbps', { minSpan: 1, padding: 0.3 }));
|
||||||
|
chart('netTxChart', 'NETTX', networkRows, 'tx_mbps', '#22c55e', ' Mbit/s', dynamicScale(networkRows, 'tx_mbps', { minSpan: 1, padding: 0.3 }));
|
||||||
|
chart('netRxPacketsChart', 'NETRXPKT', networkRows, 'rx_pps', '#f97316', ' pps', dynamicScale(networkRows, 'rx_pps', { minSpan: 10, padding: 5 }));
|
||||||
|
chart('netTxPacketsChart', 'NETTXPKT', networkRows, 'tx_pps', '#a855f7', ' pps', dynamicScale(networkRows, 'tx_pps', { minSpan: 10, padding: 5 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function resizeChartsSoon() {
|
function resizeChartsSoon() {
|
||||||
@@ -2496,6 +2582,7 @@
|
|||||||
renderSettings(data.settings);
|
renderSettings(data.settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pushNetworkSample(data);
|
||||||
renderTop(data);
|
renderTop(data);
|
||||||
renderSystemStatus(data);
|
renderSystemStatus(data);
|
||||||
renderWifi(data);
|
renderWifi(data);
|
||||||
|
|||||||
@@ -284,6 +284,16 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2 data-i18n="networkHistory">Network History</h2>
|
||||||
|
<div class="chart-grid">
|
||||||
|
<div class="chart-box"><h3 data-i18n="downloadPerSecond">Download / s</h3><div class="chart-canvas"><canvas id="netRxChart"></canvas></div></div>
|
||||||
|
<div class="chart-box"><h3 data-i18n="uploadPerSecond">Upload / s</h3><div class="chart-canvas"><canvas id="netTxChart"></canvas></div></div>
|
||||||
|
<div class="chart-box"><h3 data-i18n="rxPacketsPerSecond">RX Packets / s</h3><div class="chart-canvas"><canvas id="netRxPacketsChart"></canvas></div></div>
|
||||||
|
<div class="chart-box"><h3 data-i18n="txPacketsPerSecond">TX Packets / s</h3><div class="chart-canvas"><canvas id="netTxPacketsChart"></canvas></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user