Track LAN client connection time
This commit is contained in:
@@ -71,7 +71,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
||||
- `/tmp/control-throttling-state.json`: `vcgencmd get_throttled` 기반 스로틀링 감지 상태, 최근 지속시간, 최근 episode 이력
|
||||
- `system_notice_state`: notice 활성 상태와 기준값
|
||||
- `system_notice_logs`: notice 발생 이력
|
||||
- `wifi_observed_sessions`: 5G WiFi client의 최초/마지막 감지 시간
|
||||
- `wifi_observed_sessions`: 5G WiFi/LAN client의 최초/마지막 감지 시간
|
||||
- `wifi_client_aliases`: MAC별 수동 WiFi 호스트명
|
||||
- `app_settings`: 설정 모달에서 저장한 Control 동작값 오버라이드
|
||||
- `/home/seo/secret/control.php`: 앱 비밀번호, DB 설정, 선택적 배터리 설정
|
||||
@@ -128,7 +128,7 @@ System Notice는 다음 조건으로 동작합니다.
|
||||
|
||||
## WiFi 관측 자동화
|
||||
|
||||
5G 외부 WiFi 모듈은 `iw station dump`에 `connected time`을 제공하지 않는 경우가 있어, 서버가 MAC별 최초 감지 시각을 DB에 저장하고 경과 시간을 계산합니다.
|
||||
5G 외부 WiFi 모듈과 LAN 클라이언트는 장치별 연결 시간이 직접 제공되지 않는 경우가 있어, 서버가 MAC별 최초 감지 시각을 DB에 저장하고 경과 시간을 계산합니다.
|
||||
|
||||
기존 대시보드 조회만으로도 보정은 가능하지만, 사용자가 접속하지 않은 시간에는 최초 감지가 늦어질 수 있습니다. 이를 막기 위해 다음 systemd timer가 10초마다 관측 CLI를 실행합니다.
|
||||
|
||||
@@ -143,7 +143,7 @@ control-wifi-observe.service
|
||||
/usr/bin/php /var/www/control/bin/wifi_observe.php
|
||||
```
|
||||
|
||||
이 작업은 팬 정책, 차트 로그 저장, System Notice 기록과 분리되어 있으며 WiFi client 목록 확인과 `wifi_observed_sessions` 갱신만 수행합니다.
|
||||
이 작업은 팬 정책, 차트 로그 저장, System Notice 기록과 분리되어 있으며 WiFi/LAN client 목록 확인과 `wifi_observed_sessions` 갱신만 수행합니다.
|
||||
|
||||
호스트명은 dnsmasq lease 값을 우선 사용합니다. lease hostname이 `N/A`이면 MAC 주소 끝 6자리 기반 임시명 `기기-XXXXXX`을 표시하고, 표의 호스트명을 클릭해 저장한 수동 이름은 `wifi_client_aliases`에 보관해 이후에도 우선 표시합니다.
|
||||
|
||||
@@ -156,10 +156,10 @@ control-wifi-observe.service
|
||||
3. 대시보드는 `status` snapshot을 렌더링합니다.
|
||||
4. WebSocket 연결이 성공하면 `status` 메시지로 갱신하고, 실패하면 HTTP fallback을 사용합니다.
|
||||
5. 팬 조작은 상태 저장, 정책 적용, 로그 저장 순서로 처리합니다.
|
||||
6. WiFi client 목록은 `iw station dump`와 dnsmasq lease를 조합하고, LAN client는 `bridge fdb show br br0`의 USB LAN 포트 MAC을 dnsmasq lease와 매칭합니다. 5G 연결 시간이 `N/A`이면 MAC 기준 최초 감지 시간을 DB에 저장해 경과 시간을 계산합니다.
|
||||
6. WiFi client 목록은 `iw station dump`와 dnsmasq lease를 조합하고, LAN client는 `bridge fdb show br br0`의 USB LAN 포트 MAC을 dnsmasq lease와 매칭합니다. 5G 또는 LAN 연결 시간이 비어 있으면 MAC 기준 최초 감지 시간을 DB에 저장해 경과 시간을 계산합니다.
|
||||
7. WiFi hostname이 비어 있으면 MAC 기반 임시명을 표시하고, 수동 저장한 MAC별 이름은 DB에서 우선 적용합니다.
|
||||
8. WiFi 신호/송신 속도/수신 속도 값이 비어 있으면 실제로 존재하는 다른 무선 지표와 대역별 기준으로 값을 채웁니다.
|
||||
9. `control-wifi-observe.timer`는 사용자 접속과 무관하게 10초마다 5G 관측 세션을 갱신합니다.
|
||||
9. `control-wifi-observe.timer`는 사용자 접속과 무관하게 10초마다 5G/LAN 관측 세션을 갱신합니다.
|
||||
10. WakeLock 버튼은 활성 상태를 초록색 버튼으로 표시합니다.
|
||||
11. Reboot 버튼은 설정된 확인 문구와 관리자 암호 재입력을 모두 통과한 뒤 서버 API로 재부팅을 요청합니다.
|
||||
12. 시스템 상태는 `vcgencmd get_throttled`를 읽어 현재 저전압/스로틀링 여부와 부팅 후 이력, 최근 감지 시각, 현재/최근 지속시간을 표시합니다.
|
||||
@@ -187,7 +187,7 @@ control-wifi-observe.service
|
||||
- `apply_fan_policy()`: 팬 목표값 계산과 적용
|
||||
- `fan_control_temp()`: CPU/RP1 혼합 팬 판단 온도 계산
|
||||
- `json_out()`: API JSON 응답 표준화
|
||||
- `apply_observed_wifi_connected_time()`: 5G WiFi 연결 시간이 없는 client에 서버 관측 경과 시간 적용
|
||||
- `apply_observed_wifi_connected_time()`: 5G WiFi와 LAN 연결 시간이 없는 client에 서버 관측 경과 시간 적용
|
||||
- `wifi_client_aliases()`, `save_wifi_client_alias()`: MAC별 WiFi 호스트명 조회와 저장
|
||||
- `apply_wifi_estimates()`: WiFi 신호/송신 속도/수신 속도 결측값 보정
|
||||
- `battery_status()`: `CONTROL_BATTERY_ENABLED`가 true일 때만 배터리 센서를 읽고, 현재 기본 구성에서는 제거 상태를 반환
|
||||
@@ -206,7 +206,7 @@ control-wifi-observe.service
|
||||
- `sync_current_battery_remaining_history()`: 잔여 시간 차트 마지막 지점을 현재 snapshot 잔여 시간과 동기화
|
||||
- `batteryRemainingTitle()`, `showBatteryTooltip()`, `updateBatteryTooltip()`: 잔여 시간 계산 근거 커스텀 팝오버 표시와 실시간 갱신
|
||||
- `setting_definitions()`, `settings_payload()`, `save_settings()`, `reset_settings()`: 설정 모달 항목 정의, 조회, 저장, 초기화
|
||||
- `bin/wifi_observe.php`: 화면 접속 없이 `wifi_data()`를 호출해 5G 관측 세션을 선제 갱신
|
||||
- `bin/wifi_observe.php`: 화면 접속 없이 `wifi_data()`를 호출해 5G/LAN 관측 세션을 선제 갱신
|
||||
- `assets/wakelock.js`: WakeLock 버튼 상태와 Screen Wake Lock API 제어
|
||||
- `customAlert()`, `customConfirm()`, `customPrompt()`: 대시보드 공통 확인/입력 dialog
|
||||
- `controlLang`, `controlTheme`: 재접속 후에도 유지되는 언어/테마 localStorage key
|
||||
@@ -238,7 +238,7 @@ control-wifi-observe.service
|
||||
- 2.4G 내장 WiFi는 `hostapd-24g.service` 개별 restart 직후 일부 IoT 단말이 WPA/EAPOL 재협상 루프에 들어갈 수 있으므로, 안정화된 상태에서는 개별 restart를 피하고 필요 시 전체 reboot 또는 채널 변경으로 재초기화합니다.
|
||||
- `hostapd-24g.service`, `hostapd-5g.service`는 종료 후 해당 `wlan` 인터페이스를 `br0`에서 떼도록 두어, 모듈이 빠졌다가 돌아온 뒤 hostapd가 다시 붙이는 흐름으로 정리합니다.
|
||||
- 5G WiFi 연결 시간은 외부 모듈이 값을 제공하지 않을 때 서버가 처음 감지한 시각 기준으로 계산합니다. 현재 5G 목록에서 MAC이 사라지면 관측 세션을 즉시 종료하므로 재연결 시 0부터 다시 누적됩니다.
|
||||
- `control-wifi-observe.timer`는 10초마다 `/var/www/control/bin/wifi_observe.php`를 실행해 사용자가 대시보드에 접속하지 않아도 5G 연결 시간 카운터를 시작하고 유지합니다.
|
||||
- `control-wifi-observe.timer`는 10초마다 `/var/www/control/bin/wifi_observe.php`를 실행해 사용자가 대시보드에 접속하지 않아도 5G/LAN 연결 시간 카운터를 시작하고 유지합니다.
|
||||
- WebSocket은 장기 실행 프로세스이므로 `public/api.php`, `config/config.php`, `bin/control_ws.php` 변경을 감지하면 15초 안에 종료되고 `control-websocket.service`가 새 프로세스로 재시작합니다.
|
||||
- WebSocket 장기 실행 중 DB 연결이 끊길 수 있으므로 reconnect 로그를 확인합니다.
|
||||
- SmartThings 재로드 자동화는 서울 전력분전반과 목포 누전차단기 전력 센서의 값이 10초 이상 변하지 않으면 통합을 재로드합니다.
|
||||
|
||||
@@ -9,6 +9,7 @@ $data = wifi_data();
|
||||
$clients = is_array($data['clients'] ?? null) ? $data['clients'] : [];
|
||||
$missing5g = 0;
|
||||
$missing24 = 0;
|
||||
$missingLan = 0;
|
||||
|
||||
foreach ($clients as $client) {
|
||||
$band = (string)($client['band'] ?? '');
|
||||
@@ -20,6 +21,8 @@ foreach ($clients as $client) {
|
||||
$missing5g++;
|
||||
} elseif ($band === '2.4G') {
|
||||
$missing24++;
|
||||
} elseif ($band === 'LAN') {
|
||||
$missingLan++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +31,8 @@ echo json_encode([
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
'count24' => (int)($data['count24'] ?? 0),
|
||||
'count5' => (int)($data['count5'] ?? 0),
|
||||
'countLan' => (int)($data['countLan'] ?? 0),
|
||||
'missing_connected_time_24g' => $missing24,
|
||||
'missing_connected_time_5g' => $missing5g,
|
||||
'missing_connected_time_lan' => $missingLan,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||
|
||||
+30
-24
@@ -2739,7 +2739,7 @@ function lan_client_rows(array $leases, array $aliases, string $iface = 'eth1'):
|
||||
'tx_bitrate_source' => 'lan',
|
||||
'rx_bitrate_source' => 'lan',
|
||||
'connected_time' => 'N/A',
|
||||
'inactive_time' => 'N/A',
|
||||
'inactive_time' => '-',
|
||||
'rx_bytes' => 0,
|
||||
'tx_bytes' => 0,
|
||||
'rx_packets' => 0,
|
||||
@@ -2908,47 +2908,53 @@ function wifi_valid_mac(string $mac): bool
|
||||
return preg_match('/^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/', strtolower($mac)) === 1;
|
||||
}
|
||||
|
||||
function prune_observed_wifi_sessions(PDO $pdo, array $visibleMacs): void
|
||||
function prune_observed_wifi_sessions(PDO $pdo, array $visibleMacsByBand): void
|
||||
{
|
||||
$visibleMacs = array_values(array_unique(array_filter(
|
||||
array_map(static fn($mac) => strtolower((string)$mac), $visibleMacs),
|
||||
static fn($mac) => wifi_valid_mac($mac)
|
||||
)));
|
||||
foreach (['5G', 'LAN'] as $band) {
|
||||
$visibleMacs = array_values(array_unique(array_filter(
|
||||
array_map(static fn($mac) => strtolower((string)$mac), $visibleMacsByBand[$band] ?? []),
|
||||
static fn($mac) => wifi_valid_mac($mac)
|
||||
)));
|
||||
|
||||
if ($visibleMacs === []) {
|
||||
$pdo->exec("DELETE FROM wifi_observed_sessions WHERE band = '5G'");
|
||||
return;
|
||||
if ($visibleMacs === []) {
|
||||
$stmt = $pdo->prepare("DELETE FROM wifi_observed_sessions WHERE band = ?");
|
||||
$stmt->execute([$band]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($visibleMacs), '?'));
|
||||
$stmt = $pdo->prepare("
|
||||
DELETE FROM wifi_observed_sessions
|
||||
WHERE band = ?
|
||||
AND mac NOT IN ($placeholders)
|
||||
");
|
||||
$stmt->execute(array_merge([$band], $visibleMacs));
|
||||
}
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($visibleMacs), '?'));
|
||||
$stmt = $pdo->prepare("
|
||||
DELETE FROM wifi_observed_sessions
|
||||
WHERE band = '5G'
|
||||
AND mac NOT IN ($placeholders)
|
||||
");
|
||||
$stmt->execute($visibleMacs);
|
||||
}
|
||||
|
||||
function observed_wifi_duration_seconds(array $clients): array
|
||||
{
|
||||
$targets = [];
|
||||
$visible5gMacs = [];
|
||||
$visibleMacsByBand = [
|
||||
'5G' => [],
|
||||
'LAN' => [],
|
||||
];
|
||||
|
||||
foreach ($clients as $client) {
|
||||
$band = (string)($client['band'] ?? '');
|
||||
$mac = strtolower((string)($client['mac'] ?? ''));
|
||||
|
||||
if ($band !== '5G' || !wifi_valid_mac($mac)) {
|
||||
if (!in_array($band, ['5G', 'LAN'], true) || !wifi_valid_mac($mac)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$visible5gMacs[] = $mac;
|
||||
$visibleMacsByBand[$band][] = $mac;
|
||||
|
||||
if (!wifi_connected_time_missing($client['connected_time'] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targets[$mac] = [
|
||||
$targets[$band . ':' . $mac] = [
|
||||
'mac' => $mac,
|
||||
'band' => $band,
|
||||
'iface' => (string)($client['iface'] ?? ''),
|
||||
@@ -2960,7 +2966,7 @@ function observed_wifi_duration_seconds(array $clients): array
|
||||
try {
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
prune_observed_wifi_sessions($pdo, $visible5gMacs);
|
||||
prune_observed_wifi_sessions($pdo, $visibleMacsByBand);
|
||||
|
||||
if ($targets === []) {
|
||||
$pdo->commit();
|
||||
@@ -2998,7 +3004,7 @@ function observed_wifi_duration_seconds(array $clients): array
|
||||
FROM wifi_observed_sessions
|
||||
WHERE mac IN ($placeholders)
|
||||
");
|
||||
$stmt->execute(array_keys($targets));
|
||||
$stmt->execute(array_values(array_unique(array_column($targets, 'mac'))));
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$pdo->commit();
|
||||
@@ -3029,7 +3035,7 @@ function apply_observed_wifi_connected_time(array $clients): array
|
||||
foreach ($clients as &$client) {
|
||||
$mac = strtolower((string)($client['mac'] ?? ''));
|
||||
|
||||
if (($client['band'] ?? '') !== '5G'
|
||||
if (!in_array((string)($client['band'] ?? ''), ['5G', 'LAN'], true)
|
||||
|| !wifi_connected_time_missing($client['connected_time'] ?? null)
|
||||
|| !array_key_exists($mac, $observedSeconds)
|
||||
) {
|
||||
|
||||
+19
-2
@@ -1346,6 +1346,23 @@
|
||||
return formatDhms(parseWifiDurationSeconds(value)) || value;
|
||||
}
|
||||
|
||||
function wifiCellValue(row, key) {
|
||||
const value = row?.[key];
|
||||
const text = String(value ?? '').trim();
|
||||
if (row?.band === 'LAN' && (text === '' || text.toUpperCase() === 'N/A')) {
|
||||
return '-';
|
||||
}
|
||||
return text || '-';
|
||||
}
|
||||
|
||||
function wifiConnectedCellValue(row) {
|
||||
if (row?.band === 'LAN' && wifiCellValue(row, 'connected_time') === '-') {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return wifiConnectedTime(row?.connected_time);
|
||||
}
|
||||
|
||||
function eventStateText(eventState, activeKey, normalKey, recoveringKey, repeatedKey, seenKey, notSeenKey) {
|
||||
if (!eventState || eventState.available === false) {
|
||||
return t('na');
|
||||
@@ -1415,8 +1432,8 @@
|
||||
${wifiMetricCell(row, 'signal')}
|
||||
${wifiMetricCell(row, 'tx_bitrate')}
|
||||
${wifiMetricCell(row, 'rx_bitrate')}
|
||||
${td(wifiConnectedTime(row.connected_time))}
|
||||
${td(row.inactive_time)}
|
||||
${td(wifiConnectedCellValue(row))}
|
||||
${td(wifiCellValue(row, 'inactive_time'))}
|
||||
</tr>
|
||||
`).join('')
|
||||
: `<tr><td colspan="9">${escapeHtml(t('noWifiClients'))}</td></tr>`;
|
||||
|
||||
Reference in New Issue
Block a user