From 0caff5db57ac03d33faadf4c21b694b9e02c7e92 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Mon, 1 Jun 2026 14:16:30 +0000 Subject: [PATCH] Add brightness, media player, siren control + 5 scene keywords - Add scene keywords: good morning work, master bedroom scene, porch lights (3 variants) - Add Tier 0a: brightness control handler ("dim X to Y%", "set X to Y%") - Add Tier 0b: media player on/off/pause for Apple TV and Pioneer receiver - Add Tier 0c: per-camera siren activate/silence via switch --- api/endpoints/chat.php | 151 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/api/endpoints/chat.php b/api/endpoints/chat.php index 8245476..7d2f878 100644 --- a/api/endpoints/chat.php +++ b/api/endpoints/chat.php @@ -159,6 +159,14 @@ $sceneKeywords = [ 'front porch lights' => 'scene.outdoors_front_porch_lights', 'porch scene' => 'scene.outdoors_front_porch_lights', 'office dawn' => 'scene.office_ocean_dawn', + 'good morning work' => 'scene.good_morning_work', + 'morning work' => 'scene.good_morning_work', + 'work morning' => 'scene.good_morning_work', + 'master bedroom scene' => 'scene.master_bedroom_new_scene', + 'bedroom scene' => 'scene.master_bedroom_new_scene', + 'ceiling fan scene' => 'scene.master_bedroom_new_scene', + 'porch lights on' => 'scene.outdoors_front_porch_lights', + 'porch lights' => 'scene.outdoors_front_porch_lights', ]; $msgLower = strtolower(trim($message)); @@ -222,6 +230,13 @@ if (!$reply && preg_match('/(turn|switch|put|set)\s+(on|off)/i', $message, $acti $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)); + // Empty search means generic light command (e.g. "turn on the lights") — all lights + if ($searchMsg === '' && $preferLight) { + $bestEid = '__all_lights__'; + $bestName = 'All lights'; + $bestScore = 1; + } + foreach ($haEntityMap as $eid => $info) { if (($info['state'] ?? '') === 'unavailable') continue; $nameLower = strtolower($info['name']); @@ -324,6 +339,142 @@ if (!$reply && preg_match('/(is|are|what.s|status|state).*(on|off|light|switch|p } +// ── Tier 0a: Brightness control ────────────────────────────────────────── +if (!$reply && preg_match('/\b(dim|brighten|brightness|set.*to\s+\d+\s*%|percent)\b/i', $message) && !empty($haEntityMap)) { + $pctMatch = null; + if (preg_match('/(\d+)\s*%/', $message, $pm)) { + $pctMatch = max(1, min(100, (int)$pm[1])); + } elseif (preg_match('/\b(full|max|maximum|hundred)\b/i', $message)) { + $pctMatch = 100; + } elseif (preg_match('/\b(half|fifty)\b/i', $message)) { + $pctMatch = 50; + } elseif (preg_match('/\b(low|dim|minimum|min|quarter)\b/i', $message)) { + $pctMatch = 20; + } + + if ($pctMatch !== null) { + $searchMsg = preg_replace('/\b(dim|brighten|brightness|set|the|my|to|at|percent|%|\d+|jarvis|light|lights?)\b/i', ' ', $msgLower); + $searchMsg = trim(preg_replace('/\s+/', ' ', $searchMsg)); + $bestEid = null; $bestScore = 0; $bestName = ''; + foreach ($haEntityMap as $eid => $info) { + if ($info['domain'] !== 'light') continue; + if (($info['state'] ?? '') === 'unavailable') continue; + $nameLower = strtolower($info['name']); + $score = 0; + if ($searchMsg === '') { $bestEid = '__all_lights__'; $bestName = 'All lights'; $bestScore = 1; break; } + $words = array_filter(explode(' ', $searchMsg)); + foreach ($words as $w) { if (strlen($w) > 2 && strpos($nameLower, $w) !== false) $score += 10; } + if ($score > $bestScore) { $bestScore = $score; $bestEid = $eid; $bestName = $info['name']; } + } + if ($bestEid && $bestScore >= 0) { + $haUrl = defined('HA_URL') ? HA_URL : 'http://10.48.200.97:8123'; + $haToken = defined('HA_TOKEN') ? HA_TOKEN : ''; + $brightness = (int)round($pctMatch * 2.55); + if ($bestEid === '__all_lights__') { + $lightIds = array_keys(array_filter($haEntityMap, fn($e) => $e['domain'] === 'light')); + $payload = ['entity_id' => $lightIds, 'brightness' => $brightness]; + } else { + $payload = ['entity_id' => $bestEid, 'brightness' => $brightness]; + } + $ch = curl_init($haUrl . '/api/services/light/turn_on'); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, + CURLOPT_POSTFIELDS=>json_encode($payload), + CURLOPT_HTTPHEADER=>['Authorization: Bearer '.$haToken,'Content-Type: application/json'], + CURLOPT_TIMEOUT=>8, CURLOPT_CONNECTTIMEOUT=>3]); + $haCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_exec($ch); $haCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); + if ($haCode === 200) { + $label = ($bestEid === '__all_lights__') ? 'All lights' : $bestName; + $reply = "{$label} set to {$pctMatch}%, {$userAddr}."; + $source = 'ha:brightness'; + } + } + } +} + +// ── Tier 0b: Media player control ──────────────────────────────────────── +if (!$reply && preg_match('/\b(apple tv|appletv|pioneer|receiver|tv|television)\b/i', $message) + && preg_match('/\b(turn|switch|power|on|off|pause|play|resume|stop|mute)\b/i', $message) && !empty($haEntityMap)) { + $mediaTurnOn = (bool) preg_match('/\b(on|play|resume|start)\b/i', $message); + $mediaTurnOff = (bool) preg_match('/\b(off|stop|shutdown|power off)\b/i', $message); + $mediaPause = (bool) preg_match('/\b(pause|mute)\b/i', $message); + $isAppleTV = (bool) preg_match('/\b(apple.?tv|appletv)\b/i', $message); + $isPioneer = (bool) preg_match('/\b(pioneer|receiver)\b/i', $message); + $isTv = (bool) preg_match('/\b(tv|television)\b/i', $message); + + $targetEid = null; $targetName = ''; + if ($isAppleTV) { + $targetEid = 'media_player.apple_tv_4k'; + $targetName = 'Apple TV 4K'; + } elseif ($isPioneer) { + $targetEid = 'media_player.vsx_822_2'; + $targetName = 'Pioneer Receiver'; + } elseif ($isTv) { + $targetEid = 'media_player.apple_tv_4k'; + $targetName = 'Apple TV'; + } + + if ($targetEid) { + $haUrl = defined('HA_URL') ? HA_URL : 'http://10.48.200.97:8123'; + $haToken = defined('HA_TOKEN') ? HA_TOKEN : ''; + if ($mediaPause) { + $svc = 'media_player/media_pause'; + } elseif ($mediaTurnOff) { + $svc = 'media_player/turn_off'; + } else { + $svc = 'media_player/turn_on'; + } + $ch = curl_init($haUrl . '/api/services/' . $svc); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, + CURLOPT_POSTFIELDS=>json_encode(['entity_id'=>$targetEid]), + CURLOPT_HTTPHEADER=>['Authorization: Bearer '.$haToken,'Content-Type: application/json'], + CURLOPT_TIMEOUT=>8, CURLOPT_CONNECTTIMEOUT=>3]); + curl_exec($ch); $haCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); + if ($haCode === 200) { + if ($mediaPause) $verb = 'paused'; + elseif ($mediaTurnOff) $verb = 'powered off'; + else $verb = 'powered on'; + $reply = "{$targetName} {$verb}, {$userAddr}."; + $source = 'ha:media'; + } + } +} + +// ── Tier 0c: Camera siren toggle ───────────────────────────────────────── +if (!$reply && preg_match('/\b(siren|alarm|honk|sound (the )?alarm)\b/i', $message) + && preg_match('/\b(camera|back yard|backyard|carport|driveway|front yard)\b/i', $message)) { + $sirenOn = !preg_match('/\b(off|stop|disable|silence)\b/i', $message); + $haService = $sirenOn ? 'turn_on' : 'turn_off'; + $sirenMap = [ + 'back yard' => ['switch.camera1_siren_on_event_2', 'Back Yard'], + 'backyard' => ['switch.camera1_siren_on_event_2', 'Back Yard'], + 'carport' => ['switch.camera1_siren_on_event_3', 'Carport'], + 'front yard' => ['switch.front_yard_siren_on_event', 'Front Yard'], + 'driveway' => ['switch.down_hill_siren_on_event', 'Driveway'], + ]; + $targetSiren = null; $targetLabel = 'Camera'; + foreach ($sirenMap as $kw => [$eid, $lbl]) { + if (strpos($msgLower, $kw) !== false) { $targetSiren = $eid; $targetLabel = $lbl; break; } + } + if (!$targetSiren) { + $targetSiren = 'switch.camera1_siren_on_event_2'; + $targetLabel = 'Back Yard'; + } + $haUrl = defined('HA_URL') ? HA_URL : 'http://10.48.200.97:8123'; + $haToken = defined('HA_TOKEN') ? HA_TOKEN : ''; + $ch = curl_init($haUrl . '/api/services/switch/' . $haService); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, + CURLOPT_POSTFIELDS=>json_encode(['entity_id'=>$targetSiren]), + CURLOPT_HTTPHEADER=>['Authorization: Bearer '.$haToken,'Content-Type: application/json'], + CURLOPT_TIMEOUT=>8, CURLOPT_CONNECTTIMEOUT=>3]); + curl_exec($ch); $haCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); + if ($haCode === 200) { + $verb = $sirenOn ? 'activated' : 'silenced'; + $reply = "{$targetLabel} camera siren {$verb}, {$userAddr}."; + $source = 'ha:siren'; + } +} + // ── Email + Planner voice intents (action items, create task/appt from email) ─ if (!$reply && preg_match('/\b(email|emails|inbox|gmail|outlook|mail|unread|messages)\b/i', $message)) { $lc = strtolower($message);