iobroker.yamaha 1.2.0 → 1.3.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.
Files changed (41) hide show
  1. package/README.md +11 -7
  2. package/admin/i18n/de.json +2 -2
  3. package/admin/i18n/en.json +2 -2
  4. package/admin/i18n/es.json +2 -2
  5. package/admin/i18n/fr.json +2 -2
  6. package/admin/i18n/it.json +2 -2
  7. package/admin/i18n/nl.json +2 -2
  8. package/admin/i18n/pl.json +2 -2
  9. package/admin/i18n/pt.json +2 -2
  10. package/admin/i18n/ru.json +2 -2
  11. package/admin/i18n/uk.json +2 -2
  12. package/admin/i18n/zh-cn.json +2 -2
  13. package/build/lib/attempt-device.js +9 -3
  14. package/build/lib/attempt-device.js.map +2 -2
  15. package/build/lib/browse/browse-engine.js +238 -0
  16. package/build/lib/browse/browse-engine.js.map +7 -0
  17. package/build/lib/browse/objects.js +109 -0
  18. package/build/lib/browse/objects.js.map +7 -0
  19. package/build/lib/browse/types.js +17 -0
  20. package/build/lib/browse/types.js.map +7 -0
  21. package/build/lib/browse/xml-browse-driver.js +194 -0
  22. package/build/lib/browse/xml-browse-driver.js.map +7 -0
  23. package/build/lib/browse/ynca-browse-driver.js +229 -0
  24. package/build/lib/browse/ynca-browse-driver.js.map +7 -0
  25. package/build/lib/browse/yxc-browse-driver.js +190 -0
  26. package/build/lib/browse/yxc-browse-driver.js.map +7 -0
  27. package/build/lib/device-controller.js +49 -1
  28. package/build/lib/device-controller.js.map +2 -2
  29. package/build/lib/xml/device-controller.js +52 -3
  30. package/build/lib/xml/device-controller.js.map +2 -2
  31. package/build/lib/xml/xml-client.js +12 -0
  32. package/build/lib/xml/xml-client.js.map +2 -2
  33. package/build/lib/ynca/catalog.js +126 -0
  34. package/build/lib/ynca/catalog.js.map +2 -2
  35. package/build/lib/yxc/client-contract.js.map +1 -1
  36. package/build/lib/yxc/device-controller.js +44 -3
  37. package/build/lib/yxc/device-controller.js.map +2 -2
  38. package/build/lib/yxc/http-client.js +26 -0
  39. package/build/lib/yxc/http-client.js.map +2 -2
  40. package/io-package.json +14 -14
  41. 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 **Advanced**. 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
+ 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,12 @@ 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.3.0 (2026-08-26)
94
+
95
+ - (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)
96
+ - (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.
97
+ - (krobipd) New: Bluetooth pairing and connect controls, FM mono mode and tuning indicators on YNCA receivers.
98
+
91
99
  ### 1.2.0 (2026-08-25)
92
100
 
93
101
  - (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)
@@ -117,10 +125,6 @@ On the first connect the adapter asks the receiver which functions it supports
117
125
  - (krobipd) Upgrading from 0.5.x shows a one-time notice explaining the new object tree before the update installs.
118
126
  - (mcm1957) version has been rebuilt due to deploy problems
119
127
 
120
- ### 0.5.4 (2024-06-14) — stable
121
-
122
- - (foxriver76) updated packages
123
-
124
128
  [Older changelogs can be found there](CHANGELOG_OLD.md)
125
129
 
126
130
  ## History
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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,6 +84,9 @@ async function connectTransports(deviceId, attempts, deps) {
84
84
  }
85
85
  function attemptDevice(device, deps) {
86
86
  const { log, upsertObject, setStateAck, timers } = deps;
87
+ const delay = (ms) => new Promise((resolve) => {
88
+ timers.schedule(resolve, ms);
89
+ });
87
90
  const buildYnca = () => {
88
91
  const ynca = new import_transport_connection_adapter.TransportConnectionAdapter("ynca", device.id, setStateAck);
89
92
  ynca.bind(
@@ -93,7 +96,8 @@ function attemptDevice(device, deps) {
93
96
  setStateAck: ynca.interceptSetStateAck,
94
97
  log,
95
98
  isEntryEnabled: deps.isEntryEnabled,
96
- subunitCache: deps.yncaSubunitCache
99
+ subunitCache: deps.yncaSubunitCache,
100
+ delay
97
101
  })
98
102
  );
99
103
  return ynca;
@@ -110,7 +114,8 @@ function attemptDevice(device, deps) {
110
114
  upsertObject: yxc.interceptUpsert,
111
115
  setStateAck: yxc.interceptSetStateAck,
112
116
  reportDeviceName: deps.onDeviceName,
113
- log
117
+ log,
118
+ delay
114
119
  })
115
120
  );
