iobroker.fakeroku 1.2.0 → 1.4.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 (49) hide show
  1. package/README.md +37 -20
  2. package/admin/i18n/de.json +11 -2
  3. package/admin/i18n/en.json +11 -2
  4. package/admin/i18n/es.json +11 -2
  5. package/admin/i18n/fr.json +11 -2
  6. package/admin/i18n/it.json +11 -2
  7. package/admin/i18n/nl.json +11 -2
  8. package/admin/i18n/pl.json +11 -2
  9. package/admin/i18n/pt.json +11 -2
  10. package/admin/i18n/ru.json +11 -2
  11. package/admin/i18n/uk.json +11 -2
  12. package/admin/i18n/zh-cn.json +11 -2
  13. package/admin/jsonConfig.json +2 -2
  14. package/build/device-management.js +19 -6
  15. package/build/device-management.js.map +2 -2
  16. package/build/discovery/ssdp-responder.js +8 -5
  17. package/build/discovery/ssdp-responder.js.map +2 -2
  18. package/build/ecp/device-info.js +2 -2
  19. package/build/ecp/device-info.js.map +2 -2
  20. package/build/ecp/ecp-command.js +3 -0
  21. package/build/ecp/ecp-command.js.map +2 -2
  22. package/build/ecp/ecp-http-server.js +4 -4
  23. package/build/ecp/ecp-http-server.js.map +2 -2
  24. package/build/ecp/state-model.js +4 -1
  25. package/build/ecp/state-model.js.map +2 -2
  26. package/build/lib/constants.js +8 -2
  27. package/build/lib/constants.js.map +2 -2
  28. package/build/lib/detect-ip.js +46 -2
  29. package/build/lib/detect-ip.js.map +2 -2
  30. package/build/lib/device-identity.js +8 -2
  31. package/build/lib/device-identity.js.map +2 -2
  32. package/build/lib/errors.js +31 -0
  33. package/build/lib/errors.js.map +7 -0
  34. package/build/lib/i18n.js +18 -2
  35. package/build/lib/i18n.js.map +2 -2
  36. package/build/{ecp → lib}/lan-guard.js +8 -2
  37. package/build/lib/lan-guard.js.map +7 -0
  38. package/build/lib/object-cleanup.js +8 -0
  39. package/build/lib/object-cleanup.js.map +2 -2
  40. package/build/lib/pure-helpers.js +8 -1
  41. package/build/lib/pure-helpers.js.map +2 -2
  42. package/build/lib/rate-gate.js +58 -0
  43. package/build/lib/rate-gate.js.map +7 -0
  44. package/build/main.js +174 -29
  45. package/build/main.js.map +2 -2
  46. package/io-package.json +94 -45
  47. package/package.json +1 -1
  48. package/admin/fakeroku.png +0 -0
  49. package/build/ecp/lan-guard.js.map +0 -7
