iobroker.yamaha 2.3.2 → 2.3.3

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/yxc/device-controller.ts"],
4
- "sourcesContent": ["import { createHash } from \"node:crypto\";\nimport { parseYxcFeatures, type YxcCapabilities, type YxcTunerFeatures } from \"./capability\";\nimport { mapYxcToObjects } from \"./object-mapper\";\nimport {\n parseYxcClock,\n parseYxcDistribution,\n parseYxcPlayInfo,\n parseYxcPlaylistNames,\n parseYxcPlayQueue,\n parseYxcPresetList,\n parseYxcRecentList,\n parseYxcSignalInfo,\n parseYxcStatus,\n parseYxcTunerInfo,\n parseYxcTunerPresetLists,\n PLAYER_CLEAR,\n stateToYxc,\n type PlayerTransport,\n type YxcCommand,\n} from \"./command-mapper\";\nimport { mediaToRefresh, netusbListsToRefresh, zonesToRefresh } from \"./push\";\nimport type { ObjectDef } from \"../catalog/types\";\nimport { tName } from \"../i18n\";\nimport type { StateValue } from \"../types\";\nimport type { ConnectionHandle, ControllerLog } from \"../controller\";\nimport { errorMessage } from \"../util\";\nimport { PollDropDetector } from \"../lifecycle/poll-drop-detector\";\nimport type { ProbeMemory } from \"../lifecycle/probe-memory\";\nimport { zonePrefix } from \"./zones\";\nimport { knownScenes, resolveSceneNumber } from \"../catalog/scene-titles\";\nimport type { CommandGate } from \"../lifecycle/command-gate\";\nimport type { BrowseEngine } from \"../browse/browse-engine\";\nimport { createBrowseSurface } from \"../browse/surface\";\nimport { YxcBrowseDriver } from \"../browse/yxc-browse-driver\";\n\n/** Renew interval for the push registration + state poll, well under the ~20 min expiry. */\nconst KEEPALIVE_MS = 5 * 60 * 1000;\n\n/**\n * With push working, run the full media/list/group sweep only every Nth keepalive (6 \u00D7 5 min\n * = every 30 minutes) \u2014 a safety net against a dropped UDP packet, not the primary path.\n */\nconst PUSH_MODE_FULL_SWEEP_EVERY = 6;\n\n/**\n * Extract the model name from a getDeviceInfo response, if it carries a non-empty one.\n *\n * @param deviceInfo the getDeviceInfo response\n * @returns the model name, or undefined\n */\nfunction modelNameFrom(deviceInfo: unknown): string | undefined {\n const model = (deviceInfo as { model_name?: unknown } | null)?.model_name;\n return typeof model === \"string\" && model.length > 0 ? model : undefined;\n}\n\n/**\n * Extract the name a user gave this device from a getNameText response.\n *\n * MusicCast keeps it as the main zone's text \u2014 that is the name shown in the app and\n * the one people recognise (\"Wohnzimmer\"). A device whose zone was never renamed\n * answers with a generic zone name; the caller filters those out.\n *\n * @param nameText the getNameText response\n * @returns the main zone's text, or undefined\n */\nexport function zoneNameFrom(nameText: unknown): string | undefined {\n const zones = (nameText as { zone_list?: unknown } | null)?.zone_list;\n if (!Array.isArray(zones)) {\n return undefined;\n }\n for (const zone of zones) {\n if (typeof zone !== \"object\" || zone === null) {\n continue;\n }\n const { id, text } = zone as { id?: unknown; text?: unknown };\n if (id === \"main\" && typeof text === \"string\" && text.trim().length > 0) {\n return text.trim();\n }\n }\n return undefined;\n}\n\nimport type { YxcClientLike } from \"./client-contract\";\n\n// Re-exported so existing importers (the tests' fakes) keep resolving it from here.\nexport type { YxcClientLike };\n\n/** The adapter callbacks the controller drives \u2014 narrow, so no adapter mock is needed in tests. */\nexport interface YxcControllerDeps {\n /** The YXC (MusicCast) client for this device. */\n client: YxcClientLike;\n /** Resolve another configured device's client by IP, for forming a multiroom group. */\n clientFor?: (ip: string) => YxcClientLike | undefined;\n /** Register a push handler for this device (by IP); returns a function that unregisters it. */\n registerPush(onPush: (event: unknown) => void): () => void;\n /**\n * Whether the shared push receiver is actually listening. With push the device reports\n * its own changes, so the keepalive only has to renew the subscription and refresh the\n * zone status; without it (the port is taken \u2014 see issue #611) the poll is the ONLY\n * source of change and has to cover everything. Absent = assume no push.\n */\n pushActive?(): boolean;\n /** Per-device memory for answers that do not change while the device runs (see ProbeMemory). */\n probeMemory?: ProbeMemory;\n /** Schedule the keepalive handler; returns a function that cancels it. */\n scheduleKeepalive(handler: () => void, ms: number): () => void;\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 /** Report the name the device carries for itself, for the device object's label. */\n reportDeviceName?(name: string): void;\n /** Adapter log. */\n log: ControllerLog;\n /**\n * The device's command gate: every request is paced through it, and its signal is the\n * connection's shutdown flag \u2014 a closed gate ends pending waits and stops state writes\n * from a poll that was already in flight. Absent in older tests \u2192 no browsing.\n */\n gate?: CommandGate;\n}\n\n/**\n * Drives one MusicCast (YXC) device: read capabilities, build the object tree,\n * seed state from getStatus, and route commands both ways. Device pushes arrive\n * via the shared receiver as re-fetch signals; a keepalive poll renews the push\n * registration (the fix for the musiccast \"stops updating\" bug) and doubles as\n * the poll-only fallback when the push port is unavailable. Create-only.\n */\nexport class YxcDeviceController implements ConnectionHandle {\n private zones: string[] = [];\n private mediaBlocks: string[] = [];\n private cancelKeepalive: (() => void) | undefined;\n private cancelPush: (() => void) | undefined;\n private readonly dropDetector = new PollDropDetector();\n /** The tuner's current band, cached so a frequency write can supply it (setFreq needs band + freq). */\n private lastTunerBand = \"fm\";\n /** Each zone's currently selected input, from its status \u2014 see {@link zoneListeningTo}. */\n private readonly lastZoneInput = new Map<string, string>();\n /** The source the network player is currently on (netusb `input`, e.g. \"net_radio\"). */\n private lastNetusbInput = \"\";\n /** Which source currently feeds each zone's \"now playing\" block (v2.0.0 routing). */\n private readonly zonePlayerBlock = new Map<string, \"netusb\" | \"cd\">();\n /** Each zone's last-seen equalizer bands, cached so one band write can supply the other two. */\n private readonly lastEqualizer = new Map<string, { low: number; mid: number; high: number }>();\n /** Whether the device reports MusicCast-Link distribution (gates the dist poll and objects). */\n private hasDistribution = false;\n /** The device's last-seen distribution role (none/server/client), for the leave-group path. */\n private lastDistRole = \"none\";\n /** The tuner features (bands + preset mode) \u2014 a preset recall needs the band. */\n private tunerFeatures: YxcTunerFeatures | undefined;\n /** Whether the device reports the clock/alarm block (gates the clock poll). */\n private hasClock = false;\n /** The zones declaring `signal_info` (gates the audio-signal poll). */\n private signalZones: string[] = [];\n /** Whether netusb declares the MusicCast playlists / the play queue (gates their polls). */\n private hasMcPlaylist = false;\n private hasPlayQueue = false;\n /** Counts keepalive runs, so the safety-net sweep can run every Nth one under push. */\n private keepaliveRuns = 0;\n private browseEngine: BrowseEngine | undefined;\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: YxcControllerDeps,\n ) {}\n\n /**\n * Read capabilities, create the object tree, seed state, and wire up push +\n * keepalive.\n *\n * @returns true if the device reported capabilities and its tree was created\n */\n public async start(): Promise<boolean> {\n // Freshness guard for the (persisted) probe memory: ONE LIVE getDeviceInfo proves\n // the device behind this address is still the one the memory was learned from. A\n // swapped or factory-reset device answers a different identity \u2014 its remembered\n // YXC answers are dropped and re-probed; a transient failure keeps the memory and\n // leaves the liveness verdict to the zone check below. Also feeds the model line.\n let model: string | undefined;\n try {\n const info = await this.deps.client.getDeviceInfo();\n model = modelNameFrom(info);\n const version = (info as { system_version?: unknown } | null)?.system_version;\n const identity = `${model ?? \"\"}|${typeof version === \"number\" || typeof version === \"string\" ? version : \"\"}`;\n if (this.deps.probeMemory && this.deps.probeMemory.remembered(\"yxcIdentity\") !== identity) {\n this.deps.probeMemory.drop(\n key => key === \"features\" || key === \"name\" || key === \"model\" || key === \"yxcIdentity\",\n );\n this.deps.probeMemory.set(\"yxcIdentity\", identity);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getDeviceInfo failed (${errorMessage(e)})`);\n }\n // Capabilities and name are constant while the device runs, so on a reconnect \u2014\n // and, persisted, on a restart \u2014 they come from the per-device memory instead of\n // costing more round-trips on a connection that is being (re-)established anyway.\n // A capability set with no zone at all is not a MusicCast device answering \u2014 it is a\n // truncated or malformed answer. Returned to this run, but NOT remembered: remembering\n // it would freeze the device's shape for good (every later connect reads the memory),\n // and it would also disarm the liveness check below, which needs the zones.\n const capabilities = await this.remember(\n \"features\",\n async () => parseYxcFeatures(await this.deps.client.getFeatures()),\n features => features.zones.length > 0,\n );\n const objects = mapYxcToObjects(capabilities);\n if (objects.length === 0) {\n this.deps.log.warn(`${this.deviceId}: no capabilities reported \u2014 creating no objects`);\n return false;\n }\n // Parents before children (channels before their states) \u2014 created in order.\n for (const object of objects) {\n await this.deps.upsertObject(`${this.deviceId}.${object.id}`, object);\n }\n await this.setupSceneLists(capabilities);\n // The DAB scan counters are the one DAB detail the device reports only after a\n // station scan (status stays not_ready before) \u2014 seed the documented start state\n // (nothing scanned) so they are not left as valueless read states; a real scan\n // result overwrites them via the tuner play info.\n if (capabilities.media.includes(\"tuner\") && (capabilities.tuner?.bands ?? []).includes(\"dab\")) {\n this.emit(\"tuner.dab.totalStations\", 0);\n this.emit(\"tuner.dab.scanProgress\", 0);\n }\n if (model) {\n // The info channel and info.model already exist \u2014 the adapter creates them for\n // every device up front, so the card renders even while the device is offline.\n this.emit(\"info.model\", model);\n }\n // The name the user gave the device in the MusicCast app. Best-effort like the model\n // above: an older device that does not answer getNameText simply keeps its label.\n if (this.deps.reportDeviceName) {\n try {\n const name = await this.remember(\"name\", async () => zoneNameFrom(await this.deps.client.getNameText()));\n if (name) {\n this.deps.reportDeviceName(name);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getNameText failed (${errorMessage(e)})`);\n }\n }\n this.zones = capabilities.zones.map(zone => zone.id);\n // Zones in parallel \u2014 disjoint writes, and a zone stuck in its timeout must not hold\n // up the device's readiness.\n const zonesAnswered = await Promise.all(this.zones.map(zone => this.refreshZone(zone)));\n // The zone status is the one request of this start that ALWAYS goes to the device: the\n // capabilities above come from the probe memory on every reconnect, and model/name are\n // best-effort, so nothing before this point can tell a live device from a dead one. Without\n // the check a reconnect to a receiver that had lost power still reported \"ready \u2014\n // MusicCast \u2713\" out of memory while YNCA and XML failed honestly, and info.connection stayed\n // true for a device that was not there (krobi's RX-V6A, 2026-08-26). A device that answers\n // no zone at all is gone \u2014 a standby device still answers, it just reports power=standby.\n // No `zones.length > 0` escape any more: a device that declares NO zone cannot prove it\n // is alive either, and the whole point of this check is that nothing may report \"ready\"\n // out of memory. Everything before it \u2014 capabilities, model, name \u2014 can come from the\n // persisted memory without a single request reaching the device.\n if (!zonesAnswered.some(Boolean)) {\n this.deps.log.debug(`${this.deviceId}: no zone answered getStatus \u2014 device unreachable (YXC)`);\n return false;\n }\n this.mediaBlocks = capabilities.media;\n this.tunerFeatures = capabilities.tuner;\n this.hasClock = capabilities.clock !== undefined;\n this.signalZones = capabilities.zones.filter(zone => zone.funcs.includes(\"signal_info\")).map(zone => zone.id);\n this.hasMcPlaylist = capabilities.netusbFuncs?.includes(\"mc_playlist\") ?? false;\n this.hasPlayQueue = capabilities.netusbFuncs?.includes(\"play_queue\") ?? false;\n await this.setupBrowse(capabilities);\n await this.refreshMedia();\n // Seed the WHOLE player block with its cleared shape for every zone NOT playing a\n // media source: the routing above only writes to listening zones, so on a device\n // that starts on HDMI the block would sit valueless until the first media playback\n // (live 2.0.0 deployment check \u2014 same gap the pre-release audit found for source).\n if (this.mediaBlocks.includes(\"netusb\") || this.mediaBlocks.includes(\"cd\")) {\n for (const zone of this.zones.length > 0 ? this.zones : [\"main\"]) {\n if (!this.zonePlayerBlock.has(zone)) {\n this.emitPlayerUpdates(zone, PLAYER_CLEAR);\n }\n }\n }\n await this.refreshLists();\n this.hasDistribution = capabilities.hasDistribution ?? false;\n if (this.hasDistribution) {\n await this.refreshDistribution();\n }\n this.cancelPush = this.deps.registerPush(event => this.onPush(event));\n this.cancelKeepalive = this.deps.scheduleKeepalive(() => void this.keepalive(), KEEPALIVE_MS);\n // The adapter logs one combined \"ready\" line across all transports; this stays at debug.\n this.deps.log.debug(`${this.deviceId}: MusicCast device ready (YXC)`);\n return true;\n }\n\n /**\n * Ask the device once per adapter run and remember the answer for later reconnects.\n *\n * @param key what is being remembered\n * @param probe the request to run when nothing is remembered yet\n * @param isUsable optional plausibility check \u2014 an answer it rejects is used but not remembered\n * @returns the remembered or freshly fetched value\n */\n private remember<T>(key: string, probe: () => Promise<T>, isUsable?: (value: T) => boolean): Promise<T> {\n return this.deps.probeMemory ? this.deps.probeMemory.once(key, probe, isUsable) : probe();\n }\n\n /**\n * The zone a recall should be routed to: recalling a favourite does not just start it, it\n * also switches THAT zone to the source. Sending everything to the main zone (as this did\n * before) means someone listening in zone 2 gets their favourite in the living room\n * instead \u2014 and the main zone switched away from whatever it was playing.\n *\n * The zone actually listening to the source is the right target; the main zone is the\n * fallback when nothing matches, which is also every single-zone device.\n *\n * @param source the input the recall belongs to (a network source, or \"tuner\")\n * @returns the zone to route the recall to\n */\n private zoneListeningTo(source: string): string {\n if (!source) {\n return \"main\";\n }\n // Main first: on a device where several zones share the source, it is the natural target.\n if (this.lastZoneInput.get(\"main\") === source) {\n return \"main\";\n }\n for (const [zone, input] of this.lastZoneInput) {\n if (input === source) {\n return zone;\n }\n }\n return \"main\";\n }\n\n /**\n * Write a device-originated value \u2014 but never after the connection was closed. A poll\n * or a browse fetch that was already in flight when the adapter stopped would otherwise\n * still write into a tree that is being torn down.\n *\n * @param relativeId the state id relative to the device\n * @param value the value to write\n */\n private emit(relativeId: string, value: boolean | number | string): void {\n if (this.deps.gate?.closed) {\n return;\n }\n this.deps.setStateAck(`${this.deviceId}.${relativeId}`, value);\n }\n\n /**\n * Handle a state change: a user write (ack false) becomes a YXC 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 resolves to its number via the shared device memory \u2014 the titles may\n // have come over XML or YNCA while MusicCast owns the recall.\n const sceneMatch = /^(?:multiroom\\.(zone[234])\\.)?scene\\.recall$/.exec(stateId);\n if (sceneMatch && typeof value === \"string\" && !/^\\d+$/.test(value.trim())) {\n const zoneKey = sceneMatch[1] ?? \"main\";\n const resolved = resolveSceneNumber(value, this.deps.probeMemory, zoneKey);\n if (resolved === undefined) {\n // Same rule as the YNCA side: a write that goes nowhere leaves a trace.\n this.deps.log.debug(\n `${this.deviceId}: scene \"${value}\" is unknown for ${zoneKey} \u2014 write dropped ` +\n `(known: ${\n knownScenes(this.deps.probeMemory, zoneKey)\n .map(scene => scene.title)\n .join(\", \") || \"none yet\"\n })`,\n );\n return;\n }\n value = resolved;\n }\n // Multiroom writes need controller state (the cached role), so they bypass the pure command map.\n if (stateId === \"multiroom.group.leave\") {\n void this.leaveGroup();\n return;\n }\n if (stateId === \"multiroom.group.linkDevice\") {\n void this.linkClient(String(value));\n return;\n }\n const command = stateToYxc(stateId, value);\n if (command) {\n void this.applyCommand(stateId, command);\n }\n }\n\n /**\n * Register the supervisor's drop handler. MusicCast has no socket-drop event, so a\n * drop is inferred from a run of failed keepalive polls (see keepalive).\n *\n * @param cb invoked once when the device is judged gone\n */\n public onDrop(cb: (reason?: Error) => void): void {\n this.dropDetector.onDrop(cb);\n }\n\n /**\n * Create the browsing surface (#613) when the device has the netusb block: the\n * `netusb/getListInfo` + `setListControl` API drives an 8-line window under\n * `player.browse.*`. Skipped without a delay dep (older tests).\n *\n * @param capabilities the parsed getFeatures capabilities\n */\n private async setupBrowse(capabilities: YxcCapabilities): Promise<void> {\n const gate = this.deps.gate;\n if (!gate || !capabilities.media.includes(\"netusb\")) {\n return;\n }\n const inputs = capabilities.zones.find(zone => zone.id === \"main\")?.inputs ?? [];\n const driver = new YxcBrowseDriver(this.deps.client, inputs);\n this.browseEngine = await createBrowseSurface(driver, this.deviceId, {\n upsertObject: this.deps.upsertObject,\n emit: (id, value) => this.emit(id, value),\n log: this.deps.log,\n delay: ms => gate.delay(ms),\n });\n }\n\n /** Cancel the keepalive and unregister the push handler. Synchronous \u2014 safe from onUnload. */\n public close(): void {\n this.browseEngine?.close();\n // Closing the gate empties its queue and aborts its signal: queued requests are\n // dropped and every pending wait ends, so nothing writes after the teardown.\n this.deps.gate?.close();\n this.cancelKeepalive?.();\n this.cancelKeepalive = undefined;\n // Unregister from the shared push receiver \u2014 otherwise a push arriving after\n // teardown would setState on a controller the adapter has already dropped.\n this.cancelPush?.();\n this.cancelPush = undefined;\n }\n\n /**\n * Handle a device push: each named zone is re-fetched via getStatus, each named\n * media source via getPlayInfo (the push itself is a change signal, not a value\n * carrier). Refreshing only the named sources keeps a track-change push from\n * re-polling every source.\n *\n * @param event the parsed push event\n */\n private onPush(event: unknown): void {\n for (const zone of zonesToRefresh(event)) {\n if (this.zones.includes(zone)) {\n void this.refreshZone(zone);\n }\n }\n for (const block of mediaToRefresh(event)) {\n if (this.mediaBlocks.includes(block)) {\n void this.refreshMediaSource(block);\n }\n }\n // The favourites/recently-played lists announce their changes as flags in the push.\n const lists = netusbListsToRefresh(event);\n if (lists.presets && this.mediaBlocks.includes(\"netusb\")) {\n void this.refreshNetusbPresets();\n }\n if (lists.recent && this.mediaBlocks.includes(\"netusb\")) {\n void this.refreshNetusbRecent();\n }\n }\n\n /**\n * Poll every zone (which renews the push registration and refreshes state) and the\n * media sources. If every zone poll fails for three consecutive failed runs in a row,\n * the device is judged gone and a drop is reported so the supervisor can flip\n * info.connection and reconnect.\n */\n private async keepalive(): Promise<void> {\n // The keepalive is an async handler on an adapter timer: a rejection here is an\n // UNHANDLED rejection, and js-controller turns those into an adapter stop. Every step\n // below catches for itself today, so the guard is what makes that a guarantee instead\n // of something the next change has to remember.\n try {\n // Zones in parallel: their writes are disjoint and one zone stuck in its timeout must\n // not delay the others (a four-zone receiver used to poll them strictly in series).\n const zones = this.zones.length > 0 ? this.zones : [\"main\"];\n const anyOk = (await Promise.all(zones.map(zone => this.refreshZone(zone)))).some(Boolean);\n // Every request above already carried the subscription headers, so the push\n // registration is renewed either way. What still has to be polled depends on whether\n // push works: with push the device announces media, list and group changes itself, so\n // the full sweep only runs occasionally as a safety net (UDP can drop a packet);\n // without push it is the only way anything ever updates.\n this.keepaliveRuns++;\n const fullSweep = !this.deps.pushActive?.() || this.keepaliveRuns % PUSH_MODE_FULL_SWEEP_EVERY === 0;\n if (fullSweep) {\n await this.refreshMedia();\n await this.refreshLists();\n if (this.hasDistribution) {\n await this.refreshDistribution();\n }\n }\n this.dropDetector.record(anyOk);\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: keepalive poll failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Refresh the list-shaped surfaces: the netusb favourites and recently-played\n * lists, the tuner preset lists, and the clock/alarm settings. Each is\n * best-effort \u2014 a device without the feature answers with an error code and the\n * state simply stays.\n */\n private async refreshLists(): Promise<void> {\n if (this.mediaBlocks.includes(\"netusb\")) {\n await this.refreshNetusbPresets();\n await this.refreshNetusbRecent();\n if (this.hasMcPlaylist) {\n await this.refreshPlaylists();\n }\n if (this.hasPlayQueue) {\n await this.refreshPlayQueue();\n }\n }\n if (this.mediaBlocks.includes(\"tuner\")) {\n await this.refreshTunerPresets();\n }\n if (this.hasClock) {\n await this.refreshClock();\n }\n await this.refreshSignalInfo();\n }\n\n /** Fetch the MusicCast playlist names and write the JSON list state. */\n private async refreshPlaylists(): Promise<void> {\n try {\n const update = parseYxcPlaylistNames(await this.deps.client.getMcPlaylistName());\n if (update) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getMcPlaylistName failed: ${errorMessage(e)}`);\n }\n }\n\n /** Fetch the network player's play queue and write the JSON state. */\n private async refreshPlayQueue(): Promise<void> {\n try {\n const update = parseYxcPlayQueue(await this.deps.client.getPlayQueue());\n if (update) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getPlayQueue failed: ${errorMessage(e)}`);\n }\n }\n\n /** Fetch each declaring zone's audio-signal info and write the signal states. */\n private async refreshSignalInfo(): Promise<void> {\n for (const zone of this.signalZones) {\n try {\n for (const update of parseYxcSignalInfo(await this.deps.client.getSignalInfo(zone), zone)) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getSignalInfo(${zone}) failed: ${errorMessage(e)}`);\n }\n }\n }\n\n /** Fetch the stored netusb favourites and write the JSON list state. */\n private async refreshNetusbPresets(): Promise<void> {\n try {\n const update = parseYxcPresetList(await this.deps.client.getPresetInfo());\n if (update) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getPresetInfo failed: ${errorMessage(e)}`);\n }\n }\n\n /** Fetch the recently-played list and write the JSON list state. */\n private async refreshNetusbRecent(): Promise<void> {\n try {\n const update = parseYxcRecentList(await this.deps.client.getRecentInfo());\n if (update) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getRecentInfo failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Fetch the tuner preset lists \u2014 the shared `common` list, or one per band on\n * devices with separate lists \u2014 and write the JSON state.\n */\n private async refreshTunerPresets(): Promise<void> {\n const bands = this.tunerFeatures?.presetType === \"common\" ? [\"common\"] : (this.tunerFeatures?.bands ?? [\"fm\"]);\n const byBand: Record<string, unknown> = {};\n for (const band of bands) {\n try {\n byBand[band] = await this.deps.client.getTunerPresetInfo(band);\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getTunerPresetInfo(${band}) failed: ${errorMessage(e)}`);\n }\n }\n const update = parseYxcTunerPresetLists(byBand);\n if (update) {\n this.emit(update.id, update.value);\n }\n }\n\n /** Fetch the clock/alarm settings and write the read-only clock states. */\n private async refreshClock(): Promise<void> {\n try {\n for (const update of parseYxcClock(await this.deps.client.getClockSettings())) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getClockSettings failed: ${errorMessage(e)}`);\n }\n }\n\n /** Refresh every player source the device offers (network player, cd, tuner). */\n private async refreshMedia(): Promise<void> {\n // The three sources (netusb/cd/tuner) write disjoint states \u2014 fetch them together.\n await Promise.all(this.mediaBlocks.map(block => this.refreshMediaSource(block)));\n }\n\n /**\n * Fetch one media source's play info and write the parsed states with ack. The\n * source picks the getPlayInfo argument and the parser: netusb and cd share the\n * play-info shape (different channel), the tuner has its own band/frequency/RDS.\n *\n * @param block the media block (`netusb`, `cd`, `tuner`)\n */\n private async refreshMediaSource(block: string): Promise<void> {\n const arg = block === \"netusb\" ? undefined : block;\n try {\n const info = await this.deps.client.getPlayInfo(arg);\n if (block === \"tuner\") {\n for (const update of parseYxcTunerInfo(info)) {\n this.emit(update.id, update.value);\n if (update.id === \"tuner.band\") {\n this.lastTunerBand = String(update.value);\n }\n }\n return;\n }\n const source: \"netusb\" | \"cd\" = block === \"cd\" ? \"cd\" : \"netusb\";\n const updates = parseYxcPlayInfo(info, source);\n if (source === \"netusb\") {\n const active = updates.find(update => update.id === \"player.source\");\n if (typeof active?.value === \"string\") {\n this.lastNetusbInput = active.value;\n }\n }\n this.routePlayerBlock(source, updates);\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getPlayInfo(${arg ?? \"\"}) failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Which player source a zone's input feeds into its \"now playing\" block: `cd` for\n * the disc input, `netusb` when the zone's input IS the network player's active\n * source. Anything else (HDMI, analog, tuner) plays no media block.\n *\n * @param input the zone's currently selected input\n * @returns the feeding source, or undefined when the input is no media player\n */\n private playerBlockFor(input: string | undefined): \"netusb\" | \"cd\" | undefined {\n if (input === \"cd\" && this.mediaBlocks.includes(\"cd\")) {\n return \"cd\";\n }\n if (input !== undefined && input !== \"\" && input === this.lastNetusbInput && this.mediaBlocks.includes(\"netusb\")) {\n return \"netusb\";\n }\n return undefined;\n }\n\n /**\n * Route one source's play info into the \"now playing\" block of every zone listening\n * to it (v2.0.0): main flat, the other zones under their multiroom folder. A zone\n * that LEFT the source gets its block cleared once \u2014 the previous program's metadata\n * must not linger under a zone that no longer plays it. Drive-own `player.cd.*`\n * extras are device-global and emitted once, unprefixed.\n *\n * @param block the source the updates came from\n * @param updates the parsed flat player updates\n */\n private routePlayerBlock(block: \"netusb\" | \"cd\", updates: StateValue[]): void {\n const zones = this.zones.length > 0 ? this.zones : [\"main\"];\n for (const zone of zones) {\n const expected = this.playerBlockFor(this.lastZoneInput.get(zone));\n const previous = this.zonePlayerBlock.get(zone);\n if (previous === block && expected !== block) {\n // The zone left OUR source \u2014 clear; the new source's refresh fills its own values.\n this.zonePlayerBlock.delete(zone);\n this.emitPlayerUpdates(zone, PLAYER_CLEAR);\n }\n if (expected === block) {\n if (previous !== block) {\n this.zonePlayerBlock.set(zone, block);\n if (previous !== undefined) {\n this.emitPlayerUpdates(zone, PLAYER_CLEAR);\n }\n }\n this.emitPlayerUpdates(zone, updates);\n }\n }\n for (const update of updates) {\n if (update.id.startsWith(\"player.cd.\")) {\n this.emit(update.id, update.value);\n }\n }\n }\n\n /**\n * One scene.list JSON per scene-capable zone (v2.0.0): every recall slot the zone\n * declares (getFeatures scene_num), titled from the shared per-device memory where\n * another transport reported titles (XML Scene_Sel_Item / YNCA SCENExNAME). A\n * MusicCast-only device has no title source and lists its slots with empty titles \u2014\n * the COUNT is what its getFeatures declares (review finding: the promised list was\n * missing entirely on devices whose only transport is MusicCast).\n *\n * @param capabilities the device's parsed getFeatures capabilities\n */\n private async setupSceneLists(capabilities: YxcCapabilities): Promise<void> {\n for (const zone of capabilities.zones) {\n if (!zone.funcs.includes(\"scene\") || zone.sceneNum === undefined || zone.sceneNum <= 0) {\n continue;\n }\n const titles = new Map(knownScenes(this.deps.probeMemory, zone.id).map(scene => [scene.num, scene.title]));\n const list = Array.from({ length: zone.sceneNum }, (_unused, i) => ({\n num: i + 1,\n title: titles.get(i + 1) ?? \"\",\n }));\n const id = `${zonePrefix(zone.id)}scene.list`;\n await this.deps.upsertObject(`${this.deviceId}.${id}`, {\n id,\n type: \"state\",\n common: { name: tName(\"scenesNumberTitle\"), type: \"string\", role: \"json\", read: true, write: false },\n });\n this.emit(id, JSON.stringify(list));\n }\n }\n\n /**\n * Re-evaluate which source feeds a zone's player block after ITS input changed:\n * clear the block when the zone left a media source, and fetch the joined source's\n * play info right away so the block fills now, not at the next sweep.\n *\n * @param zone the zone whose input just changed\n */\n private retargetZonePlayer(zone: string): void {\n const expected = this.playerBlockFor(this.lastZoneInput.get(zone));\n const previous = this.zonePlayerBlock.get(zone);\n if (previous === expected) {\n return;\n }\n if (previous !== undefined) {\n this.zonePlayerBlock.delete(zone);\n this.emitPlayerUpdates(zone, PLAYER_CLEAR);\n }\n if (expected !== undefined) {\n // routePlayerBlock (inside this refresh) records the zone's new source.\n void this.refreshMediaSource(expected);\n }\n }\n\n /**\n * Emit flat player updates into one zone's block (main flat, zones prefixed),\n * skipping the device-global drive extras.\n *\n * @param zone the target zone\n * @param updates the flat player updates\n */\n private emitPlayerUpdates(zone: string, updates: StateValue[]): void {\n const prefix = zonePrefix(zone);\n for (const update of updates) {\n if (!update.id.startsWith(\"player.cd.\")) {\n this.emit(`${prefix}${update.id}`, update.value);\n }\n }\n }\n\n /**\n * Fetch the MusicCast-Link distribution info and write the parsed dist states with ack,\n * caching the role for the leave-group path.\n */\n private async refreshDistribution(): Promise<void> {\n try {\n const info = await this.deps.client.getDistributionInfo();\n for (const update of parseYxcDistribution(info)) {\n this.emit(update.id, update.value);\n if (update.id === \"multiroom.group.role\") {\n this.lastDistRole = String(update.value);\n }\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getDistributionInfo failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Leave the current MusicCast-Link group: a server stops distributing, a client clears\n * its membership. Then re-read the distribution state so the tree reflects the change.\n */\n private async leaveGroup(): Promise<void> {\n try {\n if (this.lastDistRole === \"server\") {\n await this.deps.client.stopDistribution();\n } else {\n await this.deps.client.setClientInfo({ group_id: \"\", zone: [\"main\"] });\n }\n await this.refreshDistribution();\n } catch (e) {\n // A user action failing must be visible \u2014 warn, like every other write command.\n this.deps.log.warn(`${this.deviceId}: leaveGroup failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Form a MusicCast-Link group with another configured device: give the client the shared\n * group id, add it to this device's roster as the server, and start distributing. The group\n * id is derived from this device's id, so re-linking reuses the same group rather than a new one.\n *\n * @param clientIp the IP of the client device to add (must be a configured device)\n */\n private async linkClient(clientIp: string): Promise<void> {\n const clientClient = this.deps.clientFor?.(clientIp);\n if (!clientClient) {\n this.deps.log.warn(`${this.deviceId}: cannot link ${clientIp} \u2014 not a known device`);\n return;\n }\n try {\n const groupId = createHash(\"md5\").update(this.deviceId).digest(\"hex\");\n await clientClient.setClientInfo({ group_id: groupId, zone: [\"main\"] });\n await this.deps.client.setServerInfo({ group_id: groupId, zone: \"main\", type: \"add\", client_list: [clientIp] });\n await this.deps.client.startDistribution(0);\n await this.refreshDistribution();\n } catch (e) {\n // A user action failing must be visible \u2014 warn, like every other write command.\n this.deps.log.warn(`${this.deviceId}: linkClient(${clientIp}) failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Fetch a zone's status and write its amp states with ack.\n *\n * @param zone the zone to refresh\n * @returns true if the status was fetched, false if the request failed\n */\n private async refreshZone(zone: string): Promise<boolean> {\n try {\n const status = await this.deps.client.getStatus(zone);\n const updates = parseYxcStatus(status, zone);\n for (const update of updates) {\n this.emit(update.id, update.value);\n // The EXACT id, not a suffix: this value decides which source a zone's player block\n // and its transport buttons follow. A future status field ending in \"input\" would\n // have bent that routing silently.\n if (update.id === `${zonePrefix(zone)}input` && typeof update.value === \"string\") {\n const previous = this.lastZoneInput.get(zone);\n this.lastZoneInput.set(zone, update.value);\n if (previous !== update.value) {\n // The zone changed its input \u2014 re-target its player block NOW. Media\n // pushes alone cannot cover this: a zone leaving a still-playing source\n // (or joining one another zone already plays) changes nothing about the\n // source itself, so no netusb/cd push ever arrives (2.0.0 review finding).\n this.retargetZonePlayer(zone);\n }\n }\n }\n this.cacheEqualizer(zone, updates);\n return true;\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getStatus(${zone}) failed: ${errorMessage(e)}`);\n return false;\n }\n }\n\n /**\n * Cache a zone's equalizer bands from its status updates, so a later single-band\n * write can send setEqualizer with all three (the device sets them together).\n *\n * @param zone the zone the updates belong to\n * @param updates the parsed status updates for that zone\n */\n private cacheEqualizer(zone: string, updates: StateValue[]): void {\n // Same definition the status parser uses \u2014 see lib/yxc/zones.ts on why this must not\n // be spelled out a second time.\n const prefix = zonePrefix(zone);\n const band = (b: string): number | undefined => {\n const u = updates.find(x => x.id === `${prefix}sound.equalizer.${b}`);\n return typeof u?.value === \"number\" ? u.value : undefined;\n };\n const low = band(\"low\");\n const mid = band(\"mid\");\n const high = band(\"high\");\n if (low === undefined && mid === undefined && high === undefined) {\n return;\n }\n const cur = this.lastEqualizer.get(zone);\n if (!cur) {\n // No cache yet: only a COMPLETE triple may seed it. Filling the gaps with 0 would hand\n // the write path exactly the invented 0/0/0 it refuses to send \u2014 that safeguard only\n // works if this cache never lies about a band.\n if (low === undefined || mid === undefined || high === undefined) {\n return;\n }\n this.lastEqualizer.set(zone, { low, mid, high });\n return;\n }\n this.lastEqualizer.set(zone, { low: low ?? cur.low, mid: mid ?? cur.mid, high: high ?? cur.high });\n }\n\n /**\n * Apply a mapped command. A plain command runs its client call directly; the two\n * commands that need controller-cached state (equalizer bands, tuner band) are\n * completed here \u2014 the only place that state lives.\n *\n * @param stateId the written state id, for the failure log line\n * @param command the YXC command to apply\n */\n private async applyCommand(stateId: string, command: YxcCommand): Promise<void> {\n try {\n switch (command.kind) {\n case \"run\":\n await command.run(this.deps.client);\n break;\n case \"equalizer\": {\n // The device sets all three bands in one call, so the other two come from the\n // cache. Without a cached set we must NOT invent 0/0/0 \u2014 that would silently\n // flatten the user's other two bands. The cache fills from a zone status, so\n // fetch one first; only if even that fails is the write refused (with a warning).\n const { zone, band, value } = command;\n let current = this.lastEqualizer.get(zone);\n if (!current) {\n await this.refreshZone(zone);\n current = this.lastEqualizer.get(zone);\n }\n if (!current) {\n this.deps.log.warn(\n `${this.deviceId}: not writing ${stateId} \u2014 the device has not reported its equalizer bands yet`,\n );\n break;\n }\n const next = { ...current, [band]: value };\n // Cache BEFORE the round-trip: a second band written straight afterwards\n // reads this cache at dispatch \u2014 caching after the await would make it\n // compute from the pre-write triple and undo this band on the device.\n this.lastEqualizer.set(zone, next);\n await this.deps.client.setEqualizer(next.low, next.mid, next.high, zone);\n break;\n }\n case \"tunerBand\":\n // Remember it BEFORE the round-trip: a frequency written straight afterwards\n // reads this cache synchronously at dispatch, while the band call may still\n // be in flight \u2014 and the poll that would report it back runs minutes later.\n this.lastTunerBand = command.band;\n await this.deps.client.setBand(command.band);\n break;\n case \"tunerFreq\":\n await this.deps.client.setFreq(this.lastTunerBand, command.value);\n break;\n case \"tunerPreset\": {\n // Shared-list devices recall on `common`; separate-list devices on the current band.\n const band = this.tunerFeatures?.presetType === \"common\" ? \"common\" : this.lastTunerBand;\n await this.deps.client.recallTunerPreset(band, command.value, this.zoneListeningTo(\"tuner\"));\n break;\n }\n case \"netusbPreset\":\n await this.deps.client.recallPreset(command.value, this.zoneListeningTo(this.lastNetusbInput));\n break;\n case \"netusbRecent\":\n await this.deps.client.recallRecentItem(command.value, this.zoneListeningTo(this.lastNetusbInput));\n break;\n case \"playerTransport\": {\n // The unified block's buttons act on whatever the ZONE is playing (v2.0.0) \u2014\n // derived FRESH from the zone's input, never from the routing map: a stale\n // map entry would send the command to the source the zone just left.\n const block = this.playerBlockFor(this.lastZoneInput.get(command.zone));\n if (block === undefined) {\n this.deps.log.debug(`${this.deviceId}: ${stateId} ignored \u2014 ${command.zone} is not playing a media source`);\n break;\n }\n await this.runTransport(block, command.action);\n break;\n }\n }\n } catch (e) {\n this.deps.log.warn(`${this.deviceId}: write to ${stateId} failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Run one transport action on the given player source. The CD transport routes\n * through the one `setCDPlayback(action)` method (not the per-action helpers, one\n * of which sends the wrong command in the library).\n *\n * @param block the source the zone is playing\n * @param action the transport action\n */\n private async runTransport(block: \"netusb\" | \"cd\", action: PlayerTransport): Promise<void> {\n const client = this.deps.client;\n if (block === \"netusb\") {\n const net: Record<PlayerTransport, () => Promise<unknown>> = {\n play: () => client.playNet(),\n pause: () => client.pauseNet(),\n stop: () => client.stopNet(),\n next: () => client.nextNet(),\n prev: () => client.prevNet(),\n repeatToggle: () => client.toggleNetRepeat(),\n shuffleToggle: () => client.toggleNetShuffle(),\n };\n await net[action]();\n return;\n }\n const cd: Record<PlayerTransport, () => Promise<unknown>> = {\n play: () => client.setCDPlayback(\"play\"),\n pause: () => client.setCDPlayback(\"pause\"),\n stop: () => client.setCDPlayback(\"stop\"),\n next: () => client.setCDPlayback(\"next\"),\n prev: () => client.setCDPlayback(\"previous\"),\n repeatToggle: () => client.toggleCDRepeat(),\n shuffleToggle: () => client.toggleCDShuffle(),\n };\n await cd[action]();\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createHash } from \"node:crypto\";\nimport { parseYxcFeatures, type YxcCapabilities, type YxcTunerFeatures } from \"./capability\";\nimport { mapYxcToObjects } from \"./object-mapper\";\nimport {\n parseYxcClock,\n parseYxcDistribution,\n parseYxcPlayInfo,\n parseYxcPlaylistNames,\n parseYxcPlayQueue,\n parseYxcPresetList,\n parseYxcRecentList,\n parseYxcSignalInfo,\n parseYxcStatus,\n parseYxcTunerInfo,\n parseYxcTunerPresetLists,\n PLAYER_CLEAR,\n stateToYxc,\n type PlayerTransport,\n type YxcCommand,\n} from \"./command-mapper\";\nimport { mediaToRefresh, netusbListsToRefresh, zonesToRefresh } from \"./push\";\nimport type { ObjectDef } from \"../catalog/types\";\nimport { tName } from \"../i18n\";\nimport type { StateValue } from \"../types\";\nimport type { ConnectionHandle, ControllerLog } from \"../controller\";\nimport { errorMessage } from \"../util\";\nimport { PollDropDetector } from \"../lifecycle/poll-drop-detector\";\nimport type { ProbeMemory } from \"../lifecycle/probe-memory\";\nimport { zonePrefix } from \"./zones\";\nimport { knownScenes, resolveSceneNumber } from \"../catalog/scene-titles\";\nimport type { CommandGate } from \"../lifecycle/command-gate\";\nimport type { BrowseEngine } from \"../browse/browse-engine\";\nimport { createBrowseSurface } from \"../browse/surface\";\nimport { YxcBrowseDriver } from \"../browse/yxc-browse-driver\";\n\n/** Renew interval for the push registration + state poll, well under the ~20 min expiry. */\nconst KEEPALIVE_MS = 5 * 60 * 1000;\n\n/**\n * With push working, run the full media/list/group sweep only every Nth keepalive (6 \u00D7 5 min\n * = every 30 minutes) \u2014 a safety net against a dropped UDP packet, not the primary path.\n */\nconst PUSH_MODE_FULL_SWEEP_EVERY = 6;\n\n/**\n * Extract the model name from a getDeviceInfo response, if it carries a non-empty one.\n *\n * @param deviceInfo the getDeviceInfo response\n * @returns the model name, or undefined\n */\nfunction modelNameFrom(deviceInfo: unknown): string | undefined {\n const model = (deviceInfo as { model_name?: unknown } | null)?.model_name;\n return typeof model === \"string\" && model.length > 0 ? model : undefined;\n}\n\n/**\n * Extract the name a user gave this device from a getNameText response.\n *\n * MusicCast keeps it as the main zone's text \u2014 that is the name shown in the app and\n * the one people recognise (\"Wohnzimmer\"). A device whose zone was never renamed\n * answers with a generic zone name; the caller filters those out.\n *\n * @param nameText the getNameText response\n * @returns the main zone's text, or undefined\n */\nexport function zoneNameFrom(nameText: unknown): string | undefined {\n const zones = (nameText as { zone_list?: unknown } | null)?.zone_list;\n if (!Array.isArray(zones)) {\n return undefined;\n }\n for (const zone of zones) {\n if (typeof zone !== \"object\" || zone === null) {\n continue;\n }\n const { id, text } = zone as { id?: unknown; text?: unknown };\n if (id === \"main\" && typeof text === \"string\" && text.trim().length > 0) {\n return text.trim();\n }\n }\n return undefined;\n}\n\nimport type { YxcClientLike } from \"./client-contract\";\n\n// Re-exported so existing importers (the tests' fakes) keep resolving it from here.\nexport type { YxcClientLike };\n\n/** The adapter callbacks the controller drives \u2014 narrow, so no adapter mock is needed in tests. */\nexport interface YxcControllerDeps {\n /** The YXC (MusicCast) client for this device. */\n client: YxcClientLike;\n /** Resolve another configured device's client by IP, for forming a multiroom group. */\n clientFor?: (ip: string) => YxcClientLike | undefined;\n /** Register a push handler for this device (by IP); returns a function that unregisters it. */\n registerPush(onPush: (event: unknown) => void): () => void;\n /**\n * Whether the shared push receiver is actually listening. With push the device reports\n * its own changes, so the keepalive only has to renew the subscription and refresh the\n * zone status; without it (the port is taken \u2014 see issue #611) the poll is the ONLY\n * source of change and has to cover everything. Absent = assume no push.\n */\n pushActive?(): boolean;\n /** Per-device memory for answers that do not change while the device runs (see ProbeMemory). */\n probeMemory?: ProbeMemory;\n /** Schedule the keepalive handler; returns a function that cancels it. */\n scheduleKeepalive(handler: () => void, ms: number): () => void;\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 /** Report the name the device carries for itself, for the device object's label. */\n reportDeviceName?(name: string): void;\n /** Adapter log. */\n log: ControllerLog;\n /**\n * The device's command gate: every request is paced through it, and its signal is the\n * connection's shutdown flag \u2014 a closed gate ends pending waits and stops state writes\n * from a poll that was already in flight. Absent in older tests \u2192 no browsing.\n */\n gate?: CommandGate;\n}\n\n/**\n * Drives one MusicCast (YXC) device: read capabilities, build the object tree,\n * seed state from getStatus, and route commands both ways. Device pushes arrive\n * via the shared receiver as re-fetch signals; a keepalive poll renews the push\n * registration (the fix for the musiccast \"stops updating\" bug) and doubles as\n * the poll-only fallback when the push port is unavailable. Create-only.\n */\nexport class YxcDeviceController implements ConnectionHandle {\n private zones: string[] = [];\n private mediaBlocks: string[] = [];\n private cancelKeepalive: (() => void) | undefined;\n private cancelPush: (() => void) | undefined;\n private readonly dropDetector = new PollDropDetector();\n /** The tuner's current band, cached so a frequency write can supply it (setFreq needs band + freq). */\n private lastTunerBand = \"fm\";\n /** Each zone's currently selected input, from its status \u2014 see {@link zoneListeningTo}. */\n private readonly lastZoneInput = new Map<string, string>();\n /** The source the network player is currently on (netusb `input`, e.g. \"net_radio\"). */\n private lastNetusbInput = \"\";\n /** Which source currently feeds each zone's \"now playing\" block (v2.0.0 routing). */\n private readonly zonePlayerBlock = new Map<string, \"netusb\" | \"cd\">();\n /** Each zone's last-seen equalizer bands, cached so one band write can supply the other two. */\n private readonly lastEqualizer = new Map<string, { low: number; mid: number; high: number }>();\n /** Whether the device reports MusicCast-Link distribution (gates the dist poll and objects). */\n private hasDistribution = false;\n /** The device's last-seen distribution role (none/server/client), for the leave-group path. */\n private lastDistRole = \"none\";\n /** The tuner features (bands + preset mode) \u2014 a preset recall needs the band. */\n private tunerFeatures: YxcTunerFeatures | undefined;\n /** Whether the device reports the clock/alarm block (gates the clock poll). */\n private hasClock = false;\n /** The zones declaring `signal_info` (gates the audio-signal poll). */\n private signalZones: string[] = [];\n /** Whether netusb declares the MusicCast playlists / the play queue (gates their polls). */\n private hasMcPlaylist = false;\n private hasPlayQueue = false;\n /** Counts keepalive runs, so the safety-net sweep can run every Nth one under push. */\n private keepaliveRuns = 0;\n private browseEngine: BrowseEngine | undefined;\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: YxcControllerDeps,\n ) {}\n\n /**\n * Read capabilities, create the object tree, seed state, and wire up push +\n * keepalive.\n *\n * @returns true if the device reported capabilities and its tree was created\n */\n public async start(): Promise<boolean> {\n // Freshness guard for the (persisted) probe memory: ONE LIVE getDeviceInfo proves\n // the device behind this address is still the one the memory was learned from. A\n // swapped or factory-reset device answers a different identity \u2014 its remembered\n // YXC answers are dropped and re-probed; a transient failure keeps the memory and\n // leaves the liveness verdict to the zone check below. Also feeds the model line.\n let model: string | undefined;\n try {\n const info = await this.deps.client.getDeviceInfo();\n model = modelNameFrom(info);\n const version = (info as { system_version?: unknown } | null)?.system_version;\n const identity = `${model ?? \"\"}|${typeof version === \"number\" || typeof version === \"string\" ? version : \"\"}`;\n if (this.deps.probeMemory && this.deps.probeMemory.remembered(\"yxcIdentity\") !== identity) {\n this.deps.probeMemory.drop(\n key => key === \"features\" || key === \"name\" || key === \"model\" || key === \"yxcIdentity\",\n );\n this.deps.probeMemory.set(\"yxcIdentity\", identity);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getDeviceInfo failed (${errorMessage(e)})`);\n }\n // Capabilities and name are constant while the device runs, so on a reconnect \u2014\n // and, persisted, on a restart \u2014 they come from the per-device memory instead of\n // costing more round-trips on a connection that is being (re-)established anyway.\n // A capability set with no zone at all is not a MusicCast device answering \u2014 it is a\n // truncated or malformed answer. Returned to this run, but NOT remembered: remembering\n // it would freeze the device's shape for good (every later connect reads the memory),\n // and it would also disarm the liveness check below, which needs the zones.\n const capabilities = await this.remember(\n \"features\",\n async () => parseYxcFeatures(await this.deps.client.getFeatures()),\n features => features.zones.length > 0,\n );\n const objects = mapYxcToObjects(capabilities);\n if (objects.length === 0) {\n this.deps.log.warn(`${this.deviceId}: no capabilities reported \u2014 creating no objects`);\n return false;\n }\n // Parents before children (channels before their states) \u2014 created in order.\n for (const object of objects) {\n await this.deps.upsertObject(`${this.deviceId}.${object.id}`, object);\n }\n await this.setupSceneLists(capabilities);\n // The DAB scan counters are the one DAB detail the device reports only after a\n // station scan (status stays not_ready before) \u2014 seed the documented start state\n // (nothing scanned) so they are not left as valueless read states; a real scan\n // result overwrites them via the tuner play info.\n if (capabilities.media.includes(\"tuner\") && (capabilities.tuner?.bands ?? []).includes(\"dab\")) {\n this.emit(\"tuner.dab.totalStations\", 0);\n this.emit(\"tuner.dab.scanProgress\", 0);\n }\n if (model) {\n // The info channel and info.model already exist \u2014 the adapter creates them for\n // every device up front, so the card renders even while the device is offline.\n this.emit(\"info.model\", model);\n }\n // The name the user gave the device in the MusicCast app. Best-effort like the model\n // above: an older device that does not answer getNameText simply keeps its label.\n if (this.deps.reportDeviceName) {\n try {\n const name = await this.remember(\"name\", async () => zoneNameFrom(await this.deps.client.getNameText()));\n if (name) {\n this.deps.reportDeviceName(name);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getNameText failed (${errorMessage(e)})`);\n }\n }\n this.zones = capabilities.zones.map(zone => zone.id);\n // Zones in parallel \u2014 disjoint writes, and a zone stuck in its timeout must not hold\n // up the device's readiness.\n const zonesAnswered = await Promise.all(this.zones.map(zone => this.refreshZone(zone)));\n // The zone status is the one request of this start that ALWAYS goes to the device: the\n // capabilities above come from the probe memory on every reconnect, and model/name are\n // best-effort, so nothing before this point can tell a live device from a dead one. Without\n // the check a reconnect to a receiver that had lost power still reported \"ready \u2014\n // MusicCast \u2713\" out of memory while YNCA and XML failed honestly, and info.connection stayed\n // true for a device that was not there (krobi's RX-V6A, 2026-08-26). A device that answers\n // no zone at all is gone \u2014 a standby device still answers, it just reports power=standby.\n // No `zones.length > 0` escape any more: a device that declares NO zone cannot prove it\n // is alive either, and the whole point of this check is that nothing may report \"ready\"\n // out of memory. Everything before it \u2014 capabilities, model, name \u2014 can come from the\n // persisted memory without a single request reaching the device.\n if (!zonesAnswered.some(Boolean)) {\n this.deps.log.debug(`${this.deviceId}: no zone answered getStatus \u2014 device unreachable (YXC)`);\n return false;\n }\n this.mediaBlocks = capabilities.media;\n this.tunerFeatures = capabilities.tuner;\n this.hasClock = capabilities.clock !== undefined;\n this.signalZones = capabilities.zones.filter(zone => zone.funcs.includes(\"signal_info\")).map(zone => zone.id);\n this.hasMcPlaylist = capabilities.netusbFuncs?.includes(\"mc_playlist\") ?? false;\n this.hasPlayQueue = capabilities.netusbFuncs?.includes(\"play_queue\") ?? false;\n await this.setupBrowse(capabilities);\n await this.refreshMedia();\n // Seed the WHOLE player block with its cleared shape for every zone NOT playing a\n // media source: the routing above only writes to listening zones, so on a device\n // that starts on HDMI the block would sit valueless until the first media playback\n // (live 2.0.0 deployment check \u2014 same gap the pre-release audit found for source).\n if (this.mediaBlocks.includes(\"netusb\") || this.mediaBlocks.includes(\"cd\")) {\n for (const zone of this.zones.length > 0 ? this.zones : [\"main\"]) {\n if (!this.zonePlayerBlock.has(zone)) {\n this.emitPlayerUpdates(zone, PLAYER_CLEAR);\n }\n }\n }\n await this.refreshLists();\n this.hasDistribution = capabilities.hasDistribution ?? false;\n if (this.hasDistribution) {\n await this.refreshDistribution();\n }\n this.cancelPush = this.deps.registerPush(event => this.onPush(event));\n this.cancelKeepalive = this.deps.scheduleKeepalive(() => void this.keepalive(), KEEPALIVE_MS);\n // The adapter logs one combined \"ready\" line across all transports; this stays at debug.\n this.deps.log.debug(`${this.deviceId}: MusicCast device ready (YXC)`);\n return true;\n }\n\n /**\n * Ask the device once per adapter run and remember the answer for later reconnects.\n *\n * @param key what is being remembered\n * @param probe the request to run when nothing is remembered yet\n * @param isUsable optional plausibility check \u2014 an answer it rejects is used but not remembered\n * @returns the remembered or freshly fetched value\n */\n private remember<T>(key: string, probe: () => Promise<T>, isUsable?: (value: T) => boolean): Promise<T> {\n return this.deps.probeMemory ? this.deps.probeMemory.once(key, probe, isUsable) : probe();\n }\n\n /**\n * The zone a recall should be routed to: recalling a favourite does not just start it, it\n * also switches THAT zone to the source. Sending everything to the main zone (as this did\n * before) means someone listening in zone 2 gets their favourite in the living room\n * instead \u2014 and the main zone switched away from whatever it was playing.\n *\n * The zone actually listening to the source is the right target; the main zone is the\n * fallback when nothing matches, which is also every single-zone device.\n *\n * @param source the input the recall belongs to (a network source, or \"tuner\")\n * @returns the zone to route the recall to\n */\n private zoneListeningTo(source: string): string {\n if (!source) {\n return \"main\";\n }\n // Main first: on a device where several zones share the source, it is the natural target.\n if (this.lastZoneInput.get(\"main\") === source) {\n return \"main\";\n }\n for (const [zone, input] of this.lastZoneInput) {\n if (input === source) {\n return zone;\n }\n }\n return \"main\";\n }\n\n /**\n * Write a device-originated value \u2014 but never after the connection was closed. A poll\n * or a browse fetch that was already in flight when the adapter stopped would otherwise\n * still write into a tree that is being torn down.\n *\n * @param relativeId the state id relative to the device\n * @param value the value to write\n */\n private emit(relativeId: string, value: boolean | number | string): void {\n if (this.deps.gate?.closed) {\n return;\n }\n this.deps.setStateAck(`${this.deviceId}.${relativeId}`, value);\n }\n\n /**\n * Handle a state change: a user write (ack false) becomes a YXC 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 resolves to its number via the shared device memory \u2014 the titles may\n // have come over XML or YNCA while MusicCast owns the recall.\n const sceneMatch = /^(?:multiroom\\.(zone[234])\\.)?scene\\.recall$/.exec(stateId);\n if (sceneMatch && typeof value === \"string\" && !/^\\d+$/.test(value.trim())) {\n const zoneKey = sceneMatch[1] ?? \"main\";\n const resolved = resolveSceneNumber(value, this.deps.probeMemory, zoneKey);\n if (resolved === undefined) {\n // Same rule as the YNCA side: a write that goes nowhere leaves a trace.\n this.deps.log.debug(\n `${this.deviceId}: scene \"${value}\" is unknown for ${zoneKey} \u2014 write dropped ` +\n `(known: ${\n knownScenes(this.deps.probeMemory, zoneKey)\n .map(scene => scene.title)\n .join(\", \") || \"none yet\"\n })`,\n );\n return;\n }\n value = resolved;\n }\n // Multiroom writes need controller state (the cached role), so they bypass the pure command map.\n if (stateId === \"multiroom.group.leave\") {\n void this.leaveGroup();\n return;\n }\n if (stateId === \"multiroom.group.linkDevice\") {\n void this.linkClient(String(value));\n return;\n }\n const command = stateToYxc(stateId, value);\n if (command) {\n void this.applyCommand(stateId, command);\n }\n }\n\n /**\n * Register the supervisor's drop handler. MusicCast has no socket-drop event, so a\n * drop is inferred from a run of failed keepalive polls (see keepalive).\n *\n * @param cb invoked once when the device is judged gone\n */\n public onDrop(cb: (reason?: Error) => void): void {\n this.dropDetector.onDrop(cb);\n }\n\n /**\n * Create the browsing surface (#613) when the device has the netusb block: the\n * `netusb/getListInfo` + `setListControl` API drives an 8-line window under\n * `player.browse.*`. Skipped without a delay dep (older tests).\n *\n * @param capabilities the parsed getFeatures capabilities\n */\n private async setupBrowse(capabilities: YxcCapabilities): Promise<void> {\n const gate = this.deps.gate;\n if (!gate || !capabilities.media.includes(\"netusb\")) {\n return;\n }\n const inputs = capabilities.zones.find(zone => zone.id === \"main\")?.inputs ?? [];\n const driver = new YxcBrowseDriver(this.deps.client, inputs);\n this.browseEngine = await createBrowseSurface(driver, this.deviceId, {\n upsertObject: this.deps.upsertObject,\n emit: (id, value) => this.emit(id, value),\n log: this.deps.log,\n delay: ms => gate.delay(ms),\n });\n }\n\n /** Cancel the keepalive and unregister the push handler. Synchronous \u2014 safe from onUnload. */\n public close(): void {\n this.browseEngine?.close();\n // Closing the gate empties its queue and aborts its signal: queued requests are\n // dropped and every pending wait ends, so nothing writes after the teardown.\n this.deps.gate?.close();\n this.cancelKeepalive?.();\n this.cancelKeepalive = undefined;\n // Unregister from the shared push receiver \u2014 otherwise a push arriving after\n // teardown would setState on a controller the adapter has already dropped.\n this.cancelPush?.();\n this.cancelPush = undefined;\n }\n\n /**\n * Handle a device push: each named zone is re-fetched via getStatus, each named\n * media source via getPlayInfo (the push itself is a change signal, not a value\n * carrier). Refreshing only the named sources keeps a track-change push from\n * re-polling every source.\n *\n * @param event the parsed push event\n */\n private onPush(event: unknown): void {\n for (const zone of zonesToRefresh(event)) {\n if (this.zones.includes(zone)) {\n void this.refreshZone(zone);\n }\n }\n for (const block of mediaToRefresh(event)) {\n if (this.mediaBlocks.includes(block)) {\n void this.refreshMediaSource(block);\n }\n }\n // The favourites/recently-played lists announce their changes as flags in the push.\n const lists = netusbListsToRefresh(event);\n if (lists.presets && this.mediaBlocks.includes(\"netusb\")) {\n void this.refreshNetusbPresets();\n }\n if (lists.recent && this.mediaBlocks.includes(\"netusb\")) {\n void this.refreshNetusbRecent();\n }\n }\n\n /**\n * Poll every zone (which renews the push registration and refreshes state) and the\n * media sources. If every zone poll fails for three consecutive failed runs in a row,\n * the device is judged gone and a drop is reported so the supervisor can flip\n * info.connection and reconnect.\n */\n private async keepalive(): Promise<void> {\n // The keepalive is an async handler on an adapter timer: a rejection here is an\n // UNHANDLED rejection, and js-controller turns those into an adapter stop. Every step\n // below catches for itself today, so the guard is what makes that a guarantee instead\n // of something the next change has to remember.\n try {\n // Zones in parallel: their writes are disjoint and one zone stuck in its timeout must\n // not delay the others (a four-zone receiver used to poll them strictly in series).\n const zones = this.zones.length > 0 ? this.zones : [\"main\"];\n const anyOk = (await Promise.all(zones.map(zone => this.refreshZone(zone)))).some(Boolean);\n // Every request above already carried the subscription headers, so the push\n // registration is renewed either way. What still has to be polled depends on whether\n // push works: with push the device announces media, list and group changes itself, so\n // the full sweep only runs occasionally as a safety net (UDP can drop a packet);\n // without push it is the only way anything ever updates.\n this.keepaliveRuns++;\n const fullSweep = !this.deps.pushActive?.() || this.keepaliveRuns % PUSH_MODE_FULL_SWEEP_EVERY === 0;\n if (fullSweep) {\n await this.refreshMedia();\n await this.refreshLists();\n if (this.hasDistribution) {\n await this.refreshDistribution();\n }\n }\n this.dropDetector.record(anyOk);\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: keepalive poll failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Refresh the list-shaped surfaces: the netusb favourites and recently-played\n * lists, the tuner preset lists, and the clock/alarm settings. Each is\n * best-effort \u2014 a device without the feature answers with an error code and the\n * state simply stays.\n */\n private async refreshLists(): Promise<void> {\n if (this.mediaBlocks.includes(\"netusb\")) {\n await this.refreshNetusbPresets();\n await this.refreshNetusbRecent();\n if (this.hasMcPlaylist) {\n await this.refreshPlaylists();\n }\n if (this.hasPlayQueue) {\n await this.refreshPlayQueue();\n }\n }\n if (this.mediaBlocks.includes(\"tuner\")) {\n await this.refreshTunerPresets();\n }\n if (this.hasClock) {\n await this.refreshClock();\n }\n await this.refreshSignalInfo();\n }\n\n /** Fetch the MusicCast playlist names and write the JSON list state. */\n private async refreshPlaylists(): Promise<void> {\n try {\n const update = parseYxcPlaylistNames(await this.deps.client.getMcPlaylistName());\n if (update) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getMcPlaylistName failed: ${errorMessage(e)}`);\n }\n }\n\n /** Fetch the network player's play queue and write the JSON state. */\n private async refreshPlayQueue(): Promise<void> {\n try {\n const update = parseYxcPlayQueue(await this.deps.client.getPlayQueue());\n if (update) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getPlayQueue failed: ${errorMessage(e)}`);\n }\n }\n\n /** Fetch each declaring zone's audio-signal info and write the signal states. */\n private async refreshSignalInfo(): Promise<void> {\n for (const zone of this.signalZones) {\n try {\n for (const update of parseYxcSignalInfo(await this.deps.client.getSignalInfo(zone), zone)) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getSignalInfo(${zone}) failed: ${errorMessage(e)}`);\n }\n }\n }\n\n /** Fetch the stored netusb favourites and write the JSON list state. */\n private async refreshNetusbPresets(): Promise<void> {\n try {\n const update = parseYxcPresetList(await this.deps.client.getPresetInfo());\n if (update) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getPresetInfo failed: ${errorMessage(e)}`);\n }\n }\n\n /** Fetch the recently-played list and write the JSON list state. */\n private async refreshNetusbRecent(): Promise<void> {\n try {\n const update = parseYxcRecentList(await this.deps.client.getRecentInfo());\n if (update) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getRecentInfo failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Fetch the tuner preset lists \u2014 the shared `common` list, or one per band on\n * devices with separate lists \u2014 and write the JSON state.\n */\n private async refreshTunerPresets(): Promise<void> {\n const bands = this.tunerFeatures?.presetType === \"common\" ? [\"common\"] : (this.tunerFeatures?.bands ?? [\"fm\"]);\n const byBand: Record<string, unknown> = {};\n for (const band of bands) {\n try {\n byBand[band] = await this.deps.client.getTunerPresetInfo(band);\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getTunerPresetInfo(${band}) failed: ${errorMessage(e)}`);\n }\n }\n const update = parseYxcTunerPresetLists(byBand);\n if (update) {\n this.emit(update.id, update.value);\n }\n }\n\n /** Fetch the clock/alarm settings and write the read-only clock states. */\n private async refreshClock(): Promise<void> {\n try {\n for (const update of parseYxcClock(await this.deps.client.getClockSettings())) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getClockSettings failed: ${errorMessage(e)}`);\n }\n }\n\n /** Refresh every player source the device offers (network player, cd, tuner). */\n private async refreshMedia(): Promise<void> {\n // The three sources (netusb/cd/tuner) write disjoint states \u2014 fetch them together.\n await Promise.all(this.mediaBlocks.map(block => this.refreshMediaSource(block)));\n }\n\n /**\n * Fetch one media source's play info and write the parsed states with ack. The\n * source picks the getPlayInfo argument and the parser: netusb and cd share the\n * play-info shape (different channel), the tuner has its own band/frequency/RDS.\n *\n * @param block the media block (`netusb`, `cd`, `tuner`)\n */\n private async refreshMediaSource(block: string): Promise<void> {\n const arg = block === \"netusb\" ? undefined : block;\n try {\n const info = await this.deps.client.getPlayInfo(arg);\n if (block === \"tuner\") {\n for (const update of parseYxcTunerInfo(info)) {\n this.emit(update.id, update.value);\n if (update.id === \"tuner.band\") {\n this.lastTunerBand = String(update.value);\n }\n }\n return;\n }\n const source: \"netusb\" | \"cd\" = block === \"cd\" ? \"cd\" : \"netusb\";\n const updates = parseYxcPlayInfo(info, source);\n if (source === \"netusb\") {\n const active = updates.find(update => update.id === \"player.source\");\n if (typeof active?.value === \"string\") {\n this.lastNetusbInput = active.value;\n }\n }\n this.routePlayerBlock(source, updates);\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getPlayInfo(${arg ?? \"\"}) failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Which player source a zone's input feeds into its \"now playing\" block: `cd` for\n * the disc input, `netusb` when the zone's input IS the network player's active\n * source. Anything else (HDMI, analog, tuner) plays no media block.\n *\n * @param input the zone's currently selected input\n * @returns the feeding source, or undefined when the input is no media player\n */\n private playerBlockFor(input: string | undefined): \"netusb\" | \"cd\" | undefined {\n if (input === \"cd\" && this.mediaBlocks.includes(\"cd\")) {\n return \"cd\";\n }\n if (input !== undefined && input !== \"\" && input === this.lastNetusbInput && this.mediaBlocks.includes(\"netusb\")) {\n return \"netusb\";\n }\n return undefined;\n }\n\n /**\n * Route one source's play info into the \"now playing\" block of every zone listening\n * to it (v2.0.0): main flat, the other zones under their multiroom folder. A zone\n * that LEFT the source gets its block cleared once \u2014 the previous program's metadata\n * must not linger under a zone that no longer plays it. Drive-own `player.cd.*`\n * extras are device-global and emitted once, unprefixed.\n *\n * @param block the source the updates came from\n * @param updates the parsed flat player updates\n */\n private routePlayerBlock(block: \"netusb\" | \"cd\", updates: StateValue[]): void {\n const zones = this.zones.length > 0 ? this.zones : [\"main\"];\n for (const zone of zones) {\n const expected = this.playerBlockFor(this.lastZoneInput.get(zone));\n const previous = this.zonePlayerBlock.get(zone);\n if (previous === block && expected !== block) {\n // The zone left OUR source \u2014 clear; the new source's refresh fills its own values.\n this.zonePlayerBlock.delete(zone);\n this.emitPlayerUpdates(zone, PLAYER_CLEAR);\n }\n if (expected === block) {\n if (previous !== block) {\n this.zonePlayerBlock.set(zone, block);\n if (previous !== undefined) {\n this.emitPlayerUpdates(zone, PLAYER_CLEAR);\n }\n }\n this.emitPlayerUpdates(zone, updates);\n }\n }\n for (const update of updates) {\n if (update.id.startsWith(\"player.cd.\")) {\n this.emit(update.id, update.value);\n }\n }\n }\n\n /**\n * One scene.list JSON per scene-capable zone (v2.0.0): every recall slot the zone\n * declares (getFeatures scene_num), titled from the shared per-device memory where\n * another transport reported titles (XML Scene_Sel_Item / YNCA SCENExNAME). A\n * MusicCast-only device has no title source and lists its slots with empty titles \u2014\n * the COUNT is what its getFeatures declares (review finding: the promised list was\n * missing entirely on devices whose only transport is MusicCast).\n *\n * @param capabilities the device's parsed getFeatures capabilities\n */\n private async setupSceneLists(capabilities: YxcCapabilities): Promise<void> {\n for (const zone of capabilities.zones) {\n if (!zone.funcs.includes(\"scene\") || zone.sceneNum === undefined || zone.sceneNum <= 0) {\n continue;\n }\n const titles = new Map(knownScenes(this.deps.probeMemory, zone.id).map(scene => [scene.num, scene.title]));\n const list = Array.from({ length: zone.sceneNum }, (_unused, i) => ({\n num: i + 1,\n title: titles.get(i + 1) ?? \"\",\n }));\n const id = `${zonePrefix(zone.id)}scene.list`;\n await this.deps.upsertObject(`${this.deviceId}.${id}`, {\n id,\n type: \"state\",\n common: {\n name: tName(\"scenesNumberTitle\"),\n desc: tName(\"descScenesNumberTitle\"),\n type: \"string\",\n role: \"json\",\n read: true,\n write: false,\n },\n });\n this.emit(id, JSON.stringify(list));\n }\n }\n\n /**\n * Re-evaluate which source feeds a zone's player block after ITS input changed:\n * clear the block when the zone left a media source, and fetch the joined source's\n * play info right away so the block fills now, not at the next sweep.\n *\n * @param zone the zone whose input just changed\n */\n private retargetZonePlayer(zone: string): void {\n const expected = this.playerBlockFor(this.lastZoneInput.get(zone));\n const previous = this.zonePlayerBlock.get(zone);\n if (previous === expected) {\n return;\n }\n if (previous !== undefined) {\n this.zonePlayerBlock.delete(zone);\n this.emitPlayerUpdates(zone, PLAYER_CLEAR);\n }\n if (expected !== undefined) {\n // routePlayerBlock (inside this refresh) records the zone's new source.\n void this.refreshMediaSource(expected);\n }\n }\n\n /**\n * Emit flat player updates into one zone's block (main flat, zones prefixed),\n * skipping the device-global drive extras.\n *\n * @param zone the target zone\n * @param updates the flat player updates\n */\n private emitPlayerUpdates(zone: string, updates: StateValue[]): void {\n const prefix = zonePrefix(zone);\n for (const update of updates) {\n if (!update.id.startsWith(\"player.cd.\")) {\n this.emit(`${prefix}${update.id}`, update.value);\n }\n }\n }\n\n /**\n * Fetch the MusicCast-Link distribution info and write the parsed dist states with ack,\n * caching the role for the leave-group path.\n */\n private async refreshDistribution(): Promise<void> {\n try {\n const info = await this.deps.client.getDistributionInfo();\n for (const update of parseYxcDistribution(info)) {\n this.emit(update.id, update.value);\n if (update.id === \"multiroom.group.role\") {\n this.lastDistRole = String(update.value);\n }\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getDistributionInfo failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Leave the current MusicCast-Link group: a server stops distributing, a client clears\n * its membership. Then re-read the distribution state so the tree reflects the change.\n */\n private async leaveGroup(): Promise<void> {\n try {\n if (this.lastDistRole === \"server\") {\n await this.deps.client.stopDistribution();\n } else {\n await this.deps.client.setClientInfo({ group_id: \"\", zone: [\"main\"] });\n }\n await this.refreshDistribution();\n } catch (e) {\n // A user action failing must be visible \u2014 warn, like every other write command.\n this.deps.log.warn(`${this.deviceId}: leaveGroup failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Form a MusicCast-Link group with another configured device: give the client the shared\n * group id, add it to this device's roster as the server, and start distributing. The group\n * id is derived from this device's id, so re-linking reuses the same group rather than a new one.\n *\n * @param clientIp the IP of the client device to add (must be a configured device)\n */\n private async linkClient(clientIp: string): Promise<void> {\n const clientClient = this.deps.clientFor?.(clientIp);\n if (!clientClient) {\n this.deps.log.warn(`${this.deviceId}: cannot link ${clientIp} \u2014 not a known device`);\n return;\n }\n try {\n const groupId = createHash(\"md5\").update(this.deviceId).digest(\"hex\");\n await clientClient.setClientInfo({ group_id: groupId, zone: [\"main\"] });\n await this.deps.client.setServerInfo({ group_id: groupId, zone: \"main\", type: \"add\", client_list: [clientIp] });\n await this.deps.client.startDistribution(0);\n await this.refreshDistribution();\n } catch (e) {\n // A user action failing must be visible \u2014 warn, like every other write command.\n this.deps.log.warn(`${this.deviceId}: linkClient(${clientIp}) failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Fetch a zone's status and write its amp states with ack.\n *\n * @param zone the zone to refresh\n * @returns true if the status was fetched, false if the request failed\n */\n private async refreshZone(zone: string): Promise<boolean> {\n try {\n const status = await this.deps.client.getStatus(zone);\n const updates = parseYxcStatus(status, zone);\n for (const update of updates) {\n this.emit(update.id, update.value);\n // The EXACT id, not a suffix: this value decides which source a zone's player block\n // and its transport buttons follow. A future status field ending in \"input\" would\n // have bent that routing silently.\n if (update.id === `${zonePrefix(zone)}input` && typeof update.value === \"string\") {\n const previous = this.lastZoneInput.get(zone);\n this.lastZoneInput.set(zone, update.value);\n if (previous !== update.value) {\n // The zone changed its input \u2014 re-target its player block NOW. Media\n // pushes alone cannot cover this: a zone leaving a still-playing source\n // (or joining one another zone already plays) changes nothing about the\n // source itself, so no netusb/cd push ever arrives (2.0.0 review finding).\n this.retargetZonePlayer(zone);\n }\n }\n }\n this.cacheEqualizer(zone, updates);\n return true;\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getStatus(${zone}) failed: ${errorMessage(e)}`);\n return false;\n }\n }\n\n /**\n * Cache a zone's equalizer bands from its status updates, so a later single-band\n * write can send setEqualizer with all three (the device sets them together).\n *\n * @param zone the zone the updates belong to\n * @param updates the parsed status updates for that zone\n */\n private cacheEqualizer(zone: string, updates: StateValue[]): void {\n // Same definition the status parser uses \u2014 see lib/yxc/zones.ts on why this must not\n // be spelled out a second time.\n const prefix = zonePrefix(zone);\n const band = (b: string): number | undefined => {\n const u = updates.find(x => x.id === `${prefix}sound.equalizer.${b}`);\n return typeof u?.value === \"number\" ? u.value : undefined;\n };\n const low = band(\"low\");\n const mid = band(\"mid\");\n const high = band(\"high\");\n if (low === undefined && mid === undefined && high === undefined) {\n return;\n }\n const cur = this.lastEqualizer.get(zone);\n if (!cur) {\n // No cache yet: only a COMPLETE triple may seed it. Filling the gaps with 0 would hand\n // the write path exactly the invented 0/0/0 it refuses to send \u2014 that safeguard only\n // works if this cache never lies about a band.\n if (low === undefined || mid === undefined || high === undefined) {\n return;\n }\n this.lastEqualizer.set(zone, { low, mid, high });\n return;\n }\n this.lastEqualizer.set(zone, { low: low ?? cur.low, mid: mid ?? cur.mid, high: high ?? cur.high });\n }\n\n /**\n * Apply a mapped command. A plain command runs its client call directly; the two\n * commands that need controller-cached state (equalizer bands, tuner band) are\n * completed here \u2014 the only place that state lives.\n *\n * @param stateId the written state id, for the failure log line\n * @param command the YXC command to apply\n */\n private async applyCommand(stateId: string, command: YxcCommand): Promise<void> {\n try {\n switch (command.kind) {\n case \"run\":\n await command.run(this.deps.client);\n break;\n case \"equalizer\": {\n // The device sets all three bands in one call, so the other two come from the\n // cache. Without a cached set we must NOT invent 0/0/0 \u2014 that would silently\n // flatten the user's other two bands. The cache fills from a zone status, so\n // fetch one first; only if even that fails is the write refused (with a warning).\n const { zone, band, value } = command;\n let current = this.lastEqualizer.get(zone);\n if (!current) {\n await this.refreshZone(zone);\n current = this.lastEqualizer.get(zone);\n }\n if (!current) {\n this.deps.log.warn(\n `${this.deviceId}: not writing ${stateId} \u2014 the device has not reported its equalizer bands yet`,\n );\n break;\n }\n const next = { ...current, [band]: value };\n // Cache BEFORE the round-trip: a second band written straight afterwards\n // reads this cache at dispatch \u2014 caching after the await would make it\n // compute from the pre-write triple and undo this band on the device.\n this.lastEqualizer.set(zone, next);\n await this.deps.client.setEqualizer(next.low, next.mid, next.high, zone);\n break;\n }\n case \"tunerBand\":\n // Remember it BEFORE the round-trip: a frequency written straight afterwards\n // reads this cache synchronously at dispatch, while the band call may still\n // be in flight \u2014 and the poll that would report it back runs minutes later.\n this.lastTunerBand = command.band;\n await this.deps.client.setBand(command.band);\n break;\n case \"tunerFreq\":\n await this.deps.client.setFreq(this.lastTunerBand, command.value);\n break;\n case \"tunerPreset\": {\n // Shared-list devices recall on `common`; separate-list devices on the current band.\n const band = this.tunerFeatures?.presetType === \"common\" ? \"common\" : this.lastTunerBand;\n await this.deps.client.recallTunerPreset(band, command.value, this.zoneListeningTo(\"tuner\"));\n break;\n }\n case \"netusbPreset\":\n await this.deps.client.recallPreset(command.value, this.zoneListeningTo(this.lastNetusbInput));\n break;\n case \"netusbRecent\":\n await this.deps.client.recallRecentItem(command.value, this.zoneListeningTo(this.lastNetusbInput));\n break;\n case \"playerTransport\": {\n // The unified block's buttons act on whatever the ZONE is playing (v2.0.0) \u2014\n // derived FRESH from the zone's input, never from the routing map: a stale\n // map entry would send the command to the source the zone just left.\n const block = this.playerBlockFor(this.lastZoneInput.get(command.zone));\n if (block === undefined) {\n this.deps.log.debug(`${this.deviceId}: ${stateId} ignored \u2014 ${command.zone} is not playing a media source`);\n break;\n }\n await this.runTransport(block, command.action);\n break;\n }\n }\n } catch (e) {\n this.deps.log.warn(`${this.deviceId}: write to ${stateId} failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Run one transport action on the given player source. The CD transport routes\n * through the one `setCDPlayback(action)` method (not the per-action helpers, one\n * of which sends the wrong command in the library).\n *\n * @param block the source the zone is playing\n * @param action the transport action\n */\n private async runTransport(block: \"netusb\" | \"cd\", action: PlayerTransport): Promise<void> {\n const client = this.deps.client;\n if (block === \"netusb\") {\n const net: Record<PlayerTransport, () => Promise<unknown>> = {\n play: () => client.playNet(),\n pause: () => client.pauseNet(),\n stop: () => client.stopNet(),\n next: () => client.nextNet(),\n prev: () => client.prevNet(),\n repeatToggle: () => client.toggleNetRepeat(),\n shuffleToggle: () => client.toggleNetShuffle(),\n };\n await net[action]();\n return;\n }\n const cd: Record<PlayerTransport, () => Promise<unknown>> = {\n play: () => client.setCDPlayback(\"play\"),\n pause: () => client.setCDPlayback(\"pause\"),\n stop: () => client.setCDPlayback(\"stop\"),\n next: () => client.setCDPlayback(\"next\"),\n prev: () => client.setCDPlayback(\"previous\"),\n repeatToggle: () => client.toggleCDRepeat(),\n shuffleToggle: () => client.toggleCDShuffle(),\n };\n await cd[action]();\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -35,7 +35,14 @@ const PLAYER_STATES = [
35
35
  // What the zone is playing (the netusb source name, or `cd`) — read-only display;
36
36
  // switching happens over the zone's `input` state.
37
37
  state: "source",
38
- common: { nameKey: "playingSource", type: "string", role: "text", read: true, write: false }
38
+ common: {
39
+ nameKey: "playingSource",
40
+ descKey: "descPlayingSource",
41
+ type: "string",
42
+ role: "text",
43
+ read: true,
44
+ write: false
45
+ }
39
46
  },
