iobroker.yamaha 1.2.0 → 1.4.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 +22 -19
- package/admin/i18n/de.json +2 -2
- package/admin/i18n/en.json +2 -2
- package/admin/i18n/es.json +2 -2
- package/admin/i18n/fr.json +2 -2
- package/admin/i18n/it.json +2 -2
- package/admin/i18n/nl.json +2 -2
- package/admin/i18n/pl.json +2 -2
- package/admin/i18n/pt.json +2 -2
- package/admin/i18n/ru.json +2 -2
- package/admin/i18n/uk.json +2 -2
- package/admin/i18n/zh-cn.json +2 -2
- package/build/device-management.js +4 -2
- package/build/device-management.js.map +2 -2
- package/build/lib/attempt-device.js +22 -7
- package/build/lib/attempt-device.js.map +2 -2
- package/build/lib/browse/browse-engine.js +241 -0
- package/build/lib/browse/browse-engine.js.map +7 -0
- package/build/lib/browse/objects.js +109 -0
- package/build/lib/browse/objects.js.map +7 -0
- package/build/lib/browse/surface.js +47 -0
- package/build/lib/browse/surface.js.map +7 -0
- package/build/lib/browse/types.js +34 -0
- package/build/lib/browse/types.js.map +7 -0
- package/build/lib/browse/xml-browse-driver.js +194 -0
- package/build/lib/browse/xml-browse-driver.js.map +7 -0
- package/build/lib/browse/ynca-browse-driver.js +220 -0
- package/build/lib/browse/ynca-browse-driver.js.map +7 -0
- package/build/lib/browse/yxc-browse-driver.js +190 -0
- package/build/lib/browse/yxc-browse-driver.js.map +7 -0
- package/build/lib/device-controller.js +74 -7
- package/build/lib/device-controller.js.map +3 -3
- package/build/lib/discovery.js +15 -15
- package/build/lib/discovery.js.map +2 -2
- package/build/lib/lifecycle/command-gate.js +165 -0
- package/build/lib/lifecycle/command-gate.js.map +7 -0
- package/build/lib/lifecycle/multi-transport-handle.js +7 -0
- package/build/lib/lifecycle/multi-transport-handle.js.map +2 -2
- package/build/lib/lifecycle/poll-drop-detector.js +73 -0
- package/build/lib/lifecycle/poll-drop-detector.js.map +7 -0
- package/build/lib/lifecycle/probe-memory.js +69 -0
- package/build/lib/lifecycle/probe-memory.js.map +7 -0
- package/build/lib/lifecycle/reconnect-strategy.js +5 -2
- package/build/lib/lifecycle/reconnect-strategy.js.map +2 -2
- package/build/lib/xml/catalog.js +5 -4
- package/build/lib/xml/catalog.js.map +2 -2
- package/build/lib/xml/device-controller.js +75 -35
- package/build/lib/xml/device-controller.js.map +2 -2
- package/build/lib/xml/entities.js +50 -0
- package/build/lib/xml/entities.js.map +7 -0
- package/build/lib/xml/protocol.js +6 -5
- package/build/lib/xml/protocol.js.map +2 -2
- package/build/lib/xml/xml-client.js +21 -5
- package/build/lib/xml/xml-client.js.map +2 -2
- package/build/lib/ynca/__fixtures__/capabilities-from-lines.js +40 -0
- package/build/lib/ynca/__fixtures__/capabilities-from-lines.js.map +7 -0
- package/build/lib/ynca/capability.js +2 -15
- package/build/lib/ynca/capability.js.map +2 -2
- package/build/lib/ynca/catalog.js +126 -0
- package/build/lib/ynca/catalog.js.map +2 -2
- package/build/lib/ynca/ynca-client.js +78 -23
- package/build/lib/ynca/ynca-client.js.map +2 -2
- package/build/lib/yxc/client-contract.js.map +1 -1
- package/build/lib/yxc/command-mapper.js +5 -11
- package/build/lib/yxc/command-mapper.js.map +2 -2
- package/build/lib/yxc/device-controller.js +158 -64
- package/build/lib/yxc/device-controller.js.map +2 -2
- package/build/lib/yxc/http-client.js +69 -22
- package/build/lib/yxc/http-client.js.map +2 -2
- package/build/lib/yxc/object-mapper.js +5 -6
- package/build/lib/yxc/object-mapper.js.map +2 -2
- package/build/lib/yxc/push-receiver.js +10 -0
- package/build/lib/yxc/push-receiver.js.map +2 -2
- package/build/lib/yxc/zones.js +43 -0
- package/build/lib/yxc/zones.js.map +7 -0
- package/build/main.js +14 -5
- package/build/main.js.map +2 -2
- package/io-package.json +27 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,8 @@ legacy XML protocol of the oldest pre-2010 models — behind one object tree.
|
|
|
19
19
|
- **Instant updates** — MusicCast pushes its changes, YNCA reports over its live connection
|
|
20
20
|
- **Self-healing connections** — an offline receiver joins once it answers; a single protocol reconnects on its own while the others keep running
|
|
21
21
|
- **Typed datapoints** — booleans, dropdowns and numbers with unit and range instead of raw text
|
|
22
|
-
- **Presets and favourites** — recall tuner presets and stored network/USB favourites by number, step through presets, and read the stored lists with their names (MusicCast); recently-played recall on MusicCast devices
|
|
22
|
+
- **Presets and favourites** — recall tuner presets and stored network/USB favourites by number, step through presets, save the current station to a preset slot or bookmark it, and read the stored lists with their names (MusicCast); recently-played recall on MusicCast devices
|
|
23
|
+
- **Menu browsing** — page through the Net Radio, media-server and USB menus like with the remote: the visible menu lines as datapoints, select-by-line, and a path datapoint that navigates to a favourite in one write
|
|
23
24
|
- **Clock & alarm view** — MusicCast desk-audio devices show their clock and alarm settings
|
|
24
25
|
- **Capability-driven** — states are generated from what each device reports, no hardcoded model list
|
|
25
26
|
- **Automatic discovery** — an empty device list finds and sets up MusicCast devices at startup
|
|
@@ -44,7 +45,7 @@ Devices are managed in the admin as cards. **Leave the list empty** and the adap
|
|
|
44
45
|
|
|
45
46
|
Older Yamaha receivers (before ~2010, the XML protocol) do not announce themselves on the network and must be added manually. The **XML query interval** sets how often they are polled (default 60 seconds).
|
|
46
47
|
|
|
47
|
-
The **Data points** section switches whole groups of datapoints on or off — **Playback**, **Tuner**, **Multiroom**, **HDMI**, **Scenes**, **Sound** and **
|
|
48
|
+
The **Data points** section switches whole groups of datapoints on or off — **Playback & browsing**, **Tuner**, **Multiroom**, **HDMI**, **Scenes**, **Sound**, **Advanced** and **Clock & alarm**. A switched-off group is removed from the tree and not even queried, which also speeds up the startup; the amplifier core (power, volume, mute, input, sound program, sleep) always stays on.
|
|
48
49
|
|
|
49
50
|
## State Tree
|
|
50
51
|
|
|
@@ -52,13 +53,14 @@ Each receiver becomes one device node with themed groups — the same groups the
|
|
|
52
53
|
**Data points** switches control. Only what your device reports is created.
|
|
53
54
|
|
|
54
55
|
- **Amplifier core** (always on) — power, volume, mute, input, sound program, sleep, plus the device info with model, firmware and connection.
|
|
55
|
-
- **`player`** — one channel per playback source (Spotify, USB, server, net radio, CD, …) with playback state, artist, album, track, cover art and the transport buttons.
|
|
56
|
+
- **`player`** — one channel per playback source (Spotify, USB, server, net radio, CD, …) with playback state, artist, album, track, cover art and the transport buttons. The `player.browse` folder mirrors the device's media menu: the eight visible lines (folders and titles marked by symbol), `selectLine` acts like OK on the remote, page/back/root buttons, a `rows` JSON for widgets and a `path` datapoint that walks e.g. `Bookmarks>Radio Paradise` on one write.
|
|
56
57
|
- **`tuner`** — AM/FM and DAB radio including RDS texts and frequency.
|
|
57
58
|
- **`multiroom`** — zones 2–4, Zone B, the all-zones switches (master power, party mode) and the MusicCast device group in its own `multiroom.group` folder.
|
|
58
59
|
- **`hdmi`** — the HDMI outputs and lip sync.
|
|
59
60
|
- **`scene`** — the receiver's scene names and a scene recall.
|
|
60
61
|
- **`sound`** — tone and sound processing: bass/treble, DSP modes, enhancer, equalizer, ….
|
|
61
62
|
- **`advanced`** — setup-level datapoints: maximum/initial volume, speaker configuration, input names.
|
|
63
|
+
- **`clock`** — the clock and alarm settings of MusicCast desk-audio devices (read-only).
|
|
62
64
|
|
|
63
65
|
## Troubleshooting
|
|
64
66
|
|
|
@@ -88,6 +90,23 @@ On the first connect the adapter asks the receiver which functions it supports
|
|
|
88
90
|
Placeholder for the next version (at the beginning of the line):
|
|
89
91
|
### **WORK IN PROGRESS**
|
|
90
92
|
-->
|
|
93
|
+
### 1.4.0 (2026-08-26)
|
|
94
|
+
|
|
95
|
+
- (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
|
|
96
|
+
- (krobipd) Fixed: a command the device rejects is now reported instead of counting as success, so a MusicCast device that stops answering is reconnected rather than silently freezing
|
|
97
|
+
- (krobipd) Fixed: names and menu entries containing "&" or other special characters now read and write correctly on the older XML protocol
|
|
98
|
+
- (krobipd) Fixed: writing one equalizer band no longer resets the other two when the device has not reported its bands yet
|
|
99
|
+
- (krobipd) Fixed: switching the tuner band and setting a frequency right after each other now applies the frequency to the new band
|
|
100
|
+
- (krobipd) Improved: startup with automatic discovery is much faster on networks with many devices, and a reconnect no longer re-asks what the device already told us
|
|
101
|
+
- (krobipd) Fixed: recalling a favourite, a recently played item or a tuner preset now goes to the zone that is actually listening instead of always switching the main zone
|
|
102
|
+
- (krobipd) Improved: stopping or restarting the adapter no longer leaves requests running that write to datapoints afterwards
|
|
103
|
+
|
|
104
|
+
### 1.3.0 (2026-08-26)
|
|
105
|
+
|
|
106
|
+
- (krobipd) New: menu browsing — page through the Net Radio, server and USB menus like with the remote: visible lines as datapoints, select-by-line, and a path datapoint for one-write navigation (#613)
|
|
107
|
+
- (krobipd) New: save presets from ioBroker — store the current tuner or network station to a preset slot and bookmark the playing Net Radio station on YNCA receivers.
|
|
108
|
+
- (krobipd) New: Bluetooth pairing and connect controls, FM mono mode and tuning indicators on YNCA receivers.
|
|
109
|
+
|
|
91
110
|
### 1.2.0 (2026-08-25)
|
|
92
111
|
|
|
93
112
|
- (krobipd) Fixed: volume writes work again — a written -38 dB reached the receiver as -3.8 dB, so most values were ignored; all numeric controls now send the proper wire format (#612)
|
|
@@ -105,22 +124,6 @@ On the first connect the adapter asks the receiver which functions it supports
|
|
|
105
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.
|
|
106
125
|
- (krobipd) Improved: a device that has not reported a model yet already carries its device-class symbol instead of none.
|
|
107
126
|
|
|
108
|
-
### 1.0.1 (2026-08-22)
|
|
109
|
-
|
|
110
|
-
- (krobipd) Complete rebuild: one adapter now speaks YNCA, MusicCast and the legacy XML protocol — every protocol a device answers runs in parallel on one object tree.
|
|
111
|
-
- (krobipd) New object tree with typed datapoints built from what your device reports. Old datapoints are removed automatically, the address is carried over — point scripts at the new paths.
|
|
112
|
-
- (krobipd) Instant updates: MusicCast push events and the live YNCA connection replace polling; connections heal themselves, and one protocol's hiccup reconnects just that protocol.
|
|
113
|
-
- (krobipd) Auto-discovery sets up MusicCast devices by itself when the device list is empty, and the admin shows every receiver as a card with model, address and protocol indicators.
|
|
114
|
-
- (krobipd) Whole datapoint groups such as playback sources, tuner, multiroom or scenes can be switched off in the admin — and are then not even queried from the device.
|
|
115
|
-
- (krobipd) The multiroom folder tells the scope at a glance: switches that affect all zones say so in their name, and the MusicCast device group has its own `multiroom.group` folder.
|
|
116
|
-
- (krobipd) Every device shows a type icon — receiver, stereo receiver, speaker, soundbar or CD system, detected from the reported model — in the object tree and on its admin card; the adapter logo now stays readable in light and dark mode.
|
|
117
|
-
- (krobipd) Upgrading from 0.5.x shows a one-time notice explaining the new object tree before the update installs.
|
|
118
|
-
- (mcm1957) version has been rebuilt due to deploy problems
|
|
119
|
-
|
|
120
|
-
### 0.5.4 (2024-06-14) — stable
|
|
121
|
-
|
|
122
|
-
- (foxriver76) updated packages
|
|
123
|
-
|
|
124
127
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
125
128
|
|
|
126
129
|
## History
|
package/admin/i18n/de.json
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"ipLabel": "IP",
|
|
24
24
|
"datapointsHeader": "Datenpunkte",
|
|
25
25
|
"datapointsHint": "Wähle, welche Funktionsgruppen als Datenpunkte angelegt werden. Eine ausgeschaltete Gruppe wird nicht erstellt und bestehende Objekte werden entfernt.",
|
|
26
|
-
"group_player": "Wiedergabe",
|
|
27
|
-
"group_player_help": "Spotify, Net Radio, USB, Bluetooth und die weiteren Quellen",
|
|
26
|
+
"group_player": "Wiedergabe & Browsen",
|
|
27
|
+
"group_player_help": "Spotify, Net Radio, USB, Bluetooth und die weiteren Quellen — samt Menü-Browsing (Net Radio/Server/USB durchblättern)",
|
|
28
28
|
"group_tuner": "Tuner",
|
|
29
29
|
"group_tuner_help": "FM/DAB, Frequenz, RDS",
|
|
30
30
|
"group_multiroom": "Multiroom",
|
package/admin/i18n/en.json
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"ipLabel": "IP",
|
|
24
24
|
"datapointsHeader": "Data points",
|
|
25
25
|
"datapointsHint": "Choose which function groups are created as data points. A switched-off group is not created and existing objects are removed.",
|
|
26
|
-
"group_player": "Playback",
|
|
27
|
-
"group_player_help": "Spotify, Net Radio, USB, Bluetooth and the other sources",
|
|
26
|
+
"group_player": "Playback & browsing",
|
|
27
|
+
"group_player_help": "Spotify, Net Radio, USB, Bluetooth and the other sources — including menu browsing (Net Radio/server/USB)",
|
|
28
28
|
"group_tuner": "Tuner",
|
|
29
29
|
"group_tuner_help": "FM/DAB, frequency, RDS",
|
|
30
30
|
"group_multiroom": "Multiroom",
|
package/admin/i18n/es.json
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"ipLabel": "IP",
|
|
24
24
|
"datapointsHeader": "Puntos de datos",
|
|
25
25
|
"datapointsHint": "Elige qué grupos de funciones se crean como puntos de datos. Un grupo desactivado no se crea y los objetos existentes se eliminan.",
|
|
26
|
-
"group_player": "Reproducción",
|
|
27
|
-
"group_player_help": "Spotify, Net Radio, USB, Bluetooth y las demás fuentes",
|
|
26
|
+
"group_player": "Reproducción y navegación",
|
|
27
|
+
"group_player_help": "Spotify, Net Radio, USB, Bluetooth y las demás fuentes, incluida la navegación por menús (Net Radio/servidor/USB)",
|
|
28
28
|
"group_tuner": "Sintonizador",
|
|
29
29
|
"group_tuner_help": "FM/DAB, frecuencia, RDS",
|
|
30
30
|
"group_multiroom": "Multiroom",
|
package/admin/i18n/fr.json
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"ipLabel": "IP",
|
|
24
24
|
"datapointsHeader": "Points de données",
|
|
25
25
|
"datapointsHint": "Choisissez les groupes de fonctions créés comme points de données. Un groupe désactivé n'est pas créé et les objets existants sont supprimés.",
|
|
26
|
-
"group_player": "Lecture",
|
|
27
|
-
"group_player_help": "Spotify, Net Radio, USB, Bluetooth et les autres sources",
|
|
26
|
+
"group_player": "Lecture et navigation",
|
|
27
|
+
"group_player_help": "Spotify, Net Radio, USB, Bluetooth et les autres sources — y compris la navigation dans les menus (Net Radio/serveur/USB)",
|
|
28
28
|
"group_tuner": "Tuner",
|
|
29
29
|
"group_tuner_help": "FM/DAB, fréquence, RDS",
|
|
30
30
|
"group_multiroom": "Multiroom",
|
package/admin/i18n/it.json
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"ipLabel": "IP",
|
|
24
24
|
"datapointsHeader": "Punti dati",
|
|
25
25
|
"datapointsHint": "Scegli quali gruppi di funzioni vengono creati come punti dati. Un gruppo disattivato non viene creato e gli oggetti esistenti vengono rimossi.",
|
|
26
|
-
"group_player": "Riproduzione",
|
|
27
|
-
"group_player_help": "Spotify, Net Radio, USB, Bluetooth e le altre sorgenti",
|
|
26
|
+
"group_player": "Riproduzione e navigazione",
|
|
27
|
+
"group_player_help": "Spotify, Net Radio, USB, Bluetooth e le altre sorgenti — inclusa la navigazione nei menu (Net Radio/server/USB)",
|
|
28
28
|
"group_tuner": "Sintonizzatore",
|
|
29
29
|
"group_tuner_help": "FM/DAB, frequenza, RDS",
|
|
30
30
|
"group_multiroom": "Multiroom",
|
package/admin/i18n/nl.json
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"ipLabel": "IP",
|
|
24
24
|
"datapointsHeader": "Datapunten",
|
|
25
25
|
"datapointsHint": "Kies welke functiegroepen als datapunten worden aangemaakt. Een uitgeschakelde groep wordt niet aangemaakt en bestaande objecten worden verwijderd.",
|
|
26
|
-
"group_player": "Weergave",
|
|
27
|
-
"group_player_help": "Spotify, Net Radio, USB, Bluetooth en de andere bronnen",
|
|
26
|
+
"group_player": "Weergave & bladeren",
|
|
27
|
+
"group_player_help": "Spotify, Net Radio, USB, Bluetooth en de andere bronnen — inclusief menubladeren (Net Radio/server/USB)",
|
|
28
28
|
"group_tuner": "Tuner",
|
|
29
29
|
"group_tuner_help": "FM/DAB, frequentie, RDS",
|
|
30
30
|
"group_multiroom": "Multiroom",
|
package/admin/i18n/pl.json
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"ipLabel": "IP",
|
|
24
24
|
"datapointsHeader": "Punkty danych",
|
|
25
25
|
"datapointsHint": "Wybierz, które grupy funkcji są tworzone jako punkty danych. Wyłączona grupa nie jest tworzona, a istniejące obiekty są usuwane.",
|
|
26
|
-
"group_player": "Odtwarzanie",
|
|
27
|
-
"group_player_help": "Spotify, Net Radio, USB, Bluetooth i pozostałe źródła",
|
|
26
|
+
"group_player": "Odtwarzanie i przeglądanie",
|
|
27
|
+
"group_player_help": "Spotify, Net Radio, USB, Bluetooth i pozostałe źródła — w tym przeglądanie menu (Net Radio/serwer/USB)",
|
|
28
28
|
"group_tuner": "Tuner",
|
|
29
29
|
"group_tuner_help": "FM/DAB, częstotliwość, RDS",
|
|
30
30
|
"group_multiroom": "Multiroom",
|
package/admin/i18n/pt.json
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"ipLabel": "IP",
|
|
24
24
|
"datapointsHeader": "Pontos de dados",
|
|
25
25
|
"datapointsHint": "Escolha quais grupos de funções são criados como pontos de dados. Um grupo desligado não é criado e os objetos existentes são removidos.",
|
|
26
|
-
"group_player": "Reprodução",
|
|
27
|
-
"group_player_help": "Spotify, Net Radio, USB, Bluetooth e as outras fontes",
|
|
26
|
+
"group_player": "Reprodução e navegação",
|
|
27
|
+
"group_player_help": "Spotify, Net Radio, USB, Bluetooth e as outras fontes — incluindo a navegação nos menus (Net Radio/servidor/USB)",
|
|
28
28
|
"group_tuner": "Sintonizador",
|
|
29
29
|
"group_tuner_help": "FM/DAB, frequência, RDS",
|
|
30
30
|
"group_multiroom": "Multiroom",
|
package/admin/i18n/ru.json
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"ipLabel": "IP",
|
|
24
24
|
"datapointsHeader": "Точки данных",
|
|
25
25
|
"datapointsHint": "Выберите, какие группы функций создаются как точки данных. Отключённая группа не создаётся, существующие объекты удаляются.",
|
|
26
|
-
"group_player": "Воспроизведение",
|
|
27
|
-
"group_player_help": "Spotify, Net Radio, USB, Bluetooth и другие источники",
|
|
26
|
+
"group_player": "Воспроизведение и обзор",
|
|
27
|
+
"group_player_help": "Spotify, Net Radio, USB, Bluetooth и другие источники — включая навигацию по меню (Net Radio/сервер/USB)",
|
|
28
28
|
"group_tuner": "Тюнер",
|
|
29
29
|
"group_tuner_help": "FM/DAB, частота, RDS",
|
|
30
30
|
"group_multiroom": "Multiroom",
|
package/admin/i18n/uk.json
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"ipLabel": "IP",
|
|
24
24
|
"datapointsHeader": "Точки даних",
|
|
25
25
|
"datapointsHint": "Виберіть, які групи функцій створюються як точки даних. Вимкнена група не створюється, а наявні об'єкти видаляються.",
|
|
26
|
-
"group_player": "Відтворення",
|
|
27
|
-
"group_player_help": "Spotify, Net Radio, USB, Bluetooth та інші джерела",
|
|
26
|
+
"group_player": "Відтворення та огляд",
|
|
27
|
+
"group_player_help": "Spotify, Net Radio, USB, Bluetooth та інші джерела — включно з навігацією меню (Net Radio/сервер/USB)",
|
|
28
28
|
"group_tuner": "Тюнер",
|
|
29
29
|
"group_tuner_help": "FM/DAB, частота, RDS",
|
|
30
30
|
"group_multiroom": "Multiroom",
|
package/admin/i18n/zh-cn.json
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"ipLabel": "IP",
|
|
24
24
|
"datapointsHeader": "数据点",
|
|
25
25
|
"datapointsHint": "选择将哪些功能组创建为数据点。关闭的组不会创建,已有对象会被移除。",
|
|
26
|
-
"group_player": "
|
|
27
|
-
"group_player_help": "Spotify、Net Radio、USB
|
|
26
|
+
"group_player": "播放与浏览",
|
|
27
|
+
"group_player_help": "Spotify、Net Radio、USB、蓝牙及其他音源,含菜单浏览(Net Radio/服务器/USB)",
|
|
28
28
|
"group_tuner": "调谐器",
|
|
29
29
|
"group_tuner_help": "FM/DAB、频率、RDS",
|
|
30
30
|
"group_multiroom": "多房间",
|
|
@@ -84,8 +84,10 @@ class YamahaDeviceManagement extends import_dm_utils.DeviceManagement {
|
|
|
84
84
|
async loadDevices(context) {
|
|
85
85
|
var _a;
|
|
86
86
|
for (const card of await this.cards()) {
|
|
87
|
-
const model = await
|
|
88
|
-
|
|
87
|
+
const [model, node] = await Promise.all([
|
|
88
|
+
this.adapter.getForeignStateAsync(`${this.adapter.namespace}.${card.id}.info.model`),
|
|
89
|
+
this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${card.id}`)
|
|
90
|
+
]);
|
|
89
91
|
const label = typeof ((_a = node == null ? void 0 : node.common) == null ? void 0 : _a.name) === "string" ? node.common.name : void 0;
|
|
90
92
|
context.addDevice(
|
|
91
93
|
this.toDeviceInfo(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/device-management.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n DeviceManagement,\n type ActionContext,\n type DeviceInfo,\n type DeviceLoadContext,\n type InstanceDetails,\n} from \"@iobroker/dm-utils\";\nimport { t } from \"./lib/i18n\";\nimport { iconForModel } from \"./lib/device-type\";\nimport { readDiscovered, writeDiscovered } from \"./lib/discovered-store\";\nimport { discoveredStoreDeps } from \"./lib/discovered-store-deps\";\nimport type { DeviceRecord } from \"./lib/types\";\nimport {\n TRANSPORTS,\n buildDeviceForm,\n findClash,\n rowId,\n type CardDevice,\n type ManualRow,\n} from \"./device-management-helpers\";\n\n/**\n * ioBroker device-manager backend: the Yamaha receivers as cards showing the live model,\n * the IP, and which protocols (YNCA/MusicCast/XML) are connected right now, with a manual\n * add-by-IP dialog. The card list follows the running set \u2014 the manual `native.devices`\n * table when it is filled, otherwise the auto-discovered devices \u2014 so it matches exactly\n * what the adapter runs. \"Yamaha\" is never a card line: it is the whole adapter.\n */\nexport class YamahaDeviceManagement extends DeviceManagement {\n /** The instance object id whose `native` holds the manual device table. */\n private get objId(): string {\n return `system.adapter.${this.adapter.namespace}`;\n }\n\n /** Read the manual device table (`native.devices`) as raw rows, keeping the name. */\n private async readManual(): Promise<ManualRow[]> {\n const obj = await this.adapter.getForeignObjectAsync(this.objId);\n const devices = (obj?.native as { devices?: unknown } | undefined)?.devices;\n if (!Array.isArray(devices)) {\n return [];\n }\n return devices.filter(\n (d): d is ManualRow => !!d && typeof (d as ManualRow).ip === \"string\" && (d as ManualRow).ip.length > 0,\n );\n }\n\n /**\n * Persist the manual device table; writing `native.*` restarts the adapter with the new set.\n *\n * @param rows the manual rows to store\n */\n private async writeManual(rows: ManualRow[]): Promise<void> {\n await this.adapter.extendForeignObjectAsync(this.objId, { native: { devices: rows } });\n }\n\n /**\n * The running device set as cards: the manual table when filled (manual mode), otherwise\n * the auto-discovered devices (auto mode) \u2014 the same either/or the adapter itself runs.\n *\n * @returns the cards with their source\n */\n private async cards(): Promise<CardDevice[]> {\n const manual = await this.readManual();\n if (manual.length > 0) {\n const taken = new Set<string>([\"info\"]);\n const cards: CardDevice[] = [];\n for (const row of manual) {\n const id = rowId(row);\n if (taken.has(id)) {\n continue;\n }\n taken.add(id);\n cards.push({ id, ip: row.ip, name: row.name && row.name.length > 0 ? row.name : row.ip, source: \"manual\" });\n }\n return cards;\n }\n const discovered = await readDiscovered(discoveredStoreDeps(this.adapter));\n return discovered.map(d => ({ id: d.id, ip: d.ip, name: d.id, source: \"discovered\" as const }));\n }\n\n /**\n * Populate the manager with one card per running device.\n *\n * @param context the load context\n */\n protected async loadDevices(context: DeviceLoadContext<string>): Promise<void> {\n for (const card of await this.cards()) {\n // The reported model picks the device-class silhouette shown on the card.\n const model = await this.adapter.getForeignStateAsync(`${this.adapter.namespace}.${card.id}.info.model`);\n // The card title follows the device object's name, not the table entry. On an\n // instance upgraded from the previous adapter the table entry is the receiver's\n // ip \u2014 the object carries the readable name the adapter learned from the device.\n // The table entry itself must stay put: the object id is derived from it, and\n // changing that would move the whole tree.\n const node = await this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${card.id}`);\n const label = typeof node?.common?.name === \"string\" ? node.common.name : undefined;\n context.addDevice(\n this.toDeviceInfo(\n label && label !== card.id ? { ...card, name: label } : card,\n typeof model?.val === \"string\" ? model.val : undefined,\n ),\n );\n }\n }\n\n /**\n * Build one device card: the live model, the IP as the identifier line, a connection\n * status, and one indicator per connected protocol (hidden while that protocol is not\n * connected). All live values read from the device's own `info.*` states. A discovered\n * card has no edit action \u2014 it is auto-found and re-found; it can only be removed.\n *\n * @param card the running device\n * @param model the device's reported model name, for the device-class icon\n * @returns the card descriptor\n */\n private toDeviceInfo(card: CardDevice, model?: string): DeviceInfo<string> {\n const base = `${this.adapter.namespace}.${card.id}`;\n const del = {\n id: \"delete\",\n icon: \"delete\",\n description: t(\"dmDelete\"),\n handler: async (id: string, ctx: ActionContext): Promise<{ refresh: \"devices\" }> => this.deleteDevice(id, ctx),\n };\n const edit = {\n id: \"edit\",\n icon: \"edit\",\n description: t(\"dmEdit\"),\n handler: async (id: string, ctx: ActionContext): Promise<{ refresh: \"devices\" }> => this.editDevice(id, ctx),\n };\n return {\n id: card.id,\n name: card.name,\n icon: iconForModel(model),\n model: { stateId: `${base}.info.model` },\n identifier: card.ip,\n status: {\n connection: { stateId: `${base}.info.connection`, mapping: { true: \"connected\", false: \"disconnected\" } },\n },\n // No icon: the indicator icon accepts only a reserved/`fa-*`/`data:`/URL name, so a plain\n // \"wifi\" rendered as a \"?\". The transport label as text plus a green \"on\" colour carries it;\n // `hideIfEmpty` shows only the protocols this device is actually connected over.\n indicators: TRANSPORTS.map(tr => ({\n id: `transport-${tr.id}`,\n value: { stateId: `${base}.info.transports.${tr.id}` },\n text: tr.label,\n colorOn: \"ok\",\n hideIfEmpty: true,\n })),\n actions: card.source === \"manual\" ? [edit, del] : [del],\n };\n }\n\n /**\n * The \"+ add\" action above the list and the label of the identifier line (the IP).\n *\n * @returns the instance action descriptor\n */\n protected getInstanceInfo(): InstanceDetails {\n return {\n apiVersion: \"v3\",\n identifierLabel: t(\"ipLabel\"),\n actions: [{ id: \"add\", icon: \"add\", description: t(\"dmAdd\"), handler: async ctx => this.addDevice(ctx) }],\n };\n }\n\n /**\n * Manual add: show the name+IP form, then append the device to `native.devices` (which\n * restarts the adapter and switches it to manual mode).\n *\n * @param context the action context\n * @returns a directive to reload the manager\n */\n private async addDevice(context: ActionContext): Promise<{ refresh: boolean }> {\n const manual = await this.readManual();\n const data = await context.showForm(buildDeviceForm(manual.map(r => r.ip)), { title: t(\"dmAdd\") });\n if (data && typeof data.ip === \"string\" && data.ip.trim()) {\n const row: ManualRow = { name: typeof data.name === \"string\" ? data.name.trim() : \"\", ip: data.ip.trim() };\n const clash = findClash(manual, row, -1);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: true };\n }\n manual.push(row);\n await this.writeManual(manual);\n }\n return { refresh: true };\n }\n\n /**\n * Edit a manual device via the pre-filled form (edit is offered on manual cards only).\n *\n * @param cardId the card id (= the object-tree device id)\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async editDevice(cardId: string, context: ActionContext): Promise<{ refresh: \"devices\" }> {\n const manual = await this.readManual();\n const index = manual.findIndex(r => rowId(r) === cardId);\n if (index < 0) {\n return { refresh: \"devices\" };\n }\n const current = manual[index];\n const usedIps = manual.filter((_, i) => i !== index).map(r => r.ip);\n const data = await context.showForm(buildDeviceForm(usedIps), {\n title: t(\"dmEditTitle\"),\n data: { name: current.name ?? \"\", ip: current.ip },\n });\n if (data && typeof data.ip === \"string\" && data.ip.trim()) {\n const row: ManualRow = { name: typeof data.name === \"string\" ? data.name.trim() : \"\", ip: data.ip.trim() };\n const clash = findClash(manual, row, index);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: \"devices\" };\n }\n manual[index] = row;\n await this.writeManual(manual);\n }\n return { refresh: \"devices\" };\n }\n\n /**\n * Delete a device after confirmation, from whichever source it came from: a manual card\n * from `native.devices`, a discovered card from the discovery store \u2014 the latter so the\n * standby-protection merge does not resurrect it on the next start.\n *\n * @param cardId the card id (= the object-tree device id)\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async deleteDevice(cardId: string, context: ActionContext): Promise<{ refresh: \"devices\" }> {\n const manual = await this.readManual();\n if (manual.length > 0) {\n const index = manual.findIndex(r => rowId(r) === cardId);\n if (index < 0) {\n return { refresh: \"devices\" };\n }\n const confirmed = await context.showConfirmation(t(\"dmDeleteConfirm\", manual[index].name || manual[index].ip));\n if (confirmed) {\n manual.splice(index, 1);\n await this.writeManual(manual);\n }\n return { refresh: \"devices\" };\n }\n const store = discoveredStoreDeps(this.adapter);\n const discovered = await readDiscovered(store);\n const remaining = discovered.filter((d: DeviceRecord) => d.id !== cardId);\n if (remaining.length !== discovered.length) {\n const confirmed = await context.showConfirmation(t(\"dmDeleteConfirm\", cardId));\n if (confirmed) {\n await writeDiscovered(store, remaining);\n }\n }\n return { refresh: \"devices\" };\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAMO;AACP,kBAAkB;AAClB,yBAA6B;AAC7B,8BAAgD;AAChD,mCAAoC;AAEpC,uCAOO;AASA,MAAM,+BAA+B,iCAAiB;AAAA;AAAA,EAE3D,IAAY,QAAgB;AAC1B,WAAO,kBAAkB,KAAK,QAAQ,SAAS;AAAA,EACjD;AAAA;AAAA,EAGA,MAAc,aAAmC;AAnCnD;AAoCI,UAAM,MAAM,MAAM,KAAK,QAAQ,sBAAsB,KAAK,KAAK;AAC/D,UAAM,WAAW,gCAAK,WAAL,mBAAmD;AACpE,QAAI,CAAC,MAAM,QAAQ,OAAO,GAAG;AAC3B,aAAO,CAAC;AAAA,IACV;AACA,WAAO,QAAQ;AAAA,MACb,CAAC,MAAsB,CAAC,CAAC,KAAK,OAAQ,EAAgB,OAAO,YAAa,EAAgB,GAAG,SAAS;AAAA,IACxG;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,YAAY,MAAkC;AAC1D,UAAM,KAAK,QAAQ,yBAAyB,KAAK,OAAO,EAAE,QAAQ,EAAE,SAAS,KAAK,EAAE,CAAC;AAAA,EACvF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,QAA+B;AAC3C,UAAM,SAAS,MAAM,KAAK,WAAW;AACrC,QAAI,OAAO,SAAS,GAAG;AACrB,YAAM,QAAQ,oBAAI,IAAY,CAAC,MAAM,CAAC;AACtC,YAAM,QAAsB,CAAC;AAC7B,iBAAW,OAAO,QAAQ;AACxB,cAAM,SAAK,wCAAM,GAAG;AACpB,YAAI,MAAM,IAAI,EAAE,GAAG;AACjB;AAAA,QACF;AACA,cAAM,IAAI,EAAE;AACZ,cAAM,KAAK,EAAE,IAAI,IAAI,IAAI,IAAI,MAAM,IAAI,QAAQ,IAAI,KAAK,SAAS,IAAI,IAAI,OAAO,IAAI,IAAI,QAAQ,SAAS,CAAC;AAAA,MAC5G;AACA,aAAO;AAAA,IACT;AACA,UAAM,aAAa,UAAM,4CAAe,kDAAoB,KAAK,OAAO,CAAC;AACzE,WAAO,WAAW,IAAI,QAAM,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,IAAI,MAAM,EAAE,IAAI,QAAQ,aAAsB,EAAE;AAAA,EAChG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAgB,YAAY,SAAmD;AArFjF;AAsFI,eAAW,QAAQ,MAAM,KAAK,MAAM,GAAG;
|
|
4
|
+
"sourcesContent": ["import {\n DeviceManagement,\n type ActionContext,\n type DeviceInfo,\n type DeviceLoadContext,\n type InstanceDetails,\n} from \"@iobroker/dm-utils\";\nimport { t } from \"./lib/i18n\";\nimport { iconForModel } from \"./lib/device-type\";\nimport { readDiscovered, writeDiscovered } from \"./lib/discovered-store\";\nimport { discoveredStoreDeps } from \"./lib/discovered-store-deps\";\nimport type { DeviceRecord } from \"./lib/types\";\nimport {\n TRANSPORTS,\n buildDeviceForm,\n findClash,\n rowId,\n type CardDevice,\n type ManualRow,\n} from \"./device-management-helpers\";\n\n/**\n * ioBroker device-manager backend: the Yamaha receivers as cards showing the live model,\n * the IP, and which protocols (YNCA/MusicCast/XML) are connected right now, with a manual\n * add-by-IP dialog. The card list follows the running set \u2014 the manual `native.devices`\n * table when it is filled, otherwise the auto-discovered devices \u2014 so it matches exactly\n * what the adapter runs. \"Yamaha\" is never a card line: it is the whole adapter.\n */\nexport class YamahaDeviceManagement extends DeviceManagement {\n /** The instance object id whose `native` holds the manual device table. */\n private get objId(): string {\n return `system.adapter.${this.adapter.namespace}`;\n }\n\n /** Read the manual device table (`native.devices`) as raw rows, keeping the name. */\n private async readManual(): Promise<ManualRow[]> {\n const obj = await this.adapter.getForeignObjectAsync(this.objId);\n const devices = (obj?.native as { devices?: unknown } | undefined)?.devices;\n if (!Array.isArray(devices)) {\n return [];\n }\n return devices.filter(\n (d): d is ManualRow => !!d && typeof (d as ManualRow).ip === \"string\" && (d as ManualRow).ip.length > 0,\n );\n }\n\n /**\n * Persist the manual device table; writing `native.*` restarts the adapter with the new set.\n *\n * @param rows the manual rows to store\n */\n private async writeManual(rows: ManualRow[]): Promise<void> {\n await this.adapter.extendForeignObjectAsync(this.objId, { native: { devices: rows } });\n }\n\n /**\n * The running device set as cards: the manual table when filled (manual mode), otherwise\n * the auto-discovered devices (auto mode) \u2014 the same either/or the adapter itself runs.\n *\n * @returns the cards with their source\n */\n private async cards(): Promise<CardDevice[]> {\n const manual = await this.readManual();\n if (manual.length > 0) {\n const taken = new Set<string>([\"info\"]);\n const cards: CardDevice[] = [];\n for (const row of manual) {\n const id = rowId(row);\n if (taken.has(id)) {\n continue;\n }\n taken.add(id);\n cards.push({ id, ip: row.ip, name: row.name && row.name.length > 0 ? row.name : row.ip, source: \"manual\" });\n }\n return cards;\n }\n const discovered = await readDiscovered(discoveredStoreDeps(this.adapter));\n return discovered.map(d => ({ id: d.id, ip: d.ip, name: d.id, source: \"discovered\" as const }));\n }\n\n /**\n * Populate the manager with one card per running device.\n *\n * @param context the load context\n */\n protected async loadDevices(context: DeviceLoadContext<string>): Promise<void> {\n for (const card of await this.cards()) {\n // Model and object are independent reads \u2014 fetch them together so a card with\n // several devices does not add up their round-trips.\n const [model, node] = await Promise.all([\n this.adapter.getForeignStateAsync(`${this.adapter.namespace}.${card.id}.info.model`),\n this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${card.id}`),\n ]);\n // The card title follows the device object's name, not the table entry. On an\n // instance upgraded from the previous adapter the table entry is the receiver's\n // ip \u2014 the object carries the readable name the adapter learned from the device.\n // The table entry itself must stay put: the object id is derived from it, and\n // changing that would move the whole tree.\n const label = typeof node?.common?.name === \"string\" ? node.common.name : undefined;\n context.addDevice(\n this.toDeviceInfo(\n label && label !== card.id ? { ...card, name: label } : card,\n typeof model?.val === \"string\" ? model.val : undefined,\n ),\n );\n }\n }\n\n /**\n * Build one device card: the live model, the IP as the identifier line, a connection\n * status, and one indicator per connected protocol (hidden while that protocol is not\n * connected). All live values read from the device's own `info.*` states. A discovered\n * card has no edit action \u2014 it is auto-found and re-found; it can only be removed.\n *\n * @param card the running device\n * @param model the device's reported model name, for the device-class icon\n * @returns the card descriptor\n */\n private toDeviceInfo(card: CardDevice, model?: string): DeviceInfo<string> {\n const base = `${this.adapter.namespace}.${card.id}`;\n const del = {\n id: \"delete\",\n icon: \"delete\",\n description: t(\"dmDelete\"),\n handler: async (id: string, ctx: ActionContext): Promise<{ refresh: \"devices\" }> => this.deleteDevice(id, ctx),\n };\n const edit = {\n id: \"edit\",\n icon: \"edit\",\n description: t(\"dmEdit\"),\n handler: async (id: string, ctx: ActionContext): Promise<{ refresh: \"devices\" }> => this.editDevice(id, ctx),\n };\n return {\n id: card.id,\n name: card.name,\n icon: iconForModel(model),\n model: { stateId: `${base}.info.model` },\n identifier: card.ip,\n status: {\n connection: { stateId: `${base}.info.connection`, mapping: { true: \"connected\", false: \"disconnected\" } },\n },\n // No icon: the indicator icon accepts only a reserved/`fa-*`/`data:`/URL name, so a plain\n // \"wifi\" rendered as a \"?\". The transport label as text plus a green \"on\" colour carries it;\n // `hideIfEmpty` shows only the protocols this device is actually connected over.\n indicators: TRANSPORTS.map(tr => ({\n id: `transport-${tr.id}`,\n value: { stateId: `${base}.info.transports.${tr.id}` },\n text: tr.label,\n colorOn: \"ok\",\n hideIfEmpty: true,\n })),\n actions: card.source === \"manual\" ? [edit, del] : [del],\n };\n }\n\n /**\n * The \"+ add\" action above the list and the label of the identifier line (the IP).\n *\n * @returns the instance action descriptor\n */\n protected getInstanceInfo(): InstanceDetails {\n return {\n apiVersion: \"v3\",\n identifierLabel: t(\"ipLabel\"),\n actions: [{ id: \"add\", icon: \"add\", description: t(\"dmAdd\"), handler: async ctx => this.addDevice(ctx) }],\n };\n }\n\n /**\n * Manual add: show the name+IP form, then append the device to `native.devices` (which\n * restarts the adapter and switches it to manual mode).\n *\n * @param context the action context\n * @returns a directive to reload the manager\n */\n private async addDevice(context: ActionContext): Promise<{ refresh: boolean }> {\n const manual = await this.readManual();\n const data = await context.showForm(buildDeviceForm(manual.map(r => r.ip)), { title: t(\"dmAdd\") });\n if (data && typeof data.ip === \"string\" && data.ip.trim()) {\n const row: ManualRow = { name: typeof data.name === \"string\" ? data.name.trim() : \"\", ip: data.ip.trim() };\n const clash = findClash(manual, row, -1);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: true };\n }\n manual.push(row);\n await this.writeManual(manual);\n }\n return { refresh: true };\n }\n\n /**\n * Edit a manual device via the pre-filled form (edit is offered on manual cards only).\n *\n * @param cardId the card id (= the object-tree device id)\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async editDevice(cardId: string, context: ActionContext): Promise<{ refresh: \"devices\" }> {\n const manual = await this.readManual();\n const index = manual.findIndex(r => rowId(r) === cardId);\n if (index < 0) {\n return { refresh: \"devices\" };\n }\n const current = manual[index];\n const usedIps = manual.filter((_, i) => i !== index).map(r => r.ip);\n const data = await context.showForm(buildDeviceForm(usedIps), {\n title: t(\"dmEditTitle\"),\n data: { name: current.name ?? \"\", ip: current.ip },\n });\n if (data && typeof data.ip === \"string\" && data.ip.trim()) {\n const row: ManualRow = { name: typeof data.name === \"string\" ? data.name.trim() : \"\", ip: data.ip.trim() };\n const clash = findClash(manual, row, index);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: \"devices\" };\n }\n manual[index] = row;\n await this.writeManual(manual);\n }\n return { refresh: \"devices\" };\n }\n\n /**\n * Delete a device after confirmation, from whichever source it came from: a manual card\n * from `native.devices`, a discovered card from the discovery store \u2014 the latter so the\n * standby-protection merge does not resurrect it on the next start.\n *\n * @param cardId the card id (= the object-tree device id)\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async deleteDevice(cardId: string, context: ActionContext): Promise<{ refresh: \"devices\" }> {\n const manual = await this.readManual();\n if (manual.length > 0) {\n const index = manual.findIndex(r => rowId(r) === cardId);\n if (index < 0) {\n return { refresh: \"devices\" };\n }\n const confirmed = await context.showConfirmation(t(\"dmDeleteConfirm\", manual[index].name || manual[index].ip));\n if (confirmed) {\n manual.splice(index, 1);\n await this.writeManual(manual);\n }\n return { refresh: \"devices\" };\n }\n const store = discoveredStoreDeps(this.adapter);\n const discovered = await readDiscovered(store);\n const remaining = discovered.filter((d: DeviceRecord) => d.id !== cardId);\n if (remaining.length !== discovered.length) {\n const confirmed = await context.showConfirmation(t(\"dmDeleteConfirm\", cardId));\n if (confirmed) {\n await writeDiscovered(store, remaining);\n }\n }\n return { refresh: \"devices\" };\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -31,10 +31,12 @@ var import_xml_client = require("./xml/xml-client");
|
|
|
31
31
|
var import_multi_transport_handle = require("./lifecycle/multi-transport-handle");
|
|
32
32
|
var import_transport_connection_adapter = require("./lifecycle/transport-connection-adapter");
|
|
33
33
|
var import_reconnect_strategy = require("./lifecycle/reconnect-strategy");
|
|
34
|
+
var import_command_gate = require("./lifecycle/command-gate");
|
|
34
35
|
var import_ready_line = require("./ready-line");
|
|
35
36
|
var import_util = require("./util");
|
|
36
37
|
const TRANSPORT_RECONNECT_BASE_MS = 1e3;
|
|
37
38
|
const TRANSPORT_RECONNECT_MAX_MS = 6e4;
|
|
39
|
+
const COMMAND_SPACING_MS = { ynca: 100, yxc: 0, xml: 0 };
|
|
38
40
|
async function connectTransports(deviceId, attempts, deps) {
|
|
39
41
|
var _a, _b, _c;
|
|
40
42
|
const results = await Promise.all(
|
|
@@ -84,48 +86,61 @@ async function connectTransports(deviceId, attempts, deps) {
|
|
|
84
86
|
}
|
|
85
87
|
function attemptDevice(device, deps) {
|
|
86
88
|
const { log, upsertObject, setStateAck, timers } = deps;
|
|
89
|
+
const gateFor = (transport) => new import_command_gate.CommandGate({ minSpacingMs: COMMAND_SPACING_MS[transport], timers });
|
|
87
90
|
const buildYnca = () => {
|
|
88
91
|
const ynca = new import_transport_connection_adapter.TransportConnectionAdapter("ynca", device.id, setStateAck);
|
|
92
|
+
const gate = gateFor("ynca");
|
|
89
93
|
ynca.bind(
|
|
90
94
|
new import_device_controller.YncaDeviceController(device.id, {
|
|
91
|
-
client: new import_ynca_client.YncaClient(device.ip, timers),
|
|
95
|
+
client: new import_ynca_client.YncaClient(device.ip, timers, gate),
|
|
96
|
+
gate,
|
|
92
97
|
upsertObject: ynca.interceptUpsert,
|
|
93
98
|
setStateAck: ynca.interceptSetStateAck,
|
|
94
99
|
log,
|
|
95
100
|
isEntryEnabled: deps.isEntryEnabled,
|
|
96
|
-
subunitCache: deps.yncaSubunitCache
|
|
101
|
+
subunitCache: deps.yncaSubunitCache,
|
|
102
|
+
probeMemory: deps.probeMemory
|
|
97
103
|
})
|
|
98
104
|
);
|
|
99
105
|
return ynca;
|
|
100
106
|
};
|
|
101
107
|
const buildYxc = () => {
|
|
102
108
|
const yxc = new import_transport_connection_adapter.TransportConnectionAdapter("yxc", device.id, setStateAck);
|
|
109
|
+
const gate = gateFor("yxc");
|
|
103
110
|
yxc.bind(
|
|
104
111
|
new import_device_controller2.YxcDeviceController(device.id, {
|
|
105
|
-
client: new import_http_client.YamahaYxcClient(device.ip),
|
|
106
|
-
// Resolve another configured device's client for a multiroom link — never this device
|
|
112
|
+
client: new import_http_client.YamahaYxcClient(device.ip, void 0, gate),
|
|
113
|
+
// Resolve another configured device's client for a multiroom link — never this device
|
|
114
|
+
// itself. The partner's own gate belongs to its own connection, so this one-off client
|
|
115
|
+
// stays ungated (a single link call, not a stream of commands).
|
|
107
116
|
clientFor: (ip) => ip !== device.ip && deps.knownDeviceIps.has(ip) ? new import_http_client.YamahaYxcClient(ip) : void 0,
|
|
108
117
|
registerPush: (onPush) => deps.registerPush(device.ip, onPush),
|
|
118
|
+
pushActive: deps.pushActive,
|
|
119
|
+
probeMemory: deps.probeMemory,
|
|
109
120
|
scheduleKeepalive: deps.scheduleKeepalive,
|
|
110
121
|
upsertObject: yxc.interceptUpsert,
|
|
111
122
|
setStateAck: yxc.interceptSetStateAck,
|
|
112
123
|
reportDeviceName: deps.onDeviceName,
|
|
113
|
-
log
|
|
124
|
+
log,
|
|
125
|
+
gate
|
|
114
126
|
})
|
|
115
127
|
);
|
|
116
128
|
return yxc;
|
|
117
129
|
};
|
|
118
130
|
const buildXml = () => {
|
|
119
131
|
const xml = new import_transport_connection_adapter.TransportConnectionAdapter("xml", device.id, setStateAck);
|
|
132
|
+
const gate = gateFor("xml");
|
|
120
133
|
xml.bind(
|
|
121
134
|
new import_device_controller3.XmlDeviceController(
|
|
122
135
|
device.id,
|
|
123
136
|
{
|
|
124
|
-
client: new import_xml_client.XmlClient(device.ip),
|
|
137
|
+
client: new import_xml_client.XmlClient(device.ip, void 0, gate),
|
|
125
138
|
scheduleKeepalive: deps.scheduleKeepalive,
|
|
126
139
|
upsertObject: xml.interceptUpsert,
|
|
127
140
|
setStateAck: xml.interceptSetStateAck,
|
|
128
|
-
log
|
|
141
|
+
log,
|
|
142
|
+
gate,
|
|
143
|
+
probeMemory: deps.probeMemory
|
|
129
144
|
},
|
|
130
145
|
deps.xmlPollIntervalMs
|
|
131
146
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/attempt-device.ts"],
|
|
4
|
-
"sourcesContent": ["import { YncaClient } from \"./ynca/ynca-client\";\nimport { YncaDeviceController } from \"./device-controller\";\nimport { YxcDeviceController } from \"./yxc/device-controller\";\nimport { YamahaYxcClient } from \"./yxc/http-client\";\nimport { XmlDeviceController } from \"./xml/device-controller\";\nimport { XmlClient } from \"./xml/xml-client\";\nimport { MultiTransportHandle, type ConnectableTransport } from \"./lifecycle/multi-transport-handle\";\nimport { TransportConnectionAdapter } from \"./lifecycle/transport-connection-adapter\";\nimport { ReconnectStrategy } from \"./lifecycle/reconnect-strategy\";\nimport type { ReachabilityDedup } from \"./lifecycle/reachability-dedup\";\nimport type { YncaSubunitCache } from \"./ynca/subunit-cache\";\nimport type { Transport } from \"./catalog/owner-policy\";\nimport { readyLine } from \"./ready-line\";\nimport { errorMessage } from \"./util\";\nimport type { ConnectionHandle, ControllerLog } from \"./controller\";\nimport type { ObjectDef } from \"./catalog/types\";\nimport type { DeviceRecord } from \"./types\";\n\n// Re-exported so existing importers (tests) keep resolving it from here.\nexport type { ConnectableTransport };\n\n/** Per-transport reconnect backoff bounds \u2014 same shape as the device supervisor's. */\nconst TRANSPORT_RECONNECT_BASE_MS = 1000;\nconst TRANSPORT_RECONNECT_MAX_MS = 60000;\n\n/** The adapter-bound callbacks {@link attemptDevice} drives \u2014 injected so it needs no adapter. */\nexport interface AttemptDeps {\n /** Adapter log. */\n log: ControllerLog;\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-managed timers (YNCA pacing + per-transport reconnects). */\n timers: {\n /** Schedule a one-shot timer. */\n schedule(handler: () => void, ms: number): ioBroker.Timeout | undefined;\n /** Cancel a scheduled timer. */\n cancel(handle: ioBroker.Timeout | undefined): void;\n };\n /** Register a YXC push handler for a device IP; returns a function that unregisters it. */\n registerPush(ip: string, onPush: (event: unknown) => void): () => void;\n /** Schedule a repeating keepalive; returns a function that cancels it. */\n scheduleKeepalive(handler: () => void, ms: number): () => void;\n /** How often to poll an XML/YNC device for state (ms). */\n xmlPollIntervalMs: number;\n /** Report the transports that are live after every change \u2014 the id-safe names (\"ynca\"/\"yxc\"/\"xml\"). */\n onTransports?(names: string[]): void;\n /** Report the name the device carries for itself (MusicCast), for the device object's label. */\n onDeviceName?(name: string): void;\n /** IPs of all configured devices, so a MusicCast group can resolve a client device by IP. */\n knownDeviceIps: Set<string>;\n /** Dedup for the \"no reachable transport\" warning \u2014 see {@link ConnectDeps.reachability}. */\n reachability?: ReachabilityDedup;\n /** Datapoint-group gate for the YNCA sweep \u2014 a disabled group's functions are never fetched. */\n isEntryEnabled?(id: string): boolean;\n /** Per-device cache of the YNCA AVAIL probe, held by the caller across reconnects. */\n yncaSubunitCache?: YncaSubunitCache;\n}\n\n/** One transport to try: its name and a factory building a FRESH connectable (also for reconnects). */\nexport interface TransportAttempt {\n /** The transport this attempt stands for. */\n transport: Transport;\n /** Build a fresh connectable transport (a controller behind its adapter). */\n build(): ConnectableTransport;\n}\n\n/** The adapter callbacks {@link connectTransports} drives to build and hold the unified tree. */\nexport interface ConnectDeps {\n /** Adapter log. */\n log: ControllerLog;\n /** Create or update an object in the device tree. */\n upsertObject(id: string, def: ObjectDef): Promise<void>;\n /** Report the transports that are live after every change \u2014 the id-safe names (\"ynca\"/\"yxc\"/\"xml\"). */\n onTransports?(names: string[]): void;\n /**\n * Dedup for the \"no reachable transport\" warning: without it every retry warns\n * again for as long as the device stays offline (nut2 `failedUps` pattern \u2014 first\n * failure warns, repeats stay at debug until the device answers again).\n */\n reachability?: ReachabilityDedup;\n /** Timers for the per-transport reconnect loops (absent in tests \u2192 no per-transport retry). */\n timers?: {\n /** Schedule a one-shot timer. */\n schedule(handler: () => void, ms: number): ioBroker.Timeout | undefined;\n /** Cancel a scheduled timer. */\n cancel(handle: ioBroker.Timeout | undefined): void;\n };\n}\n\n/**\n * Bring every answering transport online on ONE object tree. All candidates connect IN\n * PARALLEL (a YNCA connect timeout or long sweep no longer delays YXC/XML); the ones that\n * answer are handed to a single {@link MultiTransportHandle}, which unifies their catalogs\n * (each capability owned by exactly one transport \u2014 most-modern-but-lossless, see the\n * object-tree coordinator), routes user writes to the owner, and reconnects a single\n * dropped transport on its own while the others keep running. Ownership is resolved by\n * rank over the connected set, so the connect order/timing never changes it. A transport\n * that does not answer, or whose connect throws, is closed and left out. Returns the\n * handle over the live set, or null when no transport answered (the device is offline\n * this attempt).\n *\n * @param deviceId the id-safe device id\n * @param attempts the transports to try\n * @param deps the adapter callbacks (upsert + log + timers)\n * @returns a connection handle over the live transports, or null when none connected\n */\nexport async function connectTransports(\n deviceId: string,\n attempts: readonly TransportAttempt[],\n deps: ConnectDeps,\n): Promise<ConnectionHandle | null> {\n const results = await Promise.all(\n attempts.map(async attempt => {\n const conn = attempt.build();\n try {\n if (await conn.connect()) {\n return conn;\n }\n } catch (e) {\n deps.log.debug(`${deviceId}/${conn.transport}: transport did not connect (${errorMessage(e)})`);\n }\n conn.close();\n return null;\n }),\n );\n const live = results.filter((conn): conn is ConnectableTransport => conn !== null);\n if (live.length === 0) {\n const level = deps.reachability?.reportUnreachable() ?? \"warn\";\n deps.log[level](`${deviceId}: no reachable transport (YNCA/YXC/XML)`);\n return null;\n }\n deps.reachability?.reportReachable();\n const rebuilds = new Map(attempts.map(attempt => [attempt.transport, attempt.build] as const));\n const handle = new MultiTransportHandle(deviceId, live, {\n upsertObject: deps.upsertObject,\n log: deps.log,\n onTransports: deps.onTransports,\n rebuild: deps.timers ? transport => rebuilds.get(transport)!() : undefined,\n schedule: deps.timers ? (cb, ms) => deps.timers!.schedule(cb, ms) : undefined,\n cancel: deps.timers ? handle_ => deps.timers!.cancel(handle_ as ioBroker.Timeout | undefined) : undefined,\n backoffFactory: () => new ReconnectStrategy(TRANSPORT_RECONNECT_BASE_MS, TRANSPORT_RECONNECT_MAX_MS),\n });\n try {\n await handle.start();\n } catch (e) {\n // Building the unified tree failed (e.g. object creation errored): close every live\n // transport before rethrowing, or the supervisor's retry would leak sockets and timers \u2014\n // fatal for YNCA, where the receiver allows only ONE connection and a zombie socket\n // would block every future attempt until the adapter restarts.\n handle.close();\n throw e;\n }\n // One summary line instead of three per-transport \"ready\" lines; each controller logs its\n // own readiness at debug level for diagnostics.\n deps.log.info(\n readyLine(\n deviceId,\n live.map(conn => conn.transport),\n ),\n );\n return handle;\n}\n\n/**\n * Bring one device online across ALL its transports. Every transport that answers \u2014 YNCA (amp\n * control over a held TCP connection), YXC (MusicCast, push + poll), XML/YNC (pre-2010) \u2014 is built\n * behind a {@link TransportConnectionAdapter} and connected in parallel on one object tree, so a\n * MusicCast AVR gets YNCA base control AND the YXC-exclusive richness (multiroom, equalizer, album\n * art) instead of one transport winning and hiding the rest. Each transport is described by a\n * factory, so the handle can rebuild and reconnect a single dropped transport while the others\n * keep running. Returns a {@link ConnectionHandle} over the live set, or null when no transport\n * answers this attempt.\n *\n * @param device the configured device record\n * @param deps the adapter-bound callbacks\n * @returns a connection handle, or null when no transport connected\n */\nexport function attemptDevice(device: DeviceRecord, deps: AttemptDeps): Promise<ConnectionHandle | null> {\n const { log, upsertObject, setStateAck, timers } = deps;\n\n // 1) YNCA \u2014 amp control over a held TCP connection; a socket drop is the genuine gone-signal.\n const buildYnca = (): ConnectableTransport => {\n const ynca = new TransportConnectionAdapter(\"ynca\", device.id, setStateAck);\n ynca.bind(\n new YncaDeviceController(device.id, {\n client: new YncaClient(device.ip, timers),\n upsertObject: ynca.interceptUpsert,\n setStateAck: ynca.interceptSetStateAck,\n log,\n isEntryEnabled: deps.isEntryEnabled,\n subunitCache: deps.yncaSubunitCache,\n }),\n );\n return ynca;\n };\n\n // 2) YXC \u2014 MusicCast; polled + push. Drop reported after a run of failed keepalive polls.\n const buildYxc = (): ConnectableTransport => {\n const yxc = new TransportConnectionAdapter(\"yxc\", device.id, setStateAck);\n yxc.bind(\n new YxcDeviceController(device.id, {\n client: new YamahaYxcClient(device.ip),\n // Resolve another configured device's client for a multiroom link \u2014 never this device itself.\n clientFor: ip => (ip !== device.ip && deps.knownDeviceIps.has(ip) ? new YamahaYxcClient(ip) : undefined),\n registerPush: onPush => deps.registerPush(device.ip, onPush),\n scheduleKeepalive: deps.scheduleKeepalive,\n upsertObject: yxc.interceptUpsert,\n setStateAck: yxc.interceptSetStateAck,\n reportDeviceName: deps.onDeviceName,\n log,\n }),\n );\n return yxc;\n };\n\n // 3) XML/YNC \u2014 pre-2010 receivers; polled. A drop is reported after a run of failed polls.\n const buildXml = (): ConnectableTransport => {\n const xml = new TransportConnectionAdapter(\"xml\", device.id, setStateAck);\n xml.bind(\n new XmlDeviceController(\n device.id,\n {\n client: new XmlClient(device.ip),\n scheduleKeepalive: deps.scheduleKeepalive,\n upsertObject: xml.interceptUpsert,\n setStateAck: xml.interceptSetStateAck,\n log,\n },\n deps.xmlPollIntervalMs,\n ),\n );\n return xml;\n };\n\n return connectTransports(\n device.id,\n [\n { transport: \"ynca\", build: buildYnca },\n { transport: \"yxc\", build: buildYxc },\n { transport: \"xml\", build: buildXml },\n ],\n {\n upsertObject,\n log,\n onTransports: deps.onTransports,\n reachability: deps.reachability,\n timers: deps.timers,\n },\n );\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAA2B;AAC3B,+BAAqC;AACrC,IAAAA,4BAAoC;AACpC,yBAAgC;AAChC,IAAAA,4BAAoC;AACpC,wBAA0B;AAC1B,oCAAgE;AAChE,0CAA2C;AAC3C,gCAAkC;
|
|
4
|
+
"sourcesContent": ["import { YncaClient } from \"./ynca/ynca-client\";\nimport { YncaDeviceController } from \"./device-controller\";\nimport { YxcDeviceController } from \"./yxc/device-controller\";\nimport { YamahaYxcClient } from \"./yxc/http-client\";\nimport { XmlDeviceController } from \"./xml/device-controller\";\nimport { XmlClient } from \"./xml/xml-client\";\nimport { MultiTransportHandle, type ConnectableTransport } from \"./lifecycle/multi-transport-handle\";\nimport { TransportConnectionAdapter } from \"./lifecycle/transport-connection-adapter\";\nimport { ReconnectStrategy } from \"./lifecycle/reconnect-strategy\";\nimport { CommandGate } from \"./lifecycle/command-gate\";\nimport type { ReachabilityDedup } from \"./lifecycle/reachability-dedup\";\nimport type { YncaSubunitCache } from \"./ynca/subunit-cache\";\nimport type { ProbeMemory } from \"./lifecycle/probe-memory\";\nimport type { Transport } from \"./catalog/owner-policy\";\nimport { readyLine } from \"./ready-line\";\nimport { errorMessage } from \"./util\";\nimport type { ConnectionHandle, ControllerLog } from \"./controller\";\nimport type { ObjectDef } from \"./catalog/types\";\nimport type { DeviceRecord } from \"./types\";\n\n// Re-exported so existing importers (tests) keep resolving it from here.\nexport type { ConnectableTransport };\n\n/** Per-transport reconnect backoff bounds \u2014 same shape as the device supervisor's. */\nconst TRANSPORT_RECONNECT_BASE_MS = 1000;\nconst TRANSPORT_RECONNECT_MAX_MS = 60000;\n\n/**\n * Minimum spacing between two commands, per transport. YNCA's 100 ms is Yamaha's\n * specification (`ynca-python` protocol.py: \"YNCA spec specifies that there should be at\n * least 100 milliseconds between commands\"). The HTTP transports have no documented\n * spacing \u2014 0 ms, but they still run through a gate, which serialises them so an embedded\n * device never faces a burst of parallel requests.\n */\nconst COMMAND_SPACING_MS: Readonly<Record<Transport, number>> = { ynca: 100, yxc: 0, xml: 0 };\n\n/** The adapter-bound callbacks {@link attemptDevice} drives \u2014 injected so it needs no adapter. */\nexport interface AttemptDeps {\n /** Adapter log. */\n log: ControllerLog;\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-managed timers (YNCA pacing + per-transport reconnects). */\n timers: {\n /** Schedule a one-shot timer. */\n schedule(handler: () => void, ms: number): ioBroker.Timeout | undefined;\n /** Cancel a scheduled timer. */\n cancel(handle: ioBroker.Timeout | undefined): void;\n };\n /** Register a YXC push handler for a device IP; returns a function that unregisters it. */\n registerPush(ip: string, onPush: (event: unknown) => void): () => void;\n /** Whether the shared push receiver is listening (decides how much the keepalive polls). */\n pushActive?(): boolean;\n /** Schedule a repeating keepalive; returns a function that cancels it. */\n scheduleKeepalive(handler: () => void, ms: number): () => void;\n /** How often to poll an XML/YNC device for state (ms). */\n xmlPollIntervalMs: number;\n /** Report the transports that are live after every change \u2014 the id-safe names (\"ynca\"/\"yxc\"/\"xml\"). */\n onTransports?(names: string[]): void;\n /** Report the name the device carries for itself (MusicCast), for the device object's label. */\n onDeviceName?(name: string): void;\n /** IPs of all configured devices, so a MusicCast group can resolve a client device by IP. */\n knownDeviceIps: Set<string>;\n /** Dedup for the \"no reachable transport\" warning \u2014 see {@link ConnectDeps.reachability}. */\n reachability?: ReachabilityDedup;\n /** Datapoint-group gate for the YNCA sweep \u2014 a disabled group's functions are never fetched. */\n isEntryEnabled?(id: string): boolean;\n /** Per-device cache of the YNCA AVAIL probe, held by the caller across reconnects. */\n yncaSubunitCache?: YncaSubunitCache;\n /** Per-device memory for device answers that stay constant while it runs (held by the caller). */\n probeMemory?: ProbeMemory;\n}\n\n/** One transport to try: its name and a factory building a FRESH connectable (also for reconnects). */\nexport interface TransportAttempt {\n /** The transport this attempt stands for. */\n transport: Transport;\n /** Build a fresh connectable transport (a controller behind its adapter). */\n build(): ConnectableTransport;\n}\n\n/** The adapter callbacks {@link connectTransports} drives to build and hold the unified tree. */\nexport interface ConnectDeps {\n /** Adapter log. */\n log: ControllerLog;\n /** Create or update an object in the device tree. */\n upsertObject(id: string, def: ObjectDef): Promise<void>;\n /** Report the transports that are live after every change \u2014 the id-safe names (\"ynca\"/\"yxc\"/\"xml\"). */\n onTransports?(names: string[]): void;\n /**\n * Dedup for the \"no reachable transport\" warning: without it every retry warns\n * again for as long as the device stays offline (nut2 `failedUps` pattern \u2014 first\n * failure warns, repeats stay at debug until the device answers again).\n */\n reachability?: ReachabilityDedup;\n /** Timers for the per-transport reconnect loops (absent in tests \u2192 no per-transport retry). */\n timers?: {\n /** Schedule a one-shot timer. */\n schedule(handler: () => void, ms: number): ioBroker.Timeout | undefined;\n /** Cancel a scheduled timer. */\n cancel(handle: ioBroker.Timeout | undefined): void;\n };\n}\n\n/**\n * Bring every answering transport online on ONE object tree. All candidates connect IN\n * PARALLEL (a YNCA connect timeout or long sweep no longer delays YXC/XML); the ones that\n * answer are handed to a single {@link MultiTransportHandle}, which unifies their catalogs\n * (each capability owned by exactly one transport \u2014 most-modern-but-lossless, see the\n * object-tree coordinator), routes user writes to the owner, and reconnects a single\n * dropped transport on its own while the others keep running. Ownership is resolved by\n * rank over the connected set, so the connect order/timing never changes it. A transport\n * that does not answer, or whose connect throws, is closed and left out. Returns the\n * handle over the live set, or null when no transport answered (the device is offline\n * this attempt).\n *\n * @param deviceId the id-safe device id\n * @param attempts the transports to try\n * @param deps the adapter callbacks (upsert + log + timers)\n * @returns a connection handle over the live transports, or null when none connected\n */\nexport async function connectTransports(\n deviceId: string,\n attempts: readonly TransportAttempt[],\n deps: ConnectDeps,\n): Promise<ConnectionHandle | null> {\n const results = await Promise.all(\n attempts.map(async attempt => {\n const conn = attempt.build();\n try {\n if (await conn.connect()) {\n return conn;\n }\n } catch (e) {\n deps.log.debug(`${deviceId}/${conn.transport}: transport did not connect (${errorMessage(e)})`);\n }\n conn.close();\n return null;\n }),\n );\n const live = results.filter((conn): conn is ConnectableTransport => conn !== null);\n if (live.length === 0) {\n const level = deps.reachability?.reportUnreachable() ?? \"warn\";\n deps.log[level](`${deviceId}: no reachable transport (YNCA/YXC/XML)`);\n return null;\n }\n deps.reachability?.reportReachable();\n const rebuilds = new Map(attempts.map(attempt => [attempt.transport, attempt.build] as const));\n const handle = new MultiTransportHandle(deviceId, live, {\n upsertObject: deps.upsertObject,\n log: deps.log,\n onTransports: deps.onTransports,\n rebuild: deps.timers ? transport => rebuilds.get(transport)!() : undefined,\n schedule: deps.timers ? (cb, ms) => deps.timers!.schedule(cb, ms) : undefined,\n cancel: deps.timers ? handle_ => deps.timers!.cancel(handle_ as ioBroker.Timeout | undefined) : undefined,\n backoffFactory: () => new ReconnectStrategy(TRANSPORT_RECONNECT_BASE_MS, TRANSPORT_RECONNECT_MAX_MS),\n });\n try {\n await handle.start();\n } catch (e) {\n // Building the unified tree failed (e.g. object creation errored): close every live\n // transport before rethrowing, or the supervisor's retry would leak sockets and timers \u2014\n // fatal for YNCA, where the receiver allows only ONE connection and a zombie socket\n // would block every future attempt until the adapter restarts.\n handle.close();\n throw e;\n }\n // One summary line instead of three per-transport \"ready\" lines; each controller logs its\n // own readiness at debug level for diagnostics.\n deps.log.info(\n readyLine(\n deviceId,\n live.map(conn => conn.transport),\n ),\n );\n return handle;\n}\n\n/**\n * Bring one device online across ALL its transports. Every transport that answers \u2014 YNCA (amp\n * control over a held TCP connection), YXC (MusicCast, push + poll), XML/YNC (pre-2010) \u2014 is built\n * behind a {@link TransportConnectionAdapter} and connected in parallel on one object tree, so a\n * MusicCast AVR gets YNCA base control AND the YXC-exclusive richness (multiroom, equalizer, album\n * art) instead of one transport winning and hiding the rest. Each transport is described by a\n * factory, so the handle can rebuild and reconnect a single dropped transport while the others\n * keep running. Returns a {@link ConnectionHandle} over the live set, or null when no transport\n * answers this attempt.\n *\n * @param device the configured device record\n * @param deps the adapter-bound callbacks\n * @returns a connection handle, or null when no transport connected\n */\nexport function attemptDevice(device: DeviceRecord, deps: AttemptDeps): Promise<ConnectionHandle | null> {\n const { log, upsertObject, setStateAck, timers } = deps;\n /**\n * A fresh command gate for one transport connection. EVERY command of that transport\n * goes through it: user writes, the init sweep, the keepalive and browsing alike. It is\n * also the connection's shutdown signal \u2014 closing it empties the queue and ends every\n * pending wait, so a stopped adapter leaves nothing running.\n *\n * One gate per device AND transport, not one for the adapter: the spacing is a property\n * of the device connection, so a shared gate would let one receiver's 19-second sweep\n * block another receiver's button press.\n *\n * @param transport the transport the gate belongs to\n * @returns the gate\n */\n const gateFor = (transport: Transport): CommandGate =>\n new CommandGate({ minSpacingMs: COMMAND_SPACING_MS[transport], timers });\n\n // 1) YNCA \u2014 amp control over a held TCP connection; a socket drop is the genuine gone-signal.\n const buildYnca = (): ConnectableTransport => {\n const ynca = new TransportConnectionAdapter(\"ynca\", device.id, setStateAck);\n const gate = gateFor(\"ynca\");\n ynca.bind(\n new YncaDeviceController(device.id, {\n client: new YncaClient(device.ip, timers, gate),\n gate,\n upsertObject: ynca.interceptUpsert,\n setStateAck: ynca.interceptSetStateAck,\n log,\n isEntryEnabled: deps.isEntryEnabled,\n subunitCache: deps.yncaSubunitCache,\n probeMemory: deps.probeMemory,\n }),\n );\n return ynca;\n };\n\n // 2) YXC \u2014 MusicCast; polled + push. Drop reported after a run of failed keepalive polls.\n const buildYxc = (): ConnectableTransport => {\n const yxc = new TransportConnectionAdapter(\"yxc\", device.id, setStateAck);\n const gate = gateFor(\"yxc\");\n yxc.bind(\n new YxcDeviceController(device.id, {\n client: new YamahaYxcClient(device.ip, undefined, gate),\n // Resolve another configured device's client for a multiroom link \u2014 never this device\n // itself. The partner's own gate belongs to its own connection, so this one-off client\n // stays ungated (a single link call, not a stream of commands).\n clientFor: ip => (ip !== device.ip && deps.knownDeviceIps.has(ip) ? new YamahaYxcClient(ip) : undefined),\n registerPush: onPush => deps.registerPush(device.ip, onPush),\n pushActive: deps.pushActive,\n probeMemory: deps.probeMemory,\n scheduleKeepalive: deps.scheduleKeepalive,\n upsertObject: yxc.interceptUpsert,\n setStateAck: yxc.interceptSetStateAck,\n reportDeviceName: deps.onDeviceName,\n log,\n gate,\n }),\n );\n return yxc;\n };\n\n // 3) XML/YNC \u2014 pre-2010 receivers; polled. A drop is reported after a run of failed polls.\n const buildXml = (): ConnectableTransport => {\n const xml = new TransportConnectionAdapter(\"xml\", device.id, setStateAck);\n const gate = gateFor(\"xml\");\n xml.bind(\n new XmlDeviceController(\n device.id,\n {\n client: new XmlClient(device.ip, undefined, gate),\n scheduleKeepalive: deps.scheduleKeepalive,\n upsertObject: xml.interceptUpsert,\n setStateAck: xml.interceptSetStateAck,\n log,\n gate,\n probeMemory: deps.probeMemory,\n },\n deps.xmlPollIntervalMs,\n ),\n );\n return xml;\n };\n\n return connectTransports(\n device.id,\n [\n { transport: \"ynca\", build: buildYnca },\n { transport: \"yxc\", build: buildYxc },\n { transport: \"xml\", build: buildXml },\n ],\n {\n upsertObject,\n log,\n onTransports: deps.onTransports,\n reachability: deps.reachability,\n timers: deps.timers,\n },\n );\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["import_device_controller"]
|
|
7
7
|
}
|