package/README.md CHANGED
@@ -6,7 +6,7 @@
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 devices** on your LAN so that ECP/SSDP remotes — a
9
+ Emulates one or more **[Roku](https://www.roku.com/) devices** on your LAN so that ECP/SSDP remotes — a
10
10
  Logitech Harmony Hub or a Sofabaton X1/X2 — can trigger events in
11
11
  ioBroker. It is the **input** counterpart to the Logitech Harmony adapter: a button
12
12
  on the remote becomes a datapoint in ioBroker.
@@ -25,7 +25,7 @@ classic Harmony hub.
25
25
  - Emulates one or more Roku devices on the LAN — the Roku control protocol (ECP) over HTTP plus SSDP discovery on port 1900.
26
26
  - Full Roku control surface including `/query/device-info` with a current Roku version, beyond what a classic Harmony hub needs.
27
27
  - Clean data model per device: a `command` datapoint plus fixed `keys.<Key>` states, all created up front.
28
- - Several emulated Rokus from a single instance; discovery bound to the chosen network interface; command handling restricted to the LAN.
28
+ - Several emulated Rokus from a single instance; discovery bound to the chosen network interface; command handling restricted to the LAN and capped at 25 commands per second per emulated Roku, so a misbehaving device cannot flood ioBroker.
29
29
 
30
30
  ## Sentry / Error reporting
31
31
 
@@ -37,11 +37,12 @@ For details and how to disable it, see the [Sentry plugin documentation](https:/
37
37
 
38
38
  - Node.js >= 22
39
39
  - js-controller >= 7.2.2
40
- - admin >= 7.8.23
40
+ - admin >= 8.0.11
41
41
 
42
- ## Installation
42
+ ## Ports
43
43
 
44
- Install the adapter from the ioBroker admin.
44
+ - **TCP 8060 (listening, one per emulated Roku, configurable)** — the Roku control protocol (ECP): the remote sends its key presses here and reads the actual port from the discovery announcement.
45
+ - **UDP 1900 (multicast, listening and outgoing)** — SSDP discovery, so a Harmony hub or Sofabaton finds the emulated Rokus; fixed by the UPnP standard.
45
46
 
46
47
  ## Configuration
47
48
 
@@ -60,7 +61,7 @@ Install the adapter from the ioBroker admin.
60
61
  To add the emulated Roku to a Harmony hub, add a "Roku" device in the Harmony app
61
62
  and point it at the ioBroker host.
62
63
 
63
- ## Objects
64
+ ## State Tree
64
65
 
65
66
  At instance level:
66
67
 
@@ -83,19 +84,34 @@ not get their own objects.
83
84
  > play and pause cannot be told apart here — that is a protocol limitation, not an
84
85
  > adapter one.
85
86
 
86
- ## Usage
87
-
88
87
  In a script or Blockly rule, react to a key — e.g. when `fakeroku.0.<name>.keys.Play`
89
- becomes `true`, or watch `.command` for the last button as text.
88
+ becomes `true` or watch `.command` for the last button as text.
90
89
 
91
90
  ## Changelog
92
91
  <!--
93
92
  Placeholder for the next version (at the beginning of the line):
94
93
  ### **WORK IN PROGRESS**
95
94
  -->
95
+ ### 1.4.0 (2026-09-03)
96
+ - (krobipd) Fixed: renaming an emulated Roku could change its identity on the network, so a paired Harmony or Sofabaton lost the device and had to be set up again.
97
+ - (krobipd) Fixed: a remote key that was pressed when the adapter stopped stayed on for good. All key datapoints are now released at start-up, so the next press works again.
98
+ - (krobipd) Fixed: a device named "info" entered by hand into the configuration replaced the instance's own status channel. The name is refused now and leftovers are removed.
99
+ - (krobipd) Changed: every datapoint now carries a translated name and, where useful, a short description — in all eleven languages, in existing installations as well.
100
+ - (krobipd) Improved: a remote with a globally routable IPv6 address is accepted when it sits in the same network as the ioBroker host, not just on the reserved IPv6 ranges.
101
+ - (krobipd) New: user documentation in English and German, shown in the ioBroker documentation portal.
102
+
103
+ ### 1.3.0 (2026-09-01)
104
+ - (krobipd) Fixed: a malformed keyboard keypress from a remote (a bad %-escape in the URL) could crash the adapter.
105
+ - (krobipd) Fixed: remotes on an IPv6-only local network were refused; link-local and unique-local IPv6 addresses now count as LAN.
106
+ - (krobipd) Fixed: the adapter icon in the admin is now the same one shown on GitHub.
107
+ - (krobipd) Changed: requires admin >= 8.0.11.
108
+ - (krobipd) Improved: discovery answers only searches from your own network, and the device dialog in the admin keeps working after the device list was edited by hand.
109
+ - (krobipd) Improved: the emulated Roku reports Roku OS 15.0 (was 14.1), and the command-type datapoint lists its possible values so the admin shows them as labels.
110
+ - (krobipd) New: a misbehaving device on your network can no longer flood ioBroker — more than 25 commands per second per emulated Roku are dropped and reported in the log.
111
+
96
112
  ### 1.2.0 (2026-08-27)
97
113
  - (krobipd) Changed: the instance now reports "not connected" while a configured Roku is missing — a device whose port is taken no longer hides behind the ones that did start.
98
- - (krobipd) Fixed: stopping the adapter now clears its connection state instead of leaving the instance showing "connected" while nothing is running.
114
+ - (krobipd) Improved: shutdown now waits for its last write to land before reporting done, so the connection state cannot be lost on a slow or busy system.
99
115
  - (krobipd) New: optional error reporting via Sentry — only active if you enabled diagnostics in ioBroker, and it transmits no personal data.
100
116
 
101
117
  ### 1.1.0 (2026-08-10) — stable
@@ -109,16 +125,6 @@ becomes `true`, or watch `.command` for the last button as text.
109
125
  - (krobipd) First stable release — version 1.0.0 marks the complete rewrite as the mature, supported version of the adapter.
110
126
  - (krobipd) Upgrading from an older version now shows a one-time notice that the button data points changed from text to real boolean values, so scripts and visualizations can be checked.
111
127
 
112
- ### 0.6.0 (2026-08-05)
113
- - (krobipd) Complete rewrite. The adapter now answers the full Roku control surface — including device-info with a current Roku version — so Logitech Harmony and Sofabaton remotes pair and work reliably.
114
- - (krobipd) Works out of the box: it detects the network address to advertise on its own, no manual interface picking.
115
- - (krobipd) Manage multiple emulated Rokus from the admin UI, each as a Player or a TV.
116
- - (krobipd) Cleaner object tree — one datapoint per remote button with the correct types, plus a last-command datapoint; leftover objects from older versions are removed on start.
117
-
118
- ### 0.5.1 (2026-08-05)
119
- - (mcm1957) Adapter requires Node.js >= 22 now
120
- - (mcm1957) Dependencies have been updated
121
-
122
128
  [Older changelogs can be found there](CHANGELOG_OLD.md)
123
129
 
124
130
  ## History
@@ -137,6 +143,17 @@ users it is simply a new version of the same adapter:
137
143
  - From **0.6.0** on, [krobi](https://github.com/krobipd) rewrote the adapter from the
138
144
  ground up in TypeScript and added the full ECP surface including `device-info`.
139
145
 
146
+ ## Support
147
+
148
+ - [ioBroker Forum](https://forum.iobroker.net/)
149
+ - [GitHub Issues](https://github.com/iobroker-community-adapters/ioBroker.fakeroku/issues)
150
+
151
+ ### Support Development
152
+
153
+ This adapter is free and open source. If you find it useful, consider buying me a coffee:
154
+
155
+ [![Ko-fi](https://img.shields.io/badge/Ko--fi-Support%20me-ff5e5b?logo=ko-fi)](https://ko-fi.com/krobipd) [![PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/krobipd)
156
+
140
157
  ## License
141
158
 
142
159
  The MIT License (MIT)
@@ -3,7 +3,7 @@
3
3
  "networkInterface": "Netzwerkschnittstelle (Binden + Ankündigen)",
4
4
  "networkInterfaceTooltip": "Die Netzwerkkarte, an die sich die emulierten Rokus binden und über die sie sich ankündigen. Auf 'alle Schnittstellen' lassen — die passende IP wird automatisch erkannt, es läuft also ohne Einstellung. Nur bei mehreren Netzwerkkarten eine feste Adresse wählen.",
5
5
  "headerDevices": "Emulierte Roku-Geräte",
6
- "devicesHelp": "Jede Karte ist ein emulierter Roku. Lege einen an, dann füge in deiner Harmony- oder Sofabaton-App ein **Roku**-Gerät hinzu, das auf diesen ioBroker-Host zeigt — jeder Tastendruck landet in den Objekten des Geräts.",
6
+ "devicesHelp": "Jede Karte ist ein emulierter Roku. Lege einen an, dann füge in deiner Harmony- oder Sofabaton-App ein <b>Roku</b>-Gerät hinzu, das auf diesen ioBroker-Host zeigt — jeder Tastendruck landet in den Objekten des Geräts.",
7
7
  "deviceName": "Name",
8
8
  "devicePort": "ECP-Port",
9
9
  "portLabel": "Port",
@@ -23,5 +23,14 @@
23
23
  "supportHeader": "Unterstützen",
24
24
  "aboutInfo": "Wenn dir dieser Adapter nützt, hilft eine kleine Spende, ihn zu pflegen — danke!",
25
25
  "donateKofi": "Ko-fi",
26
- "donatePaypal": "PayPal"
26
+ "donatePaypal": "PayPal",
27
+ "channelInfo": "Adapter-Informationen",
28
+ "connectionStatus": "Verbindungsstatus",
29
+ "connectionStatusDesc": "Nur wahr, solange jeder konfigurierte emulierte Roku tatsächlich lauscht.",
30
+ "stateLastCommand": "Letzter Befehl",
31
+ "stateLastCommandDesc": "Der zuletzt empfangene Befehl dieses emulierten Roku als Klartext.",
32
+ "stateLastCommandType": "Art des letzten Befehls",
33
+ "stateLastCommandTypeDesc": "Um welche Art Befehl es sich handelte: Tastendruck, Taste gehalten oder losgelassen, App-Start oder -Installation, Texteingabe oder Suche.",
34
+ "channelKeys": "Fernbedienungstasten",
35
+ "channelKeysDesc": "Ein Datenpunkt je Taste, die dieses Gerät anbietet; ein Tastendruck setzt ihn kurz auf wahr."
27
36
  }
@@ -3,7 +3,7 @@
3
3
  "networkInterface": "Network interface (bind + advertise)",
4
4
  "networkInterfaceTooltip": "The network card the emulated Rokus bind to and advertise on. Leave it on 'all interfaces' — the routable IP is detected automatically, so it works out of the box. Only pick a specific address on a host with several network cards.",
5
5
  "headerDevices": "Emulated Roku devices",
6
- "devicesHelp": "Each card is one emulated Roku. Add one, then add a **Roku** device in your Harmony or Sofabaton app pointing at this ioBroker host — every keypress lands in the device's objects.",
6
+ "devicesHelp": "Each card is one emulated Roku. Add one, then add a <b>Roku</b> device in your Harmony or Sofabaton app pointing at this ioBroker host — every keypress lands in the device's objects.",
7
7
  "deviceName": "Name",
8
8
  "devicePort": "ECP port",
9
9
  "portLabel": "Port",
@@ -23,5 +23,14 @@
23
23
  "supportHeader": "Support",
24
24
  "aboutInfo": "If this adapter is useful to you, a small donation helps keep it maintained — thank you!",
25
25
  "donateKofi": "Ko-fi",
26
- "donatePaypal": "PayPal"
26
+ "donatePaypal": "PayPal",
27
+ "channelInfo": "Adapter information",
28
+ "connectionStatus": "Connection status",
29
+ "connectionStatusDesc": "True only while every configured emulated Roku is actually listening.",
30
+ "stateLastCommand": "Last command",
31
+ "stateLastCommandDesc": "The most recent command this emulated Roku received, as plain text.",
32
+ "stateLastCommandType": "Last command type",
33
+ "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.",
34
+ "channelKeys": "Remote keys",
35
+ "channelKeysDesc": "One state per remote key this device offers; a key press pulses it briefly."
27
36
  }
@@ -3,7 +3,7 @@
3
3
  "networkInterface": "Interfaz de red (enlace + anuncio)",
4
4
  "networkInterfaceTooltip": "La tarjeta de red a la que se enlazan y anuncian los Roku emulados. Déjala en 'todas las interfaces': la IP enrutable se detecta automáticamente y funciona sin configurar. Elige una dirección concreta solo en un host con varias tarjetas de red.",
5
5
  "headerDevices": "Dispositivos Roku emulados",
6
- "devicesHelp": "Cada tarjeta es un Roku emulado. Añade uno y luego añade un dispositivo **Roku** en tu app Harmony o Sofabaton apuntando a este host de ioBroker — cada pulsación llega a los objetos del dispositivo.",
6
+ "devicesHelp": "Cada tarjeta es un Roku emulado. Añade uno y luego añade un dispositivo <b>Roku</b> en tu app Harmony o Sofabaton apuntando a este host de ioBroker — cada pulsación llega a los objetos del dispositivo.",
7
7
  "deviceName": "Nombre",
8
8
  "devicePort": "Puerto ECP",
9
9
  "portLabel": "Puerto",
@@ -23,5 +23,14 @@
23
23
  "supportHeader": "Apoyar",
24
24
  "aboutInfo": "Si este adaptador te resulta útil, una pequeña donación ayuda a mantenerlo — ¡gracias!",
25
25
  "donateKofi": "Ko-fi",
26
- "donatePaypal": "PayPal"
26
+ "donatePaypal": "PayPal",
27
+ "channelInfo": "Información del adaptador",
28
+ "connectionStatus": "Estado de conexión",
29
+ "connectionStatusDesc": "Verdadero solo mientras cada Roku emulado configurado esté realmente a la escucha.",
30
+ "stateLastCommand": "Último comando",
31
+ "stateLastCommandDesc": "El comando más reciente que recibió este Roku emulado, como texto simple.",
32
+ "stateLastCommandType": "Tipo del último comando",
33
+ "stateLastCommandTypeDesc": "Qué tipo de comando fue: pulsar una tecla, mantenerla o soltarla, iniciar o instalar una aplicación, introducir texto o buscar.",
34
+ "channelKeys": "Teclas del mando",
35
+ "channelKeysDesc": "Un estado por cada tecla que ofrece este dispositivo; una pulsación lo pone brevemente en verdadero."
27
36
  }
@@ -3,7 +3,7 @@
3
3
  "networkInterface": "Interface réseau (liaison + annonce)",
4
4
  "networkInterfaceTooltip": "La carte réseau à laquelle les Roku émulés se lient et s'annoncent. Laissez-la sur 'toutes les interfaces' : l'IP routable est détectée automatiquement, cela fonctionne sans réglage. Ne choisissez une adresse précise que sur un hôte avec plusieurs cartes réseau.",
5
5
  "headerDevices": "Appareils Roku émulés",
6
- "devicesHelp": "Chaque carte est un Roku émulé. Ajoutez-en un, puis ajoutez un appareil **Roku** dans votre app Harmony ou Sofabaton pointant vers cet hôte ioBroker — chaque appui atterrit dans les objets de l'appareil.",
6
+ "devicesHelp": "Chaque carte est un Roku émulé. Ajoutez-en un, puis ajoutez un appareil <b>Roku</b> dans votre app Harmony ou Sofabaton pointant vers cet hôte ioBroker — chaque appui atterrit dans les objets de l'appareil.",
7
7
  "deviceName": "Nom",
8
8
  "devicePort": "Port ECP",
9
9
  "portLabel": "Port",
@@ -23,5 +23,14 @@
23
23
  "supportHeader": "Soutenir",
24
24
  "aboutInfo": "Si cet adaptateur vous est utile, un petit don aide à le maintenir — merci !",
25
25
  "donateKofi": "Ko-fi",
26
- "donatePaypal": "PayPal"
26
+ "donatePaypal": "PayPal",
27
+ "channelInfo": "Informations sur l'adaptateur",
28
+ "connectionStatus": "État de la connexion",
29
+ "connectionStatusDesc": "Vrai uniquement tant que chaque Roku émulé configuré est réellement à l'écoute.",
30
+ "stateLastCommand": "Dernière commande",
31
+ "stateLastCommandDesc": "La commande la plus récente reçue par ce Roku émulé, en texte brut.",
32
+ "stateLastCommandType": "Type de la dernière commande",
33
+ "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.",
34
+ "channelKeys": "Touches de la télécommande",
35
+ "channelKeysDesc": "Un état par touche proposée par cet appareil ; un appui le met brièvement à vrai."
27
36
  }
@@ -3,7 +3,7 @@
3
3
  "networkInterface": "Interfaccia di rete (bind + annuncio)",
4
4
  "networkInterfaceTooltip": "La scheda di rete a cui i Roku emulati si collegano e si annunciano. Lasciala su 'tutte le interfacce': l'IP instradabile viene rilevato automaticamente e funziona senza configurazione. Scegli un indirizzo specifico solo su un host con più schede di rete.",
5
5
  "headerDevices": "Dispositivi Roku emulati",
6
- "devicesHelp": "Ogni scheda è un Roku emulato. Aggiungine uno, poi aggiungi un dispositivo **Roku** nell'app Harmony o Sofabaton che punta a questo host ioBroker — ogni pressione finisce negli oggetti del dispositivo.",
6
+ "devicesHelp": "Ogni scheda è un Roku emulato. Aggiungine uno, poi aggiungi un dispositivo <b>Roku</b> nell'app Harmony o Sofabaton che punta a questo host ioBroker — ogni pressione finisce negli oggetti del dispositivo.",
7
7
  "deviceName": "Nome",
8
8
  "devicePort": "Porta ECP",
9
9
  "portLabel": "Porta",
@@ -23,5 +23,14 @@
23
23
  "supportHeader": "Sostieni",
24
24
  "aboutInfo": "Se questo adattatore ti è utile, una piccola donazione aiuta a mantenerlo — grazie!",
25
25
  "donateKofi": "Ko-fi",
26
- "donatePaypal": "PayPal"
26
+ "donatePaypal": "PayPal",
27
+ "channelInfo": "Informazioni sull'adattatore",
28
+ "connectionStatus": "Stato connessione",
29
+ "connectionStatusDesc": "Vero solo finché ogni Roku emulato configurato è effettivamente in ascolto.",
30
+ "stateLastCommand": "Ultimo comando",
31
+ "stateLastCommandDesc": "Il comando più recente ricevuto da questo Roku emulato, come testo semplice.",
32
+ "stateLastCommandType": "Tipo dell'ultimo comando",
33
+ "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.",
34
+ "channelKeys": "Tasti del telecomando",
35
+ "channelKeysDesc": "Uno stato per ogni tasto offerto da questo dispositivo; una pressione lo porta brevemente a vero."
27
36
  }
@@ -3,7 +3,7 @@
3
3
  "networkInterface": "Netwerkinterface (binden + aankondigen)",
4
4
  "networkInterfaceTooltip": "De netwerkkaart waaraan de geëmuleerde Roku's zich binden en aankondigen. Laat het op 'alle interfaces' staan — het routeerbare IP wordt automatisch gedetecteerd, dus het werkt meteen. Kies alleen een specifiek adres op een host met meerdere netwerkkaarten.",
5
5
  "headerDevices": "Geëmuleerde Roku-apparaten",
6
- "devicesHelp": "Elke kaart is één geëmuleerde Roku. Voeg er een toe en voeg daarna in je Harmony- of Sofabaton-app een **Roku**-apparaat toe dat naar deze ioBroker-host wijst — elke toetsdruk komt in de objecten van het apparaat.",
6
+ "devicesHelp": "Elke kaart is één geëmuleerde Roku. Voeg er een toe en voeg daarna in je Harmony- of Sofabaton-app een <b>Roku</b>-apparaat toe dat naar deze ioBroker-host wijst — elke toetsdruk komt in de objecten van het apparaat.",
7
7
  "deviceName": "Naam",
8
8
  "devicePort": "ECP-poort",
9
9
  "portLabel": "Poort",
@@ -23,5 +23,14 @@
23
23
  "supportHeader": "Ondersteunen",
24
24
  "aboutInfo": "Als deze adapter nuttig voor je is, helpt een kleine donatie om hem te onderhouden — bedankt!",
25
25
  "donateKofi": "Ko-fi",
26
- "donatePaypal": "PayPal"
26
+ "donatePaypal": "PayPal",
27
+ "channelInfo": "Adapterinformatie",
28
+ "connectionStatus": "Verbindingsstatus",
29
+ "connectionStatusDesc": "Alleen waar zolang elke geconfigureerde geëmuleerde Roku daadwerkelijk luistert.",
30
+ "stateLastCommand": "Laatste opdracht",
31
+ "stateLastCommandDesc": "De meest recente opdracht die deze geëmuleerde Roku ontving, als platte tekst.",
32
+ "stateLastCommandType": "Type laatste opdracht",
33
+ "stateLastCommandTypeDesc": "Wat voor soort opdracht het was: een toetsdruk, een toets ingedrukt of losgelaten, een app starten of installeren, tekstinvoer of zoeken.",
34
+ "channelKeys": "Afstandsbedieningstoetsen",
35
+ "channelKeysDesc": "Eén status per toets die dit apparaat biedt; een toetsdruk zet hem kort op waar."
27
36
  }
@@ -3,7 +3,7 @@
3
3
  "networkInterface": "Interfejs sieciowy (bind + rozgłaszanie)",
4
4
  "networkInterfaceTooltip": "Karta sieciowa, do której podłączają się i rozgłaszają emulowane Roku. Zostaw na 'wszystkich interfejsach' — routowalny adres IP jest wykrywany automatycznie, więc działa bez konfiguracji. Wybierz konkretny adres tylko na hoście z kilkoma kartami sieciowymi.",
5
5
  "headerDevices": "Emulowane urządzenia Roku",
6
- "devicesHelp": "Każda karta to jeden emulowany Roku. Dodaj urządzenie, a następnie w aplikacji Harmony lub Sofabaton dodaj urządzenie **Roku** wskazujące na tego hosta ioBroker — każde naciśnięcie trafia do obiektów urządzenia.",
6
+ "devicesHelp": "Każda karta to jeden emulowany Roku. Dodaj urządzenie, a następnie w aplikacji Harmony lub Sofabaton dodaj urządzenie <b>Roku</b> wskazujące na tego hosta ioBroker — każde naciśnięcie trafia do obiektów urządzenia.",
7
7
  "deviceName": "Nazwa",
8
8
  "devicePort": "Port ECP",
9
9
  "portLabel": "Port",
@@ -23,5 +23,14 @@
23
23
  "supportHeader": "Wsparcie",
24
24
  "aboutInfo": "Jeśli ten adapter jest dla Ciebie przydatny, mała darowizna pomaga go utrzymać — dziękuję!",
25
25
  "donateKofi": "Ko-fi",
26
- "donatePaypal": "PayPal"
26
+ "donatePaypal": "PayPal",
27
+ "channelInfo": "Informacje o adapterze",
28
+ "connectionStatus": "Stan połączenia",
29
+ "connectionStatusDesc": "Prawda tylko wtedy, gdy każdy skonfigurowany emulowany Roku faktycznie nasłuchuje.",
30
+ "stateLastCommand": "Ostatnie polecenie",
31
+ "stateLastCommandDesc": "Najnowsze polecenie odebrane przez ten emulowany Roku, jako zwykły tekst.",
32
+ "stateLastCommandType": "Typ ostatniego polecenia",
33
+ "stateLastCommandTypeDesc": "Jakiego rodzaju było to polecenie: naciśnięcie klawisza, przytrzymanie lub zwolnienie, uruchomienie lub instalacja aplikacji, wpisanie tekstu albo wyszukiwanie.",
34
+ "channelKeys": "Klawisze pilota",
35
+ "channelKeysDesc": "Jeden stan na każdy klawisz oferowany przez to urządzenie; naciśnięcie ustawia go na krótko na prawdę."
27
36
  }
