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.
- package/README.md +4 -4
- package/admin/i18n/de.json +25 -1
- package/admin/i18n/en.json +25 -1
- package/admin/i18n/es.json +25 -1
- package/admin/i18n/fr.json +25 -1
- package/admin/i18n/it.json +25 -1
- package/admin/i18n/nl.json +25 -1
- package/admin/i18n/pl.json +25 -1
- package/admin/i18n/pt.json +25 -1
- package/admin/i18n/ru.json +25 -1
- package/admin/i18n/uk.json +25 -1
- package/admin/i18n/zh-cn.json +25 -1
- package/build/lib/browse/objects.js +50 -6
- package/build/lib/browse/objects.js.map +2 -2
- package/build/lib/device-controller.js +16 -2
- package/build/lib/device-controller.js.map +2 -2
- package/build/lib/xml/device-controller.js +8 -1
- package/build/lib/xml/device-controller.js.map +2 -2
- package/build/lib/yxc/device-controller.js +8 -1
- package/build/lib/yxc/device-controller.js.map +2 -2
- package/build/lib/yxc/object-mapper.js +105 -13
- package/build/lib/yxc/object-mapper.js.map +2 -2
- package/build/main.js +1 -1
- package/build/main.js.map +2 -2
- package/io-package.json +14 -14
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/device-controller.ts"],
|
|
4
|
-
"sourcesContent": ["import { mergeYncaSubunits, type YncaCapabilities } from \"./ynca/capability\";\nimport { formatWireNumber } from \"./catalog/value-coerce\";\nimport { playTimeTwin } from \"./catalog/play-time\";\nimport type { ObjectDef } from \"./catalog/types\";\nimport { tName } from \"./i18n\";\nimport type { ConnectionHandle, ControllerLog } from \"./controller\";\nimport {\n YNCA_CATALOG,\n availGets,\n funcToEntry,\n idToEntry,\n presentYncaEntries,\n sweepGets,\n yncaCommand,\n yncaObjectsFor,\n yncaStateUpdate,\n type YncaEntry,\n} from \"./ynca/catalog\";\nimport type { YncaSubunitCache } from \"./ynca/subunit-cache\";\nimport type { CommandGate } from \"./lifecycle/command-gate\";\nimport type { ProbeMemory } from \"./lifecycle/probe-memory\";\nimport type { BrowseEngine } from \"./browse/browse-engine\";\nimport { createBrowseSurface } from \"./browse/surface\";\nimport { YNCA_BROWSE_SOURCES, YncaBrowseDriver } from \"./browse/ynca-browse-driver\";\n\n// The YNCA catalog and its lookup maps are static \u2014 built once for all devices.\n// SYS:MODELNAME is part of the catalog (info.model), so the sweep already covers it.\n// The AVAIL probe always covers the FULL catalog (not the group-filtered one), so the\n// cached subunit set reflects the device, never the current group configuration.\nconst FUNC_MAP = funcToEntry(YNCA_CATALOG);\nconst ID_MAP = idToEntry(YNCA_CATALOG);\nconst AVAIL_PROBE = availGets(YNCA_CATALOG);\n\n/**\n * Functions whose VALUE cannot change while the device runs: the 23 assignable input names\n * and the 12 scene names. They cost 35 of the ~187 paced reads of a targeted sweep (3.5 s\n * at the specification's mandatory 100 ms spacing) and answer the same thing every time, so\n * a reconnect reuses what the first connect learned.\n *\n * They live in the PERSISTED probe memory (the device object's `native.probeCache`), so the\n * saving survives a restart too \u2014 the freshness guard is the device identity, and a renamed\n * input heals through the background refresh, which re-reads them. (This used to be\n * documented as \"per adapter run, not persisted\"; that stopped being true with the\n * fast-restart rework and the comment was left behind.)\n */\nconst STATIC_FUNC = /^(INPNAME|SCENE\\d+NAME$)/;\n\n/** The zones whose player/input surface the controller routes (v2.0.0 player unification). */\nconst YNCA_ZONES: Array<{ key: string; subunit: string; prefix: string }> = [\n { key: \"main\", subunit: \"MAIN\", prefix: \"\" },\n { key: \"zone2\", subunit: \"ZONE2\", prefix: \"multiroom.zone2.\" },\n { key: \"zone3\", subunit: \"ZONE3\", prefix: \"multiroom.zone3.\" },\n { key: \"zone4\", subunit: \"ZONE4\", prefix: \"multiroom.zone4.\" },\n];\n\n/**\n * INP value \u2192 the player subunit it selects (normalized: uppercase, alphanumerics only).\n * The wire values come from the RX-V6A INPNAME/INP capture and ynca-python's input map;\n * inputs that are no media player (HDMI, AV, TUNER, \u2026) deliberately map to nothing.\n */\nconst INPUT_SUBUNITS: Record<string, string> = {\n NETRADIO: \"NETRADIO\",\n SERVER: \"SERVER\",\n USB: \"USB\",\n SPOTIFY: \"SPOTIFY\",\n DEEZER: \"DEEZER\",\n TIDAL: \"TIDAL\",\n NAPSTER: \"NAPSTER\",\n PANDORA: \"PANDORA\",\n RHAPSODY: \"RHAP\",\n SIRIUS: \"SIRIUS\",\n SIRIUSXM: \"SIRIUS\",\n SIRIUSIR: \"SIRIUS\",\n AIRPLAY: \"AIRPLAY\",\n BLUETOOTH: \"BT\",\n PC: \"PC\",\n MUSICCASTLINK: \"MCLINK\",\n IPOD: \"IPOD\",\n IPODUSB: \"IPODUSB\",\n};\n\n/**\n * The player subunit an input selection feeds, or undefined when the input is no\n * media player.\n *\n * @param input the zone's INP value (e.g. \"NET RADIO\", \"iPod (USB)\")\n * @returns the subunit (e.g. NETRADIO), or undefined\n */\nfunction playerSubunitForInput(input: string | undefined): string | undefined {\n if (typeof input !== \"string\" || input.length === 0) {\n return undefined;\n }\n return INPUT_SUBUNITS[input.toUpperCase().replace(/[^A-Z0-9]/g, \"\")];\n}\n\n/**\n * A state of the flat per-zone player block: exactly one segment below `player.`.\n * ONLY these are zone-routed \u2014 multi-segment `player.*` ids (bluetooth pairing\n * status, airplay volume interlock, per-source presets) are device-global and go\n * straight to their state (2.0.0 review finding: the broad `player.` prefix check\n * silently dropped BT/AirPlay status while no zone listened to that source).\n */\nconst FLAT_PLAYER_ID = /^player\\.[^.]+$/;\n\n/**\n * What a zone's player block is reset to when the zone leaves its playing source\n * (clear-on-switch): metadata empty, playback Stop. Filtered to the states the\n * device actually has before emitting.\n */\nconst YNCA_PLAYER_CLEAR: Array<{ id: string; value: number | string | boolean }> = [\n { id: \"player.playback\", value: 1 },\n { id: \"player.artist\", value: \"\" },\n { id: \"player.album\", value: \"\" },\n { id: \"player.track\", value: \"\" },\n { id: \"player.station\", value: \"\" },\n { id: \"player.channelName\", value: \"\" },\n { id: \"player.elapsedTime\", value: 0 },\n { id: \"player.elapsedTimeText\", value: \"\" },\n { id: \"player.totalTime\", value: 0 },\n { id: \"player.totalTimeText\", value: \"\" },\n { id: \"player.repeat\", value: 0 },\n { id: \"player.shuffle\", value: false },\n];\n\n/** Memory key for the remembered static values. */\nconst STATIC_KEY = \"yncaStaticValues\";\n\n/** Memory key for the persisted capability shape (the fast-restart layer). */\nconst CAPS_KEY = \"yncaCapabilities\";\n\n/** The persisted capability shape, keyed by the device identity that validated it. */\ninterface CachedCapabilities {\n /** SYS MODELNAME at capture time \u2014 freshness key half 1. */\n model: string;\n /** SYS VERSION at capture time \u2014 freshness key half 2. */\n firmware: string;\n /** The captured subunit\u2192function map (values are last-known, used for SHAPE only). */\n subunits: Record<string, Record<string, string>>;\n}\n\n/**\n * Whether a remembered value carries the cached-capabilities shape (API boundary \u2014\n * the persisted probe memory is untrusted storage).\n *\n * @param value the remembered value\n * @returns true when usable\n */\nfunction isCachedCapabilities(value: unknown): value is CachedCapabilities {\n const candidate = value as Partial<CachedCapabilities> | null;\n return (\n typeof candidate === \"object\" &&\n candidate !== null &&\n typeof candidate.model === \"string\" &&\n typeof candidate.firmware === \"string\" &&\n typeof candidate.subunits === \"object\" &&\n candidate.subunits !== null\n );\n}\n\n/**\n * The functions whose presence proves a subunit really serves menus \u2014 the fields a real\n * `LISTINFO=?` answer is made of (RX-A810 reference log). See {@link YncaDeviceController.probeBrowseSubunits}.\n */\nconst LIST_PROOF = /^(LISTLAYER|LISTLAYERNAME|CURRLINE|MAXLINE|LINE[1-8](TXT|ATRIB))$/;\n\n/**\n * The scenes a device declares, read from its `SCENExNAME` answers on MAIN. Used both by\n * the init and by the background refresh, so a scene renamed at the receiver reaches the\n * running session instead of waiting for the next start.\n *\n * @param subunits the swept subunit\u2192function map\n * @returns the declared scenes, lowest number first\n */\nfunction sceneTitlesOf(subunits: Record<string, Record<string, string>>): Array<{ num: number; title: string }> {\n const main = subunits.MAIN ?? {};\n const scenes: Array<{ num: number; title: string }> = [];\n for (let n = 1; n <= 12; n++) {\n const title = main[`SCENE${n}NAME`];\n if (typeof title === \"string\" && title.length > 0) {\n scenes.push({ num: n, title });\n }\n }\n return scenes;\n}\n\n/** The subset of the YNCA client the controller uses (so tests can inject a fake). */\nexport interface YncaClientLike {\n /** Open the connection. */\n connect(): Promise<void>;\n /** Run the init sweep and return the device's capabilities. */\n readCapabilities(gets: Array<{ subunit: string; func: string }>): Promise<YncaCapabilities>;\n /** Send a PUT command. */\n send(subunit: string, func: string, value: string): void;\n /** Send a GET request (the browse driver reads LISTINFO with it). */\n get(subunit: string, func: string): void;\n /** Register a handler for pushed messages. */\n onMessage(handler: (message: { subunit: string; func: string; value: string }) => void): void;\n /** Register the socket-drop handler the supervisor reconnects on. */\n onDrop(handler: (reason?: Error) => void): void;\n /** Register the refusal handler for user commands the device rejects (optional in older tests). */\n onRefusal?(handler: (command: string, verdict: \"restricted\" | \"undefined\") => void): void;\n /** Start the keepalive poll \u2014 called after the init sweep, not on connect. */\n startKeepalive(): void;\n /** Close the connection synchronously. */\n close(): void;\n}\n\nexport type { ControllerLog };\n\n/** The adapter callbacks the controller drives \u2014 narrow, so no adapter mock is needed in tests. */\nexport interface ControllerDeps {\n /** The YNCA client for this device. */\n client: YncaClientLike;\n /** Create or update an object in the device tree. */\n upsertObject(id: string, def: ObjectDef): Promise<void>;\n /** Write a state value with ack (device-originated). */\n setStateAck(id: string, value: boolean | number | string): void;\n /** Adapter log. */\n log: ControllerLog;\n /**\n * Whether a catalog entry's datapoint group is enabled. A disabled group's entries\n * are dropped BEFORE the sweep, so their GETs are never sent (previously only the\n * object/state writes were gated and the answers thrown away). Absent = all enabled.\n */\n isEntryEnabled?(id: string): boolean;\n /**\n * Per-device cache of the AVAIL probe result, held across reconnects and restarts.\n * With a valid cache the probe phase is skipped and the targeted sweep runs directly;\n * a model/firmware mismatch after the sweep invalidates it and re-probes.\n */\n subunitCache?: YncaSubunitCache;\n /**\n * The device's command gate: every line this controller puts on the wire is already\n * paced through it, and its signal is the connection's shutdown flag (a closed gate\n * ends pending waits and stops state writes). Absent in older tests \u2192 no browsing.\n */\n gate?: CommandGate;\n /** Per-device memory for answers that stay constant while the device runs (see ProbeMemory). */\n probeMemory?: ProbeMemory;\n}\n\n/**\n * Drives one YNCA device: connect, init sweep, build the object tree, and route\n * commands both ways. Create-only \u2014 orphan cleanup and legacy migration are\n * separate, gated steps.\n */\nexport class YncaDeviceController implements ConnectionHandle {\n private browseDriver: YncaBrowseDriver | undefined;\n private browseEngine: BrowseEngine | undefined;\n /**\n * Write map filtered to the entries THIS device reported \u2014 claim-with-proof for\n * writes: a command is only sent with a wire function the device answered in the\n * sweep. Until the sweep ran, the unfiltered static map answers.\n */\n private writeMap: Map<string, YncaEntry> | undefined;\n /** The device's scene titles (SCENExNAME), for the recall dropdown, the list state and title writes. */\n private sceneTitles: Array<{ num: number; title: string }> = [];\n /** The tuner's current band (AM/FM/DAB), for the band-dependent frequency/preset writes. */\n private tunerBand = \"\";\n /** Whether the device carries the DAB subunit (its FM half shares the flat tuner ids). */\n private hasDab = false;\n /** The entries THIS device reported \u2014 the per-subunit lookup behind the player routing. */\n private presentEntries: YncaEntry[] = [];\n /** Each zone's currently selected input (INP), for the player routing (v2.0.0). */\n private readonly zoneInputs = new Map<string, string>();\n /** The zones that got a player block (main plus every present ZONEn, when sources exist). */\n private playerZones: string[] = [];\n\n /**\n * @param deviceId the id-safe device id (object-tree path segment)\n * @param deps the client and adapter callbacks\n */\n public constructor(\n private readonly deviceId: string,\n private readonly deps: ControllerDeps,\n ) {}\n\n /**\n * Connect, sweep the device from the catalog, and create its object tree; wire\n * up push updates. The catalog is the single source: it drives the sweep, the\n * device\u2192state read-back and (in handleStateChange) the state\u2192wire encode.\n *\n * @returns true if the device reported capabilities and its tree was created\n */\n public async start(): Promise<boolean> {\n await this.deps.client.connect();\n // Group-filtered catalog: disabled groups are excluded from the sweep AND the objects.\n const catalog = this.deps.isEntryEnabled\n ? YNCA_CATALOG.filter(entry => this.deps.isEntryEnabled!(entry.id))\n : YNCA_CATALOG;\n const resolved = await this.resolveCapabilities(catalog);\n const { capabilities, fromCache } = resolved;\n const present = presentYncaEntries(capabilities, catalog);\n this.writeMap = idToEntry(present);\n this.presentEntries = present;\n // On the fast path `capabilities` carries the values of the LAST run \u2014 the persisted\n // layer is a SHAPE, its values are leftovers. Everything that DECIDES something\n // (which menus may be claimed, which wire function a band-routed write takes, which\n // source a zone's player buttons act on) is therefore re-read live before use.\n const live = fromCache ? await this.readDecisiveValues(capabilities) : capabilities;\n // Seed each zone's input from the sweep \u2014 the player routing needs to know what\n // every zone is listening to before the first INP push arrives.\n for (const zone of YNCA_ZONES) {\n const input = live.subunits[zone.subunit]?.INP;\n if (typeof input === \"string\") {\n this.zoneInputs.set(zone.key, input);\n }\n }\n const objects = yncaObjectsFor(capabilities, catalog);\n if (objects.length === 0) {\n this.deps.log.warn(`${this.deviceId}: no capabilities reported \u2014 creating no objects`);\n return false;\n }\n // A user command the device rejects must leave a trace: @RESTRICTED (not allowed /\n // not possible right now) and @UNDEFINED (unknown on this model) were silently\n // dropped before \u2014 the class of invisible failures behind #615.\n this.deps.client.onRefusal?.((command, verdict) =>\n this.deps.log.warn(`${this.deviceId}: device refused \"${command}\" (@${verdict.toUpperCase()})`),\n );\n // The scene titles ride the sweep as SCENExNAME answers; they become the recall\n // dropdown's labels and the one scene.list state (v2.0.0 \u2014 no per-name datapoints).\n this.sceneTitles = sceneTitlesOf(capabilities.subunits);\n // Parents before children (channels before their states) \u2014 created in order.\n for (const object of objects) {\n if (object.id === \"scene.recall\" && this.sceneTitles.length > 0) {\n object.common.states = Object.fromEntries(this.sceneTitles.map(scene => [scene.num, scene.title]));\n }\n await this.deps.upsertObject(`${this.deviceId}.${object.id}`, object);\n }\n if (this.sceneTitles.length > 0) {\n await this.deps.upsertObject(`${this.deviceId}.scene.list`, {\n id: \"scene.list\",\n type: \"state\",\n common: { name: tName(\"scenesNumberTitle\"), type: \"string\", role: \"json\", read: true, write: false },\n });\n this.deps.setStateAck(`${this.deviceId}.scene.list`, JSON.stringify(this.sceneTitles));\n }\n await this.setupZonePlayers(capabilities, objects);\n // Seed the states with the values read during the init sweep. On the fast path the\n // cached values are last-run leftovers \u2014 the states already hold exactly those, and\n // the background refresh streams the fresh ones in \u2014 so nothing is seeded there.\n if (!fromCache) {\n for (const [subunit, funcs] of Object.entries(capabilities.subunits)) {\n for (const [func, value] of Object.entries(funcs)) {\n const update = yncaStateUpdate({ subunit, func, value }, FUNC_MAP);\n if (update) {\n if (FLAT_PLAYER_ID.test(update.id)) {\n // Player-block values feed only the zones LISTENING to their source\n // (v2.0.0) \u2014 an idle source's leftover metadata must not seed the block.\n this.routePlayerUpdate(subunit, update.id, update.value);\n } else {\n this.deps.setStateAck(`${this.deviceId}.${update.id}`, update.value);\n }\n }\n }\n }\n }\n // The band decides which wire function a tuner.frequency/preset write goes to\n // (v2.0.0 unification) \u2014 read live above, kept fresh from the live pushes. Whether\n // the device HAS a DAB subunit is shape, so that half may come from the memory.\n this.hasDab = capabilities.subunits.DAB !== undefined;\n this.tunerBand = (live.subunits.DAB?.BAND ?? live.subunits.TUN?.BAND ?? \"\").toUpperCase();\n await this.setupBrowse(live);\n this.deps.client.onMessage(message => {\n // The browse driver sees every line first: list lines (LINE1TXT\u2026, LISTINFO\n // bursts, auto-feedback) are not catalogued and would otherwise be dropped.\n this.browseDriver?.handleMessage(message);\n if (message.func === \"BAND\" && (message.subunit === \"TUN\" || message.subunit === \"DAB\")) {\n this.tunerBand = message.value.toUpperCase();\n }\n if (message.func === \"INP\") {\n const zone = YNCA_ZONES.find(z => z.subunit === message.subunit);\n if (zone) {\n this.handleInputSwitch(zone.key, message.value);\n }\n }\n const update = yncaStateUpdate(message, FUNC_MAP);\n if (update) {\n if (FLAT_PLAYER_ID.test(update.id)) {\n this.routePlayerUpdate(message.subunit, update.id, update.value);\n } else {\n this.deps.setStateAck(`${this.deviceId}.${update.id}`, update.value);\n }\n }\n });\n // Start the keepalive only now the (fast-path) init is done; on the slow path the\n // sweep already ran, on the fast path the background refresh paces itself through\n // the same gate, so the 30 s poll cannot break the spacing either way.\n this.deps.client.startKeepalive();\n if (fromCache) {\n // The whole point of the persisted capability layer: the tree stood in ~1 s from\n // the remembered shape (validated by the LIVE identity answer above), and the\n // 15\u201320 s question round now runs behind the ready line as a pure value refresh \u2014\n // its answers stream into the states through the live handler just registered.\n void this.refreshInBackground(catalog);\n }\n // The adapter logs one combined \"ready\" line across all transports; this per-transport line\n // stays at debug for diagnostics.\n this.deps.log.debug(`${this.deviceId}: ${capabilities.model || \"device\"} ready (YNCA)`);\n return true;\n }\n\n /**\n * The device's capabilities \u2014 from the persisted fast-restart layer when the LIVE\n * identity (model + firmware, two paced reads, ~0.2 s) matches what the layer was\n * captured from, else from the full two-pass sweep. The identity read doubles as the\n * liveness proof the ready line rests on: a cached shape alone must never present a\n * dead device as connected (the v1.5.0 honesty rule).\n *\n * @param catalog the (group-filtered) catalog\n * @returns the capabilities and whether they came from the persisted layer\n */\n private async resolveCapabilities(\n catalog: readonly YncaEntry[],\n ): Promise<{ capabilities: YncaCapabilities; fromCache: boolean }> {\n const identity = await this.deps.client.readCapabilities([\n { subunit: \"SYS\", func: \"MODELNAME\" },\n { subunit: \"SYS\", func: \"VERSION\" },\n ]);\n const model = identity.model;\n const firmware = identity.subunits.SYS?.VERSION ?? \"\";\n const remembered = this.deps.probeMemory?.remembered(CAPS_KEY);\n if (model && isCachedCapabilities(remembered) && remembered.model === model && remembered.firmware === firmware) {\n return { capabilities: { model, subunits: remembered.subunits }, fromCache: true };\n }\n if (remembered !== undefined) {\n // A different (or updated) device behind this address: its remembered YNCA\n // answers are void. The other transports guard their own portions.\n this.deps.probeMemory?.drop(key => key === CAPS_KEY || key === STATIC_KEY);\n }\n const capabilities = await this.sweepDevice(catalog, model, firmware);\n if (capabilities.model) {\n this.deps.probeMemory?.set(CAPS_KEY, {\n model: capabilities.model,\n firmware: capabilities.subunits.SYS?.VERSION ?? firmware,\n subunits: capabilities.subunits,\n } satisfies CachedCapabilities);\n } else {\n // No model, no identity \u2014 and without an identity nothing can ever invalidate what was\n // remembered. The statics (input and scene names) are written by the sweep regardless,\n // so leaving them behind froze those names for good on a device that does not answer\n // SYS:MODELNAME. The two keys live and die together.\n this.deps.probeMemory?.drop(key => key === STATIC_KEY);\n }\n return { capabilities, fromCache: false };\n }\n\n /**\n * Re-read the handful of values the START makes DECISIONS from, and lay them over the\n * remembered shape (fresh wins).\n *\n * The persisted capability layer is a shape whose values are the last run's leftovers \u2014\n * the type says so (\"used for SHAPE only\"). Three decisions used them anyway, and a\n * receiver stands in standby most of the time, so the memory usually says `PWR=Standby`:\n * - the menu claim skips its proof while the device is not on, so a stale \"Standby\"\n * made YNCA claim `player.browse.*` UNPROVEN and displace the XML driver that does\n * probe \u2014 issue #613, brought back in through the cache;\n * - a `tuner.frequency` write is routed by the band, so a stale band sends AMFREQ where\n * FMFREQ belongs (a wrong command on the wire, not just a stale reading);\n * - the player's transport buttons are routed by the zone's input, so a stale input\n * sends play/pause to the source the zone listened to LAST time.\n *\n * Six reads at most (~0.6 s through the gate), once per connect. A drop during them\n * fails the connect \u2014 which is honest: the device is gone.\n *\n * @param remembered the capability shape from the persisted layer\n * @returns the remembered shape with the live answers laid over it\n */\n private async readDecisiveValues(remembered: YncaCapabilities): Promise<YncaCapabilities> {\n const gets: Array<{ subunit: string; func: string }> = [];\n if (remembered.subunits.MAIN !== undefined) {\n gets.push({ subunit: \"MAIN\", func: \"PWR\" });\n }\n for (const zone of YNCA_ZONES) {\n if (remembered.subunits[zone.subunit] !== undefined) {\n gets.push({ subunit: zone.subunit, func: \"INP\" });\n }\n }\n for (const subunit of [\"TUN\", \"DAB\"]) {\n if (remembered.subunits[subunit] !== undefined) {\n gets.push({ subunit, func: \"BAND\" });\n }\n }\n if (gets.length === 0) {\n return remembered;\n }\n const fresh = await this.deps.client.readCapabilities(gets);\n return { model: remembered.model, subunits: mergeYncaSubunits(remembered.subunits, fresh.subunits) };\n }\n\n /**\n * The fast path's second half: re-ask every catalogued function of the present\n * subunits \u2014 the answers stream into the states through the live message handler,\n * so current values arrive within the usual sweep time WITHOUT having gated the\n * ready line. Completion refreshes the persisted layers (capabilities, statics)\n * and the write map; a SHAPE change (a function newly answered) is only persisted \u2014\n * its object appears on the next start, because the unified tree is coordinated\n * once per connect and a late upsert would not materialize.\n *\n * @param catalog the (group-filtered) catalog\n */\n private async refreshInBackground(catalog: readonly YncaEntry[]): Promise<void> {\n try {\n const cached = this.deps.subunitCache?.get();\n const gets = sweepGets(catalog).filter(\n get => get.subunit === \"SYS\" || !cached || cached.subunits.includes(get.subunit),\n );\n const fresh = await this.deps.client.readCapabilities(gets);\n if (!fresh.model) {\n // The refresh ran into a drop \u2014 the supervisor handles the reconnect.\n return;\n }\n const statics: Record<string, Record<string, string>> = {};\n for (const [subunit, funcs] of Object.entries(fresh.subunits)) {\n for (const [func, value] of Object.entries(funcs)) {\n if (STATIC_FUNC.test(func)) {\n (statics[subunit] ??= {})[func] = value;\n }\n }\n }\n this.deps.probeMemory?.set(STATIC_KEY, statics);\n // UNION with the remembered shape (same identity \u2014 the fast path proved it):\n // a refresh while the device stands by answers many functions @RESTRICTED and\n // must not strip abilities it proved while awake; a lean standby FIRST capture\n // heals on the next awake refresh instead of staying lean forever (datapoint\n // review finding, 2.0.2).\n const remembered = this.deps.probeMemory?.remembered(CAPS_KEY);\n const subunits = isCachedCapabilities(remembered)\n ? mergeYncaSubunits(remembered.subunits, fresh.subunits)\n : fresh.subunits;\n this.deps.probeMemory?.set(CAPS_KEY, {\n model: fresh.model,\n firmware: fresh.subunits.SYS?.VERSION ?? \"\",\n subunits,\n } satisfies CachedCapabilities);\n // The write map follows the union too \u2014 a standby refresh must not shrink the\n // proven write surface until the next restart either.\n this.presentEntries = presentYncaEntries({ model: fresh.model, subunits }, catalog);\n this.writeMap = idToEntry(this.presentEntries);\n // Scene titles are not datapoints any more (v2.0.0), so nothing else carries them\n // into the running session: on the fast path they came from the memory, and a scene\n // renamed at the receiver stayed invisible until the NEXT start \u2014 including for a\n // write by title, which resolved against the old list and was dropped. The refresh\n // reads SCENExNAME anyway, so the list and the lookup follow it here.\n // (The recall dropdown's LABELS cannot follow live: the unified tree is coordinated\n // once per connection, so a later object write would not be materialised. They come\n // with the next start, out of the layer just persisted.)\n const titles = sceneTitlesOf(subunits);\n if (JSON.stringify(titles) !== JSON.stringify(this.sceneTitles)) {\n this.sceneTitles = titles;\n if (titles.length > 0) {\n this.deps.setStateAck(`${this.deviceId}.scene.list`, JSON.stringify(titles));\n }\n }\n this.deps.log.debug(`${this.deviceId}: background value refresh done (YNCA)`);\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: background value refresh failed: ${String(e)}`);\n }\n }\n\n /**\n * Read the device's capabilities with the two-pass sweep. Pass 1 probes each\n * catalogued subunit with `AVAIL=?` (~2 s); pass 2 sweeps only the subunits that\n * answered, plus SYS (which never answers AVAIL) \u2014 on a typical receiver that\n * saves a third or more of the ~39 s blind sweep. A cached probe result (per\n * device, surviving reconnects and restarts) skips pass 1 entirely; a device\n * whose model or firmware no longer matches the cache re-probes. A device that\n * answers no AVAIL at all falls back to the full blind sweep, so an unknown\n * firmware loses speed, never features.\n *\n * @param catalog the (group-filtered) catalog whose functions to sweep\n * @param model the live-read SYS model name (from resolveCapabilities)\n * @param firmware the live-read SYS firmware version\n * @returns the assembled capabilities\n */\n private async sweepDevice(catalog: readonly YncaEntry[], model: string, firmware: string): Promise<YncaCapabilities> {\n const cached = this.deps.subunitCache?.get();\n if (cached) {\n // The device's IDENTITY was already read by resolveCapabilities (two reads,\n // ~0.2 s) \u2014 checking it BEFORE sweeping is what keeps a stale cache from costing\n // a full targeted sweep, then the probe, then a second sweep (~40 s).\n if (model === cached.model && firmware === cached.firmware) {\n return await this.targetedSweep(catalog, new Set(cached.subunits));\n }\n // The device behind this IP changed (swap or firmware update) \u2014 re-probe.\n this.deps.log.debug(`${this.deviceId}: cached subunit set is stale (model/firmware changed), re-probing`);\n this.deps.subunitCache?.clear();\n }\n const probe = await this.deps.client.readCapabilities(AVAIL_PROBE);\n const present = new Set(Object.keys(probe.subunits));\n if (present.size === 0) {\n // Device ignores AVAIL \u2014 sweep blind so no function is lost.\n return await this.deps.client.readCapabilities(sweepGets(catalog));\n }\n const capabilities = await this.targetedSweep(catalog, present);\n if (capabilities.model) {\n this.deps.subunitCache?.set({\n subunits: [...present],\n model: capabilities.model,\n firmware: capabilities.subunits.SYS?.VERSION ?? \"\",\n });\n }\n return capabilities;\n }\n\n /**\n * Sweep only the present subunits' functions (SYS always included \u2014 it answers no\n * AVAIL but carries model/firmware/master power).\n *\n * @param catalog the (group-filtered) catalog whose functions to sweep\n * @param present the subunits that answered the AVAIL probe\n * @returns the assembled capabilities\n */\n private async targetedSweep(catalog: readonly YncaEntry[], present: ReadonlySet<string>): Promise<YncaCapabilities> {\n const gets = sweepGets(catalog).filter(get => get.subunit === \"SYS\" || present.has(get.subunit));\n const remembered = this.deps.probeMemory?.remembered<Record<string, Record<string, string>>>(STATIC_KEY);\n // Second connect onwards: skip those reads and put the remembered answers back in, so\n // the objects are built exactly as if the device had answered them again.\n const capabilities = await this.deps.client.readCapabilities(\n remembered ? gets.filter(get => !STATIC_FUNC.test(get.func)) : gets,\n );\n if (remembered) {\n for (const [subunit, funcs] of Object.entries(remembered)) {\n capabilities.subunits[subunit] = { ...funcs, ...capabilities.subunits[subunit] };\n }\n return capabilities;\n }\n const statics: Record<string, Record<string, string>> = {};\n for (const [subunit, funcs] of Object.entries(capabilities.subunits)) {\n for (const [func, value] of Object.entries(funcs)) {\n if (STATIC_FUNC.test(func)) {\n (statics[subunit] ??= {})[func] = value;\n }\n }\n }\n this.deps.probeMemory?.set(STATIC_KEY, statics);\n return capabilities;\n }\n\n /**\n * Handle a state change: a user write (ack false) becomes a YNCA command; an\n * acked change (the device's own echo) is ignored to avoid a resend loop.\n *\n * @param fullStateId the full state id (device id + \".\" + state)\n * @param ack whether the change is acked (device-originated)\n * @param value the new value\n */\n public handleStateChange(fullStateId: string, ack: boolean, value: unknown): void {\n if (ack) {\n return;\n }\n const prefix = `${this.deviceId}.`;\n if (!fullStateId.startsWith(prefix)) {\n return;\n }\n const stateId = fullStateId.slice(prefix.length);\n if (stateId.startsWith(\"player.browse.\")) {\n this.browseEngine?.handleWrite(stateId, value);\n return;\n }\n // A scene TITLE is as valid a recall write as its number (\"Movie Viewing\" \u2192 1).\n if (stateId === \"scene.recall\" && typeof value === \"string\" && !/^\\d+$/.test(value.trim())) {\n const needle = value.trim().toLowerCase();\n const match = this.sceneTitles.find(scene => scene.title.toLowerCase() === needle);\n if (match === undefined) {\n // A dead button has to leave a trace \u2014 this was the one write path in the adapter\n // that dropped a user action without a word (#615's lesson, applied to itself).\n this.deps.log.debug(\n `${this.deviceId}: scene \"${value}\" is not one this device declares \u2014 write dropped ` +\n `(known: ${this.sceneTitles.map(scene => scene.title).join(\", \") || \"none yet\"})`,\n );\n return;\n }\n value = match.num;\n }\n // The unified player writes go to the subunit the ZONE is listening to (v2.0.0) \u2014\n // routed here, BEFORE the generic path.\n const playerWrite = /^(?:multiroom\\.(zone[234])\\.)?player\\.(playback|repeat|shuffle|next|prev)$/.exec(stateId);\n if (playerWrite) {\n this.handlePlayerWrite(playerWrite[1] ?? \"main\", `player.${playerWrite[2]}`, value);\n return;\n }\n // The unified tuner writes are band-dependent (v2.0.0) and routed here, BEFORE the\n // generic path \u2014 one state, the right wire function for the active band.\n if (this.handleTunerWrite(stateId, value)) {\n return;\n }\n const triple = yncaCommand(stateId, value, this.writeMap ?? ID_MAP);\n if (triple) {\n this.deps.client.send(triple.subunit, triple.func, triple.value);\n }\n }\n\n /**\n * Create the per-zone player mirrors (v2.0.0): every present ZONEn gets its own\n * \"now playing\" block under its multiroom folder \u2014 zones can play different\n * sources, one shared block could not show that. The defs are clones of the flat\n * block the catalog built for the main zone, plus the `source` display each zone\n * (main included) gets.\n *\n * @param capabilities the device's swept capabilities\n * @param objects the main tree's object definitions (source of the block's shape)\n */\n private async setupZonePlayers(capabilities: YncaCapabilities, objects: ObjectDef[]): Promise<void> {\n const playerObjects = objects.filter(\n object => object.id === \"player\" || (object.type === \"state\" && FLAT_PLAYER_ID.test(object.id)),\n );\n if (!playerObjects.some(object => object.type === \"state\")) {\n this.playerZones = [];\n return;\n }\n const sourceDef = (id: string): ObjectDef => ({\n id,\n type: \"state\",\n common: { name: tName(\"playingSource\"), type: \"string\", role: \"text\", read: true, write: false },\n });\n await this.deps.upsertObject(`${this.deviceId}.player.source`, sourceDef(\"player.source\"));\n this.playerZones = [\"main\"];\n for (const zone of YNCA_ZONES) {\n if (zone.key === \"main\" || capabilities.subunits[zone.subunit] === undefined) {\n continue;\n }\n this.playerZones.push(zone.key);\n for (const object of playerObjects) {\n const id = `${zone.prefix}${object.id}`;\n await this.deps.upsertObject(`${this.deviceId}.${id}`, { ...object, id });\n }\n await this.deps.upsertObject(\n `${this.deviceId}.${zone.prefix}player.source`,\n sourceDef(`${zone.prefix}player.source`),\n );\n }\n // Seed the block's resting shape: a device already playing at adapter start must\n // not show an empty source until its first input switch, and a zone NOT playing a\n // media source must show cleared values, not valueless states (2.0.0 review + live\n // deployment check). Zones ON a source get their values from the routed sweep.\n const presentFlat = new Set(\n this.presentEntries.filter(entry => FLAT_PLAYER_ID.test(entry.id)).map(entry => entry.id),\n );\n for (const zone of YNCA_ZONES) {\n if (!this.playerZones.includes(zone.key)) {\n continue;\n }\n const input = this.zoneInputs.get(zone.key);\n const playing = playerSubunitForInput(input) !== undefined;\n this.deps.setStateAck(\n `${this.deviceId}.${zone.prefix}player.source`,\n playing && input !== undefined ? input : \"\",\n );\n if (!playing) {\n for (const clear of YNCA_PLAYER_CLEAR) {\n if (presentFlat.has(clear.id)) {\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}${clear.id}`, clear.value);\n }\n }\n }\n }\n }\n\n /**\n * Feed one player-subunit value into the block of every zone listening to that\n * source \u2014 and only those: an idle source's answer (the sweep reads them all)\n * must not overwrite what the active source shows.\n *\n * @param subunit the source subunit the value came from\n * @param id the flat player state id\n * @param value the decoded value\n */\n private routePlayerUpdate(subunit: string, id: string, value: boolean | number | string): void {\n // A playback time is published in both forms, from this one value: the seconds fill\n // the media-player slot, the readable text is what a visualisation shows.\n const twin = playTimeTwin(id, value);\n for (const zone of YNCA_ZONES) {\n if (!this.playerZones.includes(zone.key)) {\n continue;\n }\n if (playerSubunitForInput(this.zoneInputs.get(zone.key)) === subunit) {\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}${id}`, value);\n if (twin) {\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}${twin.id}`, twin.value);\n }\n }\n }\n }\n\n /**\n * Track a zone's input switch: remember the input, and when the zone changed its\n * player source, clear the block (stale metadata must not linger) and ask the new\n * source for its current state \u2014 YNCA pushes changes, but a source that was already\n * playing has nothing new to push.\n *\n * @param zoneKey the zone (`main`, `zone2`, \u2026)\n * @param input the new INP value\n */\n private handleInputSwitch(zoneKey: string, input: string): void {\n const before = playerSubunitForInput(this.zoneInputs.get(zoneKey));\n this.zoneInputs.set(zoneKey, input);\n const after = playerSubunitForInput(input);\n if (before === after || !this.playerZones.includes(zoneKey)) {\n return;\n }\n const zone = YNCA_ZONES.find(z => z.key === zoneKey);\n if (!zone) {\n return;\n }\n const presentFlat = new Set(\n this.presentEntries.filter(entry => FLAT_PLAYER_ID.test(entry.id)).map(entry => entry.id),\n );\n for (const clear of YNCA_PLAYER_CLEAR) {\n if (presentFlat.has(clear.id)) {\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}${clear.id}`, clear.value);\n }\n }\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}player.source`, after === undefined ? \"\" : input);\n if (after !== undefined) {\n // Fresh reads for the newly selected source, streamed back through the live handler.\n const funcs = new Set<string>();\n for (const entry of this.presentEntries) {\n if (entry.subunit === after && FLAT_PLAYER_ID.test(entry.id) && !entry.writeOnly) {\n funcs.add(entry.readFunc ?? entry.func);\n }\n }\n for (const func of funcs) {\n this.deps.client.get(after, func);\n }\n }\n }\n\n /**\n * Route a unified player write (playback/repeat/shuffle/next/prev) to the source\n * subunit the ZONE is listening to \u2014 with the entry that subunit itself reported\n * (claim-with-proof, like every other write).\n *\n * @param zoneKey the zone the write belongs to\n * @param flatId the flat player state id\n * @param value the written value\n */\n private handlePlayerWrite(zoneKey: string, flatId: string, value: unknown): void {\n const subunit = playerSubunitForInput(this.zoneInputs.get(zoneKey));\n if (subunit === undefined) {\n this.deps.log.debug(`${this.deviceId}: ${flatId} ignored \u2014 ${zoneKey} is not playing a media source`);\n return;\n }\n const entry = this.presentEntries.find(e => e.id === flatId && e.subunit === subunit);\n if (entry === undefined) {\n this.deps.log.debug(`${this.deviceId}: ${flatId} ignored \u2014 ${subunit} did not report it`);\n return;\n }\n const triple = yncaCommand(flatId, value, new Map([[flatId, entry]]));\n if (triple) {\n this.deps.client.send(triple.subunit, triple.func, triple.value);\n }\n }\n\n /**\n * Route the band-dependent tuner writes (v2.0.0 unification): ONE frequency state\n * in kHz and ONE preset state, sent to the wire function of the ACTIVE band \u2014\n * AM/FM on the classic TUN subunit, FM/DAB on the DAB subunit (whose FM half\n * shares the flat ids). A DAB frequency write is dropped: DAB tunes by service,\n * the device has no frequency command there.\n *\n * @param stateId the state id relative to the device\n * @param value the written value\n * @returns true when the id was a band-routed tuner write (handled here)\n */\n private handleTunerWrite(stateId: string, value: unknown): boolean {\n if (stateId === \"tuner.frequency\") {\n const khz = Number(value);\n if (!Number.isFinite(khz)) {\n return true;\n }\n if (this.hasDab) {\n if (this.tunerBand === \"FM\") {\n this.sendProven(\"DAB\", \"FMFREQ\", formatWireNumber(khz / 1000, 2));\n } else {\n this.deps.log.debug(`${this.deviceId}: DAB tunes by service \u2014 frequency write ignored`);\n }\n return true;\n }\n if (this.tunerBand === \"AM\") {\n this.sendProven(\"TUN\", \"AMFREQ\", String(Math.round(khz)));\n } else {\n this.sendProven(\"TUN\", \"FMFREQ\", formatWireNumber(khz / 1000, 2));\n }\n return true;\n }\n if (stateId === \"tuner.band\") {\n // Two subunits feed this one dropdown: AM lives only on TUN, DAB only on DAB, and FM on\n // both \u2014 on a device that has DAB its FM half lives there too (that is where its FM\n // frequency and presets are). Routing by the written VALUE keeps a dual-subunit device\n // honest instead of sending every band to whichever entry happened to be mapped last.\n const band = typeof value === \"string\" ? value : \"\";\n const subunit = band === \"AM\" ? \"TUN\" : band === \"DAB\" || this.hasDab ? \"DAB\" : \"TUN\";\n const entry = this.presentEntries.find(\n candidate => candidate.id === \"tuner.band\" && candidate.subunit === subunit,\n );\n const triple = entry ? yncaCommand(stateId, value, new Map([[stateId, entry]])) : undefined;\n if (triple) {\n this.sendProven(triple.subunit, triple.func, triple.value);\n } else {\n this.deps.log.debug(`${this.deviceId}: band \"${band}\" is not available on this device \u2014 write dropped`);\n }\n return true;\n }\n if (stateId === \"tuner.preset\" && this.hasDab) {\n const slot = Math.round(Number(value));\n if (Number.isFinite(slot) && slot >= 1) {\n this.sendProven(\"DAB\", this.tunerBand === \"DAB\" ? \"DABPRESET\" : \"FMPRESET\", String(slot));\n }\n return true;\n }\n return false;\n }\n\n /**\n * Send a band-routed write ONLY with a function THIS device reported in its sweep \u2014\n * the same claim-with-proof rule every generic write obeys (#615 class). Without it\n * the router put a blind TUN:FMFREQ on the wire for any device, tuner or not, and\n * each write surfaced as a \"device refused\" warning (test-audit finding).\n *\n * @param subunit the target subunit\n * @param func the wire function\n * @param wire the encoded wire value\n */\n private sendProven(subunit: string, func: string, wire: string): void {\n if (!this.presentEntries.some(entry => entry.subunit === subunit && entry.func === func)) {\n this.deps.log.debug(`${this.deviceId}: ${subunit}:${func} not reported by this device \u2014 write dropped`);\n return;\n }\n this.deps.client.send(subunit, func, wire);\n }\n\n /**\n * Create the browsing surface (#613) when the device reports a browsable media\n * subunit: the official YNCA list vocabulary (LISTINFO/LISTSEL/LISTPAGE/LISTCURSOR)\n * drives an 8-line window under `player.browse.*`. Skipped without a delay dep\n * (older tests) and when the playback group is switched off.\n *\n * @param capabilities the device's swept capabilities\n */\n private async setupBrowse(capabilities: YncaCapabilities): Promise<void> {\n const gate = this.deps.gate;\n if (!gate || this.deps.isEntryEnabled?.(\"player.browse.source\") === false) {\n return;\n }\n const delay = (ms: number): Promise<void> => gate.delay(ms);\n const present = await this.probeBrowseSubunits(capabilities);\n if (present.size === 0) {\n // Leaving the states uncreated is what hands browsing to another transport: the owner\n // policy ranks by modernity (yxc > ynca > xml), so an unproven YNCA claim would beat a\n // PROVEN xml one and the user would get an empty menu on a device that can browse over\n // XML \u2014 exactly issue #613's RX-V473.\n this.deps.log.debug(`${this.deviceId}: no YNCA source answers LISTINFO \u2014 leaving menus to another transport`);\n return;\n }\n const driver = new YncaBrowseDriver(this.deps.client, present, delay);\n this.browseEngine = await createBrowseSurface(driver, this.deviceId, {\n upsertObject: this.deps.upsertObject,\n emit: (id, value) => this.deps.setStateAck(`${this.deviceId}.${id}`, value),\n log: this.deps.log,\n delay,\n });\n if (this.browseEngine) {\n this.browseDriver = driver;\n }\n }\n\n /**\n * Which browsable subunits actually SERVE menus, proven by asking them.\n *\n * Carrying the subunit is NOT proof: the RX-A810 reference log answers `@SERVER:LISTINFO=?`\n * with `@UNDEFINED` while NETRADIO/PC/USB on the very same device return a full window. The\n * XML driver has always probed (`List_Info` \u2192 `<Menu_Status>`); YNCA claimed the states on\n * presence alone and, ranking higher, silently displaced the transport that could deliver.\n *\n * @param capabilities the device's swept capabilities\n * @returns the subunits that answered with list data\n */\n private async probeBrowseSubunits(capabilities: YncaCapabilities): Promise<ReadonlySet<string>> {\n const candidates = YNCA_BROWSE_SOURCES.filter(source => source.subunit in capabilities.subunits);\n if (candidates.length === 0) {\n return new Set();\n }\n // A receiver in standby answers @RESTRICTED for its media subunits, which is\n // indistinguishable from \"cannot browse\" and would strip the menus off a device that\n // serves them perfectly once it is on. Nobody browses a sleeping receiver, so keep the\n // claim and let the next connect \u2014 with the device awake \u2014 do the real probe.\n if (capabilities.subunits.MAIN?.PWR !== \"On\") {\n return new Set(candidates.map(source => source.subunit));\n }\n const answer = await this.deps.client.readCapabilities(\n candidates.map(source => ({ subunit: source.subunit, func: \"LISTINFO\" })),\n );\n // Only a real list answer counts. Both refusals \u2014 `@UNDEFINED` (function unknown) and\n // `@RESTRICTED` (source not usable right now) \u2014 carry no subunit, so they cannot be\n // attributed to one request; it is the ABSENCE of an answer that excludes a subunit.\n return new Set(\n candidates\n .map(source => source.subunit)\n .filter(subunit => Object.keys(answer.subunits[subunit] ?? {}).some(func => LIST_PROOF.test(func))),\n );\n }\n\n /**\n * Register the supervisor's drop handler \u2014 delegated to the client's socket drop,\n * which is YNCA's genuine connection-lost signal.\n *\n * @param cb invoked once when the connection drops, with the reason if known\n */\n public onDrop(cb: (reason?: Error) => void): void {\n this.deps.client.onDrop(cb);\n }\n\n /** Close the client. Synchronous \u2014 safe to call from onUnload. */\n public close(): void {\n this.browseEngine?.close();\n this.browseDriver?.close();\n this.deps.client.close();\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { mergeYncaSubunits, type YncaCapabilities } from \"./ynca/capability\";\nimport { formatWireNumber } from \"./catalog/value-coerce\";\nimport { playTimeTwin } from \"./catalog/play-time\";\nimport type { ObjectDef } from \"./catalog/types\";\nimport { tName } from \"./i18n\";\nimport type { ConnectionHandle, ControllerLog } from \"./controller\";\nimport {\n YNCA_CATALOG,\n availGets,\n funcToEntry,\n idToEntry,\n presentYncaEntries,\n sweepGets,\n yncaCommand,\n yncaObjectsFor,\n yncaStateUpdate,\n type YncaEntry,\n} from \"./ynca/catalog\";\nimport type { YncaSubunitCache } from \"./ynca/subunit-cache\";\nimport type { CommandGate } from \"./lifecycle/command-gate\";\nimport type { ProbeMemory } from \"./lifecycle/probe-memory\";\nimport type { BrowseEngine } from \"./browse/browse-engine\";\nimport { createBrowseSurface } from \"./browse/surface\";\nimport { YNCA_BROWSE_SOURCES, YncaBrowseDriver } from \"./browse/ynca-browse-driver\";\n\n// The YNCA catalog and its lookup maps are static \u2014 built once for all devices.\n// SYS:MODELNAME is part of the catalog (info.model), so the sweep already covers it.\n// The AVAIL probe always covers the FULL catalog (not the group-filtered one), so the\n// cached subunit set reflects the device, never the current group configuration.\nconst FUNC_MAP = funcToEntry(YNCA_CATALOG);\nconst ID_MAP = idToEntry(YNCA_CATALOG);\nconst AVAIL_PROBE = availGets(YNCA_CATALOG);\n\n/**\n * Functions whose VALUE cannot change while the device runs: the 23 assignable input names\n * and the 12 scene names. They cost 35 of the ~187 paced reads of a targeted sweep (3.5 s\n * at the specification's mandatory 100 ms spacing) and answer the same thing every time, so\n * a reconnect reuses what the first connect learned.\n *\n * They live in the PERSISTED probe memory (the device object's `native.probeCache`), so the\n * saving survives a restart too \u2014 the freshness guard is the device identity, and a renamed\n * input heals through the background refresh, which re-reads them. (This used to be\n * documented as \"per adapter run, not persisted\"; that stopped being true with the\n * fast-restart rework and the comment was left behind.)\n */\nconst STATIC_FUNC = /^(INPNAME|SCENE\\d+NAME$)/;\n\n/** The zones whose player/input surface the controller routes (v2.0.0 player unification). */\nconst YNCA_ZONES: Array<{ key: string; subunit: string; prefix: string }> = [\n { key: \"main\", subunit: \"MAIN\", prefix: \"\" },\n { key: \"zone2\", subunit: \"ZONE2\", prefix: \"multiroom.zone2.\" },\n { key: \"zone3\", subunit: \"ZONE3\", prefix: \"multiroom.zone3.\" },\n { key: \"zone4\", subunit: \"ZONE4\", prefix: \"multiroom.zone4.\" },\n];\n\n/**\n * INP value \u2192 the player subunit it selects (normalized: uppercase, alphanumerics only).\n * The wire values come from the RX-V6A INPNAME/INP capture and ynca-python's input map;\n * inputs that are no media player (HDMI, AV, TUNER, \u2026) deliberately map to nothing.\n */\nconst INPUT_SUBUNITS: Record<string, string> = {\n NETRADIO: \"NETRADIO\",\n SERVER: \"SERVER\",\n USB: \"USB\",\n SPOTIFY: \"SPOTIFY\",\n DEEZER: \"DEEZER\",\n TIDAL: \"TIDAL\",\n NAPSTER: \"NAPSTER\",\n PANDORA: \"PANDORA\",\n RHAPSODY: \"RHAP\",\n SIRIUS: \"SIRIUS\",\n SIRIUSXM: \"SIRIUS\",\n SIRIUSIR: \"SIRIUS\",\n AIRPLAY: \"AIRPLAY\",\n BLUETOOTH: \"BT\",\n PC: \"PC\",\n MUSICCASTLINK: \"MCLINK\",\n IPOD: \"IPOD\",\n IPODUSB: \"IPODUSB\",\n};\n\n/**\n * The player subunit an input selection feeds, or undefined when the input is no\n * media player.\n *\n * @param input the zone's INP value (e.g. \"NET RADIO\", \"iPod (USB)\")\n * @returns the subunit (e.g. NETRADIO), or undefined\n */\nfunction playerSubunitForInput(input: string | undefined): string | undefined {\n if (typeof input !== \"string\" || input.length === 0) {\n return undefined;\n }\n return INPUT_SUBUNITS[input.toUpperCase().replace(/[^A-Z0-9]/g, \"\")];\n}\n\n/**\n * A state of the flat per-zone player block: exactly one segment below `player.`.\n * ONLY these are zone-routed \u2014 multi-segment `player.*` ids (bluetooth pairing\n * status, airplay volume interlock, per-source presets) are device-global and go\n * straight to their state (2.0.0 review finding: the broad `player.` prefix check\n * silently dropped BT/AirPlay status while no zone listened to that source).\n */\nconst FLAT_PLAYER_ID = /^player\\.[^.]+$/;\n\n/**\n * What a zone's player block is reset to when the zone leaves its playing source\n * (clear-on-switch): metadata empty, playback Stop. Filtered to the states the\n * device actually has before emitting.\n */\nconst YNCA_PLAYER_CLEAR: Array<{ id: string; value: number | string | boolean }> = [\n { id: \"player.playback\", value: 1 },\n { id: \"player.artist\", value: \"\" },\n { id: \"player.album\", value: \"\" },\n { id: \"player.track\", value: \"\" },\n { id: \"player.station\", value: \"\" },\n { id: \"player.channelName\", value: \"\" },\n { id: \"player.elapsedTime\", value: 0 },\n { id: \"player.elapsedTimeText\", value: \"\" },\n { id: \"player.totalTime\", value: 0 },\n { id: \"player.totalTimeText\", value: \"\" },\n { id: \"player.repeat\", value: 0 },\n { id: \"player.shuffle\", value: false },\n];\n\n/** Memory key for the remembered static values. */\nconst STATIC_KEY = \"yncaStaticValues\";\n\n/** Memory key for the persisted capability shape (the fast-restart layer). */\nconst CAPS_KEY = \"yncaCapabilities\";\n\n/** The persisted capability shape, keyed by the device identity that validated it. */\ninterface CachedCapabilities {\n /** SYS MODELNAME at capture time \u2014 freshness key half 1. */\n model: string;\n /** SYS VERSION at capture time \u2014 freshness key half 2. */\n firmware: string;\n /** The captured subunit\u2192function map (values are last-known, used for SHAPE only). */\n subunits: Record<string, Record<string, string>>;\n}\n\n/**\n * Whether a remembered value carries the cached-capabilities shape (API boundary \u2014\n * the persisted probe memory is untrusted storage).\n *\n * @param value the remembered value\n * @returns true when usable\n */\nfunction isCachedCapabilities(value: unknown): value is CachedCapabilities {\n const candidate = value as Partial<CachedCapabilities> | null;\n return (\n typeof candidate === \"object\" &&\n candidate !== null &&\n typeof candidate.model === \"string\" &&\n typeof candidate.firmware === \"string\" &&\n typeof candidate.subunits === \"object\" &&\n candidate.subunits !== null\n );\n}\n\n/**\n * The functions whose presence proves a subunit really serves menus \u2014 the fields a real\n * `LISTINFO=?` answer is made of (RX-A810 reference log). See {@link YncaDeviceController.probeBrowseSubunits}.\n */\nconst LIST_PROOF = /^(LISTLAYER|LISTLAYERNAME|CURRLINE|MAXLINE|LINE[1-8](TXT|ATRIB))$/;\n\n/**\n * The scenes a device declares, read from its `SCENExNAME` answers on MAIN. Used both by\n * the init and by the background refresh, so a scene renamed at the receiver reaches the\n * running session instead of waiting for the next start.\n *\n * @param subunits the swept subunit\u2192function map\n * @returns the declared scenes, lowest number first\n */\nfunction sceneTitlesOf(subunits: Record<string, Record<string, string>>): Array<{ num: number; title: string }> {\n const main = subunits.MAIN ?? {};\n const scenes: Array<{ num: number; title: string }> = [];\n for (let n = 1; n <= 12; n++) {\n const title = main[`SCENE${n}NAME`];\n if (typeof title === \"string\" && title.length > 0) {\n scenes.push({ num: n, title });\n }\n }\n return scenes;\n}\n\n/** The subset of the YNCA client the controller uses (so tests can inject a fake). */\nexport interface YncaClientLike {\n /** Open the connection. */\n connect(): Promise<void>;\n /** Run the init sweep and return the device's capabilities. */\n readCapabilities(gets: Array<{ subunit: string; func: string }>): Promise<YncaCapabilities>;\n /** Send a PUT command. */\n send(subunit: string, func: string, value: string): void;\n /** Send a GET request (the browse driver reads LISTINFO with it). */\n get(subunit: string, func: string): void;\n /** Register a handler for pushed messages. */\n onMessage(handler: (message: { subunit: string; func: string; value: string }) => void): void;\n /** Register the socket-drop handler the supervisor reconnects on. */\n onDrop(handler: (reason?: Error) => void): void;\n /** Register the refusal handler for user commands the device rejects (optional in older tests). */\n onRefusal?(handler: (command: string, verdict: \"restricted\" | \"undefined\") => void): void;\n /** Start the keepalive poll \u2014 called after the init sweep, not on connect. */\n startKeepalive(): void;\n /** Close the connection synchronously. */\n close(): void;\n}\n\nexport type { ControllerLog };\n\n/** The adapter callbacks the controller drives \u2014 narrow, so no adapter mock is needed in tests. */\nexport interface ControllerDeps {\n /** The YNCA client for this device. */\n client: YncaClientLike;\n /** Create or update an object in the device tree. */\n upsertObject(id: string, def: ObjectDef): Promise<void>;\n /** Write a state value with ack (device-originated). */\n setStateAck(id: string, value: boolean | number | string): void;\n /** Adapter log. */\n log: ControllerLog;\n /**\n * Whether a catalog entry's datapoint group is enabled. A disabled group's entries\n * are dropped BEFORE the sweep, so their GETs are never sent (previously only the\n * object/state writes were gated and the answers thrown away). Absent = all enabled.\n */\n isEntryEnabled?(id: string): boolean;\n /**\n * Per-device cache of the AVAIL probe result, held across reconnects and restarts.\n * With a valid cache the probe phase is skipped and the targeted sweep runs directly;\n * a model/firmware mismatch after the sweep invalidates it and re-probes.\n */\n subunitCache?: YncaSubunitCache;\n /**\n * The device's command gate: every line this controller puts on the wire is already\n * paced through it, and its signal is the connection's shutdown flag (a closed gate\n * ends pending waits and stops state writes). Absent in older tests \u2192 no browsing.\n */\n gate?: CommandGate;\n /** Per-device memory for answers that stay constant while the device runs (see ProbeMemory). */\n probeMemory?: ProbeMemory;\n}\n\n/**\n * Drives one YNCA device: connect, init sweep, build the object tree, and route\n * commands both ways. Create-only \u2014 orphan cleanup and legacy migration are\n * separate, gated steps.\n */\nexport class YncaDeviceController implements ConnectionHandle {\n private browseDriver: YncaBrowseDriver | undefined;\n private browseEngine: BrowseEngine | undefined;\n /**\n * Write map filtered to the entries THIS device reported \u2014 claim-with-proof for\n * writes: a command is only sent with a wire function the device answered in the\n * sweep. Until the sweep ran, the unfiltered static map answers.\n */\n private writeMap: Map<string, YncaEntry> | undefined;\n /** The device's scene titles (SCENExNAME), for the recall dropdown, the list state and title writes. */\n private sceneTitles: Array<{ num: number; title: string }> = [];\n /** The tuner's current band (AM/FM/DAB), for the band-dependent frequency/preset writes. */\n private tunerBand = \"\";\n /** Whether the device carries the DAB subunit (its FM half shares the flat tuner ids). */\n private hasDab = false;\n /** The entries THIS device reported \u2014 the per-subunit lookup behind the player routing. */\n private presentEntries: YncaEntry[] = [];\n /** Each zone's currently selected input (INP), for the player routing (v2.0.0). */\n private readonly zoneInputs = new Map<string, string>();\n /** The zones that got a player block (main plus every present ZONEn, when sources exist). */\n private playerZones: string[] = [];\n\n /**\n * @param deviceId the id-safe device id (object-tree path segment)\n * @param deps the client and adapter callbacks\n */\n public constructor(\n private readonly deviceId: string,\n private readonly deps: ControllerDeps,\n ) {}\n\n /**\n * Connect, sweep the device from the catalog, and create its object tree; wire\n * up push updates. The catalog is the single source: it drives the sweep, the\n * device\u2192state read-back and (in handleStateChange) the state\u2192wire encode.\n *\n * @returns true if the device reported capabilities and its tree was created\n */\n public async start(): Promise<boolean> {\n await this.deps.client.connect();\n // Group-filtered catalog: disabled groups are excluded from the sweep AND the objects.\n const catalog = this.deps.isEntryEnabled\n ? YNCA_CATALOG.filter(entry => this.deps.isEntryEnabled!(entry.id))\n : YNCA_CATALOG;\n const resolved = await this.resolveCapabilities(catalog);\n const { capabilities, fromCache } = resolved;\n const present = presentYncaEntries(capabilities, catalog);\n this.writeMap = idToEntry(present);\n this.presentEntries = present;\n // On the fast path `capabilities` carries the values of the LAST run \u2014 the persisted\n // layer is a SHAPE, its values are leftovers. Everything that DECIDES something\n // (which menus may be claimed, which wire function a band-routed write takes, which\n // source a zone's player buttons act on) is therefore re-read live before use.\n const live = fromCache ? await this.readDecisiveValues(capabilities) : capabilities;\n // Seed each zone's input from the sweep \u2014 the player routing needs to know what\n // every zone is listening to before the first INP push arrives.\n for (const zone of YNCA_ZONES) {\n const input = live.subunits[zone.subunit]?.INP;\n if (typeof input === \"string\") {\n this.zoneInputs.set(zone.key, input);\n }\n }\n const objects = yncaObjectsFor(capabilities, catalog);\n if (objects.length === 0) {\n this.deps.log.warn(`${this.deviceId}: no capabilities reported \u2014 creating no objects`);\n return false;\n }\n // A user command the device rejects must leave a trace: @RESTRICTED (not allowed /\n // not possible right now) and @UNDEFINED (unknown on this model) were silently\n // dropped before \u2014 the class of invisible failures behind #615.\n this.deps.client.onRefusal?.((command, verdict) =>\n this.deps.log.warn(`${this.deviceId}: device refused \"${command}\" (@${verdict.toUpperCase()})`),\n );\n // The scene titles ride the sweep as SCENExNAME answers; they become the recall\n // dropdown's labels and the one scene.list state (v2.0.0 \u2014 no per-name datapoints).\n this.sceneTitles = sceneTitlesOf(capabilities.subunits);\n // Parents before children (channels before their states) \u2014 created in order.\n for (const object of objects) {\n if (object.id === \"scene.recall\" && this.sceneTitles.length > 0) {\n object.common.states = Object.fromEntries(this.sceneTitles.map(scene => [scene.num, scene.title]));\n }\n await this.deps.upsertObject(`${this.deviceId}.${object.id}`, object);\n }\n if (this.sceneTitles.length > 0) {\n await this.deps.upsertObject(`${this.deviceId}.scene.list`, {\n id: \"scene.list\",\n type: \"state\",\n common: {\n name: tName(\"scenesNumberTitle\"),\n desc: tName(\"descScenesNumberTitle\"),\n type: \"string\",\n role: \"json\",\n read: true,\n write: false,\n },\n });\n this.deps.setStateAck(`${this.deviceId}.scene.list`, JSON.stringify(this.sceneTitles));\n }\n await this.setupZonePlayers(capabilities, objects);\n // Seed the states with the values read during the init sweep. On the fast path the\n // cached values are last-run leftovers \u2014 the states already hold exactly those, and\n // the background refresh streams the fresh ones in \u2014 so nothing is seeded there.\n if (!fromCache) {\n for (const [subunit, funcs] of Object.entries(capabilities.subunits)) {\n for (const [func, value] of Object.entries(funcs)) {\n const update = yncaStateUpdate({ subunit, func, value }, FUNC_MAP);\n if (update) {\n if (FLAT_PLAYER_ID.test(update.id)) {\n // Player-block values feed only the zones LISTENING to their source\n // (v2.0.0) \u2014 an idle source's leftover metadata must not seed the block.\n this.routePlayerUpdate(subunit, update.id, update.value);\n } else {\n this.deps.setStateAck(`${this.deviceId}.${update.id}`, update.value);\n }\n }\n }\n }\n }\n // The band decides which wire function a tuner.frequency/preset write goes to\n // (v2.0.0 unification) \u2014 read live above, kept fresh from the live pushes. Whether\n // the device HAS a DAB subunit is shape, so that half may come from the memory.\n this.hasDab = capabilities.subunits.DAB !== undefined;\n this.tunerBand = (live.subunits.DAB?.BAND ?? live.subunits.TUN?.BAND ?? \"\").toUpperCase();\n await this.setupBrowse(live);\n this.deps.client.onMessage(message => {\n // The browse driver sees every line first: list lines (LINE1TXT\u2026, LISTINFO\n // bursts, auto-feedback) are not catalogued and would otherwise be dropped.\n this.browseDriver?.handleMessage(message);\n if (message.func === \"BAND\" && (message.subunit === \"TUN\" || message.subunit === \"DAB\")) {\n this.tunerBand = message.value.toUpperCase();\n }\n if (message.func === \"INP\") {\n const zone = YNCA_ZONES.find(z => z.subunit === message.subunit);\n if (zone) {\n this.handleInputSwitch(zone.key, message.value);\n }\n }\n const update = yncaStateUpdate(message, FUNC_MAP);\n if (update) {\n if (FLAT_PLAYER_ID.test(update.id)) {\n this.routePlayerUpdate(message.subunit, update.id, update.value);\n } else {\n this.deps.setStateAck(`${this.deviceId}.${update.id}`, update.value);\n }\n }\n });\n // Start the keepalive only now the (fast-path) init is done; on the slow path the\n // sweep already ran, on the fast path the background refresh paces itself through\n // the same gate, so the 30 s poll cannot break the spacing either way.\n this.deps.client.startKeepalive();\n if (fromCache) {\n // The whole point of the persisted capability layer: the tree stood in ~1 s from\n // the remembered shape (validated by the LIVE identity answer above), and the\n // 15\u201320 s question round now runs behind the ready line as a pure value refresh \u2014\n // its answers stream into the states through the live handler just registered.\n void this.refreshInBackground(catalog);\n }\n // The adapter logs one combined \"ready\" line across all transports; this per-transport line\n // stays at debug for diagnostics.\n this.deps.log.debug(`${this.deviceId}: ${capabilities.model || \"device\"} ready (YNCA)`);\n return true;\n }\n\n /**\n * The device's capabilities \u2014 from the persisted fast-restart layer when the LIVE\n * identity (model + firmware, two paced reads, ~0.2 s) matches what the layer was\n * captured from, else from the full two-pass sweep. The identity read doubles as the\n * liveness proof the ready line rests on: a cached shape alone must never present a\n * dead device as connected (the v1.5.0 honesty rule).\n *\n * @param catalog the (group-filtered) catalog\n * @returns the capabilities and whether they came from the persisted layer\n */\n private async resolveCapabilities(\n catalog: readonly YncaEntry[],\n ): Promise<{ capabilities: YncaCapabilities; fromCache: boolean }> {\n const identity = await this.deps.client.readCapabilities([\n { subunit: \"SYS\", func: \"MODELNAME\" },\n { subunit: \"SYS\", func: \"VERSION\" },\n ]);\n const model = identity.model;\n const firmware = identity.subunits.SYS?.VERSION ?? \"\";\n const remembered = this.deps.probeMemory?.remembered(CAPS_KEY);\n if (model && isCachedCapabilities(remembered) && remembered.model === model && remembered.firmware === firmware) {\n return { capabilities: { model, subunits: remembered.subunits }, fromCache: true };\n }\n if (remembered !== undefined) {\n // A different (or updated) device behind this address: its remembered YNCA\n // answers are void. The other transports guard their own portions.\n this.deps.probeMemory?.drop(key => key === CAPS_KEY || key === STATIC_KEY);\n }\n const capabilities = await this.sweepDevice(catalog, model, firmware);\n if (capabilities.model) {\n this.deps.probeMemory?.set(CAPS_KEY, {\n model: capabilities.model,\n firmware: capabilities.subunits.SYS?.VERSION ?? firmware,\n subunits: capabilities.subunits,\n } satisfies CachedCapabilities);\n } else {\n // No model, no identity \u2014 and without an identity nothing can ever invalidate what was\n // remembered. The statics (input and scene names) are written by the sweep regardless,\n // so leaving them behind froze those names for good on a device that does not answer\n // SYS:MODELNAME. The two keys live and die together.\n this.deps.probeMemory?.drop(key => key === STATIC_KEY);\n }\n return { capabilities, fromCache: false };\n }\n\n /**\n * Re-read the handful of values the START makes DECISIONS from, and lay them over the\n * remembered shape (fresh wins).\n *\n * The persisted capability layer is a shape whose values are the last run's leftovers \u2014\n * the type says so (\"used for SHAPE only\"). Three decisions used them anyway, and a\n * receiver stands in standby most of the time, so the memory usually says `PWR=Standby`:\n * - the menu claim skips its proof while the device is not on, so a stale \"Standby\"\n * made YNCA claim `player.browse.*` UNPROVEN and displace the XML driver that does\n * probe \u2014 issue #613, brought back in through the cache;\n * - a `tuner.frequency` write is routed by the band, so a stale band sends AMFREQ where\n * FMFREQ belongs (a wrong command on the wire, not just a stale reading);\n * - the player's transport buttons are routed by the zone's input, so a stale input\n * sends play/pause to the source the zone listened to LAST time.\n *\n * Six reads at most (~0.6 s through the gate), once per connect. A drop during them\n * fails the connect \u2014 which is honest: the device is gone.\n *\n * @param remembered the capability shape from the persisted layer\n * @returns the remembered shape with the live answers laid over it\n */\n private async readDecisiveValues(remembered: YncaCapabilities): Promise<YncaCapabilities> {\n const gets: Array<{ subunit: string; func: string }> = [];\n if (remembered.subunits.MAIN !== undefined) {\n gets.push({ subunit: \"MAIN\", func: \"PWR\" });\n }\n for (const zone of YNCA_ZONES) {\n if (remembered.subunits[zone.subunit] !== undefined) {\n gets.push({ subunit: zone.subunit, func: \"INP\" });\n }\n }\n for (const subunit of [\"TUN\", \"DAB\"]) {\n if (remembered.subunits[subunit] !== undefined) {\n gets.push({ subunit, func: \"BAND\" });\n }\n }\n if (gets.length === 0) {\n return remembered;\n }\n const fresh = await this.deps.client.readCapabilities(gets);\n return { model: remembered.model, subunits: mergeYncaSubunits(remembered.subunits, fresh.subunits) };\n }\n\n /**\n * The fast path's second half: re-ask every catalogued function of the present\n * subunits \u2014 the answers stream into the states through the live message handler,\n * so current values arrive within the usual sweep time WITHOUT having gated the\n * ready line. Completion refreshes the persisted layers (capabilities, statics)\n * and the write map; a SHAPE change (a function newly answered) is only persisted \u2014\n * its object appears on the next start, because the unified tree is coordinated\n * once per connect and a late upsert would not materialize.\n *\n * @param catalog the (group-filtered) catalog\n */\n private async refreshInBackground(catalog: readonly YncaEntry[]): Promise<void> {\n try {\n const cached = this.deps.subunitCache?.get();\n const gets = sweepGets(catalog).filter(\n get => get.subunit === \"SYS\" || !cached || cached.subunits.includes(get.subunit),\n );\n const fresh = await this.deps.client.readCapabilities(gets);\n if (!fresh.model) {\n // The refresh ran into a drop \u2014 the supervisor handles the reconnect.\n return;\n }\n const statics: Record<string, Record<string, string>> = {};\n for (const [subunit, funcs] of Object.entries(fresh.subunits)) {\n for (const [func, value] of Object.entries(funcs)) {\n if (STATIC_FUNC.test(func)) {\n (statics[subunit] ??= {})[func] = value;\n }\n }\n }\n this.deps.probeMemory?.set(STATIC_KEY, statics);\n // UNION with the remembered shape (same identity \u2014 the fast path proved it):\n // a refresh while the device stands by answers many functions @RESTRICTED and\n // must not strip abilities it proved while awake; a lean standby FIRST capture\n // heals on the next awake refresh instead of staying lean forever (datapoint\n // review finding, 2.0.2).\n const remembered = this.deps.probeMemory?.remembered(CAPS_KEY);\n const subunits = isCachedCapabilities(remembered)\n ? mergeYncaSubunits(remembered.subunits, fresh.subunits)\n : fresh.subunits;\n this.deps.probeMemory?.set(CAPS_KEY, {\n model: fresh.model,\n firmware: fresh.subunits.SYS?.VERSION ?? \"\",\n subunits,\n } satisfies CachedCapabilities);\n // The write map follows the union too \u2014 a standby refresh must not shrink the\n // proven write surface until the next restart either.\n this.presentEntries = presentYncaEntries({ model: fresh.model, subunits }, catalog);\n this.writeMap = idToEntry(this.presentEntries);\n // Scene titles are not datapoints any more (v2.0.0), so nothing else carries them\n // into the running session: on the fast path they came from the memory, and a scene\n // renamed at the receiver stayed invisible until the NEXT start \u2014 including for a\n // write by title, which resolved against the old list and was dropped. The refresh\n // reads SCENExNAME anyway, so the list and the lookup follow it here.\n // (The recall dropdown's LABELS cannot follow live: the unified tree is coordinated\n // once per connection, so a later object write would not be materialised. They come\n // with the next start, out of the layer just persisted.)\n const titles = sceneTitlesOf(subunits);\n if (JSON.stringify(titles) !== JSON.stringify(this.sceneTitles)) {\n this.sceneTitles = titles;\n if (titles.length > 0) {\n this.deps.setStateAck(`${this.deviceId}.scene.list`, JSON.stringify(titles));\n }\n }\n this.deps.log.debug(`${this.deviceId}: background value refresh done (YNCA)`);\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: background value refresh failed: ${String(e)}`);\n }\n }\n\n /**\n * Read the device's capabilities with the two-pass sweep. Pass 1 probes each\n * catalogued subunit with `AVAIL=?` (~2 s); pass 2 sweeps only the subunits that\n * answered, plus SYS (which never answers AVAIL) \u2014 on a typical receiver that\n * saves a third or more of the ~39 s blind sweep. A cached probe result (per\n * device, surviving reconnects and restarts) skips pass 1 entirely; a device\n * whose model or firmware no longer matches the cache re-probes. A device that\n * answers no AVAIL at all falls back to the full blind sweep, so an unknown\n * firmware loses speed, never features.\n *\n * @param catalog the (group-filtered) catalog whose functions to sweep\n * @param model the live-read SYS model name (from resolveCapabilities)\n * @param firmware the live-read SYS firmware version\n * @returns the assembled capabilities\n */\n private async sweepDevice(catalog: readonly YncaEntry[], model: string, firmware: string): Promise<YncaCapabilities> {\n const cached = this.deps.subunitCache?.get();\n if (cached) {\n // The device's IDENTITY was already read by resolveCapabilities (two reads,\n // ~0.2 s) \u2014 checking it BEFORE sweeping is what keeps a stale cache from costing\n // a full targeted sweep, then the probe, then a second sweep (~40 s).\n if (model === cached.model && firmware === cached.firmware) {\n return await this.targetedSweep(catalog, new Set(cached.subunits));\n }\n // The device behind this IP changed (swap or firmware update) \u2014 re-probe.\n this.deps.log.debug(`${this.deviceId}: cached subunit set is stale (model/firmware changed), re-probing`);\n this.deps.subunitCache?.clear();\n }\n const probe = await this.deps.client.readCapabilities(AVAIL_PROBE);\n const present = new Set(Object.keys(probe.subunits));\n if (present.size === 0) {\n // Device ignores AVAIL \u2014 sweep blind so no function is lost.\n return await this.deps.client.readCapabilities(sweepGets(catalog));\n }\n const capabilities = await this.targetedSweep(catalog, present);\n if (capabilities.model) {\n this.deps.subunitCache?.set({\n subunits: [...present],\n model: capabilities.model,\n firmware: capabilities.subunits.SYS?.VERSION ?? \"\",\n });\n }\n return capabilities;\n }\n\n /**\n * Sweep only the present subunits' functions (SYS always included \u2014 it answers no\n * AVAIL but carries model/firmware/master power).\n *\n * @param catalog the (group-filtered) catalog whose functions to sweep\n * @param present the subunits that answered the AVAIL probe\n * @returns the assembled capabilities\n */\n private async targetedSweep(catalog: readonly YncaEntry[], present: ReadonlySet<string>): Promise<YncaCapabilities> {\n const gets = sweepGets(catalog).filter(get => get.subunit === \"SYS\" || present.has(get.subunit));\n const remembered = this.deps.probeMemory?.remembered<Record<string, Record<string, string>>>(STATIC_KEY);\n // Second connect onwards: skip those reads and put the remembered answers back in, so\n // the objects are built exactly as if the device had answered them again.\n const capabilities = await this.deps.client.readCapabilities(\n remembered ? gets.filter(get => !STATIC_FUNC.test(get.func)) : gets,\n );\n if (remembered) {\n for (const [subunit, funcs] of Object.entries(remembered)) {\n capabilities.subunits[subunit] = { ...funcs, ...capabilities.subunits[subunit] };\n }\n return capabilities;\n }\n const statics: Record<string, Record<string, string>> = {};\n for (const [subunit, funcs] of Object.entries(capabilities.subunits)) {\n for (const [func, value] of Object.entries(funcs)) {\n if (STATIC_FUNC.test(func)) {\n (statics[subunit] ??= {})[func] = value;\n }\n }\n }\n this.deps.probeMemory?.set(STATIC_KEY, statics);\n return capabilities;\n }\n\n /**\n * Handle a state change: a user write (ack false) becomes a YNCA command; an\n * acked change (the device's own echo) is ignored to avoid a resend loop.\n *\n * @param fullStateId the full state id (device id + \".\" + state)\n * @param ack whether the change is acked (device-originated)\n * @param value the new value\n */\n public handleStateChange(fullStateId: string, ack: boolean, value: unknown): void {\n if (ack) {\n return;\n }\n const prefix = `${this.deviceId}.`;\n if (!fullStateId.startsWith(prefix)) {\n return;\n }\n const stateId = fullStateId.slice(prefix.length);\n if (stateId.startsWith(\"player.browse.\")) {\n this.browseEngine?.handleWrite(stateId, value);\n return;\n }\n // A scene TITLE is as valid a recall write as its number (\"Movie Viewing\" \u2192 1).\n if (stateId === \"scene.recall\" && typeof value === \"string\" && !/^\\d+$/.test(value.trim())) {\n const needle = value.trim().toLowerCase();\n const match = this.sceneTitles.find(scene => scene.title.toLowerCase() === needle);\n if (match === undefined) {\n // A dead button has to leave a trace \u2014 this was the one write path in the adapter\n // that dropped a user action without a word (#615's lesson, applied to itself).\n this.deps.log.debug(\n `${this.deviceId}: scene \"${value}\" is not one this device declares \u2014 write dropped ` +\n `(known: ${this.sceneTitles.map(scene => scene.title).join(\", \") || \"none yet\"})`,\n );\n return;\n }\n value = match.num;\n }\n // The unified player writes go to the subunit the ZONE is listening to (v2.0.0) \u2014\n // routed here, BEFORE the generic path.\n const playerWrite = /^(?:multiroom\\.(zone[234])\\.)?player\\.(playback|repeat|shuffle|next|prev)$/.exec(stateId);\n if (playerWrite) {\n this.handlePlayerWrite(playerWrite[1] ?? \"main\", `player.${playerWrite[2]}`, value);\n return;\n }\n // The unified tuner writes are band-dependent (v2.0.0) and routed here, BEFORE the\n // generic path \u2014 one state, the right wire function for the active band.\n if (this.handleTunerWrite(stateId, value)) {\n return;\n }\n const triple = yncaCommand(stateId, value, this.writeMap ?? ID_MAP);\n if (triple) {\n this.deps.client.send(triple.subunit, triple.func, triple.value);\n }\n }\n\n /**\n * Create the per-zone player mirrors (v2.0.0): every present ZONEn gets its own\n * \"now playing\" block under its multiroom folder \u2014 zones can play different\n * sources, one shared block could not show that. The defs are clones of the flat\n * block the catalog built for the main zone, plus the `source` display each zone\n * (main included) gets.\n *\n * @param capabilities the device's swept capabilities\n * @param objects the main tree's object definitions (source of the block's shape)\n */\n private async setupZonePlayers(capabilities: YncaCapabilities, objects: ObjectDef[]): Promise<void> {\n const playerObjects = objects.filter(\n object => object.id === \"player\" || (object.type === \"state\" && FLAT_PLAYER_ID.test(object.id)),\n );\n if (!playerObjects.some(object => object.type === \"state\")) {\n this.playerZones = [];\n return;\n }\n const sourceDef = (id: string): ObjectDef => ({\n id,\n type: \"state\",\n common: {\n name: tName(\"playingSource\"),\n desc: tName(\"descPlayingSource\"),\n type: \"string\",\n role: \"text\",\n read: true,\n write: false,\n },\n });\n await this.deps.upsertObject(`${this.deviceId}.player.source`, sourceDef(\"player.source\"));\n this.playerZones = [\"main\"];\n for (const zone of YNCA_ZONES) {\n if (zone.key === \"main\" || capabilities.subunits[zone.subunit] === undefined) {\n continue;\n }\n this.playerZones.push(zone.key);\n for (const object of playerObjects) {\n const id = `${zone.prefix}${object.id}`;\n await this.deps.upsertObject(`${this.deviceId}.${id}`, { ...object, id });\n }\n await this.deps.upsertObject(\n `${this.deviceId}.${zone.prefix}player.source`,\n sourceDef(`${zone.prefix}player.source`),\n );\n }\n // Seed the block's resting shape: a device already playing at adapter start must\n // not show an empty source until its first input switch, and a zone NOT playing a\n // media source must show cleared values, not valueless states (2.0.0 review + live\n // deployment check). Zones ON a source get their values from the routed sweep.\n const presentFlat = new Set(\n this.presentEntries.filter(entry => FLAT_PLAYER_ID.test(entry.id)).map(entry => entry.id),\n );\n for (const zone of YNCA_ZONES) {\n if (!this.playerZones.includes(zone.key)) {\n continue;\n }\n const input = this.zoneInputs.get(zone.key);\n const playing = playerSubunitForInput(input) !== undefined;\n this.deps.setStateAck(\n `${this.deviceId}.${zone.prefix}player.source`,\n playing && input !== undefined ? input : \"\",\n );\n if (!playing) {\n for (const clear of YNCA_PLAYER_CLEAR) {\n if (presentFlat.has(clear.id)) {\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}${clear.id}`, clear.value);\n }\n }\n }\n }\n }\n\n /**\n * Feed one player-subunit value into the block of every zone listening to that\n * source \u2014 and only those: an idle source's answer (the sweep reads them all)\n * must not overwrite what the active source shows.\n *\n * @param subunit the source subunit the value came from\n * @param id the flat player state id\n * @param value the decoded value\n */\n private routePlayerUpdate(subunit: string, id: string, value: boolean | number | string): void {\n // A playback time is published in both forms, from this one value: the seconds fill\n // the media-player slot, the readable text is what a visualisation shows.\n const twin = playTimeTwin(id, value);\n for (const zone of YNCA_ZONES) {\n if (!this.playerZones.includes(zone.key)) {\n continue;\n }\n if (playerSubunitForInput(this.zoneInputs.get(zone.key)) === subunit) {\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}${id}`, value);\n if (twin) {\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}${twin.id}`, twin.value);\n }\n }\n }\n }\n\n /**\n * Track a zone's input switch: remember the input, and when the zone changed its\n * player source, clear the block (stale metadata must not linger) and ask the new\n * source for its current state \u2014 YNCA pushes changes, but a source that was already\n * playing has nothing new to push.\n *\n * @param zoneKey the zone (`main`, `zone2`, \u2026)\n * @param input the new INP value\n */\n private handleInputSwitch(zoneKey: string, input: string): void {\n const before = playerSubunitForInput(this.zoneInputs.get(zoneKey));\n this.zoneInputs.set(zoneKey, input);\n const after = playerSubunitForInput(input);\n if (before === after || !this.playerZones.includes(zoneKey)) {\n return;\n }\n const zone = YNCA_ZONES.find(z => z.key === zoneKey);\n if (!zone) {\n return;\n }\n const presentFlat = new Set(\n this.presentEntries.filter(entry => FLAT_PLAYER_ID.test(entry.id)).map(entry => entry.id),\n );\n for (const clear of YNCA_PLAYER_CLEAR) {\n if (presentFlat.has(clear.id)) {\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}${clear.id}`, clear.value);\n }\n }\n this.deps.setStateAck(`${this.deviceId}.${zone.prefix}player.source`, after === undefined ? \"\" : input);\n if (after !== undefined) {\n // Fresh reads for the newly selected source, streamed back through the live handler.\n const funcs = new Set<string>();\n for (const entry of this.presentEntries) {\n if (entry.subunit === after && FLAT_PLAYER_ID.test(entry.id) && !entry.writeOnly) {\n funcs.add(entry.readFunc ?? entry.func);\n }\n }\n for (const func of funcs) {\n this.deps.client.get(after, func);\n }\n }\n }\n\n /**\n * Route a unified player write (playback/repeat/shuffle/next/prev) to the source\n * subunit the ZONE is listening to \u2014 with the entry that subunit itself reported\n * (claim-with-proof, like every other write).\n *\n * @param zoneKey the zone the write belongs to\n * @param flatId the flat player state id\n * @param value the written value\n */\n private handlePlayerWrite(zoneKey: string, flatId: string, value: unknown): void {\n const subunit = playerSubunitForInput(this.zoneInputs.get(zoneKey));\n if (subunit === undefined) {\n this.deps.log.debug(`${this.deviceId}: ${flatId} ignored \u2014 ${zoneKey} is not playing a media source`);\n return;\n }\n const entry = this.presentEntries.find(e => e.id === flatId && e.subunit === subunit);\n if (entry === undefined) {\n this.deps.log.debug(`${this.deviceId}: ${flatId} ignored \u2014 ${subunit} did not report it`);\n return;\n }\n const triple = yncaCommand(flatId, value, new Map([[flatId, entry]]));\n if (triple) {\n this.deps.client.send(triple.subunit, triple.func, triple.value);\n }\n }\n\n /**\n * Route the band-dependent tuner writes (v2.0.0 unification): ONE frequency state\n * in kHz and ONE preset state, sent to the wire function of the ACTIVE band \u2014\n * AM/FM on the classic TUN subunit, FM/DAB on the DAB subunit (whose FM half\n * shares the flat ids). A DAB frequency write is dropped: DAB tunes by service,\n * the device has no frequency command there.\n *\n * @param stateId the state id relative to the device\n * @param value the written value\n * @returns true when the id was a band-routed tuner write (handled here)\n */\n private handleTunerWrite(stateId: string, value: unknown): boolean {\n if (stateId === \"tuner.frequency\") {\n const khz = Number(value);\n if (!Number.isFinite(khz)) {\n return true;\n }\n if (this.hasDab) {\n if (this.tunerBand === \"FM\") {\n this.sendProven(\"DAB\", \"FMFREQ\", formatWireNumber(khz / 1000, 2));\n } else {\n this.deps.log.debug(`${this.deviceId}: DAB tunes by service \u2014 frequency write ignored`);\n }\n return true;\n }\n if (this.tunerBand === \"AM\") {\n this.sendProven(\"TUN\", \"AMFREQ\", String(Math.round(khz)));\n } else {\n this.sendProven(\"TUN\", \"FMFREQ\", formatWireNumber(khz / 1000, 2));\n }\n return true;\n }\n if (stateId === \"tuner.band\") {\n // Two subunits feed this one dropdown: AM lives only on TUN, DAB only on DAB, and FM on\n // both \u2014 on a device that has DAB its FM half lives there too (that is where its FM\n // frequency and presets are). Routing by the written VALUE keeps a dual-subunit device\n // honest instead of sending every band to whichever entry happened to be mapped last.\n const band = typeof value === \"string\" ? value : \"\";\n const subunit = band === \"AM\" ? \"TUN\" : band === \"DAB\" || this.hasDab ? \"DAB\" : \"TUN\";\n const entry = this.presentEntries.find(\n candidate => candidate.id === \"tuner.band\" && candidate.subunit === subunit,\n );\n const triple = entry ? yncaCommand(stateId, value, new Map([[stateId, entry]])) : undefined;\n if (triple) {\n this.sendProven(triple.subunit, triple.func, triple.value);\n } else {\n this.deps.log.debug(`${this.deviceId}: band \"${band}\" is not available on this device \u2014 write dropped`);\n }\n return true;\n }\n if (stateId === \"tuner.preset\" && this.hasDab) {\n const slot = Math.round(Number(value));\n if (Number.isFinite(slot) && slot >= 1) {\n this.sendProven(\"DAB\", this.tunerBand === \"DAB\" ? \"DABPRESET\" : \"FMPRESET\", String(slot));\n }\n return true;\n }\n return false;\n }\n\n /**\n * Send a band-routed write ONLY with a function THIS device reported in its sweep \u2014\n * the same claim-with-proof rule every generic write obeys (#615 class). Without it\n * the router put a blind TUN:FMFREQ on the wire for any device, tuner or not, and\n * each write surfaced as a \"device refused\" warning (test-audit finding).\n *\n * @param subunit the target subunit\n * @param func the wire function\n * @param wire the encoded wire value\n */\n private sendProven(subunit: string, func: string, wire: string): void {\n if (!this.presentEntries.some(entry => entry.subunit === subunit && entry.func === func)) {\n this.deps.log.debug(`${this.deviceId}: ${subunit}:${func} not reported by this device \u2014 write dropped`);\n return;\n }\n this.deps.client.send(subunit, func, wire);\n }\n\n /**\n * Create the browsing surface (#613) when the device reports a browsable media\n * subunit: the official YNCA list vocabulary (LISTINFO/LISTSEL/LISTPAGE/LISTCURSOR)\n * drives an 8-line window under `player.browse.*`. Skipped without a delay dep\n * (older tests) and when the playback group is switched off.\n *\n * @param capabilities the device's swept capabilities\n */\n private async setupBrowse(capabilities: YncaCapabilities): Promise<void> {\n const gate = this.deps.gate;\n if (!gate || this.deps.isEntryEnabled?.(\"player.browse.source\") === false) {\n return;\n }\n const delay = (ms: number): Promise<void> => gate.delay(ms);\n const present = await this.probeBrowseSubunits(capabilities);\n if (present.size === 0) {\n // Leaving the states uncreated is what hands browsing to another transport: the owner\n // policy ranks by modernity (yxc > ynca > xml), so an unproven YNCA claim would beat a\n // PROVEN xml one and the user would get an empty menu on a device that can browse over\n // XML \u2014 exactly issue #613's RX-V473.\n this.deps.log.debug(`${this.deviceId}: no YNCA source answers LISTINFO \u2014 leaving menus to another transport`);\n return;\n }\n const driver = new YncaBrowseDriver(this.deps.client, present, delay);\n this.browseEngine = await createBrowseSurface(driver, this.deviceId, {\n upsertObject: this.deps.upsertObject,\n emit: (id, value) => this.deps.setStateAck(`${this.deviceId}.${id}`, value),\n log: this.deps.log,\n delay,\n });\n if (this.browseEngine) {\n this.browseDriver = driver;\n }\n }\n\n /**\n * Which browsable subunits actually SERVE menus, proven by asking them.\n *\n * Carrying the subunit is NOT proof: the RX-A810 reference log answers `@SERVER:LISTINFO=?`\n * with `@UNDEFINED` while NETRADIO/PC/USB on the very same device return a full window. The\n * XML driver has always probed (`List_Info` \u2192 `<Menu_Status>`); YNCA claimed the states on\n * presence alone and, ranking higher, silently displaced the transport that could deliver.\n *\n * @param capabilities the device's swept capabilities\n * @returns the subunits that answered with list data\n */\n private async probeBrowseSubunits(capabilities: YncaCapabilities): Promise<ReadonlySet<string>> {\n const candidates = YNCA_BROWSE_SOURCES.filter(source => source.subunit in capabilities.subunits);\n if (candidates.length === 0) {\n return new Set();\n }\n // A receiver in standby answers @RESTRICTED for its media subunits, which is\n // indistinguishable from \"cannot browse\" and would strip the menus off a device that\n // serves them perfectly once it is on. Nobody browses a sleeping receiver, so keep the\n // claim and let the next connect \u2014 with the device awake \u2014 do the real probe.\n if (capabilities.subunits.MAIN?.PWR !== \"On\") {\n return new Set(candidates.map(source => source.subunit));\n }\n const answer = await this.deps.client.readCapabilities(\n candidates.map(source => ({ subunit: source.subunit, func: \"LISTINFO\" })),\n );\n // Only a real list answer counts. Both refusals \u2014 `@UNDEFINED` (function unknown) and\n // `@RESTRICTED` (source not usable right now) \u2014 carry no subunit, so they cannot be\n // attributed to one request; it is the ABSENCE of an answer that excludes a subunit.\n return new Set(\n candidates\n .map(source => source.subunit)\n .filter(subunit => Object.keys(answer.subunits[subunit] ?? {}).some(func => LIST_PROOF.test(func))),\n );\n }\n\n /**\n * Register the supervisor's drop handler \u2014 delegated to the client's socket drop,\n * which is YNCA's genuine connection-lost signal.\n *\n * @param cb invoked once when the connection drops, with the reason if known\n */\n public onDrop(cb: (reason?: Error) => void): void {\n this.deps.client.onDrop(cb);\n }\n\n /** Close the client. Synchronous \u2014 safe to call from onUnload. */\n public close(): void {\n this.browseEngine?.close();\n this.browseDriver?.close();\n this.deps.client.close();\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAAyD;AACzD,0BAAiC;AACjC,uBAA6B;AAE7B,kBAAsB;AAEtB,qBAWO;AAKP,qBAAoC;AACpC,gCAAsD;AAMtD,MAAM,eAAW,4BAAY,2BAAY;AACzC,MAAM,aAAS,0BAAU,2BAAY;AACrC,MAAM,kBAAc,0BAAU,2BAAY;AAc1C,MAAM,cAAc;AAGpB,MAAM,aAAsE;AAAA,EAC1E,EAAE,KAAK,QAAQ,SAAS,QAAQ,QAAQ,GAAG;AAAA,EAC3C,EAAE,KAAK,SAAS,SAAS,SAAS,QAAQ,mBAAmB;AAAA,EAC7D,EAAE,KAAK,SAAS,SAAS,SAAS,QAAQ,mBAAmB;AAAA,EAC7D,EAAE,KAAK,SAAS,SAAS,SAAS,QAAQ,mBAAmB;AAC/D;AAOA,MAAM,iBAAyC;AAAA,EAC7C,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,UAAU;AAAA,EACV,SAAS;AAAA,EACT,WAAW;AAAA,EACX,IAAI;AAAA,EACJ,eAAe;AAAA,EACf,MAAM;AAAA,EACN,SAAS;AACX;AASA,SAAS,sBAAsB,OAA+C;AAC5E,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAAG;AACnD,WAAO;AAAA,EACT;AACA,SAAO,eAAe,MAAM,YAAY,EAAE,QAAQ,cAAc,EAAE,CAAC;AACrE;AASA,MAAM,iBAAiB;AAOvB,MAAM,oBAA6E;AAAA,EACjF,EAAE,IAAI,mBAAmB,OAAO,EAAE;AAAA,EAClC,EAAE,IAAI,iBAAiB,OAAO,GAAG;AAAA,EACjC,EAAE,IAAI,gBAAgB,OAAO,GAAG;AAAA,EAChC,EAAE,IAAI,gBAAgB,OAAO,GAAG;AAAA,EAChC,EAAE,IAAI,kBAAkB,OAAO,GAAG;AAAA,EAClC,EAAE,IAAI,sBAAsB,OAAO,GAAG;AAAA,EACtC,EAAE,IAAI,sBAAsB,OAAO,EAAE;AAAA,EACrC,EAAE,IAAI,0BAA0B,OAAO,GAAG;AAAA,EAC1C,EAAE,IAAI,oBAAoB,OAAO,EAAE;AAAA,EACnC,EAAE,IAAI,wBAAwB,OAAO,GAAG;AAAA,EACxC,EAAE,IAAI,iBAAiB,OAAO,EAAE;AAAA,EAChC,EAAE,IAAI,kBAAkB,OAAO,MAAM;AACvC;AAGA,MAAM,aAAa;AAGnB,MAAM,WAAW;AAmBjB,SAAS,qBAAqB,OAA6C;AACzE,QAAM,YAAY;AAClB,SACE,OAAO,cAAc,YACrB,cAAc,QACd,OAAO,UAAU,UAAU,YAC3B,OAAO,UAAU,aAAa,YAC9B,OAAO,UAAU,aAAa,YAC9B,UAAU,aAAa;AAE3B;AAMA,MAAM,aAAa;AAUnB,SAAS,cAAc,UAAyF;AA7KhH;AA8KE,QAAM,QAAO,cAAS,SAAT,YAAiB,CAAC;AAC/B,QAAM,SAAgD,CAAC;AACvD,WAAS,IAAI,GAAG,KAAK,IAAI,KAAK;AAC5B,UAAM,QAAQ,KAAK,QAAQ,CAAC,MAAM;AAClC,QAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG;AACjD,aAAO,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC;AAAA,IAC/B;AAAA,EACF;AACA,SAAO;AACT;AA+DO,MAAM,qBAAiD;AAAA;AAAA;AAAA;AAAA;AAAA,EA0BrD,YACY,UACA,MACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EA5BK;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA;AAAA;AAAA,EAEA,cAAqD,CAAC;AAAA;AAAA,EAEtD,YAAY;AAAA;AAAA,EAEZ,SAAS;AAAA;AAAA,EAET,iBAA8B,CAAC;AAAA;AAAA,EAEtB,aAAa,oBAAI,IAAoB;AAAA;AAAA,EAE9C,cAAwB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBjC,MAAa,QAA0B;AA5RzC;AA6RI,UAAM,KAAK,KAAK,OAAO,QAAQ;AAE/B,UAAM,UAAU,KAAK,KAAK,iBACtB,4BAAa,OAAO,WAAS,KAAK,KAAK,eAAgB,MAAM,EAAE,CAAC,IAChE;AACJ,UAAM,WAAW,MAAM,KAAK,oBAAoB,OAAO;AACvD,UAAM,EAAE,cAAc,UAAU,IAAI;AACpC,UAAM,cAAU,mCAAmB,cAAc,OAAO;AACxD,SAAK,eAAW,0BAAU,OAAO;AACjC,SAAK,iBAAiB;AAKtB,UAAM,OAAO,YAAY,MAAM,KAAK,mBAAmB,YAAY,IAAI;AAGvE,eAAW,QAAQ,YAAY;AAC7B,YAAM,SAAQ,UAAK,SAAS,KAAK,OAAO,MAA1B,mBAA6B;AAC3C,UAAI,OAAO,UAAU,UAAU;AAC7B,aAAK,WAAW,IAAI,KAAK,KAAK,KAAK;AAAA,MACrC;AAAA,IACF;AACA,UAAM,cAAU,+BAAe,cAAc,OAAO;AACpD,QAAI,QAAQ,WAAW,GAAG;AACxB,WAAK,KAAK,IAAI,KAAK,GAAG,KAAK,QAAQ,uDAAkD;AACrF,aAAO;AAAA,IACT;AAIA,qBAAK,KAAK,QAAO,cAAjB;AAAA;AAAA,MAA6B,CAAC,SAAS,YACrC,KAAK,KAAK,IAAI,KAAK,GAAG,KAAK,QAAQ,qBAAqB,OAAO,OAAO,QAAQ,YAAY,CAAC,GAAG;AAAA;AAIhG,SAAK,cAAc,cAAc,aAAa,QAAQ;AAEtD,eAAW,UAAU,SAAS;AAC5B,UAAI,OAAO,OAAO,kBAAkB,KAAK,YAAY,SAAS,GAAG;AAC/D,eAAO,OAAO,SAAS,OAAO,YAAY,KAAK,YAAY,IAAI,WAAS,CAAC,MAAM,KAAK,MAAM,KAAK,CAAC,CAAC;AAAA,MACnG;AACA,YAAM,KAAK,KAAK,aAAa,GAAG,KAAK,QAAQ,IAAI,OAAO,EAAE,IAAI,MAAM;AAAA,IACtE;AACA,QAAI,KAAK,YAAY,SAAS,GAAG;AAC/B,YAAM,KAAK,KAAK,aAAa,GAAG,KAAK,QAAQ,eAAe;AAAA,QAC1D,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,UAAM,mBAAM,mBAAmB;AAAA,UAC/B,UAAM,mBAAM,uBAAuB;AAAA,UACnC,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,OAAO;AAAA,QACT;AAAA,MACF,CAAC;AACD,WAAK,KAAK,YAAY,GAAG,KAAK,QAAQ,eAAe,KAAK,UAAU,KAAK,WAAW,CAAC;AAAA,IACvF;AACA,UAAM,KAAK,iBAAiB,cAAc,OAAO;AAIjD,QAAI,CAAC,WAAW;AACd,iBAAW,CAAC,SAAS,KAAK,KAAK,OAAO,QAAQ,aAAa,QAAQ,GAAG;AACpE,mBAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AACjD,gBAAM,aAAS,gCAAgB,EAAE,SAAS,MAAM,MAAM,GAAG,QAAQ;AACjE,cAAI,QAAQ;AACV,gBAAI,eAAe,KAAK,OAAO,EAAE,GAAG;AAGlC,mBAAK,kBAAkB,SAAS,OAAO,IAAI,OAAO,KAAK;AAAA,YACzD,OAAO;AACL,mBAAK,KAAK,YAAY,GAAG,KAAK,QAAQ,IAAI,OAAO,EAAE,IAAI,OAAO,KAAK;AAAA,YACrE;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAIA,SAAK,SAAS,aAAa,SAAS,QAAQ;AAC5C,SAAK,cAAa,sBAAK,SAAS,QAAd,mBAAmB,SAAnB,aAA2B,UAAK,SAAS,QAAd,mBAAmB,SAA9C,YAAsD,IAAI,YAAY;AACxF,UAAM,KAAK,YAAY,IAAI;AAC3B,SAAK,KAAK,OAAO,UAAU,aAAW;AAlX1C,UAAAA;AAqXM,OAAAA,MAAA,KAAK,iBAAL,gBAAAA,IAAmB,cAAc;AACjC,UAAI,QAAQ,SAAS,WAAW,QAAQ,YAAY,SAAS,QAAQ,YAAY,QAAQ;AACvF,aAAK,YAAY,QAAQ,MAAM,YAAY;AAAA,MAC7C;AACA,UAAI,QAAQ,SAAS,OAAO;AAC1B,cAAM,OAAO,WAAW,KAAK,OAAK,EAAE,YAAY,QAAQ,OAAO;AAC/D,YAAI,MAAM;AACR,eAAK,kBAAkB,KAAK,KAAK,QAAQ,KAAK;AAAA,QAChD;AAAA,MACF;AACA,YAAM,aAAS,gCAAgB,SAAS,QAAQ;AAChD,UAAI,QAAQ;AACV,YAAI,eAAe,KAAK,OAAO,EAAE,GAAG;AAClC,eAAK,kBAAkB,QAAQ,SAAS,OAAO,IAAI,OAAO,KAAK;AAAA,QACjE,OAAO;AACL,eAAK,KAAK,YAAY,GAAG,KAAK,QAAQ,IAAI,OAAO,EAAE,IAAI,OAAO,KAAK;AAAA,QACrE;AAAA,MACF;AAAA,IACF,CAAC;AAID,SAAK,KAAK,OAAO,eAAe;AAChC,QAAI,WAAW;AAKb,WAAK,KAAK,oBAAoB,OAAO;AAAA,IACvC;AAGA,SAAK,KAAK,IAAI,MAAM,GAAG,KAAK,QAAQ,KAAK,aAAa,SAAS,QAAQ,eAAe;AACtF,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAc,oBACZ,SACiE;AArarE;AAsaI,UAAM,WAAW,MAAM,KAAK,KAAK,OAAO,iBAAiB;AAAA,MACvD,EAAE,SAAS,OAAO,MAAM,YAAY;AAAA,MACpC,EAAE,SAAS,OAAO,MAAM,UAAU;AAAA,IACpC,CAAC;AACD,UAAM,QAAQ,SAAS;AACvB,UAAM,YAAW,oBAAS,SAAS,QAAlB,mBAAuB,YAAvB,YAAkC;AACnD,UAAM,cAAa,UAAK,KAAK,gBAAV,mBAAuB,WAAW;AACrD,QAAI,SAAS,qBAAqB,UAAU,KAAK,WAAW,UAAU,SAAS,WAAW,aAAa,UAAU;AAC/G,aAAO,EAAE,cAAc,EAAE,OAAO,UAAU,WAAW,SAAS,GAAG,WAAW,KAAK;AAAA,IACnF;AACA,QAAI,eAAe,QAAW;AAG5B,iBAAK,KAAK,gBAAV,mBAAuB,KAAK,SAAO,QAAQ,YAAY,QAAQ;AAAA,IACjE;AACA,UAAM,eAAe,MAAM,KAAK,YAAY,SAAS,OAAO,QAAQ;AACpE,QAAI,aAAa,OAAO;AACtB,iBAAK,KAAK,gBAAV,mBAAuB,IAAI,UAAU;AAAA,QACnC,OAAO,aAAa;AAAA,QACpB,WAAU,wBAAa,SAAS,QAAtB,mBAA2B,YAA3B,YAAsC;AAAA,QAChD,UAAU,aAAa;AAAA,MACzB;AAAA,IACF,OAAO;AAKL,iBAAK,KAAK,gBAAV,mBAAuB,KAAK,SAAO,QAAQ;AAAA,IAC7C;AACA,WAAO,EAAE,cAAc,WAAW,MAAM;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBA,MAAc,mBAAmB,YAAyD;AACxF,UAAM,OAAiD,CAAC;AACxD,QAAI,WAAW,SAAS,SAAS,QAAW;AAC1C,WAAK,KAAK,EAAE,SAAS,QAAQ,MAAM,MAAM,CAAC;AAAA,IAC5C;AACA,eAAW,QAAQ,YAAY;AAC7B,UAAI,WAAW,SAAS,KAAK,OAAO,MAAM,QAAW;AACnD,aAAK,KAAK,EAAE,SAAS,KAAK,SAAS,MAAM,MAAM,CAAC;AAAA,MAClD;AAAA,IACF;AACA,eAAW,WAAW,CAAC,OAAO,KAAK,GAAG;AACpC,UAAI,WAAW,SAAS,OAAO,MAAM,QAAW;AAC9C,aAAK,KAAK,EAAE,SAAS,MAAM,OAAO,CAAC;AAAA,MACrC;AAAA,IACF;AACA,QAAI,KAAK,WAAW,GAAG;AACrB,aAAO;AAAA,IACT;AACA,UAAM,QAAQ,MAAM,KAAK,KAAK,OAAO,iBAAiB,IAAI;AAC1D,WAAO,EAAE,OAAO,WAAW,OAAO,cAAU,qCAAkB,WAAW,UAAU,MAAM,QAAQ,EAAE;AAAA,EACrG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAc,oBAAoB,SAA8C;AA5flF;AA6fI,QAAI;AACF,YAAM,UAAS,UAAK,KAAK,iBAAV,mBAAwB;AACvC,YAAM,WAAO,0BAAU,OAAO,EAAE;AAAA,QAC9B,SAAO,IAAI,YAAY,SAAS,CAAC,UAAU,OAAO,SAAS,SAAS,IAAI,OAAO;AAAA,MACjF;AACA,YAAM,QAAQ,MAAM,KAAK,KAAK,OAAO,iBAAiB,IAAI;AAC1D,UAAI,CAAC,MAAM,OAAO;AAEhB;AAAA,MACF;AACA,YAAM,UAAkD,CAAC;AACzD,iBAAW,CAAC,SAAS,KAAK,KAAK,OAAO,QAAQ,MAAM,QAAQ,GAAG;AAC7D,mBAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AACjD,cAAI,YAAY,KAAK,IAAI,GAAG;AAC1B,cAAC,yDAAqB,CAAC,GAAG,IAAI,IAAI;AAAA,UACpC;AAAA,QACF;AAAA,MACF;AACA,iBAAK,KAAK,gBAAV,mBAAuB,IAAI,YAAY;AAMvC,YAAM,cAAa,UAAK,KAAK,gBAAV,mBAAuB,WAAW;AACrD,YAAM,WAAW,qBAAqB,UAAU,QAC5C,qCAAkB,WAAW,UAAU,MAAM,QAAQ,IACrD,MAAM;AACV,iBAAK,KAAK,gBAAV,mBAAuB,IAAI,UAAU;AAAA,QACnC,OAAO,MAAM;AAAA,QACb,WAAU,iBAAM,SAAS,QAAf,mBAAoB,YAApB,YAA+B;AAAA,QACzC;AAAA,MACF;AAGA,WAAK,qBAAiB,mCAAmB,EAAE,OAAO,MAAM,OAAO,SAAS,GAAG,OAAO;AAClF,WAAK,eAAW,0BAAU,KAAK,cAAc;AAS7C,YAAM,SAAS,cAAc,QAAQ;AACrC,UAAI,KAAK,UAAU,MAAM,MAAM,KAAK,UAAU,KAAK,WAAW,GAAG;AAC/D,aAAK,cAAc;AACnB,YAAI,OAAO,SAAS,GAAG;AACrB,eAAK,KAAK,YAAY,GAAG,KAAK,QAAQ,eAAe,KAAK,UAAU,MAAM,CAAC;AAAA,QAC7E;AAAA,MACF;AACA,WAAK,KAAK,IAAI,MAAM,GAAG,KAAK,QAAQ,wCAAwC;AAAA,IAC9E,SAAS,GAAG;AACV,WAAK,KAAK,IAAI,MAAM,GAAG,KAAK,QAAQ,sCAAsC,OAAO,CAAC,CAAC,EAAE;AAAA,IACvF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,MAAc,YAAY,SAA+B,OAAe,UAA6C;AAtkBvH;AAukBI,UAAM,UAAS,UAAK,KAAK,iBAAV,mBAAwB;AACvC,QAAI,QAAQ;AAIV,UAAI,UAAU,OAAO,SAAS,aAAa,OAAO,UAAU;AAC1D,eAAO,MAAM,KAAK,cAAc,SAAS,IAAI,IAAI,OAAO,QAAQ,CAAC;AAAA,MACnE;AAEA,WAAK,KAAK,IAAI,MAAM,GAAG,KAAK,QAAQ,oEAAoE;AACxG,iBAAK,KAAK,iBAAV,mBAAwB;AAAA,IAC1B;AACA,UAAM,QAAQ,MAAM,KAAK,KAAK,OAAO,iBAAiB,WAAW;AACjE,UAAM,UAAU,IAAI,IAAI,OAAO,KAAK,MAAM,QAAQ,CAAC;AACnD,QAAI,QAAQ,SAAS,GAAG;AAEtB,aAAO,MAAM,KAAK,KAAK,OAAO,qBAAiB,0BAAU,OAAO,CAAC;AAAA,IACnE;AACA,UAAM,eAAe,MAAM,KAAK,cAAc,SAAS,OAAO;AAC9D,QAAI,aAAa,OAAO;AACtB,iBAAK,KAAK,iBAAV,mBAAwB,IAAI;AAAA,QAC1B,UAAU,CAAC,GAAG,OAAO;AAAA,QACrB,OAAO,aAAa;AAAA,QACpB,WAAU,wBAAa,SAAS,QAAtB,mBAA2B,YAA3B,YAAsC;AAAA,MAClD;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,cAAc,SAA+B,SAAyD;AA5mBtH;AA6mBI,UAAM,WAAO,0BAAU,OAAO,EAAE,OAAO,SAAO,IAAI,YAAY,SAAS,QAAQ,IAAI,IAAI,OAAO,CAAC;AAC/F,UAAM,cAAa,UAAK,KAAK,gBAAV,mBAAuB,WAAmD;AAG7F,UAAM,eAAe,MAAM,KAAK,KAAK,OAAO;AAAA,MAC1C,aAAa,KAAK,OAAO,SAAO,CAAC,YAAY,KAAK,IAAI,IAAI,CAAC,IAAI;AAAA,IACjE;AACA,QAAI,YAAY;AACd,iBAAW,CAAC,SAAS,KAAK,KAAK,OAAO,QAAQ,UAAU,GAAG;AACzD,qBAAa,SAAS,OAAO,IAAI,EAAE,GAAG,OAAO,GAAG,aAAa,SAAS,OAAO,EAAE;AAAA,MACjF;AACA,aAAO;AAAA,IACT;AACA,UAAM,UAAkD,CAAC;AACzD,eAAW,CAAC,SAAS,KAAK,KAAK,OAAO,QAAQ,aAAa,QAAQ,GAAG;AACpE,iBAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AACjD,YAAI,YAAY,KAAK,IAAI,GAAG;AAC1B,YAAC,yDAAqB,CAAC,GAAG,IAAI,IAAI;AAAA,QACpC;AAAA,MACF;AAAA,IACF;AACA,eAAK,KAAK,gBAAV,mBAAuB,IAAI,YAAY;AACvC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUO,kBAAkB,aAAqB,KAAc,OAAsB;AA9oBpF;AA+oBI,QAAI,KAAK;AACP;AAAA,IACF;AACA,UAAM,SAAS,GAAG,KAAK,QAAQ;AAC/B,QAAI,CAAC,YAAY,WAAW,MAAM,GAAG;AACnC;AAAA,IACF;AACA,UAAM,UAAU,YAAY,MAAM,OAAO,MAAM;AAC/C,QAAI,QAAQ,WAAW,gBAAgB,GAAG;AACxC,iBAAK,iBAAL,mBAAmB,YAAY,SAAS;AACxC;AAAA,IACF;AAEA,QAAI,YAAY,kBAAkB,OAAO,UAAU,YAAY,CAAC,QAAQ,KAAK,MAAM,KAAK,CAAC,GAAG;AAC1F,YAAM,SAAS,MAAM,KAAK,EAAE,YAAY;AACxC,YAAM,QAAQ,KAAK,YAAY,KAAK,WAAS,MAAM,MAAM,YAAY,MAAM,MAAM;AACjF,UAAI,UAAU,QAAW;AAGvB,aAAK,KAAK,IAAI;AAAA,UACZ,GAAG,KAAK,QAAQ,YAAY,KAAK,kEACpB,KAAK,YAAY,IAAI,WAAS,MAAM,KAAK,EAAE,KAAK,IAAI,KAAK,UAAU;AAAA,QAClF;AACA;AAAA,MACF;AACA,cAAQ,MAAM;AAAA,IAChB;AAGA,UAAM,cAAc,6EAA6E,KAAK,OAAO;AAC7G,QAAI,aAAa;AACf,WAAK,mBAAkB,iBAAY,CAAC,MAAb,YAAkB,QAAQ,UAAU,YAAY,CAAC,CAAC,IAAI,KAAK;AAClF;AAAA,IACF;AAGA,QAAI,KAAK,iBAAiB,SAAS,KAAK,GAAG;AACzC;AAAA,IACF;AACA,UAAM,aAAS,4BAAY,SAAS,QAAO,UAAK,aAAL,YAAiB,MAAM;AAClE,QAAI,QAAQ;AACV,WAAK,KAAK,OAAO,KAAK,OAAO,SAAS,OAAO,MAAM,OAAO,KAAK;AAAA,IACjE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAc,iBAAiB,cAAgC,SAAqC;AAClG,UAAM,gBAAgB,QAAQ;AAAA,MAC5B,YAAU,OAAO,OAAO,YAAa,OAAO,SAAS,WAAW,eAAe,KAAK,OAAO,EAAE;AAAA,IAC/F;AACA,QAAI,CAAC,cAAc,KAAK,YAAU,OAAO,SAAS,OAAO,GAAG;AAC1D,WAAK,cAAc,CAAC;AACpB;AAAA,IACF;AACA,UAAM,YAAY,CAAC,QAA2B;AAAA,MAC5C;AAAA,MACA,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,UAAM,mBAAM,eAAe;AAAA,QAC3B,UAAM,mBAAM,mBAAmB;AAAA,QAC/B,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,MACT;AAAA,IACF;AACA,UAAM,KAAK,KAAK,aAAa,GAAG,KAAK,QAAQ,kBAAkB,UAAU,eAAe,CAAC;AACzF,SAAK,cAAc,CAAC,MAAM;AAC1B,eAAW,QAAQ,YAAY;AAC7B,UAAI,KAAK,QAAQ,UAAU,aAAa,SAAS,KAAK,OAAO,MAAM,QAAW;AAC5E;AAAA,MACF;AACA,WAAK,YAAY,KAAK,KAAK,GAAG;AAC9B,iBAAW,UAAU,eAAe;AAClC,cAAM,KAAK,GAAG,KAAK,MAAM,GAAG,OAAO,EAAE;AACrC,cAAM,KAAK,KAAK,aAAa,GAAG,KAAK,QAAQ,IAAI,EAAE,IAAI,EAAE,GAAG,QAAQ,GAAG,CAAC;AAAA,MAC1E;AACA,YAAM,KAAK,KAAK;AAAA,QACd,GAAG,KAAK,QAAQ,IAAI,KAAK,MAAM;AAAA,QAC/B,UAAU,GAAG,KAAK,MAAM,eAAe;AAAA,MACzC;AAAA,IACF;AAKA,UAAM,cAAc,IAAI;AAAA,MACtB,KAAK,eAAe,OAAO,WAAS,eAAe,KAAK,MAAM,EAAE,CAAC,EAAE,IAAI,WAAS,MAAM,EAAE;AAAA,IAC1F;AACA,eAAW,QAAQ,YAAY;AAC7B,UAAI,CAAC,KAAK,YAAY,SAAS,KAAK,GAAG,GAAG;AACxC;AAAA,MACF;AACA,YAAM,QAAQ,KAAK,WAAW,IAAI,KAAK,GAAG;AAC1C,YAAM,UAAU,sBAAsB,KAAK,MAAM;AACjD,WAAK,KAAK;AAAA,QACR,GAAG,KAAK,QAAQ,IAAI,KAAK,MAAM;AAAA,QAC/B,WAAW,UAAU,SAAY,QAAQ;AAAA,MAC3C;AACA,UAAI,CAAC,SAAS;AACZ,mBAAW,SAAS,mBAAmB;AACrC,cAAI,YAAY,IAAI,MAAM,EAAE,GAAG;AAC7B,iBAAK,KAAK,YAAY,GAAG,KAAK,QAAQ,IAAI,KAAK,MAAM,GAAG,MAAM,EAAE,IAAI,MAAM,KAAK;AAAA,UACjF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,kBAAkB,SAAiB,IAAY,OAAwC;AAG7F,UAAM,WAAO,+BAAa,IAAI,KAAK;AACnC,eAAW,QAAQ,YAAY;AAC7B,UAAI,CAAC,KAAK,YAAY,SAAS,KAAK,GAAG,GAAG;AACxC;AAAA,MACF;AACA,UAAI,sBAAsB,KAAK,WAAW,IAAI,KAAK,GAAG,CAAC,MAAM,SAAS;AACpE,aAAK,KAAK,YAAY,GAAG,KAAK,QAAQ,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI,KAAK;AACnE,YAAI,MAAM;AACR,eAAK,KAAK,YAAY,GAAG,KAAK,QAAQ,IAAI,KAAK,MAAM,GAAG,KAAK,EAAE,IAAI,KAAK,KAAK;AAAA,QAC/E;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,kBAAkB,SAAiB,OAAqB;AAxyBlE;AAyyBI,UAAM,SAAS,sBAAsB,KAAK,WAAW,IAAI,OAAO,CAAC;AACjE,SAAK,WAAW,IAAI,SAAS,KAAK;AAClC,UAAM,QAAQ,sBAAsB,KAAK;AACzC,QAAI,WAAW,SAAS,CAAC,KAAK,YAAY,SAAS,OAAO,GAAG;AAC3D;AAAA,IACF;AACA,UAAM,OAAO,WAAW,KAAK,OAAK,EAAE,QAAQ,OAAO;AACnD,QAAI,CAAC,MAAM;AACT;AAAA,IACF;AACA,UAAM,cAAc,IAAI;AAAA,MACtB,KAAK,eAAe,OAAO,WAAS,eAAe,KAAK,MAAM,EAAE,CAAC,EAAE,IAAI,WAAS,MAAM,EAAE;AAAA,IAC1F;AACA,eAAW,SAAS,mBAAmB;AACrC,UAAI,YAAY,IAAI,MAAM,EAAE,GAAG;AAC7B,aAAK,KAAK,YAAY,GAAG,KAAK,QAAQ,IAAI,KAAK,MAAM,GAAG,MAAM,EAAE,IAAI,MAAM,KAAK;AAAA,MACjF;AAAA,IACF;AACA,SAAK,KAAK,YAAY,GAAG,KAAK,QAAQ,IAAI,KAAK,MAAM,iBAAiB,UAAU,SAAY,KAAK,KAAK;AACtG,QAAI,UAAU,QAAW;AAEvB,YAAM,QAAQ,oBAAI,IAAY;AAC9B,iBAAW,SAAS,KAAK,gBAAgB;AACvC,YAAI,MAAM,YAAY,SAAS,eAAe,KAAK,MAAM,EAAE,KAAK,CAAC,MAAM,WAAW;AAChF,gBAAM,KAAI,WAAM,aAAN,YAAkB,MAAM,IAAI;AAAA,QACxC;AAAA,MACF;AACA,iBAAW,QAAQ,OAAO;AACxB,aAAK,KAAK,OAAO,IAAI,OAAO,IAAI;AAAA,MAClC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,kBAAkB,SAAiB,QAAgB,OAAsB;AAC/E,UAAM,UAAU,sBAAsB,KAAK,WAAW,IAAI,OAAO,CAAC;AAClE,QAAI,YAAY,QAAW;AACzB,WAAK,KAAK,IAAI,MAAM,GAAG,KAAK,QAAQ,KAAK,MAAM,mBAAc,OAAO,gCAAgC;AACpG;AAAA,IACF;AACA,UAAM,QAAQ,KAAK,eAAe,KAAK,OAAK,EAAE,OAAO,UAAU,EAAE,YAAY,OAAO;AACpF,QAAI,UAAU,QAAW;AACvB,WAAK,KAAK,IAAI,MAAM,GAAG,KAAK,QAAQ,KAAK,MAAM,mBAAc,OAAO,oBAAoB;AACxF;AAAA,IACF;AACA,UAAM,aAAS,4BAAY,QAAQ,OAAO,oBAAI,IAAI,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC;AACpE,QAAI,QAAQ;AACV,WAAK,KAAK,OAAO,KAAK,OAAO,SAAS,OAAO,MAAM,OAAO,KAAK;AAAA,IACjE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaQ,iBAAiB,SAAiB,OAAyB;AACjE,QAAI,YAAY,mBAAmB;AACjC,YAAM,MAAM,OAAO,KAAK;AACxB,UAAI,CAAC,OAAO,SAAS,GAAG,GAAG;AACzB,eAAO;AAAA,MACT;AACA,UAAI,KAAK,QAAQ;AACf,YAAI,KAAK,cAAc,MAAM;AAC3B,eAAK,WAAW,OAAO,cAAU,sCAAiB,MAAM,KAAM,CAAC,CAAC;AAAA,QAClE,OAAO;AACL,eAAK,KAAK,IAAI,MAAM,GAAG,KAAK,QAAQ,uDAAkD;AAAA,QACxF;AACA,eAAO;AAAA,MACT;AACA,UAAI,KAAK,cAAc,MAAM;AAC3B,aAAK,WAAW,OAAO,UAAU,OAAO,KAAK,MAAM,GAAG,CAAC,CAAC;AAAA,MAC1D,OAAO;AACL,aAAK,WAAW,OAAO,cAAU,sCAAiB,MAAM,KAAM,CAAC,CAAC;AAAA,MAClE;AACA,aAAO;AAAA,IACT;AACA,QAAI,YAAY,cAAc;AAK5B,YAAM,OAAO,OAAO,UAAU,WAAW,QAAQ;AACjD,YAAM,UAAU,SAAS,OAAO,QAAQ,SAAS,SAAS,KAAK,SAAS,QAAQ;AAChF,YAAM,QAAQ,KAAK,eAAe;AAAA,QAChC,eAAa,UAAU,OAAO,gBAAgB,UAAU,YAAY;AAAA,MACtE;AACA,YAAM,SAAS,YAAQ,4BAAY,SAAS,OAAO,oBAAI,IAAI,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,CAAC,IAAI;AAClF,UAAI,QAAQ;AACV,aAAK,WAAW,OAAO,SAAS,OAAO,MAAM,OAAO,KAAK;AAAA,MAC3D,OAAO;AACL,aAAK,KAAK,IAAI,MAAM,GAAG,KAAK,QAAQ,WAAW,IAAI,wDAAmD;AAAA,MACxG;AACA,aAAO;AAAA,IACT;AACA,QAAI,YAAY,kBAAkB,KAAK,QAAQ;AAC7C,YAAM,OAAO,KAAK,MAAM,OAAO,KAAK,CAAC;AACrC,UAAI,OAAO,SAAS,IAAI,KAAK,QAAQ,GAAG;AACtC,aAAK,WAAW,OAAO,KAAK,cAAc,QAAQ,cAAc,YAAY,OAAO,IAAI,CAAC;AAAA,MAC1F;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYQ,WAAW,SAAiB,MAAc,MAAoB;AACpE,QAAI,CAAC,KAAK,eAAe,KAAK,WAAS,MAAM,YAAY,WAAW,MAAM,SAAS,IAAI,GAAG;AACxF,WAAK,KAAK,IAAI,MAAM,GAAG,KAAK,QAAQ,KAAK,OAAO,IAAI,IAAI,mDAA8C;AACtG;AAAA,IACF;AACA,SAAK,KAAK,OAAO,KAAK,SAAS,MAAM,IAAI;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,YAAY,cAA+C;AA17B3E;AA27BI,UAAM,OAAO,KAAK,KAAK;AACvB,QAAI,CAAC,UAAQ,gBAAK,MAAK,mBAAV,4BAA2B,6BAA4B,OAAO;AACzE;AAAA,IACF;AACA,UAAM,QAAQ,CAAC,OAA8B,KAAK,MAAM,EAAE;AAC1D,UAAM,UAAU,MAAM,KAAK,oBAAoB,YAAY;AAC3D,QAAI,QAAQ,SAAS,GAAG;AAKtB,WAAK,KAAK,IAAI,MAAM,GAAG,KAAK,QAAQ,6EAAwE;AAC5G;AAAA,IACF;AACA,UAAM,SAAS,IAAI,2CAAiB,KAAK,KAAK,QAAQ,SAAS,KAAK;AACpE,SAAK,eAAe,UAAM,oCAAoB,QAAQ,KAAK,UAAU;AAAA,MACnE,cAAc,KAAK,KAAK;AAAA,MACxB,MAAM,CAAC,IAAI,UAAU,KAAK,KAAK,YAAY,GAAG,KAAK,QAAQ,IAAI,EAAE,IAAI,KAAK;AAAA,MAC1E,KAAK,KAAK,KAAK;AAAA,MACf;AAAA,IACF,CAAC;AACD,QAAI,KAAK,cAAc;AACrB,WAAK,eAAe;AAAA,IACtB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAc,oBAAoB,cAA8D;AAh+BlG;AAi+BI,UAAM,aAAa,8CAAoB,OAAO,YAAU,OAAO,WAAW,aAAa,QAAQ;AAC/F,QAAI,WAAW,WAAW,GAAG;AAC3B,aAAO,oBAAI,IAAI;AAAA,IACjB;AAKA,UAAI,kBAAa,SAAS,SAAtB,mBAA4B,SAAQ,MAAM;AAC5C,aAAO,IAAI,IAAI,WAAW,IAAI,YAAU,OAAO,OAAO,CAAC;AAAA,IACzD;AACA,UAAM,SAAS,MAAM,KAAK,KAAK,OAAO;AAAA,MACpC,WAAW,IAAI,aAAW,EAAE,SAAS,OAAO,SAAS,MAAM,WAAW,EAAE;AAAA,IAC1E;AAIA,WAAO,IAAI;AAAA,MACT,WACG,IAAI,YAAU,OAAO,OAAO,EAC5B,OAAO,aAAQ;AAr/BxB,YAAAA;AAq/B2B,sBAAO,MAAKA,MAAA,OAAO,SAAS,OAAO,MAAvB,OAAAA,MAA4B,CAAC,CAAC,EAAE,KAAK,UAAQ,WAAW,KAAK,IAAI,CAAC;AAAA,OAAC;AAAA,IACtG;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQO,OAAO,IAAoC;AAChD,SAAK,KAAK,OAAO,OAAO,EAAE;AAAA,EAC5B;AAAA;AAAA,EAGO,QAAc;AApgCvB;AAqgCI,eAAK,iBAAL,mBAAmB;AACnB,eAAK,iBAAL,mBAAmB;AACnB,SAAK,KAAK,OAAO,MAAM;AAAA,EACzB;AACF;",
|
|
6
6
|
"names": ["_a"]
|
|
7
7
|
}
|
|
@@ -267,7 +267,14 @@ class XmlDeviceController {
|
|
|
267
267
|
await this.deps.upsertObject(`${this.deviceId}.${channelId}.list`, {
|
|
268
268
|
id: `${channelId}.list`,
|
|
269
269
|
type: "state",
|
|
270
|
-
common: {
|
|
270
|
+
common: {
|
|
271
|
+
name: (0, import_i18n.tName)("scenesNumberTitle"),
|
|
272
|
+
desc: (0, import_i18n.tName)("descScenesNumberTitle"),
|
|
273
|
+
type: "string",
|
|
274
|
+
role: "json",
|
|
275
|
+
read: true,
|
|
276
|
+
write: false
|
|
277
|
+
}
|
|
271
278
|
});
|
|
272
279
|
this.emit(`${channelId}.list`, JSON.stringify(scenes));
|
|
273
280
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/xml/device-controller.ts"],
|
|
4
|
-
"sourcesContent": ["import { CHANNEL_NAME_KEYS, type ObjectDef } from \"../catalog/types\";\nimport { tName } from \"../i18n\";\nimport {\n isPermanentXmlRefusal,\n parseInputList,\n parseReturnCode,\n parseSceneList,\n parseTunerInfo,\n type BasicStatus,\n type XmlScene,\n} from \"./protocol\";\nimport { parseXmlStatus, stateToXml, type XmlCommand } from \"./command-mapper\";\nimport { XML_AMP_CATALOG } from \"./catalog\";\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 type { CommandGate } from \"../lifecycle/command-gate\";\nimport type { BrowseEngine } from \"../browse/browse-engine\";\nimport { createBrowseSurface } from \"../browse/surface\";\nimport { XML_BROWSE_SOURCES, XmlBrowseDriver } from \"../browse/xml-browse-driver\";\n\n/** XML/YNC has no push channel, so the state is polled at this interval by default. */\nconst DEFAULT_POLL_INTERVAL_MS = 60 * 1000;\n\ninterface XmlZone {\n /** Unified zone key (`main`, `zone2`, \u2026). */\n key: string;\n /** XML zone element (`Main_Zone`, `Zone_2`, \u2026). */\n element: string;\n /** State-id prefix for the zone. */\n prefix: string;\n}\n\nconst XML_ZONES: XmlZone[] = [\n { key: \"main\", element: \"Main_Zone\", prefix: \"\" },\n { key: \"zone2\", element: \"Zone_2\", prefix: \"multiroom.zone2.\" },\n { key: \"zone3\", element: \"Zone_3\", prefix: \"multiroom.zone3.\" },\n { key: \"zone4\", element: \"Zone_4\", prefix: \"multiroom.zone4.\" },\n];\n\n/** The subset of the XML client the controller uses (so tests can inject a fake). */\nexport interface XmlClientLike {\n /** Read a zone's Basic_Status. */\n getStatus(zone: string): Promise<BasicStatus>;\n /** Read the device's model name (System > Config). */\n getModelName(): Promise<string | undefined>;\n /** Send an inner command to a zone. */\n send(zone: string, inner: string): Promise<void>;\n /** Read an element's inner GET request and return the raw response body. */\n getXml(element: string, inner: string): Promise<string>;\n}\n\n/** The adapter callbacks the controller drives \u2014 narrow, so no adapter mock is needed in tests. */\nexport interface XmlControllerDeps {\n /** The XML client for this device. */\n client: XmlClientLike;\n /** Schedule the keepalive poll; 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 /** 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 /** Per-device memory for answers that do not change while the device runs (see ProbeMemory). */\n probeMemory?: ProbeMemory;\n}\n\n/**\n * Drives one XML/YNC device: probe which zones answer, build the amp tree, seed\n * state, and route commands both ways. XML has no push, so state is refreshed by\n * a keepalive poll. Create-only.\n */\nexport class XmlDeviceController implements ConnectionHandle {\n private zones: XmlZone[] = [];\n private cancelKeepalive: (() => void) | undefined;\n private readonly dropDetector = new PollDropDetector();\n private browseEngine: BrowseEngine | undefined;\n /** The scenes each zone DECLARES (`Scene_Sel_Item`), for the recall write path. */\n private readonly scenesByZone = new Map<string, XmlScene[]>();\n /** Whether the device answers `<Tuner><Play_Info>` (the classic pre-2010 tuner). */\n private hasTuner = false;\n /** The amp state ids this controller actually created \u2014 the claim-with-proof gate for BOTH ways. */\n private readonly createdStates = new Set<string>();\n /** Per zone: the Basic_Status fields this device is known to deliver (persisted union). */\n private readonly zoneFields = new Map<string, Set<string>>();\n\n /**\n * @param deviceId the id-safe device id (object-tree path segment)\n * @param deps the client and adapter callbacks\n * @param pollIntervalMs how often to poll the device for state (default 60 s)\n */\n public constructor(\n private readonly deviceId: string,\n private readonly deps: XmlControllerDeps,\n private readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS,\n ) {}\n\n /**\n * Probe each zone, create the tree for the ones that answer, seed state, and\n * start the keepalive poll.\n *\n * @returns true if the main zone answered and the tree was created\n */\n public async start(): Promise<boolean> {\n // Probe all zones in parallel and keep each answering zone's status, so a zone is\n // fetched once (for the probe) and seeded from that same response \u2014 not twice.\n const probes = await Promise.all(\n XML_ZONES.map(async zone => ({ zone, status: await this.tryGetStatus(zone.element) })),\n );\n const answered = probes.filter(probe => probe.status && Object.keys(probe.status).length > 0);\n if (!answered.some(probe => probe.zone.key === \"main\")) {\n this.deps.log.debug(`${this.deviceId}: no XML main zone \u2014 creating no objects`);\n return false;\n }\n this.zones = answered.map(probe => probe.zone);\n // Freshness guard for the (persisted) probe memory: the model name is the identity\n // this transport can read. A different device behind the address drops the\n // remembered XML declarations (scenes, inputs, tuner, browse sources); a device\n // that reports no model keeps them \u2014 the YNCA/YXC guards catch a swap there.\n let model: string | undefined;\n try {\n model = await this.deps.client.getModelName();\n if (model !== undefined && this.deps.probeMemory) {\n if (this.deps.probeMemory.remembered(\"xmlModel\") !== model) {\n // Every XML-owned memory key carries the xml prefix (xmlBrowseSources,\n // xmlScenes:*, xmlInputs:*, xmlTuner, xmlModel).\n this.deps.probeMemory.drop(key => key.startsWith(\"xml\"));\n this.deps.probeMemory.set(\"xmlModel\", model);\n }\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getModelName failed (${errorMessage(e)})`);\n }\n // The zone's own input list (`Input_Sel_Item`, per zone \u2014 Main and Zone 2 differ on\n // real hardware): the device says which inputs it accepts, so the input state gets a\n // dropdown instead of a free string. Constant per model \u2014 remembered per device.\n const inputsByZone = new Map<string, string[]>();\n for (const zone of this.zones) {\n const body = await this.probeXml(\n `xmlInputs:${zone.key}`,\n zone.element,\n \"<Input><Input_Sel_Item>GetParam</Input_Sel_Item></Input>\",\n );\n inputsByZone.set(zone.key, parseInputList(body));\n }\n // Claim with proof, XML edition (2.0.1): only the states whose Basic_Status field\n // this device DELIVERS are created \u2014 a blind full-catalog rollout left valueless\n // objects (hdmi.out2, sound.direct, \u2026) standing on devices without the feature.\n // The delivered field set is a model property, remembered per zone (union, so a\n // later standby start \u2014 which may report fewer fields \u2014 cannot shrink the tree).\n for (const { zone, status } of answered) {\n const key = `xmlStatusFields:${zone.key}`;\n const remembered = this.deps.probeMemory?.remembered<string[]>(key);\n const fields = new Set<string>(Array.isArray(remembered) ? remembered : []);\n for (const field of Object.keys(status ?? {})) {\n fields.add(field);\n }\n this.deps.probeMemory?.set(key, [...fields]);\n this.zoneFields.set(zone.key, fields);\n }\n // Every parent \u2014 the zone channels included \u2014 is created by the per-state loop below\n // and named from the shared CHANNEL_NAME_KEYS table (a zone that answered has at least\n // its power state, so its channel always comes into being this way).\n const createdChannels = new Set<string>();\n for (const zone of this.zones) {\n for (const entry of XML_AMP_CATALOG) {\n // Main/system-wide features (scenes, HDMI outputs, party) exist only on the main zone.\n if (entry.mainOnly && zone.key !== \"main\") {\n continue;\n }\n // Claim with proof: skip what this device's status never delivered.\n if (entry.statusField !== undefined && !this.zoneFields.get(zone.key)?.has(entry.statusField)) {\n continue;\n }\n const stateId = `${zone.prefix}${entry.state}`;\n // A dotted state (e.g. scene.recall) needs its parent channel created first.\n const segments = stateId.split(\".\");\n for (let i = 1; i < segments.length; i++) {\n const channelId = segments.slice(0, i).join(\".\");\n if (!createdChannels.has(channelId)) {\n createdChannels.add(channelId);\n await this.deps.upsertObject(`${this.deviceId}.${channelId}`, {\n id: channelId,\n type: \"channel\",\n common: {\n // Capitalised like the catalog path does it, so the same folder cannot end up\n // called \"sound\" here and \"Sound\" there depending on which transport owns it.\n name: CHANNEL_NAME_KEYS[segments[i - 1]]\n ? tName(CHANNEL_NAME_KEYS[segments[i - 1]])\n : segments[i - 1].charAt(0).toUpperCase() + segments[i - 1].slice(1),\n },\n });\n }\n }\n const { nameKey, descKey, ...rest } = entry.common;\n const common: ObjectDef[\"common\"] = {\n ...rest,\n name: tName(nameKey),\n // An absent key means the datapoint explains itself \u2014 the fleet standard wants the\n // field empty there rather than filled with invented prose.\n ...(descKey ? { desc: tName(descKey) } : {}),\n };\n // The device's own input list becomes the dropdown (XML-owned devices only \u2014\n // where YNCA is present its dropdown wins via the owner policy).\n const inputs = entry.state === \"input\" ? inputsByZone.get(zone.key) : undefined;\n if (inputs && inputs.length > 0) {\n common.states = Object.fromEntries(inputs.map(input => [input, input]));\n }\n await this.deps.upsertObject(`${this.deviceId}.${stateId}`, {\n id: stateId,\n type: \"state\",\n common,\n });\n this.createdStates.add(stateId);\n }\n }\n await this.setupScenes(createdChannels);\n await this.setupTuner(createdChannels);\n // Seed from the statuses already fetched during the probe \u2014 no second round-trip.\n for (const { zone, status } of answered) {\n if (status) {\n this.seedZone(zone, status);\n }\n }\n // The model name (already read by the freshness guard) for the device-manager card.\n // Best-effort \u2014 a device that does not report it still connects, the line stays empty.\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 await this.setupBrowse();\n this.cancelKeepalive = this.deps.scheduleKeepalive(() => void this.keepalive(), this.pollIntervalMs);\n // The adapter logs one combined \"ready\" line across all transports; this stays at debug.\n this.deps.log.debug(`${this.deviceId}: Yamaha (XML) device ready (XML)`);\n return true;\n }\n\n /**\n * Read one element's list once per device: the answer is a property of the MODEL\n * (input lists, scene declarations), so reconnects reuse it via the probe memory.\n *\n * Only a DEFINITE answer is remembered: a body, or the model's own \"no such node\"\n * (bodyless HTTP 400 / return code 2, both captured on the RX-V6A) as \"declares none\".\n * A transient failure \u2014 timeout, connection error, HTTP 5xx, or a state-dependent\n * refusal (return code 3/4, \"not now\") \u2014 is NOT remembered: before this, one busy\n * moment during the first contact recorded \"no scenes\" for that device for good, until\n * the model changed. Now it is simply asked again on the next connect.\n *\n * @param key the probe-memory key\n * @param element the XML element to ask\n * @param inner the inner GET request\n * @returns the raw response body, or \"\" when the device (definitely or for now) has none\n */\n private async probeXml(key: string, element: string, inner: string): Promise<string> {\n const probe = async (): Promise<string> => {\n let body: string;\n try {\n body = await this.deps.client.getXml(element, inner);\n } catch (e) {\n if (isPermanentXmlRefusal(e)) {\n return \"\"; // the model has no such node \u2014 definite\n }\n throw e; // transient \u2014 not remembered\n }\n const rc = parseReturnCode(body);\n if (rc !== undefined && rc !== 0) {\n if (rc === 2) {\n return \"\"; // RC 2 = the node does not exist on this model \u2014 definite\n }\n throw new Error(`device refused ${element} probe (RC=${rc})`); // \"not now\" \u2014 not remembered\n }\n return body;\n };\n try {\n return this.deps.probeMemory ? await this.deps.probeMemory.once(key, probe) : await probe();\n } catch (e) {\n this.deps.log.debug(\n `${this.deviceId}: ${key} probe failed, asking again on the next connect (${errorMessage(e)})`,\n );\n return \"\";\n }\n }\n\n /**\n * Build the scene surface from the device's OWN declaration (#615): each zone's\n * `<Scene_Sel_Item>` names the scenes that exist, their titles and the write value\n * (`Scene N` via `<Scene_Sel>`). The predecessor blindly sent `Scene_Load` to a\n * fixed 1..12 main-zone state; the capture shows the device declaring `Scene_Sel`\n * instead \u2014 and declaring scenes for Zone 2 too.\n *\n * @param createdChannels the channel ids already created (extended here)\n */\n private async setupScenes(createdChannels: Set<string>): Promise<void> {\n for (const zone of this.zones) {\n const body = await this.probeXml(\n `xmlScenes:${zone.key}`,\n zone.element,\n \"<Scene><Scene_Sel_Item>GetParam</Scene_Sel_Item></Scene>\",\n );\n const scenes = parseSceneList(body);\n if (scenes.length === 0) {\n continue;\n }\n this.scenesByZone.set(zone.key, scenes);\n const channelId = `${zone.prefix}scene`;\n if (!createdChannels.has(channelId)) {\n createdChannels.add(channelId);\n await this.deps.upsertObject(`${this.deviceId}.${channelId}`, {\n id: channelId,\n type: \"channel\",\n common: { name: tName(CHANNEL_NAME_KEYS.scene ?? \"Scenes\") },\n });\n }\n const max = Math.max(...scenes.map(scene => scene.num));\n await this.deps.upsertObject(`${this.deviceId}.${channelId}.recall`, {\n id: `${channelId}.recall`,\n type: \"state\",\n common: {\n name: tName(\"recallScene\"),\n desc: tName(\"descRecallScene\"),\n type: \"number\",\n role: \"level\",\n read: true,\n write: true,\n min: 1,\n max,\n step: 1,\n // The declared titles as the dropdown, so the picker shows \"Movie Viewing\",\n // not a bare number.\n states: Object.fromEntries(scenes.map(scene => [scene.num, scene.title])),\n },\n });\n // ONE list state instead of a name datapoint per scene (v2.0.0): visualizations\n // read titles as VALUES (button captions \u2014 the #613 reporter's setup), and a\n // dropdown's labels are not readable, so the list carries them.\n await this.deps.upsertObject(`${this.deviceId}.${channelId}.list`, {\n id: `${channelId}.list`,\n type: \"state\",\n common: { name: tName(\"scenesNumberTitle\"), type: \"string\", role: \"json\", read: true, write: false },\n });\n this.emit(`${channelId}.list`, JSON.stringify(scenes));\n }\n }\n\n /**\n * Build the classic tuner surface (pre-2010 devices, where XML is the ONLY\n * transport \u2014 the predecessor served their tuner, the rewrite had dropped it).\n * Existence is probed once per device; the preset write is the openHAB-verified\n * `<Play_Control><Preset><Preset_Sel>`; frequency/RDS/tuned are read-only from\n * Play_Info. On newer devices YNCA/YXC own these ids via the owner policy.\n *\n * @param createdChannels the channel ids already created (extended here)\n */\n private async setupTuner(createdChannels: Set<string>): Promise<void> {\n const probe = await this.probeXml(\"xmlTuner\", \"Tuner\", \"<Play_Info>GetParam</Play_Info>\");\n if (probe.length === 0) {\n return;\n }\n this.hasTuner = true;\n if (!createdChannels.has(\"tuner\")) {\n createdChannels.add(\"tuner\");\n await this.deps.upsertObject(`${this.deviceId}.tuner`, {\n id: \"tuner\",\n type: \"channel\",\n common: { name: tName(CHANNEL_NAME_KEYS.tuner ?? \"Tuner\") },\n });\n }\n const state = async (id: string, common: ObjectDef[\"common\"]): Promise<void> => {\n await this.deps.upsertObject(`${this.deviceId}.tuner.${id}`, { id: `tuner.${id}`, type: \"state\", common });\n };\n await state(\"preset\", {\n name: tName(\"presetRecallByNumber\"),\n desc: tName(\"descPresetRecallByNumber\"),\n type: \"number\",\n role: \"level\",\n read: true,\n write: true,\n // Slot 1 upwards \u2014 `handleTunerWrite` drops a 0, so offering it as the lower bound\n // invited a write that goes nowhere.\n min: 1,\n max: 40,\n step: 1,\n });\n await state(\"frequency\", {\n name: tName(\"frequency\"),\n type: \"number\",\n role: \"value\",\n unit: \"kHz\",\n read: true,\n write: false,\n });\n await state(\"rdsService\", {\n name: tName(\"rdsStation\"),\n desc: tName(\"descRdsStation\"),\n type: \"string\",\n role: \"text\",\n read: true,\n write: false,\n });\n await state(\"rdsText\", {\n name: tName(\"rdsText\"),\n desc: tName(\"descRdsText\"),\n type: \"string\",\n role: \"text\",\n read: true,\n write: false,\n });\n await state(\"tuned\", {\n name: tName(\"tunedToAStation\"),\n desc: tName(\"descTunedToAStation\"),\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n });\n await state(\"stereo\", {\n name: tName(\"stereoReception\"),\n desc: tName(\"descStereoReception\"),\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n });\n // NOT `emitTunerInfo(probe)`: the probe body comes out of the PERSISTED memory on every\n // reconnect and restart, so seeding from it published a snapshot of an earlier session\n // \u2014 frequency, RDS station and text, \"tuned\" \u2014 as the CURRENT reading, until the first\n // poll up to a whole interval later. The existence verdict is a model property and stays\n // remembered; the values are read fresh. (Same class as the menu's resting shape, which\n // showed rows six days older than the connection until it was fixed.)\n await this.refreshTuner();\n }\n\n /**\n * Write the tuner states from a Play_Info response.\n *\n * @param xml the Play_Info response body\n */\n private emitTunerInfo(xml: string): void {\n const info = parseTunerInfo(xml);\n if (info.preset !== undefined) {\n this.emit(\"tuner.preset\", info.preset);\n }\n if (info.frequency !== undefined) {\n // Unified kHz (v2.0.0): the device reports FM in MHz, AM in kHz \u2014 normalize.\n this.emit(\"tuner.frequency\", Math.round(info.frequencyUnit === \"MHz\" ? info.frequency * 1000 : info.frequency));\n }\n if (info.rdsService !== undefined) {\n this.emit(\"tuner.rdsService\", info.rdsService);\n }\n if (info.rdsText !== undefined) {\n this.emit(\"tuner.rdsText\", info.rdsText);\n }\n if (info.tuned !== undefined) {\n this.emit(\"tuner.tuned\", info.tuned);\n }\n if (info.stereo !== undefined) {\n this.emit(\"tuner.stereo\", info.stereo);\n }\n }\n\n /**\n * Poll the tuner's Play_Info (keepalive) and write the states.\n */\n private async refreshTuner(): Promise<void> {\n try {\n this.emitTunerInfo(await this.deps.client.getXml(\"Tuner\", \"<Play_Info>GetParam</Play_Info>\"));\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: tuner Play_Info failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * A user write to `tuner.preset` \u2192 the openHAB-verified preset recall.\n *\n * @param stateId the state id relative to the device\n * @param value the written value\n * @returns true when the id was the tuner preset (handled here)\n */\n private handleTunerWrite(stateId: string, value: unknown): boolean {\n if (stateId !== \"tuner.preset\" || !this.hasTuner) {\n return stateId === \"tuner.preset\";\n }\n const num = Math.round(Number(value));\n if (!Number.isFinite(num) || num < 1) {\n return true;\n }\n void this.applyCommand({\n zone: \"Tuner\",\n inner: `<Play_Control><Preset><Preset_Sel>${num}</Preset_Sel></Preset></Play_Control>`,\n });\n return true;\n }\n\n /**\n * A user write to a zone's `scene.recall` \u2192 the DECLARED write element\n * (`<Scene><Scene_Sel>Scene N</Scene_Sel></Scene>`). Only zones that declared\n * scenes accept the write; a refusal lands in the log via applyCommand.\n *\n * @param stateId the state id relative to the device\n * @param value the written value\n * @returns true when the id was a scene recall (handled here)\n */\n private handleSceneWrite(stateId: string, value: unknown): boolean {\n const match = /^(?:multiroom\\.(zone[234])\\.)?scene\\.recall$/.exec(stateId);\n if (!match) {\n return false;\n }\n const zoneKey = match[1] ?? \"main\";\n const zone = this.zones.find(z => z.key === zoneKey);\n const scenes = this.scenesByZone.get(zoneKey);\n // A TITLE is as valid a write as a number (\"Movie Viewing\" \u2192 Scene 1).\n const byTitle =\n typeof value === \"string\" && !/^\\d+$/.test(value.trim())\n ? scenes?.find(scene => scene.title.toLowerCase() === value.trim().toLowerCase())?.num\n : undefined;\n const num = byTitle ?? Math.round(Number(value));\n if (!zone || !scenes || !scenes.some(scene => scene.num === num)) {\n return true;\n }\n void this.applyCommand({ zone: zone.element, inner: `<Scene><Scene_Sel>Scene ${num}</Scene_Sel></Scene>` });\n return true;\n }\n\n /**\n * Create the browsing surface (#613) when at least one source answers a List_Info\n * probe (NET_RADIO/SERVER/USB \u2014 the menus the predecessor adapter's users drove\n * via `Realtime.*.LINE1TXT` + `xmlCommand`). Skipped without a delay dep (older tests).\n */\n private async setupBrowse(): Promise<void> {\n const gate = this.deps.gate;\n if (!gate) {\n return;\n }\n const delay = (ms: number): Promise<void> => gate.delay(ms);\n // Which sources have a menu is a property of the MODEL, not of this connection \u2014 ask\n // once per device instead of costing three extra requests (up to five seconds on a\n // receiver that has no menus at all) on every single reconnect.\n const probe = async (): Promise<string[]> => {\n const probes = await Promise.all(\n XML_BROWSE_SOURCES.map(async source => {\n try {\n const body = await this.deps.client.getXml(source.element, \"<List_Info>GetParam</List_Info>\");\n return body.includes(\"<Menu_Status>\") ? source.key : undefined;\n } catch (e) {\n if (isPermanentXmlRefusal(e)) {\n return undefined; // this model has no menu for that source (bodyless HTTP 400)\n }\n throw e; // transient \u2014 \"no menus\" must not be remembered for good\n }\n }),\n );\n return probes.filter((key): key is string => key !== undefined);\n };\n let available: Set<string>;\n try {\n available = new Set(\n this.deps.probeMemory ? await this.deps.probeMemory.once(\"xmlBrowseSources\", probe) : await probe(),\n );\n } catch (e) {\n this.deps.log.debug(\n `${this.deviceId}: browse probe failed, asking again on the next connect (${errorMessage(e)})`,\n );\n return;\n }\n if (available.size === 0) {\n return;\n }\n const driver = new XmlBrowseDriver(this.deps.client, available, delay);\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,\n });\n }\n\n /**\n * Write a device-originated value \u2014 but never after the connection was closed. A poll\n * that was already in flight when the adapter stopped would otherwise still write into\n * 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 an XML command; an\n * acked change (the device's own echo) is ignored.\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 // Scenes and the classic tuner are device-declared (not in the static catalog).\n if (this.handleSceneWrite(stateId, value)) {\n return;\n }\n if (this.handleTunerWrite(stateId, value)) {\n return;\n }\n // Claim with proof, on the WRITE way too. Object creation has been proof-gated since\n // 2.0.1 (only fields this device's Basic_Status really delivers), but the write path was\n // not \u2014 the comment on `createdStates` claimed otherwise while it guarded the read side\n // alone. XML was the last transport without it: YNCA writes only through its per-device\n // write map, MusicCast only through device-declared endpoints. It bites on a datapoint an\n // adapter version before 2.0.1 created and that once carried a value, so no sweep removes\n // it: writing it put a blind command on the wire that the device answers with a refusal.\n if (!this.createdStates.has(stateId)) {\n this.deps.log.debug(`${this.deviceId}: ${stateId} was not reported by this device \u2014 write dropped`);\n return;\n }\n const command = stateToXml(stateId, value);\n if (command) {\n void this.applyCommand(command);\n }\n }\n\n /**\n * Register the supervisor's drop handler. XML has no push/socket-drop event, so a\n * drop is inferred from a run of failed 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 /** Cancel the keepalive poll. Synchronous \u2014 safe to call 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 }\n\n /**\n * Poll every live zone. If every zone fails for three consecutive failed polls in a\n * row, the device is judged gone and a drop is reported so the supervisor reconnects.\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 let anyOk = false;\n for (const zone of this.zones) {\n if (await this.refreshZone(zone)) {\n anyOk = true;\n }\n }\n if (this.hasTuner) {\n await this.refreshTuner();\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 * 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: XmlZone): Promise<boolean> {\n const status = await this.tryGetStatus(zone.element);\n if (!status) {\n return false;\n }\n this.seedZone(zone, status);\n return true;\n }\n\n /**\n * Write a zone's amp states from an already-fetched Basic_Status (used to seed\n * from the start-up probe without a second round-trip).\n *\n * @param zone the zone the status belongs to\n * @param status the parsed Basic_Status\n */\n private seedZone(zone: XmlZone, status: BasicStatus): void {\n // A field the device delivers for the FIRST time mid-run has no object yet\n // (claim-with-proof creates only proven fields at start): remember it \u2014 the next\n // start creates it \u2014 and skip the write, so no state lands without an object.\n const known = this.zoneFields.get(zone.key);\n if (known) {\n let grew = false;\n for (const field of Object.keys(status)) {\n if (!known.has(field)) {\n known.add(field);\n grew = true;\n }\n }\n if (grew) {\n this.deps.probeMemory?.set(`xmlStatusFields:${zone.key}`, [...known]);\n }\n }\n for (const update of parseXmlStatus(status, zone.key)) {\n if (this.createdStates.has(update.id)) {\n this.emit(update.id, update.value);\n }\n }\n }\n\n /**\n * Read a zone's status, swallowing errors (an absent zone or an offline device).\n *\n * @param element the XML zone element\n * @returns the parsed status, or undefined on failure\n */\n private async tryGetStatus(element: string): Promise<BasicStatus | undefined> {\n try {\n return await this.deps.client.getStatus(element);\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getStatus(${element}) failed: ${errorMessage(e)}`);\n return undefined;\n }\n }\n\n /**\n * Send a mapped command to the device.\n *\n * @param command the XML command to apply\n */\n private async applyCommand(command: XmlCommand): Promise<void> {\n try {\n await this.deps.client.send(command.zone, command.inner);\n } catch (e) {\n this.deps.log.warn(`${this.deviceId}: XML command failed: ${errorMessage(e)}`);\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { CHANNEL_NAME_KEYS, type ObjectDef } from \"../catalog/types\";\nimport { tName } from \"../i18n\";\nimport {\n isPermanentXmlRefusal,\n parseInputList,\n parseReturnCode,\n parseSceneList,\n parseTunerInfo,\n type BasicStatus,\n type XmlScene,\n} from \"./protocol\";\nimport { parseXmlStatus, stateToXml, type XmlCommand } from \"./command-mapper\";\nimport { XML_AMP_CATALOG } from \"./catalog\";\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 type { CommandGate } from \"../lifecycle/command-gate\";\nimport type { BrowseEngine } from \"../browse/browse-engine\";\nimport { createBrowseSurface } from \"../browse/surface\";\nimport { XML_BROWSE_SOURCES, XmlBrowseDriver } from \"../browse/xml-browse-driver\";\n\n/** XML/YNC has no push channel, so the state is polled at this interval by default. */\nconst DEFAULT_POLL_INTERVAL_MS = 60 * 1000;\n\ninterface XmlZone {\n /** Unified zone key (`main`, `zone2`, \u2026). */\n key: string;\n /** XML zone element (`Main_Zone`, `Zone_2`, \u2026). */\n element: string;\n /** State-id prefix for the zone. */\n prefix: string;\n}\n\nconst XML_ZONES: XmlZone[] = [\n { key: \"main\", element: \"Main_Zone\", prefix: \"\" },\n { key: \"zone2\", element: \"Zone_2\", prefix: \"multiroom.zone2.\" },\n { key: \"zone3\", element: \"Zone_3\", prefix: \"multiroom.zone3.\" },\n { key: \"zone4\", element: \"Zone_4\", prefix: \"multiroom.zone4.\" },\n];\n\n/** The subset of the XML client the controller uses (so tests can inject a fake). */\nexport interface XmlClientLike {\n /** Read a zone's Basic_Status. */\n getStatus(zone: string): Promise<BasicStatus>;\n /** Read the device's model name (System > Config). */\n getModelName(): Promise<string | undefined>;\n /** Send an inner command to a zone. */\n send(zone: string, inner: string): Promise<void>;\n /** Read an element's inner GET request and return the raw response body. */\n getXml(element: string, inner: string): Promise<string>;\n}\n\n/** The adapter callbacks the controller drives \u2014 narrow, so no adapter mock is needed in tests. */\nexport interface XmlControllerDeps {\n /** The XML client for this device. */\n client: XmlClientLike;\n /** Schedule the keepalive poll; 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 /** 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 /** Per-device memory for answers that do not change while the device runs (see ProbeMemory). */\n probeMemory?: ProbeMemory;\n}\n\n/**\n * Drives one XML/YNC device: probe which zones answer, build the amp tree, seed\n * state, and route commands both ways. XML has no push, so state is refreshed by\n * a keepalive poll. Create-only.\n */\nexport class XmlDeviceController implements ConnectionHandle {\n private zones: XmlZone[] = [];\n private cancelKeepalive: (() => void) | undefined;\n private readonly dropDetector = new PollDropDetector();\n private browseEngine: BrowseEngine | undefined;\n /** The scenes each zone DECLARES (`Scene_Sel_Item`), for the recall write path. */\n private readonly scenesByZone = new Map<string, XmlScene[]>();\n /** Whether the device answers `<Tuner><Play_Info>` (the classic pre-2010 tuner). */\n private hasTuner = false;\n /** The amp state ids this controller actually created \u2014 the claim-with-proof gate for BOTH ways. */\n private readonly createdStates = new Set<string>();\n /** Per zone: the Basic_Status fields this device is known to deliver (persisted union). */\n private readonly zoneFields = new Map<string, Set<string>>();\n\n /**\n * @param deviceId the id-safe device id (object-tree path segment)\n * @param deps the client and adapter callbacks\n * @param pollIntervalMs how often to poll the device for state (default 60 s)\n */\n public constructor(\n private readonly deviceId: string,\n private readonly deps: XmlControllerDeps,\n private readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS,\n ) {}\n\n /**\n * Probe each zone, create the tree for the ones that answer, seed state, and\n * start the keepalive poll.\n *\n * @returns true if the main zone answered and the tree was created\n */\n public async start(): Promise<boolean> {\n // Probe all zones in parallel and keep each answering zone's status, so a zone is\n // fetched once (for the probe) and seeded from that same response \u2014 not twice.\n const probes = await Promise.all(\n XML_ZONES.map(async zone => ({ zone, status: await this.tryGetStatus(zone.element) })),\n );\n const answered = probes.filter(probe => probe.status && Object.keys(probe.status).length > 0);\n if (!answered.some(probe => probe.zone.key === \"main\")) {\n this.deps.log.debug(`${this.deviceId}: no XML main zone \u2014 creating no objects`);\n return false;\n }\n this.zones = answered.map(probe => probe.zone);\n // Freshness guard for the (persisted) probe memory: the model name is the identity\n // this transport can read. A different device behind the address drops the\n // remembered XML declarations (scenes, inputs, tuner, browse sources); a device\n // that reports no model keeps them \u2014 the YNCA/YXC guards catch a swap there.\n let model: string | undefined;\n try {\n model = await this.deps.client.getModelName();\n if (model !== undefined && this.deps.probeMemory) {\n if (this.deps.probeMemory.remembered(\"xmlModel\") !== model) {\n // Every XML-owned memory key carries the xml prefix (xmlBrowseSources,\n // xmlScenes:*, xmlInputs:*, xmlTuner, xmlModel).\n this.deps.probeMemory.drop(key => key.startsWith(\"xml\"));\n this.deps.probeMemory.set(\"xmlModel\", model);\n }\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getModelName failed (${errorMessage(e)})`);\n }\n // The zone's own input list (`Input_Sel_Item`, per zone \u2014 Main and Zone 2 differ on\n // real hardware): the device says which inputs it accepts, so the input state gets a\n // dropdown instead of a free string. Constant per model \u2014 remembered per device.\n const inputsByZone = new Map<string, string[]>();\n for (const zone of this.zones) {\n const body = await this.probeXml(\n `xmlInputs:${zone.key}`,\n zone.element,\n \"<Input><Input_Sel_Item>GetParam</Input_Sel_Item></Input>\",\n );\n inputsByZone.set(zone.key, parseInputList(body));\n }\n // Claim with proof, XML edition (2.0.1): only the states whose Basic_Status field\n // this device DELIVERS are created \u2014 a blind full-catalog rollout left valueless\n // objects (hdmi.out2, sound.direct, \u2026) standing on devices without the feature.\n // The delivered field set is a model property, remembered per zone (union, so a\n // later standby start \u2014 which may report fewer fields \u2014 cannot shrink the tree).\n for (const { zone, status } of answered) {\n const key = `xmlStatusFields:${zone.key}`;\n const remembered = this.deps.probeMemory?.remembered<string[]>(key);\n const fields = new Set<string>(Array.isArray(remembered) ? remembered : []);\n for (const field of Object.keys(status ?? {})) {\n fields.add(field);\n }\n this.deps.probeMemory?.set(key, [...fields]);\n this.zoneFields.set(zone.key, fields);\n }\n // Every parent \u2014 the zone channels included \u2014 is created by the per-state loop below\n // and named from the shared CHANNEL_NAME_KEYS table (a zone that answered has at least\n // its power state, so its channel always comes into being this way).\n const createdChannels = new Set<string>();\n for (const zone of this.zones) {\n for (const entry of XML_AMP_CATALOG) {\n // Main/system-wide features (scenes, HDMI outputs, party) exist only on the main zone.\n if (entry.mainOnly && zone.key !== \"main\") {\n continue;\n }\n // Claim with proof: skip what this device's status never delivered.\n if (entry.statusField !== undefined && !this.zoneFields.get(zone.key)?.has(entry.statusField)) {\n continue;\n }\n const stateId = `${zone.prefix}${entry.state}`;\n // A dotted state (e.g. scene.recall) needs its parent channel created first.\n const segments = stateId.split(\".\");\n for (let i = 1; i < segments.length; i++) {\n const channelId = segments.slice(0, i).join(\".\");\n if (!createdChannels.has(channelId)) {\n createdChannels.add(channelId);\n await this.deps.upsertObject(`${this.deviceId}.${channelId}`, {\n id: channelId,\n type: \"channel\",\n common: {\n // Capitalised like the catalog path does it, so the same folder cannot end up\n // called \"sound\" here and \"Sound\" there depending on which transport owns it.\n name: CHANNEL_NAME_KEYS[segments[i - 1]]\n ? tName(CHANNEL_NAME_KEYS[segments[i - 1]])\n : segments[i - 1].charAt(0).toUpperCase() + segments[i - 1].slice(1),\n },\n });\n }\n }\n const { nameKey, descKey, ...rest } = entry.common;\n const common: ObjectDef[\"common\"] = {\n ...rest,\n name: tName(nameKey),\n // An absent key means the datapoint explains itself \u2014 the fleet standard wants the\n // field empty there rather than filled with invented prose.\n ...(descKey ? { desc: tName(descKey) } : {}),\n };\n // The device's own input list becomes the dropdown (XML-owned devices only \u2014\n // where YNCA is present its dropdown wins via the owner policy).\n const inputs = entry.state === \"input\" ? inputsByZone.get(zone.key) : undefined;\n if (inputs && inputs.length > 0) {\n common.states = Object.fromEntries(inputs.map(input => [input, input]));\n }\n await this.deps.upsertObject(`${this.deviceId}.${stateId}`, {\n id: stateId,\n type: \"state\",\n common,\n });\n this.createdStates.add(stateId);\n }\n }\n await this.setupScenes(createdChannels);\n await this.setupTuner(createdChannels);\n // Seed from the statuses already fetched during the probe \u2014 no second round-trip.\n for (const { zone, status } of answered) {\n if (status) {\n this.seedZone(zone, status);\n }\n }\n // The model name (already read by the freshness guard) for the device-manager card.\n // Best-effort \u2014 a device that does not report it still connects, the line stays empty.\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 await this.setupBrowse();\n this.cancelKeepalive = this.deps.scheduleKeepalive(() => void this.keepalive(), this.pollIntervalMs);\n // The adapter logs one combined \"ready\" line across all transports; this stays at debug.\n this.deps.log.debug(`${this.deviceId}: Yamaha (XML) device ready (XML)`);\n return true;\n }\n\n /**\n * Read one element's list once per device: the answer is a property of the MODEL\n * (input lists, scene declarations), so reconnects reuse it via the probe memory.\n *\n * Only a DEFINITE answer is remembered: a body, or the model's own \"no such node\"\n * (bodyless HTTP 400 / return code 2, both captured on the RX-V6A) as \"declares none\".\n * A transient failure \u2014 timeout, connection error, HTTP 5xx, or a state-dependent\n * refusal (return code 3/4, \"not now\") \u2014 is NOT remembered: before this, one busy\n * moment during the first contact recorded \"no scenes\" for that device for good, until\n * the model changed. Now it is simply asked again on the next connect.\n *\n * @param key the probe-memory key\n * @param element the XML element to ask\n * @param inner the inner GET request\n * @returns the raw response body, or \"\" when the device (definitely or for now) has none\n */\n private async probeXml(key: string, element: string, inner: string): Promise<string> {\n const probe = async (): Promise<string> => {\n let body: string;\n try {\n body = await this.deps.client.getXml(element, inner);\n } catch (e) {\n if (isPermanentXmlRefusal(e)) {\n return \"\"; // the model has no such node \u2014 definite\n }\n throw e; // transient \u2014 not remembered\n }\n const rc = parseReturnCode(body);\n if (rc !== undefined && rc !== 0) {\n if (rc === 2) {\n return \"\"; // RC 2 = the node does not exist on this model \u2014 definite\n }\n throw new Error(`device refused ${element} probe (RC=${rc})`); // \"not now\" \u2014 not remembered\n }\n return body;\n };\n try {\n return this.deps.probeMemory ? await this.deps.probeMemory.once(key, probe) : await probe();\n } catch (e) {\n this.deps.log.debug(\n `${this.deviceId}: ${key} probe failed, asking again on the next connect (${errorMessage(e)})`,\n );\n return \"\";\n }\n }\n\n /**\n * Build the scene surface from the device's OWN declaration (#615): each zone's\n * `<Scene_Sel_Item>` names the scenes that exist, their titles and the write value\n * (`Scene N` via `<Scene_Sel>`). The predecessor blindly sent `Scene_Load` to a\n * fixed 1..12 main-zone state; the capture shows the device declaring `Scene_Sel`\n * instead \u2014 and declaring scenes for Zone 2 too.\n *\n * @param createdChannels the channel ids already created (extended here)\n */\n private async setupScenes(createdChannels: Set<string>): Promise<void> {\n for (const zone of this.zones) {\n const body = await this.probeXml(\n `xmlScenes:${zone.key}`,\n zone.element,\n \"<Scene><Scene_Sel_Item>GetParam</Scene_Sel_Item></Scene>\",\n );\n const scenes = parseSceneList(body);\n if (scenes.length === 0) {\n continue;\n }\n this.scenesByZone.set(zone.key, scenes);\n const channelId = `${zone.prefix}scene`;\n if (!createdChannels.has(channelId)) {\n createdChannels.add(channelId);\n await this.deps.upsertObject(`${this.deviceId}.${channelId}`, {\n id: channelId,\n type: \"channel\",\n common: { name: tName(CHANNEL_NAME_KEYS.scene ?? \"Scenes\") },\n });\n }\n const max = Math.max(...scenes.map(scene => scene.num));\n await this.deps.upsertObject(`${this.deviceId}.${channelId}.recall`, {\n id: `${channelId}.recall`,\n type: \"state\",\n common: {\n name: tName(\"recallScene\"),\n desc: tName(\"descRecallScene\"),\n type: \"number\",\n role: \"level\",\n read: true,\n write: true,\n min: 1,\n max,\n step: 1,\n // The declared titles as the dropdown, so the picker shows \"Movie Viewing\",\n // not a bare number.\n states: Object.fromEntries(scenes.map(scene => [scene.num, scene.title])),\n },\n });\n // ONE list state instead of a name datapoint per scene (v2.0.0): visualizations\n // read titles as VALUES (button captions \u2014 the #613 reporter's setup), and a\n // dropdown's labels are not readable, so the list carries them.\n await this.deps.upsertObject(`${this.deviceId}.${channelId}.list`, {\n id: `${channelId}.list`,\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(`${channelId}.list`, JSON.stringify(scenes));\n }\n }\n\n /**\n * Build the classic tuner surface (pre-2010 devices, where XML is the ONLY\n * transport \u2014 the predecessor served their tuner, the rewrite had dropped it).\n * Existence is probed once per device; the preset write is the openHAB-verified\n * `<Play_Control><Preset><Preset_Sel>`; frequency/RDS/tuned are read-only from\n * Play_Info. On newer devices YNCA/YXC own these ids via the owner policy.\n *\n * @param createdChannels the channel ids already created (extended here)\n */\n private async setupTuner(createdChannels: Set<string>): Promise<void> {\n const probe = await this.probeXml(\"xmlTuner\", \"Tuner\", \"<Play_Info>GetParam</Play_Info>\");\n if (probe.length === 0) {\n return;\n }\n this.hasTuner = true;\n if (!createdChannels.has(\"tuner\")) {\n createdChannels.add(\"tuner\");\n await this.deps.upsertObject(`${this.deviceId}.tuner`, {\n id: \"tuner\",\n type: \"channel\",\n common: { name: tName(CHANNEL_NAME_KEYS.tuner ?? \"Tuner\") },\n });\n }\n const state = async (id: string, common: ObjectDef[\"common\"]): Promise<void> => {\n await this.deps.upsertObject(`${this.deviceId}.tuner.${id}`, { id: `tuner.${id}`, type: \"state\", common });\n };\n await state(\"preset\", {\n name: tName(\"presetRecallByNumber\"),\n desc: tName(\"descPresetRecallByNumber\"),\n type: \"number\",\n role: \"level\",\n read: true,\n write: true,\n // Slot 1 upwards \u2014 `handleTunerWrite` drops a 0, so offering it as the lower bound\n // invited a write that goes nowhere.\n min: 1,\n max: 40,\n step: 1,\n });\n await state(\"frequency\", {\n name: tName(\"frequency\"),\n type: \"number\",\n role: \"value\",\n unit: \"kHz\",\n read: true,\n write: false,\n });\n await state(\"rdsService\", {\n name: tName(\"rdsStation\"),\n desc: tName(\"descRdsStation\"),\n type: \"string\",\n role: \"text\",\n read: true,\n write: false,\n });\n await state(\"rdsText\", {\n name: tName(\"rdsText\"),\n desc: tName(\"descRdsText\"),\n type: \"string\",\n role: \"text\",\n read: true,\n write: false,\n });\n await state(\"tuned\", {\n name: tName(\"tunedToAStation\"),\n desc: tName(\"descTunedToAStation\"),\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n });\n await state(\"stereo\", {\n name: tName(\"stereoReception\"),\n desc: tName(\"descStereoReception\"),\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n });\n // NOT `emitTunerInfo(probe)`: the probe body comes out of the PERSISTED memory on every\n // reconnect and restart, so seeding from it published a snapshot of an earlier session\n // \u2014 frequency, RDS station and text, \"tuned\" \u2014 as the CURRENT reading, until the first\n // poll up to a whole interval later. The existence verdict is a model property and stays\n // remembered; the values are read fresh. (Same class as the menu's resting shape, which\n // showed rows six days older than the connection until it was fixed.)\n await this.refreshTuner();\n }\n\n /**\n * Write the tuner states from a Play_Info response.\n *\n * @param xml the Play_Info response body\n */\n private emitTunerInfo(xml: string): void {\n const info = parseTunerInfo(xml);\n if (info.preset !== undefined) {\n this.emit(\"tuner.preset\", info.preset);\n }\n if (info.frequency !== undefined) {\n // Unified kHz (v2.0.0): the device reports FM in MHz, AM in kHz \u2014 normalize.\n this.emit(\"tuner.frequency\", Math.round(info.frequencyUnit === \"MHz\" ? info.frequency * 1000 : info.frequency));\n }\n if (info.rdsService !== undefined) {\n this.emit(\"tuner.rdsService\", info.rdsService);\n }\n if (info.rdsText !== undefined) {\n this.emit(\"tuner.rdsText\", info.rdsText);\n }\n if (info.tuned !== undefined) {\n this.emit(\"tuner.tuned\", info.tuned);\n }\n if (info.stereo !== undefined) {\n this.emit(\"tuner.stereo\", info.stereo);\n }\n }\n\n /**\n * Poll the tuner's Play_Info (keepalive) and write the states.\n */\n private async refreshTuner(): Promise<void> {\n try {\n this.emitTunerInfo(await this.deps.client.getXml(\"Tuner\", \"<Play_Info>GetParam</Play_Info>\"));\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: tuner Play_Info failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * A user write to `tuner.preset` \u2192 the openHAB-verified preset recall.\n *\n * @param stateId the state id relative to the device\n * @param value the written value\n * @returns true when the id was the tuner preset (handled here)\n */\n private handleTunerWrite(stateId: string, value: unknown): boolean {\n if (stateId !== \"tuner.preset\" || !this.hasTuner) {\n return stateId === \"tuner.preset\";\n }\n const num = Math.round(Number(value));\n if (!Number.isFinite(num) || num < 1) {\n return true;\n }\n void this.applyCommand({\n zone: \"Tuner\",\n inner: `<Play_Control><Preset><Preset_Sel>${num}</Preset_Sel></Preset></Play_Control>`,\n });\n return true;\n }\n\n /**\n * A user write to a zone's `scene.recall` \u2192 the DECLARED write element\n * (`<Scene><Scene_Sel>Scene N</Scene_Sel></Scene>`). Only zones that declared\n * scenes accept the write; a refusal lands in the log via applyCommand.\n *\n * @param stateId the state id relative to the device\n * @param value the written value\n * @returns true when the id was a scene recall (handled here)\n */\n private handleSceneWrite(stateId: string, value: unknown): boolean {\n const match = /^(?:multiroom\\.(zone[234])\\.)?scene\\.recall$/.exec(stateId);\n if (!match) {\n return false;\n }\n const zoneKey = match[1] ?? \"main\";\n const zone = this.zones.find(z => z.key === zoneKey);\n const scenes = this.scenesByZone.get(zoneKey);\n // A TITLE is as valid a write as a number (\"Movie Viewing\" \u2192 Scene 1).\n const byTitle =\n typeof value === \"string\" && !/^\\d+$/.test(value.trim())\n ? scenes?.find(scene => scene.title.toLowerCase() === value.trim().toLowerCase())?.num\n : undefined;\n const num = byTitle ?? Math.round(Number(value));\n if (!zone || !scenes || !scenes.some(scene => scene.num === num)) {\n return true;\n }\n void this.applyCommand({ zone: zone.element, inner: `<Scene><Scene_Sel>Scene ${num}</Scene_Sel></Scene>` });\n return true;\n }\n\n /**\n * Create the browsing surface (#613) when at least one source answers a List_Info\n * probe (NET_RADIO/SERVER/USB \u2014 the menus the predecessor adapter's users drove\n * via `Realtime.*.LINE1TXT` + `xmlCommand`). Skipped without a delay dep (older tests).\n */\n private async setupBrowse(): Promise<void> {\n const gate = this.deps.gate;\n if (!gate) {\n return;\n }\n const delay = (ms: number): Promise<void> => gate.delay(ms);\n // Which sources have a menu is a property of the MODEL, not of this connection \u2014 ask\n // once per device instead of costing three extra requests (up to five seconds on a\n // receiver that has no menus at all) on every single reconnect.\n const probe = async (): Promise<string[]> => {\n const probes = await Promise.all(\n XML_BROWSE_SOURCES.map(async source => {\n try {\n const body = await this.deps.client.getXml(source.element, \"<List_Info>GetParam</List_Info>\");\n return body.includes(\"<Menu_Status>\") ? source.key : undefined;\n } catch (e) {\n if (isPermanentXmlRefusal(e)) {\n return undefined; // this model has no menu for that source (bodyless HTTP 400)\n }\n throw e; // transient \u2014 \"no menus\" must not be remembered for good\n }\n }),\n );\n return probes.filter((key): key is string => key !== undefined);\n };\n let available: Set<string>;\n try {\n available = new Set(\n this.deps.probeMemory ? await this.deps.probeMemory.once(\"xmlBrowseSources\", probe) : await probe(),\n );\n } catch (e) {\n this.deps.log.debug(\n `${this.deviceId}: browse probe failed, asking again on the next connect (${errorMessage(e)})`,\n );\n return;\n }\n if (available.size === 0) {\n return;\n }\n const driver = new XmlBrowseDriver(this.deps.client, available, delay);\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,\n });\n }\n\n /**\n * Write a device-originated value \u2014 but never after the connection was closed. A poll\n * that was already in flight when the adapter stopped would otherwise still write into\n * 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 an XML command; an\n * acked change (the device's own echo) is ignored.\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 // Scenes and the classic tuner are device-declared (not in the static catalog).\n if (this.handleSceneWrite(stateId, value)) {\n return;\n }\n if (this.handleTunerWrite(stateId, value)) {\n return;\n }\n // Claim with proof, on the WRITE way too. Object creation has been proof-gated since\n // 2.0.1 (only fields this device's Basic_Status really delivers), but the write path was\n // not \u2014 the comment on `createdStates` claimed otherwise while it guarded the read side\n // alone. XML was the last transport without it: YNCA writes only through its per-device\n // write map, MusicCast only through device-declared endpoints. It bites on a datapoint an\n // adapter version before 2.0.1 created and that once carried a value, so no sweep removes\n // it: writing it put a blind command on the wire that the device answers with a refusal.\n if (!this.createdStates.has(stateId)) {\n this.deps.log.debug(`${this.deviceId}: ${stateId} was not reported by this device \u2014 write dropped`);\n return;\n }\n const command = stateToXml(stateId, value);\n if (command) {\n void this.applyCommand(command);\n }\n }\n\n /**\n * Register the supervisor's drop handler. XML has no push/socket-drop event, so a\n * drop is inferred from a run of failed 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 /** Cancel the keepalive poll. Synchronous \u2014 safe to call 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 }\n\n /**\n * Poll every live zone. If every zone fails for three consecutive failed polls in a\n * row, the device is judged gone and a drop is reported so the supervisor reconnects.\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 let anyOk = false;\n for (const zone of this.zones) {\n if (await this.refreshZone(zone)) {\n anyOk = true;\n }\n }\n if (this.hasTuner) {\n await this.refreshTuner();\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 * 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: XmlZone): Promise<boolean> {\n const status = await this.tryGetStatus(zone.element);\n if (!status) {\n return false;\n }\n this.seedZone(zone, status);\n return true;\n }\n\n /**\n * Write a zone's amp states from an already-fetched Basic_Status (used to seed\n * from the start-up probe without a second round-trip).\n *\n * @param zone the zone the status belongs to\n * @param status the parsed Basic_Status\n */\n private seedZone(zone: XmlZone, status: BasicStatus): void {\n // A field the device delivers for the FIRST time mid-run has no object yet\n // (claim-with-proof creates only proven fields at start): remember it \u2014 the next\n // start creates it \u2014 and skip the write, so no state lands without an object.\n const known = this.zoneFields.get(zone.key);\n if (known) {\n let grew = false;\n for (const field of Object.keys(status)) {\n if (!known.has(field)) {\n known.add(field);\n grew = true;\n }\n }\n if (grew) {\n this.deps.probeMemory?.set(`xmlStatusFields:${zone.key}`, [...known]);\n }\n }\n for (const update of parseXmlStatus(status, zone.key)) {\n if (this.createdStates.has(update.id)) {\n this.emit(update.id, update.value);\n }\n }\n }\n\n /**\n * Read a zone's status, swallowing errors (an absent zone or an offline device).\n *\n * @param element the XML zone element\n * @returns the parsed status, or undefined on failure\n */\n private async tryGetStatus(element: string): Promise<BasicStatus | undefined> {\n try {\n return await this.deps.client.getStatus(element);\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getStatus(${element}) failed: ${errorMessage(e)}`);\n return undefined;\n }\n }\n\n /**\n * Send a mapped command to the device.\n *\n * @param command the XML command to apply\n */\n private async applyCommand(command: XmlCommand): Promise<void> {\n try {\n await this.deps.client.send(command.zone, command.inner);\n } catch (e) {\n this.deps.log.warn(`${this.deviceId}: XML command failed: ${errorMessage(e)}`);\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -600,7 +600,14 @@ class YxcDeviceController {
|
|
|
600
600
|
await this.deps.upsertObject(`${this.deviceId}.${id}`, {
|
|
601
601
|
id,
|
|
602
602
|
type: "state",
|
|
603
|
-
common: {
|
|
603
|
+
common: {
|
|
604
|
+
name: (0, import_i18n.tName)("scenesNumberTitle"),
|
|
605
|
+
desc: (0, import_i18n.tName)("descScenesNumberTitle"),
|
|
606
|
+
type: "string",
|
|
607
|
+
role: "json",
|
|
608
|
+
read: true,
|
|
609
|
+
write: false
|
|
610
|
+
}
|
|
604
611
|
});
|
|
605
612
|
this.emit(id, JSON.stringify(list));
|
|
606
613
|
}
|