iobroker.fakeroku 1.7.1 → 1.8.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 (44) hide show
  1. package/README.md +40 -28
  2. package/admin/i18n/de.json +3 -3
  3. package/admin/i18n/en.json +3 -3
  4. package/admin/i18n/es.json +4 -4
  5. package/admin/i18n/fr.json +3 -3
  6. package/admin/i18n/it.json +3 -3
  7. package/admin/i18n/nl.json +3 -3
  8. package/admin/i18n/pl.json +4 -4
  9. package/admin/i18n/pt.json +3 -3
  10. package/admin/i18n/ru.json +3 -3
  11. package/admin/i18n/uk.json +3 -3
  12. package/admin/i18n/zh-cn.json +4 -4
  13. package/build/device-management.js +87 -51
  14. package/build/device-management.js.map +2 -2
  15. package/build/discovery/ssdp-messages.js +18 -4
  16. package/build/discovery/ssdp-messages.js.map +2 -2
  17. package/build/discovery/ssdp-responder.js +180 -84
  18. package/build/discovery/ssdp-responder.js.map +2 -2
  19. package/build/ecp/device-info.js +32 -3
  20. package/build/ecp/device-info.js.map +2 -2
  21. package/build/ecp/ecp-command.js +2 -2
  22. package/build/ecp/ecp-command.js.map +2 -2
  23. package/build/ecp/ecp-http-server.js +46 -18
  24. package/build/ecp/ecp-http-server.js.map +3 -3
  25. package/build/ecp/state-model.js +19 -6
  26. package/build/ecp/state-model.js.map +2 -2
  27. package/build/lib/detect-ip.js +105 -49
  28. package/build/lib/detect-ip.js.map +3 -3
  29. package/build/lib/device-config.js +68 -12
  30. package/build/lib/device-config.js.map +2 -2
  31. package/build/lib/device-identity.js +5 -0
  32. package/build/lib/device-identity.js.map +2 -2
  33. package/build/lib/errors.js +32 -2
  34. package/build/lib/errors.js.map +2 -2
  35. package/build/lib/lan-guard.js +6 -19
  36. package/build/lib/lan-guard.js.map +2 -2
  37. package/build/lib/native-key-migration.js +16 -5
  38. package/build/lib/native-key-migration.js.map +2 -2
  39. package/build/lib/pure-helpers.js +6 -2
  40. package/build/lib/pure-helpers.js.map +2 -2
  41. package/build/main.js +262 -79
  42. package/build/main.js.map +3 -3
  43. package/io-package.json +62 -62
  44. package/package.json +8 -13
package/README.md CHANGED
@@ -6,14 +6,14 @@
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 — a
10
- Logitech Harmony Hub or a Sofabaton X1/X2 — can trigger events in
11
- ioBroker. It is the **input** counterpart to the Logitech Harmony adapter: a button
12
- on the remote becomes a datapoint in ioBroker.
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
12
+ Harmony adapter: a button on the remote becomes a datapoint in ioBroker.
13
13
 
14
- Unlike the classic fake-Roku, this build answers the full Roku control surface
15
- including `/query/device-info` with a **current** Roku version, so it works beyond a
16
- classic Harmony hub.
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.
17
17
 
18
18
  > **The official Roku mobile app is not supported.** It drives Rokus over Roku's
19
19
  > proprietary, undocumented ECP-2 WebSocket channel, which this emulator does not
@@ -23,9 +23,9 @@ classic Harmony hub.
23
23
  ## Features
24
24
 
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
- - Full Roku control surface including `/query/device-info` with a current Roku version, beyond what a classic Harmony hub needs.
26
+ - Answers the Roku queries controllers rely on: device info, apps, active app, media player, TV channels and app icons.
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 joins every routable network interface unless you pick one; command handling restricted to the LAN and capped at 25 commands per second per emulated Roku, so a misbehaving device cannot flood ioBroker. Releasing a key you are holding is never dropped by that cap.
28
+ - Several emulated Rokus from a single instance; with "all interfaces" every network of the host is served with the host's own address in it, a chosen interface keeps everything in its network; only clients from the host's own networks are answered, capped at 25 commands per second per emulated Roku, so a misbehaving device cannot flood ioBroker. Releasing a key you are holding is never dropped by that cap.
29
29
 
30
30
  ## Sentry / Error reporting
31
31
 