@@ -3,7 +3,7 @@
3
3
  "networkInterface": "Interface de rede (vincular + anunciar)",
4
4
  "networkInterfaceTooltip": "A placa de rede à qual os Roku emulados se vinculam e anunciam. Deixe em 'todas as interfaces' — o IP roteável é detetado automaticamente, por isso funciona sem configurar. Escolha um endereço específico apenas num host com várias placas de rede.",
5
5
  "headerDevices": "Dispositivos Roku emulados",
6
- "devicesHelp": "Cada cartão é um Roku emulado. Adicione um e depois adicione um dispositivo **Roku** na app Harmony ou Sofabaton a apontar para este host ioBroker — cada tecla cai nos objetos do dispositivo.",
6
+ "devicesHelp": "Cada cartão é um Roku emulado. Adicione um e depois adicione um dispositivo <b>Roku</b> na app Harmony ou Sofabaton a apontar para este host ioBroker — cada tecla cai nos objetos do dispositivo.",
7
7
  "deviceName": "Nome",
8
8
  "devicePort": "Porta ECP",
9
9
  "portLabel": "Porta",
@@ -23,5 +23,14 @@
23
23
  "supportHeader": "Apoiar",
24
24
  "aboutInfo": "Se este adaptador lhe é útil, um pequeno donativo ajuda a mantê-lo — obrigado!",
