From f1f038ce237d44fcb2878bcd4808dd35a4575d82 Mon Sep 17 00:00:00 2001 From: seo Date: Fri, 12 Jun 2026 16:42:50 +0900 Subject: [PATCH] Optimize Find My Device history --- README.md | 20 +++- js/main.js | 322 ++++++++++++++++++++++++++-------------------------- php/api.php | 199 ++++++++++++++++++++++++++++---- 3 files changed, 360 insertions(+), 181 deletions(-) diff --git a/README.md b/README.md index 57c724b..7a30bc4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Find My Device -Home Assistant에 기록된 Galaxy 기기의 위치, 배터리, 활동 상태, 이동 경로와 모바일 앱 센서 값을 지도 및 설정형 화면에 표시하는 개인용 나의 찾기 페이지입니다. +Home Assistant에 기록된 Galaxy Z Fold7의 위치, 배터리, 활동 상태, 이동 경로와 모바일 앱 센서 값을 지도 및 설정형 화면에 표시하는 개인용 나의 찾기 페이지입니다. ```html @@ -12,9 +12,9 @@ Home Assistant에 기록된 Galaxy 기기의 위치, 배터리, 활동 상태, - 현재 위치 지도 표시 - GPS 정확도 원 표시 -- 최근 24시간 및 사용자 지정 기간 이동 경로 표시 +- 사용자 지정 기간 이동 경로 표시 - 배터리, 충전 상태, 배터리 건강, 온도, 전력, 활동 상태, 걸음 수, 이동 거리 표시 -- 네트워크, 기기, 화면/오디오, 건강, 차량, 저장공간/데이터, 알림, 센서 정보를 섹션별로 표시 +- 네트워크, 기기, 화면/오디오, 건강/활동, 저장공간/데이터, 알림/통화, 센서 정보를 섹션별로 표시 - 자동 위치 갱신 웹훅 제어 - 강제 업데이트 요청 - 한국어/영어 전환 @@ -35,6 +35,20 @@ Home Assistant에 기록된 Galaxy 기기의 위치, 배터리, 활동 상태, 표시할 모바일 앱 센서 목록은 `/custom/common/config.php`의 `findmydevice_sensor_sections`에서 섹션 단위로 관리합니다. 화면 CSS 원본은 `/custom/common/css/findmydevice.css`를 사용합니다. 기존 운영 HTML이 `/custom/findmydevice/css/style.css`를 참조해도 호환 파일이 공용 CSS를 import합니다. +## 히스토리와 지도 + +실시간 화면은 현재 위치와 상태만 조회하고, 이동 기록은 기간 입력을 변경했을 때만 `history=1` 옵션으로 요청합니다. 서버는 Home Assistant 위치 기록을 15m 미만 미세 이동 기준으로 정리한 뒤 최대 300개 지점으로 샘플링합니다. + +Naver Map에는 전체 경로선을 먼저 그리고 시작/종료 마커와 최대 60개의 중간 포인트만 표시합니다. 기록 조회 시 지도는 경로 전체가 보이도록 자동으로 범위를 맞추며, 종료 지점 정보창을 먼저 열어 최근 위치를 바로 확인할 수 있게 합니다. + +## 센서 표시 정책 + +실제 Fold7에서 확인된 값 기준으로 의미 있는 섹션만 유지합니다. 현재 표시 대상은 배터리, 네트워크, 기기, 화면/오디오, 건강/활동, 저장공간/데이터, 알림/통화, 센서입니다. + +`unknown`, `unavailable`, ``, ``, `none`처럼 표시 가치가 낮은 상태와 Wi-Fi 미연결 시의 `0Mbps`, `0MHz`, `-1dBm` 값은 화면에서 제외합니다. Home Assistant 로그에서 속성 크기 경고가 발생하는 활성 알림 목록은 상세 속성을 노출하지 않고 개수만 표시합니다. + +Android Companion App의 실제 상태값은 API에서 한국어/영어 표시값으로 변환합니다. 예를 들어 `still`, `cellular`, `foreground_service`, `Transmitting`, `Stopped`, `portrait`, `silent` 같은 값은 화면 언어에 맞춰 표시됩니다. + ## 운영 메모 이 프로젝트는 위치, 주소, 배터리, 활동 기록을 포함하므로 private 저장소와 개인 접근 환경에서만 운영합니다. diff --git a/js/main.js b/js/main.js index c7dde0a..a7423fd 100644 --- a/js/main.js +++ b/js/main.js @@ -51,10 +51,14 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts device: "기기", displayAudio: "화면 및 오디오", health: "건강", + wellness: "건강/활동", car: "차량", storageData: "저장공간 및 데이터", notifications: "알림", + alerts: "알림/통화", environment: "센서", + networkType: "네트워크 방식", + wifiState: "Wi-Fi 상태", wifiBssid: "Wi-Fi BSSID", mobileDataRoaming: "데이터 로밍", publicIpAddress: "공인 IP", @@ -213,10 +217,14 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts device: "Device", displayAudio: "Display & Audio", health: "Health", + wellness: "Wellness", car: "Car", storageData: "Storage & Data", notifications: "Notifications", + alerts: "Alerts & Calls", environment: "Sensors", + networkType: "Network Type", + wifiState: "Wi-Fi State", wifiBssid: "Wi-Fi BSSID", mobileDataRoaming: "Data Roaming", publicIpAddress: "Public IP", @@ -329,12 +337,15 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts map: null, marker: null, circle: null, - trackLine: null, - smallMarkers: [], - openInfoWindow: null, - mapClickHandlerAdded: false, - initialInfoOpened: false, - }; + trackLine: null, + smallMarkers: [], + historyStartMarker: null, + historyEndMarker: null, + openInfoWindow: null, + mapClickHandlerAdded: false, + initialInfoOpened: false, + historyBoundsFitted: false, + }; let langCode = localStorage.getItem("currentLang") || deviceLang, lastAutoSwitchChecked, lastUpdateSwitchChecked, @@ -617,16 +628,17 @@ document.addEventListener('DOMContentLoaded', () => { }; // ✅ 사용자가 변경했거나 URL에 기간이 있으면 히스토리 모드 - if (userTriggered || hasQS) { - _overrideRange = range; - resetHistoryLayer(); - } else { - _overrideRange = null; // 라이브 모드 유지 - } - - try { - const data = await fetchDeviceData(true, range); - updateDOMWithData(data); + const shouldUseHistory = userTriggered || hasQS; + if (shouldUseHistory) { + _overrideRange = range; + resetHistoryLayer(); + } else { + _overrideRange = null; // 라이브 모드 유지 + } + + try { + const data = await fetchDeviceData(true, shouldUseHistory ? range : null); + updateDOMWithData(data); const last = data.history && data.history[data.history.length - 1]; if (last && mapState.map) { @@ -1085,128 +1097,122 @@ function resetHistoryLayer() { mapState.trackLine.setMap(null); mapState.trackLine = null; } - if (mapState.smallMarkers?.length) { - mapState.smallMarkers.forEach(m => m.setMap?.(null)); - mapState.smallMarkers = []; - } - if (mapState.openInfoWindow) { - mapState.openInfoWindow.close(); - mapState.openInfoWindow = null; - } -} - -function updateHistoryMarkers(history) { - // 안전가드 - if (!Array.isArray(history) || history.length === 0) { - // 표시된 마커를 기준으로 라인만 갱신(필요 시) - renderTrackFromDisplayedMarkers(); - return; - } - - // time 파싱 헬퍼 - const toTime = (v) => { - if (v instanceof Date) return v.getTime(); - if (typeof v === 'number' && isFinite(v)) { - // 초 단위처럼 보이면 ms로 변환 - return v < 2e10 ? v * 1000 : v; - } - const t = Date.parse(v); - return isNaN(t) ? 0 : t; - }; - - // ★ history는 스몰마커 생성만에 사용(정렬은 생성 안정성용) - history.sort((a, b) => toTime(a.time) - toTime(b.time)); - - // 1) 스몰마커 생성/중복 방지 - history.forEach(pos => { - const pointLatLng = new naver.maps.LatLng(pos.lat, pos.lng); - const exist = mapState.smallMarkers.some(marker => - marker.getPosition().equals(pointLatLng) && marker.userTime === pos.time - ); - if (!exist) { - const smallMarker = new naver.maps.Marker({ - position: pointLatLng, - map: mapState.map, - icon: { - content: ` -
-
-
-
`, - anchor: new naver.maps.Point(18, 18) - } - }); - smallMarker.userTime = pos.time; // ⚑ 표시된 마커의 시간 정보를 유지 - mapState.smallMarkers.push(smallMarker); - - const infoWindow = new naver.maps.InfoWindow({ - content: `
${getRelativeTime(pos.time)}
` - }); - - smallMarker.infoWindow = infoWindow; - - naver.maps.Event.addListener(smallMarker, "click", () => { - if (mapState.openInfoWindow && mapState.openInfoWindow !== infoWindow) { - mapState.openInfoWindow.close(); - } - if (infoWindow.getMap()) { - infoWindow.close(); - mapState.openInfoWindow = null; - } else { - infoWindow.open(mapState.map, smallMarker); - mapState.openInfoWindow = infoWindow; - } - }); - } - }); - - // 2) ★★ 지금 "화면에 표시된 스몰마커들"만으로 폴리라인 갱신 ★★ - renderTrackFromDisplayedMarkers(); - - // ★ 최초 로드시 마지막(가장 최근) 스몰마커의 InfoWindow 자동 오픈 - if (!mapState.initialInfoOpened && mapState.smallMarkers.length) { - // 시간순 정렬 후 마지막 선택 - const toTime = (v) => { - if (v instanceof Date) return v.getTime(); - if (typeof v === 'number' && isFinite(v)) return v < 2e10 ? v * 1000 : v; - const t = Date.parse(v); - return isNaN(t) ? 0 : t; - }; - - const lastMarker = mapState.smallMarkers - .slice() - .sort((a, b) => toTime(a.userTime) - toTime(b.userTime)) - .at(-1); - - if (lastMarker && lastMarker.infoWindow) { - // 이전 열린 창 닫기 - if (mapState.openInfoWindow && mapState.openInfoWindow !== lastMarker.infoWindow) { - mapState.openInfoWindow.close(); - } - - lastMarker.infoWindow.open(mapState.map, lastMarker); - mapState.openInfoWindow = lastMarker.infoWindow; - - // 보기 편하게 살짝 이동 - mapState.map.panTo(lastMarker.getPosition()); - } - - mapState.initialInfoOpened = true; - } - - // map 클릭시 열린 InfoWindow 닫기 (1회만 등록) - if (!mapState.mapClickHandlerAdded) { - naver.maps.Event.addListener(mapState.map, "click", () => { - if (mapState.openInfoWindow) { + if (mapState.smallMarkers?.length) { + mapState.smallMarkers.forEach(m => m.setMap?.(null)); + mapState.smallMarkers = []; + } + if (mapState.historyStartMarker) { + mapState.historyStartMarker.setMap(null); + mapState.historyStartMarker = null; + } + if (mapState.historyEndMarker) { + mapState.historyEndMarker.setMap(null); + mapState.historyEndMarker = null; + } + if (mapState.openInfoWindow) { + mapState.openInfoWindow.close(); + mapState.openInfoWindow = null; + } + mapState.initialInfoOpened = false; + mapState.historyBoundsFitted = false; +} + +function updateHistoryMarkers(history) { + if (!Array.isArray(history) || history.length === 0) { + return; + } + if (!mapState.map) return; + + const toTime = (v) => { + if (v instanceof Date) return v.getTime(); + if (typeof v === 'number' && isFinite(v)) return v < 2e10 ? v * 1000 : v; + const t = Date.parse(v); + return isNaN(t) ? 0 : t; + }; + + const points = history + .filter(pos => hasValidCoords(pos.lat, pos.lng)) + .sort((a, b) => toTime(a.time) - toTime(b.time)) + .map(pos => ({ ...pos, latlng: new naver.maps.LatLng(pos.lat, pos.lng) })); + + if (!points.length) return; + + if (mapState.trackLine) mapState.trackLine.setMap(null); + mapState.trackLine = new naver.maps.Polyline({ + map: mapState.map, + path: points.map(pos => pos.latlng), + strokeColor: '#007AFF', + strokeOpacity: 0.85, + strokeWeight: 4 + }); + + if (mapState.smallMarkers?.length) { + mapState.smallMarkers.forEach(marker => marker.setMap(null)); + mapState.smallMarkers = []; + } + if (mapState.historyStartMarker) mapState.historyStartMarker.setMap(null); + if (mapState.historyEndMarker) mapState.historyEndMarker.setMap(null); + + const makeMarker = (pos, type = 'point') => { + const isEndpoint = type === 'start' || type === 'end'; + const color = type === 'start' ? '#34C759' : (type === 'end' ? '#FF3B30' : '#007AFF'); + const label = type === 'start' ? (langCode === 'ko' ? '시작' : 'Start') : (type === 'end' ? (langCode === 'ko' ? '종료' : 'End') : ''); + const marker = new naver.maps.Marker({ + position: pos.latlng, + map: mapState.map, + zIndex: isEndpoint ? 90 : 50, + icon: { + content: `
${label}
`, + anchor: new naver.maps.Point(isEndpoint ? 24 : 8, isEndpoint ? 14 : 8) + } + }); + marker.userTime = pos.time; + marker.infoWindow = new naver.maps.InfoWindow({ + content: `
${label ? `${label}
` : ''}${getRelativeTime(pos.time)}
` + }); + naver.maps.Event.addListener(marker, "click", () => { + if (mapState.openInfoWindow && mapState.openInfoWindow !== marker.infoWindow) { + mapState.openInfoWindow.close(); + } + if (marker.infoWindow.getMap()) { + marker.infoWindow.close(); + mapState.openInfoWindow = null; + } else { + marker.infoWindow.open(mapState.map, marker); + mapState.openInfoWindow = marker.infoWindow; + } + }); + return marker; + }; + + const first = points[0]; + const last = points.at(-1); + mapState.historyStartMarker = makeMarker(first, 'start'); + mapState.historyEndMarker = makeMarker(last, 'end'); + + const maxDots = 60; + const step = Math.max(1, Math.ceil(points.length / maxDots)); + points.forEach((pos, idx) => { + if (idx === 0 || idx === points.length - 1 || idx % step !== 0) return; + mapState.smallMarkers.push(makeMarker(pos)); + }); + + if (!mapState.historyBoundsFitted) { + const bounds = new naver.maps.LatLngBounds(first.latlng, first.latlng); + points.forEach(pos => bounds.extend(pos.latlng)); + mapState.map.fitBounds(bounds); + mapState.historyBoundsFitted = true; + } + + if (!mapState.initialInfoOpened && mapState.historyEndMarker?.infoWindow) { + mapState.historyEndMarker.infoWindow.open(mapState.map, mapState.historyEndMarker); + mapState.openInfoWindow = mapState.historyEndMarker.infoWindow; + mapState.initialInfoOpened = true; + } + + if (!mapState.mapClickHandlerAdded) { + naver.maps.Event.addListener(mapState.map, "click", () => { + if (mapState.openInfoWindow) { mapState.openInfoWindow.close(); mapState.openInfoWindow = null; } @@ -1251,20 +1257,16 @@ function updateDOMWithData(data) { updateActivitySection(data); // 지도 초기화 - if (!mapState.map) { - initMap(latlng, data.accuracy); - if (data.history?.length > 0) updateHistoryMarkers(data.history); - } + if (!mapState.map) { + initMap(latlng, data.accuracy); + } // 메인 마커 / 정확도 원 업데이트 mapState.marker.setPosition(latlng); mapState.circle.setCenter(latlng); animateCircleRadius(Math.max(data.accuracy || 50, 10)); - // history (있으면) 업데이트 - if (data.history?.length > 0) updateHistoryMarkers(data.history); - - // autoPosition + // autoPosition const isChecked = document.getElementById('autoPositionSwitch').checked; if (isChecked && (lastLat !== data.latitude || lastLng !== data.longitude)) { mapState.map.panTo(latlng); @@ -1341,18 +1343,22 @@ function syncUpdateBtn() { } } -function fetchDeviceData(includeLang = true, range = null) { - const params = new URLSearchParams(includeLang ? { lang: langCode } : { hashonly: 1 }); - - // 우선순위: 직접 인자(range) > 전역 오버라이드(_overrideRange) > URL 쿼리 - const eff = range || _overrideRange || {}; - const qs = new URLSearchParams(window.location.search); - - const s = eff.startTime || qs.get('startTime'); - const e = eff.endTime || qs.get('endTime'); - - if (s) params.set('startTime', s); - if (e) params.set('endTime', e); +function fetchDeviceData(includeLang = true, range = null) { + const params = new URLSearchParams(includeLang ? { lang: langCode } : { hashonly: 1 }); + + // 우선순위: 직접 인자(range) > 전역 오버라이드(_overrideRange) + const eff = range || _overrideRange || {}; + const hasHistoryRange = includeLang && !!(eff.startTime || eff.endTime); + + const s = eff.startTime; + const e = eff.endTime; + + if (s) params.set('startTime', s); + if (e) params.set('endTime', e); + if (hasHistoryRange) { + params.set('history', '1'); + params.set('maxPoints', '300'); + } const url = `https://chaegeon.com/custom/findmydevice/php/api.php?${params.toString()}`; diff --git a/php/api.php b/php/api.php index c2ef56a..810d95c 100644 --- a/php/api.php +++ b/php/api.php @@ -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); ?>