From 3dfb506474cf8fa8151325c43a3efcd659310477 Mon Sep 17 00:00:00 2001 From: seo Date: Wed, 17 Jun 2026 20:07:34 +0900 Subject: [PATCH] Merge findmydevice data from both HA profiles --- README.md | 6 ++- php/api.php | 126 +++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 115 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 2566936..fb1c99c 100644 --- a/README.md +++ b/README.md @@ -40,12 +40,16 @@ Home Assistant에 기록된 Galaxy Z Fold7의 위치, 배터리, 활동 상태, 표시할 모바일 앱 센서 목록은 `/custom/common/config.php`의 `findmydevice_sensor_sections`에서 섹션 단위로 관리합니다. 화면 CSS 원본은 `/custom/common/css/findmydevice.css`를 사용합니다. 기존 운영 HTML이 `/custom/findmydevice/css/style.css`를 참조해도 호환 파일이 공용 CSS를 import합니다. -위치 갱신 명령은 `/custom/common/config.php`의 `findmydevice_location_refresh` 설정을 사용합니다. 현재 위치 엔티티를 읽는 Home Assistant 인스턴스와 같은 `seoul` 프로필의 `notify.mobile_app_seocaegeonyi_z_fold7` 서비스로 Companion App notification command를 보냅니다. 자동 업데이트가 켜지면 Android high accuracy mode를 5초 간격으로 설정하고 `force_on`을 보낸 뒤 `request_location_update`와 `command_update_sensors`를 함께 호출합니다. 자동 업데이트 중에는 10초마다 위치 요청과 센서 업데이트 요청을 반복하고, 자동 업데이트를 끄거나 페이지를 벗어나면 `force_off`를 보냅니다. +위치 갱신 명령은 `/custom/common/config.php`의 `findmydevice_location_refresh` 설정을 사용합니다. `seoul`, `main` Home Assistant 프로필의 `notify.mobile_app_seocaegeonyi_z_fold7` 서비스로 Companion App notification command를 보냅니다. 자동 업데이트가 켜지면 Android high accuracy mode를 5초 간격으로 설정하고 `force_on`을 보낸 뒤 `request_location_update`와 `command_update_sensors`를 함께 호출합니다. 자동 업데이트 중에는 10초마다 위치 요청과 센서 업데이트 요청을 반복하고, 자동 업데이트를 끄거나 페이지를 벗어나면 `force_off`를 보냅니다. ## 히스토리와 지도 실시간 화면은 현재 위치와 상태를 조회하고, 기본 기간 입력값에 해당하는 최근 하루 이동 기록도 지도에 함께 표시합니다. 기간 입력을 변경하면 해당 범위의 이동 기록을 `history=1` 옵션으로 다시 요청합니다. 서버는 기간이 길어도 Home Assistant 히스토리 API를 실행한 뒤, 브라우저 렌더링용으로 경로를 부드럽게 단순화해 응답합니다. +상태 조회는 `seoul`, `main` Home Assistant의 `/states`를 모두 읽어 entity별로 병합합니다. 한쪽에만 의미 있는 값이 있으면 해당 값을 사용하고, 양쪽 모두 값이 있으면 더 최신 `last_updated` 또는 `last_changed` 값을 사용합니다. 선택된 상태의 attributes에 빈 값이 있으면 다른 쪽 attributes로 채워 빈칸을 줄입니다. + +히스토리 조회도 `seoul`, `main` Home Assistant history를 모두 읽습니다. 서버는 시간과 좌표를 기준으로 중복 지점을 제거하고 시간순으로 병합한 뒤 기존 경로 단순화와 smoothing을 적용합니다. 응답에는 병합 전 출처별 지점 수인 `historySourceCounts`도 포함합니다. + Naver Map에는 단일 Polyline 중심의 부드러운 경로선을 먼저 그리고 시작/종료 마커와 체류 지점 마커를 표시합니다. 중간 점 마커는 긴 기간 조회에서 브라우저 부하가 커지므로 표시하지 않습니다. 기록 조회 시 지도는 경로 전체가 보이도록 자동으로 범위를 맞추며, 종료 지점 정보창을 먼저 열어 최근 위치를 바로 확인할 수 있게 합니다. 지도 기능은 NAVER 지도 API v3의 기본 컨트롤, 지도 유형, `TrafficLayer`, `StreetLayer`, `Polyline`, `PointingIcon`, `Circle`, `fitBounds`를 활용합니다. 지도 아래 도구 패널에서 일반/위성 지도 전환, 교통 레이어, 거리뷰 가능 구역 레이어, 경로 전체 보기, 경로 재생, GPS 튐 보정, Naver 지도 열기를 제어합니다. diff --git a/php/api.php b/php/api.php index 7c48de5..f5e98b5 100644 --- a/php/api.php +++ b/php/api.php @@ -26,7 +26,7 @@ function findmydevice_mobile_command(string $message, array $data = []): bool { $config = custom_config(); $refresh = $config['findmydevice_location_refresh'] ?? []; - $profile = (string)($refresh['profile'] ?? 'seoul'); + $profiles = (array)($refresh['profiles'] ?? [$refresh['profile'] ?? 'seoul']); $service = (string)($refresh['notify_service'] ?? ''); if ($service === '') { @@ -38,7 +38,12 @@ function findmydevice_mobile_command(string $message, array $data = []): bool $payload['data'] = $data; } - return custom_ha_service($profile, 'notify', $service, $payload, 5); + $ok = false; + foreach (array_values(array_unique(array_filter(array_map('strval', $profiles)))) as $profile) { + $ok = custom_ha_service($profile, 'notify', $service, $payload, 5) || $ok; + } + + return $ok; } function findmydevice_location_refresh(string $name): array @@ -100,14 +105,81 @@ if (isset($_GET['action']) && $_GET['action'] === 'webhook') { $config = custom_config(); $entities = $config['entities']; -$allStates = custom_ha_request('seoul', 'GET', '/states') ?? []; -$stateMap = []; -foreach ($allStates as $stateRow) { - if (isset($stateRow['entity_id'])) { - $stateMap[$stateRow['entity_id']] = $stateRow; - } +$findmydeviceHaProfiles = ['seoul', 'main']; + +function isMeaningfulHaValue($state): bool +{ + return !($state === null || $state === '' || $state === 'unknown' || $state === 'unavailable' || $state === '' || $state === '' || $state === 'none'); } +function haRowTimestamp(array $row): int +{ + $time = $row['last_updated'] ?? $row['last_changed'] ?? null; + return $time ? (strtotime((string)$time) ?: 0) : 0; +} + +function mergeHaAttributes(array $primary, array $fallback): array +{ + $attrs = is_array($primary['attributes'] ?? null) ? $primary['attributes'] : []; + $fallbackAttrs = is_array($fallback['attributes'] ?? null) ? $fallback['attributes'] : []; + + foreach ($fallbackAttrs as $key => $value) { + if (!array_key_exists($key, $attrs) || !isMeaningfulHaValue($attrs[$key])) { + $attrs[$key] = $value; + } + } + + return $attrs; +} + +function mergeHaStateRow(?array $current, array $candidate): array +{ + if ($current === null) { + return $candidate; + } + + $currentMeaningful = isMeaningfulHaValue($current['state'] ?? null); + $candidateMeaningful = isMeaningfulHaValue($candidate['state'] ?? null); + + if ($candidateMeaningful && !$currentMeaningful) { + $candidate['attributes'] = mergeHaAttributes($candidate, $current); + return $candidate; + } + + if (!$candidateMeaningful && $currentMeaningful) { + $current['attributes'] = mergeHaAttributes($current, $candidate); + return $current; + } + + if (haRowTimestamp($candidate) > haRowTimestamp($current)) { + $candidate['attributes'] = mergeHaAttributes($candidate, $current); + return $candidate; + } + + $current['attributes'] = mergeHaAttributes($current, $candidate); + return $current; +} + +function loadMergedStateMap(array $profiles): array +{ + $stateMap = []; + foreach ($profiles as $profile) { + $allStates = custom_ha_request((string)$profile, 'GET', '/states') ?? []; + foreach ($allStates as $stateRow) { + if (!isset($stateRow['entity_id']) || !is_array($stateRow)) { + continue; + } + $entityId = (string)$stateRow['entity_id']; + $stateRow['_source_profile'] = (string)$profile; + $stateMap[$entityId] = mergeHaStateRow($stateMap[$entityId] ?? null, $stateRow); + } + } + + return $stateMap; +} + +$stateMap = loadMergedStateMap($findmydeviceHaProfiles); + // 엔드포인트 URL $deviceUrl = $entities['device']; $geoUrl = $entities['geo']; @@ -265,20 +337,41 @@ $ascendedData = getData($fascendedUrl); $descendedData = getData($fdescendedUrl); $stepsData = getData($stepsUrl); $distanceData = getData($distanceUrl); -$historyData = $includeHistory ? (custom_ha_request('seoul', 'GET', $historyEndpoint, null, 0) ?? []) : []; +$historySets = []; +if ($includeHistory) { + foreach ($findmydeviceHaProfiles as $profile) { + $historySets[(string)$profile] = custom_ha_request((string)$profile, 'GET', $historyEndpoint, null, 0) ?? []; + } +} $triggerData = getData($triggerUrl); // 경로 기록 $path = []; -if (isset($historyData[0])) { - foreach ($historyData[0] as $item) { - $lat = $item['attributes']['latitude'] ?? null; - $lng = $item['attributes']['longitude'] ?? null; - $time = $item['last_changed'] ?? null; - if ($lat && $lng) { - $path[] = ['lat' => $lat, 'lng' => $lng, 'time' => $time]; +$historySourceCounts = []; +if ($historySets) { + $pathByKey = []; + foreach ($historySets as $profile => $historyData) { + $historySourceCounts[$profile] = isset($historyData[0]) && is_array($historyData[0]) ? count($historyData[0]) : 0; + if (!isset($historyData[0]) || !is_array($historyData[0])) { + continue; + } + foreach ($historyData[0] as $item) { + $lat = $item['attributes']['latitude'] ?? null; + $lng = $item['attributes']['longitude'] ?? null; + $time = $item['last_changed'] ?? null; + if ($lat && $lng && $time) { + $timeKey = (string)(strtotime((string)$time) ?: $time); + $key = $timeKey . ':' . round((float)$lat, 5) . ':' . round((float)$lng, 5); + if (!isset($pathByKey[$key])) { + $pathByKey[$key] = ['lat' => $lat, 'lng' => $lng, 'time' => $time, 'source' => $profile]; + } + } } } + $path = array_values($pathByKey); + usort($path, static function (array $a, array $b): int { + return (strtotime((string)$a['time']) ?: 0) <=> (strtotime((string)$b['time']) ?: 0); + }); } function pathDistanceM(array $a, array $b): float @@ -695,5 +788,6 @@ echo json_encode([ 'history' => $includeHistory ? $path : null, 'historyRawCount' => $includeHistory ? $historyRawCount : null, 'historyRenderCount' => $includeHistory ? count($path) : null, + 'historySourceCounts' => $includeHistory ? $historySourceCounts : null, ], JSON_UNESCAPED_UNICODE); ?>