iobroker.yamaha 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Release:** [![npm version](https://img.shields.io/npm/v/iobroker.yamaha)](https://www.npmjs.com/package/iobroker.yamaha) ![stable](https://iobroker.live/badges/yamaha-stable.svg) ![Installations](https://iobroker.live/badges/yamaha-installed.svg) [![npm downloads](https://img.shields.io/npm/dt/iobroker.yamaha)](https://www.npmjs.com/package/iobroker.yamaha)
4
4
 
5
- **Build:** [![Test and Release](https://github.com/iobroker-community-adapters/ioBroker.yamaha/actions/workflows/test-and-release.yml/badge.svg)](https://github.com/iobroker-community-adapters/ioBroker.yamaha/actions/workflows/test-and-release.yml) ![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen) ![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue) [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
5
+ **Build:** [![Test and Release](https://github.com/iobroker-community-adapters/ioBroker.yamaha/actions/workflows/test-and-release.yml/badge.svg)](https://github.com/iobroker-community-adapters/ioBroker.yamaha/actions/workflows/test-and-release.yml) ![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen) ![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue) [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![Sentry](https://img.shields.io/badge/error%20reporting-Sentry-362d59?logo=sentry&logoColor=white)](https://github.com/ioBroker/plugin-sentry#plugin-sentry)
6
6
 
7
7
  **Support:** [![Ko-fi](https://img.shields.io/badge/Ko--fi-Support-ff5e5b?logo=ko-fi)](https://ko-fi.com/krobipd) [![PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/krobipd)
8
8
 
@@ -26,6 +26,12 @@ legacy XML protocol of the oldest pre-2010 models — behind one object tree.
26
26
  - **Automatic discovery** — an empty device list finds and sets up MusicCast devices at startup
27
27
  - **Device manager** — receivers as admin cards with model, address, live protocol indicators and a device-type icon (receiver, stereo, speaker, soundbar, CD)
28
28
 
29
+ ## Sentry / Error reporting
30
+
31
+ **This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.** Reporting only happens if you have enabled error reporting in the ioBroker diagnostics (**System settings → Diagnostics and error reporting**). Only an anonymous installation ID is transmitted — no name, e-mail address or IP address.
32
+
33
+ For details and how to disable it, see the [Sentry plugin documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry). Error reporting requires js-controller 3.0 or newer.
34
+
29
35
  ## Requirements
30
36
 
31
37
  - Node.js >= 22
@@ -90,6 +96,18 @@ On the first connect the adapter asks the receiver which functions it supports
90
96
  Placeholder for the next version (at the beginning of the line):
91
97
  ### **WORK IN PROGRESS**
92
98
  -->
99
+ ### 1.6.0 (2026-08-27)
100
+
101
+ - (krobipd) New: three states show how many receivers are set up, how many are connected right now and whether that is all of them — one line to watch instead of every device
102
+ - (krobipd) Fixed: a receiver kept showing as connected while the adapter was stopped, and after a crash it stayed that way until it answered again — both now show the truth
103
+
104
+ ### 1.5.0 (2026-08-27)
105
+
106
+ - (krobipd) Fixed: menu browsing now works on receivers that serve their menus over the old XML protocol — the adapter no longer claims the menu datapoints on devices that cannot deliver them (#613)
107
+ - (krobipd) Fixed: a MusicCast device that loses power is now reported as offline instead of still showing a connection, so the ready message and the connection indicator tell the truth
108
+ - (krobipd) New: switching a datapoint group on or off in the settings now shows how many datapoints appeared or disappeared, so you no longer have to click through the object tree to check
109
+ - (krobipd) New: optional error reporting via Sentry — only active if you enabled diagnostics in ioBroker, and it transmits no personal data
110
+
93
111
  ### 1.4.0 (2026-08-26)
94
112
 
95
113
  - (krobipd) Fixed: commands sent in quick succession all arrive — a scene switching power, input and volume in one go used to lose everything after the first command
@@ -115,15 +133,6 @@ On the first connect the adapter asks the receiver which functions it supports
115
133
  - (krobipd) New: MusicCast selection lists — stored favourites and tuner presets with names, a recently-played list with recall by number, and the device's own allowed values as dropdowns.
116
134
  - (krobipd) New: more device detail — CD track and drive info, DAB and RDS station data, and a read-only clock and alarm view with its own datapoint group switch in the admin settings.
117
135
 
118
- ### 1.1.1 (2026-08-22)
119
-
120
- - (krobipd) Changed: Internal cleanup. No user-facing changes.
121
-
122
- ### 1.1.0 (2026-08-22)
123
-
124
- - (krobipd) Fixed: a device carried over from the old adapter is no longer called by its IP — the object folder and the admin card now show the name the device reports, or its model.
125
- - (krobipd) Improved: a device that has not reported a model yet already carries its device-class symbol instead of none.
126
-
127
136
  [Older changelogs can be found there](CHANGELOG_OLD.md)
128
137
 
129
138
  ## History
@@ -29,6 +29,7 @@ const ID_MAP = (0, import_catalog.idToEntry)(import_catalog.YNCA_CATALOG);
29
29
  const AVAIL_PROBE = (0, import_catalog.availGets)(import_catalog.YNCA_CATALOG);
30
30
  const STATIC_FUNC = /^(INPNAME|SCENE\d+NAME$)/;
31
31
  const STATIC_KEY = "yncaStaticValues";
32
+ const LIST_PROOF = /^(LISTLAYER|LISTLAYERNAME|CURRLINE|MAXLINE|LINE[1-8](TXT|ATRIB))$/;
32
33
  class YncaDeviceController {
33
34
  /**
34
35
  * @param deviceId the id-safe device id (object-tree path segment)
@@ -197,7 +198,12 @@ class YncaDeviceController {
197
198
  return;
198
199
  }
199
200
  const delay = (ms) => gate.delay(ms);
200
- const driver = new import_ynca_browse_driver.YncaBrowseDriver(this.deps.client, new Set(Object.keys(capabilities.subunits)), delay);
201
+ const present = await this.probeBrowseSubunits(capabilities);
202
+ if (present.size === 0) {
203
+ this.deps.log.debug(`${this.deviceId}: no YNCA source answers LISTINFO \u2014 leaving menus to another transport`);
204
+ return;
205
+ }
206
+ const driver = new import_ynca_browse_driver.YncaBrowseDriver(this.deps.client, present, delay);
201
207
  this.browseEngine = await (0, import_surface.createBrowseSurface)(driver, this.deviceId, {
202
208
  upsertObject: this.deps.upsertObject,
203
209
  emit: (id, value) => this.deps.setStateAck(`${this.deviceId}.${id}`, value),
@@ -208,6 +214,36 @@ class YncaDeviceController {
208
214
  this.browseDriver = driver;
209
215
  }
210
216
  }
217
+ /**
218
+ * Which browsable subunits actually SERVE menus, proven by asking them.
219
+ *
220
+ * Carrying the subunit is NOT proof: the RX-A810 reference log answers `@SERVER:LISTINFO=?`
221
+ * with `@UNDEFINED` while NETRADIO/PC/USB on the very same device return a full window. The
222
+ * XML driver has always probed (`List_Info` → `<Menu_Status>`); YNCA claimed the states on
223
+ * presence alone and, ranking higher, silently displaced the transport that could deliver.
224
+ *
225
+ * @param capabilities the device's swept capabilities
226
+ * @returns the subunits that answered with list data
227
+ */
228
+ async probeBrowseSubunits(capabilities) {
229
+ var _a;
230
+ const candidates = import_ynca_browse_driver.YNCA_BROWSE_SOURCES.filter((source) => source.subunit in capabilities.subunits);
231
+ if (candidates.length === 0) {
232
+ return /* @__PURE__ */ new Set();
233
+ }
234
+ if (((_a = capabilities.subunits.MAIN) == null ? void 0 : _a.PWR) !== "On") {
235
+ return new Set(candidates.map((source) => source.subunit));
236
+ }
237
+ const answer = await this.deps.client.readCapabilities(
238
+ candidates.map((source) => ({ subunit: source.subunit, func: "LISTINFO" }))
239
+ );
240
+ return new Set(
241
+ candidates.map((source) => source.subunit).filter((subunit) => {
242
+ var _a2;
243
+ return Object.keys((_a2 = answer.subunits[subunit]) != null ? _a2 : {}).some((func) => LIST_PROOF.test(func));
244
+ })
245
+ );
246
+ }
211
247
  /**
212
248
  * Register the supervisor's drop handler — delegated to the client's socket drop,
213
249
  * which is YNCA's genuine connection-lost signal.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/device-controller.ts"],
4
- "sourcesContent": ["import type { YncaCapabilities } from \"./ynca/capability\";\nimport type { ObjectDef } from \"./catalog/types\";\nimport type { ConnectionHandle, ControllerLog } from \"./controller\";\nimport {\n YNCA_CATALOG,\n availGets,\n funcToEntry,\n idToEntry,\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 { YncaBrowseDriver } from \"./browse/ynca-browse-driver\";\n\n// The YNCA catalog and its lookup maps are static \u2014 built once for all devices.\n// SYS:MODELNAME is part of the catalog (info.model), so the sweep already covers it.\n// The AVAIL probe always covers the FULL catalog (not the group-filtered one), so the\n// cached subunit set reflects the device, never the current group configuration.\nconst FUNC_MAP = funcToEntry(YNCA_CATALOG);\nconst ID_MAP = idToEntry(YNCA_CATALOG);\nconst AVAIL_PROBE = availGets(YNCA_CATALOG);\n\n/**\n * Functions whose VALUE cannot change while the device runs: the 23 assignable input names\n * and the 12 scene names. They cost 35 of the ~187 paced reads of a targeted sweep (3.5 s\n * at the specification's mandatory 100 ms spacing) and answer the same thing every time, so\n * a reconnect reuses what the first connect learned. Deliberately per adapter RUN, not\n * persisted: renaming an input at the receiver shows up after the next adapter restart\n * rather than needing anyone to invalidate a stored file.\n */\nconst STATIC_FUNC = /^(INPNAME|SCENE\\d+NAME$)/;\n\n/** Memory key for the remembered static values. */\nconst STATIC_KEY = \"yncaStaticValues\";\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 /** 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 /**\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 capabilities = await this.sweepDevice(catalog);\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 // Parents before children (channels before their states) \u2014 created in order.\n for (const object of objects) {\n await this.deps.upsertObject(`${this.deviceId}.${object.id}`, object);\n }\n // Seed the states with the values read during the init sweep \u2014 otherwise they\n // stay empty until the device pushes a change of its own.\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 this.deps.setStateAck(`${this.deviceId}.${update.id}`, update.value);\n }\n }\n }\n await this.setupBrowse(capabilities);\n this.deps.client.onMessage(message => {\n // The browse driver sees every line first: list lines (LINE1TXT\u2026, LISTINFO\n // bursts, auto-feedback) are not catalogued and would otherwise be dropped.\n this.browseDriver?.handleMessage(message);\n const update = yncaStateUpdate(message, FUNC_MAP);\n if (update) {\n this.deps.setStateAck(`${this.deviceId}.${update.id}`, update.value);\n }\n });\n // Start the keepalive only now the sweep is done, so its 30 s poll never collides\n // with the paced init sweep.\n this.deps.client.startKeepalive();\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 * 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 * @returns the assembled capabilities\n */\n private async sweepDevice(catalog: readonly YncaEntry[]): Promise<YncaCapabilities> {\n const cached = this.deps.subunitCache?.get();\n if (cached) {\n // Check the device's IDENTITY first (two reads, ~0.2 s) instead of sweeping and\n // finding out afterwards: the old order cost a full targeted sweep, then the probe,\n // then a second sweep (~40 s) whenever the cache was stale \u2014 slower than having no\n // cache at all.\n const identity = await this.deps.client.readCapabilities([\n { subunit: \"SYS\", func: \"MODELNAME\" },\n { subunit: \"SYS\", func: \"VERSION\" },\n ]);\n const firmware = identity.subunits.SYS?.VERSION ?? \"\";\n if (identity.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 const triple = yncaCommand(stateId, value, ID_MAP);\n if (triple) {\n this.deps.client.send(triple.subunit, triple.func, triple.value);\n }\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 driver = new YncaBrowseDriver(this.deps.client, new Set(Object.keys(capabilities.subunits)), 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 * 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;AAGA,qBAUO;AAKP,qBAAoC;AACpC,gCAAiC;AAMjC,MAAM,eAAW,4BAAY,2BAAY;AACzC,MAAM,aAAS,0BAAU,2BAAY;AACrC,MAAM,kBAAc,0BAAU,2BAAY;AAU1C,MAAM,cAAc;AAGpB,MAAM,aAAa;AA6DZ,MAAM,qBAAiD;AAAA;AAAA;AAAA;AAAA;AAAA,EAQrD,YACY,UACA,MACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EAVK;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBR,MAAa,QAA0B;AACrC,UAAM,KAAK,KAAK,OAAO,QAAQ;AAE/B,UAAM,UAAU,KAAK,KAAK,iBACtB,4BAAa,OAAO,WAAS,KAAK,KAAK,eAAgB,MAAM,EAAE,CAAC,IAChE;AACJ,UAAM,eAAe,MAAM,KAAK,YAAY,OAAO;AACnD,UAAM,cAAU,+BAAe,cAAc,OAAO;AACpD,QAAI,QAAQ,WAAW,GAAG;AACxB,WAAK,KAAK,IAAI,KAAK,GAAG,KAAK,QAAQ,uDAAkD;AACrF,aAAO;AAAA,IACT;AAEA,eAAW,UAAU,SAAS;AAC5B,YAAM,KAAK,KAAK,aAAa,GAAG,KAAK,QAAQ,IAAI,OAAO,EAAE,IAAI,MAAM;AAAA,IACtE;AAGA,eAAW,CAAC,SAAS,KAAK,KAAK,OAAO,QAAQ,aAAa,QAAQ,GAAG;AACpE,iBAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AACjD,cAAM,aAAS,gCAAgB,EAAE,SAAS,MAAM,MAAM,GAAG,QAAQ;AACjE,YAAI,QAAQ;AACV,eAAK,KAAK,YAAY,GAAG,KAAK,QAAQ,IAAI,OAAO,EAAE,IAAI,OAAO,KAAK;AAAA,QACrE;AAAA,MACF;AAAA,IACF;AACA,UAAM,KAAK,YAAY,YAAY;AACnC,SAAK,KAAK,OAAO,UAAU,aAAW;AApJ1C;AAuJM,iBAAK,iBAAL,mBAAmB,cAAc;AACjC,YAAM,aAAS,gCAAgB,SAAS,QAAQ;AAChD,UAAI,QAAQ;AACV,aAAK,KAAK,YAAY,GAAG,KAAK,QAAQ,IAAI,OAAO,EAAE,IAAI,OAAO,KAAK;AAAA,MACrE;AAAA,IACF,CAAC;AAGD,SAAK,KAAK,OAAO,eAAe;AAGhC,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;AAAA;AAAA;AAAA,EAeA,MAAc,YAAY,SAA0D;AAnLtF;AAoLI,UAAM,UAAS,UAAK,KAAK,iBAAV,mBAAwB;AACvC,QAAI,QAAQ;AAKV,YAAM,WAAW,MAAM,KAAK,KAAK,OAAO,iBAAiB;AAAA,QACvD,EAAE,SAAS,OAAO,MAAM,YAAY;AAAA,QACpC,EAAE,SAAS,OAAO,MAAM,UAAU;AAAA,MACpC,CAAC;AACD,YAAM,YAAW,oBAAS,SAAS,QAAlB,mBAAuB,YAAvB,YAAkC;AACnD,UAAI,SAAS,UAAU,OAAO,SAAS,aAAa,OAAO,UAAU;AACnE,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;AA/NtH;AAgOI,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;AAjQpF;AAkQI,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;AACA,UAAM,aAAS,4BAAY,SAAS,OAAO,MAAM;AACjD,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,EAUA,MAAc,YAAY,cAA+C;AA5R3E;AA6RI,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,SAAS,IAAI,2CAAiB,KAAK,KAAK,QAAQ,IAAI,IAAI,OAAO,KAAK,aAAa,QAAQ,CAAC,GAAG,KAAK;AACxG,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,EAQO,OAAO,IAAoC;AAChD,SAAK,KAAK,OAAO,OAAO,EAAE;AAAA,EAC5B;AAAA;AAAA,EAGO,QAAc;AAzTvB;AA0TI,eAAK,iBAAL,mBAAmB;AACnB,eAAK,iBAAL,mBAAmB;AACnB,SAAK,KAAK,OAAO,MAAM;AAAA,EACzB;AACF;",
6
- "names": []
4
+ "sourcesContent": ["import type { YncaCapabilities } from \"./ynca/capability\";\nimport type { ObjectDef } from \"./catalog/types\";\nimport type { ConnectionHandle, ControllerLog } from \"./controller\";\nimport {\n YNCA_CATALOG,\n availGets,\n funcToEntry,\n idToEntry,\n sweepGets,\n yncaCommand,\n yncaObjectsFor,\n yncaStateUpdate,\n type YncaEntry,\n} from \"./ynca/catalog\";\nimport type { YncaSubunitCache } from \"./ynca/subunit-cache\";\nimport type { CommandGate } from \"./lifecycle/command-gate\";\nimport type { ProbeMemory } from \"./lifecycle/probe-memory\";\nimport type { BrowseEngine } from \"./browse/browse-engine\";\nimport { createBrowseSurface } from \"./browse/surface\";\nimport { YNCA_BROWSE_SOURCES, YncaBrowseDriver } from \"./browse/ynca-browse-driver\";\n\n// The YNCA catalog and its lookup maps are static \u2014 built once for all devices.\n// SYS:MODELNAME is part of the catalog (info.model), so the sweep already covers it.\n// The AVAIL probe always covers the FULL catalog (not the group-filtered one), so the\n// cached subunit set reflects the device, never the current group configuration.\nconst FUNC_MAP = funcToEntry(YNCA_CATALOG);\nconst ID_MAP = idToEntry(YNCA_CATALOG);\nconst AVAIL_PROBE = availGets(YNCA_CATALOG);\n\n/**\n * Functions whose VALUE cannot change while the device runs: the 23 assignable input names\n * and the 12 scene names. They cost 35 of the ~187 paced reads of a targeted sweep (3.5 s\n * at the specification's mandatory 100 ms spacing) and answer the same thing every time, so\n * a reconnect reuses what the first connect learned. Deliberately per adapter RUN, not\n * persisted: renaming an input at the receiver shows up after the next adapter restart\n * rather than needing anyone to invalidate a stored file.\n */\nconst STATIC_FUNC = /^(INPNAME|SCENE\\d+NAME$)/;\n\n/** Memory key for the remembered static values. */\nconst STATIC_KEY = \"yncaStaticValues\";\n\n/**\n * The functions whose presence proves a subunit really serves menus \u2014 the fields a real\n * `LISTINFO=?` answer is made of (RX-A810 reference log). See {@link YncaDeviceController.probeBrowseSubunits}.\n */\nconst LIST_PROOF = /^(LISTLAYER|LISTLAYERNAME|CURRLINE|MAXLINE|LINE[1-8](TXT|ATRIB))$/;\n\n/** The subset of the YNCA client the controller uses (so tests can inject a fake). */\nexport interface YncaClientLike {\n /** Open the connection. */\n connect(): Promise<void>;\n /** Run the init sweep and return the device's capabilities. */\n readCapabilities(gets: Array<{ subunit: string; func: string }>): Promise<YncaCapabilities>;\n /** Send a PUT command. */\n send(subunit: string, func: string, value: string): void;\n /** Send a GET request (the browse driver reads LISTINFO with it). */\n get(subunit: string, func: string): void;\n /** Register a handler for pushed messages. */\n onMessage(handler: (message: { subunit: string; func: string; value: string }) => void): void;\n /** Register the socket-drop handler the supervisor reconnects on. */\n onDrop(handler: (reason?: Error) => void): void;\n /** 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 /**\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 capabilities = await this.sweepDevice(catalog);\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 // Parents before children (channels before their states) \u2014 created in order.\n for (const object of objects) {\n await this.deps.upsertObject(`${this.deviceId}.${object.id}`, object);\n }\n // Seed the states with the values read during the init sweep \u2014 otherwise they\n // stay empty until the device pushes a change of its own.\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 this.deps.setStateAck(`${this.deviceId}.${update.id}`, update.value);\n }\n }\n }\n await this.setupBrowse(capabilities);\n this.deps.client.onMessage(message => {\n // The browse driver sees every line first: list lines (LINE1TXT\u2026, LISTINFO\n // bursts, auto-feedback) are not catalogued and would otherwise be dropped.\n this.browseDriver?.handleMessage(message);\n const update = yncaStateUpdate(message, FUNC_MAP);\n if (update) {\n this.deps.setStateAck(`${this.deviceId}.${update.id}`, update.value);\n }\n });\n // Start the keepalive only now the sweep is done, so its 30 s poll never collides\n // with the paced init sweep.\n this.deps.client.startKeepalive();\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 * 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 * @returns the assembled capabilities\n */\n private async sweepDevice(catalog: readonly YncaEntry[]): Promise<YncaCapabilities> {\n const cached = this.deps.subunitCache?.get();\n if (cached) {\n // Check the device's IDENTITY first (two reads, ~0.2 s) instead of sweeping and\n // finding out afterwards: the old order cost a full targeted sweep, then the probe,\n // then a second sweep (~40 s) whenever the cache was stale \u2014 slower than having no\n // cache at all.\n const identity = await this.deps.client.readCapabilities([\n { subunit: \"SYS\", func: \"MODELNAME\" },\n { subunit: \"SYS\", func: \"VERSION\" },\n ]);\n const firmware = identity.subunits.SYS?.VERSION ?? \"\";\n if (identity.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 const triple = yncaCommand(stateId, value, ID_MAP);\n if (triple) {\n this.deps.client.send(triple.subunit, triple.func, triple.value);\n }\n }\n\n /**\n * Create the browsing surface (#613) when the device reports a browsable media\n * subunit: the official YNCA list vocabulary (LISTINFO/LISTSEL/LISTPAGE/LISTCURSOR)\n * drives an 8-line window under `player.browse.*`. Skipped without a delay dep\n * (older tests) and when the playback group is switched off.\n *\n * @param capabilities the device's swept capabilities\n */\n private async setupBrowse(capabilities: YncaCapabilities): Promise<void> {\n const gate = this.deps.gate;\n if (!gate || this.deps.isEntryEnabled?.(\"player.browse.source\") === false) {\n return;\n }\n const delay = (ms: number): Promise<void> => gate.delay(ms);\n const present = await this.probeBrowseSubunits(capabilities);\n if (present.size === 0) {\n // Leaving the states uncreated is what hands browsing to another transport: the owner\n // policy ranks by modernity (yxc > ynca > xml), so an unproven YNCA claim would beat a\n // PROVEN xml one and the user would get an empty menu on a device that can browse over\n // XML \u2014 exactly issue #613's RX-V473.\n this.deps.log.debug(`${this.deviceId}: no YNCA source answers LISTINFO \u2014 leaving menus to another transport`);\n return;\n }\n const driver = new YncaBrowseDriver(this.deps.client, present, delay);\n this.browseEngine = await createBrowseSurface(driver, this.deviceId, {\n upsertObject: this.deps.upsertObject,\n emit: (id, value) => this.deps.setStateAck(`${this.deviceId}.${id}`, value),\n log: this.deps.log,\n delay,\n });\n if (this.browseEngine) {\n this.browseDriver = driver;\n }\n }\n\n /**\n * Which browsable subunits actually SERVE menus, proven by asking them.\n *\n * Carrying the subunit is NOT proof: the RX-A810 reference log answers `@SERVER:LISTINFO=?`\n * with `@UNDEFINED` while NETRADIO/PC/USB on the very same device return a full window. The\n * XML driver has always probed (`List_Info` \u2192 `<Menu_Status>`); YNCA claimed the states on\n * presence alone and, ranking higher, silently displaced the transport that could deliver.\n *\n * @param capabilities the device's swept capabilities\n * @returns the subunits that answered with list data\n */\n private async probeBrowseSubunits(capabilities: YncaCapabilities): Promise<ReadonlySet<string>> {\n const candidates = YNCA_BROWSE_SOURCES.filter(source => source.subunit in capabilities.subunits);\n if (candidates.length === 0) {\n return new Set();\n }\n // A receiver in standby answers @RESTRICTED for its media subunits, which is\n // indistinguishable from \"cannot browse\" and would strip the menus off a device that\n // serves them perfectly once it is on. Nobody browses a sleeping receiver, so keep the\n // claim and let the next connect \u2014 with the device awake \u2014 do the real probe.\n if (capabilities.subunits.MAIN?.PWR !== \"On\") {\n return new Set(candidates.map(source => source.subunit));\n }\n const answer = await this.deps.client.readCapabilities(\n candidates.map(source => ({ subunit: source.subunit, func: \"LISTINFO\" })),\n );\n // Only a real list answer counts. Both refusals \u2014 `@UNDEFINED` (function unknown) and\n // `@RESTRICTED` (source not usable right now) \u2014 carry no subunit, so they cannot be\n // attributed to one request; it is the ABSENCE of an answer that excludes a subunit.\n return new Set(\n candidates\n .map(source => source.subunit)\n .filter(subunit => Object.keys(answer.subunits[subunit] ?? {}).some(func => LIST_PROOF.test(func))),\n );\n }\n\n /**\n * Register the supervisor's drop handler \u2014 delegated to the client's socket drop,\n * which is YNCA's genuine connection-lost signal.\n *\n * @param cb invoked once when the connection drops, with the reason if known\n */\n public onDrop(cb: (reason?: Error) => void): void {\n this.deps.client.onDrop(cb);\n }\n\n /** Close the client. Synchronous \u2014 safe to call from onUnload. */\n public close(): void {\n this.browseEngine?.close();\n this.browseDriver?.close();\n this.deps.client.close();\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["_a"]
7
7
  }
@@ -126,7 +126,11 @@ class YxcDeviceController {
126
126
  }
127
127
  }
128
128
  this.zones = capabilities.zones.map((zone) => zone.id);
129
- await Promise.all(this.zones.map((zone) => this.refreshZone(zone)));
129
+ const zonesAnswered = await Promise.all(this.zones.map((zone) => this.refreshZone(zone)));
130
+ if (this.zones.length > 0 && !zonesAnswered.some(Boolean)) {
131
+ this.deps.log.debug(`${this.deviceId}: no zone answered getStatus \u2014 device unreachable (YXC)`);
132
+ return false;
133
+ }
130
134
  this.mediaBlocks = capabilities.media;
131
135
  this.tunerFeatures = capabilities.tuner;
132
136
  this.hasClock = capabilities.clock !== void 0;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/yxc/device-controller.ts"],
4
- "sourcesContent": ["import { createHash } from \"node:crypto\";\nimport { parseYxcFeatures, type YxcCapabilities, type YxcTunerFeatures } from \"./capability\";\nimport { mapYxcToObjects } from \"./object-mapper\";\nimport {\n parseYxcClock,\n parseYxcDistribution,\n parseYxcPlayInfo,\n parseYxcPresetList,\n parseYxcRecentList,\n parseYxcStatus,\n parseYxcTunerInfo,\n parseYxcTunerPresetLists,\n stateToYxc,\n type YxcCommand,\n} from \"./command-mapper\";\nimport { mediaToRefresh, netusbListsToRefresh, zonesToRefresh } from \"./push\";\nimport type { ObjectDef } from \"../catalog/types\";\nimport type { StateValue } from \"../types\";\nimport type { ConnectionHandle, ControllerLog } from \"../controller\";\nimport { errorMessage } from \"../util\";\nimport { PollDropDetector } from \"../lifecycle/poll-drop-detector\";\nimport type { ProbeMemory } from \"../lifecycle/probe-memory\";\nimport { zonePrefix } from \"./zones\";\nimport type { CommandGate } from \"../lifecycle/command-gate\";\nimport type { BrowseEngine } from \"../browse/browse-engine\";\nimport { createBrowseSurface } from \"../browse/surface\";\nimport { YxcBrowseDriver } from \"../browse/yxc-browse-driver\";\n\n/** Renew interval for the push registration + state poll, well under the ~20 min expiry. */\nconst KEEPALIVE_MS = 5 * 60 * 1000;\n\n/**\n * With push working, run the full media/list/group sweep only every Nth keepalive (6 \u00D7 5 min\n * = every 30 minutes) \u2014 a safety net against a dropped UDP packet, not the primary path.\n */\nconst PUSH_MODE_FULL_SWEEP_EVERY = 6;\n\n/**\n * Extract the model name from a getDeviceInfo response, if it carries a non-empty one.\n *\n * @param deviceInfo the getDeviceInfo response\n * @returns the model name, or undefined\n */\nfunction modelNameFrom(deviceInfo: unknown): string | undefined {\n const model = (deviceInfo as { model_name?: unknown } | null)?.model_name;\n return typeof model === \"string\" && model.length > 0 ? model : undefined;\n}\n\n/**\n * Extract the name a user gave this device from a getNameText response.\n *\n * MusicCast keeps it as the main zone's text \u2014 that is the name shown in the app and\n * the one people recognise (\"Wohnzimmer\"). A device whose zone was never renamed\n * answers with a generic zone name; the caller filters those out.\n *\n * @param nameText the getNameText response\n * @returns the main zone's text, or undefined\n */\nexport function zoneNameFrom(nameText: unknown): string | undefined {\n const zones = (nameText as { zone_list?: unknown } | null)?.zone_list;\n if (!Array.isArray(zones)) {\n return undefined;\n }\n for (const zone of zones) {\n if (typeof zone !== \"object\" || zone === null) {\n continue;\n }\n const { id, text } = zone as { id?: unknown; text?: unknown };\n if (id === \"main\" && typeof text === \"string\" && text.trim().length > 0) {\n return text.trim();\n }\n }\n return undefined;\n}\n\nimport type { YxcClientLike } from \"./client-contract\";\n\n// Re-exported so existing importers (the tests' fakes) keep resolving it from here.\nexport type { YxcClientLike };\n\n/** The adapter callbacks the controller drives \u2014 narrow, so no adapter mock is needed in tests. */\nexport interface YxcControllerDeps {\n /** The YXC (MusicCast) client for this device. */\n client: YxcClientLike;\n /** Resolve another configured device's client by IP, for forming a multiroom group. */\n clientFor?: (ip: string) => YxcClientLike | undefined;\n /** Register a push handler for this device (by IP); returns a function that unregisters it. */\n registerPush(onPush: (event: unknown) => void): () => void;\n /**\n * Whether the shared push receiver is actually listening. With push the device reports\n * its own changes, so the keepalive only has to renew the subscription and refresh the\n * zone status; without it (the port is taken \u2014 see issue #611) the poll is the ONLY\n * source of change and has to cover everything. Absent = assume no push.\n */\n pushActive?(): boolean;\n /** Per-device memory for answers that do not change while the device runs (see ProbeMemory). */\n probeMemory?: ProbeMemory;\n /** Schedule the keepalive handler; returns a function that cancels it. */\n scheduleKeepalive(handler: () => void, ms: number): () => void;\n /** Create or update an object in the device tree. */\n upsertObject(id: string, def: ObjectDef): Promise<void>;\n /** Write a state value with ack (device-originated). */\n setStateAck(id: string, value: boolean | number | string): void;\n /** Report the name the device carries for itself, for the device object's label. */\n reportDeviceName?(name: string): void;\n /** Adapter log. */\n log: ControllerLog;\n /**\n * The device's command gate: every request is paced through it, and its signal is the\n * connection's shutdown flag \u2014 a closed gate ends pending waits and stops state writes\n * from a poll that was already in flight. Absent in older tests \u2192 no browsing.\n */\n gate?: CommandGate;\n}\n\n/**\n * Drives one MusicCast (YXC) device: read capabilities, build the object tree,\n * seed state from getStatus, and route commands both ways. Device pushes arrive\n * via the shared receiver as re-fetch signals; a keepalive poll renews the push\n * registration (the fix for the musiccast \"stops updating\" bug) and doubles as\n * the poll-only fallback when the push port is unavailable. Create-only.\n */\nexport class YxcDeviceController implements ConnectionHandle {\n private zones: string[] = [];\n private mediaBlocks: string[] = [];\n private cancelKeepalive: (() => void) | undefined;\n private cancelPush: (() => void) | undefined;\n private readonly dropDetector = new PollDropDetector();\n /** The tuner's current band, cached so a frequency write can supply it (setFreq needs band + freq). */\n private lastTunerBand = \"fm\";\n /** Each zone's currently selected input, from its status \u2014 see {@link zoneListeningTo}. */\n private readonly lastZoneInput = new Map<string, string>();\n /** The source the network player is currently on (netusb `input`, e.g. \"net_radio\"). */\n private lastNetusbInput = \"\";\n /** Each zone's last-seen equalizer bands, cached so one band write can supply the other two. */\n private readonly lastEqualizer = new Map<string, { low: number; mid: number; high: number }>();\n /** Whether the device reports MusicCast-Link distribution (gates the dist poll and objects). */\n private hasDistribution = false;\n /** The device's last-seen distribution role (none/server/client), for the leave-group path. */\n private lastDistRole = \"none\";\n /** The tuner features (bands + preset mode) \u2014 a preset recall needs the band. */\n private tunerFeatures: YxcTunerFeatures | undefined;\n /** Whether the device reports the clock/alarm block (gates the clock poll). */\n private hasClock = false;\n /** Counts keepalive runs, so the safety-net sweep can run every Nth one under push. */\n private keepaliveRuns = 0;\n private browseEngine: BrowseEngine | undefined;\n\n /**\n * @param deviceId the id-safe device id (object-tree path segment)\n * @param deps the client and adapter callbacks\n */\n public constructor(\n private readonly deviceId: string,\n private readonly deps: YxcControllerDeps,\n ) {}\n\n /**\n * Read capabilities, create the object tree, seed state, and wire up push +\n * keepalive.\n *\n * @returns true if the device reported capabilities and its tree was created\n */\n public async start(): Promise<boolean> {\n // Capabilities, model and name are constant while the device runs, so on a reconnect\n // they come from the per-device memory instead of costing three more round-trips on a\n // connection that is being re-established anyway.\n const capabilities = await this.remember(\"features\", async () =>\n parseYxcFeatures(await this.deps.client.getFeatures()),\n );\n const objects = mapYxcToObjects(capabilities);\n if (objects.length === 0) {\n this.deps.log.warn(`${this.deviceId}: no capabilities reported \u2014 creating no objects`);\n return false;\n }\n // Parents before children (channels before their states) \u2014 created in order.\n for (const object of objects) {\n await this.deps.upsertObject(`${this.deviceId}.${object.id}`, object);\n }\n // The model name (getDeviceInfo) for the device-manager card. Best-effort: a device that\n // does not answer getDeviceInfo still connects \u2014 the model line just stays empty.\n try {\n const model = await this.remember(\"model\", async () => modelNameFrom(await this.deps.client.getDeviceInfo()));\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 } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getDeviceInfo failed (${errorMessage(e)})`);\n }\n // The name the user gave the device in the MusicCast app. Best-effort like the model\n // above: an older device that does not answer getNameText simply keeps its label.\n if (this.deps.reportDeviceName) {\n try {\n const name = await this.remember(\"name\", async () => zoneNameFrom(await this.deps.client.getNameText()));\n if (name) {\n this.deps.reportDeviceName(name);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getNameText failed (${errorMessage(e)})`);\n }\n }\n this.zones = capabilities.zones.map(zone => zone.id);\n // Zones in parallel \u2014 disjoint writes, and a zone stuck in its timeout must not hold\n // up the device's readiness.\n await Promise.all(this.zones.map(zone => this.refreshZone(zone)));\n this.mediaBlocks = capabilities.media;\n this.tunerFeatures = capabilities.tuner;\n this.hasClock = capabilities.clock !== undefined;\n await this.setupBrowse(capabilities);\n await this.refreshMedia();\n await this.refreshLists();\n this.hasDistribution = capabilities.hasDistribution ?? false;\n if (this.hasDistribution) {\n await this.refreshDistribution();\n }\n this.cancelPush = this.deps.registerPush(event => this.onPush(event));\n this.cancelKeepalive = this.deps.scheduleKeepalive(() => void this.keepalive(), KEEPALIVE_MS);\n // The adapter logs one combined \"ready\" line across all transports; this stays at debug.\n this.deps.log.debug(`${this.deviceId}: MusicCast device ready (YXC)`);\n return true;\n }\n\n /**\n * Ask the device once per adapter run and remember the answer for later reconnects.\n *\n * @param key what is being remembered\n * @param probe the request to run when nothing is remembered yet\n * @returns the remembered or freshly fetched value\n */\n private remember<T>(key: string, probe: () => Promise<T>): Promise<T> {\n return this.deps.probeMemory ? this.deps.probeMemory.once(key, probe) : probe();\n }\n\n /**\n * The zone a recall should be routed to: recalling a favourite does not just start it, it\n * also switches THAT zone to the source. Sending everything to the main zone (as this did\n * before) means someone listening in zone 2 gets their favourite in the living room\n * instead \u2014 and the main zone switched away from whatever it was playing.\n *\n * The zone actually listening to the source is the right target; the main zone is the\n * fallback when nothing matches, which is also every single-zone device.\n *\n * @param source the input the recall belongs to (a network source, or \"tuner\")\n * @returns the zone to route the recall to\n */\n private zoneListeningTo(source: string): string {\n if (!source) {\n return \"main\";\n }\n // Main first: on a device where several zones share the source, it is the natural target.\n if (this.lastZoneInput.get(\"main\") === source) {\n return \"main\";\n }\n for (const [zone, input] of this.lastZoneInput) {\n if (input === source) {\n return zone;\n }\n }\n return \"main\";\n }\n\n /**\n * Write a device-originated value \u2014 but never after the connection was closed. A poll\n * or a browse fetch that was already in flight when the adapter stopped would otherwise\n * still write into a tree that is being torn down.\n *\n * @param relativeId the state id relative to the device\n * @param value the value to write\n */\n private emit(relativeId: string, value: boolean | number | string): void {\n if (this.deps.gate?.closed) {\n return;\n }\n this.deps.setStateAck(`${this.deviceId}.${relativeId}`, value);\n }\n\n /**\n * Handle a state change: a user write (ack false) becomes a YXC command; an\n * acked change (the device's own echo) is ignored to avoid a resend loop.\n *\n * @param fullStateId the full state id (device id + \".\" + state)\n * @param ack whether the change is acked (device-originated)\n * @param value the new value\n */\n public handleStateChange(fullStateId: string, ack: boolean, value: unknown): void {\n if (ack) {\n return;\n }\n const prefix = `${this.deviceId}.`;\n if (!fullStateId.startsWith(prefix)) {\n return;\n }\n const stateId = fullStateId.slice(prefix.length);\n if (stateId.startsWith(\"player.browse.\")) {\n this.browseEngine?.handleWrite(stateId, value);\n return;\n }\n // Multiroom writes need controller state (the cached role), so they bypass the pure command map.\n if (stateId === \"multiroom.group.leave\") {\n void this.leaveGroup();\n return;\n }\n if (stateId === \"multiroom.group.linkDevice\") {\n void this.linkClient(String(value));\n return;\n }\n const command = stateToYxc(stateId, value);\n if (command) {\n void this.applyCommand(stateId, command);\n }\n }\n\n /**\n * Register the supervisor's drop handler. MusicCast has no socket-drop event, so a\n * drop is inferred from a run of failed keepalive polls (see keepalive).\n *\n * @param cb invoked once when the device is judged gone\n */\n public onDrop(cb: (reason?: Error) => void): void {\n this.dropDetector.onDrop(cb);\n }\n\n /**\n * Create the browsing surface (#613) when the device has the netusb block: the\n * `netusb/getListInfo` + `setListControl` API drives an 8-line window under\n * `player.browse.*`. Skipped without a delay dep (older tests).\n *\n * @param capabilities the parsed getFeatures capabilities\n */\n private async setupBrowse(capabilities: YxcCapabilities): Promise<void> {\n const gate = this.deps.gate;\n if (!gate || !capabilities.media.includes(\"netusb\")) {\n return;\n }\n const inputs = capabilities.zones.find(zone => zone.id === \"main\")?.inputs ?? [];\n const driver = new YxcBrowseDriver(this.deps.client, inputs);\n this.browseEngine = await createBrowseSurface(driver, this.deviceId, {\n upsertObject: this.deps.upsertObject,\n emit: (id, value) => this.emit(id, value),\n log: this.deps.log,\n delay: ms => gate.delay(ms),\n });\n }\n\n /** Cancel the keepalive and unregister the push handler. Synchronous \u2014 safe from onUnload. */\n public close(): void {\n this.browseEngine?.close();\n // Closing the gate empties its queue and aborts its signal: queued requests are\n // dropped and every pending wait ends, so nothing writes after the teardown.\n this.deps.gate?.close();\n this.cancelKeepalive?.();\n this.cancelKeepalive = undefined;\n // Unregister from the shared push receiver \u2014 otherwise a push arriving after\n // teardown would setState on a controller the adapter has already dropped.\n this.cancelPush?.();\n this.cancelPush = undefined;\n }\n\n /**\n * Handle a device push: each named zone is re-fetched via getStatus, each named\n * media source via getPlayInfo (the push itself is a change signal, not a value\n * carrier). Refreshing only the named sources keeps a track-change push from\n * re-polling every source.\n *\n * @param event the parsed push event\n */\n private onPush(event: unknown): void {\n for (const zone of zonesToRefresh(event)) {\n if (this.zones.includes(zone)) {\n void this.refreshZone(zone);\n }\n }\n for (const block of mediaToRefresh(event)) {\n if (this.mediaBlocks.includes(block)) {\n void this.refreshMediaSource(block);\n }\n }\n // The favourites/recently-played lists announce their changes as flags in the push.\n const lists = netusbListsToRefresh(event);\n if (lists.presets && this.mediaBlocks.includes(\"netusb\")) {\n void this.refreshNetusbPresets();\n }\n if (lists.recent && this.mediaBlocks.includes(\"netusb\")) {\n void this.refreshNetusbRecent();\n }\n }\n\n /**\n * Poll every zone (which renews the push registration and refreshes state) and the\n * media sources. If every zone poll fails for three consecutive failed runs in a row,\n * the device is judged gone and a drop is reported so the supervisor can flip\n * info.connection and reconnect.\n */\n private async keepalive(): Promise<void> {\n // Zones in parallel: their writes are disjoint and one zone stuck in its timeout must\n // not delay the others (a four-zone receiver used to poll them strictly in series).\n const zones = this.zones.length > 0 ? this.zones : [\"main\"];\n const anyOk = (await Promise.all(zones.map(zone => this.refreshZone(zone)))).some(Boolean);\n // Every request above already carried the subscription headers, so the push\n // registration is renewed either way. What still has to be polled depends on whether\n // push works: with push the device announces media, list and group changes itself, so\n // the full sweep only runs occasionally as a safety net (UDP can drop a packet);\n // without push it is the only way anything ever updates.\n this.keepaliveRuns++;\n const fullSweep = !this.deps.pushActive?.() || this.keepaliveRuns % PUSH_MODE_FULL_SWEEP_EVERY === 0;\n if (fullSweep) {\n await this.refreshMedia();\n await this.refreshLists();\n if (this.hasDistribution) {\n await this.refreshDistribution();\n }\n }\n this.dropDetector.record(anyOk);\n }\n\n /**\n * Refresh the list-shaped surfaces: the netusb favourites and recently-played\n * lists, the tuner preset lists, and the clock/alarm settings. Each is\n * best-effort \u2014 a device without the feature answers with an error code and the\n * state simply stays.\n */\n private async refreshLists(): Promise<void> {\n if (this.mediaBlocks.includes(\"netusb\")) {\n await this.refreshNetusbPresets();\n await this.refreshNetusbRecent();\n }\n if (this.mediaBlocks.includes(\"tuner\")) {\n await this.refreshTunerPresets();\n }\n if (this.hasClock) {\n await this.refreshClock();\n }\n }\n\n /** Fetch the stored netusb favourites and write the JSON list state. */\n private async refreshNetusbPresets(): Promise<void> {\n try {\n const update = parseYxcPresetList(await this.deps.client.getPresetInfo());\n if (update) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getPresetInfo failed: ${errorMessage(e)}`);\n }\n }\n\n /** Fetch the recently-played list and write the JSON list state. */\n private async refreshNetusbRecent(): Promise<void> {\n try {\n const update = parseYxcRecentList(await this.deps.client.getRecentInfo());\n if (update) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getRecentInfo failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Fetch the tuner preset lists \u2014 the shared `common` list, or one per band on\n * devices with separate lists \u2014 and write the JSON state.\n */\n private async refreshTunerPresets(): Promise<void> {\n const bands = this.tunerFeatures?.presetType === \"common\" ? [\"common\"] : (this.tunerFeatures?.bands ?? [\"fm\"]);\n const byBand: Record<string, unknown> = {};\n for (const band of bands) {\n try {\n byBand[band] = await this.deps.client.getTunerPresetInfo(band);\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getTunerPresetInfo(${band}) failed: ${errorMessage(e)}`);\n }\n }\n const update = parseYxcTunerPresetLists(byBand);\n if (update) {\n this.emit(update.id, update.value);\n }\n }\n\n /** Fetch the clock/alarm settings and write the read-only clock states. */\n private async refreshClock(): Promise<void> {\n try {\n for (const update of parseYxcClock(await this.deps.client.getClockSettings())) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getClockSettings failed: ${errorMessage(e)}`);\n }\n }\n\n /** Refresh every player source the device offers (network player, cd, tuner). */\n private async refreshMedia(): Promise<void> {\n // The three sources (netusb/cd/tuner) write disjoint states \u2014 fetch them together.\n await Promise.all(this.mediaBlocks.map(block => this.refreshMediaSource(block)));\n }\n\n /**\n * Fetch one media source's play info and write the parsed states with ack. The\n * source picks the getPlayInfo argument and the parser: netusb and cd share the\n * play-info shape (different channel), the tuner has its own band/frequency/RDS.\n *\n * @param block the media block (`netusb`, `cd`, `tuner`)\n */\n private async refreshMediaSource(block: string): Promise<void> {\n const arg = block === \"netusb\" ? undefined : block;\n const parse = (info: unknown): StateValue[] =>\n block === \"tuner\"\n ? parseYxcTunerInfo(info)\n : parseYxcPlayInfo(info, block === \"cd\" ? \"player.cd\" : \"player.netPlayer\");\n try {\n const info = await this.deps.client.getPlayInfo(arg);\n for (const update of parse(info)) {\n this.emit(update.id, update.value);\n if (update.id === \"tuner.band\") {\n this.lastTunerBand = String(update.value);\n }\n if (update.id === \"player.netPlayer.source\" && typeof update.value === \"string\") {\n this.lastNetusbInput = update.value;\n }\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getPlayInfo(${arg ?? \"\"}) failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Fetch the MusicCast-Link distribution info and write the parsed dist states with ack,\n * caching the role for the leave-group path.\n */\n private async refreshDistribution(): Promise<void> {\n try {\n const info = await this.deps.client.getDistributionInfo();\n for (const update of parseYxcDistribution(info)) {\n this.emit(update.id, update.value);\n if (update.id === \"multiroom.group.role\") {\n this.lastDistRole = String(update.value);\n }\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getDistributionInfo failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Leave the current MusicCast-Link group: a server stops distributing, a client clears\n * its membership. Then re-read the distribution state so the tree reflects the change.\n */\n private async leaveGroup(): Promise<void> {\n try {\n if (this.lastDistRole === \"server\") {\n await this.deps.client.stopDistribution();\n } else {\n await this.deps.client.setClientInfo({ group_id: \"\", zone: [\"main\"] });\n }\n await this.refreshDistribution();\n } catch (e) {\n // A user action failing must be visible \u2014 warn, like every other write command.\n this.deps.log.warn(`${this.deviceId}: leaveGroup failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Form a MusicCast-Link group with another configured device: give the client the shared\n * group id, add it to this device's roster as the server, and start distributing. The group\n * id is derived from this device's id, so re-linking reuses the same group rather than a new one.\n *\n * @param clientIp the IP of the client device to add (must be a configured device)\n */\n private async linkClient(clientIp: string): Promise<void> {\n const clientClient = this.deps.clientFor?.(clientIp);\n if (!clientClient) {\n this.deps.log.warn(`${this.deviceId}: cannot link ${clientIp} \u2014 not a known device`);\n return;\n }\n try {\n const groupId = createHash(\"md5\").update(this.deviceId).digest(\"hex\");\n await clientClient.setClientInfo({ group_id: groupId, zone: [\"main\"] });\n await this.deps.client.setServerInfo({ group_id: groupId, zone: \"main\", type: \"add\", client_list: [clientIp] });\n await this.deps.client.startDistribution(0);\n await this.refreshDistribution();\n } catch (e) {\n // A user action failing must be visible \u2014 warn, like every other write command.\n this.deps.log.warn(`${this.deviceId}: linkClient(${clientIp}) failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Fetch a zone's status and write its amp states with ack.\n *\n * @param zone the zone to refresh\n * @returns true if the status was fetched, false if the request failed\n */\n private async refreshZone(zone: string): Promise<boolean> {\n try {\n const status = await this.deps.client.getStatus(zone);\n const updates = parseYxcStatus(status, zone);\n for (const update of updates) {\n this.emit(update.id, update.value);\n if (update.id.endsWith(\"input\") && typeof update.value === \"string\") {\n this.lastZoneInput.set(zone, update.value);\n }\n }\n this.cacheEqualizer(zone, updates);\n return true;\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getStatus(${zone}) failed: ${errorMessage(e)}`);\n return false;\n }\n }\n\n /**\n * Cache a zone's equalizer bands from its status updates, so a later single-band\n * write can send setEqualizer with all three (the device sets them together).\n *\n * @param zone the zone the updates belong to\n * @param updates the parsed status updates for that zone\n */\n private cacheEqualizer(zone: string, updates: StateValue[]): void {\n // Same definition the status parser uses \u2014 see lib/yxc/zones.ts on why this must not\n // be spelled out a second time.\n const prefix = zonePrefix(zone);\n const band = (b: string): number | undefined => {\n const u = updates.find(x => x.id === `${prefix}sound.equalizer${b}`);\n return typeof u?.value === \"number\" ? u.value : undefined;\n };\n if (band(\"Low\") === undefined && band(\"Mid\") === undefined && band(\"High\") === undefined) {\n return;\n }\n const cur = this.lastEqualizer.get(zone) ?? { low: 0, mid: 0, high: 0 };\n this.lastEqualizer.set(zone, {\n low: band(\"Low\") ?? cur.low,\n mid: band(\"Mid\") ?? cur.mid,\n high: band(\"High\") ?? cur.high,\n });\n }\n\n /**\n * Apply a mapped command. A plain command runs its client call directly; the two\n * commands that need controller-cached state (equalizer bands, tuner band) are\n * completed here \u2014 the only place that state lives.\n *\n * @param stateId the written state id, for the failure log line\n * @param command the YXC command to apply\n */\n private async applyCommand(stateId: string, command: YxcCommand): Promise<void> {\n try {\n switch (command.kind) {\n case \"run\":\n await command.run(this.deps.client);\n break;\n case \"equalizer\": {\n // The device sets all three bands in one call, so the other two come from the\n // cache. Without a cached set we must NOT invent 0/0/0 \u2014 that would silently\n // flatten the user's other two bands. The cache fills from a zone status, so\n // fetch one first; only if even that fails is the write refused (with a warning).\n const { zone, band, value } = command;\n let current = this.lastEqualizer.get(zone);\n if (!current) {\n await this.refreshZone(zone);\n current = this.lastEqualizer.get(zone);\n }\n if (!current) {\n this.deps.log.warn(\n `${this.deviceId}: not writing ${stateId} \u2014 the device has not reported its equalizer bands yet`,\n );\n break;\n }\n const next = { ...current, [band]: value };\n await this.deps.client.setEqualizer(next.low, next.mid, next.high, zone);\n this.lastEqualizer.set(zone, next);\n break;\n }\n case \"tunerBand\":\n await this.deps.client.setBand(command.band);\n // Remember it immediately: the poll that would report it back runs minutes later.\n this.lastTunerBand = command.band;\n break;\n case \"tunerFreq\":\n await this.deps.client.setFreq(this.lastTunerBand, command.value);\n break;\n case \"tunerPreset\": {\n // Shared-list devices recall on `common`; separate-list devices on the current band.\n const band = this.tunerFeatures?.presetType === \"common\" ? \"common\" : this.lastTunerBand;\n await this.deps.client.recallTunerPreset(band, command.value, this.zoneListeningTo(\"tuner\"));\n break;\n }\n case \"netusbPreset\":\n await this.deps.client.recallPreset(command.value, this.zoneListeningTo(this.lastNetusbInput));\n break;\n case \"netusbRecent\":\n await this.deps.client.recallRecentItem(command.value, this.zoneListeningTo(this.lastNetusbInput));\n break;\n }\n } catch (e) {\n this.deps.log.warn(`${this.deviceId}: write to ${stateId} failed: ${errorMessage(e)}`);\n }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createHash } from \"node:crypto\";\nimport { parseYxcFeatures, type YxcCapabilities, type YxcTunerFeatures } from \"./capability\";\nimport { mapYxcToObjects } from \"./object-mapper\";\nimport {\n parseYxcClock,\n parseYxcDistribution,\n parseYxcPlayInfo,\n parseYxcPresetList,\n parseYxcRecentList,\n parseYxcStatus,\n parseYxcTunerInfo,\n parseYxcTunerPresetLists,\n stateToYxc,\n type YxcCommand,\n} from \"./command-mapper\";\nimport { mediaToRefresh, netusbListsToRefresh, zonesToRefresh } from \"./push\";\nimport type { ObjectDef } from \"../catalog/types\";\nimport type { StateValue } from \"../types\";\nimport type { ConnectionHandle, ControllerLog } from \"../controller\";\nimport { errorMessage } from \"../util\";\nimport { PollDropDetector } from \"../lifecycle/poll-drop-detector\";\nimport type { ProbeMemory } from \"../lifecycle/probe-memory\";\nimport { zonePrefix } from \"./zones\";\nimport type { CommandGate } from \"../lifecycle/command-gate\";\nimport type { BrowseEngine } from \"../browse/browse-engine\";\nimport { createBrowseSurface } from \"../browse/surface\";\nimport { YxcBrowseDriver } from \"../browse/yxc-browse-driver\";\n\n/** Renew interval for the push registration + state poll, well under the ~20 min expiry. */\nconst KEEPALIVE_MS = 5 * 60 * 1000;\n\n/**\n * With push working, run the full media/list/group sweep only every Nth keepalive (6 \u00D7 5 min\n * = every 30 minutes) \u2014 a safety net against a dropped UDP packet, not the primary path.\n */\nconst PUSH_MODE_FULL_SWEEP_EVERY = 6;\n\n/**\n * Extract the model name from a getDeviceInfo response, if it carries a non-empty one.\n *\n * @param deviceInfo the getDeviceInfo response\n * @returns the model name, or undefined\n */\nfunction modelNameFrom(deviceInfo: unknown): string | undefined {\n const model = (deviceInfo as { model_name?: unknown } | null)?.model_name;\n return typeof model === \"string\" && model.length > 0 ? model : undefined;\n}\n\n/**\n * Extract the name a user gave this device from a getNameText response.\n *\n * MusicCast keeps it as the main zone's text \u2014 that is the name shown in the app and\n * the one people recognise (\"Wohnzimmer\"). A device whose zone was never renamed\n * answers with a generic zone name; the caller filters those out.\n *\n * @param nameText the getNameText response\n * @returns the main zone's text, or undefined\n */\nexport function zoneNameFrom(nameText: unknown): string | undefined {\n const zones = (nameText as { zone_list?: unknown } | null)?.zone_list;\n if (!Array.isArray(zones)) {\n return undefined;\n }\n for (const zone of zones) {\n if (typeof zone !== \"object\" || zone === null) {\n continue;\n }\n const { id, text } = zone as { id?: unknown; text?: unknown };\n if (id === \"main\" && typeof text === \"string\" && text.trim().length > 0) {\n return text.trim();\n }\n }\n return undefined;\n}\n\nimport type { YxcClientLike } from \"./client-contract\";\n\n// Re-exported so existing importers (the tests' fakes) keep resolving it from here.\nexport type { YxcClientLike };\n\n/** The adapter callbacks the controller drives \u2014 narrow, so no adapter mock is needed in tests. */\nexport interface YxcControllerDeps {\n /** The YXC (MusicCast) client for this device. */\n client: YxcClientLike;\n /** Resolve another configured device's client by IP, for forming a multiroom group. */\n clientFor?: (ip: string) => YxcClientLike | undefined;\n /** Register a push handler for this device (by IP); returns a function that unregisters it. */\n registerPush(onPush: (event: unknown) => void): () => void;\n /**\n * Whether the shared push receiver is actually listening. With push the device reports\n * its own changes, so the keepalive only has to renew the subscription and refresh the\n * zone status; without it (the port is taken \u2014 see issue #611) the poll is the ONLY\n * source of change and has to cover everything. Absent = assume no push.\n */\n pushActive?(): boolean;\n /** Per-device memory for answers that do not change while the device runs (see ProbeMemory). */\n probeMemory?: ProbeMemory;\n /** Schedule the keepalive handler; returns a function that cancels it. */\n scheduleKeepalive(handler: () => void, ms: number): () => void;\n /** Create or update an object in the device tree. */\n upsertObject(id: string, def: ObjectDef): Promise<void>;\n /** Write a state value with ack (device-originated). */\n setStateAck(id: string, value: boolean | number | string): void;\n /** Report the name the device carries for itself, for the device object's label. */\n reportDeviceName?(name: string): void;\n /** Adapter log. */\n log: ControllerLog;\n /**\n * The device's command gate: every request is paced through it, and its signal is the\n * connection's shutdown flag \u2014 a closed gate ends pending waits and stops state writes\n * from a poll that was already in flight. Absent in older tests \u2192 no browsing.\n */\n gate?: CommandGate;\n}\n\n/**\n * Drives one MusicCast (YXC) device: read capabilities, build the object tree,\n * seed state from getStatus, and route commands both ways. Device pushes arrive\n * via the shared receiver as re-fetch signals; a keepalive poll renews the push\n * registration (the fix for the musiccast \"stops updating\" bug) and doubles as\n * the poll-only fallback when the push port is unavailable. Create-only.\n */\nexport class YxcDeviceController implements ConnectionHandle {\n private zones: string[] = [];\n private mediaBlocks: string[] = [];\n private cancelKeepalive: (() => void) | undefined;\n private cancelPush: (() => void) | undefined;\n private readonly dropDetector = new PollDropDetector();\n /** The tuner's current band, cached so a frequency write can supply it (setFreq needs band + freq). */\n private lastTunerBand = \"fm\";\n /** Each zone's currently selected input, from its status \u2014 see {@link zoneListeningTo}. */\n private readonly lastZoneInput = new Map<string, string>();\n /** The source the network player is currently on (netusb `input`, e.g. \"net_radio\"). */\n private lastNetusbInput = \"\";\n /** Each zone's last-seen equalizer bands, cached so one band write can supply the other two. */\n private readonly lastEqualizer = new Map<string, { low: number; mid: number; high: number }>();\n /** Whether the device reports MusicCast-Link distribution (gates the dist poll and objects). */\n private hasDistribution = false;\n /** The device's last-seen distribution role (none/server/client), for the leave-group path. */\n private lastDistRole = \"none\";\n /** The tuner features (bands + preset mode) \u2014 a preset recall needs the band. */\n private tunerFeatures: YxcTunerFeatures | undefined;\n /** Whether the device reports the clock/alarm block (gates the clock poll). */\n private hasClock = false;\n /** Counts keepalive runs, so the safety-net sweep can run every Nth one under push. */\n private keepaliveRuns = 0;\n private browseEngine: BrowseEngine | undefined;\n\n /**\n * @param deviceId the id-safe device id (object-tree path segment)\n * @param deps the client and adapter callbacks\n */\n public constructor(\n private readonly deviceId: string,\n private readonly deps: YxcControllerDeps,\n ) {}\n\n /**\n * Read capabilities, create the object tree, seed state, and wire up push +\n * keepalive.\n *\n * @returns true if the device reported capabilities and its tree was created\n */\n public async start(): Promise<boolean> {\n // Capabilities, model and name are constant while the device runs, so on a reconnect\n // they come from the per-device memory instead of costing three more round-trips on a\n // connection that is being re-established anyway.\n const capabilities = await this.remember(\"features\", async () =>\n parseYxcFeatures(await this.deps.client.getFeatures()),\n );\n const objects = mapYxcToObjects(capabilities);\n if (objects.length === 0) {\n this.deps.log.warn(`${this.deviceId}: no capabilities reported \u2014 creating no objects`);\n return false;\n }\n // Parents before children (channels before their states) \u2014 created in order.\n for (const object of objects) {\n await this.deps.upsertObject(`${this.deviceId}.${object.id}`, object);\n }\n // The model name (getDeviceInfo) for the device-manager card. Best-effort: a device that\n // does not answer getDeviceInfo still connects \u2014 the model line just stays empty.\n try {\n const model = await this.remember(\"model\", async () => modelNameFrom(await this.deps.client.getDeviceInfo()));\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 } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getDeviceInfo failed (${errorMessage(e)})`);\n }\n // The name the user gave the device in the MusicCast app. Best-effort like the model\n // above: an older device that does not answer getNameText simply keeps its label.\n if (this.deps.reportDeviceName) {\n try {\n const name = await this.remember(\"name\", async () => zoneNameFrom(await this.deps.client.getNameText()));\n if (name) {\n this.deps.reportDeviceName(name);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getNameText failed (${errorMessage(e)})`);\n }\n }\n this.zones = capabilities.zones.map(zone => zone.id);\n // Zones in parallel \u2014 disjoint writes, and a zone stuck in its timeout must not hold\n // up the device's readiness.\n const zonesAnswered = await Promise.all(this.zones.map(zone => this.refreshZone(zone)));\n // The zone status is the one request of this start that ALWAYS goes to the device: the\n // capabilities above come from the probe memory on every reconnect, and model/name are\n // best-effort, so nothing before this point can tell a live device from a dead one. Without\n // the check a reconnect to a receiver that had lost power still reported \"ready \u2014\n // MusicCast \u2713\" out of memory while YNCA and XML failed honestly, and info.connection stayed\n // true for a device that was not there (krobi's RX-V6A, 2026-08-26). A device that answers\n // no zone at all is gone \u2014 a standby device still answers, it just reports power=standby.\n if (this.zones.length > 0 && !zonesAnswered.some(Boolean)) {\n this.deps.log.debug(`${this.deviceId}: no zone answered getStatus \u2014 device unreachable (YXC)`);\n return false;\n }\n this.mediaBlocks = capabilities.media;\n this.tunerFeatures = capabilities.tuner;\n this.hasClock = capabilities.clock !== undefined;\n await this.setupBrowse(capabilities);\n await this.refreshMedia();\n await this.refreshLists();\n this.hasDistribution = capabilities.hasDistribution ?? false;\n if (this.hasDistribution) {\n await this.refreshDistribution();\n }\n this.cancelPush = this.deps.registerPush(event => this.onPush(event));\n this.cancelKeepalive = this.deps.scheduleKeepalive(() => void this.keepalive(), KEEPALIVE_MS);\n // The adapter logs one combined \"ready\" line across all transports; this stays at debug.\n this.deps.log.debug(`${this.deviceId}: MusicCast device ready (YXC)`);\n return true;\n }\n\n /**\n * Ask the device once per adapter run and remember the answer for later reconnects.\n *\n * @param key what is being remembered\n * @param probe the request to run when nothing is remembered yet\n * @returns the remembered or freshly fetched value\n */\n private remember<T>(key: string, probe: () => Promise<T>): Promise<T> {\n return this.deps.probeMemory ? this.deps.probeMemory.once(key, probe) : probe();\n }\n\n /**\n * The zone a recall should be routed to: recalling a favourite does not just start it, it\n * also switches THAT zone to the source. Sending everything to the main zone (as this did\n * before) means someone listening in zone 2 gets their favourite in the living room\n * instead \u2014 and the main zone switched away from whatever it was playing.\n *\n * The zone actually listening to the source is the right target; the main zone is the\n * fallback when nothing matches, which is also every single-zone device.\n *\n * @param source the input the recall belongs to (a network source, or \"tuner\")\n * @returns the zone to route the recall to\n */\n private zoneListeningTo(source: string): string {\n if (!source) {\n return \"main\";\n }\n // Main first: on a device where several zones share the source, it is the natural target.\n if (this.lastZoneInput.get(\"main\") === source) {\n return \"main\";\n }\n for (const [zone, input] of this.lastZoneInput) {\n if (input === source) {\n return zone;\n }\n }\n return \"main\";\n }\n\n /**\n * Write a device-originated value \u2014 but never after the connection was closed. A poll\n * or a browse fetch that was already in flight when the adapter stopped would otherwise\n * still write into a tree that is being torn down.\n *\n * @param relativeId the state id relative to the device\n * @param value the value to write\n */\n private emit(relativeId: string, value: boolean | number | string): void {\n if (this.deps.gate?.closed) {\n return;\n }\n this.deps.setStateAck(`${this.deviceId}.${relativeId}`, value);\n }\n\n /**\n * Handle a state change: a user write (ack false) becomes a YXC command; an\n * acked change (the device's own echo) is ignored to avoid a resend loop.\n *\n * @param fullStateId the full state id (device id + \".\" + state)\n * @param ack whether the change is acked (device-originated)\n * @param value the new value\n */\n public handleStateChange(fullStateId: string, ack: boolean, value: unknown): void {\n if (ack) {\n return;\n }\n const prefix = `${this.deviceId}.`;\n if (!fullStateId.startsWith(prefix)) {\n return;\n }\n const stateId = fullStateId.slice(prefix.length);\n if (stateId.startsWith(\"player.browse.\")) {\n this.browseEngine?.handleWrite(stateId, value);\n return;\n }\n // Multiroom writes need controller state (the cached role), so they bypass the pure command map.\n if (stateId === \"multiroom.group.leave\") {\n void this.leaveGroup();\n return;\n }\n if (stateId === \"multiroom.group.linkDevice\") {\n void this.linkClient(String(value));\n return;\n }\n const command = stateToYxc(stateId, value);\n if (command) {\n void this.applyCommand(stateId, command);\n }\n }\n\n /**\n * Register the supervisor's drop handler. MusicCast has no socket-drop event, so a\n * drop is inferred from a run of failed keepalive polls (see keepalive).\n *\n * @param cb invoked once when the device is judged gone\n */\n public onDrop(cb: (reason?: Error) => void): void {\n this.dropDetector.onDrop(cb);\n }\n\n /**\n * Create the browsing surface (#613) when the device has the netusb block: the\n * `netusb/getListInfo` + `setListControl` API drives an 8-line window under\n * `player.browse.*`. Skipped without a delay dep (older tests).\n *\n * @param capabilities the parsed getFeatures capabilities\n */\n private async setupBrowse(capabilities: YxcCapabilities): Promise<void> {\n const gate = this.deps.gate;\n if (!gate || !capabilities.media.includes(\"netusb\")) {\n return;\n }\n const inputs = capabilities.zones.find(zone => zone.id === \"main\")?.inputs ?? [];\n const driver = new YxcBrowseDriver(this.deps.client, inputs);\n this.browseEngine = await createBrowseSurface(driver, this.deviceId, {\n upsertObject: this.deps.upsertObject,\n emit: (id, value) => this.emit(id, value),\n log: this.deps.log,\n delay: ms => gate.delay(ms),\n });\n }\n\n /** Cancel the keepalive and unregister the push handler. Synchronous \u2014 safe from onUnload. */\n public close(): void {\n this.browseEngine?.close();\n // Closing the gate empties its queue and aborts its signal: queued requests are\n // dropped and every pending wait ends, so nothing writes after the teardown.\n this.deps.gate?.close();\n this.cancelKeepalive?.();\n this.cancelKeepalive = undefined;\n // Unregister from the shared push receiver \u2014 otherwise a push arriving after\n // teardown would setState on a controller the adapter has already dropped.\n this.cancelPush?.();\n this.cancelPush = undefined;\n }\n\n /**\n * Handle a device push: each named zone is re-fetched via getStatus, each named\n * media source via getPlayInfo (the push itself is a change signal, not a value\n * carrier). Refreshing only the named sources keeps a track-change push from\n * re-polling every source.\n *\n * @param event the parsed push event\n */\n private onPush(event: unknown): void {\n for (const zone of zonesToRefresh(event)) {\n if (this.zones.includes(zone)) {\n void this.refreshZone(zone);\n }\n }\n for (const block of mediaToRefresh(event)) {\n if (this.mediaBlocks.includes(block)) {\n void this.refreshMediaSource(block);\n }\n }\n // The favourites/recently-played lists announce their changes as flags in the push.\n const lists = netusbListsToRefresh(event);\n if (lists.presets && this.mediaBlocks.includes(\"netusb\")) {\n void this.refreshNetusbPresets();\n }\n if (lists.recent && this.mediaBlocks.includes(\"netusb\")) {\n void this.refreshNetusbRecent();\n }\n }\n\n /**\n * Poll every zone (which renews the push registration and refreshes state) and the\n * media sources. If every zone poll fails for three consecutive failed runs in a row,\n * the device is judged gone and a drop is reported so the supervisor can flip\n * info.connection and reconnect.\n */\n private async keepalive(): Promise<void> {\n // Zones in parallel: their writes are disjoint and one zone stuck in its timeout must\n // not delay the others (a four-zone receiver used to poll them strictly in series).\n const zones = this.zones.length > 0 ? this.zones : [\"main\"];\n const anyOk = (await Promise.all(zones.map(zone => this.refreshZone(zone)))).some(Boolean);\n // Every request above already carried the subscription headers, so the push\n // registration is renewed either way. What still has to be polled depends on whether\n // push works: with push the device announces media, list and group changes itself, so\n // the full sweep only runs occasionally as a safety net (UDP can drop a packet);\n // without push it is the only way anything ever updates.\n this.keepaliveRuns++;\n const fullSweep = !this.deps.pushActive?.() || this.keepaliveRuns % PUSH_MODE_FULL_SWEEP_EVERY === 0;\n if (fullSweep) {\n await this.refreshMedia();\n await this.refreshLists();\n if (this.hasDistribution) {\n await this.refreshDistribution();\n }\n }\n this.dropDetector.record(anyOk);\n }\n\n /**\n * Refresh the list-shaped surfaces: the netusb favourites and recently-played\n * lists, the tuner preset lists, and the clock/alarm settings. Each is\n * best-effort \u2014 a device without the feature answers with an error code and the\n * state simply stays.\n */\n private async refreshLists(): Promise<void> {\n if (this.mediaBlocks.includes(\"netusb\")) {\n await this.refreshNetusbPresets();\n await this.refreshNetusbRecent();\n }\n if (this.mediaBlocks.includes(\"tuner\")) {\n await this.refreshTunerPresets();\n }\n if (this.hasClock) {\n await this.refreshClock();\n }\n }\n\n /** Fetch the stored netusb favourites and write the JSON list state. */\n private async refreshNetusbPresets(): Promise<void> {\n try {\n const update = parseYxcPresetList(await this.deps.client.getPresetInfo());\n if (update) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getPresetInfo failed: ${errorMessage(e)}`);\n }\n }\n\n /** Fetch the recently-played list and write the JSON list state. */\n private async refreshNetusbRecent(): Promise<void> {\n try {\n const update = parseYxcRecentList(await this.deps.client.getRecentInfo());\n if (update) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getRecentInfo failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Fetch the tuner preset lists \u2014 the shared `common` list, or one per band on\n * devices with separate lists \u2014 and write the JSON state.\n */\n private async refreshTunerPresets(): Promise<void> {\n const bands = this.tunerFeatures?.presetType === \"common\" ? [\"common\"] : (this.tunerFeatures?.bands ?? [\"fm\"]);\n const byBand: Record<string, unknown> = {};\n for (const band of bands) {\n try {\n byBand[band] = await this.deps.client.getTunerPresetInfo(band);\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getTunerPresetInfo(${band}) failed: ${errorMessage(e)}`);\n }\n }\n const update = parseYxcTunerPresetLists(byBand);\n if (update) {\n this.emit(update.id, update.value);\n }\n }\n\n /** Fetch the clock/alarm settings and write the read-only clock states. */\n private async refreshClock(): Promise<void> {\n try {\n for (const update of parseYxcClock(await this.deps.client.getClockSettings())) {\n this.emit(update.id, update.value);\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getClockSettings failed: ${errorMessage(e)}`);\n }\n }\n\n /** Refresh every player source the device offers (network player, cd, tuner). */\n private async refreshMedia(): Promise<void> {\n // The three sources (netusb/cd/tuner) write disjoint states \u2014 fetch them together.\n await Promise.all(this.mediaBlocks.map(block => this.refreshMediaSource(block)));\n }\n\n /**\n * Fetch one media source's play info and write the parsed states with ack. The\n * source picks the getPlayInfo argument and the parser: netusb and cd share the\n * play-info shape (different channel), the tuner has its own band/frequency/RDS.\n *\n * @param block the media block (`netusb`, `cd`, `tuner`)\n */\n private async refreshMediaSource(block: string): Promise<void> {\n const arg = block === \"netusb\" ? undefined : block;\n const parse = (info: unknown): StateValue[] =>\n block === \"tuner\"\n ? parseYxcTunerInfo(info)\n : parseYxcPlayInfo(info, block === \"cd\" ? \"player.cd\" : \"player.netPlayer\");\n try {\n const info = await this.deps.client.getPlayInfo(arg);\n for (const update of parse(info)) {\n this.emit(update.id, update.value);\n if (update.id === \"tuner.band\") {\n this.lastTunerBand = String(update.value);\n }\n if (update.id === \"player.netPlayer.source\" && typeof update.value === \"string\") {\n this.lastNetusbInput = update.value;\n }\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getPlayInfo(${arg ?? \"\"}) failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Fetch the MusicCast-Link distribution info and write the parsed dist states with ack,\n * caching the role for the leave-group path.\n */\n private async refreshDistribution(): Promise<void> {\n try {\n const info = await this.deps.client.getDistributionInfo();\n for (const update of parseYxcDistribution(info)) {\n this.emit(update.id, update.value);\n if (update.id === \"multiroom.group.role\") {\n this.lastDistRole = String(update.value);\n }\n }\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getDistributionInfo failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Leave the current MusicCast-Link group: a server stops distributing, a client clears\n * its membership. Then re-read the distribution state so the tree reflects the change.\n */\n private async leaveGroup(): Promise<void> {\n try {\n if (this.lastDistRole === \"server\") {\n await this.deps.client.stopDistribution();\n } else {\n await this.deps.client.setClientInfo({ group_id: \"\", zone: [\"main\"] });\n }\n await this.refreshDistribution();\n } catch (e) {\n // A user action failing must be visible \u2014 warn, like every other write command.\n this.deps.log.warn(`${this.deviceId}: leaveGroup failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Form a MusicCast-Link group with another configured device: give the client the shared\n * group id, add it to this device's roster as the server, and start distributing. The group\n * id is derived from this device's id, so re-linking reuses the same group rather than a new one.\n *\n * @param clientIp the IP of the client device to add (must be a configured device)\n */\n private async linkClient(clientIp: string): Promise<void> {\n const clientClient = this.deps.clientFor?.(clientIp);\n if (!clientClient) {\n this.deps.log.warn(`${this.deviceId}: cannot link ${clientIp} \u2014 not a known device`);\n return;\n }\n try {\n const groupId = createHash(\"md5\").update(this.deviceId).digest(\"hex\");\n await clientClient.setClientInfo({ group_id: groupId, zone: [\"main\"] });\n await this.deps.client.setServerInfo({ group_id: groupId, zone: \"main\", type: \"add\", client_list: [clientIp] });\n await this.deps.client.startDistribution(0);\n await this.refreshDistribution();\n } catch (e) {\n // A user action failing must be visible \u2014 warn, like every other write command.\n this.deps.log.warn(`${this.deviceId}: linkClient(${clientIp}) failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Fetch a zone's status and write its amp states with ack.\n *\n * @param zone the zone to refresh\n * @returns true if the status was fetched, false if the request failed\n */\n private async refreshZone(zone: string): Promise<boolean> {\n try {\n const status = await this.deps.client.getStatus(zone);\n const updates = parseYxcStatus(status, zone);\n for (const update of updates) {\n this.emit(update.id, update.value);\n if (update.id.endsWith(\"input\") && typeof update.value === \"string\") {\n this.lastZoneInput.set(zone, update.value);\n }\n }\n this.cacheEqualizer(zone, updates);\n return true;\n } catch (e) {\n this.deps.log.debug(`${this.deviceId}: getStatus(${zone}) failed: ${errorMessage(e)}`);\n return false;\n }\n }\n\n /**\n * Cache a zone's equalizer bands from its status updates, so a later single-band\n * write can send setEqualizer with all three (the device sets them together).\n *\n * @param zone the zone the updates belong to\n * @param updates the parsed status updates for that zone\n */\n private cacheEqualizer(zone: string, updates: StateValue[]): void {\n // Same definition the status parser uses \u2014 see lib/yxc/zones.ts on why this must not\n // be spelled out a second time.\n const prefix = zonePrefix(zone);\n const band = (b: string): number | undefined => {\n const u = updates.find(x => x.id === `${prefix}sound.equalizer${b}`);\n return typeof u?.value === \"number\" ? u.value : undefined;\n };\n if (band(\"Low\") === undefined && band(\"Mid\") === undefined && band(\"High\") === undefined) {\n return;\n }\n const cur = this.lastEqualizer.get(zone) ?? { low: 0, mid: 0, high: 0 };\n this.lastEqualizer.set(zone, {\n low: band(\"Low\") ?? cur.low,\n mid: band(\"Mid\") ?? cur.mid,\n high: band(\"High\") ?? cur.high,\n });\n }\n\n /**\n * Apply a mapped command. A plain command runs its client call directly; the two\n * commands that need controller-cached state (equalizer bands, tuner band) are\n * completed here \u2014 the only place that state lives.\n *\n * @param stateId the written state id, for the failure log line\n * @param command the YXC command to apply\n */\n private async applyCommand(stateId: string, command: YxcCommand): Promise<void> {\n try {\n switch (command.kind) {\n case \"run\":\n await command.run(this.deps.client);\n break;\n case \"equalizer\": {\n // The device sets all three bands in one call, so the other two come from the\n // cache. Without a cached set we must NOT invent 0/0/0 \u2014 that would silently\n // flatten the user's other two bands. The cache fills from a zone status, so\n // fetch one first; only if even that fails is the write refused (with a warning).\n const { zone, band, value } = command;\n let current = this.lastEqualizer.get(zone);\n if (!current) {\n await this.refreshZone(zone);\n current = this.lastEqualizer.get(zone);\n }\n if (!current) {\n this.deps.log.warn(\n `${this.deviceId}: not writing ${stateId} \u2014 the device has not reported its equalizer bands yet`,\n );\n break;\n }\n const next = { ...current, [band]: value };\n await this.deps.client.setEqualizer(next.low, next.mid, next.high, zone);\n this.lastEqualizer.set(zone, next);\n break;\n }\n case \"tunerBand\":\n await this.deps.client.setBand(command.band);\n // Remember it immediately: the poll that would report it back runs minutes later.\n this.lastTunerBand = command.band;\n break;\n case \"tunerFreq\":\n await this.deps.client.setFreq(this.lastTunerBand, command.value);\n break;\n case \"tunerPreset\": {\n // Shared-list devices recall on `common`; separate-list devices on the current band.\n const band = this.tunerFeatures?.presetType === \"common\" ? \"common\" : this.lastTunerBand;\n await this.deps.client.recallTunerPreset(band, command.value, this.zoneListeningTo(\"tuner\"));\n break;\n }\n case \"netusbPreset\":\n await this.deps.client.recallPreset(command.value, this.zoneListeningTo(this.lastNetusbInput));\n break;\n case \"netusbRecent\":\n await this.deps.client.recallRecentItem(command.value, this.zoneListeningTo(this.lastNetusbInput));\n break;\n }\n } catch (e) {\n this.deps.log.warn(`${this.deviceId}: write to ${stateId} failed: ${errorMessage(e)}`);\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/build/main.js CHANGED
@@ -58,6 +58,7 @@ const FETCH_TIMEOUT_MS = 4e3;
58
58
  const SSDP_SEARCH_BURST = 3;
59
59
  const SSDP_SEARCH_INTERVAL_MS = 1e3;
60
60
  const TRANSPORT_IDS = ["ynca", "yxc", "xml"];
61
+ const DATAPOINT_BALANCE_SETTLE_MS = 5e3;
61
62
  class Yamaha extends utils.Adapter {
62
63
  supervisors = [];
63
64
  /** deviceId → its supervisor, so a state change goes to ONE device, not to all of them. */
@@ -66,6 +67,20 @@ class Yamaha extends utils.Adapter {
66
67
  pushReceiver;
67
68
  /** Device-manager backend: the receivers as cards with add/edit/delete. */
68
69
  deviceManagement;
70
+ /**
71
+ * Every datapoint that existed when this run started, filled ONCE before the cleanup and
72
+ * before any device connects. Without it the balance below would report the whole tree as
73
+ * new on every restart: `upsertObject` runs `extendObject` on every state it touches (the
74
+ * role/unit retrofit), so "did the create path run?" is not the same question as "is this
75
+ * datapoint new?".
76
+ */
77
+ knownDatapoints = /* @__PURE__ */ new Set();
78
+ createdDatapoints = 0;
79
+ removedDatapoints = 0;
80
+ /** Debounce for the balance line, so one config change produces ONE line, not one per device. */
81
+ balanceTimer;
82
+ /** Set when the start-up snapshot failed — a balance without it would be wrong, so none is written. */
83
+ balanceDisabled = false;
69
84
  /**
70
85
  * @param options adapter options passed through by js-controller
71
86
  */
@@ -97,6 +112,7 @@ class Yamaha extends utils.Adapter {
97
112
  );
98
113
  const devices = configured.length > 0 ? configured : await this.autoDiscover();
99
114
  const knownDeviceIps = new Set(devices.map((device) => device.ip));
115
+ await this.snapshotExistingDatapoints();
100
116
  await this.cleanupStaleObjects(new Set(devices.map((device) => device.id)));
101
117
  this.subscribeStates("*");
102
118
  const pushReceiver = new import_push_receiver.YxcPushReceiver({
@@ -112,6 +128,7 @@ class Yamaha extends utils.Adapter {
112
128
  for (const device of devices) {
113
129
  this.deviceConnected.set(device.id, false);
114
130
  await this.ensureDeviceHeader(device.id);
131
+ await this.setState(`${device.id}.info.connection`, { val: false, ack: true });
115
132
  const reachability = new import_reachability_dedup.ReachabilityDedup();
116
133
  const subunitCache = await this.loadYncaSubunitCache(device.id);
117
134
  const probeMemory = new import_probe_memory.ProbeMemory();
@@ -131,6 +148,7 @@ class Yamaha extends utils.Adapter {
131
148
  this.supervisorById.set(device.id, supervisor);
132
149
  supervisor.start();
133
150
  }
151
+ this.writeDeviceOverview();
134
152
  } catch (e) {
135
153
  this.log.error(`onReady failed: ${(0, import_util.errorMessage)(e)}`);
136
154
  }
@@ -150,6 +168,22 @@ class Yamaha extends utils.Adapter {
150
168
  }
151
169
  const anyConnected = [...this.deviceConnected.values()].some(Boolean);
152
170
  void this.setState("info.connection", { val: anyConnected, ack: true });
171
+ this.writeDeviceOverview();
172
+ }
173
+ /**
174
+ * The three overview datapoints: how many devices this instance runs, how many are
175
+ * connected right now, and whether that is all of them. Derived from the SAME map that
176
+ * feeds the per-device markers and written in the same round — computed separately they
177
+ * would drift away from what the single devices say.
178
+ *
179
+ * `devicesAllOnline` needs at least one device: zero of zero is not "everything is fine".
180
+ */
181
+ writeDeviceOverview() {
182
+ const total = this.deviceConnected.size;
183
+ const online = [...this.deviceConnected.values()].filter(Boolean).length;
184
+ void this.setState("info.devicesTotal", { val: total, ack: true });
185
+ void this.setState("info.devicesOnline", { val: online, ack: true });
186
+ void this.setState("info.devicesAllOnline", { val: total > 0 && online === total, ack: true });
153
187
  }
154
188
  /**
155
189
  * Reflect the live transport set into a device's `info.transports.*` flags so the
@@ -173,7 +207,8 @@ class Yamaha extends utils.Adapter {
173
207
  * @param deviceIds the ids of the currently configured devices
174
208
  */
175
209
  async cleanupStaleObjects(deviceIds) {
176
- const existing = Object.keys(await this.getAdapterObjectsAsync());
210
+ const allObjects = await this.getAdapterObjectsAsync();
211
+ const existing = Object.keys(allObjects);
177
212
  const stale = (0, import_pure_helpers.staleObjects)(existing, deviceIds, this.namespace);
178
213
  const renamed = (0, import_pure_helpers.renamedObjectIds)(existing, deviceIds, this.namespace);
179
214
  const config = this.config;
@@ -193,15 +228,91 @@ class Yamaha extends utils.Adapter {
193
228
  }
194
229
  }
195
230
  if (stale.length > 0) {
196
- this.log.info(`removed ${stale.length} object(s) from a previous configuration`);
231
+ this.log.debug(`removed ${stale.length} object(s) from a previous configuration`);
197
232
  }
198
233
  if (renamed.length > 0) {
199
- this.log.info(`removed ${renamed.length} renamed object(s) from an earlier version`);
234
+ this.log.debug(`removed ${renamed.length} renamed object(s) from an earlier version`);
200
235
  }
201
236
  if (disabled.length > 0) {
202
- this.log.info(`removed ${disabled.length} object(s) from switched-off datapoint groups`);
237
+ this.log.debug(`removed ${disabled.length} object(s) from switched-off datapoint groups`);
238
+ }
239
+ this.noteDatapointsRemoved(
240
+ [...stale, ...renamed, ...disabled].filter((fullId) => {
241
+ var _a;
242
+ return ((_a = allObjects[fullId]) == null ? void 0 : _a.type) === "state";
243
+ })
244
+ );
245
+ }
246
+ /**
247
+ * Remember every datapoint that already exists, ONCE per adapter run.
248
+ *
249
+ * @see knownDatapoints for why the create path alone cannot answer "is this new?"
250
+ */
251
+ async snapshotExistingDatapoints() {
252
+ try {
253
+ for (const [fullId, object] of Object.entries(await this.getAdapterObjectsAsync())) {
254
+ if ((object == null ? void 0 : object.type) === "state") {
255
+ this.knownDatapoints.add((0, import_pure_helpers.stripNamespace)(fullId, this.namespace));
256
+ }
257
+ }
258
+ } catch (e) {
259
+ this.log.debug(`could not read the existing datapoints (${(0, import_util.errorMessage)(e)}); balance line disabled`);
260
+ this.balanceDisabled = true;
203
261
  }
204
262
  }
263
+ /**
264
+ * Count a datapoint the device tree just created — new ones only.
265
+ *
266
+ * @param id the state id relative to the namespace
267
+ */
268
+ noteDatapointCreated(id) {
269
+ if (this.knownDatapoints.has(id)) {
270
+ return;
271
+ }
272
+ this.knownDatapoints.add(id);
273
+ this.createdDatapoints++;
274
+ this.scheduleDatapointBalance();
275
+ }
276
+ /**
277
+ * Count removed datapoints, and let them count again should they ever come back.
278
+ *
279
+ * @param fullIds the removed ids, namespace included
280
+ */
281
+ noteDatapointsRemoved(fullIds) {
282
+ for (const fullId of fullIds) {
283
+ this.knownDatapoints.delete((0, import_pure_helpers.stripNamespace)(fullId, this.namespace));
284
+ this.removedDatapoints++;
285
+ }
286
+ if (fullIds.length > 0) {
287
+ this.scheduleDatapointBalance();
288
+ }
289
+ }
290
+ /**
291
+ * Log the balance once the tree has settled. A device connects asynchronously and several
292
+ * devices connect at once, so the line waits for quiet instead of firing per device — the
293
+ * user made ONE change and reads ONE result.
294
+ */
295
+ scheduleDatapointBalance() {
296
+ if (this.balanceDisabled) {
297
+ return;
298
+ }
299
+ this.clearTimeout(this.balanceTimer);
300
+ this.balanceTimer = this.setTimeout(() => {
301
+ this.balanceTimer = void 0;
302
+ const parts = [];
303
+ if (this.createdDatapoints > 0) {
304
+ parts.push(`created ${this.createdDatapoints} datapoint(s)`);
305
+ }
306
+ if (this.removedDatapoints > 0) {
307
+ parts.push(`removed ${this.removedDatapoints} datapoint(s)`);
308
+ }
309
+ this.createdDatapoints = 0;
310
+ this.removedDatapoints = 0;
311
+ if (parts.length > 0) {
312
+ this.log.info(`Object tree updated: ${parts.join(", ")}`);
313
+ }
314
+ }, DATAPOINT_BALANCE_SETTLE_MS);
315
+ }
205
316
  /**
206
317
  * Create a device's header objects (the device node, its info channel and a
207
318
  * per-device connection indicator) so its state is visible even while offline.
@@ -407,6 +518,9 @@ class Yamaha extends utils.Adapter {
407
518
  return;
408
519
  }
409
520
  await this.extendObject(id, { type: def.type, common: def.common, native: {} });
521
+ if (def.type === "state") {
522
+ this.noteDatapointCreated(id);
523
+ }
410
524
  },
411
525
  setStateAck: (id, value) => {
412
526
  if (!(0, import_groups.isGroupEnabled)(id.slice(id.indexOf(".") + 1), this.config)) {
@@ -463,15 +577,24 @@ class Yamaha extends utils.Adapter {
463
577
  onUnload(callback) {
464
578
  var _a;
465
579
  try {
580
+ this.clearTimeout(this.balanceTimer);
466
581
  (_a = this.pushReceiver) == null ? void 0 : _a.close();
467
582
  for (const supervisor of this.supervisors) {
468
583
  supervisor.close();
469
584
  }
470
- void this.setState("info.connection", { val: false, ack: true });
471
- callback();
585
+ const writes = [this.setState("info.connection", { val: false, ack: true })];
586
+ for (const deviceId of this.deviceConnected.keys()) {
587
+ this.deviceConnected.set(deviceId, false);
588
+ writes.push(this.setState(`${deviceId}.info.connection`, { val: false, ack: true }));
589
+ }
590
+ writes.push(this.setState("info.devicesOnline", { val: 0, ack: true }));
591
+ writes.push(this.setState("info.devicesAllOnline", { val: false, ack: true }));
592
+ void Promise.all(writes).catch(() => {
593
+ }).finally(callback);
594
+ return;
472
595
  } catch {
473
- callback();
474
596
  }
597
+ callback();
475
598
  }
476
599
  /**
477
600
  * Auto-discovery for an empty device table: scan the network, merge the finds with
package/build/main.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/main.ts"],
4
- "sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { createSocket } from \"node:dgram\";\nimport { get as httpGet } from \"node:http\";\nimport { networkInterfaces } from \"node:os\";\nimport { join } from \"node:path\";\nimport { attemptDevice } from \"./lib/attempt-device\";\nimport { searchInterfaces } from \"./lib/network-interfaces\";\nimport { isGroupEnabled } from \"./lib/catalog/groups\";\nimport { iconForModel } from \"./lib/device-type\";\nimport {\n LABEL_RANK,\n type LabelRank,\n legacyDeviceRow,\n mergeDiscovered,\n nextDeviceLabel,\n parseDevices,\n renamedObjectIds,\n staleObjects,\n stripNamespace,\n} from \"./lib/pure-helpers\";\nimport { errorMessage } from \"./lib/util\";\nimport { discoverYamaha } from \"./lib/discovery\";\nimport { readDiscovered, writeDiscovered } from \"./lib/discovered-store\";\nimport { discoveredStoreDeps } from \"./lib/discovered-store-deps\";\nimport { YxcPushReceiver } from \"./lib/yxc/push-receiver\";\nimport { YamahaDeviceManagement } from \"./device-management\";\nimport type { DeviceRecord } from \"./lib/types\";\nimport { DeviceSupervisor, type ConnectionHandle } from \"./lib/lifecycle/device-supervisor\";\nimport { ReconnectStrategy } from \"./lib/lifecycle/reconnect-strategy\";\nimport { ReachabilityDedup } from \"./lib/lifecycle/reachability-dedup\";\nimport { createSubunitCache, isAvailSnapshot, type YncaSubunitCache } from \"./lib/ynca/subunit-cache\";\nimport { ProbeMemory } from \"./lib/lifecycle/probe-memory\";\n\n/** Supervisor reconnect backoff bounds (exponential: 1s, 2s \u2026 capped at 60s). */\nconst RECONNECT_BASE_MS = 1000;\nconst RECONNECT_MAX_MS = 60000;\n\n/** Abort a discovery description fetch after this long, so a dead device cannot hang it. */\nconst FETCH_TIMEOUT_MS = 4000;\n\n/** How often the discovery M-SEARCH is repeated \u2014 multicast is lossy, one dropped packet must not hide a receiver. */\nconst SSDP_SEARCH_BURST = 3;\n/** Spacing between the repeated M-SEARCH sends, inside the collect window. */\nconst SSDP_SEARCH_INTERVAL_MS = 1000;\n\n/** The three transports in attempt order \u2014 also the per-transport `info.transports.*` state ids. */\nconst TRANSPORT_IDS = [\"ynca\", \"yxc\", \"xml\"] as const;\n\n/**\n * ioBroker.yamaha \u2014 controls Yamaha AV receivers and MusicCast devices.\n *\n * Each configured device is driven by a supervisor that keeps a multi-transport\n * handle online: every protocol the device answers \u2014 YNCA (amp control over a held\n * TCP connection, event-pushed), YXC (MusicCast, push + poll), XML/YNC (pre-2010,\n * polled over HTTP) \u2014 connects in parallel on one object tree, each datapoint owned\n * by the best-fitting transport. All YXC devices share one UDP push receiver, keyed\n * by source IP.\n */\nexport class Yamaha extends utils.Adapter {\n private readonly supervisors: DeviceSupervisor[] = [];\n /** deviceId \u2192 its supervisor, so a state change goes to ONE device, not to all of them. */\n private readonly supervisorById = new Map<string, DeviceSupervisor>();\n private readonly deviceConnected = new Map<string, boolean>();\n private pushReceiver: YxcPushReceiver | undefined;\n /** Device-manager backend: the receivers as cards with add/edit/delete. */\n private readonly deviceManagement: YamahaDeviceManagement;\n\n /**\n * @param options adapter options passed through by js-controller\n */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: \"yamaha\",\n });\n\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"stateChange\", this.onStateChange.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n this.deviceManagement = new YamahaDeviceManagement(this);\n }\n\n /** Start a supervisor for each configured device, then subscribe to state changes. */\n private async onReady(): Promise<void> {\n // Load admin translations for the device-manager cards \u2014 a UI concern, in its own\n // guarded block so an i18n failure never aborts core device startup below.\n try {\n await utils.I18n.init(join(this.adapterDir, \"admin\"), this);\n } catch (e) {\n this.log.warn(`could not load admin translations (${errorMessage(e)}); card labels may be untranslated`);\n }\n try {\n this.log.info('starting \u2014 a \"ready\" message will follow for each device');\n await this.setState(\"info.connection\", { val: false, ack: true });\n await this.migrateLegacyDevice();\n await this.migrateGroupZones();\n // The device list is the switch: filled \u2192 use exactly those (manual); empty\n // \u2192 discover on the network and run what is found (auto). XML/pre-2010 devices\n // never answer SSDP, so they are always added manually.\n const configured = parseDevices(this.config.devices, (dropped, takenId) =>\n this.log.warn(`device \"${dropped}\" skipped \u2014 its object id \"${takenId}\" is already used by another device`),\n );\n const devices = configured.length > 0 ? configured : await this.autoDiscover();\n const knownDeviceIps = new Set(devices.map(device => device.ip));\n await this.cleanupStaleObjects(new Set(devices.map(device => device.id)));\n this.subscribeStates(\"*\");\n const pushReceiver = new YxcPushReceiver({\n log: { debug: message => this.log.debug(message), warn: message => this.log.warn(message) },\n schedule: (cb, ms) => this.setTimeout(cb, ms),\n cancel: handle => this.clearTimeout(handle),\n });\n pushReceiver.start();\n this.pushReceiver = pushReceiver;\n if (configured.length > 0) {\n this.log.info(`setting up ${devices.length} configured device(s)...`);\n }\n for (const device of devices) {\n this.deviceConnected.set(device.id, false);\n await this.ensureDeviceHeader(device.id);\n const reachability = new ReachabilityDedup();\n const subunitCache = await this.loadYncaSubunitCache(device.id);\n // Held here, not in the controllers: those are rebuilt on every connection attempt.\n const probeMemory = new ProbeMemory();\n const supervisor = new DeviceSupervisor({\n attempt: () =>\n this.attemptDevice(device, pushReceiver, knownDeviceIps, reachability, subunitCache, probeMemory),\n schedule: (cb, ms) => this.setTimeout(cb, ms),\n cancel: handle => this.clearTimeout(handle as ioBroker.Timeout | undefined),\n onConnectionChange: connected => this.reportConnection(device.id, connected),\n backoff: new ReconnectStrategy(RECONNECT_BASE_MS, RECONNECT_MAX_MS),\n log: {\n debug: message => this.log.debug(message),\n info: message => this.log.info(message),\n warn: message => this.log.warn(message),\n },\n });\n this.supervisors.push(supervisor);\n this.supervisorById.set(device.id, supervisor);\n supervisor.start();\n }\n } catch (e) {\n this.log.error(`onReady failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Aggregate one device's connection state into the adapter's `info.connection`\n * (true while at least one device is connected).\n *\n * @param deviceId the device reporting\n * @param connected whether that device is currently connected\n */\n private reportConnection(deviceId: string, connected: boolean): void {\n this.deviceConnected.set(deviceId, connected);\n void this.setState(`${deviceId}.info.connection`, { val: connected, ack: true });\n // A drop clears the per-transport flags; a (re)connect sets them again via onTransports.\n if (!connected) {\n this.setTransports(deviceId, []);\n }\n const anyConnected = [...this.deviceConnected.values()].some(Boolean);\n void this.setState(\"info.connection\", { val: anyConnected, ack: true });\n }\n\n /**\n * Reflect the live transport set into a device's `info.transports.*` flags so the\n * device-manager card shows which protocols (YNCA/YXC/XML) are connected right now.\n *\n * @param deviceId the id-safe device id\n * @param names the transports live now (empty on a drop)\n */\n private setTransports(deviceId: string, names: string[]): void {\n const live = new Set(names);\n for (const proto of TRANSPORT_IDS) {\n void this.setState(`${deviceId}.info.transports.${proto}`, { val: live.has(proto), ack: true });\n }\n }\n\n /**\n * One-shot startup cleanup: delete every object that does not belong to a\n * configured device (the previous adapter's whole tree, and any device dropped\n * from the config). Runs before the devices connect; a configured device's\n * subtree is kept whether or not it has connected yet.\n *\n * @param deviceIds the ids of the currently configured devices\n */\n private async cleanupStaleObjects(deviceIds: Set<string>): Promise<void> {\n const existing = Object.keys(await this.getAdapterObjectsAsync());\n const stale = staleObjects(existing, deviceIds, this.namespace);\n // Old states this version renamed/moved (e.g. system.model -> info.model): delete the\n // old object so it does not linger orphaned beside the new one under a kept device.\n const renamed = renamedObjectIds(existing, deviceIds, this.namespace);\n // Objects whose datapoint group the user switched off \u2014 remove them so turning a group from\n // on to off cleans up its whole subtree (a toggle change restarts the instance, so this runs).\n const config = this.config as unknown as Record<string, unknown>;\n const disabled = existing.filter(full => {\n for (const deviceId of deviceIds) {\n const base = `${this.namespace}.${deviceId}.`;\n if (full.startsWith(base) && !isGroupEnabled(full.slice(base.length), config)) {\n return true;\n }\n }\n return false;\n });\n for (const fullId of [...stale, ...renamed, ...disabled]) {\n try {\n await this.delObjectAsync(stripNamespace(fullId, this.namespace));\n } catch {\n // already removed together with its parent\n }\n }\n if (stale.length > 0) {\n this.log.info(`removed ${stale.length} object(s) from a previous configuration`);\n }\n if (renamed.length > 0) {\n this.log.info(`removed ${renamed.length} renamed object(s) from an earlier version`);\n }\n if (disabled.length > 0) {\n this.log.info(`removed ${disabled.length} object(s) from switched-off datapoint groups`);\n }\n }\n\n /**\n * Create a device's header objects (the device node, its info channel and a\n * per-device connection indicator) so its state is visible even while offline.\n *\n * @param deviceId the id-safe device id\n */\n private async ensureDeviceHeader(deviceId: string): Promise<void> {\n // statusStates.onlineId lets the admin paint a green/red reachability symbol on the\n // device object itself (as govee does), fed by the per-device connection state.\n // extendObject with preserve:name so an upgrade adds the symbol without overwriting\n // a name the user changed.\n // A device that has not reported its model yet would sit in the tree without any\n // symbol \u2014 an upgraded instance shows that on every start before the first report,\n // and a device that never answers shows it for good. Seed the default silhouette,\n // but only when there is none: overwriting would flip a soundbar back to the\n // receiver default for the seconds until its model arrives.\n let icon: string | undefined;\n try {\n const existing = await this.getObjectAsync(deviceId);\n icon = existing?.common?.icon ? undefined : iconForModel(undefined);\n } catch {\n icon = undefined;\n }\n await this.extendObject(\n deviceId,\n {\n type: \"device\",\n common: {\n name: deviceId,\n ...(icon ? { icon } : {}),\n statusStates: { onlineId: `${this.namespace}.${deviceId}.info.connection` },\n },\n native: {},\n },\n { preserve: { common: [\"name\"] } },\n );\n await this.setObjectNotExistsAsync(`${deviceId}.info`, { type: \"channel\", common: { name: \"Info\" }, native: {} });\n await this.setObjectNotExistsAsync(`${deviceId}.info.connection`, {\n type: \"state\",\n common: { name: \"Connected\", type: \"boolean\", role: \"indicator.reachable\", read: true, write: false, def: false },\n native: {},\n });\n // Model name shown on the device-manager card. Filled by whichever transport reports it\n // (YNCA MODELNAME, YXC/XML model); created here so the card's model line binds even for an\n // offline device or a transport that does not report a model.\n await this.setObjectNotExistsAsync(`${deviceId}.info.model`, {\n type: \"state\",\n common: { name: \"Model\", type: \"string\", role: \"text\", read: true, write: false, def: \"\" },\n native: {},\n });\n // Per-transport connection flags, fed by the live set from connectTransports and read live\n // by the device-manager card indicators. Created here so an offline device's card still\n // renders all three (false) instead of nothing.\n await this.setObjectNotExistsAsync(`${deviceId}.info.transports`, {\n type: \"channel\",\n common: { name: \"Transports\" },\n native: {},\n });\n for (const proto of TRANSPORT_IDS) {\n await this.setObjectNotExistsAsync(`${deviceId}.info.transports.${proto}`, {\n type: \"state\",\n common: {\n name: `${proto.toUpperCase()} connected`,\n type: \"boolean\",\n role: \"indicator.reachable\",\n read: true,\n write: false,\n def: false,\n },\n native: {},\n });\n }\n }\n\n /** The icon last written per device, so repeated model reports do not re-write the object. */\n private readonly deviceIcons = new Map<string, string>();\n\n /** The label this adapter wrote per device, with the rank of the source behind it. */\n private readonly deviceLabels = new Map<string, { name: string; rank: LabelRank }>();\n\n /**\n * Give the device node a name a user recognises, once the device reports one.\n *\n * An instance upgraded from the previous adapter carries the receiver's ip as its\n * device name \u2014 that adapter knew nothing but an ip, so the migration had nothing\n * else to call it. The object id stays that ip for good (history and visualisation\n * bindings hang off it), but the displayed name does not have to.\n *\n * A name the user typed is never touched, and the model never replaces a name the\n * device reported for itself \u2014 see {@link nextDeviceLabel}.\n *\n * @param deviceId the id-safe device id\n * @param candidate the reported name (a MusicCast zone name, or the model)\n * @param rank how trustworthy the candidate is\n */\n private async updateDeviceLabel(deviceId: string, candidate: string, rank: LabelRank): Promise<void> {\n const own = this.deviceLabels.get(deviceId);\n try {\n const current = (await this.getObjectAsync(deviceId))?.common?.name;\n const label = nextDeviceLabel(\n typeof current === \"string\" ? current : undefined,\n deviceId,\n candidate,\n rank,\n own?.name,\n own?.rank,\n );\n if (label === undefined) {\n return;\n }\n // Deliberately without `preserve: { common: [\"name\"] }`: nextDeviceLabel has just\n // established that the present name is the adapter's own placeholder, not a user's.\n await this.extendObject(deviceId, { common: { name: label } });\n this.deviceLabels.set(deviceId, { name: label, rank });\n this.log.debug(`${deviceId}: device name set to \"${label}\"`);\n } catch (e) {\n this.log.debug(`${deviceId}: setting the device name failed (${errorMessage(e)})`);\n }\n }\n\n /**\n * Paint the device-class silhouette on the device node once the model is known \u2014\n * detected from the reported model name, written only when it actually changes.\n *\n * @param deviceId the id-safe device id\n * @param model the reported model name\n */\n private async updateDeviceIcon(deviceId: string, model: string): Promise<void> {\n const icon = iconForModel(model);\n if (this.deviceIcons.get(deviceId) === icon) {\n return;\n }\n this.deviceIcons.set(deviceId, icon);\n try {\n await this.extendObject(deviceId, { common: { icon } });\n } catch (e) {\n this.log.debug(`${deviceId}: setting device icon failed (${errorMessage(e)})`);\n }\n }\n\n /**\n * Carry over the previous adapter's single-device config into the device table.\n * The old yamaha stored one receiver as `config.ip` (older installs: `config.IP`);\n * the new adapter uses a `devices` table, so an upgraded instance would otherwise\n * start with an empty table and lose its receiver. Persists the row so the admin\n * table shows it, and fills `this.config` in memory so this run already drives it.\n */\n private async migrateLegacyDevice(): Promise<void> {\n const config = this.config as unknown as Record<string, unknown>;\n const row = legacyDeviceRow(config);\n if (!row) {\n return;\n }\n // Fill the in-memory config first, so this run already drives the device even\n // if persisting the table below fails \u2014 persistence is a convenience for the\n // admin view, not a precondition for running.\n config.devices = [row];\n try {\n await this.extendForeignObjectAsync(`system.adapter.${this.namespace}`, { native: { devices: [row] } });\n this.log.info(`carried the previous single-device config (${row.ip}) over into the device table`);\n } catch (e) {\n this.log.warn(\n `could not persist the migrated device table (${errorMessage(e)}); ` + `running with the in-memory value`,\n );\n }\n }\n\n /**\n * Fold the removed `group_zones` toggle into `group_multiroom` \u2014 zone 2/3/4 now\n * belong to the multiroom group. Existing installs that had zones on but multiroom\n * off would otherwise lose their zone datapoints after the update.\n */\n private async migrateGroupZones(): Promise<void> {\n const config = this.config as unknown as Record<string, unknown>;\n if (!(\"group_zones\" in config)) {\n return;\n }\n // (`!group_multiroom` only reads as a guard \u2014 the assignment sets it to true\n // either way, so an already-on multiroom group is not changed by it.)\n if (config.group_zones && !config.group_multiroom) {\n config.group_multiroom = true;\n }\n delete config.group_zones;\n try {\n const obj = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`);\n if (obj?.native) {\n if (obj.native.group_zones && !obj.native.group_multiroom) {\n obj.native.group_multiroom = true;\n }\n delete obj.native.group_zones;\n await this.setForeignObjectAsync(`system.adapter.${this.namespace}`, obj);\n this.log.info(\"migrated group_zones setting into group_multiroom\");\n }\n } catch (e) {\n this.log.warn(`could not persist group_zones migration (${errorMessage(e)})`);\n }\n }\n\n /**\n * Bring one device online across ALL its transports: every protocol that answers\n * \u2014 YNCA (amp control over a held TCP connection), YXC (MusicCast), XML/YNC\n * (pre-2010) \u2014 connects in parallel on one object tree. Returns a connection handle\n * the supervisor keeps, or null when no transport answers this attempt. Each\n * datapoint is owned by exactly one transport (owner-policy), so the mappers never\n * collide on a shared id.\n *\n * @param device the configured device record\n * @param pushReceiver the shared YXC push receiver\n * @param knownDeviceIps IPs of all configured devices, for resolving a multiroom client\n * @param reachability dedup for the \"no reachable transport\" warning (one instance per device,\n * held by the caller across retries \u2014 see {@link ReachabilityDedup})\n * @param yncaSubunitCache per-device cache of the YNCA AVAIL probe (skips the probe on reconnects)\n * @param probeMemory per-device memory for constant device answers (skips re-asking on reconnects)\n * @returns a connection handle, or null when no transport connected\n */\n private attemptDevice(\n device: DeviceRecord,\n pushReceiver: YxcPushReceiver,\n knownDeviceIps: Set<string>,\n reachability: ReachabilityDedup,\n yncaSubunitCache: YncaSubunitCache,\n probeMemory: ProbeMemory,\n ): Promise<ConnectionHandle | null> {\n return attemptDevice(device, {\n reachability,\n yncaSubunitCache,\n probeMemory,\n // Group gate for the YNCA sweep: a disabled group's functions are never even fetched.\n isEntryEnabled: id => isGroupEnabled(id, this.config as unknown as Record<string, unknown>),\n log: {\n debug: message => this.log.debug(message),\n info: message => this.log.info(message),\n warn: message => this.log.warn(message),\n },\n upsertObject: async (id, def) => {\n // Gate on the datapoint group: a switched-off group's objects are not created. The id is\n // \"<deviceId>.<relativeId>\"; groupOf reads the relative part.\n if (!isGroupEnabled(id.slice(id.indexOf(\".\") + 1), this.config as unknown as Record<string, unknown>)) {\n return;\n }\n await this.extendObject(id, { type: def.type, common: def.common, native: {} });\n },\n setStateAck: (id, value) => {\n // Same group gate as upsertObject, so a switched-off group seeds no orphan value either.\n if (!isGroupEnabled(id.slice(id.indexOf(\".\") + 1), this.config as unknown as Record<string, unknown>)) {\n return;\n }\n void this.setState(id, { val: value, ack: true });\n // A model report also decides the device-class icon on the device node \u2014 and, for a\n // device still carrying the ip it was migrated with, its readable name.\n if (id.endsWith(\".info.model\") && typeof value === \"string\" && value.length > 0) {\n const reporting = id.slice(0, id.indexOf(\".\"));\n void this.updateDeviceIcon(reporting, value);\n void this.updateDeviceLabel(reporting, value, LABEL_RANK.model);\n }\n },\n onDeviceName: name => void this.updateDeviceLabel(device.id, name, LABEL_RANK.deviceName),\n timers: {\n schedule: (handler, ms) => this.setTimeout(handler, ms),\n cancel: handle => this.clearTimeout(handle),\n },\n registerPush: (ip, onPush) => pushReceiver.register(ip, onPush),\n pushActive: () => pushReceiver.isListening(),\n scheduleKeepalive: (handler, ms) => {\n const timer = this.setInterval(handler, ms);\n return () => {\n if (timer) {\n this.clearInterval(timer);\n }\n };\n },\n xmlPollIntervalMs: this.xmlPollIntervalMs(),\n onTransports: names => this.setTransports(device.id, names),\n knownDeviceIps,\n });\n }\n\n /**\n * Route a state change to every device's supervisor (each forwards to its\n * active controller, which ignores ids outside its subtree and its acked echoes).\n *\n * @param id the full state id\n * @param state the new state (null when deleted)\n */\n private onStateChange(id: string, state: ioBroker.State | null | undefined): void {\n if (!state) {\n return;\n }\n const relative = stripNamespace(id, this.namespace);\n // The adapter subscribes to its whole namespace, so every one of its own acked writes\n // comes back here too \u2014 during a sweep that is hundreds of events. Route by the id's\n // first segment instead of offering each one to every device in turn.\n const deviceId = relative.slice(0, relative.indexOf(\".\"));\n this.supervisorById.get(deviceId)?.handleStateChange(relative, state.ack, state.val);\n }\n\n /**\n * Synchronous teardown \u2014 no await, call the callback immediately (SIGKILL otherwise).\n *\n * @param callback function to invoke once teardown is complete\n */\n private onUnload(callback: () => void): void {\n try {\n this.pushReceiver?.close();\n for (const supervisor of this.supervisors) {\n supervisor.close();\n }\n void this.setState(\"info.connection\", { val: false, ack: true });\n callback();\n } catch {\n callback();\n }\n }\n\n /**\n * Auto-discovery for an empty device table: scan the network, merge the finds with\n * the devices remembered from earlier runs (standby protection), persist the merged\n * set and return it. XML/pre-2010 receivers do not answer SSDP and never appear here.\n *\n * @returns the device records to run this session\n */\n private async autoDiscover(): Promise<DeviceRecord[]> {\n const store = discoveredStoreDeps(this);\n const known = await readDiscovered(store);\n this.log.info(\"auto-discovery via SSDP (older XML-only devices must be added manually)\");\n let found: Array<{ ip: string; name: string }> = [];\n try {\n found = await discoverYamaha({\n search: (target, ms) => this.ssdpSearch(target, ms),\n fetch: url => this.fetchUrl(url),\n log: { debug: message => this.log.debug(message), warn: message => this.log.warn(message) },\n });\n } catch (e) {\n this.log.warn(`auto-discovery scan failed, using the remembered devices: ${errorMessage(e)}`);\n }\n const merged = mergeDiscovered(known, found, (dropped, takenId) =>\n this.log.warn(`discovered device \"${dropped}\" skipped \u2014 its object id \"${takenId}\" is already taken`),\n );\n await writeDiscovered(store, merged);\n const remembered = merged.length - found.length;\n const suffix = remembered > 0 ? ` (${remembered} more remembered from a previous run)` : \"\";\n this.log.info(`setting up ${merged.length} discovered device(s)${suffix}...`);\n return merged;\n }\n\n /**\n * Load a device's persisted YNCA subunit-cache (the AVAIL probe result) from its\n * device object's native part, wrapped so updates persist back there. The device\n * object is the right home: writing an instance object's native restarts the\n * adapter, a device object's does not.\n *\n * @param deviceId the id-safe device id\n * @returns the per-device cache\n */\n private async loadYncaSubunitCache(deviceId: string): Promise<YncaSubunitCache> {\n let stored: unknown;\n try {\n stored = (await this.getObjectAsync(deviceId))?.native?.yncaAvail;\n } catch {\n stored = undefined;\n }\n return createSubunitCache(isAvailSnapshot(stored) ? stored : undefined, snapshot => {\n void this.extendObject(deviceId, { native: { yncaAvail: snapshot ?? null } });\n });\n }\n\n /**\n * The XML/YNC poll interval in milliseconds, from `config.xmlPollInterval`\n * (seconds, default 60).\n *\n * @returns the interval in ms\n */\n private xmlPollIntervalMs(): number {\n const seconds = Number((this.config as unknown as Record<string, unknown>).xmlPollInterval);\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 60) * 1000;\n }\n\n /**\n * Run an SSDP M-SEARCH and collect the responders' description URL and address.\n *\n * With a configured network interface the search leaves exactly that one; left empty it\n * leaves EVERY non-internal IPv4 interface at once (one socket each), because multicast\n * egress otherwise follows only the host's default route \u2014 on a multi-homed host whose\n * default route is not the AV network that means the receiver is never reached and nothing\n * is found. Responders from all interfaces are merged into one list; the caller\n * de-duplicates by address.\n *\n * @param target the search target (device type)\n * @param timeoutMs how long to collect responses\n * @returns the responders\n */\n private ssdpSearch(target: string, timeoutMs: number): Promise<Array<{ location: string; address: string }>> {\n return new Promise(resolve => {\n const bindAddrs = searchInterfaces(this.config.networkInterface, networkInterfaces());\n const responders: Array<{ location: string; address: string }> = [];\n const sockets: ReturnType<typeof createSocket>[] = [];\n let settled = false;\n const finish = (): void => {\n if (settled) {\n return;\n }\n settled = true;\n for (const socket of sockets) {\n try {\n socket.close();\n } catch {\n // already closed\n }\n }\n resolve(responders);\n };\n // Open one search socket bound to a single interface (or the default route when bindAddr\n // is undefined). Every socket shares the responders list and the one settle timeout.\n const searchFrom = (bindAddr: string | undefined): void => {\n const socket = createSocket(\"udp4\");\n sockets.push(socket);\n socket.on(\"message\", (msg, rinfo) => {\n const location = /LOCATION:\\s*(\\S+)/i.exec(msg.toString());\n if (location) {\n responders.push({ location: location[1], address: rinfo.address });\n }\n });\n socket.on(\"error\", err => {\n // One interface failing (typically a stale selected IP after a DHCP change) must not\n // kill the search on the others \u2014 warn and drop just this socket; the timeout still\n // resolves whatever the rest found.\n this.log.warn(\n `discovery socket failed${bindAddr ? ` on interface ${bindAddr}` : \"\"}: ${errorMessage(err)}${\n bindAddr ? \" \u2014 check the Network Interface setting\" : \"\"\n }`,\n );\n try {\n socket.close();\n } catch {\n // already closed\n }\n });\n const sendSearch = (): void => {\n if (settled) {\n return;\n }\n const msearch = `M-SEARCH * HTTP/1.1\\r\\nHOST: 239.255.255.250:1900\\r\\nMAN: \"ssdp:discover\"\\r\\nMX: 3\\r\\nST: ${target}\\r\\n\\r\\n`;\n try {\n socket.send(msearch, 1900, \"239.255.255.250\");\n } catch {\n // socket already closed by an error above\n }\n };\n socket.bind(0, bindAddr, () => {\n // Pin OUTGOING multicast to this interface. bind() only sets the source address; the\n // egress interface is IP_MULTICAST_IF \u2014 without it the OS uses its default route, so\n // the search can leave the wrong NIC on a multi-homed host (Node dgram docs).\n if (bindAddr) {\n try {\n socket.setMulticastInterface(bindAddr);\n } catch {\n this.log.info(`discovery: could not pin multicast egress to ${bindAddr} \u2014 using the default interface`);\n }\n }\n // Multicast is lossy and a single request can be dropped \u2014 repeat the M-SEARCH a few\n // times inside the collect window so one lost packet does not hide a receiver.\n for (let i = 0; i < SSDP_SEARCH_BURST; i++) {\n this.setTimeout(sendSearch, i * SSDP_SEARCH_INTERVAL_MS);\n }\n });\n };\n // Configured \u2192 that one interface; empty \u2192 every non-internal IPv4; none usable \u2192 default route.\n if (bindAddrs.length === 0) {\n searchFrom(undefined);\n } else {\n for (const bindAddr of bindAddrs) {\n searchFrom(bindAddr);\n }\n }\n this.setTimeout(finish, timeoutMs);\n });\n }\n\n /**\n * Fetch a URL over HTTP and resolve its body.\n *\n * @param url the URL to fetch\n * @returns the response body\n */\n private fetchUrl(url: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const req = httpGet(url, res => {\n let data = \"\";\n res.on(\"data\", chunk => (data += String(chunk)));\n // A connection dropped mid-body emits on the RESPONSE stream, not the request \u2014\n // without this handler that is an unhandled error event instead of a rejection.\n res.on(\"error\", reject);\n res.on(\"end\", () => resolve(data));\n });\n req.on(\"error\", reject);\n req.setTimeout(FETCH_TIMEOUT_MS, () => req.destroy(new Error(`fetch timed out: ${url}`)));\n });\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new Yamaha(options);\n} else {\n // Start the instance directly\n (() => new Yamaha())();\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { createSocket } from \"node:dgram\";\nimport { get as httpGet } from \"node:http\";\nimport { networkInterfaces } from \"node:os\";\nimport { join } from \"node:path\";\nimport { attemptDevice } from \"./lib/attempt-device\";\nimport { searchInterfaces } from \"./lib/network-interfaces\";\nimport { isGroupEnabled } from \"./lib/catalog/groups\";\nimport { iconForModel } from \"./lib/device-type\";\nimport {\n LABEL_RANK,\n type LabelRank,\n legacyDeviceRow,\n mergeDiscovered,\n nextDeviceLabel,\n parseDevices,\n renamedObjectIds,\n staleObjects,\n stripNamespace,\n} from \"./lib/pure-helpers\";\nimport { errorMessage } from \"./lib/util\";\nimport { discoverYamaha } from \"./lib/discovery\";\nimport { readDiscovered, writeDiscovered } from \"./lib/discovered-store\";\nimport { discoveredStoreDeps } from \"./lib/discovered-store-deps\";\nimport { YxcPushReceiver } from \"./lib/yxc/push-receiver\";\nimport { YamahaDeviceManagement } from \"./device-management\";\nimport type { DeviceRecord } from \"./lib/types\";\nimport { DeviceSupervisor, type ConnectionHandle } from \"./lib/lifecycle/device-supervisor\";\nimport { ReconnectStrategy } from \"./lib/lifecycle/reconnect-strategy\";\nimport { ReachabilityDedup } from \"./lib/lifecycle/reachability-dedup\";\nimport { createSubunitCache, isAvailSnapshot, type YncaSubunitCache } from \"./lib/ynca/subunit-cache\";\nimport { ProbeMemory } from \"./lib/lifecycle/probe-memory\";\n\n/** Supervisor reconnect backoff bounds (exponential: 1s, 2s \u2026 capped at 60s). */\nconst RECONNECT_BASE_MS = 1000;\nconst RECONNECT_MAX_MS = 60000;\n\n/** Abort a discovery description fetch after this long, so a dead device cannot hang it. */\nconst FETCH_TIMEOUT_MS = 4000;\n\n/** How often the discovery M-SEARCH is repeated \u2014 multicast is lossy, one dropped packet must not hide a receiver. */\nconst SSDP_SEARCH_BURST = 3;\n/** Spacing between the repeated M-SEARCH sends, inside the collect window. */\nconst SSDP_SEARCH_INTERVAL_MS = 1000;\n\n/** The three transports in attempt order \u2014 also the per-transport `info.transports.*` state ids. */\nconst TRANSPORT_IDS = [\"ynca\", \"yxc\", \"xml\"] as const;\n\n/**\n * How long the datapoint balance waits for quiet before it logs. Devices connect\n * asynchronously and in parallel, so the line has to outlast the slowest of them.\n */\nconst DATAPOINT_BALANCE_SETTLE_MS = 5000;\n\n/**\n * ioBroker.yamaha \u2014 controls Yamaha AV receivers and MusicCast devices.\n *\n * Each configured device is driven by a supervisor that keeps a multi-transport\n * handle online: every protocol the device answers \u2014 YNCA (amp control over a held\n * TCP connection, event-pushed), YXC (MusicCast, push + poll), XML/YNC (pre-2010,\n * polled over HTTP) \u2014 connects in parallel on one object tree, each datapoint owned\n * by the best-fitting transport. All YXC devices share one UDP push receiver, keyed\n * by source IP.\n */\nexport class Yamaha extends utils.Adapter {\n private readonly supervisors: DeviceSupervisor[] = [];\n /** deviceId \u2192 its supervisor, so a state change goes to ONE device, not to all of them. */\n private readonly supervisorById = new Map<string, DeviceSupervisor>();\n private readonly deviceConnected = new Map<string, boolean>();\n private pushReceiver: YxcPushReceiver | undefined;\n /** Device-manager backend: the receivers as cards with add/edit/delete. */\n private readonly deviceManagement: YamahaDeviceManagement;\n /**\n * Every datapoint that existed when this run started, filled ONCE before the cleanup and\n * before any device connects. Without it the balance below would report the whole tree as\n * new on every restart: `upsertObject` runs `extendObject` on every state it touches (the\n * role/unit retrofit), so \"did the create path run?\" is not the same question as \"is this\n * datapoint new?\".\n */\n private readonly knownDatapoints = new Set<string>();\n private createdDatapoints = 0;\n private removedDatapoints = 0;\n /** Debounce for the balance line, so one config change produces ONE line, not one per device. */\n private balanceTimer: ioBroker.Timeout | undefined;\n /** Set when the start-up snapshot failed \u2014 a balance without it would be wrong, so none is written. */\n private balanceDisabled = false;\n\n /**\n * @param options adapter options passed through by js-controller\n */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: \"yamaha\",\n });\n\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"stateChange\", this.onStateChange.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n this.deviceManagement = new YamahaDeviceManagement(this);\n }\n\n /** Start a supervisor for each configured device, then subscribe to state changes. */\n private async onReady(): Promise<void> {\n // Load admin translations for the device-manager cards \u2014 a UI concern, in its own\n // guarded block so an i18n failure never aborts core device startup below.\n try {\n await utils.I18n.init(join(this.adapterDir, \"admin\"), this);\n } catch (e) {\n this.log.warn(`could not load admin translations (${errorMessage(e)}); card labels may be untranslated`);\n }\n try {\n this.log.info('starting \u2014 a \"ready\" message will follow for each device');\n await this.setState(\"info.connection\", { val: false, ack: true });\n await this.migrateLegacyDevice();\n await this.migrateGroupZones();\n // The device list is the switch: filled \u2192 use exactly those (manual); empty\n // \u2192 discover on the network and run what is found (auto). XML/pre-2010 devices\n // never answer SSDP, so they are always added manually.\n const configured = parseDevices(this.config.devices, (dropped, takenId) =>\n this.log.warn(`device \"${dropped}\" skipped \u2014 its object id \"${takenId}\" is already used by another device`),\n );\n const devices = configured.length > 0 ? configured : await this.autoDiscover();\n const knownDeviceIps = new Set(devices.map(device => device.ip));\n // Before the cleanup and before any device connects \u2014 see knownDatapoints.\n await this.snapshotExistingDatapoints();\n await this.cleanupStaleObjects(new Set(devices.map(device => device.id)));\n this.subscribeStates(\"*\");\n const pushReceiver = new YxcPushReceiver({\n log: { debug: message => this.log.debug(message), warn: message => this.log.warn(message) },\n schedule: (cb, ms) => this.setTimeout(cb, ms),\n cancel: handle => this.clearTimeout(handle),\n });\n pushReceiver.start();\n this.pushReceiver = pushReceiver;\n if (configured.length > 0) {\n this.log.info(`setting up ${devices.length} configured device(s)...`);\n }\n for (const device of devices) {\n this.deviceConnected.set(device.id, false);\n await this.ensureDeviceHeader(device.id);\n // Stamp it disconnected BEFORE the first attempt: ioBroker keeps a state's last value\n // forever, so a crash or a power cut would otherwise leave the device green until it\n // reports again \u2014 and a device that never answers would stay green for good.\n await this.setState(`${device.id}.info.connection`, { val: false, ack: true });\n const reachability = new ReachabilityDedup();\n const subunitCache = await this.loadYncaSubunitCache(device.id);\n // Held here, not in the controllers: those are rebuilt on every connection attempt.\n const probeMemory = new ProbeMemory();\n const supervisor = new DeviceSupervisor({\n attempt: () =>\n this.attemptDevice(device, pushReceiver, knownDeviceIps, reachability, subunitCache, probeMemory),\n schedule: (cb, ms) => this.setTimeout(cb, ms),\n cancel: handle => this.clearTimeout(handle as ioBroker.Timeout | undefined),\n onConnectionChange: connected => this.reportConnection(device.id, connected),\n backoff: new ReconnectStrategy(RECONNECT_BASE_MS, RECONNECT_MAX_MS),\n log: {\n debug: message => this.log.debug(message),\n info: message => this.log.info(message),\n warn: message => this.log.warn(message),\n },\n });\n this.supervisors.push(supervisor);\n this.supervisorById.set(device.id, supervisor);\n supervisor.start();\n }\n this.writeDeviceOverview();\n } catch (e) {\n this.log.error(`onReady failed: ${errorMessage(e)}`);\n }\n }\n\n /**\n * Aggregate one device's connection state into the adapter's `info.connection`\n * (true while at least one device is connected).\n *\n * @param deviceId the device reporting\n * @param connected whether that device is currently connected\n */\n private reportConnection(deviceId: string, connected: boolean): void {\n this.deviceConnected.set(deviceId, connected);\n void this.setState(`${deviceId}.info.connection`, { val: connected, ack: true });\n // A drop clears the per-transport flags; a (re)connect sets them again via onTransports.\n if (!connected) {\n this.setTransports(deviceId, []);\n }\n const anyConnected = [...this.deviceConnected.values()].some(Boolean);\n void this.setState(\"info.connection\", { val: anyConnected, ack: true });\n this.writeDeviceOverview();\n }\n\n /**\n * The three overview datapoints: how many devices this instance runs, how many are\n * connected right now, and whether that is all of them. Derived from the SAME map that\n * feeds the per-device markers and written in the same round \u2014 computed separately they\n * would drift away from what the single devices say.\n *\n * `devicesAllOnline` needs at least one device: zero of zero is not \"everything is fine\".\n */\n private writeDeviceOverview(): void {\n const total = this.deviceConnected.size;\n const online = [...this.deviceConnected.values()].filter(Boolean).length;\n void this.setState(\"info.devicesTotal\", { val: total, ack: true });\n void this.setState(\"info.devicesOnline\", { val: online, ack: true });\n void this.setState(\"info.devicesAllOnline\", { val: total > 0 && online === total, ack: true });\n }\n\n /**\n * Reflect the live transport set into a device's `info.transports.*` flags so the\n * device-manager card shows which protocols (YNCA/YXC/XML) are connected right now.\n *\n * @param deviceId the id-safe device id\n * @param names the transports live now (empty on a drop)\n */\n private setTransports(deviceId: string, names: string[]): void {\n const live = new Set(names);\n for (const proto of TRANSPORT_IDS) {\n void this.setState(`${deviceId}.info.transports.${proto}`, { val: live.has(proto), ack: true });\n }\n }\n\n /**\n * One-shot startup cleanup: delete every object that does not belong to a\n * configured device (the previous adapter's whole tree, and any device dropped\n * from the config). Runs before the devices connect; a configured device's\n * subtree is kept whether or not it has connected yet.\n *\n * @param deviceIds the ids of the currently configured devices\n */\n private async cleanupStaleObjects(deviceIds: Set<string>): Promise<void> {\n const allObjects = await this.getAdapterObjectsAsync();\n const existing = Object.keys(allObjects);\n const stale = staleObjects(existing, deviceIds, this.namespace);\n // Old states this version renamed/moved (e.g. system.model -> info.model): delete the\n // old object so it does not linger orphaned beside the new one under a kept device.\n const renamed = renamedObjectIds(existing, deviceIds, this.namespace);\n // Objects whose datapoint group the user switched off \u2014 remove them so turning a group from\n // on to off cleans up its whole subtree (a toggle change restarts the instance, so this runs).\n const config = this.config as unknown as Record<string, unknown>;\n const disabled = existing.filter(full => {\n for (const deviceId of deviceIds) {\n const base = `${this.namespace}.${deviceId}.`;\n if (full.startsWith(base) && !isGroupEnabled(full.slice(base.length), config)) {\n return true;\n }\n }\n return false;\n });\n for (const fullId of [...stale, ...renamed, ...disabled]) {\n try {\n await this.delObjectAsync(stripNamespace(fullId, this.namespace));\n } catch {\n // already removed together with its parent\n }\n }\n // The reasons stay available for diagnosis; the user sees ONE balance line instead of\n // three counts they have to add up themselves.\n if (stale.length > 0) {\n this.log.debug(`removed ${stale.length} object(s) from a previous configuration`);\n }\n if (renamed.length > 0) {\n this.log.debug(`removed ${renamed.length} renamed object(s) from an earlier version`);\n }\n if (disabled.length > 0) {\n this.log.debug(`removed ${disabled.length} object(s) from switched-off datapoint groups`);\n }\n // Channels and device nodes go with them, but only datapoints are counted \u2014 that is what\n // the user switched on or off, and what they look for in the object tree.\n this.noteDatapointsRemoved(\n [...stale, ...renamed, ...disabled].filter(fullId => allObjects[fullId]?.type === \"state\"),\n );\n }\n\n /**\n * Remember every datapoint that already exists, ONCE per adapter run.\n *\n * @see knownDatapoints for why the create path alone cannot answer \"is this new?\"\n */\n private async snapshotExistingDatapoints(): Promise<void> {\n try {\n for (const [fullId, object] of Object.entries(await this.getAdapterObjectsAsync())) {\n if (object?.type === \"state\") {\n this.knownDatapoints.add(stripNamespace(fullId, this.namespace));\n }\n }\n } catch (e) {\n // Without the snapshot the balance would call every datapoint new; better to stay\n // silent about it than to log a wrong number.\n this.log.debug(`could not read the existing datapoints (${errorMessage(e)}); balance line disabled`);\n this.balanceDisabled = true;\n }\n }\n\n /**\n * Count a datapoint the device tree just created \u2014 new ones only.\n *\n * @param id the state id relative to the namespace\n */\n private noteDatapointCreated(id: string): void {\n if (this.knownDatapoints.has(id)) {\n return;\n }\n this.knownDatapoints.add(id);\n this.createdDatapoints++;\n this.scheduleDatapointBalance();\n }\n\n /**\n * Count removed datapoints, and let them count again should they ever come back.\n *\n * @param fullIds the removed ids, namespace included\n */\n private noteDatapointsRemoved(fullIds: readonly string[]): void {\n for (const fullId of fullIds) {\n this.knownDatapoints.delete(stripNamespace(fullId, this.namespace));\n this.removedDatapoints++;\n }\n if (fullIds.length > 0) {\n this.scheduleDatapointBalance();\n }\n }\n\n /**\n * Log the balance once the tree has settled. A device connects asynchronously and several\n * devices connect at once, so the line waits for quiet instead of firing per device \u2014 the\n * user made ONE change and reads ONE result.\n */\n private scheduleDatapointBalance(): void {\n if (this.balanceDisabled) {\n return;\n }\n this.clearTimeout(this.balanceTimer);\n this.balanceTimer = this.setTimeout(() => {\n this.balanceTimer = undefined;\n const parts: string[] = [];\n if (this.createdDatapoints > 0) {\n parts.push(`created ${this.createdDatapoints} datapoint(s)`);\n }\n if (this.removedDatapoints > 0) {\n parts.push(`removed ${this.removedDatapoints} datapoint(s)`);\n }\n this.createdDatapoints = 0;\n this.removedDatapoints = 0;\n // Silent when nothing changed: a plain restart must not write a line.\n if (parts.length > 0) {\n this.log.info(`Object tree updated: ${parts.join(\", \")}`);\n }\n }, DATAPOINT_BALANCE_SETTLE_MS);\n }\n\n /**\n * Create a device's header objects (the device node, its info channel and a\n * per-device connection indicator) so its state is visible even while offline.\n *\n * @param deviceId the id-safe device id\n */\n private async ensureDeviceHeader(deviceId: string): Promise<void> {\n // statusStates.onlineId lets the admin paint a green/red reachability symbol on the\n // device object itself (as govee does), fed by the per-device connection state.\n // extendObject with preserve:name so an upgrade adds the symbol without overwriting\n // a name the user changed.\n // A device that has not reported its model yet would sit in the tree without any\n // symbol \u2014 an upgraded instance shows that on every start before the first report,\n // and a device that never answers shows it for good. Seed the default silhouette,\n // but only when there is none: overwriting would flip a soundbar back to the\n // receiver default for the seconds until its model arrives.\n let icon: string | undefined;\n try {\n const existing = await this.getObjectAsync(deviceId);\n icon = existing?.common?.icon ? undefined : iconForModel(undefined);\n } catch {\n icon = undefined;\n }\n await this.extendObject(\n deviceId,\n {\n type: \"device\",\n common: {\n name: deviceId,\n ...(icon ? { icon } : {}),\n statusStates: { onlineId: `${this.namespace}.${deviceId}.info.connection` },\n },\n native: {},\n },\n { preserve: { common: [\"name\"] } },\n );\n await this.setObjectNotExistsAsync(`${deviceId}.info`, { type: \"channel\", common: { name: \"Info\" }, native: {} });\n await this.setObjectNotExistsAsync(`${deviceId}.info.connection`, {\n type: \"state\",\n common: { name: \"Connected\", type: \"boolean\", role: \"indicator.reachable\", read: true, write: false, def: false },\n native: {},\n });\n // Model name shown on the device-manager card. Filled by whichever transport reports it\n // (YNCA MODELNAME, YXC/XML model); created here so the card's model line binds even for an\n // offline device or a transport that does not report a model.\n await this.setObjectNotExistsAsync(`${deviceId}.info.model`, {\n type: \"state\",\n common: { name: \"Model\", type: \"string\", role: \"text\", read: true, write: false, def: \"\" },\n native: {},\n });\n // Per-transport connection flags, fed by the live set from connectTransports and read live\n // by the device-manager card indicators. Created here so an offline device's card still\n // renders all three (false) instead of nothing.\n await this.setObjectNotExistsAsync(`${deviceId}.info.transports`, {\n type: \"channel\",\n common: { name: \"Transports\" },\n native: {},\n });\n for (const proto of TRANSPORT_IDS) {\n await this.setObjectNotExistsAsync(`${deviceId}.info.transports.${proto}`, {\n type: \"state\",\n common: {\n name: `${proto.toUpperCase()} connected`,\n type: \"boolean\",\n role: \"indicator.reachable\",\n read: true,\n write: false,\n def: false,\n },\n native: {},\n });\n }\n }\n\n /** The icon last written per device, so repeated model reports do not re-write the object. */\n private readonly deviceIcons = new Map<string, string>();\n\n /** The label this adapter wrote per device, with the rank of the source behind it. */\n private readonly deviceLabels = new Map<string, { name: string; rank: LabelRank }>();\n\n /**\n * Give the device node a name a user recognises, once the device reports one.\n *\n * An instance upgraded from the previous adapter carries the receiver's ip as its\n * device name \u2014 that adapter knew nothing but an ip, so the migration had nothing\n * else to call it. The object id stays that ip for good (history and visualisation\n * bindings hang off it), but the displayed name does not have to.\n *\n * A name the user typed is never touched, and the model never replaces a name the\n * device reported for itself \u2014 see {@link nextDeviceLabel}.\n *\n * @param deviceId the id-safe device id\n * @param candidate the reported name (a MusicCast zone name, or the model)\n * @param rank how trustworthy the candidate is\n */\n private async updateDeviceLabel(deviceId: string, candidate: string, rank: LabelRank): Promise<void> {\n const own = this.deviceLabels.get(deviceId);\n try {\n const current = (await this.getObjectAsync(deviceId))?.common?.name;\n const label = nextDeviceLabel(\n typeof current === \"string\" ? current : undefined,\n deviceId,\n candidate,\n rank,\n own?.name,\n own?.rank,\n );\n if (label === undefined) {\n return;\n }\n // Deliberately without `preserve: { common: [\"name\"] }`: nextDeviceLabel has just\n // established that the present name is the adapter's own placeholder, not a user's.\n await this.extendObject(deviceId, { common: { name: label } });\n this.deviceLabels.set(deviceId, { name: label, rank });\n this.log.debug(`${deviceId}: device name set to \"${label}\"`);\n } catch (e) {\n this.log.debug(`${deviceId}: setting the device name failed (${errorMessage(e)})`);\n }\n }\n\n /**\n * Paint the device-class silhouette on the device node once the model is known \u2014\n * detected from the reported model name, written only when it actually changes.\n *\n * @param deviceId the id-safe device id\n * @param model the reported model name\n */\n private async updateDeviceIcon(deviceId: string, model: string): Promise<void> {\n const icon = iconForModel(model);\n if (this.deviceIcons.get(deviceId) === icon) {\n return;\n }\n this.deviceIcons.set(deviceId, icon);\n try {\n await this.extendObject(deviceId, { common: { icon } });\n } catch (e) {\n this.log.debug(`${deviceId}: setting device icon failed (${errorMessage(e)})`);\n }\n }\n\n /**\n * Carry over the previous adapter's single-device config into the device table.\n * The old yamaha stored one receiver as `config.ip` (older installs: `config.IP`);\n * the new adapter uses a `devices` table, so an upgraded instance would otherwise\n * start with an empty table and lose its receiver. Persists the row so the admin\n * table shows it, and fills `this.config` in memory so this run already drives it.\n */\n private async migrateLegacyDevice(): Promise<void> {\n const config = this.config as unknown as Record<string, unknown>;\n const row = legacyDeviceRow(config);\n if (!row) {\n return;\n }\n // Fill the in-memory config first, so this run already drives the device even\n // if persisting the table below fails \u2014 persistence is a convenience for the\n // admin view, not a precondition for running.\n config.devices = [row];\n try {\n await this.extendForeignObjectAsync(`system.adapter.${this.namespace}`, { native: { devices: [row] } });\n this.log.info(`carried the previous single-device config (${row.ip}) over into the device table`);\n } catch (e) {\n this.log.warn(\n `could not persist the migrated device table (${errorMessage(e)}); ` + `running with the in-memory value`,\n );\n }\n }\n\n /**\n * Fold the removed `group_zones` toggle into `group_multiroom` \u2014 zone 2/3/4 now\n * belong to the multiroom group. Existing installs that had zones on but multiroom\n * off would otherwise lose their zone datapoints after the update.\n */\n private async migrateGroupZones(): Promise<void> {\n const config = this.config as unknown as Record<string, unknown>;\n if (!(\"group_zones\" in config)) {\n return;\n }\n // (`!group_multiroom` only reads as a guard \u2014 the assignment sets it to true\n // either way, so an already-on multiroom group is not changed by it.)\n if (config.group_zones && !config.group_multiroom) {\n config.group_multiroom = true;\n }\n delete config.group_zones;\n try {\n const obj = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`);\n if (obj?.native) {\n if (obj.native.group_zones && !obj.native.group_multiroom) {\n obj.native.group_multiroom = true;\n }\n delete obj.native.group_zones;\n await this.setForeignObjectAsync(`system.adapter.${this.namespace}`, obj);\n this.log.info(\"migrated group_zones setting into group_multiroom\");\n }\n } catch (e) {\n this.log.warn(`could not persist group_zones migration (${errorMessage(e)})`);\n }\n }\n\n /**\n * Bring one device online across ALL its transports: every protocol that answers\n * \u2014 YNCA (amp control over a held TCP connection), YXC (MusicCast), XML/YNC\n * (pre-2010) \u2014 connects in parallel on one object tree. Returns a connection handle\n * the supervisor keeps, or null when no transport answers this attempt. Each\n * datapoint is owned by exactly one transport (owner-policy), so the mappers never\n * collide on a shared id.\n *\n * @param device the configured device record\n * @param pushReceiver the shared YXC push receiver\n * @param knownDeviceIps IPs of all configured devices, for resolving a multiroom client\n * @param reachability dedup for the \"no reachable transport\" warning (one instance per device,\n * held by the caller across retries \u2014 see {@link ReachabilityDedup})\n * @param yncaSubunitCache per-device cache of the YNCA AVAIL probe (skips the probe on reconnects)\n * @param probeMemory per-device memory for constant device answers (skips re-asking on reconnects)\n * @returns a connection handle, or null when no transport connected\n */\n private attemptDevice(\n device: DeviceRecord,\n pushReceiver: YxcPushReceiver,\n knownDeviceIps: Set<string>,\n reachability: ReachabilityDedup,\n yncaSubunitCache: YncaSubunitCache,\n probeMemory: ProbeMemory,\n ): Promise<ConnectionHandle | null> {\n return attemptDevice(device, {\n reachability,\n yncaSubunitCache,\n probeMemory,\n // Group gate for the YNCA sweep: a disabled group's functions are never even fetched.\n isEntryEnabled: id => isGroupEnabled(id, this.config as unknown as Record<string, unknown>),\n log: {\n debug: message => this.log.debug(message),\n info: message => this.log.info(message),\n warn: message => this.log.warn(message),\n },\n upsertObject: async (id, def) => {\n // Gate on the datapoint group: a switched-off group's objects are not created. The id is\n // \"<deviceId>.<relativeId>\"; groupOf reads the relative part.\n if (!isGroupEnabled(id.slice(id.indexOf(\".\") + 1), this.config as unknown as Record<string, unknown>)) {\n return;\n }\n await this.extendObject(id, { type: def.type, common: def.common, native: {} });\n if (def.type === \"state\") {\n this.noteDatapointCreated(id);\n }\n },\n setStateAck: (id, value) => {\n // Same group gate as upsertObject, so a switched-off group seeds no orphan value either.\n if (!isGroupEnabled(id.slice(id.indexOf(\".\") + 1), this.config as unknown as Record<string, unknown>)) {\n return;\n }\n void this.setState(id, { val: value, ack: true });\n // A model report also decides the device-class icon on the device node \u2014 and, for a\n // device still carrying the ip it was migrated with, its readable name.\n if (id.endsWith(\".info.model\") && typeof value === \"string\" && value.length > 0) {\n const reporting = id.slice(0, id.indexOf(\".\"));\n void this.updateDeviceIcon(reporting, value);\n void this.updateDeviceLabel(reporting, value, LABEL_RANK.model);\n }\n },\n onDeviceName: name => void this.updateDeviceLabel(device.id, name, LABEL_RANK.deviceName),\n timers: {\n schedule: (handler, ms) => this.setTimeout(handler, ms),\n cancel: handle => this.clearTimeout(handle),\n },\n registerPush: (ip, onPush) => pushReceiver.register(ip, onPush),\n pushActive: () => pushReceiver.isListening(),\n scheduleKeepalive: (handler, ms) => {\n const timer = this.setInterval(handler, ms);\n return () => {\n if (timer) {\n this.clearInterval(timer);\n }\n };\n },\n xmlPollIntervalMs: this.xmlPollIntervalMs(),\n onTransports: names => this.setTransports(device.id, names),\n knownDeviceIps,\n });\n }\n\n /**\n * Route a state change to every device's supervisor (each forwards to its\n * active controller, which ignores ids outside its subtree and its acked echoes).\n *\n * @param id the full state id\n * @param state the new state (null when deleted)\n */\n private onStateChange(id: string, state: ioBroker.State | null | undefined): void {\n if (!state) {\n return;\n }\n const relative = stripNamespace(id, this.namespace);\n // The adapter subscribes to its whole namespace, so every one of its own acked writes\n // comes back here too \u2014 during a sweep that is hundreds of events. Route by the id's\n // first segment instead of offering each one to every device in turn.\n const deviceId = relative.slice(0, relative.indexOf(\".\"));\n this.supervisorById.get(deviceId)?.handleStateChange(relative, state.ack, state.val);\n }\n\n /**\n * Synchronous teardown \u2014 no await, call the callback immediately (SIGKILL otherwise).\n *\n * @param callback function to invoke once teardown is complete\n */\n private onUnload(callback: () => void): void {\n try {\n this.clearTimeout(this.balanceTimer);\n this.pushReceiver?.close();\n for (const supervisor of this.supervisors) {\n supervisor.close();\n }\n // A stopped adapter talks to nothing, so no device may keep claiming to be connected \u2014\n // that state paints the symbol on the device object (statusStates.onlineId), and the\n // instance-wide info.connection alone would leave every device green. The overview goes\n // with them; devicesTotal stays, how many devices there are did not change.\n //\n // The callback goes LAST, after the writes: reporting \"done\" straight away loses them,\n // the host tears the process down as soon as it is told.\n const writes: Promise<unknown>[] = [this.setState(\"info.connection\", { val: false, ack: true })];\n for (const deviceId of this.deviceConnected.keys()) {\n this.deviceConnected.set(deviceId, false);\n writes.push(this.setState(`${deviceId}.info.connection`, { val: false, ack: true }));\n }\n writes.push(this.setState(\"info.devicesOnline\", { val: 0, ack: true }));\n writes.push(this.setState(\"info.devicesAllOnline\", { val: false, ack: true }));\n void Promise.all(writes)\n .catch(() => {\n /* states DB already going down \u2014 nothing left to report to */\n })\n .finally(callback);\n return;\n } catch {\n // fall through\n }\n callback();\n }\n\n /**\n * Auto-discovery for an empty device table: scan the network, merge the finds with\n * the devices remembered from earlier runs (standby protection), persist the merged\n * set and return it. XML/pre-2010 receivers do not answer SSDP and never appear here.\n *\n * @returns the device records to run this session\n */\n private async autoDiscover(): Promise<DeviceRecord[]> {\n const store = discoveredStoreDeps(this);\n const known = await readDiscovered(store);\n this.log.info(\"auto-discovery via SSDP (older XML-only devices must be added manually)\");\n let found: Array<{ ip: string; name: string }> = [];\n try {\n found = await discoverYamaha({\n search: (target, ms) => this.ssdpSearch(target, ms),\n fetch: url => this.fetchUrl(url),\n log: { debug: message => this.log.debug(message), warn: message => this.log.warn(message) },\n });\n } catch (e) {\n this.log.warn(`auto-discovery scan failed, using the remembered devices: ${errorMessage(e)}`);\n }\n const merged = mergeDiscovered(known, found, (dropped, takenId) =>\n this.log.warn(`discovered device \"${dropped}\" skipped \u2014 its object id \"${takenId}\" is already taken`),\n );\n await writeDiscovered(store, merged);\n const remembered = merged.length - found.length;\n const suffix = remembered > 0 ? ` (${remembered} more remembered from a previous run)` : \"\";\n this.log.info(`setting up ${merged.length} discovered device(s)${suffix}...`);\n return merged;\n }\n\n /**\n * Load a device's persisted YNCA subunit-cache (the AVAIL probe result) from its\n * device object's native part, wrapped so updates persist back there. The device\n * object is the right home: writing an instance object's native restarts the\n * adapter, a device object's does not.\n *\n * @param deviceId the id-safe device id\n * @returns the per-device cache\n */\n private async loadYncaSubunitCache(deviceId: string): Promise<YncaSubunitCache> {\n let stored: unknown;\n try {\n stored = (await this.getObjectAsync(deviceId))?.native?.yncaAvail;\n } catch {\n stored = undefined;\n }\n return createSubunitCache(isAvailSnapshot(stored) ? stored : undefined, snapshot => {\n void this.extendObject(deviceId, { native: { yncaAvail: snapshot ?? null } });\n });\n }\n\n /**\n * The XML/YNC poll interval in milliseconds, from `config.xmlPollInterval`\n * (seconds, default 60).\n *\n * @returns the interval in ms\n */\n private xmlPollIntervalMs(): number {\n const seconds = Number((this.config as unknown as Record<string, unknown>).xmlPollInterval);\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 60) * 1000;\n }\n\n /**\n * Run an SSDP M-SEARCH and collect the responders' description URL and address.\n *\n * With a configured network interface the search leaves exactly that one; left empty it\n * leaves EVERY non-internal IPv4 interface at once (one socket each), because multicast\n * egress otherwise follows only the host's default route \u2014 on a multi-homed host whose\n * default route is not the AV network that means the receiver is never reached and nothing\n * is found. Responders from all interfaces are merged into one list; the caller\n * de-duplicates by address.\n *\n * @param target the search target (device type)\n * @param timeoutMs how long to collect responses\n * @returns the responders\n */\n private ssdpSearch(target: string, timeoutMs: number): Promise<Array<{ location: string; address: string }>> {\n return new Promise(resolve => {\n const bindAddrs = searchInterfaces(this.config.networkInterface, networkInterfaces());\n const responders: Array<{ location: string; address: string }> = [];\n const sockets: ReturnType<typeof createSocket>[] = [];\n let settled = false;\n const finish = (): void => {\n if (settled) {\n return;\n }\n settled = true;\n for (const socket of sockets) {\n try {\n socket.close();\n } catch {\n // already closed\n }\n }\n resolve(responders);\n };\n // Open one search socket bound to a single interface (or the default route when bindAddr\n // is undefined). Every socket shares the responders list and the one settle timeout.\n const searchFrom = (bindAddr: string | undefined): void => {\n const socket = createSocket(\"udp4\");\n sockets.push(socket);\n socket.on(\"message\", (msg, rinfo) => {\n const location = /LOCATION:\\s*(\\S+)/i.exec(msg.toString());\n if (location) {\n responders.push({ location: location[1], address: rinfo.address });\n }\n });\n socket.on(\"error\", err => {\n // One interface failing (typically a stale selected IP after a DHCP change) must not\n // kill the search on the others \u2014 warn and drop just this socket; the timeout still\n // resolves whatever the rest found.\n this.log.warn(\n `discovery socket failed${bindAddr ? ` on interface ${bindAddr}` : \"\"}: ${errorMessage(err)}${\n bindAddr ? \" \u2014 check the Network Interface setting\" : \"\"\n }`,\n );\n try {\n socket.close();\n } catch {\n // already closed\n }\n });\n const sendSearch = (): void => {\n if (settled) {\n return;\n }\n const msearch = `M-SEARCH * HTTP/1.1\\r\\nHOST: 239.255.255.250:1900\\r\\nMAN: \"ssdp:discover\"\\r\\nMX: 3\\r\\nST: ${target}\\r\\n\\r\\n`;\n try {\n socket.send(msearch, 1900, \"239.255.255.250\");\n } catch {\n // socket already closed by an error above\n }\n };\n socket.bind(0, bindAddr, () => {\n // Pin OUTGOING multicast to this interface. bind() only sets the source address; the\n // egress interface is IP_MULTICAST_IF \u2014 without it the OS uses its default route, so\n // the search can leave the wrong NIC on a multi-homed host (Node dgram docs).\n if (bindAddr) {\n try {\n socket.setMulticastInterface(bindAddr);\n } catch {\n this.log.info(`discovery: could not pin multicast egress to ${bindAddr} \u2014 using the default interface`);\n }\n }\n // Multicast is lossy and a single request can be dropped \u2014 repeat the M-SEARCH a few\n // times inside the collect window so one lost packet does not hide a receiver.\n for (let i = 0; i < SSDP_SEARCH_BURST; i++) {\n this.setTimeout(sendSearch, i * SSDP_SEARCH_INTERVAL_MS);\n }\n });\n };\n // Configured \u2192 that one interface; empty \u2192 every non-internal IPv4; none usable \u2192 default route.\n if (bindAddrs.length === 0) {\n searchFrom(undefined);\n } else {\n for (const bindAddr of bindAddrs) {\n searchFrom(bindAddr);\n }\n }\n this.setTimeout(finish, timeoutMs);\n });\n }\n\n /**\n * Fetch a URL over HTTP and resolve its body.\n *\n * @param url the URL to fetch\n * @returns the response body\n */\n private fetchUrl(url: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const req = httpGet(url, res => {\n let data = \"\";\n res.on(\"data\", chunk => (data += String(chunk)));\n // A connection dropped mid-body emits on the RESPONSE stream, not the request \u2014\n // without this handler that is an unhandled error event instead of a rejection.\n res.on(\"error\", reject);\n res.on(\"end\", () => resolve(data));\n });\n req.on(\"error\", reject);\n req.setTimeout(FETCH_TIMEOUT_MS, () => req.destroy(new Error(`fetch timed out: ${url}`)));\n });\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new Yamaha(options);\n} else {\n // Start the instance directly\n (() => new Yamaha())();\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["httpGet"]
7
7
  }
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "yamaha",
4
- "version": "1.4.0",
4
+ "version": "1.6.0",
5
5
  "news": {
6
+ "1.6.0": {
7
+ "en": "Three new states show how many receivers are set up, how many are connected and whether that is all of them; a stopped adapter no longer shows a receiver as connected.",
8
+ "de": "Drei neue Datenpunkte zeigen, wie viele Receiver eingerichtet sind, wie viele verbunden sind und ob das alle sind; eine gestoppte Instanz zeigt keinen Receiver mehr als verbunden.",
9
+ "ru": "Три новых состояния показывают, сколько ресиверов настроено, сколько подключено и все ли это; остановленный адаптер больше не показывает ресивер подключённым.",
10
+ "pt": "Três novos estados mostram quantos receptores estão configurados, quantos estão ligados e se são todos; um adaptador parado deixa de mostrar um receptor como ligado.",
11
+ "nl": "Drie nieuwe datapunten tonen hoeveel receivers zijn ingesteld, hoeveel verbonden zijn en of dat alle zijn; een gestopte adapter toont geen receiver meer als verbonden.",
12
+ "fr": "Trois nouveaux états indiquent combien d'amplis sont configurés, combien sont connectés et si ce sont tous ; un adaptateur arrêté n'en affiche plus aucun comme connecté.",
13
+ "it": "Tre nuovi stati mostrano quanti sintoamplificatori sono configurati, quanti sono connessi e se sono tutti; un adapter fermo non ne mostra più nessuno come connesso.",
14
+ "es": "Tres nuevos estados muestran cuántos receptores están configurados, cuántos están conectados y si son todos; un adaptador detenido ya no muestra ninguno como conectado.",
15
+ "pl": "Trzy nowe stany pokazują, ile amplitunerów skonfigurowano, ile jest połączonych i czy to wszystkie; zatrzymany adapter nie pokazuje już żadnego jako połączonego.",
16
+ "uk": "Три нові стани показують, скільки ресиверів налаштовано, скільки під'єднано і чи це всі; зупинений адаптер більше не показує ресивер під'єднаним.",
17
+ "zh-cn": "三个新数据点显示已配置多少台功放、当前有多少台已连接以及是否全部连接;已停止的适配器不再把功放显示为已连接。"
18
+ },
19
+ "1.5.0": {
20
+ "en": "Fixed: menu browsing on receivers that serve their menus over the old XML protocol (#613)\nFixed: a MusicCast device that loses power is reported as offline instead of still showing a connection\nSwitching a datapoint group on or off now shows how many datapoints appeared or disappeared\nNew: optional error reporting via Sentry, without any personal data",
21
+ "de": "Behoben: Menü-Browsing bei Receivern, die ihr Menü über das alte XML-Protokoll liefern (#613)\nBehoben: Ein stromlos gewordenes MusicCast-Gerät wird als offline gemeldet statt weiter als verbunden\nBeim Umschalten einer Datenpunktgruppe siehst du jetzt, wie viele Datenpunkte dazugekommen oder weggefallen sind\nNeu: Optionale Fehlerberichte über Sentry, ohne persönliche Daten",
22
+ "ru": "Исправлено: навигация по меню на ресиверах, которые отдают меню по старому протоколу XML (#613)\nИсправлено: обесточенное устройство MusicCast сообщается как отключённое, а не как подключённое\nПри переключении группы точек данных теперь видно, сколько точек добавилось или исчезло\nНовое: необязательная отправка отчётов об ошибках через Sentry, без персональных данных",
23
+ "pt": "Corrigido: navegação de menus em recetores que servem o menu pelo antigo protocolo XML (#613)\nCorrigido: um dispositivo MusicCast sem energia é indicado como offline em vez de continuar ligado\nAo ativar ou desativar um grupo de pontos de dados vê agora quantos pontos surgiram ou desapareceram\nNovo: relatório de erros opcional via Sentry, sem dados pessoais",
24
+ "nl": "Opgelost: menunavigatie op ontvangers die hun menu via het oude XML-protocol aanbieden (#613)\nOpgelost: een MusicCast-apparaat zonder stroom wordt als offline gemeld in plaats van nog verbonden\nBij het in- of uitschakelen van een datapuntgroep zie je nu hoeveel datapunten erbij kwamen of verdwenen\nNieuw: optionele foutrapportage via Sentry, zonder persoonsgegevens",
25
+ "fr": "Corrigé : navigation dans les menus des amplis qui servent leur menu via l'ancien protocole XML (#613)\nCorrigé : un appareil MusicCast hors tension est signalé hors ligne au lieu de rester connecté\nEn activant ou désactivant un groupe de points de données, vous voyez désormais combien de points sont apparus ou ont disparu\nNouveau : rapport d'erreurs optionnel via Sentry, sans données personnelles",
26
+ "it": "Corretto: navigazione dei menu sui ricevitori che li offrono tramite il vecchio protocollo XML (#613)\nCorretto: un dispositivo MusicCast senza corrente viene segnalato offline invece che ancora connesso\nAttivando o disattivando un gruppo di datapoint ora vedi quanti datapoint sono comparsi o spariti\nNovità: segnalazione errori opzionale tramite Sentry, senza dati personali",
27
+ "es": "Corregido: navegación por menús en receptores que los sirven por el antiguo protocolo XML (#613)\nCorregido: un dispositivo MusicCast sin corriente se informa como desconectado en lugar de conectado\nAl activar o desactivar un grupo de puntos de datos ahora ves cuántos puntos aparecieron o desaparecieron\nNuevo: informe de errores opcional mediante Sentry, sin datos personales",
28
+ "pl": "Poprawiono: przeglądanie menu w amplitunerach udostępniających je starym protokołem XML (#613)\nPoprawiono: pozbawione zasilania urządzenie MusicCast jest zgłaszane jako offline, a nie nadal połączone\nPo włączeniu lub wyłączeniu grupy punktów danych widzisz teraz, ile punktów przybyło lub zniknęło\nNowość: opcjonalne raportowanie błędów przez Sentry, bez danych osobowych",
29
+ "uk": "Виправлено: навігація меню на ресиверах, які надають меню старим протоколом XML (#613)\nВиправлено: знеструмлений пристрій MusicCast повідомляється як офлайн, а не як підключений\nПісля вмикання чи вимикання групи точок даних тепер видно, скільки точок з'явилося або зникло\nНове: необов'язкові звіти про помилки через Sentry, без персональних даних",
30
+ "zh-cn": "修复:可通过旧版 XML 协议提供菜单的功放现在可以浏览菜单 (#613)\n修复:断电的 MusicCast 设备会报告为离线,而不再显示为已连接\n开启或关闭数据点分组后,现在可以看到新增或消失了多少数据点\n新增:可选的 Sentry 错误报告,不传输个人数据"
31
+ },
6
32
  "1.4.0": {
7
33
  "en": "Commands sent in one go all arrive, favourites land in the zone that is listening, special characters work again, and startup and reconnects are noticeably faster.",
8
34
  "de": "Mehrere Befehle auf einmal kommen alle an, Favoriten landen in der hörenden Zone, Sonderzeichen funktionieren wieder, und Start und Neuverbindung sind spürbar schneller.",
@@ -67,32 +93,6 @@
67
93
  "pl": "Całkowita przebudowa: jeden adapter obsługuje teraz protokoły YNCA, MusicCast i starszy protokół XML — każdy protokół, na który odpowiada urządzenie, działa równolegle na jednym drzewie obiektów.\nNowe drzewo obiektów z typizowanymi punktami danych zbudowanymi na podstawie danych raportowanych przez urządzenie. Stare punkty danych są automatycznie usuwane, a adres jest przenoszony — skrypty punktów znajdują się na nowych ścieżkach.\nNatychmiastowe aktualizacje: zdarzenia push MusicCast i połączenie YNCA na żywo zastępują sondowanie; połączenia naprawiają się same, a awaria jednego protokołu powoduje ponowne nawiązanie połączenia tylko dla tego protokołu.\nFunkcja automatycznego wykrywania konfiguruje urządzenia MusicCast automatycznie, gdy lista urządzeń jest pusta, a administrator wyświetla każdy odbiornik jako kartę ze wskaźnikami modelu, adresu i protokołu.\nCałe grupy punktów danych, takie jak źródła odtwarzania, tuner, funkcje multiroom czy sceny, można wyłączyć w panelu administracyjnym — wówczas nie będą one nawet wysyłane do urządzenia.\nFolder multiroom zawiera wszystkie informacje o zakresie funkcji: przełączniki, które mają wpływ na wszystkie strefy, mają taką informację w swojej nazwie, a grupa urządzeń MusicCast ma własny folder `multiroom.group`.\nKażde urządzenie wyświetla ikonę typu — odbiornik, odbiornik stereo, głośnik, soundbar lub system CD wykryty na podstawie zgłoszonego modelu — w drzewie obiektów i na karcie administratora; logo adaptera jest teraz czytelne w trybie jasnym i ciemnym.\nPodczas aktualizacji z wersji 0.5.x przed zainstalowaniem aktualizacji wyświetlany jest jednorazowy komunikat objaśniający nowe drzewo obiektów.\nwersja została przebudowana z powodu problemów z wdrożeniem",
68
94
  "uk": "Повна перебудова: один адаптер тепер підтримує YNCA, MusicCast та застарілий протокол XML — кожен протокол, якому відповідає пристрій, працює паралельно в одному дереві об'єктів.\nНове дерево об'єктів з типізованими точками даних, побудоване на основі даних вашого пристрою. Старі точки даних видаляються автоматично, адреса переноситься — скрипти вказують на нові шляхи.\nМиттєві оновлення: push-події MusicCast та пряме з’єднання YNCA замінюють опитування; з’єднання відновлюються самостійно, а збій одного протоколу повторно підключається саме до цього протоколу.\nАвтоматичне виявлення налаштовує пристрої MusicCast самостійно, коли список пристроїв порожній, а адміністратор показує кожен ресивер у вигляді картки з індикаторами моделі, адреси та протоколу.\nЦілі групи точок даних, такі як джерела відтворення, тюнер, мультирум або сцени, можна вимкнути в адміністративній панелі — і тоді вони навіть не запитуватимуться з пристрою.\nПапка multiroom одразу показує область застосування: перемикачі, які впливають на всі зони, вказують на це у своїй назві, а група пристроїв MusicCast має власну папку `multiroom.group`.\nКожен пристрій відображає значок типу — ресивер, стереоресивер, динамік, саундбар або CD-система, виявлена за даними моделі — у дереві об’єктів та на картці адміністратора; логотип адаптера тепер залишається читабельним у світлому та темному режимі.\nОновлення з версії 0.5.x показує одноразове повідомлення з поясненням нового дерева об'єктів перед встановленням оновлення.\nВерсію було перебудовано через проблеми з розгортанням",
69
95
  "zh-cn": "完全重建:现在一个适配器可以支持 YNCA、MusicCast 和传统的 XML 协议——设备响应的每个协议都在同一个对象树上并行运行。\n根据设备报告的数据,构建包含类型化数据点的新对象树。旧数据点会自动移除,地址会被保留——将脚本指向新路径即可。\n即时更新:MusicCast 推送事件和 YNCA 实时连接取代了轮询;连接会自动修复,一个协议出现故障时,只会重新连接该协议。\n当设备列表为空时,自动发现功能会自动设置 MusicCast 设备,管理员会将每个接收器显示为一张卡片,卡片上会显示型号、地址和协议指示符。\n可以在管理后台关闭整个数据点组,例如播放源、调谐器、多房间或场景——这样设备甚至都不会查询这些数据点组。\n多房间文件夹可以一目了然地显示其范围:影响所有区域的开关在其名称中会注明这一点,而 MusicCast 设备组有自己的 `multiroom.group` 文件夹。\n每个设备在对象树及其管理卡上都会显示一个类型图标——接收器、立体声接收器、扬声器、条形音箱或 CD 系统(根据报告的型号检测);适配器徽标现在在浅色和深色模式下都清晰可见。\n从 0.5.x 升级时,会在更新安装之前显示一次性通知,解释新的对象树。\n由于部署问题,版本已重新构建。"
70
- },
71
- "0.5.4": {
72
- "en": "updated packages",
73
- "de": "aktualisierte pakete",
74
- "ru": "обновления пакетов",
75
- "pt": "pacotes atualizados",
76
- "nl": "bijgewerkte pakketten",
77
- "fr": "paquets mis à jour",
78
- "it": "pacchetti aggiornati",
79
- "es": "paquetes actualizados",
80
- "pl": "zaktualizowane pakiety",
81
- "uk": "оновлені пакети",
82
- "zh-cn": "更新软件包"
83
- },
84
- "0.5.3": {
85
- "en": "Fix crash cases reported by Sentry",
86
- "de": "Reparieren Crashfälle gemeldet von Sentry",
87
- "ru": "Исправление случаев аварии, описанных Sentry",
88
- "pt": "Corrigir casos de acidente relatados por Sentry",
89
- "nl": "Vertaling:",
90
- "fr": "Troubles de crash signalés par Sentry",
91
- "it": "Correzione dei casi di crash segnalati da Sentry",
92
- "es": "Casos de accidente fijo reportados por Sentry",
93
- "pl": "Sprawy katastrofy Fix podane przez Sentry’a",
94
- "uk": "Виправлено випадки збоїв, про які повідомив Sentry",
95
- "zh-cn": "Sentry所报告的六例事故"
96
96
  }
97
97
  },
98
98
  "titleLang": {
@@ -127,6 +127,11 @@
127
127
  "receiver",
128
128
  "musiccast"
129
129
  ],
130
+ "plugins": {
131
+ "sentry": {
132
+ "dsn": "https://66f27ceff38c1ce1e6cdcc3095ba06d3@o4511524771266560.ingest.de.sentry.io/4511524773560400"
133
+ }
134
+ },
130
135
  "licenseInformation": {
131
136
  "license": "MIT",
132
137
  "type": "free"
@@ -243,6 +248,45 @@
243
248
  "def": false
244
249
  },
245
250
  "native": {}
251
+ },
252
+ {
253
+ "_id": "info.devicesTotal",
254
+ "type": "state",
255
+ "common": {
256
+ "name": "Devices total",
257
+ "type": "number",
258
+ "role": "value",
259
+ "read": true,
260
+ "write": false,
261
+ "def": 0
262
+ },
263
+ "native": {}
264
+ },
265
+ {
266
+ "_id": "info.devicesOnline",
267
+ "type": "state",
268
+ "common": {
269
+ "name": "Devices online",
270
+ "type": "number",
271
+ "role": "value",
272
+ "read": true,
273
+ "write": false,
274
+ "def": 0
275
+ },
276
+ "native": {}
277
+ },
278
+ {
279
+ "_id": "info.devicesAllOnline",
280
+ "type": "state",
281
+ "common": {
282
+ "name": "All devices online",
283
+ "type": "boolean",
284
+ "role": "indicator",
285
+ "read": true,
286
+ "write": false,
287
+ "def": false
288
+ },
289
+ "native": {}
246
290
  }
247
291
  ]
248
292
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.yamaha",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "ioBroker adapter for Yamaha AV receivers and MusicCast devices",
5
5
  "author": {
6
6
  "name": "krobi",