40
47
  {
41
48
  state: "playback",
@@ -250,6 +257,7 @@ function mapYxcToObjects(capabilities) {
250
257
  type: "state",
251
258
  common: {
252
259
  name: (0, import_i18n.tName)("cursorPad"),
260
+ desc: (0, import_i18n.tName)("descCursorPad"),
253
261
  type: "string",
254
262
  role: "state",
255
263
  read: false,
@@ -264,6 +272,7 @@ function mapYxcToObjects(capabilities) {
264
272
  type: "state",
265
273
  common: {
266
274
  name: (0, import_i18n.tName)("menuKey"),
275
+ desc: (0, import_i18n.tName)("descMenuKey"),
267
276
  type: "string",
268
277
  role: "state",
269
278
  read: false,
@@ -327,6 +336,7 @@ function mapYxcToObjects(capabilities) {
327
336
  type: "state",
328
337
  common: {
329
338
  name: (0, import_i18n.tName)("recallRecentlyPlayedNumber"),
339
+ desc: (0, import_i18n.tName)("descRecallRecentlyPlayedNumber"),
330
340
  type: "number",
331
341
  role: "level",
332
342
  read: true,
@@ -354,7 +364,14 @@ function mapYxcToObjects(capabilities) {
354
364
  objects.push({
355
365
  id: "player.cd.tray",
356
366
  type: "state",
357
- common: { name: (0, import_i18n.tName)("toggleTray"), type: "boolean", role: "button", read: false, write: true }
367
+ common: {
368
+ name: (0, import_i18n.tName)("toggleTray"),
369
+ desc: (0, import_i18n.tName)("descToggleTray"),
370
+ type: "boolean",
371
+ role: "button",
372
+ read: false,
373
+ write: true
374
+ }
358
375
  });
359
376
  objects.push({
360
377
  id: "player.cd.trackNumber",
@@ -374,7 +391,14 @@ function mapYxcToObjects(capabilities) {
374
391
  objects.push({
375
392
  id: "player.cd.deviceStatus",
376
393
  type: "state",
377
- common: { name: (0, import_i18n.tName)("driveStatus"), type: "string", role: "state", read: true, write: false }
394
+ common: {
395
+ name: (0, import_i18n.tName)("driveStatus"),
396
+ desc: (0, import_i18n.tName)("descDriveStatus"),
397
+ type: "string",
398
+ role: "state",
399
+ read: true,
400
+ write: false
401
+ }
378
402
  });
379
403
  }
380
404
  if (capabilities.media.includes("tuner")) {
@@ -435,7 +459,14 @@ function mapYxcToObjects(capabilities) {
435
459
  objects.push({
436
460
  id: "tuner.rdsProgramType",
437
461
  type: "state",
438
- common: { name: (0, import_i18n.tName)("rdsProgrammeType"), type: "string", role: "text", read: true, write: false }
462
+ common: {
463
+ name: (0, import_i18n.tName)("rdsProgrammeType"),
464
+ desc: (0, import_i18n.tName)("descRdsProgramType"),
465
+ type: "string",
466
+ role: "text",
467
+ read: true,
468
+ write: false
469
+ }
439
470
  });
440
471
  const presetCommon = {
441
472
  name: (0, import_i18n.tName)("presetRecallByNumber"),
@@ -468,7 +499,14 @@ function mapYxcToObjects(capabilities) {
468
499
  objects.push({
469
500
  id: "tuner.tuned",
470
501
  type: "state",
471
- common: { name: (0, import_i18n.tName)("tuned"), type: "boolean", role: "indicator", read: true, write: false }
502
+ common: {
503
+ name: (0, import_i18n.tName)("tuned"),
504
+ desc: (0, import_i18n.tName)("descTunedToAStation"),
505
+ type: "boolean",
506
+ role: "indicator",
507
+ read: true,
508
+ write: false
509
+ }
472
510
  });
473
511
  objects.push({
474
512
  id: "tuner.audioMode",
@@ -501,16 +539,34 @@ function mapYxcToObjects(capabilities) {
501
539
  }
502
540
  }
503
541
  if (capabilities.clock) {
504
- objects.push({ id: "clock", type: "channel", common: { name: (0, import_i18n.tName)("clockAlarm") } });
542
+ objects.push({
543
+ id: "clock",
544
+ type: "channel",
545
+ common: { name: (0, import_i18n.tName)("clockAlarm"), desc: (0, import_i18n.tName)("descClockAlarm") }
546
+ });
505
547
  objects.push({
506
548
  id: "clock.autoSync",
507
549
  type: "state",
508
- common: { name: (0, import_i18n.tName)("automaticTimeSync"), type: "boolean", role: "indicator", read: true, write: false }
550
+ common: {
551
+ name: (0, import_i18n.tName)("automaticTimeSync"),
552
+ desc: (0, import_i18n.tName)("descAutomaticTimeSync"),
553
+ type: "boolean",
554
+ role: "indicator",
555
+ read: true,
556
+ write: false
557
+ }
509
558
  });
510
559
  objects.push({
511
560
  id: "clock.format",
512
561
  type: "state",
513
- common: { name: (0, import_i18n.tName)("clockFormat"), type: "string", role: "state", read: true, write: false }
562
+ common: {
563
+ name: (0, import_i18n.tName)("clockFormat"),
564
+ desc: (0, import_i18n.tName)("descClockFormat"),
565
+ type: "string",
566
+ role: "state",
567
+ read: true,
568
+ write: false
569
+ }
514
570
  });
515
571
  objects.push({ id: "clock.alarm", type: "channel", common: { name: (0, import_i18n.tName)("alarm") } });
516
572
  objects.push({
@@ -533,12 +589,27 @@ function mapYxcToObjects(capabilities) {
533
589
  objects.push({
534
590
  id: "clock.alarm.fadeInterval",
535
591
  type: "state",
536
- common: { name: (0, import_i18n.tName)("fadeInTime"), type: "number", unit: "s", role: "value", read: true, write: false }
592
+ common: {
593
+ name: (0, import_i18n.tName)("fadeInTime"),
594
+ desc: (0, import_i18n.tName)("descFadeInTime"),
595
+ type: "number",
596
+ unit: "s",
597
+ role: "value",
598
+ read: true,
599
+ write: false
600
+ }
537
601
  });
538
602
  objects.push({
539
603
  id: "clock.alarm.fadeType",
540
604
  type: "state",
541
- common: { name: (0, import_i18n.tName)("fadeType"), type: "number", role: "value", read: true, write: false }
605
+ common: {
606
+ name: (0, import_i18n.tName)("fadeType"),
607
+ desc: (0, import_i18n.tName)("descFadeType"),
608
+ type: "number",
609
+ role: "value",
610
+ read: true,
611
+ write: false
612
+ }
542
613
  });
543
614
  objects.push({
544
615
  id: "clock.alarm.mode",
@@ -548,7 +619,14 @@ function mapYxcToObjects(capabilities) {
548
619
  objects.push({
549
620
  id: "clock.alarm.repeat",
550
621
  type: "state",
551
- common: { name: (0, import_i18n.tName)("repeatSnooze"), type: "boolean", role: "indicator", read: true, write: false }
622
+ common: {
623
+ name: (0, import_i18n.tName)("repeatSnooze"),
624
+ desc: (0, import_i18n.tName)("descRepeatSnooze"),
625
+ type: "boolean",
626
+ role: "indicator",
627
+ read: true,
628
+ write: false
629
+ }
552
630
  });
553
631
  const detailChannels = ["oneday", ...capabilities.clock.alarmModes.includes("weekly") ? import_command_mapper.ALARM_DAYS : []];
554
632
  for (const channel of detailChannels) {
@@ -587,12 +665,26 @@ function mapYxcToObjects(capabilities) {
587
665
  objects.push({
588
666
  id: "multiroom.group.leave",
589
667
  type: "state",
590
- common: { name: (0, import_i18n.tName)("leaveGroup"), type: "boolean", role: "button", read: false, write: true }
668
+ common: {
669
+ name: (0, import_i18n.tName)("leaveGroup"),
670
+ desc: (0, import_i18n.tName)("descLeaveGroup"),
671
+ type: "boolean",
672
+ role: "button",
673
+ read: false,
674
+ write: true
675
+ }
591
676
  });
592
677
  objects.push({
593
678
  id: "multiroom.group.linkDevice",
594
679
  type: "state",
595
- common: { name: (0, import_i18n.tName)("linkADeviceItsIP"), type: "string", role: "text", read: false, write: true }
680
+ common: {
681
+ name: (0, import_i18n.tName)("linkADeviceItsIP"),
682
+ desc: (0, import_i18n.tName)("descLinkADeviceItsIP"),
683
+ type: "string",
684
+ role: "text",
685
+ read: false,
686
+ write: true
687
+ }
596
688
  });
597
689
  }
598
690
  return objects;