iobroker.yamaha 2.0.0 → 2.0.1
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 +9 -12
- package/build/lib/device-controller.js +39 -11
- package/build/lib/device-controller.js.map +2 -2
- package/build/lib/pure-helpers.js +24 -0
- package/build/lib/pure-helpers.js.map +2 -2
- package/build/lib/xml/device-controller.js +39 -5
- package/build/lib/xml/device-controller.js.map +2 -2
- package/build/lib/yxc/device-controller.js +15 -4
- package/build/lib/yxc/device-controller.js.map +2 -2
- package/build/main.js +68 -12
- package/build/main.js.map +2 -2
- package/io-package.json +14 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,9 +19,10 @@ 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
|
+
- **Now playing, per zone** — one player block per zone shows source, playback state, title, artist, cover art and the transport buttons for whatever that zone is playing; zones 2–4 carry their own block
|
|
22
23
|
- **Presets and favourites** — recall tuner presets and stored network/USB favourites by number, step through presets, save the current station to a preset slot or bookmark it, and read the stored lists with their names (MusicCast); recently-played recall on MusicCast devices
|
|
23
24
|
- **Menu browsing** — page through the Net Radio, media-server and USB menus like with the remote: the visible menu lines as datapoints, select-by-line, and a path datapoint that navigates to a favourite in one write
|
|
24
|
-
- **Scenes with their names** — recall a scene by number or from a dropdown that shows the
|
|
25
|
+
- **Scenes with their names** — recall a scene by number or by its title from a dropdown that shows the names the receiver reports, per zone — plus a scene list for visualizations
|
|
25
26
|
- **On-screen remote** — cursor pad and menu keys as datapoints on MusicCast devices, for driving the receiver's own on-screen menus
|
|
26
27
|
- **Clock & alarm view** — MusicCast desk-audio devices show their clock and alarm settings
|
|
27
28
|
- **Capability-driven** — states are generated from what each device reports, no hardcoded model list
|
|
@@ -102,6 +103,13 @@ On the very first contact the adapter asks the receiver which functions it suppo
|
|
|
102
103
|
Placeholder for the next version (at the beginning of the line):
|
|
103
104
|
### **WORK IN PROGRESS**
|
|
104
105
|
-->
|
|
106
|
+
### 2.0.1 (2026-09-01)
|
|
107
|
+
|
|
108
|
+
- (krobipd) Fixed: devices on the oldest protocol no longer get datapoints their status never delivers — states are only created for what the device really answers, like on the other two protocols
|
|
109
|
+
- (krobipd) Fixed: leftover datapoints that never carried a value — zone sound settings or a second HDMI output the receiver does not have — are cleaned away once when updating the adapter
|
|
110
|
+
- (krobipd) Fixed: the player block of a zone that is not playing a media source starts with cleared values (empty texts, playback on stop) instead of staying without any value until the first playback
|
|
111
|
+
- (krobipd) Fixed: the two DAB station-scan datapoints (stations found, scan progress) start at zero instead of staying without a value until the first station scan
|
|
112
|
+
|
|
105
113
|
### 2.0.0 (2026-09-01)
|
|
106
114
|
|
|
107
115
|
- (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
|
|
@@ -137,17 +145,6 @@ On the very first contact the adapter asks the receiver which functions it suppo
|
|
|
137
145
|
- (krobipd) New: switching a datapoint group on or off in the settings now shows how many datapoints appeared or disappeared, so you no longer have to click through the object tree to check
|
|
138
146
|
- (krobipd) New: optional error reporting via Sentry — only active if you enabled diagnostics in ioBroker, and it transmits no personal data
|
|
139
147
|
|
|
140
|
-
### 1.4.0 (2026-08-26)
|
|
141
|
-
|
|
142
|
-
- (krobipd) Fixed: commands sent in quick succession all arrive — a scene switching power, input and volume in one go used to lose everything after the first command
|
|
143
|
-
- (krobipd) Fixed: a command the device rejects is now reported instead of counting as success, so a MusicCast device that stops answering is reconnected rather than silently freezing
|
|
144
|
-
- (krobipd) Fixed: names and menu entries containing "&" or other special characters now read and write correctly on the older XML protocol
|
|
145
|
-
- (krobipd) Fixed: writing one equalizer band no longer resets the other two when the device has not reported its bands yet
|
|
146
|
-
- (krobipd) Fixed: switching the tuner band and setting a frequency right after each other now applies the frequency to the new band
|
|
147
|
-
- (krobipd) Improved: startup with automatic discovery is much faster on networks with many devices, and a reconnect no longer re-asks what the device already told us
|
|
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
|
|
149
|
-
- (krobipd) Improved: stopping or restarting the adapter no longer leaves requests running that write to datapoints afterwards
|
|
150
|
-
|
|
151
148
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
152
149
|
|
|
153
150
|
## History
|
|
@@ -441,14 +441,25 @@ class YncaDeviceController {
|
|
|
441
441
|
sourceDef(`${zone.prefix}player.source`)
|
|
442
442
|
);
|
|
443
443
|
}
|
|
444
|
+
const presentFlat = new Set(
|
|
445
|
+
this.presentEntries.filter((entry) => FLAT_PLAYER_ID.test(entry.id)).map((entry) => entry.id)
|
|
446
|
+
);
|
|
444
447
|
for (const zone of YNCA_ZONES) {
|
|
445
|
-
if (this.playerZones.includes(zone.key)) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
448
|
+
if (!this.playerZones.includes(zone.key)) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const input = this.zoneInputs.get(zone.key);
|
|
452
|
+
const playing = playerSubunitForInput(input) !== void 0;
|
|
453
|
+
this.deps.setStateAck(
|
|
454
|
+
`${this.deviceId}.${zone.prefix}player.source`,
|
|
455
|
+
playing && input !== void 0 ? input : ""
|
|
456
|
+
);
|
|
457
|
+
if (!playing) {
|
|
458
|
+
for (const clear of YNCA_PLAYER_CLEAR) {
|
|
459
|
+
if (presentFlat.has(clear.id)) {
|
|
460
|
+
this.deps.setStateAck(`${this.deviceId}.${zone.prefix}${clear.id}`, clear.value);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
452
463
|
}
|
|
453
464
|
}
|
|
454
465
|
}
|
|
@@ -557,28 +568,45 @@ class YncaDeviceController {
|
|
|
557
568
|
}
|
|
558
569
|
if (this.hasDab) {
|
|
559
570
|
if (this.tunerBand === "FM") {
|
|
560
|
-
this.
|
|
571
|
+
this.sendProven("DAB", "FMFREQ", (0, import_value_coerce.formatWireNumber)(khz / 1e3, 2));
|
|
561
572
|
} else {
|
|
562
573
|
this.deps.log.debug(`${this.deviceId}: DAB tunes by service \u2014 frequency write ignored`);
|
|
563
574
|
}
|
|
564
575
|
return true;
|
|
565
576
|
}
|
|
566
577
|
if (this.tunerBand === "AM") {
|
|
567
|
-
this.
|
|
578
|
+
this.sendProven("TUN", "AMFREQ", String(Math.round(khz)));
|
|
568
579
|
} else {
|
|
569
|
-
this.
|
|
580
|
+
this.sendProven("TUN", "FMFREQ", (0, import_value_coerce.formatWireNumber)(khz / 1e3, 2));
|
|
570
581
|
}
|
|
571
582
|
return true;
|
|
572
583
|
}
|
|
573
584
|
if (stateId === "tuner.preset" && this.hasDab) {
|
|
574
585
|
const slot = Math.round(Number(value));
|
|
575
586
|
if (Number.isFinite(slot) && slot >= 1) {
|
|
576
|
-
this.
|
|
587
|
+
this.sendProven("DAB", this.tunerBand === "DAB" ? "DABPRESET" : "FMPRESET", String(slot));
|
|
577
588
|
}
|
|
578
589
|
return true;
|
|
579
590
|
}
|
|
580
591
|
return false;
|
|
581
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* Send a band-routed write ONLY with a function THIS device reported in its sweep —
|
|
595
|
+
* the same claim-with-proof rule every generic write obeys (#615 class). Without it
|
|
596
|
+
* the router put a blind TUN:FMFREQ on the wire for any device, tuner or not, and
|
|
597
|
+
* each write surfaced as a "device refused" warning (test-audit finding).
|
|
598
|
+
*
|
|
599
|
+
* @param subunit the target subunit
|
|
600
|
+
* @param func the wire function
|
|
601
|
+
* @param wire the encoded wire value
|
|
602
|
+
*/
|
|
603
|
+
sendProven(subunit, func, wire) {
|
|
604
|
+
if (!this.presentEntries.some((entry) => entry.subunit === subunit && entry.func === func)) {
|
|
605
|
+
this.deps.log.debug(`${this.deviceId}: ${subunit}:${func} not reported by this device \u2014 write dropped`);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
this.deps.client.send(subunit, func, wire);
|
|
609
|
+
}
|
|
582
610
|
/**
|
|
583
611
|
* Create the browsing surface (#613) when the device reports a browsable media
|
|
584
612
|
* subunit: the official YNCA list vocabulary (LISTINFO/LISTSEL/LISTPAGE/LISTCURSOR)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/device-controller.ts"],
|
|
4
|
-
"sourcesContent": ["import type { YncaCapabilities } from \"./ynca/capability\";\nimport { formatWireNumber } from \"./catalog/value-coerce\";\nimport type { ObjectDef } from \"./catalog/types\";\nimport type { ConnectionHandle, ControllerLog } from \"./controller\";\nimport {\n YNCA_CATALOG,\n availGets,\n funcToEntry,\n idToEntry,\n presentYncaEntries,\n sweepGets,\n yncaCommand,\n yncaObjectsFor,\n yncaStateUpdate,\n type YncaEntry,\n} from \"./ynca/catalog\";\nimport type { YncaSubunitCache } from \"./ynca/subunit-cache\";\nimport type { CommandGate } from \"./lifecycle/command-gate\";\nimport type { ProbeMemory } from \"./lifecycle/probe-memory\";\nimport type { BrowseEngine } from \"./browse/browse-engine\";\nimport { createBrowseSurface } from \"./browse/surface\";\nimport { YNCA_BROWSE_SOURCES, YncaBrowseDriver } from \"./browse/ynca-browse-driver\";\n\n// The YNCA catalog and its lookup maps are static \u2014 built once for all devices.\n// SYS:MODELNAME is part of the catalog (info.model), so the sweep already covers it.\n// The AVAIL probe always covers the FULL catalog (not the group-filtered one), so the\n// cached subunit set reflects the device, never the current group configuration.\nconst FUNC_MAP = funcToEntry(YNCA_CATALOG);\nconst ID_MAP = idToEntry(YNCA_CATALOG);\nconst AVAIL_PROBE = availGets(YNCA_CATALOG);\n\n/**\n * Functions whose VALUE cannot change while the device runs: the 23 assignable input names\n * and the 12 scene names. They cost 35 of the ~187 paced reads of a targeted sweep (3.5 s\n * at the specification's mandatory 100 ms spacing) and answer the same thing every time, so\n * a reconnect reuses what the first connect learned. Deliberately per adapter RUN, not\n * persisted: renaming an input at the receiver shows up after the next adapter restart\n * rather than needing anyone to invalidate a stored file.\n */\nconst STATIC_FUNC = /^(INPNAME|SCENE\\d+NAME$)/;\n\n/** The zones whose player/input surface the controller routes (v2.0.0 player unification). */\nconst YNCA_ZONES: Array<{ key: string; subunit: string; prefix: string }> = [\n { key: \"main\", subunit: \"MAIN\", prefix: \"\" },\n { key: \"zone2\", subunit: \"ZONE2\", prefix: \"multiroom.zone2.\" },\n { key: \"zone3\", subunit: \"ZONE3\", prefix: \"multiroom.zone3.\" },\n { key: \"zone4\", subunit: \"ZONE4\", prefix: \"multiroom.zone4.\" },\n];\n\n/**\n * INP value \u2192 the player subunit it selects (normalized: uppercase, alphanumerics only).\n * The wire values come from the RX-V6A INPNAME/INP capture and ynca-python's input map;\n * inputs that are no media player (HDMI, AV, TUNER, \u2026) deliberately map to nothing.\n */\nconst INPUT_SUBUNITS: Record<string, string> = {\n NETRADIO: \"NETRADIO\",\n SERVER: \"SERVER\",\n USB: \"USB\",\n SPOTIFY: \"SPOTIFY\",\n DEEZER: \"DEEZER\",\n TIDAL: \"TIDAL\",\n NAPSTER: \"NAPSTER\",\n PANDORA: \"PANDORA\",\n RHAPSODY: \"RHAP\",\n SIRIUS: \"SIRIUS\",\n SIRIUSXM: \"SIRIUS\",\n SIRIUSIR: \"SIRIUS\",\n AIRPLAY: \"AIRPLAY\",\n BLUETOOTH: \"BT\",\n PC: \"PC\",\n MUSICCASTLINK: \"MCLINK\",\n IPOD: \"IPOD\",\n IPODUSB: \"IPODUSB\",\n};\n\n/**\n * The player subunit an input selection feeds, or undefined when the input is no\n * media player.\n *\n * @param input the zone's INP value (e.g. \"NET RADIO\", \"iPod (USB)\")\n * @returns the subunit (e.g. NETRADIO), or undefined\n */\nfunction playerSubunitForInput(input: string | undefined): string | undefined {\n if (typeof input !== \"string\" || input.length === 0) {\n return undefined;\n }\n return INPUT_SUBUNITS[input.toUpperCase().replace(/[^A-Z0-9]/g, \"\")];\n}\n\n/**\n * A state of the flat per-zone player block: exactly one segment below `player.`.\n * ONLY these are zone-routed \u2014 multi-segment `player.*` ids (bluetooth pairing\n * status, airplay volume interlock, per-source presets) are device-global and go\n * straight to their state (2.0.0 review finding: the broad `player.` prefix check\n * silently dropped BT/AirPlay status while no zone listened to that source).\n */\nconst FLAT_PLAYER_ID = /^player\\.[^.]+$/;\n\n/**\n * What a zone's player block is reset to when the zone leaves its playing source\n * (clear-on-switch): metadata empty, playback Stop. Filtered to the states the\n * device actually has before emitting.\n */\nconst YNCA_PLAYER_CLEAR: Array<{ id: string; value: number | string | boolean }> = [\n { id: \"player.playback\", value: 1 },\n { id: \"player.artist\", value: \"\" },\n { id: \"player.album\", value: \"\" },\n { id: \"player.track\", value: \"\" },\n { id: \"player.station\", value: \"\" },\n { id: \"player.channelName\", value: \"\" },\n { id: \"player.elapsedTime\", value: \"\" },\n { id: \"player.totalTime\", value: \"\" },\n { id: \"player.repeat\", value: 0 },\n { id: \"player.shuffle\", value: false },\n];\n\n/** Memory key for the remembered static values. */\nconst STATIC_KEY = \"yncaStaticValues\";\n\n/** Memory key for the persisted capability shape (the fast-restart layer). */\nconst CAPS_KEY = \"yncaCapabilities\";\n\n/** The persisted capability shape, keyed by the device identity that validated it. */\ninterface CachedCapabilities {\n /** SYS MODELNAME at capture time \u2014 freshness key half 1. */\n model: string;\n /** SYS VERSION at capture time \u2014 freshness key half 2. */\n firmware: string;\n /** The captured subunit\u2192function map (values are last-known, used for SHAPE only). */\n subunits: Record<string, Record<string, string>>;\n}\n\n/**\n * Whether a remembered value carries the cached-capabilities shape (API boundary \u2014\n * the persisted probe memory is untrusted storage).\n *\n * @param value the remembered value\n * @returns true when usable\n */\nfunction isCachedCapabilities(value: unknown): value is CachedCapabilities {\n const candidate = value as Partial<CachedCapabilities> | null;\n return (\n typeof candidate === \"object\" &&\n candidate !== null &&\n typeof candidate.model === \"string\" &&\n typeof candidate.firmware === \"string\" &&\n typeof candidate.subunits === \"object\" &&\n candidate.subunits !== null\n );\n}\n\n/**\n * The functions whose presence proves a subunit really serves menus \u2014 the fields a real\n * `LISTINFO=?` answer is made of (RX-A810 reference log). See {@link YncaDeviceController.probeBrowseSubunits}.\n */\nconst LIST_PROOF = /^(LISTLAYER|LISTLAYERNAME|CURRLINE|MAXLINE|LINE[1-8](TXT|ATRIB))$/;\n\n/** The subset of the YNCA client the controller uses (so tests can inject a fake). */\nexport interface YncaClientLike {\n /** Open the connection. */\n connect(): Promise<void>;\n /** Run the init sweep and return the device's capabilities. */\n readCapabilities(gets: Array<{ subunit: string; func: string }>): Promise<YncaCapabilities>;\n /** Send a PUT command. */\n send(subunit: string, func: string, value: string): void;\n /** Send a GET request (the browse driver reads LISTINFO with it). */\n get(subunit: string, func: string): void;\n /** Register a handler for pushed messages. */\n onMessage(handler: (message: { subunit: string; func: string; value: string }) => void): void;\n /** Register the socket-drop handler the supervisor reconnects on. */\n onDrop(handler: (reason?: Error) => void): void;\n /** Register the refusal handler for user commands the device rejects (optional in older tests). */\n onRefusal?(handler: (command: string, verdict: \"restricted\" | \"undefined\") => void): void;\n /** Start the keepalive poll \u2014 called after the init sweep, not on connect. */\n startKeepalive(): void;\n /** Close the connection synchronously. */\n close(): void;\n}\n\nexport type { ControllerLog };\n\n/** The adapter callbacks the controller drives \u2014 narrow, so no adapter mock is needed in tests. */\nexport interface ControllerDeps {\n /** The YNCA client for this device. */\n client: YncaClientLike;\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 log. */\n log: ControllerLog;\n /**\n * Whether a catalog entry's datapoint group is enabled. A disabled group's entries\n * are dropped BEFORE the sweep, so their GETs are never sent (previously only the\n * object/state writes were gated and the answers thrown away). Absent = all enabled.\n */\n isEntryEnabled?(id: string): boolean;\n /**\n * Per-device cache of the AVAIL probe result, held across reconnects and restarts.\n * With a valid cache the probe phase is skipped and the targeted sweep runs directly;\n * a model/firmware mismatch after the sweep invalidates it and re-probes.\n */\n subunitCache?: YncaSubunitCache;\n /**\n * The device's command gate: every line this controller puts on the wire is already\n * paced through it, and its signal is the connection's shutdown flag (a closed gate\n * ends pending waits and stops state writes). Absent in older tests \u2192 no browsing.\n */\n gate?: CommandGate;\n /** Per-device memory for answers that stay constant while the device runs (see ProbeMemory). */\n probeMemory?: ProbeMemory;\n}\n\n/**\n * Drives one YNCA device: connect, init sweep, build the object tree, and route\n * commands both ways. Create-only \u2014 orphan cleanup and legacy migration are\n * separate, gated steps.\n */\nexport class YncaDeviceController implements ConnectionHandle {\n private browseDriver: YncaBrowseDriver | undefined;\n private browseEngine: BrowseEngine | undefined;\n /**\n * Write map filtered to the entries THIS device reported \u2014 claim-with-proof for\n * writes: a command is only sent with a wire function the device answered in the\n * sweep. Until the sweep ran, the unfiltered static map answers.\n */\n private writeMap: Map<string, YncaEntry> | undefined;\n /** The device's scene titles (SCENExNAME), for the recall dropdown, the list state and title writes. */\n private sceneTitles: Array<{ num: number; title: string }> = [];\n /** The tuner's current band (AM/FM/DAB), for the band-dependent frequency/preset writes. */\n private tunerBand = \"\";\n /** Whether the device carries the DAB subunit (its FM half shares the flat tuner ids). */\n private hasDab = false;\n /** The entries THIS device reported \u2014 the per-subunit lookup behind the player routing. */\n private presentEntries: YncaEntry[] = [];\n /** Each zone's currently selected input (INP), for the player routing (v2.0.0). */\n private readonly zoneInputs = new Map<string, string>();\n /** The zones that got a player block (main plus every present ZONEn, when sources exist). */\n private playerZones: string[] = [];\n\n /**\n * @param deviceId the id-safe device id (object-tree path segment)\n * @param deps the client and adapter callbacks\n */\n public constructor(\n private readonly deviceId: string,\n private readonly deps: ControllerDeps,\n ) {}\n\n /**\n * Connect, sweep the device from the catalog, and create its object tree; wire\n * up push updates. The catalog is the single source: it drives the sweep, the\n * device\u2192state read-back and (in handleStateChange) the state\u2192wire encode.\n *\n * @returns true if the device reported capabilities and its tree was created\n */\n public async start(): Promise<boolean> {\n await this.deps.client.connect();\n // Group-filtered catalog: disabled groups are excluded from the sweep AND the objects.\n const catalog = this.deps.isEntryEnabled\n ? YNCA_CATALOG.filter(entry => this.deps.isEntryEnabled!(entry.id))\n : YNCA_CATALOG;\n const resolved = await this.resolveCapabilities(catalog);\n const { capabilities, fromCache } = resolved;\n const present = presentYncaEntries(capabilities, catalog);\n this.writeMap = idToEntry(present);\n this.presentEntries = present;\n // Seed each zone's input from the sweep \u2014 the player routing needs to know what\n // every zone is listening to before the first INP push arrives.\n for (const zone of YNCA_ZONES) {\n const input = capabilities.subunits[zone.subunit]?.INP;\n if (typeof input === \"string\") {\n this.zoneInputs.set(zone.key, input);\n }\n }\n const objects = yncaObjectsFor(capabilities, catalog);\n if (objects.length === 0) {\n this.deps.log.warn(`${this.deviceId}: no capabilities reported \u2014 creating no objects`);\n return false;\n }\n // A user command the device rejects must leave a trace: @RESTRICTED (not allowed /\n // not possible right now) and @UNDEFINED (unknown on this model) were silently\n // dropped before \u2014 the class of invisible failures behind #615.\n this.deps.client.onRefusal?.((command, verdict) =>\n this.deps.log.warn(`${this.deviceId}: device refused \"${command}\" (@${verdict.toUpperCase()})`),\n );\n // The scene titles ride the sweep as SCENExNAME answers; they become the recall\n // dropdown's labels and the one scene.list state (v2.0.0 \u2014 no per-name datapoints).\n const main = capabilities.subunits.MAIN ?? {};\n this.sceneTitles = [];\n for (let n = 1; n <= 12; n++) {\n const title = main[`SCENE${n}NAME`];\n if (typeof title === \"string\" && title.length > 0) {\n this.sceneTitles.push({ num: n, title });\n }\n }\n // Parents before children (channels before their states) \u2014 created in order.\n for (const object of objects) {\n if (object.id === \"scene.recall\" && this.sceneTitles.length > 0) {\n object.common.states = Object.fromEntries(this.sceneTitles.map(scene => [scene.num, scene.title]));\n }\n await this.deps.upsertObject(`${this.deviceId}.${object.id}`, object);\n }\n if (this.sceneTitles.length > 0) {\n await this.deps.upsertObject(`${this.deviceId}.scene.list`, {\n id: \"scene.list\",\n type: \"state\",\n common: { name: \"Scenes (number + title)\", type: \"string\", role: \"json\", read: true, write: false },\n });\n this.deps.setStateAck(`${this.deviceId}.scene.list`, JSON.stringify(this.sceneTitles));\n }\n await this.setupZonePlayers(capabilities, objects);\n // Seed the states with the values read during the init sweep. On the fast path the\n // cached values are last-run leftovers \u2014 the states already hold exactly those, and\n // the background refresh streams the fresh ones in \u2014 so nothing is seeded there.\n if (!fromCache) {\n for (const [subunit, funcs] of Object.entries(capabilities.subunits)) {\n for (const [func, value] of Object.entries(funcs)) {\n const update = yncaStateUpdate({ subunit, func, value }, FUNC_MAP);\n if (update) {\n if (FLAT_PLAYER_ID.test(update.id)) {\n // Player-block values feed only the zones LISTENING to their source\n // (v2.0.0) \u2014 an idle source's leftover metadata must not seed the block.\n this.routePlayerUpdate(subunit, update.id, update.value);\n } else {\n this.deps.setStateAck(`${this.deviceId}.${update.id}`, update.value);\n }\n }\n }\n }\n }\n // The band decides which wire function a tuner.frequency/preset write goes to\n // (v2.0.0 unification) \u2014 seeded from the sweep, kept fresh from the live pushes.\n this.hasDab = capabilities.subunits.DAB !== undefined;\n this.tunerBand = (capabilities.subunits.DAB?.BAND ?? capabilities.subunits.TUN?.BAND ?? \"\").toUpperCase();\n await this.setupBrowse(capabilities);\n this.deps.client.onMessage(message => {\n // The browse driver sees every line first: list lines (LINE1TXT\u2026, LISTINFO\n // bursts, auto-feedback) are not catalogued and would otherwise be dropped.\n this.browseDriver?.handleMessage(message);\n if (message.func === \"BAND\" && (message.subunit === \"TUN\" || message.subunit === \"DAB\")) {\n this.tunerBand = message.value.toUpperCase();\n }\n if (message.func === \"INP\") {\n const zone = YNCA_ZONES.find(z => z.subunit === message.subunit);\n if (zone) {\n this.handleInputSwitch(zone.key, message.value);\n }\n }\n const update = yncaStateUpdate(message, FUNC_MAP);\n if (update) {\n if (FLAT_PLAYER_ID.test(update.id)) {\n this.routePlayerUpdate(message.subunit, update.id, update.value);\n } else {\n this.deps.setStateAck(`${this.deviceId}.${update.id}`, update.value);\n }\n }\n });\n // Start the keepalive only now the (fast-path) init is done; on the slow path the\n // sweep already ran, on the fast path the background refresh paces itself through\n // the same gate, so the 30 s poll cannot break the spacing either way.\n this.deps.client.startKeepalive();\n if (fromCache) {\n // The whole point of the persisted capability layer: the tree stood in ~1 s from\n // the remembered shape (validated by the LIVE identity answer above), and the\n // 15\u201320 s question round now runs behind the ready line as a pure value refresh \u2014\n // its answers stream into the states through the live handler just registered.\n void this.refreshInBackground(catalog);\n }\n // The adapter logs one combined \"ready\" line across all transports; this per-transport line\n // stays at debug for diagnostics.\n this.deps.log.debug(`${this.deviceId}: ${capabilities.model || \"device\"} ready (YNCA)`);\n return true;\n }\n\n /**\n * The device's capabilities \u2014 from the persisted fast-restart layer when the LIVE\n * identity (model + firmware, two paced reads, ~0.2 s) matches what the layer was\n * captured from, else from the full two-pass sweep. The identity read doubles as the\n * liveness proof the ready line rests on: a cached shape alone must never present a\n * dead device as connected (the v1.5.0 honesty rule).\n *\n * @param catalog the (group-filtered) catalog\n * @returns the capabilities and whether they came from the persisted layer\n */\n private async resolveCapabilities(\n catalog: readonly YncaEntry[],\n ): Promise<{ capabilities: YncaCapabilities; fromCache: boolean }> {\n const identity = await this.deps.client.readCapabilities([\n { subunit: \"SYS\", func: \"MODELNAME\" },\n { subunit: \"SYS\", func: \"VERSION\" },\n ]);\n const model = identity.model;\n const firmware = identity.subunits.SYS?.VERSION ?? \"\";\n const remembered = this.deps.probeMemory?.remembered(CAPS_KEY);\n if (model && isCachedCapabilities(remembered) && remembered.model === model && remembered.firmware === firmware) {\n return { capabilities: { model, subunits: remembered.subunits }, fromCache: true };\n }\n if (remembered !== undefined) {\n // A different (or updated) device behind this address: its remembered YNCA\n // answers are void. The other transports guard their own portions.\n this.deps.probeMemory?.drop(key => key === CAPS_KEY || key === STATIC_KEY);\n }\n const capabilities = await this.sweepDevice(catalog, model, firmware);\n if (capabilities.model) {\n this.deps.probeMemory?.set(CAPS_KEY, {\n model: capabilities.model,\n firmware: capabilities.subunits.SYS?.VERSION ?? firmware,\n subunits: capabilities.subunits,\n } satisfies CachedCapabilities);\n }\n return { capabilities, fromCache: false };\n }\n\n /**\n * The fast path's second half: re-ask every catalogued function of the present\n * subunits \u2014 the answers stream into the states through the live message handler,\n * so current values arrive within the usual sweep time WITHOUT having gated the\n * ready line. Completion refreshes the persisted layers (capabilities, statics)\n * and the write map; a SHAPE change (a function newly answered) is only persisted \u2014\n * its object appears on the next start, because the unified tree is coordinated\n * once per connect and a late upsert would not materialize.\n *\n * @param catalog the (group-filtered) catalog\n */\n private async refreshInBackground(catalog: readonly YncaEntry[]): Promise<void> {\n try {\n const cached = this.deps.subunitCache?.get();\n const gets = sweepGets(catalog).filter(\n get => get.subunit === \"SYS\" || !cached || cached.subunits.includes(get.subunit),\n );\n const fresh = await this.deps.client.readCapabilities(gets);\n if (!fresh.model) {\n // The refresh ran into a drop \u2014 the supervisor handles the reconnect.\n return;\n }\n const statics: Record<string, Record<string, string>> = {};\n for (const [subunit, funcs] of Object.entries(fresh.subunits)) {\n for (const [func, value] of Object.entries(funcs)) {\n if (STATIC_FUNC.test(func)) {\n (statics[subunit] ??= {})[func] = value;\n }\n }\n }\n this.deps.probeMemory?.set(STATIC_KEY, statics);\n this.deps.probeMemory?.set(CAPS_KEY, {\n model: fresh.model,\n firmware: fresh.subunits.SYS?.VERSION ?? \"\",\n subunits: fresh.subunits,\n } satisfies CachedCapabilities);\n this.presentEntries = presentYncaEntries(fresh, catalog);\n this.writeMap = idToEntry(this.presentEntries);\n this.deps.log.debug(`${this.deviceId}: background value refresh done (YNCA)`);\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: background value refresh failed: ${String(e)}`);\n }\n }\n\n /**\n * Read the device's capabilities with the two-pass sweep. Pass 1 probes each\n * catalogued subunit with `AVAIL=?` (~2 s); pass 2 sweeps only the subunits that\n * answered, plus SYS (which never answers AVAIL) \u2014 on a typical receiver that\n * saves a third or more of the ~39 s blind sweep. A cached probe result (per\n * device, surviving reconnects and restarts) skips pass 1 entirely; a device\n * whose model or firmware no longer matches the cache re-probes. A device that\n * answers no AVAIL at all falls back to the full blind sweep, so an unknown\n * firmware loses speed, never features.\n *\n * @param catalog the (group-filtered) catalog whose functions to sweep\n * @param model the live-read SYS model name (from resolveCapabilities)\n * @param firmware the live-read SYS firmware version\n * @returns the assembled capabilities\n */\n private async sweepDevice(catalog: readonly YncaEntry[], model: string, firmware: string): Promise<YncaCapabilities> {\n const cached = this.deps.subunitCache?.get();\n if (cached) {\n // The device's IDENTITY was already read by resolveCapabilities (two reads,\n // ~0.2 s) \u2014 checking it BEFORE sweeping is what keeps a stale cache from costing\n // a full targeted sweep, then the probe, then a second sweep (~40 s).\n if (model === cached.model && firmware === cached.firmware) {\n return await this.targetedSweep(catalog, new Set(cached.subunits));\n }\n // The device behind this IP changed (swap or firmware update) \u2014 re-probe.\n this.deps.log.debug(`${this.deviceId}: cached subunit set is stale (model/firmware changed), re-probing`);\n this.deps.subunitCache?.clear();\n }\n const probe = await this.deps.client.readCapabilities(AVAIL_PROBE);\n const present = new Set(Object.keys(probe.subunits));\n if (present.size === 0) {\n // Device ignores AVAIL \u2014 sweep blind so no function is lost.\n return await this.deps.client.readCapabilities(sweepGets(catalog));\n }\n const capabilities = await this.targetedSweep(catalog, present);\n if (capabilities.model) {\n this.deps.subunitCache?.set({\n subunits: [...present],\n model: capabilities.model,\n firmware: capabilities.subunits.SYS?.VERSION ?? \"\",\n });\n }\n return capabilities;\n }\n\n /**\n * Sweep only the present subunits' functions (SYS always included \u2014 it answers no\n * AVAIL but carries model/firmware/master power).\n *\n * @param catalog the (group-filtered) catalog whose functions to sweep\n * @param present the subunits that answered the AVAIL probe\n * @returns the assembled capabilities\n */\n private async targetedSweep(catalog: readonly YncaEntry[], present: ReadonlySet<string>): Promise<YncaCapabilities> {\n const gets = sweepGets(catalog).filter(get => get.subunit === \"SYS\" || present.has(get.subunit));\n const remembered = this.deps.probeMemory?.remembered<Record<string, Record<string, string>>>(STATIC_KEY);\n // Second connect onwards: skip those reads and put the remembered answers back in, so\n // the objects are built exactly as if the device had answered them again.\n const capabilities = await this.deps.client.readCapabilities(\n remembered ? gets.filter(get => !STATIC_FUNC.test(get.func)) : gets,\n );\n if (remembered) {\n for (const [subunit, funcs] of Object.entries(remembered)) {\n capabilities.subunits[subunit] = { ...funcs, ...capabilities.subunits[subunit] };\n }\n return capabilities;\n }\n const statics: Record<string, Record<string, string>> = {};\n for (const [subunit, funcs] of Object.entries(capabilities.subunits)) {\n for (const [func, value] of Object.entries(funcs)) {\n if (STATIC_FUNC.test(func)) {\n (statics[subunit] ??= {})[func] = value;\n }\n }\n }\n this.deps.probeMemory?.set(STATIC_KEY, statics);\n return capabilities;\n }\n\n /**\n * Handle a state change: a user write (ack false) becomes a YNCA command; an\n * acked change (the device's own echo) is ignored to avoid a resend loop.\n *\n * @param fullStateId the full state id (device id + \".\" + state)\n * @param ack whether the change is acked (device-originated)\n * @param value the new value\n */\n public handleStateChange(fullStateId: string, ack: boolean, value: unknown): void {\n if (ack) {\n return;\n }\n const prefix = `${this.deviceId}.`;\n if (!fullStateId.startsWith(prefix)) {\n return;\n }\n const stateId = fullStateId.slice(prefix.length);\n if (stateId.startsWith(\"player.browse.\")) {\n this.browseEngine?.handleWrite(stateId, value);\n return;\n }\n // A scene TITLE is as valid a recall write as its number (\"Movie Viewing\" \u2192 1).\n if (stateId === \"scene.recall\" && typeof value === \"string\" && !/^\\d+$/.test(value.trim())) {\n const needle = value.trim().toLowerCase();\n const match = this.sceneTitles.find(scene => scene.title.toLowerCase() === needle);\n if (match === undefined) {\n return;\n }\n value = match.num;\n }\n // The unified player writes go to the subunit the ZONE is listening to (v2.0.0) \u2014\n // routed here, BEFORE the generic path.\n const playerWrite = /^(?:multiroom\\.(zone[234])\\.)?player\\.(playback|repeat|shuffle|next|prev)$/.exec(stateId);\n if (playerWrite) {\n this.handlePlayerWrite(playerWrite[1] ?? \"main\", `player.${playerWrite[2]}`, value);\n return;\n }\n // The unified tuner writes are band-dependent (v2.0.0) and routed here, BEFORE the\n // generic path \u2014 one state, the right wire function for the active band.\n if (this.handleTunerWrite(stateId, value)) {\n return;\n }\n const triple = yncaCommand(stateId, value, this.writeMap ?? ID_MAP);\n if (triple) {\n this.deps.client.send(triple.subunit, triple.func, triple.value);\n }\n }\n\n /**\n * Create the per-zone player mirrors (v2.0.0): every present ZONEn gets its own\n * \"now playing\" block under its multiroom folder \u2014 zones can play different\n * sources, one shared block could not show that. The defs are clones of the flat\n * block the catalog built for the main zone, plus the `source` display each zone\n * (main included) gets.\n *\n * @param capabilities the device's swept capabilities\n * @param objects the main tree's object definitions (source of the block's shape)\n */\n private async setupZonePlayers(capabilities: YncaCapabilities, objects: ObjectDef[]): Promise<void> {\n const playerObjects = objects.filter(\n object => object.id === \"player\" || (object.type === \"state\" && FLAT_PLAYER_ID.test(object.id)),\n );\n if (!playerObjects.some(object => object.type === \"state\")) {\n this.playerZones = [];\n return;\n }\n const sourceDef = (id: string): ObjectDef => ({\n id,\n type: \"state\",\n common: { name: \"Playing source\", type: \"string\", role: \"text\", read: true, write: false },\n });\n await this.deps.upsertObject(`${this.deviceId}.player.source`, sourceDef(\"player.source\"));\n this.playerZones = [\"main\"];\n for (const zone of YNCA_ZONES) {\n if (zone.key === \"main\" || capabilities.subunits[zone.subunit] === undefined) {\n continue;\n }\n this.playerZones.push(zone.key);\n for (const object of playerObjects) {\n const id = `${zone.prefix}${object.id}`;\n await this.deps.upsertObject(`${this.deviceId}.${id}`, { ...object, id });\n }\n await this.deps.upsertObject(\n `${this.deviceId}.${zone.prefix}player.source`,\n sourceDef(`${zone.prefix}player.source`),\n );\n }\n // Seed the source display: a device already playing at adapter start must not\n // show an empty source until its first input switch (2.0.0 review finding \u2014 the\n // sweep seeds the metadata, but `player.source` is controller-derived).\n for (const zone of YNCA_ZONES) {\n if (this.playerZones.includes(zone.key)) {\n const input = this.zoneInputs.get(zone.key);\n const playing = playerSubunitForInput(input) !== undefined;\n this.deps.setStateAck(\n `${this.deviceId}.${zone.prefix}player.source`,\n playing && input !== undefined ? input : \"\",\n );\n }\n }\n }\n\n /**\n * Feed one player-subunit value into the block of every zone listening to that\n * source \u2014 and only those: an idle source's answer (the sweep reads them all)\n * must not overwrite what the active source shows.\n *\n * @param subunit the source subunit the value came from\n * @param id the flat player state id\n * @param value the decoded value\n */\n private routePlayerUpdate(subunit: string, id: string, value: boolean | number | string): void {\n for (const zone of YNCA_ZONES) {\n if (!this.playerZones.includes(zone.key)) {\n continue;\n }\n if (playerSubunitForInput(this.zoneInputs.get(zone.key)) === subunit) {\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}${id}`, value);\n }\n }\n }\n\n /**\n * Track a zone's input switch: remember the input, and when the zone changed its\n * player source, clear the block (stale metadata must not linger) and ask the new\n * source for its current state \u2014 YNCA pushes changes, but a source that was already\n * playing has nothing new to push.\n *\n * @param zoneKey the zone (`main`, `zone2`, \u2026)\n * @param input the new INP value\n */\n private handleInputSwitch(zoneKey: string, input: string): void {\n const before = playerSubunitForInput(this.zoneInputs.get(zoneKey));\n this.zoneInputs.set(zoneKey, input);\n const after = playerSubunitForInput(input);\n if (before === after || !this.playerZones.includes(zoneKey)) {\n return;\n }\n const zone = YNCA_ZONES.find(z => z.key === zoneKey);\n if (!zone) {\n return;\n }\n const presentFlat = new Set(\n this.presentEntries.filter(entry => FLAT_PLAYER_ID.test(entry.id)).map(entry => entry.id),\n );\n for (const clear of YNCA_PLAYER_CLEAR) {\n if (presentFlat.has(clear.id)) {\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}${clear.id}`, clear.value);\n }\n }\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}player.source`, after === undefined ? \"\" : input);\n if (after !== undefined) {\n // Fresh reads for the newly selected source, streamed back through the live handler.\n const funcs = new Set<string>();\n for (const entry of this.presentEntries) {\n if (entry.subunit === after && FLAT_PLAYER_ID.test(entry.id) && !entry.writeOnly) {\n funcs.add(entry.readFunc ?? entry.func);\n }\n }\n for (const func of funcs) {\n this.deps.client.get(after, func);\n }\n }\n }\n\n /**\n * Route a unified player write (playback/repeat/shuffle/next/prev) to the source\n * subunit the ZONE is listening to \u2014 with the entry that subunit itself reported\n * (claim-with-proof, like every other write).\n *\n * @param zoneKey the zone the write belongs to\n * @param flatId the flat player state id\n * @param value the written value\n */\n private handlePlayerWrite(zoneKey: string, flatId: string, value: unknown): void {\n const subunit = playerSubunitForInput(this.zoneInputs.get(zoneKey));\n if (subunit === undefined) {\n this.deps.log.debug(`${this.deviceId}: ${flatId} ignored \u2014 ${zoneKey} is not playing a media source`);\n return;\n }\n const entry = this.presentEntries.find(e => e.id === flatId && e.subunit === subunit);\n if (entry === undefined) {\n this.deps.log.debug(`${this.deviceId}: ${flatId} ignored \u2014 ${subunit} did not report it`);\n return;\n }\n const triple = yncaCommand(flatId, value, new Map([[flatId, entry]]));\n if (triple) {\n this.deps.client.send(triple.subunit, triple.func, triple.value);\n }\n }\n\n /**\n * Route the band-dependent tuner writes (v2.0.0 unification): ONE frequency state\n * in kHz and ONE preset state, sent to the wire function of the ACTIVE band \u2014\n * AM/FM on the classic TUN subunit, FM/DAB on the DAB subunit (whose FM half\n * shares the flat ids). A DAB frequency write is dropped: DAB tunes by service,\n * the device has no frequency command there.\n *\n * @param stateId the state id relative to the device\n * @param value the written value\n * @returns true when the id was a band-routed tuner write (handled here)\n */\n private handleTunerWrite(stateId: string, value: unknown): boolean {\n if (stateId === \"tuner.frequency\") {\n const khz = Number(value);\n if (!Number.isFinite(khz)) {\n return true;\n }\n if (this.hasDab) {\n if (this.tunerBand === \"FM\") {\n this.deps.client.send(\"DAB\", \"FMFREQ\", formatWireNumber(khz / 1000, 2));\n } else {\n this.deps.log.debug(`${this.deviceId}: DAB tunes by service \u2014 frequency write ignored`);\n }\n return true;\n }\n if (this.tunerBand === \"AM\") {\n this.deps.client.send(\"TUN\", \"AMFREQ\", String(Math.round(khz)));\n } else {\n this.deps.client.send(\"TUN\", \"FMFREQ\", formatWireNumber(khz / 1000, 2));\n }\n return true;\n }\n if (stateId === \"tuner.preset\" && this.hasDab) {\n const slot = Math.round(Number(value));\n if (Number.isFinite(slot) && slot >= 1) {\n this.deps.client.send(\"DAB\", this.tunerBand === \"DAB\" ? \"DABPRESET\" : \"FMPRESET\", String(slot));\n }\n return true;\n }\n return false;\n }\n\n /**\n * Create the browsing surface (#613) when the device reports a browsable media\n * subunit: the official YNCA list vocabulary (LISTINFO/LISTSEL/LISTPAGE/LISTCURSOR)\n * drives an 8-line window under `player.browse.*`. Skipped without a delay dep\n * (older tests) and when the playback group is switched off.\n *\n * @param capabilities the device's swept capabilities\n */\n private async setupBrowse(capabilities: YncaCapabilities): Promise<void> {\n const gate = this.deps.gate;\n if (!gate || this.deps.isEntryEnabled?.(\"player.browse.source\") === false) {\n return;\n }\n const delay = (ms: number): Promise<void> => gate.delay(ms);\n const present = await this.probeBrowseSubunits(capabilities);\n if (present.size === 0) {\n // Leaving the states uncreated is what hands browsing to another transport: the owner\n // policy ranks by modernity (yxc > ynca > xml), so an unproven YNCA claim would beat a\n // PROVEN xml one and the user would get an empty menu on a device that can browse over\n // XML \u2014 exactly issue #613's RX-V473.\n this.deps.log.debug(`${this.deviceId}: no YNCA source answers LISTINFO \u2014 leaving menus to another transport`);\n return;\n }\n const driver = new YncaBrowseDriver(this.deps.client, present, delay);\n this.browseEngine = await createBrowseSurface(driver, this.deviceId, {\n upsertObject: this.deps.upsertObject,\n emit: (id, value) => this.deps.setStateAck(`${this.deviceId}.${id}`, value),\n log: this.deps.log,\n delay,\n });\n if (this.browseEngine) {\n this.browseDriver = driver;\n }\n }\n\n /**\n * Which browsable subunits actually SERVE menus, proven by asking them.\n *\n * Carrying the subunit is NOT proof: the RX-A810 reference log answers `@SERVER:LISTINFO=?`\n * with `@UNDEFINED` while NETRADIO/PC/USB on the very same device return a full window. The\n * XML driver has always probed (`List_Info` \u2192 `<Menu_Status>`); YNCA claimed the states on\n * presence alone and, ranking higher, silently displaced the transport that could deliver.\n *\n * @param capabilities the device's swept capabilities\n * @returns the subunits that answered with list data\n */\n private async probeBrowseSubunits(capabilities: YncaCapabilities): Promise<ReadonlySet<string>> {\n const candidates = YNCA_BROWSE_SOURCES.filter(source => source.subunit in capabilities.subunits);\n if (candidates.length === 0) {\n return new Set();\n }\n // A receiver in standby answers @RESTRICTED for its media subunits, which is\n // indistinguishable from \"cannot browse\" and would strip the menus off a device that\n // serves them perfectly once it is on. Nobody browses a sleeping receiver, so keep the\n // claim and let the next connect \u2014 with the device awake \u2014 do the real probe.\n if (capabilities.subunits.MAIN?.PWR !== \"On\") {\n return new Set(candidates.map(source => source.subunit));\n }\n const answer = await this.deps.client.readCapabilities(\n candidates.map(source => ({ subunit: source.subunit, func: \"LISTINFO\" })),\n );\n // Only a real list answer counts. Both refusals \u2014 `@UNDEFINED` (function unknown) and\n // `@RESTRICTED` (source not usable right now) \u2014 carry no subunit, so they cannot be\n // attributed to one request; it is the ABSENCE of an answer that excludes a subunit.\n return new Set(\n candidates\n .map(source => source.subunit)\n .filter(subunit => Object.keys(answer.subunits[subunit] ?? {}).some(func => LIST_PROOF.test(func))),\n );\n }\n\n /**\n * Register the supervisor's drop handler \u2014 delegated to the client's socket drop,\n * which is YNCA's genuine connection-lost signal.\n *\n * @param cb invoked once when the connection drops, with the reason if known\n */\n public onDrop(cb: (reason?: Error) => void): void {\n this.deps.client.onDrop(cb);\n }\n\n /** Close the client. Synchronous \u2014 safe to call from onUnload. */\n public close(): void {\n this.browseEngine?.close();\n this.browseDriver?.close();\n this.deps.client.close();\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { YncaCapabilities } from \"./ynca/capability\";\nimport { formatWireNumber } from \"./catalog/value-coerce\";\nimport type { ObjectDef } from \"./catalog/types\";\nimport type { ConnectionHandle, ControllerLog } from \"./controller\";\nimport {\n YNCA_CATALOG,\n availGets,\n funcToEntry,\n idToEntry,\n presentYncaEntries,\n sweepGets,\n yncaCommand,\n yncaObjectsFor,\n yncaStateUpdate,\n type YncaEntry,\n} from \"./ynca/catalog\";\nimport type { YncaSubunitCache } from \"./ynca/subunit-cache\";\nimport type { CommandGate } from \"./lifecycle/command-gate\";\nimport type { ProbeMemory } from \"./lifecycle/probe-memory\";\nimport type { BrowseEngine } from \"./browse/browse-engine\";\nimport { createBrowseSurface } from \"./browse/surface\";\nimport { YNCA_BROWSE_SOURCES, YncaBrowseDriver } from \"./browse/ynca-browse-driver\";\n\n// The YNCA catalog and its lookup maps are static \u2014 built once for all devices.\n// SYS:MODELNAME is part of the catalog (info.model), so the sweep already covers it.\n// The AVAIL probe always covers the FULL catalog (not the group-filtered one), so the\n// cached subunit set reflects the device, never the current group configuration.\nconst FUNC_MAP = funcToEntry(YNCA_CATALOG);\nconst ID_MAP = idToEntry(YNCA_CATALOG);\nconst AVAIL_PROBE = availGets(YNCA_CATALOG);\n\n/**\n * Functions whose VALUE cannot change while the device runs: the 23 assignable input names\n * and the 12 scene names. They cost 35 of the ~187 paced reads of a targeted sweep (3.5 s\n * at the specification's mandatory 100 ms spacing) and answer the same thing every time, so\n * a reconnect reuses what the first connect learned. Deliberately per adapter RUN, not\n * persisted: renaming an input at the receiver shows up after the next adapter restart\n * rather than needing anyone to invalidate a stored file.\n */\nconst STATIC_FUNC = /^(INPNAME|SCENE\\d+NAME$)/;\n\n/** The zones whose player/input surface the controller routes (v2.0.0 player unification). */\nconst YNCA_ZONES: Array<{ key: string; subunit: string; prefix: string }> = [\n { key: \"main\", subunit: \"MAIN\", prefix: \"\" },\n { key: \"zone2\", subunit: \"ZONE2\", prefix: \"multiroom.zone2.\" },\n { key: \"zone3\", subunit: \"ZONE3\", prefix: \"multiroom.zone3.\" },\n { key: \"zone4\", subunit: \"ZONE4\", prefix: \"multiroom.zone4.\" },\n];\n\n/**\n * INP value \u2192 the player subunit it selects (normalized: uppercase, alphanumerics only).\n * The wire values come from the RX-V6A INPNAME/INP capture and ynca-python's input map;\n * inputs that are no media player (HDMI, AV, TUNER, \u2026) deliberately map to nothing.\n */\nconst INPUT_SUBUNITS: Record<string, string> = {\n NETRADIO: \"NETRADIO\",\n SERVER: \"SERVER\",\n USB: \"USB\",\n SPOTIFY: \"SPOTIFY\",\n DEEZER: \"DEEZER\",\n TIDAL: \"TIDAL\",\n NAPSTER: \"NAPSTER\",\n PANDORA: \"PANDORA\",\n RHAPSODY: \"RHAP\",\n SIRIUS: \"SIRIUS\",\n SIRIUSXM: \"SIRIUS\",\n SIRIUSIR: \"SIRIUS\",\n AIRPLAY: \"AIRPLAY\",\n BLUETOOTH: \"BT\",\n PC: \"PC\",\n MUSICCASTLINK: \"MCLINK\",\n IPOD: \"IPOD\",\n IPODUSB: \"IPODUSB\",\n};\n\n/**\n * The player subunit an input selection feeds, or undefined when the input is no\n * media player.\n *\n * @param input the zone's INP value (e.g. \"NET RADIO\", \"iPod (USB)\")\n * @returns the subunit (e.g. NETRADIO), or undefined\n */\nfunction playerSubunitForInput(input: string | undefined): string | undefined {\n if (typeof input !== \"string\" || input.length === 0) {\n return undefined;\n }\n return INPUT_SUBUNITS[input.toUpperCase().replace(/[^A-Z0-9]/g, \"\")];\n}\n\n/**\n * A state of the flat per-zone player block: exactly one segment below `player.`.\n * ONLY these are zone-routed \u2014 multi-segment `player.*` ids (bluetooth pairing\n * status, airplay volume interlock, per-source presets) are device-global and go\n * straight to their state (2.0.0 review finding: the broad `player.` prefix check\n * silently dropped BT/AirPlay status while no zone listened to that source).\n */\nconst FLAT_PLAYER_ID = /^player\\.[^.]+$/;\n\n/**\n * What a zone's player block is reset to when the zone leaves its playing source\n * (clear-on-switch): metadata empty, playback Stop. Filtered to the states the\n * device actually has before emitting.\n */\nconst YNCA_PLAYER_CLEAR: Array<{ id: string; value: number | string | boolean }> = [\n { id: \"player.playback\", value: 1 },\n { id: \"player.artist\", value: \"\" },\n { id: \"player.album\", value: \"\" },\n { id: \"player.track\", value: \"\" },\n { id: \"player.station\", value: \"\" },\n { id: \"player.channelName\", value: \"\" },\n { id: \"player.elapsedTime\", value: \"\" },\n { id: \"player.totalTime\", value: \"\" },\n { id: \"player.repeat\", value: 0 },\n { id: \"player.shuffle\", value: false },\n];\n\n/** Memory key for the remembered static values. */\nconst STATIC_KEY = \"yncaStaticValues\";\n\n/** Memory key for the persisted capability shape (the fast-restart layer). */\nconst CAPS_KEY = \"yncaCapabilities\";\n\n/** The persisted capability shape, keyed by the device identity that validated it. */\ninterface CachedCapabilities {\n /** SYS MODELNAME at capture time \u2014 freshness key half 1. */\n model: string;\n /** SYS VERSION at capture time \u2014 freshness key half 2. */\n firmware: string;\n /** The captured subunit\u2192function map (values are last-known, used for SHAPE only). */\n subunits: Record<string, Record<string, string>>;\n}\n\n/**\n * Whether a remembered value carries the cached-capabilities shape (API boundary \u2014\n * the persisted probe memory is untrusted storage).\n *\n * @param value the remembered value\n * @returns true when usable\n */\nfunction isCachedCapabilities(value: unknown): value is CachedCapabilities {\n const candidate = value as Partial<CachedCapabilities> | null;\n return (\n typeof candidate === \"object\" &&\n candidate !== null &&\n typeof candidate.model === \"string\" &&\n typeof candidate.firmware === \"string\" &&\n typeof candidate.subunits === \"object\" &&\n candidate.subunits !== null\n );\n}\n\n/**\n * The functions whose presence proves a subunit really serves menus \u2014 the fields a real\n * `LISTINFO=?` answer is made of (RX-A810 reference log). See {@link YncaDeviceController.probeBrowseSubunits}.\n */\nconst LIST_PROOF = /^(LISTLAYER|LISTLAYERNAME|CURRLINE|MAXLINE|LINE[1-8](TXT|ATRIB))$/;\n\n/** The subset of the YNCA client the controller uses (so tests can inject a fake). */\nexport interface YncaClientLike {\n /** Open the connection. */\n connect(): Promise<void>;\n /** Run the init sweep and return the device's capabilities. */\n readCapabilities(gets: Array<{ subunit: string; func: string }>): Promise<YncaCapabilities>;\n /** Send a PUT command. */\n send(subunit: string, func: string, value: string): void;\n /** Send a GET request (the browse driver reads LISTINFO with it). */\n get(subunit: string, func: string): void;\n /** Register a handler for pushed messages. */\n onMessage(handler: (message: { subunit: string; func: string; value: string }) => void): void;\n /** Register the socket-drop handler the supervisor reconnects on. */\n onDrop(handler: (reason?: Error) => void): void;\n /** Register the refusal handler for user commands the device rejects (optional in older tests). */\n onRefusal?(handler: (command: string, verdict: \"restricted\" | \"undefined\") => void): void;\n /** Start the keepalive poll \u2014 called after the init sweep, not on connect. */\n startKeepalive(): void;\n /** Close the connection synchronously. */\n close(): void;\n}\n\nexport type { ControllerLog };\n\n/** The adapter callbacks the controller drives \u2014 narrow, so no adapter mock is needed in tests. */\nexport interface ControllerDeps {\n /** The YNCA client for this device. */\n client: YncaClientLike;\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 log. */\n log: ControllerLog;\n /**\n * Whether a catalog entry's datapoint group is enabled. A disabled group's entries\n * are dropped BEFORE the sweep, so their GETs are never sent (previously only the\n * object/state writes were gated and the answers thrown away). Absent = all enabled.\n */\n isEntryEnabled?(id: string): boolean;\n /**\n * Per-device cache of the AVAIL probe result, held across reconnects and restarts.\n * With a valid cache the probe phase is skipped and the targeted sweep runs directly;\n * a model/firmware mismatch after the sweep invalidates it and re-probes.\n */\n subunitCache?: YncaSubunitCache;\n /**\n * The device's command gate: every line this controller puts on the wire is already\n * paced through it, and its signal is the connection's shutdown flag (a closed gate\n * ends pending waits and stops state writes). Absent in older tests \u2192 no browsing.\n */\n gate?: CommandGate;\n /** Per-device memory for answers that stay constant while the device runs (see ProbeMemory). */\n probeMemory?: ProbeMemory;\n}\n\n/**\n * Drives one YNCA device: connect, init sweep, build the object tree, and route\n * commands both ways. Create-only \u2014 orphan cleanup and legacy migration are\n * separate, gated steps.\n */\nexport class YncaDeviceController implements ConnectionHandle {\n private browseDriver: YncaBrowseDriver | undefined;\n private browseEngine: BrowseEngine | undefined;\n /**\n * Write map filtered to the entries THIS device reported \u2014 claim-with-proof for\n * writes: a command is only sent with a wire function the device answered in the\n * sweep. Until the sweep ran, the unfiltered static map answers.\n */\n private writeMap: Map<string, YncaEntry> | undefined;\n /** The device's scene titles (SCENExNAME), for the recall dropdown, the list state and title writes. */\n private sceneTitles: Array<{ num: number; title: string }> = [];\n /** The tuner's current band (AM/FM/DAB), for the band-dependent frequency/preset writes. */\n private tunerBand = \"\";\n /** Whether the device carries the DAB subunit (its FM half shares the flat tuner ids). */\n private hasDab = false;\n /** The entries THIS device reported \u2014 the per-subunit lookup behind the player routing. */\n private presentEntries: YncaEntry[] = [];\n /** Each zone's currently selected input (INP), for the player routing (v2.0.0). */\n private readonly zoneInputs = new Map<string, string>();\n /** The zones that got a player block (main plus every present ZONEn, when sources exist). */\n private playerZones: string[] = [];\n\n /**\n * @param deviceId the id-safe device id (object-tree path segment)\n * @param deps the client and adapter callbacks\n */\n public constructor(\n private readonly deviceId: string,\n private readonly deps: ControllerDeps,\n ) {}\n\n /**\n * Connect, sweep the device from the catalog, and create its object tree; wire\n * up push updates. The catalog is the single source: it drives the sweep, the\n * device\u2192state read-back and (in handleStateChange) the state\u2192wire encode.\n *\n * @returns true if the device reported capabilities and its tree was created\n */\n public async start(): Promise<boolean> {\n await this.deps.client.connect();\n // Group-filtered catalog: disabled groups are excluded from the sweep AND the objects.\n const catalog = this.deps.isEntryEnabled\n ? YNCA_CATALOG.filter(entry => this.deps.isEntryEnabled!(entry.id))\n : YNCA_CATALOG;\n const resolved = await this.resolveCapabilities(catalog);\n const { capabilities, fromCache } = resolved;\n const present = presentYncaEntries(capabilities, catalog);\n this.writeMap = idToEntry(present);\n this.presentEntries = present;\n // Seed each zone's input from the sweep \u2014 the player routing needs to know what\n // every zone is listening to before the first INP push arrives.\n for (const zone of YNCA_ZONES) {\n const input = capabilities.subunits[zone.subunit]?.INP;\n if (typeof input === \"string\") {\n this.zoneInputs.set(zone.key, input);\n }\n }\n const objects = yncaObjectsFor(capabilities, catalog);\n if (objects.length === 0) {\n this.deps.log.warn(`${this.deviceId}: no capabilities reported \u2014 creating no objects`);\n return false;\n }\n // A user command the device rejects must leave a trace: @RESTRICTED (not allowed /\n // not possible right now) and @UNDEFINED (unknown on this model) were silently\n // dropped before \u2014 the class of invisible failures behind #615.\n this.deps.client.onRefusal?.((command, verdict) =>\n this.deps.log.warn(`${this.deviceId}: device refused \"${command}\" (@${verdict.toUpperCase()})`),\n );\n // The scene titles ride the sweep as SCENExNAME answers; they become the recall\n // dropdown's labels and the one scene.list state (v2.0.0 \u2014 no per-name datapoints).\n const main = capabilities.subunits.MAIN ?? {};\n this.sceneTitles = [];\n for (let n = 1; n <= 12; n++) {\n const title = main[`SCENE${n}NAME`];\n if (typeof title === \"string\" && title.length > 0) {\n this.sceneTitles.push({ num: n, title });\n }\n }\n // Parents before children (channels before their states) \u2014 created in order.\n for (const object of objects) {\n if (object.id === \"scene.recall\" && this.sceneTitles.length > 0) {\n object.common.states = Object.fromEntries(this.sceneTitles.map(scene => [scene.num, scene.title]));\n }\n await this.deps.upsertObject(`${this.deviceId}.${object.id}`, object);\n }\n if (this.sceneTitles.length > 0) {\n await this.deps.upsertObject(`${this.deviceId}.scene.list`, {\n id: \"scene.list\",\n type: \"state\",\n common: { name: \"Scenes (number + title)\", type: \"string\", role: \"json\", read: true, write: false },\n });\n this.deps.setStateAck(`${this.deviceId}.scene.list`, JSON.stringify(this.sceneTitles));\n }\n await this.setupZonePlayers(capabilities, objects);\n // Seed the states with the values read during the init sweep. On the fast path the\n // cached values are last-run leftovers \u2014 the states already hold exactly those, and\n // the background refresh streams the fresh ones in \u2014 so nothing is seeded there.\n if (!fromCache) {\n for (const [subunit, funcs] of Object.entries(capabilities.subunits)) {\n for (const [func, value] of Object.entries(funcs)) {\n const update = yncaStateUpdate({ subunit, func, value }, FUNC_MAP);\n if (update) {\n if (FLAT_PLAYER_ID.test(update.id)) {\n // Player-block values feed only the zones LISTENING to their source\n // (v2.0.0) \u2014 an idle source's leftover metadata must not seed the block.\n this.routePlayerUpdate(subunit, update.id, update.value);\n } else {\n this.deps.setStateAck(`${this.deviceId}.${update.id}`, update.value);\n }\n }\n }\n }\n }\n // The band decides which wire function a tuner.frequency/preset write goes to\n // (v2.0.0 unification) \u2014 seeded from the sweep, kept fresh from the live pushes.\n this.hasDab = capabilities.subunits.DAB !== undefined;\n this.tunerBand = (capabilities.subunits.DAB?.BAND ?? capabilities.subunits.TUN?.BAND ?? \"\").toUpperCase();\n await this.setupBrowse(capabilities);\n this.deps.client.onMessage(message => {\n // The browse driver sees every line first: list lines (LINE1TXT\u2026, LISTINFO\n // bursts, auto-feedback) are not catalogued and would otherwise be dropped.\n this.browseDriver?.handleMessage(message);\n if (message.func === \"BAND\" && (message.subunit === \"TUN\" || message.subunit === \"DAB\")) {\n this.tunerBand = message.value.toUpperCase();\n }\n if (message.func === \"INP\") {\n const zone = YNCA_ZONES.find(z => z.subunit === message.subunit);\n if (zone) {\n this.handleInputSwitch(zone.key, message.value);\n }\n }\n const update = yncaStateUpdate(message, FUNC_MAP);\n if (update) {\n if (FLAT_PLAYER_ID.test(update.id)) {\n this.routePlayerUpdate(message.subunit, update.id, update.value);\n } else {\n this.deps.setStateAck(`${this.deviceId}.${update.id}`, update.value);\n }\n }\n });\n // Start the keepalive only now the (fast-path) init is done; on the slow path the\n // sweep already ran, on the fast path the background refresh paces itself through\n // the same gate, so the 30 s poll cannot break the spacing either way.\n this.deps.client.startKeepalive();\n if (fromCache) {\n // The whole point of the persisted capability layer: the tree stood in ~1 s from\n // the remembered shape (validated by the LIVE identity answer above), and the\n // 15\u201320 s question round now runs behind the ready line as a pure value refresh \u2014\n // its answers stream into the states through the live handler just registered.\n void this.refreshInBackground(catalog);\n }\n // The adapter logs one combined \"ready\" line across all transports; this per-transport line\n // stays at debug for diagnostics.\n this.deps.log.debug(`${this.deviceId}: ${capabilities.model || \"device\"} ready (YNCA)`);\n return true;\n }\n\n /**\n * The device's capabilities \u2014 from the persisted fast-restart layer when the LIVE\n * identity (model + firmware, two paced reads, ~0.2 s) matches what the layer was\n * captured from, else from the full two-pass sweep. The identity read doubles as the\n * liveness proof the ready line rests on: a cached shape alone must never present a\n * dead device as connected (the v1.5.0 honesty rule).\n *\n * @param catalog the (group-filtered) catalog\n * @returns the capabilities and whether they came from the persisted layer\n */\n private async resolveCapabilities(\n catalog: readonly YncaEntry[],\n ): Promise<{ capabilities: YncaCapabilities; fromCache: boolean }> {\n const identity = await this.deps.client.readCapabilities([\n { subunit: \"SYS\", func: \"MODELNAME\" },\n { subunit: \"SYS\", func: \"VERSION\" },\n ]);\n const model = identity.model;\n const firmware = identity.subunits.SYS?.VERSION ?? \"\";\n const remembered = this.deps.probeMemory?.remembered(CAPS_KEY);\n if (model && isCachedCapabilities(remembered) && remembered.model === model && remembered.firmware === firmware) {\n return { capabilities: { model, subunits: remembered.subunits }, fromCache: true };\n }\n if (remembered !== undefined) {\n // A different (or updated) device behind this address: its remembered YNCA\n // answers are void. The other transports guard their own portions.\n this.deps.probeMemory?.drop(key => key === CAPS_KEY || key === STATIC_KEY);\n }\n const capabilities = await this.sweepDevice(catalog, model, firmware);\n if (capabilities.model) {\n this.deps.probeMemory?.set(CAPS_KEY, {\n model: capabilities.model,\n firmware: capabilities.subunits.SYS?.VERSION ?? firmware,\n subunits: capabilities.subunits,\n } satisfies CachedCapabilities);\n }\n return { capabilities, fromCache: false };\n }\n\n /**\n * The fast path's second half: re-ask every catalogued function of the present\n * subunits \u2014 the answers stream into the states through the live message handler,\n * so current values arrive within the usual sweep time WITHOUT having gated the\n * ready line. Completion refreshes the persisted layers (capabilities, statics)\n * and the write map; a SHAPE change (a function newly answered) is only persisted \u2014\n * its object appears on the next start, because the unified tree is coordinated\n * once per connect and a late upsert would not materialize.\n *\n * @param catalog the (group-filtered) catalog\n */\n private async refreshInBackground(catalog: readonly YncaEntry[]): Promise<void> {\n try {\n const cached = this.deps.subunitCache?.get();\n const gets = sweepGets(catalog).filter(\n get => get.subunit === \"SYS\" || !cached || cached.subunits.includes(get.subunit),\n );\n const fresh = await this.deps.client.readCapabilities(gets);\n if (!fresh.model) {\n // The refresh ran into a drop \u2014 the supervisor handles the reconnect.\n return;\n }\n const statics: Record<string, Record<string, string>> = {};\n for (const [subunit, funcs] of Object.entries(fresh.subunits)) {\n for (const [func, value] of Object.entries(funcs)) {\n if (STATIC_FUNC.test(func)) {\n (statics[subunit] ??= {})[func] = value;\n }\n }\n }\n this.deps.probeMemory?.set(STATIC_KEY, statics);\n this.deps.probeMemory?.set(CAPS_KEY, {\n model: fresh.model,\n firmware: fresh.subunits.SYS?.VERSION ?? \"\",\n subunits: fresh.subunits,\n } satisfies CachedCapabilities);\n this.presentEntries = presentYncaEntries(fresh, catalog);\n this.writeMap = idToEntry(this.presentEntries);\n this.deps.log.debug(`${this.deviceId}: background value refresh done (YNCA)`);\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: background value refresh failed: ${String(e)}`);\n }\n }\n\n /**\n * Read the device's capabilities with the two-pass sweep. Pass 1 probes each\n * catalogued subunit with `AVAIL=?` (~2 s); pass 2 sweeps only the subunits that\n * answered, plus SYS (which never answers AVAIL) \u2014 on a typical receiver that\n * saves a third or more of the ~39 s blind sweep. A cached probe result (per\n * device, surviving reconnects and restarts) skips pass 1 entirely; a device\n * whose model or firmware no longer matches the cache re-probes. A device that\n * answers no AVAIL at all falls back to the full blind sweep, so an unknown\n * firmware loses speed, never features.\n *\n * @param catalog the (group-filtered) catalog whose functions to sweep\n * @param model the live-read SYS model name (from resolveCapabilities)\n * @param firmware the live-read SYS firmware version\n * @returns the assembled capabilities\n */\n private async sweepDevice(catalog: readonly YncaEntry[], model: string, firmware: string): Promise<YncaCapabilities> {\n const cached = this.deps.subunitCache?.get();\n if (cached) {\n // The device's IDENTITY was already read by resolveCapabilities (two reads,\n // ~0.2 s) \u2014 checking it BEFORE sweeping is what keeps a stale cache from costing\n // a full targeted sweep, then the probe, then a second sweep (~40 s).\n if (model === cached.model && firmware === cached.firmware) {\n return await this.targetedSweep(catalog, new Set(cached.subunits));\n }\n // The device behind this IP changed (swap or firmware update) \u2014 re-probe.\n this.deps.log.debug(`${this.deviceId}: cached subunit set is stale (model/firmware changed), re-probing`);\n this.deps.subunitCache?.clear();\n }\n const probe = await this.deps.client.readCapabilities(AVAIL_PROBE);\n const present = new Set(Object.keys(probe.subunits));\n if (present.size === 0) {\n // Device ignores AVAIL \u2014 sweep blind so no function is lost.\n return await this.deps.client.readCapabilities(sweepGets(catalog));\n }\n const capabilities = await this.targetedSweep(catalog, present);\n if (capabilities.model) {\n this.deps.subunitCache?.set({\n subunits: [...present],\n model: capabilities.model,\n firmware: capabilities.subunits.SYS?.VERSION ?? \"\",\n });\n }\n return capabilities;\n }\n\n /**\n * Sweep only the present subunits' functions (SYS always included \u2014 it answers no\n * AVAIL but carries model/firmware/master power).\n *\n * @param catalog the (group-filtered) catalog whose functions to sweep\n * @param present the subunits that answered the AVAIL probe\n * @returns the assembled capabilities\n */\n private async targetedSweep(catalog: readonly YncaEntry[], present: ReadonlySet<string>): Promise<YncaCapabilities> {\n const gets = sweepGets(catalog).filter(get => get.subunit === \"SYS\" || present.has(get.subunit));\n const remembered = this.deps.probeMemory?.remembered<Record<string, Record<string, string>>>(STATIC_KEY);\n // Second connect onwards: skip those reads and put the remembered answers back in, so\n // the objects are built exactly as if the device had answered them again.\n const capabilities = await this.deps.client.readCapabilities(\n remembered ? gets.filter(get => !STATIC_FUNC.test(get.func)) : gets,\n );\n if (remembered) {\n for (const [subunit, funcs] of Object.entries(remembered)) {\n capabilities.subunits[subunit] = { ...funcs, ...capabilities.subunits[subunit] };\n }\n return capabilities;\n }\n const statics: Record<string, Record<string, string>> = {};\n for (const [subunit, funcs] of Object.entries(capabilities.subunits)) {\n for (const [func, value] of Object.entries(funcs)) {\n if (STATIC_FUNC.test(func)) {\n (statics[subunit] ??= {})[func] = value;\n }\n }\n }\n this.deps.probeMemory?.set(STATIC_KEY, statics);\n return capabilities;\n }\n\n /**\n * Handle a state change: a user write (ack false) becomes a YNCA command; an\n * acked change (the device's own echo) is ignored to avoid a resend loop.\n *\n * @param fullStateId the full state id (device id + \".\" + state)\n * @param ack whether the change is acked (device-originated)\n * @param value the new value\n */\n public handleStateChange(fullStateId: string, ack: boolean, value: unknown): void {\n if (ack) {\n return;\n }\n const prefix = `${this.deviceId}.`;\n if (!fullStateId.startsWith(prefix)) {\n return;\n }\n const stateId = fullStateId.slice(prefix.length);\n if (stateId.startsWith(\"player.browse.\")) {\n this.browseEngine?.handleWrite(stateId, value);\n return;\n }\n // A scene TITLE is as valid a recall write as its number (\"Movie Viewing\" \u2192 1).\n if (stateId === \"scene.recall\" && typeof value === \"string\" && !/^\\d+$/.test(value.trim())) {\n const needle = value.trim().toLowerCase();\n const match = this.sceneTitles.find(scene => scene.title.toLowerCase() === needle);\n if (match === undefined) {\n return;\n }\n value = match.num;\n }\n // The unified player writes go to the subunit the ZONE is listening to (v2.0.0) \u2014\n // routed here, BEFORE the generic path.\n const playerWrite = /^(?:multiroom\\.(zone[234])\\.)?player\\.(playback|repeat|shuffle|next|prev)$/.exec(stateId);\n if (playerWrite) {\n this.handlePlayerWrite(playerWrite[1] ?? \"main\", `player.${playerWrite[2]}`, value);\n return;\n }\n // The unified tuner writes are band-dependent (v2.0.0) and routed here, BEFORE the\n // generic path \u2014 one state, the right wire function for the active band.\n if (this.handleTunerWrite(stateId, value)) {\n return;\n }\n const triple = yncaCommand(stateId, value, this.writeMap ?? ID_MAP);\n if (triple) {\n this.deps.client.send(triple.subunit, triple.func, triple.value);\n }\n }\n\n /**\n * Create the per-zone player mirrors (v2.0.0): every present ZONEn gets its own\n * \"now playing\" block under its multiroom folder \u2014 zones can play different\n * sources, one shared block could not show that. The defs are clones of the flat\n * block the catalog built for the main zone, plus the `source` display each zone\n * (main included) gets.\n *\n * @param capabilities the device's swept capabilities\n * @param objects the main tree's object definitions (source of the block's shape)\n */\n private async setupZonePlayers(capabilities: YncaCapabilities, objects: ObjectDef[]): Promise<void> {\n const playerObjects = objects.filter(\n object => object.id === \"player\" || (object.type === \"state\" && FLAT_PLAYER_ID.test(object.id)),\n );\n if (!playerObjects.some(object => object.type === \"state\")) {\n this.playerZones = [];\n return;\n }\n const sourceDef = (id: string): ObjectDef => ({\n id,\n type: \"state\",\n common: { name: \"Playing source\", type: \"string\", role: \"text\", read: true, write: false },\n });\n await this.deps.upsertObject(`${this.deviceId}.player.source`, sourceDef(\"player.source\"));\n this.playerZones = [\"main\"];\n for (const zone of YNCA_ZONES) {\n if (zone.key === \"main\" || capabilities.subunits[zone.subunit] === undefined) {\n continue;\n }\n this.playerZones.push(zone.key);\n for (const object of playerObjects) {\n const id = `${zone.prefix}${object.id}`;\n await this.deps.upsertObject(`${this.deviceId}.${id}`, { ...object, id });\n }\n await this.deps.upsertObject(\n `${this.deviceId}.${zone.prefix}player.source`,\n sourceDef(`${zone.prefix}player.source`),\n );\n }\n // Seed the block's resting shape: a device already playing at adapter start must\n // not show an empty source until its first input switch, and a zone NOT playing a\n // media source must show cleared values, not valueless states (2.0.0 review + live\n // deployment check). Zones ON a source get their values from the routed sweep.\n const presentFlat = new Set(\n this.presentEntries.filter(entry => FLAT_PLAYER_ID.test(entry.id)).map(entry => entry.id),\n );\n for (const zone of YNCA_ZONES) {\n if (!this.playerZones.includes(zone.key)) {\n continue;\n }\n const input = this.zoneInputs.get(zone.key);\n const playing = playerSubunitForInput(input) !== undefined;\n this.deps.setStateAck(\n `${this.deviceId}.${zone.prefix}player.source`,\n playing && input !== undefined ? input : \"\",\n );\n if (!playing) {\n for (const clear of YNCA_PLAYER_CLEAR) {\n if (presentFlat.has(clear.id)) {\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}${clear.id}`, clear.value);\n }\n }\n }\n }\n }\n\n /**\n * Feed one player-subunit value into the block of every zone listening to that\n * source \u2014 and only those: an idle source's answer (the sweep reads them all)\n * must not overwrite what the active source shows.\n *\n * @param subunit the source subunit the value came from\n * @param id the flat player state id\n * @param value the decoded value\n */\n private routePlayerUpdate(subunit: string, id: string, value: boolean | number | string): void {\n for (const zone of YNCA_ZONES) {\n if (!this.playerZones.includes(zone.key)) {\n continue;\n }\n if (playerSubunitForInput(this.zoneInputs.get(zone.key)) === subunit) {\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}${id}`, value);\n }\n }\n }\n\n /**\n * Track a zone's input switch: remember the input, and when the zone changed its\n * player source, clear the block (stale metadata must not linger) and ask the new\n * source for its current state \u2014 YNCA pushes changes, but a source that was already\n * playing has nothing new to push.\n *\n * @param zoneKey the zone (`main`, `zone2`, \u2026)\n * @param input the new INP value\n */\n private handleInputSwitch(zoneKey: string, input: string): void {\n const before = playerSubunitForInput(this.zoneInputs.get(zoneKey));\n this.zoneInputs.set(zoneKey, input);\n const after = playerSubunitForInput(input);\n if (before === after || !this.playerZones.includes(zoneKey)) {\n return;\n }\n const zone = YNCA_ZONES.find(z => z.key === zoneKey);\n if (!zone) {\n return;\n }\n const presentFlat = new Set(\n this.presentEntries.filter(entry => FLAT_PLAYER_ID.test(entry.id)).map(entry => entry.id),\n );\n for (const clear of YNCA_PLAYER_CLEAR) {\n if (presentFlat.has(clear.id)) {\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}${clear.id}`, clear.value);\n }\n }\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}player.source`, after === undefined ? \"\" : input);\n if (after !== undefined) {\n // Fresh reads for the newly selected source, streamed back through the live handler.\n const funcs = new Set<string>();\n for (const entry of this.presentEntries) {\n if (entry.subunit === after && FLAT_PLAYER_ID.test(entry.id) && !entry.writeOnly) {\n funcs.add(entry.readFunc ?? entry.func);\n }\n }\n for (const func of funcs) {\n this.deps.client.get(after, func);\n }\n }\n }\n\n /**\n * Route a unified player write (playback/repeat/shuffle/next/prev) to the source\n * subunit the ZONE is listening to \u2014 with the entry that subunit itself reported\n * (claim-with-proof, like every other write).\n *\n * @param zoneKey the zone the write belongs to\n * @param flatId the flat player state id\n * @param value the written value\n */\n private handlePlayerWrite(zoneKey: string, flatId: string, value: unknown): void {\n const subunit = playerSubunitForInput(this.zoneInputs.get(zoneKey));\n if (subunit === undefined) {\n this.deps.log.debug(`${this.deviceId}: ${flatId} ignored \u2014 ${zoneKey} is not playing a media source`);\n return;\n }\n const entry = this.presentEntries.find(e => e.id === flatId && e.subunit === subunit);\n if (entry === undefined) {\n this.deps.log.debug(`${this.deviceId}: ${flatId} ignored \u2014 ${subunit} did not report it`);\n return;\n }\n const triple = yncaCommand(flatId, value, new Map([[flatId, entry]]));\n if (triple) {\n this.deps.client.send(triple.subunit, triple.func, triple.value);\n }\n }\n\n /**\n * Route the band-dependent tuner writes (v2.0.0 unification): ONE frequency state\n * in kHz and ONE preset state, sent to the wire function of the ACTIVE band \u2014\n * AM/FM on the classic TUN subunit, FM/DAB on the DAB subunit (whose FM half\n * shares the flat ids). A DAB frequency write is dropped: DAB tunes by service,\n * the device has no frequency command there.\n *\n * @param stateId the state id relative to the device\n * @param value the written value\n * @returns true when the id was a band-routed tuner write (handled here)\n */\n private handleTunerWrite(stateId: string, value: unknown): boolean {\n if (stateId === \"tuner.frequency\") {\n const khz = Number(value);\n if (!Number.isFinite(khz)) {\n return true;\n }\n if (this.hasDab) {\n if (this.tunerBand === \"FM\") {\n this.sendProven(\"DAB\", \"FMFREQ\", formatWireNumber(khz / 1000, 2));\n } else {\n this.deps.log.debug(`${this.deviceId}: DAB tunes by service \u2014 frequency write ignored`);\n }\n return true;\n }\n if (this.tunerBand === \"AM\") {\n this.sendProven(\"TUN\", \"AMFREQ\", String(Math.round(khz)));\n } else {\n this.sendProven(\"TUN\", \"FMFREQ\", formatWireNumber(khz / 1000, 2));\n }\n return true;\n }\n if (stateId === \"tuner.preset\" && this.hasDab) {\n const slot = Math.round(Number(value));\n if (Number.isFinite(slot) && slot >= 1) {\n this.sendProven(\"DAB\", this.tunerBand === \"DAB\" ? \"DABPRESET\" : \"FMPRESET\", String(slot));\n }\n return true;\n }\n return false;\n }\n\n /**\n * Send a band-routed write ONLY with a function THIS device reported in its sweep \u2014\n * the same claim-with-proof rule every generic write obeys (#615 class). Without it\n * the router put a blind TUN:FMFREQ on the wire for any device, tuner or not, and\n * each write surfaced as a \"device refused\" warning (test-audit finding).\n *\n * @param subunit the target subunit\n * @param func the wire function\n * @param wire the encoded wire value\n */\n private sendProven(subunit: string, func: string, wire: string): void {\n if (!this.presentEntries.some(entry => entry.subunit === subunit && entry.func === func)) {\n this.deps.log.debug(`${this.deviceId}: ${subunit}:${func} not reported by this device \u2014 write dropped`);\n return;\n }\n this.deps.client.send(subunit, func, wire);\n }\n\n /**\n * Create the browsing surface (#613) when the device reports a browsable media\n * subunit: the official YNCA list vocabulary (LISTINFO/LISTSEL/LISTPAGE/LISTCURSOR)\n * drives an 8-line window under `player.browse.*`. Skipped without a delay dep\n * (older tests) and when the playback group is switched off.\n *\n * @param capabilities the device's swept capabilities\n */\n private async setupBrowse(capabilities: YncaCapabilities): Promise<void> {\n const gate = this.deps.gate;\n if (!gate || this.deps.isEntryEnabled?.(\"player.browse.source\") === false) {\n return;\n }\n const delay = (ms: number): Promise<void> => gate.delay(ms);\n const present = await this.probeBrowseSubunits(capabilities);\n if (present.size === 0) {\n // Leaving the states uncreated is what hands browsing to another transport: the owner\n // policy ranks by modernity (yxc > ynca > xml), so an unproven YNCA claim would beat a\n // PROVEN xml one and the user would get an empty menu on a device that can browse over\n // XML \u2014 exactly issue #613's RX-V473.\n this.deps.log.debug(`${this.deviceId}: no YNCA source answers LISTINFO \u2014 leaving menus to another transport`);\n return;\n }\n const driver = new YncaBrowseDriver(this.deps.client, present, delay);\n this.browseEngine = await createBrowseSurface(driver, this.deviceId, {\n upsertObject: this.deps.upsertObject,\n emit: (id, value) => this.deps.setStateAck(`${this.deviceId}.${id}`, value),\n log: this.deps.log,\n delay,\n });\n if (this.browseEngine) {\n this.browseDriver = driver;\n }\n }\n\n /**\n * Which browsable subunits actually SERVE menus, proven by asking them.\n *\n * Carrying the subunit is NOT proof: the RX-A810 reference log answers `@SERVER:LISTINFO=?`\n * with `@UNDEFINED` while NETRADIO/PC/USB on the very same device return a full window. The\n * XML driver has always probed (`List_Info` \u2192 `<Menu_Status>`); YNCA claimed the states on\n * presence alone and, ranking higher, silently displaced the transport that could deliver.\n *\n * @param capabilities the device's swept capabilities\n * @returns the subunits that answered with list data\n */\n private async probeBrowseSubunits(capabilities: YncaCapabilities): Promise<ReadonlySet<string>> {\n const candidates = YNCA_BROWSE_SOURCES.filter(source => source.subunit in capabilities.subunits);\n if (candidates.length === 0) {\n return new Set();\n }\n // A receiver in standby answers @RESTRICTED for its media subunits, which is\n // indistinguishable from \"cannot browse\" and would strip the menus off a device that\n // serves them perfectly once it is on. Nobody browses a sleeping receiver, so keep the\n // claim and let the next connect \u2014 with the device awake \u2014 do the real probe.\n if (capabilities.subunits.MAIN?.PWR !== \"On\") {\n return new Set(candidates.map(source => source.subunit));\n }\n const answer = await this.deps.client.readCapabilities(\n candidates.map(source => ({ subunit: source.subunit, func: \"LISTINFO\" })),\n );\n // Only a real list answer counts. Both refusals \u2014 `@UNDEFINED` (function unknown) and\n // `@RESTRICTED` (source not usable right now) \u2014 carry no subunit, so they cannot be\n // attributed to one request; it is the ABSENCE of an answer that excludes a subunit.\n return new Set(\n candidates\n .map(source => source.subunit)\n .filter(subunit => Object.keys(answer.subunits[subunit] ?? {}).some(func => LIST_PROOF.test(func))),\n );\n }\n\n /**\n * Register the supervisor's drop handler \u2014 delegated to the client's socket drop,\n * which is YNCA's genuine connection-lost signal.\n *\n * @param cb invoked once when the connection drops, with the reason if known\n */\n public onDrop(cb: (reason?: Error) => void): void {\n this.deps.client.onDrop(cb);\n }\n\n /** Close the client. Synchronous \u2014 safe to call from onUnload. */\n public close(): void {\n this.browseEngine?.close();\n this.browseDriver?.close();\n this.deps.client.close();\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["_a"]
|
|
7
7
|
}
|
|
@@ -24,6 +24,7 @@ __export(pure_helpers_exports, {
|
|
|
24
24
|
isUsefulDeviceName: () => isUsefulDeviceName,
|
|
25
25
|
legacyDeviceRow: () => legacyDeviceRow,
|
|
26
26
|
mergeDiscovered: () => mergeDiscovered,
|
|
27
|
+
neverWrittenStateIds: () => neverWrittenStateIds,
|
|
27
28
|
nextDeviceLabel: () => nextDeviceLabel,
|
|
28
29
|
parseDevices: () => parseDevices,
|
|
29
30
|
renamedObjectIds: () => renamedObjectIds,
|
|
@@ -277,6 +278,28 @@ const RENAMED_CHANNELS = [
|
|
|
277
278
|
"player.musicCastLink",
|
|
278
279
|
"lipSync"
|
|
279
280
|
];
|
|
281
|
+
function neverWrittenStateIds(objects, states, deviceIds, namespace) {
|
|
282
|
+
const ids = [];
|
|
283
|
+
for (const [fullId, object] of Object.entries(objects)) {
|
|
284
|
+
const common = object == null ? void 0 : object.common;
|
|
285
|
+
if ((object == null ? void 0 : object.type) !== "state" || (common == null ? void 0 : common.read) === false) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if ((common == null ? void 0 : common.custom) && Object.keys(common.custom).length > 0) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const relative = stripNamespace(fullId, namespace);
|
|
292
|
+
const top = relative.split(".")[0];
|
|
293
|
+
if (!deviceIds.has(top) || relative.slice(top.length + 1).startsWith("info.")) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const state = states[fullId];
|
|
297
|
+
if (!state || (state.val === null || state.val === void 0) && !state.lc) {
|
|
298
|
+
ids.push(fullId);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return ids;
|
|
302
|
+
}
|
|
280
303
|
function renamedObjectIds(existing, deviceIds, namespace) {
|
|
281
304
|
var _a, _b;
|
|
282
305
|
const stale = [];
|
|
@@ -340,6 +363,7 @@ function nextDeviceLabel(current, deviceId, candidate, rank, ownName, ownRank) {
|
|
|
340
363
|
isUsefulDeviceName,
|
|
341
364
|
legacyDeviceRow,
|
|
342
365
|
mergeDiscovered,
|
|
366
|
+
neverWrittenStateIds,
|
|
343
367
|
nextDeviceLabel,
|
|
344
368
|
parseDevices,
|
|
345
369
|
renamedObjectIds,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/pure-helpers.ts"],
|
|
4
|
-
"sourcesContent": ["import type { DeviceRecord } from \"./types\";\nimport type { DiscoveredDevice } from \"./discovery\";\n\ninterface ConfiguredDevice {\n name?: string;\n ip: string;\n}\n\n/**\n * True when a raw config row carries a non-empty ip (the name is optional \u2014 a row\n * with an ip but no name is valid and falls back to the ip as its id, so a device\n * is never silently dropped just because its name was left blank).\n *\n * @param entry a raw config row from the admin device table\n * @returns whether the row is a valid configured device\n */\nfunction isConfiguredDevice(entry: unknown): entry is ConfiguredDevice {\n if (typeof entry !== \"object\" || entry === null) {\n return false;\n }\n const candidate = entry as { name?: unknown; ip?: unknown };\n return (\n typeof candidate.ip === \"string\" &&\n candidate.ip.length > 0 &&\n (candidate.name === undefined || typeof candidate.name === \"string\")\n );\n}\n\n/**\n * Make a string safe for use as an ioBroker object id segment.\n *\n * @param raw the raw string (e.g. a device name)\n * @returns the string with id-unsafe characters replaced by underscores\n */\nexport function sanitizeId(raw: string): string {\n return raw.replace(/[^A-Za-z0-9\\-_]/g, \"_\");\n}\n\n/**\n * Strip the adapter namespace (e.g. `yamaha.0`) from a full state id, leaving the\n * device-relative path (e.g. `living.power`).\n *\n * @param fullId the full state id\n * @param namespace the adapter namespace\n * @returns the id relative to the adapter instance\n */\nexport function stripNamespace(fullId: string, namespace: string): string {\n return fullId.slice(namespace.length + 1);\n}\n\n/**\n * Turn the admin device table (untrusted native config) into device records.\n * Invalid rows are dropped, as are rows whose id collides with the adapter's own\n * reserved `info` branch or with an id already taken \u2014 two names that sanitise to\n * the same id (e.g. \"Living Room\" and \"Living.Room\") would otherwise share one\n * object tree. A dropped collision is reported via `onCollision` so the device does\n * not just silently \"not exist\" for the user.\n *\n * @param raw the raw `native.devices` value\n * @param onCollision called with the dropped row's name/ip and the clashing id\n * @returns validated, de-duplicated device records\n */\nexport function parseDevices(raw: unknown, onCollision?: (dropped: string, takenId: string) => void): DeviceRecord[] {\n if (!Array.isArray(raw)) {\n return [];\n }\n const records: DeviceRecord[] = [];\n const taken = new Set<string>([\"info\"]); // reserved: the adapter's own info channel\n for (const entry of raw) {\n if (!isConfiguredDevice(entry)) {\n continue;\n }\n // Fall back to the ip as the id when the name is blank, so the device still appears\n // instead of vanishing silently.\n const id = sanitizeId(entry.name && entry.name.length > 0 ? entry.name : entry.ip);\n if (taken.has(id)) {\n onCollision?.(entry.name || entry.ip, id);\n continue;\n }\n taken.add(id);\n records.push({ id, ip: entry.ip });\n }\n return records;\n}\n\n/**\n * Merge freshly discovered devices into the set already known from earlier runs\n * (the auto-discovery standby protection). A known device is kept even when this\n * run's scan did not find it \u2014 a receiver in deep standby answers no SSDP, and its\n * object tree must survive. New addresses are added; a discovered device is turned\n * into a record via its friendly name (or its ip when it advertises none), and one\n * whose id would collide with an already-kept device is skipped (reported via\n * `onCollision`, so the missing device is explainable). De-duplicated by ip.\n *\n * @param known the device records remembered from earlier runs\n * @param found the devices discovered this run\n * @param onCollision called with the dropped device's name/ip and the clashing id\n * @returns the merged records, de-duplicated by ip\n */\nexport function mergeDiscovered(\n known: DeviceRecord[],\n found: DiscoveredDevice[],\n onCollision?: (dropped: string, takenId: string) => void,\n): DeviceRecord[] {\n const byIp = new Map<string, DeviceRecord>();\n const takenIds = new Set<string>([\"info\"]); // reserved: the adapter's own info channel\n for (const device of known) {\n if (byIp.has(device.ip) || takenIds.has(device.id)) {\n continue;\n }\n byIp.set(device.ip, device);\n takenIds.add(device.id);\n }\n for (const device of found) {\n if (byIp.has(device.ip)) {\n continue;\n }\n const id = sanitizeId(device.name || device.ip);\n if (takenIds.has(id)) {\n onCollision?.(device.name || device.ip, id);\n continue;\n }\n takenIds.add(id);\n byIp.set(device.ip, { id, ip: device.ip });\n }\n return [...byIp.values()];\n}\n\n/**\n * From all object ids under the instance, pick the stale ones to delete on start:\n * everything that does not belong to a configured device and is outside the\n * adapter's own `info` branch. Removes the previous adapter's whole object tree\n * and any device dropped from the config. Keyed on the configured device ids\n * (not on what was created this run), so it works with the async connect where a\n * device's tree may only appear later \u2014 a configured device's subtree is kept\n * regardless of whether it has connected yet. Deepest first, so children go\n * before their parents.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the stale ids to delete, deepest first\n */\nexport function staleObjects(existing: string[], deviceIds: Set<string>, namespace: string): string[] {\n // No configured devices \u2192 never wipe the whole tree (a user who cleared the\n // device table by accident would otherwise lose every object in one pass).\n if (deviceIds.size === 0) {\n return [];\n }\n const isKept = (fullId: string): boolean => {\n const top = stripNamespace(fullId, namespace).split(\".\")[0];\n return top === \"info\" || deviceIds.has(top);\n };\n return existing.filter(id => !isKept(id)).sort((a, b) => b.length - a.length);\n}\n\n/**\n * Relative state ids an earlier version created under a different path and that this\n * version has renamed or moved. On start-up the old object is deleted so it does not\n * linger orphaned beside the new one \u2014 {@link staleObjects} only removes whole\n * non-configured device trees, not renamed states inside a device that is kept.\n */\n/**\n * The per-source playback-block states of the pre-2.0.0 tree \u2014 the same 19 states\n * used to sit under every player source folder, almost all permanently empty. In\n * 2.0.0 ONE flat block per zone replaces them; these copies are removed on the\n * first start (the sources that keep genuinely own states keep those untouched).\n */\nconst V2_PLAYER_BLOCK_STATES = [\n \"playback\",\n \"artist\",\n \"album\",\n \"track\",\n \"station\",\n \"channelName\",\n \"totalTime\",\n \"elapsedTime\",\n \"repeat\",\n \"shuffle\",\n \"albumArt\",\n \"source\",\n \"play\",\n \"pause\",\n \"stop\",\n \"next\",\n \"prev\",\n \"repeatToggle\",\n \"shuffleToggle\",\n];\n\n/** The source folders that stay in 2.0.0 (own states remain) but lose their block copy. */\nconst V2_SLIMMED_SOURCES = [\n \"netPlayer\",\n \"cd\",\n \"netRadio\",\n \"server\",\n \"usb\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"pc\",\n \"airplay\",\n \"bluetooth\",\n];\n\nexport const RENAMED_STATE_IDS = [\n \"hdmiOut\",\n \"directMode\",\n \"masterPower\",\n \"party\",\n \"partyMute\",\n \"distributionEnable\",\n \"partyEnable\",\n \"remoteCode\",\n // v1.0.0 multiroom regroup: the MusicCast-Link states moved into their own\n // multiroom.group folder, so the tree itself tells device-group from all-zones scope.\n \"multiroom.distributionEnable\",\n // Short-lived v1.0.0 pre-cut id \u2014 mislabeled \"active\", the field means \"enabled for use\".\n \"multiroom.group.streamingActive\",\n \"multiroom.role\",\n \"multiroom.groupId\",\n \"multiroom.groupName\",\n \"multiroom.serverZone\",\n \"multiroom.clientList\",\n \"multiroom.linkClient\",\n \"multiroom.leaveGroup\",\n // ---- v2.0.0 tree rework ------------------------------------------------------\n // Player unification: the per-source copies of the playback block are gone (the\n // slimmed folders keep only their own preset/pairing/drive states).\n ...V2_SLIMMED_SOURCES.flatMap(source => V2_PLAYER_BLOCK_STATES.map(state => `player.${source}.${state}`)),\n // Scenes: the twelve per-name datapoints became the recall dropdown + scene.list.\n ...Array.from({ length: 12 }, (_unused, i) => `scene.name${i + 1}`),\n // Tuner unification: ONE band/frequency/preset; the DAB subunit's FM half moved\n // onto the flat tuner ids, the two per-band frequencies became tuner.frequency.\n \"tuner.amFrequency\",\n \"tuner.fmFrequency\",\n \"tuner.dab.band\",\n \"tuner.dab.preset\",\n \"tuner.dab.fmPreset\",\n \"tuner.dab.fmFrequency\",\n \"tuner.dab.fmSearchMode\",\n \"tuner.dab.fmRdsService\",\n \"tuner.dab.fmRdsProgramType\",\n \"tuner.dab.fmRdsText\",\n \"tuner.dab.fmRdsClock\",\n \"tuner.dab.fmStereo\",\n \"tuner.dab.fmTuned\",\n \"tuner.dab.audioMode\",\n // Sound polish: equalizer and signal info each moved into their own subfolder.\n \"sound.equalizerMode\",\n \"sound.equalizerLow\",\n \"sound.equalizerMid\",\n \"sound.equalizerHigh\",\n \"sound.signalFormat\",\n \"sound.signalSampling\",\n \"sound.signalBits\",\n \"sound.signalBitrate\",\n // HDMI polish: the lip-sync offsets moved into the hdmi folder (the lipSync\n // channel itself is in RENAMED_CHANNELS); the A/B toggles joined the speakers.\n \"advanced.speakerA\",\n \"advanced.speakerB\",\n];\n\n/**\n * Old channel prefixes whose whole subtree this version moved out \u2014 the `system`\n * grab-bag is gone (model/firmware \u2192 info, HDMI outputs \u2192 hdmi, speaker patterns \u2192\n * speakers, input names \u2192 inputNames, master power \u2192 multiroom.masterPower). The channel and\n * every state under it are removed.\n */\nexport const RENAMED_CHANNELS = [\n // pre-0.11 system folder\n \"system\",\n // v0.18.1 multiroom regroup: zone2/3/4, zoneB, flat multiroom states moved under multiroom/.\n \"zone2\",\n \"zone3\",\n \"zone4\",\n \"zoneB\",\n // v1.0.0: stray per-zone copies of the device-global YXC states (the zone loop used to\n // prefix them too, yielding multiroom.zoneN.multiroom.* junk) are swept away.\n \"multiroom.zone2.multiroom\",\n \"multiroom.zone3.multiroom\",\n \"multiroom.zone4.multiroom\",\n // Regrouping: media sources moved under player/, DAB under tuner/, dist \u2192 multiroom. The old\n // flat channels (and their whole subtree) are deleted so the new grouped ones do not sit\n // beside orphaned copies on an upgraded instance.\n \"netRadio\",\n \"server\",\n \"usb\",\n \"spotify\",\n \"deezer\",\n \"tidal\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"airplay\",\n \"bluetooth\",\n \"pc\",\n \"musicCastLink\",\n \"ipod\",\n \"ipodUsb\",\n \"netPlayer\",\n \"cd\",\n \"dab\",\n \"dist\",\n // Sound/Advanced regroup: DSP/tone-tuning states moved under sound.*, setup-only states\n // (+ the speakers/initialVolume/inputNames subtrees, already dotted before) under advanced.*.\n // {@link renamedObjectIds} also checks these against a stripped zone2/3/4 prefix, so one\n // entry here catches a MAIN state and its zoned copies (e.g. \"straight\" and \"zone2.straight\").\n \"straight\",\n \"enhancer\",\n \"pureDirect\",\n \"direct\",\n \"adaptiveDrc\",\n \"surroundAI\",\n \"surroundDecoder\",\n \"cinemaDsp3d\",\n \"extraBass\",\n \"bass\",\n \"treble\",\n \"subwooferTrim\",\n \"balance\",\n \"dialogueLevel\",\n \"dialogueLift\",\n \"dtsDialogueControl\",\n \"monaural\",\n \"surround3d\",\n \"adaptiveDspLevel\",\n \"audioSelect\",\n \"linkControl\",\n \"linkAudioDelay\",\n \"linkAudioQuality\",\n \"contentsDisplay\",\n \"equalizerLow\",\n \"equalizerMid\",\n \"equalizerHigh\",\n \"clearVoice\",\n \"bassExtension\",\n \"ypaoVolume\",\n \"maxVolume\",\n \"speakerA\",\n \"speakerB\",\n \"speakers\",\n \"initialVolume\",\n \"inputNames\",\n // ---- v2.0.0 tree rework: the always-empty source channels are gone entirely\n // (their playback lives in the flat per-zone block), lip sync moved into hdmi.\n \"player.spotify\",\n \"player.deezer\",\n \"player.tidal\",\n \"player.ipod\",\n \"player.ipodUsb\",\n \"player.musicCastLink\",\n \"lipSync\",\n];\n\n/**\n * The full ids of renamed old states (and old channel subtrees) that still exist\n * under a configured device, to be deleted on start-up so no orphan lingers beside\n * the new object. Deepest first, so children go before their parents.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full old ids to delete, deepest first\n */\nexport function renamedObjectIds(existing: string[], deviceIds: Set<string>, namespace: string): string[] {\n const stale: string[] = [];\n for (const deviceId of deviceIds) {\n const base = `${namespace}.${deviceId}.`;\n for (const full of existing) {\n if (!full.startsWith(base)) {\n continue;\n }\n const rel = full.slice(base.length);\n // Strip an optional zone prefix before matching too, so one RENAMED_CHANNELS entry\n // (e.g. \"straight\") catches both the MAIN state and its zoned copies \u2014 the old flat\n // \"zone2.\" form and today's \"multiroom.zone2.\" form alike (v2.0.0 renames live in\n // zoned folders, e.g. multiroom.zone2.scene.name1).\n const zone = /^(?:multiroom\\.)?zone[234]\\./.exec(rel)?.[0] ?? \"\";\n const template = rel.slice(zone.length);\n const renamedState = RENAMED_STATE_IDS.includes(rel) || RENAMED_STATE_IDS.includes(template);\n const underRenamedChannel = RENAMED_CHANNELS.some(\n ch => rel === ch || rel.startsWith(`${ch}.`) || template === ch || template.startsWith(`${ch}.`),\n );\n if (renamedState || underRenamedChannel) {\n stale.push(full);\n }\n }\n }\n return stale.sort((a, b) => b.length - a.length);\n}\n\n/**\n * The device row to carry over from the previous adapter's single-device config,\n * or undefined if nothing needs migrating. The old yamaha stored one receiver as\n * `config.ip` (older installs: `config.IP`); the new adapter uses a `devices`\n * table. Only migrates when the table is still empty, so it runs once.\n *\n * The old value could be a hostname (its HTTP client resolved names) and could\n * carry a `:port` suffix (its HTTP library split host:port). A port suffix would\n * break every transport here \u2014 YNCA is TCP :50000, YXC/XML build `http://<ip>\u2026`\n * \u2014 so it is stripped; the host/IP itself is carried over as-is.\n *\n * @param config the instance's native config\n * @returns the row to add to the devices table, or undefined\n */\nexport function legacyDeviceRow(config: Record<string, unknown>): { name: string; ip: string } | undefined {\n if (Array.isArray(config.devices) && config.devices.length > 0) {\n return undefined;\n }\n const raw =\n typeof config.ip === \"string\" && config.ip\n ? config.ip\n : typeof config.IP === \"string\" && config.IP\n ? config.IP\n : undefined;\n if (!raw) {\n return undefined;\n }\n const ip = raw.trim().replace(/:\\d+$/, \"\");\n return ip ? { name: ip, ip } : undefined;\n}\n\n/**\n * How trustworthy a display-name candidate is. A name the device carries for itself\n * (the MusicCast zone name a user typed in the app) beats its model designation.\n */\nexport const LABEL_RANK = { model: 1, deviceName: 2 } as const;\n\n/** Rank of a display-name candidate \u2014 see {@link LABEL_RANK}. */\nexport type LabelRank = (typeof LABEL_RANK)[keyof typeof LABEL_RANK];\n\n/**\n * Zone names that say nothing about the device \u2014 a receiver ships with these and a\n * user who never renamed the zone would end up with \"Main Zone\" as the device name.\n */\nconst GENERIC_ZONE_NAMES = new Set([\"main\", \"main zone\", \"mainzone\", \"zone\", \"zone 1\", \"zone1\"]);\n\n/**\n * Is this candidate worth showing as a device name?\n *\n * @param candidate the reported name\n * @returns true when it carries information about this particular device\n */\nexport function isUsefulDeviceName(candidate: string | undefined): boolean {\n const trimmed = (candidate ?? \"\").trim();\n return trimmed.length > 0 && !GENERIC_ZONE_NAMES.has(trimmed.toLowerCase());\n}\n\n/**\n * The display name to write onto a device object, or undefined to leave it alone.\n *\n * An upgraded instance carries the IP as its device name: the previous adapter knew\n * only an IP, so the migration had nothing else to call the device, and the object id\n * \u2014 which must not change, every history and visualisation binding hangs off it \u2014\n * became that IP. This decides when the adapter may replace that placeholder with\n * something a user recognises.\n *\n * Two things must never be overwritten: a name the user typed, and a better name by a\n * weaker source. The adapter therefore only writes over its own placeholder (the id\n * itself) or over what it wrote last, and only when the new candidate ranks at least\n * as high as the one behind the current name.\n *\n * @param current the device object's present `common.name`\n * @param deviceId the object id, which is also the placeholder name\n * @param candidate the newly reported name\n * @param rank how trustworthy the candidate is\n * @param ownName the name this adapter wrote last for the device, if any\n * @param ownRank the rank behind {@link ownName}\n * @returns the name to write, or undefined when the current name stays\n */\nexport function nextDeviceLabel(\n current: string | undefined,\n deviceId: string,\n candidate: string | undefined,\n rank: LabelRank,\n ownName?: string,\n ownRank?: LabelRank,\n): string | undefined {\n const wanted = (candidate ?? \"\").trim();\n if (!isUsefulDeviceName(wanted) || wanted === current) {\n return undefined;\n }\n const isPlaceholder = current === undefined || current === deviceId;\n const isOurs = ownName !== undefined && current === ownName;\n if (!isPlaceholder && !isOurs) {\n return undefined; // the user named this device \u2014 theirs wins\n }\n if (isOurs && ownRank !== undefined && rank < ownRank) {\n return undefined; // do not fall back from a device name to its model\n }\n return wanted;\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import type { DeviceRecord } from \"./types\";\nimport type { DiscoveredDevice } from \"./discovery\";\n\ninterface ConfiguredDevice {\n name?: string;\n ip: string;\n}\n\n/**\n * True when a raw config row carries a non-empty ip (the name is optional \u2014 a row\n * with an ip but no name is valid and falls back to the ip as its id, so a device\n * is never silently dropped just because its name was left blank).\n *\n * @param entry a raw config row from the admin device table\n * @returns whether the row is a valid configured device\n */\nfunction isConfiguredDevice(entry: unknown): entry is ConfiguredDevice {\n if (typeof entry !== \"object\" || entry === null) {\n return false;\n }\n const candidate = entry as { name?: unknown; ip?: unknown };\n return (\n typeof candidate.ip === \"string\" &&\n candidate.ip.length > 0 &&\n (candidate.name === undefined || typeof candidate.name === \"string\")\n );\n}\n\n/**\n * Make a string safe for use as an ioBroker object id segment.\n *\n * @param raw the raw string (e.g. a device name)\n * @returns the string with id-unsafe characters replaced by underscores\n */\nexport function sanitizeId(raw: string): string {\n return raw.replace(/[^A-Za-z0-9\\-_]/g, \"_\");\n}\n\n/**\n * Strip the adapter namespace (e.g. `yamaha.0`) from a full state id, leaving the\n * device-relative path (e.g. `living.power`).\n *\n * @param fullId the full state id\n * @param namespace the adapter namespace\n * @returns the id relative to the adapter instance\n */\nexport function stripNamespace(fullId: string, namespace: string): string {\n return fullId.slice(namespace.length + 1);\n}\n\n/**\n * Turn the admin device table (untrusted native config) into device records.\n * Invalid rows are dropped, as are rows whose id collides with the adapter's own\n * reserved `info` branch or with an id already taken \u2014 two names that sanitise to\n * the same id (e.g. \"Living Room\" and \"Living.Room\") would otherwise share one\n * object tree. A dropped collision is reported via `onCollision` so the device does\n * not just silently \"not exist\" for the user.\n *\n * @param raw the raw `native.devices` value\n * @param onCollision called with the dropped row's name/ip and the clashing id\n * @returns validated, de-duplicated device records\n */\nexport function parseDevices(raw: unknown, onCollision?: (dropped: string, takenId: string) => void): DeviceRecord[] {\n if (!Array.isArray(raw)) {\n return [];\n }\n const records: DeviceRecord[] = [];\n const taken = new Set<string>([\"info\"]); // reserved: the adapter's own info channel\n for (const entry of raw) {\n if (!isConfiguredDevice(entry)) {\n continue;\n }\n // Fall back to the ip as the id when the name is blank, so the device still appears\n // instead of vanishing silently.\n const id = sanitizeId(entry.name && entry.name.length > 0 ? entry.name : entry.ip);\n if (taken.has(id)) {\n onCollision?.(entry.name || entry.ip, id);\n continue;\n }\n taken.add(id);\n records.push({ id, ip: entry.ip });\n }\n return records;\n}\n\n/**\n * Merge freshly discovered devices into the set already known from earlier runs\n * (the auto-discovery standby protection). A known device is kept even when this\n * run's scan did not find it \u2014 a receiver in deep standby answers no SSDP, and its\n * object tree must survive. New addresses are added; a discovered device is turned\n * into a record via its friendly name (or its ip when it advertises none), and one\n * whose id would collide with an already-kept device is skipped (reported via\n * `onCollision`, so the missing device is explainable). De-duplicated by ip.\n *\n * @param known the device records remembered from earlier runs\n * @param found the devices discovered this run\n * @param onCollision called with the dropped device's name/ip and the clashing id\n * @returns the merged records, de-duplicated by ip\n */\nexport function mergeDiscovered(\n known: DeviceRecord[],\n found: DiscoveredDevice[],\n onCollision?: (dropped: string, takenId: string) => void,\n): DeviceRecord[] {\n const byIp = new Map<string, DeviceRecord>();\n const takenIds = new Set<string>([\"info\"]); // reserved: the adapter's own info channel\n for (const device of known) {\n if (byIp.has(device.ip) || takenIds.has(device.id)) {\n continue;\n }\n byIp.set(device.ip, device);\n takenIds.add(device.id);\n }\n for (const device of found) {\n if (byIp.has(device.ip)) {\n continue;\n }\n const id = sanitizeId(device.name || device.ip);\n if (takenIds.has(id)) {\n onCollision?.(device.name || device.ip, id);\n continue;\n }\n takenIds.add(id);\n byIp.set(device.ip, { id, ip: device.ip });\n }\n return [...byIp.values()];\n}\n\n/**\n * From all object ids under the instance, pick the stale ones to delete on start:\n * everything that does not belong to a configured device and is outside the\n * adapter's own `info` branch. Removes the previous adapter's whole object tree\n * and any device dropped from the config. Keyed on the configured device ids\n * (not on what was created this run), so it works with the async connect where a\n * device's tree may only appear later \u2014 a configured device's subtree is kept\n * regardless of whether it has connected yet. Deepest first, so children go\n * before their parents.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the stale ids to delete, deepest first\n */\nexport function staleObjects(existing: string[], deviceIds: Set<string>, namespace: string): string[] {\n // No configured devices \u2192 never wipe the whole tree (a user who cleared the\n // device table by accident would otherwise lose every object in one pass).\n if (deviceIds.size === 0) {\n return [];\n }\n const isKept = (fullId: string): boolean => {\n const top = stripNamespace(fullId, namespace).split(\".\")[0];\n return top === \"info\" || deviceIds.has(top);\n };\n return existing.filter(id => !isKept(id)).sort((a, b) => b.length - a.length);\n}\n\n/**\n * Relative state ids an earlier version created under a different path and that this\n * version has renamed or moved. On start-up the old object is deleted so it does not\n * linger orphaned beside the new one \u2014 {@link staleObjects} only removes whole\n * non-configured device trees, not renamed states inside a device that is kept.\n */\n/**\n * The per-source playback-block states of the pre-2.0.0 tree \u2014 the same 19 states\n * used to sit under every player source folder, almost all permanently empty. In\n * 2.0.0 ONE flat block per zone replaces them; these copies are removed on the\n * first start (the sources that keep genuinely own states keep those untouched).\n */\nconst V2_PLAYER_BLOCK_STATES = [\n \"playback\",\n \"artist\",\n \"album\",\n \"track\",\n \"station\",\n \"channelName\",\n \"totalTime\",\n \"elapsedTime\",\n \"repeat\",\n \"shuffle\",\n \"albumArt\",\n \"source\",\n \"play\",\n \"pause\",\n \"stop\",\n \"next\",\n \"prev\",\n \"repeatToggle\",\n \"shuffleToggle\",\n];\n\n/** The source folders that stay in 2.0.0 (own states remain) but lose their block copy. */\nconst V2_SLIMMED_SOURCES = [\n \"netPlayer\",\n \"cd\",\n \"netRadio\",\n \"server\",\n \"usb\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"pc\",\n \"airplay\",\n \"bluetooth\",\n];\n\nexport const RENAMED_STATE_IDS = [\n \"hdmiOut\",\n \"directMode\",\n \"masterPower\",\n \"party\",\n \"partyMute\",\n \"distributionEnable\",\n \"partyEnable\",\n \"remoteCode\",\n // v1.0.0 multiroom regroup: the MusicCast-Link states moved into their own\n // multiroom.group folder, so the tree itself tells device-group from all-zones scope.\n \"multiroom.distributionEnable\",\n // Short-lived v1.0.0 pre-cut id \u2014 mislabeled \"active\", the field means \"enabled for use\".\n \"multiroom.group.streamingActive\",\n \"multiroom.role\",\n \"multiroom.groupId\",\n \"multiroom.groupName\",\n \"multiroom.serverZone\",\n \"multiroom.clientList\",\n \"multiroom.linkClient\",\n \"multiroom.leaveGroup\",\n // ---- v2.0.0 tree rework ------------------------------------------------------\n // Player unification: the per-source copies of the playback block are gone (the\n // slimmed folders keep only their own preset/pairing/drive states).\n ...V2_SLIMMED_SOURCES.flatMap(source => V2_PLAYER_BLOCK_STATES.map(state => `player.${source}.${state}`)),\n // Scenes: the twelve per-name datapoints became the recall dropdown + scene.list.\n ...Array.from({ length: 12 }, (_unused, i) => `scene.name${i + 1}`),\n // Tuner unification: ONE band/frequency/preset; the DAB subunit's FM half moved\n // onto the flat tuner ids, the two per-band frequencies became tuner.frequency.\n \"tuner.amFrequency\",\n \"tuner.fmFrequency\",\n \"tuner.dab.band\",\n \"tuner.dab.preset\",\n \"tuner.dab.fmPreset\",\n \"tuner.dab.fmFrequency\",\n \"tuner.dab.fmSearchMode\",\n \"tuner.dab.fmRdsService\",\n \"tuner.dab.fmRdsProgramType\",\n \"tuner.dab.fmRdsText\",\n \"tuner.dab.fmRdsClock\",\n \"tuner.dab.fmStereo\",\n \"tuner.dab.fmTuned\",\n \"tuner.dab.audioMode\",\n // Sound polish: equalizer and signal info each moved into their own subfolder.\n \"sound.equalizerMode\",\n \"sound.equalizerLow\",\n \"sound.equalizerMid\",\n \"sound.equalizerHigh\",\n \"sound.signalFormat\",\n \"sound.signalSampling\",\n \"sound.signalBits\",\n \"sound.signalBitrate\",\n // HDMI polish: the lip-sync offsets moved into the hdmi folder (the lipSync\n // channel itself is in RENAMED_CHANNELS); the A/B toggles joined the speakers.\n \"advanced.speakerA\",\n \"advanced.speakerB\",\n];\n\n/**\n * Old channel prefixes whose whole subtree this version moved out \u2014 the `system`\n * grab-bag is gone (model/firmware \u2192 info, HDMI outputs \u2192 hdmi, speaker patterns \u2192\n * speakers, input names \u2192 inputNames, master power \u2192 multiroom.masterPower). The channel and\n * every state under it are removed.\n */\nexport const RENAMED_CHANNELS = [\n // pre-0.11 system folder\n \"system\",\n // v0.18.1 multiroom regroup: zone2/3/4, zoneB, flat multiroom states moved under multiroom/.\n \"zone2\",\n \"zone3\",\n \"zone4\",\n \"zoneB\",\n // v1.0.0: stray per-zone copies of the device-global YXC states (the zone loop used to\n // prefix them too, yielding multiroom.zoneN.multiroom.* junk) are swept away.\n \"multiroom.zone2.multiroom\",\n \"multiroom.zone3.multiroom\",\n \"multiroom.zone4.multiroom\",\n // Regrouping: media sources moved under player/, DAB under tuner/, dist \u2192 multiroom. The old\n // flat channels (and their whole subtree) are deleted so the new grouped ones do not sit\n // beside orphaned copies on an upgraded instance.\n \"netRadio\",\n \"server\",\n \"usb\",\n \"spotify\",\n \"deezer\",\n \"tidal\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"airplay\",\n \"bluetooth\",\n \"pc\",\n \"musicCastLink\",\n \"ipod\",\n \"ipodUsb\",\n \"netPlayer\",\n \"cd\",\n \"dab\",\n \"dist\",\n // Sound/Advanced regroup: DSP/tone-tuning states moved under sound.*, setup-only states\n // (+ the speakers/initialVolume/inputNames subtrees, already dotted before) under advanced.*.\n // {@link renamedObjectIds} also checks these against a stripped zone2/3/4 prefix, so one\n // entry here catches a MAIN state and its zoned copies (e.g. \"straight\" and \"zone2.straight\").\n \"straight\",\n \"enhancer\",\n \"pureDirect\",\n \"direct\",\n \"adaptiveDrc\",\n \"surroundAI\",\n \"surroundDecoder\",\n \"cinemaDsp3d\",\n \"extraBass\",\n \"bass\",\n \"treble\",\n \"subwooferTrim\",\n \"balance\",\n \"dialogueLevel\",\n \"dialogueLift\",\n \"dtsDialogueControl\",\n \"monaural\",\n \"surround3d\",\n \"adaptiveDspLevel\",\n \"audioSelect\",\n \"linkControl\",\n \"linkAudioDelay\",\n \"linkAudioQuality\",\n \"contentsDisplay\",\n \"equalizerLow\",\n \"equalizerMid\",\n \"equalizerHigh\",\n \"clearVoice\",\n \"bassExtension\",\n \"ypaoVolume\",\n \"maxVolume\",\n \"speakerA\",\n \"speakerB\",\n \"speakers\",\n \"initialVolume\",\n \"inputNames\",\n // ---- v2.0.0 tree rework: the always-empty source channels are gone entirely\n // (their playback lives in the flat per-zone block), lip sync moved into hdmi.\n \"player.spotify\",\n \"player.deezer\",\n \"player.tidal\",\n \"player.ipod\",\n \"player.ipodUsb\",\n \"player.musicCastLink\",\n \"lipSync\",\n];\n\n/**\n * Read-capable state objects under a configured device whose state NEVER carried a\n * value (no value, no last-change). They are over-declarations of an earlier adapter\n * version \u2014 today's claim-with-proof creation would not make them \u2014 and deleting them\n * is lossless: there is no value and no history to lose, and anything a transport\n * legitimately claims is recreated right after, when the device connects. Runs once\n * per adapter version (the caller keeps a marker), so a freshly created state that is\n * merely waiting for its first value does not flap on every start. Excluded: buttons\n * and other write-only states (naturally valueless), states carrying per-state user\n * settings (`common.custom`, e.g. a history binding \u2014 a deliberate user link is never\n * deleted behind their back), and the `info.` header the adapter itself maintains for\n * every device, connected or not.\n *\n * @param objects all objects under the instance, keyed by full id\n * @param states all states under the instance, keyed by full id\n * @param deviceIds the ids of the devices to sweep\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full ids of never-filled read-capable states\n */\nexport function neverWrittenStateIds(\n objects: Record<string, { type?: string; common?: unknown } | undefined>,\n states: Record<string, { val?: unknown; lc?: number } | null | undefined>,\n deviceIds: Set<string>,\n namespace: string,\n): string[] {\n const ids: string[] = [];\n for (const [fullId, object] of Object.entries(objects)) {\n const common = object?.common as { read?: boolean; custom?: Record<string, unknown> } | undefined;\n if (object?.type !== \"state\" || common?.read === false) {\n continue;\n }\n if (common?.custom && Object.keys(common.custom).length > 0) {\n continue;\n }\n const relative = stripNamespace(fullId, namespace);\n const top = relative.split(\".\")[0];\n if (!deviceIds.has(top) || relative.slice(top.length + 1).startsWith(\"info.\")) {\n continue;\n }\n const state = states[fullId];\n if (!state || ((state.val === null || state.val === undefined) && !state.lc)) {\n ids.push(fullId);\n }\n }\n return ids;\n}\n\n/**\n * The full ids of renamed old states (and old channel subtrees) that still exist\n * under a configured device, to be deleted on start-up so no orphan lingers beside\n * the new object. Deepest first, so children go before their parents.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full old ids to delete, deepest first\n */\nexport function renamedObjectIds(existing: string[], deviceIds: Set<string>, namespace: string): string[] {\n const stale: string[] = [];\n for (const deviceId of deviceIds) {\n const base = `${namespace}.${deviceId}.`;\n for (const full of existing) {\n if (!full.startsWith(base)) {\n continue;\n }\n const rel = full.slice(base.length);\n // Strip an optional zone prefix before matching too, so one RENAMED_CHANNELS entry\n // (e.g. \"straight\") catches both the MAIN state and its zoned copies \u2014 the old flat\n // \"zone2.\" form and today's \"multiroom.zone2.\" form alike (v2.0.0 renames live in\n // zoned folders, e.g. multiroom.zone2.scene.name1).\n const zone = /^(?:multiroom\\.)?zone[234]\\./.exec(rel)?.[0] ?? \"\";\n const template = rel.slice(zone.length);\n const renamedState = RENAMED_STATE_IDS.includes(rel) || RENAMED_STATE_IDS.includes(template);\n const underRenamedChannel = RENAMED_CHANNELS.some(\n ch => rel === ch || rel.startsWith(`${ch}.`) || template === ch || template.startsWith(`${ch}.`),\n );\n if (renamedState || underRenamedChannel) {\n stale.push(full);\n }\n }\n }\n return stale.sort((a, b) => b.length - a.length);\n}\n\n/**\n * The device row to carry over from the previous adapter's single-device config,\n * or undefined if nothing needs migrating. The old yamaha stored one receiver as\n * `config.ip` (older installs: `config.IP`); the new adapter uses a `devices`\n * table. Only migrates when the table is still empty, so it runs once.\n *\n * The old value could be a hostname (its HTTP client resolved names) and could\n * carry a `:port` suffix (its HTTP library split host:port). A port suffix would\n * break every transport here \u2014 YNCA is TCP :50000, YXC/XML build `http://<ip>\u2026`\n * \u2014 so it is stripped; the host/IP itself is carried over as-is.\n *\n * @param config the instance's native config\n * @returns the row to add to the devices table, or undefined\n */\nexport function legacyDeviceRow(config: Record<string, unknown>): { name: string; ip: string } | undefined {\n if (Array.isArray(config.devices) && config.devices.length > 0) {\n return undefined;\n }\n const raw =\n typeof config.ip === \"string\" && config.ip\n ? config.ip\n : typeof config.IP === \"string\" && config.IP\n ? config.IP\n : undefined;\n if (!raw) {\n return undefined;\n }\n const ip = raw.trim().replace(/:\\d+$/, \"\");\n return ip ? { name: ip, ip } : undefined;\n}\n\n/**\n * How trustworthy a display-name candidate is. A name the device carries for itself\n * (the MusicCast zone name a user typed in the app) beats its model designation.\n */\nexport const LABEL_RANK = { model: 1, deviceName: 2 } as const;\n\n/** Rank of a display-name candidate \u2014 see {@link LABEL_RANK}. */\nexport type LabelRank = (typeof LABEL_RANK)[keyof typeof LABEL_RANK];\n\n/**\n * Zone names that say nothing about the device \u2014 a receiver ships with these and a\n * user who never renamed the zone would end up with \"Main Zone\" as the device name.\n */\nconst GENERIC_ZONE_NAMES = new Set([\"main\", \"main zone\", \"mainzone\", \"zone\", \"zone 1\", \"zone1\"]);\n\n/**\n * Is this candidate worth showing as a device name?\n *\n * @param candidate the reported name\n * @returns true when it carries information about this particular device\n */\nexport function isUsefulDeviceName(candidate: string | undefined): boolean {\n const trimmed = (candidate ?? \"\").trim();\n return trimmed.length > 0 && !GENERIC_ZONE_NAMES.has(trimmed.toLowerCase());\n}\n\n/**\n * The display name to write onto a device object, or undefined to leave it alone.\n *\n * An upgraded instance carries the IP as its device name: the previous adapter knew\n * only an IP, so the migration had nothing else to call the device, and the object id\n * \u2014 which must not change, every history and visualisation binding hangs off it \u2014\n * became that IP. This decides when the adapter may replace that placeholder with\n * something a user recognises.\n *\n * Two things must never be overwritten: a name the user typed, and a better name by a\n * weaker source. The adapter therefore only writes over its own placeholder (the id\n * itself) or over what it wrote last, and only when the new candidate ranks at least\n * as high as the one behind the current name.\n *\n * @param current the device object's present `common.name`\n * @param deviceId the object id, which is also the placeholder name\n * @param candidate the newly reported name\n * @param rank how trustworthy the candidate is\n * @param ownName the name this adapter wrote last for the device, if any\n * @param ownRank the rank behind {@link ownName}\n * @returns the name to write, or undefined when the current name stays\n */\nexport function nextDeviceLabel(\n current: string | undefined,\n deviceId: string,\n candidate: string | undefined,\n rank: LabelRank,\n ownName?: string,\n ownRank?: LabelRank,\n): string | undefined {\n const wanted = (candidate ?? \"\").trim();\n if (!isUsefulDeviceName(wanted) || wanted === current) {\n return undefined;\n }\n const isPlaceholder = current === undefined || current === deviceId;\n const isOurs = ownName !== undefined && current === ownName;\n if (!isPlaceholder && !isOurs) {\n return undefined; // the user named this device \u2014 theirs wins\n }\n if (isOurs && ownRank !== undefined && rank < ownRank) {\n return undefined; // do not fall back from a device name to its model\n }\n return wanted;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|