외출 귀가 팬 정책 추가
This commit is contained in:
@@ -12,6 +12,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
|||||||
|
|
||||||
- 팬 모드 `auto`, `manual`, `off` 제어
|
- 팬 모드 `auto`, `manual`, `off` 제어
|
||||||
- PWM slider 기반 수동 팬 제어
|
- PWM slider 기반 수동 팬 제어
|
||||||
|
- 서울 HA의 휴대폰 device tracker로 외출/귀가를 감지해 상태 변화 시 1회만 팬 모드 자동 전환
|
||||||
- 오버레이 설정 모달에서 보안 정책, 팬 자동곡선, 이벤트 판정, 팬 이상감지 파라미터 조정
|
- 오버레이 설정 모달에서 보안 정책, 팬 자동곡선, 이벤트 판정, 팬 이상감지 파라미터 조정
|
||||||
- 설정 모달에서 라즈베리파이 정품 7인치 DSI 터치 디스플레이 상태 확인, 밝기/백라이트 즉시 적용, 콘솔 회전, 터치 좌표 보정, LCD/터치 비활성화 부팅 설정 조정
|
- 설정 모달에서 라즈베리파이 정품 7인치 DSI 터치 디스플레이 상태 확인, 밝기/백라이트 즉시 적용, 콘솔 회전, 터치 좌표 보정, LCD/터치 비활성화 부팅 설정 조정
|
||||||
- CPU 온도, 팬 RPM 핵심 차트
|
- CPU 온도, 팬 RPM 핵심 차트
|
||||||
@@ -71,6 +72,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
|||||||
## 데이터/저장소
|
## 데이터/저장소
|
||||||
|
|
||||||
- `control_state`: 팬 모드와 PWM 상태
|
- `control_state`: 팬 모드와 PWM 상태
|
||||||
|
- `presence_fan_policy_state`: 외출/귀가 기반 팬 정책의 마지막 확인/적용 상태
|
||||||
- `sensor_logs`: 온도, RPM, PWM, load, memory, disk, uptime. 과거 배터리 컬럼은 호환을 위해 유지하지만 UPS 제거 상태에서는 새 배터리값을 기록하지 않습니다.
|
- `sensor_logs`: 온도, RPM, PWM, load, memory, disk, uptime. 과거 배터리 컬럼은 호환을 위해 유지하지만 UPS 제거 상태에서는 새 배터리값을 기록하지 않습니다.
|
||||||
- `power_event_states`: 저전압/스로틀링 episode 상태, 최근 지속시간, 최근 이벤트 이력
|
- `power_event_states`: 저전압/스로틀링 episode 상태, 최근 지속시간, 최근 이벤트 이력
|
||||||
- `battery_profile_cache`: 배터리 잔여시간 계산용 장기 프로파일 캐시
|
- `battery_profile_cache`: 배터리 잔여시간 계산용 장기 프로파일 캐시
|
||||||
@@ -97,7 +99,7 @@ API 응답의 `battery.enabled`는 false, `battery.removed`는 true로 내려가
|
|||||||
|
|
||||||
설정 가능한 항목은 다음 범위입니다.
|
설정 가능한 항목은 다음 범위입니다.
|
||||||
|
|
||||||
- 팬 자동 제어: 팬 시작 온도, 최대 온도, 즉시 최대 PWM 온도, RP1 온도 반영 비율, 팬 판단 온도 여유, 자동 상승/하강 PWM step
|
- 팬 자동 제어: 팬 시작 온도, 최대 온도, 즉시 최대 PWM 온도, RP1 온도 반영 비율, 팬 판단 온도 여유, 자동 상승/하강 PWM step, 외출/귀가 팬 모드 자동 전환, 외출 시 PWM
|
||||||
- 보안 정책: 자동 로그인 유지 기간, remember 쿠키 Secure/SameSite, CSRF 길이, remember 토큰 길이, User-Agent/IP 접두사 검증, 로그인 실패 잠금, 재부팅 허용 여부, 재부팅 확인 문구, 명령 timeout, WiFi 조작 허용 여부
|
- 보안 정책: 자동 로그인 유지 기간, remember 쿠키 Secure/SameSite, CSRF 길이, remember 토큰 길이, User-Agent/IP 접두사 검증, 로그인 실패 잠금, 재부팅 허용 여부, 재부팅 확인 문구, 명령 timeout, WiFi 조작 허용 여부
|
||||||
- 화면/진단 표시: 프로세스 후보 수, 사용자 서비스 로그 줄 수, 사용자 서비스 캐시 시간, 팬 이상 이력 수
|
- 화면/진단 표시: 프로세스 후보 수, 사용자 서비스 로그 줄 수, 사용자 서비스 캐시 시간, 팬 이상 이력 수
|
||||||
- 터치 디스플레이: 현재 DSI 연결 상태, `800x480` 모드, framebuffer, 백라이트 밝기, 자동감지/수동 overlay 상태 표시와 밝기 즉시 적용, 콘솔 회전, LCD 출력, 터치 입력, `invx`/`invy`/`swapxy`, 터치 좌표 크기 저장
|
- 터치 디스플레이: 현재 DSI 연결 상태, `800x480` 모드, framebuffer, 백라이트 밝기, 자동감지/수동 overlay 상태 표시와 밝기 즉시 적용, 콘솔 회전, LCD 출력, 터치 입력, `invx`/`invy`/`swapxy`, 터치 좌표 크기 저장
|
||||||
@@ -118,6 +120,7 @@ API 응답의 `battery.enabled`는 false, `battery.removed`는 true로 내려가
|
|||||||
- UPS/배터리 잔여 시간: 현재 운영 구성에서는 표시하지 않음
|
- UPS/배터리 잔여 시간: 현재 운영 구성에서는 표시하지 않음
|
||||||
- dmesg: 진단 패널에서 열었을 때만 1초마다 갱신, 닫으면 중지
|
- dmesg: 진단 패널에서 열었을 때만 1초마다 갱신, 닫으면 중지
|
||||||
- WiFi 관측 보정 timer: 10초마다 `bin/wifi_observe.php` 실행
|
- WiFi 관측 보정 timer: 10초마다 `bin/wifi_observe.php` 실행
|
||||||
|
- 외출/귀가 팬 정책: 팬 정책 적용 루프에서 기본 30초마다 서울 HA의 휴대폰 device tracker를 확인
|
||||||
- WebSocket 소스 변경 감지: 15초마다 확인 후 재시작
|
- WebSocket 소스 변경 감지: 15초마다 확인 후 재시작
|
||||||
- 팬 슬라이더 자동 적용 debounce: 약 450ms
|
- 팬 슬라이더 자동 적용 debounce: 약 450ms
|
||||||
|
|
||||||
|
|||||||
@@ -170,6 +170,8 @@ function dmesg_log_throttled(string $key, string $message, int $seconds = 30): v
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
sync_presence_fan_policy();
|
||||||
|
|
||||||
$state = get_control_state();
|
$state = get_control_state();
|
||||||
|
|
||||||
$mode = (string)($state['mode'] ?? 'auto');
|
$mode = (string)($state['mode'] ?? 'auto');
|
||||||
|
|||||||
@@ -156,6 +156,9 @@ function setting_definitions(): array
|
|||||||
'fan.control_temp_headroom' => ['group' => 'fan', 'label' => '팬 판단 온도 여유', 'type' => 'number', 'default' => 1.5, 'min' => 0, 'max' => 8, 'step' => 0.1, 'unit' => 'C'],
|
'fan.control_temp_headroom' => ['group' => 'fan', 'label' => '팬 판단 온도 여유', 'type' => 'number', 'default' => 1.5, 'min' => 0, 'max' => 8, 'step' => 0.1, 'unit' => 'C'],
|
||||||
'fan.ramp_up_step' => ['group' => 'fan', 'label' => '자동 상승 PWM step', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 80, 'step' => 1, 'unit' => 'PWM'],
|
'fan.ramp_up_step' => ['group' => 'fan', 'label' => '자동 상승 PWM step', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 80, 'step' => 1, 'unit' => 'PWM'],
|
||||||
'fan.ramp_down_step' => ['group' => 'fan', 'label' => '자동 하강 PWM step', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 80, 'step' => 1, 'unit' => 'PWM'],
|
'fan.ramp_down_step' => ['group' => 'fan', 'label' => '자동 하강 PWM step', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 80, 'step' => 1, 'unit' => 'PWM'],
|
||||||
|
'fan.presence_policy_enabled' => ['group' => 'fan', 'label' => '외출/귀가 팬 모드 자동 전환', 'type' => 'boolean', 'default' => 1],
|
||||||
|
'fan.presence_check_seconds' => ['group' => 'fan', 'label' => '외출/귀가 확인 주기', 'type' => 'number', 'default' => 30, 'min' => 10, 'max' => 300, 'step' => 5, 'unit' => '초'],
|
||||||
|
'fan.away_manual_pwm' => ['group' => 'fan', 'label' => '외출 시 수동 PWM', 'type' => 'number', 'default' => 255, 'min' => 0, 'max' => 255, 'step' => 1, 'unit' => 'PWM'],
|
||||||
'event.recent_window_seconds' => ['group' => 'event', 'label' => '저전압/스로틀링 최근 판정창', 'type' => 'number', 'default' => 600, 'min' => 60, 'max' => 7200, 'step' => 30, 'unit' => '초'],
|
'event.recent_window_seconds' => ['group' => 'event', 'label' => '저전압/스로틀링 최근 판정창', 'type' => 'number', 'default' => 600, 'min' => 60, 'max' => 7200, 'step' => 30, 'unit' => '초'],
|
||||||
'event.recovery_seconds' => ['group' => 'event', 'label' => '복구 중 유지 시간', 'type' => 'number', 'default' => 300, 'min' => 30, 'max' => 3600, 'step' => 30, 'unit' => '초'],
|
'event.recovery_seconds' => ['group' => 'event', 'label' => '복구 중 유지 시간', 'type' => 'number', 'default' => 300, 'min' => 30, 'max' => 3600, 'step' => 30, 'unit' => '초'],
|
||||||
'event.repeated_count' => ['group' => 'event', 'label' => '반복 발생 기준 횟수', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 20, 'step' => 1, 'unit' => '회'],
|
'event.repeated_count' => ['group' => 'event', 'label' => '반복 발생 기준 횟수', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 20, 'step' => 1, 'unit' => '회'],
|
||||||
@@ -426,6 +429,22 @@ function bootstrap_db(): void
|
|||||||
COLLATE=utf8mb4_unicode_ci
|
COLLATE=utf8mb4_unicode_ci
|
||||||
");
|
");
|
||||||
|
|
||||||
|
$pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS presence_fan_policy_state (
|
||||||
|
id TINYINT UNSIGNED NOT NULL PRIMARY KEY,
|
||||||
|
last_presence VARCHAR(16) NULL,
|
||||||
|
last_checked_at DATETIME NULL,
|
||||||
|
last_seen_at DATETIME NULL,
|
||||||
|
last_applied_presence VARCHAR(16) NULL,
|
||||||
|
last_applied_at DATETIME NULL,
|
||||||
|
last_error VARCHAR(255) NULL,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB
|
||||||
|
DEFAULT CHARSET=utf8mb4
|
||||||
|
COLLATE=utf8mb4_unicode_ci
|
||||||
|
");
|
||||||
|
|
||||||
$pdo->exec("
|
$pdo->exec("
|
||||||
CREATE TABLE IF NOT EXISTS power_event_states (
|
CREATE TABLE IF NOT EXISTS power_event_states (
|
||||||
event_key VARCHAR(32) NOT NULL PRIMARY KEY,
|
event_key VARCHAR(32) NOT NULL PRIMARY KEY,
|
||||||
@@ -617,6 +636,13 @@ function bootstrap_db(): void
|
|||||||
(1, 'auto', 120)
|
(1, 'auto', 120)
|
||||||
");
|
");
|
||||||
|
|
||||||
|
$pdo->exec("
|
||||||
|
INSERT IGNORE INTO presence_fan_policy_state
|
||||||
|
(id)
|
||||||
|
VALUES
|
||||||
|
(1)
|
||||||
|
");
|
||||||
|
|
||||||
foreach ([
|
foreach ([
|
||||||
"ALTER TABLE sensor_logs ADD COLUMN create_ip VARCHAR(64) NULL",
|
"ALTER TABLE sensor_logs ADD COLUMN create_ip VARCHAR(64) NULL",
|
||||||
"ALTER TABLE sensor_logs ADD COLUMN disk_total_kb BIGINT NULL AFTER mem_free_mb",
|
"ALTER TABLE sensor_logs ADD COLUMN disk_total_kb BIGINT NULL AFTER mem_free_mb",
|
||||||
@@ -1114,6 +1140,237 @@ function set_control_state(string $mode, int $manualPwm): void
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function presence_fan_policy_state(): array
|
||||||
|
{
|
||||||
|
$stmt = db()->query("
|
||||||
|
SELECT *
|
||||||
|
FROM presence_fan_policy_state
|
||||||
|
WHERE id = 1
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
|
||||||
|
return $stmt->fetch() ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_presence_fan_policy_state(array $values): void
|
||||||
|
{
|
||||||
|
$allowed = [
|
||||||
|
'last_presence',
|
||||||
|
'last_checked_at',
|
||||||
|
'last_seen_at',
|
||||||
|
'last_applied_presence',
|
||||||
|
'last_applied_at',
|
||||||
|
'last_error',
|
||||||
|
];
|
||||||
|
|
||||||
|
$sets = [];
|
||||||
|
$params = [];
|
||||||
|
foreach ($allowed as $key) {
|
||||||
|
if (!array_key_exists($key, $values)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$sets[] = $key . ' = :' . $key;
|
||||||
|
$params[':' . $key] = $values[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sets === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = db()->prepare("
|
||||||
|
UPDATE presence_fan_policy_state
|
||||||
|
SET " . implode(', ', $sets) . "
|
||||||
|
WHERE id = 1
|
||||||
|
");
|
||||||
|
$stmt->execute($params);
|
||||||
|
}
|
||||||
|
|
||||||
|
function control_common_config(): array
|
||||||
|
{
|
||||||
|
static $config = null;
|
||||||
|
if ($config !== null) {
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = '/mnt/synology-web/custom/common/config.php';
|
||||||
|
if (!is_file($file)) {
|
||||||
|
return $config = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$loaded = require $file;
|
||||||
|
return $config = is_array($loaded) ? $loaded : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function control_ha_state(string $base, string $token, string $entity): array
|
||||||
|
{
|
||||||
|
$ch = curl_init(rtrim($base, '/') . '/api/states/' . rawurlencode($entity));
|
||||||
|
if (!$ch) {
|
||||||
|
throw new RuntimeException('HA curl init failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Authorization: Bearer ' . $token,
|
||||||
|
'Content-Type: application/json',
|
||||||
|
],
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 5,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$errno = curl_errno($ch);
|
||||||
|
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($errno !== 0 || $status < 200 || $status >= 300) {
|
||||||
|
throw new RuntimeException('HA state read failed: status=' . $status . ', errno=' . $errno);
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_decode((string)$response, true);
|
||||||
|
if (!is_array($json)) {
|
||||||
|
throw new RuntimeException('HA state JSON parse failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $json;
|
||||||
|
}
|
||||||
|
|
||||||
|
function control_distance_m(float $lat1, float $lon1, float $lat2, float $lon2): float
|
||||||
|
{
|
||||||
|
$earth = 6371000.0;
|
||||||
|
$dLat = deg2rad($lat2 - $lat1);
|
||||||
|
$dLon = deg2rad($lon2 - $lon1);
|
||||||
|
$a = sin($dLat / 2) ** 2
|
||||||
|
+ cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon / 2) ** 2;
|
||||||
|
|
||||||
|
return $earth * 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||||
|
}
|
||||||
|
|
||||||
|
function current_control_presence(): array
|
||||||
|
{
|
||||||
|
$config = control_common_config();
|
||||||
|
$ha = $config['ha']['seoul'] ?? [];
|
||||||
|
$entity = (string)($config['entities']['device'] ?? '');
|
||||||
|
$home = $config['findmydevice_home'] ?? [];
|
||||||
|
|
||||||
|
$base = (string)($ha['base'] ?? '');
|
||||||
|
$token = (string)($ha['token'] ?? '');
|
||||||
|
if ($base === '' || $token === '' || $entity === '') {
|
||||||
|
throw new RuntimeException('Seoul HA presence config is missing.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$state = control_ha_state($base, $token, $entity);
|
||||||
|
$rawState = strtolower(trim((string)($state['state'] ?? '')));
|
||||||
|
$attrs = is_array($state['attributes'] ?? null) ? $state['attributes'] : [];
|
||||||
|
|
||||||
|
if (in_array($rawState, ['home'], true)) {
|
||||||
|
return ['presence' => 'home', 'source' => 'device_tracker', 'raw_state' => $rawState];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($rawState, ['not_home', 'away'], true)) {
|
||||||
|
return ['presence' => 'away', 'source' => 'device_tracker', 'raw_state' => $rawState];
|
||||||
|
}
|
||||||
|
|
||||||
|
$lat = $attrs['latitude'] ?? null;
|
||||||
|
$lon = $attrs['longitude'] ?? null;
|
||||||
|
if (is_numeric($lat) && is_numeric($lon) && is_numeric($home['latitude'] ?? null) && is_numeric($home['longitude'] ?? null)) {
|
||||||
|
$radius = max(1.0, (float)($home['radius_m'] ?? 150));
|
||||||
|
$distance = control_distance_m((float)$home['latitude'], (float)$home['longitude'], (float)$lat, (float)$lon);
|
||||||
|
return [
|
||||||
|
'presence' => $distance <= $radius ? 'home' : 'away',
|
||||||
|
'source' => 'gps_distance',
|
||||||
|
'raw_state' => $rawState,
|
||||||
|
'distance_m' => round($distance, 1),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException('Presence state is unavailable: ' . ($rawState ?: 'empty'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sync_presence_fan_policy(bool $force = false): array
|
||||||
|
{
|
||||||
|
if (!(bool)setting_value('fan.presence_policy_enabled')) {
|
||||||
|
return ['ok' => true, 'skipped' => 'disabled'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$state = presence_fan_policy_state();
|
||||||
|
$lastChecked = strtotime((string)($state['last_checked_at'] ?? '')) ?: 0;
|
||||||
|
$interval = max(10, (int)setting_value('fan.presence_check_seconds'));
|
||||||
|
if (!$force && $lastChecked > 0 && time() - $lastChecked < $interval) {
|
||||||
|
return ['ok' => true, 'skipped' => 'throttled'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$presence = current_control_presence();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
update_presence_fan_policy_state([
|
||||||
|
'last_checked_at' => $now,
|
||||||
|
'last_error' => mb_substr($e->getMessage(), 0, 255, 'UTF-8'),
|
||||||
|
]);
|
||||||
|
return ['ok' => false, 'error' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentPresence = (string)$presence['presence'];
|
||||||
|
$previousPresence = (string)($state['last_presence'] ?? '');
|
||||||
|
$changed = $previousPresence === '' || $previousPresence !== $currentPresence;
|
||||||
|
$update = [
|
||||||
|
'last_presence' => $currentPresence,
|
||||||
|
'last_checked_at' => $now,
|
||||||
|
'last_seen_at' => $now,
|
||||||
|
'last_error' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$applied = false;
|
||||||
|
if ($changed) {
|
||||||
|
$fan = get_control_state();
|
||||||
|
$mode = (string)($fan['mode'] ?? 'auto');
|
||||||
|
$manualPwm = max(0, min(255, (int)($fan['manual_pwm'] ?? 120)));
|
||||||
|
$targetPwm = max(0, min(255, (int)setting_value('fan.away_manual_pwm')));
|
||||||
|
|
||||||
|
if ($currentPresence === 'away' && ($mode !== 'manual' || $manualPwm !== $targetPwm)) {
|
||||||
|
set_control_state('manual', $targetPwm);
|
||||||
|
add_fan_action(
|
||||||
|
'presence_fan_policy',
|
||||||
|
'manual',
|
||||||
|
$targetPwm,
|
||||||
|
'presence=away, previous_presence=' . ($previousPresence ?: 'unknown')
|
||||||
|
. ', previous_fan=' . $mode . ':' . $manualPwm
|
||||||
|
. ', source=' . ($presence['source'] ?? 'unknown'),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
$applied = true;
|
||||||
|
} elseif ($currentPresence === 'home' && $mode !== 'auto') {
|
||||||
|
set_control_state('auto', $manualPwm);
|
||||||
|
add_fan_action(
|
||||||
|
'presence_fan_policy',
|
||||||
|
'auto',
|
||||||
|
$manualPwm,
|
||||||
|
'presence=home, previous_presence=' . ($previousPresence ?: 'unknown')
|
||||||
|
. ', previous_fan=' . $mode . ':' . $manualPwm
|
||||||
|
. ', source=' . ($presence['source'] ?? 'unknown'),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
$applied = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($applied) {
|
||||||
|
$update['last_applied_presence'] = $currentPresence;
|
||||||
|
$update['last_applied_at'] = $now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_presence_fan_policy_state($update);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'presence' => $currentPresence,
|
||||||
|
'changed' => $changed,
|
||||||
|
'applied' => $applied,
|
||||||
|
'source' => $presence['source'] ?? 'unknown',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function rp1_temp_c(): ?float
|
function rp1_temp_c(): ?float
|
||||||
{
|
{
|
||||||
foreach (glob('/sys/class/hwmon/hwmon*') ?: [] as $dir) {
|
foreach (glob('/sys/class/hwmon/hwmon*') ?: [] as $dir) {
|
||||||
|
|||||||
@@ -519,6 +519,8 @@ function write_sys_value(string $path, int $value): bool
|
|||||||
|
|
||||||
function apply_fan_policy(): array
|
function apply_fan_policy(): array
|
||||||
{
|
{
|
||||||
|
sync_presence_fan_policy();
|
||||||
|
|
||||||
$state = get_control_state();
|
$state = get_control_state();
|
||||||
$paths = fan_paths();
|
$paths = fan_paths();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user