iobroker.yamaha 2.6.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -108,6 +108,12 @@ On the very first contact the adapter asks the receiver which functions it suppo
108
108
  Placeholder for the next version (at the beginning of the line):
109
109
  ### **WORK IN PROGRESS**
110
110
  -->
111
+ ### 2.7.1 (2026-09-09)
112
+
113
+ - (krobipd) Improved: The first connection asks a receiver only what its generation can answer, so zones, trigger sockets and per-input settings follow the device, not a catalogue.
114
+ - (krobipd) New: A datapoint the receiver reveals later now appears at once — a function it starts answering, a value it reports for the first time, a status field it begins delivering.
115
+ - (krobipd) Changed: A datapoint that never carried a value is removed only after two starts confirm it, so a receiver left in standby no longer loses datapoints it still has.
116
+
111
117
  ### 2.6.0 (2026-09-09)
112
118
 
113
119
  - (krobipd) Fixed: input and sound program lists now offer only what the receiver itself declares or proves it has, instead of every value any Yamaha may have (#619)
@@ -136,11 +142,6 @@ On the very first contact the adapter asks the receiver which functions it suppo
136
142
  - (krobipd) Fixed: bass, treble and subwoofer trim showed doubled values on MusicCast receivers — that scale counts in half decibels and was labelled as decibels
137
143
  - (krobipd) Fixed: a receiver that was in standby when the adapter started could end up with an empty media menu until the next restart
138
144
 
139
- ### 2.4.0 (2026-09-03)
140
-
141
- - (krobipd) New: the on-screen remote reaches every protocol now — the cursor pad and the menu keys work on YNCA and pre-2010 XML receivers, not just on MusicCast
142
- - (krobipd) Changed: stepping one menu level back no longer switches to a substitute key on a receiver that rejects it — on those models the new cursor pad does it
143
-
144
145
  [Older changelogs can be found there](CHANGELOG_OLD.md)
145
146
 
146
147
  ## History
@@ -158,6 +158,12 @@ class YncaDeviceController {
158
158
  presentSubunits = /* @__PURE__ */ new Set();
159
159
  /** The enum values this device ever reported (see OBSERVED_KEY), persisted on every addition. */
160
160
  observed = {};
161
+ /** The (group-filtered) catalog of this connection, for rebuilding objects mid-session. */
162
+ catalog = [];
163
+ /** The capability shape the current object tree was built from — grown by pushes and refreshes. */
164
+ shape = { model: "", subunits: {} };
165
+ /** Object id → the definition last upserted, so a republish writes only what really changed. */
166
+ published = /* @__PURE__ */ new Map();
161
167
  /**
162
168
  * Connect, sweep the device from the catalog, and create its object tree; wire
163
169
  * up push updates. The catalog is the single source: it drives the sweep, the
@@ -169,6 +175,7 @@ class YncaDeviceController {
169
175
  var _a, _b, _c, _d, _e, _f, _g, _h, _i;
170
176
  await this.deps.client.connect();
171
177
  const catalog = this.deps.isEntryEnabled ? import_catalog.YNCA_CATALOG.filter((entry) => this.deps.isEntryEnabled(entry.id)) : import_catalog.YNCA_CATALOG;
178
+ this.catalog = catalog;
172
179
  const resolved = await this.resolveCapabilities(catalog);
173
180
  const { capabilities, fromCache } = resolved;
174
181
  const present = (0, import_catalog.presentYncaEntries)(capabilities, catalog);
@@ -184,6 +191,7 @@ class YncaDeviceController {
184
191
  this.recordObservedAll(capabilities);
185
192
  this.recordObservedAll(live);
186
193
  const evidence = this.inputEvidence(capabilities);
194
+ this.shape = { model: capabilities.model, subunits: capabilities.subunits };
187
195
  const objects = (0, import_catalog.yncaObjectsFor)(capabilities, catalog, this.statesResolver(live, evidence));
188
196
  if (objects.length === 0) {
189
197
  this.deps.log.warn(`${this.deviceId}: no capabilities reported \u2014 creating no objects`);
@@ -212,7 +220,7 @@ class YncaDeviceController {
212
220
  if (titles !== void 0 && titles.length > 0) {
213
221
  object.common.states = Object.fromEntries(titles.map((scene) => [scene.num, scene.title]));
214
222
  }
215
- await this.deps.upsertObject(`${this.deviceId}.${object.id}`, object);
223
+ await this.upsertTracked(object);
216
224
  }
217
225
  for (const zone of YNCA_ZONES) {
218
226
  const titles = zone.key === "main" ? this.sceneTitles : this.zoneSceneTitles.get(zone.key);
@@ -318,7 +326,8 @@ class YncaDeviceController {
318
326
  (_h = this.deps.probeMemory) == null ? void 0 : _h.set(CAPS_KEY, {
319
327
  model: capabilities.model,
320
328
  firmware: (_g = (_f = capabilities.subunits.SYS) == null ? void 0 : _f.VERSION) != null ? _g : firmware,
321
- subunits: capabilities.subunits
329
+ subunits: capabilities.subunits,
330
+ complete: true
322
331
  });
323
332
  } else {
324
333
  (_i = this.deps.probeMemory) == null ? void 0 : _i.drop((key) => key === STATIC_KEY);
@@ -372,9 +381,9 @@ class YncaDeviceController {
372
381
  * subunits — the answers stream into the states through the live message handler,
373
382
  * so current values arrive within the usual sweep time WITHOUT having gated the
374
383
  * ready line. Completion refreshes the persisted layers (capabilities, statics)
375
- * and the write map; a SHAPE change (a function newly answered) is only persisted —
376
- * its object appears on the next start, because the unified tree is coordinated
377
- * once per connect and a late upsert would not materialize.
384
+ * and the write map; a SHAPE change (a function newly answered) is persisted AND published
385
+ * since 2.7.0 the objects are rebuilt from the grown shape and the handle re-coordinates the
386
+ * unified tree, so the datapoint appears in this session instead of one start later.
378
387
  *
379
388
  * @param catalog the (group-filtered) catalog
380
389
  */
@@ -385,7 +394,7 @@ class YncaDeviceController {
385
394
  const gets = (0, import_catalog.sweepGets)(catalog).filter(
386
395
  (get) => get.subunit === "SYS" || !cached || cached.subunits.includes(get.subunit)
387
396
  );
388
- const fresh = await this.deps.client.readCapabilities(gets);
397
+ const fresh = await this.sweepInPasses((0, import_catalog.planSweep)(gets, this.inputEvidence({ model: "", subunits: {} })));
389
398
  if (!fresh.model) {
390
399
  return;
391
400
  }
@@ -403,10 +412,13 @@ class YncaDeviceController {
403
412
  (_g = this.deps.probeMemory) == null ? void 0 : _g.set(CAPS_KEY, {
404
413
  model: fresh.model,
405
414
  firmware: (_f = (_e = fresh.subunits.SYS) == null ? void 0 : _e.VERSION) != null ? _f : "",
406
- subunits
415
+ subunits,
416
+ complete: true
407
417
  });
408
418
  this.presentEntries = (0, import_catalog.presentYncaEntries)({ model: fresh.model, subunits }, catalog);
409
419
  this.writeMap = (0, import_catalog.idToEntry)(this.presentEntries);
420
+ this.shape = { model: fresh.model || this.shape.model, subunits };
421
+ await this.republishObjects();
410
422
  const titles = sceneTitlesOf(subunits);
411
423
  if (JSON.stringify(titles) !== JSON.stringify(this.sceneTitles)) {
412
424
  this.sceneTitles = titles;
@@ -495,9 +507,11 @@ class YncaDeviceController {
495
507
  (get) => (get.subunit === "SYS" || present.has(get.subunit)) && !answered.has(`${get.subunit}:${get.func}`)
496
508
  );
497
509
  const remembered = (_a = this.deps.probeMemory) == null ? void 0 : _a.remembered(STATIC_KEY);
498
- const swept = await this.deps.client.readCapabilities(
499
- remembered ? gets.filter((get) => !STATIC_FUNC.test(get.func)) : gets
510
+ const plan = (0, import_catalog.planSweep)(
511
+ remembered ? gets.filter((get) => !STATIC_FUNC.test(get.func)) : gets,
512
+ this.inputEvidence(basic)
500
513
  );
514
+ const swept = await this.sweepInPasses(plan);
501
515
  const capabilities = {
502
516
  model: swept.model || basic.model,
503
517
  subunits: (0, import_capability.mergeYncaSubunits)(basic.subunits, swept.subunits)
@@ -519,6 +533,27 @@ class YncaDeviceController {
519
533
  (_c = this.deps.probeMemory) == null ? void 0 : _c.set(STATIC_KEY, statics);
520
534
  return capabilities;
521
535
  }
536
+ /**
537
+ * Send a planned sweep: the first pass, then — only for the SYS families whose head answered
538
+ * in it — the family members (`planSweep`, `SYS_FUNCTION_FAMILIES`). One GET on the head
539
+ * decides up to 43 GETs of a family a device has as a whole or not at all.
540
+ *
541
+ * @param plan the planned GET lists
542
+ * @returns the merged answers of both passes
543
+ */
544
+ async sweepInPasses(plan) {
545
+ const first = await this.deps.client.readCapabilities(plan.first);
546
+ const answeredHeads = import_catalog.SYS_FUNCTION_FAMILIES.filter((family) => {
547
+ var _a;
548
+ return ((_a = first.subunits.SYS) == null ? void 0 : _a[family.head]) !== void 0;
549
+ });
550
+ const second = plan.families.filter((get) => answeredHeads.some((family) => get.func.startsWith(family.prefix)));
551
+ if (second.length === 0) {
552
+ return first;
553
+ }
554
+ const members = await this.deps.client.readCapabilities(second);
555
+ return { model: first.model || members.model, subunits: (0, import_capability.mergeYncaSubunits)(first.subunits, members.subunits) };
556
+ }
522
557
  /**
523
558
  * Read every bundle of the present subunits (BASIC and SCENENAME per zone, SIGINFO/RDSINFO on
524
559
  * the tuner, METAINFO per player — see `bundleGets`) in ONE paced request list; the answers
@@ -591,6 +626,49 @@ class YncaDeviceController {
591
626
  }
592
627
  list.push(value);
593
628
  (_e = this.deps.probeMemory) == null ? void 0 : _e.set(OBSERVED_KEY, this.observed);
629
+ void this.republishObjects();
630
+ }
631
+ /**
632
+ * Rebuild this transport's objects from the shape it knows now and upsert them. Idempotent by
633
+ * construction: the transport adapter keeps the last definition per id, so an unchanged object
634
+ * is neither written nor signalled — only a real change reaches the handle's re-coordination.
635
+ * Silent before the tree stood (no shape yet) and while nothing is present.
636
+ */
637
+ async republishObjects() {
638
+ if (this.presentEntries.length === 0 || Object.keys(this.shape.subunits).length === 0) {
639
+ return;
640
+ }
641
+ try {
642
+ const objects = (0, import_catalog.yncaObjectsFor)(
643
+ this.shape,
644
+ this.catalog,
645
+ this.statesResolver(this.shape, this.inputEvidence(this.shape))
646
+ );
647
+ for (const object of objects) {
648
+ const titles = this.sceneTitlesFor(object.id);
649
+ if (titles !== void 0 && titles.length > 0) {
650
+ object.common.states = Object.fromEntries(titles.map((scene) => [scene.num, scene.title]));
651
+ }
652
+ await this.upsertTracked(object);
653
+ }
654
+ } catch (e) {
655
+ this.deps.log.debug(`${this.deviceId}: could not republish the object tree: ${String(e)}`);
656
+ }
657
+ }
658
+ /**
659
+ * Upsert an object and remember its definition, so the next republish writes only what really
660
+ * changed. A device answering a new value republishes the whole tree; without this every one of
661
+ * ~250 definitions would be handed on, and the handle would re-coordinate for each of them.
662
+ *
663
+ * @param object the object definition to write
664
+ */
665
+ async upsertTracked(object) {
666
+ const fingerprint = JSON.stringify(object);
667
+ if (this.published.get(object.id) === fingerprint) {
668
+ return;
669
+ }
670
+ this.published.set(object.id, fingerprint);
671
+ await this.deps.upsertObject(`${this.deviceId}.${object.id}`, object);
594
672
  }
595
673
  /**
596
674
  * What this connect learned about the device's inputs: the probe's verdicts (present /