iobroker.fakeroku 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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 and capped at 25 commands per second per emulated Roku, so a misbehaving device cannot flood ioBroker.
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.
29
29
 
30
30
  ## Sentry / Error reporting
31
31
 
@@ -57,7 +57,7 @@ For details and how to disable it, see the [Sentry plugin documentation](https:/
57
57
  and the dialog refuses a name or port already in use) and a **type**. You can
58
58
  emulate several Rokus from one instance — each needs its own port.
59
59
  - **Type** — *Player* (default) exposes the 16 standard navigation and playback keys;
60
- *TV* additionally exposes volume, power, channel and input keys. Choose *TV* only if
60
+ *TV* additionally exposes volume, channel and input keys plus a power-off key. Choose *TV* only if
61
61
  you want those extra keys as ioBroker triggers.
62
62
 
63
63
  To add the emulated Roku to a Harmony hub, add a "Roku" device in the Harmony app
@@ -69,7 +69,7 @@ At instance level:
69
69
 
70
70
  | Datapoint | Type | Meaning |
71
71
  |---|---|---|
72
- | `info.connection` | boolean, read-only | `true` only while **every** configured Roku is actually listening. If one of them cannot start — usually because its port is already taken — the instance stays disconnected and the log names the device and the port. |
72
+ | `info.connection` | boolean, read-only | `true` only while **every** configured Roku is actually listening. If one of them cannot start — usually because its port is already taken — the log names the device and the port, and the adapter retries that device every minute until it comes up. |
73
73
 
74
74
  For every emulated Roku (`fakeroku.0.<name>`):
75
75
 
@@ -77,7 +77,7 @@ For every emulated Roku (`fakeroku.0.<name>`):
77
77
  |---|---|---|
78
78
  | `.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
79
  | `.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\*, Power, Channel\*, HDMI/AV inputs — all created up front. A keypress pulses it `true` for a moment; keydown/keyup hold it. |
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. |
81
81
 
82
82
  Free keyboard input (`Lit_x`) and app launches show up in `.command` only — they do
83
83
  not get their own objects.
@@ -95,7 +95,17 @@ becomes `true` — or watch `.command` for the last button as text.
95
95
  ### **WORK IN PROGRESS**
96
96
  -->
97
97
 
