Control 알림과 notice 설정 세분화

This commit is contained in:
seo
2026-06-26 03:21:01 +09:00
parent 7a9d4af9ad
commit da8c4e863d
4 changed files with 84 additions and 32 deletions
+7 -6
View File
@@ -111,10 +111,11 @@ WebSocket 장기 실행 프로세스에서는 1분 단위 단기 집계 결과
설정 가능한 항목은 다음 범위입니다. 설정 가능한 항목은 다음 범위입니다.
- 팬 자동 제어: 팬 시작 온도, 최대 온도, 즉시 최대 PWM 온도, 자동 상승/하강 PWM step - 팬 자동 제어: 팬 시작 온도, 최대 온도, 즉시 최대 PWM 온도, 자동 상승/하강 PWM step
- 보안 정책: 자동 로그인 유지 기간, remember 쿠키 Secure/SameSite, CSRF 길이, remember 토큰 길이, User-Agent/IP 접두사 검증, 로그인 실패 잠금, 재부팅 확인 문구, 명령 timeout, WiFi 조작 허용 여부 - 보안 정책: 자동 로그인 유지 기간, remember 쿠키 Secure/SameSite, CSRF 길이, remember 토큰 길이, User-Agent/IP 접두사 검증, 로그인 실패 잠금, 재부팅 허용 여부, 재부팅 확인 문구, 명령 timeout, WiFi 조작 허용 여부
- 알림 정책: 배터리 긴급/위험/경고/복구 기준, 배터리 낮음/복구 알림 쿨다운, 시스템 유의사항 알림 쿨다운
- 저전압/스로틀링: 최근 판정창, 복구 중 유지 시간, 반복 발생 기준 횟수 - 저전압/스로틀링: 최근 판정창, 복구 중 유지 시간, 반복 발생 기준 횟수
- 팬 이상감지: 기준 표본 수, 최신 표본 제외 수, RPM/온도 이상 감지 차이, RPM/온도 복구 차이 - 팬 이상감지: 기준 표본 수, 최신 표본 제외 수, RPM/온도 이상 감지 차이, RPM/온도 복구 차이, RPM 감시 시작 PWM, alert 유지 중 반복 기록, 반복 기록 간격, 하강 변화 원인 후보 표시 여부
- 배터리 예측: 단기/장기 학습 범위, 후보 필터 비율, 부하 보정 강도, 전압 하한, 용량 모델 가중치 - 배터리 예측: 단기/장기 학습 범위, 후보 필터 비율, 부하 보정 강도, 전압 하한, 용량 모델 가중치, 상태/잔여시간 차트 표본 수
## 갱신 주기 ## 갱신 주기
@@ -139,9 +140,9 @@ Control은 서버에서 Home Assistant webhook으로 알림 payload를 보내고
- 사용 속성: `tag`, `group`, `channel`, `importance`, `priority`, `ttl`, `sticky`, `persistent`, `renotify`, `color`, `notification_icon`, `vibrationPattern`, `ledColor`, `visibility`, `timeout`, `clickAction`, `url`, `actions` - 사용 속성: `tag`, `group`, `channel`, `importance`, `priority`, `ttl`, `sticky`, `persistent`, `renotify`, `color`, `notification_icon`, `vibrationPattern`, `ledColor`, `visibility`, `timeout`, `clickAction`, `url`, `actions`
- 발송 결과는 `ha_notify_logs`에 저장 - 발송 결과는 `ha_notify_logs`에 저장
배터리 SOC 20% 이하 경고는 `control-battery-low` tag를 사용합니다. SOC 값이 조금씩 달라져도 Android 알림은 같은 tag로 갱신되어 알림 목록에 여러 장이 계속 쌓이지 않습니다. 20% 이하에서는 `주의`, 15% 이하에서는 `경고`, 10% 이하에서는 `위험`, 5% 이하에서는 `긴급` 단계로 채널, 중요도, 아이콘, 진동 패턴을 조정합니다. SOC가 20%를 초과하면 `clear_notification`으로 해당 tag 알림을 제거합니다. 배터리 SOC 낮음 경고는 `control-battery-low` tag를 사용합니다. SOC 값이 조금씩 달라져도 Android 알림은 같은 tag로 갱신되어 알림 목록에 여러 장이 계속 쌓이지 않습니다. 주의/경고/위험/긴급/복구 기준과 알림 쿨다운은 설정 모달의 알림 정책에서 조정합니다.
System Notice는 `control-system-notice` tag를 사용하며, 기본 10분 쿨다운으로 급격한 팬/온도 변화 알림을 제한합니다. 쿨다운은 `/home/seo/secret/control.php``ha_notify.cooldowns`에서 조정할 수 있습니다. System Notice는 `control-system-notice` tag를 사용하며, 설정된 쿨다운으로 급격한 팬/온도 변화 알림을 제한합니다. alert 상태는 신규 진입과 유지 상태를 분리해 관리하며, alert 유지 중 반복 기록 여부와 기록 간격도 설정 모달에서 조정합니다.
Android 알림 채널은 생성 후 휴대폰 설정에 의해 중요도와 진동 패턴이 고정될 수 있습니다. 채널을 다시 만들 필요가 있을 때는 운영 CLI로 제거 명령을 보낸 뒤 다음 알림에서 재생성합니다. Android 알림 채널은 생성 후 휴대폰 설정에 의해 중요도와 진동 패턴이 고정될 수 있습니다. 채널을 다시 만들 필요가 있을 때는 운영 CLI로 제거 명령을 보낸 뒤 다음 알림에서 재생성합니다.
@@ -209,7 +210,7 @@ control-wifi-observe.service
- `ha_notify_due()`: 동일 tag 기준 쿨다운 판정 - `ha_notify_due()`: 동일 tag 기준 쿨다운 판정
- `clear_ha_notification()`: 같은 tag의 Android 알림 제거 명령 발송 - `clear_ha_notification()`: 같은 tag의 Android 알림 제거 명령 발송
- `battery_low_notify_profile()`: 배터리 SOC 단계별 채널, 중요도, 아이콘, 진동 정책 선택 - `battery_low_notify_profile()`: 배터리 SOC 단계별 채널, 중요도, 아이콘, 진동 정책 선택
- `send_battery_low_notify_if_needed()`: 배터리 SOC 20% 이하 알림 발송 - `send_battery_low_notify_if_needed()`: 설정된 배터리 SOC 기준에 따라 낮음/복구 알림 발송
- `ha_notify_log_rows()`: 대시보드 HA 알림 이력 조회 - `ha_notify_log_rows()`: 대시보드 HA 알림 이력 조회
- `battery_trend_history()`: 최근 24시간 배터리 기록을 1분 단위로 집계 - `battery_trend_history()`: 최근 24시간 배터리 기록을 1분 단위로 집계
- `battery_profile_history()`: 최대 45일 배터리 기록을 약 5분 간격으로 샘플링하고 파일 캐시로 보관 - `battery_profile_history()`: 최대 45일 배터리 기록을 약 5분 간격으로 샘플링하고 파일 캐시로 보관
+33 -7
View File
@@ -147,6 +147,7 @@ function setting_definitions(): array
'security.login_window_seconds' => ['group' => 'security', 'label' => '로그인 실패 집계 시간', 'type' => 'number', 'default' => 600, 'min' => 60, 'max' => 86400, 'step' => 60, 'unit' => '초'], 'security.login_window_seconds' => ['group' => 'security', 'label' => '로그인 실패 집계 시간', 'type' => 'number', 'default' => 600, 'min' => 60, 'max' => 86400, 'step' => 60, 'unit' => '초'],
'security.login_lock_seconds' => ['group' => 'security', 'label' => '로그인 잠금 시간', 'type' => 'number', 'default' => 300, 'min' => 10, 'max' => 86400, 'step' => 10, 'unit' => '초'], 'security.login_lock_seconds' => ['group' => 'security', 'label' => '로그인 잠금 시간', 'type' => 'number', 'default' => 300, 'min' => 10, 'max' => 86400, 'step' => 10, 'unit' => '초'],
'security.reboot_phrase' => ['group' => 'security', 'label' => '재부팅 확인 문구', 'type' => 'text', 'default' => '재부팅', 'min_length' => 2, 'max_length' => 32], 'security.reboot_phrase' => ['group' => 'security', 'label' => '재부팅 확인 문구', 'type' => 'text', 'default' => '재부팅', 'min_length' => 2, 'max_length' => 32],
'security.allow_reboot' => ['group' => 'security', 'label' => '재부팅 실행 허용', 'type' => 'boolean', 'default' => 1],
'security.reboot_timeout_seconds' => ['group' => 'security', 'label' => '재부팅 명령 timeout', 'type' => 'number', 'default' => 5, 'min' => 2, 'max' => 30, 'step' => 1, 'unit' => '초'], 'security.reboot_timeout_seconds' => ['group' => 'security', 'label' => '재부팅 명령 timeout', 'type' => 'number', 'default' => 5, 'min' => 2, 'max' => 30, 'step' => 1, 'unit' => '초'],
'security.wifi_command_timeout_seconds' => ['group' => 'security', 'label' => 'WiFi 명령 timeout', 'type' => 'number', 'default' => 25, 'min' => 5, 'max' => 90, 'step' => 1, 'unit' => '초'], 'security.wifi_command_timeout_seconds' => ['group' => 'security', 'label' => 'WiFi 명령 timeout', 'type' => 'number', 'default' => 25, 'min' => 5, 'max' => 90, 'step' => 1, 'unit' => '초'],
'security.shell_default_timeout_seconds' => ['group' => 'security', 'label' => '기본 명령 timeout', 'type' => 'number', 'default' => 8, 'min' => 2, 'max' => 60, 'step' => 1, 'unit' => '초'], 'security.shell_default_timeout_seconds' => ['group' => 'security', 'label' => '기본 명령 timeout', 'type' => 'number', 'default' => 8, 'min' => 2, 'max' => 60, 'step' => 1, 'unit' => '초'],
@@ -167,6 +168,17 @@ function setting_definitions(): array
'notice.temp_delta_threshold' => ['group' => 'notice', 'label' => '온도 이상 감지 차이', 'type' => 'number', 'default' => 3.0, 'min' => 0.5, 'max' => 15, 'step' => 0.1, 'unit' => 'C'], 'notice.temp_delta_threshold' => ['group' => 'notice', 'label' => '온도 이상 감지 차이', 'type' => 'number', 'default' => 3.0, 'min' => 0.5, 'max' => 15, 'step' => 0.1, 'unit' => 'C'],
'notice.recover_rpm_delta' => ['group' => 'notice', 'label' => '팬 RPM 복구 차이', 'type' => 'number', 'default' => 500, 'min' => 50, 'max' => 3000, 'step' => 50, 'unit' => 'RPM'], 'notice.recover_rpm_delta' => ['group' => 'notice', 'label' => '팬 RPM 복구 차이', 'type' => 'number', 'default' => 500, 'min' => 50, 'max' => 3000, 'step' => 50, 'unit' => 'RPM'],
'notice.recover_temp_delta' => ['group' => 'notice', 'label' => '온도 복구 차이', 'type' => 'number', 'default' => 1.5, 'min' => 0.2, 'max' => 10, 'step' => 0.1, 'unit' => 'C'], 'notice.recover_temp_delta' => ['group' => 'notice', 'label' => '온도 복구 차이', 'type' => 'number', 'default' => 1.5, 'min' => 0.2, 'max' => 10, 'step' => 0.1, 'unit' => 'C'],
'notice.rpm_expected_pwm' => ['group' => 'notice', 'label' => 'RPM 감시 시작 PWM', 'type' => 'number', 'default' => 20, 'min' => 0, 'max' => 255, 'step' => 1, 'unit' => 'PWM'],
'notice.log_during_alert' => ['group' => 'notice', 'label' => 'Alert 유지 중 반복 기록', 'type' => 'boolean', 'default' => 0],
'notice.log_interval_seconds' => ['group' => 'notice', 'label' => 'Alert 반복 기록 간격', 'type' => 'number', 'default' => 300, 'min' => 30, 'max' => 3600, 'step' => 30, 'unit' => '초'],
'notice.downward_only_process_hide' => ['group' => 'notice', 'label' => '하강 변화 원인 후보 숨김', 'type' => 'boolean', 'default' => 1],
'notify.battery_emergency_percent' => ['group' => 'notify', 'label' => '배터리 긴급 기준', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 30, 'step' => 0.5, 'unit' => '%'],
'notify.battery_critical_percent' => ['group' => 'notify', 'label' => '배터리 위험 기준', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 40, 'step' => 0.5, 'unit' => '%'],
'notify.battery_warning_percent' => ['group' => 'notify', 'label' => '배터리 경고 기준', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 50, 'step' => 0.5, 'unit' => '%'],
'notify.battery_clear_percent' => ['group' => 'notify', 'label' => '배터리 복구 기준', 'type' => 'number', 'default' => 20, 'min' => 2, 'max' => 80, 'step' => 0.5, 'unit' => '%'],
'notify.battery_low_cooldown_seconds' => ['group' => 'notify', 'label' => '배터리 낮음 알림 쿨다운', 'type' => 'number', 'default' => HA_NOTIFY_BATTERY_LOW_COOLDOWN, 'min' => 1, 'max' => 86400, 'step' => 1, 'unit' => '초'],
'notify.battery_clear_cooldown_seconds' => ['group' => 'notify', 'label' => '배터리 복구 알림 쿨다운', 'type' => 'number', 'default' => HA_NOTIFY_BATTERY_CLEAR_COOLDOWN, 'min' => 1, 'max' => 86400, 'step' => 1, 'unit' => '초'],
'notify.system_notice_cooldown_seconds' => ['group' => 'notify', 'label' => '시스템 유의사항 알림 쿨다운', 'type' => 'number', 'default' => HA_NOTIFY_SYSTEM_NOTICE_COOLDOWN, 'min' => 10, 'max' => 86400, 'step' => 10, 'unit' => '초'],
'battery.trend_hours' => ['group' => 'battery', 'label' => '단기 배터리 학습 범위', 'type' => 'number', 'default' => 24, 'min' => 1, 'max' => 48, 'step' => 1, 'unit' => '시간'], 'battery.trend_hours' => ['group' => 'battery', 'label' => '단기 배터리 학습 범위', 'type' => 'number', 'default' => 24, 'min' => 1, 'max' => 48, 'step' => 1, 'unit' => '시간'],
'battery.profile_days' => ['group' => 'battery', 'label' => '장기 배터리 학습 범위', 'type' => 'number', 'default' => 45, 'min' => 3, 'max' => 90, 'step' => 1, 'unit' => '일'], 'battery.profile_days' => ['group' => 'battery', 'label' => '장기 배터리 학습 범위', 'type' => 'number', 'default' => 45, 'min' => 3, 'max' => 90, 'step' => 1, 'unit' => '일'],
'battery.load_adjust_recent' => ['group' => 'battery', 'label' => '최근 추세 부하 보정 강도', 'type' => 'number', 'default' => 0.45, 'min' => 0, 'max' => 1, 'step' => 0.01, 'unit' => 'x'], 'battery.load_adjust_recent' => ['group' => 'battery', 'label' => '최근 추세 부하 보정 강도', 'type' => 'number', 'default' => 0.45, 'min' => 0, 'max' => 1, 'step' => 0.01, 'unit' => 'x'],
@@ -178,6 +190,8 @@ function setting_definitions(): array
'battery.candidate_min_ratio' => ['group' => 'battery', 'label' => '후보 최소 속도 비율', 'type' => 'number', 'default' => 0.38, 'min' => 0.05, 'max' => 1, 'step' => 0.01, 'unit' => 'x'], 'battery.candidate_min_ratio' => ['group' => 'battery', 'label' => '후보 최소 속도 비율', 'type' => 'number', 'default' => 0.38, 'min' => 0.05, 'max' => 1, 'step' => 0.01, 'unit' => 'x'],
'battery.candidate_max_ratio' => ['group' => 'battery', 'label' => '후보 최대 속도 비율', 'type' => 'number', 'default' => 2.65, 'min' => 1, 'max' => 8, 'step' => 0.05, 'unit' => 'x'], 'battery.candidate_max_ratio' => ['group' => 'battery', 'label' => '후보 최대 속도 비율', 'type' => 'number', 'default' => 2.65, 'min' => 1, 'max' => 8, 'step' => 0.05, 'unit' => 'x'],
'battery.capacity_model_weight' => ['group' => 'battery', 'label' => '용량 모델 가중치', 'type' => 'number', 'default' => 0.18, 'min' => 0, 'max' => 1, 'step' => 0.01, 'unit' => 'x'], 'battery.capacity_model_weight' => ['group' => 'battery', 'label' => '용량 모델 가중치', 'type' => 'number', 'default' => 0.18, 'min' => 0, 'max' => 1, 'step' => 0.01, 'unit' => 'x'],
'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' => '개'],
]; ];
} }
@@ -246,6 +260,7 @@ function settings_payload(): array
$values = setting_rows(); $values = setting_rows();
$groups = [ $groups = [
'security' => '보안 정책', 'security' => '보안 정책',
'notify' => '알림 정책',
'fan' => '팬 자동 제어', 'fan' => '팬 자동 제어',
'battery' => '배터리 예측', 'battery' => '배터리 예측',
'event' => '저전압/스로틀링', 'event' => '저전압/스로틀링',
@@ -501,7 +516,7 @@ function bootstrap_db(): void
$pdo->exec(" $pdo->exec("
CREATE TABLE IF NOT EXISTS remember_tokens ( CREATE TABLE IF NOT EXISTS remember_tokens (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
selector CHAR(24) NOT NULL, selector VARCHAR(64) NOT NULL,
validator_hash CHAR(64) NOT NULL, validator_hash CHAR(64) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL, expires_at DATETIME NOT NULL,
@@ -583,6 +598,7 @@ function bootstrap_db(): void
"ALTER TABLE sensor_logs DROP COLUMN created_ip", "ALTER TABLE sensor_logs DROP COLUMN created_ip",
"ALTER TABLE fan_spike_logs ADD COLUMN spike_key VARCHAR(32) NULL AFTER created_at", "ALTER TABLE fan_spike_logs ADD COLUMN spike_key VARCHAR(32) NULL AFTER created_at",
"ALTER TABLE fan_spike_logs ADD UNIQUE KEY uniq_spike_key (spike_key)", "ALTER TABLE fan_spike_logs ADD UNIQUE KEY uniq_spike_key (spike_key)",
"ALTER TABLE remember_tokens MODIFY selector VARCHAR(64) NOT NULL",
"ALTER TABLE system_notice_state ADD COLUMN active_temp_delta DECIMAL(8,2) NULL AFTER active_reason", "ALTER TABLE system_notice_state ADD COLUMN active_temp_delta DECIMAL(8,2) NULL AFTER active_reason",
"ALTER TABLE system_notice_state ADD COLUMN active_rpm_delta DECIMAL(12,2) NULL AFTER active_temp_delta", "ALTER TABLE system_notice_state ADD COLUMN active_rpm_delta DECIMAL(12,2) NULL AFTER active_temp_delta",
"ALTER TABLE system_notice_state ADD COLUMN process_signature VARCHAR(255) NULL AFTER active_rpm_delta", "ALTER TABLE system_notice_state ADD COLUMN process_signature VARCHAR(255) NULL AFTER active_rpm_delta",
@@ -949,7 +965,11 @@ function clear_ha_notification(string $tag, string $title = 'Control 알림 해
function battery_low_notify_profile(float $percent): array function battery_low_notify_profile(float $percent): array
{ {
if ($percent <= 5.0) { $emergency = (float)setting_value('notify.battery_emergency_percent');
$critical = max($emergency, (float)setting_value('notify.battery_critical_percent'));
$warning = max($critical, (float)setting_value('notify.battery_warning_percent'));
if ($percent <= $emergency) {
return [ return [
'label' => '긴급', 'label' => '긴급',
'level' => 'critical', 'level' => 'critical',
@@ -964,7 +984,7 @@ function battery_low_notify_profile(float $percent): array
]; ];
} }
if ($percent <= 10.0) { if ($percent <= $critical) {
return [ return [
'label' => '위험', 'label' => '위험',
'level' => 'critical', 'level' => 'critical',
@@ -979,7 +999,7 @@ function battery_low_notify_profile(float $percent): array
]; ];
} }
if ($percent <= 15.0) { if ($percent <= $warning) {
return [ return [
'label' => '경고', 'label' => '경고',
'level' => 'warning', 'level' => 'warning',
@@ -1025,13 +1045,19 @@ function send_battery_low_notify_if_needed(array $battery): array
: null; : null;
$tag = 'control-battery-low'; $tag = 'control-battery-low';
if ($percent > 20.0) { $warning = max(
(float)setting_value('notify.battery_emergency_percent'),
(float)setting_value('notify.battery_critical_percent'),
(float)setting_value('notify.battery_warning_percent')
);
$clearPercent = max($warning + 0.5, (float)setting_value('notify.battery_clear_percent'));
if ($percent > $clearPercent) {
$latestLow = latest_ha_notify_epoch_by_tag($tag); $latestLow = latest_ha_notify_epoch_by_tag($tag);
$latestClear = latest_ha_notify_epoch_by_tag($tag . '-clear'); $latestClear = latest_ha_notify_epoch_by_tag($tag . '-clear');
if ( if (
$latestLow > 0 $latestLow > 0
&& $latestLow > $latestClear && $latestLow > $latestClear
&& ha_notify_due($tag . '-clear', HA_NOTIFY_BATTERY_CLEAR_COOLDOWN) && ha_notify_due($tag . '-clear', (int)setting_value('notify.battery_clear_cooldown_seconds'))
) { ) {
return clear_ha_notification($tag, '배터리 SOC 복구'); return clear_ha_notification($tag, '배터리 SOC 복구');
} }
@@ -1043,7 +1069,7 @@ function send_battery_low_notify_if_needed(array $battery): array
]; ];
} }
if (!ha_notify_due($tag, HA_NOTIFY_BATTERY_LOW_COOLDOWN)) { if (!ha_notify_due($tag, (int)setting_value('notify.battery_low_cooldown_seconds'))) {
return [ return [
'sent' => 0, 'sent' => 0,
'failed' => 0, 'failed' => 0,
+33 -15
View File
@@ -1720,7 +1720,7 @@ function sync_current_battery_remaining_history(array $history, array $currentRo
} }
$history[] = $currentRow; $history[] = $currentRow;
return array_slice($history, -240); return array_slice($history, -(int)setting_value('battery.history_sync_limit'));
} }
function process_service_name(int $pid): string function process_service_name(int $pid): string
@@ -1973,7 +1973,8 @@ function fan_spike_analysis(array $history, array $fan, array $system, array $pr
]; ];
} }
$rpmExpected = $mode !== 'off' && ($pwm >= 20 || $pwmAvg >= 20); $rpmExpectedPwm = (float)setting_value('notice.rpm_expected_pwm');
$rpmExpected = $mode !== 'off' && ($pwm >= $rpmExpectedPwm || $pwmAvg >= $rpmExpectedPwm);
$reasons = []; $reasons = [];
$rpmThreshold = (float)setting_value('notice.rpm_delta_threshold'); $rpmThreshold = (float)setting_value('notice.rpm_delta_threshold');
@@ -1984,11 +1985,12 @@ function fan_spike_analysis(array $history, array $fan, array $system, array $pr
if ($rpmExpected && abs($rpmDelta) >= $rpmThreshold) $reasons[] = 'RPM ' . signed_delta_text($rpmDelta); if ($rpmExpected && abs($rpmDelta) >= $rpmThreshold) $reasons[] = 'RPM ' . signed_delta_text($rpmDelta);
if (abs($tempDelta) >= $tempThreshold) $reasons[] = 'TEMP ' . signed_delta_text($tempDelta, 'C'); if (abs($tempDelta) >= $tempThreshold) $reasons[] = 'TEMP ' . signed_delta_text($tempDelta, 'C');
$spiking = false; $alertStarted = false;
if ($currentState === 'normal') { if ($currentState === 'normal') {
$spiking = $reasons !== []; $alertStarted = $reasons !== [];
if ($spiking) { if ($alertStarted) {
$currentState = 'alert';
save_system_notice_state( save_system_notice_state(
'alert', 'alert',
$tempAvg, $tempAvg,
@@ -2031,14 +2033,17 @@ function fan_spike_analysis(array $history, array $fan, array $system, array $pr
} }
$summary = 'No system notice detected in recent samples.'; $summary = 'No system notice detected in recent samples.';
if ($spiking) { if ($alertStarted) {
$summary = 'System notice: ' . implode(', ', $reasons); $summary = 'System notice: ' . implode(', ', $reasons);
} elseif ($currentState === 'alert' && $reasons !== []) { } elseif ($currentState === 'alert' && $reasons !== []) {
$summary = 'System notice active: ' . implode(', ', $reasons); $summary = 'System notice active: ' . implode(', ', $reasons);
} }
$alertActive = $currentState === 'alert';
return [ return [
'active' => $spiking, 'active' => $alertActive,
'alert_started' => $alertStarted,
'summary' => $summary, 'summary' => $summary,
'rpm_delta' => round($rpmDelta, 1), 'rpm_delta' => round($rpmDelta, 1),
'pwm_delta' => round($pwmDelta, 1), 'pwm_delta' => round($pwmDelta, 1),
@@ -2046,7 +2051,7 @@ function fan_spike_analysis(array $history, array $fan, array $system, array $pr
'rpm_avg' => round($rpmAvg, 1), 'rpm_avg' => round($rpmAvg, 1),
'pwm_avg' => round($pwmAvg, 1), 'pwm_avg' => round($pwmAvg, 1),
'temp_avg' => round($tempAvg, 1), 'temp_avg' => round($tempAvg, 1),
'notice_state' => $spiking ? 'alert' : $currentState, 'notice_state' => $currentState,
'baseline_source' => $currentState === 'alert' ? 'frozen_alert' : 'rolling', 'baseline_source' => $currentState === 'alert' ? 'frozen_alert' : 'rolling',
'baseline_samples' => (int)$rollingBaseline['samples'], 'baseline_samples' => (int)$rollingBaseline['samples'],
]; ];
@@ -2148,8 +2153,9 @@ function add_fan_spike_log(array $spike, array $fan, array $system, array $proce
$rpmDelta = round((float)($spike['rpm_delta'] ?? 0), 1); $rpmDelta = round((float)($spike['rpm_delta'] ?? 0), 1);
$pwmDelta = round((float)($spike['pwm_delta'] ?? 0), 1); $pwmDelta = round((float)($spike['pwm_delta'] ?? 0), 1);
$tempDelta = round((float)($spike['temp_delta'] ?? 0), 1); $tempDelta = round((float)($spike['temp_delta'] ?? 0), 1);
$spikeKey = 'fan:' . date('YmdHi'); $interval = max(60, (int)setting_value('notice.log_interval_seconds'));
$loggedProcesses = notice_downward_only($tempDelta, $rpmDelta) $spikeKey = 'fan:' . (int)floor(time() / $interval);
$loggedProcesses = ((bool)setting_value('notice.downward_only_process_hide') && notice_downward_only($tempDelta, $rpmDelta))
? ['cpu' => [], 'memory' => []] ? ['cpu' => [], 'memory' => []]
: $processes; : $processes;
@@ -2412,7 +2418,7 @@ function send_fan_spike_notify(array $spike, array $fan, array $system, array $p
. number_format((float)($fan['rpm'] ?? 0), 0) . number_format((float)($fan['rpm'] ?? 0), 0)
. ' RPM / PWM ' . ' RPM / PWM '
. number_format((float)($fan['pwm'] ?? 0), 0) . number_format((float)($fan['pwm'] ?? 0), 0)
. (notice_downward_only($tempDelta, $rpmDelta) ? '' : "\n원인 후보: " . expected_process_detail_text($processes)); . (((bool)setting_value('notice.downward_only_process_hide') && notice_downward_only($tempDelta, $rpmDelta)) ? '' : "\n원인 후보: " . expected_process_detail_text($processes));
return send_ha_notify([ return send_ha_notify([
'title' => '시스템 유의사항', 'title' => '시스템 유의사항',
@@ -3224,7 +3230,7 @@ function collect_snapshot(bool $applyFan = true): array
'throttling' => $throttledStatuses['throttling'], 'throttling' => $throttledStatuses['throttling'],
]); ]);
$history = add_battery_remaining_history(sensor_history(240)); $history = add_battery_remaining_history(sensor_history((int)setting_value('battery.history_chart_limit')));
$batteryTrend = battery_trend_history((int)setting_value('battery.trend_hours')); $batteryTrend = battery_trend_history((int)setting_value('battery.trend_hours'));
$batteryProfile = battery_profile_history((int)setting_value('battery.profile_days')); $batteryProfile = battery_profile_history((int)setting_value('battery.profile_days'));
$battery['remaining'] = battery_remaining_estimate($battery, $history, $batteryTrend, $batteryProfile); $battery['remaining'] = battery_remaining_estimate($battery, $history, $batteryTrend, $batteryProfile);
@@ -3251,23 +3257,27 @@ function collect_snapshot(bool $applyFan = true): array
'mem_used_mb' => $mem['used_mb'], 'mem_used_mb' => $mem['used_mb'],
'mem_percent' => $mem['total_mb'] > 0 ? round($mem['used_mb'] / $mem['total_mb'] * 100, 1) : 0, 'mem_percent' => $mem['total_mb'] > 0 ? round($mem['used_mb'] / $mem['total_mb'] * 100, 1) : 0,
], $battery['remaining']['seconds'] ?? null); ], $battery['remaining']['seconds'] ?? null);
$history = array_slice($history, -(int)setting_value('battery.history_sync_limit'));
$processes = process_resource_data(6); $processes = process_resource_data(6);
$fanSpike = fan_spike_analysis($history, $fan, $system, $processes); $fanSpike = fan_spike_analysis($history, $fan, $system, $processes);
if (!empty($fanSpike['active'])) { $shouldLogNotice = !empty($fanSpike['alert_started'])
|| (!empty($fanSpike['active']) && (bool)setting_value('notice.log_during_alert'));
if ($shouldLogNotice) {
$latestNoticeNotify = latest_system_notice_notify_epoch(); $latestNoticeNotify = latest_system_notice_notify_epoch();
$spikeLogId = add_fan_spike_log($fanSpike, $fan, $system, $processes); $spikeLogId = add_fan_spike_log($fanSpike, $fan, $system, $processes);
if ($spikeLogId > 0) { if ($spikeLogId > 0) {
$fanSpike['log_id'] = $spikeLogId; $fanSpike['log_id'] = $spikeLogId;
if ($latestNoticeNotify <= 0 || time() - $latestNoticeNotify >= HA_NOTIFY_SYSTEM_NOTICE_COOLDOWN) { $noticeCooldown = (int)setting_value('notify.system_notice_cooldown_seconds');
if ($latestNoticeNotify <= 0 || time() - $latestNoticeNotify >= $noticeCooldown) {
$fanSpike['notify'] = send_fan_spike_notify($fanSpike, $fan, $system, $processes); $fanSpike['notify'] = send_fan_spike_notify($fanSpike, $fan, $system, $processes);
} else { } else {
$fanSpike['notify'] = [ $fanSpike['notify'] = [
'sent' => 0, 'sent' => 0,
'failed' => 0, 'failed' => 0,
'skipped' => 'system_notice_cooldown', 'skipped' => 'system_notice_cooldown',
'cooldown_seconds' => HA_NOTIFY_SYSTEM_NOTICE_COOLDOWN, 'cooldown_seconds' => $noticeCooldown,
]; ];
} }
} }
@@ -3383,6 +3393,14 @@ function control_api_dispatch(): void
$password = (string)($_POST['password'] ?? ''); $password = (string)($_POST['password'] ?? '');
$expectedPhrase = (string)setting_value('security.reboot_phrase'); $expectedPhrase = (string)setting_value('security.reboot_phrase');
if (!(bool)setting_value('security.allow_reboot')) {
json_out([
'ok' => false,
'error' => 'reboot_disabled',
'message' => '재부팅 실행이 설정에서 비활성화되어 있습니다.',
], 403);
}
if ($phrase !== $expectedPhrase) { if ($phrase !== $expectedPhrase) {
json_out([ json_out([
'ok' => false, 'ok' => false,
+11 -4
View File
@@ -625,7 +625,7 @@
return ` return `
<span class="settings-toggle"> <span class="settings-toggle">
<input data-setting-key="${key}" type="checkbox" value="1" ${checked ? 'checked' : ''}> <input data-setting-key="${key}" type="checkbox" value="1" ${checked ? 'checked' : ''}>
<span>${checked ? 'ON' : 'OFF'}</span> <span>${checked ? '켜기' : '끄기'}</span>
</span> </span>
`; `;
} }
@@ -662,6 +662,11 @@
if (item.type === 'select' && item.options && item.options[item.default]) { if (item.type === 'select' && item.options && item.options[item.default]) {
return item.options[item.default]; return item.options[item.default];
} }
if (item.type === 'boolean') {
return item.default === true || item.default === 1 || item.default === '1' || item.default === 'true'
? '켜기'
: '끄기';
}
return item.default ?? ''; return item.default ?? '';
}; };
@@ -687,7 +692,7 @@
els.settingsBody.querySelectorAll('.settings-toggle input').forEach(input => { els.settingsBody.querySelectorAll('.settings-toggle input').forEach(input => {
input.addEventListener('change', () => { input.addEventListener('change', () => {
const label = input.closest('.settings-toggle')?.querySelector('span'); const label = input.closest('.settings-toggle')?.querySelector('span');
if (label) label.textContent = input.checked ? 'ON' : 'OFF'; if (label) label.textContent = input.checked ? '켜기' : '끄기';
}); });
input.closest('.settings-toggle')?.addEventListener('click', () => { input.closest('.settings-toggle')?.addEventListener('click', () => {
input.checked = !input.checked; input.checked = !input.checked;
@@ -1382,8 +1387,10 @@
const m = noticeMetrics(row); const m = noticeMetrics(row);
const reasons = []; const reasons = [];
if (Math.abs(m.tempDelta) >= 3) reasons.push(t(m.tempDelta >= 0 ? 'tempHigher' : 'tempLower')); const tempThreshold = Number(settingByKey('notice.temp_delta_threshold', 3));
if (Math.abs(m.rpmDelta) >= 1000) reasons.push(t(m.rpmDelta >= 0 ? 'rpmHigher' : 'rpmLower')); const rpmThreshold = Number(settingByKey('notice.rpm_delta_threshold', 1000));
if (Math.abs(m.tempDelta) >= tempThreshold) reasons.push(t(m.tempDelta >= 0 ? 'tempHigher' : 'tempLower'));
if (Math.abs(m.rpmDelta) >= rpmThreshold) reasons.push(t(m.rpmDelta >= 0 ? 'rpmHigher' : 'rpmLower'));
return `${t('recordedReason')}: ${reasons.length ? reasons.join(', ') : t('instantChange')}`; return `${t('recordedReason')}: ${reasons.length ? reasons.join(', ') : t('instantChange')}`;
} }