25
25
  "donateKofi": "Ko-fi",
26
- "donatePaypal": "PayPal"
26
+ "donatePaypal": "PayPal",
27
+ "channelInfo": "Informação do adaptador",
28
+ "connectionStatus": "Estado da ligação",
29
+ "connectionStatusDesc": "Verdadeiro apenas enquanto todos os Roku emulados configurados estiverem realmente à escuta.",
30
+ "stateLastCommand": "Último comando",
31
+ "stateLastCommandDesc": "O comando mais recente recebido por este Roku emulado, em texto simples.",
32
+ "stateLastCommandType": "Tipo do último comando",
33
+ "stateLastCommandTypeDesc": "Que tipo de comando foi: pressionar uma tecla, manter ou soltar, iniciar ou instalar uma aplicação, introduzir texto ou pesquisar.",
34
+ "channelKeys": "Teclas do comando",
35
+ "channelKeysDesc": "Um estado por cada tecla que este dispositivo oferece; uma pressão coloca-o brevemente a verdadeiro."
27
36
  }
@@ -3,7 +3,7 @@
3
3
  "networkInterface": "Сетевой интерфейс (привязка + объявление)",
4
4
  "networkInterfaceTooltip": "Сетевая карта, к которой привязываются и на которой объявляются эмулируемые Roku. Оставьте 'все интерфейсы' — маршрутизируемый IP определяется автоматически, поэтому работает без настройки. Выбирайте конкретный адрес только на хосте с несколькими сетевыми картами.",
5
5
  "headerDevices": "Эмулируемые устройства Roku",
6
- "devicesHelp": "Каждая карточка — это один эмулируемый Roku. Добавьте устройство, затем в приложении Harmony или Sofabaton добавьте устройство **Roku**, указывающее на этот хост ioBroker — каждое нажатие попадает в объекты устройства.",
6
+ "devicesHelp": "Каждая карточка — это один эмулируемый Roku. Добавьте устройство, затем в приложении Harmony или Sofabaton добавьте устройство <b>Roku</b>, указывающее на этот хост ioBroker — каждое нажатие попадает в объекты устройства.",
7
7
  "deviceName": "Имя",
8
8
  "devicePort": "Порт ECP",
9
9
  "portLabel": "Порт",
@@ -23,5 +23,14 @@
23
23
  "supportHeader": "Поддержать",
24
24
  "aboutInfo": "Если адаптер вам полезен, небольшое пожертвование помогает его поддерживать — спасибо!",
25
25
  "donateKofi": "Ko-fi",
26
- "donatePaypal": "PayPal"
26
+ "donatePaypal": "PayPal",
27
+ "channelInfo": "Информация об адаптере",
28
+ "connectionStatus": "Статус подключения",
29
+ "connectionStatusDesc": "Истина только пока каждый настроенный эмулированный Roku действительно слушает.",
30
+ "stateLastCommand": "Последняя команда",
31
+ "stateLastCommandDesc": "Последняя команда, полученная этим эмулированным Roku, в виде текста.",
32
+ "stateLastCommandType": "Тип последней команды",
33
+ "stateLastCommandTypeDesc": "Какого рода была команда: нажатие кнопки, удержание или отпускание, запуск или установка приложения, ввод текста или поиск.",
34
+ "channelKeys": "Кнопки пульта",
35
+ "channelKeysDesc": "По одному состоянию на каждую кнопку пульта этого устройства; нажатие кратко переводит его в истину."
27
36
  }
@@ -3,7 +3,7 @@
3
3
  "networkInterface": "Мережевий інтерфейс (прив'язка + оголошення)",
4
4
  "networkInterfaceTooltip": "Мережева карта, до якої прив'язуються та на якій оголошуються емульовані Roku. Залиште 'всі інтерфейси' — маршрутизовану IP-адресу визначено автоматично, тож працює без налаштування. Вибирайте конкретну адресу лише на хості з кількома мережевими картами.",
5
5
  "headerDevices": "Емульовані пристрої Roku",