98
- ### 1.6.1 (2026-09-07)
98
+ ### 1.7.0 (2026-09-16)
99
+
100
+ - (krobipd) Fixed: button datapoints keep their value and their room and function assignment when the adapter starts.
101
+ - (krobipd) Fixed: after an emulated Roku drops out, its port is free again instead of staying blocked until ioBroker restarts.
102
+ - (krobipd) Fixed: stopping the instance no longer leaves it reported as connected.
103
+ - (krobipd) Fixed: a key you hold right after a short press stays pressed instead of being released early.
104
+ - (krobipd) Fixed: the device dialog now also refuses a name that would collide with an existing device in the object tree.
105
+ - (krobipd) Improved: after the host gets a new IP address, remotes find the emulated Rokus again without restarting the instance.
106
+ - (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.
107
+
108
+ ### 1.6.1 (2026-09-07) — stable
99
109
 
100
110
  - (krobipd) Changed: installing straight from GitHub is no longer offered — the adapter is built before publishing, so it is installed from the ioBroker repository instead.
101
111
 
@@ -126,15 +136,6 @@ becomes `true` — or watch `.command` for the last button as text.
126
136
  - (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.
127
137
  - (krobipd) New: user documentation in English and German, shown in the ioBroker documentation portal.
128
138
 
129
- ### 1.3.0 (2026-09-01)
130
- - (krobipd) Fixed: a malformed keyboard keypress from a remote (a bad %-escape in the URL) could crash the adapter.
131
- - (krobipd) Fixed: remotes on an IPv6-only local network were refused; link-local and unique-local IPv6 addresses now count as LAN.
132
- - (krobipd) Fixed: the adapter icon in the admin is now the same one shown on GitHub.
133
- - (krobipd) Changed: requires admin >= 8.0.11.
134
- - (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.
135
- - (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.
136
- - (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.
137
-
138
139
  [Older changelogs can be found there](CHANGELOG_OLD.md)
139
140
 
140
141
  ## History
@@ -189,7 +190,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
189
190
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
190
191
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
191
192
  THE SOFTWARE.
192
-
193
- ---
194
-
195
- _Developed with assistance from Claude.ai_
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "headerNetwork": "Netzwerk",
3
- "networkInterface": "Netzwerkschnittstelle (Binden + Ankündigen)",
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.",
3
+ "bind": "Netzwerkschnittstelle (Binden + Ankündigen)",
4
+ "bindTooltip": "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
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",
@@ -10,8 +10,8 @@
10
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).",
11
11
  "deviceTypeLabel": "Typ",
12
12
  "deviceTypePlayer": "Player (Streaming-Box)",
13
- "deviceTypeTv": "TV (mit Lautstärke, Power, Kanal, Eingang)",
14
- "deviceTypeHint": "„Player“ bietet die 16 Standard-Fernbedienungstasten. „TV“ zusätzlich Lautstärke-, Power-, Kanal- und Eingangstasten.",
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“.",
15
15
  "dmAdd": "Roku-Gerät hinzufügen",
16
16
  "dmEdit": "Bearbeiten",
17
17
  "dmDelete": "Löschen",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "headerNetwork": "Network",
3
- "networkInterface": "Network interface (bind + advertise)",
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.",
3
+ "bind": "Network interface (bind + advertise)",
4
+ "bindTooltip": "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
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",
@@ -10,8 +10,8 @@
10
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).",
11
11
  "deviceTypeLabel": "Type",
12
12
  "deviceTypePlayer": "Player (streaming box)",
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.",
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.",
15
15
  "dmAdd": "Add Roku device",
16
16
  "dmEdit": "Edit",
17
17
  "dmDelete": "Delete",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "headerNetwork": "Red",
3
- "networkInterface": "Interfaz de red (enlace + anuncio)",
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.",
3
+ "bind": "Interfaz de red (enlace + anuncio)",
4
+ "bindTooltip": "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
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",
@@ -10,8 +10,8 @@
10
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).",
11
11
  "deviceTypeLabel": "Tipo",
12
12
  "deviceTypePlayer": "Player (reproductor)",
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, canal y entrada.",
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.",
15
15
  "dmAdd": "Añadir dispositivo Roku",
16
16
  "dmEdit": "Editar",
17
17
  "dmDelete": "Eliminar",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "headerNetwork": "Réseau",
3
- "networkInterface": "Interface réseau (liaison + annonce)",
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.",
3
+ "bind": "Interface réseau (liaison + annonce)",
4
+ "bindTooltip": "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
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",
@@ -10,8 +10,8 @@
10
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).",
11
11
  "deviceTypeLabel": "Type",
12
12
  "deviceTypePlayer": "Player (box de streaming)",
13
- "deviceTypeTv": "TV (avec volume, alimentation, chaîne, entrée)",
14
- "deviceTypeHint": "« Player » expose les 16 touches standard de la télécommande. « TV » ajoute les touches volume, alimentation, chaîne et entrée.",
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.",
15
15
  "dmAdd": "Ajouter un appareil Roku",
16
16
  "dmEdit": "Modifier",
17
17
  "dmDelete": "Supprimer",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "headerNetwork": "Rete",
3
- "networkInterface": "Interfaccia di rete (bind + annuncio)",
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.",
3
+ "bind": "Interfaccia di rete (bind + annuncio)",
4
+ "bindTooltip": "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
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",
@@ -10,8 +10,8 @@
10
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).",
11
11
  "deviceTypeLabel": "Tipo",
12
12
  "deviceTypePlayer": "Player (box di streaming)",
13
- "deviceTypeTv": "TV (con volume, accensione, canale, ingresso)",
14
- "deviceTypeHint": "«Player» espone i 16 tasti standard del telecomando. «TV» aggiunge i tasti volume, accensione, canale e ingresso.",
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.",
15
15
  "dmAdd": "Aggiungi dispositivo Roku",
16
16
  "dmEdit": "Modifica",
17
17
  "dmDelete": "Elimina",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "headerNetwork": "Netwerk",
3
- "networkInterface": "Netwerkinterface (binden + aankondigen)",
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.",
3
+ "bind": "Netwerkinterface (binden + aankondigen)",
4
+ "bindTooltip": "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
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",
@@ -10,8 +10,8 @@
10
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).",
11
11
  "deviceTypeLabel": "Type",
12
12
  "deviceTypePlayer": "Player (streaming-box)",
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.",
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.",
15
15
  "dmAdd": "Roku-apparaat toevoegen",
