iobroker.yamaha 2.1.0 → 2.2.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 +18 -11
- package/admin/i18n/de.json +3 -0
- package/admin/i18n/en.json +3 -0
- package/admin/i18n/es.json +3 -0
- package/admin/i18n/fr.json +3 -0
- package/admin/i18n/it.json +3 -0
- package/admin/i18n/nl.json +3 -0
- package/admin/i18n/pl.json +3 -0
- package/admin/i18n/pt.json +3 -0
- package/admin/i18n/ru.json +3 -0
- package/admin/i18n/uk.json +3 -0
- package/admin/i18n/zh-cn.json +3 -0
- package/build/lib/browse/xml-browse-driver.js +16 -3
- package/build/lib/browse/xml-browse-driver.js.map +2 -2
- package/build/lib/browse/ynca-browse-driver.js +10 -1
- package/build/lib/browse/ynca-browse-driver.js.map +2 -2
- package/build/lib/catalog/build-objects.js +6 -1
- package/build/lib/catalog/build-objects.js.map +2 -2
- package/build/lib/catalog/owner-policy.js +2 -23
- package/build/lib/catalog/owner-policy.js.map +2 -2
- package/build/lib/catalog/play-time.js +72 -0
- package/build/lib/catalog/play-time.js.map +7 -0
- package/build/lib/catalog/scene-titles.js +2 -11
- package/build/lib/catalog/scene-titles.js.map +2 -2
- package/build/lib/catalog/types.js.map +2 -2
- package/build/lib/catalog/value-coerce.js +2 -1
- package/build/lib/catalog/value-coerce.js.map +2 -2
- package/build/lib/device-controller.js +82 -15
- package/build/lib/device-controller.js.map +2 -2
- package/build/lib/lifecycle/poll-drop-detector.js +13 -2
- package/build/lib/lifecycle/poll-drop-detector.js.map +2 -2
- package/build/lib/lifecycle/probe-memory.js +11 -1
- package/build/lib/lifecycle/probe-memory.js.map +2 -2
- package/build/lib/xml/device-controller.js +24 -12
- package/build/lib/xml/device-controller.js.map +2 -2
- package/build/lib/xml/xml-client.js +11 -4
- package/build/lib/xml/xml-client.js.map +2 -2
- package/build/lib/ynca/catalog.js +52 -8
- package/build/lib/ynca/catalog.js.map +2 -2
- package/build/lib/ynca/ynca-client.js +1 -0
- package/build/lib/ynca/ynca-client.js.map +2 -2
- package/build/lib/yxc/command-mapper.js +5 -0
- package/build/lib/yxc/command-mapper.js.map +2 -2
- package/build/lib/yxc/device-controller.js +26 -16
- package/build/lib/yxc/device-controller.js.map +2 -2
- package/build/lib/yxc/http-client.js +5 -2
- package/build/lib/yxc/http-client.js.map +2 -2
- package/build/lib/yxc/object-mapper.js +18 -0
- package/build/lib/yxc/object-mapper.js.map +2 -2
- package/build/main.js +58 -7
- package/build/main.js.map +2 -2
- package/io-package.json +75 -27
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -103,6 +103,24 @@ On the very first contact the adapter asks the receiver which functions it suppo
|
|
|
103
103
|
Placeholder for the next version (at the beginning of the line):
|
|
104
104
|
### **WORK IN PROGRESS**
|
|
105
105
|
-->
|
|
106
|
+
### 2.2.0 (2026-09-03)
|
|
107
|
+
|
|
108
|
+
- (krobipd) Changed: the playback times are now a number in seconds, so the media player, Alexa and Google can show them on every receiver
|
|
109
|
+
- (krobipd) New: the readable playback times ("1:23") moved to their own datapoints, next to the seconds — bind whichever form your visualization needs
|
|
110
|
+
- (krobipd) Fixed: after a restart the adapter asks the receiver again instead of trusting what it remembered, so the menu, the tuner band and each zone's source are the current ones
|
|
111
|
+
- (krobipd) Fixed: setting a tuner frequency on a MusicCast device never reached it — the command was incomplete
|
|
112
|
+
- (krobipd) Fixed: commands to a receiver from before 2010 are now sent the way every comparable program sends them; older models can reject the previous form outright
|
|
113
|
+
- (krobipd) Fixed: a scene you renamed at the receiver now appears while the adapter runs, and recalling a scene by an unknown name says so in the log instead of doing nothing
|
|
114
|
+
- (krobipd) Fixed: after a restart the tuner of an older receiver no longer shows the station of the previous session until the next poll
|
|
115
|
+
- (krobipd) Fixed: an incomplete answer from a MusicCast device is no longer kept as that device's capabilities, and a device that answers nothing is no longer shown as connected
|
|
116
|
+
- (krobipd) Fixed: going one menu level back on an older receiver no longer jumps two levels when the reply gets lost
|
|
117
|
+
- (krobipd) Fixed: a play or pause sent from a script or a visualization now reaches the receiver in every case
|
|
118
|
+
- (krobipd) Improved: each of the 23 input-name datapoints now carries the input it names, instead of all reading the same
|
|
119
|
+
|
|
120
|
+
### 2.1.1 (2026-09-02)
|
|
121
|
+
|
|
122
|
+
- (krobipd) Fixed: on an updated instance the info datapoints now get the translated names too, instead of keeping the ones an older version had written
|
|
123
|
+
|
|
106
124
|
### 2.1.0 (2026-09-02)
|
|
107
125
|
|
|
108
126
|
- (krobipd) New: every datapoint name is now shown in your ioBroker language, in eleven languages; a name you changed yourself stays untouched
|
|
@@ -128,17 +146,6 @@ On the very first contact the adapter asks the receiver which functions it suppo
|
|
|
128
146
|
|
|
129
147
|
- (krobipd) Fixed: the update cleanup now removes every never-filled leftover datapoint — a history recording setting no longer shields it, because nothing was ever recorded there and nothing is lost
|
|
130
148
|
|
|
131
|
-
### 2.0.2 (2026-09-01)
|
|
132
|
-
|
|
133
|
-
- (krobipd) Fixed: a restart while the receiver stands by no longer forgets abilities the device proved while awake — the remembered capability map only ever grows for the same device and firmware
|
|
134
|
-
|
|
135
|
-
### 2.0.1 (2026-09-01)
|
|
136
|
-
|
|
137
|
-
- (krobipd) Fixed: devices on the oldest protocol no longer get datapoints their status never delivers — states are only created for what the device really answers, like on the other two protocols
|
|
138
|
-
- (krobipd) Fixed: leftover datapoints that never carried a value — zone sound settings or a second HDMI output the receiver does not have — are cleaned away once when updating the adapter
|
|
139
|
-
- (krobipd) Fixed: the player block of a zone that is not playing a media source starts with cleared values (empty texts, playback on stop) instead of staying without any value until the first playback
|
|
140
|
-
- (krobipd) Fixed: the two DAB station-scan datapoints (stations found, scan progress) start at zero instead of staying without a value until the first station scan
|
|
141
|
-
|
|
142
149
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
143
150
|
|
|
144
151
|
## History
|
package/admin/i18n/de.json
CHANGED
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"Disc time": "Disc-Zeit",
|
|
99
99
|
"Drive status": "Laufwerkszustand",
|
|
100
100
|
"Elapsed time": "Gespielte Zeit",
|
|
101
|
+
"Elapsed time (readable)": "Abgelaufene Zeit (lesbar)",
|
|
101
102
|
"Enhancer": "Enhancer",
|
|
102
103
|
"Ensemble label": "Ensemble-Name",
|
|
103
104
|
"Equalizer": "Equalizer",
|
|
@@ -128,6 +129,7 @@
|
|
|
128
129
|
"Initial volume mode": "Einschalt-Lautstärkemodus",
|
|
129
130
|
"Input": "Eingang",
|
|
130
131
|
"Input name (display)": "Eingangsname (Anzeige)",
|
|
132
|
+
"Input name (%s)": "Eingangsname (%s)",
|
|
131
133
|
"Input names": "Eingangsnamen",
|
|
132
134
|
"Leave group": "Gruppe verlassen",
|
|
133
135
|
"Link a device (its IP)": "Gerät verbinden (dessen IP)",
|
|
@@ -231,6 +233,7 @@
|
|
|
231
233
|
"Total entries": "Einträge gesamt",
|
|
232
234
|
"Total stations": "Gefundene Sender",
|
|
233
235
|
"Total time": "Gesamtzeit",
|
|
236
|
+
"Total time (readable)": "Gesamtzeit (lesbar)",
|
|
234
237
|
"Total tracks": "Titel gesamt",
|
|
235
238
|
"Track": "Titel",
|
|
236
239
|
"Track number": "Titelnummer",
|
package/admin/i18n/en.json
CHANGED
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"Disc time": "Disc time",
|
|
99
99
|
"Drive status": "Drive status",
|
|
100
100
|
"Elapsed time": "Elapsed time",
|
|
101
|
+
"Elapsed time (readable)": "Elapsed time (readable)",
|
|
101
102
|
"Enhancer": "Enhancer",
|
|
102
103
|
"Ensemble label": "Ensemble label",
|
|
103
104
|
"Equalizer": "Equalizer",
|
|
@@ -128,6 +129,7 @@
|
|
|
128
129
|
"Initial volume mode": "Initial volume mode",
|
|
129
130
|
"Input": "Input",
|
|
130
131
|
"Input name (display)": "Input name (display)",
|
|
132
|
+
"Input name (%s)": "Input name (%s)",
|
|
131
133
|
"Input names": "Input names",
|
|
132
134
|
"Leave group": "Leave group",
|
|
133
135
|
"Link a device (its IP)": "Link a device (its IP)",
|
|
@@ -231,6 +233,7 @@
|
|
|
231
233
|
"Total entries": "Total entries",
|
|
232
234
|
"Total stations": "Total stations",
|
|
233
235
|
"Total time": "Total time",
|
|
236
|
+
"Total time (readable)": "Total time (readable)",
|
|
234
237
|
"Total tracks": "Total tracks",
|
|
235
238
|
"Track": "Track",
|
|
236
239
|
"Track number": "Track number",
|
package/admin/i18n/es.json
CHANGED
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"Disc time": "Tiempo del disco",
|
|
99
99
|
"Drive status": "Estado de la unidad",
|
|
100
100
|
"Elapsed time": "Tiempo transcurrido",
|
|
101
|
+
"Elapsed time (readable)": "Tiempo transcurrido (legible)",
|
|
101
102
|
"Enhancer": "Enhancer",
|
|
102
103
|
"Ensemble label": "Nombre del conjunto",
|
|
103
104
|
"Equalizer": "Ecualizador",
|
|
@@ -128,6 +129,7 @@
|
|
|
128
129
|
"Initial volume mode": "Modo del volumen inicial",
|
|
129
130
|
"Input": "Entrada",
|
|
130
131
|
"Input name (display)": "Nombre de la entrada (pantalla)",
|
|
132
|
+
"Input name (%s)": "Nombre de la entrada (%s)",
|
|
131
133
|
"Input names": "Nombres de las entradas",
|
|
132
134
|
"Leave group": "Salir del grupo",
|
|
133
135
|
"Link a device (its IP)": "Vincular un dispositivo (su IP)",
|
|
@@ -231,6 +233,7 @@
|
|
|
231
233
|
"Total entries": "Total de entradas",
|
|
232
234
|
"Total stations": "Emisoras encontradas",
|
|
233
235
|
"Total time": "Duración total",
|
|
236
|
+
"Total time (readable)": "Tiempo total (legible)",
|
|
234
237
|
"Total tracks": "Pistas totales",
|
|
235
238
|
"Track": "Pista",
|
|
236
239
|
"Track number": "Número de pista",
|
package/admin/i18n/fr.json
CHANGED
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"Disc time": "Durée du disque",
|
|
99
99
|
"Drive status": "État du lecteur",
|
|
100
100
|
"Elapsed time": "Temps écoulé",
|
|
101
|
+
"Elapsed time (readable)": "Temps écoulé (lisible)",
|
|
101
102
|
"Enhancer": "Enhancer",
|
|
102
103
|
"Ensemble label": "Nom de l'ensemble",
|
|
103
104
|
"Equalizer": "Égaliseur",
|
|
@@ -128,6 +129,7 @@
|
|
|
128
129
|
"Initial volume mode": "Mode du volume initial",
|
|
129
130
|
"Input": "Entrée",
|
|
130
131
|
"Input name (display)": "Nom de l'entrée (afficheur)",
|
|
132
|
+
"Input name (%s)": "Nom de l'entrée (%s)",
|
|
131
133
|
"Input names": "Noms des entrées",
|
|
132
134
|
"Leave group": "Quitter le groupe",
|
|
133
135
|
"Link a device (its IP)": "Lier un appareil (son IP)",
|
|
@@ -231,6 +233,7 @@
|
|
|
231
233
|
"Total entries": "Nombre d'entrées",
|
|
232
234
|
"Total stations": "Stations trouvées",
|
|
233
235
|
"Total time": "Durée totale",
|
|
236
|
+
"Total time (readable)": "Durée totale (lisible)",
|
|
234
237
|
"Total tracks": "Nombre de pistes",
|
|
235
238
|
"Track": "Piste",
|
|
236
239
|
"Track number": "Numéro de piste",
|
package/admin/i18n/it.json
CHANGED
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"Disc time": "Durata del disco",
|
|
99
99
|
"Drive status": "Stato dell'unità",
|
|
100
100
|
"Elapsed time": "Tempo trascorso",
|
|
101
|
+
"Elapsed time (readable)": "Tempo trascorso (leggibile)",
|
|
101
102
|
"Enhancer": "Enhancer",
|
|
102
103
|
"Ensemble label": "Nome dell'ensemble",
|
|
103
104
|
"Equalizer": "Equalizzatore",
|
|
@@ -128,6 +129,7 @@
|
|
|
128
129
|
"Initial volume mode": "Modalità volume iniziale",
|
|
129
130
|
"Input": "Ingresso",
|
|
130
131
|
"Input name (display)": "Nome ingresso (display)",
|
|
132
|
+
"Input name (%s)": "Nome dell'ingresso (%s)",
|
|
131
133
|
"Input names": "Nomi ingressi",
|
|
132
134
|
"Leave group": "Esci dal gruppo",
|
|
133
135
|
"Link a device (its IP)": "Collega un dispositivo (il suo IP)",
|
|
@@ -231,6 +233,7 @@
|
|
|
231
233
|
"Total entries": "Voci totali",
|
|
232
234
|
"Total stations": "Stazioni trovate",
|
|
233
235
|
"Total time": "Durata totale",
|
|
236
|
+
"Total time (readable)": "Durata totale (leggibile)",
|
|
234
237
|
"Total tracks": "Tracce totali",
|
|
235
238
|
"Track": "Traccia",
|
|
236
239
|
"Track number": "Numero traccia",
|
package/admin/i18n/nl.json
CHANGED
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"Disc time": "Schijftijd",
|
|
99
99
|
"Drive status": "Stationstatus",
|
|
100
100
|
"Elapsed time": "Verstreken tijd",
|
|
101
|
+
"Elapsed time (readable)": "Verstreken tijd (leesbaar)",
|
|
101
102
|
"Enhancer": "Enhancer",
|
|
102
103
|
"Ensemble label": "Ensemblenaam",
|
|
103
104
|
"Equalizer": "Equalizer",
|
|
@@ -128,6 +129,7 @@
|
|
|
128
129
|
"Initial volume mode": "Startvolumemodus",
|
|
129
130
|
"Input": "Ingang",
|
|
130
131
|
"Input name (display)": "Ingangsnaam (display)",
|
|
132
|
+
"Input name (%s)": "Ingangsnaam (%s)",
|
|
131
133
|
"Input names": "Ingangsnamen",
|
|
132
134
|
"Leave group": "Groep verlaten",
|
|
133
135
|
"Link a device (its IP)": "Apparaat koppelen (zijn IP)",
|
|
@@ -231,6 +233,7 @@
|
|
|
231
233
|
"Total entries": "Aantal items",
|
|
232
234
|
"Total stations": "Gevonden zenders",
|
|
233
235
|
"Total time": "Totale tijd",
|
|
236
|
+
"Total time (readable)": "Totale tijd (leesbaar)",
|
|
234
237
|
"Total tracks": "Aantal tracks",
|
|
235
238
|
"Track": "Track",
|
|
236
239
|
"Track number": "Tracknummer",
|
package/admin/i18n/pl.json
CHANGED
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"Disc time": "Czas płyty",
|
|
99
99
|
"Drive status": "Stan napędu",
|
|
100
100
|
"Elapsed time": "Czas odtwarzania",
|
|
101
|
+
"Elapsed time (readable)": "Czas odtwarzania (czytelnie)",
|
|
101
102
|
"Enhancer": "Enhancer",
|
|
102
103
|
"Ensemble label": "Nazwa zespołu",
|
|
103
104
|
"Equalizer": "Korektor",
|
|
@@ -128,6 +129,7 @@
|
|
|
128
129
|
"Initial volume mode": "Tryb głośności początkowej",
|
|
129
130
|
"Input": "Wejście",
|
|
130
131
|
"Input name (display)": "Nazwa wejścia (wyświetlacz)",
|
|
132
|
+
"Input name (%s)": "Nazwa wejścia (%s)",
|
|
131
133
|
"Input names": "Nazwy wejść",
|
|
132
134
|
"Leave group": "Opuść grupę",
|
|
133
135
|
"Link a device (its IP)": "Połącz urządzenie (jego IP)",
|
|
@@ -231,6 +233,7 @@
|
|
|
231
233
|
"Total entries": "Liczba pozycji",
|
|
232
234
|
"Total stations": "Znalezione stacje",
|
|
233
235
|
"Total time": "Czas całkowity",
|
|
236
|
+
"Total time (readable)": "Czas całkowity (czytelnie)",
|
|
234
237
|
"Total tracks": "Liczba utworów",
|
|
235
238
|
"Track": "Utwór",
|
|
236
239
|
"Track number": "Numer utworu",
|
package/admin/i18n/pt.json
CHANGED
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"Disc time": "Tempo do disco",
|
|
99
99
|
"Drive status": "Estado da unidade",
|
|
100
100
|
"Elapsed time": "Tempo decorrido",
|
|
101
|
+
"Elapsed time (readable)": "Tempo decorrido (legível)",
|
|
101
102
|
"Enhancer": "Enhancer",
|
|
102
103
|
"Ensemble label": "Nome do conjunto",
|
|
103
104
|
"Equalizer": "Equalizador",
|
|
@@ -128,6 +129,7 @@
|
|
|
128
129
|
"Initial volume mode": "Modo do volume inicial",
|
|
129
130
|
"Input": "Entrada",
|
|
130
131
|
"Input name (display)": "Nome da entrada (visor)",
|
|
132
|
+
"Input name (%s)": "Nome da entrada (%s)",
|
|
131
133
|
"Input names": "Nomes das entradas",
|
|
132
134
|
"Leave group": "Sair do grupo",
|
|
133
135
|
"Link a device (its IP)": "Ligar um dispositivo (o seu IP)",
|
|
@@ -231,6 +233,7 @@
|
|
|
231
233
|
"Total entries": "Total de entradas",
|
|
232
234
|
"Total stations": "Total de estações",
|
|
233
235
|
"Total time": "Duração total",
|
|
236
|
+
"Total time (readable)": "Tempo total (legível)",
|
|
234
237
|
"Total tracks": "Total de faixas",
|
|
235
238
|
"Track": "Faixa",
|
|
236
239
|
"Track number": "Número da faixa",
|
package/admin/i18n/ru.json
CHANGED
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"Disc time": "Время диска",
|
|
99
99
|
"Drive status": "Состояние привода",
|
|
100
100
|
"Elapsed time": "Прошло времени",
|
|
101
|
+
"Elapsed time (readable)": "Прошедшее время (в читаемом виде)",
|
|
101
102
|
"Enhancer": "Enhancer",
|
|
102
103
|
"Ensemble label": "Название ансамбля",
|
|
103
104
|
"Equalizer": "Эквалайзер",
|
|
@@ -128,6 +129,7 @@
|
|
|
128
129
|
"Initial volume mode": "Режим начальной громкости",
|
|
129
130
|
"Input": "Вход",
|
|
130
131
|
"Input name (display)": "Название входа (на дисплее)",
|
|
132
|
+
"Input name (%s)": "Название входа (%s)",
|
|
131
133
|
"Input names": "Названия входов",
|
|
132
134
|
"Leave group": "Покинуть группу",
|
|
133
135
|
"Link a device (its IP)": "Связать устройство (его IP)",
|
|
@@ -231,6 +233,7 @@
|
|
|
231
233
|
"Total entries": "Всего записей",
|
|
232
234
|
"Total stations": "Найдено станций",
|
|
233
235
|
"Total time": "Общая длительность",
|
|
236
|
+
"Total time (readable)": "Общее время (в читаемом виде)",
|
|
234
237
|
"Total tracks": "Всего треков",
|
|
235
238
|
"Track": "Трек",
|
|
236
239
|
"Track number": "Номер трека",
|
package/admin/i18n/uk.json
CHANGED
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"Disc time": "Час диска",
|
|
99
99
|
"Drive status": "Стан приводу",
|
|
100
100
|
"Elapsed time": "Минуло часу",
|
|
101
|
+
"Elapsed time (readable)": "Минулий час (у зручному вигляді)",
|
|
101
102
|
"Enhancer": "Enhancer",
|
|
102
103
|
"Ensemble label": "Назва ансамблю",
|
|
103
104
|
"Equalizer": "Еквалайзер",
|
|
@@ -128,6 +129,7 @@
|
|
|
128
129
|
"Initial volume mode": "Режим початкової гучності",
|
|
129
130
|
"Input": "Вхід",
|
|
130
131
|
"Input name (display)": "Назва входу (дисплей)",
|
|
132
|
+
"Input name (%s)": "Назва входу (%s)",
|
|
131
133
|
"Input names": "Назви входів",
|
|
132
134
|
"Leave group": "Покинути групу",
|
|
133
135
|
"Link a device (its IP)": "Пов'язати пристрій (його IP)",
|
|
@@ -231,6 +233,7 @@
|
|
|
231
233
|
"Total entries": "Усього записів",
|
|
232
234
|
"Total stations": "Знайдено станцій",
|
|
233
235
|
"Total time": "Загальна тривалість",
|
|
236
|
+
"Total time (readable)": "Загальний час (у зручному вигляді)",
|
|
234
237
|
"Total tracks": "Усього треків",
|
|
235
238
|
"Track": "Трек",
|
|
236
239
|
"Track number": "Номер треку",
|
package/admin/i18n/zh-cn.json
CHANGED
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"Disc time": "碟片时间",
|
|
99
99
|
"Drive status": "光驱状态",
|
|
100
100
|
"Elapsed time": "已播放时间",
|
|
101
|
+
"Elapsed time (readable)": "已播放时长(易读)",
|
|
101
102
|
"Enhancer": "音质增强",
|
|
102
103
|
"Ensemble label": "复用名称",
|
|
103
104
|
"Equalizer": "均衡器",
|
|
@@ -128,6 +129,7 @@
|
|
|
128
129
|
"Initial volume mode": "初始音量模式",
|
|
129
130
|
"Input": "输入",
|
|
130
131
|
"Input name (display)": "输入名称(显示)",
|
|
132
|
+
"Input name (%s)": "输入名称(%s)",
|
|
131
133
|
"Input names": "输入名称",
|
|
132
134
|
"Leave group": "离开群组",
|
|
133
135
|
"Link a device (its IP)": "连接设备(其 IP)",
|
|
@@ -231,6 +233,7 @@
|
|
|
231
233
|
"Total entries": "条目总数",
|
|
232
234
|
"Total stations": "电台总数",
|
|
233
235
|
"Total time": "总时长",
|
|
236
|
+
"Total time (readable)": "总时长(易读)",
|
|
234
237
|
"Total tracks": "曲目总数",
|
|
235
238
|
"Track": "曲目",
|
|
236
239
|
"Track number": "曲目编号",
|
|
@@ -121,19 +121,32 @@ class XmlBrowseDriver {
|
|
|
121
121
|
/** Go one menu level back — falling back to `Left` when the device refuses `Return`. */
|
|
122
122
|
async back() {
|
|
123
123
|
try {
|
|
124
|
-
await this.
|
|
124
|
+
await this.send(`<Cursor>${this.backCursor}</Cursor>`);
|
|
125
125
|
} catch (e) {
|
|
126
126
|
if (this.backCursor !== "Return") {
|
|
127
127
|
throw e;
|
|
128
128
|
}
|
|
129
129
|
this.backCursor = "Left";
|
|
130
|
-
await this.
|
|
130
|
+
await this.send("<Cursor>Left</Cursor>");
|
|
131
131
|
}
|
|
132
|
+
await this.fetch();
|
|
132
133
|
}
|
|
133
134
|
/** Return to the menu root. */
|
|
134
135
|
async home() {
|
|
135
136
|
await this.control("<Cursor>Return to Home</Cursor>");
|
|
136
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Send a List_Control command to the active source — WITHOUT reading the window back,
|
|
140
|
+
* so a caller can tell a device refusal from a failed follow-up read (see {@link back}).
|
|
141
|
+
*
|
|
142
|
+
* @param inner the List_Control payload (Direct_Sel, Cursor, Jump_Line)
|
|
143
|
+
*/
|
|
144
|
+
async send(inner) {
|
|
145
|
+
if (!this.active) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
await this.client.send(this.active.element, `<List_Control>${inner}</List_Control>`);
|
|
149
|
+
}
|
|
137
150
|
/**
|
|
138
151
|
* Send a List_Control command to the active source and read the window back.
|
|
139
152
|
*
|
|
@@ -143,7 +156,7 @@ class XmlBrowseDriver {
|
|
|
143
156
|
if (!this.active) {
|
|
144
157
|
return;
|
|
145
158
|
}
|
|
146
|
-
await this.
|
|
159
|
+
await this.send(inner);
|
|
147
160
|
await this.fetch();
|
|
148
161
|
}
|
|
149
162
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/browse/xml-browse-driver.ts"],
|
|
4
|
-
"sourcesContent": ["import { ROW_KIND_BY_ATTRIBUTE, type BrowseDriver, type BrowseRow } from \"./types\";\nimport type { BrowseEngine } from \"./browse-engine\";\nimport { decodeXmlText } from \"../xml/entities\";\n\n/** How often to re-read while the device reports Menu_Status Busy. */\nconst BUSY_POLL_MS = 500;\n/** How many busy polls before giving up on one read. */\nconst MAX_BUSY_POLLS = 10;\n\n/**\n * The XML/YNC sources with a List_Info menu (the ones the predecessor adapter's\n * users browsed; rxv drives the same three), with the transport-neutral source key\n * and the input name that activates the source.\n */\nexport const XML_BROWSE_SOURCES: ReadonlyArray<{ element: string; key: string; label: string; input: string }> = [\n { element: \"NET_RADIO\", key: \"netRadio\", label: \"Net Radio\", input: \"NET RADIO\" },\n { element: \"SERVER\", key: \"server\", label: \"Media server\", input: \"SERVER\" },\n { element: \"USB\", key: \"usb\", label: \"USB\", input: \"USB\" },\n];\n\n/** The client surface the driver needs (a slice of the XML client). */\nexport interface XmlBrowseClient {\n /** Send an inner PUT command to an element (a zone or a source). */\n send(element: string, inner: string): Promise<void>;\n /** Read an element's inner GET request and return the raw response body. */\n getXml(element: string, inner: string): Promise<string>;\n}\n\n/** A parsed List_Info response. */\nexport interface XmlListInfo {\n /** Whether the menu is ready (false = the device is still fetching it). */\n ready: boolean;\n /** The menu title. */\n menuName: string;\n /** The menu depth (1 = root). */\n layer: number;\n /** The cursor's absolute line number. */\n currentLine: number;\n /** Total entries in the menu. */\n totalItems: number;\n /** The visible rows. */\n rows: BrowseRow[];\n}\n\n/**\n * Parse a `<List_Info>` response (menu status, layer, name, cursor, and the eight\n * `Current_List` lines with text + attribute \u2014 the shape rxv reads).\n *\n * @param xml the List_Info response body\n * @returns the parsed window\n */\nexport function parseXmlListInfo(xml: string): XmlListInfo {\n const rows: BrowseRow[] = [];\n const linePattern = /<Line_([1-8])>\\s*<Txt>([^<]*)<\\/Txt>\\s*<Attribute>([^<]*)<\\/Attribute>/g;\n for (let match = linePattern.exec(xml); match; match = linePattern.exec(xml)) {\n if (match[2].length > 0) {\n rows.push({\n line: Number(match[1]),\n text: decodeXmlText(match[2]),\n kind: ROW_KIND_BY_ATTRIBUTE[match[3]] ?? \"item\",\n });\n }\n }\n return {\n ready: !/<Menu_Status>Busy<\\/Menu_Status>/.test(xml),\n menuName: decodeXmlText(/<Menu_Name>([^<]*)<\\/Menu_Name>/.exec(xml)?.[1] ?? \"\"),\n layer: Number(/<Menu_Layer>(\\d+)<\\/Menu_Layer>/.exec(xml)?.[1] ?? 0),\n currentLine: Number(/<Current_Line>(\\d+)<\\/Current_Line>/.exec(xml)?.[1] ?? 1),\n totalItems: Number(/<Max_Line>(\\d+)<\\/Max_Line>/.exec(xml)?.[1] ?? 0),\n rows,\n };\n}\n\n/**\n * The XML/YNC list driver over `<List_Info>` + `<List_Control>` (the predecessor\n * adapter's browsing path). Pull-based: every operation re-reads the window, polling\n * while the device reports the menu as busy (levels come from the catalog service).\n * The cursor line stands in for the window start \u2014 the same approximation rxv makes.\n */\nexport class XmlBrowseDriver implements BrowseDriver {\n private engine: BrowseEngine | undefined;\n private active: { element: string; key: string; input: string } | undefined;\n private lastTotal = 0;\n /**\n * The cursor value this device accepts for \"one level back\". The spec vocabulary is\n * `Return`, but the 2012 generation (RX-V473 class) refuses it and accepts `Left`\n * instead (user-measured on the predecessor adapter, #613). First refusal switches\n * permanently for this device.\n */\n private backCursor: \"Return\" | \"Left\" = \"Return\";\n\n /**\n * @param client the XML client slice (send + getXml)\n * @param available the source keys whose List_Info the start-up probe answered\n * @param delay adapter-managed delay\n */\n public constructor(\n private readonly client: XmlBrowseClient,\n private readonly available: ReadonlySet<string>,\n private readonly delay: (ms: number) => Promise<void>,\n ) {}\n\n /**\n * Attach the engine that renders the windows (set after both are constructed).\n *\n * @param engine the browse engine\n */\n public attach(engine: BrowseEngine): void {\n this.engine = engine;\n }\n\n /** @returns the selectable sources this device offers (state value \u2192 label) */\n public sources(): Record<string, string> {\n const entries = XML_BROWSE_SOURCES.filter(source => this.available.has(source.key));\n return Object.fromEntries(entries.map(source => [source.key, source.label]));\n }\n\n /**\n * Open a source's menu: switch the main-zone input to it and read the window.\n *\n * @param source the source key (from {@link sources})\n */\n public async open(source: string): Promise<void> {\n const entry = XML_BROWSE_SOURCES.find(s => s.key === source && this.available.has(s.key));\n if (!entry) {\n return;\n }\n this.active = entry;\n await this.client.send(\"Main_Zone\", `<Input><Input_Sel>${entry.input}</Input_Sel></Input>`);\n await this.fetch();\n }\n\n /**\n * Select a visible line via Direct_Sel \u2014 a folder opens, a playable item starts.\n *\n * @param line the line number (1\u20138)\n */\n public async select(line: number): Promise<void> {\n await this.control(`<Direct_Sel>Line_${line}</Direct_Sel>`);\n }\n\n /** Show the previous 8 lines (jump the cursor back a page). */\n public async pageUp(): Promise<void> {\n await this.jumpBy(-8);\n }\n\n /** Show the next 8 lines (jump the cursor forward a page). */\n public async pageDown(): Promise<void> {\n await this.jumpBy(8);\n }\n\n /** Go one menu level back \u2014 falling back to `Left` when the device refuses `Return`. */\n public async back(): Promise<void> {\n try {\n await this.
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAyE;AAEzE,sBAA8B;AAG9B,MAAM,eAAe;AAErB,MAAM,iBAAiB;AAOhB,MAAM,qBAAoG;AAAA,EAC/G,EAAE,SAAS,aAAa,KAAK,YAAY,OAAO,aAAa,OAAO,YAAY;AAAA,EAChF,EAAE,SAAS,UAAU,KAAK,UAAU,OAAO,gBAAgB,OAAO,SAAS;AAAA,EAC3E,EAAE,SAAS,OAAO,KAAK,OAAO,OAAO,OAAO,OAAO,MAAM;AAC3D;AAiCO,SAAS,iBAAiB,KAA0B;AAnD3D;AAoDE,QAAM,OAAoB,CAAC;AAC3B,QAAM,cAAc;AACpB,WAAS,QAAQ,YAAY,KAAK,GAAG,GAAG,OAAO,QAAQ,YAAY,KAAK,GAAG,GAAG;AAC5E,QAAI,MAAM,CAAC,EAAE,SAAS,GAAG;AACvB,WAAK,KAAK;AAAA,QACR,MAAM,OAAO,MAAM,CAAC,CAAC;AAAA,QACrB,UAAM,+BAAc,MAAM,CAAC,CAAC;AAAA,QAC5B,OAAM,wCAAsB,MAAM,CAAC,CAAC,MAA9B,YAAmC;AAAA,MAC3C,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AAAA,IACL,OAAO,CAAC,mCAAmC,KAAK,GAAG;AAAA,IACnD,cAAU,gCAAc,6CAAkC,KAAK,GAAG,MAA1C,mBAA8C,OAA9C,YAAoD,EAAE;AAAA,IAC9E,OAAO,QAAO,6CAAkC,KAAK,GAAG,MAA1C,mBAA8C,OAA9C,YAAoD,CAAC;AAAA,IACnE,aAAa,QAAO,iDAAsC,KAAK,GAAG,MAA9C,mBAAkD,OAAlD,YAAwD,CAAC;AAAA,IAC7E,YAAY,QAAO,yCAA8B,KAAK,GAAG,MAAtC,mBAA0C,OAA1C,YAAgD,CAAC;AAAA,IACpE;AAAA,EACF;AACF;AAQO,MAAM,gBAAwC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiB5C,YACY,QACA,WACA,OACjB;AAHiB;AACA;AACA;AAAA,EAChB;AAAA,EApBK;AAAA,EACA;AAAA,EACA,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOZ,aAAgC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBjC,OAAO,QAA4B;AACxC,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA,EAGO,UAAkC;AACvC,UAAM,UAAU,mBAAmB,OAAO,YAAU,KAAK,UAAU,IAAI,OAAO,GAAG,CAAC;AAClF,WAAO,OAAO,YAAY,QAAQ,IAAI,YAAU,CAAC,OAAO,KAAK,OAAO,KAAK,CAAC,CAAC;AAAA,EAC7E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAa,KAAK,QAA+B;AAC/C,UAAM,QAAQ,mBAAmB,KAAK,OAAK,EAAE,QAAQ,UAAU,KAAK,UAAU,IAAI,EAAE,GAAG,CAAC;AACxF,QAAI,CAAC,OAAO;AACV;AAAA,IACF;AACA,SAAK,SAAS;AACd,UAAM,KAAK,OAAO,KAAK,aAAa,qBAAqB,MAAM,KAAK,sBAAsB;AAC1F,UAAM,KAAK,MAAM;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAa,OAAO,MAA6B;AAC/C,UAAM,KAAK,QAAQ,oBAAoB,IAAI,eAAe;AAAA,EAC5D;AAAA;AAAA,EAGA,MAAa,SAAwB;AACnC,UAAM,KAAK,OAAO,EAAE;AAAA,EACtB;AAAA;AAAA,EAGA,MAAa,WAA0B;AACrC,UAAM,KAAK,OAAO,CAAC;AAAA,EACrB;AAAA;AAAA,EAGA,MAAa,OAAsB;
|
|
4
|
+
"sourcesContent": ["import { ROW_KIND_BY_ATTRIBUTE, type BrowseDriver, type BrowseRow } from \"./types\";\nimport type { BrowseEngine } from \"./browse-engine\";\nimport { decodeXmlText } from \"../xml/entities\";\n\n/** How often to re-read while the device reports Menu_Status Busy. */\nconst BUSY_POLL_MS = 500;\n/** How many busy polls before giving up on one read. */\nconst MAX_BUSY_POLLS = 10;\n\n/**\n * The XML/YNC sources with a List_Info menu (the ones the predecessor adapter's\n * users browsed; rxv drives the same three), with the transport-neutral source key\n * and the input name that activates the source.\n */\nexport const XML_BROWSE_SOURCES: ReadonlyArray<{ element: string; key: string; label: string; input: string }> = [\n { element: \"NET_RADIO\", key: \"netRadio\", label: \"Net Radio\", input: \"NET RADIO\" },\n { element: \"SERVER\", key: \"server\", label: \"Media server\", input: \"SERVER\" },\n { element: \"USB\", key: \"usb\", label: \"USB\", input: \"USB\" },\n];\n\n/** The client surface the driver needs (a slice of the XML client). */\nexport interface XmlBrowseClient {\n /** Send an inner PUT command to an element (a zone or a source). */\n send(element: string, inner: string): Promise<void>;\n /** Read an element's inner GET request and return the raw response body. */\n getXml(element: string, inner: string): Promise<string>;\n}\n\n/** A parsed List_Info response. */\nexport interface XmlListInfo {\n /** Whether the menu is ready (false = the device is still fetching it). */\n ready: boolean;\n /** The menu title. */\n menuName: string;\n /** The menu depth (1 = root). */\n layer: number;\n /** The cursor's absolute line number. */\n currentLine: number;\n /** Total entries in the menu. */\n totalItems: number;\n /** The visible rows. */\n rows: BrowseRow[];\n}\n\n/**\n * Parse a `<List_Info>` response (menu status, layer, name, cursor, and the eight\n * `Current_List` lines with text + attribute \u2014 the shape rxv reads).\n *\n * @param xml the List_Info response body\n * @returns the parsed window\n */\nexport function parseXmlListInfo(xml: string): XmlListInfo {\n const rows: BrowseRow[] = [];\n const linePattern = /<Line_([1-8])>\\s*<Txt>([^<]*)<\\/Txt>\\s*<Attribute>([^<]*)<\\/Attribute>/g;\n for (let match = linePattern.exec(xml); match; match = linePattern.exec(xml)) {\n if (match[2].length > 0) {\n rows.push({\n line: Number(match[1]),\n text: decodeXmlText(match[2]),\n kind: ROW_KIND_BY_ATTRIBUTE[match[3]] ?? \"item\",\n });\n }\n }\n return {\n ready: !/<Menu_Status>Busy<\\/Menu_Status>/.test(xml),\n menuName: decodeXmlText(/<Menu_Name>([^<]*)<\\/Menu_Name>/.exec(xml)?.[1] ?? \"\"),\n layer: Number(/<Menu_Layer>(\\d+)<\\/Menu_Layer>/.exec(xml)?.[1] ?? 0),\n currentLine: Number(/<Current_Line>(\\d+)<\\/Current_Line>/.exec(xml)?.[1] ?? 1),\n totalItems: Number(/<Max_Line>(\\d+)<\\/Max_Line>/.exec(xml)?.[1] ?? 0),\n rows,\n };\n}\n\n/**\n * The XML/YNC list driver over `<List_Info>` + `<List_Control>` (the predecessor\n * adapter's browsing path). Pull-based: every operation re-reads the window, polling\n * while the device reports the menu as busy (levels come from the catalog service).\n * The cursor line stands in for the window start \u2014 the same approximation rxv makes.\n */\nexport class XmlBrowseDriver implements BrowseDriver {\n private engine: BrowseEngine | undefined;\n private active: { element: string; key: string; input: string } | undefined;\n private lastTotal = 0;\n /**\n * The cursor value this device accepts for \"one level back\". The spec vocabulary is\n * `Return`, but the 2012 generation (RX-V473 class) refuses it and accepts `Left`\n * instead (user-measured on the predecessor adapter, #613). First refusal switches\n * permanently for this device.\n */\n private backCursor: \"Return\" | \"Left\" = \"Return\";\n\n /**\n * @param client the XML client slice (send + getXml)\n * @param available the source keys whose List_Info the start-up probe answered\n * @param delay adapter-managed delay\n */\n public constructor(\n private readonly client: XmlBrowseClient,\n private readonly available: ReadonlySet<string>,\n private readonly delay: (ms: number) => Promise<void>,\n ) {}\n\n /**\n * Attach the engine that renders the windows (set after both are constructed).\n *\n * @param engine the browse engine\n */\n public attach(engine: BrowseEngine): void {\n this.engine = engine;\n }\n\n /** @returns the selectable sources this device offers (state value \u2192 label) */\n public sources(): Record<string, string> {\n const entries = XML_BROWSE_SOURCES.filter(source => this.available.has(source.key));\n return Object.fromEntries(entries.map(source => [source.key, source.label]));\n }\n\n /**\n * Open a source's menu: switch the main-zone input to it and read the window.\n *\n * @param source the source key (from {@link sources})\n */\n public async open(source: string): Promise<void> {\n const entry = XML_BROWSE_SOURCES.find(s => s.key === source && this.available.has(s.key));\n if (!entry) {\n return;\n }\n this.active = entry;\n await this.client.send(\"Main_Zone\", `<Input><Input_Sel>${entry.input}</Input_Sel></Input>`);\n await this.fetch();\n }\n\n /**\n * Select a visible line via Direct_Sel \u2014 a folder opens, a playable item starts.\n *\n * @param line the line number (1\u20138)\n */\n public async select(line: number): Promise<void> {\n await this.control(`<Direct_Sel>Line_${line}</Direct_Sel>`);\n }\n\n /** Show the previous 8 lines (jump the cursor back a page). */\n public async pageUp(): Promise<void> {\n await this.jumpBy(-8);\n }\n\n /** Show the next 8 lines (jump the cursor forward a page). */\n public async pageDown(): Promise<void> {\n await this.jumpBy(8);\n }\n\n /** Go one menu level back \u2014 falling back to `Left` when the device refuses `Return`. */\n public async back(): Promise<void> {\n // ONLY the command is guarded, never the window read that follows it. `control()`\n // does both, so catching around it treated a failed read as \"the device refuses\n // Return\" \u2014 it then sent a SECOND back (the user jumped two levels) and switched\n // this connection to `Left` for good, over what was a passing network hiccup.\n try {\n await this.send(`<Cursor>${this.backCursor}</Cursor>`);\n } catch (e) {\n // Only a refusal of the DEFAULT vocabulary triggers the generation fallback;\n // a device that also refuses Left (or a transport error) surfaces normally.\n if (this.backCursor !== \"Return\") {\n throw e;\n }\n this.backCursor = \"Left\";\n await this.send(\"<Cursor>Left</Cursor>\");\n }\n await this.fetch();\n }\n\n /** Return to the menu root. */\n public async home(): Promise<void> {\n await this.control(\"<Cursor>Return to Home</Cursor>\");\n }\n\n /**\n * Send a List_Control command to the active source \u2014 WITHOUT reading the window back,\n * so a caller can tell a device refusal from a failed follow-up read (see {@link back}).\n *\n * @param inner the List_Control payload (Direct_Sel, Cursor, Jump_Line)\n */\n private async send(inner: string): Promise<void> {\n if (!this.active) {\n return;\n }\n await this.client.send(this.active.element, `<List_Control>${inner}</List_Control>`);\n }\n\n /**\n * Send a List_Control command to the active source and read the window back.\n *\n * @param inner the List_Control payload (Direct_Sel, Cursor, Jump_Line)\n */\n private async control(inner: string): Promise<void> {\n if (!this.active) {\n return;\n }\n await this.send(inner);\n await this.fetch();\n }\n\n /**\n * Jump the cursor by a page, clamped to the menu bounds.\n *\n * @param delta the line offset (\u00B18)\n */\n private async jumpBy(delta: number): Promise<void> {\n if (!this.active) {\n return;\n }\n const current = await this.readWindow();\n if (!current) {\n return;\n }\n const target = Math.min(Math.max(1, current.currentLine + delta), Math.max(1, this.lastTotal));\n await this.control(`<Jump_Line>${target}</Jump_Line>`);\n }\n\n /** Read the window (polling while busy) and render it to the engine. */\n private async fetch(): Promise<void> {\n const window = await this.readWindow();\n if (window) {\n this.lastTotal = window.totalItems;\n this.engine?.onWindow({\n menuName: window.menuName,\n layer: window.layer,\n totalItems: window.totalItems,\n currentLine: window.currentLine,\n rows: window.rows,\n });\n }\n }\n\n /**\n * Read the active source's List_Info, polling while the device reports Busy.\n *\n * @returns the parsed window, or undefined without an active source / when busy persists\n */\n private async readWindow(): Promise<XmlListInfo | undefined> {\n if (!this.active) {\n return undefined;\n }\n for (let attempt = 0; attempt < MAX_BUSY_POLLS; attempt++) {\n const info = parseXmlListInfo(await this.client.getXml(this.active.element, \"<List_Info>GetParam</List_Info>\"));\n if (info.ready) {\n return info;\n }\n await this.delay(BUSY_POLL_MS);\n }\n return undefined;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -58,6 +58,8 @@ class YncaBrowseDriver {
|
|
|
58
58
|
kinds = /* @__PURE__ */ new Map();
|
|
59
59
|
/** True while a settle delay is pending, so one burst renders once. */
|
|
60
60
|
renderPending = false;
|
|
61
|
+
/** True between asking for a window and the first field of the answer — see {@link refresh}. */
|
|
62
|
+
awaitingWindow = false;
|
|
61
63
|
closed = false;
|
|
62
64
|
/**
|
|
63
65
|
* Attach the engine that renders the windows (set after both are constructed).
|
|
@@ -119,6 +121,7 @@ class YncaBrowseDriver {
|
|
|
119
121
|
/** Re-read the current window (`LISTINFO=?` answers with the full field burst). */
|
|
120
122
|
refresh() {
|
|
121
123
|
if (this.active) {
|
|
124
|
+
this.awaitingWindow = true;
|
|
122
125
|
this.client.get(this.active.subunit, "LISTINFO");
|
|
123
126
|
}
|
|
124
127
|
}
|
|
@@ -137,6 +140,11 @@ class YncaBrowseDriver {
|
|
|
137
140
|
if (!this.active || message.subunit !== this.active.subunit) {
|
|
138
141
|
return;
|
|
139
142
|
}
|
|
143
|
+
const isWindowField = /^LINE[1-8](TXT|ATRIB)$/.test(message.func) || ["LISTLAYERNAME", "LISTLAYER", "MAXLINE", "CURRLINE"].includes(message.func);
|
|
144
|
+
if (isWindowField && this.awaitingWindow) {
|
|
145
|
+
this.awaitingWindow = false;
|
|
146
|
+
this.resetAssembly();
|
|
147
|
+
}
|
|
140
148
|
const line = /^LINE([1-8])(TXT|ATRIB)$/.exec(message.func);
|
|
141
149
|
if (line) {
|
|
142
150
|
const n = Number(line[1]);
|
|
@@ -171,8 +179,9 @@ class YncaBrowseDriver {
|
|
|
171
179
|
this.client.send(this.active.subunit, func, value);
|
|
172
180
|
this.refresh();
|
|
173
181
|
}
|
|
174
|
-
/** Clear the assembly
|
|
182
|
+
/** Clear the window assembly — on a source switch, and when a freshly asked window starts arriving. */
|
|
175
183
|
resetAssembly() {
|
|
184
|
+
this.awaitingWindow = false;
|
|
176
185
|
this.menuName = "";
|
|
177
186
|
this.layer = 0;
|
|
178
187
|
this.totalItems = 0;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/browse/ynca-browse-driver.ts"],
|
|
4
|
-
"sourcesContent": ["import { ROW_KIND_BY_ATTRIBUTE, type BrowseDriver, type BrowseRow, type BrowseRowKind } from \"./types\";\nimport type { BrowseEngine } from \"./browse-engine\";\n\n/** Collect a burst of list lines for this long before rendering the window. */\nconst BURST_SETTLE_MS = 200;\n\n/**\n * The browsable YNCA subunits (official RX-V671 command list + the all-commands\n * corpus: every one carries LISTINFO/LISTSEL/LISTCURSOR/LISTPAGE), with the\n * transport-neutral source key and the `@MAIN:INP` wire value that activates the\n * source (browsing follows the active input, like the remote).\n */\nexport const YNCA_BROWSE_SOURCES: ReadonlyArray<{ subunit: string; key: string; label: string; input: string }> = [\n { subunit: \"NETRADIO\", key: \"netRadio\", label: \"Net Radio\", input: \"NET RADIO\" },\n { subunit: \"SERVER\", key: \"server\", label: \"Media server\", input: \"SERVER\" },\n { subunit: \"PC\", key: \"pc\", label: \"PC\", input: \"PC\" },\n { subunit: \"USB\", key: \"usb\", label: \"USB\", input: \"USB\" },\n { subunit: \"IPOD\", key: \"ipod\", label: \"iPod\", input: \"iPod\" },\n { subunit: \"IPODUSB\", key: \"ipodUsb\", label: \"iPod (USB)\", input: \"iPod (USB)\" },\n { subunit: \"NAPSTER\", key: \"napster\", label: \"Napster\", input: \"Napster\" },\n { subunit: \"PANDORA\", key: \"pandora\", label: \"Pandora\", input: \"Pandora\" },\n { subunit: \"RHAP\", key: \"rhapsody\", label: \"Rhapsody\", input: \"Rhapsody\" },\n { subunit: \"SIRIUS\", key: \"sirius\", label: \"SiriusXM\", input: \"SIRIUS\" },\n];\n\n/** The client surface the driver needs (a slice of the YNCA client). */\nexport interface YncaBrowseClient {\n /** Send a PUT command. */\n send(subunit: string, func: string, value: string): void;\n /** Send a GET request. */\n get(subunit: string, func: string): void;\n}\n\n/**\n * The YNCA list driver: navigation writes go out as LISTSEL/LISTPAGE/LISTCURSOR\n * commands, the window comes back as a burst of LISTLAYER/LISTLAYERNAME/CURRLINE/\n * MAXLINE/LINE1TXT\u2026LINE8TXT lines \u2014 solicited by a `LISTINFO=?` read, and pushed\n * unsolicited over the held connection whenever the list changes (\"Initial Auto\n * Feedback: Available\" in the official command list). The controller feeds every\n * received line into {@link handleMessage}; lines for the active subunit update the\n * window assembly, which is rendered to the engine once the burst settles.\n */\nexport class YncaBrowseDriver implements BrowseDriver {\n private active: { subunit: string; key: string; input: string } | undefined;\n private engine: BrowseEngine | undefined;\n /** The window assembly the bursts fill. */\n private menuName = \"\";\n private layer = 0;\n private totalItems = 0;\n private currentLine = 1;\n private readonly texts = new Map<number, string>();\n private readonly kinds = new Map<number, BrowseRowKind>();\n /** True while a settle delay is pending, so one burst renders once. */\n private renderPending = false;\n private closed = false;\n\n /**\n * @param client the YNCA client slice (send + get)\n * @param present the browsable subunits this device reported in the AVAIL probe\n * @param delay adapter-managed delay\n */\n public constructor(\n private readonly client: YncaBrowseClient,\n private readonly present: ReadonlySet<string>,\n private readonly delay: (ms: number) => Promise<void>,\n ) {}\n\n /**\n * Attach the engine that renders the windows (set after both are constructed).\n *\n * @param engine the browse engine\n */\n public attach(engine: BrowseEngine): void {\n this.engine = engine;\n }\n\n /** Stop rendering (the controller is closing). */\n public close(): void {\n this.closed = true;\n }\n\n /** @returns the selectable sources this device offers (state value \u2192 label) */\n public sources(): Record<string, string> {\n const entries = YNCA_BROWSE_SOURCES.filter(source => this.present.has(source.subunit));\n return Object.fromEntries(entries.map(source => [source.key, source.label]));\n }\n\n /**\n * Open a source's menu: switch the main-zone input to it (browsing follows the\n * active input, like the remote) and read the current window.\n *\n * @param source the source key (from {@link sources})\n */\n public open(source: string): void {\n const entry = YNCA_BROWSE_SOURCES.find(s => s.key === source && this.present.has(s.subunit));\n if (!entry) {\n return;\n }\n this.active = entry;\n this.resetAssembly();\n this.client.send(\"MAIN\", \"INP\", entry.input);\n this.refresh();\n }\n\n /**\n * Select a visible line \u2014 `@<SUB>:LISTSEL=Line_n` acts like OK on that line.\n *\n * @param line the line number (1\u20138)\n */\n public select(line: number): void {\n this.command(\"LISTSEL\", `Line_${line}`);\n }\n\n /** Show the previous 8 lines. */\n public pageUp(): void {\n this.command(\"LISTPAGE\", \"Up\");\n }\n\n /** Show the next 8 lines. */\n public pageDown(): void {\n this.command(\"LISTPAGE\", \"Down\");\n }\n\n /** Go one menu level back. */\n public back(): void {\n this.command(\"LISTCURSOR\", \"Back\");\n }\n\n /** Return to the menu root. */\n public home(): void {\n this.command(\"LISTCURSOR\", \"Back to Home\");\n }\n\n /** Re-read the current window (`LISTINFO=?` answers with the full field burst). */\n private refresh(): void {\n if (this.active) {\n this.client.get(this.active.subunit, \"LISTINFO\");\n }\n }\n\n /**\n * Feed one received YNCA line. Lines of the active subunit's list functions update\n * the window assembly; anything else is ignored. Called by the controller for every\n * message, so unsolicited auto-feedback keeps the window live.\n *\n * @param message the decoded YNCA message\n * @param message.subunit the message's subunit\n * @param message.func the message's function name\n * @param message.value the message's raw wire value\n */\n public handleMessage(message: { subunit: string; func: string; value: string }): void {\n if (!this.active || message.subunit !== this.active.subunit) {\n return;\n }\n const line = /^LINE([1-8])(TXT|ATRIB)$/.exec(message.func);\n if (line) {\n const n = Number(line[1]);\n if (line[2] === \"TXT\") {\n this.texts.set(n, message.value);\n } else {\n this.kinds.set(n, ROW_KIND_BY_ATTRIBUTE[message.value] ?? \"item\");\n }\n } else if (message.func === \"LISTLAYERNAME\") {\n this.menuName = message.value;\n } else if (message.func === \"LISTLAYER\") {\n this.layer = Number(message.value) || 0;\n } else if (message.func === \"MAXLINE\") {\n this.totalItems = Number(message.value) || 0;\n } else if (message.func === \"CURRLINE\") {\n this.currentLine = Number(message.value) || 1;\n } else {\n return;\n }\n this.scheduleRender();\n }\n\n /**\n * Send a navigation command to the active subunit and read the window back.\n *\n * @param func the list function (LISTSEL, LISTPAGE, LISTCURSOR)\n * @param value the wire value\n */\n private command(func: string, value: string): void {\n if (!this.active) {\n return;\n }\n this.client.send(this.active.subunit, func, value);\n this.refresh();\n }\n\n /** Clear the assembly
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6F;AAI7F,MAAM,kBAAkB;AAQjB,MAAM,sBAAqG;AAAA,EAChH,EAAE,SAAS,YAAY,KAAK,YAAY,OAAO,aAAa,OAAO,YAAY;AAAA,EAC/E,EAAE,SAAS,UAAU,KAAK,UAAU,OAAO,gBAAgB,OAAO,SAAS;AAAA,EAC3E,EAAE,SAAS,MAAM,KAAK,MAAM,OAAO,MAAM,OAAO,KAAK;AAAA,EACrD,EAAE,SAAS,OAAO,KAAK,OAAO,OAAO,OAAO,OAAO,MAAM;AAAA,EACzD,EAAE,SAAS,QAAQ,KAAK,QAAQ,OAAO,QAAQ,OAAO,OAAO;AAAA,EAC7D,EAAE,SAAS,WAAW,KAAK,WAAW,OAAO,cAAc,OAAO,aAAa;AAAA,EAC/E,EAAE,SAAS,WAAW,KAAK,WAAW,OAAO,WAAW,OAAO,UAAU;AAAA,EACzE,EAAE,SAAS,WAAW,KAAK,WAAW,OAAO,WAAW,OAAO,UAAU;AAAA,EACzE,EAAE,SAAS,QAAQ,KAAK,YAAY,OAAO,YAAY,OAAO,WAAW;AAAA,EACzE,EAAE,SAAS,UAAU,KAAK,UAAU,OAAO,YAAY,OAAO,SAAS;AACzE;AAmBO,MAAM,iBAAyC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,
|
|
4
|
+
"sourcesContent": ["import { ROW_KIND_BY_ATTRIBUTE, type BrowseDriver, type BrowseRow, type BrowseRowKind } from \"./types\";\nimport type { BrowseEngine } from \"./browse-engine\";\n\n/** Collect a burst of list lines for this long before rendering the window. */\nconst BURST_SETTLE_MS = 200;\n\n/**\n * The browsable YNCA subunits (official RX-V671 command list + the all-commands\n * corpus: every one carries LISTINFO/LISTSEL/LISTCURSOR/LISTPAGE), with the\n * transport-neutral source key and the `@MAIN:INP` wire value that activates the\n * source (browsing follows the active input, like the remote).\n */\nexport const YNCA_BROWSE_SOURCES: ReadonlyArray<{ subunit: string; key: string; label: string; input: string }> = [\n { subunit: \"NETRADIO\", key: \"netRadio\", label: \"Net Radio\", input: \"NET RADIO\" },\n { subunit: \"SERVER\", key: \"server\", label: \"Media server\", input: \"SERVER\" },\n { subunit: \"PC\", key: \"pc\", label: \"PC\", input: \"PC\" },\n { subunit: \"USB\", key: \"usb\", label: \"USB\", input: \"USB\" },\n { subunit: \"IPOD\", key: \"ipod\", label: \"iPod\", input: \"iPod\" },\n { subunit: \"IPODUSB\", key: \"ipodUsb\", label: \"iPod (USB)\", input: \"iPod (USB)\" },\n { subunit: \"NAPSTER\", key: \"napster\", label: \"Napster\", input: \"Napster\" },\n { subunit: \"PANDORA\", key: \"pandora\", label: \"Pandora\", input: \"Pandora\" },\n { subunit: \"RHAP\", key: \"rhapsody\", label: \"Rhapsody\", input: \"Rhapsody\" },\n { subunit: \"SIRIUS\", key: \"sirius\", label: \"SiriusXM\", input: \"SIRIUS\" },\n];\n\n/** The client surface the driver needs (a slice of the YNCA client). */\nexport interface YncaBrowseClient {\n /** Send a PUT command. */\n send(subunit: string, func: string, value: string): void;\n /** Send a GET request. */\n get(subunit: string, func: string): void;\n}\n\n/**\n * The YNCA list driver: navigation writes go out as LISTSEL/LISTPAGE/LISTCURSOR\n * commands, the window comes back as a burst of LISTLAYER/LISTLAYERNAME/CURRLINE/\n * MAXLINE/LINE1TXT\u2026LINE8TXT lines \u2014 solicited by a `LISTINFO=?` read, and pushed\n * unsolicited over the held connection whenever the list changes (\"Initial Auto\n * Feedback: Available\" in the official command list). The controller feeds every\n * received line into {@link handleMessage}; lines for the active subunit update the\n * window assembly, which is rendered to the engine once the burst settles.\n */\nexport class YncaBrowseDriver implements BrowseDriver {\n private active: { subunit: string; key: string; input: string } | undefined;\n private engine: BrowseEngine | undefined;\n /** The window assembly the bursts fill. */\n private menuName = \"\";\n private layer = 0;\n private totalItems = 0;\n private currentLine = 1;\n private readonly texts = new Map<number, string>();\n private readonly kinds = new Map<number, BrowseRowKind>();\n /** True while a settle delay is pending, so one burst renders once. */\n private renderPending = false;\n /** True between asking for a window and the first field of the answer \u2014 see {@link refresh}. */\n private awaitingWindow = false;\n private closed = false;\n\n /**\n * @param client the YNCA client slice (send + get)\n * @param present the browsable subunits this device reported in the AVAIL probe\n * @param delay adapter-managed delay\n */\n public constructor(\n private readonly client: YncaBrowseClient,\n private readonly present: ReadonlySet<string>,\n private readonly delay: (ms: number) => Promise<void>,\n ) {}\n\n /**\n * Attach the engine that renders the windows (set after both are constructed).\n *\n * @param engine the browse engine\n */\n public attach(engine: BrowseEngine): void {\n this.engine = engine;\n }\n\n /** Stop rendering (the controller is closing). */\n public close(): void {\n this.closed = true;\n }\n\n /** @returns the selectable sources this device offers (state value \u2192 label) */\n public sources(): Record<string, string> {\n const entries = YNCA_BROWSE_SOURCES.filter(source => this.present.has(source.subunit));\n return Object.fromEntries(entries.map(source => [source.key, source.label]));\n }\n\n /**\n * Open a source's menu: switch the main-zone input to it (browsing follows the\n * active input, like the remote) and read the current window.\n *\n * @param source the source key (from {@link sources})\n */\n public open(source: string): void {\n const entry = YNCA_BROWSE_SOURCES.find(s => s.key === source && this.present.has(s.subunit));\n if (!entry) {\n return;\n }\n this.active = entry;\n this.resetAssembly();\n this.client.send(\"MAIN\", \"INP\", entry.input);\n this.refresh();\n }\n\n /**\n * Select a visible line \u2014 `@<SUB>:LISTSEL=Line_n` acts like OK on that line.\n *\n * @param line the line number (1\u20138)\n */\n public select(line: number): void {\n this.command(\"LISTSEL\", `Line_${line}`);\n }\n\n /** Show the previous 8 lines. */\n public pageUp(): void {\n this.command(\"LISTPAGE\", \"Up\");\n }\n\n /** Show the next 8 lines. */\n public pageDown(): void {\n this.command(\"LISTPAGE\", \"Down\");\n }\n\n /** Go one menu level back. */\n public back(): void {\n this.command(\"LISTCURSOR\", \"Back\");\n }\n\n /** Return to the menu root. */\n public home(): void {\n this.command(\"LISTCURSOR\", \"Back to Home\");\n }\n\n /** Re-read the current window (`LISTINFO=?` answers with the full field burst). */\n private refresh(): void {\n if (this.active) {\n // Cleared when the ANSWER starts arriving, not here: clearing eagerly would let a\n // render that is already pending paint eight empty lines before the new window shows\n // up. Every reference device answers with all eight lines (empty ones included), so\n // today the assembly is fully overwritten anyway \u2014 this makes it independent of the\n // firmware, for one that sends only the filled lines and would otherwise show the\n // tail of the PREVIOUS window under a shorter menu.\n this.awaitingWindow = true;\n this.client.get(this.active.subunit, \"LISTINFO\");\n }\n }\n\n /**\n * Feed one received YNCA line. Lines of the active subunit's list functions update\n * the window assembly; anything else is ignored. Called by the controller for every\n * message, so unsolicited auto-feedback keeps the window live.\n *\n * @param message the decoded YNCA message\n * @param message.subunit the message's subunit\n * @param message.func the message's function name\n * @param message.value the message's raw wire value\n */\n public handleMessage(message: { subunit: string; func: string; value: string }): void {\n if (!this.active || message.subunit !== this.active.subunit) {\n return;\n }\n const isWindowField =\n /^LINE[1-8](TXT|ATRIB)$/.test(message.func) ||\n [\"LISTLAYERNAME\", \"LISTLAYER\", \"MAXLINE\", \"CURRLINE\"].includes(message.func);\n if (isWindowField && this.awaitingWindow) {\n // The first field of the answer we asked for: everything still standing belongs to the\n // window before it.\n this.awaitingWindow = false;\n this.resetAssembly();\n }\n const line = /^LINE([1-8])(TXT|ATRIB)$/.exec(message.func);\n if (line) {\n const n = Number(line[1]);\n if (line[2] === \"TXT\") {\n this.texts.set(n, message.value);\n } else {\n this.kinds.set(n, ROW_KIND_BY_ATTRIBUTE[message.value] ?? \"item\");\n }\n } else if (message.func === \"LISTLAYERNAME\") {\n this.menuName = message.value;\n } else if (message.func === \"LISTLAYER\") {\n this.layer = Number(message.value) || 0;\n } else if (message.func === \"MAXLINE\") {\n this.totalItems = Number(message.value) || 0;\n } else if (message.func === \"CURRLINE\") {\n this.currentLine = Number(message.value) || 1;\n } else {\n return;\n }\n this.scheduleRender();\n }\n\n /**\n * Send a navigation command to the active subunit and read the window back.\n *\n * @param func the list function (LISTSEL, LISTPAGE, LISTCURSOR)\n * @param value the wire value\n */\n private command(func: string, value: string): void {\n if (!this.active) {\n return;\n }\n this.client.send(this.active.subunit, func, value);\n this.refresh();\n }\n\n /** Clear the window assembly \u2014 on a source switch, and when a freshly asked window starts arriving. */\n private resetAssembly(): void {\n this.awaitingWindow = false;\n this.menuName = \"\";\n this.layer = 0;\n this.totalItems = 0;\n this.currentLine = 1;\n this.texts.clear();\n this.kinds.clear();\n }\n\n /** Render once the current burst has settled (avoids 8+ renders per LISTINFO answer). */\n private scheduleRender(): void {\n if (this.renderPending) {\n return;\n }\n this.renderPending = true;\n void this.delay(BURST_SETTLE_MS).then(() => {\n this.renderPending = false;\n if (!this.closed) {\n this.render();\n }\n });\n }\n\n /** Assemble the window from the collected fields and hand it to the engine. */\n private render(): void {\n const rows: BrowseRow[] = [];\n for (let line = 1; line <= 8; line++) {\n const text = this.texts.get(line) ?? \"\";\n if (text.length > 0) {\n rows.push({ line, text, kind: this.kinds.get(line) ?? \"item\" });\n }\n }\n this.engine?.onWindow({\n menuName: this.menuName,\n layer: this.layer,\n totalItems: this.totalItems,\n currentLine: this.currentLine,\n rows,\n });\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -28,6 +28,7 @@ function capitalize(segment) {
|
|
|
28
28
|
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
29
29
|
}
|
|
30
30
|
function catalogToObjects(entries) {
|
|
31
|
+
var _a;
|
|
31
32
|
const objects = [];
|
|
32
33
|
const channels = /* @__PURE__ */ new Set();
|
|
33
34
|
for (const entry of entries) {
|
|
@@ -47,7 +48,11 @@ function catalogToObjects(entries) {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
const common = (0, import_value_coerce.specToCommon)(entry.spec, { write: entry.write, role: entry.role });
|
|
50
|
-
objects.push({
|
|
51
|
+
objects.push({
|
|
52
|
+
id: entry.id,
|
|
53
|
+
type: "state",
|
|
54
|
+
common: { name: (0, import_i18n.tName)(entry.nameKey, ...(_a = entry.nameArgs) != null ? _a : []), ...common }
|
|
55
|
+
});
|
|
51
56
|
}
|
|
52
57
|
return objects;
|
|
53
58
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/catalog/build-objects.ts"],
|
|
4
|
-
"sourcesContent": ["import { specToCommon } from \"./value-coerce\";\nimport { CHANNEL_NAME_KEYS, type CatalogEntry, type ObjectDef } from \"./types\";\nimport { tName } from \"../i18n\";\n\n/**\n * Capitalise the first character, as the fallback channel name for a channel id\n * not in {@link CHANNEL_NAME_KEYS}.\n *\n * @param segment the channel id segment\n * @returns the segment with its first character upper-cased\n */\nfunction capitalize(segment: string): string {\n return segment.charAt(0).toUpperCase() + segment.slice(1);\n}\n\n/**\n * Turn catalog entries into the object tree: a channel object for every dotted\n * parent path (created once, before its states) and a typed state object per\n * entry. The caller passes only the entries the device actually reports, so the\n * tree is device-specific while the catalog stays device-agnostic.\n *\n * @param entries the catalog entries that apply to the device\n * @returns the object definitions to create, parents before children\n */\nexport function catalogToObjects(entries: CatalogEntry[]): ObjectDef[] {\n const objects: ObjectDef[] = [];\n const channels = new Set<string>();\n for (const entry of entries) {\n const segments = entry.id.split(\".\");\n for (let i = 1; i < segments.length; i++) {\n const channelId = segments.slice(0, i).join(\".\");\n if (!channels.has(channelId)) {\n channels.add(channelId);\n const segment = segments[i - 1];\n objects.push({\n id: channelId,\n type: \"channel\",\n // A listed channel is translated; an unlisted one keeps its capitalised id, which is\n // a device-derived name and therefore has no translation to give.\n common: { name: CHANNEL_NAME_KEYS[segment] ? tName(CHANNEL_NAME_KEYS[segment]) : capitalize(segment) },\n });\n }\n }\n const common = specToCommon(entry.spec, { write: entry.write, role: entry.role });\n objects.push({
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAA6B;AAC7B,mBAAqE;AACrE,kBAAsB;AAStB,SAAS,WAAW,SAAyB;AAC3C,SAAO,QAAQ,OAAO,CAAC,EAAE,YAAY,IAAI,QAAQ,MAAM,CAAC;AAC1D;AAWO,SAAS,iBAAiB,SAAsC;
|
|
4
|
+
"sourcesContent": ["import { specToCommon } from \"./value-coerce\";\nimport { CHANNEL_NAME_KEYS, type CatalogEntry, type ObjectDef } from \"./types\";\nimport { tName } from \"../i18n\";\n\n/**\n * Capitalise the first character, as the fallback channel name for a channel id\n * not in {@link CHANNEL_NAME_KEYS}.\n *\n * @param segment the channel id segment\n * @returns the segment with its first character upper-cased\n */\nfunction capitalize(segment: string): string {\n return segment.charAt(0).toUpperCase() + segment.slice(1);\n}\n\n/**\n * Turn catalog entries into the object tree: a channel object for every dotted\n * parent path (created once, before its states) and a typed state object per\n * entry. The caller passes only the entries the device actually reports, so the\n * tree is device-specific while the catalog stays device-agnostic.\n *\n * @param entries the catalog entries that apply to the device\n * @returns the object definitions to create, parents before children\n */\nexport function catalogToObjects(entries: CatalogEntry[]): ObjectDef[] {\n const objects: ObjectDef[] = [];\n const channels = new Set<string>();\n for (const entry of entries) {\n const segments = entry.id.split(\".\");\n for (let i = 1; i < segments.length; i++) {\n const channelId = segments.slice(0, i).join(\".\");\n if (!channels.has(channelId)) {\n channels.add(channelId);\n const segment = segments[i - 1];\n objects.push({\n id: channelId,\n type: \"channel\",\n // A listed channel is translated; an unlisted one keeps its capitalised id, which is\n // a device-derived name and therefore has no translation to give.\n common: { name: CHANNEL_NAME_KEYS[segment] ? tName(CHANNEL_NAME_KEYS[segment]) : capitalize(segment) },\n });\n }\n }\n const common = specToCommon(entry.spec, { write: entry.write, role: entry.role });\n objects.push({\n id: entry.id,\n type: \"state\",\n common: { name: tName(entry.nameKey, ...(entry.nameArgs ?? [])), ...common },\n });\n }\n return objects;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAA6B;AAC7B,mBAAqE;AACrE,kBAAsB;AAStB,SAAS,WAAW,SAAyB;AAC3C,SAAO,QAAQ,OAAO,CAAC,EAAE,YAAY,IAAI,QAAQ,MAAM,CAAC;AAC1D;AAWO,SAAS,iBAAiB,SAAsC;AAxBvE;AAyBE,QAAM,UAAuB,CAAC;AAC9B,QAAM,WAAW,oBAAI,IAAY;AACjC,aAAW,SAAS,SAAS;AAC3B,UAAM,WAAW,MAAM,GAAG,MAAM,GAAG;AACnC,aAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACxC,YAAM,YAAY,SAAS,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG;AAC/C,UAAI,CAAC,SAAS,IAAI,SAAS,GAAG;AAC5B,iBAAS,IAAI,SAAS;AACtB,cAAM,UAAU,SAAS,IAAI,CAAC;AAC9B,gBAAQ,KAAK;AAAA,UACX,IAAI;AAAA,UACJ,MAAM;AAAA;AAAA;AAAA,UAGN,QAAQ,EAAE,MAAM,+BAAkB,OAAO,QAAI,mBAAM,+BAAkB,OAAO,CAAC,IAAI,WAAW,OAAO,EAAE;AAAA,QACvG,CAAC;AAAA,MACH;AAAA,IACF;AACA,UAAM,aAAS,kCAAa,MAAM,MAAM,EAAE,OAAO,MAAM,OAAO,MAAM,MAAM,KAAK,CAAC;AAChF,YAAQ,KAAK;AAAA,MACX,IAAI,MAAM;AAAA,MACV,MAAM;AAAA,MACN,QAAQ,EAAE,UAAM,mBAAM,MAAM,SAAS,IAAI,WAAM,aAAN,YAAkB,CAAC,CAAE,GAAG,GAAG,OAAO;AAAA,IAC7E,CAAC;AAAA,EACH;AACA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -21,8 +21,7 @@ __export(owner_policy_exports, {
|
|
|
21
21
|
ZONE_PREFIX: () => ZONE_PREFIX,
|
|
22
22
|
canonicalIdOf: () => canonicalIdOf,
|
|
23
23
|
capabilityKeyOf: () => capabilityKeyOf,
|
|
24
|
-
pickOwner: () => pickOwner
|
|
25
|
-
resolveOwnership: () => resolveOwnership
|
|
24
|
+
pickOwner: () => pickOwner
|
|
26
25
|
});
|
|
27
26
|
module.exports = __toCommonJS(owner_policy_exports);
|
|
28
27
|
const MODERNITY = ["yxc", "ynca", "xml"];
|
|
@@ -83,31 +82,11 @@ function pickOwner(key, candidates) {
|
|
|
83
82
|
const owner = (_b = preference.find((t) => candidates.includes(t))) != null ? _b : MODERNITY.find((t) => candidates.includes(t));
|
|
84
83
|
return owner != null ? owner : candidates[0];
|
|
85
84
|
}
|
|
86
|
-
function resolveOwnership(offered) {
|
|
87
|
-
var _a;
|
|
88
|
-
const candidates = /* @__PURE__ */ new Map();
|
|
89
|
-
for (const transport of MODERNITY) {
|
|
90
|
-
for (const key of (_a = offered[transport]) != null ? _a : []) {
|
|
91
|
-
const list = candidates.get(key);
|
|
92
|
-
if (list) {
|
|
93
|
-
list.push(transport);
|
|
94
|
-
} else {
|
|
95
|
-
candidates.set(key, [transport]);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
const owners = /* @__PURE__ */ new Map();
|
|
100
|
-
for (const [key, cands] of candidates) {
|
|
101
|
-
owners.set(key, pickOwner(key, cands));
|
|
102
|
-
}
|
|
103
|
-
return owners;
|
|
104
|
-
}
|
|
105
85
|
// Annotate the CommonJS export names for ESM import in node:
|
|
106
86
|
0 && (module.exports = {
|
|
107
87
|
ZONE_PREFIX,
|
|
108
88
|
canonicalIdOf,
|
|
109
89
|
capabilityKeyOf,
|
|
110
|
-
pickOwner
|
|
111
|
-
resolveOwnership
|
|
90
|
+
pickOwner
|
|
112
91
|
});
|
|
113
92
|
//# sourceMappingURL=owner-policy.js.map
|