iobroker.androidtv 0.1.1 → 0.2.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/remote.ts"],
4
- "sourcesContent": ["import { EventEmitter } from 'node:events';\nimport * as tls from 'node:tls';\nimport type { ClientCertificate } from './certificate';\nimport { KEY_CODES } from './keycodes';\nimport {\n Feature,\n KeyDirection,\n decodeRemoteMessage,\n encodeAppLink,\n encodeImeText,\n encodeKeyInject,\n encodePingResponse,\n encodeRemoteConfigure,\n encodeRemoteSetActive,\n type KeyDirectionValue,\n type RemoteDeviceInfo,\n type VolumeInfo,\n} from './messages';\nimport { FrameDecoder, frame } from './protobuf';\nimport { NODE_TIMERS, type TimerHandle, type Timers } from './timers';\n\n/** Default remote control port of the Android TV Remote Service. */\nexport const DEFAULT_REMOTE_PORT = 6466;\n\n/** TV pings every 5 s; three missed pings \u2192 the link is dead. */\nconst IDLE_TIMEOUT_MS = 16_000;\nconst CONNECT_TIMEOUT_MS = 5_000;\nconst RECONNECT_MIN_MS = 2_000;\n/** Consecutive TLS alerts (certificate unknown) before the device is reported as unpaired. */\nconst UNPAIRED_AFTER_ALERTS = 2;\n/** Consecutive silent closes (no message at all) before the device is reported as unpaired. */\nconst UNPAIRED_AFTER_SILENT_CLOSES = 5;\n\n/** Options for a remote connection. */\nexport interface RemoteOptions {\n /** Timer functions (the adapter passes its own so timers are cleared on unload) */\n timers?: Timers;\n /** IP address or hostname of the TV */\n host: string;\n /** TCP port of the TV service */\n port?: number;\n /** Client certificate used for mutual TLS */\n certificate: ClientCertificate;\n /** Request IME feature (needed for current app and text input). */\n enableIme: boolean;\n /** Upper bound for the reconnect backoff in ms. */\n reconnectMaxMs: number;\n /** First reconnect delay in ms (default 2000; lower values only for tests). */\n reconnectMinMs?: number;\n /** Connection is considered dead without any message for this time (default 16000; tests only). */\n idleTimeoutMs?: number;\n}\n\n/** Events emitted by {@link RemoteConnection}. */\nexport interface RemoteEvents {\n /** TV accepted the connection and is ready for commands. */\n ready: [deviceInfo: RemoteDeviceInfo, peerCertificate: Buffer | undefined];\n /** Connection lost (will reconnect automatically unless stopped). */\n disconnected: [reason: string];\n /** TLS handshake was refused - the client certificate is not paired. */\n unpaired: [];\n /** Screen on/off. */\n power: [on: boolean];\n /** Foreground app changed (package name) */\n currentApp: [appPackage: string];\n /** Volume or mute state changed */\n volume: [volume: VolumeInfo];\n /** Non-fatal problems worth a debug/warn log. */\n log: [level: 'debug' | 'warn', message: string];\n}\n\n/**\n * Persistent, self-healing connection to the remote port of one TV.\n * Event-driven: no polling, the TV pushes power/app/volume changes.\n */\nexport class RemoteConnection extends EventEmitter<RemoteEvents> {\n private socket: tls.TLSSocket | undefined;\n private readonly decoder = new FrameDecoder();\n private idleTimer: TimerHandle | undefined;\n private reconnectTimer: TimerHandle | undefined;\n private reconnectDelay: number;\n private readonly minDelay: number;\n private running = false;\n private activeFeatures: number;\n private imeCounter = 0;\n private fieldCounter = 0;\n private ready = false;\n\n /** Last known screen state, undefined until the TV reported it. */\n public isOn: boolean | undefined;\n\n /** Timer functions: injected by the adapter or plain Node timers. */\n private get timers(): Timers {\n return this.options.timers ?? NODE_TIMERS;\n }\n\n /** @param options - Connection options */\n public constructor(private readonly options: RemoteOptions) {\n super();\n this.minDelay = options.reconnectMinMs ?? RECONNECT_MIN_MS;\n this.reconnectDelay = this.minDelay;\n this.activeFeatures =\n Feature.PING |\n Feature.KEY |\n Feature.POWER |\n Feature.VOLUME |\n Feature.APP_LINK |\n (options.enableIme ? Feature.IME : 0);\n }\n\n /** Whether commands can be sent. */\n public get isReady(): boolean {\n return this.ready;\n }\n\n /** Starts connecting; reconnects automatically until {@link stop} is called. */\n public start(): void {\n if (this.running) {\n return;\n }\n this.running = true;\n this.connect();\n }\n\n /** Closes the connection and cancels reconnects. */\n public stop(): void {\n this.running = false;\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = undefined;\n this.teardown();\n }\n\n /** Drops the current connection and reconnects immediately (e.g. after a new pairing). */\n public restart(): void {\n this.stop();\n this.reconnectDelay = this.minDelay;\n this.alertFailures = 0;\n this.silentFailures = 0;\n this.unpairedReported = false;\n this.start();\n }\n\n /**\n * Sends a key press.\n *\n * @param key - Key name without `KEYCODE_` prefix (case-insensitive) or numeric key code\n * @param direction - SHORT (default), START_LONG or END_LONG\n */\n public sendKey(key: string | number, direction: KeyDirectionValue = KeyDirection.SHORT): void {\n const code = typeof key === 'number' ? key : resolveKeyCode(key);\n if (code === undefined) {\n throw new Error(`Unknown key \"${key}\"`);\n }\n this.send(encodeKeyInject(code, direction), `key ${key}`);\n }\n\n /**\n * Launches an app by deep link or package name.\n *\n * @param appLinkOrPackage - URL (e.g. https://www.netflix.com/title) or package (org.xbmc.kodi)\n */\n public launchApp(appLinkOrPackage: string): void {\n const target = appLinkOrPackage.trim();\n if (!target) {\n throw new Error('App link must not be empty');\n }\n const link = /^[a-z][a-z0-9+.-]*:/i.test(target) ? target : `market://launch?id=${target}`;\n this.send(encodeAppLink(link), `app link ${link}`);\n }\n\n /**\n * Types text into the focused input field (requires IME).\n *\n * @param text - Text\n */\n public sendText(text: string): void {\n if (!text) {\n throw new Error('Text must not be empty');\n }\n if (!(this.activeFeatures & Feature.IME)) {\n throw new Error('Text input requires the IME option');\n }\n this.send(encodeImeText(text, this.imeCounter, this.fieldCounter), 'text input');\n }\n\n private send(payload: Buffer, description: string): void {\n if (!this.ready || !this.socket || this.socket.destroyed) {\n throw new Error('Not connected to the TV');\n }\n this.lastCommand = description;\n this.socket.write(frame(payload));\n this.armIdleTimer();\n }\n\n private write(payload: Buffer): void {\n this.socket?.write(frame(payload));\n }\n\n private connect(): void {\n this.decoder.reset();\n this.receivedAny = false;\n const socket = tls.connect({\n host: this.options.host,\n port: this.options.port ?? DEFAULT_REMOTE_PORT,\n cert: this.options.certificate.cert,\n key: this.options.certificate.key,\n // @KI_HINWEIS: TV-Zertifikat ist selbstsigniert; die Gegenseite pr\u00FCft unser Client-Zertifikat (mTLS).\n rejectUnauthorized: false,\n timeout: CONNECT_TIMEOUT_MS,\n });\n this.socket = socket;\n let secured = false;\n let tlsRejected = false;\n\n socket.once('timeout', () => {\n if (!secured) {\n socket.destroy(new Error('connect timeout'));\n }\n });\n socket.once('secureConnect', () => {\n secured = true;\n this.peerCertificate = socket.getPeerCertificate(true)?.raw;\n socket.setTimeout(0);\n socket.setKeepAlive(true, 10_000);\n this.armIdleTimer();\n });\n socket.on('data', chunk => this.onData(chunk));\n socket.on('error', (err: Error & { code?: string }) => {\n if (/alert|handshake|certificate|SSL/i.test(err.message)) {\n tlsRejected = true;\n }\n this.emit(\n 'log',\n 'debug',\n `${this.options.host}: socket error: ${[err.code, err.message].filter(Boolean).join(' ')}`,\n );\n });\n socket.once('close', () => {\n // @KI_HINWEIS: Ein nicht gekoppelter Client wird abgewiesen \u2013 die SHIELD schickt dabei (auch bei TLS 1.3\n // nach dem Handshake) den Alert \u201Ecertificate unknown\u201C (Feldtest) \u2192 eindeutig. Schlie\u00DFt ein TV dagegen\n // nur still, ohne eine Nachricht zu senden (z. B. Dienst startet gerade nach dem Booten), ist das nur ein\n // schwaches Indiz und z\u00E4hlt erst nach mehreren Versuchen in Folge.\n const failure = tlsRejected ? 'alert' : secured && !this.receivedAny ? 'silent' : undefined;\n this.onClose(failure);\n });\n }\n\n private receivedAny = false;\n private lastCommand: string | undefined;\n private alertFailures = 0;\n private silentFailures = 0;\n private unpairedReported = false;\n private peerCertificate: Buffer | undefined;\n\n private onClose(failure: 'alert' | 'silent' | undefined): void {\n const wasReady = this.ready;\n this.teardown();\n if (wasReady) {\n this.emit('disconnected', 'connection closed');\n }\n if (!this.running) {\n return;\n }\n const maxDelay = Math.max(this.options.reconnectMaxMs, this.minDelay);\n this.alertFailures = failure === 'alert' ? this.alertFailures + 1 : 0;\n this.silentFailures = failure ? this.silentFailures + 1 : 0;\n let delay: number;\n if (this.alertFailures >= UNPAIRED_AFTER_ALERTS || this.silentFailures >= UNPAIRED_AFTER_SILENT_CLOSES) {\n // @KI_HINWEIS: Nicht dauerhaft aufgeben (Fehlalarm z. B. beim Neustart des TV m\u00F6glich), aber nur noch\n // im maximalen Intervall versuchen. Nach erfolgreicher Kopplung ruft der Adapter restart() auf.\n if (!this.unpairedReported) {\n this.unpairedReported = true;\n this.emit('unpaired');\n }\n delay = maxDelay;\n } else {\n delay = wasReady ? this.minDelay : this.reconnectDelay;\n this.reconnectDelay = Math.min(this.reconnectDelay * 2, maxDelay);\n }\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = undefined;\n if (this.running) {\n this.connect();\n }\n }, delay);\n }\n\n private teardown(): void {\n this.timers.clearTimeout(this.idleTimer);\n this.idleTimer = undefined;\n this.ready = false;\n if (this.socket) {\n // @KI_HINWEIS: Alle Listener entfernen, damit ein versp\u00E4tetes 'close' des alten Sockets nicht die\n // neue Verbindung abbaut (restart). Ein leerer error-Listener verhindert unhandled 'error'.\n const old = this.socket;\n this.socket = undefined;\n old.removeAllListeners();\n old.on('error', () => undefined);\n old.destroy();\n }\n }\n\n private armIdleTimer(): void {\n this.timers.clearTimeout(this.idleTimer);\n this.idleTimer = this.timers.setTimeout(() => {\n this.emit('log', 'debug', `${this.options.host}: idle timeout, reconnecting`);\n this.socket?.destroy();\n }, this.options.idleTimeoutMs ?? IDLE_TIMEOUT_MS);\n }\n\n private onData(chunk: Buffer): void {\n this.receivedAny = true;\n this.armIdleTimer();\n let frames: Buffer[];\n try {\n frames = this.decoder.push(chunk);\n } catch (e) {\n this.emit('log', 'warn', `${this.options.host}: invalid data (${(e as Error).message})`);\n this.socket?.destroy();\n return;\n }\n for (const raw of frames) {\n try {\n this.handleMessage(raw);\n } catch (e) {\n this.emit('log', 'debug', `${this.options.host}: cannot parse message: ${(e as Error).message}`);\n }\n }\n }\n\n private handleMessage(raw: Buffer): void {\n const msg = decodeRemoteMessage(raw);\n switch (msg.type) {\n case 'configure':\n this.activeFeatures &= msg.features;\n if (!(msg.features & Feature.KEY) || !(msg.features & Feature.APP_LINK)) {\n this.emit(\n 'log',\n 'warn',\n `${this.options.host}: TV does not offer key/app-link control. Clear the storage of the system app \"Android TV Remote Service\" on the TV.`,\n );\n }\n this.write(encodeRemoteConfigure(this.activeFeatures));\n this.deviceInfo = msg.deviceInfo;\n break;\n case 'setActive':\n this.write(encodeRemoteSetActive(this.activeFeatures));\n break;\n case 'start':\n if (!this.ready) {\n this.ready = true;\n this.reconnectDelay = this.minDelay;\n this.alertFailures = 0;\n this.silentFailures = 0;\n this.unpairedReported = false;\n this.emit('ready', this.deviceInfo, this.peerCertificate);\n }\n this.isOn = msg.started;\n this.emit('power', msg.started);\n break;\n case 'pingRequest':\n this.write(encodePingResponse(msg.val1));\n break;\n case 'imeKeyInject':\n if (msg.appPackage) {\n this.emit('currentApp', msg.appPackage);\n }\n break;\n case 'imeBatchEdit':\n this.imeCounter = msg.imeCounter;\n this.fieldCounter = msg.fieldCounter;\n break;\n case 'volume':\n this.emit('volume', msg.volume);\n break;\n case 'error': {\n // @KI_HINWEIS: Feldtest SHIELD (Remote-Dienst 7.00): App-Links ohne http(s)-Schema (market://,\n // android-app://, intent:) werden mit remote_error abgelehnt, danach schlie\u00DFt der TV die Verbindung\n // (Reconnect nach ~2 s). Deshalb einen konkreten Hinweis statt einer allgemeinen Meldung loggen.\n const cmd = this.lastCommand ?? 'last command';\n const hint = cmd.startsWith('app link ')\n ? ' - this device accepts only https deep links (e.g. https://www.netflix.com/title), not package names'\n : '';\n this.emit('log', 'warn', `${this.options.host}: TV rejected ${cmd}${hint}`);\n break;\n }\n default:\n this.emit('log', 'debug', `${this.options.host}: unhandled message fields ${msg.fields.join(',')}`);\n }\n }\n\n private deviceInfo: RemoteDeviceInfo = {};\n}\n\n/**\n * Resolves a key name to its code: \"home\", \"KEYCODE_HOME\", \"Dpad_Up\" and \"3\" are accepted.\n *\n * @param key - Key name or numeric string\n */\nexport function resolveKeyCode(key: string): number | undefined {\n const k = key.trim();\n if (/^\\d+$/.test(k)) {\n return Number(k);\n }\n return KEY_CODES[k.toUpperCase().replace(/^KEYCODE_/, '')];\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { EventEmitter } from 'node:events';\nimport * as tls from 'node:tls';\nimport type { ClientCertificate } from './certificate';\nimport { KEY_CODES } from './keycodes';\nimport {\n Feature,\n KeyDirection,\n decodeRemoteMessage,\n encodeAppLink,\n encodeImeText,\n encodeKeyInject,\n encodePingResponse,\n encodeRemoteConfigure,\n encodeRemoteSetActive,\n type KeyDirectionValue,\n type RemoteDeviceInfo,\n type VolumeInfo,\n} from './messages';\nimport { FrameDecoder, frame } from './protobuf';\nimport { NODE_TIMERS, type TimerHandle, type Timers } from './timers';\n\n/** Default remote control port of the Android TV Remote Service. */\nexport const DEFAULT_REMOTE_PORT = 6466;\n\n/** TV pings every 5 s; three missed pings \u2192 the link is dead. */\nconst IDLE_TIMEOUT_MS = 16_000;\nconst CONNECT_TIMEOUT_MS = 5_000;\nconst RECONNECT_MIN_MS = 2_000;\n/** Consecutive TLS alerts (certificate unknown) before the device is reported as unpaired. */\nconst UNPAIRED_AFTER_ALERTS = 2;\n/** Consecutive silent closes (no message at all) before the device is reported as unpaired. */\nconst UNPAIRED_AFTER_SILENT_CLOSES = 5;\n\n/** Options for a remote connection. */\nexport interface RemoteOptions {\n /** Timer functions (the adapter passes its own so timers are cleared on unload) */\n timers?: Timers;\n /** IP address or hostname of the TV */\n host: string;\n /** TCP port of the TV service */\n port?: number;\n /** Client certificate used for mutual TLS */\n certificate: ClientCertificate;\n /** Request IME feature (needed for current app and text input). */\n enableIme: boolean;\n /** Upper bound for the reconnect backoff in ms. */\n reconnectMaxMs: number;\n /** First reconnect delay in ms (default 2000; lower values only for tests). */\n reconnectMinMs?: number;\n /** Connection is considered dead without any message for this time (default 16000; tests only). */\n idleTimeoutMs?: number;\n}\n\n/** Events emitted by {@link RemoteConnection}. */\nexport interface RemoteEvents {\n /** TV accepted the connection and is ready for commands. */\n ready: [deviceInfo: RemoteDeviceInfo, peerCertificate: Buffer | undefined];\n /** Connection lost (will reconnect automatically unless stopped). */\n disconnected: [reason: string];\n /** TLS handshake was refused - the client certificate is not paired. */\n unpaired: [];\n /** Screen on/off. */\n power: [on: boolean];\n /** Foreground app changed (package name) */\n currentApp: [appPackage: string];\n /** Volume or mute state changed */\n volume: [volume: VolumeInfo];\n /** Non-fatal problems worth a debug/warn log. */\n log: [level: 'debug' | 'warn', message: string];\n}\n\n/**\n * Persistent, self-healing connection to the remote port of one TV.\n * Event-driven: no polling, the TV pushes power/app/volume changes.\n */\nexport class RemoteConnection extends EventEmitter<RemoteEvents> {\n private socket: tls.TLSSocket | undefined;\n private readonly decoder = new FrameDecoder();\n private idleTimer: TimerHandle | undefined;\n private reconnectTimer: TimerHandle | undefined;\n private reconnectDelay: number;\n private readonly minDelay: number;\n private running = false;\n private activeFeatures: number;\n private imeCounter = 0;\n private fieldCounter = 0;\n private ready = false;\n\n /** Last known screen state, undefined until the TV reported it. */\n public isOn: boolean | undefined;\n\n /** Timer functions: injected by the adapter or plain Node timers. */\n private get timers(): Timers {\n return this.options.timers ?? NODE_TIMERS;\n }\n\n /** @param options - Connection options */\n public constructor(private readonly options: RemoteOptions) {\n super();\n this.minDelay = options.reconnectMinMs ?? RECONNECT_MIN_MS;\n this.reconnectDelay = this.minDelay;\n this.activeFeatures =\n Feature.PING |\n Feature.KEY |\n Feature.POWER |\n Feature.VOLUME |\n Feature.APP_LINK |\n (options.enableIme ? Feature.IME : 0);\n }\n\n /** Whether commands can be sent. */\n public get isReady(): boolean {\n return this.ready;\n }\n\n /** Starts connecting; reconnects automatically until {@link stop} is called. */\n public start(): void {\n if (this.running) {\n return;\n }\n this.running = true;\n this.connect();\n }\n\n /** Closes the connection and cancels reconnects. */\n public stop(): void {\n this.running = false;\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = undefined;\n this.teardown();\n }\n\n /** Retries a pending reconnect immediately with the shortest backoff (e.g. after Wake-on-LAN). */\n public reconnectNow(): void {\n if (!this.running || this.ready || !this.reconnectTimer) {\n return;\n }\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = undefined;\n this.reconnectDelay = this.minDelay;\n this.connect();\n }\n\n /** Drops the current connection and reconnects immediately (e.g. after a new pairing). */\n public restart(): void {\n this.stop();\n this.reconnectDelay = this.minDelay;\n this.alertFailures = 0;\n this.silentFailures = 0;\n this.unpairedReported = false;\n this.start();\n }\n\n /**\n * Sends a key press.\n *\n * @param key - Key name without `KEYCODE_` prefix (case-insensitive) or numeric key code\n * @param direction - SHORT (default), START_LONG or END_LONG\n */\n public sendKey(key: string | number, direction: KeyDirectionValue = KeyDirection.SHORT): void {\n const code = typeof key === 'number' ? key : resolveKeyCode(key);\n if (code === undefined) {\n throw new Error(`Unknown key \"${key}\"`);\n }\n this.send(encodeKeyInject(code, direction), `key ${key}`);\n }\n\n /**\n * Launches an app by deep link or package name.\n *\n * @param appLinkOrPackage - URL (e.g. https://www.netflix.com/title) or package (org.xbmc.kodi)\n */\n public launchApp(appLinkOrPackage: string): void {\n const target = appLinkOrPackage.trim();\n if (!target) {\n throw new Error('App link must not be empty');\n }\n const link = /^[a-z][a-z0-9+.-]*:/i.test(target) ? target : `market://launch?id=${target}`;\n this.send(encodeAppLink(link), `app link ${link}`);\n }\n\n /**\n * Types text into the focused input field (requires IME).\n *\n * @param text - Text\n */\n public sendText(text: string): void {\n if (!text) {\n throw new Error('Text must not be empty');\n }\n if (!(this.activeFeatures & Feature.IME)) {\n throw new Error('Text input requires the IME option');\n }\n this.send(encodeImeText(text, this.imeCounter, this.fieldCounter), 'text input');\n }\n\n private send(payload: Buffer, description: string): void {\n if (!this.ready || !this.socket || this.socket.destroyed) {\n throw new Error('Not connected to the TV');\n }\n this.lastCommand = description;\n this.socket.write(frame(payload));\n this.armIdleTimer();\n }\n\n private write(payload: Buffer): void {\n this.socket?.write(frame(payload));\n }\n\n private connect(): void {\n this.decoder.reset();\n this.receivedAny = false;\n const socket = tls.connect({\n host: this.options.host,\n port: this.options.port ?? DEFAULT_REMOTE_PORT,\n cert: this.options.certificate.cert,\n key: this.options.certificate.key,\n // @KI_HINWEIS: TV-Zertifikat ist selbstsigniert; die Gegenseite pr\u00FCft unser Client-Zertifikat (mTLS).\n rejectUnauthorized: false,\n timeout: CONNECT_TIMEOUT_MS,\n });\n this.socket = socket;\n let secured = false;\n let tlsRejected = false;\n\n socket.once('timeout', () => {\n if (!secured) {\n socket.destroy(new Error('connect timeout'));\n }\n });\n socket.once('secureConnect', () => {\n secured = true;\n this.peerCertificate = socket.getPeerCertificate(true)?.raw;\n socket.setTimeout(0);\n socket.setKeepAlive(true, 10_000);\n this.armIdleTimer();\n });\n socket.on('data', chunk => this.onData(chunk));\n socket.on('error', (err: Error & { code?: string }) => {\n if (/alert|handshake|certificate|SSL/i.test(err.message)) {\n tlsRejected = true;\n }\n this.emit(\n 'log',\n 'debug',\n `${this.options.host}: socket error: ${[err.code, err.message].filter(Boolean).join(' ')}`,\n );\n });\n socket.once('close', () => {\n // @KI_HINWEIS: Ein nicht gekoppelter Client wird abgewiesen \u2013 die SHIELD schickt dabei (auch bei TLS 1.3\n // nach dem Handshake) den Alert \u201Ecertificate unknown\u201C (Feldtest) \u2192 eindeutig. Schlie\u00DFt ein TV dagegen\n // nur still, ohne eine Nachricht zu senden (z. B. Dienst startet gerade nach dem Booten), ist das nur ein\n // schwaches Indiz und z\u00E4hlt erst nach mehreren Versuchen in Folge.\n const failure = tlsRejected ? 'alert' : secured && !this.receivedAny ? 'silent' : undefined;\n this.onClose(failure);\n });\n }\n\n private receivedAny = false;\n private lastCommand: string | undefined;\n private alertFailures = 0;\n private silentFailures = 0;\n private unpairedReported = false;\n private peerCertificate: Buffer | undefined;\n\n private onClose(failure: 'alert' | 'silent' | undefined): void {\n const wasReady = this.ready;\n this.teardown();\n if (wasReady) {\n this.emit('disconnected', 'connection closed');\n }\n if (!this.running) {\n return;\n }\n const maxDelay = Math.max(this.options.reconnectMaxMs, this.minDelay);\n this.alertFailures = failure === 'alert' ? this.alertFailures + 1 : 0;\n this.silentFailures = failure ? this.silentFailures + 1 : 0;\n let delay: number;\n if (this.alertFailures >= UNPAIRED_AFTER_ALERTS || this.silentFailures >= UNPAIRED_AFTER_SILENT_CLOSES) {\n // @KI_HINWEIS: Nicht dauerhaft aufgeben (Fehlalarm z. B. beim Neustart des TV m\u00F6glich), aber nur noch\n // im maximalen Intervall versuchen. Nach erfolgreicher Kopplung ruft der Adapter restart() auf.\n if (!this.unpairedReported) {\n this.unpairedReported = true;\n this.emit('unpaired');\n }\n delay = maxDelay;\n } else {\n delay = wasReady ? this.minDelay : this.reconnectDelay;\n this.reconnectDelay = Math.min(this.reconnectDelay * 2, maxDelay);\n }\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = undefined;\n if (this.running) {\n this.connect();\n }\n }, delay);\n }\n\n private teardown(): void {\n this.timers.clearTimeout(this.idleTimer);\n this.idleTimer = undefined;\n this.ready = false;\n if (this.socket) {\n // @KI_HINWEIS: Alle Listener entfernen, damit ein versp\u00E4tetes 'close' des alten Sockets nicht die\n // neue Verbindung abbaut (restart). Ein leerer error-Listener verhindert unhandled 'error'.\n const old = this.socket;\n this.socket = undefined;\n old.removeAllListeners();\n old.on('error', () => undefined);\n old.destroy();\n }\n }\n\n private armIdleTimer(): void {\n this.timers.clearTimeout(this.idleTimer);\n this.idleTimer = this.timers.setTimeout(() => {\n this.emit('log', 'debug', `${this.options.host}: idle timeout, reconnecting`);\n this.socket?.destroy();\n }, this.options.idleTimeoutMs ?? IDLE_TIMEOUT_MS);\n }\n\n private onData(chunk: Buffer): void {\n this.receivedAny = true;\n this.armIdleTimer();\n let frames: Buffer[];\n try {\n frames = this.decoder.push(chunk);\n } catch (e) {\n this.emit('log', 'warn', `${this.options.host}: invalid data (${(e as Error).message})`);\n this.socket?.destroy();\n return;\n }\n for (const raw of frames) {\n try {\n this.handleMessage(raw);\n } catch (e) {\n this.emit('log', 'debug', `${this.options.host}: cannot parse message: ${(e as Error).message}`);\n }\n }\n }\n\n private handleMessage(raw: Buffer): void {\n const msg = decodeRemoteMessage(raw);\n switch (msg.type) {\n case 'configure':\n this.activeFeatures &= msg.features;\n if (!(msg.features & Feature.KEY) || !(msg.features & Feature.APP_LINK)) {\n this.emit(\n 'log',\n 'warn',\n `${this.options.host}: TV does not offer key/app-link control. Clear the storage of the system app \"Android TV Remote Service\" on the TV.`,\n );\n }\n this.write(encodeRemoteConfigure(this.activeFeatures));\n this.deviceInfo = msg.deviceInfo;\n break;\n case 'setActive':\n this.write(encodeRemoteSetActive(this.activeFeatures));\n break;\n case 'start':\n if (!this.ready) {\n this.ready = true;\n this.reconnectDelay = this.minDelay;\n this.alertFailures = 0;\n this.silentFailures = 0;\n this.unpairedReported = false;\n this.emit('ready', this.deviceInfo, this.peerCertificate);\n }\n this.isOn = msg.started;\n this.emit('power', msg.started);\n break;\n case 'pingRequest':\n this.write(encodePingResponse(msg.val1));\n break;\n case 'imeKeyInject':\n if (msg.appPackage) {\n this.emit('currentApp', msg.appPackage);\n }\n break;\n case 'imeBatchEdit':\n this.imeCounter = msg.imeCounter;\n this.fieldCounter = msg.fieldCounter;\n break;\n case 'volume':\n this.emit('volume', msg.volume);\n break;\n case 'error': {\n // @KI_HINWEIS: Feldtest SHIELD (Remote-Dienst 7.00): App-Links ohne http(s)-Schema (market://,\n // android-app://, intent:) werden mit remote_error abgelehnt, danach schlie\u00DFt der TV die Verbindung\n // (Reconnect nach ~2 s). Deshalb einen konkreten Hinweis statt einer allgemeinen Meldung loggen.\n const cmd = this.lastCommand ?? 'last command';\n const hint = cmd.startsWith('app link ')\n ? ' - this device accepts only https deep links (e.g. https://www.netflix.com/title), not package names'\n : '';\n this.emit('log', 'warn', `${this.options.host}: TV rejected ${cmd}${hint}`);\n break;\n }\n default:\n this.emit('log', 'debug', `${this.options.host}: unhandled message fields ${msg.fields.join(',')}`);\n }\n }\n\n private deviceInfo: RemoteDeviceInfo = {};\n}\n\n/**\n * Resolves a key name to its code: \"home\", \"KEYCODE_HOME\", \"Dpad_Up\" and \"3\" are accepted.\n *\n * @param key - Key name or numeric string\n */\nexport function resolveKeyCode(key: string): number | undefined {\n const k = key.trim();\n if (/^\\d+$/.test(k)) {\n return Number(k);\n }\n return KEY_CODES[k.toUpperCase().replace(/^KEYCODE_/, '')];\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["_a"]
7
7
  }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var wol_exports = {};
