mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
Phase 2: facts_collector TTL guards — reduce external polling
- Proxmox: skip if data < 10 min old (was: every 3 min unconditionally) - Ollama: skip if data < 15 min old (model list rarely changes) - Site health: skip if data < 5 min old (was: 7 HTTP calls every 3 min) - Home Assistant: removed entirely (HA agent pushes 212 entities every 30s) Fast local reads (CPU/mem/network pings) still run every 3 min. External HTTP calls now fire only when data is actually stale. Saves ~140 site-check HTTP calls/hour and ~60 Proxmox API calls/hour in steady state.
This commit is contained in:
@@ -19,6 +19,18 @@ function collect_all(): array {
|
|||||||
$results = [];
|
$results = [];
|
||||||
$ttl = 300; // 5-minute TTL on live facts
|
$ttl = 300; // 5-minute TTL on live facts
|
||||||
|
|
||||||
|
// Returns true if a fact category has been updated within $secs seconds.
|
||||||
|
// Prevents expensive external calls when data is still fresh.
|
||||||
|
$fresh = function(string $cat, int $secs): bool {
|
||||||
|
$row = JarvisDB::query(
|
||||||
|
'SELECT updated_at FROM kb_facts WHERE fact_category=? ORDER BY updated_at DESC LIMIT 1',
|
||||||
|
[$cat]
|
||||||
|
);
|
||||||
|
if (empty($row[0]['updated_at'])) return false;
|
||||||
|
return (time() - strtotime($row[0]['updated_at'])) < $secs;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// ── System ────────────────────────────────────────────────────────────
|
// ── System ────────────────────────────────────────────────────────────
|
||||||
try {
|
try {
|
||||||
$stat1 = file_get_contents('/proc/stat');
|
$stat1 = file_get_contents('/proc/stat');
|
||||||
@@ -94,8 +106,10 @@ function collect_all(): array {
|
|||||||
$results['network'] = 'error: ' . $e->getMessage();
|
$results['network'] = 'error: ' . $e->getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Proxmox ───────────────────────────────────────────────────────────
|
// ── Proxmox (TTL 10 min) ─────────────────────────────────────────────
|
||||||
try {
|
if ($fresh('proxmox', 600)) {
|
||||||
|
$results['proxmox'] = 'skipped (fresh)';
|
||||||
|
} else try {
|
||||||
if (defined('PROXMOX_TOKEN_ID') && PROXMOX_TOKEN_ID) {
|
if (defined('PROXMOX_TOKEN_ID') && PROXMOX_TOKEN_ID) {
|
||||||
$base = 'https://orbisne.fortiddns.com:' . PROXMOX_PORT . '/api2/json';
|
$base = 'https://orbisne.fortiddns.com:' . PROXMOX_PORT . '/api2/json';
|
||||||
$auth = 'Authorization: PVEAPIToken=' . PROXMOX_USER . '!' . PROXMOX_TOKEN_ID . '=' . PROXMOX_TOKEN_VAL;
|
$auth = 'Authorization: PVEAPIToken=' . PROXMOX_USER . '!' . PROXMOX_TOKEN_ID . '=' . PROXMOX_TOKEN_VAL;
|
||||||
@@ -126,116 +140,9 @@ function collect_all(): array {
|
|||||||
$results['proxmox'] = 'error: ' . $e->getMessage();
|
$results['proxmox'] = 'error: ' . $e->getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Home Assistant ────────────────────────────────────────────────────
|
// ── Home Assistant — skipped (HA agent pushes entities every 30s) ────
|
||||||
try {
|
$results['ha'] = 'skipped (agent push active)';
|
||||||
if (defined('HA_URL') && defined('HA_TOKEN') && HA_TOKEN !== 'YOUR_HA_TOKEN_HERE') {
|
|
||||||
$haUrl = HA_URL;
|
|
||||||
$haToken = HA_TOKEN;
|
|
||||||
$haHdr = ['Authorization: Bearer ' . $haToken, 'Content-Type: application/json'];
|
|
||||||
|
|
||||||
// Fetch all entity states
|
|
||||||
$ch = curl_init($haUrl . '/api/states');
|
|
||||||
curl_setopt_array($ch, [
|
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
|
||||||
CURLOPT_HTTPHEADER => $haHdr,
|
|
||||||
CURLOPT_TIMEOUT => 12,
|
|
||||||
CURLOPT_CONNECTTIMEOUT => 5,
|
|
||||||
]);
|
|
||||||
$resp = curl_exec($ch);
|
|
||||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
if ($code === 200) {
|
|
||||||
$allStates = json_decode($resp, true) ?? [];
|
|
||||||
|
|
||||||
// Domains to index for control
|
|
||||||
$controlDomains = ['light','switch','input_boolean','climate','cover','fan',
|
|
||||||
'scene','script','lawn_mower','vacuum','media_player'];
|
|
||||||
// Switch keywords to skip (camera/HACS settings, not real devices)
|
|
||||||
$skipKeywords = ['pre_release','_record','_ftp_','_push_','_hub_ringtone',
|
|
||||||
'_siren_on','_email_on','_manual_record','_infrared_',
|
|
||||||
'do_not_disturb','matter_server','zerotier','mariadb',
|
|
||||||
'spotify','file_editor','ssh_web','uptime_kuma',
|
|
||||||
'adguard_home_','adguard_protection','adguard_parental',
|
|
||||||
'adguard_safe','adguard_filter','adguard_query',
|
|
||||||
'assist_microphone','folding_home','music_assistant',
|
|
||||||
'get_hacs','mealie','mosquitto','social_to',
|
|
||||||
'motion_detection','front_yard_record','down_hill_record',
|
|
||||||
'camera1_record','back_yard_record','nvr_'];
|
|
||||||
|
|
||||||
$entityMap = [];
|
|
||||||
$statusCount = ['online' => 0, 'offline' => 0, 'unavailable' => 0];
|
|
||||||
|
|
||||||
foreach ($allStates as $s) {
|
|
||||||
$eid = $s['entity_id'];
|
|
||||||
$domain = explode('.', $eid)[0];
|
|
||||||
$name = $s['attributes']['friendly_name'] ?? $eid;
|
|
||||||
$state = $s['state'];
|
|
||||||
|
|
||||||
if (!in_array($domain, $controlDomains)) continue;
|
|
||||||
|
|
||||||
// Skip camera/HACS internals for switches
|
|
||||||
if ($domain === 'switch') {
|
|
||||||
$skip = false;
|
|
||||||
foreach ($skipKeywords as $kw) {
|
|
||||||
if (strpos($eid, $kw) !== false) { $skip = true; break; }
|
|
||||||
}
|
|
||||||
if ($skip) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$entityMap[$eid] = ['name' => $name, 'state' => $state, 'domain' => $domain];
|
|
||||||
|
|
||||||
if ($state === 'unavailable' || $state === 'unknown') {
|
|
||||||
$statusCount['unavailable']++;
|
|
||||||
} elseif (in_array($state, ['on','open','playing','mowing','home','active','idle'])) {
|
|
||||||
$statusCount['online']++;
|
|
||||||
} elseif (in_array($state, ['off','closed','paused','docked','away'])) {
|
|
||||||
$statusCount['offline']++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store entity map as JSON for chat.php to use
|
|
||||||
KBEngine::storeFact('ha', 'entity_map', json_encode($entityMap), 'ha', 270);
|
|
||||||
KBEngine::storeFact('ha', 'entity_count', count($entityMap), 'ha', $ttl);
|
|
||||||
KBEngine::storeFact('ha', 'online_count', $statusCount['online'], 'ha', $ttl);
|
|
||||||
KBEngine::storeFact('ha', 'offline_count', $statusCount['offline'], 'ha', $ttl);
|
|
||||||
KBEngine::storeFact('ha', 'unavail_count', $statusCount['unavailable'], 'ha', $ttl);
|
|
||||||
KBEngine::storeFact('ha', 'ha_status', 'online', 'ha', $ttl);
|
|
||||||
|
|
||||||
// Store individual sensor facts
|
|
||||||
$sensorDomains = ['sensor','binary_sensor','weather'];
|
|
||||||
$interestingPatterns = ['temperature','humidity','battery','power','energy',
|
|
||||||
'voltage','current','illuminance','co2','pm25'];
|
|
||||||
foreach ($allStates as $s) {
|
|
||||||
$domain = explode('.', $s['entity_id'])[0];
|
|
||||||
if (!in_array($domain, $sensorDomains)) continue;
|
|
||||||
$eid = $s['entity_id'];
|
|
||||||
foreach ($interestingPatterns as $pat) {
|
|
||||||
if (strpos($eid, $pat) !== false) {
|
|
||||||
$name = $s['attributes']['friendly_name'] ?? $eid;
|
|
||||||
$unit = $s['attributes']['unit_of_measurement'] ?? '';
|
|
||||||
KBEngine::storeFact('ha_sensors', $eid,
|
|
||||||
$s['state'] . ($unit ? " {$unit}" : ''), 'ha', $ttl);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$results['ha'] = sprintf('ok (%d entities, %d on, %d off, %d unavail)',
|
|
||||||
count($entityMap), $statusCount['online'],
|
|
||||||
$statusCount['offline'], $statusCount['unavailable']);
|
|
||||||
} else {
|
|
||||||
KBEngine::storeFact('ha', 'ha_status', 'unreachable', 'ha', $ttl);
|
|
||||||
$results['ha'] = "unreachable (HTTP {$code})";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
KBEngine::storeFact('ha', 'ha_status', 'token not configured', 'ha', $ttl);
|
|
||||||
$results['ha'] = 'skipped (no token)';
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
KBEngine::storeFact('ha', 'ha_status', 'error', 'ha', $ttl);
|
|
||||||
$results['ha'] = 'error: ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Digital Ocean ─────────────────────────────────────────────────────
|
// ── Digital Ocean ─────────────────────────────────────────────────────
|
||||||
try {
|
try {
|
||||||
@@ -247,8 +154,10 @@ function collect_all(): array {
|
|||||||
$results['do_server'] = 'error: ' . $e->getMessage();
|
$results['do_server'] = 'error: ' . $e->getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Ollama ────────────────────────────────────────────────────────────
|
// ── Ollama (TTL 15 min) ───────────────────────────────────────────────
|
||||||
try {
|
if ($fresh('ollama', 900)) {
|
||||||
|
$results['ollama'] = 'skipped (fresh)';
|
||||||
|
} else try {
|
||||||
$ollamaHost = defined('OLLAMA_HOST') ? OLLAMA_HOST : 'http://10.48.200.95:11434';
|
$ollamaHost = defined('OLLAMA_HOST') ? OLLAMA_HOST : 'http://10.48.200.95:11434';
|
||||||
$ch = curl_init($ollamaHost . '/api/tags');
|
$ch = curl_init($ollamaHost . '/api/tags');
|
||||||
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5]);
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5]);
|
||||||
@@ -278,8 +187,10 @@ function collect_all(): array {
|
|||||||
$results['ollama'] = 'error: ' . $e->getMessage();
|
$results['ollama'] = 'error: ' . $e->getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Site Health ───────────────────────────────────────────────────────
|
// ── Site Health (TTL 5 min) ───────────────────────────────────────────
|
||||||
try {
|
if ($fresh('sites', 300)) {
|
||||||
|
$results['sites'] = 'skipped (fresh)';
|
||||||
|
} else try {
|
||||||
$sites = [
|
$sites = [
|
||||||
'jarvis' => 'https://jarvis.orbishosting.com',
|
'jarvis' => 'https://jarvis.orbishosting.com',
|
||||||
'tomsjavajive' => 'https://tomsjavajive.com',
|
'tomsjavajive' => 'https://tomsjavajive.com',
|
||||||
|
|||||||
Reference in New Issue
Block a user