iobroker.fakeroku 1.9.0 → 1.10.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
@@ -75,11 +75,16 @@ At instance level:
75
75
  | Datapoint | Type | Meaning |
76
76
  |---|---|---|
77
77
  | `info.connection` | boolean, read-only | `true` only while everything runs: **every** configured Roku listens, discovery answers, and the chosen address exists. Anything less shows the instance yellow; the log names the cause once, and the adapter retries a Roku or discovery that is down every minute. |
78
+ | `info.devicesTotal` | number, read-only | How many emulated Rokus are configured. |
79
+ | `info.devicesOnline` | number, read-only | How many of them listen right now. |
80
+ | `info.devicesAllOnline` | boolean, read-only | `true` while every configured Roku listens; `false` while none is configured. |
78
81
 
79
82
  For every emulated Roku (`fakeroku.0.<name>`):
80
83
 
81
84
  | Datapoint | Type | Meaning |
82
85
  |---|---|---|
86
+ | `.info.online` | boolean, read-only | `true` while this Roku listens on its port. The device shows a green or grey symbol in the object tree, its card in the device manager online or offline. |
87
+ | `.info.error` | string, read-only | Why this Roku does not run, e.g. `port 8061 is already in use (another program or another instance holds it)`; empty while it runs, `Unknown` while the adapter is off or starting. The card shows it as a warning. |
83
88
  | `.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. |
84
89
  | `.keys.<Key>` | boolean, read-only | One state per remote key the device type exposes — a *Player* has the 16 navigation/playback keys, a *TV* adds Volume\*, PowerOn/PowerOff/Power, Sleep, Channel\*, Tuner/HDMI/AV inputs — all created up front. A keypress pulses it `true` for a moment; keydown/keyup hold it. Key names are read in any case (`home` is `Home`). |
85
90
 
@@ -100,6 +105,12 @@ becomes `true` — or watch `.command` for the last button as text.
100
105
  Placeholder for the next version (at the beginning of the line):
101
106
  ### **WORK IN PROGRESS**
102
107
  -->
108
+ ### 1.10.0 (2026-10-03)
109
+
110
+ - (krobipd) New: every emulated Roku shows whether it runs and, if not, why — a green or grey symbol in the object tree, online or offline on its card with the reason.
111
+ - (krobipd) New: three datapoints count the emulated Rokus — how many are configured, how many run, and whether all of them run.
112
+ - (krobipd) Fixed: the connection datapoint's description says what green means — every Roku listening, discovery answering and the chosen address present.
113
+
103
114
  ### 1.9.0 (2026-10-03)
104
115
 
105
116
  - (krobipd) Changed: the emulated Roku answers only what a Harmony or Sofabaton reads — Home Assistant and openHAB can no longer set it up.
@@ -136,10 +147,6 @@ becomes `true` — or watch `.command` for the last button as text.
136
147
  - (krobipd) New: the TV profile adds the PowerOn, Power, Sleep and InputTuner keys and announces itself the way real Roku TVs do.
137
148
  - (krobipd) New: every new emulated Roku gets its own network identity instead of one every installation with the same name would share.
138
149
 
139
- ### 1.7.1 (2026-09-16) — stable
140
-
141
- - (krobipd) Changed: the instance restarts once after this update to clear a setting left behind by an older version; nothing else changes for you.
142
-
143
150
  [Older changelogs can be found there](CHANGELOG_OLD.md)
144
151
 
145
152
  ## History
@@ -27,9 +27,21 @@
27
27
  "donatePaypal": "PayPal",
28
28
  "channelInfo": "Adapter-Informationen",
29
29
  "connectionStatus": "Verbindungsstatus",
30
- "connectionStatusDesc": "Nur wahr, solange jeder konfigurierte emulierte Roku tatsächlich lauscht.",
30
+ "connectionStatusDesc": "Nur wahr, solange alles läuft: jeder konfigurierte emulierte Roku lauscht, die Erkennung antwortet und die gewählte Adresse existiert.",
31
31
  "stateLastCommand": "Letzter Befehl",
32
32
  "stateLastCommandDesc": "Der zuletzt empfangene Befehl dieses emulierten Roku als Klartext.",
33
33
  "channelKeys": "Fernbedienungstasten",
34
- "channelKeysDesc": "Ein Datenpunkt je Taste, die dieses Gerät anbietet; ein Tastendruck setzt ihn kurz auf wahr."
34
+ "channelKeysDesc": "Ein Datenpunkt je Taste, die dieses Gerät anbietet; ein Tastendruck setzt ihn kurz auf wahr.",
35
+ "channelDeviceInfo": "Status",
36
+ "channelDeviceInfoDesc": "Ob dieser emulierte Roku läuft und, falls nicht, warum.",
37
+ "stateOnline": "Online",
38
+ "stateOnlineDesc": "Wahr, solange dieser emulierte Roku auf seinem ECP-Port lauscht.",
39
+ "stateError": "Fehler",
40
+ "stateErrorDesc": "Warum dieser emulierte Roku nicht läuft; leer, solange er läuft, Unknown, solange der Adapter aus ist oder startet.",
41
+ "devicesTotal": "Emulierte Rokus",
42
+ "devicesTotalDesc": "Wie viele emulierte Rokus eingerichtet sind.",
43
+ "devicesOnline": "Emulierte Rokus online",
44
+ "devicesOnlineDesc": "Wie viele emulierte Rokus gerade lauschen.",
45
+ "devicesAllOnline": "Alle emulierten Rokus online",
46
+ "devicesAllOnlineDesc": "Wahr, solange jeder eingerichtete emulierte Roku lauscht; falsch, solange keiner eingerichtet ist."
35
47
  }
@@ -27,9 +27,21 @@
27
27
  "donatePaypal": "PayPal",
28
28
  "channelInfo": "Adapter information",
29
29
  "connectionStatus": "Connection status",
30
- "connectionStatusDesc": "True only while every configured emulated Roku is actually listening.",
30
+ "connectionStatusDesc": "True only while everything runs: every configured emulated Roku listens, discovery answers and the chosen address exists.",
31
31
  "stateLastCommand": "Last command",
32
32
  "stateLastCommandDesc": "The most recent command this emulated Roku received, as plain text.",
33
33
  "channelKeys": "Remote keys",
34
- "channelKeysDesc": "One state per remote key this device offers; a key press pulses it briefly."
34
+ "channelKeysDesc": "One state per remote key this device offers; a key press pulses it briefly.",
35
+ "channelDeviceInfo": "Status",
36
+ "channelDeviceInfoDesc": "Whether this emulated Roku runs and, if not, why.",
37
+ "stateOnline": "Online",
38
+ "stateOnlineDesc": "True while this emulated Roku listens on its ECP port.",
39
+ "stateError": "Error",
40
+ "stateErrorDesc": "Why this emulated Roku does not run; empty while it runs, Unknown while the adapter is off or starting.",
41
+ "devicesTotal": "Emulated Rokus",
42
+ "devicesTotalDesc": "How many emulated Rokus are configured.",
43
+ "devicesOnline": "Emulated Rokus online",
44
+ "devicesOnlineDesc": "How many emulated Rokus listen right now.",
45
+ "devicesAllOnline": "All emulated Rokus online",
46
+ "devicesAllOnlineDesc": "True while every configured emulated Roku listens; false while none is configured."
35
47
  }
@@ -27,9 +27,21 @@
27
27
  "donatePaypal": "PayPal",
28
28
  "channelInfo": "Información del adaptador",
29
29
  "connectionStatus": "Estado de conexión",
30
- "connectionStatusDesc": "Verdadero solo mientras cada Roku emulado configurado esté realmente a la escucha.",
30
+ "connectionStatusDesc": "Verdadero solo mientras todo funciona: cada Roku emulado configurado escucha, la detección responde y la dirección elegida existe.",
31
31
  "stateLastCommand": "Último comando",
32
32
  "stateLastCommandDesc": "El comando más reciente que recibió este Roku emulado, como texto simple.",
33
33
  "channelKeys": "Teclas del mando",
34
- "channelKeysDesc": "Un estado por cada tecla que ofrece este dispositivo; una pulsación lo pone brevemente en verdadero."
34
+ "channelKeysDesc": "Un estado por cada tecla que ofrece este dispositivo; una pulsación lo pone brevemente en verdadero.",
35
+ "channelDeviceInfo": "Estado",
36
+ "channelDeviceInfoDesc": "Si este Roku emulado funciona y, si no, por qué.",
37
+ "stateOnline": "En línea",
38
+ "stateOnlineDesc": "Verdadero mientras este Roku emulado escucha en su puerto ECP.",
39
+ "stateError": "Error",
40
+ "stateErrorDesc": "Por qué este Roku emulado no funciona; vacío mientras funciona, Unknown mientras el adaptador está apagado o arrancando.",
41
+ "devicesTotal": "Rokus emulados",
42
+ "devicesTotalDesc": "Cuántos Rokus emulados están configurados.",
43
+ "devicesOnline": "Rokus emulados en línea",
44
+ "devicesOnlineDesc": "Cuántos Rokus emulados escuchan ahora mismo.",
45
+ "devicesAllOnline": "Todos los Rokus emulados en línea",
46
+ "devicesAllOnlineDesc": "Verdadero mientras cada Roku emulado configurado escucha; falso mientras no hay ninguno configurado."
35
47
  }
@@ -27,9 +27,21 @@
27
27
  "donatePaypal": "PayPal",
28
28
  "channelInfo": "Informations sur l'adaptateur",
29
29
  "connectionStatus": "État de la connexion",
30
- "connectionStatusDesc": "Vrai uniquement tant que chaque Roku émulé configuré est réellement à l'écoute.",
30
+ "connectionStatusDesc": "Vrai uniquement tant que tout fonctionne : chaque Roku émulé configuré écoute, la découverte répond et l'adresse choisie existe.",
31
31
  "stateLastCommand": "Dernière commande",
32
32
  "stateLastCommandDesc": "La commande la plus récente reçue par ce Roku émulé, en texte brut.",
33
33
  "channelKeys": "Touches de la télécommande",
34
- "channelKeysDesc": "Un état par touche proposée par cet appareil ; un appui le met brièvement à vrai."
34
+ "channelKeysDesc": "Un état par touche proposée par cet appareil ; un appui le met brièvement à vrai.",
35
+ "channelDeviceInfo": "État",
36
+ "channelDeviceInfoDesc": "Si ce Roku émulé fonctionne et, sinon, pourquoi.",
37
+ "stateOnline": "En ligne",
38
+ "stateOnlineDesc": "Vrai tant que ce Roku émulé écoute sur son port ECP.",
39
+ "stateError": "Erreur",
40
+ "stateErrorDesc": "Pourquoi ce Roku émulé ne fonctionne pas ; vide tant qu'il fonctionne, Unknown tant que l'adaptateur est arrêté ou démarre.",
41
+ "devicesTotal": "Rokus émulés",
42
+ "devicesTotalDesc": "Combien de Rokus émulés sont configurés.",
43
+ "devicesOnline": "Rokus émulés en ligne",
44
+ "devicesOnlineDesc": "Combien de Rokus émulés écoutent en ce moment.",
45
+ "devicesAllOnline": "Tous les Rokus émulés en ligne",
46
+ "devicesAllOnlineDesc": "Vrai tant que chaque Roku émulé configuré écoute ; faux tant qu'aucun n'est configuré."
35
47
  }
@@ -27,9 +27,21 @@
27
27
  "donatePaypal": "PayPal",
28
28
  "channelInfo": "Informazioni sull'adattatore",
29
29
  "connectionStatus": "Stato connessione",
30
- "connectionStatusDesc": "Vero solo finché ogni Roku emulato configurato è effettivamente in ascolto.",
30
+ "connectionStatusDesc": "Vero solo finché tutto funziona: ogni Roku emulato configurato è in ascolto, il rilevamento risponde e l'indirizzo scelto esiste.",
31
31
  "stateLastCommand": "Ultimo comando",
32
32
  "stateLastCommandDesc": "Il comando più recente ricevuto da questo Roku emulato, come testo semplice.",
33
33
  "channelKeys": "Tasti del telecomando",
34
- "channelKeysDesc": "Uno stato per ogni tasto offerto da questo dispositivo; una pressione lo porta brevemente a vero."
34
+ "channelKeysDesc": "Uno stato per ogni tasto offerto da questo dispositivo; una pressione lo porta brevemente a vero.",
35
+ "channelDeviceInfo": "Stato",
36
+ "channelDeviceInfoDesc": "Se questo Roku emulato è in funzione e, in caso contrario, perché.",
37
+ "stateOnline": "Online",
38
+ "stateOnlineDesc": "Vero finché questo Roku emulato è in ascolto sulla sua porta ECP.",
39
+ "stateError": "Errore",
40
+ "stateErrorDesc": "Perché questo Roku emulato non è in funzione; vuoto finché funziona, Unknown finché l'adattatore è spento o si avvia.",
41
+ "devicesTotal": "Roku emulati",
42
+ "devicesTotalDesc": "Quanti Roku emulati sono configurati.",
43
+ "devicesOnline": "Roku emulati online",
44
+ "devicesOnlineDesc": "Quanti Roku emulati sono in ascolto in questo momento.",
45
+ "devicesAllOnline": "Tutti i Roku emulati online",
46
+ "devicesAllOnlineDesc": "Vero finché ogni Roku emulato configurato è in ascolto; falso finché nessuno è configurato."
35
47
  }
@@ -27,9 +27,21 @@
27
27
  "donatePaypal": "PayPal",
28
28
  "channelInfo": "Adapterinformatie",
29
29
  "connectionStatus": "Verbindingsstatus",
30
- "connectionStatusDesc": "Alleen waar zolang elke geconfigureerde geëmuleerde Roku daadwerkelijk luistert.",
30
+ "connectionStatusDesc": "Alleen waar zolang alles draait: elke geconfigureerde geëmuleerde Roku luistert, de detectie antwoordt en het gekozen adres bestaat.",
31
31
  "stateLastCommand": "Laatste opdracht",
32
32
  "stateLastCommandDesc": "De meest recente opdracht die deze geëmuleerde Roku ontving, als platte tekst.",
33
33
  "channelKeys": "Afstandsbedieningstoetsen",
34
- "channelKeysDesc": "Eén status per toets die dit apparaat biedt; een toetsdruk zet hem kort op waar."
34
+ "channelKeysDesc": "Eén status per toets die dit apparaat biedt; een toetsdruk zet hem kort op waar.",
35
+ "channelDeviceInfo": "Status",
36
+ "channelDeviceInfoDesc": "Of deze geëmuleerde Roku draait en, zo niet, waarom.",
37
+ "stateOnline": "Online",
38
+ "stateOnlineDesc": "Waar zolang deze geëmuleerde Roku op zijn ECP-poort luistert.",
39
+ "stateError": "Fout",
40
+ "stateErrorDesc": "Waarom deze geëmuleerde Roku niet draait; leeg zolang hij draait, Unknown zolang de adapter uit staat of start.",
41
+ "devicesTotal": "Geëmuleerde Roku's",
42
+ "devicesTotalDesc": "Hoeveel geëmuleerde Roku's er zijn ingesteld.",
43
+ "devicesOnline": "Geëmuleerde Roku's online",
44
+ "devicesOnlineDesc": "Hoeveel geëmuleerde Roku's op dit moment luisteren.",
45
+ "devicesAllOnline": "Alle geëmuleerde Roku's online",
46
+ "devicesAllOnlineDesc": "Waar zolang elke ingestelde geëmuleerde Roku luistert; onwaar zolang er geen is ingesteld."
35
47
  }
@@ -27,9 +27,21 @@
27
27
  "donatePaypal": "PayPal",
28
28
  "channelInfo": "Informacje o adapterze",
29
29
  "connectionStatus": "Stan połączenia",
30
- "connectionStatusDesc": "Prawda tylko wtedy, gdy każdy skonfigurowany emulowany Roku faktycznie nasłuchuje.",
30
+ "connectionStatusDesc": "Prawda tylko wtedy, gdy wszystko działa: każdy skonfigurowany emulowany Roku nasłuchuje, wykrywanie odpowiada, a wybrany adres istnieje.",
31
31
  "stateLastCommand": "Ostatnie polecenie",
32
32
  "stateLastCommandDesc": "Najnowsze polecenie odebrane przez ten emulowany Roku, jako zwykły tekst.",
33
33
  "channelKeys": "Klawisze pilota",
34
- "channelKeysDesc": "Jeden stan na każdy klawisz oferowany przez to urządzenie; naciśnięcie ustawia go na krótko na wartość true."
34
+ "channelKeysDesc": "Jeden stan na każdy klawisz oferowany przez to urządzenie; naciśnięcie ustawia go na krótko na wartość true.",
35
+ "channelDeviceInfo": "Stan",
36
+ "channelDeviceInfoDesc": "Czy ten emulowany Roku działa, a jeśli nie, to dlaczego.",
37
+ "stateOnline": "Online",
38
+ "stateOnlineDesc": "Prawda, dopóki ten emulowany Roku nasłuchuje na swoim porcie ECP.",
39
+ "stateError": "Błąd",
40
+ "stateErrorDesc": "Dlaczego ten emulowany Roku nie działa; puste, gdy działa, Unknown, gdy adapter jest wyłączony lub się uruchamia.",
41
+ "devicesTotal": "Emulowane Roku",
42
+ "devicesTotalDesc": "Ile emulowanych Roku jest skonfigurowanych.",
43
+ "devicesOnline": "Emulowane Roku online",
44
+ "devicesOnlineDesc": "Ile emulowanych Roku nasłuchuje w tej chwili.",
45
+ "devicesAllOnline": "Wszystkie emulowane Roku online",
46
+ "devicesAllOnlineDesc": "Prawda, dopóki nasłuchuje każdy skonfigurowany emulowany Roku; fałsz, dopóki żaden nie jest skonfigurowany."
35
47
  }
@@ -27,9 +27,21 @@
27
27
  "donatePaypal": "PayPal",
28
28
  "channelInfo": "Informação do adaptador",
29
29
  "connectionStatus": "Estado da ligação",
30
- "connectionStatusDesc": "Verdadeiro apenas enquanto todos os Roku emulados configurados estiverem realmente à escuta.",
30
+ "connectionStatusDesc": "Verdadeiro apenas enquanto tudo funciona: cada Roku emulado configurado está à escuta, a descoberta responde e o endereço escolhido existe.",
31
31
  "stateLastCommand": "Último comando",
32
32
  "stateLastCommandDesc": "O comando mais recente recebido por este Roku emulado, em texto simples.",
33
33
  "channelKeys": "Teclas do comando",
34
- "channelKeysDesc": "Um estado por cada tecla que este dispositivo oferece; uma pressão coloca-o brevemente a verdadeiro."
34
+ "channelKeysDesc": "Um estado por cada tecla que este dispositivo oferece; uma pressão coloca-o brevemente a verdadeiro.",
35
+ "channelDeviceInfo": "Estado",
36
+ "channelDeviceInfoDesc": "Se este Roku emulado está a funcionar e, se não, porquê.",
37
+ "stateOnline": "Online",
38
+ "stateOnlineDesc": "Verdadeiro enquanto este Roku emulado está à escuta na sua porta ECP.",
39
+ "stateError": "Erro",
40
+ "stateErrorDesc": "Porque é que este Roku emulado não funciona; vazio enquanto funciona, Unknown enquanto o adaptador está desligado ou a arrancar.",
41
+ "devicesTotal": "Rokus emulados",
42
+ "devicesTotalDesc": "Quantos Rokus emulados estão configurados.",
43
+ "devicesOnline": "Rokus emulados online",
44
+ "devicesOnlineDesc": "Quantos Rokus emulados estão à escuta neste momento.",
45
+ "devicesAllOnline": "Todos os Rokus emulados online",
46
+ "devicesAllOnlineDesc": "Verdadeiro enquanto cada Roku emulado configurado está à escuta; falso enquanto nenhum está configurado."
35
47
  }
@@ -27,9 +27,21 @@
27
27
  "donatePaypal": "PayPal",
28
28
  "channelInfo": "Информация об адаптере",
29
29
  "connectionStatus": "Статус подключения",
30
- "connectionStatusDesc": "Истина только пока каждый настроенный эмулированный Roku действительно слушает.",
30
+ "connectionStatusDesc": "Истина только пока всё работает: каждый настроенный эмулированный Roku слушает, обнаружение отвечает и выбранный адрес существует.",
31
31
  "stateLastCommand": "Последняя команда",
32
32
  "stateLastCommandDesc": "Последняя команда, полученная этим эмулированным Roku, в виде текста.",
33
33
  "channelKeys": "Кнопки пульта",
34
- "channelKeysDesc": "По одному состоянию на каждую кнопку пульта этого устройства; нажатие кратко переводит его в истину."
34
+ "channelKeysDesc": "По одному состоянию на каждую кнопку пульта этого устройства; нажатие кратко переводит его в истину.",
35
+ "channelDeviceInfo": "Состояние",
36
+ "channelDeviceInfoDesc": "Работает ли этот эмулированный Roku и, если нет, почему.",
37
+ "stateOnline": "В сети",
38
+ "stateOnlineDesc": "Истина, пока этот эмулированный Roku слушает свой порт ECP.",
39
+ "stateError": "Ошибка",
40
+ "stateErrorDesc": "Почему этот эмулированный Roku не работает; пусто, пока он работает, Unknown, пока адаптер выключен или запускается.",
41
+ "devicesTotal": "Эмулированные Roku",
42
+ "devicesTotalDesc": "Сколько эмулированных Roku настроено.",
43
+ "devicesOnline": "Эмулированные Roku в сети",
44
+ "devicesOnlineDesc": "Сколько эмулированных Roku сейчас слушают.",
45
+ "devicesAllOnline": "Все эмулированные Roku в сети",
46
+ "devicesAllOnlineDesc": "Истина, пока слушает каждый настроенный эмулированный Roku; ложь, пока ни один не настроен."
35
47
  }
@@ -27,9 +27,21 @@
27
27
  "donatePaypal": "PayPal",
28
28
  "channelInfo": "Інформація про адаптер",
29
29
  "connectionStatus": "Стан з'єднання",
30
- "connectionStatusDesc": "Істина лише поки кожен налаштований емульований Roku справді слухає.",
30
+ "connectionStatusDesc": "Істина лише поки все працює: кожен налаштований емульований Roku слухає, виявлення відповідає і вибрана адреса існує.",
31
31
  "stateLastCommand": "Остання команда",
32
32
  "stateLastCommandDesc": "Найновіша команда, яку отримав цей емульований Roku, у вигляді тексту.",
33
33
  "channelKeys": "Кнопки пульта",
34
- "channelKeysDesc": "По одному стану на кожну кнопку, яку пропонує цей пристрій; натискання ненадовго переводить його в істину."
34
+ "channelKeysDesc": "По одному стану на кожну кнопку, яку пропонує цей пристрій; натискання ненадовго переводить його в істину.",
35
+ "channelDeviceInfo": "Стан",
36
+ "channelDeviceInfoDesc": "Чи працює цей емульований Roku і, якщо ні, чому.",
37
+ "stateOnline": "У мережі",
38
+ "stateOnlineDesc": "Істина, поки цей емульований Roku слухає свій порт ECP.",
39
+ "stateError": "Помилка",
40
+ "stateErrorDesc": "Чому цей емульований Roku не працює; порожньо, поки він працює, Unknown, поки адаптер вимкнений або запускається.",
41
+ "devicesTotal": "Емульовані Roku",
42
+ "devicesTotalDesc": "Скільки емульованих Roku налаштовано.",
43
+ "devicesOnline": "Емульовані Roku у мережі",
44
+ "devicesOnlineDesc": "Скільки емульованих Roku зараз слухають.",
45
+ "devicesAllOnline": "Усі емульовані Roku у мережі",
46
+ "devicesAllOnlineDesc": "Істина, поки слухає кожен налаштований емульований Roku; хибність, поки жоден не налаштований."
35
47
  }
@@ -27,9 +27,21 @@
27
27
  "donatePaypal": "PayPal",
28
28
  "channelInfo": "适配器信息",
29
29
  "connectionStatus": "连接状态",
30
- "connectionStatusDesc": "仅当每个已配置的模拟 Roku 都在监听时才为真。",
30
+ "connectionStatusDesc": "仅当一切正常运行时才为真:每个已配置的模拟 Roku 都在监听、发现服务有应答且所选地址存在。",
31
31
  "stateLastCommand": "最后一条命令",
32
32
  "stateLastCommandDesc": "该模拟 Roku 最近收到的命令,以纯文本表示。",
33
33
  "channelKeys": "遥控器按键",
34
- "channelKeysDesc": "该设备提供的每个遥控器按键各有一个状态;按下时会短暂置为真。"
34
+ "channelKeysDesc": "该设备提供的每个遥控器按键各有一个状态;按下时会短暂置为真。",
35
+ "channelDeviceInfo": "状态",
36
+ "channelDeviceInfoDesc": "此模拟 Roku 是否在运行,如未运行则说明原因。",
37
+ "stateOnline": "在线",
38
+ "stateOnlineDesc": "此模拟 Roku 在其 ECP 端口上监听时为真。",
39
+ "stateError": "错误",
40
+ "stateErrorDesc": "此模拟 Roku 未运行的原因;运行时为空,适配器关闭或启动时为 Unknown。",
41
+ "devicesTotal": "模拟 Roku 数量",
42
+ "devicesTotalDesc": "已配置的模拟 Roku 数量。",
43
+ "devicesOnline": "在线的模拟 Roku",
44
+ "devicesOnlineDesc": "当前正在监听的模拟 Roku 数量。",
45
+ "devicesAllOnline": "所有模拟 Roku 均在线",
46
+ "devicesAllOnlineDesc": "每个已配置的模拟 Roku 都在监听时为真;未配置任何 Roku 时为假。"
35
47
  }
@@ -187,15 +187,24 @@ class FakerokuDeviceManagement extends import_dm_utils.DeviceManagement {
187
187
  * The name needs no fallback: a row without a usable one never becomes a DeviceRow
188
188
  * (lib/device-config.ts drops it).
189
189
  *
190
+ * The status follows the Roku's own `info.online` and `info.error` live — the admin reads
191
+ * and subscribes the states itself, so a port that frees up turns the card green without a
192
+ * reload. An empty reason shows no warning.
193
+ *
190
194
  * @param device the stored device
191
195
  * @returns the card descriptor
192
196
  */
193
197
  toDeviceInfo(device) {
198
+ const info = `${this.adapter.namespace}.${device.objectId}.info`;
194
199
  return {
195
200
  id: device.identity,
196
201
  name: device.name,
197
202
  identifier: String(device.port),
198
203
  model: (0, import_i18n.t)(device.type === "tv" ? "deviceTypeTv" : "deviceTypePlayer"),
204
+ status: {
205
+ connection: { stateId: `${info}.online`, mapping: { true: "connected", false: "disconnected" } },
206
+ warning: { stateId: `${info}.error` }
207
+ },
199
208
  actions: [
200
209
  {
201
210
  id: "edit",
@@ -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 { instanceObjectId, RESERVED_IDS } from \"./lib/constants\";\nimport {\n deviceTreeOf,\n findClash,\n nextFreePort,\n normalizePort,\n normalizeType,\n toDeviceRows,\n type DeviceRow,\n} from \"./lib/device-config\";\nimport { randomIdentity } from \"./lib/device-identity\";\nimport { t } from \"./lib/i18n\";\nimport { sanitizeId, UNSAFE_ID_CHAR } 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 \u2014 built from the same pattern\n * as `sanitizeId`, because the dialog has to answer the same question as the runtime and cannot call into its code.\n */\nconst ID_EXPRESSION = `(data.name||'').trim().replace(/${UNSAFE_ID_CHAR.source}/g,'_')`;\n\n/** The add/edit form for one emulated Roku, and the rule that keeps its OK button off. */\nexport interface DeviceForm {\n /** The jsonConfig panel schema. */\n schema: JsonFormSchema;\n /**\n * The expression the admin evaluates to disable OK (dm-gui-components `getOkButton`: OK is\n * switched off ONLY by this rule or by an unchanged form \u2014 a field validator marks the field,\n * it never stops the button).\n */\n applyDisabledRule: string;\n}\n\n/**\n * The add/edit form for one emulated Roku: name, ECP port and device type.\n *\n * The name rules the backend knows \u2014 not empty, not taken, and for a NEW device not an id that\n * is reserved or that another device already occupies \u2014 and the port rule are expressions the\n * admin evaluates. They mark the field red (`validator`), switch OK off (the returned\n * `applyDisabledRule`) and show the reason as a line of text (`staticText` with `hidden`) \u2014 the\n * text fields show no validator message of their own. `findClash` still answers for anyone\n * bypassing the dialog. Labels are resolved translation objects so the embedded form is\n * language-correct.\n *\n * @param usedNames names taken by OTHER devices (the edited device is excluded)\n * @param usedPorts ports taken by OTHER devices\n * @param usedIds object ids the OTHER devices occupy \u2014 for a new device; `null` when editing,\n * since an existing device keeps its object id whatever it is renamed to\n * @returns the schema and the OK rule\n */\nexport function buildDeviceForm(\n usedNames: readonly string[],\n usedPorts: readonly number[],\n usedIds: readonly string[] | null,\n): DeviceForm {\n const nameList = JSON.stringify(usedNames.map(n => n.trim().toLowerCase()));\n const portList = JSON.stringify([...usedPorts]);\n const nameFilled = \"(data.name||'').trim().length>0\";\n const nameFree = `!${nameList}.includes((data.name||'').trim().toLowerCase())`;\n // Reserved ids first, then the object ids the other devices already occupy.\n const idFree =\n usedIds === null\n ? \"true\"\n : `${ID_EXPRESSION}.length>0 && !${JSON.stringify([...RESERVED_IDS, ...usedIds])}.includes(${ID_EXPRESSION})`;\n const nameOk = `(${nameFilled} && ${nameFree} && ${idFree})`;\n const portOk = `(!${portList}.includes(Number(data.port)))`;\n return {\n applyDisabledRule: `!(${nameOk} && ${portOk})`,\n schema: {\n type: \"panel\",\n items: {\n name: {\n type: \"text\",\n label: t(\"deviceName\"),\n validator: nameOk,\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: portOk,\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 _nameRejected: {\n type: \"staticText\",\n text: t(\"deviceNameRejected\"),\n hidden: `!${nameFilled} || ${nameOk}`,\n style: { color: \"#d32f2f\" },\n sm: 12,\n },\n _portRejected: {\n type: \"staticText\",\n text: t(\"devicePortInUse\"),\n hidden: portOk,\n style: { color: \"#d32f2f\" },\n sm: 12,\n },\n _portHint: { type: \"staticText\", text: t(\"devicePortHint\"), sm: 12 },\n _typeHint: { type: \"staticText\", text: t(\"deviceTypeHint\"), sm: 12 },\n },\n },\n };\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 and the object id are 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\" | \"objectId\"> {\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 instanceObjectId(this.adapter.namespace);\n }\n\n /**\n * Read the device list from the live config object, through the shared normaliser \u2014 against\n * the object tree the installation has, exactly like the runtime, so a row without a stored\n * object id or type resolves to the tree it already owns (lib/device-config.ts).\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.\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 const objects = await this.adapter.getAdapterObjectsAsync();\n const prefix = `${this.adapter.namespace}.`;\n const relative = new Map<string, string | undefined>();\n for (const [id, o] of Object.entries(objects)) {\n if (id.startsWith(prefix)) {\n relative.set(id.slice(prefix.length), o?.type);\n }\n }\n return (\n toDeviceRows(\n devices,\n deviceTreeOf(relative.keys(), id => relative.get(id)),\n ) ?? []\n );\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 and object id 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 \u2014 and the\n * same holds for the object id and for a type read from the tree.\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, objectId: row.objectId };\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).\n *\n * @param device the stored device\n * @returns the card descriptor\n */\n private toDeviceInfo(device: DeviceRow): DeviceInfo<string> {\n return {\n id: device.identity,\n name: device.name,\n identifier: String(device.port),\n model: t(device.type === \"tv\" ? \"deviceTypeTv\" : \"deviceTypePlayer\"),\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 its own\n * random identity and a fixed object id.\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(d => d.objectId);\n const form = buildDeviceForm(usedNames, usedPorts, usedIds);\n const data = await context.showForm(form.schema, {\n title: t(\"dmAdd\"),\n data: { type: \"player\", port: nextFreePort(usedPorts) },\n applyDisabledRule: form.applyDisabledRule,\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 // A new device has never been announced: it gets its own identity (not one derived from\n // its name, which every installation with the same name would share) and its object id\n // is fixed from now on.\n stored.push({ ...clean, uuid: randomIdentity(), objectId: sanitizeId(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 // The object id stays whatever the device is renamed to \u2014 no id rule for an edit.\n const form = buildDeviceForm(usedNames, usedPorts, null);\n const data = await context.showForm(form.schema, {\n title: t(\"dmEditTitle\"),\n data: { name: current.name, port: current.port, type: current.type },\n applyDisabledRule: form.applyDisabledRule,\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 // Same identity (the remote stays paired), same object id (the tree, its rooms and history\n // stay where they are) \u2014 a rename changes the name the card and the object show.\n stored[index] = { ...clean, uuid: current.identity, objectId: current.objectId };\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 { instanceObjectId, RESERVED_IDS } from \"./lib/constants\";\nimport {\n deviceTreeOf,\n findClash,\n nextFreePort,\n normalizePort,\n normalizeType,\n toDeviceRows,\n type DeviceRow,\n} from \"./lib/device-config\";\nimport { randomIdentity } from \"./lib/device-identity\";\nimport { t } from \"./lib/i18n\";\nimport { sanitizeId, UNSAFE_ID_CHAR } 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 \u2014 built from the same pattern\n * as `sanitizeId`, because the dialog has to answer the same question as the runtime and cannot call into its code.\n */\nconst ID_EXPRESSION = `(data.name||'').trim().replace(/${UNSAFE_ID_CHAR.source}/g,'_')`;\n\n/** The add/edit form for one emulated Roku, and the rule that keeps its OK button off. */\nexport interface DeviceForm {\n /** The jsonConfig panel schema. */\n schema: JsonFormSchema;\n /**\n * The expression the admin evaluates to disable OK (dm-gui-components `getOkButton`: OK is\n * switched off ONLY by this rule or by an unchanged form \u2014 a field validator marks the field,\n * it never stops the button).\n */\n applyDisabledRule: string;\n}\n\n/**\n * The add/edit form for one emulated Roku: name, ECP port and device type.\n *\n * The name rules the backend knows \u2014 not empty, not taken, and for a NEW device not an id that\n * is reserved or that another device already occupies \u2014 and the port rule are expressions the\n * admin evaluates. They mark the field red (`validator`), switch OK off (the returned\n * `applyDisabledRule`) and show the reason as a line of text (`staticText` with `hidden`) \u2014 the\n * text fields show no validator message of their own. `findClash` still answers for anyone\n * bypassing the dialog. Labels are resolved translation objects so the embedded form is\n * language-correct.\n *\n * @param usedNames names taken by OTHER devices (the edited device is excluded)\n * @param usedPorts ports taken by OTHER devices\n * @param usedIds object ids the OTHER devices occupy \u2014 for a new device; `null` when editing,\n * since an existing device keeps its object id whatever it is renamed to\n * @returns the schema and the OK rule\n */\nexport function buildDeviceForm(\n usedNames: readonly string[],\n usedPorts: readonly number[],\n usedIds: readonly string[] | null,\n): DeviceForm {\n const nameList = JSON.stringify(usedNames.map(n => n.trim().toLowerCase()));\n const portList = JSON.stringify([...usedPorts]);\n const nameFilled = \"(data.name||'').trim().length>0\";\n const nameFree = `!${nameList}.includes((data.name||'').trim().toLowerCase())`;\n // Reserved ids first, then the object ids the other devices already occupy.\n const idFree =\n usedIds === null\n ? \"true\"\n : `${ID_EXPRESSION}.length>0 && !${JSON.stringify([...RESERVED_IDS, ...usedIds])}.includes(${ID_EXPRESSION})`;\n const nameOk = `(${nameFilled} && ${nameFree} && ${idFree})`;\n const portOk = `(!${portList}.includes(Number(data.port)))`;\n return {\n applyDisabledRule: `!(${nameOk} && ${portOk})`,\n schema: {\n type: \"panel\",\n items: {\n name: {\n type: \"text\",\n label: t(\"deviceName\"),\n validator: nameOk,\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: portOk,\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 _nameRejected: {\n type: \"staticText\",\n text: t(\"deviceNameRejected\"),\n hidden: `!${nameFilled} || ${nameOk}`,\n style: { color: \"#d32f2f\" },\n sm: 12,\n },\n _portRejected: {\n type: \"staticText\",\n text: t(\"devicePortInUse\"),\n hidden: portOk,\n style: { color: \"#d32f2f\" },\n sm: 12,\n },\n _portHint: { type: \"staticText\", text: t(\"devicePortHint\"), sm: 12 },\n _typeHint: { type: \"staticText\", text: t(\"deviceTypeHint\"), sm: 12 },\n },\n },\n };\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 and the object id are 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\" | \"objectId\"> {\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 instanceObjectId(this.adapter.namespace);\n }\n\n /**\n * Read the device list from the live config object, through the shared normaliser \u2014 against\n * the object tree the installation has, exactly like the runtime, so a row without a stored\n * object id or type resolves to the tree it already owns (lib/device-config.ts).\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.\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 const objects = await this.adapter.getAdapterObjectsAsync();\n const prefix = `${this.adapter.namespace}.`;\n const relative = new Map<string, string | undefined>();\n for (const [id, o] of Object.entries(objects)) {\n if (id.startsWith(prefix)) {\n relative.set(id.slice(prefix.length), o?.type);\n }\n }\n return (\n toDeviceRows(\n devices,\n deviceTreeOf(relative.keys(), id => relative.get(id)),\n ) ?? []\n );\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 and object id 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 \u2014 and the\n * same holds for the object id and for a type read from the tree.\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, objectId: row.objectId };\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).\n *\n * The status follows the Roku's own `info.online` and `info.error` live \u2014 the admin reads\n * and subscribes the states itself, so a port that frees up turns the card green without a\n * reload. An empty reason shows no warning.\n *\n * @param device the stored device\n * @returns the card descriptor\n */\n private toDeviceInfo(device: DeviceRow): DeviceInfo<string> {\n const info = `${this.adapter.namespace}.${device.objectId}.info`;\n return {\n id: device.identity,\n name: device.name,\n identifier: String(device.port),\n model: t(device.type === \"tv\" ? \"deviceTypeTv\" : \"deviceTypePlayer\"),\n status: {\n connection: { stateId: `${info}.online`, mapping: { true: \"connected\", false: \"disconnected\" } },\n warning: { stateId: `${info}.error` },\n },\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 its own\n * random identity and a fixed object id.\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(d => d.objectId);\n const form = buildDeviceForm(usedNames, usedPorts, usedIds);\n const data = await context.showForm(form.schema, {\n title: t(\"dmAdd\"),\n data: { type: \"player\", port: nextFreePort(usedPorts) },\n applyDisabledRule: form.applyDisabledRule,\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 // A new device has never been announced: it gets its own identity (not one derived from\n // its name, which every installation with the same name would share) and its object id\n // is fixed from now on.\n stored.push({ ...clean, uuid: randomIdentity(), objectId: sanitizeId(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 // The object id stays whatever the device is renamed to \u2014 no id rule for an edit.\n const form = buildDeviceForm(usedNames, usedPorts, null);\n const data = await context.showForm(form.schema, {\n title: t(\"dmEditTitle\"),\n data: { name: current.name, port: current.port, type: current.type },\n applyDisabledRule: form.applyDisabledRule,\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 // Same identity (the remote stays paired), same object id (the tree, its rooms and history\n // stay where they are) \u2014 a rename changes the name the card and the object show.\n stored[index] = { ...clean, uuid: current.identity, objectId: current.objectId };\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
  }
@@ -30,7 +30,13 @@ const RESERVED_IDS = /* @__PURE__ */ new Set(["info"]);
30
30
  function instanceObjectId(namespace) {
31
31
  return `system.adapter.${namespace}`;
32
32
  }
33
- const OWN_INFO_IDS = /* @__PURE__ */ new Set(["info", "info.connection"]);
33
+ const OWN_INFO_IDS = /* @__PURE__ */ new Set([
34
+ "info",
35
+ "info.connection",
36
+ "info.devicesTotal",
37
+ "info.devicesOnline",
38
+ "info.devicesAllOnline"
39
+ ]);
34
40
  const LANGUAGES = ["en", "de", "ru", "pt", "nl", "fr", "it", "es", "pl", "uk", "zh-cn"];
35
41
  // Annotate the CommonJS export names for ESM import in node:
36
42
  0 && (module.exports = {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/constants.ts"],
4
- "sourcesContent": ["/**\n * Shared constants for the Roku emulator \u2014 kept in one place so the runtime\n * (main.ts) and the admin device-manager (device-management.ts) cannot drift apart.\n */\n\n/**\n * The real-Roku ECP port and this adapter's default. Harmony and Sofabaton read the port\n * from the SSDP advertisement; Home Assistant (rokuecp) and Homey always use 8060, so\n * 8060 belongs to the Roku they should control.\n */\nexport const DEFAULT_ECP_PORT = 8060;\n\n/**\n * Object-id segments the adapter reserves for its own tree \u2014 an emulated Roku may\n * not take one. `info` carries `info.connection`, the instance's own status; a\n * device of that name would turn the adapter's channel into a device object and\n * hang its command/keys states underneath it.\n *\n * Enforced in BOTH directions: the device manager refuses the name in the dialog,\n * and the runtime skips such a row, because native.devices is user-editable\n * (expert mode, CLI) and can carry what the dialog never allowed.\n */\nexport const RESERVED_IDS: ReadonlySet<string> = new Set([\"info\"]);\n\n/**\n * The id of this instance's own object \u2014 where `native.devices` and the settings live.\n *\n * @param namespace the adapter namespace, e.g. `fakeroku.0`\n * @returns `system.adapter.<namespace>`\n */\nexport function instanceObjectId(namespace: string): string {\n return `system.adapter.${namespace}`;\n}\n\n/** The adapter's own objects below `info` \u2014 never device leftovers, never swept. */\nexport const OWN_INFO_IDS: ReadonlySet<string> = new Set([\"info\", \"info.connection\"]);\n\n/** The eleven languages every ioBroker manifest and admin translation carries. */\nexport const LANGUAGES = [\"en\", \"de\", \"ru\", \"pt\", \"nl\", \"fr\", \"it\", \"es\", \"pl\", \"uk\", \"zh-cn\"] as const;\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUO,MAAM,mBAAmB;AAYzB,MAAM,eAAoC,oBAAI,IAAI,CAAC,MAAM,CAAC;AAQ1D,SAAS,iBAAiB,WAA2B;AAC1D,SAAO,kBAAkB,SAAS;AACpC;AAGO,MAAM,eAAoC,oBAAI,IAAI,CAAC,QAAQ,iBAAiB,CAAC;AAG7E,MAAM,YAAY,CAAC,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,OAAO;",
4
+ "sourcesContent": ["/**\n * Shared constants for the Roku emulator \u2014 kept in one place so the runtime\n * (main.ts) and the admin device-manager (device-management.ts) cannot drift apart.\n */\n\n/**\n * The real-Roku ECP port and this adapter's default. Harmony and Sofabaton read the port\n * from the SSDP advertisement; Home Assistant (rokuecp) and Homey always use 8060, so\n * 8060 belongs to the Roku they should control.\n */\nexport const DEFAULT_ECP_PORT = 8060;\n\n/**\n * Object-id segments the adapter reserves for its own tree \u2014 an emulated Roku may\n * not take one. `info` carries `info.connection`, the instance's own status; a\n * device of that name would turn the adapter's channel into a device object and\n * hang its command/keys states underneath it.\n *\n * Enforced in BOTH directions: the device manager refuses the name in the dialog,\n * and the runtime skips such a row, because native.devices is user-editable\n * (expert mode, CLI) and can carry what the dialog never allowed.\n */\nexport const RESERVED_IDS: ReadonlySet<string> = new Set([\"info\"]);\n\n/**\n * The id of this instance's own object \u2014 where `native.devices` and the settings live.\n *\n * @param namespace the adapter namespace, e.g. `fakeroku.0`\n * @returns `system.adapter.<namespace>`\n */\nexport function instanceObjectId(namespace: string): string {\n return `system.adapter.${namespace}`;\n}\n\n/** The adapter's own objects below `info` \u2014 never device leftovers, never swept. */\nexport const OWN_INFO_IDS: ReadonlySet<string> = new Set([\n \"info\",\n \"info.connection\",\n \"info.devicesTotal\",\n \"info.devicesOnline\",\n \"info.devicesAllOnline\",\n]);\n\n/** The eleven languages every ioBroker manifest and admin translation carries. */\nexport const LANGUAGES = [\"en\", \"de\", \"ru\", \"pt\", \"nl\", \"fr\", \"it\", \"es\", \"pl\", \"uk\", \"zh-cn\"] as const;\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUO,MAAM,mBAAmB;AAYzB,MAAM,eAAoC,oBAAI,IAAI,CAAC,MAAM,CAAC;AAQ1D,SAAS,iBAAiB,WAA2B;AAC1D,SAAO,kBAAkB,SAAS;AACpC;AAGO,MAAM,eAAoC,oBAAI,IAAI;AAAA,EACvD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGM,MAAM,YAAY,CAAC,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,OAAO;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/object-cleanup.ts"],
4
- "sourcesContent": ["import { OWN_INFO_IDS } from \"./constants\";\n\n/**\n * Direct children of a device that an earlier version created and this one does not: the old adapter's `apps` node\n * and `commandType` (1.3.0\u20131.8.2, the kind of the last command; `command` carries the command itself).\n */\nconst RETIRED_DEVICE_CHILDREN: ReadonlySet<string> = new Set([\"apps\", \"commandType\"]);\n\n/**\n * Decide which existing adapter objects are orphaned after the current device\n * tree has been (re)created. Pure \u2014 the caller performs the actual deletes \u2014 so\n * the tricky \"what is stale\" logic is unit-testable without a live adapter.\n *\n * Three kinds of orphan are collected:\n * - a whole device sub-tree whose device is no longer configured (rename/removal),\n * - a direct child of a device an earlier version created and this one does not ({@link RETIRED_DEVICE_CHILDREN}),\n * - a `<device>.keys.<Key>` state whose key is no longer part of the device's\n * type (e.g. the TV keys after switching a device from \"tv\" back to \"player\").\n *\n * The adapter's own objects \u2014 `info` and `info.connection` \u2014 are never touched.\n * Anything ELSE below `info` is: a hand-edited device row called \"info\" used to\n * create `info.command` / `info.keys.*` there, and blanket-skipping the whole\n * subtree meant those leftovers stayed for good, even after the row was removed.\n *\n * @param existingIds adapter object ids relative to the namespace (e.g. \"ioBroker.keys.Home\")\n * @param configuredDeviceIds the id-safe names of the currently configured devices\n * @param validKeysByDevice per configured device, the key names its type currently exposes\n * @returns the relative ids to delete (recursively); de-duplicated\n */\nexport function planObjectCleanup(\n existingIds: readonly string[],\n configuredDeviceIds: ReadonlySet<string>,\n validKeysByDevice: ReadonlyMap<string, ReadonlySet<string>>,\n): string[] {\n const del = new Set<string>();\n for (const id of existingIds) {\n const parts = id.split(\".\");\n const device = parts[0];\n if (OWN_INFO_IDS.has(id)) {\n continue; // the adapter's own status objects, not a device\n }\n if (device === \"info\") {\n // Leftover from a hand-edited device row named \"info\". Collect the topmost\n // foreign node (`info.command`, `info.keys`) \u2014 the recursive delete takes its\n // children, so listing those separately would only produce failing deletes.\n const child = `info.${parts[1]}`;\n if (!OWN_INFO_IDS.has(child)) {\n del.add(child);\n }\n continue;\n }\n if (!configuredDeviceIds.has(device)) {\n del.add(device); // whole orphaned device tree \u2014 recursive delete drops the children\n continue;\n }\n if (parts.length === 2 && RETIRED_DEVICE_CHILDREN.has(parts[1])) {\n del.add(id); // created by an earlier version, written by nobody now\n } else if (parts[1] === \"keys\" && parts.length === 3) {\n const valid = validKeysByDevice.get(device);\n if (valid && !valid.has(parts[2])) {\n del.add(`${device}.keys.${parts[2]}`); // key no longer part of this device's type\n }\n }\n }\n return [...del];\n}\n\n/**\n * Find the objects that still carry a `native` attribute this adapter does not write.\n *\n * Every object this adapter creates declares `native: {}`. The pre-0.5.0 adapter wrote\n * `native: { url: \"keys/Home\" }` on every key state, and `extendObject` merges \u2014 an\n * attribute only the stored object carries survives forever, and writing `null` would store\n * `null` rather than remove it. So an installation upgraded from <= 0.4.0 keeps a dead\n * attribute on every key datapoint until something writes the object in full.\n *\n * Pure: the caller performs the writes, and it decides nothing about `common` \u2014 the object\n * is handed back as it was read so the full write can preserve it (that is where\n * `common.custom`, the user's own history configuration, lives).\n *\n * @param objects the adapter's objects, keyed relative to the namespace\n * @param deleted ids the orphan sweep is removing \u2014 writing to those would be pointless\n * @returns the ids and objects to rewrite without their stale native attributes\n */\nexport function planNativePrune(\n objects: ReadonlyMap<string, ioBroker.Object>,\n deleted: ReadonlySet<string> = new Set(),\n): [string, ioBroker.Object][] {\n const stale: [string, ioBroker.Object][] = [];\n for (const [id, obj] of objects) {\n if (deleted.has(id) || [...deleted].some(prefix => id.startsWith(`${prefix}.`))) {\n continue;\n }\n if (obj.native && typeof obj.native === \"object\" && Object.keys(obj.native).length > 0) {\n stale.push([id, obj]);\n }\n }\n return stale;\n}\n"],
4
+ "sourcesContent": ["import { OWN_INFO_IDS } from \"./constants\";\n\n/**\n * Direct children of a device that an earlier version created and this one does not: the old adapter's `apps` node\n * and `commandType` (1.3.0\u20131.8.2, the kind of the last command; `command` carries the command itself).\n */\nconst RETIRED_DEVICE_CHILDREN: ReadonlySet<string> = new Set([\"apps\", \"commandType\"]);\n\n/**\n * Decide which existing adapter objects are orphaned after the current device\n * tree has been (re)created. Pure \u2014 the caller performs the actual deletes \u2014 so\n * the tricky \"what is stale\" logic is unit-testable without a live adapter.\n *\n * Three kinds of orphan are collected:\n * - a whole device sub-tree whose device is no longer configured (rename/removal),\n * - a direct child of a device an earlier version created and this one does not ({@link RETIRED_DEVICE_CHILDREN}),\n * - a `<device>.keys.<Key>` state whose key is no longer part of the device's\n * type (e.g. the TV keys after switching a device from \"tv\" back to \"player\").\n *\n * The adapter's own objects \u2014 `info`, `info.connection` and the three device counts \u2014 are never touched.\n * Anything ELSE below `info` is: a hand-edited device row called \"info\" used to\n * create `info.command` / `info.keys.*` there, and blanket-skipping the whole\n * subtree meant those leftovers stayed for good, even after the row was removed.\n *\n * @param existingIds adapter object ids relative to the namespace (e.g. \"ioBroker.keys.Home\")\n * @param configuredDeviceIds the id-safe names of the currently configured devices\n * @param validKeysByDevice per configured device, the key names its type currently exposes\n * @returns the relative ids to delete (recursively); de-duplicated\n */\nexport function planObjectCleanup(\n existingIds: readonly string[],\n configuredDeviceIds: ReadonlySet<string>,\n validKeysByDevice: ReadonlyMap<string, ReadonlySet<string>>,\n): string[] {\n const del = new Set<string>();\n for (const id of existingIds) {\n const parts = id.split(\".\");\n const device = parts[0];\n if (OWN_INFO_IDS.has(id)) {\n continue; // the adapter's own status objects, not a device\n }\n if (device === \"info\") {\n // Leftover from a hand-edited device row named \"info\". Collect the topmost\n // foreign node (`info.command`, `info.keys`) \u2014 the recursive delete takes its\n // children, so listing those separately would only produce failing deletes.\n const child = `info.${parts[1]}`;\n if (!OWN_INFO_IDS.has(child)) {\n del.add(child);\n }\n continue;\n }\n if (!configuredDeviceIds.has(device)) {\n del.add(device); // whole orphaned device tree \u2014 recursive delete drops the children\n continue;\n }\n if (parts.length === 2 && RETIRED_DEVICE_CHILDREN.has(parts[1])) {\n del.add(id); // created by an earlier version, written by nobody now\n } else if (parts[1] === \"keys\" && parts.length === 3) {\n const valid = validKeysByDevice.get(device);\n if (valid && !valid.has(parts[2])) {\n del.add(`${device}.keys.${parts[2]}`); // key no longer part of this device's type\n }\n }\n }\n return [...del];\n}\n\n/**\n * Find the objects that still carry a `native` attribute this adapter does not write.\n *\n * Every object this adapter creates declares `native: {}`. The pre-0.5.0 adapter wrote\n * `native: { url: \"keys/Home\" }` on every key state, and `extendObject` merges \u2014 an\n * attribute only the stored object carries survives forever, and writing `null` would store\n * `null` rather than remove it. So an installation upgraded from <= 0.4.0 keeps a dead\n * attribute on every key datapoint until something writes the object in full.\n *\n * Pure: the caller performs the writes, and it decides nothing about `common` \u2014 the object\n * is handed back as it was read so the full write can preserve it (that is where\n * `common.custom`, the user's own history configuration, lives).\n *\n * @param objects the adapter's objects, keyed relative to the namespace\n * @param deleted ids the orphan sweep is removing \u2014 writing to those would be pointless\n * @returns the ids and objects to rewrite without their stale native attributes\n */\nexport function planNativePrune(\n objects: ReadonlyMap<string, ioBroker.Object>,\n deleted: ReadonlySet<string> = new Set(),\n): [string, ioBroker.Object][] {\n const stale: [string, ioBroker.Object][] = [];\n for (const [id, obj] of objects) {\n if (deleted.has(id) || [...deleted].some(prefix => id.startsWith(`${prefix}.`))) {\n continue;\n }\n if (obj.native && typeof obj.native === \"object\" && Object.keys(obj.native).length > 0) {\n stale.push([id, obj]);\n }\n }\n return stale;\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAA6B;AAM7B,MAAM,0BAA+C,oBAAI,IAAI,CAAC,QAAQ,aAAa,CAAC;AAuB7E,SAAS,kBACd,aACA,qBACA,mBACU;AACV,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,MAAM,aAAa;AAC5B,UAAM,QAAQ,GAAG,MAAM,GAAG;AAC1B,UAAM,SAAS,MAAM,CAAC;AACtB,QAAI,8BAAa,IAAI,EAAE,GAAG;AACxB;AAAA,IACF;AACA,QAAI,WAAW,QAAQ;AAIrB,YAAM,QAAQ,QAAQ,MAAM,CAAC,CAAC;AAC9B,UAAI,CAAC,8BAAa,IAAI,KAAK,GAAG;AAC5B,YAAI,IAAI,KAAK;AAAA,MACf;AACA;AAAA,IACF;AACA,QAAI,CAAC,oBAAoB,IAAI,MAAM,GAAG;AACpC,UAAI,IAAI,MAAM;AACd;AAAA,IACF;AACA,QAAI,MAAM,WAAW,KAAK,wBAAwB,IAAI,MAAM,CAAC,CAAC,GAAG;AAC/D,UAAI,IAAI,EAAE;AAAA,IACZ,WAAW,MAAM,CAAC,MAAM,UAAU,MAAM,WAAW,GAAG;AACpD,YAAM,QAAQ,kBAAkB,IAAI,MAAM;AAC1C,UAAI,SAAS,CAAC,MAAM,IAAI,MAAM,CAAC,CAAC,GAAG;AACjC,YAAI,IAAI,GAAG,MAAM,SAAS,MAAM,CAAC,CAAC,EAAE;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AACA,SAAO,CAAC,GAAG,GAAG;AAChB;AAmBO,SAAS,gBACd,SACA,UAA+B,oBAAI,IAAI,GACV;AAC7B,QAAM,QAAqC,CAAC;AAC5C,aAAW,CAAC,IAAI,GAAG,KAAK,SAAS;AAC/B,QAAI,QAAQ,IAAI,EAAE,KAAK,CAAC,GAAG,OAAO,EAAE,KAAK,YAAU,GAAG,WAAW,GAAG,MAAM,GAAG,CAAC,GAAG;AAC/E;AAAA,IACF;AACA,QAAI,IAAI,UAAU,OAAO,IAAI,WAAW,YAAY,OAAO,KAAK,IAAI,MAAM,EAAE,SAAS,GAAG;AACtF,YAAM,KAAK,CAAC,IAAI,GAAG,CAAC;AAAA,IACtB;AAAA,EACF;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }