Merge findmydevice data from both HA profiles
This commit is contained in:
@@ -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 지도 열기를 제어합니다.
|
||||
|
||||
+103
-9
@@ -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') ?? [];
|
||||
$findmydeviceHaProfiles = ['seoul', 'main'];
|
||||
|
||||
function isMeaningfulHaValue($state): bool
|
||||
{
|
||||
return !($state === null || $state === '' || $state === 'unknown' || $state === 'unavailable' || $state === '<not connected>' || $state === '<not available>' || $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'])) {
|
||||
$stateMap[$stateRow['entity_id']] = $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,21 +337,42 @@ $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])) {
|
||||
$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) {
|
||||
$path[] = ['lat' => $lat, 'lng' => $lng, 'time' => $time];
|
||||
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);
|
||||
?>
|
||||
|
||||
Reference in New Issue
Block a user