iobroker.yamaha 2.6.0 → 2.7.2

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.
Files changed (38) hide show
  1. package/README.md +12 -15
  2. package/admin/i18n/de.json +2 -0
  3. package/admin/i18n/en.json +2 -0
  4. package/admin/i18n/es.json +2 -0
  5. package/admin/i18n/fr.json +2 -0
  6. package/admin/i18n/it.json +2 -0
  7. package/admin/i18n/nl.json +2 -0
  8. package/admin/i18n/pl.json +2 -0
  9. package/admin/i18n/pt.json +2 -0
  10. package/admin/i18n/ru.json +2 -0
  11. package/admin/i18n/uk.json +2 -0
  12. package/admin/i18n/zh-cn.json +2 -0
  13. package/build/lib/device-controller.js +87 -9
  14. package/build/lib/device-controller.js.map +2 -2
  15. package/build/lib/lifecycle/capability-profile.js +247 -0
  16. package/build/lib/lifecycle/capability-profile.js.map +7 -0
  17. package/build/lib/lifecycle/discovery-schema.js +1 -1
  18. package/build/lib/lifecycle/discovery-schema.js.map +2 -2
  19. package/build/lib/lifecycle/multi-transport-handle.js +35 -0
  20. package/build/lib/lifecycle/multi-transport-handle.js.map +2 -2
  21. package/build/lib/lifecycle/probe-memory.js +2 -0
  22. package/build/lib/lifecycle/probe-memory.js.map +2 -2
  23. package/build/lib/lifecycle/transport-connection-adapter.js +32 -11
  24. package/build/lib/lifecycle/transport-connection-adapter.js.map +2 -2
  25. package/build/lib/xml/device-controller.js +93 -50
  26. package/build/lib/xml/device-controller.js.map +3 -3
  27. package/build/lib/xml/protocol.js +17 -0
  28. package/build/lib/xml/protocol.js.map +2 -2
  29. package/build/lib/ynca/catalog.js +91 -2
  30. package/build/lib/ynca/catalog.js.map +2 -2
  31. package/build/lib/yxc/device-controller.js +48 -5
  32. package/build/lib/yxc/device-controller.js.map +2 -2
  33. package/build/lib/yxc/object-mapper.js +45 -12
  34. package/build/lib/yxc/object-mapper.js.map +2 -2
  35. package/build/main.js +54 -69
  36. package/build/main.js.map +3 -3
  37. package/io-package.json +27 -27
  38. package/package.json +1 -1
package/README.md CHANGED
@@ -108,6 +108,18 @@ 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.2 (2026-09-09)
112
+
113
+ - (krobipd) Fixed: The volume readout now follows the scale the receiver is actually showing, so a receiver set to numbers no longer reports them as decibels.
114
+ - (krobipd) New: Inputs now appear under the names the receiver carries for them, so a socket named “Apple TV” reads that way instead of HDMI1.
115
+ - (krobipd) Changed: After this update every receiver is asked about its abilities once more, so the first start takes a little longer than usual.
116
+
117
+ ### 2.7.1 (2026-09-09)
118
+
119
+ - (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.
120
+ - (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.
121
+ - (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.
122
+
111
123
  ### 2.6.0 (2026-09-09)
112
124
 
113
125
  - (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)
@@ -126,21 +138,6 @@ On the very first contact the adapter asks the receiver which functions it suppo
126
138
 
127
139
  - (krobipd) Changed: installing straight from GitHub is no longer offered — the adapter is built before publishing, so it is installed from the ioBroker repository instead
128
140
 
129
- ### 2.5.0 (2026-09-07)
130
-
131
- - (krobipd) Fixed: switching off a datapoint group now clears it in every zone — turning off "Sound" used to leave the zone 2/3/4 sound datapoints standing, and "Playback" left 304 of them
132
- - (krobipd) Fixed: folders explain themselves on all three protocols now — on MusicCast and older XML receivers the explanation was missing, so a speaker or soundbar got almost none
133
- - (krobipd) Improved: numeric datapoints carry the limits the device itself declares, so a slider offers exactly the range the receiver accepts instead of an open number field
134
- - (krobipd) New: 61 setup datapoints of the 2010 receiver generation — speaker configuration, HDMI and lip-sync settings, trigger assignment, subwoofer trim, YPAO volume and the RDS clock
135
- - (krobipd) New: the device-wide MusicCast settings are readable and writable — automatic standby, display brightness and the two HDMI outputs, created only where the device really offers them
136
- - (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
- - (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
-
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
141
  [Older changelogs can be found there](CHANGELOG_OLD.md)
145
142
 
146
143
  ## History
@@ -256,6 +256,7 @@
256
256
  "volume": "Lautstärke",
257
257
  "descVolume": "Wie laut die Zone spielt: die beiden Text-Protokolle melden Dezibel, MusicCast seine eigene Schrittzahl — die Einheit sagt, welche.",
258
258
  "volumeDB": "Lautstärke (dB)",
259
+ "volumeDisplay": "Lautstärke (Anzeige)",
259
260
  "volumeDisplayMode": "Lautstärke-Anzeigeart",
260
261
  "volumeInterlock": "Lautstärke-Kopplung",
261
262
  "yncaControlPort": "YNCA-Steuerport",
@@ -384,6 +385,7 @@
384
385
  "descElapsedTime": "In Sekunden, damit man damit rechnen kann; die lesbare Form steht daneben.",
385
386
  "descTotalTime": "In Sekunden, damit man damit rechnen kann; die lesbare Form steht daneben.",
386
387
  "descVolumeDB": "Dieselbe Lautstärke, wie sie das Display des Receivers zeigt, in Dezibel.",
388
+ "descVolumeDisplay": "Dieselbe Lautstärke, wie sie das Display des Receivers zeigt, in der gerade eingestellten Skala.",
387
389
  "descVolumeDisplayMode": "Ob der Receiver die Lautstärke in Dezibel oder als schlichte Zahl anzeigt.",
388
390
  "descAudioSelect": "Von welcher Buchse der aktuelle Eingang seinen Ton nimmt: automatisch, HDMI, koaxial/optisch oder analog.",
389
391
  "descLinkControl": "Wägt Stabilität gegen Verzögerung ab, wenn an verbundene Geräte gestreamt wird.",
@@ -256,6 +256,7 @@
256
256
  "volume": "Volume",
257
257
  "descVolume": "How loud the zone plays: the two text protocols report decibels, MusicCast its own step count — the unit tells you which.",
258
258
  "volumeDB": "Volume (dB)",
259
+ "volumeDisplay": "Volume (display)",
259
260
  "volumeDisplayMode": "Volume display mode",
260
261
  "volumeInterlock": "Volume interlock",
261
262
  "yncaControlPort": "YNCA control port",
@@ -384,6 +385,7 @@
384
385
  "descElapsedTime": "In seconds, so it can be calculated with; a readable form sits next to it.",
385
386
  "descTotalTime": "In seconds, so it can be calculated with; a readable form sits next to it.",
386
387
  "descVolumeDB": "The same volume as the receiver's own display shows it, in decibels.",
388
+ "descVolumeDisplay": "The same volume the receiver's display shows, in whichever scale it is set to.",
387
389
  "descVolumeDisplayMode": "Whether the receiver shows volume in decibels or as a plain number.",
388
390
  "descAudioSelect": "Which terminal's sound the current input uses: automatic, HDMI, coaxial/optical or analogue.",
389
391
  "descLinkControl": "Trades stability against delay when streaming to linked devices.",
@@ -256,6 +256,7 @@
256
256
  "volume": "Volumen",
257
257
  "descVolume": "Lo alto que suena la zona: los dos protocolos de texto dan decibelios, MusicCast sus propios pasos — la unidad indica cuál.",
258
258
  "volumeDB": "Volumen (dB)",
259
+ "volumeDisplay": "Volumen (pantalla)",
259
260
  "volumeDisplayMode": "Modo de visualización del volumen",
260
261
  "volumeInterlock": "Bloqueo de volumen",
261
262
  "yncaControlPort": "Puerto de control YNCA",
@@ -384,6 +385,7 @@
384
385
  "descElapsedTime": "En segundos, para poder calcular; la forma legible está al lado.",
385
386
  "descTotalTime": "En segundos, para poder calcular; la forma legible está al lado.",
386
387
  "descVolumeDB": "El mismo volumen que muestra la pantalla del receptor, en decibelios.",
388
+ "descVolumeDisplay": "El mismo volumen que muestra la pantalla del receptor, en la escala seleccionada actualmente.",
387
389
  "descVolumeDisplayMode": "Si el receptor muestra el volumen en decibelios o como número.",
388
390
  "descAudioSelect": "De qué terminal toma el sonido la entrada actual: automático, HDMI, coaxial/óptico o analógico.",
389
391
  "descLinkControl": "Equilibra estabilidad y retardo al transmitir a aparatos enlazados.",
@@ -256,6 +256,7 @@
256
256
  "volume": "Volume",
257
257
  "descVolume": "Le niveau sonore de la zone : les deux protocoles texte donnent des décibels, MusicCast ses propres pas — l'unité indique lequel.",
258
258
  "volumeDB": "Volume (dB)",
259
+ "volumeDisplay": "Volume (afficheur)",
259
260
  "volumeDisplayMode": "Mode d'affichage du volume",
260
261
  "volumeInterlock": "Verrouillage du volume",
261
262
  "yncaControlPort": "Port de commande YNCA",
@@ -384,6 +385,7 @@
384
385
  "descElapsedTime": "En secondes, pour pouvoir calculer ; la forme lisible est à côté.",
385
386
  "descTotalTime": "En secondes, pour pouvoir calculer ; la forme lisible est à côté.",
386
387
  "descVolumeDB": "Le même volume que l'afficheur de l'ampli, en décibels.",
388
+ "descVolumeDisplay": "Le même volume que l'afficheur de l'ampli, dans l'échelle actuellement réglée.",
387
389
  "descVolumeDisplayMode": "Si l'ampli affiche le volume en décibels ou en nombre simple.",
388
390
  "descAudioSelect": "De quelle prise l'entrée actuelle tire son son : automatique, HDMI, coaxiale/optique ou analogique.",
389
391
  "descLinkControl": "Arbitre entre stabilité et latence lors de la diffusion vers les appareils liés.",
@@ -256,6 +256,7 @@
256
256
  "volume": "Volume",
257
257
  "descVolume": "Quanto forte suona la zona: i due protocolli testuali danno decibel, MusicCast i propri passi — l'unità dice quale.",
258
258
  "volumeDB": "Volume (dB)",
259
+ "volumeDisplay": "Volume (display)",
259
260
  "volumeDisplayMode": "Modalità di visualizzazione del volume",
260
261
  "volumeInterlock": "Blocco volume",
261
262
  "yncaControlPort": "Porta di controllo YNCA",
@@ -384,6 +385,7 @@
384
385
  "descElapsedTime": "In secondi, così è calcolabile; la forma leggibile è accanto.",
385
386
  "descTotalTime": "In secondi, così è calcolabile; la forma leggibile è accanto.",
386
387
  "descVolumeDB": "Lo stesso volume mostrato dal display del ricevitore, in decibel.",
388
+ "descVolumeDisplay": "Lo stesso volume mostrato dal display del ricevitore, nella scala attualmente impostata.",
387
389
  "descVolumeDisplayMode": "Se il ricevitore mostra il volume in decibel o come numero.",
388
390
  "descAudioSelect": "Da quale terminale l'ingresso corrente prende l'audio: automatico, HDMI, coassiale/ottico o analogico.",
389
391
  "descLinkControl": "Bilancia stabilità e ritardo nello streaming verso apparecchi collegati.",
@@ -256,6 +256,7 @@
256
256
  "volume": "Volume",
257
257
  "descVolume": "Hoe hard de zone speelt: de twee tekstprotocollen geven decibel, MusicCast zijn eigen stappen — de eenheid zegt welke.",
258
258
  "volumeDB": "Volume (dB)",
259
+ "volumeDisplay": "Volume (display)",
259
260
  "volumeDisplayMode": "Weergavemodus volume",
260
261
  "volumeInterlock": "Volumekoppeling",
261
262
  "yncaControlPort": "YNCA-besturingspoort",
@@ -384,6 +385,7 @@
384
385
  "descElapsedTime": "In seconden, zodat ermee gerekend kan worden; de leesbare vorm staat ernaast.",
385
386
  "descTotalTime": "In seconden, zodat ermee gerekend kan worden; de leesbare vorm staat ernaast.",
386
387
  "descVolumeDB": "Hetzelfde volume als op het display van de receiver, in decibel.",
388
+ "descVolumeDisplay": "Hetzelfde volume als op het display van de receiver, in de nu ingestelde schaal.",
387
389
  "descVolumeDisplayMode": "Of de receiver het volume in decibel of als getal toont.",
388
390
  "descAudioSelect": "Van welke aansluiting de huidige ingang zijn geluid neemt: automatisch, HDMI, coaxiaal/optisch of analoog.",
389
391
  "descLinkControl": "Weegt stabiliteit tegen vertraging af bij streamen naar gekoppelde apparaten.",
@@ -256,6 +256,7 @@
256
256
  "volume": "Głośność",
257
257
  "descVolume": "Jak głośno gra strefa: dwa protokoły tekstowe podają decybele, MusicCast własne kroki — jednostka mówi, który z nich.",
258
258
  "volumeDB": "Głośność (dB)",
259
+ "volumeDisplay": "Głośność (wyświetlacz)",
259
260
  "volumeDisplayMode": "Tryb wyświetlania głośności",
260
261
  "volumeInterlock": "Sprzężenie głośności",
261
262
  "yncaControlPort": "Port sterowania YNCA",
@@ -384,6 +385,7 @@
384
385
  "descElapsedTime": "W sekundach, by dało się liczyć; czytelna forma obok.",
385
386
  "descTotalTime": "W sekundach, by dało się liczyć; czytelna forma obok.",
386
387
  "descVolumeDB": "Ta sama głośność co na wyświetlaczu amplitunera, w decybelach.",
388
+ "descVolumeDisplay": "Ta sama głośność co na wyświetlaczu amplitunera, w aktualnie ustawionej skali.",
387
389
  "descVolumeDisplayMode": "Czy amplituner pokazuje głośność w decybelach, czy jako liczbę.",
388
390
  "descAudioSelect": "Z którego gniazda bieżące wejście pobiera dźwięk: automatycznie, HDMI, koncentryczne/optyczne lub analogowe.",
389
391
  "descLinkControl": "Równoważy stabilność i opóźnienie przy przesyłaniu do połączonych urządzeń.",
@@ -256,6 +256,7 @@
256
256
  "volume": "Volume",
257
257
  "descVolume": "Quão alto a zona toca: os dois protocolos de texto dão decibéis, o MusicCast os seus próprios passos — a unidade indica qual.",
258
258
  "volumeDB": "Volume (dB)",
259
+ "volumeDisplay": "Volume (visor)",
259
260
  "volumeDisplayMode": "Modo de exibição do volume",
260
261
  "volumeInterlock": "Bloqueio de volume",
261
262
  "yncaControlPort": "Porta de controlo YNCA",
@@ -384,6 +385,7 @@
384
385
  "descElapsedTime": "Em segundos, para poder calcular; a forma legível está ao lado.",
385
386
  "descTotalTime": "Em segundos, para poder calcular; a forma legível está ao lado.",
386
387
  "descVolumeDB": "O mesmo volume que o visor do recetor mostra, em decibéis.",
388
+ "descVolumeDisplay": "O mesmo volume que o visor do recetor mostra, na escala atualmente selecionada.",
387
389
  "descVolumeDisplayMode": "Se o recetor mostra o volume em decibéis ou como número simples.",
388
390
  "descAudioSelect": "De que terminal o som da entrada atual vem: automático, HDMI, coaxial/ótico ou analógico.",
389
391
  "descLinkControl": "Equilibra estabilidade e atraso ao transmitir para aparelhos ligados.",
@@ -256,6 +256,7 @@
256
256
  "volume": "Громкость",
257
257
  "descVolume": "Насколько громко играет зона: два текстовых протокола дают децибелы, MusicCast — свои шаги; единица измерения показывает, что именно.",
258
258
  "volumeDB": "Громкость (дБ)",
259
+ "volumeDisplay": "Громкость (дисплей)",
259
260
  "volumeDisplayMode": "Режим отображения громкости",
260
261
  "volumeInterlock": "Связка громкости",
261
262
  "yncaControlPort": "Порт управления YNCA",
@@ -384,6 +385,7 @@
384
385
  "descElapsedTime": "В секундах, чтобы с этим можно было считать; читаемая форма рядом.",
385
386
  "descTotalTime": "В секундах, чтобы с этим можно было считать; читаемая форма рядом.",
386
387
  "descVolumeDB": "Та же громкость, что показывает дисплей ресивера, в децибелах.",
388
+ "descVolumeDisplay": "Та же громкость, что показывает дисплей ресивера, в текущей выбранной шкале.",
387
389
  "descVolumeDisplayMode": "Показывает ли ресивер громкость в децибелах или простым числом.",
388
390
  "descAudioSelect": "Звук какого разъёма использует текущий вход: автоматически, HDMI, коаксиальный/оптический или аналоговый.",
389
391
  "descLinkControl": "Баланс стабильности и задержки при передаче на связанные устройства.",
@@ -256,6 +256,7 @@
256
256
  "volume": "Гучність",
257
257
  "descVolume": "Наскільки гучно грає зона: два текстові протоколи дають децибели, MusicCast — власні кроки; одиниця показує, що саме.",
258
258
  "volumeDB": "Гучність (дБ)",
259
+ "volumeDisplay": "Гучність (дисплей)",
259
260
  "volumeDisplayMode": "Режим показу гучності",
260
261
  "volumeInterlock": "Зв'язка гучності",
261
262
  "yncaControlPort": "Порт керування YNCA",
@@ -384,6 +385,7 @@
384
385
  "descElapsedTime": "У секундах, щоб можна було рахувати; зручна форма поруч.",
385
386
  "descTotalTime": "У секундах, щоб можна було рахувати; зручна форма поруч.",
386
387
  "descVolumeDB": "Та сама гучність, що на дисплеї ресивера, у децибелах.",
388
+ "descVolumeDisplay": "Та сама гучність, що на дисплеї ресивера, у поточно вибраній шкалі.",
387
389
  "descVolumeDisplayMode": "Чи показує ресивер гучність у децибелах, чи простим числом.",
388
390
  "descAudioSelect": "Звук якого роз'єму використовує поточний вхід: автоматично, HDMI, коаксіальний/оптичний чи аналоговий.",
389
391
  "descLinkControl": "Баланс стабільності та затримки при передачі на пов'язані пристрої.",
@@ -256,6 +256,7 @@
256
256
  "volume": "音量",
257
257
  "descVolume": "该区域的音量:两种文本协议给出分贝,MusicCast 给出自身的步进值,单位标明是哪一种。",
258
258
  "volumeDB": "音量 (dB)",
259
+ "volumeDisplay": "音量(显示)",
259
260
  "volumeDisplayMode": "音量显示方式",
260
261
  "volumeInterlock": "音量联动",
261
262
  "yncaControlPort": "YNCA 控制端口",
@@ -384,6 +385,7 @@
384
385
  "descElapsedTime": "以秒为单位便于计算;易读形式在旁边。",
385
386
  "descTotalTime": "以秒为单位便于计算;易读形式在旁边。",
386
387
  "descVolumeDB": "与功放显示屏一致的音量,单位分贝。",
388
+ "descVolumeDisplay": "与功放显示屏一致的音量,采用当前设置的刻度。",
387
389
  "descVolumeDisplayMode": "功放以分贝还是普通数字显示音量。",
388
390
  "descAudioSelect": "当前输入使用哪个端子的声音:自动、HDMI、同轴/光纤或模拟。",
389
391
  "descLinkControl": "向联动设备串流时在稳定性与延迟之间取舍。",
@@ -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 /