iobroker.yamaha 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -17
- package/admin/i18n/de.json +2 -0
- package/admin/i18n/en.json +2 -0
- package/admin/i18n/es.json +2 -0
- package/admin/i18n/fr.json +2 -0
- package/admin/i18n/it.json +2 -0
- package/admin/i18n/nl.json +2 -0
- package/admin/i18n/pl.json +2 -0
- package/admin/i18n/pt.json +2 -0
- package/admin/i18n/ru.json +2 -0
- package/admin/i18n/uk.json +2 -0
- package/admin/i18n/zh-cn.json +2 -0
- package/admin/jsonConfig.json +10 -0
- package/build/device-management.js +9 -1
- package/build/device-management.js.map +2 -2
- package/build/lib/attempt-device.js +1 -0
- package/build/lib/attempt-device.js.map +2 -2
- package/build/lib/catalog/groups.js +5 -1
- package/build/lib/catalog/groups.js.map +2 -2
- package/build/lib/catalog/object-tree-coordinator.js.map +2 -2
- package/build/lib/catalog/owner-policy.js +2 -0
- package/build/lib/catalog/owner-policy.js.map +2 -2
- package/build/lib/catalog/value-coerce.js +12 -0
- package/build/lib/catalog/value-coerce.js.map +2 -2
- package/build/lib/pure-helpers.js +27 -0
- package/build/lib/pure-helpers.js.map +2 -2
- package/build/lib/xml/command-mapper.js.map +2 -2
- package/build/lib/xml/device-controller.js.map +2 -2
- package/build/lib/ynca/catalog.js +97 -18
- package/build/lib/ynca/catalog.js.map +2 -2
- package/build/lib/yxc/capability.js +56 -4
- package/build/lib/yxc/capability.js.map +2 -2
- package/build/lib/yxc/catalog.js +21 -0
- package/build/lib/yxc/catalog.js.map +2 -2
- package/build/lib/yxc/client-contract.js.map +1 -1
- package/build/lib/yxc/command-mapper.js +241 -6
- package/build/lib/yxc/command-mapper.js.map +2 -2
- package/build/lib/yxc/device-controller.js +121 -3
- package/build/lib/yxc/device-controller.js.map +2 -2
- package/build/lib/yxc/http-client.js +73 -0
- package/build/lib/yxc/http-client.js.map +2 -2
- package/build/lib/yxc/object-mapper.js +204 -6
- package/build/lib/yxc/object-mapper.js.map +2 -2
- package/build/lib/yxc/push.js +10 -0
- package/build/lib/yxc/push.js.map +2 -2
- package/build/main.js +57 -2
- package/build/main.js.map +2 -2
- package/io-package.json +29 -2
- package/package.json +14 -4
package/README.md
CHANGED
|
@@ -19,6 +19,8 @@ legacy XML protocol of the oldest pre-2010 models — behind one object tree.
|
|
|
19
19
|
- **Instant updates** — MusicCast pushes its changes, YNCA reports over its live connection
|
|
20
20
|
- **Self-healing connections** — an offline receiver joins once it answers; a single protocol reconnects on its own while the others keep running
|
|
21
21
|
- **Typed datapoints** — booleans, dropdowns and numbers with unit and range instead of raw text
|
|
22
|
+
- **Presets and favourites** — recall tuner presets and stored network/USB favourites by number, step through presets, and read the stored lists with their names (MusicCast); recently-played recall on MusicCast devices
|
|
23
|
+
- **Clock & alarm view** — MusicCast desk-audio devices show their clock and alarm settings
|
|
22
24
|
- **Capability-driven** — states are generated from what each device reports, no hardcoded model list
|
|
23
25
|
- **Automatic discovery** — an empty device list finds and sets up MusicCast devices at startup
|
|
24
26
|
- **Device manager** — receivers as admin cards with model, address, live protocol indicators and a device-type icon (receiver, stereo, speaker, soundbar, CD)
|
|
@@ -31,7 +33,7 @@ legacy XML protocol of the oldest pre-2010 models — behind one object tree.
|
|
|
31
33
|
|
|
32
34
|
## Ports
|
|
33
35
|
|
|
34
|
-
- **UDP 41100 (listening)** — MusicCast devices push their change events to this port on the ioBroker host.
|
|
36
|
+
- **UDP 41100 (listening)** — MusicCast devices push their change events to this port on the ioBroker host.
|
|
35
37
|
- **UDP 1900 (multicast, outgoing)** — the SSDP discovery search at startup.
|
|
36
38
|
- **TCP 50000 (outgoing)** — the YNCA control connection to each receiver.
|
|
37
39
|
- **TCP 80 (outgoing)** — the MusicCast and XML protocol requests to each device.
|
|
@@ -86,6 +88,23 @@ On the first connect the adapter asks the receiver which functions it supports
|
|
|
86
88
|
Placeholder for the next version (at the beginning of the line):
|
|
87
89
|
### **WORK IN PROGRESS**
|
|
88
90
|
-->
|
|
91
|
+
### 1.2.0 (2026-08-25)
|
|
92
|
+
|
|
93
|
+
- (krobipd) Fixed: volume writes work again — a written -38 dB reached the receiver as -3.8 dB, so most values were ignored; all numeric controls now send the proper wire format (#612)
|
|
94
|
+
- (krobipd) Fixed: the FM frequency datapoint now shows MHz (it was mislabelled kHz) and accepts direct frequency writes in the form the tuner expects.
|
|
95
|
+
- (krobipd) New: preset selection — recall tuner presets by number with up/down stepping, and recall stored network or USB favourites per source on YNCA receivers (#613)
|
|
96
|
+
- (krobipd) New: MusicCast selection lists — stored favourites and tuner presets with names, a recently-played list with recall by number, and the device's own allowed values as dropdowns.
|
|
97
|
+
- (krobipd) New: more device detail — CD track and drive info, DAB and RDS station data, and a read-only clock and alarm view with its own datapoint group switch in the admin settings.
|
|
98
|
+
|
|
99
|
+
### 1.1.1 (2026-08-22)
|
|
100
|
+
|
|
101
|
+
- (krobipd) Changed: Internal cleanup. No user-facing changes.
|
|
102
|
+
|
|
103
|
+
### 1.1.0 (2026-08-22)
|
|
104
|
+
|
|
105
|
+
- (krobipd) Fixed: a device carried over from the old adapter is no longer called by its IP — the object folder and the admin card now show the name the device reports, or its model.
|
|
106
|
+
- (krobipd) Improved: a device that has not reported a model yet already carries its device-class symbol instead of none.
|
|
107
|
+
|
|
89
108
|
### 1.0.1 (2026-08-22)
|
|
90
109
|
|
|
91
110
|
- (krobipd) Complete rebuild: one adapter now speaks YNCA, MusicCast and the legacy XML protocol — every protocol a device answers runs in parallel on one object tree.
|
|
@@ -102,22 +121,6 @@ On the first connect the adapter asks the receiver which functions it supports
|
|
|
102
121
|
|
|
103
122
|
- (foxriver76) updated packages
|
|
104
123
|
|
|
105
|
-
### 0.5.3 (2022-06-17)
|
|
106
|
-
|
|
107
|
-
- (Apollon77) Fix crash cases reported by Sentry
|
|
108
|
-
|
|
109
|
-
### 0.5.2 (2022-04-23)
|
|
110
|
-
|
|
111
|
-
- (Apollon77) Fix crash cases reported by Sentry
|
|
112
|
-
|
|
113
|
-
### 0.5.1 (2022-03-29)
|
|
114
|
-
|
|
115
|
-
- (Apollon77) Fix crash cases reported by Sentry
|
|
116
|
-
- (Apollon77) fix type of pureDirect
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
124
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
122
125
|
|
|
123
126
|
## History
|
package/admin/i18n/de.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "HDMI-Ausgänge und Lippensynchron-Versatz, für jede Zone",
|
|
34
34
|
"group_scene": "Szenen",
|
|
35
35
|
"group_scene_help": "Gespeicherte Szenen abrufen",
|
|
36
|
+
"group_clock": "Uhr & Wecker",
|
|
37
|
+
"group_clock_help": "Uhr- und Wecker-Datenpunkte",
|
|
36
38
|
"group_sound": "Klang",
|
|
37
39
|
"group_sound_help": "Klangmodi, Klangregler, Subwoofer, Balance und Equalizer",
|
|
38
40
|
"group_advanced": "Erweitert",
|
package/admin/i18n/en.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "HDMI outputs and lip-sync offset, for every zone",
|
|
34
34
|
"group_scene": "Scenes",
|
|
35
35
|
"group_scene_help": "Recall saved scenes",
|
|
36
|
+
"group_clock": "Clock & alarm",
|
|
37
|
+
"group_clock_help": "Clock and alarm datapoints",
|
|
36
38
|
"group_sound": "Sound",
|
|
37
39
|
"group_sound_help": "Sound-processing modes, tone controls, subwoofer, balance and equalizer",
|
|
38
40
|
"group_advanced": "Advanced",
|
package/admin/i18n/es.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "Salidas HDMI y desfase de sincronización labial, para cada zona",
|
|
34
34
|
"group_scene": "Escenas",
|
|
35
35
|
"group_scene_help": "Recuperar escenas guardadas",
|
|
36
|
+
"group_clock": "Reloj y alarma",
|
|
37
|
+
"group_clock_help": "Puntos de datos de reloj y alarma",
|
|
36
38
|
"group_sound": "Sonido",
|
|
37
39
|
"group_sound_help": "Modos de procesamiento de sonido, control de tono, subwoofer, balance y ecualizador",
|
|
38
40
|
"group_advanced": "Avanzado",
|
package/admin/i18n/fr.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "Sorties HDMI et décalage de synchronisation labiale, pour chaque zone",
|
|
34
34
|
"group_scene": "Scènes",
|
|
35
35
|
"group_scene_help": "Rappeler les scènes enregistrées",
|
|
36
|
+
"group_clock": "Horloge et réveil",
|
|
37
|
+
"group_clock_help": "Points de données horloge et réveil",
|
|
36
38
|
"group_sound": "Son",
|
|
37
39
|
"group_sound_help": "Modes de traitement du son, réglages de tonalité, caisson de basses, balance et égaliseur",
|
|
38
40
|
"group_advanced": "Avancé",
|
package/admin/i18n/it.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "Uscite HDMI e sfasamento lip-sync, per ogni zona",
|
|
34
34
|
"group_scene": "Scene",
|
|
35
35
|
"group_scene_help": "Richiama le scene salvate",
|
|
36
|
+
"group_clock": "Orologio e sveglia",
|
|
37
|
+
"group_clock_help": "Punti dati di orologio e sveglia",
|
|
36
38
|
"group_sound": "Audio",
|
|
37
39
|
"group_sound_help": "Modalità di elaborazione audio, toni, subwoofer, bilanciamento ed equalizzatore",
|
|
38
40
|
"group_advanced": "Avanzate",
|
package/admin/i18n/nl.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "HDMI-uitgangen en lip-sync-offset, voor elke zone",
|
|
34
34
|
"group_scene": "Scènes",
|
|
35
35
|
"group_scene_help": "Opgeslagen scènes oproepen",
|
|
36
|
+
"group_clock": "Klok & wekker",
|
|
37
|
+
"group_clock_help": "Klok- en wekkerdatapunten",
|
|
36
38
|
"group_sound": "Geluid",
|
|
37
39
|
"group_sound_help": "Geluidsmodi, klankregeling, subwoofer, balans en equalizer",
|
|
38
40
|
"group_advanced": "Geavanceerd",
|
package/admin/i18n/pl.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "Wyjścia HDMI i przesunięcie synchronizacji ust, dla każdej strefy",
|
|
34
34
|
"group_scene": "Sceny",
|
|
35
35
|
"group_scene_help": "Przywoływanie zapisanych scen",
|
|
36
|
+
"group_clock": "Zegar i budzik",
|
|
37
|
+
"group_clock_help": "Punkty danych zegara i budzika",
|
|
36
38
|
"group_sound": "Dźwięk",
|
|
37
39
|
"group_sound_help": "Tryby przetwarzania dźwięku, tony, subwoofer, balans i korektor",
|
|
38
40
|
"group_advanced": "Zaawansowane",
|
package/admin/i18n/pt.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "Saídas HDMI e desvio de sincronização labial, para cada zona",
|
|
34
34
|
"group_scene": "Cenas",
|
|
35
35
|
"group_scene_help": "Recuperar cenas guardadas",
|
|
36
|
+
"group_clock": "Relógio e alarme",
|
|
37
|
+
"group_clock_help": "Pontos de dados de relógio e alarme",
|
|
36
38
|
"group_sound": "Som",
|
|
37
39
|
"group_sound_help": "Modos de processamento de som, controlo de tom, subwoofer, balanço e equalizador",
|
|
38
40
|
"group_advanced": "Avançado",
|
package/admin/i18n/ru.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "Выходы HDMI и смещение синхронизации звука и видео для каждой зоны",
|
|
34
34
|
"group_scene": "Сцены",
|
|
35
35
|
"group_scene_help": "Вызов сохранённых сцен",
|
|
36
|
+
"group_clock": "Часы и будильник",
|
|
37
|
+
"group_clock_help": "Точки данных часов и будильника",
|
|
36
38
|
"group_sound": "Звук",
|
|
37
39
|
"group_sound_help": "Режимы обработки звука, тембр, сабвуфер, баланс и эквалайзер",
|
|
38
40
|
"group_advanced": "Расширенные",
|
package/admin/i18n/uk.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "Виходи HDMI та зсув синхронізації звуку й відео для кожної зони",
|
|
34
34
|
"group_scene": "Сцени",
|
|
35
35
|
"group_scene_help": "Виклик збережених сцен",
|
|
36
|
+
"group_clock": "Годинник і будильник",
|
|
37
|
+
"group_clock_help": "Точки даних годинника та будильника",
|
|
36
38
|
"group_sound": "Звук",
|
|
37
39
|
"group_sound_help": "Режими обробки звуку, тембр, сабвуфер, баланс і еквалайзер",
|
|
38
40
|
"group_advanced": "Розширені",
|
package/admin/i18n/zh-cn.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "每个区域的 HDMI 输出和唇音同步偏移",
|
|
34
34
|
"group_scene": "场景",
|
|
35
35
|
"group_scene_help": "调用已保存的场景",
|
|
36
|
+
"group_clock": "时钟与闹钟",
|
|
37
|
+
"group_clock_help": "时钟和闹钟数据点",
|
|
36
38
|
"group_sound": "音效",
|
|
37
39
|
"group_sound_help": "音效处理模式、音调、低音炮、平衡和均衡器",
|
|
38
40
|
"group_advanced": "高级",
|
package/admin/jsonConfig.json
CHANGED
|
@@ -110,6 +110,16 @@
|
|
|
110
110
|
"lg": 4,
|
|
111
111
|
"xl": 3
|
|
112
112
|
},
|
|
113
|
+
"group_clock": {
|
|
114
|
+
"type": "checkbox",
|
|
115
|
+
"label": "group_clock",
|
|
116
|
+
"help": "group_clock_help",
|
|
117
|
+
"xs": 12,
|
|
118
|
+
"sm": 6,
|
|
119
|
+
"md": 4,
|
|
120
|
+
"lg": 4,
|
|
121
|
+
"xl": 3
|
|
122
|
+
},
|
|
113
123
|
"group_sound": {
|
|
114
124
|
"type": "checkbox",
|
|
115
125
|
"label": "group_sound",
|
|
@@ -82,9 +82,17 @@ class YamahaDeviceManagement extends import_dm_utils.DeviceManagement {
|
|
|
82
82
|
* @param context the load context
|
|
83
83
|
*/
|
|
84
84
|
async loadDevices(context) {
|
|
85
|
+
var _a;
|
|
85
86
|
for (const card of await this.cards()) {
|
|
86
87
|
const model = await this.adapter.getForeignStateAsync(`${this.adapter.namespace}.${card.id}.info.model`);
|
|
87
|
-
|
|
88
|
+
const node = await this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${card.id}`);
|
|
89
|
+
const label = typeof ((_a = node == null ? void 0 : node.common) == null ? void 0 : _a.name) === "string" ? node.common.name : void 0;
|
|
90
|
+
context.addDevice(
|
|
91
|
+
this.toDeviceInfo(
|
|
92
|
+
label && label !== card.id ? { ...card, name: label } : card,
|
|
93
|
+
typeof (model == null ? void 0 : model.val) === "string" ? model.val : void 0
|
|
94
|
+
)
|
|
95
|
+
);
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/device-management.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n DeviceManagement,\n type ActionContext,\n type DeviceInfo,\n type DeviceLoadContext,\n type InstanceDetails,\n} from \"@iobroker/dm-utils\";\nimport { t } from \"./lib/i18n\";\nimport { iconForModel } from \"./lib/device-type\";\nimport { readDiscovered, writeDiscovered } from \"./lib/discovered-store\";\nimport { discoveredStoreDeps } from \"./lib/discovered-store-deps\";\nimport type { DeviceRecord } from \"./lib/types\";\nimport {\n TRANSPORTS,\n buildDeviceForm,\n findClash,\n rowId,\n type CardDevice,\n type ManualRow,\n} from \"./device-management-helpers\";\n\n/**\n * ioBroker device-manager backend: the Yamaha receivers as cards showing the live model,\n * the IP, and which protocols (YNCA/MusicCast/XML) are connected right now, with a manual\n * add-by-IP dialog. The card list follows the running set \u2014 the manual `native.devices`\n * table when it is filled, otherwise the auto-discovered devices \u2014 so it matches exactly\n * what the adapter runs. \"Yamaha\" is never a card line: it is the whole adapter.\n */\nexport class YamahaDeviceManagement extends DeviceManagement {\n /** The instance object id whose `native` holds the manual device table. */\n private get objId(): string {\n return `system.adapter.${this.adapter.namespace}`;\n }\n\n /** Read the manual device table (`native.devices`) as raw rows, keeping the name. */\n private async readManual(): Promise<ManualRow[]> {\n const obj = await this.adapter.getForeignObjectAsync(this.objId);\n const devices = (obj?.native as { devices?: unknown } | undefined)?.devices;\n if (!Array.isArray(devices)) {\n return [];\n }\n return devices.filter(\n (d): d is ManualRow => !!d && typeof (d as ManualRow).ip === \"string\" && (d as ManualRow).ip.length > 0,\n );\n }\n\n /**\n * Persist the manual device table; writing `native.*` restarts the adapter with the new set.\n *\n * @param rows the manual rows to store\n */\n private async writeManual(rows: ManualRow[]): Promise<void> {\n await this.adapter.extendForeignObjectAsync(this.objId, { native: { devices: rows } });\n }\n\n /**\n * The running device set as cards: the manual table when filled (manual mode), otherwise\n * the auto-discovered devices (auto mode) \u2014 the same either/or the adapter itself runs.\n *\n * @returns the cards with their source\n */\n private async cards(): Promise<CardDevice[]> {\n const manual = await this.readManual();\n if (manual.length > 0) {\n const taken = new Set<string>([\"info\"]);\n const cards: CardDevice[] = [];\n for (const row of manual) {\n const id = rowId(row);\n if (taken.has(id)) {\n continue;\n }\n taken.add(id);\n cards.push({ id, ip: row.ip, name: row.name && row.name.length > 0 ? row.name : row.ip, source: \"manual\" });\n }\n return cards;\n }\n const discovered = await readDiscovered(discoveredStoreDeps(this.adapter));\n return discovered.map(d => ({ id: d.id, ip: d.ip, name: d.id, source: \"discovered\" as const }));\n }\n\n /**\n * Populate the manager with one card per running device.\n *\n * @param context the load context\n */\n protected async loadDevices(context: DeviceLoadContext<string>): Promise<void> {\n for (const card of await this.cards()) {\n // The reported model picks the device-class silhouette shown on the card.\n const model = await this.adapter.getForeignStateAsync(`${this.adapter.namespace}.${card.id}.info.model`);\n context.addDevice(this.toDeviceInfo(card, typeof model?.val === \"string\" ? model.val : undefined));\n }\n }\n\n /**\n * Build one device card: the live model, the IP as the identifier line, a connection\n * status, and one indicator per connected protocol (hidden while that protocol is not\n * connected). All live values read from the device's own `info.*` states. A discovered\n * card has no edit action \u2014 it is auto-found and re-found; it can only be removed.\n *\n * @param card the running device\n * @param model the device's reported model name, for the device-class icon\n * @returns the card descriptor\n */\n private toDeviceInfo(card: CardDevice, model?: string): DeviceInfo<string> {\n const base = `${this.adapter.namespace}.${card.id}`;\n const del = {\n id: \"delete\",\n icon: \"delete\",\n description: t(\"dmDelete\"),\n handler: async (id: string, ctx: ActionContext): Promise<{ refresh: \"devices\" }> => this.deleteDevice(id, ctx),\n };\n const edit = {\n id: \"edit\",\n icon: \"edit\",\n description: t(\"dmEdit\"),\n handler: async (id: string, ctx: ActionContext): Promise<{ refresh: \"devices\" }> => this.editDevice(id, ctx),\n };\n return {\n id: card.id,\n name: card.name,\n icon: iconForModel(model),\n model: { stateId: `${base}.info.model` },\n identifier: card.ip,\n status: {\n connection: { stateId: `${base}.info.connection`, mapping: { true: \"connected\", false: \"disconnected\" } },\n },\n // No icon: the indicator icon accepts only a reserved/`fa-*`/`data:`/URL name, so a plain\n // \"wifi\" rendered as a \"?\". The transport label as text plus a green \"on\" colour carries it;\n // `hideIfEmpty` shows only the protocols this device is actually connected over.\n indicators: TRANSPORTS.map(tr => ({\n id: `transport-${tr.id}`,\n value: { stateId: `${base}.info.transports.${tr.id}` },\n text: tr.label,\n colorOn: \"ok\",\n hideIfEmpty: true,\n })),\n actions: card.source === \"manual\" ? [edit, del] : [del],\n };\n }\n\n /**\n * The \"+ add\" action above the list and the label of the identifier line (the IP).\n *\n * @returns the instance action descriptor\n */\n protected getInstanceInfo(): InstanceDetails {\n return {\n apiVersion: \"v3\",\n identifierLabel: t(\"ipLabel\"),\n actions: [{ id: \"add\", icon: \"add\", description: t(\"dmAdd\"), handler: async ctx => this.addDevice(ctx) }],\n };\n }\n\n /**\n * Manual add: show the name+IP form, then append the device to `native.devices` (which\n * restarts the adapter and switches it to manual mode).\n *\n * @param context the action context\n * @returns a directive to reload the manager\n */\n private async addDevice(context: ActionContext): Promise<{ refresh: boolean }> {\n const manual = await this.readManual();\n const data = await context.showForm(buildDeviceForm(manual.map(r => r.ip)), { title: t(\"dmAdd\") });\n if (data && typeof data.ip === \"string\" && data.ip.trim()) {\n const row: ManualRow = { name: typeof data.name === \"string\" ? data.name.trim() : \"\", ip: data.ip.trim() };\n const clash = findClash(manual, row, -1);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: true };\n }\n manual.push(row);\n await this.writeManual(manual);\n }\n return { refresh: true };\n }\n\n /**\n * Edit a manual device via the pre-filled form (edit is offered on manual cards only).\n *\n * @param cardId the card id (= the object-tree device id)\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async editDevice(cardId: string, context: ActionContext): Promise<{ refresh: \"devices\" }> {\n const manual = await this.readManual();\n const index = manual.findIndex(r => rowId(r) === cardId);\n if (index < 0) {\n return { refresh: \"devices\" };\n }\n const current = manual[index];\n const usedIps = manual.filter((_, i) => i !== index).map(r => r.ip);\n const data = await context.showForm(buildDeviceForm(usedIps), {\n title: t(\"dmEditTitle\"),\n data: { name: current.name ?? \"\", ip: current.ip },\n });\n if (data && typeof data.ip === \"string\" && data.ip.trim()) {\n const row: ManualRow = { name: typeof data.name === \"string\" ? data.name.trim() : \"\", ip: data.ip.trim() };\n const clash = findClash(manual, row, index);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: \"devices\" };\n }\n manual[index] = row;\n await this.writeManual(manual);\n }\n return { refresh: \"devices\" };\n }\n\n /**\n * Delete a device after confirmation, from whichever source it came from: a manual card\n * from `native.devices`, a discovered card from the discovery store \u2014 the latter so the\n * standby-protection merge does not resurrect it on the next start.\n *\n * @param cardId the card id (= the object-tree device id)\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async deleteDevice(cardId: string, context: ActionContext): Promise<{ refresh: \"devices\" }> {\n const manual = await this.readManual();\n if (manual.length > 0) {\n const index = manual.findIndex(r => rowId(r) === cardId);\n if (index < 0) {\n return { refresh: \"devices\" };\n }\n const confirmed = await context.showConfirmation(t(\"dmDeleteConfirm\", manual[index].name || manual[index].ip));\n if (confirmed) {\n manual.splice(index, 1);\n await this.writeManual(manual);\n }\n return { refresh: \"devices\" };\n }\n const store = discoveredStoreDeps(this.adapter);\n const discovered = await readDiscovered(store);\n const remaining = discovered.filter((d: DeviceRecord) => d.id !== cardId);\n if (remaining.length !== discovered.length) {\n const confirmed = await context.showConfirmation(t(\"dmDeleteConfirm\", cardId));\n if (confirmed) {\n await writeDiscovered(store, remaining);\n }\n }\n return { refresh: \"devices\" };\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAMO;AACP,kBAAkB;AAClB,yBAA6B;AAC7B,8BAAgD;AAChD,mCAAoC;AAEpC,uCAOO;AASA,MAAM,+BAA+B,iCAAiB;AAAA;AAAA,EAE3D,IAAY,QAAgB;AAC1B,WAAO,kBAAkB,KAAK,QAAQ,SAAS;AAAA,EACjD;AAAA;AAAA,EAGA,MAAc,aAAmC;AAnCnD;AAoCI,UAAM,MAAM,MAAM,KAAK,QAAQ,sBAAsB,KAAK,KAAK;AAC/D,UAAM,WAAW,gCAAK,WAAL,mBAAmD;AACpE,QAAI,CAAC,MAAM,QAAQ,OAAO,GAAG;AAC3B,aAAO,CAAC;AAAA,IACV;AACA,WAAO,QAAQ;AAAA,MACb,CAAC,MAAsB,CAAC,CAAC,KAAK,OAAQ,EAAgB,OAAO,YAAa,EAAgB,GAAG,SAAS;AAAA,IACxG;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,YAAY,MAAkC;AAC1D,UAAM,KAAK,QAAQ,yBAAyB,KAAK,OAAO,EAAE,QAAQ,EAAE,SAAS,KAAK,EAAE,CAAC;AAAA,EACvF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,QAA+B;AAC3C,UAAM,SAAS,MAAM,KAAK,WAAW;AACrC,QAAI,OAAO,SAAS,GAAG;AACrB,YAAM,QAAQ,oBAAI,IAAY,CAAC,MAAM,CAAC;AACtC,YAAM,QAAsB,CAAC;AAC7B,iBAAW,OAAO,QAAQ;AACxB,cAAM,SAAK,wCAAM,GAAG;AACpB,YAAI,MAAM,IAAI,EAAE,GAAG;AACjB;AAAA,QACF;AACA,cAAM,IAAI,EAAE;AACZ,cAAM,KAAK,EAAE,IAAI,IAAI,IAAI,IAAI,MAAM,IAAI,QAAQ,IAAI,KAAK,SAAS,IAAI,IAAI,OAAO,IAAI,IAAI,QAAQ,SAAS,CAAC;AAAA,MAC5G;AACA,aAAO;AAAA,IACT;AACA,UAAM,aAAa,UAAM,4CAAe,kDAAoB,KAAK,OAAO,CAAC;AACzE,WAAO,WAAW,IAAI,QAAM,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,IAAI,MAAM,EAAE,IAAI,QAAQ,aAAsB,EAAE;AAAA,EAChG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAgB,YAAY,SAAmD;
|
|
4
|
+
"sourcesContent": ["import {\n DeviceManagement,\n type ActionContext,\n type DeviceInfo,\n type DeviceLoadContext,\n type InstanceDetails,\n} from \"@iobroker/dm-utils\";\nimport { t } from \"./lib/i18n\";\nimport { iconForModel } from \"./lib/device-type\";\nimport { readDiscovered, writeDiscovered } from \"./lib/discovered-store\";\nimport { discoveredStoreDeps } from \"./lib/discovered-store-deps\";\nimport type { DeviceRecord } from \"./lib/types\";\nimport {\n TRANSPORTS,\n buildDeviceForm,\n findClash,\n rowId,\n type CardDevice,\n type ManualRow,\n} from \"./device-management-helpers\";\n\n/**\n * ioBroker device-manager backend: the Yamaha receivers as cards showing the live model,\n * the IP, and which protocols (YNCA/MusicCast/XML) are connected right now, with a manual\n * add-by-IP dialog. The card list follows the running set \u2014 the manual `native.devices`\n * table when it is filled, otherwise the auto-discovered devices \u2014 so it matches exactly\n * what the adapter runs. \"Yamaha\" is never a card line: it is the whole adapter.\n */\nexport class YamahaDeviceManagement extends DeviceManagement {\n /** The instance object id whose `native` holds the manual device table. */\n private get objId(): string {\n return `system.adapter.${this.adapter.namespace}`;\n }\n\n /** Read the manual device table (`native.devices`) as raw rows, keeping the name. */\n private async readManual(): Promise<ManualRow[]> {\n const obj = await this.adapter.getForeignObjectAsync(this.objId);\n const devices = (obj?.native as { devices?: unknown } | undefined)?.devices;\n if (!Array.isArray(devices)) {\n return [];\n }\n return devices.filter(\n (d): d is ManualRow => !!d && typeof (d as ManualRow).ip === \"string\" && (d as ManualRow).ip.length > 0,\n );\n }\n\n /**\n * Persist the manual device table; writing `native.*` restarts the adapter with the new set.\n *\n * @param rows the manual rows to store\n */\n private async writeManual(rows: ManualRow[]): Promise<void> {\n await this.adapter.extendForeignObjectAsync(this.objId, { native: { devices: rows } });\n }\n\n /**\n * The running device set as cards: the manual table when filled (manual mode), otherwise\n * the auto-discovered devices (auto mode) \u2014 the same either/or the adapter itself runs.\n *\n * @returns the cards with their source\n */\n private async cards(): Promise<CardDevice[]> {\n const manual = await this.readManual();\n if (manual.length > 0) {\n const taken = new Set<string>([\"info\"]);\n const cards: CardDevice[] = [];\n for (const row of manual) {\n const id = rowId(row);\n if (taken.has(id)) {\n continue;\n }\n taken.add(id);\n cards.push({ id, ip: row.ip, name: row.name && row.name.length > 0 ? row.name : row.ip, source: \"manual\" });\n }\n return cards;\n }\n const discovered = await readDiscovered(discoveredStoreDeps(this.adapter));\n return discovered.map(d => ({ id: d.id, ip: d.ip, name: d.id, source: \"discovered\" as const }));\n }\n\n /**\n * Populate the manager with one card per running device.\n *\n * @param context the load context\n */\n protected async loadDevices(context: DeviceLoadContext<string>): Promise<void> {\n for (const card of await this.cards()) {\n // The reported model picks the device-class silhouette shown on the card.\n const model = await this.adapter.getForeignStateAsync(`${this.adapter.namespace}.${card.id}.info.model`);\n // The card title follows the device object's name, not the table entry. On an\n // instance upgraded from the previous adapter the table entry is the receiver's\n // ip \u2014 the object carries the readable name the adapter learned from the device.\n // The table entry itself must stay put: the object id is derived from it, and\n // changing that would move the whole tree.\n const node = await this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${card.id}`);\n const label = typeof node?.common?.name === \"string\" ? node.common.name : undefined;\n context.addDevice(\n this.toDeviceInfo(\n label && label !== card.id ? { ...card, name: label } : card,\n typeof model?.val === \"string\" ? model.val : undefined,\n ),\n );\n }\n }\n\n /**\n * Build one device card: the live model, the IP as the identifier line, a connection\n * status, and one indicator per connected protocol (hidden while that protocol is not\n * connected). All live values read from the device's own `info.*` states. A discovered\n * card has no edit action \u2014 it is auto-found and re-found; it can only be removed.\n *\n * @param card the running device\n * @param model the device's reported model name, for the device-class icon\n * @returns the card descriptor\n */\n private toDeviceInfo(card: CardDevice, model?: string): DeviceInfo<string> {\n const base = `${this.adapter.namespace}.${card.id}`;\n const del = {\n id: \"delete\",\n icon: \"delete\",\n description: t(\"dmDelete\"),\n handler: async (id: string, ctx: ActionContext): Promise<{ refresh: \"devices\" }> => this.deleteDevice(id, ctx),\n };\n const edit = {\n id: \"edit\",\n icon: \"edit\",\n description: t(\"dmEdit\"),\n handler: async (id: string, ctx: ActionContext): Promise<{ refresh: \"devices\" }> => this.editDevice(id, ctx),\n };\n return {\n id: card.id,\n name: card.name,\n icon: iconForModel(model),\n model: { stateId: `${base}.info.model` },\n identifier: card.ip,\n status: {\n connection: { stateId: `${base}.info.connection`, mapping: { true: \"connected\", false: \"disconnected\" } },\n },\n // No icon: the indicator icon accepts only a reserved/`fa-*`/`data:`/URL name, so a plain\n // \"wifi\" rendered as a \"?\". The transport label as text plus a green \"on\" colour carries it;\n // `hideIfEmpty` shows only the protocols this device is actually connected over.\n indicators: TRANSPORTS.map(tr => ({\n id: `transport-${tr.id}`,\n value: { stateId: `${base}.info.transports.${tr.id}` },\n text: tr.label,\n colorOn: \"ok\",\n hideIfEmpty: true,\n })),\n actions: card.source === \"manual\" ? [edit, del] : [del],\n };\n }\n\n /**\n * The \"+ add\" action above the list and the label of the identifier line (the IP).\n *\n * @returns the instance action descriptor\n */\n protected getInstanceInfo(): InstanceDetails {\n return {\n apiVersion: \"v3\",\n identifierLabel: t(\"ipLabel\"),\n actions: [{ id: \"add\", icon: \"add\", description: t(\"dmAdd\"), handler: async ctx => this.addDevice(ctx) }],\n };\n }\n\n /**\n * Manual add: show the name+IP form, then append the device to `native.devices` (which\n * restarts the adapter and switches it to manual mode).\n *\n * @param context the action context\n * @returns a directive to reload the manager\n */\n private async addDevice(context: ActionContext): Promise<{ refresh: boolean }> {\n const manual = await this.readManual();\n const data = await context.showForm(buildDeviceForm(manual.map(r => r.ip)), { title: t(\"dmAdd\") });\n if (data && typeof data.ip === \"string\" && data.ip.trim()) {\n const row: ManualRow = { name: typeof data.name === \"string\" ? data.name.trim() : \"\", ip: data.ip.trim() };\n const clash = findClash(manual, row, -1);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: true };\n }\n manual.push(row);\n await this.writeManual(manual);\n }\n return { refresh: true };\n }\n\n /**\n * Edit a manual device via the pre-filled form (edit is offered on manual cards only).\n *\n * @param cardId the card id (= the object-tree device id)\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async editDevice(cardId: string, context: ActionContext): Promise<{ refresh: \"devices\" }> {\n const manual = await this.readManual();\n const index = manual.findIndex(r => rowId(r) === cardId);\n if (index < 0) {\n return { refresh: \"devices\" };\n }\n const current = manual[index];\n const usedIps = manual.filter((_, i) => i !== index).map(r => r.ip);\n const data = await context.showForm(buildDeviceForm(usedIps), {\n title: t(\"dmEditTitle\"),\n data: { name: current.name ?? \"\", ip: current.ip },\n });\n if (data && typeof data.ip === \"string\" && data.ip.trim()) {\n const row: ManualRow = { name: typeof data.name === \"string\" ? data.name.trim() : \"\", ip: data.ip.trim() };\n const clash = findClash(manual, row, index);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: \"devices\" };\n }\n manual[index] = row;\n await this.writeManual(manual);\n }\n return { refresh: \"devices\" };\n }\n\n /**\n * Delete a device after confirmation, from whichever source it came from: a manual card\n * from `native.devices`, a discovered card from the discovery store \u2014 the latter so the\n * standby-protection merge does not resurrect it on the next start.\n *\n * @param cardId the card id (= the object-tree device id)\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async deleteDevice(cardId: string, context: ActionContext): Promise<{ refresh: \"devices\" }> {\n const manual = await this.readManual();\n if (manual.length > 0) {\n const index = manual.findIndex(r => rowId(r) === cardId);\n if (index < 0) {\n return { refresh: \"devices\" };\n }\n const confirmed = await context.showConfirmation(t(\"dmDeleteConfirm\", manual[index].name || manual[index].ip));\n if (confirmed) {\n manual.splice(index, 1);\n await this.writeManual(manual);\n }\n return { refresh: \"devices\" };\n }\n const store = discoveredStoreDeps(this.adapter);\n const discovered = await readDiscovered(store);\n const remaining = discovered.filter((d: DeviceRecord) => d.id !== cardId);\n if (remaining.length !== discovered.length) {\n const confirmed = await context.showConfirmation(t(\"dmDeleteConfirm\", cardId));\n if (confirmed) {\n await writeDiscovered(store, remaining);\n }\n }\n return { refresh: \"devices\" };\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/attempt-device.ts"],
|
|
4
|
-
"sourcesContent": ["import { YncaClient } from \"./ynca/ynca-client\";\nimport { YncaDeviceController } from \"./device-controller\";\nimport { YxcDeviceController } from \"./yxc/device-controller\";\nimport { YamahaYxcClient } from \"./yxc/http-client\";\nimport { XmlDeviceController } from \"./xml/device-controller\";\nimport { XmlClient } from \"./xml/xml-client\";\nimport { MultiTransportHandle, type ConnectableTransport } from \"./lifecycle/multi-transport-handle\";\nimport { TransportConnectionAdapter } from \"./lifecycle/transport-connection-adapter\";\nimport { ReconnectStrategy } from \"./lifecycle/reconnect-strategy\";\nimport type { ReachabilityDedup } from \"./lifecycle/reachability-dedup\";\nimport type { YncaSubunitCache } from \"./ynca/subunit-cache\";\nimport type { Transport } from \"./catalog/owner-policy\";\nimport { readyLine } from \"./ready-line\";\nimport { errorMessage } from \"./util\";\nimport type { ConnectionHandle, ControllerLog } from \"./controller\";\nimport type { ObjectDef } from \"./catalog/types\";\nimport type { DeviceRecord } from \"./types\";\n\n// Re-exported so existing importers (tests) keep resolving it from here.\nexport type { ConnectableTransport };\n\n/** Per-transport reconnect backoff bounds \u2014 same shape as the device supervisor's. */\nconst TRANSPORT_RECONNECT_BASE_MS = 1000;\nconst TRANSPORT_RECONNECT_MAX_MS = 60000;\n\n/** The adapter-bound callbacks {@link attemptDevice} drives \u2014 injected so it needs no adapter. */\nexport interface AttemptDeps {\n /** Adapter log. */\n log: ControllerLog;\n /** Create or update an object in the device tree. */\n upsertObject(id: string, def: ObjectDef): Promise<void>;\n /** Write a state value with ack (device-originated). */\n setStateAck(id: string, value: boolean | number | string): void;\n /** Adapter-managed timers (YNCA pacing + per-transport reconnects). */\n timers: {\n /** Schedule a one-shot timer. */\n schedule(handler: () => void, ms: number): ioBroker.Timeout | undefined;\n /** Cancel a scheduled timer. */\n cancel(handle: ioBroker.Timeout | undefined): void;\n };\n /** Register a YXC push handler for a device IP; returns a function that unregisters it. */\n registerPush(ip: string, onPush: (event: unknown) => void): () => void;\n /** Schedule a repeating keepalive; returns a function that cancels it. */\n scheduleKeepalive(handler: () => void, ms: number): () => void;\n /** How often to poll an XML/YNC device for state (ms). */\n xmlPollIntervalMs: number;\n /** Report the transports that are live after every change \u2014 the id-safe names (\"ynca\"/\"yxc\"/\"xml\"). */\n onTransports?(names: string[]): void;\n /** IPs of all configured devices, so a MusicCast group can resolve a client device by IP. */\n knownDeviceIps: Set<string>;\n /** Dedup for the \"no reachable transport\" warning \u2014 see {@link ConnectDeps.reachability}. */\n reachability?: ReachabilityDedup;\n /** Datapoint-group gate for the YNCA sweep \u2014 a disabled group's functions are never fetched. */\n isEntryEnabled?(id: string): boolean;\n /** Per-device cache of the YNCA AVAIL probe, held by the caller across reconnects. */\n yncaSubunitCache?: YncaSubunitCache;\n}\n\n/** One transport to try: its name and a factory building a FRESH connectable (also for reconnects). */\nexport interface TransportAttempt {\n /** The transport this attempt stands for. */\n transport: Transport;\n /** Build a fresh connectable transport (a controller behind its adapter). */\n build(): ConnectableTransport;\n}\n\n/** The adapter callbacks {@link connectTransports} drives to build and hold the unified tree. */\nexport interface ConnectDeps {\n /** Adapter log. */\n log: ControllerLog;\n /** Create or update an object in the device tree. */\n upsertObject(id: string, def: ObjectDef): Promise<void>;\n /** Report the transports that are live after every change \u2014 the id-safe names (\"ynca\"/\"yxc\"/\"xml\"). */\n onTransports?(names: string[]): void;\n /**\n * Dedup for the \"no reachable transport\" warning: without it every retry warns\n * again for as long as the device stays offline (nut2 `failedUps` pattern \u2014 first\n * failure warns, repeats stay at debug until the device answers again).\n */\n reachability?: ReachabilityDedup;\n /** Timers for the per-transport reconnect loops (absent in tests \u2192 no per-transport retry). */\n timers?: {\n /** Schedule a one-shot timer. */\n schedule(handler: () => void, ms: number): ioBroker.Timeout | undefined;\n /** Cancel a scheduled timer. */\n cancel(handle: ioBroker.Timeout | undefined): void;\n };\n}\n\n/**\n * Bring every answering transport online on ONE object tree. All candidates connect IN\n * PARALLEL (a YNCA connect timeout or long sweep no longer delays YXC/XML); the ones that\n * answer are handed to a single {@link MultiTransportHandle}, which unifies their catalogs\n * (each capability owned by exactly one transport \u2014 most-modern-but-lossless, see the\n * object-tree coordinator), routes user writes to the owner, and reconnects a single\n * dropped transport on its own while the others keep running. Ownership is resolved by\n * rank over the connected set, so the connect order/timing never changes it. A transport\n * that does not answer, or whose connect throws, is closed and left out. Returns the\n * handle over the live set, or null when no transport answered (the device is offline\n * this attempt).\n *\n * @param deviceId the id-safe device id\n * @param attempts the transports to try\n * @param deps the adapter callbacks (upsert + log + timers)\n * @returns a connection handle over the live transports, or null when none connected\n */\nexport async function connectTransports(\n deviceId: string,\n attempts: readonly TransportAttempt[],\n deps: ConnectDeps,\n): Promise<ConnectionHandle | null> {\n const results = await Promise.all(\n attempts.map(async attempt => {\n const conn = attempt.build();\n try {\n if (await conn.connect()) {\n return conn;\n }\n } catch (e) {\n deps.log.debug(`${deviceId}/${conn.transport}: transport did not connect (${errorMessage(e)})`);\n }\n conn.close();\n return null;\n }),\n );\n const live = results.filter((conn): conn is ConnectableTransport => conn !== null);\n if (live.length === 0) {\n const level = deps.reachability?.reportUnreachable() ?? \"warn\";\n deps.log[level](`${deviceId}: no reachable transport (YNCA/YXC/XML)`);\n return null;\n }\n deps.reachability?.reportReachable();\n const rebuilds = new Map(attempts.map(attempt => [attempt.transport, attempt.build] as const));\n const handle = new MultiTransportHandle(deviceId, live, {\n upsertObject: deps.upsertObject,\n log: deps.log,\n onTransports: deps.onTransports,\n rebuild: deps.timers ? transport => rebuilds.get(transport)!() : undefined,\n schedule: deps.timers ? (cb, ms) => deps.timers!.schedule(cb, ms) : undefined,\n cancel: deps.timers ? handle_ => deps.timers!.cancel(handle_ as ioBroker.Timeout | undefined) : undefined,\n backoffFactory: () => new ReconnectStrategy(TRANSPORT_RECONNECT_BASE_MS, TRANSPORT_RECONNECT_MAX_MS),\n });\n try {\n await handle.start();\n } catch (e) {\n // Building the unified tree failed (e.g. object creation errored): close every live\n // transport before rethrowing, or the supervisor's retry would leak sockets and timers \u2014\n // fatal for YNCA, where the receiver allows only ONE connection and a zombie socket\n // would block every future attempt until the adapter restarts.\n handle.close();\n throw e;\n }\n // One summary line instead of three per-transport \"ready\" lines; each controller logs its\n // own readiness at debug level for diagnostics.\n deps.log.info(\n readyLine(\n deviceId,\n live.map(conn => conn.transport),\n ),\n );\n return handle;\n}\n\n/**\n * Bring one device online across ALL its transports. Every transport that answers \u2014 YNCA (amp\n * control over a held TCP connection), YXC (MusicCast, push + poll), XML/YNC (pre-2010) \u2014 is built\n * behind a {@link TransportConnectionAdapter} and connected in parallel on one object tree, so a\n * MusicCast AVR gets YNCA base control AND the YXC-exclusive richness (multiroom, equalizer, album\n * art) instead of one transport winning and hiding the rest. Each transport is described by a\n * factory, so the handle can rebuild and reconnect a single dropped transport while the others\n * keep running. Returns a {@link ConnectionHandle} over the live set, or null when no transport\n * answers this attempt.\n *\n * @param device the configured device record\n * @param deps the adapter-bound callbacks\n * @returns a connection handle, or null when no transport connected\n */\nexport function attemptDevice(device: DeviceRecord, deps: AttemptDeps): Promise<ConnectionHandle | null> {\n const { log, upsertObject, setStateAck, timers } = deps;\n\n // 1) YNCA \u2014 amp control over a held TCP connection; a socket drop is the genuine gone-signal.\n const buildYnca = (): ConnectableTransport => {\n const ynca = new TransportConnectionAdapter(\"ynca\", device.id, setStateAck);\n ynca.bind(\n new YncaDeviceController(device.id, {\n client: new YncaClient(device.ip, timers),\n upsertObject: ynca.interceptUpsert,\n setStateAck: ynca.interceptSetStateAck,\n log,\n isEntryEnabled: deps.isEntryEnabled,\n subunitCache: deps.yncaSubunitCache,\n }),\n );\n return ynca;\n };\n\n // 2) YXC \u2014 MusicCast; polled + push. Drop reported after a run of failed keepalive polls.\n const buildYxc = (): ConnectableTransport => {\n const yxc = new TransportConnectionAdapter(\"yxc\", device.id, setStateAck);\n yxc.bind(\n new YxcDeviceController(device.id, {\n client: new YamahaYxcClient(device.ip),\n // Resolve another configured device's client for a multiroom link \u2014 never this device itself.\n clientFor: ip => (ip !== device.ip && deps.knownDeviceIps.has(ip) ? new YamahaYxcClient(ip) : undefined),\n registerPush: onPush => deps.registerPush(device.ip, onPush),\n scheduleKeepalive: deps.scheduleKeepalive,\n upsertObject: yxc.interceptUpsert,\n setStateAck: yxc.interceptSetStateAck,\n log,\n }),\n );\n return yxc;\n };\n\n // 3) XML/YNC \u2014 pre-2010 receivers; polled. A drop is reported after a run of failed polls.\n const buildXml = (): ConnectableTransport => {\n const xml = new TransportConnectionAdapter(\"xml\", device.id, setStateAck);\n xml.bind(\n new XmlDeviceController(\n device.id,\n {\n client: new XmlClient(device.ip),\n scheduleKeepalive: deps.scheduleKeepalive,\n upsertObject: xml.interceptUpsert,\n setStateAck: xml.interceptSetStateAck,\n log,\n },\n deps.xmlPollIntervalMs,\n ),\n );\n return xml;\n };\n\n return connectTransports(\n device.id,\n [\n { transport: \"ynca\", build: buildYnca },\n { transport: \"yxc\", build: buildYxc },\n { transport: \"xml\", build: buildXml },\n ],\n {\n upsertObject,\n log,\n onTransports: deps.onTransports,\n reachability: deps.reachability,\n timers: deps.timers,\n },\n );\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAA2B;AAC3B,+BAAqC;AACrC,IAAAA,4BAAoC;AACpC,yBAAgC;AAChC,IAAAA,4BAAoC;AACpC,wBAA0B;AAC1B,oCAAgE;AAChE,0CAA2C;AAC3C,gCAAkC;AAIlC,wBAA0B;AAC1B,kBAA6B;AAS7B,MAAM,8BAA8B;AACpC,MAAM,6BAA6B;
|
|
4
|
+
"sourcesContent": ["import { YncaClient } from \"./ynca/ynca-client\";\nimport { YncaDeviceController } from \"./device-controller\";\nimport { YxcDeviceController } from \"./yxc/device-controller\";\nimport { YamahaYxcClient } from \"./yxc/http-client\";\nimport { XmlDeviceController } from \"./xml/device-controller\";\nimport { XmlClient } from \"./xml/xml-client\";\nimport { MultiTransportHandle, type ConnectableTransport } from \"./lifecycle/multi-transport-handle\";\nimport { TransportConnectionAdapter } from \"./lifecycle/transport-connection-adapter\";\nimport { ReconnectStrategy } from \"./lifecycle/reconnect-strategy\";\nimport type { ReachabilityDedup } from \"./lifecycle/reachability-dedup\";\nimport type { YncaSubunitCache } from \"./ynca/subunit-cache\";\nimport type { Transport } from \"./catalog/owner-policy\";\nimport { readyLine } from \"./ready-line\";\nimport { errorMessage } from \"./util\";\nimport type { ConnectionHandle, ControllerLog } from \"./controller\";\nimport type { ObjectDef } from \"./catalog/types\";\nimport type { DeviceRecord } from \"./types\";\n\n// Re-exported so existing importers (tests) keep resolving it from here.\nexport type { ConnectableTransport };\n\n/** Per-transport reconnect backoff bounds \u2014 same shape as the device supervisor's. */\nconst TRANSPORT_RECONNECT_BASE_MS = 1000;\nconst TRANSPORT_RECONNECT_MAX_MS = 60000;\n\n/** The adapter-bound callbacks {@link attemptDevice} drives \u2014 injected so it needs no adapter. */\nexport interface AttemptDeps {\n /** Adapter log. */\n log: ControllerLog;\n /** Create or update an object in the device tree. */\n upsertObject(id: string, def: ObjectDef): Promise<void>;\n /** Write a state value with ack (device-originated). */\n setStateAck(id: string, value: boolean | number | string): void;\n /** Adapter-managed timers (YNCA pacing + per-transport reconnects). */\n timers: {\n /** Schedule a one-shot timer. */\n schedule(handler: () => void, ms: number): ioBroker.Timeout | undefined;\n /** Cancel a scheduled timer. */\n cancel(handle: ioBroker.Timeout | undefined): void;\n };\n /** Register a YXC push handler for a device IP; returns a function that unregisters it. */\n registerPush(ip: string, onPush: (event: unknown) => void): () => void;\n /** Schedule a repeating keepalive; returns a function that cancels it. */\n scheduleKeepalive(handler: () => void, ms: number): () => void;\n /** How often to poll an XML/YNC device for state (ms). */\n xmlPollIntervalMs: number;\n /** Report the transports that are live after every change \u2014 the id-safe names (\"ynca\"/\"yxc\"/\"xml\"). */\n onTransports?(names: string[]): void;\n /** Report the name the device carries for itself (MusicCast), for the device object's label. */\n onDeviceName?(name: string): void;\n /** IPs of all configured devices, so a MusicCast group can resolve a client device by IP. */\n knownDeviceIps: Set<string>;\n /** Dedup for the \"no reachable transport\" warning \u2014 see {@link ConnectDeps.reachability}. */\n reachability?: ReachabilityDedup;\n /** Datapoint-group gate for the YNCA sweep \u2014 a disabled group's functions are never fetched. */\n isEntryEnabled?(id: string): boolean;\n /** Per-device cache of the YNCA AVAIL probe, held by the caller across reconnects. */\n yncaSubunitCache?: YncaSubunitCache;\n}\n\n/** One transport to try: its name and a factory building a FRESH connectable (also for reconnects). */\nexport interface TransportAttempt {\n /** The transport this attempt stands for. */\n transport: Transport;\n /** Build a fresh connectable transport (a controller behind its adapter). */\n build(): ConnectableTransport;\n}\n\n/** The adapter callbacks {@link connectTransports} drives to build and hold the unified tree. */\nexport interface ConnectDeps {\n /** Adapter log. */\n log: ControllerLog;\n /** Create or update an object in the device tree. */\n upsertObject(id: string, def: ObjectDef): Promise<void>;\n /** Report the transports that are live after every change \u2014 the id-safe names (\"ynca\"/\"yxc\"/\"xml\"). */\n onTransports?(names: string[]): void;\n /**\n * Dedup for the \"no reachable transport\" warning: without it every retry warns\n * again for as long as the device stays offline (nut2 `failedUps` pattern \u2014 first\n * failure warns, repeats stay at debug until the device answers again).\n */\n reachability?: ReachabilityDedup;\n /** Timers for the per-transport reconnect loops (absent in tests \u2192 no per-transport retry). */\n timers?: {\n /** Schedule a one-shot timer. */\n schedule(handler: () => void, ms: number): ioBroker.Timeout | undefined;\n /** Cancel a scheduled timer. */\n cancel(handle: ioBroker.Timeout | undefined): void;\n };\n}\n\n/**\n * Bring every answering transport online on ONE object tree. All candidates connect IN\n * PARALLEL (a YNCA connect timeout or long sweep no longer delays YXC/XML); the ones that\n * answer are handed to a single {@link MultiTransportHandle}, which unifies their catalogs\n * (each capability owned by exactly one transport \u2014 most-modern-but-lossless, see the\n * object-tree coordinator), routes user writes to the owner, and reconnects a single\n * dropped transport on its own while the others keep running. Ownership is resolved by\n * rank over the connected set, so the connect order/timing never changes it. A transport\n * that does not answer, or whose connect throws, is closed and left out. Returns the\n * handle over the live set, or null when no transport answered (the device is offline\n * this attempt).\n *\n * @param deviceId the id-safe device id\n * @param attempts the transports to try\n * @param deps the adapter callbacks (upsert + log + timers)\n * @returns a connection handle over the live transports, or null when none connected\n */\nexport async function connectTransports(\n deviceId: string,\n attempts: readonly TransportAttempt[],\n deps: ConnectDeps,\n): Promise<ConnectionHandle | null> {\n const results = await Promise.all(\n attempts.map(async attempt => {\n const conn = attempt.build();\n try {\n if (await conn.connect()) {\n return conn;\n }\n } catch (e) {\n deps.log.debug(`${deviceId}/${conn.transport}: transport did not connect (${errorMessage(e)})`);\n }\n conn.close();\n return null;\n }),\n );\n const live = results.filter((conn): conn is ConnectableTransport => conn !== null);\n if (live.length === 0) {\n const level = deps.reachability?.reportUnreachable() ?? \"warn\";\n deps.log[level](`${deviceId}: no reachable transport (YNCA/YXC/XML)`);\n return null;\n }\n deps.reachability?.reportReachable();\n const rebuilds = new Map(attempts.map(attempt => [attempt.transport, attempt.build] as const));\n const handle = new MultiTransportHandle(deviceId, live, {\n upsertObject: deps.upsertObject,\n log: deps.log,\n onTransports: deps.onTransports,\n rebuild: deps.timers ? transport => rebuilds.get(transport)!() : undefined,\n schedule: deps.timers ? (cb, ms) => deps.timers!.schedule(cb, ms) : undefined,\n cancel: deps.timers ? handle_ => deps.timers!.cancel(handle_ as ioBroker.Timeout | undefined) : undefined,\n backoffFactory: () => new ReconnectStrategy(TRANSPORT_RECONNECT_BASE_MS, TRANSPORT_RECONNECT_MAX_MS),\n });\n try {\n await handle.start();\n } catch (e) {\n // Building the unified tree failed (e.g. object creation errored): close every live\n // transport before rethrowing, or the supervisor's retry would leak sockets and timers \u2014\n // fatal for YNCA, where the receiver allows only ONE connection and a zombie socket\n // would block every future attempt until the adapter restarts.\n handle.close();\n throw e;\n }\n // One summary line instead of three per-transport \"ready\" lines; each controller logs its\n // own readiness at debug level for diagnostics.\n deps.log.info(\n readyLine(\n deviceId,\n live.map(conn => conn.transport),\n ),\n );\n return handle;\n}\n\n/**\n * Bring one device online across ALL its transports. Every transport that answers \u2014 YNCA (amp\n * control over a held TCP connection), YXC (MusicCast, push + poll), XML/YNC (pre-2010) \u2014 is built\n * behind a {@link TransportConnectionAdapter} and connected in parallel on one object tree, so a\n * MusicCast AVR gets YNCA base control AND the YXC-exclusive richness (multiroom, equalizer, album\n * art) instead of one transport winning and hiding the rest. Each transport is described by a\n * factory, so the handle can rebuild and reconnect a single dropped transport while the others\n * keep running. Returns a {@link ConnectionHandle} over the live set, or null when no transport\n * answers this attempt.\n *\n * @param device the configured device record\n * @param deps the adapter-bound callbacks\n * @returns a connection handle, or null when no transport connected\n */\nexport function attemptDevice(device: DeviceRecord, deps: AttemptDeps): Promise<ConnectionHandle | null> {\n const { log, upsertObject, setStateAck, timers } = deps;\n\n // 1) YNCA \u2014 amp control over a held TCP connection; a socket drop is the genuine gone-signal.\n const buildYnca = (): ConnectableTransport => {\n const ynca = new TransportConnectionAdapter(\"ynca\", device.id, setStateAck);\n ynca.bind(\n new YncaDeviceController(device.id, {\n client: new YncaClient(device.ip, timers),\n upsertObject: ynca.interceptUpsert,\n setStateAck: ynca.interceptSetStateAck,\n log,\n isEntryEnabled: deps.isEntryEnabled,\n subunitCache: deps.yncaSubunitCache,\n }),\n );\n return ynca;\n };\n\n // 2) YXC \u2014 MusicCast; polled + push. Drop reported after a run of failed keepalive polls.\n const buildYxc = (): ConnectableTransport => {\n const yxc = new TransportConnectionAdapter(\"yxc\", device.id, setStateAck);\n yxc.bind(\n new YxcDeviceController(device.id, {\n client: new YamahaYxcClient(device.ip),\n // Resolve another configured device's client for a multiroom link \u2014 never this device itself.\n clientFor: ip => (ip !== device.ip && deps.knownDeviceIps.has(ip) ? new YamahaYxcClient(ip) : undefined),\n registerPush: onPush => deps.registerPush(device.ip, onPush),\n scheduleKeepalive: deps.scheduleKeepalive,\n upsertObject: yxc.interceptUpsert,\n setStateAck: yxc.interceptSetStateAck,\n reportDeviceName: deps.onDeviceName,\n log,\n }),\n );\n return yxc;\n };\n\n // 3) XML/YNC \u2014 pre-2010 receivers; polled. A drop is reported after a run of failed polls.\n const buildXml = (): ConnectableTransport => {\n const xml = new TransportConnectionAdapter(\"xml\", device.id, setStateAck);\n xml.bind(\n new XmlDeviceController(\n device.id,\n {\n client: new XmlClient(device.ip),\n scheduleKeepalive: deps.scheduleKeepalive,\n upsertObject: xml.interceptUpsert,\n setStateAck: xml.interceptSetStateAck,\n log,\n },\n deps.xmlPollIntervalMs,\n ),\n );\n return xml;\n };\n\n return connectTransports(\n device.id,\n [\n { transport: \"ynca\", build: buildYnca },\n { transport: \"yxc\", build: buildYxc },\n { transport: \"xml\", build: buildXml },\n ],\n {\n upsertObject,\n log,\n onTransports: deps.onTransports,\n reachability: deps.reachability,\n timers: deps.timers,\n },\n );\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["import_device_controller"]
|
|
7
7
|
}
|
|
@@ -30,7 +30,8 @@ const SWITCHABLE_GROUPS = [
|
|
|
30
30
|
"hdmi",
|
|
31
31
|
"scene",
|
|
32
32
|
"sound",
|
|
33
|
-
"advanced"
|
|
33
|
+
"advanced",
|
|
34
|
+
"clock"
|
|
34
35
|
];
|
|
35
36
|
function groupOf(stateId) {
|
|
36
37
|
const seg = stateId.includes(".") ? stateId.slice(0, stateId.indexOf(".")) : stateId;
|
|
@@ -55,6 +56,9 @@ function groupOf(stateId) {
|
|
|
55
56
|
if (seg === "scene") {
|
|
56
57
|
return "scene";
|
|
57
58
|
}
|
|
59
|
+
if (seg === "clock") {
|
|
60
|
+
return "clock";
|
|
61
|
+
}
|
|
58
62
|
return "amp";
|
|
59
63
|
}
|
|
60
64
|
function isGroupEnabled(stateId, config) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/catalog/groups.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Datapoint groups: the thematic buckets that are both the object-tree structure and the\n * admin on/off switches. Every state id maps to exactly one group by its (zone-stripped) first\n * path segment (its channel). A disabled group's objects are never created and any existing ones\n * are pruned, the same way beszel gates its metric categories. `amp` is the always-on core \u2014 no\n * switch exists for it, exactly like beszel's unconditional `info.online`/`info.status`.\n *\n * Every group is a real channel prefix in the tree (`player.*`, `tuner.*`, `hdmi.*`,\n * `multiroom.*`, `scene.*`, `sound.*`, `advanced.*`) \u2014 the toggle and the folder a datapoint\n * visually sits in are the same thing, never decoupled. Zone 2/3/4, Zone B, masterPower, party\n * and distribution all live under `multiroom.*`. Legacy flat pre-v0.15.0 ids need no handling\n * here: the start-up cleanup deletes them via `RENAMED_CHANNELS` before anything queries them.\n */\n\n/** The switchable groups. `amp` is the amplifier core and can never be turned off. */\nexport type GroupId = \"amp\" | \"player\" | \"tuner\" | \"multiroom\" | \"hdmi\" | \"scene\" | \"sound\" | \"advanced\";\n\n/** The groups a user can switch off, in display order (amp is always on and not listed here). */\nexport const SWITCHABLE_GROUPS: readonly GroupId[] = [\n \"player\",\n \"tuner\",\n \"multiroom\",\n \"hdmi\",\n \"scene\",\n \"sound\",\n \"advanced\",\n];\n\n/**\n * The datapoint group a state id belongs to, decided by its first path segment. Every group is a\n * real folder prefix in the tree, so the first segment of the id determines the group directly:\n * `multiroom.*` (including zones, masterPower, party), `player.*`, `tuner.*`, `hdmi.*`, `sound.*`,\n * `advanced.*`, `scene.*`. Anything not matched \u2014 the amplifier core (power, volume, input,\n * sleep \u2026), `info.*` \u2014 is the always-on `amp` group.\n *\n * @param stateId the device-relative state id (e.g. \"multiroom.zone2.power\", \"player.spotify.playback\")\n * @returns the group the state belongs to\n */\nexport function groupOf(stateId: string): GroupId {\n const seg = stateId.includes(\".\") ? stateId.slice(0, stateId.indexOf(\".\")) : stateId;\n\n if (seg === \"multiroom\") {\n return \"multiroom\";\n }\n if (seg === \"hdmi\" || seg === \"lipSync\") {\n return \"hdmi\";\n }\n if (seg === \"player\") {\n return \"player\";\n }\n if (seg === \"tuner\") {\n return \"tuner\";\n }\n if (seg === \"sound\") {\n return \"sound\";\n }\n if (seg === \"advanced\") {\n return \"advanced\";\n }\n if (seg === \"scene\") {\n return \"scene\";\n }\n return \"amp\";\n}\n\n/**\n * Whether a state's group is switched on. The amplifier core is always on; every other group is\n * on unless its `group_<id>` flag is explicitly false \u2014 default-on, so a fresh install and every\n * existing install keep all groups until the user turns one off.\n *\n * @param stateId the device-relative state id\n * @param config the adapter native config (carries `group_player`, `group_tuner`, \u2026 booleans)\n * @returns true if the state's group is enabled\n */\nexport function isGroupEnabled(stateId: string, config: Record<string, unknown>): boolean {\n const group = groupOf(stateId);\n if (group === \"amp\") {\n return true;\n }\n return config[`group_${group}`] !== false;\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBO,MAAM,oBAAwC;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAYO,SAAS,QAAQ,SAA0B;AAChD,QAAM,MAAM,QAAQ,SAAS,GAAG,IAAI,QAAQ,MAAM,GAAG,QAAQ,QAAQ,GAAG,CAAC,IAAI;AAE7E,MAAI,QAAQ,aAAa;AACvB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,UAAU,QAAQ,WAAW;AACvC,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,UAAU;AACpB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS;AACnB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS;AACnB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,YAAY;AACtB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS;AACnB,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAWO,SAAS,eAAe,SAAiB,QAA0C;AACxF,QAAM,QAAQ,QAAQ,OAAO;AAC7B,MAAI,UAAU,OAAO;AACnB,WAAO;AAAA,EACT;AACA,SAAO,OAAO,SAAS,KAAK,EAAE,MAAM;AACtC;",
|
|
4
|
+
"sourcesContent": ["/**\n * Datapoint groups: the thematic buckets that are both the object-tree structure and the\n * admin on/off switches. Every state id maps to exactly one group by its (zone-stripped) first\n * path segment (its channel). A disabled group's objects are never created and any existing ones\n * are pruned, the same way beszel gates its metric categories. `amp` is the always-on core \u2014 no\n * switch exists for it, exactly like beszel's unconditional `info.online`/`info.status`.\n *\n * Every group is a real channel prefix in the tree (`player.*`, `tuner.*`, `hdmi.*`,\n * `multiroom.*`, `scene.*`, `sound.*`, `advanced.*`) \u2014 the toggle and the folder a datapoint\n * visually sits in are the same thing, never decoupled. Zone 2/3/4, Zone B, masterPower, party\n * and distribution all live under `multiroom.*`. Legacy flat pre-v0.15.0 ids need no handling\n * here: the start-up cleanup deletes them via `RENAMED_CHANNELS` before anything queries them.\n */\n\n/** The switchable groups. `amp` is the amplifier core and can never be turned off. */\nexport type GroupId = \"amp\" | \"player\" | \"tuner\" | \"multiroom\" | \"hdmi\" | \"scene\" | \"sound\" | \"advanced\" | \"clock\";\n\n/** The groups a user can switch off, in display order (amp is always on and not listed here). */\nexport const SWITCHABLE_GROUPS: readonly GroupId[] = [\n \"player\",\n \"tuner\",\n \"multiroom\",\n \"hdmi\",\n \"scene\",\n \"sound\",\n \"advanced\",\n \"clock\",\n];\n\n/**\n * The datapoint group a state id belongs to, decided by its first path segment. Every group is a\n * real folder prefix in the tree, so the first segment of the id determines the group directly:\n * `multiroom.*` (including zones, masterPower, party), `player.*`, `tuner.*`, `hdmi.*`, `sound.*`,\n * `advanced.*`, `scene.*`. Anything not matched \u2014 the amplifier core (power, volume, input,\n * sleep \u2026), `info.*` \u2014 is the always-on `amp` group.\n *\n * @param stateId the device-relative state id (e.g. \"multiroom.zone2.power\", \"player.spotify.playback\")\n * @returns the group the state belongs to\n */\nexport function groupOf(stateId: string): GroupId {\n const seg = stateId.includes(\".\") ? stateId.slice(0, stateId.indexOf(\".\")) : stateId;\n\n if (seg === \"multiroom\") {\n return \"multiroom\";\n }\n if (seg === \"hdmi\" || seg === \"lipSync\") {\n return \"hdmi\";\n }\n if (seg === \"player\") {\n return \"player\";\n }\n if (seg === \"tuner\") {\n return \"tuner\";\n }\n if (seg === \"sound\") {\n return \"sound\";\n }\n if (seg === \"advanced\") {\n return \"advanced\";\n }\n if (seg === \"scene\") {\n return \"scene\";\n }\n if (seg === \"clock\") {\n return \"clock\";\n }\n return \"amp\";\n}\n\n/**\n * Whether a state's group is switched on. The amplifier core is always on; every other group is\n * on unless its `group_<id>` flag is explicitly false \u2014 default-on, so a fresh install and every\n * existing install keep all groups until the user turns one off.\n *\n * @param stateId the device-relative state id\n * @param config the adapter native config (carries `group_player`, `group_tuner`, \u2026 booleans)\n * @returns true if the state's group is enabled\n */\nexport function isGroupEnabled(stateId: string, config: Record<string, unknown>): boolean {\n const group = groupOf(stateId);\n if (group === \"amp\") {\n return true;\n }\n return config[`group_${group}`] !== false;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBO,MAAM,oBAAwC;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAYO,SAAS,QAAQ,SAA0B;AAChD,QAAM,MAAM,QAAQ,SAAS,GAAG,IAAI,QAAQ,MAAM,GAAG,QAAQ,QAAQ,GAAG,CAAC,IAAI;AAE7E,MAAI,QAAQ,aAAa;AACvB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,UAAU,QAAQ,WAAW;AACvC,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,UAAU;AACpB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS;AACnB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS;AACnB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,YAAY;AACtB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS;AACnB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS;AACnB,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAWO,SAAS,eAAe,SAAiB,QAA0C;AACxF,QAAM,QAAQ,QAAQ,OAAO;AAC7B,MAAI,UAAU,OAAO;AACnB,WAAO;AAAA,EACT;AACA,SAAO,OAAO,SAAS,KAAK,EAAE,MAAM;AACtC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -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 const order: string[] = [];\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 order.push(canonicalId);\n }\n entry.defs.set(transport, obj);\n }\n }\n const ownerByCanonicalId = new Map<string, Transport>();\n const resolved: ObjectDef[] = order.map(canonicalId => {\n const entry = byId.get(canonicalId);\n if (!entry) {\n throw new Error(`coordinateObjectTree: missing entry for ${canonicalId}`);\n }\n const owner = pickOwner(entry.key, [...entry.defs.keys()]);\n ownerByCanonicalId.set(canonicalId, owner);\n const ownerDef = entry.defs.get(owner);\n if (!ownerDef) {\n throw new Error(`coordinateObjectTree: owner ${owner} has no def for ${canonicalId}`);\n }\n return { ...ownerDef, id: canonicalId };\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;AACA,QAAM,QAAkB,CAAC;AACzB,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;AAC3B,cAAM,KAAK,WAAW;AAAA,MACxB;AACA,YAAM,KAAK,IAAI,WAAW,GAAG;AAAA,IAC/B;AAAA,EACF;AACA,QAAM,qBAAqB,oBAAI,IAAuB;AACtD,QAAM,WAAwB,MAAM,IAAI,iBAAe;AACrD,UAAM,QAAQ,KAAK,IAAI,WAAW;
|
|
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 const order: string[] = [];\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 order.push(canonicalId);\n }\n entry.defs.set(transport, obj);\n }\n }\n const ownerByCanonicalId = new Map<string, Transport>();\n const resolved: ObjectDef[] = order.map(canonicalId => {\n const entry = byId.get(canonicalId);\n // Both throws below are internal invariants, not reachable states: `order` is\n // built from `byId`'s own keys, and pickOwner always returns one of the\n // candidates it was handed (= this entry's own defs). They exist so a future\n // change to either side fails loudly instead of writing an empty object.\n if (!entry) {\n throw new Error(`coordinateObjectTree: missing entry for ${canonicalId}`);\n }\n const owner = pickOwner(entry.key, [...entry.defs.keys()]);\n ownerByCanonicalId.set(canonicalId, owner);\n const ownerDef = entry.defs.get(owner);\n if (!ownerDef) {\n throw new Error(`coordinateObjectTree: owner ${owner} has no def for ${canonicalId}`);\n }\n return { ...ownerDef, id: canonicalId };\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;AACA,QAAM,QAAkB,CAAC;AACzB,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;AAC3B,cAAM,KAAK,WAAW;AAAA,MACxB;AACA,YAAM,KAAK,IAAI,WAAW,GAAG;AAAA,IAC/B;AAAA,EACF;AACA,QAAM,qBAAqB,oBAAI,IAAuB;AACtD,QAAM,WAAwB,MAAM,IAAI,iBAAe;AACrD,UAAM,QAAQ,KAAK,IAAI,WAAW;AAKlC,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MAAM,2CAA2C,WAAW,EAAE;AAAA,IAC1E;AACA,UAAM,YAAQ,+BAAU,MAAM,KAAK,CAAC,GAAG,MAAM,KAAK,KAAK,CAAC,CAAC;AACzD,uBAAmB,IAAI,aAAa,KAAK;AACzC,UAAM,WAAW,MAAM,KAAK,IAAI,KAAK;AACrC,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,MAAM,+BAA+B,KAAK,mBAAmB,WAAW,EAAE;AAAA,IACtF;AACA,WAAO,EAAE,GAAG,UAAU,IAAI,YAAY;AAAA,EACxC,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
|
}
|
|
@@ -30,6 +30,8 @@ const OWNER_OVERRIDES = {
|
|
|
30
30
|
volume: ["ynca", "xml", "yxc"],
|
|
31
31
|
// §3c write loss — YXC is read-only for these, YNCA (and often XML) is writable.
|
|
32
32
|
"advanced.maxVolume": ["ynca", "xml", "yxc"],
|
|
33
|
+
// The DAB preset is a writable recall on YNCA but display-only in the YXC play info.
|
|
34
|
+
"tuner.dab.preset": ["ynca", "yxc"],
|
|
33
35
|
"sound.extraBass": ["ynca", "xml", "yxc"],
|
|
34
36
|
"sound.adaptiveDrc": ["ynca", "xml", "yxc"],
|
|
35
37
|
"sound.surroundDecoder": ["ynca", "yxc"],
|
|
@@ -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 \"sound.extraBass\": [\"ynca\", \"xml\", \"yxc\"],\n \"sound.adaptiveDrc\": [\"ynca\", \"xml\", \"yxc\"],\n \"sound.surroundDecoder\": [\"ynca\", \"yxc\"],\n \"sound.dialogueLift\": [\"xml\", \"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 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,
|
|
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 // The DAB preset is a writable recall on YNCA but display-only in the YXC play info.\n \"tuner.dab.preset\": [\"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 // \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,EAE3C,oBAAoB,CAAC,QAAQ,KAAK;AAAA,EAClC,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,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;AArD/E;AAsDE,QAAM,WAAW,QAAQ,QAAQ,gCAAgC,EAAE;AACnE,UAAO,oBAAS,SAAS,MAAlB,mBAAsB,cAAtB,YAAmC;AAC5C;AAYO,SAAS,cAAc,WAAsB,SAAyB;AApE7E;AAqEE,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;AAxFpF;AAyFE,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;AAzGjH;AA0GE,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
|
}
|
|
@@ -20,6 +20,7 @@ var value_coerce_exports = {};
|
|
|
20
20
|
__export(value_coerce_exports, {
|
|
21
21
|
decode: () => decode,
|
|
22
22
|
encode: () => encode,
|
|
23
|
+
formatWireNumber: () => formatWireNumber,
|
|
23
24
|
isWritableValue: () => isWritableValue,
|
|
24
25
|
specToCommon: () => specToCommon
|
|
25
26
|
});
|
|
@@ -99,11 +100,21 @@ function isWritableValue(value, numeric) {
|
|
|
99
100
|
}
|
|
100
101
|
return Number.isFinite(Number(value));
|
|
101
102
|
}
|
|
103
|
+
function formatWireNumber(value, decimals, step) {
|
|
104
|
+
const snapped = step ? Math.round(value / step) * step : value;
|
|
105
|
+
const magnitude = Math.abs(snapped).toFixed(decimals);
|
|
106
|
+
const sign = snapped < 0 && Number(magnitude) !== 0 ? "-" : "";
|
|
107
|
+
return sign + magnitude;
|
|
108
|
+
}
|
|
102
109
|
function encode(spec, value) {
|
|
103
110
|
switch (spec.kind) {
|
|
104
111
|
case "onoff":
|
|
105
112
|
return value ? spec.on : spec.off;
|
|
106
113
|
case "number":
|
|
114
|
+
if (spec.decimals !== void 0) {
|
|
115
|
+
return formatWireNumber(Number(value), spec.decimals, spec.step);
|
|
116
|
+
}
|
|
117
|
+
return String(value);
|
|
107
118
|
case "enum":
|
|
108
119
|
case "text":
|
|
109
120
|
return String(value);
|
|
@@ -119,6 +130,7 @@ function encode(spec, value) {
|
|
|
119
130
|
0 && (module.exports = {
|
|
120
131
|
decode,
|
|
121
132
|
encode,
|
|
133
|
+
formatWireNumber,
|
|
122
134
|
isWritableValue,
|
|
123
135
|
specToCommon
|
|
124
136
|
});
|