@@ -43,21 +43,25 @@ For details and how to disable it, see the [Sentry plugin documentation](https:/
43
43
 
44
44
  ## Ports
45
45
 
46
- - **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.
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
47
  - **UDP 1900 (multicast, listening and outgoing)** — SSDP discovery, so a Harmony hub or Sofabaton finds the emulated Rokus; fixed by the UPnP standard.
48
48
 
49
49
  ## Configuration
50
50
 
51
- - **Network interface** — the network card the emulated Rokus bind to and advertise
52
- on. Leave it on "all interfaces" and the adapter runs out of the box — it detects
53
- the routable IP automatically. Pick a specific address only on a host with several
54
- network cards.
51
+ - **Network interface** — leave it on "all interfaces" and the adapter runs out of the
52
+ box: every network of the host is served, each with the host's own address in it.
53
+ Pick a specific address only if the emulated Rokus should exist in one network only —
54
+ then everything stays in that network. An address the host does not carry is waited
55
+ for up to two minutes at start-up, then reported; the adapter never falls back to
56
+ another network.
55
57
  - **Emulated Roku devices** — managed as cards: **+ Add** opens a dialog with a
56
58
  **name**, an **ECP port** (`8060` the real Roku port; a free one is pre-selected,
57
- and the dialog refuses a name or port already in use) and a **type**. You can
58
- emulate several Rokus from one instance — each needs its own port.
59
+ and the dialog does not let you confirm a name or port already in use) and a **type**.
60
+ You can emulate several Rokus from one instance — each needs its own port. Renaming a
61
+ card keeps its datapoints, their room and function assignments and their history
62
+ settings.
59
63
  - **Type** — *Player* (default) exposes the 16 standard navigation and playback keys;
60
- *TV* additionally exposes volume, channel and input keys plus a power-off key. Choose *TV* only if
64
+ *TV* additionally exposes volume, power, channel and input keys. Choose *TV* only if
61
65
  you want those extra keys as ioBroker triggers.
62
66
 
63
67
  To add the emulated Roku to a Harmony hub, add a "Roku" device in the Harmony app
@@ -77,10 +81,12 @@ For every emulated Roku (`fakeroku.0.<name>`):
77
81
  |---|---|---|
