iobroker.fakeroku 1.8.1 → 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.
Files changed (57) hide show
  1. package/README.md +32 -27
  2. package/admin/i18n/de.json +1 -3
  3. package/admin/i18n/en.json +1 -3
  4. package/admin/i18n/es.json +1 -3
  5. package/admin/i18n/fr.json +1 -3
  6. package/admin/i18n/it.json +1 -3
  7. package/admin/i18n/nl.json +1 -3
  8. package/admin/i18n/pl.json +1 -3
  9. package/admin/i18n/pt.json +1 -3
  10. package/admin/i18n/ru.json +1 -3
  11. package/admin/i18n/uk.json +1 -3
  12. package/admin/i18n/zh-cn.json +1 -3
  13. package/build/command-handler.js +171 -0
  14. package/build/command-handler.js.map +7 -0
  15. package/build/device-management.js +6 -7
  16. package/build/device-management.js.map +2 -2
  17. package/build/discovery/ssdp-messages.js +11 -6
  18. package/build/discovery/ssdp-messages.js.map +2 -2
  19. package/build/discovery/ssdp-responder.js +50 -48
  20. package/build/discovery/ssdp-responder.js.map +2 -2
  21. package/build/ecp/device-info.js +5 -25
  22. package/build/ecp/device-info.js.map +2 -2
  23. package/build/ecp/ecp-command.js +0 -3
  24. package/build/ecp/ecp-command.js.map +2 -2
  25. package/build/ecp/ecp-http-server.js +20 -35
  26. package/build/ecp/ecp-http-server.js.map +2 -2
  27. package/build/ecp/state-model.js +0 -1
  28. package/build/ecp/state-model.js.map +2 -2
  29. package/build/lib/constants.js +10 -2
  30. package/build/lib/constants.js.map +2 -2
  31. package/build/lib/detect-ip.js +13 -119
  32. package/build/lib/detect-ip.js.map +3 -3
  33. package/build/lib/device-config.js +7 -10
  34. package/build/lib/device-config.js.map +2 -2
  35. package/build/lib/device-identity.js.map +1 -1
  36. package/build/lib/{errors.js → err-text.js} +4 -4
  37. package/build/lib/err-text.js.map +7 -0
  38. package/build/lib/i18n.js +2 -2
  39. package/build/lib/i18n.js.map +2 -2
  40. package/build/lib/known-objects.js +227 -0
  41. package/build/lib/known-objects.js.map +7 -0
  42. package/build/lib/{lan-guard.js → log-throttle.js} +27 -17
  43. package/build/lib/log-throttle.js.map +7 -0
  44. package/build/lib/native-key-migration.js +0 -3
  45. package/build/lib/native-key-migration.js.map +2 -2
  46. package/build/lib/network-address.js +85 -0
  47. package/build/lib/network-address.js.map +7 -0
  48. package/build/lib/object-cleanup.js +3 -2
  49. package/build/lib/object-cleanup.js.map +2 -2
  50. package/build/lib/pure-helpers.js +4 -1
  51. package/build/lib/pure-helpers.js.map +2 -2
  52. package/build/main.js +384 -454
  53. package/build/main.js.map +2 -2
  54. package/io-package.json +28 -28
  55. package/package.json +6 -6
  56. package/build/lib/errors.js.map +0 -7
  57. package/build/lib/lan-guard.js.map +0 -7
package/README.md CHANGED
@@ -6,20 +6,23 @@
6
6
 
