iobroker.assistant 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/admin/i18n/de.json +7 -1
- package/admin/i18n/en.json +7 -1
- package/admin/i18n/es.json +7 -1
- package/admin/i18n/fr.json +7 -1
- package/admin/i18n/it.json +7 -1
- package/admin/i18n/nl.json +7 -1
- package/admin/i18n/pl.json +7 -1
- package/admin/i18n/pt.json +7 -1
- package/admin/i18n/ru.json +7 -1
- package/admin/i18n/uk.json +7 -1
- package/admin/i18n/zh-cn.json +7 -1
- package/admin/jsonConfig.json +32 -0
- package/build/lib/youtube.js +625 -0
- package/build/lib/youtube.js.map +1 -0
- package/build/main.js +436 -2
- package/build/main.js.map +1 -1
- package/io-package.json +15 -1
- package/package.json +3 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"youtube.js","sourceRoot":"","sources":["../../src/lib/youtube.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * YouTube on the TV: \"spiele den Kanal Sailing OceanAir ab\" → find the channel, switch the TV on, play it.\n *\n * Playing goes through the `youtube-remote` adapter (YouTube Lounge API, \"Link with TV\"): its `youtube` state\n * takes a video id, a playlist id (`PL…`), a channel id (`UC…`, plays the uploads) or a URL. What it cannot\n * do is turn a spoken name into one of those — handles and names need a search. That search lives here and\n * uses InnerTube, the JSON API the YouTube web page itself talks to: no API key and no quota, unlike the Data\n * API (100 units per search, 10 000 a day). The price is that Google does not document it; the parser\n * therefore walks the whole response for the renderer objects it knows instead of following one fixed path,\n * so a reshuffled page layout still yields results.\n *\n * Which player is meant comes from a settings table (name, player, power state, room). A request without a\n * name goes to the player in the room it came from, then to the first row — \"the TV\" in a one-TV household.\n * A row normally names a youtube-remote **instance**, not its device: the device id comes from the TV's\n * screen id and changes when the TV is paired again, so the device is looked up at runtime\n * ({@link pickDevice}).\n */\n\n/** What to search for. `video` is also the kind of a plain \"play X on YouTube\". */\nexport type YouTubeKind = 'channel' | 'video' | 'playlist';\n\n/** One configured YouTube player. */\nexport interface YouTubePlayer {\n /** As configured; '' until the device of an instance row was found (then the device name). */\n name: string;\n /** The youtube-remote instance the row names (`youtube-remote.0`), '' if the row names a device. */\n instance: string;\n /** Device object of the player, e.g. `youtube-remote.0.tv_8198d994`; '' until resolved. */\n base: string;\n /** The state that takes what to play (`<base>.youtube`); '' until resolved. */\n target: string;\n /** State that switches the TV on (`switch.power`), '' = none. */\n power: string;\n room: string;\n /** Seconds to give the TV after it reported \"on\" before the YouTube app can take a command. */\n delaySec: number;\n}\n\n/** One row of the settings table (everything arrives as text, the number column as number). */\nexport interface YouTubePlayerRow {\n name?: string;\n id?: string;\n power?: string;\n room?: string;\n delay?: number | string;\n}\n\n/** One search result. */\nexport interface YouTubeHit {\n kind: YouTubeKind;\n /** Video id, playlist id or channel id — exactly what the `youtube` state of the player takes. */\n id: string;\n title: string;\n /** Channel of a video or playlist. */\n channel?: string;\n /** `@handle` of a channel, if the result shows it. */\n handle?: string;\n}\n\n/** A play request the rule-based path understood. */\nexport interface YouTubeCommand {\n /** `auto` = nothing said what it is: a channel of that name if there is one, else videos. */\n kind: YouTubeKind | 'auto';\n query: string;\n /** Player name as configured, if the request named one. */\n player?: string;\n}\n\n/** Default boot time of a TV between \"power reports on\" and \"YouTube app answers\". */\nexport const DEFAULT_POWER_DELAY_SEC = 8;\n\n/** Fold a word or name for comparing: lower case, umlauts spelled out, punctuation gone, single spaces. */\nexport function fold(text: unknown): string {\n if (typeof text !== 'string') {\n return '';\n }\n return text\n .toLowerCase()\n .replace(/ä/g, 'ae')\n .replace(/ö/g, 'oe')\n .replace(/ü/g, 'ue')\n .replace(/ß/g, 'ss')\n .replace(/ё/g, 'е')\n .replace(/[^\\p{L}\\p{N}]+/gu, ' ')\n .trim()\n .replace(/\\s+/g, ' ');\n}\n\n/** Same word up to an inflected ending (\"Wohnzimmer\"/\"Wohnzimmers\", \"телевизор\"/\"телевизоре\"). */\nfunction sameWord(a: string, b: string): boolean {\n if (a === b) {\n return true;\n }\n const short = a.length < b.length ? a : b;\n const long = a.length < b.length ? b : a;\n return short.length >= 4 && long.startsWith(short) && long.length - short.length <= 2;\n}\n\n/** `youtube-remote.0` — an instance, as opposed to one of its devices. */\nexport function isInstanceId(id: string): boolean {\n return /^[\\w-]+\\.\\d+$/.test(id);\n}\n\n/**\n * Turn settings-table rows into players; rows without a player are skipped, the first name wins. A row may\n * name the instance (the usual case) or, for an instance with several TVs, one device or its `youtube` state.\n */\nexport function parsePlayerRows(rows: YouTubePlayerRow[] | undefined): YouTubePlayer[] {\n const out: YouTubePlayer[] = [];\n const seen = new Set<string>();\n for (const row of rows || []) {\n const id = String(row?.id ?? '')\n .trim()\n .replace(/\\.youtube$/, '');\n if (!id) {\n continue;\n }\n const instance = isInstanceId(id) ? id : '';\n const room = String(row?.room ?? '').trim();\n // An instance row without name and room is named after its device once that is found.\n const name = String(row?.name ?? '').trim() || room || (instance ? '' : id.split('.').pop() || id);\n const key = fold(name);\n if (key && seen.has(key)) {\n continue;\n }\n seen.add(key);\n const delay = Number(row?.delay);\n out.push({\n name,\n instance,\n base: instance ? '' : id,\n target: instance ? '' : `${id}.youtube`,\n power: String(row?.power ?? '').trim(),\n room,\n delaySec:\n row?.delay === undefined || row.delay === '' || !Number.isFinite(delay)\n ? DEFAULT_POWER_DELAY_SEC\n : Math.min(120, Math.max(0, delay)),\n });\n }\n return out;\n}\n\n/** A paired device of a youtube-remote instance. */\nexport interface YouTubeDevice {\n id: string;\n name: string;\n}\n\n/**\n * The device an instance row means: the one named like the row (name or room), else the first — with one\n * TV per instance, which is the normal case, simply that one. `null` while nothing is paired.\n */\nexport function pickDevice(\n devices: YouTubeDevice[],\n player: Pick<YouTubePlayer, 'name' | 'room'>,\n): YouTubeDevice | null {\n if (!devices.length) {\n return null;\n }\n const sorted = [...devices].sort((a, b) => a.id.localeCompare(b.id));\n return (\n // Every word of the row must be in the device name: a row \"Schlafzimmer TV\" is not the device \"TV\".\n sorted.find(d => !!player.name && nameMatches(player.name, d.name)) ||\n sorted.find(d => !!player.room && nameMatches(player.room, d.name)) ||\n sorted[0]\n );\n}\n\n/** Does a configured name (all its words) match what was said? */\nfunction nameMatches(name: string, wanted: string): boolean {\n const a = fold(name);\n const b = fold(wanted);\n if (!a || !b) {\n return false;\n }\n if (a === b) {\n return true;\n }\n const nameWords = a.split(' ');\n const saidWords = b.split(' ');\n return nameWords.every(n => saidWords.some(s => sameWord(n, s)));\n}\n\n/**\n * Which player is meant: the named one (name, then room), else the one in the room the request came from,\n * else the first row. A name that matches nothing is `null` — playing on the wrong TV is worse than asking.\n */\nexport function findPlayer(players: YouTubePlayer[], wanted?: string, room?: string): YouTubePlayer | null {\n if (!players.length) {\n return null;\n }\n const name = (wanted || '').trim();\n if (name) {\n return (\n players.find(p => fold(p.name) === fold(name)) ||\n players.find(p => p.room && fold(p.room) === fold(name)) ||\n players.find(p => nameMatches(p.name, name) || (!!p.room && nameMatches(p.room, name))) ||\n null\n );\n }\n if (room) {\n const here = players.find(p => (p.room && fold(p.room) === fold(room)) || fold(p.name) === fold(room));\n if (here) {\n return here;\n }\n }\n return players[0];\n}\n\n/** Does a value of a power state mean \"on\"? */\nexport function isPowerOn(value: unknown): boolean {\n if (typeof value === 'boolean') {\n return value;\n }\n if (typeof value === 'number') {\n return value > 0;\n }\n return typeof value === 'string' && /^(on|true|1|an|ein|power_?on|вкл)$/i.test(value.trim());\n}\n\n/**\n * The query as something the player takes without a search: a youtube.com/youtu.be URL, a channel id, a\n * playlist id, or — when a video is asked for — an 11-character video id. `null` = needs a search.\n */\nexport function directYouTubeTarget(query: string, kind: YouTubeKind): string | null {\n const q = query.trim();\n if (/^(https?:\\/\\/)?((www|m|music)\\.)?(youtube\\.com|youtu\\.be)\\//i.test(q)) {\n return q;\n }\n if (/^UC[\\w-]{22}$/.test(q) || /^(PL|UU|OL|FL)[\\w-]{10,}$/.test(q)) {\n return q;\n }\n // A plain word of 11 letters (\"Tagesschaus\") is no id: a real one nearly always mixes in digits,\n // capitals or -/_.\n if (kind === 'video' && /^[\\w-]{11}$/.test(q) && /[\\d_A-Z-]/.test(q.slice(1))) {\n return q;\n }\n return null;\n}\n\n/** InnerTube search filter (`sp` parameter of the results page): only channels / videos / playlists. */\nconst SEARCH_FILTER: Record<YouTubeKind, string> = {\n video: 'EgIQAQ==',\n channel: 'EgIQAg==',\n playlist: 'EgIQAw==',\n};\n\n/** Text of an InnerTube text object (`{simpleText}` or `{runs:[{text}]}`). */\nfunction textOf(node: unknown): string {\n if (!node || typeof node !== 'object') {\n return '';\n }\n const n = node as { simpleText?: unknown; runs?: { text?: unknown }[]; content?: unknown };\n if (typeof n.simpleText === 'string') {\n return n.simpleText;\n }\n if (typeof n.content === 'string') {\n return n.content;\n }\n if (Array.isArray(n.runs)) {\n return n.runs.map(r => (typeof r?.text === 'string' ? r.text : '')).join('');\n }\n return '';\n}\n\n/**\n * Pull the results of one kind out of an InnerTube search response. Walks the whole tree: the renderer\n * objects stay stable for years, the containers around them do not (playlists already moved from\n * `playlistRenderer` to `lockupViewModel`).\n */\nexport function parseSearchResults(response: unknown, kind: YouTubeKind): YouTubeHit[] {\n const hits: YouTubeHit[] = [];\n const seen = new Set<string>();\n const add = (hit: YouTubeHit): void => {\n if (hit.id && !seen.has(hit.id)) {\n seen.add(hit.id);\n hits.push(hit);\n }\n };\n const walk = (node: unknown, depth: number): void => {\n if (!node || typeof node !== 'object' || depth > 40) {\n return;\n }\n if (Array.isArray(node)) {\n for (const item of node) {\n walk(item, depth + 1);\n }\n return;\n }\n for (const [key, value] of Object.entries(node as Record<string, unknown>)) {\n const v = value as Record<string, any>;\n if (key === 'channelRenderer' && kind === 'channel' && typeof v?.channelId === 'string') {\n // YouTube shows the handle in the field that used to hold the subscriber count.\n const handle = [textOf(v.subscriberCountText), textOf(v.videoCountText)].find(t => t.startsWith('@'));\n add({ kind, id: v.channelId, title: textOf(v.title), handle });\n } else if (key === 'videoRenderer' && kind === 'video' && typeof v?.videoId === 'string') {\n add({ kind, id: v.videoId, title: textOf(v.title), channel: textOf(v.ownerText) || undefined });\n } else if (key === 'playlistRenderer' && kind === 'playlist' && typeof v?.playlistId === 'string') {\n add({\n kind,\n id: v.playlistId,\n title: textOf(v.title),\n channel: textOf(v.shortBylineText) || undefined,\n });\n } else if (key === 'lockupViewModel' && typeof v?.contentId === 'string') {\n const type = String(v.contentType || '');\n const meta = v.metadata?.lockupMetadataViewModel;\n if (\n (kind === 'playlist' && type === 'LOCKUP_CONTENT_TYPE_PLAYLIST') ||\n (kind === 'video' && type === 'LOCKUP_CONTENT_TYPE_VIDEO')\n ) {\n add({ kind, id: v.contentId, title: textOf(meta?.title) });\n }\n }\n walk(value, depth + 1);\n }\n };\n walk(response, 0);\n return hits;\n}\n\n/** Edit distance of two short strings (names, not texts — the matrix is small). */\nfunction editDistance(a: string, b: string): number {\n let prev = Array.from({ length: b.length + 1 }, (_, k) => k);\n for (let i = 1; i <= a.length; i++) {\n const cur = [i];\n for (let k = 1; k <= b.length; k++) {\n cur[k] = Math.min(prev[k] + 1, cur[k - 1] + 1, prev[k - 1] + (a[i - 1] === b[k - 1] ? 0 : 1));\n }\n prev = cur;\n }\n return prev[b.length];\n}\n\n/**\n * Is a spoken name the same name? Spaces do not count and a typo or two per eight letters is allowed,\n * because the name went through speech recognition: \"Sailing Ocean Air\" is the channel \"Sailing Oceanaire\".\n */\nexport function similarName(said: string, name: string): boolean {\n const a = fold(said).replace(/ /g, '');\n const b = fold(name).replace(/ /g, '');\n if (!a || !b) {\n return false;\n }\n return a === b || editDistance(a, b) <= Math.max(1, Math.floor(Math.min(a.length, b.length) / 8));\n}\n\n/** Is this channel the one that was named — by its title (without the \" | tagline\") or its handle? */\nexport function isNamedChannel(hit: YouTubeHit, query: string): boolean {\n return similarName(query, spokenTitle(hit)) || (!!hit.handle && similarName(query, hit.handle.replace(/^@/, '')));\n}\n\n/**\n * The hit that was most likely meant: a channel whose name is the query (see {@link isNamedChannel}), a\n * video or playlist titled like it, otherwise YouTube's own first result. The name match matters when a\n * bigger channel outranks the one that was named.\n */\nexport function pickBest(hits: YouTubeHit[], query: string): YouTubeHit | null {\n if (!hits.length) {\n return null;\n }\n const q = fold(query);\n return hits.find(h => (h.kind === 'channel' ? isNamedChannel(h, query) : fold(h.title) === q)) || hits[0];\n}\n\n/** The title as it should be spoken: without the \" | tagline\" channels like to append. */\nexport function spokenTitle(hit: YouTubeHit): string {\n return hit.title.split(' | ')[0].trim() || hit.title;\n}\n\nexport interface SearchOptions {\n /** Interface language of the results (ISO, e.g. `de`). */\n language?: string;\n timeoutMs?: number;\n fetchFn?: typeof fetch;\n}\n\n/** Search YouTube for channels, videos or playlists; throws on a network or HTTP error. */\nexport async function searchYouTube(query: string, kind: YouTubeKind, opts: SearchOptions = {}): Promise<YouTubeHit[]> {\n const fetchFn = opts.fetchFn || fetch;\n const res = await fetchFn('https://www.youtube.com/youtubei/v1/search?prettyPrint=false', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n context: { client: { clientName: 'WEB', clientVersion: '2.20250101.00.00', hl: opts.language || 'en' } },\n query,\n params: SEARCH_FILTER[kind],\n }),\n signal: AbortSignal.timeout(opts.timeoutMs || 10_000),\n });\n if (res.status !== 200) {\n throw new Error(`YouTube search answered HTTP ${res.status}`);\n }\n return parseSearchResults(await res.json(), kind);\n}\n\n// ── Rule-based \"play … on YouTube\" ──────────────────────────────────────────\n\nconst VERBS = new Set([\n 'spiel',\n 'spiele',\n 'spielt',\n 'abspielen',\n 'zeig',\n 'zeige',\n 'starte',\n 'start',\n 'play',\n 'show',\n 'put',\n 'включи',\n 'поставь',\n 'запусти',\n 'покажи',\n 'воспроизведи',\n]);\nconst POLITE = new Set(['bitte', 'mal', 'please', 'пожалуйста']);\nconst FILLERS = new Set([\n 'mir',\n 'uns',\n 'me',\n 'us',\n 'мне',\n 'нам',\n 'den',\n 'das',\n 'die',\n 'einen',\n 'ein',\n 'eine',\n 'the',\n 'a',\n 'an',\n 'some',\n 'mal',\n 'bitte',\n]);\nconst KIND_WORDS: Record<string, YouTubeKind> = {\n kanal: 'channel',\n channel: 'channel',\n канал: 'channel',\n video: 'video',\n clip: 'video',\n видео: 'video',\n ролик: 'video',\n playlist: 'playlist',\n wiedergabeliste: 'playlist',\n плейлист: 'playlist',\n};\nconst OF_WORDS = new Set(['von', 'vom', 'of', 'from', 'by', 'от']);\nconst YOUTUBE_WORDS = new Set(['youtube', 'ютуб', 'ютубе', 'ютюб', 'ютюбе']);\nconst PREPOSITIONS = new Set(['auf', 'im', 'in', 'am', 'an', 'bei', 'dem', 'der', 'on', 'at', 'the', 'на', 'в', 'во']);\n/** Prepositions that only ever say \"where\" — what follows them right after the verb is a place, not a title. */\nconst LOCATIVE = new Set(['im', 'в', 'во']);\nconst ARTICLES = new Set(['der', 'dem', 'den', 'the']);\nconst TRAILING = new Set(['ab', 'please', 'bitte', 'пожалуйста', 'jetzt', 'now']);\n/** A \"query\" that is really a request to repeat — not something to search for. */\nconst NOT_A_QUERY = new Set(['nochmal', 'noch mal', 'weiter', 'again', 'more', 'ещё', 'еще', 'ещё раз', 'еще раз']);\n\n/**\n * Understand \"spiele den Kanal Sailing OceanAir (auf dem Fernseher) ab\", \"play the channel X on the bedroom\n * TV\", \"включи канал X\", \"spiele im Wohnzimmer Sailing Ocean Air auf YouTube\". Needs a kind word\n * (Kanal/channel/канал, Video, Playlist) or the word YouTube — \"spiele Musik im Wohnzimmer\" stays with the\n * rest of the pipeline. Without a kind word the kind is `auto`. A player name or room right after the verb\n * or at the end is cut off the query, as is a place right after the verb (\"im Bad\") — that one is then\n * looked up as a player and answered as unknown, which beats searching YouTube for \"im Bad X\". Everything\n * else stays in the query with its spelling, because it is what gets searched for.\n */\nexport function parseYouTubeCommand(text: string, players: YouTubePlayer[]): YouTubeCommand | null {\n const words = text\n .trim()\n .split(/\\s+/)\n .map(w => w.replace(/^[„\"“”'«»]+|[„\"“”'«».,!?;:]+$/g, ''))\n .filter(Boolean);\n const low = words.map(w => fold(w));\n let i = 0;\n while (i < low.length && POLITE.has(low[i])) {\n i++;\n }\n if (i >= low.length || !VERBS.has(low[i])) {\n return null;\n }\n i++;\n const sawYouTube = low.some(w => YOUTUBE_WORDS.has(w));\n // Player names and rooms, longest first so \"Schlafzimmer TV\" wins over \"TV\".\n const candidates = players\n .flatMap(p => [\n { player: p.name, words: fold(p.name).split(' ') },\n ...(p.room ? [{ player: p.name || p.room, words: fold(p.room).split(' ') }] : []),\n ])\n .filter(c => c.words[0])\n .sort((a, b) => b.words.length - a.words.length);\n const nameAt = (at: number): { player: string; end: number } | null => {\n for (const c of candidates) {\n if (c.words.every((w, k) => at + k < low.length && sameWord(w, low[at + k]))) {\n return { player: c.player, end: at + c.words.length };\n }\n }\n return null;\n };\n /** \"im Wohnzimmer\", \"auf dem Fernseher\", \"в спальне\" starting at `at`; `null` if it is no place. */\n const placeAt = (at: number): { player: string; end: number } | null => {\n let j = at;\n while (j < low.length && PREPOSITIONS.has(low[j])) {\n j++;\n }\n if (j === at || j >= low.length) {\n return null;\n }\n const named = nameAt(j);\n if (named) {\n return named;\n }\n const locative = LOCATIVE.has(low[at]) || (low[at] === 'in' && ARTICLES.has(low[at + 1]));\n return locative ? { player: words[j], end: j + 1 } : null;\n };\n let kind: YouTubeKind | 'auto' | null = null;\n let player: string | undefined;\n // Fillers, \"auf YouTube\", the kind word and the place may come in any order before the name.\n for (;;) {\n if (i < low.length && FILLERS.has(low[i])) {\n i++;\n } else if (i + 1 < low.length && PREPOSITIONS.has(low[i]) && YOUTUBE_WORDS.has(low[i + 1])) {\n i += 2;\n } else if (i < low.length && YOUTUBE_WORDS.has(low[i])) {\n i++;\n } else if (!kind && i < low.length && KIND_WORDS[low[i]]) {\n kind = KIND_WORDS[low[i]];\n i++;\n if (i < low.length && OF_WORDS.has(low[i])) {\n i++;\n }\n } else if (!player && i < low.length) {\n const place = placeAt(i);\n // Only if something is left to search for.\n if (!place || place.end >= low.length) {\n break;\n }\n player = place.player;\n i = place.end;\n } else {\n break;\n }\n }\n if (!kind) {\n if (!sawYouTube) {\n return null;\n }\n kind = 'auto';\n }\n let end = words.length;\n const trimTail = (): void => {\n for (;;) {\n if (end > i && TRAILING.has(low[end - 1])) {\n end--;\n } else if (end > i && YOUTUBE_WORDS.has(low[end - 1])) {\n end--;\n while (end > i && PREPOSITIONS.has(low[end - 1])) {\n end--;\n }\n } else {\n return;\n }\n }\n };\n trimTail();\n // A trailing player: its name or room, preceded by a preposition so a channel called like a room is\n // not eaten (\"Kanal Küche\" without \"in der\" stays a channel).\n for (const c of player ? [] : candidates) {\n const n = c.words.length;\n if (end - n <= i) {\n continue;\n }\n const tail = low.slice(end - n, end);\n if (!c.words.every((w, k) => sameWord(w, tail[k]))) {\n continue;\n }\n let start = end - n;\n if (!PREPOSITIONS.has(low[start - 1])) {\n continue;\n }\n while (start > i && PREPOSITIONS.has(low[start - 1])) {\n start--;\n }\n if (start <= i) {\n continue; // nothing would be left to search for\n }\n player = c.player;\n end = start;\n trimTail();\n break;\n }\n const query = words.slice(i, end).join(' ').trim();\n if (!query || NOT_A_QUERY.has(fold(query))) {\n return null;\n }\n return { kind, query, ...(player ? { player } : {}) };\n}\n\n// ── Spoken replies (de/en/ru) ───────────────────────────────────────────────\n\nexport type YouTubeReplyKey =\n | 'playing'\n | 'poweringOn'\n | 'notFound'\n | 'searchFailed'\n | 'noConfirm'\n | 'adapterDown'\n | 'unknownPlayer'\n | 'noControl'\n | 'noDevice';\n\n/** \"the channel „X“\" in the reply language. */\nfunction describeWhat(lang: string, kind: YouTubeKind, title: string): string {\n if (lang === 'de') {\n return `${kind === 'channel' ? 'den Kanal' : kind === 'playlist' ? 'die Playlist' : 'das Video'} „${title}“`;\n }\n if (lang === 'ru') {\n return `${kind === 'channel' ? 'канал' : kind === 'playlist' ? 'плейлист' : 'видео'} «${title}»`;\n }\n return `the ${kind} \"${title}\"`;\n}\n\n/** What nothing was found of: \"keinen Kanal\", \"no channel\", \"канал\". */\nfunction describeNone(lang: string, kind: YouTubeKind): string {\n if (lang === 'de') {\n return kind === 'channel' ? 'keinen Kanal' : kind === 'playlist' ? 'keine Playlist' : 'kein Video';\n }\n if (lang === 'ru') {\n return kind === 'channel' ? 'канал' : kind === 'playlist' ? 'плейлист' : 'видео';\n }\n return `no ${kind}`;\n}\n\n/** A spoken reply of the YouTube feature. */\nexport function youtubeReply(\n lang: string,\n key: YouTubeReplyKey,\n v: { kind?: YouTubeKind; title?: string; query?: string; player?: string; names?: string; instance?: string } = {},\n): string {\n const kind = v.kind || 'video';\n const what = describeWhat(lang, kind, v.title || v.query || '');\n const player = v.player || '';\n if (lang === 'de') {\n switch (key) {\n case 'playing':\n return `Ich spiele ${what} auf ${player}.`;\n case 'poweringOn':\n return `Ich schalte ${player} ein und spiele dann ${what}.`;\n case 'notFound':\n return `Ich habe auf YouTube ${describeNone(lang, kind)} „${v.query}“ gefunden.`;\n case 'searchFailed':\n return 'Die YouTube-Suche ist gerade nicht erreichbar.';\n case 'noConfirm':\n return `${player} hat nicht reagiert. Ist der Fernseher an und die YouTube-App verbunden?`;\n case 'adapterDown':\n return `Der YouTube-Adapter ${v.instance} läuft nicht.`;\n case 'unknownPlayer':\n return `Einen Player „${v.query}“ kenne ich nicht. Ich kenne: ${v.names}.`;\n case 'noControl':\n return 'Die Gerätesteuerung ist in den Einstellungen ausgeschaltet.';\n case 'noDevice':\n return `Mit ${v.instance} ist noch kein Fernseher gekoppelt.`;\n }\n }\n if (lang === 'ru') {\n switch (key) {\n case 'playing':\n return `Включаю ${what} — ${player}.`;\n case 'poweringOn':\n return `Включаю ${player}, потом запущу ${what}.`;\n case 'notFound':\n return `На YouTube не нашла ${describeNone(lang, kind)} «${v.query}».`;\n case 'searchFailed':\n return 'Поиск YouTube сейчас недоступен.';\n case 'noConfirm':\n return `${player} не ответил. Телевизор включён и приложение YouTube подключено?`;\n case 'adapterDown':\n return `Адаптер YouTube ${v.instance} не запущен.`;\n case 'unknownPlayer':\n return `Плеер «${v.query}» мне не известен. Я знаю: ${v.names}.`;\n case 'noControl':\n return 'Управление устройствами отключено в настройках.';\n case 'noDevice':\n return `С ${v.instance} ещё не связан ни один телевизор.`;\n }\n }\n switch (key) {\n case 'playing':\n return `Playing ${what} on ${player}.`;\n case 'poweringOn':\n return `Switching ${player} on, then playing ${what}.`;\n case 'notFound':\n return `I found ${describeNone(lang, kind)} \"${v.query}\" on YouTube.`;\n case 'searchFailed':\n return 'The YouTube search cannot be reached right now.';\n case 'noConfirm':\n return `${player} did not respond. Is the TV on and the YouTube app linked?`;\n case 'adapterDown':\n return `The YouTube adapter ${v.instance} is not running.`;\n case 'unknownPlayer':\n return `I don't know a player \"${v.query}\". I know: ${v.names}.`;\n case 'noControl':\n return 'Device control is switched off in the settings.';\n case 'noDevice':\n return `No TV is paired with ${v.instance} yet.`;\n }\n return '';\n}\n"]}
|
package/build/main.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
const os = __importStar(require("node:os"));
|
|
37
37
|
const fs = __importStar(require("node:fs"));
|
|
38
38
|
const node_child_process_1 = require("node:child_process");
|
|
39
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
39
40
|
const adapter_core_1 = require("@iobroker/adapter-core");
|
|
40
41
|
const path = __importStar(require("node:path"));
|
|
41
42
|
const mcp_server_1 = require("@iobroker/mcp-server");
|
|
@@ -54,6 +55,7 @@ const ask_1 = require("./lib/ask");
|
|
|
54
55
|
const presence_1 = require("./lib/presence");
|
|
55
56
|
const targets_1 = require("./lib/targets");
|
|
56
57
|
const routines_1 = require("./lib/routines");
|
|
58
|
+
const youtube_1 = require("./lib/youtube");
|
|
57
59
|
const notifications_1 = require("./lib/notifications");
|
|
58
60
|
const triggers_1 = require("./lib/triggers");
|
|
59
61
|
const voiceServer_1 = require("./lib/voice/voiceServer");
|
|
@@ -75,6 +77,12 @@ const WEATHER_CTX_TTL = 5 * 60 * 1000;
|
|
|
75
77
|
/** How long to wait for a device to confirm a write, and how often to look. */
|
|
76
78
|
const ACK_TIMEOUT_MS = 2000;
|
|
77
79
|
const ACK_POLL_MS = 150;
|
|
80
|
+
/** How long a TV may take to report "on" after it was switched on for YouTube. */
|
|
81
|
+
const TV_POWER_TIMEOUT_MS = 30_000;
|
|
82
|
+
/** How long youtube-remote may take to acknowledge a play command (it goes through Google's lounge). */
|
|
83
|
+
const YOUTUBE_ACK_TIMEOUT_MS = 8000;
|
|
84
|
+
/** Pause before the play command is repeated once on a TV that just booted. */
|
|
85
|
+
const YOUTUBE_RETRY_MS = 5000;
|
|
78
86
|
/**
|
|
79
87
|
* Did the device end up with the value we asked for? Compared loosely: a dimmer told `30` may report
|
|
80
88
|
* `30.0`, a switch told `true` may report `1`, and a lamp told 100 % may settle at 99 % — insisting on
|
|
@@ -248,6 +256,13 @@ class Assistant extends adapter_core_1.Adapter {
|
|
|
248
256
|
routines = [];
|
|
249
257
|
/** Named announcement targets (roadmap B2): groups of rooms and people, from the configuration. */
|
|
250
258
|
announceTargets = [];
|
|
259
|
+
/** YouTube players from the configuration; the first one is the default. */
|
|
260
|
+
youtubePlayers = [];
|
|
261
|
+
/**
|
|
262
|
+
* Where the request being answered came from, for the LLM tools: a tool only gets the model's
|
|
263
|
+
* arguments, but "play X" from the bedroom satellite should go to the bedroom TV.
|
|
264
|
+
*/
|
|
265
|
+
requestOrigin = new node_async_hooks_1.AsyncLocalStorage();
|
|
251
266
|
/** Who is at home (roadmap B1); mirrored into `presence.*`. Null until onReady. */
|
|
252
267
|
presence = null;
|
|
253
268
|
/** Satellites currently set to Do-Not-Disturb, by state id (mirrors `satellites.<id>.dnd`). */
|
|
@@ -302,6 +317,18 @@ class Assistant extends adapter_core_1.Adapter {
|
|
|
302
317
|
// Only useful with satellites: without voice there is nothing to speak on.
|
|
303
318
|
tools.push(this.buildAnnounceTool());
|
|
304
319
|
}
|
|
320
|
+
this.youtubePlayers = (0, youtube_1.parsePlayerRows)(cfg.youtubePlayers);
|
|
321
|
+
if (this.youtubePlayers.length) {
|
|
322
|
+
// Before the tools are built: their descriptions carry the player names, which an instance
|
|
323
|
+
// row only gets from its device.
|
|
324
|
+
for (const player of this.youtubePlayers) {
|
|
325
|
+
if (!(await this.resolveYouTubePlayer(player))) {
|
|
326
|
+
this.log.warn(`YouTube: ${player.instance} has no paired TV yet — pair one in its settings.`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
tools.push(...this.buildYouTubeTools());
|
|
330
|
+
this.log.info(`YouTube players: ${this.youtubePlayers.map(p => p.name).join(', ')}.`);
|
|
331
|
+
}
|
|
305
332
|
this.log.info(`ioBroker tools enabled (${tools.length}): ${tools.map(t => t.name).join(', ')}`);
|
|
306
333
|
if (denied.length) {
|
|
307
334
|
this.log.debug(`Tools denied by access settings: ${denied.join(', ')}`);
|
|
@@ -1929,6 +1956,27 @@ class Assistant extends adapter_core_1.Adapter {
|
|
|
1929
1956
|
}
|
|
1930
1957
|
return;
|
|
1931
1958
|
}
|
|
1959
|
+
// Scripts: find and play something on a configured YouTube player, the TV switched on first.
|
|
1960
|
+
// message = { query, kind?: 'channel'|'video'|'playlist', player? } → { ok, text, title?, id?, player? }
|
|
1961
|
+
// Settings: the "Player" column of the YouTube table (selectSendTo).
|
|
1962
|
+
if (obj.command === 'getYouTubeInstances') {
|
|
1963
|
+
const list = await this.getYouTubeInstances();
|
|
1964
|
+
if (obj.callback) {
|
|
1965
|
+
this.sendTo(obj.from, obj.command, list, obj.callback);
|
|
1966
|
+
}
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
if (obj.command === 'playYouTube') {
|
|
1970
|
+
const m = (obj.message || {});
|
|
1971
|
+
const kind = m.kind === 'channel' || m.kind === 'playlist' || m.kind === 'video' ? m.kind : 'auto';
|
|
1972
|
+
const res = String(m.query || '').trim()
|
|
1973
|
+
? await this.playYouTube(String(m.query).trim(), kind, m.player)
|
|
1974
|
+
: { ok: false, text: 'no query given' };
|
|
1975
|
+
if (obj.callback) {
|
|
1976
|
+
this.sendTo(obj.from, obj.command, this.youtubeResultData(res), obj.callback);
|
|
1977
|
+
}
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1932
1980
|
// Scripts / settings: which wake words an ESPHome satellite offers and which are listening.
|
|
1933
1981
|
// message = { device? } — omitted means every connected one.
|
|
1934
1982
|
if (obj.command === 'getWakeWords') {
|
|
@@ -2222,9 +2270,9 @@ class Assistant extends adapter_core_1.Adapter {
|
|
|
2222
2270
|
* later turns have the full thread.
|
|
2223
2271
|
*/
|
|
2224
2272
|
answer(question, source = '', origin = {}) {
|
|
2225
|
-
return this.journal
|
|
2273
|
+
return this.requestOrigin.run({ source, room: origin.room || '' }, () => this.journal
|
|
2226
2274
|
? this.journal.run(question, source, origin, () => this.answerTracked(question, source))
|
|
2227
|
-
: this.answerTracked(question, source);
|
|
2275
|
+
: this.answerTracked(question, source));
|
|
2228
2276
|
}
|
|
2229
2277
|
async answerTracked(question, source) {
|
|
2230
2278
|
const useCtx = this.config.useConversationContext !== false;
|
|
@@ -2347,6 +2395,15 @@ class Assistant extends adapter_core_1.Adapter {
|
|
|
2347
2395
|
}
|
|
2348
2396
|
// Tier 0: rule-based NLU (device commands) — fastest, offline, free.
|
|
2349
2397
|
if (this.config.useLocalNlu) {
|
|
2398
|
+
// "Spiele den Kanal X ab" first: the device NLU would find the TV in "… auf dem Fernseher" and
|
|
2399
|
+
// answer with its power state.
|
|
2400
|
+
const yt = this.youtubePlayers.length ? (0, youtube_1.parseYouTubeCommand)(question, this.youtubePlayers) : null;
|
|
2401
|
+
if (yt) {
|
|
2402
|
+
this.log.info(`YouTube request understood locally: ${yt.kind} "${yt.query}" (source='${source}').`);
|
|
2403
|
+
this.journal?.note({ tier: 'nlu', detail: `youtube ${yt.kind}` });
|
|
2404
|
+
const res = await this.playYouTube(yt.query, yt.kind, yt.player);
|
|
2405
|
+
return res.text;
|
|
2406
|
+
}
|
|
2350
2407
|
try {
|
|
2351
2408
|
const handled = await this.tryLocalNlu(question, source);
|
|
2352
2409
|
if (handled !== null) {
|
|
@@ -3938,6 +3995,383 @@ class Assistant extends adapter_core_1.Adapter {
|
|
|
3938
3995
|
},
|
|
3939
3996
|
};
|
|
3940
3997
|
}
|
|
3998
|
+
// ── YouTube on the TV (youtube-remote players) ──────────────────────────
|
|
3999
|
+
/** The player a request means: the named one, else the one in the requesting room, else the first. */
|
|
4000
|
+
youtubePlayerFor(name) {
|
|
4001
|
+
return (0, youtube_1.findPlayer)(this.youtubePlayers, name, this.requestOrigin.getStore()?.room || '');
|
|
4002
|
+
}
|
|
4003
|
+
/**
|
|
4004
|
+
* Find the device of a row that names a youtube-remote instance. Done again before every command: the
|
|
4005
|
+
* device id comes from the TV's screen id, so pairing the TV again replaces the device while we run.
|
|
4006
|
+
* false = nothing is paired with that instance.
|
|
4007
|
+
*/
|
|
4008
|
+
async resolveYouTubePlayer(player) {
|
|
4009
|
+
if (!player.instance) {
|
|
4010
|
+
return true;
|
|
4011
|
+
}
|
|
4012
|
+
let devices = [];
|
|
4013
|
+
try {
|
|
4014
|
+
const objs = await this.getForeignObjectsAsync(`${player.instance}.*`, 'device');
|
|
4015
|
+
devices = Object.values(objs).map(o => ({ id: o._id, name: this.youtubeDeviceName(o) }));
|
|
4016
|
+
}
|
|
4017
|
+
catch (e) {
|
|
4018
|
+
this.log.debug(`YouTube: cannot read the devices of ${player.instance}: ${e.message}`);
|
|
4019
|
+
}
|
|
4020
|
+
const device = (0, youtube_1.pickDevice)(devices, player);
|
|
4021
|
+
player.base = device?.id || '';
|
|
4022
|
+
player.target = device ? `${device.id}.youtube` : '';
|
|
4023
|
+
if (device && !player.name) {
|
|
4024
|
+
player.name = device.name || player.instance;
|
|
4025
|
+
}
|
|
4026
|
+
return !!device;
|
|
4027
|
+
}
|
|
4028
|
+
/** Display name of an object, in the voice language if it is translated. */
|
|
4029
|
+
youtubeDeviceName(obj) {
|
|
4030
|
+
const name = obj.common?.name;
|
|
4031
|
+
if (typeof name === 'string') {
|
|
4032
|
+
return name;
|
|
4033
|
+
}
|
|
4034
|
+
const lang = String(this.config.voiceLanguage || this.language || 'en');
|
|
4035
|
+
return name?.[lang] || name?.en || obj._id.split('.').pop() || obj._id;
|
|
4036
|
+
}
|
|
4037
|
+
/**
|
|
4038
|
+
* Options of the "Player" column: every youtube-remote instance (it finds its TV itself), and for an
|
|
4039
|
+
* instance with several paired TVs also each of them, to pin a row to one.
|
|
4040
|
+
*/
|
|
4041
|
+
async getYouTubeInstances() {
|
|
4042
|
+
const options = [];
|
|
4043
|
+
let instances = [];
|
|
4044
|
+
try {
|
|
4045
|
+
const view = await this.getObjectViewAsync('system', 'instance', {
|
|
4046
|
+
startkey: 'system.adapter.youtube-remote.',
|
|
4047
|
+
endkey: 'system.adapter.youtube-remote.香',
|
|
4048
|
+
});
|
|
4049
|
+
instances = (view?.rows || []).map(r => r.id.replace(/^system\.adapter\./, '')).sort();
|
|
4050
|
+
}
|
|
4051
|
+
catch (e) {
|
|
4052
|
+
this.log.warn(`getYouTubeInstances failed: ${e.message}`);
|
|
4053
|
+
}
|
|
4054
|
+
for (const instance of instances) {
|
|
4055
|
+
const objs = await this.getForeignObjectsAsync(`${instance}.*`, 'device').catch(() => ({}));
|
|
4056
|
+
const devices = Object.values(objs)
|
|
4057
|
+
.map(o => ({ id: o._id, name: this.youtubeDeviceName(o) }))
|
|
4058
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
4059
|
+
const names = devices.map(d => d.name).join(', ');
|
|
4060
|
+
options.push({ label: names ? `${instance} (${names})` : `${instance} (no TV paired)`, value: instance });
|
|
4061
|
+
if (devices.length > 1) {
|
|
4062
|
+
for (const d of devices) {
|
|
4063
|
+
options.push({ label: `${instance} › ${d.name}`, value: d.id });
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
return options;
|
|
4068
|
+
}
|
|
4069
|
+
/**
|
|
4070
|
+
* Find `query` on YouTube and play it on a player, switching its TV on first. The search runs before
|
|
4071
|
+
* anything is switched: a TV woken for a channel that does not exist would be the worst outcome.
|
|
4072
|
+
*
|
|
4073
|
+
* A TV that has to boot takes 10–30 s, so in that case the reply comes right away and the rest runs
|
|
4074
|
+
* in the background ({@link playWhenTvIsOn}) — over voice, half a minute of silence reads as "did not
|
|
4075
|
+
* hear me". An already running TV is answered only after the player acknowledged the command.
|
|
4076
|
+
*/
|
|
4077
|
+
async playYouTube(query, kind, playerName) {
|
|
4078
|
+
const lang = String(this.config.voiceLanguage || this.language || 'en');
|
|
4079
|
+
const player = this.youtubePlayerFor(playerName);
|
|
4080
|
+
if (!player) {
|
|
4081
|
+
const names = this.youtubePlayers.map(p => p.name).join(', ');
|
|
4082
|
+
return { ok: false, text: (0, youtube_1.youtubeReply)(lang, 'unknownPlayer', { query: playerName, names }) };
|
|
4083
|
+
}
|
|
4084
|
+
if (!(await this.resolveYouTubePlayer(player))) {
|
|
4085
|
+
return {
|
|
4086
|
+
ok: false,
|
|
4087
|
+
player: player.name,
|
|
4088
|
+
text: (0, youtube_1.youtubeReply)(lang, 'noDevice', { instance: player.instance }),
|
|
4089
|
+
};
|
|
4090
|
+
}
|
|
4091
|
+
if (!this.config.allowWriteStates) {
|
|
4092
|
+
return { ok: false, player: player.name, text: (0, youtube_1.youtubeReply)(lang, 'noControl') };
|
|
4093
|
+
}
|
|
4094
|
+
let hit = null;
|
|
4095
|
+
let alternatives = [];
|
|
4096
|
+
// Unsaid kind: what is named like a channel is the channel ("spiele Sailing OceanAir auf YouTube"),
|
|
4097
|
+
// anything else a video search — a video search for a channel name finds some video about it.
|
|
4098
|
+
const searchKind = kind === 'auto' ? 'video' : kind;
|
|
4099
|
+
const direct = (0, youtube_1.directYouTubeTarget)(query, searchKind);
|
|
4100
|
+
if (direct) {
|
|
4101
|
+
hit = { kind: searchKind, id: direct, title: query };
|
|
4102
|
+
}
|
|
4103
|
+
else {
|
|
4104
|
+
try {
|
|
4105
|
+
if (kind === 'auto') {
|
|
4106
|
+
const channels = await (0, youtube_1.searchYouTube)(query, 'channel', { language: lang });
|
|
4107
|
+
hit = channels.find(h => (0, youtube_1.isNamedChannel)(h, query)) || null;
|
|
4108
|
+
alternatives = channels.filter(h => h !== hit).slice(0, 3);
|
|
4109
|
+
}
|
|
4110
|
+
if (!hit) {
|
|
4111
|
+
const hits = await (0, youtube_1.searchYouTube)(query, searchKind, { language: lang });
|
|
4112
|
+
hit = (0, youtube_1.pickBest)(hits, query);
|
|
4113
|
+
alternatives = hits.filter(h => h !== hit).slice(0, 3);
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
catch (e) {
|
|
4117
|
+
this.log.warn(`YouTube search for "${query}" failed: ${e.message}`);
|
|
4118
|
+
return { ok: false, player: player.name, text: (0, youtube_1.youtubeReply)(lang, 'searchFailed') };
|
|
4119
|
+
}
|
|
4120
|
+
if (!hit) {
|
|
4121
|
+
return {
|
|
4122
|
+
ok: false,
|
|
4123
|
+
player: player.name,
|
|
4124
|
+
text: (0, youtube_1.youtubeReply)(lang, 'notFound', { kind: searchKind, query }),
|
|
4125
|
+
};
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
const title = (0, youtube_1.spokenTitle)(hit);
|
|
4129
|
+
this.log.info(`YouTube: ${kind} "${query}" → ${hit.kind} ${hit.id} (${hit.title}) on ${player.name}.`);
|
|
4130
|
+
const reply = { kind: hit.kind, title, player: player.name };
|
|
4131
|
+
if ((await this.tvPower(player)) === 'off') {
|
|
4132
|
+
try {
|
|
4133
|
+
await this.switchTvOn(player);
|
|
4134
|
+
}
|
|
4135
|
+
catch (e) {
|
|
4136
|
+
this.log.warn(`YouTube: cannot switch ${player.power} on: ${e.message}`);
|
|
4137
|
+
}
|
|
4138
|
+
void this.playWhenTvIsOn(player, hit);
|
|
4139
|
+
return {
|
|
4140
|
+
ok: true,
|
|
4141
|
+
hit,
|
|
4142
|
+
alternatives,
|
|
4143
|
+
player: player.name,
|
|
4144
|
+
poweringOn: true,
|
|
4145
|
+
text: (0, youtube_1.youtubeReply)(lang, 'poweringOn', reply),
|
|
4146
|
+
};
|
|
4147
|
+
}
|
|
4148
|
+
if (await this.sendToYouTubePlayer(player, hit.id)) {
|
|
4149
|
+
return { ok: true, hit, alternatives, player: player.name, text: (0, youtube_1.youtubeReply)(lang, 'playing', reply) };
|
|
4150
|
+
}
|
|
4151
|
+
const instance = player.base.split('.').slice(0, 2).join('.');
|
|
4152
|
+
const alive = (await this.getForeignStateAsync(`system.adapter.${instance}.alive`).catch(() => null))?.val;
|
|
4153
|
+
return {
|
|
4154
|
+
ok: false,
|
|
4155
|
+
hit,
|
|
4156
|
+
player: player.name,
|
|
4157
|
+
text: alive ? (0, youtube_1.youtubeReply)(lang, 'noConfirm', reply) : (0, youtube_1.youtubeReply)(lang, 'adapterDown', { instance }),
|
|
4158
|
+
};
|
|
4159
|
+
}
|
|
4160
|
+
/**
|
|
4161
|
+
* Whether the TV of a player is on. `unknown` when there is nothing to tell it by: no power state, or
|
|
4162
|
+
* one that is a button — pressing a toggle button on a TV that is already on would switch it off.
|
|
4163
|
+
*/
|
|
4164
|
+
async tvPower(player) {
|
|
4165
|
+
if (!player.power) {
|
|
4166
|
+
return 'unknown';
|
|
4167
|
+
}
|
|
4168
|
+
const obj = await this.getForeignObjectAsync(player.power).catch(() => null);
|
|
4169
|
+
if (!obj) {
|
|
4170
|
+
this.log.warn(`YouTube player ${player.name}: power state ${player.power} does not exist.`);
|
|
4171
|
+
return 'unknown';
|
|
4172
|
+
}
|
|
4173
|
+
const common = obj.common;
|
|
4174
|
+
if (common.read === false || String(common.role || '').startsWith('button')) {
|
|
4175
|
+
this.log.debug(`YouTube player ${player.name}: ${player.power} is a button, the TV state is unknown.`);
|
|
4176
|
+
return 'unknown';
|
|
4177
|
+
}
|
|
4178
|
+
const state = await this.getForeignStateAsync(player.power).catch(() => null);
|
|
4179
|
+
return (0, youtube_1.isPowerOn)(state?.val) ? 'on' : 'off';
|
|
4180
|
+
}
|
|
4181
|
+
/** Write "on" to the power state, in the type the state has. */
|
|
4182
|
+
async switchTvOn(player) {
|
|
4183
|
+
const obj = await this.getForeignObjectAsync(player.power);
|
|
4184
|
+
const type = obj?.common?.type;
|
|
4185
|
+
const val = type === 'number' ? 1 : type === 'string' ? 'on' : true;
|
|
4186
|
+
await this.setForeignStateAsync(player.power, { val, ack: false });
|
|
4187
|
+
this.journal?.stateAccess('set_state', { id: player.power, value: val });
|
|
4188
|
+
this.log.info(`YouTube: switching ${player.name} on (${player.power} = ${JSON.stringify(val)}).`);
|
|
4189
|
+
}
|
|
4190
|
+
/**
|
|
4191
|
+
* After the TV was switched on: wait until its power state confirms (an `ack` from the TV adapter — our
|
|
4192
|
+
* own write already says `true`), give the TV its boot time, then play. The YouTube app is often not
|
|
4193
|
+
* reachable over the lounge right after boot, so a command that is not acknowledged is sent once more.
|
|
4194
|
+
*/
|
|
4195
|
+
async playWhenTvIsOn(player, hit) {
|
|
4196
|
+
try {
|
|
4197
|
+
const until = Date.now() + TV_POWER_TIMEOUT_MS;
|
|
4198
|
+
let on = false;
|
|
4199
|
+
while (!on && Date.now() < until) {
|
|
4200
|
+
await this.delay(1000);
|
|
4201
|
+
const st = await this.getForeignStateAsync(player.power).catch(() => null);
|
|
4202
|
+
on = !!st?.ack && (0, youtube_1.isPowerOn)(st.val);
|
|
4203
|
+
}
|
|
4204
|
+
if (!on) {
|
|
4205
|
+
this.log.warn(`YouTube: ${player.power} did not report "on" within ${TV_POWER_TIMEOUT_MS / 1000} s — trying to play anyway.`);
|
|
4206
|
+
}
|
|
4207
|
+
await this.delay(player.delaySec * 1000);
|
|
4208
|
+
let ok = await this.sendToYouTubePlayer(player, hit.id);
|
|
4209
|
+
if (!ok) {
|
|
4210
|
+
await this.delay(YOUTUBE_RETRY_MS);
|
|
4211
|
+
ok = await this.sendToYouTubePlayer(player, hit.id);
|
|
4212
|
+
}
|
|
4213
|
+
if (ok) {
|
|
4214
|
+
this.log.info(`YouTube: playing ${hit.id} (${hit.title}) on ${player.name}.`);
|
|
4215
|
+
}
|
|
4216
|
+
else {
|
|
4217
|
+
this.log.warn(`YouTube: ${player.name} did not take ${hit.id} after power-on. If the TV needs longer to start, raise its start delay.`);
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
catch (e) {
|
|
4221
|
+
this.log.warn(`YouTube: playing on ${player.name} failed: ${e.message}`);
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
/**
|
|
4225
|
+
* Hand a video/playlist/channel id to the player and wait for its acknowledgement: youtube-remote acks
|
|
4226
|
+
* `youtube` (back to '') only once the TV took the command, and leaves it unacknowledged on failure.
|
|
4227
|
+
*/
|
|
4228
|
+
async sendToYouTubePlayer(player, id) {
|
|
4229
|
+
await this.setForeignStateAsync(player.target, { val: id, ack: false });
|
|
4230
|
+
this.journal?.stateAccess('set_state', { id: player.target, value: id });
|
|
4231
|
+
const until = Date.now() + YOUTUBE_ACK_TIMEOUT_MS;
|
|
4232
|
+
while (Date.now() < until) {
|
|
4233
|
+
await this.delay(250);
|
|
4234
|
+
if ((await this.getForeignStateAsync(player.target).catch(() => null))?.ack) {
|
|
4235
|
+
return true;
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
return false;
|
|
4239
|
+
}
|
|
4240
|
+
/** A play result as tool / sendTo data: without the internal objects, with the alternatives as titles. */
|
|
4241
|
+
youtubeResultData(res) {
|
|
4242
|
+
return {
|
|
4243
|
+
ok: res.ok,
|
|
4244
|
+
text: res.text,
|
|
4245
|
+
...(res.player ? { player: res.player } : {}),
|
|
4246
|
+
...(res.hit ? { kind: res.hit.kind, id: res.hit.id, title: res.hit.title } : {}),
|
|
4247
|
+
...(res.hit?.channel ? { channel: res.hit.channel } : {}),
|
|
4248
|
+
...(res.poweringOn ? { poweringOn: true } : {}),
|
|
4249
|
+
...(res.alternatives?.length
|
|
4250
|
+
? { otherResults: res.alternatives.map(h => ({ id: h.id, title: h.title })) }
|
|
4251
|
+
: {}),
|
|
4252
|
+
};
|
|
4253
|
+
}
|
|
4254
|
+
/**
|
|
4255
|
+
* LLM tools for the YouTube players: `play_youtube` (search + power-on + play) and `youtube_player`
|
|
4256
|
+
* (pause, skip, volume, what is playing). The configured names go into the descriptions, the model
|
|
4257
|
+
* cannot discover them otherwise.
|
|
4258
|
+
*/
|
|
4259
|
+
buildYouTubeTools() {
|
|
4260
|
+
const names = this.youtubePlayers.map((p, i) => (i === 0 ? `${p.name} (default)` : p.name)).join(', ');
|
|
4261
|
+
const playerParam = {
|
|
4262
|
+
type: 'string',
|
|
4263
|
+
description: `Player name or room: ${names}. Leave it out for the player in the room the user is in, else the default.`,
|
|
4264
|
+
};
|
|
4265
|
+
const play = {
|
|
4266
|
+
name: 'play_youtube',
|
|
4267
|
+
description: 'Play something from YouTube on the TV: finds the channel, video or playlist by name, switches ' +
|
|
4268
|
+
'the TV on if it is off and starts playing. Use this for every "play … on YouTube", ' +
|
|
4269
|
+
'"spiele den Kanal …", "zeig das Video …" — never write the player states yourself. kind ' +
|
|
4270
|
+
'"channel" plays the newest uploads of a channel. The reply text of the result can be said as is; ' +
|
|
4271
|
+
'otherResults are the next search hits, offer them if the user says it was the wrong one ' +
|
|
4272
|
+
'(pass the id as query to play one).',
|
|
4273
|
+
parameters: {
|
|
4274
|
+
type: 'object',
|
|
4275
|
+
properties: {
|
|
4276
|
+
query: {
|
|
4277
|
+
type: 'string',
|
|
4278
|
+
description: 'Name of the channel, title of the video/playlist, a search phrase, or an id/URL',
|
|
4279
|
+
},
|
|
4280
|
+
kind: {
|
|
4281
|
+
type: 'string',
|
|
4282
|
+
enum: ['channel', 'video', 'playlist'],
|
|
4283
|
+
description: 'Leave it out if the user did not say: a channel of that name wins, else videos',
|
|
4284
|
+
},
|
|
4285
|
+
player: playerParam,
|
|
4286
|
+
},
|
|
4287
|
+
required: ['query'],
|
|
4288
|
+
additionalProperties: false,
|
|
4289
|
+
},
|
|
4290
|
+
run: async (args) => {
|
|
4291
|
+
const query = typeof args.query === 'string' ? args.query.trim() : '';
|
|
4292
|
+
if (!query) {
|
|
4293
|
+
return JSON.stringify({ ok: false, error: 'no query given' });
|
|
4294
|
+
}
|
|
4295
|
+
const kind = args.kind === 'channel' || args.kind === 'playlist' || args.kind === 'video' ? args.kind : 'auto';
|
|
4296
|
+
const player = typeof args.player === 'string' ? args.player : undefined;
|
|
4297
|
+
return JSON.stringify(this.youtubeResultData(await this.playYouTube(query, kind, player)));
|
|
4298
|
+
},
|
|
4299
|
+
};
|
|
4300
|
+
const control = {
|
|
4301
|
+
name: 'youtube_player',
|
|
4302
|
+
description: 'Control a YouTube player or ask what it plays: action play (resume), pause, next, previous, ' +
|
|
4303
|
+
'volume (with "volume" 0-100), mute, unmute, or status (title, channel, play state, TV power).',
|
|
4304
|
+
parameters: {
|
|
4305
|
+
type: 'object',
|
|
4306
|
+
properties: {
|
|
4307
|
+
action: {
|
|
4308
|
+
type: 'string',
|
|
4309
|
+
enum: ['play', 'pause', 'next', 'previous', 'volume', 'mute', 'unmute', 'status'],
|
|
4310
|
+
},
|
|
4311
|
+
volume: { type: 'number', description: 'Volume 0-100, only for action volume' },
|
|
4312
|
+
player: playerParam,
|
|
4313
|
+
},
|
|
4314
|
+
required: ['action'],
|
|
4315
|
+
additionalProperties: false,
|
|
4316
|
+
},
|
|
4317
|
+
run: async (args) => {
|
|
4318
|
+
const player = this.youtubePlayerFor(typeof args.player === 'string' ? args.player : undefined);
|
|
4319
|
+
if (!player) {
|
|
4320
|
+
return JSON.stringify({ ok: false, error: `unknown player, known are: ${names}` });
|
|
4321
|
+
}
|
|
4322
|
+
if (!(await this.resolveYouTubePlayer(player))) {
|
|
4323
|
+
return JSON.stringify({ ok: false, error: `no TV is paired with ${player.instance}` });
|
|
4324
|
+
}
|
|
4325
|
+
const action = typeof args.action === 'string' ? args.action : '';
|
|
4326
|
+
if (action === 'status') {
|
|
4327
|
+
const ids = ['title', 'artist', 'state', 'connected', 'volume'].map(s => `${player.base}.${s}`);
|
|
4328
|
+
const states = await this.getForeignStatesAsync(`${player.base}.*`).catch(() => ({}));
|
|
4329
|
+
const val = (i) => states[ids[i]]?.val ?? null;
|
|
4330
|
+
const power = await this.tvPower(player);
|
|
4331
|
+
return JSON.stringify({
|
|
4332
|
+
ok: true,
|
|
4333
|
+
data: {
|
|
4334
|
+
player: player.name,
|
|
4335
|
+
title: val(0),
|
|
4336
|
+
channel: val(1),
|
|
4337
|
+
state: { 0: 'paused', 1: 'playing', 2: 'stopped' }[String(val(2))],
|
|
4338
|
+
connected: val(3),
|
|
4339
|
+
volume: val(4),
|
|
4340
|
+
...(power !== 'unknown' ? { tv: power } : {}),
|
|
4341
|
+
},
|
|
4342
|
+
});
|
|
4343
|
+
}
|
|
4344
|
+
if (!this.config.allowWriteStates) {
|
|
4345
|
+
return JSON.stringify({ ok: false, error: 'device control is disabled in the settings' });
|
|
4346
|
+
}
|
|
4347
|
+
let id;
|
|
4348
|
+
let value;
|
|
4349
|
+
if (action === 'volume') {
|
|
4350
|
+
const v = Number(args.volume);
|
|
4351
|
+
if (!Number.isFinite(v)) {
|
|
4352
|
+
return JSON.stringify({ ok: false, error: 'volume 0-100 needed' });
|
|
4353
|
+
}
|
|
4354
|
+
id = `${player.base}.volume`;
|
|
4355
|
+
value = Math.min(100, Math.max(0, Math.round(v)));
|
|
4356
|
+
}
|
|
4357
|
+
else if (action === 'mute' || action === 'unmute') {
|
|
4358
|
+
id = `${player.base}.muted`;
|
|
4359
|
+
value = action === 'mute';
|
|
4360
|
+
}
|
|
4361
|
+
else if (['play', 'pause', 'next', 'previous'].includes(action)) {
|
|
4362
|
+
id = `${player.base}.${action === 'previous' ? 'prev' : action}`;
|
|
4363
|
+
value = true;
|
|
4364
|
+
}
|
|
4365
|
+
else {
|
|
4366
|
+
return JSON.stringify({ ok: false, error: `unknown action '${action}'` });
|
|
4367
|
+
}
|
|
4368
|
+
await this.setForeignStateAsync(id, { val: value, ack: false });
|
|
4369
|
+
this.journal?.stateAccess('set_state', { id, value });
|
|
4370
|
+
return JSON.stringify({ ok: true, data: { player: player.name, [action]: value } });
|
|
4371
|
+
},
|
|
4372
|
+
};
|
|
4373
|
+
return [play, control];
|
|
4374
|
+
}
|
|
3941
4375
|
// ── Presence: who is at home (roadmap B1) ───────────────────────────────
|
|
3942
4376
|
/**
|
|
3943
4377
|
* Build the presence tracker from the configured states, subscribe to them and mirror the result into
|