'Method Not Allowed']); exit; } header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-store, no-cache, must-revalidate'); header('Expires: 0'); custom_require_auth(); $requestData = custom_read_request_data(); 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); } $ok = custom_ha_webhook($allowed[$name]); custom_json(['result' => $ok ? 'ok' : 'fail'], $ok ? 200 : 502); } $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; } } // 엔드포인트 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']; // 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}"; // 공통 요청 함수 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 formatSensorValue($value, $definition, $lang, $maps) { if ($value === null) return null; if (($definition['format'] ?? '') === 'bool') { return boolText($value, $lang); } if (($definition['format'] ?? '') === 'duration') { if (!is_numeric($value)) return $value; $seconds = (int)$value; 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 ($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; } if (($definition['format'] ?? '') === 'datetime') { return $value; } if (isset($definition['map'], $maps[$definition['map']])) { $value = mappedText($value, $lang, $maps[$definition['map']]); } 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); $historyData = custom_ha_request('seoul', 'GET', $historyEndpoint) ?? []; $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]; } } } $lang = $_GET['lang'] ?? 'en'; $activityMap = [ 'ko' => [ 'Stationary' => '가만히 있음', 'Walking' => '걷는 중', 'Running' => '달리는 중', 'Automotive' => '차량 이동 중', 'Cycling' => '자전거 이동 중', 'Unknown' => '-', ], 'en' => [ 'Stationary' => 'Stationary', 'Walking' => 'Walking', 'Running' => 'Running', 'Automotive' => 'Automotive', 'Cycling' => 'Cycling', 'Unknown' => '-', ] ]; $aaccuracyMap = [ 'ko' => [ 'Low' => '낮음', 'Medium' => '중간', 'High' => '높음', 'Unknown' => '-', ], 'en' => [ 'Low' => 'Low', 'Medium' => 'Medium', 'High' => 'High', 'Unknown' => '-', ] ]; $connectionMap = [ 'ko' => [ 'Wi-Fi' => '와이파이', 'Cellular' => '셀룰러', 'none' => '-', 'unknown' => '-', ], 'en' => [ 'Wi-Fi' => 'Wi-Fi', 'Cellular' => 'Cellular', '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'], '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" => "앱 실행", "Background Fetch" => "백그라운드 갱신", "Push Notification" => "푸시", "Periodic" => "주기적", "Significant Location Change" => "중요한 위치 변화", "Manual" => "수동", "Signaled" => "시그널 됨", "Siri" => "Siri 실행", "Watch Context" => "워치 상태 전송", "Geographic Region Entered" => "지정 지역에 들어옴", "Geographic Region Exited" => "지정 지역을 벗어남", ], '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", ] ]; $activityState = $activityData['state'] ?? null; $aaccuracyState = $activityData['attributes']['Confidence'] ?? $activityData['attributes']['confidence'] ?? null; $connectionState = $connectionData['state'] ?? null; $batteryState = $batteryStateData['state'] ?? null; $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' => $triggerMap[$lang][$triggerData['state']] ?? $triggerData['state'] ?? null, '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, 'sensors' => $sensorSections, ]); 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' => $triggerMap[$lang][$triggerData['state']] ?? $triggerData['state'] ?? null, '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, 'sensors' => $sensorSections, 'history' => $path ?? null, ], JSON_UNESCAPED_UNICODE); ?>