fix: HA voice control — domain preference scoring, exact match priority, skip unavailable, fresh entity_map

This commit is contained in:
2026-05-31 19:22:40 +00:00
parent 18649c47df
commit 721607cfb0
+33 -9
View File
@@ -136,7 +136,8 @@ if (!$reply) {
// ── Tier 0: Home Assistant Control ─────────────────────────────────────── // ── Tier 0: Home Assistant Control ───────────────────────────────────────
// Uses entity_map stored by facts_collector to resolve natural language → entity // Uses entity_map stored by facts_collector to resolve natural language → entity
$haEntityMapRow = JarvisDB::query( $haEntityMapRow = JarvisDB::query(
'SELECT fact_value FROM kb_facts WHERE category=? AND fact_key=? LIMIT 1', 'SELECT fact_value FROM kb_facts WHERE category=? AND fact_key=?
AND (expires_at IS NULL OR expires_at > NOW()) ORDER BY updated_at DESC LIMIT 1',
['ha', 'entity_map'] ['ha', 'entity_map']
); );
$haEntityMap = ($haEntityMapRow && !empty($haEntityMapRow[0]['fact_value'])) $haEntityMap = ($haEntityMapRow && !empty($haEntityMapRow[0]['fact_value']))
@@ -213,22 +214,45 @@ if (!$reply && preg_match('/(turn|switch|put|set)\s+(on|off)/i', $message, $acti
} }
if (!$bestEid) { if (!$bestEid) {
// Build search terms from message (remove control words with word boundaries) // Detect preferred domain from message
$searchMsg = preg_replace('/\b(turn|switch|put|set|the|my|all|please|jarvis|on|off|lights?|lamps?)\b/i', ' ', $msgLower); $preferLight = (bool) preg_match('/\blight(s)?\b/i', $message);
$preferSwitch = (bool) preg_match('/\b(switch|plug|outlet|strip)\b/i', $message);
// Strip control words — keep device/room name
$searchMsg = preg_replace('/\b(turn|switch|put|set|the|my|all|please|jarvis|on|off|lights?|lamps?|plugs?|strips?)\b/i', ' ', $msgLower);
$searchMsg = trim(preg_replace('/\s+/', ' ', $searchMsg)); $searchMsg = trim(preg_replace('/\s+/', ' ', $searchMsg));
foreach ($haEntityMap as $eid => $info) { foreach ($haEntityMap as $eid => $info) {
if (($info['state'] ?? '') === 'unavailable') continue;
$nameLower = strtolower($info['name']); $nameLower = strtolower($info['name']);
// Score: exact substring match = 10, word overlap = words matched $domain = $info['domain'] ?? '';
if ($searchMsg && strpos($nameLower, $searchMsg) !== false) {
$score = 10;
} else {
$words = array_filter(explode(' ', $searchMsg));
$score = 0; $score = 0;
// Exact name match = highest priority
if ($nameLower === $searchMsg) {
$score = 100;
// Search is substring of name
} elseif ($searchMsg && strpos($nameLower, $searchMsg) !== false) {
$score = 40;
// Name is substring of search (e.g. search="living room light", name="Living Room")
} elseif ($searchMsg && strpos($searchMsg, $nameLower) !== false) {
$score = 30;
} else {
// Word overlap scoring
$words = array_filter(explode(' ', $searchMsg));
foreach ($words as $w) { foreach ($words as $w) {
if (strlen($w) > 2 && strpos($nameLower, $w) !== false) $score++; if (strlen($w) > 2 && strpos($nameLower, $w) !== false) $score += 8;
} }
} }
if ($score <= 0) continue;
// Domain preference bonus
if ($preferLight && $domain === 'light') $score += 20;
if ($preferSwitch && $domain === 'switch') $score += 20;
// Penalty for clearly wrong domain when user specified one
if ($preferLight && in_array($domain, ['media_player','sensor','climate'])) $score -= 15;
if ($score > $bestScore) { if ($score > $bestScore) {
$bestScore = $score; $bestScore = $score;
$bestEid = $eid; $bestEid = $eid;