7
7
  **Support:** [![Ko-fi](https://img.shields.io/badge/Ko--fi-Support-ff5e5b?logo=ko-fi)](https://ko-fi.com/krobipd) [![PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/krobipd)
8
8
 
9
- Emulates one or more **[Roku](https://www.roku.com/) devices** on your LAN so that ECP/SSDP remotes and
10
- controllers — a Logitech Harmony Hub, a Sofabaton X1/X2, Home Assistant's Roku integration,
11
- openHAB — can trigger events in ioBroker. It is the **input** counterpart to the Logitech
9
+ > [!IMPORTANT]
10
+ > This adapter cannot be installed from github
11
+
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
12
14
  Harmony adapter: a button on the remote becomes a datapoint in ioBroker.
13
15
 
14
- Besides the key presses it answers the queries these controllers make before and while
15
- they use a Roku — device info, app list, active app, media player, TV channels, app icons —
16
- 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.
17
18
 
18
19
  > **The official Roku mobile app is not supported.** It drives Rokus over Roku's
19
20
  > proprietary, undocumented ECP-2 WebSocket channel, which this emulator does not
20
21
  > implement. Use a Harmony hub or a Sofabaton — those speak the classic ECP this
21
22
  > adapter serves.
22
23
 
24
+ [🇺🇸 Documentation](./docs/en/README.md) · [🇩🇪 Dokumentation](./docs/de/README.md)
25
+
23
26
  ## Features
24
27
 
25
28
  - Emulates one or more Roku devices on the LAN — the Roku control protocol (ECP) over HTTP plus SSDP discovery on port 1900.
@@ -37,13 +40,11 @@ For details and how to disable it, see the [Sentry plugin documentation](https:/
37
40
 
38
41
  - Node.js >= 22
39
42
  - js-controller >= 7.2.2
40
- - admin >= 8.0.11
41
-
42
- > The adapter CANNOT be installed via GitHub: The adapter must be installed via the ioBroker repository (stable or latest).
43
+ - admin >= 8.0.14
43
44
 
44
45
  ## Ports
45
46
 
46
- - **TCP 8060 (listening, one per emulated Roku, configurable)** — the Roku control protocol (ECP): the remote sends its key presses here. A Harmony or Sofabaton reads the port from the discovery announcement; Home Assistant and Homey always use 8060.
47
+ - **TCP 8060 (listening, one per emulated Roku, configurable)** — the Roku control protocol (ECP): the remote sends its key presses here.
47
48
  - **UDP 1900 (multicast, listening and outgoing)** — SSDP discovery, so a Harmony hub or Sofabaton finds the emulated Rokus; fixed by the UPnP standard.
48
49
 
49
50
  ## Configuration
@@ -73,19 +74,18 @@ At instance level:
73
74
 
74
75
  | Datapoint | Type | Meaning |
75
76
  |---|---|---|
76
- | `info.connection` | boolean, read-only | `true` only while **every** configured Roku is actually listening. If one of them cannot start — usually because its port is already taken — the log names the device and the port, and the adapter retries that device every minute until it comes up. |
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. |
77
78
 
78
79
  For every emulated Roku (`fakeroku.0.<name>`):
79
80
 
80
81
  | Datapoint | Type | Meaning |
81
82
  |---|---|---|
82
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. |
83
- | `.commandType` | string, read-only | `keypress` / `keydown` / `keyup` / `launch` / `install` / `input` / `search`. |
84
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`). |
85
85
 
86
86
  Free keyboard input (`Lit_x`) and app launches show up in `.command` only — they do
87
87
  not get their own objects. An app button of a remote that sends app launches (a
88
- Sofabaton, Home Assistant) arrives as `launch:<id>`; a Harmony Hub did not send its app
88
+ Sofabaton) arrives as `launch:<id>`; a Harmony Hub did not send its app
89
89
  buttons to the emulated Roku.
90
90
 
91
91
  > Note: the Roku remote sends the **same** `Play` command for play and pause, so
@@ -100,6 +100,25 @@ becomes `true` — or watch `.command` for the last button as text.
100
100
  Placeholder for the next version (at the beginning of the line):
101
101
  ### **WORK IN PROGRESS**
102
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
+
113
+ ### 1.8.2 (2026-10-02)
114
+
115
+ - (krobipd) Changed: a new instance starts switched off — check the settings, then switch it on.
116
+ - (krobipd) Fixed: the README and the user documentation name admin 8.0.14, the version the adapter actually requires.
117
+ - (krobipd) Fixed: the device manager answers right after a restart instead of failing until the translations are loaded.
118
+ - (krobipd) Improved: the type of the last command shows a readable label in the language you set instead of a protocol word.
119
+ - (krobipd) Improved: a start writes only datapoints that changed and reads the key states in one request — no needless updates for history adapters.
120
+ - (krobipd) Improved: the README links the detailed user documentation in English and German.
121
+
103
122
  ### 1.8.1 (2026-09-27)
104
123
 
105
124
  - (krobipd) Improved: the note the Admin shows before an update from 0.x is short now: the apps folder is removed, app launches arrive as launch:<id> in command.
@@ -121,20 +140,6 @@ becomes `true` — or watch `.command` for the last button as text.
121
140
 
122
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.
123
142
 
124
- ### 1.7.0 (2026-09-16)
125
-
126
- - (krobipd) Fixed: button datapoints keep their value and their room and function assignment when the adapter starts.
127
- - (krobipd) Fixed: after an emulated Roku drops out, its port is free again instead of staying blocked until ioBroker restarts.
128
- - (krobipd) Fixed: stopping the instance no longer leaves it reported as connected.
129
- - (krobipd) Fixed: a key you hold right after a short press stays pressed instead of being released early.
130
- - (krobipd) Fixed: the device dialog now also refuses a name that would collide with an existing device in the object tree.
131
- - (krobipd) Improved: after the host gets a new IP address, remotes find the emulated Rokus again without restarting the instance.
132
- - (krobipd) Changed: the network interface setting moved to the standard settings key (bind); the instance restarts once after this update.
133
-
134
- ### 1.6.1 (2026-09-07) — stable
135
-
136
- - (krobipd) Changed: installing straight from GitHub is no longer offered — the adapter is built before publishing, so it is installed from the ioBroker repository instead.
137
-
138
143
  [Older changelogs can be found there](CHANGELOG_OLD.md)
139
144
 
140
145
  ## History
@@ -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; Home Assistant und Homey nutzen immer 8060 — gib 8060 dem Roku, den sie steuern sollen.",
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,8 +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
33
  "channelKeys": "Fernbedienungstasten",
36
34
  "channelKeysDesc": "Ein Datenpunkt je Taste, die dieses Gerät anbietet; ein Tastendruck setzt ihn kurz auf wahr."
37
35
  }
@@ -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; Home Assistant and Homey always use 8060 — give 8060 to the Roku they should control.",
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,8 +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
33
  "channelKeys": "Remote keys",
36
34
  "channelKeysDesc": "One state per remote key this device offers; a key press pulses it briefly."
37
35
  }
@@ -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; Home Assistant y Homey usan siempre el 8060: asigna el 8060 al Roku que deben controlar.",
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,8 +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
33
  "channelKeys": "Teclas del mando",
36
34
  "channelKeysDesc": "Un estado por cada tecla que ofrece este dispositivo; una pulsación lo pone brevemente en verdadero."
37
35
  }
@@ -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 ; Home Assistant et Homey utilisent toujours 8060 — attribuez 8060 au Roku qu'ils doivent piloter.",
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,8 +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
33
  "channelKeys": "Touches de la télécommande",
36
34
  "channelKeysDesc": "Un état par touche proposée par cet appareil ; un appui le met brièvement à vrai."
37
35
  }
@@ -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; Home Assistant e Homey usano sempre la 8060 — assegna la 8060 al Roku che devono controllare.",
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,8 +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
33
  "channelKeys": "Tasti del telecomando",
36
34
  "channelKeysDesc": "Uno stato per ogni tasto offerto da questo dispositivo; una pressione lo porta brevemente a vero."
37
35
  }
@@ -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; Home Assistant en Homey gebruiken altijd 8060 — geef 8060 aan de Roku die zij moeten bedienen.",
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,8 +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
33
  "channelKeys": "Afstandsbedieningstoetsen",
36
34
  "channelKeysDesc": "Eén status per toets die dit apparaat biedt; een toetsdruk zet hem kort op waar."
37
35
  }
@@ -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; Home Assistant i Homey zawsze używają 8060 — nadaj 8060 temu Roku, którym mają sterować.",
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,8 +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
33
  "channelKeys": "Klawisze pilota",
36
34
  "channelKeysDesc": "Jeden stan na każdy klawisz oferowany przez to urządzenie; naciśnięcie ustawia go na krótko na wartość true."
37
35
  }
@@ -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; o Home Assistant e o Homey usam sempre a 8060 — atribua a 8060 ao Roku que devem controlar.",
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,8 +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
33
  "channelKeys": "Teclas do comando",
36
34
  "channelKeysDesc": "Um estado por cada tecla que este dispositivo oferece; uma pressão coloca-o brevemente a verdadeiro."
37
35
  }
@@ -7,7 +7,7 @@
7
7
  "deviceName": "Имя",
8
8
  "devicePort": "Порт ECP",
9
9
  "portLabel": "Порт",
10
- "devicePortHint": "Порт ECP, на котором отвечает этот Roku. Каждому эмулированному Roku нужен свой порт. Harmony и Sofabaton берут порт из обнаружения; Home Assistant и Homey всегда используют 8060 — назначьте 8060 тому Roku, которым они должны управлять.",
10
+ "devicePortHint": "Порт ECP, на котором отвечает этот Roku. Каждому эмулированному Roku нужен свой порт. Harmony и Sofabaton берут порт из обнаружения.",
11
11
  "deviceTypeLabel": "Тип",
12
12
  "deviceTypePlayer": "Проигрыватель (приставка)",
13
13
  "deviceTypeTv": "ТВ (с громкостью, питанием, каналом, входом)",
@@ -30,8 +30,6 @@
30
30
  "connectionStatusDesc": "Истина только пока каждый настроенный эмулированный Roku действительно слушает.",
31
31
  "stateLastCommand": "Последняя команда",
32
32
  "stateLastCommandDesc": "Последняя команда, полученная этим эмулированным Roku, в виде текста.",
33
- "stateLastCommandType": "Тип последней команды",
34
- "stateLastCommandTypeDesc": "Какого рода была команда: нажатие кнопки, удержание или отпускание, запуск или установка приложения, ввод текста или поиск.",
35
33
  "channelKeys": "Кнопки пульта",
36
34
  "channelKeysDesc": "По одному состоянию на каждую кнопку пульта этого устройства; нажатие кратко переводит его в истину."
37
35
  }
@@ -7,7 +7,7 @@
7
7
  "deviceName": "Назва",
8
8
  "devicePort": "Порт ECP",
9
9
  "portLabel": "Порт",
10
- "devicePortHint": "Порт ECP, на якому відповідає цей Roku. Кожному емульованому Roku потрібен власний порт. Harmony і Sofabaton беруть порт із виявлення; Home Assistant і Homey завжди використовують 8060 — призначте 8060 тому Roku, яким вони мають керувати.",
10
+ "devicePortHint": "Порт ECP, на якому відповідає цей Roku. Кожному емульованому Roku потрібен власний порт. Harmony і Sofabaton беруть порт із виявлення.",
11
11
  "deviceTypeLabel": "Тип",
12
12
  "deviceTypePlayer": "Програвач (приставка)",
13
13
  "deviceTypeTv": "ТБ (з гучністю, живленням, каналом, входом)",
@@ -30,8 +30,6 @@
30
30
  "connectionStatusDesc": "Істина лише поки кожен налаштований емульований Roku справді слухає.",
31
31
  "stateLastCommand": "Остання команда",
32
32
  "stateLastCommandDesc": "Найновіша команда, яку отримав цей емульований Roku, у вигляді тексту.",
33
- "stateLastCommandType": "Тип останньої команди",
34
- "stateLastCommandTypeDesc": "Якого роду була команда: натискання кнопки, утримання чи відпускання, запуск або встановлення застосунку, введення тексту чи пошук.",
35
33
  "channelKeys": "Кнопки пульта",
36
34
  "channelKeysDesc": "По одному стану на кожну кнопку, яку пропонує цей пристрій; натискання ненадовго переводить його в істину."
37
35
  }
@@ -7,7 +7,7 @@
7
7
  "deviceName": "名称",
8
8
  "devicePort": "ECP 端口",
9
9
  "portLabel": "端口",
10
- "devicePortHint": "此 Roku 响应的 ECP 端口。每个模拟 Roku 都需要自己的端口。Harmony 和 Sofabaton 从发现信息中读取端口;Home Assistant 和 Homey 始终使用 8060——请把 8060 分配给它们要控制的 Roku。",
10
+ "devicePortHint": "此 Roku 响应的 ECP 端口。每个模拟 Roku 都需要自己的端口。Harmony 和 Sofabaton 从发现信息中读取端口。",
11
11
  "deviceTypeLabel": "类型",
12
12
  "deviceTypePlayer": "播放器(流媒体盒子)",
13
13
  "deviceTypeTv": "电视(带音量、电源、频道、输入)",
@@ -30,8 +30,6 @@
30
30
  "connectionStatusDesc": "仅当每个已配置的模拟 Roku 都在监听时才为真。",
31
31
  "stateLastCommand": "最后一条命令",
32
32
  "stateLastCommandDesc": "该模拟 Roku 最近收到的命令,以纯文本表示。",
33
- "stateLastCommandType": "最后一条命令的类型",
34
- "stateLastCommandTypeDesc": "命令的种类:按键、按住或松开按键、启动或安装应用、输入文本或搜索。",
35
33
  "channelKeys": "遥控器按键",
36
34
  "channelKeysDesc": "该设备提供的每个遥控器按键各有一个状态;按下时会短暂置为真。"
37
35
  }
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var command_handler_exports = {};
20
+ __export(command_handler_exports, {
21
+ CommandHandler: () => CommandHandler,
22
+ HOLD_MAX_MS: () => HOLD_MAX_MS,
23
+ KEY_PULSE_MS: () => KEY_PULSE_MS,
24
+ MAX_COMMANDS_PER_SECOND: () => MAX_COMMANDS_PER_SECOND,
25
+ RATE_WARN_INTERVAL_MS: () => RATE_WARN_INTERVAL_MS
26
+ });
27
+ module.exports = __toCommonJS(command_handler_exports);
28
+ var import_state_model = require("./ecp/state-model");
29
+ var import_log_throttle = require("./lib/log-throttle");
30
+ var import_rate_gate = require("./lib/rate-gate");
31
+ const KEY_PULSE_MS = 50;
32
+ const HOLD_MAX_MS = 3e4;
33
+ const MAX_COMMANDS_PER_SECOND = 25;
34
+ const RATE_WARN_INTERVAL_MS = 6e4;
35
+ class CommandHandler {
36
+ /**
37
+ * @param host the adapter's writer, timers and log
38
+ */
39
+ constructor(host) {
40
+ this.host = host;
41
+ }
42
+ host;
43
+ pulseTimers = /* @__PURE__ */ new Set();
44
+ /** Per held key id, its watchdog timer — so a keydown without a keyup cannot pin it true forever. */
45
+ holdTimers = /* @__PURE__ */ new Map();
46
+ gates = /* @__PURE__ */ new Map();
47
+ rateWarnings = new import_log_throttle.LogThrottle(RATE_WARN_INTERVAL_MS);
48
+ /**
49
+ * Apply a received ECP command to the device's states: record it in `command`, and pulse or hold
50
+ * the standard key if the device carries it. The server logs only what this accepted, so the rate gate covers the
51
+ * log as well as the states database.
52
+ *
53
+ * @param device the emulated Roku the command is for
54
+ * @param cmd the parsed ECP command
55
+ * @returns true if the command was applied, false if the rate gate dropped it
56
+ */
57
+ apply(device, cmd) {
58
+ var _a;
59
+ const write = (0, import_state_model.commandToStateWrite)(cmd);
60
+ const releaseOf = ((_a = write.holdKey) == null ? void 0 : _a.value) === false ? `${device.id}.keys.${write.holdKey.key}` : null;
61
+ const isRelease = releaseOf !== null && this.holdTimers.has(releaseOf);
62
+ if (!isRelease && !this.admit(device)) {
63
+ return false;
64
+ }
65
+ this.host.writeState(`${device.id}.command`, write.command);
66
+ if (write.pulseKey && device.keys.has(write.pulseKey)) {
67
+ this.pulse(`${device.id}.keys.${write.pulseKey}`);
68
+ } else if (write.holdKey && device.keys.has(write.holdKey.key)) {
69
+ this.hold(`${device.id}.keys.${write.holdKey.key}`, write.holdKey.value);
70
+ }
71
+ return true;
72
+ }
73
+ /** Disarm every timer — the adapter is going down. */
74
+ dispose() {
75
+ for (const timer of this.pulseTimers) {
76
+ this.host.clearTimeout(timer);
77
+ }
78
+ this.pulseTimers.clear();
79
+ for (const timer of this.holdTimers.values()) {
80
+ this.host.clearTimeout(timer);
81
+ }
82
+ this.holdTimers.clear();
83
+ this.gates.clear();
84
+ }
85
+ /**
86
+ * A keypress: true now, false {@link KEY_PULSE_MS} later. A keypress on a HELD key ends the hold, or its watchdog
87
+ * would later write a release for a key the pulse already released.
88
+ *
89
+ * @param id the full key state id
90
+ */
91
+ pulse(id) {
92
+ this.clearHoldTimer(id);
93
+ this.host.writeState(id, true);
94
+ const timer = this.host.setTimeout(() => {
95
+ if (timer) {
96
+ this.pulseTimers.delete(timer);
97
+ }
98
+ if (!this.holdTimers.has(id)) {
99
+ this.host.writeState(id, false);
100
+ }
101
+ }, KEY_PULSE_MS);
102
+ if (timer) {
103
+ this.pulseTimers.add(timer);
104
+ }
105
+ }
106
+ /**
107
+ * A keydown holds the key true until its keyup; a watchdog releases it after {@link HOLD_MAX_MS} when the keyup is
108
+ * lost. A repeated keydown re-arms it.
109
+ *
110
+ * @param id the full key state id
111
+ * @param down true for keydown, false for keyup
112
+ */
113
+ hold(id, down) {
114
+ this.host.writeState(id, down);
115
+ this.clearHoldTimer(id);
116
+ if (down) {
117
+ const timer = this.host.setTimeout(() => {
118
+ this.holdTimers.delete(id);
119
+ this.host.writeState(id, false);
120
+ }, HOLD_MAX_MS);
121
+ if (timer) {
122
+ this.holdTimers.set(id, timer);
123
+ }
124
+ }
125
+ }
126
+ /**
127
+ * Disarm the hold watchdog of one key, if it has one.
128
+ *
129
+ * @param id the full key state id
130
+ */
131
+ clearHoldTimer(id) {
132
+ const timer = this.holdTimers.get(id);
133
+ if (timer) {
134
+ this.host.clearTimeout(timer);
135
+ this.holdTimers.delete(id);
136
+ }
137
+ }
138
+ /**
139
+ * The rate gate: {@link MAX_COMMANDS_PER_SECOND} per device, the excess dropped and reported once per
140
+ * {@link RATE_WARN_INTERVAL_MS}. Every accepted command costs three writes plus one when the pulse ends.
141
+ *
142
+ * @param device the emulated Roku
143
+ * @returns true if the command may be applied
144
+ */
145
+ admit(device) {
146
+ const now = Date.now();
147
+ let gate = this.gates.get(device.id);
148
+ if (!gate) {
149
+ gate = new import_rate_gate.RateGate(MAX_COMMANDS_PER_SECOND, now);
150
+ this.gates.set(device.id, gate);
151
+ }
152
+ if (gate.allow(now)) {
153
+ return true;
154
+ }
155
+ if (this.rateWarnings.due(device.id, now)) {
156
+ this.host.warn(
157
+ `Emulated Roku "${device.name}" receives more than ${MAX_COMMANDS_PER_SECOND} commands per second \u2014 dropping the excess (a misbehaving controller?)`
158
+ );
159
+ }
160
+ return false;
161
+ }
162
+ }
163
+ // Annotate the CommonJS export names for ESM import in node:
164
+ 0 && (module.exports = {
165
+ CommandHandler,
166
+ HOLD_MAX_MS,
167
+ KEY_PULSE_MS,
168
+ MAX_COMMANDS_PER_SECOND,
169
+ RATE_WARN_INTERVAL_MS
170
+ });
171
+ //# sourceMappingURL=command-handler.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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`, 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
+ "names": []
7
+ }
@@ -29,7 +29,7 @@ var import_device_config = require("./lib/device-config");
29
29
  var import_device_identity = require("./lib/device-identity");
30
30
  var import_i18n = require("./lib/i18n");
31
31
  var import_pure_helpers = require("./lib/pure-helpers");
32
- const ID_EXPRESSION = "(data.name||'').trim().replace(/[^A-Za-z0-9\\-_]/g,'_')";
32
+ const ID_EXPRESSION = `(data.name||'').trim().replace(/${import_pure_helpers.UNSAFE_ID_CHAR.source}/g,'_')`;
33
33
  function buildDeviceForm(usedNames, usedPorts, usedIds) {
34
34
  const nameList = JSON.stringify(usedNames.map((n) => n.trim().toLowerCase()));
35
35
  const portList = JSON.stringify([...usedPorts]);
@@ -98,7 +98,7 @@ function cleanDevice(raw) {
98
98
  }
99
99
  class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
100
100
  get objId() {
101
- return `system.adapter.${this.adapter.namespace}`;
101
+ return (0, import_constants.instanceObjectId)(this.adapter.namespace);
102
102
  }
103
103
  /**
104
104
  * Read the device list from the live config object, through the shared normaliser — against
@@ -185,8 +185,7 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
185
185
  * the runtime gives when two names map to one object id.
186
186
  *
187
187
  * The name needs no fallback: a row without a usable one never becomes a DeviceRow
188
- * (lib/device-config.ts drops it), so `Roku <n>` was a branch nothing could reach —
189
- * the mutation run of 1.6.0 found it by having no test that could fail on its removal.
188
+ * (lib/device-config.ts drops it).
190
189
  *
191
190
  * @param device the stored device
192
191
  * @returns the card descriptor
@@ -226,8 +225,8 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
226
225
  };
227
226
  }
228
227
  /**
229
- * Manual add: pre-select a free port, show the form, and append the device with
230
- * a stable derived uuid.
228
+ * Manual add: pre-select a free port, show the form, and append the device with its own
229
+ * random identity and a fixed object id.
231
230
  *
232
231
  * @param context the action context
233
232
  * @returns a directive to reload the manager
@@ -236,7 +235,7 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
236
235
  const devices = await this.readDevices();
237
236
  const usedNames = devices.map((d) => d.name);
238
237
  const usedPorts = devices.map((d) => d.port);
239
- const usedIds = devices.map(import_device_config.deviceObjectId);
238
+ const usedIds = devices.map((d) => d.objectId);
240
239
  const form = buildDeviceForm(usedNames, usedPorts, usedIds);
241
240
  const data = await context.showForm(form.schema, {
242
241
  title: (0, import_i18n.t)("dmAdd"),