Use configured home location

This commit is contained in:
seo
2026-06-18 17:19:29 +09:00
parent 25fe661cc1
commit 325d521044
3 changed files with 46 additions and 11 deletions
+5 -3
View File
@@ -15,7 +15,7 @@ Home Assistant에 기록된 Galaxy Z Fold7의 위치, 배터리, 활동 상태,
- 사용자 지정 기간 이동 경로 표시 - 사용자 지정 기간 이동 경로 표시
- 이동 기록 요약, 경로 전체 보기, 경로 재생, 시간 슬라이더 - 이동 기록 요약, 경로 전체 보기, 경로 재생, 시간 슬라이더
- 부드러운 렌더링용 경로선, GPS 튐 보정 토글, 체류 지점 자동 표시 - 부드러운 렌더링용 경로선, GPS 튐 보정 토글, 체류 지점 자동 표시
- 현재 위치 기준 집 50m, 500m/1km 반경 표시와 선택 지점 Naver 지도 열기 - 설정된 집 좌표 기준 집 50m, 현재 위치 기준 500m/1km 반경 표시와 선택 지점 Naver 지도 열기
- 집 체류 합산, 외출/복귀 세션, 마지막 복귀 시각과 현재 상태 표시 - 집 체류 합산, 외출/복귀 세션, 마지막 복귀 시각과 현재 상태 표시
- 배터리, 충전 상태, 배터리 건강, 온도, 전력, 활동 상태, 걸음 수, 이동 거리 표시 - 배터리, 충전 상태, 배터리 건강, 온도, 전력, 활동 상태, 걸음 수, 이동 거리 표시
- 네트워크, 기기, 화면/오디오, 건강/활동, 저장공간/데이터, 알림/통화, 센서 정보를 섹션별로 표시 - 네트워크, 기기, 화면/오디오, 건강/활동, 저장공간/데이터, 알림/통화, 센서 정보를 섹션별로 표시
@@ -41,6 +41,8 @@ Home Assistant에 기록된 Galaxy Z Fold7의 위치, 배터리, 활동 상태,
위치 갱신 명령은 `/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`를 보냅니다. 위치 갱신 명령은 `/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`를 보냅니다.
집 판정 중심은 `/custom/common/config.php``findmydevice_home` 설정을 사용합니다. 현재 집 좌표는 `37.558181, 127.204440`, 반경은 `50m`입니다. API 응답의 `home` 값을 브라우저가 사용하므로 설정 변경 시 집 원, 체류/외출 판정, GPS 튐 흡수 기준이 같은 좌표로 적용됩니다.
## 히스토리와 지도 ## 히스토리와 지도
실시간 화면은 현재 위치와 상태를 조회하고, 기본 기간 입력값에 해당하는 최근 하루 이동 기록도 지도에 함께 표시합니다. 기간 입력을 변경하면 해당 범위의 이동 기록을 `history=1` 옵션으로 다시 요청합니다. 서버는 기간이 길어도 Home Assistant 히스토리 API를 실행한 뒤, 브라우저 렌더링용으로 경로를 부드럽게 단순화해 응답합니다. 실시간 화면은 현재 위치와 상태를 조회하고, 기본 기간 입력값에 해당하는 최근 하루 이동 기록도 지도에 함께 표시합니다. 기간 입력을 변경하면 해당 범위의 이동 기록을 `history=1` 옵션으로 다시 요청합니다. 서버는 기간이 길어도 Home Assistant 히스토리 API를 실행한 뒤, 브라우저 렌더링용으로 경로를 부드럽게 단순화해 응답합니다.
@@ -57,11 +59,11 @@ Naver Map에는 단일 Polyline 중심의 부드러운 경로선을 먼저 그
경로선은 Douglas-Peucker 단순화와 가벼운 좌표 smoothing을 거쳐 긴 기간에서도 완만한 단일 선으로 표시합니다. `GPS 튐 보정`이 켜져 있으면 집 중심 1.2km 안의 장기 체류성 흔들림은 집 위치로 흡수하고, 집 주변 보정 지점은 30분 단위로 줄여 지도 낙서를 줄입니다. 이전/다음 지점이 가까운데 중간 지점만 250m 이상 튀고 왕복 속도가 80km/h를 넘는 단발 스파이크, 500m 이상 220km/h 초과 이동, 2km 이상 120km/h 초과 이동, 정확도 300m 초과 지점은 제외합니다. 30분 이상 기록 공백은 선을 끊고 이동 거리 합산에서도 제외합니다. 지도 아래 `GPS 튐 보정` 버튼으로 원본 렌더링 경로와 보정 경로를 전환할 수 있습니다. 경로선은 Douglas-Peucker 단순화와 가벼운 좌표 smoothing을 거쳐 긴 기간에서도 완만한 단일 선으로 표시합니다. `GPS 튐 보정`이 켜져 있으면 집 중심 1.2km 안의 장기 체류성 흔들림은 집 위치로 흡수하고, 집 주변 보정 지점은 30분 단위로 줄여 지도 낙서를 줄입니다. 이전/다음 지점이 가까운데 중간 지점만 250m 이상 튀고 왕복 속도가 80km/h를 넘는 단발 스파이크, 500m 이상 220km/h 초과 이동, 2km 이상 120km/h 초과 이동, 정확도 300m 초과 지점은 제외합니다. 30분 이상 기록 공백은 선을 끊고 이동 거리 합산에서도 제외합니다. 지도 아래 `GPS 튐 보정` 버튼으로 원본 렌더링 경로와 보정 경로를 전환할 수 있습니다.
집 판정은 현재 위치를 중심점으로 한 50m 반경을 기준으로 합니다. GPS 튐 보정이 켜져 있으면 집 주변 1.2km 안의 지점은 집 주변 GPS 흔들림으로 흡수하고, 외출은 연속된 외부 지점이 5분 이상 지속될 때만 세션으로 확정합니다. 외부 지점 사이의 공백이 2시간을 넘으면 해당 구간은 알 수 없는 구간으로 보고 외출 지속시간에 합산하지 않습니다. 여러 번 나갔다 돌아온 경우 집 체류 시간은 합산하고, 외출은 출발/복귀 단위의 세션으로 따로 보여줍니다. 집 판정은 설정된 집 좌표를 중심점으로 한 50m 반경을 기준으로 합니다. GPS 튐 보정이 켜져 있으면 집 주변 1.2km 안의 지점은 집 주변 GPS 흔들림으로 흡수하고, 외출은 연속된 외부 지점이 5분 이상 지속될 때만 세션으로 확정합니다. 외부 지점 사이의 공백이 2시간을 넘으면 해당 구간은 알 수 없는 구간으로 보고 외출 지속시간에 합산하지 않습니다. 여러 번 나갔다 돌아온 경우 집 체류 시간은 합산하고, 외출은 출발/복귀 단위의 세션으로 따로 보여줍니다.
기록 시간 슬라이더를 움직이면 선택 마커가 해당 시점으로 이동하고, 선택 시각과 추정 속도를 지도 아래에 표시합니다. 경로 재생도 같은 선택 마커를 사용합니다. 80m 반경 안에서 10분 이상 머문 구간은 체류 지점 마커로 묶어 표시하며, 클릭 시 체류 시간과 시작/종료 시각을 확인할 수 있습니다. 기록 시간 슬라이더를 움직이면 선택 마커가 해당 시점으로 이동하고, 선택 시각과 추정 속도를 지도 아래에 표시합니다. 경로 재생도 같은 선택 마커를 사용합니다. 80m 반경 안에서 10분 이상 머문 구간은 체류 지점 마커로 묶어 표시하며, 클릭 시 체류 시간과 시작/종료 시각을 확인할 수 있습니다.
라이브 위치에는 집 50m 원, GPS 정확도 원, 500m/1km 현재 위치 반경 원을 함께 표시합니다. `Naver 지도` 버튼은 히스토리 선택 지점 또는 현재 위치를 Naver 지도에서 바로 엽니다. 라이브 위치에는 설정된 집 50m 원, GPS 정확도 원, 500m/1km 현재 위치 반경 원을 함께 표시합니다. `Naver 지도` 버튼은 히스토리 선택 지점 또는 현재 위치를 Naver 지도에서 바로 엽니다.
## 센서 표시 정책 ## 센서 표시 정책
+32 -7
View File
@@ -1312,6 +1312,29 @@ const HOME_JITTER_SAMPLE_SECONDS = 30 * 60;
const TRACK_MAX_GAP_SECONDS = 30 * 60; const TRACK_MAX_GAP_SECONDS = 30 * 60;
const AWAY_MAX_GAP_SECONDS = 2 * 60 * 60; const AWAY_MAX_GAP_SECONDS = 2 * 60 * 60;
function homeConfigFromData(data) {
const home = data?.home || {};
const radius = Number(home.radius_m || HOME_RADIUS_M);
return {
lat: Number(home.latitude),
lng: Number(home.longitude),
radius: Number.isFinite(radius) && radius > 0 ? radius : HOME_RADIUS_M,
};
}
function homeRadiusMeters() {
return mapState.homeCenter?.radius || HOME_RADIUS_M;
}
function updateConfiguredHome(data) {
const home = homeConfigFromData(data);
if (hasValidCoords(home.lat, home.lng)) {
updateHomeCenter(home.lat, home.lng, home.radius);
return true;
}
return false;
}
function pointWithLatLng(point, lat, lng, extra = {}) { function pointWithLatLng(point, lat, lng, extra = {}) {
return { return {
...point, ...point,
@@ -1408,12 +1431,13 @@ function buildStayPoints(points) {
return stays; return stays;
} }
function updateHomeCenter(lat, lng) { function updateHomeCenter(lat, lng, radius = HOME_RADIUS_M) {
if (!hasValidCoords(lat, lng)) return; if (!hasValidCoords(lat, lng)) return;
mapState.homeCenter = { mapState.homeCenter = {
lat: Number(lat), lat: Number(lat),
lng: Number(lng), lng: Number(lng),
latlng: new naver.maps.LatLng(Number(lat), Number(lng)) latlng: new naver.maps.LatLng(Number(lat), Number(lng)),
radius: Math.max(1, Number(radius) || HOME_RADIUS_M)
}; };
updateHomeCircle(); updateHomeCircle();
} }
@@ -1424,7 +1448,7 @@ function updateHomeCircle() {
mapState.homeCircle = new naver.maps.Circle({ mapState.homeCircle = new naver.maps.Circle({
map: mapState.map, map: mapState.map,
center: mapState.homeCenter.latlng, center: mapState.homeCenter.latlng,
radius: 50, radius: homeRadiusMeters(),
strokeColor: '#5856d6', strokeColor: '#5856d6',
strokeOpacity: 0.8, strokeOpacity: 0.8,
strokeWeight: 2, strokeWeight: 2,
@@ -1433,6 +1457,7 @@ function updateHomeCircle() {
}); });
} else { } else {
mapState.homeCircle.setCenter(mapState.homeCenter.latlng); mapState.homeCircle.setCenter(mapState.homeCenter.latlng);
mapState.homeCircle.setRadius(homeRadiusMeters());
} }
} }
@@ -1443,7 +1468,7 @@ function distanceFromHome(point) {
function classifyHomePoint(point) { function classifyHomePoint(point) {
const distance = distanceFromHome(point); const distance = distanceFromHome(point);
if (distance <= HOME_RADIUS_M) return 'home'; if (distance <= homeRadiusMeters()) return 'home';
if (mapState.filterOutliers && distance <= HOME_JITTER_ABSORB_RADIUS_M) return 'home'; if (mapState.filterOutliers && distance <= HOME_JITTER_ABSORB_RADIUS_M) return 'home';
if (distance <= HOME_NEAR_RADIUS_M) return 'near'; if (distance <= HOME_NEAR_RADIUS_M) return 'near';
return 'away'; return 'away';
@@ -1583,7 +1608,7 @@ function renderHistorySummary() {
<span>${t.historyMaxGap}</span><strong>${formatDuration(Math.round(stats.maxGap))}</strong> <span>${t.historyMaxGap}</span><strong>${formatDuration(Math.round(stats.maxGap))}</strong>
<span>${t.historyPoints}</span><strong>${stats.rawPoints && stats.rawPoints !== stats.renderPoints ? `${formatNumber(stats.renderPoints)} / ${formatNumber(stats.rawPoints)}` : formatNumber(stats.points)}</strong> <span>${t.historyPoints}</span><strong>${stats.rawPoints && stats.rawPoints !== stats.renderPoints ? `${formatNumber(stats.renderPoints)} / ${formatNumber(stats.rawPoints)}` : formatNumber(stats.points)}</strong>
<span>${t.historyStops}</span><strong>${formatNumber(stats.stops || 0)}</strong> <span>${t.historyStops}</span><strong>${formatNumber(stats.stops || 0)}</strong>
<span>${t.homeRadius}</span><strong>${mapState.filterOutliers ? '50m + GPS 1.2km' : '50m'}</strong> <span>${t.homeRadius}</span><strong>${mapState.filterOutliers ? `${formatNumber(homeRadiusMeters())}m + GPS 1.2km` : `${formatNumber(homeRadiusMeters())}m`}</strong>
<span>${t.homeStay}</span><strong>${home ? formatDuration(Math.round(home.homeSeconds)) : '-'}</strong> <span>${t.homeStay}</span><strong>${home ? formatDuration(Math.round(home.homeSeconds)) : '-'}</strong>
<span>${t.awayTime}</span><strong>${home ? formatDuration(Math.round(home.awaySeconds)) : '-'}</strong> <span>${t.awayTime}</span><strong>${home ? formatDuration(Math.round(home.awaySeconds)) : '-'}</strong>
<span>${t.awaySessions}</span><strong>${home ? formatNumber(sessions.length) : '-'}</strong> <span>${t.awaySessions}</span><strong>${home ? formatNumber(sessions.length) : '-'}</strong>
@@ -2088,7 +2113,7 @@ function updateDOMWithData(data) {
if (!mapState.map && baseLatLng) { if (!mapState.map && baseLatLng) {
initMap(baseLatLng, data.accuracy || 50); initMap(baseLatLng, data.accuracy || 50);
} }
updateHomeCenter(data.latitude, data.longitude); updateConfiguredHome(data) || updateHomeCenter(data.latitude, data.longitude);
// 히스토리 궤적/포인트 업데이트 // 히스토리 궤적/포인트 업데이트
if (data.history?.length > 0) { if (data.history?.length > 0) {
@@ -2105,7 +2130,7 @@ function updateDOMWithData(data) {
// ── 라이브 모드 기존 로직 ── // ── 라이브 모드 기존 로직 ──
const latlng = new naver.maps.LatLng(data.latitude, data.longitude); const latlng = new naver.maps.LatLng(data.latitude, data.longitude);
updateHomeCenter(data.latitude, data.longitude); updateConfiguredHome(data) || updateHomeCenter(data.latitude, data.longitude);
// DOM 업데이트 // DOM 업데이트
updateDOM(data); updateDOM(data);
+8
View File
@@ -106,6 +106,12 @@ if (isset($_GET['action']) && $_GET['action'] === 'webhook') {
$config = custom_config(); $config = custom_config();
$entities = $config['entities']; $entities = $config['entities'];
$findmydeviceHaProfiles = ['seoul', 'main']; $findmydeviceHaProfiles = ['seoul', 'main'];
$findmydeviceHomeConfig = is_array($config['findmydevice_home'] ?? null) ? $config['findmydevice_home'] : [];
$findmydeviceHome = [
'latitude' => isset($findmydeviceHomeConfig['latitude']) ? (float)$findmydeviceHomeConfig['latitude'] : null,
'longitude' => isset($findmydeviceHomeConfig['longitude']) ? (float)$findmydeviceHomeConfig['longitude'] : null,
'radius_m' => max(1, (int)($findmydeviceHomeConfig['radius_m'] ?? 50)),
];
function isMeaningfulHaValue($state): bool function isMeaningfulHaValue($state): bool
{ {
@@ -771,6 +777,7 @@ if (isset($_GET['hashonly']) && $_GET['hashonly'] == '1') {
'aaccuracy' => $aaccuracyMap[$lang][$aaccuracyState] ?? $aaccuracyState ?? null, 'aaccuracy' => $aaccuracyMap[$lang][$aaccuracyState] ?? $aaccuracyState ?? null,
'connection' => $connectionMap[$lang][$connectionState] ?? $connectionState ?? null, 'connection' => $connectionMap[$lang][$connectionState] ?? $connectionState ?? null,
'sensors' => $sensorSections, 'sensors' => $sensorSections,
'home' => $findmydeviceHome,
]); ]);
echo json_encode(['hash' => hash('sha256', $raw)]); echo json_encode(['hash' => hash('sha256', $raw)]);
@@ -794,6 +801,7 @@ echo json_encode([
'aaccuracy' => $aaccuracyMap[$lang][$aaccuracyState] ?? $aaccuracyState ?? null, 'aaccuracy' => $aaccuracyMap[$lang][$aaccuracyState] ?? $aaccuracyState ?? null,
'connection' => $connectionMap[$lang][$connectionState] ?? $connectionState ?? null, 'connection' => $connectionMap[$lang][$connectionState] ?? $connectionState ?? null,
'sensors' => $sensorSections, 'sensors' => $sensorSections,
'home' => $findmydeviceHome,
'history' => $includeHistory ? $path : null, 'history' => $includeHistory ? $path : null,
'historyRawCount' => $includeHistory ? $historyRawCount : null, 'historyRawCount' => $includeHistory ? $historyRawCount : null,
'historyRenderCount' => $includeHistory ? count($path) : null, 'historyRenderCount' => $includeHistory ? count($path) : null,