20
+ __export(wol_exports, {
21
+ WOL_PORT: () => WOL_PORT,
22
+ magicPacket: () => magicPacket,
23
+ parseMac: () => parseMac,
24
+ sendWakeOnLan: () => sendWakeOnLan,
25
+ wolTargets: () => wolTargets
26
+ });
27
+ module.exports = __toCommonJS(wol_exports);
28
+ var import_node_dgram = require("node:dgram");
29
+ var import_node_net = require("node:net");
30
+ const WOL_PORT = 9;
31
+ function parseMac(mac) {
32
+ const hex = (mac != null ? mac : "").trim().replace(/[:\-.\s]/g, "");
33
+ if (!/^[0-9a-f]{12}$/i.test(hex) || /^0{12}$/.test(hex)) {
34
+ return void 0;
35
+ }
36
+ return Buffer.from(hex, "hex");
37
+ }
38
+ function magicPacket(mac) {
39
+ return Buffer.concat([Buffer.alloc(6, 255), ...Array.from({ length: 16 }, () => mac)]);
40
+ }
41
+ function wolTargets(host, broadcast) {
42
+ const targets = /* @__PURE__ */ new Set();
43
+ const configured = broadcast == null ? void 0 : broadcast.trim();
44
+ if (configured && ((0, import_node_net.isIPv4)(configured) || (0, import_node_net.isIPv6)(configured))) {
45
+ targets.add(configured);
46
+ }
47
+ if ((0, import_node_net.isIPv4)(host)) {
48
+ targets.add("255.255.255.255");
49
+ targets.add(host.replace(/\.\d+$/, ".255"));
50
+ targets.add(host);
51
+ } else if ((0, import_node_net.isIPv6)(host)) {
52
+ targets.add(host);
53
+ }
54
+ return [...targets];
55
+ }
56
+ async function sendWakeOnLan(mac, targets, port = WOL_PORT) {
57
+ const bytes = parseMac(mac);
58
+ if (!bytes) {
59
+ throw new Error(`Invalid MAC address "${mac}"`);
60
+ }
61
+ const packet = magicPacket(bytes);
62
+ let sent = 0;
63
+ for (const family of ["udp4", "udp6"]) {
64
+ const list = targets.filter((t) => family === "udp4" ? (0, import_node_net.isIPv4)(t) : (0, import_node_net.isIPv6)(t));
65
+ if (!list.length) {
66
+ continue;
67
+ }
68
+ const socket = (0, import_node_dgram.createSocket)(family);
69
+ try {
70
+ await new Promise((resolve, reject) => {
71
+ socket.once("error", reject);
72
+ socket.bind(0, () => {
73
+ socket.off("error", reject);
74
+ resolve();
75
+ });
76
+ });
77
+ if (family === "udp4") {
78
+ socket.setBroadcast(true);
79
+ }
80
+ for (let round = 0; round < 3; round++) {
81
+ for (const target of list) {
82
+ try {
83
+ await new Promise(
84
+ (resolve, reject) => socket.send(packet, port, target, (err) => err ? reject(err) : resolve())
85
+ );
86
+ sent++;
87
+ } catch {
88
+ }
89
+ }
90
+ }
91
+ } finally {
92
+ socket.close();
93
+ }
94
+ }
95
+ return sent;
96
+ }
97
+ // Annotate the CommonJS export names for ESM import in node:
98
+ 0 && (module.exports = {
99
+ WOL_PORT,
100
+ magicPacket,
101
+ parseMac,
102
+ sendWakeOnLan,
103
+ wolTargets
104
+ });
105
+ //# sourceMappingURL=wol.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/wol.ts"],
4
+ "sourcesContent": ["/**\n * Wake-on-LAN: wakes devices that switched off their network connection in deep standby\n * (e.g. Fire TV after a while), so neither Remote v2 nor ADB can reach them anymore.\n */\nimport { createSocket } from 'node:dgram';\nimport { isIPv4, isIPv6 } from 'node:net';\n\n/** Default UDP port for magic packets. */\nexport const WOL_PORT = 9;\n\n/**\n * Parses a MAC address (\"aa:bb:cc:dd:ee:ff\", \"aa-bb-\u2026\" or \"aabbccddeeff\").\n *\n * @param mac - MAC address as text\n * @returns 6 bytes or undefined if invalid\n */\nexport function parseMac(mac: string | null | undefined): Buffer | undefined {\n const hex = (mac ?? '').trim().replace(/[:\\-.\\s]/g, '');\n if (!/^[0-9a-f]{12}$/i.test(hex) || /^0{12}$/.test(hex)) {\n return undefined;\n }\n return Buffer.from(hex, 'hex');\n}\n\n/**\n * Builds the magic packet: 6 \u00D7 0xFF followed by the MAC 16 times.\n *\n * @param mac - 6 byte MAC address\n */\nexport function magicPacket(mac: Buffer): Buffer {\n return Buffer.concat([Buffer.alloc(6, 0xff), ...Array.from({ length: 16 }, () => mac)]);\n}\n\n// @KI_HINWEIS: Ein Broadcast verl\u00E4sst das eigene Netz nicht. Liegt der TV in einem anderen Subnetz/VLAN als\n// ioBroker (Feldtest: TVs in 192.168.20.0/24), helfen nur ein gerichteter Broadcast (Router muss ihn weiterleiten)\n// oder Unicast an die TV-IP, solange der Router den ARP-Eintrag noch kennt. Deshalb alle drei Ziele senden.\n// IPv6 kennt keinen Broadcast: dort nur Unicast an die Adresse des Ger\u00E4ts.\n/**\n * Destinations for the magic packet.\n *\n * @param host - IP address of the device\n * @param broadcast - Optional broadcast address from the configuration\n */\nexport function wolTargets(host: string, broadcast?: string): string[] {\n const targets = new Set<string>();\n const configured = broadcast?.trim();\n if (configured && (isIPv4(configured) || isIPv6(configured))) {\n targets.add(configured);\n }\n if (isIPv4(host)) {\n targets.add('255.255.255.255');\n targets.add(host.replace(/\\.\\d+$/, '.255'));\n targets.add(host);\n } else if (isIPv6(host)) {\n targets.add(host);\n }\n return [...targets];\n}\n\n/**\n * Sends the magic packet to all destinations (each 3 times, UDP is lossy).\n *\n * @param mac - MAC address as text\n * @param targets - Destination addresses, see {@link wolTargets}\n * @param port - UDP port\n * @returns Number of packets sent\n */\nexport async function sendWakeOnLan(mac: string, targets: string[], port = WOL_PORT): Promise<number> {\n const bytes = parseMac(mac);\n if (!bytes) {\n throw new Error(`Invalid MAC address \"${mac}\"`);\n }\n const packet = magicPacket(bytes);\n let sent = 0;\n for (const family of ['udp4', 'udp6'] as const) {\n const list = targets.filter(t => (family === 'udp4' ? isIPv4(t) : isIPv6(t)));\n if (!list.length) {\n continue;\n }\n const socket = createSocket(family);\n try {\n await new Promise<void>((resolve, reject) => {\n socket.once('error', reject);\n socket.bind(0, () => {\n socket.off('error', reject);\n resolve();\n });\n });\n if (family === 'udp4') {\n socket.setBroadcast(true);\n }\n for (let round = 0; round < 3; round++) {\n for (const target of list) {\n try {\n await new Promise<void>((resolve, reject) =>\n socket.send(packet, port, target, err => (err ? reject(err) : resolve())),\n );\n sent++;\n } catch {\n // @KI_HINWEIS: Einzelne Ziele d\u00FCrfen scheitern (z. B. EACCES/ENETUNREACH f\u00FCr ein Netz),\n // die anderen werden trotzdem versucht.\n }\n }\n }\n } finally {\n socket.close();\n }\n }\n return sent;\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAIA,wBAA6B;AAC7B,sBAA+B;AAGxB,MAAM,WAAW;AAQjB,SAAS,SAAS,KAAoD;AACzE,QAAM,OAAO,oBAAO,IAAI,KAAK,EAAE,QAAQ,aAAa,EAAE;AACtD,MAAI,CAAC,kBAAkB,KAAK,GAAG,KAAK,UAAU,KAAK,GAAG,GAAG;AACrD,WAAO;AAAA,EACX;AACA,SAAO,OAAO,KAAK,KAAK,KAAK;AACjC;AAOO,SAAS,YAAY,KAAqB;AAC7C,SAAO,OAAO,OAAO,CAAC,OAAO,MAAM,GAAG,GAAI,GAAG,GAAG,MAAM,KAAK,EAAE,QAAQ,GAAG,GAAG,MAAM,GAAG,CAAC,CAAC;AAC1F;AAYO,SAAS,WAAW,MAAc,WAA8B;AACnE,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,aAAa,uCAAW;AAC9B,MAAI,mBAAe,wBAAO,UAAU,SAAK,wBAAO,UAAU,IAAI;AAC1D,YAAQ,IAAI,UAAU;AAAA,EAC1B;AACA,UAAI,wBAAO,IAAI,GAAG;AACd,YAAQ,IAAI,iBAAiB;AAC7B,YAAQ,IAAI,KAAK,QAAQ,UAAU,MAAM,CAAC;AAC1C,YAAQ,IAAI,IAAI;AAAA,EACpB,eAAW,wBAAO,IAAI,GAAG;AACrB,YAAQ,IAAI,IAAI;AAAA,EACpB;AACA,SAAO,CAAC,GAAG,OAAO;AACtB;AAUA,eAAsB,cAAc,KAAa,SAAmB,OAAO,UAA2B;AAClG,QAAM,QAAQ,SAAS,GAAG;AAC1B,MAAI,CAAC,OAAO;AACR,UAAM,IAAI,MAAM,wBAAwB,GAAG,GAAG;AAAA,EAClD;AACA,QAAM,SAAS,YAAY,KAAK;AAChC,MAAI,OAAO;AACX,aAAW,UAAU,CAAC,QAAQ,MAAM,GAAY;AAC5C,UAAM,OAAO,QAAQ,OAAO,OAAM,WAAW,aAAS,wBAAO,CAAC,QAAI,wBAAO,CAAC,CAAE;AAC5E,QAAI,CAAC,KAAK,QAAQ;AACd;AAAA,IACJ;AACA,UAAM,aAAS,gCAAa,MAAM;AAClC,QAAI;AACA,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AACzC,eAAO,KAAK,SAAS,MAAM;AAC3B,eAAO,KAAK,GAAG,MAAM;AACjB,iBAAO,IAAI,SAAS,MAAM;AAC1B,kBAAQ;AAAA,QACZ,CAAC;AAAA,MACL,CAAC;AACD,UAAI,WAAW,QAAQ;AACnB,eAAO,aAAa,IAAI;AAAA,MAC5B;AACA,eAAS,QAAQ,GAAG,QAAQ,GAAG,SAAS;AACpC,mBAAW,UAAU,MAAM;AACvB,cAAI;AACA,kBAAM,IAAI;AAAA,cAAc,CAAC,SAAS,WAC9B,OAAO,KAAK,QAAQ,MAAM,QAAQ,SAAQ,MAAM,OAAO,GAAG,IAAI,QAAQ,CAAE;AAAA,YAC5E;AACA;AAAA,UACJ,QAAQ;AAAA,UAGR;AAAA,QACJ;AAAA,MACJ;AAAA,IACJ,UAAE;AACE,aAAO,MAAM;AAAA,IACjB;AAAA,EACJ;AACA,SAAO;AACX;",
6
+ "names": []
7
+ }
package/build/main.js CHANGED
@@ -29,6 +29,7 @@ var import_certificate = require("./lib/certificate");
29
29
  var import_pairing = require("./lib/pairing");
30
30
  var import_remote = require("./lib/remote");
31
31
  var import_states = require("./lib/states");
32
+ var import_wol = require("./lib/wol");
32
33
  const PAIRING_SESSION_TTL_MS = 12e4;
33
34
  const DEFAULT_RECONNECT_MAX_S = 60;
34
35
  const DEFAULT_ADB_POLL_S = 5;
@@ -408,6 +409,16 @@ class Androidtv extends utils.Adapter {
408
409
  void this.setDeviceStates(id, values);
409
410
  void this.updateConnectionState();
410
411
  void this.refreshStatus(runtime);
412
+ adb.findWakeSettings().then(
413
+ (found) => this.log.debug(`${label}: wake settings: ${found.length ? found.join(" | ") : "none found"}`)
414
+ ).catch(() => void 0);
415
+ if (runtime.wakePendingUntil && Date.now() < runtime.wakePendingUntil) {
416
+ runtime.wakePendingUntil = void 0;
417
+ runtime.powerTarget = { value: true, until: Date.now() + 5e3 };
418
+ adb.setPower(true).catch(
419
+ (e) => this.log.debug(`${label}: switching on after Wake-on-LAN failed: ${e.message}`)
420
+ );
421
+ }
411
422
  });
412
423
  adb.on("authPending", () => {
413
424
  var _a;
@@ -743,10 +754,35 @@ class Androidtv extends utils.Adapter {
743
754
  } else if ((_b = device.adb) == null ? void 0 : _b.isReady) {
744
755
  device.powerTarget = { value: wanted, until: Date.now() + 5e3 };
745
756
  await device.adb.setPower(wanted);
757
+ } else if (wanted) {
758
+ await this.wakeOnLan(device);
746
759
  } else {
747
760
  throw new Error("not connected");
748
761
  }
749
762
  }
763
+ /**
764
+ * Wakes an unreachable device (deep standby without network) with a magic packet and reconnects right away.
765
+ *
766
+ * @param device - Device
767
+ */
768
+ async wakeOnLan(device) {
769
+ var _a, _b, _c;
770
+ const mac = (_a = device.config.wolMac) == null ? void 0 : _a.trim();
771
+ if (!mac) {
772
+ throw new Error(
773
+ "not connected. To switch it on from deep standby, enter its MAC address in the instance settings (Wake-on-LAN)"
774
+ );
775
+ }
776
+ const targets = (0, import_wol.wolTargets)(device.config.host.trim(), device.config.wolBroadcast);
777
+ const sent = await (0, import_wol.sendWakeOnLan)(mac, targets);
778
+ if (!sent) {
779
+ throw new Error(`Wake-on-LAN could not be sent (${targets.join(", ")})`);
780
+ }
781
+ this.log.info(`${device.label}: not reachable, sent Wake-on-LAN to ${mac}`);
782
+ device.wakePendingUntil = Date.now() + 9e4;
783
+ (_b = device.remote) == null ? void 0 : _b.reconnectNow();
784
+ (_c = device.adb) == null ? void 0 : _c.reconnectNow();
785
+ }
750
786
  // ------------------------------------------------------------------------------------------------------------
751
787
  // Admin messages (pairing, status)
752
788
  // ------------------------------------------------------------------------------------------------------------