78
82
  | `.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. |
79
83
  | `.commandType` | string, read-only | `keypress` / `keydown` / `keyup` / `launch` / `install` / `input` / `search`. |
80
- | `.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\*, PowerOff, Channel\*, HDMI/AV inputs — all created up front. A keypress pulses it `true` for a moment; keydown/keyup hold it. |
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`). |
81
85
 
82
86
  Free keyboard input (`Lit_x`) and app launches show up in `.command` only — they do
83
- not get their own objects.
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
89
+ buttons to the emulated Roku.
84
90
 
85
91
  > Note: the Roku remote sends the **same** `Play` command for play and pause, so
86
92
  > play and pause cannot be told apart here — that is a protocol limitation, not an
@@ -94,10 +100,22 @@ becomes `true` — or watch `.command` for the last button as text.
94
100
  Placeholder for the next version (at the beginning of the line):
95
101
  ### **WORK IN PROGRESS**
96
102
  -->
103
+ ### 1.8.0 (2026-09-25)
97
104
 
98
- ### 1.7.1 (2026-09-16)
105
+ - (krobipd) Fixed: Home Assistant and openHAB can set up the emulated Roku again — it now answers the active-app, media-player and TV-channel queries they send.
106
+ - (krobipd) Fixed: keys sent in any upper or lower case (home, POWERON) press the right button, and spaces typed in Home Assistant arrive as spaces.
107
+ - (krobipd) Fixed: an upgrade from the old adapter keeps every object tree with its rooms and history, also for names with an umlaut, a bracket or a double space.
108
+ - (krobipd) Fixed: renaming a device only changes its displayed name; its datapoints and scripts pointing at them stay where they are.
109
+ - (krobipd) Fixed: an instance started before the network is up starts its devices and adds discovery as soon as the host has an address.
110
+ - (krobipd) Fixed: the device dialog greys out OK for a taken name or port and says why.
111
+ - (krobipd) Changed: only devices in the host's own networks are answered; a chosen network interface keeps everything in its network and is never swapped for another.
112
+ - (krobipd) Changed: a chosen network interface that does not exist is waited for up to two minutes at start, then reported, instead of being replaced.
113
+ - (krobipd) New: the TV profile adds the PowerOn, Power, Sleep and InputTuner keys and announces itself the way real Roku TVs do.
114
+ - (krobipd) New: every new emulated Roku gets its own network identity instead of one every installation with the same name would share.
99
115
 
100
- - (krobipd) Fixed: a second instance on the same host is possible again — an instance carried over from an older version still claimed the whole machine.
116
+ ### 1.7.1 (2026-09-16) — stable
117
+
118
+ - (krobipd) Changed: the instance restarts once after this update to clear a setting left behind by an older version; nothing else changes for you.
101
119
 
102
120
  ### 1.7.0 (2026-09-16)
103
121
 
@@ -107,7 +125,7 @@ becomes `true` — or watch `.command` for the last button as text.
107
125
  - (krobipd) Fixed: a key you hold right after a short press stays pressed instead of being released early.
108
126
  - (krobipd) Fixed: the device dialog now also refuses a name that would collide with an existing device in the object tree.
109
127
  - (krobipd) Improved: after the host gets a new IP address, remotes find the emulated Rokus again without restarting the instance.
110
- - (krobipd) Improved: the admin now warns you when a port you enter is already used by another adapter on this host; the instance restarts once after this update.
128
+ - (krobipd) Changed: the network interface setting moved to the standard settings key (bind); the instance restarts once after this update.
111
129
 
112
130
  ### 1.6.1 (2026-09-07) — stable
113
131
 
@@ -126,12 +144,6 @@ becomes `true` — or watch `.command` for the last button as text.
126
144
  - (krobipd) Changed: the adapter can now run in compact mode, sharing one process with other adapters instead of claiming its own.
127
145
  - (krobipd) Changed: more than one instance may run on the same machine again; only the ports have to differ.
128
146
 
129
- ### 1.5.0 (2026-09-03)
130
-
131
- - (krobipd) Fixed: deleting the last emulated Roku left all of its datapoints behind for good. They are now removed whenever the configuration says a device is gone.
132
- - (krobipd) Fixed: on a host running Docker the adapter could announce itself under a container address no remote can reach. A real network address is preferred now.
133
- - (krobipd) Fixed: an emulated Roku whose server died while running left the instance showing "connected". It now reports the failure and names the device.
134
-
135
147
  [Older changelogs can be found there](CHANGELOG_OLD.md)
136
148
 
137
149
  ## History
@@ -7,11 +7,11 @@
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 einen eigenen Port — nicht doppelt vergeben. Standard 8060 (der echte Roku-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.",
11
11
  "deviceTypeLabel": "Typ",
12
12
  "deviceTypePlayer": "Player (Streaming-Box)",
13
- "deviceTypeTv": "TV (mit Lautstärke, Power aus, Kanal, Eingang)",
14
- "deviceTypeHint": "„Player“ bietet die 16 Standard-Fernbedienungstasten. „TV“ zusätzlich Lautstärke-, Kanal- und Eingangstasten sowie „Power aus“.",
13
+ "deviceTypeTv": "TV (mit Lautstärke, Ein/Aus, Kanal, Eingang)",
14
+ "deviceTypeHint": "„Player“ bietet die 16 Standard-Fernbedienungstasten. „TV“ zusätzlich Lautstärke-, Ein/Aus-, Kanal- und Eingangstasten.",
15
15
  "dmAdd": "Roku-Gerät hinzufügen",
16
16
  "dmEdit": "Bearbeiten",
17
17
  "dmDelete": "Löschen",
@@ -7,11 +7,11 @@
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 — do not reuse one. Default 8060 (the real Roku 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.",
11
11
  "deviceTypeLabel": "Type",
12
12
  "deviceTypePlayer": "Player (streaming box)",
13
- "deviceTypeTv": "TV (adds volume, power off, channel, input)",
14
- "deviceTypeHint": "\"Player\" exposes the 16 standard remote keys. \"TV\" additionally exposes volume, channel and input keys plus a power-off key.",
13
+ "deviceTypeTv": "TV (adds volume, power, channel, input)",
14
+ "deviceTypeHint": "\"Player\" exposes the 16 standard remote keys. \"TV\" additionally exposes volume, power, channel and input keys.",
15
15
  "dmAdd": "Add Roku device",
16
16
  "dmEdit": "Edit",
17
17
  "dmDelete": "Delete",
@@ -7,11 +7,11 @@
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 — no reutilices ninguno. Predeterminado 8060 (el puerto real de Roku).",
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.",
11
11
  "deviceTypeLabel": "Tipo",
12
- "deviceTypePlayer": "Player (reproductor)",
13
- "deviceTypeTv": "TV (con volumen, apagado, canal, entrada)",
14
- "deviceTypeHint": "«Player» expone las 16 teclas estándar del mando. «TV» añade las teclas de volumen, canal y entrada más una tecla de apagado.",
12
+ "deviceTypePlayer": "Reproductor (dispositivo de streaming)",
13
+ "deviceTypeTv": "TV (con volumen, encendido, canal, entrada)",
14
+ "deviceTypeHint": "«Player» expone las 16 teclas estándar del mando. «TV» añade las teclas de volumen, encendido/apagado, canal y entrada.",
15
15
  "dmAdd": "Añadir dispositivo Roku",
16
16
  "dmEdit": "Editar",
17
17
  "dmDelete": "Eliminar",
@@ -7,11 +7,11 @@
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 — n'en réutilisez pas. Par défaut 8060 (le vrai port Roku).",
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.",
11
11
  "deviceTypeLabel": "Type",
12
12
  "deviceTypePlayer": "Player (box de streaming)",
13
- "deviceTypeTv": "TV (avec volume, extinction, chaîne, entrée)",
14
- "deviceTypeHint": "« Player » expose les 16 touches standard de la télécommande. « TV » ajoute les touches volume, chaîne et entrée ainsi qu’une touche d’extinction.",
13
+ "deviceTypeTv": "TV (avec volume, marche/arrêt, chaîne, entrée)",
14
+ "deviceTypeHint": "« Player » expose les 16 touches standard de la télécommande. « TV » ajoute les touches volume, marche/arrêt, chaîne et entrée.",
15
15
  "dmAdd": "Ajouter un appareil Roku",
16
16
  "dmEdit": "Modifier",
17
17
  "dmDelete": "Supprimer",
@@ -7,11 +7,11 @@
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 — non riutilizzarne una. Predefinita 8060 (la porta reale del Roku).",
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.",
11
11
  "deviceTypeLabel": "Tipo",
12
12
  "deviceTypePlayer": "Player (box di streaming)",
13
- "deviceTypeTv": "TV (con volume, spegnimento, canale, ingresso)",
14
- "deviceTypeHint": "«Player» espone i 16 tasti standard del telecomando. «TV» aggiunge i tasti volume, canale e ingresso più un tasto di spegnimento.",
13
+ "deviceTypeTv": "TV (con volume, accensione, canale, ingresso)",
14
+ "deviceTypeHint": "«Player» espone i 16 tasti standard del telecomando. «TV» aggiunge i tasti volume, accensione/spegnimento, canale e ingresso.",
15
15
  "dmAdd": "Aggiungi dispositivo Roku",
16
16
  "dmEdit": "Modifica",
17
17
  "dmDelete": "Elimina",
@@ -7,11 +7,11 @@
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 — gebruik er geen dubbel. Standaard 8060 (de echte Roku-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.",
11
11
  "deviceTypeLabel": "Type",
12
12
  "deviceTypePlayer": "Player (streaming-box)",
13
- "deviceTypeTv": "TV (met volume, uitschakelen, kanaal, ingang)",
14
- "deviceTypeHint": "„Player“ biedt de 16 standaardtoetsen. „TV“ voegt volume-, kanaal- en ingangstoetsen toe plus een uitschakeltoets.",
13
+ "deviceTypeTv": "TV (met volume, aan/uit, kanaal, ingang)",
14
+ "deviceTypeHint": "„Player“ biedt de 16 standaardtoetsen. „TV“ voegt volume-, aan/uit-, kanaal- en ingangstoetsen toe.",
15
15
  "dmAdd": "Roku-apparaat toevoegen",
16
16
  "dmEdit": "Bewerken",
17
17
  "dmDelete": "Verwijderen",
@@ -7,11 +7,11 @@
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 — nie używaj tego samego. Domyślnie 8060 (prawdziwy port Roku).",
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ć.",
11
11
  "deviceTypeLabel": "Typ",
12
12
  "deviceTypePlayer": "Player (przystawka)",
13
- "deviceTypeTv": "TV (z głośnością, wyłączaniem, kanałem, wejściem)",
14
- "deviceTypeHint": "„Player“ udostępnia 16 standardowych przycisków pilota. „TV“ dodaje przyciski głośności, kanału i wejścia oraz przycisk wyłączania.",
13
+ "deviceTypeTv": "TV (z głośnością, zasilaniem, kanałem, wejściem)",
14
+ "deviceTypeHint": "„Player“ udostępnia 16 standardowych przycisków pilota. „TV“ dodaje przyciski głośności, zasilania, kanału i wejścia.",
15
15
  "dmAdd": "Dodaj urządzenie Roku",
16
16
  "dmEdit": "Edytuj",
17
17
  "dmDelete": "Usuń",
@@ -33,5 +33,5 @@
33
33
  "stateLastCommandType": "Typ ostatniego polecenia",
34
34
  "stateLastCommandTypeDesc": "Jakiego rodzaju było to polecenie: naciśnięcie klawisza, przytrzymanie lub zwolnienie, uruchomienie lub instalacja aplikacji, wpisanie tekstu albo wyszukiwanie.",
35
35
  "channelKeys": "Klawisze pilota",
36
- "channelKeysDesc": "Jeden stan na każdy klawisz oferowany przez to urządzenie; naciśnięcie ustawia go na krótko na prawdę."
36
+ "channelKeysDesc": "Jeden stan na każdy klawisz oferowany przez to urządzenie; naciśnięcie ustawia go na krótko na wartość true."
37
37
  }
@@ -7,11 +7,11 @@
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 — não reutilize. Predefinição 8060 (a porta real do Roku).",
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.",
11
11
  "deviceTypeLabel": "Tipo",
12
12
  "deviceTypePlayer": "Player (box de streaming)",
13
- "deviceTypeTv": "TV (com volume, desligar, canal, entrada)",
14
- "deviceTypeHint": "«Player» expõe as 16 teclas padrão do comando. «TV» adiciona teclas de volume, canal e entrada mais uma tecla de desligar.",
13
+ "deviceTypeTv": "TV (com volume, ligar/desligar, canal, entrada)",
14
+ "deviceTypeHint": "«Player» expõe as 16 teclas padrão do comando. «TV» adiciona teclas de volume, ligar/desligar, canal e entrada.",
15
15
  "dmAdd": "Adicionar dispositivo Roku",
16
16
  "dmEdit": "Editar",
17
17
  "dmDelete": "Eliminar",
@@ -7,11 +7,11 @@
7
7
  "deviceName": "Имя",
8
8
  "devicePort": "Порт ECP",
9
9
  "portLabel": "Порт",
10
- "devicePortHint": "ECP-порт, на котором отвечает этот Roku. Каждому эмулированному Roku нужен свой порт — не используйте один и тот же. По умолчанию 8060 (реальный порт Roku).",
10
+ "devicePortHint": "Порт ECP, на котором отвечает этот Roku. Каждому эмулированному Roku нужен свой порт. Harmony и Sofabaton берут порт из обнаружения; Home Assistant и Homey всегда используют 8060 — назначьте 8060 тому Roku, которым они должны управлять.",
11
11
  "deviceTypeLabel": "Тип",
12
12
  "deviceTypePlayer": "Проигрыватель (приставка)",
13
- "deviceTypeTv": "ТВ (с громкостью, выключением, каналом, входом)",
14
- "deviceTypeHint": "«Проигрыватель» даёт 16 стандартных кнопок пульта. «ТВ» добавляет кнопки громкости, канала и входа, а также кнопку выключения.",
13
+ "deviceTypeTv": "ТВ (с громкостью, питанием, каналом, входом)",
14
+ "deviceTypeHint": "«Проигрыватель» даёт 16 стандартных кнопок пульта. «ТВ» добавляет кнопки громкости, питания, канала и входа.",
15
15
  "dmAdd": "Добавить устройство Roku",
16
16
  "dmEdit": "Изменить",
17
17
  "dmDelete": "Удалить",
@@ -7,11 +7,11 @@
7
7
  "deviceName": "Назва",
8
8
  "devicePort": "Порт ECP",
9
9
  "portLabel": "Порт",
10
- "devicePortHint": "ECP-порт, на якому відповідає цей Roku. Кожному емульованому Roku потрібен власний порт — не використовуйте один і той самий. За замовчуванням 8060 (справжній порт Roku).",
10
+ "devicePortHint": "Порт ECP, на якому відповідає цей Roku. Кожному емульованому Roku потрібен власний порт. Harmony і Sofabaton беруть порт із виявлення; Home Assistant і Homey завжди використовують 8060 — призначте 8060 тому Roku, яким вони мають керувати.",
11
11
  "deviceTypeLabel": "Тип",
12
12
  "deviceTypePlayer": "Програвач (приставка)",
13
- "deviceTypeTv": "ТБ (з гучністю, вимкненням, каналом, входом)",
14
- "deviceTypeHint": "«Програвач» надає 16 стандартних кнопок пульта. «ТБ» додає кнопки гучності, каналу та входу, а також кнопку вимкнення.",
13
+ "deviceTypeTv": "ТБ (з гучністю, живленням, каналом, входом)",
14
+ "deviceTypeHint": "«Програвач» надає 16 стандартних кнопок пульта. «ТБ» додає кнопки гучності, живлення, каналу та входу.",
15
15
  "dmAdd": "Додати пристрій Roku",
16
16
  "dmEdit": "Редагувати",
17
17
  "dmDelete": "Видалити",
@@ -7,11 +7,11 @@
7
7
  "deviceName": "名称",
8
8
  "devicePort": "ECP 端口",
9
9
  "portLabel": "端口",
10
- "devicePortHint": "此 Roku 应答的 ECP 端口。每个模拟的 Roku 都需要自己的端口——不要重复使用。默认 8060(真实的 Roku 端口)。",
10
+ "devicePortHint": "此 Roku 响应的 ECP 端口。每个模拟 Roku 都需要自己的端口。Harmony 和 Sofabaton 从发现信息中读取端口;Home Assistant 和 Homey 始终使用 8060——请把 8060 分配给它们要控制的 Roku。",
11
11
  "deviceTypeLabel": "类型",
12
12
  "deviceTypePlayer": "播放器(流媒体盒子)",
13
- "deviceTypeTv": "电视(带音量、关机、频道、输入)",
14
- "deviceTypeHint": "“播放器”提供 16 个标准遥控按键。“电视”额外提供音量、频道和输入按键,以及一个关机按键。",
13
+ "deviceTypeTv": "电视(带音量、电源、频道、输入)",
14
+ "deviceTypeHint": "“播放器”提供 16 个标准遥控按键。“电视”额外提供音量、电源、频道和输入按键。",
15
15
  "dmAdd": "添加 Roku 设备",
16
16
  "dmEdit": "编辑",
17
17
  "dmDelete": "删除",
@@ -19,7 +19,7 @@
19
19
  "dmDeleteConfirm": "删除 Roku 设备“%s”?",
20
20
  "deviceNameInUse": "此名称已被另一台设备使用。",
21
21
  "devicePortInUse": "此端口已被另一台 Roku 使用。",
22
- "deviceNameInvalid": "无法使用此名称——它是保留名称,或与另一设备生成相同的对象 id。",
22
+ "deviceNameInvalid": "无法使用此名称——它是保留名称,或与另一设备生成相同的对象 ID。",
23
23
  "deviceNameRejected": "此名称无法使用:它已被占用、为适配器保留,或与另一设备生成相同的对象 ID。",
24
24
  "supportHeader": "支持",
25
25
  "aboutInfo": "如果此适配器对您有用,一点捐赠有助于维护它——谢谢!",
@@ -28,47 +28,67 @@ var import_constants = require("./lib/constants");
28
28
  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
+ var import_pure_helpers = require("./lib/pure-helpers");
31
32
  const ID_EXPRESSION = "(data.name||'').trim().replace(/[^A-Za-z0-9\\-_]/g,'_')";
32
33
  function buildDeviceForm(usedNames, usedPorts, usedIds) {
33
34
  const nameList = JSON.stringify(usedNames.map((n) => n.trim().toLowerCase()));
34
- const idList = JSON.stringify([...import_constants.RESERVED_IDS, ...usedIds]);
35
35
  const portList = JSON.stringify([...usedPorts]);
36
+ const nameFilled = "(data.name||'').trim().length>0";
37
+ const nameFree = `!${nameList}.includes((data.name||'').trim().toLowerCase())`;
38
+ const idFree = usedIds === null ? "true" : `${ID_EXPRESSION}.length>0 && !${JSON.stringify([...import_constants.RESERVED_IDS, ...usedIds])}.includes(${ID_EXPRESSION})`;
39
+ const nameOk = `(${nameFilled} && ${nameFree} && ${idFree})`;
40
+ const portOk = `(!${portList}.includes(Number(data.port)))`;
36
41
  return {
37
- type: "panel",
38
- items: {
39
- name: {
40
- type: "text",
41
- label: (0, import_i18n.t)("deviceName"),
42
- validator: `${ID_EXPRESSION}.length>0 && !${nameList}.includes((data.name||'').trim().toLowerCase()) && !${idList}.includes(${ID_EXPRESSION})`,
43
- validatorErrorText: (0, import_i18n.t)("deviceNameRejected"),
44
- validatorNoSaveOnError: true,
45
- sm: 12,
46
- md: 6
47
- },
48
- port: {
49
- type: "number",
50
- label: (0, import_i18n.t)("devicePort"),
51
- min: 1,
52
- max: 65535,
53
- validator: `!${portList}.includes(Number(data.port))`,
54
- validatorErrorText: (0, import_i18n.t)("devicePortInUse"),
55
- validatorNoSaveOnError: true,
56
- sm: 12,
57
- md: 3
58
- },
59
- type: {
60
- type: "select",
61
- label: (0, import_i18n.t)("deviceTypeLabel"),
62
- default: "player",
63
- options: [
64
- { label: (0, import_i18n.t)("deviceTypePlayer"), value: "player" },
65
- { label: (0, import_i18n.t)("deviceTypeTv"), value: "tv" }
66
- ],
67
- sm: 12,
68
- md: 3
69
- },
70
- _portHint: { type: "staticText", text: (0, import_i18n.t)("devicePortHint"), sm: 12 },
71
- _typeHint: { type: "staticText", text: (0, import_i18n.t)("deviceTypeHint"), sm: 12 }
42
+ applyDisabledRule: `!(${nameOk} && ${portOk})`,
43
+ schema: {
44
+ type: "panel",
45
+ items: {
46
+ name: {
47
+ type: "text",
48
+ label: (0, import_i18n.t)("deviceName"),
49
+ validator: nameOk,
50
+ validatorNoSaveOnError: true,
51
+ sm: 12,
52
+ md: 6
53
+ },
54
+ port: {
55
+ type: "number",
56
+ label: (0, import_i18n.t)("devicePort"),
57
+ min: 1,
58
+ max: 65535,
59
+ validator: portOk,
60
+ validatorNoSaveOnError: true,
61
+ sm: 12,
62
+ md: 3
63
+ },
64
+ type: {
65
+ type: "select",
66
+ label: (0, import_i18n.t)("deviceTypeLabel"),
67
+ default: "player",
68
+ options: [
69
+ { label: (0, import_i18n.t)("deviceTypePlayer"), value: "player" },
70
+ { label: (0, import_i18n.t)("deviceTypeTv"), value: "tv" }
71
+ ],
72
+ sm: 12,
73
+ md: 3
74
+ },
75
+ _nameRejected: {
76
+ type: "staticText",
77
+ text: (0, import_i18n.t)("deviceNameRejected"),
78
+ hidden: `!${nameFilled} || ${nameOk}`,
79
+ style: { color: "#d32f2f" },
80
+ sm: 12
81
+ },
82
+ _portRejected: {
83
+ type: "staticText",
84
+ text: (0, import_i18n.t)("devicePortInUse"),
85
+ hidden: portOk,
86
+ style: { color: "#d32f2f" },
87
+ sm: 12
88
+ },
89
+ _portHint: { type: "staticText", text: (0, import_i18n.t)("devicePortHint"), sm: 12 },
90
+ _typeHint: { type: "staticText", text: (0, import_i18n.t)("deviceTypeHint"), sm: 12 }
91
+ }
72
92
  }
73
93
  };
74
94
  }
@@ -81,11 +101,13 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
81
101
  return `system.adapter.${this.adapter.namespace}`;
82
102
  }
83
103
  /**
84
- * Read the device list from the live config object, through the shared normaliser.
104
+ * Read the device list from the live config object, through the shared normaliser — against
105
+ * the object tree the installation has, exactly like the runtime, so a row without a stored
106
+ * object id or type resolves to the tree it already owns (lib/device-config.ts).
85
107
  *
86
108
  * Every row keeps BOTH names: the trimmed one the card shows and the next write stores,
87
109
  * and the stored one the identity was resolved from. Nothing here re-derives an identity
88
- * from the display name — that is what unpaired the remote before (lib/device-config.ts).
110
+ * from the display name — that is what unpaired the remote before.
89
111
  *
90
112
  * @returns the configured devices (normalised), or an empty list
91
113
  */
@@ -93,7 +115,18 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
93
115
  var _a, _b;
94
116
  const obj = await this.adapter.getForeignObjectAsync(this.objId);
95
117
  const devices = (_a = obj == null ? void 0 : obj.native) == null ? void 0 : _a.devices;
96
- return (_b = (0, import_device_config.toDeviceRows)(devices)) != null ? _b : [];
118
+ const objects = await this.adapter.getAdapterObjectsAsync();
119
+ const prefix = `${this.adapter.namespace}.`;
120
+ const relative = /* @__PURE__ */ new Map();
121
+ for (const [id, o] of Object.entries(objects)) {
122
+ if (id.startsWith(prefix)) {
123
+ relative.set(id.slice(prefix.length), o == null ? void 0 : o.type);
124
+ }
125
+ }
126
+ return (_b = (0, import_device_config.toDeviceRows)(
127
+ devices,
128
+ (0, import_device_config.deviceTreeOf)(relative.keys(), (id) => relative.get(id))
129
+ )) != null ? _b : [];
97
130
  }
98
131
  /**
99
132
  * Persist the device list.
@@ -114,19 +147,20 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
114
147
  }
115
148
  /**
116
149
  * The stored shape of a row the user did NOT touch: its name exactly as it was, the
117
- * normalised port and type, and the identity resolved when the row was read.
150
+ * normalised port and type, and the identity and object id resolved when the row was read.
118
151
  *
119
152
  * The name stays untouched on purpose. Writing the trimmed form would move that device's
120
153
  * object id on the next start — a tree wandering, and every script pointing into it
121
154
  * breaking, because someone edited a different card. Trimming happens where the user
122
155
  * pressed save, nowhere else. Persisting the identity is safe by contrast: it is exactly
123
- * the value the runtime is already advertising for that row, just no longer derived.
156
+ * the value the runtime is already advertising for that row, just no longer derived — and the
157
+ * same holds for the object id and for a type read from the tree.
124
158
  *
125
159
  * @param row the normalised row
126
160
  * @returns the row as it is persisted
127
161
  */
128
162
  static toStored(row) {
129
- return { name: row.storedName, port: row.port, type: row.type, uuid: row.identity };
163
+ return { name: row.storedName, port: row.port, type: row.type, uuid: row.identity, objectId: row.objectId };
130
164
  }
131
165
  /**
132
166
  * Populate the manager with one card per configured device.
@@ -158,12 +192,11 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
158
192
  * @returns the card descriptor
159
193
  */
160
194
  toDeviceInfo(device) {
161
- const kind = device.type === "tv" ? "TV" : "Player";
162
195
  return {
163
196
  id: device.identity,
164
197
  name: device.name,
165
198
  identifier: String(device.port),
166
- model: kind,
199
+ model: (0, import_i18n.t)(device.type === "tv" ? "deviceTypeTv" : "deviceTypePlayer"),
167
200
  actions: [
168
201
  {
169
202
  id: "edit",
@@ -204,9 +237,11 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
204
237
  const usedNames = devices.map((d) => d.name);
205
238
  const usedPorts = devices.map((d) => d.port);
206
239
  const usedIds = devices.map(import_device_config.deviceObjectId);
207
- const data = await context.showForm(buildDeviceForm(usedNames, usedPorts, usedIds), {
240
+ const form = buildDeviceForm(usedNames, usedPorts, usedIds);
241
+ const data = await context.showForm(form.schema, {
208
242
  title: (0, import_i18n.t)("dmAdd"),
209
- data: { type: "player", port: (0, import_device_config.nextFreePort)(usedPorts) }
243
+ data: { type: "player", port: (0, import_device_config.nextFreePort)(usedPorts) },
244
+ applyDisabledRule: form.applyDisabledRule
210
245
  });
211
246
  if (data && typeof data.name === "string" && data.name.trim()) {
212
247
  const clean = cleanDevice(data);
@@ -216,7 +251,7 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
216
251
  return { refresh: true };
217
252
  }
218
253
  const stored = devices.map((d) => FakerokuDeviceManagement.toStored(d));
219
- stored.push({ ...clean, uuid: (0, import_device_identity.deriveUuid)(clean.name) });
254
+ stored.push({ ...clean, uuid: (0, import_device_identity.randomIdentity)(), objectId: (0, import_pure_helpers.sanitizeId)(clean.name) });
220
255
  await this.writeDevices(stored);
221
256
  }
222
257
  return { refresh: true };
@@ -243,10 +278,11 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
243
278
  }
244
279
  const usedNames = devices.filter((_, i) => i !== index).map((d) => d.name);
245
280
  const usedPorts = devices.filter((_, i) => i !== index).map((d) => d.port);
246
- const usedIds = devices.filter((_, i) => i !== index).map(import_device_config.deviceObjectId);
247
- const data = await context.showForm(buildDeviceForm(usedNames, usedPorts, usedIds), {
281
+ const form = buildDeviceForm(usedNames, usedPorts, null);
282
+ const data = await context.showForm(form.schema, {
248
283
  title: (0, import_i18n.t)("dmEditTitle"),
249
- data: { name: current.name, port: current.port, type: current.type }
284
+ data: { name: current.name, port: current.port, type: current.type },
285
+ applyDisabledRule: form.applyDisabledRule
250
286
  });
251
287
  if (data && typeof data.name === "string" && data.name.trim()) {
252
288
  const clean = cleanDevice(data);
@@ -256,7 +292,7 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
256
292
  return { refresh: "devices" };
257
293
  }
258
294
  const stored = devices.map((d) => FakerokuDeviceManagement.toStored(d));
259
- stored[index] = { ...clean, uuid: current.identity };
295
+ stored[index] = { ...clean, uuid: current.identity, objectId: current.objectId };
260
296
  await this.writeDevices(stored);
261
297
  }
262
298
  return { refresh: "devices" };