16
16
  "dmEdit": "Bewerken",
17
17
  "dmDelete": "Verwijderen",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "headerNetwork": "Sieć",
3
- "networkInterface": "Interfejs sieciowy (bind + rozgłaszanie)",
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.",
3
+ "bind": "Interfejs sieciowy (bind + rozgłaszanie)",
4
+ "bindTooltip": "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
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",
@@ -10,8 +10,8 @@
10
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).",
11
11
  "deviceTypeLabel": "Typ",
12
12
  "deviceTypePlayer": "Player (przystawka)",
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.",
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.",
15
15
  "dmAdd": "Dodaj urządzenie Roku",
16
16
  "dmEdit": "Edytuj",
17
17
  "dmDelete": "Usuń",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "headerNetwork": "Rede",
3
- "networkInterface": "Interface de rede (vincular + anunciar)",
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.",
3
+ "bind": "Interface de rede (vincular + anunciar)",
4
+ "bindTooltip": "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
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",
@@ -10,8 +10,8 @@
10
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).",
11
11
  "deviceTypeLabel": "Tipo",
12
12
  "deviceTypePlayer": "Player (box de streaming)",
13
- "deviceTypeTv": "TV (com volume, energia, canal, entrada)",
14
- "deviceTypeHint": "«Player» expõe as 16 teclas padrão do comando. «TV» adiciona teclas de volume, energia, canal e entrada.",
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.",
15
15
  "dmAdd": "Adicionar dispositivo Roku",
16
16
  "dmEdit": "Editar",
17
17
  "dmDelete": "Eliminar",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "headerNetwork": "Сеть",
3
- "networkInterface": "Сетевой интерфейс (привязка + объявление)",
4
- "networkInterfaceTooltip": "Сетевая карта, к которой привязываются и на которой объявляются эмулируемые Roku. Оставьте 'все интерфейсы' — маршрутизируемый IP определяется автоматически, поэтому работает без настройки. Выбирайте конкретный адрес только на хосте с несколькими сетевыми картами.",
3
+ "bind": "Сетевой интерфейс (привязка + объявление)",
4
+ "bindTooltip": "Сетевая карта, к которой привязываются и на которой объявляются эмулируемые Roku. Оставьте 'все интерфейсы' — маршрутизируемый IP определяется автоматически, поэтому работает без настройки. Выбирайте конкретный адрес только на хосте с несколькими сетевыми картами.",
5
5
  "headerDevices": "Эмулируемые устройства Roku",
6
6
  "devicesHelp": "Каждая карточка — это один эмулируемый Roku. Добавьте устройство, затем в приложении Harmony или Sofabaton добавьте устройство <b>Roku</b>, указывающее на этот хост ioBroker — каждое нажатие попадает в объекты устройства.",
7
7
  "deviceName": "Имя",
@@ -10,8 +10,8 @@
10
10
  "devicePortHint": "ECP-порт, на котором отвечает этот Roku. Каждому эмулированному Roku нужен свой порт — не используйте один и тот же. По умолчанию 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": "Удалить",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "headerNetwork": "Мережа",
3
- "networkInterface": "Мережевий інтерфейс (прив'язка + оголошення)",
4
- "networkInterfaceTooltip": "Мережева карта, до якої прив'язуються та на якій оголошуються емульовані Roku. Залиште 'всі інтерфейси' — маршрутизовану IP-адресу визначено автоматично, тож працює без налаштування. Вибирайте конкретну адресу лише на хості з кількома мережевими картами.",
3
+ "bind": "Мережевий інтерфейс (прив'язка + оголошення)",
4
+ "bindTooltip": "Мережева карта, до якої прив'язуються та на якій оголошуються емульовані Roku. Залиште 'всі інтерфейси' — маршрутизовану IP-адресу визначено автоматично, тож працює без налаштування. Вибирайте конкретну адресу лише на хості з кількома мережевими картами.",
5
5
  "headerDevices": "Емульовані пристрої Roku",
6
6
  "devicesHelp": "Кожна картка — це один емульований Roku. Додайте пристрій, потім у застосунку Harmony або Sofabaton додайте пристрій <b>Roku</b>, що вказує на цей хост ioBroker — кожне натискання потрапляє в об'єкти пристрою.",
