From 4559301fa6f01de08cccf91a96f26b6e5b02ac37 Mon Sep 17 00:00:00 2001 From: seo Date: Fri, 12 Jun 2026 15:18:13 +0900 Subject: [PATCH] Support Galaxy sensor fields and remove debug file --- README.md | 6 +- debug.html | 175 --------------------------------------- js/main.js | 231 ++++++++++++++++++++++++++++++++++++++++++---------- php/api.php | 115 +++++++++++++++++++++++--- 4 files changed, 294 insertions(+), 233 deletions(-) delete mode 100644 debug.html diff --git a/README.md b/README.md index 0cecdab..72a76c5 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # Find My Device -Home Assistant에 기록된 Fold7 위치, 배터리, 활동 상태, 이동 경로를 지도에 표시하는 개인용 나의 찾기 페이지입니다. +Home Assistant에 기록된 Galaxy 기기의 위치, 배터리, 활동 상태, 이동 경로를 지도에 표시하는 개인용 나의 찾기 페이지입니다. ## 기능 - 현재 위치 지도 표시 - GPS 정확도 원 표시 - 최근 24시간 및 사용자 지정 기간 이동 경로 표시 -- 배터리, 워치 배터리, 활동 상태, 걸음 수, 이동 거리 표시 +- 배터리, 충전 상태, 배터리 건강, 온도, 전력, 활동 상태, 걸음 수, 이동 거리 표시 +- Wi-Fi, 모바일 데이터, 전화 상태, 화면 활성, Doze, 절전, 잠금 상태 표시 - 자동 위치 갱신 웹훅 제어 - 강제 업데이트 요청 - 한국어/영어 전환 @@ -15,7 +16,6 @@ Home Assistant에 기록된 Fold7 위치, 배터리, 활동 상태, 이동 경 ## 구조 -- `debug.html`: 페이지 삽입용 HTML 조각 - `php/api.php`: Home Assistant 상태/히스토리 조회 API - `js/main.js`: 지도 렌더링, 상태 갱신, 히스토리 표시 - `css/style.css`: iOS 설정 화면 스타일 diff --git a/debug.html b/debug.html deleted file mode 100644 index 51124b5..0000000 --- a/debug.html +++ /dev/null @@ -1,175 +0,0 @@ - - -
-
- - -
- Language Toggle -
-
- -
-
-
-
- -
- -
-
-
- - -
-
- -
- -
- -
-
- - -
-
- - -
-
- -
-
- -
- -
-
-
- - -
-
- - -
-
- - -
-
- -
-
- -
- -
-
-
- - -
-
- - -
-
- - -
-
- -
-
- -
- -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- -
-
-
- - -
-
-
-
- -
- - - - - - - - - -
- - - \ No newline at end of file diff --git a/js/main.js b/js/main.js index 23076a5..1621fde 100644 --- a/js/main.js +++ b/js/main.js @@ -15,21 +15,39 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts autoCurrentUpdate: "현재 위치 자동 이동", autoUpdate: "자동 업데이트", forceUpdate: "업데이트 하기", - battery: "배터리", - batteryState: "배터리 상태", - batteryLevel: "배터리 잔량", - watchbatteryState: "워치 배터리 상태", - watchbatteryLevel: "워치 배터리 잔량", - activity: "활동", - currentActivity: "현재 활동", - activityAccuracy: "활동 정확도", + battery: "배터리", + batteryState: "배터리 상태", + batteryLevel: "배터리 잔량", + watchbatteryState: "워치 배터리 상태", + watchbatteryLevel: "워치 배터리 잔량", + isCharging: "충전 여부", + chargerType: "충전 방식", + batteryHealth: "배터리 건강", + batteryTemperature: "배터리 온도", + batteryPower: "배터리 전력", + remainingChargeTime: "충전 남은 시간", + batteryCycleCount: "배터리 사이클", + activity: "활동", + currentActivity: "현재 활동", + activityAccuracy: "활동 정확도", distance: "도보로 움직인 거리", steps: "걸음 수", ascended: "오른 계단", descended: "내려간 계단", - etc: "기타", - connection: "연결 방법", - editPage: "페이지 수정", + etc: "기타", + connection: "연결 방법", + wifiConnection: "Wi-Fi", + wifiIp: "Wi-Fi IP", + wifiLinkSpeed: "Wi-Fi 속도", + wifiFrequency: "Wi-Fi 주파수", + wifiSignalStrength: "Wi-Fi 신호", + mobileData: "모바일 데이터", + phoneState: "전화 상태", + interactive: "화면 활성", + dozeMode: "Doze 모드", + powerSave: "절전 모드", + deviceLocked: "잠금 상태", + editPage: "페이지 수정", pwa: "눌러 홈 화면에 추가해 바로 보기", history: "기록", startTime: "시작", @@ -50,21 +68,39 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts autoCurrentUpdate: "Current Location Auto Update", autoUpdate: "Auto Update", forceUpdate: "Force Update", - battery: "Battery", - batteryState: "Battery State", - batteryLevel: "Battery Level", - watchbatteryState: "Watch Battery State", - watchbatteryLevel: "Watch Battery Level", - activity: "Activity", + battery: "Battery", + batteryState: "Battery State", + batteryLevel: "Battery Level", + watchbatteryState: "Watch Battery State", + watchbatteryLevel: "Watch Battery Level", + isCharging: "Charging", + chargerType: "Charger Type", + batteryHealth: "Battery Health", + batteryTemperature: "Battery Temp", + batteryPower: "Battery Power", + remainingChargeTime: "Remaining Charge", + batteryCycleCount: "Battery Cycles", + activity: "Activity", currentActivity: "Current Activity", activityAccuracy: "Activity Confidence", distance: "Distance walked", steps: "Steps", ascended: "Floors Ascended", descended: "Floors Descended", - etc: "Others", - connection: "Network Type", - editPage: "Edit Page", + etc: "Others", + connection: "Network Type", + wifiConnection: "Wi-Fi", + wifiIp: "Wi-Fi IP", + wifiLinkSpeed: "Wi-Fi Speed", + wifiFrequency: "Wi-Fi Frequency", + wifiSignalStrength: "Wi-Fi Signal", + mobileData: "Mobile Data", + phoneState: "Phone State", + interactive: "Interactive", + dozeMode: "Doze Mode", + powerSave: "Power Save", + deviceLocked: "Device Locked", + editPage: "Edit Page", pwa: "Add to Home Screen for quick access!", history: "History", startTime: "Start", @@ -538,29 +574,133 @@ function setCookieHours(name, value, hours) { document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; expires=${d.toUTCString()}; path=/; SameSite=Lax`; } -function updateDOM(data) { - const textMap = { - currentPosition: data.address, +function updateDOM(data) { + const textMap = { + currentPosition: data.address, lastUpdateTime: data.updated ? getRelativeTime(data.updated) : "-", nowActivity: data.activity, connectionType: data.connection, updateTrigger: data.trigger, - batteryStatus: data.state, - Battery: data.level > 0 ? `${data.level}%` : "-", - WbatteryStatus: data.wstate, - WBattery: data.wlevel > 0 ? `${data.wlevel}%` : "-", - todaySteps: data.steps > 0 ? `${formatNumber(data.steps)}${langCode === "ko" ? "걸음" : "steps"}` : "-", + batteryStatus: data.state, + Battery: data.level > 0 ? `${data.level}%` : "-", + WbatteryStatus: data.wstate, + WBattery: data.wlevel > 0 ? `${data.wlevel}%` : "-", + todaySteps: data.steps > 0 ? `${formatNumber(data.steps)}${langCode === "ko" ? "걸음" : "steps"}` : "-", todayAscended: data.ascended > 0 ? `${formatNumber(data.ascended)}${langCode === "ko" ? "층" : "floors"}` : "-", todayDescended: data.descended > 0 ? `${formatNumber(data.descended)}${langCode === "ko" ? "층" : "floors"}` : "-", todayDistance: data.distance > 0 ? `${formatNumber(data.distance)}m` : "-", accuracyValue: data.accuracy > 0 ? `${formatNumber(data.accuracy)}m` : "-" }; - for (const [id, value] of Object.entries(textMap)) { - const el = document.getElementById(id); - if (el) el.textContent = value || "-"; - } -} + for (const [id, value] of Object.entries(textMap)) { + const el = document.getElementById(id); + if (el) el.textContent = value || "-"; + } + + toggleOptionalRow('WbatteryStatus', data.wstate); + toggleOptionalRow('WBattery', data.wlevel); + updateGalaxyRows(data.galaxy || {}); +} + +function updateGalaxyRows(galaxy) { + const t = translations[langCode] || translations.ko; + const rows = [ + ['isCharging', t.isCharging, galaxy.isCharging], + ['chargerType', t.chargerType, galaxy.chargerType], + ['batteryHealth', t.batteryHealth, galaxy.batteryHealth], + ['batteryTemperature', t.batteryTemperature, formatWithUnit(galaxy.batteryTemperature, '°C')], + ['batteryPower', t.batteryPower, formatWithUnit(galaxy.batteryPower, 'W')], + ['remainingChargeTime', t.remainingChargeTime, formatDuration(galaxy.remainingChargeTime)], + ['batteryCycleCount', t.batteryCycleCount, galaxy.batteryCycleCount], + ['wifiConnection', t.wifiConnection, galaxy.wifiConnection], + ['wifiIp', t.wifiIp, galaxy.wifiIp], + ['wifiLinkSpeed', t.wifiLinkSpeed, formatWithUnit(galaxy.wifiLinkSpeed, 'Mbps')], + ['wifiFrequency', t.wifiFrequency, formatWithUnit(galaxy.wifiFrequency, 'MHz')], + ['wifiSignalStrength', t.wifiSignalStrength, formatWithUnit(galaxy.wifiSignalStrength, 'dBm')], + ['mobileData', t.mobileData, galaxy.mobileData], + ['phoneState', t.phoneState, galaxy.phoneState], + ['interactive', t.interactive, galaxy.interactive], + ['dozeMode', t.dozeMode, galaxy.dozeMode], + ['powerSave', t.powerSave, galaxy.powerSave], + ['deviceLocked', t.deviceLocked, galaxy.deviceLocked], + ]; + + rows.forEach(([key, label, value]) => { + const targetSection = ['isCharging', 'chargerType', 'batteryHealth', 'batteryTemperature', 'batteryPower', 'remainingChargeTime', 'batteryCycleCount'].includes(key) + ? findSectionByTitle(langCode === 'ko' ? '배터리' : 'Battery') + : findSectionByTitle(langCode === 'ko' ? '기타' : 'Others'); + upsertOptionalRow(targetSection, `galaxy_${key}`, label, value); + }); +} + +function findSectionByTitle(titleText) { + const titles = document.querySelectorAll('.settingTitle'); + for (const title of titles) { + if (title.textContent.trim() === titleText) { + const section = title.nextElementSibling; + if (section && section.classList.contains('settingSection')) return section; + } + } + return null; +} + +function upsertOptionalRow(section, id, label, value) { + if (!section) return; + const displayValue = normalizeDisplayValue(value); + let row = document.getElementById(id); + + if (!displayValue) { + if (row) row.remove(); + return; + } + + if (!row) { + row = document.createElement('div'); + row.className = 'settingRow'; + row.id = id; + row.innerHTML = ``; + section.appendChild(row); + } + + row.children[0].textContent = label; + row.children[1].textContent = displayValue; +} + +function toggleOptionalRow(valueId, value) { + const el = document.getElementById(valueId); + const row = el?.closest('.settingRow'); + if (!row) return; + row.style.display = normalizeDisplayValue(value) ? '' : 'none'; +} + +function normalizeDisplayValue(value) { + if (value === null || typeof value === 'undefined' || value === '' || value === '-' || value === 'unknown' || value === 'unavailable') { + return ''; + } + return String(value); +} + +function formatWithUnit(value, unit) { + const normalized = normalizeDisplayValue(value); + if (!normalized) return ''; + return /[a-zA-Z%°]/.test(normalized) ? normalized : `${normalized}${unit}`; +} + +function formatDuration(value) { + const normalized = normalizeDisplayValue(value); + if (!normalized) return ''; + const seconds = Number(normalized); + if (!Number.isFinite(seconds)) return normalized; + if (seconds <= 0) return '0s'; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + return [ + h ? `${h}h` : '', + m ? `${m}m` : '', + s && !h ? `${s}s` : '', + ].filter(Boolean).join(' '); +} function updateActivitySection(data) { const validActivities = ["Stationary", "Walking", "Running", "In Vehicle", "Cycling", "가만히 있음", "걷는 중", "달리는 중", "차량 이동 중", "자전거 이동 중"]; @@ -1006,18 +1146,19 @@ function debounce(fn, delay = 400) { }; } -function resetDisplayValues() { - const ids = [ - 'currentPosition', 'accuracyValue', 'lastUpdateTime', 'updateTrigger', - 'batteryStatus', 'Battery', 'WbatteryStatus', 'WBattery', - 'nowActivity', 'nowactivityAccuracy', 'todayDistance', - 'todaySteps', 'todayAscended', 'todayDescended', 'connectionType' - ]; - ids.forEach(id => { - const el = document.getElementById(id); - if (el) el.textContent = '-'; - }); -} +function resetDisplayValues() { + const ids = [ + 'currentPosition', 'accuracyValue', 'lastUpdateTime', 'updateTrigger', + 'batteryStatus', 'Battery', 'WbatteryStatus', 'WBattery', + 'nowActivity', 'nowactivityAccuracy', 'todayDistance', + 'todaySteps', 'todayAscended', 'todayDescended', 'connectionType' + ]; + ids.forEach(id => { + const el = document.getElementById(id); + if (el) el.textContent = '-'; + }); + document.querySelectorAll('[id^="galaxy_"]').forEach(row => row.remove()); +} function triggerForceUpdate() { callDeviceWebhook('force_update'); diff --git a/php/api.php b/php/api.php index 51a9842..3ffe0c5 100644 --- a/php/api.php +++ b/php/api.php @@ -42,8 +42,6 @@ $deviceUrl = $entities['device']; $geoUrl = $entities['geo']; $batteryStateUrl = $entities['battery_state']; $batteryLevelUrl = $entities['battery_level']; -$watchBatteryStateUrl = $entities['watch_battery_state']; -$watchBatteryLevelUrl = $entities['watch_battery_level']; $activityUrl = $entities['activity']; $connectionUrl = $entities['connection']; $fascendedUrl = $entities['daily_floors']; @@ -77,13 +75,39 @@ function getData($entityId) { return custom_ha_state('seoul', $entityId) ?? []; } +function getEntityData($entities, $key) { + if (empty($entities[$key])) { + return []; + } + return getData($entities[$key]); +} + +function stateValue($data) { + $state = $data['state'] ?? null; + if ($state === null || $state === 'unknown' || $state === 'unavailable' || $state === '' || $state === '') { + return null; + } + return $state; +} + +function boolText($state, $lang) { + if ($state === null) return null; + if ($lang === 'ko') { + return $state === 'on' ? '켜짐' : ($state === 'off' ? '꺼짐' : $state); + } + return $state === 'on' ? 'On' : ($state === 'off' ? 'Off' : $state); +} + +function mappedText($state, $lang, $map) { + if ($state === null) return null; + return $map[$lang][$state] ?? $map[$lang][strtolower((string)$state)] ?? $state; +} + // 데이터 불러오기 $deviceData = getData($deviceUrl); $geoData = getData($geoUrl); $batteryStateData = getData($batteryStateUrl); $batteryLevelData = getData($batteryLevelUrl); -$watchBatteryStateData = getData($watchBatteryStateUrl); -$watchBatteryLevelData = getData($watchBatteryLevelUrl); $activityData = getData($activityUrl); $connectionData = getData($connectionUrl); $ascendedData = getData($fascendedUrl); @@ -92,6 +116,27 @@ $stepsData = getData($stepsUrl); $distanceData = getData($distanceUrl); $historyData = custom_ha_request('seoul', 'GET', $historyEndpoint) ?? []; $triggerData = getData($triggerUrl); +$galaxyData = [ + 'isCharging' => getEntityData($entities, 'is_charging'), + 'chargerType' => getEntityData($entities, 'charger_type'), + 'batteryHealth' => getEntityData($entities, 'battery_health'), + 'batteryTemperature' => getEntityData($entities, 'battery_temperature'), + 'batteryPower' => getEntityData($entities, 'battery_power'), + 'remainingChargeTime' => getEntityData($entities, 'remaining_charge_time'), + 'batteryCycleCount' => getEntityData($entities, 'battery_cycle_count'), + 'wifiConnection' => getEntityData($entities, 'wifi_connection'), + 'wifiBssid' => getEntityData($entities, 'wifi_bssid'), + 'wifiIp' => getEntityData($entities, 'wifi_ip'), + 'wifiLinkSpeed' => getEntityData($entities, 'wifi_link_speed'), + 'wifiFrequency' => getEntityData($entities, 'wifi_frequency'), + 'wifiSignalStrength' => getEntityData($entities, 'wifi_signal_strength'), + 'mobileData' => getEntityData($entities, 'mobile_data'), + 'phoneState' => getEntityData($entities, 'phone_state'), + 'interactive' => getEntityData($entities, 'interactive'), + 'dozeMode' => getEntityData($entities, 'doze_mode'), + 'powerSave' => getEntityData($entities, 'power_save'), + 'deviceLocked' => getEntityData($entities, 'device_locked'), +]; // 경로 기록 $path = []; @@ -160,18 +205,43 @@ $connectionMap = [ $batteryStateMap = [ 'ko' => [ 'Charging' => '충전 중', + 'Discharging' => '방전 중', 'Not Charging' => '충전중이 아님', 'Full' => '가득참', + 'charging' => '충전 중', + 'discharging' => '방전 중', + 'not_charging' => '충전중이 아님', + 'full' => '가득참', 'Unknown' => '-', ], 'en' => [ 'Charging' => 'Charging', + 'Discharging' => 'Discharging', 'Not Charging' => 'Not Charging', 'Full' => 'Full', + 'charging' => 'Charging', + 'discharging' => 'Discharging', + 'not_charging' => 'Not Charging', + 'full' => 'Full', 'Unknown' => '-', ] ]; +$androidValueMap = [ + 'chargerType' => [ + 'ko' => ['none' => '없음', 'wireless' => '무선', 'ac' => '유선', 'usb' => 'USB'], + 'en' => ['none' => 'None', 'wireless' => 'Wireless', 'ac' => 'AC', 'usb' => 'USB'], + ], + 'batteryHealth' => [ + 'ko' => ['good' => '양호', 'overheat' => '과열', 'dead' => '불량', 'over_voltage' => '과전압', 'cold' => '저온'], + 'en' => ['good' => 'Good', 'overheat' => 'Overheat', 'dead' => 'Dead', 'over_voltage' => 'Over voltage', 'cold' => 'Cold'], + ], + 'phoneState' => [ + 'ko' => ['idle' => '대기', 'ringing' => '수신 중', 'offhook' => '통화 중'], + 'en' => ['idle' => 'Idle', 'ringing' => 'Ringing', 'offhook' => 'Off hook'], + ], +]; + $triggerMap = [ 'ko' => [ "Launch" => "앱 실행", @@ -202,10 +272,9 @@ $triggerMap = [ ]; $activityState = $activityData['state'] ?? null; -$aaccuracyState = $activityData['attributes']['Confidence'] ?? null; +$aaccuracyState = $activityData['attributes']['Confidence'] ?? $activityData['attributes']['confidence'] ?? null; $connectionState = $connectionData['state'] ?? null; $batteryState = $batteryStateData['state'] ?? null; -$watchBatteryState = $watchBatteryStateData['state'] ?? null; $address = null; @@ -224,6 +293,34 @@ if (($deviceData['state'] ?? null) !== 'not_home') { $address = trim($locality . ' ' . ($geoData['attributes']['Name'] ?? '')); } +$galaxy = [ + 'isCharging' => boolText(stateValue($galaxyData['isCharging']), $lang), + 'chargerType' => mappedText(stateValue($galaxyData['chargerType']), $lang, $androidValueMap['chargerType']), + 'batteryHealth' => mappedText(stateValue($galaxyData['batteryHealth']), $lang, $androidValueMap['batteryHealth']), + 'batteryTemperature' => stateValue($galaxyData['batteryTemperature']), + 'batteryPower' => stateValue($galaxyData['batteryPower']), + 'remainingChargeTime' => stateValue($galaxyData['remainingChargeTime']), + 'batteryCycleCount' => stateValue($galaxyData['batteryCycleCount']), + 'wifiConnection' => stateValue($galaxyData['wifiConnection']), + 'wifiBssid' => stateValue($galaxyData['wifiBssid']), + 'wifiIp' => stateValue($galaxyData['wifiIp']), + 'wifiLinkSpeed' => stateValue($galaxyData['wifiLinkSpeed']), + 'wifiFrequency' => stateValue($galaxyData['wifiFrequency']), + 'wifiSignalStrength' => stateValue($galaxyData['wifiSignalStrength']), + 'mobileData' => boolText(stateValue($galaxyData['mobileData']), $lang), + 'phoneState' => mappedText(stateValue($galaxyData['phoneState']), $lang, $androidValueMap['phoneState']), + 'interactive' => boolText(stateValue($galaxyData['interactive']), $lang), + 'dozeMode' => boolText(stateValue($galaxyData['dozeMode']), $lang), + 'powerSave' => boolText(stateValue($galaxyData['powerSave']), $lang), + 'deviceLocked' => boolText(stateValue($galaxyData['deviceLocked']), $lang), +]; + +if (!$galaxy['wifiConnection']) { + foreach (['wifiBssid', 'wifiIp', 'wifiLinkSpeed', 'wifiFrequency', 'wifiSignalStrength'] as $wifiKey) { + $galaxy[$wifiKey] = null; + } +} + // 해시 요청만 들어온 경우 if (isset($_GET['hashonly']) && $_GET['hashonly'] == '1') { $raw = json_encode([ @@ -235,8 +332,6 @@ if (isset($_GET['hashonly']) && $_GET['hashonly'] == '1') { 'address' => $address, 'state' => $batteryStateMap[$lang][$batteryState] ?? $batteryState ?? null, 'level' => $batteryLevelData['state'] ?? null, - 'wstate' => $batteryStateMap[$lang][$watchBatteryState] ?? $watchBatteryState ?? null, - 'wlevel' => $watchBatteryLevelData['state'] ?? null, 'ascended' => $ascendedData['state'] ?? null, 'descended' => $descendedData['state'] ?? null, 'steps' => $stepsData['state'] ?? null, @@ -244,6 +339,7 @@ if (isset($_GET['hashonly']) && $_GET['hashonly'] == '1') { 'activity' => $activityMap[$lang][$activityState] ?? $activityState ?? null, 'aaccuracy' => $aaccuracyMap[$lang][$aaccuracyState] ?? $aaccuracyState ?? null, 'connection' => $connectionMap[$lang][$connectionState] ?? $connectionState ?? null, + 'galaxy' => $galaxy, ]); echo json_encode(['hash' => hash('sha256', $raw)]); @@ -259,8 +355,6 @@ echo json_encode([ 'address' => $address, 'state' => $batteryStateMap[$lang][$batteryState] ?? $batteryState ?? null, 'level' => $batteryLevelData['state'] ?? null, - 'wstate' => $batteryStateMap[$lang][$watchBatteryState] ?? $watchBatteryState ?? null, - 'wlevel' => $watchBatteryLevelData['state'] ?? null, 'ascended' => $ascendedData['state'] ?? null, 'descended' => $descendedData['state'] ?? null, 'steps' => $stepsData['state'] ?? null, @@ -268,6 +362,7 @@ echo json_encode([ 'activity' => $activityMap[$lang][$activityState] ?? $activityState ?? null, 'aaccuracy' => $aaccuracyMap[$lang][$aaccuracyState] ?? $aaccuracyState ?? null, 'connection' => $connectionMap[$lang][$connectionState] ?? $connectionState ?? null, + 'galaxy' => $galaxy, 'history' => $path ?? null, ], JSON_UNESCAPED_UNICODE); ?>