iobroker.yamaha 1.7.0 → 2.0.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 +22 -14
- package/build/lib/catalog/groups.js +1 -1
- package/build/lib/catalog/groups.js.map +2 -2
- package/build/lib/catalog/object-tree-coordinator.js +10 -1
- package/build/lib/catalog/object-tree-coordinator.js.map +2 -2
- package/build/lib/catalog/owner-policy.js +7 -2
- package/build/lib/catalog/owner-policy.js.map +2 -2
- package/build/lib/catalog/scene-titles.js +81 -0
- package/build/lib/catalog/scene-titles.js.map +7 -0
- package/build/lib/catalog/types.js +2 -1
- package/build/lib/catalog/types.js.map +2 -2
- package/build/lib/device-controller.js +306 -8
- package/build/lib/device-controller.js.map +2 -2
- package/build/lib/pure-helpers.js +82 -3
- package/build/lib/pure-helpers.js.map +2 -2
- package/build/lib/xml/device-controller.js +18 -13
- package/build/lib/xml/device-controller.js.map +2 -2
- package/build/lib/ynca/catalog.js +57 -53
- package/build/lib/ynca/catalog.js.map +2 -2
- package/build/lib/yxc/capability.js +1 -1
- package/build/lib/yxc/capability.js.map +1 -1
- package/build/lib/yxc/catalog.js +4 -4
- package/build/lib/yxc/catalog.js.map +1 -1
- package/build/lib/yxc/command-mapper.js +46 -37
- package/build/lib/yxc/command-mapper.js.map +2 -2
- package/build/lib/yxc/device-controller.js +202 -15
- package/build/lib/yxc/device-controller.js.map +2 -2
- package/build/lib/yxc/http-client.js +5 -3
- package/build/lib/yxc/http-client.js.map +2 -2
- package/build/lib/yxc/object-mapper.js +25 -18
- package/build/lib/yxc/object-mapper.js.map +2 -2
- package/build/main.js +13 -1
- package/build/main.js.map +2 -2
- package/io-package.json +54 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ For details and how to disable it, see the [Sentry plugin documentation](https:/
|
|
|
38
38
|
|
|
39
39
|
- Node.js >= 22
|
|
40
40
|
- js-controller >= 7.2.2
|
|
41
|
-
- admin >=
|
|
41
|
+
- admin >= 8.0.11
|
|
42
42
|
|
|
43
43
|
## Ports
|
|
44
44
|
|
|
@@ -61,17 +61,21 @@ Each receiver becomes one device node with themed groups — the same groups the
|
|
|
61
61
|
**Data points** switches control. Only what your device reports is created.
|
|
62
62
|
|
|
63
63
|
- **Amplifier core** (always on) — power, volume, mute, input, sound program, sleep, plus the device info with model, firmware, IP address and connection; MusicCast devices add an on-screen `remote` (cursor pad, menu keys).
|
|
64
|
-
- **`player`** —
|
|
65
|
-
- **`tuner`** — AM
|
|
66
|
-
- **`multiroom`** — zones 2–4, Zone B, the all-zones switches (master power, party mode) and the MusicCast device group in its own `multiroom.group` folder.
|
|
67
|
-
- **`hdmi`** — the HDMI outputs and lip
|
|
68
|
-
- **`scene`** — the receiver
|
|
69
|
-
- **`sound`** — tone and sound processing: bass/treble, DSP modes, enhancer, equalizer
|
|
70
|
-
- **`advanced`** — setup-level datapoints: maximum/initial volume, speaker configuration
|
|
64
|
+
- **`player`** — ONE "now playing" block per zone: `player.source` says what the zone is listening to, and playback state, artist, album, track, cover art, times and the transport buttons always describe exactly that — whatever source is playing. Zones 2–4 get their own block under `multiroom.zoneN.player`. The source folders keep only what is genuinely their own: preset recall & save for net radio/server/USB, the MusicCast favourite/recent/playlist/queue lists under `player.netPlayer`, the CD drive states, Bluetooth pairing and the AirPlay volume interlock. The `player.browse` folder mirrors the device's media menu: the eight visible lines (folders and titles marked by symbol), `selectLine` acts like OK on the remote, page/back/root buttons, a `rows` JSON for widgets and a `path` datapoint that walks e.g. `Bookmarks>Radio Paradise` on one write.
|
|
65
|
+
- **`tuner`** — one band, one frequency (kHz on every generation) and one preset for AM, FM and DAB, plus RDS texts and reception flags; only genuinely DAB-specific detail (service, ensemble, DLS, …) sits under `tuner.dab`.
|
|
66
|
+
- **`multiroom`** — zones 2–4 (each with its own player and scene block), Zone B, the all-zones switches (master power, party mode) and the MusicCast device group in its own `multiroom.group` folder.
|
|
67
|
+
- **`hdmi`** — the HDMI outputs and the two lip-sync offsets.
|
|
68
|
+
- **`scene`** — a recall dropdown carrying the titles the receiver reports (writable by number or title) and a `scene.list` JSON with every scene slot — titled where the device reports titles; zones with their own scenes carry theirs under `multiroom.zoneN.scene`.
|
|
69
|
+
- **`sound`** — tone and sound processing: bass/treble, DSP modes, enhancer, the equalizer in its own `sound.equalizer` folder and the current audio signal under `sound.signal` on MusicCast devices.
|
|
70
|
+
- **`advanced`** — setup-level datapoints: maximum/initial volume, the speaker configuration (A/B switches included) under `advanced.speakers`, input names.
|
|
71
71
|
- **`clock`** — the clock and alarm settings of MusicCast desk-audio devices (read-only).
|
|
72
72
|
|
|
73
73
|
## Troubleshooting
|
|
74
74
|
|
|
75
|
+
### Upgrading from 1.x
|
|
76
|
+
|
|
77
|
+
Version 2.0.0 reworks the object tree. On the first start the adapter removes the old datapoints itself and creates the new ones: the per-source player copies become one `player` block per zone, the scene name datapoints become the recall dropdown plus `scene.list`, the two tuner frequencies become one `tuner.frequency` in kHz, the equalizer and signal info move under `sound.equalizer`/`sound.signal`, the lip-sync offsets under `hdmi`, and the speaker A/B switches under `advanced.speakers`. Point scripts and visualizations at the new paths.
|
|
78
|
+
|
|
75
79
|
### Upgrading from 0.5.x
|
|
76
80
|
|
|
77
81
|
Version 1.0.0 is a complete rebuild. On the first start after the update the old datapoints (`volume`, `power`, `Commands.*`, `Realtime.*`, …) are removed and your receiver is recreated as a device; its IP address is carried over automatically. Point scripts and visualizations at the new paths — for example `yamaha.0.<device>.power` instead of `yamaha.0.power`.
|
|
@@ -98,6 +102,16 @@ On the very first contact the adapter asks the receiver which functions it suppo
|
|
|
98
102
|
Placeholder for the next version (at the beginning of the line):
|
|
99
103
|
### **WORK IN PROGRESS**
|
|
100
104
|
-->
|
|
105
|
+
### 2.0.0 (2026-09-01)
|
|
106
|
+
|
|
107
|
+
- (krobipd) Changed: one player block per zone now shows what is playing — source, title, artist, cover art and the transport buttons — instead of an identical, mostly empty block under every source
|
|
108
|
+
- (krobipd) Changed: the scene name datapoints are gone — the recall dropdown carries the receiver's own titles, and scene.list offers every scene with number and title for visualizations
|
|
109
|
+
- (krobipd) Changed: the tuner has one band, one frequency (kHz on every generation) and one preset for AM, FM and DAB; only genuinely DAB-specific detail stays under tuner.dab
|
|
110
|
+
- (krobipd) Changed: the equalizer and the audio-signal info moved into their own folders under sound, the lip-sync offsets into hdmi, the speaker A/B switches to the speaker settings
|
|
111
|
+
- (krobipd) New: zones can show different programs — zones 2 to 4 get their own player block, and play/pause/skip always act on whatever that zone is playing
|
|
112
|
+
- (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
|
|
113
|
+
- (krobipd) Fixed: restarting or updating the adapter while a receiver was still connecting no longer produces shutdown warnings about timers in the log
|
|
114
|
+
|
|
101
115
|
### 1.7.0 (2026-09-01)
|
|
102
116
|
|
|
103
117
|
- (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)
|
|
@@ -134,12 +148,6 @@ On the very first contact the adapter asks the receiver which functions it suppo
|
|
|
134
148
|
- (krobipd) Fixed: recalling a favourite, a recently played item or a tuner preset now goes to the zone that is actually listening instead of always switching the main zone
|
|
135
149
|
- (krobipd) Improved: stopping or restarting the adapter no longer leaves requests running that write to datapoints afterwards
|
|
136
150
|
|
|
137
|
-
### 1.3.0 (2026-08-26)
|
|
138
|
-
|
|
139
|
-
- (krobipd) New: menu browsing — page through the Net Radio, server and USB menus like with the remote: visible lines as datapoints, select-by-line, and a path datapoint for one-write navigation (#613)
|
|
140
|
-
- (krobipd) New: save presets from ioBroker — store the current tuner or network station to a preset slot and bookmark the playing Net Radio station on YNCA receivers.
|
|
141
|
-
- (krobipd) New: Bluetooth pairing and connect controls, FM mono mode and tuning indicators on YNCA receivers.
|
|
142
|
-
|
|
143
151
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
144
152
|
|
|
145
153
|
## History
|
|
@@ -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\" | \"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\"
|
|
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,
|
|
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\") {\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,QAAQ;AAClB,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
|
}
|
|
@@ -49,7 +49,16 @@ function coordinateObjectTree(contributions) {
|
|
|
49
49
|
if (!ownerDef) {
|
|
50
50
|
throw new Error(`coordinateObjectTree: owner ${owner} has no def for ${canonicalId}`);
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
const resolvedDef = { ...ownerDef, id: canonicalId };
|
|
53
|
+
if (!resolvedDef.common.states) {
|
|
54
|
+
for (const def of entry.defs.values()) {
|
|
55
|
+
if (def.common.states) {
|
|
56
|
+
resolvedDef.common = { ...resolvedDef.common, states: def.common.states };
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return resolvedDef;
|
|
53
62
|
});
|
|
54
63
|
resolved.sort((a, b) => a.id.split(".").length - b.id.split(".").length);
|
|
55
64
|
return { objects: resolved, ownerByCanonicalId };
|
|
@@ -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 // 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
|
|
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;
|
|
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 // 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;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;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
|
}
|
|
@@ -30,8 +30,13 @@ 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
|
-
//
|
|
34
|
-
|
|
33
|
+
// §3c write loss on the unified player block (v2.0.0): YXC reads playback/repeat/
|
|
34
|
+
// shuffle but cannot WRITE them (its API has only toggle/transport endpoints, which
|
|
35
|
+
// stay YXC-owned buttons); YNCA sets all three directly. Zone mirrors collapse to
|
|
36
|
+
// the same template, so this covers multiroom.zoneN.player.* too.
|
|
37
|
+
"player.playback": ["ynca", "yxc"],
|
|
38
|
+
"player.repeat": ["ynca", "yxc"],
|
|
39
|
+
"player.shuffle": ["ynca", "yxc"],
|
|
35
40
|
"sound.extraBass": ["ynca", "xml", "yxc"],
|
|
36
41
|
"sound.adaptiveDrc": ["ynca", "xml", "yxc"],
|
|
37
42
|
"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 //
|
|
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,
|
|
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,EAErC,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;AAhE/E;AAiEE,QAAM,WAAW,QAAQ,QAAQ,gCAAgC,EAAE;AACnE,UAAO,oBAAS,SAAS,MAAlB,mBAAsB,cAAtB,YAAmC;AAC5C;AAYO,SAAS,cAAc,WAAsB,SAAyB;AA/E7E;AAgFE,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;AAnGpF;AAoGE,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;AApHjH;AAqHE,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
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var scene_titles_exports = {};
|
|
20
|
+
__export(scene_titles_exports, {
|
|
21
|
+
knownScenes: () => knownScenes,
|
|
22
|
+
resolveSceneNumber: () => resolveSceneNumber,
|
|
23
|
+
sceneStatesMap: () => sceneStatesMap
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(scene_titles_exports);
|
|
26
|
+
var import_protocol = require("../xml/protocol");
|
|
27
|
+
function knownScenes(memory, zoneKey) {
|
|
28
|
+
var _a;
|
|
29
|
+
if (!memory) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const xml = memory.remembered(`xmlScenes:${zoneKey}`);
|
|
33
|
+
if (typeof xml === "string" && xml.length > 0) {
|
|
34
|
+
const scenes = (0, import_protocol.parseSceneList)(xml);
|
|
35
|
+
if (scenes.length > 0) {
|
|
36
|
+
return scenes;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (zoneKey === "main") {
|
|
40
|
+
const statics = memory.remembered("yncaStaticValues");
|
|
41
|
+
const main = (_a = statics == null ? void 0 : statics.MAIN) != null ? _a : {};
|
|
42
|
+
const scenes = [];
|
|
43
|
+
for (let n = 1; n <= 12; n++) {
|
|
44
|
+
const title = main[`SCENE${n}NAME`];
|
|
45
|
+
if (typeof title === "string" && title.length > 0) {
|
|
46
|
+
scenes.push({ num: n, title });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return scenes;
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
function resolveSceneNumber(value, memory, zoneKey) {
|
|
54
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
55
|
+
return Math.round(value);
|
|
56
|
+
}
|
|
57
|
+
if (typeof value !== "string") {
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
const trimmed = value.trim();
|
|
61
|
+
if (/^\d+$/.test(trimmed)) {
|
|
62
|
+
return Number(trimmed);
|
|
63
|
+
}
|
|
64
|
+
const needle = trimmed.toLowerCase();
|
|
65
|
+
const match = knownScenes(memory, zoneKey).find((scene) => scene.title.toLowerCase() === needle);
|
|
66
|
+
return match == null ? void 0 : match.num;
|
|
67
|
+
}
|
|
68
|
+
function sceneStatesMap(memory, zoneKey) {
|
|
69
|
+
const scenes = knownScenes(memory, zoneKey);
|
|
70
|
+
if (scenes.length === 0) {
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
return Object.fromEntries(scenes.map((scene) => [scene.num, scene.title]));
|
|
74
|
+
}
|
|
75
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
76
|
+
0 && (module.exports = {
|
|
77
|
+
knownScenes,
|
|
78
|
+
resolveSceneNumber,
|
|
79
|
+
sceneStatesMap
|
|
80
|
+
});
|
|
81
|
+
//# sourceMappingURL=scene-titles.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/catalog/scene-titles.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ProbeMemory } from \"../lifecycle/probe-memory\";\nimport { parseSceneList } from \"../xml/protocol\";\n\n/**\n * Scene titles, cross-transport. The device reports its scene titles over XML\n * (`Scene_Sel_Item`, per zone) and/or YNCA (`SCENExNAME`, main zone) \u2014 while the\n * scene RECALL may be owned by a third transport (MusicCast). Both title sources\n * land in the shared per-device probe memory, so every controller can resolve a\n * written title to its number and render the one `scene.list` state, regardless\n * of which transport owns the write.\n */\n\n/** One scene for the `scene.list` JSON state. */\nexport interface SceneListEntry {\n /** The 1-based scene number (the recall value). */\n num: number;\n /** The scene's title as the device reports it. */\n title: string;\n}\n\n/** The probe-memory shape of the YNCA static values (see device-controller STATIC_KEY). */\ntype YncaStatics = Record<string, Record<string, string>>;\n\n/**\n * The known scenes of a zone, from whichever transport reported titles: the XML\n * declaration first (per zone), the YNCA scene names as the fallback (main only).\n *\n * @param memory the device's shared probe memory\n * @param zoneKey the zone (`main`, `zone2`, \u2026)\n * @returns the scenes with titles, empty when no transport reported any\n */\nexport function knownScenes(memory: ProbeMemory | undefined, zoneKey: string): SceneListEntry[] {\n if (!memory) {\n return [];\n }\n const xml = memory.remembered<string>(`xmlScenes:${zoneKey}`);\n if (typeof xml === \"string\" && xml.length > 0) {\n // Parsed lazily from the remembered raw declaration \u2014 ONE stored form, one parser.\n const scenes = parseSceneList(xml);\n if (scenes.length > 0) {\n return scenes;\n }\n }\n if (zoneKey === \"main\") {\n const statics = memory.remembered<YncaStatics>(\"yncaStaticValues\");\n const main = statics?.MAIN ?? {};\n const scenes: SceneListEntry[] = [];\n for (let n = 1; n <= 12; n++) {\n const title = main[`SCENE${n}NAME`];\n if (typeof title === \"string\" && title.length > 0) {\n scenes.push({ num: n, title });\n }\n }\n return scenes;\n }\n return [];\n}\n\n/**\n * Resolve a scene-recall write to its number: a number (or numeric string) passes\n * through, a TITLE is looked up case-insensitively in the zone's known scenes \u2014\n * so `scene.recall = \"Movie Viewing\"` works wherever the device reported titles\n * (the govee dual-write pattern).\n *\n * @param value the written value\n * @param memory the device's shared probe memory\n * @param zoneKey the zone (`main`, `zone2`, \u2026)\n * @returns the scene number, or undefined when unresolvable\n */\nexport function resolveSceneNumber(\n value: unknown,\n memory: ProbeMemory | undefined,\n zoneKey: string,\n): number | undefined {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return Math.round(value);\n }\n if (typeof value !== \"string\") {\n return undefined;\n }\n const trimmed = value.trim();\n if (/^\\d+$/.test(trimmed)) {\n return Number(trimmed);\n }\n const needle = trimmed.toLowerCase();\n const match = knownScenes(memory, zoneKey).find(scene => scene.title.toLowerCase() === needle);\n return match?.num;\n}\n\n/**\n * The dropdown label map for a zone's scene recall (number \u2192 title), or undefined\n * when no transport reported titles.\n *\n * @param memory the device's shared probe memory\n * @param zoneKey the zone\n * @returns the states map, or undefined\n */\nexport function sceneStatesMap(memory: ProbeMemory | undefined, zoneKey: string): Record<string, string> | undefined {\n const scenes = knownScenes(memory, zoneKey);\n if (scenes.length === 0) {\n return undefined;\n }\n return Object.fromEntries(scenes.map(scene => [scene.num, scene.title]));\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,sBAA+B;AA8BxB,SAAS,YAAY,QAAiC,SAAmC;AA/BhG;AAgCE,MAAI,CAAC,QAAQ;AACX,WAAO,CAAC;AAAA,EACV;AACA,QAAM,MAAM,OAAO,WAAmB,aAAa,OAAO,EAAE;AAC5D,MAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,GAAG;AAE7C,UAAM,aAAS,gCAAe,GAAG;AACjC,QAAI,OAAO,SAAS,GAAG;AACrB,aAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI,YAAY,QAAQ;AACtB,UAAM,UAAU,OAAO,WAAwB,kBAAkB;AACjE,UAAM,QAAO,wCAAS,SAAT,YAAiB,CAAC;AAC/B,UAAM,SAA2B,CAAC;AAClC,aAAS,IAAI,GAAG,KAAK,IAAI,KAAK;AAC5B,YAAM,QAAQ,KAAK,QAAQ,CAAC,MAAM;AAClC,UAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG;AACjD,eAAO,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC;AAAA,MAC/B;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,SAAO,CAAC;AACV;AAaO,SAAS,mBACd,OACA,QACA,SACoB;AACpB,MAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GAAG;AACvD,WAAO,KAAK,MAAM,KAAK;AAAA,EACzB;AACA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO;AAAA,EACT;AACA,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,QAAQ,KAAK,OAAO,GAAG;AACzB,WAAO,OAAO,OAAO;AAAA,EACvB;AACA,QAAM,SAAS,QAAQ,YAAY;AACnC,QAAM,QAAQ,YAAY,QAAQ,OAAO,EAAE,KAAK,WAAS,MAAM,MAAM,YAAY,MAAM,MAAM;AAC7F,SAAO,+BAAO;AAChB;AAUO,SAAS,eAAe,QAAiC,SAAqD;AACnH,QAAM,SAAS,YAAY,QAAQ,OAAO;AAC1C,MAAI,OAAO,WAAW,GAAG;AACvB,WAAO;AAAA,EACT;AACA,SAAO,OAAO,YAAY,OAAO,IAAI,WAAS,CAAC,MAAM,KAAK,MAAM,KAAK,CAAC,CAAC;AACzE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/catalog/types.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ValueSpec } from \"./value-coerce\";\n\n/**\n * One catalogued device function \u2192 one ioBroker state. The catalog is\n * device-agnostic: it lists every function a protocol can expose; the\n * per-device mapper picks the entries the device actually reports.\n *\n * **Single source rule.** Each protocol catalog extends this with its own\n * protocol key (YNCA: the subunit function name; YXC: the getStatus field +\n * write method; XML: the Basic_Status field + PUT builder). The init sweep, the\n * device\u2192state read-back and the user-write encode are all derived from that one\n * extended list \u2014 never a second state\u2194function table beside the catalog (the\n * `AMP_STATES` vs `STATE_MAPPINGS` split this rewrite removes). `catalogToObjects`\n * reads only the object fields below and ignores the protocol key.\n */\nexport interface CatalogEntry {\n /** State id relative to the device \u2014 dotted for a channel (e.g. `power`, `sound.bass`, `zone2.power`). */\n id: string;\n /** Display name shown in the object browser. */\n name: string;\n /** Value semantics \u2014 drives type/role/states/range via {@link ValueSpec}. */\n spec: ValueSpec;\n /** Whether the user can write this state. */\n write: boolean;\n /** Explicit ioBroker role; when omitted it is derived from the spec kind. */\n role?: string;\n}\n\n/** An object to create in the device tree: a state or a channel. */\nexport interface ObjectDef {\n /** Object id relative to the device. */\n id: string;\n /** Object kind. */\n type: \"state\" | \"channel\";\n /** ioBroker common part. */\n common: {\n name: string;\n type?: \"boolean\" | \"number\" | \"string\";\n role?: string;\n read?: boolean;\n write?: boolean;\n unit?: string;\n min?: number;\n max?: number;\n step?: number;\n states?: Record<string, string>;\n };\n}\n\n/**\n * Display names for the channel ids the catalogs use. A channel id not listed\n * here falls back to its capitalised segment, so a new channel still renders \u2014 but\n * every channel a catalog actually creates is named here, so nothing shows a raw id\n * like \"Pc\" or \"Ipod\" in the object browser. Keys match the real channel segments\n * (verified against the built catalogs); do not add speculative entries.\n */\nexport const CHANNEL_NAMES: Record<string, string> = {\n // Device info (metadata beside the per-device connection indicator)\n info: \"Info\",\n // Zones\n zone2: \"Zone 2\",\n zone3: \"Zone 3\",\n zone4: \"Zone 4\",\n zoneB: \"Zone B\",\n // Amplifier groups\n sound: \"Sound\",\n advanced: \"Advanced\",\n hdmi: \"HDMI\",\n speakers: \"Speakers\",\n scene: \"Scenes\",\n remote: \"Remote control\",\n inputNames: \"Input names\",\n initialVolume: \"Initial volume\",\n
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAwDO,MAAM,gBAAwC;AAAA;AAAA,EAEnD,MAAM;AAAA;AAAA,EAEN,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA;AAAA,EAEP,OAAO;AAAA,EACP,UAAU;AAAA,EACV,MAAM;AAAA,EACN,UAAU;AAAA,EACV,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,
|
|
4
|
+
"sourcesContent": ["import type { ValueSpec } from \"./value-coerce\";\n\n/**\n * One catalogued device function \u2192 one ioBroker state. The catalog is\n * device-agnostic: it lists every function a protocol can expose; the\n * per-device mapper picks the entries the device actually reports.\n *\n * **Single source rule.** Each protocol catalog extends this with its own\n * protocol key (YNCA: the subunit function name; YXC: the getStatus field +\n * write method; XML: the Basic_Status field + PUT builder). The init sweep, the\n * device\u2192state read-back and the user-write encode are all derived from that one\n * extended list \u2014 never a second state\u2194function table beside the catalog (the\n * `AMP_STATES` vs `STATE_MAPPINGS` split this rewrite removes). `catalogToObjects`\n * reads only the object fields below and ignores the protocol key.\n */\nexport interface CatalogEntry {\n /** State id relative to the device \u2014 dotted for a channel (e.g. `power`, `sound.bass`, `zone2.power`). */\n id: string;\n /** Display name shown in the object browser. */\n name: string;\n /** Value semantics \u2014 drives type/role/states/range via {@link ValueSpec}. */\n spec: ValueSpec;\n /** Whether the user can write this state. */\n write: boolean;\n /** Explicit ioBroker role; when omitted it is derived from the spec kind. */\n role?: string;\n}\n\n/** An object to create in the device tree: a state or a channel. */\nexport interface ObjectDef {\n /** Object id relative to the device. */\n id: string;\n /** Object kind. */\n type: \"state\" | \"channel\";\n /** ioBroker common part. */\n common: {\n name: string;\n type?: \"boolean\" | \"number\" | \"string\";\n role?: string;\n read?: boolean;\n write?: boolean;\n unit?: string;\n min?: number;\n max?: number;\n step?: number;\n states?: Record<string, string>;\n };\n}\n\n/**\n * Display names for the channel ids the catalogs use. A channel id not listed\n * here falls back to its capitalised segment, so a new channel still renders \u2014 but\n * every channel a catalog actually creates is named here, so nothing shows a raw id\n * like \"Pc\" or \"Ipod\" in the object browser. Keys match the real channel segments\n * (verified against the built catalogs); do not add speculative entries.\n */\nexport const CHANNEL_NAMES: Record<string, string> = {\n // Device info (metadata beside the per-device connection indicator)\n info: \"Info\",\n // Zones\n zone2: \"Zone 2\",\n zone3: \"Zone 3\",\n zone4: \"Zone 4\",\n zoneB: \"Zone B\",\n // Amplifier groups\n sound: \"Sound\",\n advanced: \"Advanced\",\n hdmi: \"HDMI\",\n speakers: \"Speakers\",\n scene: \"Scenes\",\n remote: \"Remote control\",\n inputNames: \"Input names\",\n initialVolume: \"Initial volume\",\n equalizer: \"Equalizer\",\n signal: \"Audio signal\",\n // Tuner\n tuner: \"Tuner\",\n dab: \"DAB\",\n // Media player container + multiroom\n player: \"Media player\",\n multiroom: \"Multiroom\",\n // The MusicCast-Link folder under multiroom \u2014 a group of linked DEVICES, not zones.\n group: \"MusicCast group (linked devices)\",\n // Media player sources\n netRadio: \"Net radio\",\n server: \"Media server\",\n usb: \"USB\",\n spotify: \"Spotify\",\n deezer: \"Deezer\",\n tidal: \"Tidal\",\n napster: \"Napster\",\n pandora: \"Pandora\",\n rhapsody: \"Rhapsody\",\n sirius: \"SiriusXM\",\n airplay: \"AirPlay\",\n bluetooth: \"Bluetooth\",\n pc: \"PC\",\n musicCastLink: \"MusicCast Link\",\n ipod: \"iPod\",\n ipodUsb: \"iPod (USB)\",\n // YXC/XML media channels\n cd: \"CD\",\n netPlayer: \"Network player\",\n clock: \"Clock\",\n};\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAwDO,MAAM,gBAAwC;AAAA;AAAA,EAEnD,MAAM;AAAA;AAAA,EAEN,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA;AAAA,EAEP,OAAO;AAAA,EACP,UAAU;AAAA,EACV,MAAM;AAAA,EACN,UAAU;AAAA,EACV,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EAER,OAAO;AAAA,EACP,KAAK;AAAA;AAAA,EAEL,QAAQ;AAAA,EACR,WAAW;AAAA;AAAA,EAEX,OAAO;AAAA;AAAA,EAEP,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,WAAW;AAAA,EACX,IAAI;AAAA,EACJ,eAAe;AAAA,EACf,MAAM;AAAA,EACN,SAAS;AAAA;AAAA,EAET,IAAI;AAAA,EACJ,WAAW;AAAA,EACX,OAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|