7
7
  "deviceName": "Назва",
@@ -10,8 +10,8 @@
10
10
  "devicePortHint": "ECP-порт, на якому відповідає цей Roku. Кожному емульованому Roku потрібен власний порт — не використовуйте один і той самий. За замовчуванням 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": "Видалити",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "headerNetwork": "网络",
3
- "networkInterface": "网络接口(绑定 + 广播)",
4
- "networkInterfaceTooltip": "模拟 Roku 绑定并广播的网卡。保留为“所有接口”即可——可路由的 IP 会被自动检测,因此无需配置即可运行。仅在有多块网卡的主机上才选择具体地址。",
3
+ "bind": "网络接口(绑定 + 广播)",
4
+ "bindTooltip": "模拟 Roku 绑定并广播的网卡。保留为“所有接口”即可——可路由的 IP 会被自动检测,因此无需配置即可运行。仅在有多块网卡的主机上才选择具体地址。",
5
5
  "headerDevices": "模拟的 Roku 设备",
6
6
  "devicesHelp": "每张卡片是一个模拟的 Roku。添加一个后,在 Harmony 或 Sofabaton 应用中添加一个指向此 ioBroker 主机的 <b>Roku</b> 设备——每次按键都会进入该设备的对象。",
7
7
  "deviceName": "名称",
@@ -10,8 +10,8 @@
10
10
  "devicePortHint": "此 Roku 应答的 ECP 端口。每个模拟的 Roku 都需要自己的端口——不要重复使用。默认 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,10 +7,10 @@
7
7
  "text": "headerNetwork",
8
8
  "size": 4
9
9
  },
