mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
feat: HA scene control via voice
- ha.php: GET scenes action returns live scene list from HA; POST scene_activate triggers by entity_id - chat.php: ha_scene intent fetches all scenes live, fuzzy token-matches against message, calls scene.turn_on; falls back to listing available scenes if no match - KB intents: 14 patterns covering good night/morning/goodbye/kitchen lights/ocean dawn/porch + generic activate/run/trigger/set scene Scenes available: Good Night, Good Morning, Good Morning Work, Goodbye, Kitchen Lights On/Off, Front Porch Lights, Office Ocean Dawn, Master Bedroom Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1647,6 +1647,57 @@ if (!$reply) {
|
|||||||
if ($matched && $matched['action'] === 'action') {
|
if ($matched && $matched['action'] === 'action') {
|
||||||
switch ($matched['intent']) {
|
switch ($matched['intent']) {
|
||||||
|
|
||||||
|
case 'ha_scene': {
|
||||||
|
// Fetch all HA scenes and fuzzy-match against the message
|
||||||
|
$haScenes = @json_decode(@file_get_contents(
|
||||||
|
HA_URL . '/api/states',
|
||||||
|
false,
|
||||||
|
stream_context_create(['http' => ['timeout' => 5, 'header' => 'Authorization: Bearer ' . HA_TOKEN]])
|
||||||
|
), true) ?? [];
|
||||||
|
|
||||||
|
$scenes = array_filter($haScenes, fn($s) => str_starts_with($s['entity_id'] ?? '', 'scene.'));
|
||||||
|
$msgLow = strtolower($message);
|
||||||
|
|
||||||
|
// Score each scene against the message
|
||||||
|
$best = null; $bestScore = 0;
|
||||||
|
foreach ($scenes as $s) {
|
||||||
|
$name = strtolower($s['attributes']['friendly_name'] ?? '');
|
||||||
|
$id = strtolower(str_replace(['scene.', '_'], ['', ' '], $s['entity_id']));
|
||||||
|
// Token overlap score
|
||||||
|
$tokens = array_filter(explode(' ', "$name $id"));
|
||||||
|
$score = 0;
|
||||||
|
foreach ($tokens as $tok) {
|
||||||
|
if (strlen($tok) > 2 && str_contains($msgLow, $tok)) $score++;
|
||||||
|
}
|
||||||
|
// Bonus for exact phrase match
|
||||||
|
if ($name && str_contains($msgLow, $name)) $score += 5;
|
||||||
|
if ($score > $bestScore) { $bestScore = $score; $best = $s; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($best && $bestScore >= 1) {
|
||||||
|
$sceneName = $best['attributes']['friendly_name'] ?? $best['entity_id'];
|
||||||
|
@file_get_contents(
|
||||||
|
HA_URL . '/api/services/scene/turn_on',
|
||||||
|
false,
|
||||||
|
stream_context_create(['http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' => "Authorization: Bearer " . HA_TOKEN . "
|
||||||
|
Content-Type: application/json",
|
||||||
|
'content' => json_encode(['entity_id' => $best['entity_id']]),
|
||||||
|
'timeout' => 5,
|
||||||
|
]])
|
||||||
|
);
|
||||||
|
$reply = "Activating {$sceneName}, {$userAddr}.";
|
||||||
|
} else {
|
||||||
|
// List available scenes
|
||||||
|
$names = array_map(fn($s) => $s['attributes']['friendly_name'] ?? $s['entity_id'], $scenes);
|
||||||
|
$list = implode(', ', array_values($names));
|
||||||
|
$reply = "I didn't catch which scene, {$userAddr}. Available scenes: {$list}.";
|
||||||
|
}
|
||||||
|
$source = 'intent:ha_scene';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'restart_agent':
|
case 'restart_agent':
|
||||||
// Extract target hostname from message
|
// Extract target hostname from message
|
||||||
$msgLow = strtolower($message);
|
$msgLow = strtolower($message);
|
||||||
|
|||||||
@@ -38,6 +38,34 @@ if (!$configured) {
|
|||||||
exit;
|
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
|
// Live service call (toggle device) — always direct to HA, never cached
|
||||||
if ($method === 'POST' && $action === 'service') {
|
if ($method === 'POST' && $action === 'service') {
|
||||||
$domain = $data['domain'] ?? '';
|
$domain = $data['domain'] ?? '';
|
||||||
|
|||||||
Reference in New Issue
Block a user