6
- "devicesHelp": "Кожна картка — це один емульований Roku. Додайте пристрій, потім у застосунку Harmony або Sofabaton додайте пристрій **Roku**, що вказує на цей хост ioBroker — кожне натискання потрапляє в об'єкти пристрою.",
6
+ "devicesHelp": "Кожна картка — це один емульований Roku. Додайте пристрій, потім у застосунку Harmony або Sofabaton додайте пристрій <b>Roku</b>, що вказує на цей хост ioBroker — кожне натискання потрапляє в об'єкти пристрою.",
7
7
  "deviceName": "Назва",
8
8
  "devicePort": "Порт ECP",
9
9
  "portLabel": "Порт",
@@ -23,5 +23,14 @@
23
23
  "supportHeader": "Підтримати",
24
24
  "aboutInfo": "Якщо цей адаптер вам корисний, невелика пожертва допомагає його підтримувати — дякую!",
25
25
  "donateKofi": "Ko-fi",
26
- "donatePaypal": "PayPal"
26
+ "donatePaypal": "PayPal",
27
+ "channelInfo": "Інформація про адаптер",
28
+ "connectionStatus": "Стан з'єднання",
29
+ "connectionStatusDesc": "Істина лише поки кожен налаштований емульований Roku справді слухає.",
30
+ "stateLastCommand": "Остання команда",
31
+ "stateLastCommandDesc": "Найновіша команда, яку отримав цей емульований Roku, у вигляді тексту.",
32
+ "stateLastCommandType": "Тип останньої команди",
33
+ "stateLastCommandTypeDesc": "Якого роду була команда: натискання кнопки, утримання чи відпускання, запуск або встановлення застосунку, введення тексту чи пошук.",
34
+ "channelKeys": "Кнопки пульта",
35
+ "channelKeysDesc": "По одному стану на кожну кнопку, яку пропонує цей пристрій; натискання ненадовго переводить його в істину."
27
36
  }
@@ -3,7 +3,7 @@
3
3
  "networkInterface": "网络接口(绑定 + 广播)",
4
4
  "networkInterfaceTooltip": "模拟 Roku 绑定并广播的网卡。保留为“所有接口”即可——可路由的 IP 会被自动检测,因此无需配置即可运行。仅在有多块网卡的主机上才选择具体地址。",
5
5
  "headerDevices": "模拟的 Roku 设备",
6
- "devicesHelp": "每张卡片是一个模拟的 Roku。添加一个后,在 Harmony 或 Sofabaton 应用中添加一个指向此 ioBroker 主机的 **Roku** 设备——每次按键都会进入该设备的对象。",
6
+ "devicesHelp": "每张卡片是一个模拟的 Roku。添加一个后,在 Harmony 或 Sofabaton 应用中添加一个指向此 ioBroker 主机的 <b>Roku</b> 设备——每次按键都会进入该设备的对象。",
7
7
  "deviceName": "名称",
8
8
  "devicePort": "ECP 端口",
9
9
  "portLabel": "端口",
@@ -23,5 +23,14 @@
23
23
  "supportHeader": "支持",
24
24
  "aboutInfo": "如果此适配器对您有用,一点捐赠有助于维护它——谢谢!",
25
25
  "donateKofi": "Ko-fi",
26
- "donatePaypal": "PayPal"
26
+ "donatePaypal": "PayPal",
27
+ "channelInfo": "适配器信息",
28
+ "connectionStatus": "连接状态",
29
+ "connectionStatusDesc": "仅当每个已配置的模拟 Roku 都在监听时才为真。",
30
+ "stateLastCommand": "最后一条命令",
31
+ "stateLastCommandDesc": "该模拟 Roku 最近收到的命令,以纯文本表示。",
32
+ "stateLastCommandType": "最后一条命令的类型",
33
+ "stateLastCommandTypeDesc": "命令的种类:按键、按住或松开按键、启动或安装应用、输入文本或搜索。",
34
+ "channelKeys": "遥控器按键",
35
+ "channelKeysDesc": "该设备提供的每个遥控器按键各有一个状态;按下时会短暂置为真。"
27
36
  }
@@ -5,7 +5,7 @@
5
5
  "_headerNetwork": {
6
6
  "type": "header",
7
7
  "text": "headerNetwork",
8
- "size": 3
8
+ "size": 4
9
9
  },
10
10
  "networkInterface": {
11
11
  "type": "ip",
@@ -24,7 +24,7 @@
24
24
  "newLine": true,
25
25
  "type": "header",
26
26
  "text": "headerDevices",
27
- "size": 3
27
+ "size": 4
28
28
  },
