iobroker.yamaha 2.0.2 → 2.0.4
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 +14 -18
- package/admin/i18n/de.json +0 -2
- package/admin/i18n/en.json +0 -2
- package/admin/i18n/es.json +0 -2
- package/admin/i18n/fr.json +0 -2
- package/admin/i18n/it.json +0 -2
- package/admin/i18n/nl.json +0 -2
- package/admin/i18n/pl.json +0 -2
- package/admin/i18n/pt.json +0 -2
- package/admin/i18n/ru.json +0 -2
- package/admin/i18n/uk.json +0 -2
- package/admin/i18n/zh-cn.json +0 -2
- package/build/lib/browse/yxc-browse-driver.js +1 -1
- package/build/lib/browse/yxc-browse-driver.js.map +2 -2
- package/build/lib/catalog/object-tree-coordinator.js +1 -7
- package/build/lib/catalog/object-tree-coordinator.js.map +2 -2
- package/build/lib/catalog/owner-policy.js +7 -0
- package/build/lib/catalog/owner-policy.js.map +2 -2
- package/build/lib/lifecycle/reconnect-strategy.js +1 -1
- package/build/lib/lifecycle/reconnect-strategy.js.map +2 -2
- package/build/lib/pure-helpers.js +0 -3
- package/build/lib/pure-helpers.js.map +2 -2
- package/build/lib/util.js +3 -0
- package/build/lib/util.js.map +2 -2
- package/build/lib/xml/device-controller.js +54 -39
- package/build/lib/xml/device-controller.js.map +2 -2
- package/build/lib/xml/protocol.js +18 -0
- package/build/lib/xml/protocol.js.map +2 -2
- package/build/lib/xml/xml-client.js +11 -2
- package/build/lib/xml/xml-client.js.map +2 -2
- package/build/lib/ynca/ynca-client.js +7 -3
- package/build/lib/ynca/ynca-client.js.map +2 -2
- package/build/lib/yxc/device-controller.js +13 -8
- package/build/lib/yxc/device-controller.js.map +2 -2
- package/build/lib/yxc/http-client.js +10 -1
- package/build/lib/yxc/http-client.js.map +2 -2
- package/build/lib/yxc/object-mapper.js +12 -40
- package/build/lib/yxc/object-mapper.js.map +2 -2
- package/build/lib/yxc/zones.js +0 -9
- package/build/lib/yxc/zones.js.map +2 -2
- package/build/main.js +90 -12
- package/build/main.js.map +2 -2
- package/io-package.json +27 -27
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -103,6 +103,20 @@ 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.0.4 (2026-09-02)
|
|
107
|
+
|
|
108
|
+
- (krobipd) Fixed: a short outage of the ioBroker database no longer restarts the adapter — a failed write is logged once and the next value is written normally
|
|
109
|
+
- (krobipd) Fixed: the protocol badges on the device card are reset when a device starts and when the adapter stops, so a crash no longer leaves a green badge next to a red dot
|
|
110
|
+
- (krobipd) Fixed: stopping or restarting the adapter while it is still searching the network now stops it completely — it no longer keeps working half-started until the next restart
|
|
111
|
+
- (krobipd) Fixed: the scene list keeps its titles on MusicCast receivers that also answer over XML or YNCA — the title-less MusicCast list no longer replaces it at startup
|
|
112
|
+
- (krobipd) Fixed: a timeout while probing an older receiver for scenes or menus is asked again on the next connect instead of being remembered as "none" until the next restart
|
|
113
|
+
- (krobipd) Fixed: writing one equalizer band before the receiver reported all three no longer sends zeros for the other two — the zone status is fetched first
|
|
114
|
+
- (krobipd) Improved: a receiver that is off for a while is retried no later than the configured maximum wait, and an oversized answer from a wrong host can no longer eat the adapter's memory
|
|
115
|
+
|
|
116
|
+
### 2.0.3 (2026-09-01)
|
|
117
|
+
|
|
118
|
+
- (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
|
|
119
|
+
|
|
106
120
|
### 2.0.2 (2026-09-01)
|
|
107
121
|
|
|
108
122
|
- (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
|
|
@@ -124,24 +138,6 @@ On the very first contact the adapter asks the receiver which functions it suppo
|
|
|
124
138
|
- (krobipd) Fixed: the on-screen remote keys and the tuner preset stepping on MusicCast devices count as button presses again — they no longer wait behind a running background refresh
|
|
125
139
|
- (krobipd) Fixed: restarting or updating the adapter while a receiver was still connecting no longer produces shutdown warnings about timers in the log
|
|
126
140
|
|
|
127
|
-
### 1.7.0 (2026-09-01)
|
|
128
|
-
|
|
129
|
-
- (krobipd) Fixed: scene recall now uses the command each device declares itself and the protocol that can deliver it — on 2012-generation receivers a scene write did nothing at all (#615)
|
|
130
|
-
- (krobipd) New: scenes show their titles ("Movie Viewing", …) as datapoints and in the recall dropdown, and zones with their own scenes get their own scene recall
|
|
131
|
-
- (krobipd) Fixed: the menu back button falls back to the older cursor command when a device refuses the standard one, so leaving a folder works on 2012-generation receivers too (#613)
|
|
132
|
-
- (krobipd) New: the adapter now reports when the receiver refuses a command, including the device's own answer — a dead button no longer fails without a trace
|
|
133
|
-
- (krobipd) New: bass and treble on MusicCast-generation receivers over YNCA, dialogue level, DAB signal details, the paired Bluetooth device name and more — the device is asked in its own dialect
|
|
134
|
-
- (krobipd) New: on-screen remote control datapoints (cursor pad and menu keys), the current audio signal info, MusicCast playlists and the play queue on MusicCast devices
|
|
135
|
-
- (krobipd) New: receivers from before 2010 get their tuner back — preset recall, frequency and radio text — and every input dropdown lists exactly the inputs the device really has
|
|
136
|
-
- (krobipd) New: the receiver's IP address is shown as a datapoint, so diagnosis and the device's own web pages are one click away
|
|
137
|
-
- (krobipd) Improved: restarts are fast — the adapter remembers what each device can do, brings it online in seconds and refreshes values in the background; only the first contact asks everything
|
|
138
|
-
- (krobipd) Improved: known devices no longer wait for the network search at startup — it runs in the background and only adds newcomers
|
|
139
|
-
|
|
140
|
-
### 1.6.0 (2026-08-27)
|
|
141
|
-
|
|
142
|
-
- (krobipd) New: three states show how many receivers are set up, how many are connected right now and whether that is all of them — one line to watch instead of every device
|
|
143
|
-
- (krobipd) Fixed: a receiver kept showing as connected while the adapter was stopped, and after a crash it stayed that way until it answered again — both now show the truth
|
|
144
|
-
|
|
145
141
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
146
142
|
|
|
147
143
|
## History
|
package/admin/i18n/de.json
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"header_devices": "Geräte",
|
|
3
|
-
"introText": "Lass die Tabelle leer, dann findet der Adapter MusicCast-Geräte beim Start selbst im Netzwerk — nichts einzutragen. Trage Geräte (Name + IP) ein, um stattdessen nur diese zu nutzen. Ältere Receiver (vor ~2010, XML-Protokoll) melden sich nicht im Netzwerk und müssen hier immer manuell eingetragen werden.",
|
|
4
|
-
"devicesTable": "Yamaha-Geräte",
|
|
5
3
|
"columnName": "Name",
|
|
6
4
|
"columnIp": "IP-Adresse",
|
|
7
5
|
"label_xmlPollInterval": "XML-Abfrageintervall (Sekunden)",
|
package/admin/i18n/en.json
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"header_devices": "Devices",
|
|
3
|
-
"introText": "Leave the table empty and the adapter finds MusicCast devices on the network by itself at startup — nothing to enter. Add devices (name + IP) to use only those instead. Older receivers (before ~2010, XML protocol) do not announce themselves on the network and must always be added here manually.",
|
|
4
|
-
"devicesTable": "Yamaha devices",
|
|
5
3
|
"columnName": "Name",
|
|
6
4
|
"columnIp": "IP address",
|
|
7
5
|
"label_xmlPollInterval": "XML query interval (seconds)",
|
package/admin/i18n/es.json
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"header_devices": "Dispositivos",
|
|
3
|
-
"introText": "Deja la tabla vacía y el adaptador encuentra por sí mismo los dispositivos MusicCast en la red al iniciar — nada que introducir. Añade dispositivos (nombre + IP) para usar solo esos. Los receptores antiguos (antes de ~2010, protocolo XML) no se anuncian en la red y siempre se añaden aquí manualmente.",
|
|
4
|
-
"devicesTable": "Dispositivos Yamaha",
|
|
5
3
|
"columnName": "Nombre",
|
|
6
4
|
"columnIp": "Dirección IP",
|
|
7
5
|
"label_xmlPollInterval": "Intervalo de consulta XML (segundos)",
|
package/admin/i18n/fr.json
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"header_devices": "Appareils",
|
|
3
|
-
"introText": "Laissez le tableau vide et l'adaptateur trouve lui-même les appareils MusicCast sur le réseau au démarrage — rien à saisir. Ajoutez des appareils (nom + IP) pour n'utiliser que ceux-là. Les récepteurs anciens (avant ~2010, protocole XML) ne s'annoncent pas sur le réseau et doivent toujours être ajoutés ici manuellement.",
|
|
4
|
-
"devicesTable": "Appareils Yamaha",
|
|
5
3
|
"columnName": "Nom",
|
|
6
4
|
"columnIp": "Adresse IP",
|
|
7
5
|
"label_xmlPollInterval": "Intervalle d'interrogation XML (secondes)",
|
package/admin/i18n/it.json
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"header_devices": "Dispositivi",
|
|
3
|
-
"introText": "Lascia la tabella vuota e l'adattatore trova da solo i dispositivi MusicCast sulla rete all'avvio — niente da inserire. Aggiungi dispositivi (nome + IP) per usare solo quelli. I ricevitori più vecchi (prima del ~2010, protocollo XML) non si annunciano sulla rete e vanno sempre aggiunti qui manualmente.",
|
|
4
|
-
"devicesTable": "Dispositivi Yamaha",
|
|
5
3
|
"columnName": "Nome",
|
|
6
4
|
"columnIp": "Indirizzo IP",
|
|
7
5
|
"label_xmlPollInterval": "Intervallo di interrogazione XML (secondi)",
|
package/admin/i18n/nl.json
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"header_devices": "Apparaten",
|
|
3
|
-
"introText": "Laat de tabel leeg en de adapter vindt bij het opstarten zelf MusicCast-apparaten op het netwerk — niets in te voeren. Voeg apparaten (naam + IP) toe om alleen die te gebruiken. Oudere receivers (van vóór ~2010, XML-protocol) melden zich niet op het netwerk en moeten hier altijd handmatig worden toegevoegd.",
|
|
4
|
-
"devicesTable": "Yamaha-apparaten",
|
|
5
3
|
"columnName": "Naam",
|
|
6
4
|
"columnIp": "IP-adres",
|
|
7
5
|
"label_xmlPollInterval": "XML-opvraaginterval (seconden)",
|
package/admin/i18n/pl.json
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"header_devices": "Urządzenia",
|
|
3
|
-
"introText": "Pozostaw tabelę pustą, a adapter sam znajdzie urządzenia MusicCast w sieci przy starcie — nic nie trzeba wpisywać. Dodaj urządzenia (nazwa + IP), aby używać tylko ich. Starsze amplitunery (sprzed ~2010, protokół XML) nie zgłaszają się w sieci i zawsze dodaje się je tutaj ręcznie.",
|
|
4
|
-
"devicesTable": "Urządzenia Yamaha",
|
|
5
3
|
"columnName": "Nazwa",
|
|
6
4
|
"columnIp": "Adres IP",
|
|
7
5
|
"label_xmlPollInterval": "Interwał odpytywania XML (sekundy)",
|
package/admin/i18n/pt.json
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"header_devices": "Dispositivos",
|
|
3
|
-
"introText": "Deixe a tabela vazia e o adaptador encontra sozinho os dispositivos MusicCast na rede ao arrancar — nada a introduzir. Adicione dispositivos (nome + IP) para usar apenas esses. Recetores antigos (antes de ~2010, protocolo XML) não se anunciam na rede e devem ser sempre adicionados aqui manualmente.",
|
|
4
|
-
"devicesTable": "Dispositivos Yamaha",
|
|
5
3
|
"columnName": "Nome",
|
|
6
4
|
"columnIp": "Endereço IP",
|
|
7
5
|
"label_xmlPollInterval": "Intervalo de consulta XML (segundos)",
|
package/admin/i18n/ru.json
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"header_devices": "Устройства",
|
|
3
|
-
"introText": "Оставьте таблицу пустой, и адаптер при запуске сам найдёт устройства MusicCast в сети — ничего вводить не нужно. Добавьте устройства (имя + IP), чтобы использовать только их. Старые ресиверы (до ~2010, протокол XML) не объявляют себя в сети и всегда добавляются здесь вручную.",
|
|
4
|
-
"devicesTable": "Устройства Yamaha",
|
|
5
3
|
"columnName": "Имя",
|
|
6
4
|
"columnIp": "IP-адрес",
|
|
7
5
|
"label_xmlPollInterval": "Интервал опроса XML (секунды)",
|
package/admin/i18n/uk.json
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"header_devices": "Пристрої",
|
|
3
|
-
"introText": "Залиште таблицю порожньою, і адаптер під час запуску сам знайде пристрої MusicCast у мережі — нічого вводити не потрібно. Додайте пристрої (ім'я + IP), щоб використовувати лише їх. Старіші ресивери (до ~2010, протокол XML) не оголошують себе в мережі й завжди додаються тут вручну.",
|
|
4
|
-
"devicesTable": "Пристрої Yamaha",
|
|
5
3
|
"columnName": "Назва",
|
|
6
4
|
"columnIp": "IP-адреса",
|
|
7
5
|
"label_xmlPollInterval": "Інтервал опитування XML (секунди)",
|
package/admin/i18n/zh-cn.json
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"header_devices": "设备",
|
|
3
|
-
"introText": "将表格留空,适配器会在启动时自动在网络中查找 MusicCast 设备——无需输入。添加设备(名称 + IP)则只使用这些设备。较旧的接收机(约 2010 年之前,XML 协议)不会在网络中通告自己,必须始终在此手动添加。",
|
|
4
|
-
"devicesTable": "Yamaha 设备",
|
|
5
3
|
"columnName": "名称",
|
|
6
4
|
"columnIp": "IP 地址",
|
|
7
5
|
"label_xmlPollInterval": "XML 查询间隔(秒)",
|
|
@@ -148,7 +148,7 @@ class YxcBrowseDriver {
|
|
|
148
148
|
return void 0;
|
|
149
149
|
}
|
|
150
150
|
const response = await this.client.getListInfo(this.active.input, this.index);
|
|
151
|
-
if (!response
|
|
151
|
+
if (!response) {
|
|
152
152
|
return void 0;
|
|
153
153
|
}
|
|
154
154
|
const entries = Array.isArray(response.list_info) ? response.list_info : [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/browse/yxc-browse-driver.ts"],
|
|
4
|
-
"sourcesContent": ["import type { BrowseDriver, BrowseRow } from \"./types\";\nimport type { BrowseEngine } from \"./browse-engine\";\n\n/** The engine's page size \u2014 YXC serves any window, we keep the device's 8-line form. */\nconst PAGE_SIZE = 8;\n/** Upper bound for the return-to-root loop (menus are never this deep). */\nconst MAX_HOME_STEPS = 16;\n\n/**\n * The netusb inputs the YXC list API can browse (aiomusiccast BROWSABLE_INPUTS),\n * with the transport-neutral source key and display label. The driver offers the\n * intersection with the device's own input list.\n */\nexport const YXC_BROWSE_SOURCES: ReadonlyArray<{ input: string; key: string; label: string }> = [\n { input: \"net_radio\", key: \"netRadio\", label: \"Net Radio\" },\n { input: \"server\", key: \"server\", label: \"Media server\" },\n { input: \"usb\", key: \"usb\", label: \"USB\" },\n { input: \"napster\", key: \"napster\", label: \"Napster\" },\n { input: \"pandora\", key: \"pandora\", label: \"Pandora\" },\n { input: \"rhapsody\", key: \"rhapsody\", label: \"Rhapsody\" },\n { input: \"siriusxm\", key: \"sirius\", label: \"SiriusXM\" },\n { input: \"juke\", key: \"juke\", label: \"JUKE\" },\n { input: \"radiko\", key: \"radiko\", label: \"radiko\" },\n { input: \"qobuz\", key: \"qobuz\", label: \"Qobuz\" },\n { input: \"deezer\", key: \"deezer\", label: \"Deezer\" },\n { input: \"amazon_music\", key: \"amazonMusic\", label: \"Amazon Music\" },\n];\n\n/** The client surface the driver needs (a slice of the YXC client). */\nexport interface YxcBrowseClient {\n /** Read one window of a netusb source's list. */\n getListInfo(input: string, index: number, size?: number): Promise<unknown>;\n /** Drive the netusb list: select/play an absolute index, or go one level back. */\n setListControl(type: \"select\" | \"play\" | \"return\", index?: number, zone?: string): Promise<unknown>;\n}\n\n/** One raw entry of a getListInfo response. */\ninterface RawListEntry {\n /** The entry's text. */\n text?: unknown;\n /** The capability bitmask (b1 selectable, b2 playable). */\n attribute?: unknown;\n /** The entry's thumbnail URL. */\n thumbnail?: unknown;\n}\n\n/**\n * The YXC (MusicCast) list driver over `netusb/getListInfo` + `netusb/setListControl`.\n * Pull-based: every operation fetches the resulting window and hands it to the engine.\n * The absolute entry index is tracked here (the API pages by index, not by cursor);\n * playing an item hands the playback to the netusb input automatically.\n */\nexport class YxcBrowseDriver implements BrowseDriver {\n private engine: BrowseEngine | undefined;\n private active: { input: string; key: string } | undefined;\n /** The absolute (0-based) index of the window's first entry. */\n private index = 0;\n /** The last window's rows, for the select semantics (folder vs playable). */\n private rows: BrowseRow[] = [];\n private totalItems = 0;\n\n /**\n * @param client the YXC client slice (getListInfo + setListControl)\n * @param inputList the device's netusb input list (getFeatures `input_list`)\n */\n public constructor(\n private readonly client: YxcBrowseClient,\n private readonly inputList: readonly string[],\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 = YXC_BROWSE_SOURCES.filter(source => this.inputList.includes(source.input));\n return Object.fromEntries(entries.map(source => [source.key, source.label]));\n }\n\n /**\n * Open a source's list at the device's current position in it.\n *\n * @param source the source key (from {@link sources})\n */\n public async open(source: string): Promise<void> {\n const entry = YXC_BROWSE_SOURCES.find(s => s.key === source && this.inputList.includes(s.input));\n if (!entry) {\n return;\n }\n this.active = entry;\n this.index = 0;\n await this.fetch();\n }\n\n /**\n * Select a visible line: a playable item starts (the device switches to netusb),\n * a folder opens.\n *\n * @param line the line number (1\u20138)\n */\n public async select(line: number): Promise<void> {\n const row = this.rows.find(r => r.line === line);\n if (!this.active || !row || row.kind === \"unselectable\" || row.kind === \"unplayable\") {\n return;\n }\n const absolute = this.index + line - 1;\n if (row.kind === \"item\") {\n await this.client.setListControl(\"play\", absolute, \"main\");\n await this.fetch();\n return;\n }\n await this.client.setListControl(\"select\", absolute);\n this.index = 0;\n await this.fetch();\n }\n\n /** Show the previous 8 lines. */\n public async pageUp(): Promise<void> {\n this.index = Math.max(0, this.index - PAGE_SIZE);\n await this.fetch();\n }\n\n /** Show the next 8 lines. */\n public async pageDown(): Promise<void> {\n if (this.index + PAGE_SIZE < this.totalItems) {\n this.index += PAGE_SIZE;\n }\n await this.fetch();\n }\n\n /** Go one menu level back. */\n public async back(): Promise<void> {\n if (!this.active) {\n return;\n }\n await this.client.setListControl(\"return\");\n this.index = 0;\n await this.fetch();\n }\n\n /** Return to the menu root: step back until the device reports layer 1. */\n public async home(): Promise<void> {\n if (!this.active) {\n return;\n }\n for (let step = 0; step < MAX_HOME_STEPS; step++) {\n const layer = await this.fetch();\n if (layer === undefined || layer <= 1) {\n return;\n }\n await this.client.setListControl(\"return\");\n this.index = 0;\n }\n }\n\n /**\n * Fetch the current window, render it to the engine, and report the menu layer.\n *\n * @returns the menu layer, or undefined when the source answered with an error\n */\n private async fetch(): Promise<number | undefined> {\n if (!this.active) {\n return undefined;\n }\n const response = (await this.client.getListInfo(this.active.input, this.index)) as Record<string, unknown> | null;\n if (!response
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import type { BrowseDriver, BrowseRow } from \"./types\";\nimport type { BrowseEngine } from \"./browse-engine\";\n\n/** The engine's page size \u2014 YXC serves any window, we keep the device's 8-line form. */\nconst PAGE_SIZE = 8;\n/** Upper bound for the return-to-root loop (menus are never this deep). */\nconst MAX_HOME_STEPS = 16;\n\n/**\n * The netusb inputs the YXC list API can browse (aiomusiccast BROWSABLE_INPUTS),\n * with the transport-neutral source key and display label. The driver offers the\n * intersection with the device's own input list.\n */\nexport const YXC_BROWSE_SOURCES: ReadonlyArray<{ input: string; key: string; label: string }> = [\n { input: \"net_radio\", key: \"netRadio\", label: \"Net Radio\" },\n { input: \"server\", key: \"server\", label: \"Media server\" },\n { input: \"usb\", key: \"usb\", label: \"USB\" },\n { input: \"napster\", key: \"napster\", label: \"Napster\" },\n { input: \"pandora\", key: \"pandora\", label: \"Pandora\" },\n { input: \"rhapsody\", key: \"rhapsody\", label: \"Rhapsody\" },\n { input: \"siriusxm\", key: \"sirius\", label: \"SiriusXM\" },\n { input: \"juke\", key: \"juke\", label: \"JUKE\" },\n { input: \"radiko\", key: \"radiko\", label: \"radiko\" },\n { input: \"qobuz\", key: \"qobuz\", label: \"Qobuz\" },\n { input: \"deezer\", key: \"deezer\", label: \"Deezer\" },\n { input: \"amazon_music\", key: \"amazonMusic\", label: \"Amazon Music\" },\n];\n\n/** The client surface the driver needs (a slice of the YXC client). */\nexport interface YxcBrowseClient {\n /** Read one window of a netusb source's list. */\n getListInfo(input: string, index: number, size?: number): Promise<unknown>;\n /** Drive the netusb list: select/play an absolute index, or go one level back. */\n setListControl(type: \"select\" | \"play\" | \"return\", index?: number, zone?: string): Promise<unknown>;\n}\n\n/** One raw entry of a getListInfo response. */\ninterface RawListEntry {\n /** The entry's text. */\n text?: unknown;\n /** The capability bitmask (b1 selectable, b2 playable). */\n attribute?: unknown;\n /** The entry's thumbnail URL. */\n thumbnail?: unknown;\n}\n\n/**\n * The YXC (MusicCast) list driver over `netusb/getListInfo` + `netusb/setListControl`.\n * Pull-based: every operation fetches the resulting window and hands it to the engine.\n * The absolute entry index is tracked here (the API pages by index, not by cursor);\n * playing an item hands the playback to the netusb input automatically.\n */\nexport class YxcBrowseDriver implements BrowseDriver {\n private engine: BrowseEngine | undefined;\n private active: { input: string; key: string } | undefined;\n /** The absolute (0-based) index of the window's first entry. */\n private index = 0;\n /** The last window's rows, for the select semantics (folder vs playable). */\n private rows: BrowseRow[] = [];\n private totalItems = 0;\n\n /**\n * @param client the YXC client slice (getListInfo + setListControl)\n * @param inputList the device's netusb input list (getFeatures `input_list`)\n */\n public constructor(\n private readonly client: YxcBrowseClient,\n private readonly inputList: readonly string[],\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 = YXC_BROWSE_SOURCES.filter(source => this.inputList.includes(source.input));\n return Object.fromEntries(entries.map(source => [source.key, source.label]));\n }\n\n /**\n * Open a source's list at the device's current position in it.\n *\n * @param source the source key (from {@link sources})\n */\n public async open(source: string): Promise<void> {\n const entry = YXC_BROWSE_SOURCES.find(s => s.key === source && this.inputList.includes(s.input));\n if (!entry) {\n return;\n }\n this.active = entry;\n this.index = 0;\n await this.fetch();\n }\n\n /**\n * Select a visible line: a playable item starts (the device switches to netusb),\n * a folder opens.\n *\n * @param line the line number (1\u20138)\n */\n public async select(line: number): Promise<void> {\n const row = this.rows.find(r => r.line === line);\n if (!this.active || !row || row.kind === \"unselectable\" || row.kind === \"unplayable\") {\n return;\n }\n const absolute = this.index + line - 1;\n if (row.kind === \"item\") {\n await this.client.setListControl(\"play\", absolute, \"main\");\n await this.fetch();\n return;\n }\n await this.client.setListControl(\"select\", absolute);\n this.index = 0;\n await this.fetch();\n }\n\n /** Show the previous 8 lines. */\n public async pageUp(): Promise<void> {\n this.index = Math.max(0, this.index - PAGE_SIZE);\n await this.fetch();\n }\n\n /** Show the next 8 lines. */\n public async pageDown(): Promise<void> {\n if (this.index + PAGE_SIZE < this.totalItems) {\n this.index += PAGE_SIZE;\n }\n await this.fetch();\n }\n\n /** Go one menu level back. */\n public async back(): Promise<void> {\n if (!this.active) {\n return;\n }\n await this.client.setListControl(\"return\");\n this.index = 0;\n await this.fetch();\n }\n\n /** Return to the menu root: step back until the device reports layer 1. */\n public async home(): Promise<void> {\n if (!this.active) {\n return;\n }\n for (let step = 0; step < MAX_HOME_STEPS; step++) {\n const layer = await this.fetch();\n if (layer === undefined || layer <= 1) {\n return;\n }\n await this.client.setListControl(\"return\");\n this.index = 0;\n }\n }\n\n /**\n * Fetch the current window, render it to the engine, and report the menu layer.\n *\n * @returns the menu layer, or undefined when the source answered with an error\n */\n private async fetch(): Promise<number | undefined> {\n if (!this.active) {\n return undefined;\n }\n // A refusal (response_code != 0) never arrives here: the client's transport already\n // turns it into an error, which the engine reports. Only a null body is left to guard.\n const response = (await this.client.getListInfo(this.active.input, this.index)) as Record<string, unknown> | null;\n if (!response) {\n return undefined;\n }\n const entries = Array.isArray(response.list_info) ? (response.list_info as RawListEntry[]) : [];\n this.rows = entries.slice(0, PAGE_SIZE).map((entry, i) => {\n // b1 = capable of Select (a container to enter), b2 = capable of Play.\n const attribute = typeof entry.attribute === \"number\" ? entry.attribute : 0;\n const playable = (attribute & 0b100) !== 0;\n const selectable = (attribute & 0b10) !== 0;\n const row: BrowseRow = {\n line: i + 1,\n text: typeof entry.text === \"string\" ? entry.text : \"\",\n kind: playable ? \"item\" : selectable ? \"folder\" : \"unselectable\",\n };\n if (typeof entry.thumbnail === \"string\" && entry.thumbnail.length > 0) {\n row.thumbnail = entry.thumbnail;\n }\n return row;\n });\n this.totalItems = typeof response.max_line === \"number\" ? response.max_line : this.rows.length;\n const layer = typeof response.menu_layer === \"number\" ? response.menu_layer : 0;\n this.engine?.onWindow({\n menuName: typeof response.menu_name === \"string\" ? response.menu_name : \"\",\n layer,\n totalItems: this.totalItems,\n currentLine: this.index + 1,\n rows: this.rows,\n });\n return layer;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -23,7 +23,6 @@ __export(object_tree_coordinator_exports, {
|
|
|
23
23
|
module.exports = __toCommonJS(object_tree_coordinator_exports);
|
|
24
24
|
var import_owner_policy = require("./owner-policy");
|
|
25
25
|
function coordinateObjectTree(contributions) {
|
|
26
|
-
const order = [];
|
|
27
26
|
const byId = /* @__PURE__ */ new Map();
|
|
28
27
|
for (const { transport, objects } of contributions) {
|
|
29
28
|
for (const obj of objects) {
|
|
@@ -32,17 +31,12 @@ function coordinateObjectTree(contributions) {
|
|
|
32
31
|
if (!entry) {
|
|
33
32
|
entry = { key: (0, import_owner_policy.capabilityKeyOf)(transport, obj.id), defs: /* @__PURE__ */ new Map() };
|
|
34
33
|
byId.set(canonicalId, entry);
|
|
35
|
-
order.push(canonicalId);
|
|
36
34
|
}
|
|
37
35
|
entry.defs.set(transport, obj);
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
38
|
const ownerByCanonicalId = /* @__PURE__ */ new Map();
|
|
41
|
-
const resolved =
|
|
42
|
-
const entry = byId.get(canonicalId);
|
|
43
|
-
if (!entry) {
|
|
44
|
-
throw new Error(`coordinateObjectTree: missing entry for ${canonicalId}`);
|
|
45
|
-
}
|
|
39
|
+
const resolved = [...byId].map(([canonicalId, entry]) => {
|
|
46
40
|
const owner = (0, import_owner_policy.pickOwner)(entry.key, [...entry.defs.keys()]);
|
|
47
41
|
ownerByCanonicalId.set(canonicalId, owner);
|
|
48
42
|
const ownerDef = entry.defs.get(owner);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/catalog/object-tree-coordinator.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ObjectDef } from \"./types\";\nimport { canonicalIdOf, capabilityKeyOf, pickOwner, type Transport } from \"./owner-policy\";\n\n/** One transport's contribution: the objects its catalog builds for this device. */\nexport interface TransportObjects {\n /** The transport these objects come from. */\n transport: Transport;\n /** The objects the transport's catalog builds (its own state ids, possibly drifting/zoned). */\n objects: readonly ObjectDef[];\n}\n\n/**\n * Merge the present transports' catalogs into one unified object tree. Each capability appears\n * exactly once, emitted by its owning transport (see {@link pickOwner}) under the canonical id;\n * drifting ids and per-zone duplicates across transports collapse to one node. Objects are\n * ordered parents-before-children so the intermediate channels exist before their states.\n *\n * @param contributions the objects each present transport offers\n * @returns the deduplicated tree and the owner of each canonical id (for write routing)\n */\nexport function coordinateObjectTree(contributions: readonly TransportObjects[]): {\n objects: ObjectDef[];\n ownerByCanonicalId: Map<string, Transport>;\n} {\n
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,0BAA0E;AAmBnE,SAAS,qBAAqB,eAGnC;
|
|
4
|
+
"sourcesContent": ["import type { ObjectDef } from \"./types\";\nimport { canonicalIdOf, capabilityKeyOf, pickOwner, type Transport } from \"./owner-policy\";\n\n/** One transport's contribution: the objects its catalog builds for this device. */\nexport interface TransportObjects {\n /** The transport these objects come from. */\n transport: Transport;\n /** The objects the transport's catalog builds (its own state ids, possibly drifting/zoned). */\n objects: readonly ObjectDef[];\n}\n\n/**\n * Merge the present transports' catalogs into one unified object tree. Each capability appears\n * exactly once, emitted by its owning transport (see {@link pickOwner}) under the canonical id;\n * drifting ids and per-zone duplicates across transports collapse to one node. Objects are\n * ordered parents-before-children so the intermediate channels exist before their states.\n *\n * @param contributions the objects each present transport offers\n * @returns the deduplicated tree and the owner of each canonical id (for write routing)\n */\nexport function coordinateObjectTree(contributions: readonly TransportObjects[]): {\n objects: ObjectDef[];\n ownerByCanonicalId: Map<string, Transport>;\n} {\n // Map iteration follows insertion order, so first-seen order needs no side list.\n const byId = new Map<string, { key: string; defs: Map<Transport, ObjectDef> }>();\n for (const { transport, objects } of contributions) {\n for (const obj of objects) {\n const canonicalId = canonicalIdOf(transport, obj.id);\n let entry = byId.get(canonicalId);\n if (!entry) {\n entry = { key: capabilityKeyOf(transport, obj.id), defs: new Map() };\n byId.set(canonicalId, entry);\n }\n entry.defs.set(transport, obj);\n }\n }\n const ownerByCanonicalId = new Map<string, Transport>();\n const resolved: ObjectDef[] = [...byId].map(([canonicalId, entry]) => {\n const owner = pickOwner(entry.key, [...entry.defs.keys()]);\n ownerByCanonicalId.set(canonicalId, owner);\n const ownerDef = entry.defs.get(owner);\n // Internal invariant, not a reachable state: pickOwner always returns one of the\n // candidates it was handed (= this entry's own defs). It exists so a future change\n // to pickOwner fails loudly instead of writing an empty object.\n if (!ownerDef) {\n throw new Error(`coordinateObjectTree: owner ${owner} has no def for ${canonicalId}`);\n }\n // Dropdown borrowing: the owner wins the WRITE PATH, but another transport may know\n // the value labels the owner cannot deliver \u2014 the scene titles come over XML/YNCA\n // while MusicCast owns the recall, the device's own input list comes over XML while\n // YNCA owns the input. The label map is pure presentation, so borrowing it never\n // changes routing; borrowed in modernity-independent claim order (first with one).\n const resolvedDef: ObjectDef = { ...ownerDef, id: canonicalId };\n if (!resolvedDef.common.states) {\n for (const def of entry.defs.values()) {\n if (def.common.states) {\n resolvedDef.common = { ...resolvedDef.common, states: def.common.states };\n break;\n }\n }\n }\n return resolvedDef;\n });\n // Parents before children: shallower id paths (fewer dotted segments) first. Array.sort is\n // stable (ES2019+), so equal-depth objects keep their first-seen order.\n resolved.sort((a, b) => a.id.split(\".\").length - b.id.split(\".\").length);\n return { objects: resolved, ownerByCanonicalId };\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,0BAA0E;AAmBnE,SAAS,qBAAqB,eAGnC;AAEA,QAAM,OAAO,oBAAI,IAA8D;AAC/E,aAAW,EAAE,WAAW,QAAQ,KAAK,eAAe;AAClD,eAAW,OAAO,SAAS;AACzB,YAAM,kBAAc,mCAAc,WAAW,IAAI,EAAE;AACnD,UAAI,QAAQ,KAAK,IAAI,WAAW;AAChC,UAAI,CAAC,OAAO;AACV,gBAAQ,EAAE,SAAK,qCAAgB,WAAW,IAAI,EAAE,GAAG,MAAM,oBAAI,IAAI,EAAE;AACnE,aAAK,IAAI,aAAa,KAAK;AAAA,MAC7B;AACA,YAAM,KAAK,IAAI,WAAW,GAAG;AAAA,IAC/B;AAAA,EACF;AACA,QAAM,qBAAqB,oBAAI,IAAuB;AACtD,QAAM,WAAwB,CAAC,GAAG,IAAI,EAAE,IAAI,CAAC,CAAC,aAAa,KAAK,MAAM;AACpE,UAAM,YAAQ,+BAAU,MAAM,KAAK,CAAC,GAAG,MAAM,KAAK,KAAK,CAAC,CAAC;AACzD,uBAAmB,IAAI,aAAa,KAAK;AACzC,UAAM,WAAW,MAAM,KAAK,IAAI,KAAK;AAIrC,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,MAAM,+BAA+B,KAAK,mBAAmB,WAAW,EAAE;AAAA,IACtF;AAMA,UAAM,cAAyB,EAAE,GAAG,UAAU,IAAI,YAAY;AAC9D,QAAI,CAAC,YAAY,OAAO,QAAQ;AAC9B,iBAAW,OAAO,MAAM,KAAK,OAAO,GAAG;AACrC,YAAI,IAAI,OAAO,QAAQ;AACrB,sBAAY,SAAS,EAAE,GAAG,YAAY,QAAQ,QAAQ,IAAI,OAAO,OAAO;AACxE;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT,CAAC;AAGD,WAAS,KAAK,CAAC,GAAG,MAAM,EAAE,GAAG,MAAM,GAAG,EAAE,SAAS,EAAE,GAAG,MAAM,GAAG,EAAE,MAAM;AACvE,SAAO,EAAE,SAAS,UAAU,mBAAmB;AACjD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -47,6 +47,13 @@ const OWNER_OVERRIDES = {
|
|
|
47
47
|
// and the 2012 generation (RX-V473/V475) answers the YNCA scene put with
|
|
48
48
|
// @RESTRICTED (ynca-python PRACTICALITIES). The proven writers go first.
|
|
49
49
|
"scene.recall": ["yxc", "xml", "ynca"],
|
|
50
|
+
// The scene LIST is presentation: number + title per slot. MusicCast knows the slot
|
|
51
|
+
// COUNT but no titles; XML declares the titles per zone, YNCA the main zone's names.
|
|
52
|
+
// By modernity MusicCast would own it and — connecting in seconds while the YNCA
|
|
53
|
+
// names ride a 19 s sweep — publish a title-less list on the first contact that then
|
|
54
|
+
// stands until the next restart. The title sources come first; a MusicCast-only
|
|
55
|
+
// device still owns its list alone.
|
|
56
|
+
"scene.list": ["xml", "ynca", "yxc"],
|
|
50
57
|
// §3d richness loss — YNCA carries an enum dropdown that YXC/XML flatten to a free string.
|
|
51
58
|
input: ["ynca", "yxc", "xml"],
|
|
52
59
|
soundProgram: ["ynca", "yxc", "xml"],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/catalog/owner-policy.ts"],
|
|
4
|
-
"sourcesContent": ["/** The transports the adapter speaks. Ordered most-modern-first \u2014 the default ownership rank. */\nexport type Transport = \"yxc\" | \"ynca\" | \"xml\";\n\n/**\n * Ownership preference by modernity, used when a shared capability is equally good on each\n * transport (krobi: \"wenn mehrere Protokolle dasselbe k\u00F6nnen, das modernste nutzen\"). YXC is\n * push + structured JSON, YNCA the text-poll base, XML the pre-2010 fallback.\n */\nconst MODERNITY: readonly Transport[] = [\"yxc\", \"ynca\", \"xml\"];\n\n/**\n * Per-capability ownership preference that OVERRIDES pure modernity \u2014 from the capability\n * census (`Ressourcen/yamaha/capability-census-2026-08-11.md` \u00A73). These are the shared keys\n * where YXC is present but NOT equivalent: wrong scale, read-only, or a poorer type. Each list\n * is the preferred owner order for that key; the first present transport wins.\n */\nconst OWNER_OVERRIDES: Record<string, readonly Transport[]> = {\n // \u00A73a scale conflict \u2014 YXC volume is the raw 0..161 device scale, YNCA/XML are dB. Keep dB.\n volume: [\"ynca\", \"xml\", \"yxc\"],\n // \u00A73c write loss \u2014 YXC is read-only for these, YNCA (and often XML) is writable.\n \"advanced.maxVolume\": [\"ynca\", \"xml\", \"yxc\"],\n // \u00A73c write loss on the unified player block (v2.0.0): YXC reads playback/repeat/\n // shuffle but cannot WRITE them (its API has only toggle/transport endpoints, which\n // stay YXC-owned buttons); YNCA sets all three directly. Zone mirrors collapse to\n // the same template, so this covers multiroom.zoneN.player.* too.\n \"player.playback\": [\"ynca\", \"yxc\"],\n \"player.repeat\": [\"ynca\", \"yxc\"],\n \"player.shuffle\": [\"ynca\", \"yxc\"],\n \"sound.extraBass\": [\"ynca\", \"xml\", \"yxc\"],\n \"sound.adaptiveDrc\": [\"ynca\", \"xml\", \"yxc\"],\n \"sound.surroundDecoder\": [\"ynca\", \"yxc\"],\n \"sound.dialogueLift\": [\"xml\", \"yxc\"],\n // Write-proof beats modernity rank for the scene TRIGGER (#615): YXC declares the\n // recall endpoint per zone (device-verified), XML declares the write value in its\n // Scene_Sel_Item list \u2014 but YNCA's claim rests on the scene NAMES being readable,\n // and the 2012 generation (RX-V473/V475) answers the YNCA scene put with\n // @RESTRICTED (ynca-python PRACTICALITIES). The proven writers go first.\n \"scene.recall\": [\"yxc\", \"xml\", \"ynca\"],\n // \u00A73d richness loss \u2014 YNCA carries an enum dropdown that YXC/XML flatten to a free string.\n input: [\"ynca\", \"yxc\", \"xml\"],\n soundProgram: [\"ynca\", \"yxc\", \"xml\"],\n sleep: [\"ynca\", \"xml\", \"yxc\"],\n \"tuner.band\": [\"ynca\", \"yxc\"],\n};\n\n/**\n * Per-transport state-id \u2192 canonical capability key, for the ids that drift between transports\n * (census \u00A73f, verified against the catalogs). The canonical key is the census left column; the\n * transports not listed already use the canonical id. Zone prefixes are stripped separately.\n */\nconst ID_DRIFT: Partial<Record<Transport, Readonly<Record<string, string>>>> = {\n yxc: { subwooferVolume: \"sound.subwooferTrim\", \"multiroom.partyEnable\": \"multiroom.party\" },\n xml: { hdmiOut1: \"hdmi.out1\", hdmiOut2: \"hdmi.out2\" },\n};\n\n/**\n * The transport-neutral capability key for a transport's state id: strip a `zone2/3/4.` prefix\n * to the per-zone template, then resolve any id drift to the canonical key. Two transports that\n * express the same capability under different ids therefore map to the same key.\n *\n * @param transport the transport the state id comes from\n * @param stateId the transport's own state id (may carry a zone prefix)\n * @returns the canonical capability key\n */\nexport function capabilityKeyOf(transport: Transport, stateId: string): string {\n const template = stateId.replace(/^(?:multiroom\\.)?zone[234]\\./, \"\");\n return ID_DRIFT[transport]?.[template] ?? template;\n}\n\n/**\n * The drift-resolved, transport-neutral OBJECT id \u2014 like {@link capabilityKeyOf} but KEEPS the\n * zone prefix. capabilityKeyOf drops the zone to decide ownership per template; canonicalIdOf\n * keeps it to place the per-zone node in the unified tree. Two transports' zoned ids for the\n * same capability therefore collapse to one node id.\n *\n * @param transport the transport the state id comes from\n * @param stateId the transport's own state id\n * @returns the canonical object id (zone prefix kept, drift resolved)\n */\nexport function canonicalIdOf(transport: Transport, stateId: string): string {\n const zone = /^(?:multiroom\\.)?zone[234]\\./.exec(stateId)?.[0] ?? \"\";\n const template = stateId.slice(zone.length);\n const resolved = ID_DRIFT[transport]?.[template] ?? template;\n if (zone && !zone.startsWith(\"multiroom.\")) {\n return `multiroom.${zone}${resolved}`;\n }\n return zone + resolved;\n}\n\n/**\n * Decide which transport owns a capability, given the transports that actually offer it on\n * this device. Default is the most modern; a census-driven override wins where the modern\n * transport would be lossy. An override that lists none of the present candidates falls back\n * to modernity, so any non-empty candidate set resolves.\n *\n * @param key the transport-neutral capability key (the unified state id)\n * @param candidates the present transports that offer this key (non-empty)\n * @returns the owning transport\n */\nexport function pickOwner(key: string, candidates: readonly Transport[]): Transport {\n const preference = OWNER_OVERRIDES[key] ?? MODERNITY;\n // The MODERNITY fallback is defence for a FUTURE override: every entry in the\n // current table lists at least two transports, so an override can never miss all\n // present candidates while more than one is present \u2014 today it is unobservable.\n const owner = preference.find(t => candidates.includes(t)) ?? MODERNITY.find(t => candidates.includes(t));\n return owner ?? candidates[0];\n}\n\n/**\n * Build the owner map for a device: given which capability keys each present transport offers,\n * resolve every key to exactly one owning transport (via {@link pickOwner}). This is the\n * dedup decision behind the unified object tree \u2014 each state is written by one transport only.\n *\n * @param offered the capability keys each present transport offers (absent transport = none)\n * @returns a map from capability key to its owning transport\n */\nexport function resolveOwnership(offered: Partial<Record<Transport, readonly string[]>>): Map<string, Transport> {\n const candidates = new Map<string, Transport[]>();\n // Iterate in modernity order so the candidate lists are deterministic.\n for (const transport of MODERNITY) {\n for (const key of offered[transport] ?? []) {\n const list = candidates.get(key);\n if (list) {\n list.push(transport);\n } else {\n candidates.set(key, [transport]);\n }\n }\n }\n const owners = new Map<string, Transport>();\n for (const [key, cands] of candidates) {\n owners.set(key, pickOwner(key, cands));\n }\n return owners;\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,MAAM,YAAkC,CAAC,OAAO,QAAQ,KAAK;AAQ7D,MAAM,kBAAwD;AAAA;AAAA,EAE5D,QAAQ,CAAC,QAAQ,OAAO,KAAK;AAAA;AAAA,EAE7B,sBAAsB,CAAC,QAAQ,OAAO,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,EAK3C,mBAAmB,CAAC,QAAQ,KAAK;AAAA,EACjC,iBAAiB,CAAC,QAAQ,KAAK;AAAA,EAC/B,kBAAkB,CAAC,QAAQ,KAAK;AAAA,EAChC,mBAAmB,CAAC,QAAQ,OAAO,KAAK;AAAA,EACxC,qBAAqB,CAAC,QAAQ,OAAO,KAAK;AAAA,EAC1C,yBAAyB,CAAC,QAAQ,KAAK;AAAA,EACvC,sBAAsB,CAAC,OAAO,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMnC,gBAAgB,CAAC,OAAO,OAAO,MAAM;AAAA;AAAA,
|
|
4
|
+
"sourcesContent": ["/** The transports the adapter speaks. Ordered most-modern-first \u2014 the default ownership rank. */\nexport type Transport = \"yxc\" | \"ynca\" | \"xml\";\n\n/**\n * Ownership preference by modernity, used when a shared capability is equally good on each\n * transport (krobi: \"wenn mehrere Protokolle dasselbe k\u00F6nnen, das modernste nutzen\"). YXC is\n * push + structured JSON, YNCA the text-poll base, XML the pre-2010 fallback.\n */\nconst MODERNITY: readonly Transport[] = [\"yxc\", \"ynca\", \"xml\"];\n\n/**\n * Per-capability ownership preference that OVERRIDES pure modernity \u2014 from the capability\n * census (`Ressourcen/yamaha/capability-census-2026-08-11.md` \u00A73). These are the shared keys\n * where YXC is present but NOT equivalent: wrong scale, read-only, or a poorer type. Each list\n * is the preferred owner order for that key; the first present transport wins.\n */\nconst OWNER_OVERRIDES: Record<string, readonly Transport[]> = {\n // \u00A73a scale conflict \u2014 YXC volume is the raw 0..161 device scale, YNCA/XML are dB. Keep dB.\n volume: [\"ynca\", \"xml\", \"yxc\"],\n // \u00A73c write loss \u2014 YXC is read-only for these, YNCA (and often XML) is writable.\n \"advanced.maxVolume\": [\"ynca\", \"xml\", \"yxc\"],\n // \u00A73c write loss on the unified player block (v2.0.0): YXC reads playback/repeat/\n // shuffle but cannot WRITE them (its API has only toggle/transport endpoints, which\n // stay YXC-owned buttons); YNCA sets all three directly. Zone mirrors collapse to\n // the same template, so this covers multiroom.zoneN.player.* too.\n \"player.playback\": [\"ynca\", \"yxc\"],\n \"player.repeat\": [\"ynca\", \"yxc\"],\n \"player.shuffle\": [\"ynca\", \"yxc\"],\n \"sound.extraBass\": [\"ynca\", \"xml\", \"yxc\"],\n \"sound.adaptiveDrc\": [\"ynca\", \"xml\", \"yxc\"],\n \"sound.surroundDecoder\": [\"ynca\", \"yxc\"],\n \"sound.dialogueLift\": [\"xml\", \"yxc\"],\n // Write-proof beats modernity rank for the scene TRIGGER (#615): YXC declares the\n // recall endpoint per zone (device-verified), XML declares the write value in its\n // Scene_Sel_Item list \u2014 but YNCA's claim rests on the scene NAMES being readable,\n // and the 2012 generation (RX-V473/V475) answers the YNCA scene put with\n // @RESTRICTED (ynca-python PRACTICALITIES). The proven writers go first.\n \"scene.recall\": [\"yxc\", \"xml\", \"ynca\"],\n // The scene LIST is presentation: number + title per slot. MusicCast knows the slot\n // COUNT but no titles; XML declares the titles per zone, YNCA the main zone's names.\n // By modernity MusicCast would own it and \u2014 connecting in seconds while the YNCA\n // names ride a 19 s sweep \u2014 publish a title-less list on the first contact that then\n // stands until the next restart. The title sources come first; a MusicCast-only\n // device still owns its list alone.\n \"scene.list\": [\"xml\", \"ynca\", \"yxc\"],\n // \u00A73d richness loss \u2014 YNCA carries an enum dropdown that YXC/XML flatten to a free string.\n input: [\"ynca\", \"yxc\", \"xml\"],\n soundProgram: [\"ynca\", \"yxc\", \"xml\"],\n sleep: [\"ynca\", \"xml\", \"yxc\"],\n \"tuner.band\": [\"ynca\", \"yxc\"],\n};\n\n/**\n * Per-transport state-id \u2192 canonical capability key, for the ids that drift between transports\n * (census \u00A73f, verified against the catalogs). The canonical key is the census left column; the\n * transports not listed already use the canonical id. Zone prefixes are stripped separately.\n */\nconst ID_DRIFT: Partial<Record<Transport, Readonly<Record<string, string>>>> = {\n yxc: { subwooferVolume: \"sound.subwooferTrim\", \"multiroom.partyEnable\": \"multiroom.party\" },\n xml: { hdmiOut1: \"hdmi.out1\", hdmiOut2: \"hdmi.out2\" },\n};\n\n/**\n * The transport-neutral capability key for a transport's state id: strip a `zone2/3/4.` prefix\n * to the per-zone template, then resolve any id drift to the canonical key. Two transports that\n * express the same capability under different ids therefore map to the same key.\n *\n * @param transport the transport the state id comes from\n * @param stateId the transport's own state id (may carry a zone prefix)\n * @returns the canonical capability key\n */\nexport function capabilityKeyOf(transport: Transport, stateId: string): string {\n const template = stateId.replace(/^(?:multiroom\\.)?zone[234]\\./, \"\");\n return ID_DRIFT[transport]?.[template] ?? template;\n}\n\n/**\n * The drift-resolved, transport-neutral OBJECT id \u2014 like {@link capabilityKeyOf} but KEEPS the\n * zone prefix. capabilityKeyOf drops the zone to decide ownership per template; canonicalIdOf\n * keeps it to place the per-zone node in the unified tree. Two transports' zoned ids for the\n * same capability therefore collapse to one node id.\n *\n * @param transport the transport the state id comes from\n * @param stateId the transport's own state id\n * @returns the canonical object id (zone prefix kept, drift resolved)\n */\nexport function canonicalIdOf(transport: Transport, stateId: string): string {\n const zone = /^(?:multiroom\\.)?zone[234]\\./.exec(stateId)?.[0] ?? \"\";\n const template = stateId.slice(zone.length);\n const resolved = ID_DRIFT[transport]?.[template] ?? template;\n if (zone && !zone.startsWith(\"multiroom.\")) {\n return `multiroom.${zone}${resolved}`;\n }\n return zone + resolved;\n}\n\n/**\n * Decide which transport owns a capability, given the transports that actually offer it on\n * this device. Default is the most modern; a census-driven override wins where the modern\n * transport would be lossy. An override that lists none of the present candidates falls back\n * to modernity, so any non-empty candidate set resolves.\n *\n * @param key the transport-neutral capability key (the unified state id)\n * @param candidates the present transports that offer this key (non-empty)\n * @returns the owning transport\n */\nexport function pickOwner(key: string, candidates: readonly Transport[]): Transport {\n const preference = OWNER_OVERRIDES[key] ?? MODERNITY;\n // The MODERNITY fallback is defence for a FUTURE override: every entry in the\n // current table lists at least two transports, so an override can never miss all\n // present candidates while more than one is present \u2014 today it is unobservable.\n const owner = preference.find(t => candidates.includes(t)) ?? MODERNITY.find(t => candidates.includes(t));\n return owner ?? candidates[0];\n}\n\n/**\n * Build the owner map for a device: given which capability keys each present transport offers,\n * resolve every key to exactly one owning transport (via {@link pickOwner}). This is the\n * dedup decision behind the unified object tree \u2014 each state is written by one transport only.\n *\n * @param offered the capability keys each present transport offers (absent transport = none)\n * @returns a map from capability key to its owning transport\n */\nexport function resolveOwnership(offered: Partial<Record<Transport, readonly string[]>>): Map<string, Transport> {\n const candidates = new Map<string, Transport[]>();\n // Iterate in modernity order so the candidate lists are deterministic.\n for (const transport of MODERNITY) {\n for (const key of offered[transport] ?? []) {\n const list = candidates.get(key);\n if (list) {\n list.push(transport);\n } else {\n candidates.set(key, [transport]);\n }\n }\n }\n const owners = new Map<string, Transport>();\n for (const [key, cands] of candidates) {\n owners.set(key, pickOwner(key, cands));\n }\n return owners;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,MAAM,YAAkC,CAAC,OAAO,QAAQ,KAAK;AAQ7D,MAAM,kBAAwD;AAAA;AAAA,EAE5D,QAAQ,CAAC,QAAQ,OAAO,KAAK;AAAA;AAAA,EAE7B,sBAAsB,CAAC,QAAQ,OAAO,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,EAK3C,mBAAmB,CAAC,QAAQ,KAAK;AAAA,EACjC,iBAAiB,CAAC,QAAQ,KAAK;AAAA,EAC/B,kBAAkB,CAAC,QAAQ,KAAK;AAAA,EAChC,mBAAmB,CAAC,QAAQ,OAAO,KAAK;AAAA,EACxC,qBAAqB,CAAC,QAAQ,OAAO,KAAK;AAAA,EAC1C,yBAAyB,CAAC,QAAQ,KAAK;AAAA,EACvC,sBAAsB,CAAC,OAAO,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMnC,gBAAgB,CAAC,OAAO,OAAO,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOrC,cAAc,CAAC,OAAO,QAAQ,KAAK;AAAA;AAAA,EAEnC,OAAO,CAAC,QAAQ,OAAO,KAAK;AAAA,EAC5B,cAAc,CAAC,QAAQ,OAAO,KAAK;AAAA,EACnC,OAAO,CAAC,QAAQ,OAAO,KAAK;AAAA,EAC5B,cAAc,CAAC,QAAQ,KAAK;AAC9B;AAOA,MAAM,WAAyE;AAAA,EAC7E,KAAK,EAAE,iBAAiB,uBAAuB,yBAAyB,kBAAkB;AAAA,EAC1F,KAAK,EAAE,UAAU,aAAa,UAAU,YAAY;AACtD;AAWO,SAAS,gBAAgB,WAAsB,SAAyB;AAvE/E;AAwEE,QAAM,WAAW,QAAQ,QAAQ,gCAAgC,EAAE;AACnE,UAAO,oBAAS,SAAS,MAAlB,mBAAsB,cAAtB,YAAmC;AAC5C;AAYO,SAAS,cAAc,WAAsB,SAAyB;AAtF7E;AAuFE,QAAM,QAAO,0CAA+B,KAAK,OAAO,MAA3C,mBAA+C,OAA/C,YAAqD;AAClE,QAAM,WAAW,QAAQ,MAAM,KAAK,MAAM;AAC1C,QAAM,YAAW,oBAAS,SAAS,MAAlB,mBAAsB,cAAtB,YAAmC;AACpD,MAAI,QAAQ,CAAC,KAAK,WAAW,YAAY,GAAG;AAC1C,WAAO,aAAa,IAAI,GAAG,QAAQ;AAAA,EACrC;AACA,SAAO,OAAO;AAChB;AAYO,SAAS,UAAU,KAAa,YAA6C;AA1GpF;AA2GE,QAAM,cAAa,qBAAgB,GAAG,MAAnB,YAAwB;AAI3C,QAAM,SAAQ,gBAAW,KAAK,OAAK,WAAW,SAAS,CAAC,CAAC,MAA3C,YAAgD,UAAU,KAAK,OAAK,WAAW,SAAS,CAAC,CAAC;AACxG,SAAO,wBAAS,WAAW,CAAC;AAC9B;AAUO,SAAS,iBAAiB,SAAgF;AA3HjH;AA4HE,QAAM,aAAa,oBAAI,IAAyB;AAEhD,aAAW,aAAa,WAAW;AACjC,eAAW,QAAO,aAAQ,SAAS,MAAjB,YAAsB,CAAC,GAAG;AAC1C,YAAM,OAAO,WAAW,IAAI,GAAG;AAC/B,UAAI,MAAM;AACR,aAAK,KAAK,SAAS;AAAA,MACrB,OAAO;AACL,mBAAW,IAAI,KAAK,CAAC,SAAS,CAAC;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AACA,QAAM,SAAS,oBAAI,IAAuB;AAC1C,aAAW,CAAC,KAAK,KAAK,KAAK,YAAY;AACrC,WAAO,IAAI,KAAK,UAAU,KAAK,KAAK,CAAC;AAAA,EACvC;AACA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -42,7 +42,7 @@ class ReconnectStrategy {
|
|
|
42
42
|
nextDelay() {
|
|
43
43
|
const delay = Math.min(this.baseMs * 2 ** this.attempt, this.maxMs);
|
|
44
44
|
this.attempt++;
|
|
45
|
-
return Math.round(delay * (1
|
|
45
|
+
return Math.round(delay * (1 - this.jitter * Math.random()));
|
|
46
46
|
}
|
|
47
47
|
/** Reset the backoff to the base delay after a successful connection. */
|
|
48
48
|
reset() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/lifecycle/reconnect-strategy.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Exponential backoff for reconnect attempts, capped at a maximum delay and spread by a\n * small random offset.\n *\n * The offset matters once more than one device is configured: a router reboot or a switch\n * losing power takes every receiver \u2014 and every one of their transports \u2014 down at the same\n * moment, so without it they all retry in lockstep for as long as the outage lasts, each\n * wave hitting the network together. Up to 20 % jitter breaks the convoy apart while\n * keeping the backoff's shape.\n */\nexport class ReconnectStrategy {\n private attempt = 0;\n\n /**\n * @param baseMs the first delay in milliseconds\n * @param maxMs the maximum delay in milliseconds\n * @param jitter fraction of the delay to spread randomly (0 disables it \u2014 used by tests\n * that assert exact delays)\n */\n public constructor(\n private readonly baseMs: number,\n private readonly maxMs: number,\n private readonly jitter = 0.2,\n ) {}\n\n /**\n * Get the next backoff delay and advance the attempt counter.\n *\n * @returns the delay in milliseconds for the next reconnect attempt\n */\n public nextDelay(): number {\n const delay = Math.min(this.baseMs * 2 ** this.attempt, this.maxMs);\n this.attempt++;\n return Math.round(delay * (1
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
4
|
+
"sourcesContent": ["/**\n * Exponential backoff for reconnect attempts, capped at a maximum delay and spread by a\n * small random offset BELOW it.\n *\n * The offset matters once more than one device is configured: a router reboot or a switch\n * losing power takes every receiver \u2014 and every one of their transports \u2014 down at the same\n * moment, so without it they all retry in lockstep for as long as the outage lasts, each\n * wave hitting the network together. Up to 20 % jitter breaks the convoy apart while\n * keeping the backoff's shape. It spreads downward so the cap stays a cap (an upward\n * spread let the \"60 s ceiling\" reach 72 s) \u2014 and so the spread still works AT the\n * ceiling, where every device of a long outage ends up.\n */\nexport class ReconnectStrategy {\n private attempt = 0;\n\n /**\n * @param baseMs the first delay in milliseconds\n * @param maxMs the maximum delay in milliseconds\n * @param jitter fraction of the delay to spread randomly (0 disables it \u2014 used by tests\n * that assert exact delays)\n */\n public constructor(\n private readonly baseMs: number,\n private readonly maxMs: number,\n private readonly jitter = 0.2,\n ) {}\n\n /**\n * Get the next backoff delay and advance the attempt counter.\n *\n * @returns the delay in milliseconds for the next reconnect attempt\n */\n public nextDelay(): number {\n const delay = Math.min(this.baseMs * 2 ** this.attempt, this.maxMs);\n this.attempt++;\n return Math.round(delay * (1 - this.jitter * Math.random()));\n }\n\n /** Reset the backoff to the base delay after a successful connection. */\n public reset(): void {\n this.attempt = 0;\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAYO,MAAM,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAStB,YACY,QACA,OACA,SAAS,KAC1B;AAHiB;AACA;AACA;AAAA,EAChB;AAAA,EAZK,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBX,YAAoB;AACzB,UAAM,QAAQ,KAAK,IAAI,KAAK,SAAS,KAAK,KAAK,SAAS,KAAK,KAAK;AAClE,SAAK;AACL,WAAO,KAAK,MAAM,SAAS,IAAI,KAAK,SAAS,KAAK,OAAO,EAAE;AAAA,EAC7D;AAAA;AAAA,EAGO,QAAc;AACnB,SAAK,UAAU;AAAA,EACjB;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -285,9 +285,6 @@ function neverWrittenStateIds(objects, states, deviceIds, namespace) {
|
|
|
285
285
|
if ((object == null ? void 0 : object.type) !== "state" || (common == null ? void 0 : common.read) === false) {
|
|
286
286
|
continue;
|
|
287
287
|
}
|
|
288
|
-
if ((common == null ? void 0 : common.custom) && Object.keys(common.custom).length > 0) {
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
288
|
const relative = stripNamespace(fullId, namespace);
|
|
292
289
|
const top = relative.split(".")[0];
|
|
293
290
|
if (!deviceIds.has(top) || relative.slice(top.length + 1).startsWith("info.")) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/pure-helpers.ts"],
|
|
4
|
-
"sourcesContent": ["import type { DeviceRecord } from \"./types\";\nimport type { DiscoveredDevice } from \"./discovery\";\n\ninterface ConfiguredDevice {\n name?: string;\n ip: string;\n}\n\n/**\n * True when a raw config row carries a non-empty ip (the name is optional \u2014 a row\n * with an ip but no name is valid and falls back to the ip as its id, so a device\n * is never silently dropped just because its name was left blank).\n *\n * @param entry a raw config row from the admin device table\n * @returns whether the row is a valid configured device\n */\nfunction isConfiguredDevice(entry: unknown): entry is ConfiguredDevice {\n if (typeof entry !== \"object\" || entry === null) {\n return false;\n }\n const candidate = entry as { name?: unknown; ip?: unknown };\n return (\n typeof candidate.ip === \"string\" &&\n candidate.ip.length > 0 &&\n (candidate.name === undefined || typeof candidate.name === \"string\")\n );\n}\n\n/**\n * Make a string safe for use as an ioBroker object id segment.\n *\n * @param raw the raw string (e.g. a device name)\n * @returns the string with id-unsafe characters replaced by underscores\n */\nexport function sanitizeId(raw: string): string {\n return raw.replace(/[^A-Za-z0-9\\-_]/g, \"_\");\n}\n\n/**\n * Strip the adapter namespace (e.g. `yamaha.0`) from a full state id, leaving the\n * device-relative path (e.g. `living.power`).\n *\n * @param fullId the full state id\n * @param namespace the adapter namespace\n * @returns the id relative to the adapter instance\n */\nexport function stripNamespace(fullId: string, namespace: string): string {\n return fullId.slice(namespace.length + 1);\n}\n\n/**\n * Turn the admin device table (untrusted native config) into device records.\n * Invalid rows are dropped, as are rows whose id collides with the adapter's own\n * reserved `info` branch or with an id already taken \u2014 two names that sanitise to\n * the same id (e.g. \"Living Room\" and \"Living.Room\") would otherwise share one\n * object tree. A dropped collision is reported via `onCollision` so the device does\n * not just silently \"not exist\" for the user.\n *\n * @param raw the raw `native.devices` value\n * @param onCollision called with the dropped row's name/ip and the clashing id\n * @returns validated, de-duplicated device records\n */\nexport function parseDevices(raw: unknown, onCollision?: (dropped: string, takenId: string) => void): DeviceRecord[] {\n if (!Array.isArray(raw)) {\n return [];\n }\n const records: DeviceRecord[] = [];\n const taken = new Set<string>([\"info\"]); // reserved: the adapter's own info channel\n for (const entry of raw) {\n if (!isConfiguredDevice(entry)) {\n continue;\n }\n // Fall back to the ip as the id when the name is blank, so the device still appears\n // instead of vanishing silently.\n const id = sanitizeId(entry.name && entry.name.length > 0 ? entry.name : entry.ip);\n if (taken.has(id)) {\n onCollision?.(entry.name || entry.ip, id);\n continue;\n }\n taken.add(id);\n records.push({ id, ip: entry.ip });\n }\n return records;\n}\n\n/**\n * Merge freshly discovered devices into the set already known from earlier runs\n * (the auto-discovery standby protection). A known device is kept even when this\n * run's scan did not find it \u2014 a receiver in deep standby answers no SSDP, and its\n * object tree must survive. New addresses are added; a discovered device is turned\n * into a record via its friendly name (or its ip when it advertises none), and one\n * whose id would collide with an already-kept device is skipped (reported via\n * `onCollision`, so the missing device is explainable). De-duplicated by ip.\n *\n * @param known the device records remembered from earlier runs\n * @param found the devices discovered this run\n * @param onCollision called with the dropped device's name/ip and the clashing id\n * @returns the merged records, de-duplicated by ip\n */\nexport function mergeDiscovered(\n known: DeviceRecord[],\n found: DiscoveredDevice[],\n onCollision?: (dropped: string, takenId: string) => void,\n): DeviceRecord[] {\n const byIp = new Map<string, DeviceRecord>();\n const takenIds = new Set<string>([\"info\"]); // reserved: the adapter's own info channel\n for (const device of known) {\n if (byIp.has(device.ip) || takenIds.has(device.id)) {\n continue;\n }\n byIp.set(device.ip, device);\n takenIds.add(device.id);\n }\n for (const device of found) {\n if (byIp.has(device.ip)) {\n continue;\n }\n const id = sanitizeId(device.name || device.ip);\n if (takenIds.has(id)) {\n onCollision?.(device.name || device.ip, id);\n continue;\n }\n takenIds.add(id);\n byIp.set(device.ip, { id, ip: device.ip });\n }\n return [...byIp.values()];\n}\n\n/**\n * From all object ids under the instance, pick the stale ones to delete on start:\n * everything that does not belong to a configured device and is outside the\n * adapter's own `info` branch. Removes the previous adapter's whole object tree\n * and any device dropped from the config. Keyed on the configured device ids\n * (not on what was created this run), so it works with the async connect where a\n * device's tree may only appear later \u2014 a configured device's subtree is kept\n * regardless of whether it has connected yet. Deepest first, so children go\n * before their parents.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the stale ids to delete, deepest first\n */\nexport function staleObjects(existing: string[], deviceIds: Set<string>, namespace: string): string[] {\n // No configured devices \u2192 never wipe the whole tree (a user who cleared the\n // device table by accident would otherwise lose every object in one pass).\n if (deviceIds.size === 0) {\n return [];\n }\n const isKept = (fullId: string): boolean => {\n const top = stripNamespace(fullId, namespace).split(\".\")[0];\n return top === \"info\" || deviceIds.has(top);\n };\n return existing.filter(id => !isKept(id)).sort((a, b) => b.length - a.length);\n}\n\n/**\n * Relative state ids an earlier version created under a different path and that this\n * version has renamed or moved. On start-up the old object is deleted so it does not\n * linger orphaned beside the new one \u2014 {@link staleObjects} only removes whole\n * non-configured device trees, not renamed states inside a device that is kept.\n */\n/**\n * The per-source playback-block states of the pre-2.0.0 tree \u2014 the same 19 states\n * used to sit under every player source folder, almost all permanently empty. In\n * 2.0.0 ONE flat block per zone replaces them; these copies are removed on the\n * first start (the sources that keep genuinely own states keep those untouched).\n */\nconst V2_PLAYER_BLOCK_STATES = [\n \"playback\",\n \"artist\",\n \"album\",\n \"track\",\n \"station\",\n \"channelName\",\n \"totalTime\",\n \"elapsedTime\",\n \"repeat\",\n \"shuffle\",\n \"albumArt\",\n \"source\",\n \"play\",\n \"pause\",\n \"stop\",\n \"next\",\n \"prev\",\n \"repeatToggle\",\n \"shuffleToggle\",\n];\n\n/** The source folders that stay in 2.0.0 (own states remain) but lose their block copy. */\nconst V2_SLIMMED_SOURCES = [\n \"netPlayer\",\n \"cd\",\n \"netRadio\",\n \"server\",\n \"usb\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"pc\",\n \"airplay\",\n \"bluetooth\",\n];\n\nexport const RENAMED_STATE_IDS = [\n \"hdmiOut\",\n \"directMode\",\n \"masterPower\",\n \"party\",\n \"partyMute\",\n \"distributionEnable\",\n \"partyEnable\",\n \"remoteCode\",\n // v1.0.0 multiroom regroup: the MusicCast-Link states moved into their own\n // multiroom.group folder, so the tree itself tells device-group from all-zones scope.\n \"multiroom.distributionEnable\",\n // Short-lived v1.0.0 pre-cut id \u2014 mislabeled \"active\", the field means \"enabled for use\".\n \"multiroom.group.streamingActive\",\n \"multiroom.role\",\n \"multiroom.groupId\",\n \"multiroom.groupName\",\n \"multiroom.serverZone\",\n \"multiroom.clientList\",\n \"multiroom.linkClient\",\n \"multiroom.leaveGroup\",\n // ---- v2.0.0 tree rework ------------------------------------------------------\n // Player unification: the per-source copies of the playback block are gone (the\n // slimmed folders keep only their own preset/pairing/drive states).\n ...V2_SLIMMED_SOURCES.flatMap(source => V2_PLAYER_BLOCK_STATES.map(state => `player.${source}.${state}`)),\n // Scenes: the twelve per-name datapoints became the recall dropdown + scene.list.\n ...Array.from({ length: 12 }, (_unused, i) => `scene.name${i + 1}`),\n // Tuner unification: ONE band/frequency/preset; the DAB subunit's FM half moved\n // onto the flat tuner ids, the two per-band frequencies became tuner.frequency.\n \"tuner.amFrequency\",\n \"tuner.fmFrequency\",\n \"tuner.dab.band\",\n \"tuner.dab.preset\",\n \"tuner.dab.fmPreset\",\n \"tuner.dab.fmFrequency\",\n \"tuner.dab.fmSearchMode\",\n \"tuner.dab.fmRdsService\",\n \"tuner.dab.fmRdsProgramType\",\n \"tuner.dab.fmRdsText\",\n \"tuner.dab.fmRdsClock\",\n \"tuner.dab.fmStereo\",\n \"tuner.dab.fmTuned\",\n \"tuner.dab.audioMode\",\n // Sound polish: equalizer and signal info each moved into their own subfolder.\n \"sound.equalizerMode\",\n \"sound.equalizerLow\",\n \"sound.equalizerMid\",\n \"sound.equalizerHigh\",\n \"sound.signalFormat\",\n \"sound.signalSampling\",\n \"sound.signalBits\",\n \"sound.signalBitrate\",\n // HDMI polish: the lip-sync offsets moved into the hdmi folder (the lipSync\n // channel itself is in RENAMED_CHANNELS); the A/B toggles joined the speakers.\n \"advanced.speakerA\",\n \"advanced.speakerB\",\n];\n\n/**\n * Old channel prefixes whose whole subtree this version moved out \u2014 the `system`\n * grab-bag is gone (model/firmware \u2192 info, HDMI outputs \u2192 hdmi, speaker patterns \u2192\n * speakers, input names \u2192 inputNames, master power \u2192 multiroom.masterPower). The channel and\n * every state under it are removed.\n */\nexport const RENAMED_CHANNELS = [\n // pre-0.11 system folder\n \"system\",\n // v0.18.1 multiroom regroup: zone2/3/4, zoneB, flat multiroom states moved under multiroom/.\n \"zone2\",\n \"zone3\",\n \"zone4\",\n \"zoneB\",\n // v1.0.0: stray per-zone copies of the device-global YXC states (the zone loop used to\n // prefix them too, yielding multiroom.zoneN.multiroom.* junk) are swept away.\n \"multiroom.zone2.multiroom\",\n \"multiroom.zone3.multiroom\",\n \"multiroom.zone4.multiroom\",\n // Regrouping: media sources moved under player/, DAB under tuner/, dist \u2192 multiroom. The old\n // flat channels (and their whole subtree) are deleted so the new grouped ones do not sit\n // beside orphaned copies on an upgraded instance.\n \"netRadio\",\n \"server\",\n \"usb\",\n \"spotify\",\n \"deezer\",\n \"tidal\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"airplay\",\n \"bluetooth\",\n \"pc\",\n \"musicCastLink\",\n \"ipod\",\n \"ipodUsb\",\n \"netPlayer\",\n \"cd\",\n \"dab\",\n \"dist\",\n // Sound/Advanced regroup: DSP/tone-tuning states moved under sound.*, setup-only states\n // (+ the speakers/initialVolume/inputNames subtrees, already dotted before) under advanced.*.\n // {@link renamedObjectIds} also checks these against a stripped zone2/3/4 prefix, so one\n // entry here catches a MAIN state and its zoned copies (e.g. \"straight\" and \"zone2.straight\").\n \"straight\",\n \"enhancer\",\n \"pureDirect\",\n \"direct\",\n \"adaptiveDrc\",\n \"surroundAI\",\n \"surroundDecoder\",\n \"cinemaDsp3d\",\n \"extraBass\",\n \"bass\",\n \"treble\",\n \"subwooferTrim\",\n \"balance\",\n \"dialogueLevel\",\n \"dialogueLift\",\n \"dtsDialogueControl\",\n \"monaural\",\n \"surround3d\",\n \"adaptiveDspLevel\",\n \"audioSelect\",\n \"linkControl\",\n \"linkAudioDelay\",\n \"linkAudioQuality\",\n \"contentsDisplay\",\n \"equalizerLow\",\n \"equalizerMid\",\n \"equalizerHigh\",\n \"clearVoice\",\n \"bassExtension\",\n \"ypaoVolume\",\n \"maxVolume\",\n \"speakerA\",\n \"speakerB\",\n \"speakers\",\n \"initialVolume\",\n \"inputNames\",\n // ---- v2.0.0 tree rework: the always-empty source channels are gone entirely\n // (their playback lives in the flat per-zone block), lip sync moved into hdmi.\n \"player.spotify\",\n \"player.deezer\",\n \"player.tidal\",\n \"player.ipod\",\n \"player.ipodUsb\",\n \"player.musicCastLink\",\n \"lipSync\",\n];\n\n/**\n * Read-capable state objects under a configured device whose state NEVER carried a\n * value (no value, no last-change). They are over-declarations of an earlier adapter\n * version \u2014 today's claim-with-proof creation would not make them \u2014 and deleting them\n * is lossless: there is no value and no history to lose, and anything a transport\n * legitimately claims is recreated right after, when the device connects. Runs once\n * per adapter version (the caller keeps a marker), so a freshly created state that is\n * merely waiting for its first value does not flap on every start. Excluded: buttons\n * and other write-only states (naturally valueless), states carrying per-state user\n * settings (`common.custom`, e.g. a history binding \u2014 a deliberate user link is never\n * deleted behind their back), and the `info.` header the adapter itself maintains for\n * every device, connected or not.\n *\n * @param objects all objects under the instance, keyed by full id\n * @param states all states under the instance, keyed by full id\n * @param deviceIds the ids of the devices to sweep\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full ids of never-filled read-capable states\n */\nexport function neverWrittenStateIds(\n objects: Record<string, { type?: string; common?: unknown } | undefined>,\n states: Record<string, { val?: unknown; lc?: number } | null | undefined>,\n deviceIds: Set<string>,\n namespace: string,\n): string[] {\n const ids: string[] = [];\n for (const [fullId, object] of Object.entries(objects)) {\n const common = object?.common as { read?: boolean; custom?: Record<string, unknown> } | undefined;\n if (object?.type !== \"state\" || common?.read === false) {\n continue;\n }\n if (common?.custom && Object.keys(common.custom).length > 0) {\n continue;\n }\n const relative = stripNamespace(fullId, namespace);\n const top = relative.split(\".\")[0];\n if (!deviceIds.has(top) || relative.slice(top.length + 1).startsWith(\"info.\")) {\n continue;\n }\n const state = states[fullId];\n if (!state || ((state.val === null || state.val === undefined) && !state.lc)) {\n ids.push(fullId);\n }\n }\n return ids;\n}\n\n/**\n * The full ids of renamed old states (and old channel subtrees) that still exist\n * under a configured device, to be deleted on start-up so no orphan lingers beside\n * the new object. Deepest first, so children go before their parents.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full old ids to delete, deepest first\n */\nexport function renamedObjectIds(existing: string[], deviceIds: Set<string>, namespace: string): string[] {\n const stale: string[] = [];\n for (const deviceId of deviceIds) {\n const base = `${namespace}.${deviceId}.`;\n for (const full of existing) {\n if (!full.startsWith(base)) {\n continue;\n }\n const rel = full.slice(base.length);\n // Strip an optional zone prefix before matching too, so one RENAMED_CHANNELS entry\n // (e.g. \"straight\") catches both the MAIN state and its zoned copies \u2014 the old flat\n // \"zone2.\" form and today's \"multiroom.zone2.\" form alike (v2.0.0 renames live in\n // zoned folders, e.g. multiroom.zone2.scene.name1).\n const zone = /^(?:multiroom\\.)?zone[234]\\./.exec(rel)?.[0] ?? \"\";\n const template = rel.slice(zone.length);\n const renamedState = RENAMED_STATE_IDS.includes(rel) || RENAMED_STATE_IDS.includes(template);\n const underRenamedChannel = RENAMED_CHANNELS.some(\n ch => rel === ch || rel.startsWith(`${ch}.`) || template === ch || template.startsWith(`${ch}.`),\n );\n if (renamedState || underRenamedChannel) {\n stale.push(full);\n }\n }\n }\n return stale.sort((a, b) => b.length - a.length);\n}\n\n/**\n * The device row to carry over from the previous adapter's single-device config,\n * or undefined if nothing needs migrating. The old yamaha stored one receiver as\n * `config.ip` (older installs: `config.IP`); the new adapter uses a `devices`\n * table. Only migrates when the table is still empty, so it runs once.\n *\n * The old value could be a hostname (its HTTP client resolved names) and could\n * carry a `:port` suffix (its HTTP library split host:port). A port suffix would\n * break every transport here \u2014 YNCA is TCP :50000, YXC/XML build `http://<ip>\u2026`\n * \u2014 so it is stripped; the host/IP itself is carried over as-is.\n *\n * @param config the instance's native config\n * @returns the row to add to the devices table, or undefined\n */\nexport function legacyDeviceRow(config: Record<string, unknown>): { name: string; ip: string } | undefined {\n if (Array.isArray(config.devices) && config.devices.length > 0) {\n return undefined;\n }\n const raw =\n typeof config.ip === \"string\" && config.ip\n ? config.ip\n : typeof config.IP === \"string\" && config.IP\n ? config.IP\n : undefined;\n if (!raw) {\n return undefined;\n }\n const ip = raw.trim().replace(/:\\d+$/, \"\");\n return ip ? { name: ip, ip } : undefined;\n}\n\n/**\n * How trustworthy a display-name candidate is. A name the device carries for itself\n * (the MusicCast zone name a user typed in the app) beats its model designation.\n */\nexport const LABEL_RANK = { model: 1, deviceName: 2 } as const;\n\n/** Rank of a display-name candidate \u2014 see {@link LABEL_RANK}. */\nexport type LabelRank = (typeof LABEL_RANK)[keyof typeof LABEL_RANK];\n\n/**\n * Zone names that say nothing about the device \u2014 a receiver ships with these and a\n * user who never renamed the zone would end up with \"Main Zone\" as the device name.\n */\nconst GENERIC_ZONE_NAMES = new Set([\"main\", \"main zone\", \"mainzone\", \"zone\", \"zone 1\", \"zone1\"]);\n\n/**\n * Is this candidate worth showing as a device name?\n *\n * @param candidate the reported name\n * @returns true when it carries information about this particular device\n */\nexport function isUsefulDeviceName(candidate: string | undefined): boolean {\n const trimmed = (candidate ?? \"\").trim();\n return trimmed.length > 0 && !GENERIC_ZONE_NAMES.has(trimmed.toLowerCase());\n}\n\n/**\n * The display name to write onto a device object, or undefined to leave it alone.\n *\n * An upgraded instance carries the IP as its device name: the previous adapter knew\n * only an IP, so the migration had nothing else to call the device, and the object id\n * \u2014 which must not change, every history and visualisation binding hangs off it \u2014\n * became that IP. This decides when the adapter may replace that placeholder with\n * something a user recognises.\n *\n * Two things must never be overwritten: a name the user typed, and a better name by a\n * weaker source. The adapter therefore only writes over its own placeholder (the id\n * itself) or over what it wrote last, and only when the new candidate ranks at least\n * as high as the one behind the current name.\n *\n * @param current the device object's present `common.name`\n * @param deviceId the object id, which is also the placeholder name\n * @param candidate the newly reported name\n * @param rank how trustworthy the candidate is\n * @param ownName the name this adapter wrote last for the device, if any\n * @param ownRank the rank behind {@link ownName}\n * @returns the name to write, or undefined when the current name stays\n */\nexport function nextDeviceLabel(\n current: string | undefined,\n deviceId: string,\n candidate: string | undefined,\n rank: LabelRank,\n ownName?: string,\n ownRank?: LabelRank,\n): string | undefined {\n const wanted = (candidate ?? \"\").trim();\n if (!isUsefulDeviceName(wanted) || wanted === current) {\n return undefined;\n }\n const isPlaceholder = current === undefined || current === deviceId;\n const isOurs = ownName !== undefined && current === ownName;\n if (!isPlaceholder && !isOurs) {\n return undefined; // the user named this device \u2014 theirs wins\n }\n if (isOurs && ownRank !== undefined && rank < ownRank) {\n return undefined; // do not fall back from a device name to its model\n }\n return wanted;\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import type { DeviceRecord } from \"./types\";\nimport type { DiscoveredDevice } from \"./discovery\";\n\ninterface ConfiguredDevice {\n name?: string;\n ip: string;\n}\n\n/**\n * True when a raw config row carries a non-empty ip (the name is optional \u2014 a row\n * with an ip but no name is valid and falls back to the ip as its id, so a device\n * is never silently dropped just because its name was left blank).\n *\n * @param entry a raw config row from the admin device table\n * @returns whether the row is a valid configured device\n */\nfunction isConfiguredDevice(entry: unknown): entry is ConfiguredDevice {\n if (typeof entry !== \"object\" || entry === null) {\n return false;\n }\n const candidate = entry as { name?: unknown; ip?: unknown };\n return (\n typeof candidate.ip === \"string\" &&\n candidate.ip.length > 0 &&\n (candidate.name === undefined || typeof candidate.name === \"string\")\n );\n}\n\n/**\n * Make a string safe for use as an ioBroker object id segment.\n *\n * @param raw the raw string (e.g. a device name)\n * @returns the string with id-unsafe characters replaced by underscores\n */\nexport function sanitizeId(raw: string): string {\n return raw.replace(/[^A-Za-z0-9\\-_]/g, \"_\");\n}\n\n/**\n * Strip the adapter namespace (e.g. `yamaha.0`) from a full state id, leaving the\n * device-relative path (e.g. `living.power`).\n *\n * @param fullId the full state id\n * @param namespace the adapter namespace\n * @returns the id relative to the adapter instance\n */\nexport function stripNamespace(fullId: string, namespace: string): string {\n return fullId.slice(namespace.length + 1);\n}\n\n/**\n * Turn the admin device table (untrusted native config) into device records.\n * Invalid rows are dropped, as are rows whose id collides with the adapter's own\n * reserved `info` branch or with an id already taken \u2014 two names that sanitise to\n * the same id (e.g. \"Living Room\" and \"Living.Room\") would otherwise share one\n * object tree. A dropped collision is reported via `onCollision` so the device does\n * not just silently \"not exist\" for the user.\n *\n * @param raw the raw `native.devices` value\n * @param onCollision called with the dropped row's name/ip and the clashing id\n * @returns validated, de-duplicated device records\n */\nexport function parseDevices(raw: unknown, onCollision?: (dropped: string, takenId: string) => void): DeviceRecord[] {\n if (!Array.isArray(raw)) {\n return [];\n }\n const records: DeviceRecord[] = [];\n const taken = new Set<string>([\"info\"]); // reserved: the adapter's own info channel\n for (const entry of raw) {\n if (!isConfiguredDevice(entry)) {\n continue;\n }\n // Fall back to the ip as the id when the name is blank, so the device still appears\n // instead of vanishing silently.\n const id = sanitizeId(entry.name && entry.name.length > 0 ? entry.name : entry.ip);\n if (taken.has(id)) {\n onCollision?.(entry.name || entry.ip, id);\n continue;\n }\n taken.add(id);\n records.push({ id, ip: entry.ip });\n }\n return records;\n}\n\n/**\n * Merge freshly discovered devices into the set already known from earlier runs\n * (the auto-discovery standby protection). A known device is kept even when this\n * run's scan did not find it \u2014 a receiver in deep standby answers no SSDP, and its\n * object tree must survive. New addresses are added; a discovered device is turned\n * into a record via its friendly name (or its ip when it advertises none), and one\n * whose id would collide with an already-kept device is skipped (reported via\n * `onCollision`, so the missing device is explainable). De-duplicated by ip.\n *\n * @param known the device records remembered from earlier runs\n * @param found the devices discovered this run\n * @param onCollision called with the dropped device's name/ip and the clashing id\n * @returns the merged records, de-duplicated by ip\n */\nexport function mergeDiscovered(\n known: DeviceRecord[],\n found: DiscoveredDevice[],\n onCollision?: (dropped: string, takenId: string) => void,\n): DeviceRecord[] {\n const byIp = new Map<string, DeviceRecord>();\n const takenIds = new Set<string>([\"info\"]); // reserved: the adapter's own info channel\n for (const device of known) {\n if (byIp.has(device.ip) || takenIds.has(device.id)) {\n continue;\n }\n byIp.set(device.ip, device);\n takenIds.add(device.id);\n }\n for (const device of found) {\n if (byIp.has(device.ip)) {\n continue;\n }\n const id = sanitizeId(device.name || device.ip);\n if (takenIds.has(id)) {\n onCollision?.(device.name || device.ip, id);\n continue;\n }\n takenIds.add(id);\n byIp.set(device.ip, { id, ip: device.ip });\n }\n return [...byIp.values()];\n}\n\n/**\n * From all object ids under the instance, pick the stale ones to delete on start:\n * everything that does not belong to a configured device and is outside the\n * adapter's own `info` branch. Removes the previous adapter's whole object tree\n * and any device dropped from the config. Keyed on the configured device ids\n * (not on what was created this run), so it works with the async connect where a\n * device's tree may only appear later \u2014 a configured device's subtree is kept\n * regardless of whether it has connected yet. Deepest first, so children go\n * before their parents.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the stale ids to delete, deepest first\n */\nexport function staleObjects(existing: string[], deviceIds: Set<string>, namespace: string): string[] {\n // No configured devices \u2192 never wipe the whole tree (a user who cleared the\n // device table by accident would otherwise lose every object in one pass).\n if (deviceIds.size === 0) {\n return [];\n }\n const isKept = (fullId: string): boolean => {\n const top = stripNamespace(fullId, namespace).split(\".\")[0];\n return top === \"info\" || deviceIds.has(top);\n };\n return existing.filter(id => !isKept(id)).sort((a, b) => b.length - a.length);\n}\n\n/**\n * Relative state ids an earlier version created under a different path and that this\n * version has renamed or moved. On start-up the old object is deleted so it does not\n * linger orphaned beside the new one \u2014 {@link staleObjects} only removes whole\n * non-configured device trees, not renamed states inside a device that is kept.\n */\n/**\n * The per-source playback-block states of the pre-2.0.0 tree \u2014 the same 19 states\n * used to sit under every player source folder, almost all permanently empty. In\n * 2.0.0 ONE flat block per zone replaces them; these copies are removed on the\n * first start (the sources that keep genuinely own states keep those untouched).\n */\nconst V2_PLAYER_BLOCK_STATES = [\n \"playback\",\n \"artist\",\n \"album\",\n \"track\",\n \"station\",\n \"channelName\",\n \"totalTime\",\n \"elapsedTime\",\n \"repeat\",\n \"shuffle\",\n \"albumArt\",\n \"source\",\n \"play\",\n \"pause\",\n \"stop\",\n \"next\",\n \"prev\",\n \"repeatToggle\",\n \"shuffleToggle\",\n];\n\n/** The source folders that stay in 2.0.0 (own states remain) but lose their block copy. */\nconst V2_SLIMMED_SOURCES = [\n \"netPlayer\",\n \"cd\",\n \"netRadio\",\n \"server\",\n \"usb\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"pc\",\n \"airplay\",\n \"bluetooth\",\n];\n\nexport const RENAMED_STATE_IDS = [\n \"hdmiOut\",\n \"directMode\",\n \"masterPower\",\n \"party\",\n \"partyMute\",\n \"distributionEnable\",\n \"partyEnable\",\n \"remoteCode\",\n // v1.0.0 multiroom regroup: the MusicCast-Link states moved into their own\n // multiroom.group folder, so the tree itself tells device-group from all-zones scope.\n \"multiroom.distributionEnable\",\n // Short-lived v1.0.0 pre-cut id \u2014 mislabeled \"active\", the field means \"enabled for use\".\n \"multiroom.group.streamingActive\",\n \"multiroom.role\",\n \"multiroom.groupId\",\n \"multiroom.groupName\",\n \"multiroom.serverZone\",\n \"multiroom.clientList\",\n \"multiroom.linkClient\",\n \"multiroom.leaveGroup\",\n // ---- v2.0.0 tree rework ------------------------------------------------------\n // Player unification: the per-source copies of the playback block are gone (the\n // slimmed folders keep only their own preset/pairing/drive states).\n ...V2_SLIMMED_SOURCES.flatMap(source => V2_PLAYER_BLOCK_STATES.map(state => `player.${source}.${state}`)),\n // Scenes: the twelve per-name datapoints became the recall dropdown + scene.list.\n ...Array.from({ length: 12 }, (_unused, i) => `scene.name${i + 1}`),\n // Tuner unification: ONE band/frequency/preset; the DAB subunit's FM half moved\n // onto the flat tuner ids, the two per-band frequencies became tuner.frequency.\n \"tuner.amFrequency\",\n \"tuner.fmFrequency\",\n \"tuner.dab.band\",\n \"tuner.dab.preset\",\n \"tuner.dab.fmPreset\",\n \"tuner.dab.fmFrequency\",\n \"tuner.dab.fmSearchMode\",\n \"tuner.dab.fmRdsService\",\n \"tuner.dab.fmRdsProgramType\",\n \"tuner.dab.fmRdsText\",\n \"tuner.dab.fmRdsClock\",\n \"tuner.dab.fmStereo\",\n \"tuner.dab.fmTuned\",\n \"tuner.dab.audioMode\",\n // Sound polish: equalizer and signal info each moved into their own subfolder.\n \"sound.equalizerMode\",\n \"sound.equalizerLow\",\n \"sound.equalizerMid\",\n \"sound.equalizerHigh\",\n \"sound.signalFormat\",\n \"sound.signalSampling\",\n \"sound.signalBits\",\n \"sound.signalBitrate\",\n // HDMI polish: the lip-sync offsets moved into the hdmi folder (the lipSync\n // channel itself is in RENAMED_CHANNELS); the A/B toggles joined the speakers.\n \"advanced.speakerA\",\n \"advanced.speakerB\",\n];\n\n/**\n * Old channel prefixes whose whole subtree this version moved out \u2014 the `system`\n * grab-bag is gone (model/firmware \u2192 info, HDMI outputs \u2192 hdmi, speaker patterns \u2192\n * speakers, input names \u2192 inputNames, master power \u2192 multiroom.masterPower). The channel and\n * every state under it are removed.\n */\nexport const RENAMED_CHANNELS = [\n // pre-0.11 system folder\n \"system\",\n // v0.18.1 multiroom regroup: zone2/3/4, zoneB, flat multiroom states moved under multiroom/.\n \"zone2\",\n \"zone3\",\n \"zone4\",\n \"zoneB\",\n // v1.0.0: stray per-zone copies of the device-global YXC states (the zone loop used to\n // prefix them too, yielding multiroom.zoneN.multiroom.* junk) are swept away.\n \"multiroom.zone2.multiroom\",\n \"multiroom.zone3.multiroom\",\n \"multiroom.zone4.multiroom\",\n // Regrouping: media sources moved under player/, DAB under tuner/, dist \u2192 multiroom. The old\n // flat channels (and their whole subtree) are deleted so the new grouped ones do not sit\n // beside orphaned copies on an upgraded instance.\n \"netRadio\",\n \"server\",\n \"usb\",\n \"spotify\",\n \"deezer\",\n \"tidal\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"airplay\",\n \"bluetooth\",\n \"pc\",\n \"musicCastLink\",\n \"ipod\",\n \"ipodUsb\",\n \"netPlayer\",\n \"cd\",\n \"dab\",\n \"dist\",\n // Sound/Advanced regroup: DSP/tone-tuning states moved under sound.*, setup-only states\n // (+ the speakers/initialVolume/inputNames subtrees, already dotted before) under advanced.*.\n // {@link renamedObjectIds} also checks these against a stripped zone2/3/4 prefix, so one\n // entry here catches a MAIN state and its zoned copies (e.g. \"straight\" and \"zone2.straight\").\n \"straight\",\n \"enhancer\",\n \"pureDirect\",\n \"direct\",\n \"adaptiveDrc\",\n \"surroundAI\",\n \"surroundDecoder\",\n \"cinemaDsp3d\",\n \"extraBass\",\n \"bass\",\n \"treble\",\n \"subwooferTrim\",\n \"balance\",\n \"dialogueLevel\",\n \"dialogueLift\",\n \"dtsDialogueControl\",\n \"monaural\",\n \"surround3d\",\n \"adaptiveDspLevel\",\n \"audioSelect\",\n \"linkControl\",\n \"linkAudioDelay\",\n \"linkAudioQuality\",\n \"contentsDisplay\",\n \"equalizerLow\",\n \"equalizerMid\",\n \"equalizerHigh\",\n \"clearVoice\",\n \"bassExtension\",\n \"ypaoVolume\",\n \"maxVolume\",\n \"speakerA\",\n \"speakerB\",\n \"speakers\",\n \"initialVolume\",\n \"inputNames\",\n // ---- v2.0.0 tree rework: the always-empty source channels are gone entirely\n // (their playback lives in the flat per-zone block), lip sync moved into hdmi.\n \"player.spotify\",\n \"player.deezer\",\n \"player.tidal\",\n \"player.ipod\",\n \"player.ipodUsb\",\n \"player.musicCastLink\",\n \"lipSync\",\n];\n\n/**\n * Read-capable state objects under a configured device whose state NEVER carried a\n * value (no value, no last-change). They are over-declarations of an earlier adapter\n * version \u2014 today's claim-with-proof creation would not make them \u2014 and deleting them\n * is lossless: there is no value and no history to lose, and anything a transport\n * legitimately claims is recreated right after, when the device connects. Runs once\n * per adapter version (the caller keeps a marker), so a freshly created state that is\n * merely waiting for its first value does not flap on every start. Excluded: buttons\n * and other write-only states (naturally valueless) and the `info.` header the adapter\n * itself maintains for every device, connected or not. A recording setting\n * (`common.custom`) is deliberately NO factor: whether anyone records a datapoint is\n * the user's business and says nothing about whether the datapoint belongs in the\n * tree \u2014 that is solely the adapter's responsibility (krobi, 2.0.3). On a never-filled\n * state there is nothing recorded to lose anyway.\n *\n * @param objects all objects under the instance, keyed by full id\n * @param states all states under the instance, keyed by full id\n * @param deviceIds the ids of the devices to sweep\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full ids of never-filled read-capable states\n */\nexport function neverWrittenStateIds(\n objects: Record<string, { type?: string; common?: unknown } | undefined>,\n states: Record<string, { val?: unknown; lc?: number } | null | undefined>,\n deviceIds: Set<string>,\n namespace: string,\n): string[] {\n const ids: string[] = [];\n for (const [fullId, object] of Object.entries(objects)) {\n const common = object?.common as { read?: boolean } | undefined;\n if (object?.type !== \"state\" || common?.read === false) {\n continue;\n }\n const relative = stripNamespace(fullId, namespace);\n const top = relative.split(\".\")[0];\n if (!deviceIds.has(top) || relative.slice(top.length + 1).startsWith(\"info.\")) {\n continue;\n }\n const state = states[fullId];\n if (!state || ((state.val === null || state.val === undefined) && !state.lc)) {\n ids.push(fullId);\n }\n }\n return ids;\n}\n\n/**\n * The full ids of renamed old states (and old channel subtrees) that still exist\n * under a configured device, to be deleted on start-up so no orphan lingers beside\n * the new object. Deepest first, so children go before their parents.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full old ids to delete, deepest first\n */\nexport function renamedObjectIds(existing: string[], deviceIds: Set<string>, namespace: string): string[] {\n const stale: string[] = [];\n for (const deviceId of deviceIds) {\n const base = `${namespace}.${deviceId}.`;\n for (const full of existing) {\n if (!full.startsWith(base)) {\n continue;\n }\n const rel = full.slice(base.length);\n // Strip an optional zone prefix before matching too, so one RENAMED_CHANNELS entry\n // (e.g. \"straight\") catches both the MAIN state and its zoned copies \u2014 the old flat\n // \"zone2.\" form and today's \"multiroom.zone2.\" form alike (v2.0.0 renames live in\n // zoned folders, e.g. multiroom.zone2.scene.name1).\n const zone = /^(?:multiroom\\.)?zone[234]\\./.exec(rel)?.[0] ?? \"\";\n const template = rel.slice(zone.length);\n const renamedState = RENAMED_STATE_IDS.includes(rel) || RENAMED_STATE_IDS.includes(template);\n const underRenamedChannel = RENAMED_CHANNELS.some(\n ch => rel === ch || rel.startsWith(`${ch}.`) || template === ch || template.startsWith(`${ch}.`),\n );\n if (renamedState || underRenamedChannel) {\n stale.push(full);\n }\n }\n }\n return stale.sort((a, b) => b.length - a.length);\n}\n\n/**\n * The device row to carry over from the previous adapter's single-device config,\n * or undefined if nothing needs migrating. The old yamaha stored one receiver as\n * `config.ip` (older installs: `config.IP`); the new adapter uses a `devices`\n * table. Only migrates when the table is still empty, so it runs once.\n *\n * The old value could be a hostname (its HTTP client resolved names) and could\n * carry a `:port` suffix (its HTTP library split host:port). A port suffix would\n * break every transport here \u2014 YNCA is TCP :50000, YXC/XML build `http://<ip>\u2026`\n * \u2014 so it is stripped; the host/IP itself is carried over as-is.\n *\n * @param config the instance's native config\n * @returns the row to add to the devices table, or undefined\n */\nexport function legacyDeviceRow(config: Record<string, unknown>): { name: string; ip: string } | undefined {\n if (Array.isArray(config.devices) && config.devices.length > 0) {\n return undefined;\n }\n const raw =\n typeof config.ip === \"string\" && config.ip\n ? config.ip\n : typeof config.IP === \"string\" && config.IP\n ? config.IP\n : undefined;\n if (!raw) {\n return undefined;\n }\n const ip = raw.trim().replace(/:\\d+$/, \"\");\n return ip ? { name: ip, ip } : undefined;\n}\n\n/**\n * How trustworthy a display-name candidate is. A name the device carries for itself\n * (the MusicCast zone name a user typed in the app) beats its model designation.\n */\nexport const LABEL_RANK = { model: 1, deviceName: 2 } as const;\n\n/** Rank of a display-name candidate \u2014 see {@link LABEL_RANK}. */\nexport type LabelRank = (typeof LABEL_RANK)[keyof typeof LABEL_RANK];\n\n/**\n * Zone names that say nothing about the device \u2014 a receiver ships with these and a\n * user who never renamed the zone would end up with \"Main Zone\" as the device name.\n */\nconst GENERIC_ZONE_NAMES = new Set([\"main\", \"main zone\", \"mainzone\", \"zone\", \"zone 1\", \"zone1\"]);\n\n/**\n * Is this candidate worth showing as a device name?\n *\n * @param candidate the reported name\n * @returns true when it carries information about this particular device\n */\nexport function isUsefulDeviceName(candidate: string | undefined): boolean {\n const trimmed = (candidate ?? \"\").trim();\n return trimmed.length > 0 && !GENERIC_ZONE_NAMES.has(trimmed.toLowerCase());\n}\n\n/**\n * The display name to write onto a device object, or undefined to leave it alone.\n *\n * An upgraded instance carries the IP as its device name: the previous adapter knew\n * only an IP, so the migration had nothing else to call the device, and the object id\n * \u2014 which must not change, every history and visualisation binding hangs off it \u2014\n * became that IP. This decides when the adapter may replace that placeholder with\n * something a user recognises.\n *\n * Two things must never be overwritten: a name the user typed, and a better name by a\n * weaker source. The adapter therefore only writes over its own placeholder (the id\n * itself) or over what it wrote last, and only when the new candidate ranks at least\n * as high as the one behind the current name.\n *\n * @param current the device object's present `common.name`\n * @param deviceId the object id, which is also the placeholder name\n * @param candidate the newly reported name\n * @param rank how trustworthy the candidate is\n * @param ownName the name this adapter wrote last for the device, if any\n * @param ownRank the rank behind {@link ownName}\n * @returns the name to write, or undefined when the current name stays\n */\nexport function nextDeviceLabel(\n current: string | undefined,\n deviceId: string,\n candidate: string | undefined,\n rank: LabelRank,\n ownName?: string,\n ownRank?: LabelRank,\n): string | undefined {\n const wanted = (candidate ?? \"\").trim();\n if (!isUsefulDeviceName(wanted) || wanted === current) {\n return undefined;\n }\n const isPlaceholder = current === undefined || current === deviceId;\n const isOurs = ownName !== undefined && current === ownName;\n if (!isPlaceholder && !isOurs) {\n return undefined; // the user named this device \u2014 theirs wins\n }\n if (isOurs && ownRank !== undefined && rank < ownRank) {\n return undefined; // do not fall back from a device name to its model\n }\n return wanted;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,SAAS,mBAAmB,OAA2C;AACrE,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,WAAO;AAAA,EACT;AACA,QAAM,YAAY;AAClB,SACE,OAAO,UAAU,OAAO,YACxB,UAAU,GAAG,SAAS,MACrB,UAAU,SAAS,UAAa,OAAO,UAAU,SAAS;AAE/D;AAQO,SAAS,WAAW,KAAqB;AAC9C,SAAO,IAAI,QAAQ,oBAAoB,GAAG;AAC5C;AAUO,SAAS,eAAe,QAAgB,WAA2B;AACxE,SAAO,OAAO,MAAM,UAAU,SAAS,CAAC;AAC1C;AAcO,SAAS,aAAa,KAAc,aAA0E;AACnH,MAAI,CAAC,MAAM,QAAQ,GAAG,GAAG;AACvB,WAAO,CAAC;AAAA,EACV;AACA,QAAM,UAA0B,CAAC;AACjC,QAAM,QAAQ,oBAAI,IAAY,CAAC,MAAM,CAAC;AACtC,aAAW,SAAS,KAAK;AACvB,QAAI,CAAC,mBAAmB,KAAK,GAAG;AAC9B;AAAA,IACF;AAGA,UAAM,KAAK,WAAW,MAAM,QAAQ,MAAM,KAAK,SAAS,IAAI,MAAM,OAAO,MAAM,EAAE;AACjF,QAAI,MAAM,IAAI,EAAE,GAAG;AACjB,iDAAc,MAAM,QAAQ,MAAM,IAAI;AACtC;AAAA,IACF;AACA,UAAM,IAAI,EAAE;AACZ,YAAQ,KAAK,EAAE,IAAI,IAAI,MAAM,GAAG,CAAC;AAAA,EACnC;AACA,SAAO;AACT;AAgBO,SAAS,gBACd,OACA,OACA,aACgB;AAChB,QAAM,OAAO,oBAAI,IAA0B;AAC3C,QAAM,WAAW,oBAAI,IAAY,CAAC,MAAM,CAAC;AACzC,aAAW,UAAU,OAAO;AAC1B,QAAI,KAAK,IAAI,OAAO,EAAE,KAAK,SAAS,IAAI,OAAO,EAAE,GAAG;AAClD;AAAA,IACF;AACA,SAAK,IAAI,OAAO,IAAI,MAAM;AAC1B,aAAS,IAAI,OAAO,EAAE;AAAA,EACxB;AACA,aAAW,UAAU,OAAO;AAC1B,QAAI,KAAK,IAAI,OAAO,EAAE,GAAG;AACvB;AAAA,IACF;AACA,UAAM,KAAK,WAAW,OAAO,QAAQ,OAAO,EAAE;AAC9C,QAAI,SAAS,IAAI,EAAE,GAAG;AACpB,iDAAc,OAAO,QAAQ,OAAO,IAAI;AACxC;AAAA,IACF;AACA,aAAS,IAAI,EAAE;AACf,SAAK,IAAI,OAAO,IAAI,EAAE,IAAI,IAAI,OAAO,GAAG,CAAC;AAAA,EAC3C;AACA,SAAO,CAAC,GAAG,KAAK,OAAO,CAAC;AAC1B;AAiBO,SAAS,aAAa,UAAoB,WAAwB,WAA6B;AAGpG,MAAI,UAAU,SAAS,GAAG;AACxB,WAAO,CAAC;AAAA,EACV;AACA,QAAM,SAAS,CAAC,WAA4B;AAC1C,UAAM,MAAM,eAAe,QAAQ,SAAS,EAAE,MAAM,GAAG,EAAE,CAAC;AAC1D,WAAO,QAAQ,UAAU,UAAU,IAAI,GAAG;AAAA,EAC5C;AACA,SAAO,SAAS,OAAO,QAAM,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;AAC9E;AAcA,MAAM,yBAAyB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,MAAM,qBAAqB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA,EAIA,GAAG,mBAAmB,QAAQ,YAAU,uBAAuB,IAAI,WAAS,UAAU,MAAM,IAAI,KAAK,EAAE,CAAC;AAAA;AAAA,EAExG,GAAG,MAAM,KAAK,EAAE,QAAQ,GAAG,GAAG,CAAC,SAAS,MAAM,aAAa,IAAI,CAAC,EAAE;AAAA;AAAA;AAAA,EAGlE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AACF;AAQO,MAAM,mBAAmB;AAAA;AAAA,EAE9B;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA,EAIA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAuBO,SAAS,qBACd,SACA,QACA,WACA,WACU;AACV,QAAM,MAAgB,CAAC;AACvB,aAAW,CAAC,QAAQ,MAAM,KAAK,OAAO,QAAQ,OAAO,GAAG;AACtD,UAAM,SAAS,iCAAQ;AACvB,SAAI,iCAAQ,UAAS,YAAW,iCAAQ,UAAS,OAAO;AACtD;AAAA,IACF;AACA,UAAM,WAAW,eAAe,QAAQ,SAAS;AACjD,UAAM,MAAM,SAAS,MAAM,GAAG,EAAE,CAAC;AACjC,QAAI,CAAC,UAAU,IAAI,GAAG,KAAK,SAAS,MAAM,IAAI,SAAS,CAAC,EAAE,WAAW,OAAO,GAAG;AAC7E;AAAA,IACF;AACA,UAAM,QAAQ,OAAO,MAAM;AAC3B,QAAI,CAAC,UAAW,MAAM,QAAQ,QAAQ,MAAM,QAAQ,WAAc,CAAC,MAAM,IAAK;AAC5E,UAAI,KAAK,MAAM;AAAA,IACjB;AAAA,EACF;AACA,SAAO;AACT;AAYO,SAAS,iBAAiB,UAAoB,WAAwB,WAA6B;AA7Z1G;AA8ZE,QAAM,QAAkB,CAAC;AACzB,aAAW,YAAY,WAAW;AAChC,UAAM,OAAO,GAAG,SAAS,IAAI,QAAQ;AACrC,eAAW,QAAQ,UAAU;AAC3B,UAAI,CAAC,KAAK,WAAW,IAAI,GAAG;AAC1B;AAAA,MACF;AACA,YAAM,MAAM,KAAK,MAAM,KAAK,MAAM;AAKlC,YAAM,QAAO,0CAA+B,KAAK,GAAG,MAAvC,mBAA2C,OAA3C,YAAiD;AAC9D,YAAM,WAAW,IAAI,MAAM,KAAK,MAAM;AACtC,YAAM,eAAe,kBAAkB,SAAS,GAAG,KAAK,kBAAkB,SAAS,QAAQ;AAC3F,YAAM,sBAAsB,iBAAiB;AAAA,QAC3C,QAAM,QAAQ,MAAM,IAAI,WAAW,GAAG,EAAE,GAAG,KAAK,aAAa,MAAM,SAAS,WAAW,GAAG,EAAE,GAAG;AAAA,MACjG;AACA,UAAI,gBAAgB,qBAAqB;AACvC,cAAM,KAAK,IAAI;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACA,SAAO,MAAM,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;AACjD;AAgBO,SAAS,gBAAgB,QAA2E;AACzG,MAAI,MAAM,QAAQ,OAAO,OAAO,KAAK,OAAO,QAAQ,SAAS,GAAG;AAC9D,WAAO;AAAA,EACT;AACA,QAAM,MACJ,OAAO,OAAO,OAAO,YAAY,OAAO,KACpC,OAAO,KACP,OAAO,OAAO,OAAO,YAAY,OAAO,KACtC,OAAO,KACP;AACR,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,EACT;AACA,QAAM,KAAK,IAAI,KAAK,EAAE,QAAQ,SAAS,EAAE;AACzC,SAAO,KAAK,EAAE,MAAM,IAAI,GAAG,IAAI;AACjC;AAMO,MAAM,aAAa,EAAE,OAAO,GAAG,YAAY,EAAE;AASpD,MAAM,qBAAqB,oBAAI,IAAI,CAAC,QAAQ,aAAa,YAAY,QAAQ,UAAU,OAAO,CAAC;AAQxF,SAAS,mBAAmB,WAAwC;AACzE,QAAM,WAAW,gCAAa,IAAI,KAAK;AACvC,SAAO,QAAQ,SAAS,KAAK,CAAC,mBAAmB,IAAI,QAAQ,YAAY,CAAC;AAC5E;AAwBO,SAAS,gBACd,SACA,UACA,WACA,MACA,SACA,SACoB;AACpB,QAAM,UAAU,gCAAa,IAAI,KAAK;AACtC,MAAI,CAAC,mBAAmB,MAAM,KAAK,WAAW,SAAS;AACrD,WAAO;AAAA,EACT;AACA,QAAM,gBAAgB,YAAY,UAAa,YAAY;AAC3D,QAAM,SAAS,YAAY,UAAa,YAAY;AACpD,MAAI,CAAC,iBAAiB,CAAC,QAAQ;AAC7B,WAAO;AAAA,EACT;AACA,MAAI,UAAU,YAAY,UAAa,OAAO,SAAS;AACrD,WAAO;AAAA,EACT;AACA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/lib/util.js
CHANGED
|
@@ -18,14 +18,17 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var util_exports = {};
|
|
20
20
|
__export(util_exports, {
|
|
21
|
+
MAX_HTTP_BODY_BYTES: () => MAX_HTTP_BODY_BYTES,
|
|
21
22
|
errorMessage: () => errorMessage
|
|
22
23
|
});
|
|
23
24
|
module.exports = __toCommonJS(util_exports);
|
|
24
25
|
function errorMessage(e) {
|
|
25
26
|
return e instanceof Error ? e.message : String(e);
|
|
26
27
|
}
|
|
28
|
+
const MAX_HTTP_BODY_BYTES = 1024 * 1024;
|
|
27
29
|
// Annotate the CommonJS export names for ESM import in node:
|
|
28
30
|
0 && (module.exports = {
|
|
31
|
+
MAX_HTTP_BODY_BYTES,
|
|
29
32
|
errorMessage
|
|
30
33
|
});
|
|
31
34
|
//# sourceMappingURL=util.js.map
|
package/build/lib/util.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/util.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * The message of an unknown thrown value \u2014 `Error.message` when it is an Error, else\n * its string form. One helper for the `e instanceof Error ? e.message : String(e)`\n * idiom the transports would otherwise repeat in every catch.\n *\n * @param e the caught value\n * @returns a human-readable message\n */\nexport function errorMessage(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAQO,SAAS,aAAa,GAAoB;AAC/C,SAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AAClD;",
|
|
4
|
+
"sourcesContent": ["/**\n * The message of an unknown thrown value \u2014 `Error.message` when it is an Error, else\n * its string form. One helper for the `e instanceof Error ? e.message : String(e)`\n * idiom the transports would otherwise repeat in every catch.\n *\n * @param e the caught value\n * @returns a human-readable message\n */\nexport function errorMessage(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n\n/**\n * Cap for every HTTP response body the adapter reads from a device: MusicCast JSON, XML\n * answers, the UPnP description of the network search. Real answers are a few KB (the\n * largest observed, the RX-V6A getFeatures map, is ~5 KB). A body growing past this is\n * not a Yamaha answering \u2014 a misbehaving device or a foreign host at that address\n * streaming without end would otherwise grow the process's memory without bound.\n */\nexport const MAX_HTTP_BODY_BYTES = 1024 * 1024;\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQO,SAAS,aAAa,GAAoB;AAC/C,SAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AAClD;AASO,MAAM,sBAAsB,OAAO;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|