116
121
  return yxc;
@@ -125,7 +130,8 @@ function attemptDevice(device, deps) {
125
130
  scheduleKeepalive: deps.scheduleKeepalive,
126
131
  upsertObject: xml.interceptUpsert,
127
132
  setStateAck: xml.interceptSetStateAck,
128
- log
133
+ log,
134
+ delay
129
135
  },
130
136
  deps.xmlPollIntervalMs
131
137
  )
@@ -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": "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",
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 // Adapter-managed delay for the browse engine/drivers (pacing, busy polls, path walk).\n const delay = (ms: number): Promise<void> =>\n new Promise(resolve => {\n timers.schedule(resolve, ms);\n });\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 delay,\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 delay,\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 delay,\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
  }
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var browse_engine_exports = {};
20
+ __export(browse_engine_exports, {
21
+ BrowseEngine: () => BrowseEngine,
22
+ rowLabel: () => rowLabel
23
+ });
24
+ module.exports = __toCommonJS(browse_engine_exports);
25
+ var import_util = require("../util");
26
+ const WAIT_POLL_MS = 250;
27
+ const STEP_TIMEOUT_MS = 15e3;
28
+ const MAX_SEARCH_PAGES = 32;
29
+ function rowLabel(row) {
30
+ if (row.kind === "folder") {
31
+ return `\u{1F4C1} ${row.text}`;
32
+ }
33
+ if (row.kind === "item") {
34
+ return `\u266A ${row.text}`;
35
+ }
36
+ return row.text;
37
+ }
38
+ class BrowseEngine {
39
+ /**
40
+ * @param driver the transport's list operations
41
+ * @param deps the adapter-bound callbacks
42
+ */
43
+ constructor(driver, deps) {
44
+ this.driver = driver;
45
+ this.deps = deps;
46
+ }
47
+ window;
48
+ /** Bumped on every window report — the path walk waits on it. */
49
+ windowVersion = 0;
50
+ /** Serializes operations: a write while an operation runs is dropped with a log line. */
51
+ running = false;
52
+ closed = false;
53
+ /** Seed the surface: the source dropdown exists via the objects; states start empty. */
54
+ seed() {
55
+ this.deps.emit("player.browse.busy", false);
56
+ this.deps.emit("player.browse.path", "");
57
+ }
58
+ /** Stop accepting operations (the controller is closing). */
59
+ close() {
60
+ this.closed = true;
61
+ }
62
+ /**
63
+ * Render a fresh window the driver reports (a fetch result or, on YNCA, an
64
+ * unsolicited auto-feedback push).
65
+ *
66
+ * @param window the window snapshot
67
+ */
68
+ onWindow(window) {
69
+ this.window = window;
70
+ this.windowVersion++;
71
+ this.deps.emit("player.browse.menuName", window.menuName);
72
+ this.deps.emit("player.browse.layer", window.layer);
73
+ this.deps.emit("player.browse.totalItems", window.totalItems);
74
+ this.deps.emit("player.browse.currentLine", window.currentLine);
75
+ for (let line = 1; line <= 8; line++) {
76
+ const row = window.rows.find((r) => r.line === line);
77
+ this.deps.emit(`player.browse.line${line}`, row ? rowLabel(row) : "");
78
+ }
79
+ this.deps.emit("player.browse.rows", JSON.stringify(window.rows));
80
+ }
81
+ /**
82
+ * Handle a user write to one of the browse states.
83
+ *
84
+ * @param stateId the state id relative to the device (`player.browse.…`)
85
+ * @param value the written value
86
+ */
87
+ handleWrite(stateId, value) {
88
+ const sub = stateId.slice("player.browse.".length);
89
+ switch (sub) {
90
+ case "source":
91
+ if (typeof value === "string" && value in this.driver.sources()) {
92
+ void this.run(`open ${value}`, async () => {
93
+ await this.driver.open(value);
94
+ this.deps.emit("player.browse.source", value);
95
+ });
96
+ }
97
+ return;
98
+ case "selectLine": {
99
+ const line = Number(value);
100
+ if (Number.isInteger(line) && line >= 1 && line <= 8) {
101
+ void this.run(`select line ${line}`, () => this.driver.select(line));
102
+ }
103
+ return;
104
+ }
105
+ case "pageUp":
106
+ void this.run("page up", () => this.driver.pageUp());
107
+ return;
108
+ case "pageDown":
109
+ void this.run("page down", () => this.driver.pageDown());
110
+ return;
111
+ case "back":
112
+ void this.run("back", () => this.driver.back());
113
+ return;
114
+ case "home":
115
+ void this.run("home", () => this.driver.home());
116
+ return;
117
+ case "path":
118
+ if (typeof value === "string" && value.trim().length > 0) {
119
+ void this.run(`path ${value}`, () => this.walkPath(value));
120
+ }
121
+ return;
122
+ default:
123
+ return;
124
+ }
125
+ }
126
+ /**
127
+ * Run one operation, serialized: while one runs, further writes are dropped (the
128
+ * busy state shows why). Failures land in the log as warnings — a user action
129
+ * failing must be visible.
130
+ *
131
+ * @param what the operation, for the log line
132
+ * @param op the operation to run
133
+ */
134
+ async run(what, op) {
135
+ if (this.closed) {
136
+ return;
137
+ }
138
+ if (this.running) {
139
+ this.deps.log.debug(`browse: "${what}" dropped \u2014 another browse operation is still running`);
140
+ return;
141
+ }
142
+ this.running = true;
143
+ this.deps.emit("player.browse.busy", true);
144
+ try {
145
+ await op();
146
+ } catch (e) {
147
+ this.deps.log.warn(`browse: ${what} failed: ${(0, import_util.errorMessage)(e)}`);
148
+ } finally {
149
+ this.running = false;
150
+ if (!this.closed) {
151
+ this.deps.emit("player.browse.busy", false);
152
+ }
153
+ }
154
+ }
155
+ /**
156
+ * Wait until the driver reports a window fresher than `sinceVersion` (or the step
157
+ * timeout passes — menu levels are served by the device/catalog service and can be
158
+ * slow, so this polls rather than racing).
159
+ *
160
+ * @param sinceVersion the window version before the operation
161
+ * @returns true if a fresh window arrived, false on timeout
162
+ */
163
+ async waitForWindow(sinceVersion) {
164
+ const deadline = STEP_TIMEOUT_MS / WAIT_POLL_MS;
165
+ for (let i = 0; i < deadline; i++) {
166
+ if (this.closed) {
167
+ return false;
168
+ }
169
+ if (this.windowVersion > sinceVersion) {
170
+ return true;
171
+ }
172
+ await this.deps.delay(WAIT_POLL_MS);
173
+ }
174
+ return this.windowVersion > sinceVersion;
175
+ }
176
+ /**
177
+ * Walk a `>`-separated path from the menu root, selecting each segment by its text
178
+ * (paging forward when it is not in the visible window) — the one-write navigation
179
+ * for scripts and scenes. The final segment behaves like any selection: a folder
180
+ * opens, a playable item starts.
181
+ *
182
+ * @param path the path, e.g. `Bookmarks>Radio Paradise`
183
+ */
184
+ async walkPath(path) {
185
+ const segments = path.split(">").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
186
+ if (segments.length === 0) {
187
+ return;
188
+ }
189
+ let version = this.windowVersion;
190
+ await this.driver.home();
191
+ if (!await this.waitForWindow(version)) {
192
+ this.deps.log.warn(`browse: path "${path}" aborted \u2014 the menu root did not load`);
193
+ return;
194
+ }
195
+ for (const segment of segments) {
196
+ if (!await this.findAndSelect(segment)) {
197
+ this.deps.log.warn(`browse: path "${path}" aborted \u2014 "${segment}" not found in this menu`);
198
+ return;
199
+ }
200
+ version = this.windowVersion;
201
+ await this.waitForWindow(version);
202
+ }
203
+ }
204
+ /**
205
+ * Find a row by its text in the current menu, paging forward as needed, and select it.
206
+ *
207
+ * @param text the row text to find (without the symbol prefix)
208
+ * @returns true when the row was found and selected
209
+ */
210
+ async findAndSelect(text) {
211
+ for (let page = 0; page < MAX_SEARCH_PAGES; page++) {
212
+ const window = this.window;
213
+ if (!window) {
214
+ return false;
215
+ }
216
+ const row = window.rows.find((r) => r.text === text && r.kind !== "unselectable");
217
+ if (row) {
218
+ await this.driver.select(row.line);
219
+ return true;
220
+ }
221
+ if (window.currentLine - 1 + window.rows.length >= window.totalItems) {
222
+ return false;
223
+ }
224
+ const version = this.windowVersion;
225
+ await this.driver.pageDown();
226
+ if (!await this.waitForWindow(version)) {
227
+ return false;
228
+ }
229
+ }
230
+ return false;
231
+ }
232
+ }
233
+ // Annotate the CommonJS export names for ESM import in node:
234
+ 0 && (module.exports = {
235
+ BrowseEngine,
236
+ rowLabel
237
+ });
238
+ //# sourceMappingURL=browse-engine.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/browse/browse-engine.ts"],
4
+ "sourcesContent": ["import type { BrowseDriver, BrowseRow, BrowseWindow } from \"./types\";\nimport type { ControllerLog } from \"../controller\";\nimport { errorMessage } from \"../util\";\n\n/** Poll interval while waiting for the device to deliver a fresh window. */\nconst WAIT_POLL_MS = 250;\n/** How long one navigation step (select/page/back/home) may take before the walk aborts. */\nconst STEP_TIMEOUT_MS = 15000;\n/** Hard page cap while searching one path segment, beyond the totalItems-derived bound. */\nconst MAX_SEARCH_PAGES = 32;\n\n/** The adapter-bound callbacks the engine drives. */\nexport interface BrowseEngineDeps {\n /** Write a browse state (id relative to the device, e.g. `player.browse.line1`) with ack. */\n emit(id: string, value: boolean | number | string): void;\n /** Adapter log. */\n log: ControllerLog;\n /** Adapter-managed delay (so no native timer outlives onUnload). */\n delay(ms: number): Promise<void>;\n}\n\n/**\n * Prefix a row's text with its kind symbol, so a plain VIS button caption already\n * shows whether the line is a folder or a playable title.\n *\n * @param row the row to label\n * @returns the display text\n */\nexport function rowLabel(row: BrowseRow): string {\n if (row.kind === \"folder\") {\n return `\\u{1F4C1} ${row.text}`;\n }\n if (row.kind === \"item\") {\n return `\u266A ${row.text}`;\n }\n return row.text;\n}\n\n/**\n * The transport-neutral browsing engine: it owns the `player.browse.*` states, turns\n * user writes into driver operations, and renders every window the driver reports.\n * Exactly one engine is active per device \u2014 the one inside the owning transport's\n * controller (writes only ever reach the owner, and a non-owner's state writes are\n * filtered by the transport adapter).\n */\nexport class BrowseEngine {\n private window: BrowseWindow | undefined;\n /** Bumped on every window report \u2014 the path walk waits on it. */\n private windowVersion = 0;\n /** Serializes operations: a write while an operation runs is dropped with a log line. */\n private running = false;\n private closed = false;\n\n /**\n * @param driver the transport's list operations\n * @param deps the adapter-bound callbacks\n */\n public constructor(\n private readonly driver: BrowseDriver,\n private readonly deps: BrowseEngineDeps,\n ) {}\n\n /** Seed the surface: the source dropdown exists via the objects; states start empty. */\n public seed(): void {\n this.deps.emit(\"player.browse.busy\", false);\n this.deps.emit(\"player.browse.path\", \"\");\n }\n\n /** Stop accepting operations (the controller is closing). */\n public close(): void {\n this.closed = true;\n }\n\n /**\n * Render a fresh window the driver reports (a fetch result or, on YNCA, an\n * unsolicited auto-feedback push).\n *\n * @param window the window snapshot\n */\n public onWindow(window: BrowseWindow): void {\n this.window = window;\n this.windowVersion++;\n this.deps.emit(\"player.browse.menuName\", window.menuName);\n this.deps.emit(\"player.browse.layer\", window.layer);\n this.deps.emit(\"player.browse.totalItems\", window.totalItems);\n this.deps.emit(\"player.browse.currentLine\", window.currentLine);\n for (let line = 1; line <= 8; line++) {\n const row = window.rows.find(r => r.line === line);\n this.deps.emit(`player.browse.line${line}`, row ? rowLabel(row) : \"\");\n }\n this.deps.emit(\"player.browse.rows\", JSON.stringify(window.rows));\n }\n\n /**\n * Handle a user write to one of the browse states.\n *\n * @param stateId the state id relative to the device (`player.browse.\u2026`)\n * @param value the written value\n */\n public handleWrite(stateId: string, value: unknown): void {\n const sub = stateId.slice(\"player.browse.\".length);\n switch (sub) {\n case \"source\":\n if (typeof value === \"string\" && value in this.driver.sources()) {\n void this.run(`open ${value}`, async () => {\n await this.driver.open(value);\n this.deps.emit(\"player.browse.source\", value);\n });\n }\n return;\n case \"selectLine\": {\n const line = Number(value);\n if (Number.isInteger(line) && line >= 1 && line <= 8) {\n void this.run(`select line ${line}`, () => this.driver.select(line));\n }\n return;\n }\n case \"pageUp\":\n void this.run(\"page up\", () => this.driver.pageUp());\n return;\n case \"pageDown\":\n void this.run(\"page down\", () => this.driver.pageDown());\n return;\n case \"back\":\n void this.run(\"back\", () => this.driver.back());\n return;\n case \"home\":\n void this.run(\"home\", () => this.driver.home());\n return;\n case \"path\":\n if (typeof value === \"string\" && value.trim().length > 0) {\n void this.run(`path ${value}`, () => this.walkPath(value));\n }\n return;\n default:\n return;\n }\n }\n\n /**\n * Run one operation, serialized: while one runs, further writes are dropped (the\n * busy state shows why). Failures land in the log as warnings \u2014 a user action\n * failing must be visible.\n *\n * @param what the operation, for the log line\n * @param op the operation to run\n */\n private async run(what: string, op: () => Promise<void> | void): Promise<void> {\n if (this.closed) {\n return;\n }\n if (this.running) {\n this.deps.log.debug(`browse: \"${what}\" dropped \u2014 another browse operation is still running`);\n return;\n }\n this.running = true;\n this.deps.emit(\"player.browse.busy\", true);\n try {\n await op();\n } catch (e) {\n this.deps.log.warn(`browse: ${what} failed: ${errorMessage(e)}`);\n } finally {\n this.running = false;\n if (!this.closed) {\n this.deps.emit(\"player.browse.busy\", false);\n }\n }\n }\n\n /**\n * Wait until the driver reports a window fresher than `sinceVersion` (or the step\n * timeout passes \u2014 menu levels are served by the device/catalog service and can be\n * slow, so this polls rather than racing).\n *\n * @param sinceVersion the window version before the operation\n * @returns true if a fresh window arrived, false on timeout\n */\n private async waitForWindow(sinceVersion: number): Promise<boolean> {\n const deadline = STEP_TIMEOUT_MS / WAIT_POLL_MS;\n for (let i = 0; i < deadline; i++) {\n if (this.closed) {\n return false;\n }\n if (this.windowVersion > sinceVersion) {\n return true;\n }\n await this.deps.delay(WAIT_POLL_MS);\n }\n return this.windowVersion > sinceVersion;\n }\n\n /**\n * Walk a `>`-separated path from the menu root, selecting each segment by its text\n * (paging forward when it is not in the visible window) \u2014 the one-write navigation\n * for scripts and scenes. The final segment behaves like any selection: a folder\n * opens, a playable item starts.\n *\n * @param path the path, e.g. `Bookmarks>Radio Paradise`\n */\n private async walkPath(path: string): Promise<void> {\n const segments = path\n .split(\">\")\n .map(segment => segment.trim())\n .filter(segment => segment.length > 0);\n if (segments.length === 0) {\n return;\n }\n let version = this.windowVersion;\n await this.driver.home();\n if (!(await this.waitForWindow(version))) {\n this.deps.log.warn(`browse: path \"${path}\" aborted \u2014 the menu root did not load`);\n return;\n }\n for (const segment of segments) {\n if (!(await this.findAndSelect(segment))) {\n this.deps.log.warn(`browse: path \"${path}\" aborted \u2014 \"${segment}\" not found in this menu`);\n return;\n }\n version = this.windowVersion;\n // Give the device a chance to deliver the next level; a playable final segment\n // may not change the window at all, so a timeout here is not an error.\n await this.waitForWindow(version);\n }\n }\n\n /**\n * Find a row by its text in the current menu, paging forward as needed, and select it.\n *\n * @param text the row text to find (without the symbol prefix)\n * @returns true when the row was found and selected\n */\n private async findAndSelect(text: string): Promise<boolean> {\n for (let page = 0; page < MAX_SEARCH_PAGES; page++) {\n const window = this.window;\n if (!window) {\n return false;\n }\n const row = window.rows.find(r => r.text === text && r.kind !== \"unselectable\");\n if (row) {\n await this.driver.select(row.line);\n return true;\n }\n // Stop at the last page: the window already shows the menu's tail.\n if (window.currentLine - 1 + window.rows.length >= window.totalItems) {\n return false;\n }\n const version = this.windowVersion;\n await this.driver.pageDown();\n if (!(await this.waitForWindow(version))) {\n return false;\n }\n }\n return false;\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }