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 +6 -5
- package/build/lib/device-controller.js +87 -9
- package/build/lib/device-controller.js.map +2 -2
- package/build/lib/lifecycle/capability-profile.js +247 -0
- package/build/lib/lifecycle/capability-profile.js.map +7 -0
- package/build/lib/lifecycle/discovery-schema.js.map +2 -2
- package/build/lib/lifecycle/multi-transport-handle.js +35 -0
- package/build/lib/lifecycle/multi-transport-handle.js.map +2 -2
- package/build/lib/lifecycle/probe-memory.js +2 -0
- package/build/lib/lifecycle/probe-memory.js.map +2 -2
- package/build/lib/lifecycle/transport-connection-adapter.js +32 -11
- package/build/lib/lifecycle/transport-connection-adapter.js.map +2 -2
- package/build/lib/xml/device-controller.js +81 -50
- package/build/lib/xml/device-controller.js.map +2 -2
- package/build/lib/ynca/catalog.js +91 -2
- package/build/lib/ynca/catalog.js.map +2 -2
- package/build/lib/yxc/device-controller.js.map +1 -1
- package/build/main.js +54 -69
- package/build/main.js.map +3 -3
- package/io-package.json +14 -27
- package/package.json +1 -1
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.
|
|
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
|
|
376
|
-
*
|
|
377
|
-
*
|
|
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.
|
|
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
|
|
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 /
|