Optimize Find My Device history

This commit is contained in:
seo
2026-06-12 16:42:50 +09:00
parent 5d73ac520b
commit f1f038ce23
3 changed files with 360 additions and 181 deletions
+179 -20
View File
@@ -76,6 +76,8 @@ if ($startParam && $endParam) {
// API URL 생성
$historyEndpoint = "/history/period/{$startTime}?filter_entity_id=" . rawurlencode($entities['device']) . "&end_time={$endTime}";
$includeHistory = isset($_GET['history']) && $_GET['history'] === '1' && !isset($_GET['hashonly']);
$historyMaxPoints = max(20, min(1000, (int)($_GET['maxPoints'] ?? 300)));
// 공통 요청 함수
function getData($entityId) {
@@ -111,29 +113,59 @@ function mappedText($state, $lang, $map) {
return $map[$lang][$state] ?? $map[$lang][strtolower((string)$state)] ?? $state;
}
function durationText(int $seconds, string $lang): ?string
{
if ($seconds <= 0) return null;
$days = intdiv($seconds, 86400);
$seconds %= 86400;
$hours = intdiv($seconds, 3600);
$seconds %= 3600;
$minutes = intdiv($seconds, 60);
$seconds %= 60;
$parts = [];
if ($lang === 'ko') {
if ($days) $parts[] = $days . '일';
if ($hours) $parts[] = $hours . '시간';
if ($minutes) $parts[] = $minutes . '분';
if ($seconds && !$days && !$hours) $parts[] = $seconds . '초';
} else {
if ($days) $parts[] = $days . 'd';
if ($hours) $parts[] = $hours . 'h';
if ($minutes) $parts[] = $minutes . 'm';
if ($seconds && !$days && !$hours) $parts[] = $seconds . 's';
}
return implode(' ', $parts) ?: null;
}
function formatSensorValue($value, $definition, $lang, $maps) {
if ($value === null) return null;
if (isset($definition['omit_values'])) {
$omitValues = array_map('strval', (array)$definition['omit_values']);
if (in_array((string)$value, $omitValues, true)) {
return null;
}
}
if (($definition['format'] ?? '') === 'bool') {
return boolText($value, $lang);
}
if (($definition['format'] ?? '') === 'duration') {
if (!is_numeric($value)) return $value;
$seconds = (int)$value;
if ($seconds <= 0) return null;
$days = intdiv($seconds, 86400);
$seconds %= 86400;
$hours = intdiv($seconds, 3600);
$seconds %= 3600;
$minutes = intdiv($seconds, 60);
$seconds %= 60;
$parts = [];
if ($days) $parts[] = $days . 'd';
if ($hours) $parts[] = $hours . 'h';
if ($minutes) $parts[] = $minutes . 'm';
if ($seconds && !$days && !$hours) $parts[] = $seconds . 's';
return implode(' ', $parts) ?: null;
return durationText((int)$value, $lang);
}
if (($definition['format'] ?? '') === 'duration_ms') {
if (!is_numeric($value)) return $value;
return durationText((int)round(((float)$value) / 1000), $lang);
}
if (($definition['format'] ?? '') === 'duration_min') {
if (!is_numeric($value)) return $value;
return durationText((int)round(((float)$value) * 60), $lang);
}
if (($definition['format'] ?? '') === 'datetime') {
@@ -144,6 +176,14 @@ function formatSensorValue($value, $definition, $lang, $maps) {
$value = mappedText($value, $lang, $maps[$definition['map']]);
}
if (isset($definition['scale']) && is_numeric($value)) {
$value = (float)$value * (float)$definition['scale'];
}
if (isset($definition['decimals']) && is_numeric($value)) {
$value = round((float)$value, (int)$definition['decimals']);
}
if (isset($definition['unit']) && is_numeric($value)) {
return $value . $definition['unit'];
}
@@ -162,7 +202,7 @@ $ascendedData = getData($fascendedUrl);
$descendedData = getData($fdescendedUrl);
$stepsData = getData($stepsUrl);
$distanceData = getData($distanceUrl);
$historyData = custom_ha_request('seoul', 'GET', $historyEndpoint) ?? [];
$historyData = $includeHistory ? (custom_ha_request('seoul', 'GET', $historyEndpoint) ?? []) : [];
$triggerData = getData($triggerUrl);
// 경로 기록
@@ -178,23 +218,80 @@ if (isset($historyData[0])) {
}
}
function path_distance_m(array $a, array $b): float
{
$earth = 6371000;
$lat1 = deg2rad((float)$a['lat']);
$lat2 = deg2rad((float)$b['lat']);
$dLat = $lat2 - $lat1;
$dLng = deg2rad((float)$b['lng'] - (float)$a['lng']);
$h = sin($dLat / 2) ** 2 + cos($lat1) * cos($lat2) * sin($dLng / 2) ** 2;
return 2 * $earth * asin(min(1, sqrt($h)));
}
function simplify_path(array $path, int $maxPoints): array
{
$count = count($path);
if ($count <= $maxPoints) {
return $path;
}
$filtered = [];
$last = null;
foreach ($path as $point) {
if ($last === null || path_distance_m($last, $point) >= 15) {
$filtered[] = $point;
$last = $point;
}
}
if (count($filtered) <= $maxPoints) {
return $filtered;
}
$sampled = [];
$lastIndex = count($filtered) - 1;
for ($i = 0; $i < $maxPoints; $i++) {
$idx = (int)round($i * $lastIndex / max(1, $maxPoints - 1));
$sampled[] = $filtered[$idx];
}
return $sampled;
}
$path = simplify_path($path, $historyMaxPoints);
$lang = $_GET['lang'] ?? 'en';
$activityMap = [
'ko' => [
'Stationary' => '가만히 있음',
'still' => '가만히 있음',
'Walking' => '걷는 중',
'walking' => '걷는 중',
'Running' => '달리는 중',
'running' => '달리는 중',
'Automotive' => '차량 이동 중',
'in_vehicle' => '차량 이동 중',
'Cycling' => '자전거 이동 중',
'on_bicycle' => '자전거 이동 중',
'on_foot' => '도보 이동 중',
'tilting' => '기울어짐',
'Unknown' => '-',
],
'en' => [
'Stationary' => 'Stationary',
'still' => 'Stationary',
'Walking' => 'Walking',
'walking' => 'Walking',
'Running' => 'Running',
'running' => 'Running',
'Automotive' => 'Automotive',
'in_vehicle' => 'In vehicle',
'Cycling' => 'Cycling',
'on_bicycle' => 'On bicycle',
'on_foot' => 'On foot',
'tilting' => 'Tilting',
'Unknown' => '-',
]
];
@@ -217,13 +314,23 @@ $aaccuracyMap = [
$connectionMap = [
'ko' => [
'Wi-Fi' => '와이파이',
'wifi' => '와이파이',
'Cellular' => '셀룰러',
'cellular' => '셀룰러',
'ethernet' => '이더넷',
'bluetooth' => '블루투스',
'vpn' => 'VPN',
'none' => '-',
'unknown' => '-',
],
'en' => [
'Wi-Fi' => 'Wi-Fi',
'wifi' => 'Wi-Fi',
'Cellular' => 'Cellular',
'cellular' => 'Cellular',
'ethernet' => 'Ethernet',
'bluetooth' => 'Bluetooth',
'vpn' => 'VPN',
'none' => '-',
'unknown' => '-',
]
@@ -256,17 +363,61 @@ $batteryStateMap = [
$androidValueMap = [
'chargerType' => [
'ko' => ['none' => '없음', 'wireless' => '무선', 'ac' => '유선', 'usb' => 'USB'],
'en' => ['none' => 'None', 'wireless' => 'Wireless', 'ac' => 'AC', 'usb' => 'USB'],
'ko' => ['none' => '없음', 'wireless' => '무선', 'ac' => '유선', 'usb' => 'USB', 'dock' => '도크'],
'en' => ['none' => 'None', 'wireless' => 'Wireless', 'ac' => 'AC', 'usb' => 'USB', 'dock' => 'Dock'],
],
'batteryHealth' => [
'ko' => ['good' => '양호', 'overheat' => '과열', 'dead' => '불량', 'over_voltage' => '과전압', 'cold' => '저온'],
'en' => ['good' => 'Good', 'overheat' => 'Overheat', 'dead' => 'Dead', 'over_voltage' => 'Over voltage', 'cold' => 'Cold'],
'ko' => ['good' => '양호', 'overheat' => '과열', 'overheated' => '과열', 'dead' => '불량', 'failed' => '오류', 'over_voltage' => '과전압', 'cold' => '저온'],
'en' => ['good' => 'Good', 'overheat' => 'Overheated', 'overheated' => 'Overheated', 'dead' => 'Dead', 'failed' => 'Failed', 'over_voltage' => 'Over voltage', 'cold' => 'Cold'],
],
'phoneState' => [
'ko' => ['idle' => '대기', 'ringing' => '수신 중', 'offhook' => '통화 중'],
'en' => ['idle' => 'Idle', 'ringing' => 'Ringing', 'offhook' => 'Off hook'],
],
'networkType' => [
'ko' => ['cellular' => '셀룰러', 'wifi' => 'Wi-Fi', 'ethernet' => '이더넷', 'bluetooth' => '블루투스', 'vpn' => 'VPN', 'usb' => 'USB', 'lowpan' => 'LoWPAN', 'wifi_aware' => 'Wi-Fi Aware'],
'en' => ['cellular' => 'Cellular', 'wifi' => 'Wi-Fi', 'ethernet' => 'Ethernet', 'bluetooth' => 'Bluetooth', 'vpn' => 'VPN', 'usb' => 'USB', 'lowpan' => 'LoWPAN', 'wifi_aware' => 'Wi-Fi Aware'],
],
'appStandbyBucket' => [
'ko' => ['never' => '제한 없음', 'active' => '활성', 'working_set' => '작업 세트', 'frequent' => '자주 사용', 'rare' => '드묾', 'restricted' => '제한됨'],
'en' => ['never' => 'Never', 'active' => 'Active', 'working_set' => 'Working set', 'frequent' => 'Frequent', 'rare' => 'Rare', 'restricted' => 'Restricted'],
],
'appImportance' => [
'ko' => ['foreground' => '포그라운드', 'foreground_service' => '포그라운드 서비스', 'visible' => '표시 중', 'perceptible' => '인지 가능', 'service' => '서비스', 'cached' => '캐시됨', 'gone' => '종료됨'],
'en' => ['foreground' => 'Foreground', 'foreground_service' => 'Foreground service', 'visible' => 'Visible', 'perceptible' => 'Perceptible', 'service' => 'Service', 'cached' => 'Cached', 'gone' => 'Gone'],
],
'screenOrientation' => [
'ko' => ['portrait' => '세로', 'landscape' => '가로', 'square' => '정사각형'],
'en' => ['portrait' => 'Portrait', 'landscape' => 'Landscape', 'square' => 'Square'],
],
'doNotDisturb' => [
'ko' => ['off' => '꺼짐', 'alarms_only' => '알람만', 'priority_only' => '우선순위만', 'total_silence' => '완전 무음'],
'en' => ['off' => 'Off', 'alarms_only' => 'Alarms only', 'priority_only' => 'Priority only', 'total_silence' => 'Total silence'],
],
'ringerMode' => [
'ko' => ['normal' => '일반', 'silent' => '무음', 'vibrate' => '진동'],
'en' => ['normal' => 'Normal', 'silent' => 'Silent', 'vibrate' => 'Vibrate'],
],
'audioMode' => [
'ko' => ['normal' => '일반', 'ringing' => '벨 울림', 'in_call' => '통화 중', 'in_communication' => '통신 중', 'call_screening' => '통화 선별', 'call_redirect' => '통화 전환', 'communication_redirect' => '통신 전환'],
'en' => ['normal' => 'Normal', 'ringing' => 'Ringing', 'in_call' => 'In call', 'in_communication' => 'In communication', 'call_screening' => 'Call screening', 'call_redirect' => 'Call redirect', 'communication_redirect' => 'Communication redirect'],
],
'bleTransmitter' => [
'ko' => ['Transmitting' => '송신 중', 'Stopped' => '중지됨', 'Bluetooth is turned off' => '블루투스 꺼짐', 'Unable to transmit' => '송신 불가'],
'en' => ['Transmitting' => 'Transmitting', 'Stopped' => 'Stopped', 'Bluetooth is turned off' => 'Bluetooth is off', 'Unable to transmit' => 'Unable to transmit'],
],
'beaconMonitor' => [
'ko' => ['Monitoring' => '감시 중', 'Stopped' => '중지됨', 'Bluetooth is turned off' => '블루투스 꺼짐'],
'en' => ['Monitoring' => 'Monitoring', 'Stopped' => 'Stopped', 'Bluetooth is turned off' => 'Bluetooth is off'],
],
'mediaSession' => [
'ko' => ['Playing' => '재생 중', 'Paused' => '일시정지', 'Stopped' => '중지됨', 'Buffering' => '버퍼링', 'Connecting' => '연결 중', 'Error' => '오류', 'Fast Forwarding' => '빨리감기', 'None' => '없음', 'Rewinding' => '되감기', 'Skip to Next' => '다음으로 이동', 'Skip to Previous' => '이전으로 이동', 'Skip to Queue Item' => '대기열 항목 이동'],
'en' => ['Playing' => 'Playing', 'Paused' => 'Paused', 'Stopped' => 'Stopped', 'Buffering' => 'Buffering', 'Connecting' => 'Connecting', 'Error' => 'Error', 'Fast Forwarding' => 'Fast forwarding', 'None' => 'None', 'Rewinding' => 'Rewinding', 'Skip to Next' => 'Skip to next', 'Skip to Previous' => 'Skip to previous', 'Skip to Queue Item' => 'Skip to queue item'],
],
'proximity' => [
'ko' => ['far' => '멀리', 'near' => '가까이'],
'en' => ['far' => 'Far', 'near' => 'Near'],
],
];
$triggerMap = [
@@ -282,6 +433,10 @@ $triggerMap = [
"Watch Context" => "워치 상태 전송",
"Geographic Region Entered" => "지정 지역에 들어옴",
"Geographic Region Exited" => "지정 지역을 벗어남",
"android.intent.action.TIME_TICK" => "Android 시간 틱",
"android.intent.action.SCREEN_ON" => "화면 켜짐",
"android.intent.action.SCREEN_OFF" => "화면 꺼짐",
"io.homeassistant.companion.android.UPDATE_SENSORS" => "센서 업데이트",
],
'en' => [
"Launch" => "Launch",
@@ -295,6 +450,10 @@ $triggerMap = [
"Watch Context" => "Watch Context",
"Geographic Region Entered" => "Geographic Region Entered",
"Geographic Region Exited" => "Geographic Region Exited",
"android.intent.action.TIME_TICK" => "Android Time Tick",
"android.intent.action.SCREEN_ON" => "Screen On",
"android.intent.action.SCREEN_OFF" => "Screen Off",
"io.homeassistant.companion.android.UPDATE_SENSORS" => "Sensor Update",
]
];
@@ -383,6 +542,6 @@ echo json_encode([
'aaccuracy' => $aaccuracyMap[$lang][$aaccuracyState] ?? $aaccuracyState ?? null,
'connection' => $connectionMap[$lang][$connectionState] ?? $connectionState ?? null,
'sensors' => $sensorSections,
'history' => $path ?? null,
'history' => $includeHistory ? $path : null,
], JSON_UNESCAPED_UNICODE);
?>