iobroker.fakeroku 1.8.2 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -19
- package/admin/i18n/de.json +1 -11
- package/admin/i18n/en.json +1 -11
- package/admin/i18n/es.json +1 -11
- package/admin/i18n/fr.json +1 -11
- package/admin/i18n/it.json +1 -11
- package/admin/i18n/nl.json +1 -11
- package/admin/i18n/pl.json +1 -11
- package/admin/i18n/pt.json +1 -11
- package/admin/i18n/ru.json +1 -11
- package/admin/i18n/uk.json +1 -11
- package/admin/i18n/zh-cn.json +1 -11
- package/build/command-handler.js +1 -2
- package/build/command-handler.js.map +2 -2
- package/build/discovery/ssdp-responder.js.map +1 -1
- package/build/ecp/device-info.js +2 -19
- package/build/ecp/device-info.js.map +2 -2
- package/build/ecp/ecp-command.js.map +1 -1
- package/build/ecp/ecp-http-server.js +1 -9
- package/build/ecp/ecp-http-server.js.map +2 -2
- package/build/ecp/state-model.js +0 -1
- package/build/ecp/state-model.js.map +2 -2
- package/build/lib/detect-ip.js +7 -71
- package/build/lib/detect-ip.js.map +2 -2
- package/build/lib/device-config.js +1 -0
- package/build/lib/device-config.js.map +2 -2
- package/build/lib/i18n.js +2 -7
- package/build/lib/i18n.js.map +2 -2
- package/build/lib/network-address.js +85 -0
- package/build/lib/network-address.js.map +7 -0
- package/build/lib/object-cleanup.js +3 -2
- package/build/lib/object-cleanup.js.map +2 -2
- package/build/main.js +219 -259
- package/build/main.js.map +2 -2
- package/io-package.json +14 -14
- package/package.json +3 -3
- package/build/lib/lan-guard.js +0 -43
- package/build/lib/lan-guard.js.map +0 -7
package/README.md
CHANGED
|
@@ -9,14 +9,12 @@
|
|
|
9
9
|
> [!IMPORTANT]
|
|
10
10
|
> This adapter cannot be installed from github
|
|
11
11
|
|
|
12
|
-
Emulates one or more **[Roku](https://www.roku.com/) devices** on your LAN so that
|
|
13
|
-
|
|
14
|
-
openHAB — can trigger events in ioBroker. It is the **input** counterpart to the Logitech
|
|
12
|
+
Emulates one or more **[Roku](https://www.roku.com/) devices** on your LAN so that a Logitech Harmony Hub or
|
|
13
|
+
a Sofabaton X1/X2 can trigger events in ioBroker. It is the **input** counterpart to the Logitech
|
|
15
14
|
Harmony adapter: a button on the remote becomes a datapoint in ioBroker.
|
|
16
15
|
|
|
17
|
-
Besides the key presses it answers
|
|
18
|
-
|
|
19
|
-
so they accept the emulated device instead of giving up on it.
|
|
16
|
+
Besides the key presses it answers what these remotes read when they pair — device info,
|
|
17
|
+
app list, app icons.
|
|
20
18
|
|
|
21
19
|
> **The official Roku mobile app is not supported.** It drives Rokus over Roku's
|
|
22
20
|
> proprietary, undocumented ECP-2 WebSocket channel, which this emulator does not
|
|
@@ -76,19 +74,18 @@ At instance level:
|
|
|
76
74
|
|
|
77
75
|
| Datapoint | Type | Meaning |
|
|
78
76
|
|---|---|---|
|
|
79
|
-
| `info.connection` | boolean, read-only | `true` only while **every** configured Roku
|
|
77
|
+
| `info.connection` | boolean, read-only | `true` only while everything runs: **every** configured Roku listens, discovery answers, and the chosen address exists. Anything less shows the instance yellow; the log names the cause once, and the adapter retries a Roku or discovery that is down every minute. |
|
|
80
78
|
|
|
81
79
|
For every emulated Roku (`fakeroku.0.<name>`):
|
|
82
80
|
|
|
83
81
|
| Datapoint | Type | Meaning |
|
|
84
82
|
|---|---|---|
|
|
85
83
|
| `.command` | string, read-only | The last command as plain text (`Home`, `Lit_a`, `launch:12`, `search:news`). One datapoint for everything — no object-per-character sprawl. |
|
|
86
|
-
| `.commandType` | string, read-only | `keypress` / `keydown` / `keyup` / `launch` / `install` / `input` / `search`. |
|
|
87
84
|
| `.keys.<Key>` | boolean, read-only | One state per remote key the device type exposes — a *Player* has the 16 navigation/playback keys, a *TV* adds Volume\*, PowerOn/PowerOff/Power, Sleep, Channel\*, Tuner/HDMI/AV inputs — all created up front. A keypress pulses it `true` for a moment; keydown/keyup hold it. Key names are read in any case (`home` is `Home`). |
|
|
88
85
|
|
|
89
86
|
Free keyboard input (`Lit_x`) and app launches show up in `.command` only — they do
|
|
90
87
|
not get their own objects. An app button of a remote that sends app launches (a
|
|
91
|
-
Sofabaton
|
|
88
|
+
Sofabaton) arrives as `launch:<id>`; a Harmony Hub did not send its app
|
|
92
89
|
buttons to the emulated Roku.
|
|
93
90
|
|
|
94
91
|
> Note: the Roku remote sends the **same** `Play` command for play and pause, so
|
|
@@ -103,6 +100,16 @@ becomes `true` — or watch `.command` for the last button as text.
|
|
|
103
100
|
Placeholder for the next version (at the beginning of the line):
|
|
104
101
|
### **WORK IN PROGRESS**
|
|
105
102
|
-->
|
|
103
|
+
### 1.9.0 (2026-10-03)
|
|
104
|
+
|
|
105
|
+
- (krobipd) Changed: the emulated Roku answers only what a Harmony or Sofabaton reads — Home Assistant and openHAB can no longer set it up.
|
|
106
|
+
- (krobipd) Changed: the instance shows green only while every Roku and discovery run, and yellow otherwise.
|
|
107
|
+
- (krobipd) Changed: a Roku whose server stopped while running and a discovery that failed start again within a minute instead of waiting for a restart.
|
|
108
|
+
- (krobipd) Changed: with no Roku configured or none running, the instance stays up so the device manager works, and asks you to add a Roku.
|
|
109
|
+
- (krobipd) Changed: a chosen address that is missing on the host no longer leaves the Rokus off — they listen on all addresses until you choose another.
|
|
110
|
+
- (krobipd) Changed: requests from the ioBroker host itself or from self-assigned addresses outside the host's networks are refused.
|
|
111
|
+
- (krobipd) Changed: the datapoint with the kind of the last command is gone — the last command itself still arrives as before; existing installations drop it on the next start.
|
|
112
|
+
|
|
106
113
|
### 1.8.2 (2026-10-02)
|
|
107
114
|
|
|
108
115
|
- (krobipd) Changed: a new instance starts switched off — check the settings, then switch it on.
|
|
@@ -133,16 +140,6 @@ becomes `true` — or watch `.command` for the last button as text.
|
|
|
133
140
|
|
|
134
141
|
- (krobipd) Changed: the instance restarts once after this update to clear a setting left behind by an older version; nothing else changes for you.
|
|
135
142
|
|
|
136
|
-
### 1.7.0 (2026-09-16)
|
|
137
|
-
|
|
138
|
-
- (krobipd) Fixed: button datapoints keep their value and their room and function assignment when the adapter starts.
|
|
139
|
-
- (krobipd) Fixed: after an emulated Roku drops out, its port is free again instead of staying blocked until ioBroker restarts.
|
|
140
|
-
- (krobipd) Fixed: stopping the instance no longer leaves it reported as connected.
|
|
141
|
-
- (krobipd) Fixed: a key you hold right after a short press stays pressed instead of being released early.
|
|
142
|
-
- (krobipd) Fixed: the device dialog now also refuses a name that would collide with an existing device in the object tree.
|
|
143
|
-
- (krobipd) Improved: after the host gets a new IP address, remotes find the emulated Rokus again without restarting the instance.
|
|
144
|
-
- (krobipd) Changed: the network interface setting moved to the standard settings key (bind); the instance restarts once after this update.
|
|
145
|
-
|
|
146
143
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
147
144
|
|
|
148
145
|
## History
|
package/admin/i18n/de.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"deviceName": "Name",
|
|
8
8
|
"devicePort": "ECP-Port",
|
|
9
9
|
"portLabel": "Port",
|
|
10
|
-
"devicePortHint": "Der ECP-Port, auf dem dieser Roku antwortet. Jeder emulierte Roku braucht seinen eigenen Port. Harmony und Sofabaton lesen den Port aus der Erkennung
|
|
10
|
+
"devicePortHint": "Der ECP-Port, auf dem dieser Roku antwortet. Jeder emulierte Roku braucht seinen eigenen Port. Harmony und Sofabaton lesen den Port aus der Erkennung.",
|
|
11
11
|
"deviceTypeLabel": "Typ",
|
|
12
12
|
"deviceTypePlayer": "Player (Streaming-Box)",
|
|
13
13
|
"deviceTypeTv": "TV (mit Lautstärke, Ein/Aus, Kanal, Eingang)",
|
|
@@ -30,16 +30,6 @@
|
|
|
30
30
|
"connectionStatusDesc": "Nur wahr, solange jeder konfigurierte emulierte Roku tatsächlich lauscht.",
|
|
31
31
|
"stateLastCommand": "Letzter Befehl",
|
|
32
32
|
"stateLastCommandDesc": "Der zuletzt empfangene Befehl dieses emulierten Roku als Klartext.",
|
|
33
|
-
"stateLastCommandType": "Art des letzten Befehls",
|
|
34
|
-
"stateLastCommandTypeDesc": "Um welche Art Befehl es sich handelte: Tastendruck, Taste gehalten oder losgelassen, App-Start oder -Installation, Texteingabe oder Suche.",
|
|
35
|
-
"commandType_none": "Noch kein Befehl",
|
|
36
|
-
"commandType_keypress": "Tastendruck",
|
|
37
|
-
"commandType_keydown": "Taste gehalten",
|
|
38
|
-
"commandType_keyup": "Taste losgelassen",
|
|
39
|
-
"commandType_launch": "App-Start",
|
|
40
|
-
"commandType_install": "App-Installation",
|
|
41
|
-
"commandType_input": "Texteingabe",
|
|
42
|
-
"commandType_search": "Suche",
|
|
43
33
|
"channelKeys": "Fernbedienungstasten",
|
|
44
34
|
"channelKeysDesc": "Ein Datenpunkt je Taste, die dieses Gerät anbietet; ein Tastendruck setzt ihn kurz auf wahr."
|
|
45
35
|
}
|
package/admin/i18n/en.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"deviceName": "Name",
|
|
8
8
|
"devicePort": "ECP port",
|
|
9
9
|
"portLabel": "Port",
|
|
10
|
-
"devicePortHint": "The ECP port this Roku answers on. Each emulated Roku needs its own port. Harmony and Sofabaton read the port from the discovery
|
|
10
|
+
"devicePortHint": "The ECP port this Roku answers on. Each emulated Roku needs its own port. Harmony and Sofabaton read the port from the discovery.",
|
|
11
11
|
"deviceTypeLabel": "Type",
|
|
12
12
|
"deviceTypePlayer": "Player (streaming box)",
|
|
13
13
|
"deviceTypeTv": "TV (adds volume, power, channel, input)",
|
|
@@ -30,16 +30,6 @@
|
|
|
30
30
|
"connectionStatusDesc": "True only while every configured emulated Roku is actually listening.",
|
|
31
31
|
"stateLastCommand": "Last command",
|
|
32
32
|
"stateLastCommandDesc": "The most recent command this emulated Roku received, as plain text.",
|
|
33
|
-
"stateLastCommandType": "Last command type",
|
|
34
|
-
"stateLastCommandTypeDesc": "What kind of command it was: a key press, a key held or released, an app launch or install, a text input or a search.",
|
|
35
|
-
"commandType_none": "No command yet",
|
|
36
|
-
"commandType_keypress": "Key press",
|
|
37
|
-
"commandType_keydown": "Key held",
|
|
38
|
-
"commandType_keyup": "Key released",
|
|
39
|
-
"commandType_launch": "App launch",
|
|
40
|
-
"commandType_install": "App install",
|
|
41
|
-
"commandType_input": "Text input",
|
|
42
|
-
"commandType_search": "Search",
|
|
43
33
|
"channelKeys": "Remote keys",
|
|
44
34
|
"channelKeysDesc": "One state per remote key this device offers; a key press pulses it briefly."
|
|
45
35
|
}
|
package/admin/i18n/es.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"deviceName": "Nombre",
|
|
8
8
|
"devicePort": "Puerto ECP",
|
|
9
9
|
"portLabel": "Puerto",
|
|
10
|
-
"devicePortHint": "El puerto ECP en el que responde este Roku. Cada Roku emulado necesita su propio puerto. Harmony y Sofabaton leen el puerto del descubrimiento
|
|
10
|
+
"devicePortHint": "El puerto ECP en el que responde este Roku. Cada Roku emulado necesita su propio puerto. Harmony y Sofabaton leen el puerto del descubrimiento.",
|
|
11
11
|
"deviceTypeLabel": "Tipo",
|
|
12
12
|
"deviceTypePlayer": "Reproductor (dispositivo de streaming)",
|
|
13
13
|
"deviceTypeTv": "TV (con volumen, encendido, canal, entrada)",
|
|
@@ -30,16 +30,6 @@
|
|
|
30
30
|
"connectionStatusDesc": "Verdadero solo mientras cada Roku emulado configurado esté realmente a la escucha.",
|
|
31
31
|
"stateLastCommand": "Último comando",
|
|
32
32
|
"stateLastCommandDesc": "El comando más reciente que recibió este Roku emulado, como texto simple.",
|
|
33
|
-
"stateLastCommandType": "Tipo del último comando",
|
|
34
|
-
"stateLastCommandTypeDesc": "Qué tipo de comando fue: pulsar una tecla, mantenerla o soltarla, iniciar o instalar una aplicación, introducir texto o buscar.",
|
|
35
|
-
"commandType_none": "Aún sin comando",
|
|
36
|
-
"commandType_keypress": "Pulsación de tecla",
|
|
37
|
-
"commandType_keydown": "Tecla mantenida",
|
|
38
|
-
"commandType_keyup": "Tecla soltada",
|
|
39
|
-
"commandType_launch": "Inicio de app",
|
|
40
|
-
"commandType_install": "Instalación de app",
|
|
41
|
-
"commandType_input": "Entrada de texto",
|
|
42
|
-
"commandType_search": "Búsqueda",
|
|
43
33
|
"channelKeys": "Teclas del mando",
|
|
44
34
|
"channelKeysDesc": "Un estado por cada tecla que ofrece este dispositivo; una pulsación lo pone brevemente en verdadero."
|
|
45
35
|
}
|
package/admin/i18n/fr.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"deviceName": "Nom",
|
|
8
8
|
"devicePort": "Port ECP",
|
|
9
9
|
"portLabel": "Port",
|
|
10
|
-
"devicePortHint": "Le port ECP sur lequel ce Roku répond. Chaque Roku émulé a besoin de son propre port. Harmony et Sofabaton lisent le port dans la découverte
|
|
10
|
+
"devicePortHint": "Le port ECP sur lequel ce Roku répond. Chaque Roku émulé a besoin de son propre port. Harmony et Sofabaton lisent le port dans la découverte.",
|
|
11
11
|
"deviceTypeLabel": "Type",
|
|
12
12
|
"deviceTypePlayer": "Player (box de streaming)",
|
|
13
13
|
"deviceTypeTv": "TV (avec volume, marche/arrêt, chaîne, entrée)",
|
|
@@ -30,16 +30,6 @@
|
|
|
30
30
|
"connectionStatusDesc": "Vrai uniquement tant que chaque Roku émulé configuré est réellement à l'écoute.",
|
|
31
31
|
"stateLastCommand": "Dernière commande",
|
|
32
32
|
"stateLastCommandDesc": "La commande la plus récente reçue par ce Roku émulé, en texte brut.",
|
|
33
|
-
"stateLastCommandType": "Type de la dernière commande",
|
|
34
|
-
"stateLastCommandTypeDesc": "De quel type de commande il s'agissait : appui sur une touche, touche maintenue ou relâchée, lancement ou installation d'une application, saisie de texte ou recherche.",
|
|
35
|
-
"commandType_none": "Aucune commande pour l'instant",
|
|
36
|
-
"commandType_keypress": "Appui sur une touche",
|
|
37
|
-
"commandType_keydown": "Touche maintenue",
|
|
38
|
-
"commandType_keyup": "Touche relâchée",
|
|
39
|
-
"commandType_launch": "Lancement d'appli",
|
|
40
|
-
"commandType_install": "Installation d'appli",
|
|
41
|
-
"commandType_input": "Saisie de texte",
|
|
42
|
-
"commandType_search": "Recherche",
|
|
43
33
|
"channelKeys": "Touches de la télécommande",
|
|
44
34
|
"channelKeysDesc": "Un état par touche proposée par cet appareil ; un appui le met brièvement à vrai."
|
|
45
35
|
}
|
package/admin/i18n/it.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"deviceName": "Nome",
|
|
8
8
|
"devicePort": "Porta ECP",
|
|
9
9
|
"portLabel": "Porta",
|
|
10
|
-
"devicePortHint": "La porta ECP su cui risponde questo Roku. Ogni Roku emulato ha bisogno della propria porta. Harmony e Sofabaton leggono la porta dal rilevamento
|
|
10
|
+
"devicePortHint": "La porta ECP su cui risponde questo Roku. Ogni Roku emulato ha bisogno della propria porta. Harmony e Sofabaton leggono la porta dal rilevamento.",
|
|
11
11
|
"deviceTypeLabel": "Tipo",
|
|
12
12
|
"deviceTypePlayer": "Player (box di streaming)",
|
|
13
13
|
"deviceTypeTv": "TV (con volume, accensione, canale, ingresso)",
|
|
@@ -30,16 +30,6 @@
|
|
|
30
30
|
"connectionStatusDesc": "Vero solo finché ogni Roku emulato configurato è effettivamente in ascolto.",
|
|
31
31
|
"stateLastCommand": "Ultimo comando",
|
|
32
32
|
"stateLastCommandDesc": "Il comando più recente ricevuto da questo Roku emulato, come testo semplice.",
|
|
33
|
-
"stateLastCommandType": "Tipo dell'ultimo comando",
|
|
34
|
-
"stateLastCommandTypeDesc": "Di che tipo di comando si trattava: pressione di un tasto, tasto tenuto premuto o rilasciato, avvio o installazione di un'app, immissione di testo o ricerca.",
|
|
35
|
-
"commandType_none": "Ancora nessun comando",
|
|
36
|
-
"commandType_keypress": "Pressione tasto",
|
|
37
|
-
"commandType_keydown": "Tasto tenuto premuto",
|
|
38
|
-
"commandType_keyup": "Tasto rilasciato",
|
|
39
|
-
"commandType_launch": "Avvio app",
|
|
40
|
-
"commandType_install": "Installazione app",
|
|
41
|
-
"commandType_input": "Inserimento testo",
|
|
42
|
-
"commandType_search": "Ricerca",
|
|
43
33
|
"channelKeys": "Tasti del telecomando",
|
|
44
34
|
"channelKeysDesc": "Uno stato per ogni tasto offerto da questo dispositivo; una pressione lo porta brevemente a vero."
|
|
45
35
|
}
|
package/admin/i18n/nl.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"deviceName": "Naam",
|
|
8
8
|
"devicePort": "ECP-poort",
|
|
9
9
|
"portLabel": "Poort",
|
|
10
|
-
"devicePortHint": "De ECP-poort waarop deze Roku antwoordt. Elke geëmuleerde Roku heeft een eigen poort nodig. Harmony en Sofabaton lezen de poort uit de detectie
|
|
10
|
+
"devicePortHint": "De ECP-poort waarop deze Roku antwoordt. Elke geëmuleerde Roku heeft een eigen poort nodig. Harmony en Sofabaton lezen de poort uit de detectie.",
|
|
11
11
|
"deviceTypeLabel": "Type",
|
|
12
12
|
"deviceTypePlayer": "Player (streaming-box)",
|
|
13
13
|
"deviceTypeTv": "TV (met volume, aan/uit, kanaal, ingang)",
|
|
@@ -30,16 +30,6 @@
|
|
|
30
30
|
"connectionStatusDesc": "Alleen waar zolang elke geconfigureerde geëmuleerde Roku daadwerkelijk luistert.",
|
|
31
31
|
"stateLastCommand": "Laatste opdracht",
|
|
32
32
|
"stateLastCommandDesc": "De meest recente opdracht die deze geëmuleerde Roku ontving, als platte tekst.",
|
|
33
|
-
"stateLastCommandType": "Type laatste opdracht",
|
|
34
|
-
"stateLastCommandTypeDesc": "Wat voor soort opdracht het was: een toetsdruk, een toets ingedrukt of losgelaten, een app starten of installeren, tekstinvoer of zoeken.",
|
|
35
|
-
"commandType_none": "Nog geen opdracht",
|
|
36
|
-
"commandType_keypress": "Toetsdruk",
|
|
37
|
-
"commandType_keydown": "Toets vastgehouden",
|
|
38
|
-
"commandType_keyup": "Toets losgelaten",
|
|
39
|
-
"commandType_launch": "App starten",
|
|
40
|
-
"commandType_install": "App installeren",
|
|
41
|
-
"commandType_input": "Tekstinvoer",
|
|
42
|
-
"commandType_search": "Zoeken",
|
|
43
33
|
"channelKeys": "Afstandsbedieningstoetsen",
|
|
44
34
|
"channelKeysDesc": "Eén status per toets die dit apparaat biedt; een toetsdruk zet hem kort op waar."
|
|
45
35
|
}
|
package/admin/i18n/pl.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"deviceName": "Nazwa",
|
|
8
8
|
"devicePort": "Port ECP",
|
|
9
9
|
"portLabel": "Port",
|
|
10
|
-
"devicePortHint": "Port ECP, na którym odpowiada ten Roku. Każdy emulowany Roku potrzebuje własnego portu. Harmony i Sofabaton odczytują port z wykrywania
|
|
10
|
+
"devicePortHint": "Port ECP, na którym odpowiada ten Roku. Każdy emulowany Roku potrzebuje własnego portu. Harmony i Sofabaton odczytują port z wykrywania.",
|
|
11
11
|
"deviceTypeLabel": "Typ",
|
|
12
12
|
"deviceTypePlayer": "Player (przystawka)",
|
|
13
13
|
"deviceTypeTv": "TV (z głośnością, zasilaniem, kanałem, wejściem)",
|
|
@@ -30,16 +30,6 @@
|
|
|
30
30
|
"connectionStatusDesc": "Prawda tylko wtedy, gdy każdy skonfigurowany emulowany Roku faktycznie nasłuchuje.",
|
|
31
31
|
"stateLastCommand": "Ostatnie polecenie",
|
|
32
32
|
"stateLastCommandDesc": "Najnowsze polecenie odebrane przez ten emulowany Roku, jako zwykły tekst.",
|
|
33
|
-
"stateLastCommandType": "Typ ostatniego polecenia",
|
|
34
|
-
"stateLastCommandTypeDesc": "Jakiego rodzaju było to polecenie: naciśnięcie klawisza, przytrzymanie lub zwolnienie, uruchomienie lub instalacja aplikacji, wpisanie tekstu albo wyszukiwanie.",
|
|
35
|
-
"commandType_none": "Jeszcze brak polecenia",
|
|
36
|
-
"commandType_keypress": "Naciśnięcie klawisza",
|
|
37
|
-
"commandType_keydown": "Klawisz przytrzymany",
|
|
38
|
-
"commandType_keyup": "Klawisz zwolniony",
|
|
39
|
-
"commandType_launch": "Uruchomienie aplikacji",
|
|
40
|
-
"commandType_install": "Instalacja aplikacji",
|
|
41
|
-
"commandType_input": "Wprowadzanie tekstu",
|
|
42
|
-
"commandType_search": "Wyszukiwanie",
|
|
43
33
|
"channelKeys": "Klawisze pilota",
|
|
44
34
|
"channelKeysDesc": "Jeden stan na każdy klawisz oferowany przez to urządzenie; naciśnięcie ustawia go na krótko na wartość true."
|
|
45
35
|
}
|
package/admin/i18n/pt.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"deviceName": "Nome",
|
|
8
8
|
"devicePort": "Porta ECP",
|
|
9
9
|
"portLabel": "Porta",
|
|
10
|
-
"devicePortHint": "A porta ECP em que este Roku responde. Cada Roku emulado precisa da sua própria porta. O Harmony e o Sofabaton leem a porta na descoberta
|
|
10
|
+
"devicePortHint": "A porta ECP em que este Roku responde. Cada Roku emulado precisa da sua própria porta. O Harmony e o Sofabaton leem a porta na descoberta.",
|
|
11
11
|
"deviceTypeLabel": "Tipo",
|
|
12
12
|
"deviceTypePlayer": "Player (box de streaming)",
|
|
13
13
|
"deviceTypeTv": "TV (com volume, ligar/desligar, canal, entrada)",
|
|
@@ -30,16 +30,6 @@
|
|
|
30
30
|
"connectionStatusDesc": "Verdadeiro apenas enquanto todos os Roku emulados configurados estiverem realmente à escuta.",
|
|
31
31
|
"stateLastCommand": "Último comando",
|
|
32
32
|
"stateLastCommandDesc": "O comando mais recente recebido por este Roku emulado, em texto simples.",
|
|
33
|
-
"stateLastCommandType": "Tipo do último comando",
|
|
34
|
-
"stateLastCommandTypeDesc": "Que tipo de comando foi: pressionar uma tecla, manter ou soltar, iniciar ou instalar uma aplicação, introduzir texto ou pesquisar.",
|
|
35
|
-
"commandType_none": "Ainda sem comando",
|
|
36
|
-
"commandType_keypress": "Toque de tecla",
|
|
37
|
-
"commandType_keydown": "Tecla mantida",
|
|
38
|
-
"commandType_keyup": "Tecla solta",
|
|
39
|
-
"commandType_launch": "Abrir aplicação",
|
|
40
|
-
"commandType_install": "Instalar aplicação",
|
|
41
|
-
"commandType_input": "Entrada de texto",
|
|
42
|
-
"commandType_search": "Pesquisa",
|
|
43
33
|
"channelKeys": "Teclas do comando",
|
|
44
34
|
"channelKeysDesc": "Um estado por cada tecla que este dispositivo oferece; uma pressão coloca-o brevemente a verdadeiro."
|
|
45
35
|
}
|
package/admin/i18n/ru.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"deviceName": "Имя",
|
|
8
8
|
"devicePort": "Порт ECP",
|
|
9
9
|
"portLabel": "Порт",
|
|
10
|
-
"devicePortHint": "Порт ECP, на котором отвечает этот Roku. Каждому эмулированному Roku нужен свой порт. Harmony и Sofabaton берут порт из
|
|
10
|
+
"devicePortHint": "Порт ECP, на котором отвечает этот Roku. Каждому эмулированному Roku нужен свой порт. Harmony и Sofabaton берут порт из обнаружения.",
|
|
11
11
|
"deviceTypeLabel": "Тип",
|
|
12
12
|
"deviceTypePlayer": "Проигрыватель (приставка)",
|
|
13
13
|
"deviceTypeTv": "ТВ (с громкостью, питанием, каналом, входом)",
|
|
@@ -30,16 +30,6 @@
|
|
|
30
30
|
"connectionStatusDesc": "Истина только пока каждый настроенный эмулированный Roku действительно слушает.",
|
|
31
31
|
"stateLastCommand": "Последняя команда",
|
|
32
32
|
"stateLastCommandDesc": "Последняя команда, полученная этим эмулированным Roku, в виде текста.",
|
|
33
|
-
"stateLastCommandType": "Тип последней команды",
|
|
34
|
-
"stateLastCommandTypeDesc": "Какого рода была команда: нажатие кнопки, удержание или отпускание, запуск или установка приложения, ввод текста или поиск.",
|
|
35
|
-
"commandType_none": "Команд ещё не было",
|
|
36
|
-
"commandType_keypress": "Нажатие клавиши",
|
|
37
|
-
"commandType_keydown": "Клавиша удерживается",
|
|
38
|
-
"commandType_keyup": "Клавиша отпущена",
|
|
39
|
-
"commandType_launch": "Запуск приложения",
|
|
40
|
-
"commandType_install": "Установка приложения",
|
|
41
|
-
"commandType_input": "Ввод текста",
|
|
42
|
-
"commandType_search": "Поиск",
|
|
43
33
|
"channelKeys": "Кнопки пульта",
|
|
44
34
|
"channelKeysDesc": "По одному состоянию на каждую кнопку пульта этого устройства; нажатие кратко переводит его в истину."
|
|
45
35
|
}
|
package/admin/i18n/uk.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"deviceName": "Назва",
|
|
8
8
|
"devicePort": "Порт ECP",
|
|
9
9
|
"portLabel": "Порт",
|
|
10
|
-
"devicePortHint": "Порт ECP, на якому відповідає цей Roku. Кожному емульованому Roku потрібен власний порт. Harmony і Sofabaton беруть порт із
|
|
10
|
+
"devicePortHint": "Порт ECP, на якому відповідає цей Roku. Кожному емульованому Roku потрібен власний порт. Harmony і Sofabaton беруть порт із виявлення.",
|
|
11
11
|
"deviceTypeLabel": "Тип",
|
|
12
12
|
"deviceTypePlayer": "Програвач (приставка)",
|
|
13
13
|
"deviceTypeTv": "ТБ (з гучністю, живленням, каналом, входом)",
|
|
@@ -30,16 +30,6 @@
|
|
|
30
30
|
"connectionStatusDesc": "Істина лише поки кожен налаштований емульований Roku справді слухає.",
|
|
31
31
|
"stateLastCommand": "Остання команда",
|
|
32
32
|
"stateLastCommandDesc": "Найновіша команда, яку отримав цей емульований Roku, у вигляді тексту.",
|
|
33
|
-
"stateLastCommandType": "Тип останньої команди",
|
|
34
|
-
"stateLastCommandTypeDesc": "Якого роду була команда: натискання кнопки, утримання чи відпускання, запуск або встановлення застосунку, введення тексту чи пошук.",
|
|
35
|
-
"commandType_none": "Команд ще не було",
|
|
36
|
-
"commandType_keypress": "Натискання клавіші",
|
|
37
|
-
"commandType_keydown": "Клавіша утримується",
|
|
38
|
-
"commandType_keyup": "Клавішу відпущено",
|
|
39
|
-
"commandType_launch": "Запуск застосунку",
|
|
40
|
-
"commandType_install": "Встановлення застосунку",
|
|
41
|
-
"commandType_input": "Введення тексту",
|
|
42
|
-
"commandType_search": "Пошук",
|
|
43
33
|
"channelKeys": "Кнопки пульта",
|
|
44
34
|
"channelKeysDesc": "По одному стану на кожну кнопку, яку пропонує цей пристрій; натискання ненадовго переводить його в істину."
|
|
45
35
|
}
|
package/admin/i18n/zh-cn.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"deviceName": "名称",
|
|
8
8
|
"devicePort": "ECP 端口",
|
|
9
9
|
"portLabel": "端口",
|
|
10
|
-
"devicePortHint": "此 Roku 响应的 ECP 端口。每个模拟 Roku 都需要自己的端口。Harmony 和 Sofabaton
|
|
10
|
+
"devicePortHint": "此 Roku 响应的 ECP 端口。每个模拟 Roku 都需要自己的端口。Harmony 和 Sofabaton 从发现信息中读取端口。",
|
|
11
11
|
"deviceTypeLabel": "类型",
|
|
12
12
|
"deviceTypePlayer": "播放器(流媒体盒子)",
|
|
13
13
|
"deviceTypeTv": "电视(带音量、电源、频道、输入)",
|
|
@@ -30,16 +30,6 @@
|
|
|
30
30
|
"connectionStatusDesc": "仅当每个已配置的模拟 Roku 都在监听时才为真。",
|
|
31
31
|
"stateLastCommand": "最后一条命令",
|
|
32
32
|
"stateLastCommandDesc": "该模拟 Roku 最近收到的命令,以纯文本表示。",
|
|
33
|
-
"stateLastCommandType": "最后一条命令的类型",
|
|
34
|
-
"stateLastCommandTypeDesc": "命令的种类:按键、按住或松开按键、启动或安装应用、输入文本或搜索。",
|
|
35
|
-
"commandType_none": "尚无命令",
|
|
36
|
-
"commandType_keypress": "按键",
|
|
37
|
-
"commandType_keydown": "按住按键",
|
|
38
|
-
"commandType_keyup": "松开按键",
|
|
39
|
-
"commandType_launch": "启动应用",
|
|
40
|
-
"commandType_install": "安装应用",
|
|
41
|
-
"commandType_input": "文本输入",
|
|
42
|
-
"commandType_search": "搜索",
|
|
43
33
|
"channelKeys": "遥控器按键",
|
|
44
34
|
"channelKeysDesc": "该设备提供的每个遥控器按键各有一个状态;按下时会短暂置为真。"
|
|
45
35
|
}
|
package/build/command-handler.js
CHANGED
|
@@ -46,7 +46,7 @@ class CommandHandler {
|
|
|
46
46
|
gates = /* @__PURE__ */ new Map();
|
|
47
47
|
rateWarnings = new import_log_throttle.LogThrottle(RATE_WARN_INTERVAL_MS);
|
|
48
48
|
/**
|
|
49
|
-
* Apply a received ECP command to the device's states: record it in `command
|
|
49
|
+
* Apply a received ECP command to the device's states: record it in `command`, and pulse or hold
|
|
50
50
|
* the standard key if the device carries it. The server logs only what this accepted, so the rate gate covers the
|
|
51
51
|
* log as well as the states database.
|
|
52
52
|
*
|
|
@@ -63,7 +63,6 @@ class CommandHandler {
|
|
|
63
63
|
return false;
|
|
64
64
|
}
|
|
65
65
|
this.host.writeState(`${device.id}.command`, write.command);
|
|
66
|
-
this.host.writeState(`${device.id}.commandType`, write.commandType);
|
|
67
66
|
if (write.pulseKey && device.keys.has(write.pulseKey)) {
|
|
68
67
|
this.pulse(`${device.id}.keys.${write.pulseKey}`);
|
|
69
68
|
} else if (write.holdKey && device.keys.has(write.holdKey.key)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/command-handler.ts"],
|
|
4
|
-
"sourcesContent": ["import type { CommandEvent } from \"./ecp/ecp-command\";\nimport { commandToStateWrite } from \"./ecp/state-model\";\nimport { LogThrottle } from \"./lib/log-throttle\";\nimport { RateGate } from \"./lib/rate-gate\";\n\n/** How long a keypress pulses its keys.<Key> state true before falling back to false. */\nexport const KEY_PULSE_MS = 50;\n/** Safety cap for a held key: a keydown with no matching keyup resets after this. */\nexport const HOLD_MAX_MS = 30_000;\n/** Commands accepted per second and emulated Roku; the excess is dropped (see lib/rate-gate.ts). */\nexport const MAX_COMMANDS_PER_SECOND = 25;\n/** How often at most the dropped-commands warning repeats per device. */\nexport const RATE_WARN_INTERVAL_MS = 60_000;\n\n/** The emulated Roku a command is for. */\nexport interface CommandTarget {\n /** The id-safe device path segment. */\n readonly id: string;\n /** The configured device name, for the warning the user reads. */\n readonly name: string;\n /** The key names this device's type carries \u2014 only those have a state to write. */\n readonly keys: ReadonlySet<string>;\n}\n\n/** What the handler needs from the adapter: writing, the managed timers and the log. */\nexport interface CommandHost {\n /** Fire-and-forget state write (relative id). */\n writeState(id: string, val: string | boolean): void;\n /**\n * The adapter's managed timeout \u2014 undefined while the adapter shuts down. A property, not a method signature: the\n * repochecker reads a timer method signature as a plain timer call (S5005).\n */\n setTimeout: (callback: () => void, ms: number) => ioBroker.Timeout | undefined;\n /** The adapter's managed clearTimeout. */\n clearTimeout: (timer: ioBroker.Timeout) => void;\n /** A warning the user should see. */\n warn(message: string): void;\n}\n\n/**\n * Turns the ECP commands of every emulated Roku into state writes: `command
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,yBAAoC;AACpC,0BAA4B;AAC5B,uBAAyB;AAGlB,MAAM,eAAe;AAErB,MAAM,cAAc;AAEpB,MAAM,0BAA0B;AAEhC,MAAM,wBAAwB;AAgC9B,MAAM,eAAe;AAAA;AAAA;AAAA;AAAA,EAUnB,YAA6B,MAAmB;AAAnB;AAAA,EAAoB;AAAA,EAApB;AAAA,EATnB,cAAc,oBAAI,IAAsB;AAAA;AAAA,EAExC,aAAa,oBAAI,IAA8B;AAAA,EAC/C,QAAQ,oBAAI,IAAsB;AAAA,EAClC,eAAe,IAAI,gCAAY,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgB9D,MAAM,QAAuB,KAA4B;AAjElE;AAkEI,UAAM,YAAQ,wCAAoB,GAAG;AAIrC,UAAM,cAAY,WAAM,YAAN,mBAAe,WAAU,QAAQ,GAAG,OAAO,EAAE,SAAS,MAAM,QAAQ,GAAG,KAAK;AAC9F,UAAM,YAAY,cAAc,QAAQ,KAAK,WAAW,IAAI,SAAS;AACrE,QAAI,CAAC,aAAa,CAAC,KAAK,MAAM,MAAM,GAAG;AACrC,aAAO;AAAA,IACT;AACA,SAAK,KAAK,WAAW,GAAG,OAAO,EAAE,YAAY,MAAM,OAAO;
|
|
4
|
+
"sourcesContent": ["import type { CommandEvent } from \"./ecp/ecp-command\";\nimport { commandToStateWrite } from \"./ecp/state-model\";\nimport { LogThrottle } from \"./lib/log-throttle\";\nimport { RateGate } from \"./lib/rate-gate\";\n\n/** How long a keypress pulses its keys.<Key> state true before falling back to false. */\nexport const KEY_PULSE_MS = 50;\n/** Safety cap for a held key: a keydown with no matching keyup resets after this. */\nexport const HOLD_MAX_MS = 30_000;\n/** Commands accepted per second and emulated Roku; the excess is dropped (see lib/rate-gate.ts). */\nexport const MAX_COMMANDS_PER_SECOND = 25;\n/** How often at most the dropped-commands warning repeats per device. */\nexport const RATE_WARN_INTERVAL_MS = 60_000;\n\n/** The emulated Roku a command is for. */\nexport interface CommandTarget {\n /** The id-safe device path segment. */\n readonly id: string;\n /** The configured device name, for the warning the user reads. */\n readonly name: string;\n /** The key names this device's type carries \u2014 only those have a state to write. */\n readonly keys: ReadonlySet<string>;\n}\n\n/** What the handler needs from the adapter: writing, the managed timers and the log. */\nexport interface CommandHost {\n /** Fire-and-forget state write (relative id). */\n writeState(id: string, val: string | boolean): void;\n /**\n * The adapter's managed timeout \u2014 undefined while the adapter shuts down. A property, not a method signature: the\n * repochecker reads a timer method signature as a plain timer call (S5005).\n */\n setTimeout: (callback: () => void, ms: number) => ioBroker.Timeout | undefined;\n /** The adapter's managed clearTimeout. */\n clearTimeout: (timer: ioBroker.Timeout) => void;\n /** A warning the user should see. */\n warn(message: string): void;\n}\n\n/**\n * Turns the ECP commands of every emulated Roku into state writes: `command`, a pulse for a keypress,\n * a hold with a watchdog for keydown/keyup \u2014 behind a rate gate per device, the write-flood protection for the states\n * database. Owns its timers and releases them in {@link dispose}.\n */\nexport class CommandHandler {\n private readonly pulseTimers = new Set<ioBroker.Timeout>();\n /** Per held key id, its watchdog timer \u2014 so a keydown without a keyup cannot pin it true forever. */\n private readonly holdTimers = new Map<string, ioBroker.Timeout>();\n private readonly gates = new Map<string, RateGate>();\n private readonly rateWarnings = new LogThrottle(RATE_WARN_INTERVAL_MS);\n\n /**\n * @param host the adapter's writer, timers and log\n */\n public constructor(private readonly host: CommandHost) {}\n\n /**\n * Apply a received ECP command to the device's states: record it in `command`, and pulse or hold\n * the standard key if the device carries it. The server logs only what this accepted, so the rate gate covers the\n * log as well as the states database.\n *\n * @param device the emulated Roku the command is for\n * @param cmd the parsed ECP command\n * @returns true if the command was applied, false if the rate gate dropped it\n */\n public apply(device: CommandTarget, cmd: CommandEvent): boolean {\n const write = commandToStateWrite(cmd);\n // The release of a key that is actually HELD never passes the rate gate: a dropped keyup would leave the key true\n // until the watchdog, so the flood protection would falsify the tree. Asked by the held state, not by the request,\n // so a flood of keyups for a key nobody holds still meets the gate.\n const releaseOf = write.holdKey?.value === false ? `${device.id}.keys.${write.holdKey.key}` : null;\n const isRelease = releaseOf !== null && this.holdTimers.has(releaseOf);\n if (!isRelease && !this.admit(device)) {\n return false;\n }\n this.host.writeState(`${device.id}.command`, write.command);\n // A key only this device's type lacks lands in `command` alone \u2014 there is no state to write.\n if (write.pulseKey && device.keys.has(write.pulseKey)) {\n this.pulse(`${device.id}.keys.${write.pulseKey}`);\n } else if (write.holdKey && device.keys.has(write.holdKey.key)) {\n this.hold(`${device.id}.keys.${write.holdKey.key}`, write.holdKey.value);\n }\n return true;\n }\n\n /** Disarm every timer \u2014 the adapter is going down. */\n public dispose(): void {\n for (const timer of this.pulseTimers) {\n this.host.clearTimeout(timer);\n }\n this.pulseTimers.clear();\n for (const timer of this.holdTimers.values()) {\n this.host.clearTimeout(timer);\n }\n this.holdTimers.clear();\n this.gates.clear();\n }\n\n /**\n * A keypress: true now, false {@link KEY_PULSE_MS} later. A keypress on a HELD key ends the hold, or its watchdog\n * would later write a release for a key the pulse already released.\n *\n * @param id the full key state id\n */\n private pulse(id: string): void {\n this.clearHoldTimer(id);\n this.host.writeState(id, true);\n const timer = this.host.setTimeout(() => {\n if (timer) {\n this.pulseTimers.delete(timer);\n }\n // A keydown inside the pulse window owns the key now \u2014 its keyup writes the release.\n if (!this.holdTimers.has(id)) {\n this.host.writeState(id, false);\n }\n }, KEY_PULSE_MS);\n if (timer) {\n this.pulseTimers.add(timer);\n }\n }\n\n /**\n * A keydown holds the key true until its keyup; a watchdog releases it after {@link HOLD_MAX_MS} when the keyup is\n * lost. A repeated keydown re-arms it.\n *\n * @param id the full key state id\n * @param down true for keydown, false for keyup\n */\n private hold(id: string, down: boolean): void {\n this.host.writeState(id, down);\n this.clearHoldTimer(id);\n if (down) {\n const timer = this.host.setTimeout(() => {\n this.holdTimers.delete(id);\n this.host.writeState(id, false);\n }, HOLD_MAX_MS);\n if (timer) {\n this.holdTimers.set(id, timer);\n }\n }\n }\n\n /**\n * Disarm the hold watchdog of one key, if it has one.\n *\n * @param id the full key state id\n */\n private clearHoldTimer(id: string): void {\n const timer = this.holdTimers.get(id);\n if (timer) {\n this.host.clearTimeout(timer);\n this.holdTimers.delete(id);\n }\n }\n\n /**\n * The rate gate: {@link MAX_COMMANDS_PER_SECOND} per device, the excess dropped and reported once per\n * {@link RATE_WARN_INTERVAL_MS}. Every accepted command costs three writes plus one when the pulse ends.\n *\n * @param device the emulated Roku\n * @returns true if the command may be applied\n */\n private admit(device: CommandTarget): boolean {\n const now = Date.now();\n let gate = this.gates.get(device.id);\n if (!gate) {\n gate = new RateGate(MAX_COMMANDS_PER_SECOND, now);\n this.gates.set(device.id, gate);\n }\n if (gate.allow(now)) {\n return true;\n }\n if (this.rateWarnings.due(device.id, now)) {\n this.host.warn(\n `Emulated Roku \"${device.name}\" receives more than ${MAX_COMMANDS_PER_SECOND} commands per second \u2014 dropping the excess (a misbehaving controller?)`,\n );\n }\n return false;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/discovery/ssdp-responder.ts"],
|
|
4
|
-
"sourcesContent": ["import * as dgram from \"node:dgram\";\nimport type { Membership } from \"../lib/detect-ip\";\nimport { errText } from \"../lib/err-text\";\nimport { LogThrottle } from \"../lib/log-throttle\";\nimport type { AdapterLogger } from \"../lib/logger\";\nimport {\n answersSearch,\n buildAliveNotify,\n buildByebyeNotify,\n buildSearchResponse,\n rokuSearchTarget,\n type RokuAdvert,\n} from \"./ssdp-messages\";\n\nconst SSDP_PORT = 1900;\nconst MULTICAST_ADDR = \"239.255.255.250\";\n/** How often at most a repeating socket problem (a failed answer, a receive error) is logged. */\nconst PROBLEM_LOG_INTERVAL_MS = 60_000;\n\n/** Configuration for the Roku SSDP responder. */\nexport interface RokuSsdpResponderConfig {\n /** Emulated Rokus to answer for. */\n devices: RokuAdvert[];\n /**\n * The chosen interface address, or `undefined` for \"all interfaces\". With a chosen interface\n * the outgoing multicast (NOTIFY) is pinned to it and every answer carries it.\n */\n bindIp: string | undefined;\n /** Routable IP announced when nothing more specific is known \u2014 never `0.0.0.0`. */\n advertiseIp: string;\n /**\n * Interfaces to join the multicast group on \u2014 one entry per interface. With a chosen interface\n * this is just that one; in the auto case it is every real interface of the host, so a\n * multi-homed host hears M-SEARCH on all its LANs. Empty \u2192 OS default only.\n */\n membershipInterfaces: Membership[];\n /** Logger. */\n logger: AdapterLogger;\n /**\n * Whether a searching client may be answered \u2014 the adapter's trust boundary (`lib/lan-guard.ts`), narrowed to the\n * chosen interface's network when there is one.\n */\n isClientAllowed: (address: string) => boolean;\n /**\n * The host's own address in the network of a searching client (auto case). An answer must carry\n * an address the asking remote can reach \u2014 on a host with several networks that is the address\n * of the network the search came from, not the one primary address. `undefined` \u2192 advertiseIp.\n */\n advertiseFor?: (remote: string) => string | undefined;\n /**\n * Called at most once if the socket closes AFTER a successful start without {@link stop} being\n * called \u2014 discovery is gone and the adapter should stop announcing into it.\n */\n onFatalError: (err: Error) => void;\n}\n\n/** A socket that sends the NOTIFY/byebye of ONE interface, with that interface's address. */\ninterface Sender {\n /** The interface the sender belongs to. */\n iface: string;\n /** The interface address \u2014 the LOCATION its announcements carry. */\n address: string;\n /** The socket, bound to the address and pinned to the interface for multicast. */\n socket: dgram.Socket;\n}\n\n/**\n * Roku SSDP responder. Answers M-SEARCH(roku:ecp and relatives) with Roku's exact response format\n * (built by hand \u2014 node-ssdp appends a `::device` suffix to the USN that Roku never uses) and\n * announces ssdp:alive / ssdp:byebye.\n *\n * Receiving: one socket on 0.0.0.0:1900 (shared via `reuseAddr`), in the multicast group on every\n * interface of {@link RokuSsdpResponderConfig.membershipInterfaces} \u2014 one membership per interface.\n *\n * Sending: an answer goes back unicast to the searching remote and carries the address of the\n * network it came from. A NOTIFY in the auto case goes out once per interface, from a small\n * sender socket bound to that interface's address, so every LAN hears an address it can reach;\n * with a chosen interface it goes out of the receiving socket, pinned to that interface.\n *\n * Owns no timers: the adapter drives {@link announce} on a managed interval and bounds\n * {@link start} with a managed timeout.\n */\nexport class RokuSsdpResponder {\n private socket: dgram.Socket | undefined;\n private fatalReported = false;\n /** Set by {@link stop}: a close after that is ours, not a death to report. */\n private closing = false;\n /** Interfaces already in the group \u2014 a second join through another address throws EADDRINUSE. */\n private readonly joined = new Set<string>();\n /** The per-interface NOTIFY senders of the auto case. */\n private readonly senders = new Map<string, Sender>();\n /** Repeating problems, logged at most once per minute and kind. */\n private readonly problems = new LogThrottle(PROBLEM_LOG_INTERVAL_MS);\n /**\n * The devices this responder answers for \u2014 its OWN list, not the caller's array. A device whose\n * port was busy at start-up joins when its retry succeeds, one whose server died leaves;\n * {@link addDevice} and {@link removeDevice} are the only ways in and out.\n */\n private readonly devices: RokuAdvert[];\n /** The address announced when nothing more specific is known \u2014 follows the host in the auto case. */\n private advertiseIp: string;\n /** The interfaces the host has now \u2014 follows the host in the auto case. */\n private memberships: Membership[];\n\n /**\n * @param config responder configuration\n */\n public constructor(private readonly config: Readonly<RokuSsdpResponderConfig>) {\n this.devices = [...config.devices];\n this.advertiseIp = config.advertiseIp;\n this.memberships = [...config.membershipInterfaces];\n }\n\n /**\n * Announce one more device from now on \u2014 an emulated Roku that started late.\n *\n * @param device the advert to answer for\n */\n public addDevice(device: RokuAdvert): void {\n if (!this.devices.some(d => d.uuid === device.uuid)) {\n this.devices.push(device);\n }\n }\n\n /**\n * Stop answering for a device \u2014 its ECP server died.\n *\n * @param uuid the identity of the device to drop\n */\n public removeDevice(uuid: string): void {\n const at = this.devices.findIndex(d => d.uuid === uuid);\n if (at >= 0) {\n this.devices.splice(at, 1);\n }\n }\n\n /** Bind on 1900, join multicast on the selected interface(s), start answering. Rejects on bind error. */\n public async start(): Promise<void> {\n const socket = dgram.createSocket({ type: \"udp4\", reuseAddr: true });\n this.socket = socket;\n\n await new Promise<void>((resolve, reject) => {\n const onBindError = (err: Error): void => reject(err);\n socket.once(\"error\", onBindError);\n socket.bind(SSDP_PORT, () => {\n socket.removeListener(\"error\", onBindError);\n this.joinMulticast(socket, this.memberships);\n // Pin OUTGOING multicast (NOTIFY) to the chosen interface. Binding the socket only sets\n // the source address; the multicast egress interface is IP_MULTICAST_IF (Node dgram\n // docs), so without this a NOTIFY can leave the wrong NIC on a multi-homed host.\n if (this.config.bindIp) {\n try {\n socket.setMulticastInterface(this.config.bindIp);\n } catch (e) {\n this.config.logger.warn(\n `SSDP: could not pin multicast egress to ${this.config.bindIp}: ${errText(e)} \u2014 NOTIFY may use the default interface`,\n );\n }\n }\n socket.on(\"error\", (err: Error) => this.onSocketError(err));\n socket.on(\"close\", () => this.onSocketClose());\n socket.on(\"message\", (msg, rinfo) => this.onMessage(msg.toString(\"utf8\"), rinfo.address, rinfo.port));\n resolve();\n });\n });\n if (!this.config.bindIp) {\n this.openSenders(this.memberships);\n }\n\n const join = this.memberships.length ? this.memberships.map(m => `${m.iface} ${m.address}`).join(\", \") : \"default\";\n this.config.logger.debug(`Roku SSDP responder on :${SSDP_PORT}, advertising ${this.advertiseIp} (join: ${join})`);\n }\n\n /**\n * Follow the host while running \u2014 the automatic case only. The fallback address moves with a\n * DHCP change, and interfaces that came up since the start are joined and get their own NOTIFY\n * sender; one that went away loses its sender. An interface already in the group is not joined\n * again (EADDRINUSE on every pass otherwise).\n *\n * @param advertiseIp the routable IP to announce when nothing more specific is known\n * @param membershipInterfaces the interfaces the host has now\n * @returns true if the fallback address actually changed\n */\n public refreshAdvertise(advertiseIp: string, membershipInterfaces: Membership[]): boolean {\n const changed = advertiseIp !== this.advertiseIp;\n this.advertiseIp = advertiseIp;\n this.memberships = [...membershipInterfaces];\n if (this.socket) {\n if (membershipInterfaces.length > 0) {\n this.joinMulticast(this.socket, membershipInterfaces);\n }\n if (!this.config.bindIp) {\n const present = new Set(membershipInterfaces.map(m => `${m.iface}|${m.address}`));\n for (const [key, sender] of this.senders) {\n if (!present.has(key)) {\n this.closeSender(key, sender);\n }\n }\n this.openSenders(membershipInterfaces);\n }\n }\n return changed;\n }\n\n /**\n * Join the multicast group on each interface not joined yet, or on the OS default when none is known.\n *\n * @param socket the bound SSDP socket\n * @param ifaces the interfaces to be in the group on\n */\n private joinMulticast(socket: dgram.Socket, ifaces: readonly Membership[]): void {\n if (ifaces.length === 0) {\n this.tryJoin(socket, undefined);\n return;\n }\n for (const m of ifaces) {\n if (!this.joined.has(m.iface)) {\n this.tryJoin(socket, m);\n }\n }\n }\n\n /**\n * Join the group on one interface; a failure warns but does not throw, so one bad interface\n * cannot stop the responder.\n *\n * @param socket the bound SSDP socket\n * @param m the interface to join on, or undefined for the OS default\n */\n private tryJoin(socket: dgram.Socket, m: Membership | undefined): void {\n try {\n socket.addMembership(MULTICAST_ADDR, m?.address);\n if (m) {\n this.joined.add(m.iface);\n }\n } catch (e) {\n // Usually an OS routing issue (the interface is not in the multicast table), or ENOBUFS on\n // a host past the kernel's limit of group memberships per socket. Don't die: a paired\n // controller still reaches the ECP port. Warn so the \"no discovery\" symptom is findable.\n this.config.logger.warn(\n `SSDP multicast join failed on ${m ? `${m.iface} (${m.address})` : \"default interface\"}: ${errText(e)} \u2014 discovery may be incomplete`,\n );\n }\n }\n\n /**\n * Open a NOTIFY sender for every interface that has none yet (auto case). A failed sender only\n * costs that interface its proactive announcement; answers to searches still reach it.\n *\n * @param memberships the interfaces to announce on\n */\n private openSenders(memberships: Membership[]): void {\n for (const m of memberships) {\n const key = `${m.iface}|${m.address}`;\n if (this.senders.has(key)) {\n continue;\n }\n try {\n const socket = dgram.createSocket({ type: \"udp4\", reuseAddr: true });\n socket.on(\"error\", (err: Error) =>\n this.logProblem(`sender ${m.iface}`, `SSDP sender on ${m.iface}: ${errText(err)}`),\n );\n socket.bind({ address: m.address, port: 0 }, () => {\n try {\n socket.setMulticastInterface(m.address);\n } catch (e) {\n this.logProblem(\n `sender ${m.iface}`,\n `SSDP sender on ${m.iface} could not pin its interface: ${errText(e)}`,\n );\n }\n });\n this.senders.set(key, { iface: m.iface, address: m.address, socket });\n } catch (e) {\n this.logProblem(`sender ${m.iface}`, `SSDP sender on ${m.iface} could not open: ${errText(e)}`);\n }\n }\n }\n\n /**\n * Close one NOTIFY sender.\n *\n * @param key its map key\n * @param sender the sender\n */\n private closeSender(key: string, sender: Sender): void {\n try {\n sender.socket.close();\n } catch {\n // already closed\n }\n this.senders.delete(key);\n }\n\n /**\n * An error on the receiving socket after a good start. On Linux/macOS this is a receive error\n * (`recvmsg`, e.g. ENOBUFS under memory pressure): Node reports it, and the socket keeps\n * receiving. Closing it here would turn a passing hiccup into discovery that stays off until a\n * restart \u2014 so it is logged (throttled) and the socket stays.\n *\n * @param err the socket error\n */\n private onSocketError(err: Error): void {\n this.logProblem(\"socket\", `SSDP socket error: ${errText(err)}`);\n }\n\n /** The receiving socket closed. If {@link stop} did not close it, discovery is gone \u2014 report once. */\n private onSocketClose(): void {\n if (this.closing) {\n return;\n }\n this.socket = undefined;\n if (!this.fatalReported) {\n this.fatalReported = true;\n this.config.onFatalError(new Error(\"SSDP socket closed\"));\n }\n }\n\n /**\n * Log a problem that can repeat \u2014 at most once per minute and kind, so a remote that keeps\n * searching from an unreachable address cannot fill the log.\n *\n * @param kind what the problem is about\n * @param line the log line\n */\n private logProblem(kind: string, line: string): void {\n if (this.problems.due(kind)) {\n this.config.logger.warn(line);\n }\n }\n\n private onMessage(text: string, address: string, port: number): void {\n const target = rokuSearchTarget(text);\n if (!target) {\n return;\n }\n // Port 1900 is bound on every interface. A search from outside the adapter's networks gets\n // no answer: the ECP server would refuse that client anyway, and replying to a spoofed\n // source would make the emulator a small reflection amplifier.\n if (!this.config.isClientAllowed(address)) {\n this.config.logger.debug(`SSDP search from ${address} ignored \u2014 not in the adapter's networks`);\n return;\n }\n const location = this.config.bindIp ?? this.config.advertiseFor?.(address) ?? this.advertiseIp;\n for (const device of this.devices.filter(d => answersSearch(d, target))) {\n const response = Buffer.from(buildSearchResponse(device, location, target));\n this.socket?.send(response, port, address, err => {\n if (err) {\n this.logProblem(\"answer\", `SSDP response send failed: ${errText(err)}`);\n }\n });\n }\n }\n\n /**\n * The sockets and addresses an announcement goes out through: one per interface, or the receiver.\n *\n * @param receiver the bound receiving socket\n * @returns the senders with their addresses\n */\n private announcers(receiver: dgram.Socket): { socket: dgram.Socket; address: string }[] {\n if (this.senders.size > 0) {\n return [...this.senders.values()].map(s => ({ socket: s.socket, address: s.address }));\n }\n return [{ socket: receiver, address: this.advertiseIp }];\n }\n\n /** Send one proactive ssdp:alive burst for every device. The adapter calls this on a managed interval. */\n public announce(): void {\n if (!this.socket) {\n return;\n }\n for (const { socket, address } of this.announcers(this.socket)) {\n for (const device of this.devices) {\n try {\n socket.send(Buffer.from(buildAliveNotify(device, address)), SSDP_PORT, MULTICAST_ADDR, err => {\n if (err) {\n this.config.logger.debug(`SSDP NOTIFY send failed: ${errText(err)}`);\n }\n });\n } catch (e) {\n this.config.logger.debug(`SSDP NOTIFY send failed: ${errText(e)}`);\n }\n }\n }\n }\n\n /**\n * Say goodbye for every device and resolve once the datagrams are out. Without this a\n * controller that keeps its list from discovery keeps the emulated Roku for the announced\n * max-age. It always resolves: a `dgram` send reports back through its callback either way, and\n * a synchronous throw is caught. The host's `stopTimeout` bounds teardown as a whole.\n *\n * The sockets stay open until {@link stop} \u2014 sending into a closed one would throw.\n *\n * @returns a promise that always resolves\n */\n public byebye(): Promise<void> {\n const outs = this.socket ? this.announcers(this.socket) : [];\n if (outs.length === 0 || this.devices.length === 0) {\n return Promise.resolve();\n }\n return new Promise<void>(resolve => {\n let open = outs.length * this.devices.length;\n const done = (): void => {\n if (--open === 0) {\n resolve();\n }\n };\n for (const { socket } of outs) {\n for (const device of this.devices) {\n try {\n socket.send(Buffer.from(buildByebyeNotify(device)), SSDP_PORT, MULTICAST_ADDR, err => {\n if (err) {\n this.config.logger.debug(`SSDP byebye send failed: ${errText(err)}`);\n }\n done();\n });\n } catch (e) {\n this.config.logger.debug(`SSDP byebye send failed: ${errText(e)}`);\n done();\n }\n }\n }\n });\n }\n\n /** Synchronous close \u2014 safe to call from onUnload. Closing the socket drops its memberships. */\n public stop(): void {\n this.closing = true;\n for (const [key, sender] of this.senders) {\n this.closeSender(key, sender);\n }\n if (this.socket) {\n try {\n this.socket.close();\n } catch {\n // socket already closed\n }\n this.socket = undefined;\n }\n }\n}\n"],
|
|
4
|
+
"sourcesContent": ["import * as dgram from \"node:dgram\";\nimport type { Membership } from \"../lib/detect-ip\";\nimport { errText } from \"../lib/err-text\";\nimport { LogThrottle } from \"../lib/log-throttle\";\nimport type { AdapterLogger } from \"../lib/logger\";\nimport {\n answersSearch,\n buildAliveNotify,\n buildByebyeNotify,\n buildSearchResponse,\n rokuSearchTarget,\n type RokuAdvert,\n} from \"./ssdp-messages\";\n\nconst SSDP_PORT = 1900;\nconst MULTICAST_ADDR = \"239.255.255.250\";\n/** How often at most a repeating socket problem (a failed answer, a receive error) is logged. */\nconst PROBLEM_LOG_INTERVAL_MS = 60_000;\n\n/** Configuration for the Roku SSDP responder. */\nexport interface RokuSsdpResponderConfig {\n /** Emulated Rokus to answer for. */\n devices: RokuAdvert[];\n /**\n * The chosen interface address, or `undefined` for \"all interfaces\". With a chosen interface\n * the outgoing multicast (NOTIFY) is pinned to it and every answer carries it.\n */\n bindIp: string | undefined;\n /** Routable IP announced when nothing more specific is known \u2014 never `0.0.0.0`. */\n advertiseIp: string;\n /**\n * Interfaces to join the multicast group on \u2014 one entry per interface. With a chosen interface\n * this is just that one; in the auto case it is every real interface of the host, so a\n * multi-homed host hears M-SEARCH on all its LANs. Empty \u2192 OS default only.\n */\n membershipInterfaces: Membership[];\n /** Logger. */\n logger: AdapterLogger;\n /**\n * Whether a searching client may be answered \u2014 the adapter's trust boundary (`isOwnPeer` of the fleet master\n * `lib/network-address.ts`), narrowed to the chosen interface's network when there is one.\n */\n isClientAllowed: (address: string) => boolean;\n /**\n * The host's own address in the network of a searching client (auto case). An answer must carry\n * an address the asking remote can reach \u2014 on a host with several networks that is the address\n * of the network the search came from, not the one primary address. `undefined` \u2192 advertiseIp.\n */\n advertiseFor?: (remote: string) => string | undefined;\n /**\n * Called at most once if the socket closes AFTER a successful start without {@link stop} being\n * called \u2014 discovery is gone and the adapter should stop announcing into it.\n */\n onFatalError: (err: Error) => void;\n}\n\n/** A socket that sends the NOTIFY/byebye of ONE interface, with that interface's address. */\ninterface Sender {\n /** The interface the sender belongs to. */\n iface: string;\n /** The interface address \u2014 the LOCATION its announcements carry. */\n address: string;\n /** The socket, bound to the address and pinned to the interface for multicast. */\n socket: dgram.Socket;\n}\n\n/**\n * Roku SSDP responder. Answers M-SEARCH(roku:ecp and relatives) with Roku's exact response format\n * (built by hand \u2014 node-ssdp appends a `::device` suffix to the USN that Roku never uses) and\n * announces ssdp:alive / ssdp:byebye.\n *\n * Receiving: one socket on 0.0.0.0:1900 (shared via `reuseAddr`), in the multicast group on every\n * interface of {@link RokuSsdpResponderConfig.membershipInterfaces} \u2014 one membership per interface.\n *\n * Sending: an answer goes back unicast to the searching remote and carries the address of the\n * network it came from. A NOTIFY in the auto case goes out once per interface, from a small\n * sender socket bound to that interface's address, so every LAN hears an address it can reach;\n * with a chosen interface it goes out of the receiving socket, pinned to that interface.\n *\n * Owns no timers: the adapter drives {@link announce} on a managed interval and bounds\n * {@link start} with a managed timeout.\n */\nexport class RokuSsdpResponder {\n private socket: dgram.Socket | undefined;\n private fatalReported = false;\n /** Set by {@link stop}: a close after that is ours, not a death to report. */\n private closing = false;\n /** Interfaces already in the group \u2014 a second join through another address throws EADDRINUSE. */\n private readonly joined = new Set<string>();\n /** The per-interface NOTIFY senders of the auto case. */\n private readonly senders = new Map<string, Sender>();\n /** Repeating problems, logged at most once per minute and kind. */\n private readonly problems = new LogThrottle(PROBLEM_LOG_INTERVAL_MS);\n /**\n * The devices this responder answers for \u2014 its OWN list, not the caller's array. A device whose\n * port was busy at start-up joins when its retry succeeds, one whose server died leaves;\n * {@link addDevice} and {@link removeDevice} are the only ways in and out.\n */\n private readonly devices: RokuAdvert[];\n /** The address announced when nothing more specific is known \u2014 follows the host in the auto case. */\n private advertiseIp: string;\n /** The interfaces the host has now \u2014 follows the host in the auto case. */\n private memberships: Membership[];\n\n /**\n * @param config responder configuration\n */\n public constructor(private readonly config: Readonly<RokuSsdpResponderConfig>) {\n this.devices = [...config.devices];\n this.advertiseIp = config.advertiseIp;\n this.memberships = [...config.membershipInterfaces];\n }\n\n /**\n * Announce one more device from now on \u2014 an emulated Roku that started late.\n *\n * @param device the advert to answer for\n */\n public addDevice(device: RokuAdvert): void {\n if (!this.devices.some(d => d.uuid === device.uuid)) {\n this.devices.push(device);\n }\n }\n\n /**\n * Stop answering for a device \u2014 its ECP server died.\n *\n * @param uuid the identity of the device to drop\n */\n public removeDevice(uuid: string): void {\n const at = this.devices.findIndex(d => d.uuid === uuid);\n if (at >= 0) {\n this.devices.splice(at, 1);\n }\n }\n\n /** Bind on 1900, join multicast on the selected interface(s), start answering. Rejects on bind error. */\n public async start(): Promise<void> {\n const socket = dgram.createSocket({ type: \"udp4\", reuseAddr: true });\n this.socket = socket;\n\n await new Promise<void>((resolve, reject) => {\n const onBindError = (err: Error): void => reject(err);\n socket.once(\"error\", onBindError);\n socket.bind(SSDP_PORT, () => {\n socket.removeListener(\"error\", onBindError);\n this.joinMulticast(socket, this.memberships);\n // Pin OUTGOING multicast (NOTIFY) to the chosen interface. Binding the socket only sets\n // the source address; the multicast egress interface is IP_MULTICAST_IF (Node dgram\n // docs), so without this a NOTIFY can leave the wrong NIC on a multi-homed host.\n if (this.config.bindIp) {\n try {\n socket.setMulticastInterface(this.config.bindIp);\n } catch (e) {\n this.config.logger.warn(\n `SSDP: could not pin multicast egress to ${this.config.bindIp}: ${errText(e)} \u2014 NOTIFY may use the default interface`,\n );\n }\n }\n socket.on(\"error\", (err: Error) => this.onSocketError(err));\n socket.on(\"close\", () => this.onSocketClose());\n socket.on(\"message\", (msg, rinfo) => this.onMessage(msg.toString(\"utf8\"), rinfo.address, rinfo.port));\n resolve();\n });\n });\n if (!this.config.bindIp) {\n this.openSenders(this.memberships);\n }\n\n const join = this.memberships.length ? this.memberships.map(m => `${m.iface} ${m.address}`).join(\", \") : \"default\";\n this.config.logger.debug(`Roku SSDP responder on :${SSDP_PORT}, advertising ${this.advertiseIp} (join: ${join})`);\n }\n\n /**\n * Follow the host while running \u2014 the automatic case only. The fallback address moves with a\n * DHCP change, and interfaces that came up since the start are joined and get their own NOTIFY\n * sender; one that went away loses its sender. An interface already in the group is not joined\n * again (EADDRINUSE on every pass otherwise).\n *\n * @param advertiseIp the routable IP to announce when nothing more specific is known\n * @param membershipInterfaces the interfaces the host has now\n * @returns true if the fallback address actually changed\n */\n public refreshAdvertise(advertiseIp: string, membershipInterfaces: Membership[]): boolean {\n const changed = advertiseIp !== this.advertiseIp;\n this.advertiseIp = advertiseIp;\n this.memberships = [...membershipInterfaces];\n if (this.socket) {\n if (membershipInterfaces.length > 0) {\n this.joinMulticast(this.socket, membershipInterfaces);\n }\n if (!this.config.bindIp) {\n const present = new Set(membershipInterfaces.map(m => `${m.iface}|${m.address}`));\n for (const [key, sender] of this.senders) {\n if (!present.has(key)) {\n this.closeSender(key, sender);\n }\n }\n this.openSenders(membershipInterfaces);\n }\n }\n return changed;\n }\n\n /**\n * Join the multicast group on each interface not joined yet, or on the OS default when none is known.\n *\n * @param socket the bound SSDP socket\n * @param ifaces the interfaces to be in the group on\n */\n private joinMulticast(socket: dgram.Socket, ifaces: readonly Membership[]): void {\n if (ifaces.length === 0) {\n this.tryJoin(socket, undefined);\n return;\n }\n for (const m of ifaces) {\n if (!this.joined.has(m.iface)) {\n this.tryJoin(socket, m);\n }\n }\n }\n\n /**\n * Join the group on one interface; a failure warns but does not throw, so one bad interface\n * cannot stop the responder.\n *\n * @param socket the bound SSDP socket\n * @param m the interface to join on, or undefined for the OS default\n */\n private tryJoin(socket: dgram.Socket, m: Membership | undefined): void {\n try {\n socket.addMembership(MULTICAST_ADDR, m?.address);\n if (m) {\n this.joined.add(m.iface);\n }\n } catch (e) {\n // Usually an OS routing issue (the interface is not in the multicast table), or ENOBUFS on\n // a host past the kernel's limit of group memberships per socket. Don't die: a paired\n // controller still reaches the ECP port. Warn so the \"no discovery\" symptom is findable.\n this.config.logger.warn(\n `SSDP multicast join failed on ${m ? `${m.iface} (${m.address})` : \"default interface\"}: ${errText(e)} \u2014 discovery may be incomplete`,\n );\n }\n }\n\n /**\n * Open a NOTIFY sender for every interface that has none yet (auto case). A failed sender only\n * costs that interface its proactive announcement; answers to searches still reach it.\n *\n * @param memberships the interfaces to announce on\n */\n private openSenders(memberships: Membership[]): void {\n for (const m of memberships) {\n const key = `${m.iface}|${m.address}`;\n if (this.senders.has(key)) {\n continue;\n }\n try {\n const socket = dgram.createSocket({ type: \"udp4\", reuseAddr: true });\n socket.on(\"error\", (err: Error) =>\n this.logProblem(`sender ${m.iface}`, `SSDP sender on ${m.iface}: ${errText(err)}`),\n );\n socket.bind({ address: m.address, port: 0 }, () => {\n try {\n socket.setMulticastInterface(m.address);\n } catch (e) {\n this.logProblem(\n `sender ${m.iface}`,\n `SSDP sender on ${m.iface} could not pin its interface: ${errText(e)}`,\n );\n }\n });\n this.senders.set(key, { iface: m.iface, address: m.address, socket });\n } catch (e) {\n this.logProblem(`sender ${m.iface}`, `SSDP sender on ${m.iface} could not open: ${errText(e)}`);\n }\n }\n }\n\n /**\n * Close one NOTIFY sender.\n *\n * @param key its map key\n * @param sender the sender\n */\n private closeSender(key: string, sender: Sender): void {\n try {\n sender.socket.close();\n } catch {\n // already closed\n }\n this.senders.delete(key);\n }\n\n /**\n * An error on the receiving socket after a good start. On Linux/macOS this is a receive error\n * (`recvmsg`, e.g. ENOBUFS under memory pressure): Node reports it, and the socket keeps\n * receiving. Closing it here would turn a passing hiccup into discovery that stays off until a\n * restart \u2014 so it is logged (throttled) and the socket stays.\n *\n * @param err the socket error\n */\n private onSocketError(err: Error): void {\n this.logProblem(\"socket\", `SSDP socket error: ${errText(err)}`);\n }\n\n /** The receiving socket closed. If {@link stop} did not close it, discovery is gone \u2014 report once. */\n private onSocketClose(): void {\n if (this.closing) {\n return;\n }\n this.socket = undefined;\n if (!this.fatalReported) {\n this.fatalReported = true;\n this.config.onFatalError(new Error(\"SSDP socket closed\"));\n }\n }\n\n /**\n * Log a problem that can repeat \u2014 at most once per minute and kind, so a remote that keeps\n * searching from an unreachable address cannot fill the log.\n *\n * @param kind what the problem is about\n * @param line the log line\n */\n private logProblem(kind: string, line: string): void {\n if (this.problems.due(kind)) {\n this.config.logger.warn(line);\n }\n }\n\n private onMessage(text: string, address: string, port: number): void {\n const target = rokuSearchTarget(text);\n if (!target) {\n return;\n }\n // Port 1900 is bound on every interface. A search from outside the adapter's networks gets\n // no answer: the ECP server would refuse that client anyway, and replying to a spoofed\n // source would make the emulator a small reflection amplifier.\n if (!this.config.isClientAllowed(address)) {\n this.config.logger.debug(`SSDP search from ${address} ignored \u2014 not in the adapter's networks`);\n return;\n }\n const location = this.config.bindIp ?? this.config.advertiseFor?.(address) ?? this.advertiseIp;\n for (const device of this.devices.filter(d => answersSearch(d, target))) {\n const response = Buffer.from(buildSearchResponse(device, location, target));\n this.socket?.send(response, port, address, err => {\n if (err) {\n this.logProblem(\"answer\", `SSDP response send failed: ${errText(err)}`);\n }\n });\n }\n }\n\n /**\n * The sockets and addresses an announcement goes out through: one per interface, or the receiver.\n *\n * @param receiver the bound receiving socket\n * @returns the senders with their addresses\n */\n private announcers(receiver: dgram.Socket): { socket: dgram.Socket; address: string }[] {\n if (this.senders.size > 0) {\n return [...this.senders.values()].map(s => ({ socket: s.socket, address: s.address }));\n }\n return [{ socket: receiver, address: this.advertiseIp }];\n }\n\n /** Send one proactive ssdp:alive burst for every device. The adapter calls this on a managed interval. */\n public announce(): void {\n if (!this.socket) {\n return;\n }\n for (const { socket, address } of this.announcers(this.socket)) {\n for (const device of this.devices) {\n try {\n socket.send(Buffer.from(buildAliveNotify(device, address)), SSDP_PORT, MULTICAST_ADDR, err => {\n if (err) {\n this.config.logger.debug(`SSDP NOTIFY send failed: ${errText(err)}`);\n }\n });\n } catch (e) {\n this.config.logger.debug(`SSDP NOTIFY send failed: ${errText(e)}`);\n }\n }\n }\n }\n\n /**\n * Say goodbye for every device and resolve once the datagrams are out. Without this a\n * controller that keeps its list from discovery keeps the emulated Roku for the announced\n * max-age. It always resolves: a `dgram` send reports back through its callback either way, and\n * a synchronous throw is caught. The host's `stopTimeout` bounds teardown as a whole.\n *\n * The sockets stay open until {@link stop} \u2014 sending into a closed one would throw.\n *\n * @returns a promise that always resolves\n */\n public byebye(): Promise<void> {\n const outs = this.socket ? this.announcers(this.socket) : [];\n if (outs.length === 0 || this.devices.length === 0) {\n return Promise.resolve();\n }\n return new Promise<void>(resolve => {\n let open = outs.length * this.devices.length;\n const done = (): void => {\n if (--open === 0) {\n resolve();\n }\n };\n for (const { socket } of outs) {\n for (const device of this.devices) {\n try {\n socket.send(Buffer.from(buildByebyeNotify(device)), SSDP_PORT, MULTICAST_ADDR, err => {\n if (err) {\n this.config.logger.debug(`SSDP byebye send failed: ${errText(err)}`);\n }\n done();\n });\n } catch (e) {\n this.config.logger.debug(`SSDP byebye send failed: ${errText(e)}`);\n done();\n }\n }\n }\n });\n }\n\n /** Synchronous close \u2014 safe to call from onUnload. Closing the socket drops its memberships. */\n public stop(): void {\n this.closing = true;\n for (const [key, sender] of this.senders) {\n this.closeSender(key, sender);\n }\n if (this.socket) {\n try {\n this.socket.close();\n } catch {\n // socket already closed\n }\n this.socket = undefined;\n }\n }\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/ecp/device-info.js
CHANGED
|
@@ -21,13 +21,10 @@ __export(device_info_exports, {
|
|
|
21
21
|
APP_ICON_PNG: () => APP_ICON_PNG,
|
|
22
22
|
DEFAULT_APPS: () => DEFAULT_APPS,
|
|
23
23
|
SOFTWARE_VERSION: () => SOFTWARE_VERSION,
|
|
24
|
-
buildActiveAppXml: () => buildActiveAppXml,
|
|
25
24
|
buildAppsXml: () => buildAppsXml,
|
|
26
25
|
buildDescXml: () => buildDescXml,
|
|
27
26
|
buildDeviceInfoXml: () => buildDeviceInfoXml,
|
|
28
|
-
|
|
29
|
-
buildScpdXml: () => buildScpdXml,
|
|
30
|
-
buildTvChannelsXml: () => buildTvChannelsXml
|
|
27
|
+
buildScpdXml: () => buildScpdXml
|
|
31
28
|
});
|
|
32
29
|
module.exports = __toCommonJS(device_info_exports);
|
|
33
30
|
var import_ssdp_messages = require("../discovery/ssdp-messages");
|
|
@@ -128,17 +125,6 @@ function buildScpdXml() {
|
|
|
128
125
|
<serviceStateTable/>
|
|
129
126
|
</scpd>`;
|
|
130
127
|
}
|
|
131
|
-
function buildActiveAppXml() {
|
|
132
|
-
return `<active-app>
|
|
133
|
-
<app>Roku</app>
|
|
134
|
-
</active-app>`;
|
|
135
|
-
}
|
|
136
|
-
function buildMediaPlayerXml() {
|
|
137
|
-
return `<player error="false" state="close"/>`;
|
|
138
|
-
}
|
|
139
|
-
function buildTvChannelsXml() {
|
|
140
|
-
return `<tv-channels/>`;
|
|
141
|
-
}
|
|
142
128
|
const APP_ICON_PNG = Buffer.from(
|
|
143
129
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=",
|
|
144
130
|
"base64"
|
|
@@ -148,12 +134,9 @@ const APP_ICON_PNG = Buffer.from(
|
|
|
148
134
|
APP_ICON_PNG,
|
|
149
135
|
DEFAULT_APPS,
|
|
150
136
|
SOFTWARE_VERSION,
|
|
151
|
-
buildActiveAppXml,
|
|
152
137
|
buildAppsXml,
|
|
153
138
|
buildDescXml,
|
|
154
139
|
buildDeviceInfoXml,
|
|
155
|
-
|
|
156
|
-
buildScpdXml,
|
|
157
|
-
buildTvChannelsXml
|
|
140
|
+
buildScpdXml
|
|
158
141
|
});
|
|
159
142
|
//# sourceMappingURL=device-info.js.map
|