10
- "networkInterface": {
10
+ "bind": {
11
11
  "type": "ip",
12
- "label": "networkInterface",
13
- "tooltip": "networkInterfaceTooltip",
12
+ "label": "bind",
13
+ "tooltip": "bindTooltip",
14
14
  "listenOnAllPorts": true,
15
15
  "onlyIp4": true,
16
16
  "noInternal": true,
@@ -34,7 +34,10 @@
34
34
  "md": 12,
35
35
  "lg": 12,
36
36
  "xl": 12,
37
- "style": { "fontSize": 14, "marginBottom": 8 }
37
+ "style": {
38
+ "fontSize": 14,
39
+ "marginBottom": 8
40
+ }
38
41
  },
39
42
  "_deviceManager": {
40
43
  "type": "deviceManager",
@@ -58,7 +61,10 @@
58
61
  "md": 12,
59
62
  "lg": 12,
60
63
  "xl": 12,
61
- "style": { "fontSize": 14, "marginBottom": 16 }
64
+ "style": {
65
+ "fontSize": 14,
66
+ "marginBottom": 16
67
+ }
62
68
  },
63
69
  "_kofiLink": {
64
70
  "type": "staticLink",
@@ -28,11 +28,10 @@ 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");
32
31
  const ID_EXPRESSION = "(data.name||'').trim().replace(/[^A-Za-z0-9\\-_]/g,'_')";
33
- function buildDeviceForm(usedNames, usedPorts) {
32
+ function buildDeviceForm(usedNames, usedPorts, usedIds) {
34
33
  const nameList = JSON.stringify(usedNames.map((n) => n.trim().toLowerCase()));
35
- const idList = JSON.stringify([...import_constants.RESERVED_IDS, ...usedNames.map((n) => (0, import_pure_helpers.sanitizeId)(n.trim()))]);
34
+ const idList = JSON.stringify([...import_constants.RESERVED_IDS, ...usedIds]);
36
35
  const portList = JSON.stringify([...usedPorts]);
37
36
  return {
38
37
  type: "panel",
@@ -204,7 +203,8 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
204
203
  const devices = await this.readDevices();
205
204
  const usedNames = devices.map((d) => d.name);
206
205
  const usedPorts = devices.map((d) => d.port);
207
- const data = await context.showForm(buildDeviceForm(usedNames, usedPorts), {
206
+ const usedIds = devices.map(import_device_config.deviceObjectId);
207
+ const data = await context.showForm(buildDeviceForm(usedNames, usedPorts, usedIds), {
208
208
  title: (0, import_i18n.t)("dmAdd"),
209
209
  data: { type: "player", port: (0, import_device_config.nextFreePort)(usedPorts) }
210
210
  });
@@ -243,7 +243,8 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
243
243
  }
244
244
  const usedNames = devices.filter((_, i) => i !== index).map((d) => d.name);
245
245
  const usedPorts = devices.filter((_, i) => i !== index).map((d) => d.port);
246
- const data = await context.showForm(buildDeviceForm(usedNames, usedPorts), {
246
+ const usedIds = devices.filter((_, i) => i !== index).map(import_device_config.deviceObjectId);
247
+ const data = await context.showForm(buildDeviceForm(usedNames, usedPorts, usedIds), {
247
248
  title: (0, import_i18n.t)("dmEditTitle"),
248
249
  data: { name: current.name, port: current.port, type: current.type }
249
250
  });
@@ -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 { RESERVED_IDS } from \"./lib/constants\";\nimport {\n findClash,\n nextFreePort,\n normalizePort,\n normalizeType,\n toDeviceRows,\n type DeviceRow,\n} from \"./lib/device-config\";\nimport { deriveUuid } from \"./lib/device-identity\";\nimport { t } from \"./lib/i18n\";\nimport { sanitizeId } from \"./lib/pure-helpers\";\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 */\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 object id a name would take, as JavaScript the admin evaluates inside a validator.\n * Mirrors `sanitizeId` (lib/pure-helpers.ts) \u2014 the dialog has to answer the same question\n * as the runtime, and it cannot call into the adapter's code.\n */\nconst ID_EXPRESSION = \"(data.name||'').trim().replace(/[^A-Za-z0-9\\\\-_]/g,'_')\";\n\n/**\n * The add/edit form for one emulated Roku: name, ECP port and device type.\n *\n * The name validator answers all three name rules the backend knows \u2014 already taken,\n * reserved for the adapter's own tree, and \"sanitizes to the same object id as another\n * device\" \u2014 so the OK button greys out instead of the dialog accepting a name that the\n * backend then refuses after the round-trip. One field carries one error text, so it names\n * all three cases; `findClash` still returns the specific reason for anyone bypassing the\n * dialog. Labels are resolved translation objects so the embedded form is 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 // Reserved ids first, then the object ids the other devices already occupy.\n const idList = JSON.stringify([...RESERVED_IDS, ...usedNames.map(n => sanitizeId(n.trim()))]);\n const portList = JSON.stringify([...usedPorts]);\n return {\n type: \"panel\",\n items: {\n name: {\n type: \"text\",\n label: t(\"deviceName\"),\n validator:\n `${ID_EXPRESSION}.length>0` +\n ` && !${nameList}.includes((data.name||'').trim().toLowerCase())` +\n ` && !${idList}.includes(${ID_EXPRESSION})`,\n validatorErrorText: t(\"deviceNameRejected\"),\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, valid port, valid type) so\n * native.devices stays tidy. Same rules as reading a stored row (lib/device-config.ts), the\n * 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 return { name, port: normalizePort(raw.port), type: normalizeType(raw.type) };\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, through the shared normaliser.\n *\n * Every row keeps BOTH names: the trimmed one the card shows and the next write stores,\n * and the stored one the identity was resolved from. Nothing here re-derives an identity\n * from the display name \u2014 that is what unpaired the remote before (lib/device-config.ts).\n *\n * @returns the configured devices (normalised), or an empty list\n */\n private async readDevices(): Promise<DeviceRow[]> {\n const obj = await this.adapter.getForeignObjectAsync(this.objId);\n const devices = (obj?.native as { devices?: unknown } | undefined)?.devices;\n return toDeviceRows(devices) ?? [];\n }\n\n /**\n * Persist the device list.\n *\n * \u26A0\uFE0F The full array only replaces the stored one because the key is literally called\n * `devices`: js-controller clears the old array before merging for exactly four names\n * (`common.members`, `native.repositories`, `native.certificates`, `native.devices` \u2014\n * adapter.ts `_extendForeignObject`). Under any other name `extend(true, \u2026)` would merge\n * arrays element-wise, and deleting the second of two devices would leave both in place.\n * Renaming this config key silently breaks deletion; `device-management.test.ts` pins it.\n *\n * Writing `native.*` restarts the adapter, which re-creates the object trees and servers.\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 * The stored shape of a row the user did NOT touch: its name exactly as it was, the\n * normalised port and type, and the identity resolved when the row was read.\n *\n * The name stays untouched on purpose. Writing the trimmed form would move that device's\n * object id on the next start \u2014 a tree wandering, and every script pointing into it\n * breaking, because someone edited a different card. Trimming happens where the user\n * pressed save, nowhere else. Persisting the identity is safe by contrast: it is exactly\n * the value the runtime is already advertising for that row, just no longer derived.\n *\n * @param row the normalised row\n * @returns the row as it is persisted\n */\n private static toStored(row: DeviceRow): RokuDeviceConfig {\n return { name: row.storedName, port: row.port, type: row.type, uuid: row.identity };\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 for (const device of devices) {\n context.addDevice(this.toDeviceInfo(device));\n }\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 * The card is keyed by the device's SSDP identity, not by its list position: a position\n * is only valid for as long as the view is fresh, so a second admin tab (or a list that\n * changed underneath) would send edit/delete to a different device than the card clicked.\n * Two hand-edited rows can share an identity; the first one wins, which is the same answer\n * the runtime gives when two names map to one object id.\n *\n * The name needs no fallback: a row without a usable one never becomes a DeviceRow\n * (lib/device-config.ts drops it), so `Roku <n>` was a branch nothing could reach \u2014\n * the mutation run of 1.6.0 found it by having no test that could fail on its removal.\n *\n * @param device the stored device\n * @returns the card descriptor\n */\n private toDeviceInfo(device: DeviceRow): DeviceInfo<string> {\n const kind = device.type === \"tv\" ? \"TV\" : \"Player\";\n return {\n id: device.identity,\n name: device.name,\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(id, context),\n },\n {\n id: \"delete\",\n icon: \"delete\",\n description: t(\"dmDelete\"),\n handler: async (id: string, context: ActionContext) => this.deleteDevice(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 => 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 const stored = devices.map(d => FakerokuDeviceManagement.toStored(d));\n stored.push({ ...clean, uuid: deriveUuid(clean.name) });\n await this.writeDevices(stored);\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 identity is preserved so the pairing survives a rename.\n *\n * The identity comes from the row as it was READ (lib/device-config.ts resolves it from\n * the stored name), never derived here: deriving it from the name in the form would move\n * the SSDP identity on a plain rename \u2014 and deriving it from the trimmed stored name would\n * move it even when the user changed nothing at all.\n *\n * @param cardId the card's id \u2014 the device's identity\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async editDevice(cardId: string, context: ActionContext): Promise<DeviceResult> {\n const devices = await this.readDevices();\n const index = devices.findIndex(d => d.identity === cardId);\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 => d.port);\n const data = await context.showForm(buildDeviceForm(usedNames, usedPorts), {\n title: t(\"dmEditTitle\"),\n data: { name: current.name, port: current.port, type: current.type },\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 const stored = devices.map(d => FakerokuDeviceManagement.toStored(d));\n stored[index] = { ...clean, uuid: current.identity };\n await this.writeDevices(stored);\n }\n return { refresh: \"devices\" };\n }\n\n /**\n * Delete a device after confirmation.\n *\n * @param cardId the card's id \u2014 the device's identity\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async deleteDevice(cardId: string, context: ActionContext): Promise<DeviceResult> {\n const devices = await this.readDevices();\n const index = devices.findIndex(d => d.identity === cardId);\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 const stored = devices.map(d => FakerokuDeviceManagement.toStored(d));\n stored.splice(index, 1);\n await this.writeDevices(stored);\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 { RESERVED_IDS } from \"./lib/constants\";\nimport {\n deviceObjectId,\n findClash,\n nextFreePort,\n normalizePort,\n normalizeType,\n toDeviceRows,\n type DeviceRow,\n} from \"./lib/device-config\";\nimport { deriveUuid } from \"./lib/device-identity\";\nimport { t } from \"./lib/i18n\";\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 */\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 object id a name would take, as JavaScript the admin evaluates inside a validator.\n * Mirrors `sanitizeId` (lib/pure-helpers.ts) \u2014 the dialog has to answer the same question\n * as the runtime, and it cannot call into the adapter's code.\n */\nconst ID_EXPRESSION = \"(data.name||'').trim().replace(/[^A-Za-z0-9\\\\-_]/g,'_')\";\n\n/**\n * The add/edit form for one emulated Roku: name, ECP port and device type.\n *\n * The name validator answers all three name rules the backend knows \u2014 already taken,\n * reserved for the adapter's own tree, and \"sanitizes to the same object id as another\n * device\" \u2014 so the OK button greys out instead of the dialog accepting a name that the\n * backend then refuses after the round-trip. One field carries one error text, so it names\n * all three cases; `findClash` still returns the specific reason for anyone bypassing the\n * dialog. Labels are resolved translation objects so the embedded form is 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 * @param usedIds object ids the OTHER devices occupy at runtime \u2014 from their STORED names,\n * which is not the same as sanitising what the list displays\n * @returns the jsonConfig panel schema for one device\n */\nexport function buildDeviceForm(\n usedNames: readonly string[],\n usedPorts: readonly number[],\n usedIds: readonly string[],\n): JsonFormSchema {\n const nameList = JSON.stringify(usedNames.map(n => n.trim().toLowerCase()));\n // Reserved ids first, then the object ids the other devices already occupy.\n const idList = JSON.stringify([...RESERVED_IDS, ...usedIds]);\n const portList = JSON.stringify([...usedPorts]);\n return {\n type: \"panel\",\n items: {\n name: {\n type: \"text\",\n label: t(\"deviceName\"),\n validator:\n `${ID_EXPRESSION}.length>0` +\n ` && !${nameList}.includes((data.name||'').trim().toLowerCase())` +\n ` && !${idList}.includes(${ID_EXPRESSION})`,\n validatorErrorText: t(\"deviceNameRejected\"),\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, valid port, valid type) so\n * native.devices stays tidy. Same rules as reading a stored row (lib/device-config.ts), the\n * 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 return { name, port: normalizePort(raw.port), type: normalizeType(raw.type) };\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, through the shared normaliser.\n *\n * Every row keeps BOTH names: the trimmed one the card shows and the next write stores,\n * and the stored one the identity was resolved from. Nothing here re-derives an identity\n * from the display name \u2014 that is what unpaired the remote before (lib/device-config.ts).\n *\n * @returns the configured devices (normalised), or an empty list\n */\n private async readDevices(): Promise<DeviceRow[]> {\n const obj = await this.adapter.getForeignObjectAsync(this.objId);\n const devices = (obj?.native as { devices?: unknown } | undefined)?.devices;\n return toDeviceRows(devices) ?? [];\n }\n\n /**\n * Persist the device list.\n *\n * \u26A0\uFE0F The full array only replaces the stored one because the key is literally called\n * `devices`: js-controller clears the old array before merging for exactly four names\n * (`common.members`, `native.repositories`, `native.certificates`, `native.devices` \u2014\n * adapter.ts `_extendForeignObject`). Under any other name `extend(true, \u2026)` would merge\n * arrays element-wise, and deleting the second of two devices would leave both in place.\n * Renaming this config key silently breaks deletion; `device-management.test.ts` pins it.\n *\n * Writing `native.*` restarts the adapter, which re-creates the object trees and servers.\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 * The stored shape of a row the user did NOT touch: its name exactly as it was, the\n * normalised port and type, and the identity resolved when the row was read.\n *\n * The name stays untouched on purpose. Writing the trimmed form would move that device's\n * object id on the next start \u2014 a tree wandering, and every script pointing into it\n * breaking, because someone edited a different card. Trimming happens where the user\n * pressed save, nowhere else. Persisting the identity is safe by contrast: it is exactly\n * the value the runtime is already advertising for that row, just no longer derived.\n *\n * @param row the normalised row\n * @returns the row as it is persisted\n */\n private static toStored(row: DeviceRow): RokuDeviceConfig {\n return { name: row.storedName, port: row.port, type: row.type, uuid: row.identity };\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 for (const device of devices) {\n context.addDevice(this.toDeviceInfo(device));\n }\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 * The card is keyed by the device's SSDP identity, not by its list position: a position\n * is only valid for as long as the view is fresh, so a second admin tab (or a list that\n * changed underneath) would send edit/delete to a different device than the card clicked.\n * Two hand-edited rows can share an identity; the first one wins, which is the same answer\n * the runtime gives when two names map to one object id.\n *\n * The name needs no fallback: a row without a usable one never becomes a DeviceRow\n * (lib/device-config.ts drops it), so `Roku <n>` was a branch nothing could reach \u2014\n * the mutation run of 1.6.0 found it by having no test that could fail on its removal.\n *\n * @param device the stored device\n * @returns the card descriptor\n */\n private toDeviceInfo(device: DeviceRow): DeviceInfo<string> {\n const kind = device.type === \"tv\" ? \"TV\" : \"Player\";\n return {\n id: device.identity,\n name: device.name,\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(id, context),\n },\n {\n id: \"delete\",\n icon: \"delete\",\n description: t(\"dmDelete\"),\n handler: async (id: string, context: ActionContext) => this.deleteDevice(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 => d.port);\n const usedIds = devices.map(deviceObjectId);\n const data = await context.showForm(buildDeviceForm(usedNames, usedPorts, usedIds), {\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 const stored = devices.map(d => FakerokuDeviceManagement.toStored(d));\n stored.push({ ...clean, uuid: deriveUuid(clean.name) });\n await this.writeDevices(stored);\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 identity is preserved so the pairing survives a rename.\n *\n * The identity comes from the row as it was READ (lib/device-config.ts resolves it from\n * the stored name), never derived here: deriving it from the name in the form would move\n * the SSDP identity on a plain rename \u2014 and deriving it from the trimmed stored name would\n * move it even when the user changed nothing at all.\n *\n * @param cardId the card's id \u2014 the device's identity\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async editDevice(cardId: string, context: ActionContext): Promise<DeviceResult> {\n const devices = await this.readDevices();\n const index = devices.findIndex(d => d.identity === cardId);\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 => d.port);\n const usedIds = devices.filter((_, i) => i !== index).map(deviceObjectId);\n const data = await context.showForm(buildDeviceForm(usedNames, usedPorts, usedIds), {\n title: t(\"dmEditTitle\"),\n data: { name: current.name, port: current.port, type: current.type },\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 const stored = devices.map(d => FakerokuDeviceManagement.toStored(d));\n stored[index] = { ...clean, uuid: current.identity };\n await this.writeDevices(stored);\n }\n return { refresh: \"devices\" };\n }\n\n /**\n * Delete a device after confirmation.\n *\n * @param cardId the card's id \u2014 the device's identity\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async deleteDevice(cardId: string, context: ActionContext): Promise<DeviceResult> {\n const devices = await this.readDevices();\n const index = devices.findIndex(d => d.identity === cardId);\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 const stored = devices.map(d => FakerokuDeviceManagement.toStored(d));\n stored.splice(index, 1);\n await this.writeDevices(stored);\n }\n return { refresh: \"devices\" };\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -109,6 +109,39 @@ class RokuSsdpResponder {
109
109
  `Roku SSDP responder on :${SSDP_PORT}, advertising ${this.config.advertiseIp} (join: ${join})`
110
110
  );
111
111
  }
112
+ /**
113
+ * Follow a host address change while running — the automatic case only.
114
+ *
115
+ * The advertised IP is baked into every LOCATION header, and a controller caches it for
116
+ * the announced max-age (an hour). After a DHCP lease change or a move to another network
117
+ * the discovery would keep pointing at an address nobody serves, in front of an instance
118
+ * that looks perfectly healthy, until someone restarts it by hand.
119
+ *
120
+ * The group joins matter just as much: a membership was taken on the OLD interface
121
+ * address, so the socket stops hearing M-SEARCH on the new one. Only interfaces that are
122
+ * genuinely new are joined — re-joining one the socket already has throws EADDRINUSE and
123
+ * would put a warning in the log on every pass.
124
+ *
125
+ * @param advertiseIp the routable IP to announce from now on
126
+ * @param membershipInterfaces the interface IPs the socket should be in the group on
127
+ * @returns true if the advertised address actually changed
128
+ */
129
+ refreshAdvertise(advertiseIp, membershipInterfaces) {
130
+ if (advertiseIp === this.config.advertiseIp) {
131
+ return false;
132
+ }
133
+ this.config.advertiseIp = advertiseIp;
134
+ const known = new Set(this.config.membershipInterfaces);
135
+ this.config.membershipInterfaces = [...membershipInterfaces];
136
+ if (this.socket) {
137
+ for (const ip of membershipInterfaces) {
138
+ if (!known.has(ip)) {
139
+ this.tryJoin(this.socket, ip);
140
+ }
141
+ }
142
+ }
143
+ return true;
144
+ }
112
145
  /**
113
146
  * Join the multicast group on each selected interface, or on the OS default when
114
147
  * none is known.