잔여 시간 계산에 장기 학습 프로파일 반영
This commit is contained in:
+202
-3
@@ -793,6 +793,91 @@ function battery_trend_history(int $hours = 24): array
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function battery_profile_history(int $days = 45): array
|
||||
{
|
||||
$days = max(3, min(90, $days));
|
||||
static $cache = [];
|
||||
$cacheKey = (string)$days;
|
||||
$now = time();
|
||||
$cacheFile = sys_get_temp_dir() . '/control-battery-profile-' . $days . 'd.json';
|
||||
|
||||
if (
|
||||
isset($cache[$cacheKey])
|
||||
&& ($now - (int)$cache[$cacheKey]['time']) < 600
|
||||
&& is_array($cache[$cacheKey]['rows'])
|
||||
) {
|
||||
return $cache[$cacheKey]['rows'];
|
||||
}
|
||||
|
||||
if (is_readable($cacheFile)) {
|
||||
$raw = @file_get_contents($cacheFile);
|
||||
$decoded = is_string($raw) ? json_decode($raw, true) : null;
|
||||
if (
|
||||
is_array($decoded)
|
||||
&& isset($decoded['created_at'], $decoded['rows'])
|
||||
&& is_array($decoded['rows'])
|
||||
&& ($now - (int)$decoded['created_at']) < 21600
|
||||
) {
|
||||
$cache[$cacheKey] = [
|
||||
'time' => $now,
|
||||
'rows' => $decoded['rows'],
|
||||
];
|
||||
return $decoded['rows'];
|
||||
}
|
||||
}
|
||||
|
||||
$maxId = (int)db()->query("SELECT MAX(id) FROM sensor_logs")->fetchColumn();
|
||||
$minId = max(1, $maxId - 3000000);
|
||||
|
||||
$stmt = db()->query("
|
||||
SELECT
|
||||
recorded_at AS time,
|
||||
battery_percent,
|
||||
battery_voltage,
|
||||
cpu_watts,
|
||||
cpu_load_1,
|
||||
1 AS samples
|
||||
FROM sensor_logs FORCE INDEX (idx_recorded_at)
|
||||
WHERE recorded_at >= DATE_SUB(NOW(), INTERVAL {$days} DAY)
|
||||
AND battery_percent IS NOT NULL
|
||||
AND id >= {$minId}
|
||||
AND MOD(id, 300) = 0
|
||||
ORDER BY recorded_at ASC
|
||||
");
|
||||
|
||||
$rows = [];
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
$time = strtotime((string)($row['time'] ?? ''));
|
||||
if ($time <= 0 || !is_numeric($row['battery_percent'] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'time' => $time,
|
||||
'battery_percent' => (float)$row['battery_percent'],
|
||||
'battery_voltage' => is_numeric($row['battery_voltage'] ?? null) ? (float)$row['battery_voltage'] : null,
|
||||
'cpu_watts' => is_numeric($row['cpu_watts'] ?? null) ? (float)$row['cpu_watts'] : null,
|
||||
'cpu_load_1' => is_numeric($row['cpu_load_1'] ?? null) ? (float)$row['cpu_load_1'] : null,
|
||||
'samples' => (int)($row['samples'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
$cache[$cacheKey] = [
|
||||
'time' => $now,
|
||||
'rows' => $rows,
|
||||
];
|
||||
@file_put_contents(
|
||||
$cacheFile,
|
||||
json_encode([
|
||||
'created_at' => $now,
|
||||
'days' => $days,
|
||||
'rows' => $rows,
|
||||
], JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function battery_trend_candidate(array $trendRows, float $currentPercent, int $windowSeconds, ?float $recentWatts): ?array
|
||||
{
|
||||
if (count($trendRows) < 6) {
|
||||
@@ -862,6 +947,110 @@ function battery_trend_candidate(array $trendRows, float $currentPercent, int $w
|
||||
'weight' => max(0.05, $confidence),
|
||||
'avg_watts' => $windowWatts === null ? null : round($windowWatts, 3),
|
||||
'load_factor' => round($loadFactor, 3),
|
||||
'source' => 'recent_window',
|
||||
];
|
||||
}
|
||||
|
||||
function battery_learned_profile_candidate(array $profileRows, float $currentPercent, ?float $recentWatts): ?array
|
||||
{
|
||||
if (count($profileRows) < 24) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$rates = [];
|
||||
$dropSum = 0.0;
|
||||
$sampleSum = 0;
|
||||
$socBuckets = [];
|
||||
$minSoc = max(0.0, $currentPercent - 70.0);
|
||||
$maxSoc = min(100.0, $currentPercent + 8.0);
|
||||
|
||||
for ($i = 1, $count = count($profileRows); $i < $count; $i++) {
|
||||
$prev = $profileRows[$i - 1];
|
||||
$row = $profileRows[$i];
|
||||
$elapsed = (int)$row['time'] - (int)$prev['time'];
|
||||
if ($elapsed < 240 || $elapsed > 900) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$startSoc = (float)$prev['battery_percent'];
|
||||
$endSoc = (float)$row['battery_percent'];
|
||||
$avgSoc = ($startSoc + $endSoc) / 2;
|
||||
if ($avgSoc < $minSoc || $avgSoc > $maxSoc) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$drop = $startSoc - $endSoc;
|
||||
if ($drop < 0.03 || $drop > 8.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$avgWatts = numeric_trimmed_average([$prev['cpu_watts'] ?? null, $row['cpu_watts'] ?? null], 0.0);
|
||||
$loadFactor = 1.0;
|
||||
if ($recentWatts !== null && $recentWatts > 0 && $avgWatts !== null && $avgWatts > 0) {
|
||||
$loadFactor = 1 + ((clamp_float($recentWatts / $avgWatts, 0.55, 1.95) - 1) * 0.5);
|
||||
}
|
||||
|
||||
$socDistance = abs($currentPercent - $avgSoc);
|
||||
$socWeight = 1 / (1 + ($socDistance / 18));
|
||||
$ageDays = max(0.0, ($now - (int)$row['time']) / 86400);
|
||||
$recencyWeight = 1 / (1 + ($ageDays / 45));
|
||||
$sampleWeight = min(1.0, max(1, ((int)$prev['samples'] + (int)$row['samples']) / 2) / 30);
|
||||
$weight = max(0.01, $socWeight * $recencyWeight * $sampleWeight);
|
||||
|
||||
$rates[] = [
|
||||
'rate' => ($drop / $elapsed) * $loadFactor,
|
||||
'weight' => $weight,
|
||||
'soc' => $avgSoc,
|
||||
'drop' => $drop,
|
||||
'watts' => $avgWatts,
|
||||
];
|
||||
$dropSum += $drop;
|
||||
$sampleSum += (int)$prev['samples'] + (int)$row['samples'];
|
||||
$bucket = (int)(floor($avgSoc / 10) * 10);
|
||||
$socBuckets[$bucket] = true;
|
||||
}
|
||||
|
||||
if (count($rates) < 12 || $dropSum < 1.0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$weightedRate = 0.0;
|
||||
$weightSum = 0.0;
|
||||
foreach ($rates as $row) {
|
||||
$weightedRate += (float)$row['rate'] * (float)$row['weight'];
|
||||
$weightSum += (float)$row['weight'];
|
||||
}
|
||||
|
||||
if ($weightedRate <= 0 || $weightSum <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rate = $weightedRate / $weightSum;
|
||||
$seconds = (int)round($currentPercent / $rate);
|
||||
if ($seconds <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$watts = numeric_trimmed_average(array_column($rates, 'watts'), 0.1);
|
||||
$confidence = min(1.0, count($rates) / 240)
|
||||
* min(1.0, $dropSum / 18)
|
||||
* min(1.0, count($socBuckets) / 5);
|
||||
|
||||
return [
|
||||
'seconds' => $seconds,
|
||||
'rate_per_second' => $rate,
|
||||
'drop_percent' => round($dropSum, 3),
|
||||
'elapsed_seconds' => null,
|
||||
'window_seconds' => null,
|
||||
'sample_count' => $sampleSum,
|
||||
'confidence' => round($confidence, 4),
|
||||
'weight' => max(0.05, $confidence * 0.9),
|
||||
'avg_watts' => $watts === null ? null : round($watts, 3),
|
||||
'load_factor' => null,
|
||||
'source' => 'learned_profile',
|
||||
'intervals' => count($rates),
|
||||
'soc_buckets' => count($socBuckets),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -898,7 +1087,7 @@ function battery_power_fallback_estimate(float $percent, array $history): array
|
||||
];
|
||||
}
|
||||
|
||||
function battery_remaining_estimate(array $battery, array $history, array $trendRows = []): array
|
||||
function battery_remaining_estimate(array $battery, array $history, array $trendRows = [], array $profileRows = []): array
|
||||
{
|
||||
$percent = $battery['percent'] ?? null;
|
||||
if ($percent === null || $percent === '' || !is_numeric($percent)) {
|
||||
@@ -922,6 +1111,11 @@ function battery_remaining_estimate(array $battery, array $history, array $trend
|
||||
}
|
||||
}
|
||||
|
||||
$learned = battery_learned_profile_candidate($profileRows, $percent, $recentWatts);
|
||||
if ($learned !== null) {
|
||||
$candidates[] = $learned;
|
||||
}
|
||||
|
||||
if ($candidates !== []) {
|
||||
$weightedRate = 0.0;
|
||||
$weightSum = 0.0;
|
||||
@@ -944,12 +1138,16 @@ function battery_remaining_estimate(array $battery, array $history, array $trend
|
||||
'recent_watts' => $recentWatts === null ? null : round($recentWatts, 3),
|
||||
'drop_per_hour' => round($rate * 3600, 3),
|
||||
'confidence' => round(array_sum(array_column($candidates, 'confidence')) / count($candidates), 3),
|
||||
'sources' => array_values(array_unique(array_column($candidates, 'source'))),
|
||||
'windows' => array_map(static function (array $candidate): array {
|
||||
return [
|
||||
'minutes' => (int)round($candidate['window_seconds'] / 60),
|
||||
'source' => $candidate['source'],
|
||||
'minutes' => $candidate['window_seconds'] === null ? null : (int)round($candidate['window_seconds'] / 60),
|
||||
'drop_percent' => $candidate['drop_percent'],
|
||||
'confidence' => $candidate['confidence'],
|
||||
'load_factor' => $candidate['load_factor'],
|
||||
'intervals' => $candidate['intervals'] ?? null,
|
||||
'soc_buckets' => $candidate['soc_buckets'] ?? null,
|
||||
];
|
||||
}, $candidates),
|
||||
];
|
||||
@@ -2346,7 +2544,8 @@ function collect_snapshot(bool $applyFan = true): array
|
||||
|
||||
$history = add_battery_remaining_history(sensor_history(240));
|
||||
$batteryTrend = battery_trend_history(24);
|
||||
$battery['remaining'] = battery_remaining_estimate($battery, $history, $batteryTrend);
|
||||
$batteryProfile = battery_profile_history(45);
|
||||
$battery['remaining'] = battery_remaining_estimate($battery, $history, $batteryTrend, $batteryProfile);
|
||||
|
||||
$processes = process_resource_data(6);
|
||||
$fanSpike = fan_spike_analysis($history, $fan, $system, $processes);
|
||||
|
||||
Reference in New Issue
Block a user