true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 8, CURLOPT_CONNECTTIMEOUT => 4, CURLOPT_SSL_VERIFYPEER => false, ]); if ($method === 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); } $resp = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code >= 200 && $code < 300 && $resp) { return json_decode($resp, true); } return null; } $configured = !(HA_TOKEN === 'YOUR_HA_TOKEN_HERE' || strpos(HA_URL, '10.48.200.X') !== false); if (!$configured) { echo json_encode(['configured' => false, 'message' => 'HA token not configured.', 'entities' => []]); exit; } // Scene list if ($method === 'GET' && $action === 'scenes') { $states = haRequest('/states') ?? []; $scenes = []; foreach ($states as $s) { if (str_starts_with($s['entity_id'] ?? '', 'scene.')) { $scenes[] = [ 'entity_id' => $s['entity_id'], 'name' => $s['attributes']['friendly_name'] ?? $s['entity_id'], ]; } } echo json_encode(['scenes' => $scenes]); exit; } // Scene activate if ($method === 'POST' && $action === 'scene_activate') { $entity_id = $data['entity_id'] ?? ''; if ($entity_id && str_starts_with($entity_id, 'scene.')) { $result = haRequest('/services/scene/turn_on', 'POST', ['entity_id' => $entity_id]); echo json_encode(['success' => true, 'entity_id' => $entity_id]); } else { echo json_encode(['error' => 'Invalid scene entity_id']); } exit; } // Live service call (toggle device) — always direct to HA, never cached if ($method === 'POST' && $action === 'service') { $domain = $data['domain'] ?? ''; $service = $data['service'] ?? ''; $entity_id = $data['entity_id'] ?? ''; if ($domain && $service && $entity_id) { $result = haRequest("/services/{$domain}/{$service}", 'POST', ['entity_id' => $entity_id]); echo json_encode(['success' => true, 'result' => $result]); } else { echo json_encode(['error' => 'Missing domain/service/entity_id']); } exit; } // Serve entities from ha_entities table (real-time agent push data) $skipDomains = ['sensor','binary_sensor','button','update','select','number', 'device_tracker','event','image','person','zone','tts','conversation', 'assist_satellite','input_button','media_player','scene','water_heater', 'alarm_control_panel','automation','script','calendar','notify','weather','camera','siren','remote','todo','lawn_mower']; $skipKeywords = [ // HACS / system toggles 'pre_release','get_hacs','matter_server','zerotier','mariadb', 'spotify_connect','file_editor','ssh_web','uptime_kuma','adguard_', 'folding_home','music_assistant','mealie','mosquitto','social_to', 'assist_microphone','cec_scanner','esphome_device_builder', // Camera controls '_record','_ftp_','_push_','_hub_ringtone','_siren_on', '_email_on','_manual_record','_infrared_','motion_detection', 'front_yard_record','down_hill_record','camera1_record', 'back_yard_record','nvr_', // Echo / smart display noise 'do_not_disturb', // Konnected security panel switches 'floodlight', 'konnected', // Energy / power monitoring (sensors, not controls) '_energy','_power','_voltage','_current','_consumption', 'electricity_maps', ]; $rows = JarvisDB::query( "SELECT entity_id, entity_name, domain, state, UNIX_TIMESTAMP(updated_at) as updated_ts FROM ha_entities WHERE state NOT IN ('unavailable','unknown') ORDER BY domain, entity_name" ) ?? []; $grouped = []; $latestTs = 0; foreach ($rows as $e) { $dom = $e['domain']; if (in_array($dom, $skipDomains)) continue; $skip = false; if ($dom === 'switch') { foreach ($skipKeywords as $kw) { if (strpos($e['entity_id'], $kw) !== false) { $skip = true; break; } } } if ($skip) continue; if ((int)$e['updated_ts'] > $latestTs) $latestTs = (int)$e['updated_ts']; $grouped[$dom][] = [ 'entity_id' => $e['entity_id'], 'name' => $e['entity_name'], 'state' => $e['state'], ]; } echo json_encode([ 'configured' => true, 'entities' => $grouped, 'cache_age_s' => $latestTs > 0 ? (int)(time() - $latestTs) : -1, 'cached_at' => $latestTs > 0 ? date('c', $latestTs) : null, ]);