29
29
  "_devicesHelp": {
30
30
  "type": "staticText",
@@ -30,7 +30,6 @@ var import_constants = require("./lib/constants");
30
30
  var import_device_identity = require("./lib/device-identity");
31
31
  var import_i18n = require("./lib/i18n");
32
32
  var import_pure_helpers = require("./lib/pure-helpers");
33
- const RESERVED_IDS = /* @__PURE__ */ new Set(["info"]);
34
33
  function nextFreePort(usedPorts) {
35
34
  const taken = new Set(usedPorts);
36
35
  let port = import_constants.DEFAULT_ECP_PORT;
@@ -90,7 +89,7 @@ function cleanDevice(raw) {
90
89
  function findClash(devices, candidate, exceptIndex) {
91
90
  const name = candidate.name.trim().toLowerCase();
92
91
  const id = (0, import_pure_helpers.sanitizeId)(candidate.name.trim());
93
- if (id === "" || RESERVED_IDS.has(id)) {
92
+ if (id === "" || import_constants.RESERVED_IDS.has(id)) {
94
93
  return (0, import_i18n.t)("deviceNameInvalid");
95
94
  }
96
95
  for (let i = 0; i < devices.length; i++) {
@@ -116,13 +115,19 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
116
115
  /**
117
116
  * Read the device list from the live config object.
118
117
  *
119
- * @returns the configured devices, or an empty list
118
+ * @returns the configured devices (normalised), or an empty list
120
119
  */
121
120
  async readDevices() {
122
121
  var _a;
123
122
  const obj = await this.adapter.getForeignObjectAsync(this.objId);
124
123
  const devices = (_a = obj == null ? void 0 : obj.native) == null ? void 0 : _a.devices;
125
- return Array.isArray(devices) ? devices : [];
124
+ if (!Array.isArray(devices)) {
125
+ return [];
126
+ }
127
+ return devices.filter((d) => typeof d === "object" && d !== null).map((d) => {
128
+ const clean = cleanDevice(d);
129
+ return typeof d.uuid === "string" && d.uuid ? { ...clean, uuid: d.uuid } : clean;
130
+ });
126
131
  }
127
132
  /**
128
133
  * Persist the device list. Writing `native.*` restarts the adapter, which
@@ -146,6 +151,8 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
146
151
  * Build one device card. The model (Player/TV) and the ECP port each get their
147
152
  * own line — the port via `identifier` (labelled in getInstanceInfo). No
148
153
  * manufacturer line: it is always "Roku" and tells the user nothing for an emulator.
154
+ * The row comes from readDevices(), so the port is already a number; only the name
155
+ * can still be empty (a row saved without one) and gets a numbered stand-in.
149
156
  *
150
157
  * @param device the stored device
151
158
  * @param index its list position — the (per-session stable) card id
@@ -156,7 +163,7 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
156
163
  return {
157
164
  id: String(index),
158
165
  name: device.name || `Roku ${index + 1}`,
159
- identifier: String(device.port || import_constants.DEFAULT_ECP_PORT),
166
+ identifier: String(device.port),
160
167
  model: kind,
161
168
  actions: [
162
169
  {
@@ -217,6 +224,12 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
217
224
  * Edit a device via the pre-filled form. Its own name/port are excluded from
218
225
  * the clash check, and its uuid is preserved so the pairing survives a rename.
219
226
  *
227
+ * A row that carries no uuid yet — the manifest's default device, or anything
228
+ * written before 0.8.0 — gets one derived from its PREVIOUS name, because that
229
+ * is the identity main.ts is advertising right now (it derives from the stored
230
+ * name and never writes the value back). Deriving from the NEW name here would
231
+ * change the SSDP identity on a plain rename and silently unpair the remote.
232
+ *
220
233
  * @param index the device's list position
221
234
  * @param context the action context
222
235
  * @returns a directive to reload the list
@@ -240,7 +253,7 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
240
253
  await context.showMessage(clash);
241
254
  return { refresh: "devices" };
242
255
  }
243
- devices[index] = { ...clean, uuid: current.uuid || (0, import_device_identity.deriveUuid)(clean.name) };
256
+ devices[index] = { ...clean, uuid: (0, import_device_identity.resolveDeviceUuid)(current) };
244
257
  await this.writeDevices(devices);
245
258
  }
246
259
  return { refresh: "devices" };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/device-management.ts"],
4
- "sourcesContent": ["import {\n DeviceManagement,\n type ActionContext,\n type DeviceInfo,\n type DeviceLoadContext,\n type JsonFormSchema,\n} from \"@iobroker/dm-utils\";\nimport { DEFAULT_ECP_PORT } from \"./lib/constants\";\nimport { deriveUuid } from \"./lib/device-identity\";\nimport { t } from \"./lib/i18n\";\nimport { sanitizeId } from \"./lib/pure-helpers\";\nimport type { DeviceType } from \"./ecp/state-model\";\n\n/** Object-id segments the adapter reserves for its own tree \u2014 a device may not take them. */\nconst RESERVED_IDS = new Set([\"info\"]);\n\n/** One emulated Roku as stored in the adapter's native.devices. */\ninterface RokuDeviceConfig {\n name: string;\n port: number;\n type: DeviceType;\n /** Stable SSDP identity \u2014 carried through edits so the controller pairing survives. */\n uuid?: string;\n}\n\n/** Manager directive: reload the whole view. */\ntype InstanceResult = { refresh: boolean };\n/** Manager directive: reload the device list. */\ntype DeviceResult = { refresh: \"devices\" };\n\n/**\n * The lowest free ECP port at or above the real-Roku default, so a newly added\n * device never pre-selects a port another emulated Roku already uses.\n *\n * @param usedPorts the ports already taken by other devices\n * @returns the first free port >= 8060\n */\nexport function nextFreePort(usedPorts: readonly number[]): number {\n const taken = new Set(usedPorts);\n let port = DEFAULT_ECP_PORT;\n while (taken.has(port)) {\n port++;\n }\n return port;\n}\n\n/**\n * The add/edit form for one emulated Roku: name, ECP port and device type. The\n * name and port carry a live validator against the values already in use (the\n * OK button greys out on a clash); a hint explains that each Roku needs its own\n * port. Labels are resolved translation objects so the embedded form is\n * language-correct.\n *\n * @param usedNames names taken by OTHER devices (the edited device is excluded)\n * @param usedPorts ports taken by OTHER devices\n * @returns the jsonConfig panel schema for one device\n */\nexport function buildDeviceForm(usedNames: readonly string[], usedPorts: readonly number[]): JsonFormSchema {\n const nameList = JSON.stringify(usedNames.map(n => n.trim().toLowerCase()));\n const portList = JSON.stringify([...usedPorts]);\n return {\n type: \"panel\",\n items: {\n name: {\n type: \"text\",\n label: t(\"deviceName\"),\n validator: `!${nameList}.includes((data.name||'').trim().toLowerCase())`,\n validatorErrorText: t(\"deviceNameInUse\"),\n validatorNoSaveOnError: true,\n sm: 12,\n md: 6,\n },\n port: {\n type: \"number\",\n label: t(\"devicePort\"),\n min: 1,\n max: 65535,\n validator: `!${portList}.includes(Number(data.port))`,\n validatorErrorText: t(\"devicePortInUse\"),\n validatorNoSaveOnError: true,\n sm: 12,\n md: 3,\n },\n type: {\n type: \"select\",\n label: t(\"deviceTypeLabel\"),\n default: \"player\",\n options: [\n { label: t(\"deviceTypePlayer\"), value: \"player\" },\n { label: t(\"deviceTypeTv\"), value: \"tv\" },\n ],\n sm: 12,\n md: 3,\n },\n _portHint: { type: \"staticText\", text: t(\"devicePortHint\"), sm: 12 },\n _typeHint: { type: \"staticText\", text: t(\"deviceTypeHint\"), sm: 12 },\n },\n } as unknown as JsonFormSchema;\n}\n\n/**\n * Normalise raw form values into a clean device (trimmed name, numeric port,\n * valid type) so native.devices stays tidy. The uuid is set by the caller.\n *\n * @param raw the submitted form values\n * @returns the normalised device config (without uuid)\n */\nexport function cleanDevice(raw: Record<string, unknown>): Omit<RokuDeviceConfig, \"uuid\"> {\n const name = typeof raw.name === \"string\" ? raw.name.trim() : \"\";\n const port = Number(raw.port) || DEFAULT_ECP_PORT;\n const type: DeviceType = raw.type === \"tv\" ? \"tv\" : \"player\";\n return { name, port, type };\n}\n\n/**\n * A name/port clash against the other devices, as a ready-to-show message \u2014 the\n * backend safety net behind the form validator (the dialog validator may not\n * fire in every admin version; this never lets a duplicate through).\n *\n * @param devices the full current device list\n * @param candidate the name+port being added/edited\n * @param candidate.name the candidate device name\n * @param candidate.port the candidate ECP port\n * @param exceptIndex the list position to ignore (the device being edited), or -1\n * @returns a translated clash message, or null if free\n */\nexport function findClash(\n devices: readonly RokuDeviceConfig[],\n candidate: { name: string; port: number },\n exceptIndex: number,\n): ioBroker.StringOrTranslated | null {\n const name = candidate.name.trim().toLowerCase();\n // The object-tree path is sanitizeId(name); guard the two ways it can go wrong\n // regardless of the plain-name check: a name that sanitizes to a reserved id\n // (\"info\" would collide with the adapter's own channel), and two different\n // names that sanitize to the SAME id (\"My Roku\" and \"My*Roku\" \u2192 \"My_Roku\").\n const id = sanitizeId(candidate.name.trim());\n if (id === \"\" || RESERVED_IDS.has(id)) {\n return t(\"deviceNameInvalid\");\n }\n for (let i = 0; i < devices.length; i++) {\n if (i === exceptIndex) {\n continue;\n }\n if (devices[i].name.trim().toLowerCase() === name) {\n return t(\"deviceNameInUse\");\n }\n if (sanitizeId(devices[i].name.trim()) === id) {\n return t(\"deviceNameInvalid\");\n }\n if (Number(devices[i].port) === candidate.port) {\n return t(\"devicePortInUse\");\n }\n }\n return null;\n}\n\n/**\n * ioBroker device-manager backend: the emulated Rokus as cards with manual\n * add/edit/delete \u2014 no scan, the devices are purely user-defined. Owns no state;\n * it reads and writes the adapter's own `native.devices` config array.\n */\nexport class FakerokuDeviceManagement extends DeviceManagement {\n private get objId(): string {\n return `system.adapter.${this.adapter.namespace}`;\n }\n\n /**\n * Read the device list from the live config object.\n *\n * @returns the configured devices, or an empty list\n */\n private async readDevices(): Promise<RokuDeviceConfig[]> {\n const obj = await this.adapter.getForeignObjectAsync(this.objId);\n const devices = (obj?.native as { devices?: unknown } | undefined)?.devices;\n return Array.isArray(devices) ? (devices as RokuDeviceConfig[]) : [];\n }\n\n /**\n * Persist the device list. Writing `native.*` restarts the adapter, which\n * re-creates the object trees and servers with the new devices.\n *\n * @param devices the full device list to store\n */\n private async writeDevices(devices: RokuDeviceConfig[]): Promise<void> {\n await this.adapter.extendForeignObjectAsync(this.objId, { native: { devices } });\n }\n\n /**\n * Populate the manager with one card per configured device.\n *\n * @param context the load context\n */\n protected async loadDevices(context: DeviceLoadContext<string>): Promise<void> {\n const devices = await this.readDevices();\n devices.forEach((device, index) => context.addDevice(this.toDeviceInfo(device, index)));\n }\n\n /**\n * Build one device card. The model (Player/TV) and the ECP port each get their\n * own line \u2014 the port via `identifier` (labelled in getInstanceInfo). No\n * manufacturer line: it is always \"Roku\" and tells the user nothing for an emulator.\n *\n * @param device the stored device\n * @param index its list position \u2014 the (per-session stable) card id\n * @returns the card descriptor\n */\n private toDeviceInfo(device: RokuDeviceConfig, index: number): DeviceInfo<string> {\n const kind = device.type === \"tv\" ? \"TV\" : \"Player\";\n return {\n id: String(index),\n name: device.name || `Roku ${index + 1}`,\n identifier: String(device.port || DEFAULT_ECP_PORT),\n model: kind,\n actions: [\n {\n id: \"edit\",\n icon: \"edit\",\n description: t(\"dmEdit\"),\n handler: async (id: string, context: ActionContext) => this.editDevice(Number(id), context),\n },\n {\n id: \"delete\",\n icon: \"delete\",\n description: t(\"dmDelete\"),\n handler: async (id: string, context: ActionContext) => this.deleteDevice(Number(id), context),\n },\n ],\n };\n }\n\n /**\n * The \"+ add\" action above the list, plus the label for the port shown on each card.\n *\n * @returns the instance action descriptor\n */\n protected getInstanceInfo(): ReturnType<DeviceManagement[\"getInstanceInfo\"]> {\n return {\n apiVersion: \"v3\",\n identifierLabel: t(\"portLabel\"),\n actions: [{ id: \"add\", icon: \"add\", description: t(\"dmAdd\"), handler: async context => this.addDevice(context) }],\n };\n }\n\n /**\n * Manual add: pre-select a free port, show the form, and append the device with\n * a stable derived uuid.\n *\n * @param context the action context\n * @returns a directive to reload the manager\n */\n private async addDevice(context: ActionContext): Promise<InstanceResult> {\n const devices = await this.readDevices();\n const usedNames = devices.map(d => d.name);\n const usedPorts = devices.map(d => Number(d.port));\n const data = await context.showForm(buildDeviceForm(usedNames, usedPorts), {\n title: t(\"dmAdd\"),\n data: { type: \"player\", port: nextFreePort(usedPorts) },\n });\n if (data && typeof data.name === \"string\" && data.name.trim()) {\n const clean = cleanDevice(data);\n const clash = findClash(devices, clean, -1);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: true };\n }\n devices.push({ ...clean, uuid: deriveUuid(clean.name) });\n await this.writeDevices(devices);\n }\n return { refresh: true };\n }\n\n /**\n * Edit a device via the pre-filled form. Its own name/port are excluded from\n * the clash check, and its uuid is preserved so the pairing survives a rename.\n *\n * @param index the device's list position\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async editDevice(index: number, context: ActionContext): Promise<DeviceResult> {\n const devices = await this.readDevices();\n const current = devices[index];\n if (!current) {\n return { refresh: \"devices\" };\n }\n const usedNames = devices.filter((_, i) => i !== index).map(d => d.name);\n const usedPorts = devices.filter((_, i) => i !== index).map(d => Number(d.port));\n const data = await context.showForm(buildDeviceForm(usedNames, usedPorts), {\n title: t(\"dmEditTitle\"),\n data: { ...current },\n });\n if (data && typeof data.name === \"string\" && data.name.trim()) {\n const clean = cleanDevice(data);\n const clash = findClash(devices, clean, index);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: \"devices\" };\n }\n devices[index] = { ...clean, uuid: current.uuid || deriveUuid(clean.name) };\n await this.writeDevices(devices);\n }\n return { refresh: \"devices\" };\n }\n\n /**\n * Delete a device after confirmation.\n *\n * @param index the device's list position\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async deleteDevice(index: number, context: ActionContext): Promise<DeviceResult> {\n const devices = await this.readDevices();\n const target = devices[index];\n if (!target) {\n return { refresh: \"devices\" };\n }\n const confirmed = await context.showConfirmation(t(\"dmDeleteConfirm\", target.name || \"\"));\n if (confirmed) {\n devices.splice(index, 1);\n await this.writeDevices(devices);\n }\n return { refresh: \"devices\" };\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {\n DeviceManagement,\n type ActionContext,\n type DeviceInfo,\n type DeviceLoadContext,\n type JsonFormSchema,\n} from \"@iobroker/dm-utils\";\nimport { DEFAULT_ECP_PORT, RESERVED_IDS } from \"./lib/constants\";\nimport { deriveUuid, resolveDeviceUuid } from \"./lib/device-identity\";\nimport { t } from \"./lib/i18n\";\nimport { sanitizeId } from \"./lib/pure-helpers\";\nimport type { DeviceType } from \"./ecp/state-model\";\n\n/**\n * One emulated Roku as stored in the adapter's native.devices \u2014 the manifest's own\n * element type, so the manager and the runtime (main.ts) read the same shape.\n * `uuid` is the stable SSDP identity, carried through edits so the pairing survives.\n * A row without one (the manifest default, or a hand-written config) is identified\n * by `deriveUuid(name)` at runtime, so an edit must derive from the OLD name.\n */\ntype RokuDeviceConfig = ioBroker.AdapterConfig[\"devices\"][number];\n\n/** Manager directive: reload the whole view. */\ntype InstanceResult = { refresh: boolean };\n/** Manager directive: reload the device list. */\ntype DeviceResult = { refresh: \"devices\" };\n\n/**\n * The lowest free ECP port at or above the real-Roku default, so a newly added\n * device never pre-selects a port another emulated Roku already uses.\n *\n * @param usedPorts the ports already taken by other devices\n * @returns the first free port >= 8060\n */\nexport function nextFreePort(usedPorts: readonly number[]): number {\n const taken = new Set(usedPorts);\n let port = DEFAULT_ECP_PORT;\n while (taken.has(port)) {\n port++;\n }\n return port;\n}\n\n/**\n * The add/edit form for one emulated Roku: name, ECP port and device type. The\n * name and port carry a live validator against the values already in use (the\n * OK button greys out on a clash); a hint explains that each Roku needs its own\n * port. Labels are resolved translation objects so the embedded form is\n * language-correct.\n *\n * @param usedNames names taken by OTHER devices (the edited device is excluded)\n * @param usedPorts ports taken by OTHER devices\n * @returns the jsonConfig panel schema for one device\n */\nexport function buildDeviceForm(usedNames: readonly string[], usedPorts: readonly number[]): JsonFormSchema {\n const nameList = JSON.stringify(usedNames.map(n => n.trim().toLowerCase()));\n const portList = JSON.stringify([...usedPorts]);\n return {\n type: \"panel\",\n items: {\n name: {\n type: \"text\",\n label: t(\"deviceName\"),\n validator: `!${nameList}.includes((data.name||'').trim().toLowerCase())`,\n validatorErrorText: t(\"deviceNameInUse\"),\n validatorNoSaveOnError: true,\n sm: 12,\n md: 6,\n },\n port: {\n type: \"number\",\n label: t(\"devicePort\"),\n min: 1,\n max: 65535,\n validator: `!${portList}.includes(Number(data.port))`,\n validatorErrorText: t(\"devicePortInUse\"),\n validatorNoSaveOnError: true,\n sm: 12,\n md: 3,\n },\n type: {\n type: \"select\",\n label: t(\"deviceTypeLabel\"),\n default: \"player\",\n options: [\n { label: t(\"deviceTypePlayer\"), value: \"player\" },\n { label: t(\"deviceTypeTv\"), value: \"tv\" },\n ],\n sm: 12,\n md: 3,\n },\n _portHint: { type: \"staticText\", text: t(\"devicePortHint\"), sm: 12 },\n _typeHint: { type: \"staticText\", text: t(\"deviceTypeHint\"), sm: 12 },\n },\n } as unknown as JsonFormSchema;\n}\n\n/**\n * Normalise raw form values into a clean device (trimmed name, numeric port,\n * valid type) so native.devices stays tidy. The uuid is set by the caller.\n *\n * @param raw the submitted form values\n * @returns the normalised device config (without uuid)\n */\nexport function cleanDevice(raw: Record<string, unknown>): Omit<RokuDeviceConfig, \"uuid\"> {\n const name = typeof raw.name === \"string\" ? raw.name.trim() : \"\";\n const port = Number(raw.port) || DEFAULT_ECP_PORT;\n const type: DeviceType = raw.type === \"tv\" ? \"tv\" : \"player\";\n return { name, port, type };\n}\n\n/**\n * A name/port clash against the other devices, as a ready-to-show message \u2014 the\n * backend safety net behind the form validator (the dialog validator may not\n * fire in every admin version; this never lets a duplicate through).\n *\n * @param devices the full current device list\n * @param candidate the name+port being added/edited\n * @param candidate.name the candidate device name\n * @param candidate.port the candidate ECP port\n * @param exceptIndex the list position to ignore (the device being edited), or -1\n * @returns a translated clash message, or null if free\n */\nexport function findClash(\n devices: readonly RokuDeviceConfig[],\n candidate: { name: string; port: number },\n exceptIndex: number,\n): ioBroker.StringOrTranslated | null {\n const name = candidate.name.trim().toLowerCase();\n // The object-tree path is sanitizeId(name); guard the two ways it can go wrong\n // regardless of the plain-name check: a name that sanitizes to a reserved id\n // (\"info\" would collide with the adapter's own channel), and two different\n // names that sanitize to the SAME id (\"My Roku\" and \"My*Roku\" \u2192 \"My_Roku\").\n const id = sanitizeId(candidate.name.trim());\n if (id === \"\" || RESERVED_IDS.has(id)) {\n return t(\"deviceNameInvalid\");\n }\n for (let i = 0; i < devices.length; i++) {\n if (i === exceptIndex) {\n continue;\n }\n if (devices[i].name.trim().toLowerCase() === name) {\n return t(\"deviceNameInUse\");\n }\n if (sanitizeId(devices[i].name.trim()) === id) {\n return t(\"deviceNameInvalid\");\n }\n if (Number(devices[i].port) === candidate.port) {\n return t(\"devicePortInUse\");\n }\n }\n return null;\n}\n\n/**\n * ioBroker device-manager backend: the emulated Rokus as cards with manual\n * add/edit/delete \u2014 no scan, the devices are purely user-defined. Owns no state;\n * it reads and writes the adapter's own `native.devices` config array.\n */\nexport class FakerokuDeviceManagement extends DeviceManagement {\n private get objId(): string {\n return `system.adapter.${this.adapter.namespace}`;\n }\n\n /**\n * Read the device list from the live config object.\n *\n * @returns the configured devices (normalised), or an empty list\n */\n private async readDevices(): Promise<RokuDeviceConfig[]> {\n const obj = await this.adapter.getForeignObjectAsync(this.objId);\n const devices = (obj?.native as { devices?: unknown } | undefined)?.devices;\n if (!Array.isArray(devices)) {\n return [];\n }\n // native.devices is user-editable (expert mode, CLI). Normalise every row the\n // same way the dialog does, so a hand-edited entry (numeric name, missing port)\n // shows up as a card the user can fix instead of throwing inside add/edit.\n return devices\n .filter((d): d is Record<string, unknown> => typeof d === \"object\" && d !== null)\n .map(d => {\n const clean = cleanDevice(d);\n return typeof d.uuid === \"string\" && d.uuid ? { ...clean, uuid: d.uuid } : clean;\n });\n }\n\n /**\n * Persist the device list. Writing `native.*` restarts the adapter, which\n * re-creates the object trees and servers with the new devices.\n *\n * @param devices the full device list to store\n */\n private async writeDevices(devices: RokuDeviceConfig[]): Promise<void> {\n await this.adapter.extendForeignObjectAsync(this.objId, { native: { devices } });\n }\n\n /**\n * Populate the manager with one card per configured device.\n *\n * @param context the load context\n */\n protected async loadDevices(context: DeviceLoadContext<string>): Promise<void> {\n const devices = await this.readDevices();\n devices.forEach((device, index) => context.addDevice(this.toDeviceInfo(device, index)));\n }\n\n /**\n * Build one device card. The model (Player/TV) and the ECP port each get their\n * own line \u2014 the port via `identifier` (labelled in getInstanceInfo). No\n * manufacturer line: it is always \"Roku\" and tells the user nothing for an emulator.\n * The row comes from readDevices(), so the port is already a number; only the name\n * can still be empty (a row saved without one) and gets a numbered stand-in.\n *\n * @param device the stored device\n * @param index its list position \u2014 the (per-session stable) card id\n * @returns the card descriptor\n */\n private toDeviceInfo(device: RokuDeviceConfig, index: number): DeviceInfo<string> {\n const kind = device.type === \"tv\" ? \"TV\" : \"Player\";\n return {\n id: String(index),\n name: device.name || `Roku ${index + 1}`,\n identifier: String(device.port),\n model: kind,\n actions: [\n {\n id: \"edit\",\n icon: \"edit\",\n description: t(\"dmEdit\"),\n handler: async (id: string, context: ActionContext) => this.editDevice(Number(id), context),\n },\n {\n id: \"delete\",\n icon: \"delete\",\n description: t(\"dmDelete\"),\n handler: async (id: string, context: ActionContext) => this.deleteDevice(Number(id), context),\n },\n ],\n };\n }\n\n /**\n * The \"+ add\" action above the list, plus the label for the port shown on each card.\n *\n * @returns the instance action descriptor\n */\n protected getInstanceInfo(): ReturnType<DeviceManagement[\"getInstanceInfo\"]> {\n return {\n apiVersion: \"v3\",\n identifierLabel: t(\"portLabel\"),\n actions: [{ id: \"add\", icon: \"add\", description: t(\"dmAdd\"), handler: async context => this.addDevice(context) }],\n };\n }\n\n /**\n * Manual add: pre-select a free port, show the form, and append the device with\n * a stable derived uuid.\n *\n * @param context the action context\n * @returns a directive to reload the manager\n */\n private async addDevice(context: ActionContext): Promise<InstanceResult> {\n const devices = await this.readDevices();\n const usedNames = devices.map(d => d.name);\n const usedPorts = devices.map(d => Number(d.port));\n const data = await context.showForm(buildDeviceForm(usedNames, usedPorts), {\n title: t(\"dmAdd\"),\n data: { type: \"player\", port: nextFreePort(usedPorts) },\n });\n if (data && typeof data.name === \"string\" && data.name.trim()) {\n const clean = cleanDevice(data);\n const clash = findClash(devices, clean, -1);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: true };\n }\n devices.push({ ...clean, uuid: deriveUuid(clean.name) });\n await this.writeDevices(devices);\n }\n return { refresh: true };\n }\n\n /**\n * Edit a device via the pre-filled form. Its own name/port are excluded from\n * the clash check, and its uuid is preserved so the pairing survives a rename.\n *\n * A row that carries no uuid yet \u2014 the manifest's default device, or anything\n * written before 0.8.0 \u2014 gets one derived from its PREVIOUS name, because that\n * is the identity main.ts is advertising right now (it derives from the stored\n * name and never writes the value back). Deriving from the NEW name here would\n * change the SSDP identity on a plain rename and silently unpair the remote.\n *\n * @param index the device's list position\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async editDevice(index: number, context: ActionContext): Promise<DeviceResult> {\n const devices = await this.readDevices();\n const current = devices[index];\n if (!current) {\n return { refresh: \"devices\" };\n }\n const usedNames = devices.filter((_, i) => i !== index).map(d => d.name);\n const usedPorts = devices.filter((_, i) => i !== index).map(d => Number(d.port));\n const data = await context.showForm(buildDeviceForm(usedNames, usedPorts), {\n title: t(\"dmEditTitle\"),\n data: { ...current },\n });\n if (data && typeof data.name === \"string\" && data.name.trim()) {\n const clean = cleanDevice(data);\n const clash = findClash(devices, clean, index);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: \"devices\" };\n }\n devices[index] = { ...clean, uuid: resolveDeviceUuid(current) };\n await this.writeDevices(devices);\n }\n return { refresh: \"devices\" };\n }\n\n /**\n * Delete a device after confirmation.\n *\n * @param index the device's list position\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async deleteDevice(index: number, context: ActionContext): Promise<DeviceResult> {\n const devices = await this.readDevices();\n const target = devices[index];\n if (!target) {\n return { refresh: \"devices\" };\n }\n const confirmed = await context.showConfirmation(t(\"dmDeleteConfirm\", target.name || \"\"));\n if (confirmed) {\n devices.splice(index, 1);\n await this.writeDevices(devices);\n }\n return { refresh: \"devices\" };\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }