iobroker.androidtv 0.1.2 → 0.2.1
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/LICENSE +1 -1
- package/README.md +12 -2
- package/admin/i18n/de.json +6 -1
- package/admin/i18n/en.json +6 -1
- package/admin/i18n/es.json +6 -1
- package/admin/i18n/fr.json +6 -1
- package/admin/i18n/it.json +6 -1
- package/admin/i18n/nl.json +6 -1
- package/admin/i18n/pl.json +6 -1
- package/admin/i18n/pt.json +6 -1
- package/admin/i18n/ru.json +6 -1
- package/admin/i18n/uk.json +6 -1
- package/admin/i18n/zh-cn.json +6 -1
- package/admin/jsonConfig.json +30 -2
- package/build/lib/adb/client.js +10 -0
- package/build/lib/adb/client.js.map +2 -2
- package/build/lib/adb/device.js +15 -0
- package/build/lib/adb/device.js.map +2 -2
- package/build/lib/remote.js +10 -0
- package/build/lib/remote.js.map +2 -2
- package/build/lib/states.js +2 -2
- package/build/lib/states.js.map +2 -2
- package/build/lib/wol.js +105 -0
- package/build/lib/wol.js.map +7 -0
- package/build/main.js +36 -0
- package/build/main.js.map +2 -2
- package/io-package.json +28 -2
- package/package.json +2 -2
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 Schimi <
|
|
3
|
+
Copyright (c) 2026 Schimi <github@world-mg.de>
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ Tip: a link can also open a specific video, e.g. `https://www.youtube.com/watch?
|
|
|
108
108
|
- **"The device cannot be reached for pairing"**: check the IP address and that the device is on. A Fire TV cannot be paired – choose the device type *Amazon Fire TV*.
|
|
109
109
|
- **Status "Please confirm Allow debugging on the TV"**: confirm the question on the TV screen and tick *Always allow*.
|
|
110
110
|
- **Status "Not connected"**: the device is off, unplugged or not reachable. The adapter reconnects automatically as soon as it is back.
|
|
111
|
-
- **
|
|
111
|
+
- **The device cannot be switched on from ioBroker after a while**: in deep standby some devices (e.g. Fire TV) switch off their network connection. The adapter can then wake them with Wake-on-LAN: switch on *Wake on LAN* (or *network standby*) on the device and enter its **MAC address** in the device entry of the instance settings. You find it in the network settings of the device or in your router; with a cable connection use the MAC of the LAN port (devices like the SHIELD have a different one for Wi-Fi). If ioBroker and the TV are in different networks and it does not work, enter the broadcast address of the TV network in the expert settings (e.g. `192.168.20.255`).
|
|
112
112
|
- **The TV keeps showing "Use keyboard on mobile device"**: switch on the expert mode in the instance settings and untick *Current app & text input* for this device.
|
|
113
113
|
- **An app button does nothing**: some apps cannot be started by app ID on a SHIELD without playback info. Use the web link or choose *+ playback info*.
|
|
114
114
|
- **Volume shows nothing**: if the volume is controlled by your TV or AV receiver (HDMI-CEC), the device cannot report it.
|
|
@@ -183,6 +183,16 @@ Bug reports and ideas in the [issues](https://github.com/Schimi1983/ioBroker.and
|
|
|
183
183
|
Placeholder for the next version (at the beginning of the line):
|
|
184
184
|
### **WORK IN PROGRESS**
|
|
185
185
|
-->
|
|
186
|
+
### 0.2.1 (2026-10-31)
|
|
187
|
+
|
|
188
|
+
- (Schimi) Clearer error text for an invalid MAC address, translated into all languages
|
|
189
|
+
- (Schimi) Fast forward and rewind buttons use the official roles `button.fastforward` / `button.fastreverse`
|
|
190
|
+
|
|
191
|
+
### 0.2.0 (2026-10-29)
|
|
192
|
+
|
|
193
|
+
- (Schimi) Wake-on-LAN: `power` = on also switches on devices in deep standby without network (enter the MAC address in the device settings; optional broadcast address in the expert settings)
|
|
194
|
+
- (Schimi) Faster reconnect after Wake-on-LAN
|
|
195
|
+
|
|
186
196
|
### 0.1.2 (2026-10-29)
|
|
187
197
|
|
|
188
198
|
- (Schimi) Repository checker: removed news entry of the unpublished version 0.0.1, added CHANGELOG_OLD.md
|
|
@@ -203,7 +213,7 @@ Bug reports and ideas in the [issues](https://github.com/Schimi1983/ioBroker.and
|
|
|
203
213
|
|
|
204
214
|
MIT License
|
|
205
215
|
|
|
206
|
-
Copyright (c) 2026 Schimi <
|
|
216
|
+
Copyright (c) 2026 Schimi <github@world-mg.de>
|
|
207
217
|
|
|
208
218
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
209
219
|
of this software and associated documentation files (the "Software"), to deal
|
package/admin/i18n/de.json
CHANGED
|
@@ -35,5 +35,10 @@
|
|
|
35
35
|
"header_support": "Entwicklung unterstützen",
|
|
36
36
|
"support_text": "Der Adapter ist kostenlos. Wenn er dir nützt, kannst du die Entwicklung freiwillig unterstützen.",
|
|
37
37
|
"link_sponsor": "GitHub Sponsors",
|
|
38
|
-
"link_paypal": "Per PayPal spenden"
|
|
38
|
+
"link_paypal": "Per PayPal spenden",
|
|
39
|
+
"MAC address (Wake-on-LAN)": "MAC-Adresse (Wake-on-LAN)",
|
|
40
|
+
"wolMacHelp": "Optional, nur nötig zum Einschalten aus dem tiefen Standby. Steht in den Netzwerkeinstellungen des Geräts oder im Router (bei Kabel: die MAC der LAN-Schnittstelle)",
|
|
41
|
+
"Wake-on-LAN broadcast address": "Wake-on-LAN-Broadcast-Adresse",
|
|
42
|
+
"wolBroadcastHelp": "Nur nötig, wenn das Gerät in einem anderen Netz ist (z. B. 192.168.20.255)",
|
|
43
|
+
"wolMacInvalid": "Ungültige MAC-Adresse. Format: aa:bb:cc:dd:ee:ff"
|
|
39
44
|
}
|
package/admin/i18n/en.json
CHANGED
|
@@ -35,5 +35,10 @@
|
|
|
35
35
|
"header_support": "Support the development",
|
|
36
36
|
"support_text": "The adapter is free. If it is useful to you, you can support its development voluntarily.",
|
|
37
37
|
"link_sponsor": "GitHub Sponsors",
|
|
38
|
-
"link_paypal": "Donate via PayPal"
|
|
38
|
+
"link_paypal": "Donate via PayPal",
|
|
39
|
+
"MAC address (Wake-on-LAN)": "MAC address (Wake-on-LAN)",
|
|
40
|
+
"wolMacHelp": "Optional, only needed to switch the device on from deep standby. Shown in the network settings of the device or in your router (with a cable: the MAC of the LAN port)",
|
|
41
|
+
"Wake-on-LAN broadcast address": "Wake-on-LAN broadcast address",
|
|
42
|
+
"wolBroadcastHelp": "Only needed if the device is in another network (e.g. 192.168.20.255)",
|
|
43
|
+
"wolMacInvalid": "Invalid MAC address. Format: aa:bb:cc:dd:ee:ff"
|
|
39
44
|
}
|
package/admin/i18n/es.json
CHANGED
|
@@ -35,5 +35,10 @@
|
|
|
35
35
|
"header_support": "Apoyar el desarrollo",
|
|
36
36
|
"support_text": "El adaptador es gratuito. Si te resulta útil, puedes apoyar su desarrollo de forma voluntaria.",
|
|
37
37
|
"link_sponsor": "GitHub Sponsors",
|
|
38
|
-
"link_paypal": "Donar con PayPal"
|
|
38
|
+
"link_paypal": "Donar con PayPal",
|
|
39
|
+
"MAC address (Wake-on-LAN)": "Dirección MAC (Wake-on-LAN)",
|
|
40
|
+
"wolMacHelp": "Opcional, solo necesario para encender el dispositivo desde el modo de espera profundo. Aparece en los ajustes de red del dispositivo o en el router (con cable: la MAC del puerto LAN)",
|
|
41
|
+
"Wake-on-LAN broadcast address": "Dirección de difusión Wake-on-LAN",
|
|
42
|
+
"wolBroadcastHelp": "Solo si el dispositivo está en otra red (p. ej. 192.168.20.255)",
|
|
43
|
+
"wolMacInvalid": "Dirección MAC no válida. Formato: aa:bb:cc:dd:ee:ff"
|
|
39
44
|
}
|
package/admin/i18n/fr.json
CHANGED
|
@@ -35,5 +35,10 @@
|
|
|
35
35
|
"header_support": "Soutenir le développement",
|
|
36
36
|
"support_text": "L'adaptateur est gratuit. S'il vous est utile, vous pouvez soutenir son développement de manière volontaire.",
|
|
37
37
|
"link_sponsor": "GitHub Sponsors",
|
|
38
|
-
"link_paypal": "Faire un don via PayPal"
|
|
38
|
+
"link_paypal": "Faire un don via PayPal",
|
|
39
|
+
"MAC address (Wake-on-LAN)": "Adresse MAC (Wake-on-LAN)",
|
|
40
|
+
"wolMacHelp": "Facultatif, nécessaire uniquement pour allumer l'appareil depuis la veille profonde. Indiquée dans les paramètres réseau de l'appareil ou dans le routeur (avec câble : la MAC du port LAN)",
|
|
41
|
+
"Wake-on-LAN broadcast address": "Adresse de diffusion Wake-on-LAN",
|
|
42
|
+
"wolBroadcastHelp": "Uniquement si l'appareil est dans un autre réseau (p. ex. 192.168.20.255)",
|
|
43
|
+
"wolMacInvalid": "Adresse MAC invalide. Format : aa:bb:cc:dd:ee:ff"
|
|
39
44
|
}
|
package/admin/i18n/it.json
CHANGED
|
@@ -35,5 +35,10 @@
|
|
|
35
35
|
"header_support": "Sostieni lo sviluppo",
|
|
36
36
|
"support_text": "L'adattatore è gratuito. Se ti è utile, puoi sostenerne lo sviluppo su base volontaria.",
|
|
37
37
|
"link_sponsor": "GitHub Sponsors",
|
|
38
|
-
"link_paypal": "Dona con PayPal"
|
|
38
|
+
"link_paypal": "Dona con PayPal",
|
|
39
|
+
"MAC address (Wake-on-LAN)": "Indirizzo MAC (Wake-on-LAN)",
|
|
40
|
+
"wolMacHelp": "Facoltativo, serve solo per accendere il dispositivo dallo standby profondo. Indicato nelle impostazioni di rete del dispositivo o nel router (con cavo: il MAC della porta LAN)",
|
|
41
|
+
"Wake-on-LAN broadcast address": "Indirizzo broadcast Wake-on-LAN",
|
|
42
|
+
"wolBroadcastHelp": "Necessario solo se il dispositivo è in un'altra rete (es. 192.168.20.255)",
|
|
43
|
+
"wolMacInvalid": "Indirizzo MAC non valido. Formato: aa:bb:cc:dd:ee:ff"
|
|
39
44
|
}
|
package/admin/i18n/nl.json
CHANGED
|
@@ -35,5 +35,10 @@
|
|
|
35
35
|
"header_support": "Ontwikkeling steunen",
|
|
36
36
|
"support_text": "De adapter is gratis. Als hij nuttig voor je is, kun je de ontwikkeling vrijwillig steunen.",
|
|
37
37
|
"link_sponsor": "GitHub Sponsors",
|
|
38
|
-
"link_paypal": "Doneren via PayPal"
|
|
38
|
+
"link_paypal": "Doneren via PayPal",
|
|
39
|
+
"MAC address (Wake-on-LAN)": "MAC-adres (Wake-on-LAN)",
|
|
40
|
+
"wolMacHelp": "Optioneel, alleen nodig om het apparaat uit diepe stand-by in te schakelen. Staat in de netwerkinstellingen van het apparaat of in de router (met kabel: het MAC van de LAN-poort)",
|
|
41
|
+
"Wake-on-LAN broadcast address": "Wake-on-LAN-broadcastadres",
|
|
42
|
+
"wolBroadcastHelp": "Alleen nodig als het apparaat in een ander netwerk zit (bijv. 192.168.20.255)",
|
|
43
|
+
"wolMacInvalid": "Ongeldig MAC-adres. Formaat: aa:bb:cc:dd:ee:ff"
|
|
39
44
|
}
|
package/admin/i18n/pl.json
CHANGED
|
@@ -35,5 +35,10 @@
|
|
|
35
35
|
"header_support": "Wesprzyj rozwój",
|
|
36
36
|
"support_text": "Adapter jest darmowy. Jeśli jest dla Ciebie przydatny, możesz dobrowolnie wesprzeć jego rozwój.",
|
|
37
37
|
"link_sponsor": "GitHub Sponsors",
|
|
38
|
-
"link_paypal": "Wpłać przez PayPal"
|
|
38
|
+
"link_paypal": "Wpłać przez PayPal",
|
|
39
|
+
"MAC address (Wake-on-LAN)": "Adres MAC (Wake-on-LAN)",
|
|
40
|
+
"wolMacHelp": "Opcjonalnie, potrzebny tylko do włączenia urządzenia z głębokiego czuwania. Podany w ustawieniach sieci urządzenia lub w routerze (przy kablu: MAC portu LAN)",
|
|
41
|
+
"Wake-on-LAN broadcast address": "Adres rozgłoszeniowy Wake-on-LAN",
|
|
42
|
+
"wolBroadcastHelp": "Potrzebne tylko, gdy urządzenie jest w innej sieci (np. 192.168.20.255)",
|
|
43
|
+
"wolMacInvalid": "Nieprawidłowy adres MAC. Format: aa:bb:cc:dd:ee:ff"
|
|
39
44
|
}
|
package/admin/i18n/pt.json
CHANGED
|
@@ -35,5 +35,10 @@
|
|
|
35
35
|
"header_support": "Apoiar o desenvolvimento",
|
|
36
36
|
"support_text": "O adaptador é gratuito. Se lhe for útil, pode apoiar o seu desenvolvimento de forma voluntária.",
|
|
37
37
|
"link_sponsor": "GitHub Sponsors",
|
|
38
|
-
"link_paypal": "Doar via PayPal"
|
|
38
|
+
"link_paypal": "Doar via PayPal",
|
|
39
|
+
"MAC address (Wake-on-LAN)": "Endereço MAC (Wake-on-LAN)",
|
|
40
|
+
"wolMacHelp": "Opcional, só necessário para ligar o dispositivo a partir do modo de espera profundo. Indicado nas definições de rede do dispositivo ou no router (com cabo: o MAC da porta LAN)",
|
|
41
|
+
"Wake-on-LAN broadcast address": "Endereço de broadcast Wake-on-LAN",
|
|
42
|
+
"wolBroadcastHelp": "Só necessário se o dispositivo estiver noutra rede (p. ex. 192.168.20.255)",
|
|
43
|
+
"wolMacInvalid": "Endereço MAC inválido. Formato: aa:bb:cc:dd:ee:ff"
|
|
39
44
|
}
|
package/admin/i18n/ru.json
CHANGED
|
@@ -35,5 +35,10 @@
|
|
|
35
35
|
"header_support": "Поддержать разработку",
|
|
36
36
|
"support_text": "Адаптер бесплатный. Если он вам полезен, вы можете добровольно поддержать его разработку.",
|
|
37
37
|
"link_sponsor": "GitHub Sponsors",
|
|
38
|
-
"link_paypal": "Пожертвовать через PayPal"
|
|
38
|
+
"link_paypal": "Пожертвовать через PayPal",
|
|
39
|
+
"MAC address (Wake-on-LAN)": "MAC-адрес (Wake-on-LAN)",
|
|
40
|
+
"wolMacHelp": "Необязательно, нужно только для включения из глубокого режима ожидания. Указан в сетевых настройках устройства или в роутере (при кабеле: MAC порта LAN)",
|
|
41
|
+
"Wake-on-LAN broadcast address": "Широковещательный адрес Wake-on-LAN",
|
|
42
|
+
"wolBroadcastHelp": "Нужно, только если устройство в другой сети (например, 192.168.20.255)",
|
|
43
|
+
"wolMacInvalid": "Неверный MAC-адрес. Формат: aa:bb:cc:dd:ee:ff"
|
|
39
44
|
}
|
package/admin/i18n/uk.json
CHANGED
|
@@ -35,5 +35,10 @@
|
|
|
35
35
|
"header_support": "Підтримати розробку",
|
|
36
36
|
"support_text": "Адаптер безкоштовний. Якщо він вам корисний, ви можете добровільно підтримати його розробку.",
|
|
37
37
|
"link_sponsor": "GitHub Sponsors",
|
|
38
|
-
"link_paypal": "Пожертвувати через PayPal"
|
|
38
|
+
"link_paypal": "Пожертвувати через PayPal",
|
|
39
|
+
"MAC address (Wake-on-LAN)": "MAC-адреса (Wake-on-LAN)",
|
|
40
|
+
"wolMacHelp": "Необов'язково, потрібно лише для ввімкнення з глибокого режиму очікування. Вказано в мережевих налаштуваннях пристрою або в роутері (з кабелем: MAC порту LAN)",
|
|
41
|
+
"Wake-on-LAN broadcast address": "Широкомовна адреса Wake-on-LAN",
|
|
42
|
+
"wolBroadcastHelp": "Потрібно, лише якщо пристрій в іншій мережі (наприклад, 192.168.20.255)",
|
|
43
|
+
"wolMacInvalid": "Неправильна MAC-адреса. Формат: aa:bb:cc:dd:ee:ff"
|
|
39
44
|
}
|
package/admin/i18n/zh-cn.json
CHANGED
|
@@ -35,5 +35,10 @@
|
|
|
35
35
|
"header_support": "支持开发",
|
|
36
36
|
"support_text": "该适配器免费提供。如果它对您有帮助,您可以自愿支持其开发。",
|
|
37
37
|
"link_sponsor": "GitHub Sponsors",
|
|
38
|
-
"link_paypal": "通过 PayPal 捐赠"
|
|
38
|
+
"link_paypal": "通过 PayPal 捐赠",
|
|
39
|
+
"MAC address (Wake-on-LAN)": "MAC 地址(网络唤醒)",
|
|
40
|
+
"wolMacHelp": "可选,仅在需要从深度待机唤醒设备时使用。可在设备的网络设置或路由器中找到(有线连接时为 LAN 接口的 MAC)",
|
|
41
|
+
"Wake-on-LAN broadcast address": "网络唤醒广播地址",
|
|
42
|
+
"wolBroadcastHelp": "仅当设备位于其他网络时需要(例如 192.168.20.255)",
|
|
43
|
+
"wolMacInvalid": "MAC 地址无效。格式:aa:bb:cc:dd:ee:ff"
|
|
39
44
|
}
|
package/admin/jsonConfig.json
CHANGED
|
@@ -104,6 +104,21 @@
|
|
|
104
104
|
"lg": 6,
|
|
105
105
|
"xl": 6
|
|
106
106
|
},
|
|
107
|
+
{
|
|
108
|
+
"type": "text",
|
|
109
|
+
"attr": "wolMac",
|
|
110
|
+
"label": "MAC address (Wake-on-LAN)",
|
|
111
|
+
"help": "wolMacHelp",
|
|
112
|
+
"default": "",
|
|
113
|
+
"xs": 12,
|
|
114
|
+
"sm": 12,
|
|
115
|
+
"md": 6,
|
|
116
|
+
"lg": 4,
|
|
117
|
+
"xl": 4,
|
|
118
|
+
"validator": "!data.wolMac || /^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$/.test(data.wolMac.trim())",
|
|
119
|
+
"validatorErrorText": "wolMacInvalid",
|
|
120
|
+
"validatorNoSaveOnError": true
|
|
121
|
+
},
|
|
107
122
|
{
|
|
108
123
|
"type": "checkbox",
|
|
109
124
|
"attr": "enableIme",
|
|
@@ -133,6 +148,19 @@
|
|
|
133
148
|
"lg": 3,
|
|
134
149
|
"xl": 3
|
|
135
150
|
},
|
|
151
|
+
{
|
|
152
|
+
"type": "text",
|
|
153
|
+
"attr": "wolBroadcast",
|
|
154
|
+
"label": "Wake-on-LAN broadcast address",
|
|
155
|
+
"help": "wolBroadcastHelp",
|
|
156
|
+
"default": "",
|
|
157
|
+
"expertMode": true,
|
|
158
|
+
"xs": 12,
|
|
159
|
+
"sm": 6,
|
|
160
|
+
"md": 3,
|
|
161
|
+
"lg": 3,
|
|
162
|
+
"xl": 3
|
|
163
|
+
},
|
|
136
164
|
{
|
|
137
165
|
"type": "header",
|
|
138
166
|
"text": "Connect",
|
|
@@ -407,7 +435,7 @@
|
|
|
407
435
|
"href": "https://github.com/sponsors/Schimi1983",
|
|
408
436
|
"button": true,
|
|
409
437
|
"variant": "outlined",
|
|
410
|
-
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+
|
|
438
|
+
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iI0VBNEFBQSIgZD0iTTE3LjYyNSAxLjQ5OWMtMi4zMiAwLTQuMzU0IDEuMjAzLTUuNjI1IDMuMDMtMS4yNzEtMS44MjctMy4zMDUtMy4wMy01LjYyNS0zLjAzQzMuMTI5IDEuNDk5IDAgNC4yNTMgMCA4LjI0OWMwIDQuMjc1IDMuMDY4IDcuODQ3IDUuODI4IDEwLjIyN2EzMy4xNCAzMy4xNCAwIDAgMCA1LjYxNiAzLjg3NmwuMDI4LjAxNy4wMDguMDAzLS4wMDEuMDAzYy4xNjMuMDg1LjM0Mi4xMjYuNTIxLjEyNS4xNzkuMDAxLjM1OC0uMDQxLjUyMS0uMTI1bC0uMDAxLS4wMDMuMDA4LS4wMDMuMDI4LS4wMTdhMzMuMTQgMzMuMTQgMCAwIDAgNS42MTYtMy44NzZDMjAuOTMyIDE2LjA5NiAyNCAxMi41MjQgMjQgOC4yNDljMC0zLjk5Ni0zLjEyOS02Ljc1LTYuMzc1LTYuNzV6bS0uOTE5IDE1LjI3NWEzMC43NjYgMzAuNzY2IDAgMCAxLTQuNzAzIDMuMzE2bC0uMDA0LS4wMDItLjAwNC4wMDJhMzAuOTU1IDMwLjk1NSAwIDAgMS00LjcwMy0zLjMxNmMtMi42NzctMi4zMDctNS4wNDctNS4yOTgtNS4wNDctOC41MjMgMC0yLjc1NCAyLjEyMS00LjUgNC4xMjUtNC41IDIuMDYgMCAzLjkxNCAxLjQ3OSA0LjU0NCAzLjY4NC4xNDMuNDk1LjU5Ni43OTcgMS4wODYuNzk2LjQ5LjAwMS45NDMtLjMwMiAxLjA4NS0uNzk2LjYzLTIuMjA1IDIuNDg0LTMuNjg0IDQuNTQ0LTMuNjg0IDIuMDA0IDAgNC4xMjUgMS43NDYgNC4xMjUgNC41IDAgMy4yMjUtMi4zNyA2LjIxNi01LjA0OCA4LjUyM3oiLz48L3N2Zz4=",
|
|
411
439
|
"newLine": true,
|
|
412
440
|
"xs": 12,
|
|
413
441
|
"sm": 6,
|
|
@@ -427,7 +455,7 @@
|
|
|
427
455
|
"md": 4,
|
|
428
456
|
"lg": 3,
|
|
429
457
|
"xl": 3,
|
|
430
|
-
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+
|
|
458
|
+
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iIzAwMjk5MSIgZD0iTTE1LjYwNyA0LjY1M0g4Ljk0MUw2LjY0NSAxOS4yNTFIMS44Mkw0Ljg2MiAwaDcuOTk1YzMuNzU0IDAgNi4zNzUgMi4yOTQgNi40NzMgNS41MTMtLjY0OC0uNDc4LTIuMTA1LS44Ni0zLjcyMi0uODZtNi41NyA1LjU0NmMwIDMuNDEtMy4wMSA2Ljg1My02Ljk1OCA2Ljg1M2gtMi40OTNMMTEuNTk1IDI0SDYuNzRsMS44NDUtMTEuNTM4aDMuNTkyYzQuMjA4IDAgNy4zNDYtMy42MzQgNy4xNTMtNi45NDlhNS4yNCA1LjI0IDAgMCAxIDIuODQ4IDQuNjg2TTkuNjUzIDUuNTQ2aDYuNDA4Yy45MDcgMCAxLjk0Mi4yMjIgMi4zNjMuNTQxLS4xOTUgMi43NDEtMi42NTUgNS40ODMtNi40NDEgNS40ODNIOC43MTRaIi8+PC9zdmc+"
|
|
431
459
|
}
|
|
432
460
|
}
|
|
433
461
|
}
|
package/build/lib/adb/client.js
CHANGED
|
@@ -89,6 +89,16 @@ class AdbClient extends import_node_events.EventEmitter {
|
|
|
89
89
|
this.reconnectTimer = void 0;
|
|
90
90
|
this.teardown("stopped");
|
|
91
91
|
}
|
|
92
|
+
/** Retries a pending reconnect immediately with the shortest backoff (e.g. after Wake-on-LAN). */
|
|
93
|
+
reconnectNow() {
|
|
94
|
+
if (!this.running || this.ready || !this.reconnectTimer) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.timers.clearTimeout(this.reconnectTimer);
|
|
98
|
+
this.reconnectTimer = void 0;
|
|
99
|
+
this.reconnectDelay = this.minDelay;
|
|
100
|
+
this.connect();
|
|
101
|
+
}
|
|
92
102
|
/**
|
|
93
103
|
* Executes a shell command and returns its output (stdout and stderr combined).
|
|
94
104
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/adb/client.ts"],
|
|
4
|
-
"sourcesContent": ["import { EventEmitter } from 'node:events';\nimport * as net from 'node:net';\nimport { adbPublicKeyPayload, signAdbToken, type AdbKey } from './auth';\nimport {\n ADB_MAX_PAYLOAD,\n ADB_VERSION,\n AdbAuthType,\n AdbCommand,\n AdbPacketDecoder,\n commandName,\n encodeAdbPacket,\n type AdbPacket,\n} from './protocol';\nimport { NODE_TIMERS, type TimerHandle, type Timers } from '../timers';\n\n/** Default ADB port for network debugging. */\nexport const DEFAULT_ADB_PORT = 5555;\n\nconst CONNECT_TIMEOUT_MS = 5_000;\nconst RECONNECT_MIN_MS = 2_000;\nconst DEFAULT_SHELL_TIMEOUT_MS = 10_000;\n/** Without any answer to CNXN (no AUTH, no CNXN) within this time the connection is considered dead. */\nconst HANDSHAKE_TIMEOUT_MS = 10_000;\n\n/** Device information from the CNXN banner. */\nexport interface AdbDeviceInfo {\n /** Device model (ro.product.model) */\n model?: string;\n /** Product name (ro.product.name) */\n product?: string;\n /** Device code name (ro.product.device) */\n device?: string;\n}\n\n/** Options for {@link AdbClient}. */\nexport interface AdbClientOptions {\n /** Timer functions (the adapter passes its own so timers are cleared on unload) */\n timers?: Timers;\n /** IP address or hostname */\n host: string;\n /** TCP port, default 5555 */\n port?: number;\n /** RSA key of this ioBroker instance */\n key: AdbKey;\n /** Name shown in the authorization dialog on the TV */\n keyName: string;\n /** Upper bound of 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}\n\n/** Events emitted by {@link AdbClient}. */\nexport interface AdbClientEvents {\n /** Connected and authorized, shell commands can be executed. */\n ready: [info: AdbDeviceInfo];\n /** The device shows the \"Allow USB/network debugging?\" dialog \u2013 the user must confirm it on the TV. */\n authPending: [];\n /** Connection lost (reconnects automatically unless stopped). */\n disconnected: [reason: string];\n /** Diagnostic messages. */\n log: [level: 'debug' | 'warn', message: string];\n}\n\n/** Pending shell stream. */\ninterface ShellStream {\n remoteId: number;\n chunks: Buffer[];\n resolve: (output: string) => void;\n reject: (err: Error) => void;\n timer: TimerHandle | undefined;\n}\n\n/**\n * Self-healing ADB connection with RSA authentication and shell command execution.\n * Uses one TCP connection; every shell command is a short-lived ADB stream.\n */\nexport class AdbClient extends EventEmitter<AdbClientEvents> {\n private socket: net.Socket | undefined;\n private readonly decoder = new AdbPacketDecoder();\n private running = false;\n private ready = false;\n private signatureSent = false;\n private publicKeySent = false;\n private reconnectTimer: TimerHandle | undefined;\n private handshakeTimer: TimerHandle | undefined;\n private reconnectDelay: number;\n private readonly minDelay: number;\n private maxPayload = ADB_MAX_PAYLOAD;\n private nextLocalId = 1;\n private readonly streams = new Map<number, ShellStream>();\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: AdbClientOptions) {\n super();\n this.minDelay = options.reconnectMinMs ?? RECONNECT_MIN_MS;\n this.reconnectDelay = this.minDelay;\n }\n\n /** Whether shell commands can be executed. */\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('stopped');\n }\n\n /**\n * Executes a shell command and returns its output (stdout and stderr combined).\n *\n * @param command - Shell command line, e.g. \"dumpsys media_session\"\n * @param timeoutMs - Timeout; on expiry the connection is considered broken and re-established\n */\n public shell(command: string, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS): Promise<string> {\n if (!this.ready || !this.socket) {\n return Promise.reject(new Error('ADB not connected'));\n }\n const localId = this.nextLocalId;\n this.nextLocalId = this.nextLocalId >= 0x7fffffff ? 1 : this.nextLocalId + 1;\n return new Promise((resolve, reject) => {\n const timer = this.timers.setTimeout(() => {\n this.streams.delete(localId);\n reject(\n new Error(`ADB command timed out: ${command.length > 60 ? `${command.slice(0, 60)}\u2026` : command}`),\n );\n // @KI_HINWEIS: Eine h\u00E4ngende Shell deutet auf eine tote TCP-Verbindung hin (z. B. TV vom Strom\n // getrennt, kein FIN). Verbindung neu aufbauen statt weitere Kommandos ins Leere zu schicken.\n this.socket?.destroy(new Error('shell timeout'));\n }, timeoutMs);\n this.streams.set(localId, { remoteId: 0, chunks: [], resolve, reject, timer });\n this.write(AdbCommand.OPEN, localId, 0, Buffer.from(`shell:${command}\\0`, 'utf8'));\n });\n }\n\n private write(command: number, arg0: number, arg1: number, payload: Buffer = Buffer.alloc(0)): void {\n this.socket?.write(encodeAdbPacket({ command, arg0, arg1, payload }));\n }\n\n private connect(): void {\n this.decoder.reset();\n this.signatureSent = false;\n this.publicKeySent = false;\n const socket = net.connect({ host: this.options.host, port: this.options.port ?? DEFAULT_ADB_PORT });\n this.socket = socket;\n socket.setNoDelay(true);\n socket.setTimeout(CONNECT_TIMEOUT_MS);\n socket.once('timeout', () => socket.destroy(new Error('connect timeout')));\n socket.once('connect', () => {\n socket.setTimeout(0);\n socket.setKeepAlive(true, 10_000);\n this.write(AdbCommand.CNXN, ADB_VERSION, ADB_MAX_PAYLOAD, Buffer.from('host::\\0', 'utf8'));\n this.armHandshakeTimer();\n });\n socket.on('data', chunk => this.onData(chunk));\n socket.on('error', (err: Error & { code?: string }) =>\n this.emit(\n 'log',\n 'debug',\n `${this.options.host}: ADB socket error: ${[err.code, err.message].filter(Boolean).join(' ')}`,\n ),\n );\n socket.once('close', () => this.onClose());\n }\n\n private armHandshakeTimer(): void {\n this.timers.clearTimeout(this.handshakeTimer);\n this.handshakeTimer = this.timers.setTimeout(() => {\n this.emit('log', 'debug', `${this.options.host}: no ADB handshake answer, reconnecting`);\n this.socket?.destroy();\n }, HANDSHAKE_TIMEOUT_MS);\n }\n\n private onClose(): void {\n const wasReady = this.ready;\n this.teardown('connection closed');\n if (wasReady) {\n this.emit('disconnected', 'connection closed');\n }\n if (!this.running) {\n return;\n }\n const delay = wasReady ? this.minDelay : this.reconnectDelay;\n this.reconnectDelay = Math.min(this.reconnectDelay * 2, Math.max(this.options.reconnectMaxMs, this.minDelay));\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(reason: string): void {\n this.timers.clearTimeout(this.handshakeTimer);\n this.handshakeTimer = undefined;\n this.ready = false;\n for (const stream of this.streams.values()) {\n this.timers.clearTimeout(stream.timer);\n stream.reject(new Error(`ADB ${reason}`));\n }\n this.streams.clear();\n if (this.socket) {\n // @KI_HINWEIS: Wie in RemoteConnection: Listener entfernen, damit ein versp\u00E4tetes 'close' des alten\n // Sockets keine neue Verbindung abbaut; leerer error-Listener gegen 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 onData(chunk: Buffer): void {\n let packets: AdbPacket[];\n try {\n packets = this.decoder.push(chunk);\n } catch (e) {\n this.emit('log', 'warn', `${this.options.host}: ${(e as Error).message}`);\n this.socket?.destroy();\n return;\n }\n for (const packet of packets) {\n this.handlePacket(packet);\n }\n }\n\n private handlePacket(p: AdbPacket): void {\n switch (p.command) {\n case AdbCommand.AUTH:\n this.handleAuth(p);\n break;\n case AdbCommand.CNXN: {\n this.timers.clearTimeout(this.handshakeTimer);\n this.handshakeTimer = undefined;\n this.maxPayload = Math.min(p.arg1 || ADB_MAX_PAYLOAD, ADB_MAX_PAYLOAD);\n this.ready = true;\n this.reconnectDelay = this.minDelay;\n this.emit('ready', parseBanner(p.payload.toString('utf8')));\n break;\n }\n case AdbCommand.OKAY: {\n const stream = this.streams.get(p.arg1);\n if (stream && !stream.remoteId) {\n stream.remoteId = p.arg0;\n }\n break;\n }\n case AdbCommand.WRTE: {\n const stream = this.streams.get(p.arg1);\n if (stream) {\n stream.chunks.push(p.payload);\n }\n // Flow control: every WRTE must be acknowledged, otherwise the device stops sending.\n this.write(AdbCommand.OKAY, p.arg1, p.arg0);\n break;\n }\n case AdbCommand.CLSE: {\n const stream = this.streams.get(p.arg1);\n if (stream) {\n this.streams.delete(p.arg1);\n this.timers.clearTimeout(stream.timer);\n if (stream.remoteId) {\n this.write(AdbCommand.CLSE, p.arg1, stream.remoteId);\n stream.resolve(Buffer.concat(stream.chunks).toString('utf8'));\n } else {\n // CLSE without prior OKAY = OPEN was rejected (e.g. service unknown)\n stream.reject(new Error('ADB stream rejected by device'));\n }\n }\n break;\n }\n default:\n this.emit('log', 'debug', `${this.options.host}: ignoring ADB packet ${commandName(p.command)}`);\n }\n }\n\n private handleAuth(p: AdbPacket): void {\n if (p.arg0 !== AdbAuthType.TOKEN) {\n return;\n }\n if (!this.signatureSent) {\n this.signatureSent = true;\n this.write(AdbCommand.AUTH, AdbAuthType.SIGNATURE, 0, signAdbToken(this.options.key, p.payload));\n this.armHandshakeTimer();\n return;\n }\n if (!this.publicKeySent) {\n // Signature not accepted \u2192 key unknown to the device: send public key, the TV shows a confirmation dialog.\n this.publicKeySent = true;\n this.timers.clearTimeout(this.handshakeTimer);\n this.handshakeTimer = undefined; // the user may need some time to confirm\n this.write(\n AdbCommand.AUTH,\n AdbAuthType.RSAPUBLICKEY,\n 0,\n adbPublicKeyPayload(this.options.key, this.options.keyName),\n );\n this.emit('authPending');\n }\n }\n\n /** Maximum payload size negotiated with the device (for diagnostics). */\n public get negotiatedMaxPayload(): number {\n return this.maxPayload;\n }\n}\n\n/**\n * Parses the CNXN banner, e.g. \"device::ro.product.name=darcy;ro.product.model=SHIELD Android TV;ro.product.device=darcy;features=\u2026\".\n *\n * @param banner - Banner text\n */\nexport function parseBanner(banner: string): AdbDeviceInfo {\n const props = new Map<string, string>();\n const body = banner.replace(/\\0+$/, '').split('::')[1] ?? '';\n for (const part of body.split(';')) {\n const idx = part.indexOf('=');\n if (idx > 0) {\n props.set(part.slice(0, idx), part.slice(idx + 1));\n }\n }\n return {\n model: props.get('ro.product.model'),\n product: props.get('ro.product.name'),\n device: props.get('ro.product.device'),\n };\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAA6B;AAC7B,UAAqB;AACrB,kBAA+D;AAC/D,sBASO;AACP,oBAA2D;AAGpD,MAAM,mBAAmB;AAEhC,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AACzB,MAAM,2BAA2B;AAEjC,MAAM,uBAAuB;AAuDtB,MAAM,kBAAkB,gCAA8B;AAAA;AAAA,EAqBlD,YAA6B,SAA2B;AAlGnE;AAmGQ,UAAM;AAD0B;AAEhC,SAAK,YAAW,aAAQ,mBAAR,YAA0B;AAC1C,SAAK,iBAAiB,KAAK;AAAA,EAC/B;AAAA,EAxBQ;AAAA,EACS,UAAU,IAAI,iCAAiB;AAAA,EACxC,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACS;AAAA,EACT,aAAa;AAAA,EACb,cAAc;AAAA,EACL,UAAU,oBAAI,IAAyB;AAAA;AAAA,EAGxD,IAAY,SAAiB;AA7FjC;AA8FQ,YAAO,UAAK,QAAQ,WAAb,YAAuB;AAAA,EAClC;AAAA;AAAA,EAUA,IAAW,UAAmB;AAC1B,WAAO,KAAK;AAAA,EAChB;AAAA;AAAA,EAGO,QAAc;AACjB,QAAI,KAAK,SAAS;AACd;AAAA,IACJ;AACA,SAAK,UAAU;AACf,SAAK,QAAQ;AAAA,EACjB;AAAA;AAAA,EAGO,OAAa;AAChB,SAAK,UAAU;AACf,SAAK,OAAO,aAAa,KAAK,cAAc;AAC5C,SAAK,iBAAiB;AACtB,SAAK,SAAS,SAAS;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQO,MAAM,SAAiB,YAAY,0BAA2C;AACjF,QAAI,CAAC,KAAK,SAAS,CAAC,KAAK,QAAQ;AAC7B,aAAO,QAAQ,OAAO,IAAI,MAAM,mBAAmB,CAAC;AAAA,IACxD;AACA,UAAM,UAAU,KAAK;AACrB,SAAK,cAAc,KAAK,eAAe,aAAa,IAAI,KAAK,cAAc;AAC3E,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACpC,YAAM,QAAQ,KAAK,OAAO,WAAW,MAAM;
|
|
4
|
+
"sourcesContent": ["import { EventEmitter } from 'node:events';\nimport * as net from 'node:net';\nimport { adbPublicKeyPayload, signAdbToken, type AdbKey } from './auth';\nimport {\n ADB_MAX_PAYLOAD,\n ADB_VERSION,\n AdbAuthType,\n AdbCommand,\n AdbPacketDecoder,\n commandName,\n encodeAdbPacket,\n type AdbPacket,\n} from './protocol';\nimport { NODE_TIMERS, type TimerHandle, type Timers } from '../timers';\n\n/** Default ADB port for network debugging. */\nexport const DEFAULT_ADB_PORT = 5555;\n\nconst CONNECT_TIMEOUT_MS = 5_000;\nconst RECONNECT_MIN_MS = 2_000;\nconst DEFAULT_SHELL_TIMEOUT_MS = 10_000;\n/** Without any answer to CNXN (no AUTH, no CNXN) within this time the connection is considered dead. */\nconst HANDSHAKE_TIMEOUT_MS = 10_000;\n\n/** Device information from the CNXN banner. */\nexport interface AdbDeviceInfo {\n /** Device model (ro.product.model) */\n model?: string;\n /** Product name (ro.product.name) */\n product?: string;\n /** Device code name (ro.product.device) */\n device?: string;\n}\n\n/** Options for {@link AdbClient}. */\nexport interface AdbClientOptions {\n /** Timer functions (the adapter passes its own so timers are cleared on unload) */\n timers?: Timers;\n /** IP address or hostname */\n host: string;\n /** TCP port, default 5555 */\n port?: number;\n /** RSA key of this ioBroker instance */\n key: AdbKey;\n /** Name shown in the authorization dialog on the TV */\n keyName: string;\n /** Upper bound of 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}\n\n/** Events emitted by {@link AdbClient}. */\nexport interface AdbClientEvents {\n /** Connected and authorized, shell commands can be executed. */\n ready: [info: AdbDeviceInfo];\n /** The device shows the \"Allow USB/network debugging?\" dialog \u2013 the user must confirm it on the TV. */\n authPending: [];\n /** Connection lost (reconnects automatically unless stopped). */\n disconnected: [reason: string];\n /** Diagnostic messages. */\n log: [level: 'debug' | 'warn', message: string];\n}\n\n/** Pending shell stream. */\ninterface ShellStream {\n remoteId: number;\n chunks: Buffer[];\n resolve: (output: string) => void;\n reject: (err: Error) => void;\n timer: TimerHandle | undefined;\n}\n\n/**\n * Self-healing ADB connection with RSA authentication and shell command execution.\n * Uses one TCP connection; every shell command is a short-lived ADB stream.\n */\nexport class AdbClient extends EventEmitter<AdbClientEvents> {\n private socket: net.Socket | undefined;\n private readonly decoder = new AdbPacketDecoder();\n private running = false;\n private ready = false;\n private signatureSent = false;\n private publicKeySent = false;\n private reconnectTimer: TimerHandle | undefined;\n private handshakeTimer: TimerHandle | undefined;\n private reconnectDelay: number;\n private readonly minDelay: number;\n private maxPayload = ADB_MAX_PAYLOAD;\n private nextLocalId = 1;\n private readonly streams = new Map<number, ShellStream>();\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: AdbClientOptions) {\n super();\n this.minDelay = options.reconnectMinMs ?? RECONNECT_MIN_MS;\n this.reconnectDelay = this.minDelay;\n }\n\n /** Whether shell commands can be executed. */\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('stopped');\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 /**\n * Executes a shell command and returns its output (stdout and stderr combined).\n *\n * @param command - Shell command line, e.g. \"dumpsys media_session\"\n * @param timeoutMs - Timeout; on expiry the connection is considered broken and re-established\n */\n public shell(command: string, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS): Promise<string> {\n if (!this.ready || !this.socket) {\n return Promise.reject(new Error('ADB not connected'));\n }\n const localId = this.nextLocalId;\n this.nextLocalId = this.nextLocalId >= 0x7fffffff ? 1 : this.nextLocalId + 1;\n return new Promise((resolve, reject) => {\n const timer = this.timers.setTimeout(() => {\n this.streams.delete(localId);\n reject(\n new Error(`ADB command timed out: ${command.length > 60 ? `${command.slice(0, 60)}\u2026` : command}`),\n );\n // @KI_HINWEIS: Eine h\u00E4ngende Shell deutet auf eine tote TCP-Verbindung hin (z. B. TV vom Strom\n // getrennt, kein FIN). Verbindung neu aufbauen statt weitere Kommandos ins Leere zu schicken.\n this.socket?.destroy(new Error('shell timeout'));\n }, timeoutMs);\n this.streams.set(localId, { remoteId: 0, chunks: [], resolve, reject, timer });\n this.write(AdbCommand.OPEN, localId, 0, Buffer.from(`shell:${command}\\0`, 'utf8'));\n });\n }\n\n private write(command: number, arg0: number, arg1: number, payload: Buffer = Buffer.alloc(0)): void {\n this.socket?.write(encodeAdbPacket({ command, arg0, arg1, payload }));\n }\n\n private connect(): void {\n this.decoder.reset();\n this.signatureSent = false;\n this.publicKeySent = false;\n const socket = net.connect({ host: this.options.host, port: this.options.port ?? DEFAULT_ADB_PORT });\n this.socket = socket;\n socket.setNoDelay(true);\n socket.setTimeout(CONNECT_TIMEOUT_MS);\n socket.once('timeout', () => socket.destroy(new Error('connect timeout')));\n socket.once('connect', () => {\n socket.setTimeout(0);\n socket.setKeepAlive(true, 10_000);\n this.write(AdbCommand.CNXN, ADB_VERSION, ADB_MAX_PAYLOAD, Buffer.from('host::\\0', 'utf8'));\n this.armHandshakeTimer();\n });\n socket.on('data', chunk => this.onData(chunk));\n socket.on('error', (err: Error & { code?: string }) =>\n this.emit(\n 'log',\n 'debug',\n `${this.options.host}: ADB socket error: ${[err.code, err.message].filter(Boolean).join(' ')}`,\n ),\n );\n socket.once('close', () => this.onClose());\n }\n\n private armHandshakeTimer(): void {\n this.timers.clearTimeout(this.handshakeTimer);\n this.handshakeTimer = this.timers.setTimeout(() => {\n this.emit('log', 'debug', `${this.options.host}: no ADB handshake answer, reconnecting`);\n this.socket?.destroy();\n }, HANDSHAKE_TIMEOUT_MS);\n }\n\n private onClose(): void {\n const wasReady = this.ready;\n this.teardown('connection closed');\n if (wasReady) {\n this.emit('disconnected', 'connection closed');\n }\n if (!this.running) {\n return;\n }\n const delay = wasReady ? this.minDelay : this.reconnectDelay;\n this.reconnectDelay = Math.min(this.reconnectDelay * 2, Math.max(this.options.reconnectMaxMs, this.minDelay));\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(reason: string): void {\n this.timers.clearTimeout(this.handshakeTimer);\n this.handshakeTimer = undefined;\n this.ready = false;\n for (const stream of this.streams.values()) {\n this.timers.clearTimeout(stream.timer);\n stream.reject(new Error(`ADB ${reason}`));\n }\n this.streams.clear();\n if (this.socket) {\n // @KI_HINWEIS: Wie in RemoteConnection: Listener entfernen, damit ein versp\u00E4tetes 'close' des alten\n // Sockets keine neue Verbindung abbaut; leerer error-Listener gegen 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 onData(chunk: Buffer): void {\n let packets: AdbPacket[];\n try {\n packets = this.decoder.push(chunk);\n } catch (e) {\n this.emit('log', 'warn', `${this.options.host}: ${(e as Error).message}`);\n this.socket?.destroy();\n return;\n }\n for (const packet of packets) {\n this.handlePacket(packet);\n }\n }\n\n private handlePacket(p: AdbPacket): void {\n switch (p.command) {\n case AdbCommand.AUTH:\n this.handleAuth(p);\n break;\n case AdbCommand.CNXN: {\n this.timers.clearTimeout(this.handshakeTimer);\n this.handshakeTimer = undefined;\n this.maxPayload = Math.min(p.arg1 || ADB_MAX_PAYLOAD, ADB_MAX_PAYLOAD);\n this.ready = true;\n this.reconnectDelay = this.minDelay;\n this.emit('ready', parseBanner(p.payload.toString('utf8')));\n break;\n }\n case AdbCommand.OKAY: {\n const stream = this.streams.get(p.arg1);\n if (stream && !stream.remoteId) {\n stream.remoteId = p.arg0;\n }\n break;\n }\n case AdbCommand.WRTE: {\n const stream = this.streams.get(p.arg1);\n if (stream) {\n stream.chunks.push(p.payload);\n }\n // Flow control: every WRTE must be acknowledged, otherwise the device stops sending.\n this.write(AdbCommand.OKAY, p.arg1, p.arg0);\n break;\n }\n case AdbCommand.CLSE: {\n const stream = this.streams.get(p.arg1);\n if (stream) {\n this.streams.delete(p.arg1);\n this.timers.clearTimeout(stream.timer);\n if (stream.remoteId) {\n this.write(AdbCommand.CLSE, p.arg1, stream.remoteId);\n stream.resolve(Buffer.concat(stream.chunks).toString('utf8'));\n } else {\n // CLSE without prior OKAY = OPEN was rejected (e.g. service unknown)\n stream.reject(new Error('ADB stream rejected by device'));\n }\n }\n break;\n }\n default:\n this.emit('log', 'debug', `${this.options.host}: ignoring ADB packet ${commandName(p.command)}`);\n }\n }\n\n private handleAuth(p: AdbPacket): void {\n if (p.arg0 !== AdbAuthType.TOKEN) {\n return;\n }\n if (!this.signatureSent) {\n this.signatureSent = true;\n this.write(AdbCommand.AUTH, AdbAuthType.SIGNATURE, 0, signAdbToken(this.options.key, p.payload));\n this.armHandshakeTimer();\n return;\n }\n if (!this.publicKeySent) {\n // Signature not accepted \u2192 key unknown to the device: send public key, the TV shows a confirmation dialog.\n this.publicKeySent = true;\n this.timers.clearTimeout(this.handshakeTimer);\n this.handshakeTimer = undefined; // the user may need some time to confirm\n this.write(\n AdbCommand.AUTH,\n AdbAuthType.RSAPUBLICKEY,\n 0,\n adbPublicKeyPayload(this.options.key, this.options.keyName),\n );\n this.emit('authPending');\n }\n }\n\n /** Maximum payload size negotiated with the device (for diagnostics). */\n public get negotiatedMaxPayload(): number {\n return this.maxPayload;\n }\n}\n\n/**\n * Parses the CNXN banner, e.g. \"device::ro.product.name=darcy;ro.product.model=SHIELD Android TV;ro.product.device=darcy;features=\u2026\".\n *\n * @param banner - Banner text\n */\nexport function parseBanner(banner: string): AdbDeviceInfo {\n const props = new Map<string, string>();\n const body = banner.replace(/\\0+$/, '').split('::')[1] ?? '';\n for (const part of body.split(';')) {\n const idx = part.indexOf('=');\n if (idx > 0) {\n props.set(part.slice(0, idx), part.slice(idx + 1));\n }\n }\n return {\n model: props.get('ro.product.model'),\n product: props.get('ro.product.name'),\n device: props.get('ro.product.device'),\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/lib/adb/device.js
CHANGED
|
@@ -203,6 +203,21 @@ class AdbDevice extends import_node_events.EventEmitter {
|
|
|
203
203
|
return { browserOnly: false };
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Lists Android settings that look like Wake-on-LAN / network standby switches (diagnostics).
|
|
208
|
+
*
|
|
209
|
+
* @returns Matching lines "namespace: key=value"
|
|
210
|
+
*/
|
|
211
|
+
async findWakeSettings() {
|
|
212
|
+
const out = await this.client.shell(
|
|
213
|
+
'for n in global secure system; do settings list $n 2>/dev/null | grep -iE "wol|wake_on|wakeonlan|network_standby|lan_wake|networkstandby" | sed "s/^/$n: /"; done'
|
|
214
|
+
);
|
|
215
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
216
|
+
}
|
|
217
|
+
/** Retries a pending ADB reconnect immediately (e.g. after Wake-on-LAN). */
|
|
218
|
+
reconnectNow() {
|
|
219
|
+
this.client.reconnectNow();
|
|
220
|
+
}
|
|
206
221
|
/**
|
|
207
222
|
* Reads a system property, e.g. ro.product.manufacturer.
|
|
208
223
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/adb/device.ts"],
|
|
4
|
-
"sourcesContent": ["import { EventEmitter } from 'node:events';\nimport { KEY_CODES } from '../keycodes';\nimport type { AdbKey } from './auth';\nimport { AdbClient, type AdbDeviceInfo } from './client';\nimport { PACKAGE_PATTERN, POLL_COMMAND, parsePollOutput, shellQuote, type PollResult } from './parsers';\nimport { NODE_TIMERS, type TimerHandle, type Timers } from '../timers';\n\n/** Options for {@link AdbDevice}. */\nexport interface AdbDeviceOptions {\n /** Timer functions (the adapter passes its own so timers are cleared on unload) */\n timers?: Timers;\n /** IP address or hostname */\n host: string;\n /** ADB port */\n port: number;\n /** RSA key of this instance */\n key: AdbKey;\n /** Name shown on the TV in the authorization dialog */\n keyName: string;\n /** Poll interval in ms while the device is on */\n pollIntervalMs: number;\n /** Upper bound of the reconnect backoff in ms */\n reconnectMaxMs: number;\n /**\n * Returns the screen state known from another source (Remote v2), or undefined if ADB is the only source.\n * When it returns false, no polling traffic is generated at all.\n */\n externalPower: () => boolean | undefined;\n}\n\n/** Events emitted by {@link AdbDevice}. */\nexport interface AdbDeviceEvents {\n /** ready */\n ready: [info: AdbDeviceInfo];\n /** authPending */\n authPending: [];\n /** disconnected */\n disconnected: [];\n /** poll */\n poll: [result: PollResult];\n /** log */\n log: [level: 'debug' | 'warn', message: string];\n}\n\nconst KEYCODE_WAKEUP = 224;\nconst LAUNCHER_CATEGORIES = ['android.intent.category.LEANBACK_LAUNCHER', 'android.intent.category.LAUNCHER'];\nconst KEYCODE_SLEEP = 223;\n\n/**\n * ADB based device access: periodic status polling and commands via shell.\n * Polls only while the device is on (or, without another power source, polls just the power state).\n */\nexport class AdbDevice extends EventEmitter<AdbDeviceEvents> {\n private readonly client: AdbClient;\n private pollTimer: TimerHandle | undefined;\n private polling = false;\n private stopped = true;\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 - Device options */\n public constructor(private readonly options: AdbDeviceOptions) {\n super();\n this.client = new AdbClient({\n timers: options.timers,\n host: options.host,\n port: options.port,\n key: options.key,\n keyName: options.keyName,\n reconnectMaxMs: options.reconnectMaxMs,\n });\n this.client.on('ready', info => {\n this.emit('ready', info);\n this.schedulePoll(0);\n });\n this.client.on('authPending', () => this.emit('authPending'));\n this.client.on('disconnected', () => {\n this.timers.clearTimeout(this.pollTimer);\n this.emit('disconnected');\n });\n this.client.on('log', (level, msg) => this.emit('log', level, msg));\n }\n\n /** Whether commands can be sent. */\n public get isReady(): boolean {\n return this.client.isReady;\n }\n\n /** Starts the connection and polling. */\n public start(): void {\n this.stopped = false;\n this.client.start();\n }\n\n /** Stops polling and closes the connection. */\n public stop(): void {\n this.stopped = true;\n this.timers.clearTimeout(this.pollTimer);\n this.pollTimer = undefined;\n this.client.stop();\n }\n\n /** Triggers a poll soon, e.g. after a command or when the screen was switched on. */\n public pollSoon(): void {\n this.schedulePoll(1_000);\n }\n\n private schedulePoll(delay: number): void {\n this.timers.clearTimeout(this.pollTimer);\n if (this.stopped) {\n return;\n }\n this.pollTimer = this.timers.setTimeout(() => void this.poll(), delay);\n }\n\n private async poll(): Promise<void> {\n if (this.stopped || this.polling || !this.client.isReady) {\n return;\n }\n // @KI_HINWEIS: Mit Remote v2 als Quelle f\u00FCr den Bildschirmzustand gar nicht abfragen, solange das Ger\u00E4t\n // aus ist (kein ADB-Verkehr im Standby). Nur ADB: der Befehl liefert im Standby nur die Power-Zeile.\n if (this.options.externalPower() === false) {\n this.schedulePoll(this.options.pollIntervalMs);\n return;\n }\n this.polling = true;\n try {\n const output = await this.client.shell(POLL_COMMAND);\n this.emit('poll', parsePollOutput(output));\n } catch (e) {\n this.emit('log', 'debug', `${this.options.host}: ADB poll failed: ${(e as Error).message}`);\n } finally {\n this.polling = false;\n this.schedulePoll(this.options.pollIntervalMs);\n }\n }\n\n /**\n * Sends a key press via `input keyevent`.\n *\n * @param key - Key name without KEYCODE_ prefix or numeric code\n */\n public async sendKey(key: string | number): Promise<void> {\n const code =\n typeof key === 'number'\n ? key\n : /^\\d+$/.test(key.trim())\n ? Number(key)\n : KEY_CODES[\n key\n .trim()\n .toUpperCase()\n .replace(/^KEYCODE_/, '')\n ];\n if (code === undefined || !Number.isInteger(code)) {\n throw new Error(`Unknown key \"${key}\"`);\n }\n // @KI_HINWEIS: Feldtest Fire OS 7: `cmd media_session dispatch` wird best\u00E4tigt, wirkt aber nicht (Prime Music\n // pausierte nicht). Deshalb immer `input keyevent` (~1,5\u20132,5 s auf dem Fire TV, startet eine Java-VM).\n await this.client.shell(`input keyevent ${code}`);\n this.pollSoon();\n }\n\n /**\n * Switches the device on or off with the idempotent WAKEUP/SLEEP keys.\n *\n * @param on - Desired state\n */\n public async setPower(on: boolean): Promise<void> {\n await this.client.shell(`input keyevent ${on ? KEYCODE_WAKEUP : KEYCODE_SLEEP}`);\n this.pollSoon();\n }\n\n /**\n * Starts an app by package name (launcher activity) or opens a URL/deep link.\n *\n * @param target - Package name (e.g. org.xbmc.kodi) or URL with scheme\n */\n public async launch(target: string): Promise<void> {\n const value = target.trim();\n if (/^[a-z][a-z0-9+.-]*:/i.test(value)) {\n const { component, browserOnly } = await this.resolveLinkHandler(value);\n if (browserOnly) {\n throw new Error(\n `no app on the device opens \"${value}\" (only a web browser would). Enter the app ID instead \u2013 start the app on the TV and read it from currentApp`,\n );\n }\n await this.client.shell(\n `am start -a android.intent.action.VIEW -d ${shellQuote(value)}${component ? ` -n ${component}` : ''}`,\n );\n } else {\n if (!PACKAGE_PATTERN.test(value)) {\n throw new Error(`Invalid package name \"${value}\"`);\n }\n // Fast path: resolve the launcher activity (native `cmd package`) and start it with `am start`.\n // TV launcher category first (Android TV / Fire TV), then the phone launcher category.\n for (const category of LAUNCHER_CATEGORIES) {\n const resolved = await this.client.shell(\n `cmd package resolve-activity --brief -a android.intent.action.MAIN -c ${category} ${value}`,\n );\n const component = /^\\s*([A-Za-z][\\w.]*\\/[\\w.$]+)\\s*$/m.exec(resolved)?.[1];\n if (component) {\n await this.client.shell(`am start -a android.intent.action.MAIN -c ${category} -n ${component}`);\n this.pollSoon();\n return;\n }\n }\n // Fallback (older devices without `cmd package resolve-activity`): monkey, slow but universal\n let out = await this.client.shell(`monkey -p ${value} -c android.intent.category.LEANBACK_LAUNCHER 1`);\n if (/No activities found|monkey aborted/i.test(out)) {\n out = await this.client.shell(`monkey -p ${value} -c android.intent.category.LAUNCHER 1`);\n }\n if (/No activities found|monkey aborted/i.test(out)) {\n throw new Error(`App \"${value}\" not found on the device`);\n }\n }\n this.pollSoon();\n }\n\n /**\n * Finds the app (not a browser) that opens a link, so that Android does not show the \"Open with\" chooser.\n *\n * @param url - Link to open\n * @returns component \"package/activity\" of the app, and whether only browsers would open the link\n */\n private async resolveLinkHandler(url: string): Promise<{ component?: string; browserOnly: boolean }> {\n // @KI_HINWEIS: Feldtest Fire TV: `am start -d https://www.youtube.com/\u2026` \u00F6ffnete den Auswahldialog\n // (YouTube-App und Browser). L\u00F6sung ohne Liste bekannter Apps: Handler des Links abfragen und alle Apps\n // abziehen, die auch eine beliebige Webseite \u00F6ffnen (= Browser). Bleibt eine App \u00FCbrig, direkt starten.\n // \u00D6ffnen nur Browser den Link (Fire TV: app.primevideo.com \u2192 Silk), nicht starten \u2013 auf einem TV unerw\u00FCnscht.\n if (!/^https?:/i.test(url)) {\n return { browserOnly: false };\n }\n const query = (u: string): Promise<string> =>\n this.client.shell(\n `cmd package query-activities --components -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d ${shellQuote(u)}`,\n );\n try {\n const [specific, generic] = await Promise.all([query(url), query('https://example.com/')]);\n const parse = (out: string): string[] =>\n [...out.matchAll(/^\\s*([A-Za-z][\\w.]*\\/[\\w.$]+)\\s*$/gm)].map(m => m[1]);\n const handlers = parse(specific);\n const browsers = new Set(parse(generic).map(c => c.split('/')[0]));\n const candidates = handlers.filter(c => !browsers.has(c.split('/')[0]));\n this.emit(\n 'log',\n 'debug',\n `${this.options.host}: link handlers ${JSON.stringify(handlers)}, apps ${JSON.stringify(candidates)}`,\n );\n return { component: candidates[0], browserOnly: handlers.length > 0 && candidates.length === 0 };\n } catch (e) {\n this.emit('log', 'debug', `${this.options.host}: cannot resolve link handler: ${(e as Error).message}`);\n return { browserOnly: false };\n }\n }\n\n /**\n * Reads a system property, e.g. ro.product.manufacturer.\n *\n * @param name - Property name\n */\n public async getProp(name: string): Promise<string | undefined> {\n if (!/^[\\w.]+$/.test(name)) {\n throw new Error(`Invalid property name \"${name}\"`);\n }\n const value = (await this.client.shell(`getprop ${name}`)).trim();\n return value || undefined;\n }\n\n /**\n * Types text into the focused input field via `input text`.\n *\n * @param text - Text (spaces are supported)\n */\n public async sendText(text: string): Promise<void> {\n if (!text) {\n throw new Error('Text must not be empty');\n }\n // `input text` expects %s for spaces; everything else is quoted for the shell\n await this.client.shell(`input text ${shellQuote(text.replace(/ /g, '%s'))}`);\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { EventEmitter } from 'node:events';\nimport { KEY_CODES } from '../keycodes';\nimport type { AdbKey } from './auth';\nimport { AdbClient, type AdbDeviceInfo } from './client';\nimport { PACKAGE_PATTERN, POLL_COMMAND, parsePollOutput, shellQuote, type PollResult } from './parsers';\nimport { NODE_TIMERS, type TimerHandle, type Timers } from '../timers';\n\n/** Options for {@link AdbDevice}. */\nexport interface AdbDeviceOptions {\n /** Timer functions (the adapter passes its own so timers are cleared on unload) */\n timers?: Timers;\n /** IP address or hostname */\n host: string;\n /** ADB port */\n port: number;\n /** RSA key of this instance */\n key: AdbKey;\n /** Name shown on the TV in the authorization dialog */\n keyName: string;\n /** Poll interval in ms while the device is on */\n pollIntervalMs: number;\n /** Upper bound of the reconnect backoff in ms */\n reconnectMaxMs: number;\n /**\n * Returns the screen state known from another source (Remote v2), or undefined if ADB is the only source.\n * When it returns false, no polling traffic is generated at all.\n */\n externalPower: () => boolean | undefined;\n}\n\n/** Events emitted by {@link AdbDevice}. */\nexport interface AdbDeviceEvents {\n /** ready */\n ready: [info: AdbDeviceInfo];\n /** authPending */\n authPending: [];\n /** disconnected */\n disconnected: [];\n /** poll */\n poll: [result: PollResult];\n /** log */\n log: [level: 'debug' | 'warn', message: string];\n}\n\nconst KEYCODE_WAKEUP = 224;\nconst LAUNCHER_CATEGORIES = ['android.intent.category.LEANBACK_LAUNCHER', 'android.intent.category.LAUNCHER'];\nconst KEYCODE_SLEEP = 223;\n\n/**\n * ADB based device access: periodic status polling and commands via shell.\n * Polls only while the device is on (or, without another power source, polls just the power state).\n */\nexport class AdbDevice extends EventEmitter<AdbDeviceEvents> {\n private readonly client: AdbClient;\n private pollTimer: TimerHandle | undefined;\n private polling = false;\n private stopped = true;\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 - Device options */\n public constructor(private readonly options: AdbDeviceOptions) {\n super();\n this.client = new AdbClient({\n timers: options.timers,\n host: options.host,\n port: options.port,\n key: options.key,\n keyName: options.keyName,\n reconnectMaxMs: options.reconnectMaxMs,\n });\n this.client.on('ready', info => {\n this.emit('ready', info);\n this.schedulePoll(0);\n });\n this.client.on('authPending', () => this.emit('authPending'));\n this.client.on('disconnected', () => {\n this.timers.clearTimeout(this.pollTimer);\n this.emit('disconnected');\n });\n this.client.on('log', (level, msg) => this.emit('log', level, msg));\n }\n\n /** Whether commands can be sent. */\n public get isReady(): boolean {\n return this.client.isReady;\n }\n\n /** Starts the connection and polling. */\n public start(): void {\n this.stopped = false;\n this.client.start();\n }\n\n /** Stops polling and closes the connection. */\n public stop(): void {\n this.stopped = true;\n this.timers.clearTimeout(this.pollTimer);\n this.pollTimer = undefined;\n this.client.stop();\n }\n\n /** Triggers a poll soon, e.g. after a command or when the screen was switched on. */\n public pollSoon(): void {\n this.schedulePoll(1_000);\n }\n\n private schedulePoll(delay: number): void {\n this.timers.clearTimeout(this.pollTimer);\n if (this.stopped) {\n return;\n }\n this.pollTimer = this.timers.setTimeout(() => void this.poll(), delay);\n }\n\n private async poll(): Promise<void> {\n if (this.stopped || this.polling || !this.client.isReady) {\n return;\n }\n // @KI_HINWEIS: Mit Remote v2 als Quelle f\u00FCr den Bildschirmzustand gar nicht abfragen, solange das Ger\u00E4t\n // aus ist (kein ADB-Verkehr im Standby). Nur ADB: der Befehl liefert im Standby nur die Power-Zeile.\n if (this.options.externalPower() === false) {\n this.schedulePoll(this.options.pollIntervalMs);\n return;\n }\n this.polling = true;\n try {\n const output = await this.client.shell(POLL_COMMAND);\n this.emit('poll', parsePollOutput(output));\n } catch (e) {\n this.emit('log', 'debug', `${this.options.host}: ADB poll failed: ${(e as Error).message}`);\n } finally {\n this.polling = false;\n this.schedulePoll(this.options.pollIntervalMs);\n }\n }\n\n /**\n * Sends a key press via `input keyevent`.\n *\n * @param key - Key name without KEYCODE_ prefix or numeric code\n */\n public async sendKey(key: string | number): Promise<void> {\n const code =\n typeof key === 'number'\n ? key\n : /^\\d+$/.test(key.trim())\n ? Number(key)\n : KEY_CODES[\n key\n .trim()\n .toUpperCase()\n .replace(/^KEYCODE_/, '')\n ];\n if (code === undefined || !Number.isInteger(code)) {\n throw new Error(`Unknown key \"${key}\"`);\n }\n // @KI_HINWEIS: Feldtest Fire OS 7: `cmd media_session dispatch` wird best\u00E4tigt, wirkt aber nicht (Prime Music\n // pausierte nicht). Deshalb immer `input keyevent` (~1,5\u20132,5 s auf dem Fire TV, startet eine Java-VM).\n await this.client.shell(`input keyevent ${code}`);\n this.pollSoon();\n }\n\n /**\n * Switches the device on or off with the idempotent WAKEUP/SLEEP keys.\n *\n * @param on - Desired state\n */\n public async setPower(on: boolean): Promise<void> {\n await this.client.shell(`input keyevent ${on ? KEYCODE_WAKEUP : KEYCODE_SLEEP}`);\n this.pollSoon();\n }\n\n /**\n * Starts an app by package name (launcher activity) or opens a URL/deep link.\n *\n * @param target - Package name (e.g. org.xbmc.kodi) or URL with scheme\n */\n public async launch(target: string): Promise<void> {\n const value = target.trim();\n if (/^[a-z][a-z0-9+.-]*:/i.test(value)) {\n const { component, browserOnly } = await this.resolveLinkHandler(value);\n if (browserOnly) {\n throw new Error(\n `no app on the device opens \"${value}\" (only a web browser would). Enter the app ID instead \u2013 start the app on the TV and read it from currentApp`,\n );\n }\n await this.client.shell(\n `am start -a android.intent.action.VIEW -d ${shellQuote(value)}${component ? ` -n ${component}` : ''}`,\n );\n } else {\n if (!PACKAGE_PATTERN.test(value)) {\n throw new Error(`Invalid package name \"${value}\"`);\n }\n // Fast path: resolve the launcher activity (native `cmd package`) and start it with `am start`.\n // TV launcher category first (Android TV / Fire TV), then the phone launcher category.\n for (const category of LAUNCHER_CATEGORIES) {\n const resolved = await this.client.shell(\n `cmd package resolve-activity --brief -a android.intent.action.MAIN -c ${category} ${value}`,\n );\n const component = /^\\s*([A-Za-z][\\w.]*\\/[\\w.$]+)\\s*$/m.exec(resolved)?.[1];\n if (component) {\n await this.client.shell(`am start -a android.intent.action.MAIN -c ${category} -n ${component}`);\n this.pollSoon();\n return;\n }\n }\n // Fallback (older devices without `cmd package resolve-activity`): monkey, slow but universal\n let out = await this.client.shell(`monkey -p ${value} -c android.intent.category.LEANBACK_LAUNCHER 1`);\n if (/No activities found|monkey aborted/i.test(out)) {\n out = await this.client.shell(`monkey -p ${value} -c android.intent.category.LAUNCHER 1`);\n }\n if (/No activities found|monkey aborted/i.test(out)) {\n throw new Error(`App \"${value}\" not found on the device`);\n }\n }\n this.pollSoon();\n }\n\n /**\n * Finds the app (not a browser) that opens a link, so that Android does not show the \"Open with\" chooser.\n *\n * @param url - Link to open\n * @returns component \"package/activity\" of the app, and whether only browsers would open the link\n */\n private async resolveLinkHandler(url: string): Promise<{ component?: string; browserOnly: boolean }> {\n // @KI_HINWEIS: Feldtest Fire TV: `am start -d https://www.youtube.com/\u2026` \u00F6ffnete den Auswahldialog\n // (YouTube-App und Browser). L\u00F6sung ohne Liste bekannter Apps: Handler des Links abfragen und alle Apps\n // abziehen, die auch eine beliebige Webseite \u00F6ffnen (= Browser). Bleibt eine App \u00FCbrig, direkt starten.\n // \u00D6ffnen nur Browser den Link (Fire TV: app.primevideo.com \u2192 Silk), nicht starten \u2013 auf einem TV unerw\u00FCnscht.\n if (!/^https?:/i.test(url)) {\n return { browserOnly: false };\n }\n const query = (u: string): Promise<string> =>\n this.client.shell(\n `cmd package query-activities --components -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d ${shellQuote(u)}`,\n );\n try {\n const [specific, generic] = await Promise.all([query(url), query('https://example.com/')]);\n const parse = (out: string): string[] =>\n [...out.matchAll(/^\\s*([A-Za-z][\\w.]*\\/[\\w.$]+)\\s*$/gm)].map(m => m[1]);\n const handlers = parse(specific);\n const browsers = new Set(parse(generic).map(c => c.split('/')[0]));\n const candidates = handlers.filter(c => !browsers.has(c.split('/')[0]));\n this.emit(\n 'log',\n 'debug',\n `${this.options.host}: link handlers ${JSON.stringify(handlers)}, apps ${JSON.stringify(candidates)}`,\n );\n return { component: candidates[0], browserOnly: handlers.length > 0 && candidates.length === 0 };\n } catch (e) {\n this.emit('log', 'debug', `${this.options.host}: cannot resolve link handler: ${(e as Error).message}`);\n return { browserOnly: false };\n }\n }\n\n /**\n * Lists Android settings that look like Wake-on-LAN / network standby switches (diagnostics).\n *\n * @returns Matching lines \"namespace: key=value\"\n */\n public async findWakeSettings(): Promise<string[]> {\n const out = await this.client.shell(\n 'for n in global secure system; do settings list $n 2>/dev/null | grep -iE \"wol|wake_on|wakeonlan|network_standby|lan_wake|networkstandby\" | sed \"s/^/$n: /\"; done',\n );\n return out\n .split('\\n')\n .map(l => l.trim())\n .filter(Boolean);\n }\n\n /** Retries a pending ADB reconnect immediately (e.g. after Wake-on-LAN). */\n public reconnectNow(): void {\n this.client.reconnectNow();\n }\n\n /**\n * Reads a system property, e.g. ro.product.manufacturer.\n *\n * @param name - Property name\n */\n public async getProp(name: string): Promise<string | undefined> {\n if (!/^[\\w.]+$/.test(name)) {\n throw new Error(`Invalid property name \"${name}\"`);\n }\n const value = (await this.client.shell(`getprop ${name}`)).trim();\n return value || undefined;\n }\n\n /**\n * Types text into the focused input field via `input text`.\n *\n * @param text - Text (spaces are supported)\n */\n public async sendText(text: string): Promise<void> {\n if (!text) {\n throw new Error('Text must not be empty');\n }\n // `input text` expects %s for spaces; everything else is quoted for the shell\n await this.client.shell(`input text ${shellQuote(text.replace(/ /g, '%s'))}`);\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/lib/remote.js
CHANGED
|
@@ -92,6 +92,16 @@ class RemoteConnection extends import_node_events.EventEmitter {
|
|
|
92
92
|
this.reconnectTimer = void 0;
|
|
93
93
|
this.teardown();
|
|
94
94
|
}
|
|
95
|
+
/** Retries a pending reconnect immediately with the shortest backoff (e.g. after Wake-on-LAN). */
|
|
96
|
+
reconnectNow() {
|
|
97
|
+
if (!this.running || this.ready || !this.reconnectTimer) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.timers.clearTimeout(this.reconnectTimer);
|
|
101
|
+
this.reconnectTimer = void 0;
|
|
102
|
+
this.reconnectDelay = this.minDelay;
|
|
103
|
+
this.connect();
|
|
104
|
+
}
|
|
95
105
|
/** Drops the current connection and reconnects immediately (e.g. after a new pairing). */
|
|
96
106
|
restart() {
|
|
97
107
|
this.stop();
|