'Method Not Allowed']); exit; } header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-store, no-cache, must-revalidate'); header('Expires: 0'); if (function_exists('set_time_limit')) { @set_time_limit(0); } function findmydevice_is_pwa_request(): bool { if (($_SERVER['HTTP_X_CUSTOM_PWA'] ?? '') !== 'findmydevice') { return false; } $allowedHosts = ['chaegeon.com', 'www.chaegeon.com']; foreach (['HTTP_ORIGIN', 'HTTP_REFERER'] as $key) { $value = (string)($_SERVER[$key] ?? ''); if ($value === '') { continue; } $host = parse_url($value, PHP_URL_HOST); if ($host !== null && $host !== false && !in_array(strtolower($host), $allowedHosts, true)) { return false; } } return true; } function findmydevice_require_auth(): void { if (function_exists('custom_has_auth_cookie') && custom_has_auth_cookie()) { return; } if (function_exists('custom_is_site_admin') && custom_is_site_admin()) { if (function_exists('custom_issue_auth_cookie')) { custom_issue_auth_cookie(true); } return; } if (findmydevice_is_pwa_request()) { return; } custom_json(['result' => 'unauthorized'], 401); } findmydevice_require_auth(); $requestData = custom_read_request_data(); function findmydevice_mobile_command(string $message, array $data = []): bool { $config = custom_config(); $refresh = $config['findmydevice_location_refresh'] ?? []; $profiles = (array)($refresh['profiles'] ?? [$refresh['profile'] ?? 'seoul']); $service = (string)($refresh['notify_service'] ?? ''); if ($service === '') { return false; } $payload = ['message' => $message]; if ($data) { $payload['data'] = $data; } $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_ha_profiles(): array { $config = custom_config(); $refresh = $config['findmydevice_location_refresh'] ?? []; return array_values(array_unique(array_filter(array_map('strval', (array)($refresh['profiles'] ?? [$refresh['profile'] ?? 'seoul']))))); } function findmydevice_state_timestamp(?array $state): int { if (!$state) { return 0; } $time = $state['last_updated'] ?? $state['last_changed'] ?? null; return $time ? (strtotime((string)$time) ?: 0) : 0; } function findmydevice_latest_state(string $entityId): ?array { $latest = null; foreach (findmydevice_ha_profiles() as $profile) { $state = custom_ha_state($profile, $entityId); if (!$state) { continue; } $state['_source_profile'] = $profile; if (!$latest || findmydevice_state_timestamp($state) > findmydevice_state_timestamp($latest)) { $latest = $state; } } return $latest; } function findmydevice_distance_meters(float $lat1, float $lng1, float $lat2, float $lng2): float { $earth = 6371000; $dLat = deg2rad($lat2 - $lat1); $dLng = deg2rad($lng2 - $lng1); $a = sin($dLat / 2) ** 2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) ** 2; return $earth * 2 * atan2(sqrt($a), sqrt(1 - $a)); } function findmydevice_current_position(): ?array { $config = custom_config(); $device = (string)($config['entities']['device'] ?? ''); if ($device === '') { return null; } $state = findmydevice_latest_state($device); $attrs = is_array($state['attributes'] ?? null) ? $state['attributes'] : []; if (!isset($attrs['latitude'], $attrs['longitude']) || !is_numeric($attrs['latitude']) || !is_numeric($attrs['longitude'])) { return null; } return [ 'latitude' => (float)$attrs['latitude'], 'longitude' => (float)$attrs['longitude'], 'state' => $state['state'] ?? null, 'source' => $state['_source_profile'] ?? null, ]; } function findmydevice_quiet_zone(?array $position): ?array { if (!$position) { return null; } $config = custom_config(); foreach ((array)($config['findmydevice_quiet_zones'] ?? []) as $zone) { if (!is_array($zone) || !isset($zone['latitude'], $zone['longitude'])) { continue; } $radius = max(1, (int)($zone['radius_m'] ?? 150)); $distance = findmydevice_distance_meters( (float)$position['latitude'], (float)$position['longitude'], (float)$zone['latitude'], (float)$zone['longitude'] ); if ($distance <= $radius) { return [ 'name' => (string)($zone['name'] ?? ''), 'label' => (string)($zone['label'] ?? $zone['name'] ?? 'quiet_zone'), 'distance_m' => round($distance, 1), 'radius_m' => $radius, ]; } } return null; } function findmydevice_required_bluetooth_connected(): bool { $config = custom_config(); $refresh = $config['findmydevice_location_refresh'] ?? []; $targets = array_map('strtolower', array_map('strval', (array)($refresh['required_bluetooth_names'] ?? ['Niro']))); $targets = array_values(array_filter($targets)); if (!$targets) { return true; } $entity = (string)($config['findmydevice_sensor_sections']['environment']['bluetoothConnection']['entity'] ?? 'sensor.seocaegeonyi_z_fold7_bluetooth_connection'); $state = findmydevice_latest_state($entity); $attrs = is_array($state['attributes'] ?? null) ? $state['attributes'] : []; $values = []; if (isset($state['state'])) { $values[] = (string)$state['state']; } foreach ((array)($attrs['connected_paired_devices'] ?? []) as $device) { $values[] = (string)$device; if (str_contains((string)$device, ' (')) { $values[] = rtrim(substr((string)$device, strrpos((string)$device, ' (') + 2), ')'); } } foreach ($values as $value) { $lower = strtolower($value); foreach ($targets as $target) { if ($target !== '' && str_contains($lower, $target)) { return true; } } } return false; } function findmydevice_is_charging(): bool { $config = custom_config(); $entity = (string)($config['entities']['is_charging'] ?? ''); if ($entity === '') { return false; } $state = findmydevice_latest_state($entity); return strtolower((string)($state['state'] ?? '')) === 'on'; } function findmydevice_auto_tracking_policy(): array { $position = findmydevice_current_position(); $zone = findmydevice_quiet_zone($position); $bluetooth = findmydevice_required_bluetooth_connected(); $charging = findmydevice_is_charging(); return [ 'allowed' => !$zone && ($charging || $bluetooth), 'position' => $position, 'quiet_zone' => $zone, 'charging' => $charging, 'required_bluetooth_connected' => $bluetooth, ]; } function findmydevice_bool_result_exists(array $results): bool { foreach ($results as $value) { if ($value === true) { return true; } } return false; } function findmydevice_location_refresh(string $name): array { $config = custom_config(); $refresh = $config['findmydevice_location_refresh'] ?? []; $interval = max(1, (int)($refresh['high_accuracy_interval'] ?? 1)); $results = []; if ($name === 'auto_on') { $policy = findmydevice_auto_tracking_policy(); if (!$policy['allowed']) { $results['policy'] = 'blocked'; $results['quietZone'] = $policy['quiet_zone']; $results['charging'] = $policy['charging']; $results['requiredBluetoothConnected'] = $policy['required_bluetooth_connected']; $results['allowWebhook'] = false; $results['high_accuracy_off'] = findmydevice_mobile_command('command_high_accuracy_mode', [ 'command' => 'force_off', ]); return $results; } $results['policy'] = 'allowed'; $results['quietZone'] = null; $results['charging'] = $policy['charging']; $results['requiredBluetoothConnected'] = $policy['required_bluetooth_connected']; $results['high_accuracy_interval'] = findmydevice_mobile_command('command_high_accuracy_mode', [ 'command' => 'high_accuracy_set_update_interval', 'high_accuracy_update_interval' => $interval, ]); $results['high_accuracy_on'] = findmydevice_mobile_command('command_high_accuracy_mode', [ 'command' => 'force_on', ]); $results['request_location'] = findmydevice_mobile_command('request_location_update'); $results['update_sensors'] = findmydevice_mobile_command('command_update_sensors'); } elseif ($name === 'auto_tick') { $policy = findmydevice_auto_tracking_policy(); if (!$policy['allowed']) { $results['policy'] = 'blocked'; $results['quietZone'] = $policy['quiet_zone']; $results['charging'] = $policy['charging']; $results['requiredBluetoothConnected'] = $policy['required_bluetooth_connected']; $results['allowWebhook'] = false; $results['high_accuracy_off'] = findmydevice_mobile_command('command_high_accuracy_mode', [ 'command' => 'force_off', ]); return $results; } $results['policy'] = 'allowed'; $results['quietZone'] = null; $results['charging'] = $policy['charging']; $results['requiredBluetoothConnected'] = $policy['required_bluetooth_connected']; $results['request_location'] = findmydevice_mobile_command('request_location_update'); $results['update_sensors'] = findmydevice_mobile_command('command_update_sensors'); } elseif ($name === 'force_update') { $results['request_location'] = findmydevice_mobile_command('request_location_update'); $results['update_sensors'] = findmydevice_mobile_command('command_update_sensors'); } elseif ($name === 'auto_off') { $results['high_accuracy_off'] = findmydevice_mobile_command('command_high_accuracy_mode', [ 'command' => 'force_off', ]); } return $results; } if (isset($_GET['action']) && $_GET['action'] === 'webhook') { $name = (string)($requestData['name'] ?? ''); $allowed = [ 'auto_on' => 'find_auto_on', 'auto_off' => 'find_auto_off', 'auto_tick' => 'find_auto_tick', 'force_update' => 'find_force_update', ]; if (!isset($allowed[$name])) { custom_json(['result' => 'invalid_webhook'], 400); } $mobileResults = findmydevice_location_refresh($name); $webhookOk = ($mobileResults['allowWebhook'] ?? true) === false ? false : custom_ha_webhook($allowed[$name]); $mobileOk = !$mobileResults || findmydevice_bool_result_exists($mobileResults); $ok = $webhookOk || $mobileOk; custom_json([ 'result' => $ok ? 'ok' : 'fail', 'webhook' => $webhookOk ? 'ok' : 'fail', 'mobileCommands' => $mobileResults, ], $ok ? 200 : 502); } $config = custom_config(); $entities = $config['entities']; $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)), 'label' => '서울 집', ]; $findmydeviceHomes = []; foreach ((array)($config['findmydevice_quiet_zones'] ?? []) as $zone) { if (!is_array($zone) || !isset($zone['latitude'], $zone['longitude'])) { continue; } $findmydeviceHomes[] = [ 'name' => (string)($zone['name'] ?? ''), 'label' => (string)($zone['label'] ?? $zone['name'] ?? '집'), 'latitude' => (float)$zone['latitude'], 'longitude' => (float)$zone['longitude'], 'radius_m' => max(1, (int)($zone['radius_m'] ?? $findmydeviceHome['radius_m'])), ]; } if (!$findmydeviceHomes && isset($findmydeviceHome['latitude'], $findmydeviceHome['longitude'])) { $findmydeviceHomes[] = $findmydeviceHome; } 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']; $batteryStateUrl = $entities['battery_state']; $batteryLevelUrl = $entities['battery_level']; $activityUrl = $entities['activity']; $connectionUrl = $entities['connection']; $fascendedUrl = $entities['daily_floors']; $fdescendedUrl = $entities['daily_floors']; $stepsUrl = $entities['daily_steps']; $distanceUrl = $entities['daily_distance']; $triggerUrl = $entities['trigger']; $highAccuracyModeUrl = $entities['high_accuracy_mode'] ?? 'binary_sensor.seocaegeonyi_z_fold7_high_accuracy_mode'; // GET 파라미터 읽기 $startParam = $_GET['startTime'] ?? null; $endParam = $_GET['endTime'] ?? null; // Home Assistant에서 인식 가능한 UTC 포맷 $haFormat = 'Y-m-d\TH:i:s\Z'; if ($startParam && $endParam) { // 전달받은 값을 DateTime으로 변환 후 UTC 포맷으로 변환 $startTime = (new DateTime($startParam))->setTimezone(new DateTimeZone('UTC'))->format($haFormat); $endTime = (new DateTime($endParam))->setTimezone(new DateTimeZone('UTC'))->format($haFormat); } else { // 기본값: 최근 24시간 $startTime = (new DateTime('-24 hours'))->setTimezone(new DateTimeZone('UTC'))->format($haFormat); $endTime = (new DateTime())->setTimezone(new DateTimeZone('UTC'))->format($haFormat); } // 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']); // 공통 요청 함수 function getData($entityId) { global $stateMap; return $stateMap[$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 === '' || $state === 'none') { 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; } function findmydevice_starts_with(string $value, string $prefix): bool { return strncmp($value, $prefix, strlen($prefix)) === 0; } function readableTriggerCode(string $value, string $lang): string { $text = trim(preg_replace('/\s+/', ' ', str_replace(['_', '.', '-'], ' ', $value))); if ($text === '') { return $value; } return $lang === 'ko' ? $text : ucwords(strtolower($text)); } function formatTriggerValue($state, string $lang, array $map): ?string { if ($state === null) { return null; } $state = trim((string)$state); if ($state === '' || $state === 'unknown' || $state === 'unavailable') { return null; } $language = isset($map[$lang]) ? $lang : 'ko'; if (isset($map[$language][$state])) { return $map[$language][$state]; } $prefixes = [ 'android.intent.action.' => $language === 'ko' ? 'Android 동작' : 'Android action', 'android.bluetooth.device.action.' => $language === 'ko' ? '블루투스 기기' : 'Bluetooth device', 'android.bluetooth.adapter.action.' => $language === 'ko' ? '블루투스' : 'Bluetooth', 'android.media.' => $language === 'ko' ? '미디어' : 'Media', 'android.net.wifi.' => $language === 'ko' ? 'Wi-Fi' : 'Wi-Fi', 'android.net.' => $language === 'ko' ? '네트워크' : 'Network', 'android.nfc.action.' => $language === 'ko' ? 'NFC' : 'NFC', 'android.os.action.' => $language === 'ko' ? 'Android 시스템' : 'Android system', 'android.app.action.' => $language === 'ko' ? 'Android 앱' : 'Android app', 'io.homeassistant.companion.android.' => $language === 'ko' ? 'Home Assistant 앱' : 'Home Assistant app', 'com.google.android.gms.location.' => $language === 'ko' ? 'Google 위치' : 'Google location', 'androidx.car.app.connection.action.' => $language === 'ko' ? '차량 연결' : 'Car connection', ]; foreach ($prefixes as $prefix => $label) { if (findmydevice_starts_with($state, $prefix)) { return $label . ': ' . readableTriggerCode(substr($state, strlen($prefix)), $language); } } return readableTriggerCode($state, $language); } 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; 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') { return $value; } if (isset($definition['map'], $maps[$definition['map']])) { $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']; } return $value; } // 데이터 불러오기 $deviceData = getData($deviceUrl); $geoData = getData($geoUrl); $batteryStateData = getData($batteryStateUrl); $batteryLevelData = getData($batteryLevelUrl); $activityData = getData($activityUrl); $connectionData = getData($connectionUrl); $ascendedData = getData($fascendedUrl); $descendedData = getData($fdescendedUrl); $stepsData = getData($stepsUrl); $distanceData = getData($distanceUrl); $highAccuracyModeData = getData($highAccuracyModeUrl); $historySets = []; if ($includeHistory) { foreach ($findmydeviceHaProfiles as $profile) { $historySets[(string)$profile] = custom_ha_request((string)$profile, 'GET', $historyEndpoint, null, 0) ?? []; } } $triggerData = getData($triggerUrl); // 경로 기록 $path = []; $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; $accuracy = $item['attributes']['gps_accuracy'] ?? $item['attributes']['accuracy'] ?? 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, 'accuracy' => is_numeric($accuracy) ? (float)$accuracy : null, '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); }); } $historyRawCount = count($path); $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' => '-', ] ]; $aaccuracyMap = [ 'ko' => [ 'Low' => '낮음', 'Medium' => '중간', 'High' => '높음', 'Unknown' => '-', ], 'en' => [ 'Low' => 'Low', 'Medium' => 'Medium', 'High' => 'High', 'Unknown' => '-', ] ]; $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' => '-', ] ]; $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', 'dock' => '도크'], 'en' => ['none' => 'None', 'wireless' => 'Wireless', 'ac' => 'AC', 'usb' => 'USB', 'dock' => 'Dock'], ], 'batteryHealth' => [ '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'], ], 'lastUsedApp' => [ 'ko' => [ 'ci.dvn.composecoffee.app' => '컴포즈커피', 'co.kr.waldlust.megacoffee' => '메가MGC커피', 'com.adobe.reader' => 'Adobe Acrobat', 'com.android.settings' => '설정', 'com.everytime.v2' => '에브리타임', 'com.ex.hipass_app' => '고속도로 통행료', 'com.google.android.apps.youtube.music' => 'YouTube Music', 'com.google.android.projection.gearhead' => 'Android Auto', 'com.google.android.youtube' => 'YouTube', 'com.ibk.android.ionebank' => 'IBK i-ONE Bank', 'com.instagram.android' => 'Instagram', 'com.instagram.barcelona' => 'Threads', 'com.kakao.talk' => '카카오톡', 'com.nbdproject.macarong' => '마카롱', 'com.nhn.android.nmap' => '네이버 지도', 'com.openai.chatgpt' => 'ChatGPT', 'com.samsung.android.email.provider' => '삼성 이메일', 'com.samsung.android.incallui' => '전화', 'com.samsung.android.messaging' => '삼성 메시지', 'com.samsung.android.spay' => '삼성월렛', 'com.sec.android.app.launcher' => 'One UI 홈', 'com.sec.android.app.popupcalculator' => '계산기', 'com.sec.android.app.sbrowser' => '삼성 인터넷', 'com.sec.android.gallery3d' => '갤러리', 'com.synology.dsdrive' => 'Synology Drive', 'com.synology.projectkailash' => 'Synology Photos', 'com.wooricard.smartapp' => '우리카드', 'droom.sleepIfUCan' => '알라미', 'kr.co.angelinus.gift.m' => '엔제리너스', 'kr.co.iparking.android' => '아이파킹', 'mureung.obdproject' => 'OBD 프로젝트', 'viva.republica.toss' => '토스', ], 'en' => [ 'ci.dvn.composecoffee.app' => 'Compose Coffee', 'co.kr.waldlust.megacoffee' => 'Mega MGC Coffee', 'com.adobe.reader' => 'Adobe Acrobat', 'com.android.settings' => 'Settings', 'com.everytime.v2' => 'Everytime', 'com.ex.hipass_app' => 'Highway Toll', 'com.google.android.apps.youtube.music' => 'YouTube Music', 'com.google.android.projection.gearhead' => 'Android Auto', 'com.google.android.youtube' => 'YouTube', 'com.ibk.android.ionebank' => 'IBK i-ONE Bank', 'com.instagram.android' => 'Instagram', 'com.instagram.barcelona' => 'Threads', 'com.kakao.talk' => 'KakaoTalk', 'com.nbdproject.macarong' => 'Macarong', 'com.nhn.android.nmap' => 'Naver Map', 'com.openai.chatgpt' => 'ChatGPT', 'com.samsung.android.email.provider' => 'Samsung Email', 'com.samsung.android.incallui' => 'Phone', 'com.samsung.android.messaging' => 'Samsung Messages', 'com.samsung.android.spay' => 'Samsung Wallet', 'com.sec.android.app.launcher' => 'One UI Home', 'com.sec.android.app.popupcalculator' => 'Calculator', 'com.sec.android.app.sbrowser' => 'Samsung Internet', 'com.sec.android.gallery3d' => 'Gallery', 'com.synology.dsdrive' => 'Synology Drive', 'com.synology.projectkailash' => 'Synology Photos', 'com.wooricard.smartapp' => 'Woori Card', 'droom.sleepIfUCan' => 'Alarmy', 'kr.co.angelinus.gift.m' => 'Angelinus', 'kr.co.iparking.android' => 'iParking', 'mureung.obdproject' => 'OBD Project', 'viva.republica.toss' => 'Toss', ], ], '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 = [ 'ko' => [ "Launch" => "앱 실행", "Background Fetch" => "백그라운드 갱신", "Push Notification" => "푸시 알림", "Periodic" => "주기 갱신", "Significant Location Change" => "큰 위치 변화", "Manual" => "수동 갱신", "Signaled" => "외부 신호", "Siri" => "Siri 실행", "Watch Context" => "워치 상태 전송", "Geographic Region Entered" => "지정 지역에 들어옴", "Geographic Region Exited" => "지정 지역을 벗어남", "SensorWorker" => "센서 자동 갱신", "android.app.action.NEXT_ALARM_CLOCK_CHANGED" => "다음 알람 변경", "android.bluetooth.device.action.ACL_CONNECTED" => "블루투스 기기 연결", "android.bluetooth.device.action.ACL_DISCONNECTED" => "블루투스 기기 연결 해제", "android.intent.action.ACTION_POWER_CONNECTED" => "충전기 연결", "android.intent.action.ACTION_POWER_DISCONNECTED" => "충전기 분리", "android.intent.action.BATTERY_CHANGED" => "배터리 상태 변경", "android.intent.action.CONFIGURATION_CHANGED" => "기기 설정 변경", "android.intent.action.HEADSET_PLUG" => "헤드셋 연결 변경", "android.intent.action.PHONE_STATE" => "전화 상태 변경", "android.intent.action.TIME_TICK" => "1분 주기 갱신", "android.intent.action.SCREEN_ON" => "화면 켜짐", "android.intent.action.SCREEN_OFF" => "화면 꺼짐", "android.intent.action.USER_PRESENT" => "잠금 해제", "android.intent.action.BOOT_COMPLETED" => "부팅 완료", "android.intent.action.MY_PACKAGE_REPLACED" => "Home Assistant 앱 업데이트", "android.intent.action.PACKAGE_REPLACED" => "앱 패키지 업데이트", "android.media.AUDIO_BECOMING_NOISY" => "오디오 출력 변경", "android.media.RINGER_MODE_CHANGED" => "벨소리 모드 변경", "android.media.VOLUME_CHANGED_ACTION" => "볼륨 변경", "android.net.conn.CONNECTIVITY_CHANGE" => "네트워크 연결 변경", "android.net.wifi.STATE_CHANGE" => "Wi-Fi 연결 변경", "android.net.wifi.WIFI_AP_STATE_CHANGED" => "핫스팟 상태 변경", "android.net.wifi.WIFI_STATE_CHANGED" => "Wi-Fi 상태 변경", "androidx.car.app.connection.action.CAR_CONNECTION_UPDATED" => "차량 연결 상태 변경", "android.nfc.action.ADAPTER_STATE_CHANGED" => "NFC 상태 변경", "android.os.action.DEVICE_IDLE_MODE_CHANGED" => "절전 대기 상태 변경", "android.bluetooth.adapter.action.STATE_CHANGED" => "블루투스 상태 변경", "android.location.PROVIDERS_CHANGED" => "위치 제공자 변경", "android.location.MODE_CHANGED" => "위치 모드 변경", "com.google.android.gms.location.LOCATION_UPDATE" => "Google 위치 갱신", "io.homeassistant.companion.android.UPDATE_SENSORS" => "센서 갱신 명령", ], 'en' => [ "Launch" => "Launch", "Background Fetch" => "Background Fetch", "Push Notification" => "Push Notification", "Periodic" => "Periodic", "Significant Location Change" => "Significant Location Change", "Manual" => "Manual", "Signaled" => "Signaled", "Siri" => "Siri", "Watch Context" => "Watch Context", "Geographic Region Entered" => "Geographic Region Entered", "Geographic Region Exited" => "Geographic Region Exited", "SensorWorker" => "Sensor Worker", "android.app.action.NEXT_ALARM_CLOCK_CHANGED" => "Next Alarm Changed", "android.bluetooth.device.action.ACL_CONNECTED" => "Bluetooth Device Connected", "android.bluetooth.device.action.ACL_DISCONNECTED" => "Bluetooth Device Disconnected", "android.intent.action.ACTION_POWER_CONNECTED" => "Power Connected", "android.intent.action.ACTION_POWER_DISCONNECTED" => "Power Disconnected", "android.intent.action.BATTERY_CHANGED" => "Battery Changed", "android.intent.action.CONFIGURATION_CHANGED" => "Configuration Changed", "android.intent.action.HEADSET_PLUG" => "Headset Changed", "android.intent.action.PHONE_STATE" => "Phone State Changed", "android.intent.action.TIME_TICK" => "Minute Tick", "android.intent.action.SCREEN_ON" => "Screen On", "android.intent.action.SCREEN_OFF" => "Screen Off", "android.intent.action.USER_PRESENT" => "User Unlocked", "android.intent.action.BOOT_COMPLETED" => "Boot Completed", "android.intent.action.MY_PACKAGE_REPLACED" => "Home Assistant App Updated", "android.intent.action.PACKAGE_REPLACED" => "App Package Updated", "android.media.AUDIO_BECOMING_NOISY" => "Audio Output Changed", "android.media.RINGER_MODE_CHANGED" => "Ringer Mode Changed", "android.media.VOLUME_CHANGED_ACTION" => "Volume Changed", "android.net.conn.CONNECTIVITY_CHANGE" => "Network Changed", "android.net.wifi.STATE_CHANGE" => "Wi-Fi Connection Changed", "android.net.wifi.WIFI_AP_STATE_CHANGED" => "Hotspot State Changed", "android.net.wifi.WIFI_STATE_CHANGED" => "Wi-Fi State Changed", "androidx.car.app.connection.action.CAR_CONNECTION_UPDATED" => "Car Connection Changed", "android.nfc.action.ADAPTER_STATE_CHANGED" => "NFC State Changed", "android.os.action.DEVICE_IDLE_MODE_CHANGED" => "Device Idle Mode Changed", "android.bluetooth.adapter.action.STATE_CHANGED" => "Bluetooth State Changed", "android.location.PROVIDERS_CHANGED" => "Location Provider Changed", "android.location.MODE_CHANGED" => "Location Mode Changed", "com.google.android.gms.location.LOCATION_UPDATE" => "Google Location Update", "io.homeassistant.companion.android.UPDATE_SENSORS" => "Sensor Update Command", ] ]; $activityState = $activityData['state'] ?? null; $aaccuracyState = $activityData['attributes']['Confidence'] ?? $activityData['attributes']['confidence'] ?? null; $connectionState = $connectionData['state'] ?? null; $batteryState = $batteryStateData['state'] ?? null; $highAccuracyMode = strtolower((string)($highAccuracyModeData['state'] ?? '')) === 'on'; $address = null; if (($deviceData['state'] ?? null) !== 'not_home') { $address = $deviceData['state']; } else { // Locality 값 확인 $locality = $geoData['attributes']['Locality'] ?? null; // Locality가 없거나 N/A면 Sub Administrative Area로 대체 if (empty($locality) || $locality === 'N/A') { $locality = $geoData['attributes']['Sub Administrative Area'] ?? ''; } // Sub Administrative Area 도 없을 경우 Name만 사용 $address = trim($locality . ' ' . ($geoData['attributes']['Name'] ?? '')); } $sensorSections = []; $flatSensors = []; foreach (($config['findmydevice_sensor_sections'] ?? []) as $sectionKey => $definitions) { $sensorSections[$sectionKey] = []; foreach ($definitions as $sensorKey => $definition) { if (!empty($definition['requires']) && empty($flatSensors[$definition['requires']])) { continue; } $raw = stateValue(getData($definition['entity'])); $value = formatSensorValue($raw, $definition, $lang, $androidValueMap); if ($value === null || $value === '') { continue; } $sensorSections[$sectionKey][$sensorKey] = $value; $flatSensors[$sensorKey] = $value; } if (!$sensorSections[$sectionKey]) { unset($sensorSections[$sectionKey]); } } // 해시 요청만 들어온 경우 if (isset($_GET['hashonly']) && $_GET['hashonly'] == '1') { $raw = json_encode([ 'latitude' => $deviceData['attributes']['latitude'] ?? null, 'longitude' => $deviceData['attributes']['longitude'] ?? null, 'accuracy' => $deviceData['attributes']['gps_accuracy'] ?? null, 'updated' => $deviceData['last_changed'] ?? null, 'trigger' => formatTriggerValue($triggerData['state'] ?? null, $lang, $triggerMap), 'address' => $address, 'state' => $batteryStateMap[$lang][$batteryState] ?? $batteryState ?? null, 'level' => $batteryLevelData['state'] ?? null, 'ascended' => $ascendedData['state'] ?? null, 'descended' => $descendedData['state'] ?? null, 'steps' => $stepsData['state'] ?? null, 'distance' => $distanceData['state'] ?? null, 'activity' => $activityMap[$lang][$activityState] ?? $activityState ?? null, 'aaccuracy' => $aaccuracyMap[$lang][$aaccuracyState] ?? $aaccuracyState ?? null, 'connection' => $connectionMap[$lang][$connectionState] ?? $connectionState ?? null, 'highAccuracyMode' => $highAccuracyMode, 'sensors' => $sensorSections, 'home' => $findmydeviceHome, 'homes' => $findmydeviceHomes, ]); echo json_encode(['hash' => hash('sha256', $raw)]); exit; } echo json_encode([ 'latitude' => $deviceData['attributes']['latitude'] ?? null, 'longitude' => $deviceData['attributes']['longitude'] ?? null, 'accuracy' => $deviceData['attributes']['gps_accuracy'] ?? null, 'updated' => $deviceData['last_changed'] ?? null, 'trigger' => formatTriggerValue($triggerData['state'] ?? null, $lang, $triggerMap), 'address' => $address, 'state' => $batteryStateMap[$lang][$batteryState] ?? $batteryState ?? null, 'level' => $batteryLevelData['state'] ?? null, 'ascended' => $ascendedData['state'] ?? null, 'descended' => $descendedData['state'] ?? null, 'steps' => $stepsData['state'] ?? null, 'distance' => $distanceData['state'] ?? null, 'activity' => $activityMap[$lang][$activityState] ?? $activityState ?? null, 'aaccuracy' => $aaccuracyMap[$lang][$aaccuracyState] ?? $aaccuracyState ?? null, 'connection' => $connectionMap[$lang][$connectionState] ?? $connectionState ?? null, 'highAccuracyMode' => $highAccuracyMode, 'sensors' => $sensorSections, 'home' => $findmydeviceHome, 'homes' => $findmydeviceHomes, 'history' => $includeHistory ? $path : null, 'historyRawCount' => $includeHistory ? $historyRawCount : null, 'historyRenderCount' => $includeHistory ? count($path) : null, 'historySourceCounts' => $includeHistory ? $historySourceCounts : null, ], JSON_UNESCAPED_UNICODE); ?>