UPS 제거 상태에 맞춘 배터리 기능 정리
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
## 프로젝트 성격
|
||||
|
||||
Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 웹에서 관리하기 위한 내부 운영 도구입니다. 로그인 후 대시보드에서 온도, 팬 RPM, PWM, WiFi client, 배터리, notice를 확인하고 필요한 제어 명령을 실행합니다. 첫 화면은 운영 판단에 필요한 핵심 정보 위주로 구성하고, 상세 차트와 진단성 데이터는 접힘 영역에 둡니다.
|
||||
Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 웹에서 관리하기 위한 내부 운영 도구입니다. 로그인 후 대시보드에서 온도, 팬 RPM, PWM, WiFi client, 저전압/스로틀링, notice를 확인하고 필요한 제어 명령을 실행합니다. 첫 화면은 운영 판단에 필요한 핵심 정보 위주로 구성하고, 상세 차트와 진단성 데이터는 접힘 영역에 둡니다.
|
||||
|
||||
상태 갱신은 WebSocket을 우선 사용하고, 연결 실패 시 HTTP fallback으로 전환합니다. 팬 정책 적용과 센서 수집은 백그라운드 작업과 API 호출을 통해 DB에 기록됩니다.
|
||||
|
||||
@@ -12,10 +12,10 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
||||
|
||||
- 팬 모드 `auto`, `manual`, `off` 제어
|
||||
- PWM slider 기반 수동 팬 제어
|
||||
- 오버레이 설정 모달에서 보안 정책, 팬 자동곡선, 이벤트 판정, 팬 이상감지, 배터리 예측 파라미터 조정
|
||||
- CPU 온도, 팬 RPM, 배터리 SOC, 배터리 전압 핵심 차트
|
||||
- RP1 온도, 팬 효율, CPU 전력, 잔여 시간 상세 차트는 접힘 영역에서 확인
|
||||
- 배터리 잔여 시간은 최근 24시간 SOC 방전 추세와 최대 45일 장기 학습 프로파일을 함께 사용하고, CPU 전력 변화로 현재 부하를 보정
|
||||
- 오버레이 설정 모달에서 보안 정책, 팬 자동곡선, 이벤트 판정, 팬 이상감지 파라미터 조정
|
||||
- CPU 온도, 팬 RPM 핵심 차트
|
||||
- RP1 온도, 팬 효율, CPU 전력 상세 차트는 접힘 영역에서 확인
|
||||
- UPS/배터리 잔여 시간 기능은 장치 제거 후 비활성 상태로 두고, 배터리 센서 조회와 장기 학습 계산은 돌리지 않음
|
||||
- 라즈베리파이 저전압/스로틀링 현재 상태, 복구 중/반복 발생 판정, 부팅 후 이력, 최근 감지 시각, 지속시간, 최근 10분 통계 표시
|
||||
- WiFi client 목록과 2.4G/5G client 수 표시
|
||||
- 5G 외부 WiFi 모듈이 연결 시간을 제공하지 않는 경우 서버 관측 기반 연결 시간 보정
|
||||
@@ -61,7 +61,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
||||
## 데이터/저장소
|
||||
|
||||
- `control_state`: 팬 모드와 PWM 상태
|
||||
- `sensor_logs`: 온도, RPM, PWM, 배터리, load, memory, disk, uptime
|
||||
- `sensor_logs`: 온도, RPM, PWM, load, memory, disk, uptime. 과거 배터리 컬럼은 호환을 위해 유지하지만 UPS 제거 상태에서는 새 배터리값을 기록하지 않습니다.
|
||||
- `/tmp/control-low-voltage-state.json`: `vcgencmd get_throttled` 기반 저전압 감지 상태, 최근 지속시간, 최근 episode 이력
|
||||
- `/tmp/control-throttling-state.json`: `vcgencmd get_throttled` 기반 스로틀링 감지 상태, 최근 지속시간, 최근 episode 이력
|
||||
- `system_notice_state`: notice 활성 상태와 기준값
|
||||
@@ -69,38 +69,16 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
||||
- `wifi_observed_sessions`: 5G WiFi client의 최초/마지막 감지 시간
|
||||
- `wifi_client_aliases`: MAC별 수동 WiFi 호스트명
|
||||
- `app_settings`: 설정 모달에서 저장한 Control 동작값 오버라이드
|
||||
- `/home/seo/secret/control.php`: 앱 비밀번호, DB 설정, 배터리 설정
|
||||
- 배터리 용량 설정은 `/home/seo/secret/control.php`의 `battery.cell_capacity_mah`, `battery.parallel_cells`, `battery.nominal_voltage`, `battery.capacity_wh`를 사용합니다.
|
||||
- `/home/seo/secret/control.php`: 앱 비밀번호, DB 설정, 선택적 배터리 설정
|
||||
- UPS/배터리 기능은 `/home/seo/secret/control.php`의 `battery.enabled`가 명시적으로 true일 때만 동작합니다. 현재 운영 기준은 UPS 제거 상태이므로 기본값은 false입니다.
|
||||
|
||||
## 배터리 잔여 시간 계산
|
||||
## UPS/배터리 상태
|
||||
|
||||
잔여 시간은 단순히 현재 CPU 전력 평균만으로 계산하지 않습니다. 실제 배터리 SOC가 DB에서 어떤 속도로 떨어졌는지를 먼저 사용합니다.
|
||||
UPS가 과부하 상황에서 전압 하강과 강제 종료를 반복해 현재 운영 구성에서는 제거되어 있습니다. 이에 따라 Control은 기본적으로 MAX17048 배터리 센서를 읽지 않고, 배터리 SOC/전압 차트와 잔여 시간 UI도 표시하지 않습니다.
|
||||
|
||||
1. `sensor_logs`의 최근 24시간 배터리 기록을 1분 단위로 SQL 집계합니다.
|
||||
2. 30분, 1시간, 3시간, 6시간, 12시간, 24시간 창을 각각 검사합니다.
|
||||
3. 각 창의 시작/끝 SOC는 평균 한 점이 아니라 가장자리 구간의 중앙값으로 잡아 순간 튐을 줄입니다.
|
||||
4. 45분, 2시간, 4시간, 8시간, 16시간, 24시간 창은 가중 선형회귀로 SOC 기울기를 다시 계산합니다.
|
||||
5. 회귀 후보는 중앙절대편차 기준으로 잔차가 큰 SOC 튐을 제거한 뒤 한 번 더 회귀해 기울기를 보정합니다.
|
||||
6. 전압도 1분 집계 기록에서 가중 회귀로 기울기를 계산하고, 현재 전압에서 동적 하한 전압까지 남은 시간을 별도 후보로 만듭니다.
|
||||
7. SOC가 충분히 줄어든 방전 구간만 후보로 사용하고, 충전 또는 회복처럼 보이는 구간은 제외합니다.
|
||||
8. 최대 45일 기록은 약 5분 간격으로 샘플링해 SOC 구간별 장기 방전 프로파일과 SOC 1%당 실제 사용 Wh를 학습합니다.
|
||||
9. 장기 프로파일은 현재 SOC와 가까운 구간, 최근에 관측된 구간, 샘플 수가 많은 구간에 더 높은 가중치를 줍니다.
|
||||
10. 최근 CPU 전력이 각 후보 구간의 평균 전력보다 높거나 낮으면 방전 속도에 완만하게 반영해 현재 부하 변화를 보정합니다.
|
||||
11. 단기 중앙값 후보, 강건 회귀 후보, 전압 기울기 후보, 에너지 학습 후보, 용량 전력 후보, 장기 학습 후보를 신뢰도와 안정도 가중치로 합산합니다.
|
||||
12. 후보끼리 방전 속도가 크게 어긋나면 중앙값 기준으로 튄 후보를 제외하거나 가중치를 낮춥니다.
|
||||
13. 최종 신뢰도는 후보별 신뢰도 가중 평균에 후보 간 방전 속도 분산을 반영해 계산합니다.
|
||||
14. 용량 기반 후보와 fallback은 설정된 최소 시스템 전력보다 낮은 순간 전력값을 그대로 믿지 않고 최소 전력으로 끌어올려 계산합니다.
|
||||
15. 1s8p 18650 배터리팩에서 비현실적으로 긴 값이 나오지 않도록 `잔여시간 현실 상한`을 100% 기준 최대 시간으로 두고, 현재 SOC에 비례해 최종 후보를 제한합니다.
|
||||
16. 상한을 넘은 후보는 버리지 않고 상한값으로 눌러 반영하며, 신뢰도와 가중치를 낮춰 다른 실제 방전 후보를 더 우선하게 합니다.
|
||||
17. SOC 추세가 부족하면 마지막 대체값으로 배터리 용량 `BATTERY_CAPACITY_WH`와 최근 CPU 전력의 절사 평균을 사용합니다.
|
||||
API 응답의 `battery.enabled`는 false, `battery.removed`는 true로 내려가며 `battery.remaining.display`는 `UPS 제거됨`으로 표시됩니다. 과거에 저장된 `sensor_logs`의 배터리 컬럼은 DB 호환을 위해 남겨 두지만 새 snapshot에서는 배터리값을 추가하지 않습니다.
|
||||
|
||||
WebSocket 장기 실행 프로세스에서는 1분 단위 단기 집계 결과를 55초 동안, 약 5분 간격 장기 학습 샘플을 10분 동안 재사용해 DB 부담을 줄입니다. 장기 학습 샘플은 파일 캐시로도 보관해 다른 PHP 프로세스에서도 빠르게 재사용합니다.
|
||||
|
||||
대시보드의 잔여 시간 값에는 Control UI 스타일의 커스텀 계산 근거 팝오버가 붙습니다. 기본 브라우저 `title` tooltip은 사용하지 않으며, 마우스 hover 또는 키보드 focus 중에도 상태 갱신이 들어오면 팝오버 내용과 위치가 즉시 갱신됩니다. `SOC 다중 시간창 추세`로 표시되면 실제 DB 방전 속도 기반이며, 계산 출처에 `장기 학습`이 포함되면 누적 기록에서 학습한 SOC/부하 구간별 방전 경험도 반영된 상태입니다. `배터리 용량과 최근 전력 평균`이면 SOC 추세가 부족해 용량 기반 fallback이 사용된 상태입니다.
|
||||
|
||||
팝오버는 계산 잔여 시간, 현실 상한, 상한 전 계산값, 시간당 SOC 감소율, 추세 신뢰도, 후보 수, 후보 분산, 최근 CPU 전력, 계산 출처, 반영 구간을 표시합니다. 각 후보별로 최근 30분/1시간/3시간/6시간/12시간/24시간 감소율, 신뢰도, 부하 보정 배율을 표시하고, 강건 회귀 후보는 튐 제거 후 남은 유효점 비율과 후보 안정도를 함께 표시합니다. 전압 후보는 하한 전압과 시간당 전압 하강량을, 에너지 학습 후보는 SOC 1%당 실제 Wh를, 장기 학습 후보는 반영된 학습 구간 수와 SOC 구간 수를 함께 표시합니다.
|
||||
|
||||
잔여 시간 차트의 마지막 지점은 현재 snapshot의 `battery.remaining.seconds`와 동기화합니다. DB에 저장된 마지막 센서 기록이 몇 초 늦어도 좌측 상태 영역과 차트의 최신 잔여 시간이 서로 다른 값을 보여주지 않도록, API 응답 history 끝에 현재 snapshot row를 반영합니다.
|
||||
나중에 UPS 구성을 다시 붙일 경우 `/home/seo/secret/control.php`의 `battery.enabled`를 true로 두면 기존 배터리 측정/학습 코드가 다시 사용됩니다.
|
||||
|
||||
## 설정 모달
|
||||
|
||||
@@ -113,7 +91,7 @@ WebSocket 장기 실행 프로세스에서는 1분 단위 단기 집계 결과
|
||||
- 화면/진단 표시: 프로세스 후보 수, 사용자 서비스 로그 줄 수, 사용자 서비스 캐시 시간, 팬 이상 이력 수
|
||||
- 저전압/스로틀링: 최근 판정창, 복구 중 유지 시간, 반복 발생 기준 횟수
|
||||
- 팬 이상감지: 기준 표본 수, 최신 표본 제외 수, RPM/온도 이상 감지 차이, RPM/온도 복구 차이, RPM 감시 시작 PWM, alert 유지 중 반복 기록, 반복 기록 간격, 하강 변화 원인 후보 표시 여부
|
||||
- 배터리 예측: 단기/장기 학습 범위, 후보 필터 비율, 부하 보정 강도, 전압 하한, 용량 모델 가중치, 잔여시간 현실 상한, 최소 시스템 전력, 상한 초과 후보 감쇠, 에너지/전압 모델 사용 여부, 상태/잔여시간 차트 표본 수
|
||||
- UPS/배터리 설정: 현재 UPS 제거 상태에서는 설정 모달에서 숨겨지고, `battery.enabled=true`일 때만 배터리 예측 항목을 노출합니다.
|
||||
|
||||
설정 모달이 열려 있는 동안 1초 상태 갱신은 폼 DOM을 다시 만들지 않습니다. 입력 중인 값은 저장, 기본값 초기화, 닫기 전까지 유지되며, 백그라운드 snapshot은 내부 설정 payload만 갱신합니다. 단순 boolean 설정은 `켜기`/`끄기` 세그먼트로 표시합니다.
|
||||
|
||||
@@ -123,7 +101,7 @@ WebSocket 장기 실행 프로세스에서는 1분 단위 단기 집계 결과
|
||||
- WebSocket 최초 연결: 즉시 1회 상태 전송
|
||||
- WebSocket 끊김 시 HTTP fallback 상태 갱신: 2초마다
|
||||
- 탭 복귀: 즉시 1회 상태 갱신
|
||||
- 잔여 시간 커스텀 팝오버: 상태 갱신과 동일하며 hover/focus 중에도 내용과 위치를 계속 갱신
|
||||
- UPS/배터리 잔여 시간: 현재 운영 구성에서는 표시하지 않음
|
||||
- dmesg: 진단 패널에서 열었을 때만 1초마다 갱신, 닫으면 중지
|
||||
- WiFi 관측 보정 timer: 10초마다 `bin/wifi_observe.php` 실행
|
||||
- WebSocket 소스 변경 감지: 15초마다 확인 후 재시작
|
||||
@@ -181,10 +159,9 @@ control-wifi-observe.service
|
||||
11. Reboot 버튼은 설정된 확인 문구와 관리자 암호 재입력을 모두 통과한 뒤 서버 API로 재부팅을 요청합니다.
|
||||
12. 시스템 상태는 `vcgencmd get_throttled`를 읽어 현재 저전압/스로틀링 여부와 부팅 후 이력, 최근 감지 시각, 현재/최근 지속시간을 표시합니다.
|
||||
13. 저전압/스로틀링 상태 파일에는 최근 episode 시작/종료를 보관하고, 최근 10분 발생 횟수, 감지 누적시간, 감지 비율을 계산합니다.
|
||||
14. 배터리 잔여 시간은 최근 24시간 SOC 방전 추세와 최대 45일 장기 학습 프로파일을 함께 평가하고, 현재 CPU 전력 변화와 현실 상한을 반영합니다.
|
||||
15. 잔여 시간 차트의 마지막 지점은 현재 snapshot의 잔여 시간과 같은 값을 사용하도록 history 응답을 동기화합니다.
|
||||
16. System Notice 조건이 맞으면 Control이 `system_notice_logs`에 이력을 저장합니다.
|
||||
17. 대시보드 첫 화면에는 팬/전원/스로틀링/WiFi/핵심 차트를 우선 배치하고, 상세 차트, 프로세스 후보, 사용자 서비스, dmesg는 접힘 영역으로 분리합니다.
|
||||
14. UPS/배터리 장치가 제거되어 배터리 센서 조회와 잔여 시간 학습 계산은 기본적으로 돌리지 않습니다.
|
||||
15. System Notice 조건이 맞으면 Control이 `system_notice_logs`에 이력을 저장합니다.
|
||||
16. 대시보드 첫 화면에는 팬/전원/스로틀링/WiFi/핵심 차트를 우선 배치하고, 상세 차트, 프로세스 후보, 사용자 서비스, dmesg는 접힘 영역으로 분리합니다.
|
||||
|
||||
## 팬 판단 온도
|
||||
|
||||
@@ -208,8 +185,9 @@ control-wifi-observe.service
|
||||
- `apply_observed_wifi_connected_time()`: 5G WiFi 연결 시간이 없는 client에 서버 관측 경과 시간 적용
|
||||
- `wifi_client_aliases()`, `save_wifi_client_alias()`: MAC별 WiFi 호스트명 조회와 저장
|
||||
- `apply_wifi_estimates()`: WiFi 신호/송신 속도/수신 속도 결측값 보정
|
||||
- `battery_trend_history()`: 최근 24시간 배터리 기록을 1분 단위로 집계
|
||||
- `battery_profile_history()`: 최대 45일 배터리 기록을 약 5분 간격으로 샘플링하고 파일 캐시로 보관
|
||||
- `battery_status()`: `CONTROL_BATTERY_ENABLED`가 true일 때만 배터리 센서를 읽고, 현재 기본 구성에서는 제거 상태를 반환
|
||||
- `battery_trend_history()`: 배터리 기능을 다시 켤 때 최근 24시간 배터리 기록을 1분 단위로 집계
|
||||
- `battery_profile_history()`: 배터리 기능을 다시 켤 때 최대 45일 배터리 기록을 약 5분 간격으로 샘플링하고 파일 캐시로 보관
|
||||
- `weighted_linear_regression()`, `numeric_mad()`: SOC 기울기 계산과 회귀 잔차 튐 제거
|
||||
- `battery_regression_trend_candidate()`: 1분 SOC 집계를 강건 회귀로 분석해 정밀 방전 속도 후보 계산
|
||||
- `battery_voltage_trend_candidate()`: 전압 기울기와 동적 하한 전압으로 잔여 시간 후보 계산
|
||||
@@ -227,7 +205,8 @@ control-wifi-observe.service
|
||||
- `assets/wakelock.js`: WakeLock 버튼 상태와 Screen Wake Lock API 제어
|
||||
- `customAlert()`, `customConfirm()`, `customPrompt()`: 대시보드 공통 확인/입력 dialog
|
||||
- `controlLang`, `controlTheme`: 재접속 후에도 유지되는 언어/테마 localStorage key
|
||||
- `BATTERY_CELL_CAPACITY_MAH`, `BATTERY_PARALLEL_CELLS`, `BATTERY_CAPACITY_MAH`, `BATTERY_NOMINAL_VOLTAGE`, `BATTERY_CAPACITY_WH`: 병렬 배터리팩 용량과 잔여시간 계산 기준
|
||||
- `CONTROL_BATTERY_ENABLED`: UPS/배터리 센서와 잔여시간 학습 계산 사용 여부
|
||||
- `BATTERY_CELL_CAPACITY_MAH`, `BATTERY_PARALLEL_CELLS`, `BATTERY_CAPACITY_MAH`, `BATTERY_NOMINAL_VOLTAGE`, `BATTERY_CAPACITY_WH`: 배터리 기능을 다시 켤 때 쓰는 병렬 배터리팩 용량과 잔여시간 계산 기준
|
||||
|
||||
## 보안
|
||||
|
||||
@@ -240,13 +219,12 @@ control-wifi-observe.service
|
||||
- 로그인 화면은 CSRF 검사를 수행하고, 설정한 실패 횟수/집계 시간/잠금 시간에 따라 세션 단위 잠금을 적용합니다.
|
||||
- Remember login은 설정에 따라 Secure/SameSite, 토큰 길이, 만료 기간, User-Agent 고정, IPv4 접두사 고정을 조정할 수 있습니다.
|
||||
- WiFi restart/reload 허용 여부와 root 명령 timeout은 설정 모달에서 조정합니다.
|
||||
- 앱 비밀번호와 DB/배터리 설정은 저장소 밖 secret 파일로 관리합니다.
|
||||
- 앱 비밀번호와 DB/선택적 배터리 설정은 저장소 밖 secret 파일로 관리합니다.
|
||||
|
||||
## 운영 체크포인트
|
||||
|
||||
- 센서 수집 주기와 DB 증가량을 확인합니다.
|
||||
- 배터리팩 변경 시 `/home/seo/secret/control.php`의 `battery` 값을 먼저 갱신합니다. 현재 기준은 `2200mAh x 8 parallel = 17600mAh`, nominal `65.12Wh`이며, SOC 추세가 부족한 경우의 fallback 계산에 사용됩니다.
|
||||
- 잔여 시간이 갑자기 흔들리면 커스텀 팝오버의 `계산 출처`, `반영 구간`, `시간당 SOC 감소`, `추세 신뢰도`, `최근 CPU 전력`을 먼저 확인합니다.
|
||||
- UPS/배터리를 다시 붙일 때는 `/home/seo/secret/control.php`의 `battery.enabled`와 용량값을 먼저 확인합니다.
|
||||
- WiFi client 표는 갱신 중에도 가로 스크롤 위치를 보존합니다.
|
||||
- 하드웨어 또는 OS 변경 후 fan sysfs 경로를 확인합니다.
|
||||
- 2.4G 내장 WiFi는 `hostapd-24g.service` 개별 restart 직후 일부 IoT 단말이 WPA/EAPOL 재협상 루프에 들어갈 수 있으므로, 안정화된 상태에서는 개별 restart를 피하고 필요 시 전체 reboot 또는 채널 변경으로 재초기화합니다.
|
||||
|
||||
+23
-1
@@ -14,6 +14,7 @@ if (!is_file($controlSecretFile)) {
|
||||
$controlSecretConfig = require $controlSecretFile;
|
||||
$controlDbConfig = is_array($controlSecretConfig['db'] ?? null) ? $controlSecretConfig['db'] : [];
|
||||
$controlBatteryConfig = is_array($controlSecretConfig['battery'] ?? null) ? $controlSecretConfig['battery'] : [];
|
||||
define('CONTROL_BATTERY_ENABLED', (bool)($controlBatteryConfig['enabled'] ?? false));
|
||||
|
||||
define('APP_PASSWORD', (string)($controlSecretConfig['app_password'] ?? ''));
|
||||
define('DB_HOST', (string)($controlDbConfig['host'] ?? '127.0.0.1'));
|
||||
@@ -126,7 +127,7 @@ function clamp_float(float $value, float $min, float $max): float
|
||||
|
||||
function setting_definitions(): array
|
||||
{
|
||||
return [
|
||||
$definitions = [
|
||||
'security.remember_days' => ['group' => 'security', 'label' => '자동 로그인 유지 기간', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365, 'step' => 1, 'unit' => '일'],
|
||||
'security.cookie_secure_mode' => ['group' => 'security', 'label' => 'Remember 쿠키 Secure', 'type' => 'select', 'default' => 'auto', 'options' => ['auto' => 'HTTPS일 때 자동', 'always' => '항상 사용', 'never' => '사용 안함']],
|
||||
'security.cookie_samesite' => ['group' => 'security', 'label' => 'Remember 쿠키 SameSite', 'type' => 'select', 'default' => 'Lax', 'options' => ['Lax' => 'Lax', 'Strict' => 'Strict', 'None' => 'None']],
|
||||
@@ -191,6 +192,16 @@ function setting_definitions(): array
|
||||
'battery.history_chart_limit' => ['group' => 'battery', 'label' => '상태 차트 표본 수', 'type' => 'number', 'default' => 240, 'min' => 60, 'max' => 1500, 'step' => 10, 'unit' => '개'],
|
||||
'battery.history_sync_limit' => ['group' => 'battery', 'label' => '잔여시간 차트 표본 수', 'type' => 'number', 'default' => 240, 'min' => 60, 'max' => 1500, 'step' => 10, 'unit' => '개'],
|
||||
];
|
||||
|
||||
if (!CONTROL_BATTERY_ENABLED) {
|
||||
foreach (array_keys($definitions) as $key) {
|
||||
if (str_starts_with($key, 'battery.')) {
|
||||
unset($definitions[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $definitions;
|
||||
}
|
||||
|
||||
function setting_rows(bool $reload = false): array
|
||||
@@ -1123,6 +1134,15 @@ function i2cget_word_bytes(int $bus, int $address, int $register): ?array
|
||||
|
||||
function battery_status(): array
|
||||
{
|
||||
if (!CONTROL_BATTERY_ENABLED) {
|
||||
return [
|
||||
'enabled' => false,
|
||||
'removed' => true,
|
||||
'voltage' => null,
|
||||
'percent' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$voltage = null;
|
||||
$percent = null;
|
||||
|
||||
@@ -1140,6 +1160,8 @@ function battery_status(): array
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => true,
|
||||
'removed' => false,
|
||||
'voltage' => $voltage,
|
||||
'percent' => $percent,
|
||||
];
|
||||
|
||||
+43
-32
@@ -3219,8 +3219,10 @@ function collect_snapshot(bool $applyFan = true): array
|
||||
'watts' => isset($latest['cpu_watts']) ? (float)$latest['cpu_watts'] : null,
|
||||
];
|
||||
$battery = $applyFan ? battery_status() : [
|
||||
'voltage' => isset($latest['battery_voltage']) ? (float)$latest['battery_voltage'] : null,
|
||||
'percent' => isset($latest['battery_percent']) ? (float)$latest['battery_percent'] : null,
|
||||
'enabled' => CONTROL_BATTERY_ENABLED,
|
||||
'removed' => !CONTROL_BATTERY_ENABLED,
|
||||
'voltage' => CONTROL_BATTERY_ENABLED && isset($latest['battery_voltage']) ? (float)$latest['battery_voltage'] : null,
|
||||
'percent' => CONTROL_BATTERY_ENABLED && isset($latest['battery_percent']) ? (float)$latest['battery_percent'] : null,
|
||||
];
|
||||
|
||||
if ($applyFan) {
|
||||
@@ -3231,8 +3233,8 @@ function collect_snapshot(bool $applyFan = true): array
|
||||
'rp1_temp_c' => $rp1Temp,
|
||||
'cpu_voltage' => $cpuPower['voltage'],
|
||||
'cpu_watts' => $cpuPower['watts'],
|
||||
'battery_voltage' => $battery['voltage'],
|
||||
'battery_percent' => $battery['percent'],
|
||||
'battery_voltage' => CONTROL_BATTERY_ENABLED ? $battery['voltage'] : null,
|
||||
'battery_percent' => CONTROL_BATTERY_ENABLED ? $battery['percent'] : null,
|
||||
'pwm_value' => $pwm,
|
||||
'pwm_percent' => round($pwm / 255 * 100, 2),
|
||||
'pwm_mode' => $policy['mode'],
|
||||
@@ -3278,34 +3280,43 @@ function collect_snapshot(bool $applyFan = true): array
|
||||
'throttling' => $throttledStatuses['throttling'],
|
||||
]);
|
||||
|
||||
$history = add_battery_remaining_history(sensor_history((int)setting_value('battery.history_chart_limit')));
|
||||
$batteryTrend = battery_trend_history((int)setting_value('battery.trend_hours'));
|
||||
$batteryProfile = battery_profile_history((int)setting_value('battery.profile_days'));
|
||||
$battery['remaining'] = battery_remaining_estimate($battery, $history, $batteryTrend, $batteryProfile);
|
||||
$history = sync_current_battery_remaining_history($history, [
|
||||
'time' => date('Y-m-d H:i:s'),
|
||||
'temp_c' => $temp,
|
||||
'fan_rpm' => $rpm,
|
||||
'fan_efficiency' => fan_efficiency($rpm, $temp, $cpuPower['watts'] ?? null),
|
||||
'rp1_temp_c' => $rp1Temp,
|
||||
'cpu_voltage' => $cpuPower['voltage'],
|
||||
'cpu_watts' => $cpuPower['watts'],
|
||||
'battery_voltage' => $battery['voltage'],
|
||||
'battery_percent' => $battery['percent'],
|
||||
'fan_pwm' => $pwm,
|
||||
'pwm_percent' => round($pwm / 255 * 100, 2),
|
||||
'pwm_mode' => $policy['mode'],
|
||||
'cpu_load_1' => round((float)$load[0], 2),
|
||||
'cpu_load_5' => round((float)$load[1], 2),
|
||||
'cpu_load_15' => round((float)$load[2], 2),
|
||||
'disk_total_kb' => $disk['total_kb'],
|
||||
'disk_used_kb' => $disk['used_kb'],
|
||||
'disk_free_kb' => $disk['free_kb'],
|
||||
'mem_total_mb' => $mem['total_mb'],
|
||||
'mem_used_mb' => $mem['used_mb'],
|
||||
'mem_percent' => $mem['total_mb'] > 0 ? round($mem['used_mb'] / $mem['total_mb'] * 100, 1) : 0,
|
||||
], $battery['remaining']['seconds'] ?? null);
|
||||
$history = array_slice($history, -(int)setting_value('battery.history_sync_limit'));
|
||||
if (CONTROL_BATTERY_ENABLED) {
|
||||
$history = add_battery_remaining_history(sensor_history((int)setting_value('battery.history_chart_limit')));
|
||||
$batteryTrend = battery_trend_history((int)setting_value('battery.trend_hours'));
|
||||
$batteryProfile = battery_profile_history((int)setting_value('battery.profile_days'));
|
||||
$battery['remaining'] = battery_remaining_estimate($battery, $history, $batteryTrend, $batteryProfile);
|
||||
$history = sync_current_battery_remaining_history($history, [
|
||||
'time' => date('Y-m-d H:i:s'),
|
||||
'temp_c' => $temp,
|
||||
'fan_rpm' => $rpm,
|
||||
'fan_efficiency' => fan_efficiency($rpm, $temp, $cpuPower['watts'] ?? null),
|
||||
'rp1_temp_c' => $rp1Temp,
|
||||
'cpu_voltage' => $cpuPower['voltage'],
|
||||
'cpu_watts' => $cpuPower['watts'],
|
||||
'battery_voltage' => $battery['voltage'],
|
||||
'battery_percent' => $battery['percent'],
|
||||
'fan_pwm' => $pwm,
|
||||
'pwm_percent' => round($pwm / 255 * 100, 2),
|
||||
'pwm_mode' => $policy['mode'],
|
||||
'cpu_load_1' => round((float)$load[0], 2),
|
||||
'cpu_load_5' => round((float)$load[1], 2),
|
||||
'cpu_load_15' => round((float)$load[2], 2),
|
||||
'disk_total_kb' => $disk['total_kb'],
|
||||
'disk_used_kb' => $disk['used_kb'],
|
||||
'disk_free_kb' => $disk['free_kb'],
|
||||
'mem_total_mb' => $mem['total_mb'],
|
||||
'mem_used_mb' => $mem['used_mb'],
|
||||
'mem_percent' => $mem['total_mb'] > 0 ? round($mem['used_mb'] / $mem['total_mb'] * 100, 1) : 0,
|
||||
], $battery['remaining']['seconds'] ?? null);
|
||||
$history = array_slice($history, -(int)setting_value('battery.history_sync_limit'));
|
||||
} else {
|
||||
$history = sensor_history(240);
|
||||
$battery['remaining'] = [
|
||||
'display' => 'UPS 제거됨',
|
||||
'seconds' => null,
|
||||
'source' => 'battery_removed',
|
||||
];
|
||||
}
|
||||
|
||||
$processes = process_resource_data((int)setting_value('display.process_limit'));
|
||||
$fanSpike = fan_spike_analysis($history, $fan, $system, $processes);
|
||||
|
||||
+16
-6
@@ -1444,10 +1444,14 @@
|
||||
setText(els.statusThrottlingLast, throttling.last_detected_at || '-');
|
||||
setText(els.statusThrottlingDuration, eventDurationText(throttling));
|
||||
setText(els.statusThrottlingRecent, eventRecentText(throttling));
|
||||
setText(els.statusBatteryVoltage, battery.voltage === null || battery.voltage === undefined ? '-' : `${Number(battery.voltage).toFixed(3)} V`);
|
||||
setText(els.statusBatterySoc, battery.percent === null || battery.percent === undefined ? '-' : `${Number(battery.percent).toFixed(2)}%`);
|
||||
setText(els.statusBatteryRemaining, battery.remaining?.display || '-');
|
||||
if (els.statusBatteryVoltage) {
|
||||
setText(els.statusBatteryVoltage, battery.voltage === null || battery.voltage === undefined ? '-' : `${Number(battery.voltage).toFixed(3)} V`);
|
||||
}
|
||||
if (els.statusBatterySoc) {
|
||||
setText(els.statusBatterySoc, battery.percent === null || battery.percent === undefined ? '-' : `${Number(battery.percent).toFixed(2)}%`);
|
||||
}
|
||||
if (els.statusBatteryRemaining) {
|
||||
setText(els.statusBatteryRemaining, battery.remaining?.display || '-');
|
||||
els.statusBatteryRemaining.removeAttribute('title');
|
||||
updateBatteryTooltip(batteryRemainingTitle(battery.remaining));
|
||||
}
|
||||
@@ -1952,9 +1956,15 @@
|
||||
chart('fanRpmChart', 'RPM', rows, 'fan_rpm', '#3b82f6', ' RPM', dynamicScale(rows, 'fan_rpm', { minSpan: 1000, padding: 400 }));
|
||||
chart('fanEfficiencyChart', 'FANEFF', rows, 'fan_efficiency', '#84cc16', '', dynamicScale(rows, 'fan_efficiency', { minSpan: 20, ceiling: 100 }));
|
||||
chart('cpuWattChart', 'CPUW', rows, 'cpu_watts', '#a855f7', 'W', dynamicScale(rows, 'cpu_watts', { minSpan: 1, padding: 0.4 }, cpuWattSmoothing), { transform: cpuWattSmoothing });
|
||||
chart('batterySocChart', 'BATTERYSOC', rows, 'battery_percent', '#f59e0b', '%', dynamicScale(rows, 'battery_percent', { minSpan: 10, ceiling: 110 }));
|
||||
chart('remainingChart', 'REMAINING', rows, 'battery_remaining_seconds', '#06b6d4', 's', dynamicScale(rows, 'battery_remaining_seconds', { minSpan: 1800, padding: 600 }), { tooltipFormat: formatDurationSeconds });
|
||||
chart('batteryVoltageChart', 'BATTERYV', rows, 'battery_voltage', '#14b8a6', 'V', dynamicScale(rows, 'battery_voltage', { minSpan: 0.2, padding: 0.06 }));
|
||||
if (document.getElementById('batterySocChart')) {
|
||||
chart('batterySocChart', 'BATTERYSOC', rows, 'battery_percent', '#f59e0b', '%', dynamicScale(rows, 'battery_percent', { minSpan: 10, ceiling: 110 }));
|
||||
}
|
||||
if (document.getElementById('remainingChart')) {
|
||||
chart('remainingChart', 'REMAINING', rows, 'battery_remaining_seconds', '#06b6d4', 's', dynamicScale(rows, 'battery_remaining_seconds', { minSpan: 1800, padding: 600 }), { tooltipFormat: formatDurationSeconds });
|
||||
}
|
||||
if (document.getElementById('batteryVoltageChart')) {
|
||||
chart('batteryVoltageChart', 'BATTERYV', rows, 'battery_voltage', '#14b8a6', 'V', dynamicScale(rows, 'battery_voltage', { minSpan: 0.2, padding: 0.06 }));
|
||||
}
|
||||
}
|
||||
|
||||
function resizeChartsSoon() {
|
||||
|
||||
@@ -222,9 +222,6 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
||||
<div class="status-row"><div class="status-key" data-i18n="throttlingLast">Last Throttling</div><div class="status-value" id="statusThrottlingLast">-</div></div>
|
||||
<div class="status-row"><div class="status-key" data-i18n="throttlingDuration">Throttle Duration</div><div class="status-value" id="statusThrottlingDuration">-</div></div>
|
||||
<div class="status-row"><div class="status-key" data-i18n="throttlingRecent">Throttle 10m</div><div class="status-value" id="statusThrottlingRecent">-</div></div>
|
||||
<div class="status-row"><div class="status-key" data-i18n="batteryV">Battery V</div><div class="status-value" id="statusBatteryVoltage">-</div></div>
|
||||
<div class="status-row"><div class="status-key" data-i18n="batterySoc">Battery SOC</div><div class="status-value" id="statusBatterySoc">-</div></div>
|
||||
<div class="status-row"><div class="status-key" data-i18n="remaining">Remaining</div><div class="status-value interactive" id="statusBatteryRemaining" tabindex="0" aria-describedby="batteryRuntimeTooltip">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -236,8 +233,6 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
||||
<div class="chart-grid">
|
||||
<div class="chart-box"><h3 data-i18n="temperature">Temperature</h3><div class="chart-canvas"><canvas id="tempChart"></canvas></div></div>
|
||||
<div class="chart-box"><h3 data-i18n="fanRpm">Fan RPM</h3><div class="chart-canvas"><canvas id="fanRpmChart"></canvas></div></div>
|
||||
<div class="chart-box"><h3 data-i18n="batterySoc">Battery SOC</h3><div class="chart-canvas"><canvas id="batterySocChart"></canvas></div></div>
|
||||
<div class="chart-box"><h3 data-i18n="batteryVoltage">Battery Voltage</h3><div class="chart-canvas"><canvas id="batteryVoltageChart"></canvas></div></div>
|
||||
</div>
|
||||
<details class="details-panel" id="secondaryChartDetails">
|
||||
<summary data-i18n="detailCharts">Detail Charts</summary>
|
||||
@@ -246,7 +241,6 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
||||
<div class="chart-box"><h3 data-i18n="rp1Temp">RP1 Temp</h3><div class="chart-canvas"><canvas id="rp1TempChart"></canvas></div></div>
|
||||
<div class="chart-box"><h3 data-i18n="fanEfficiency">Fan Efficiency</h3><div class="chart-canvas"><canvas id="fanEfficiencyChart"></canvas></div></div>
|
||||
<div class="chart-box"><h3 data-i18n="cpuWatt">CPU Watt</h3><div class="chart-canvas"><canvas id="cpuWattChart"></canvas></div></div>
|
||||
<div class="chart-box"><h3 data-i18n="remaining">Remaining</h3><div class="chart-canvas"><canvas id="remainingChart"></canvas></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
Reference in New Issue
Block a user