iobroker.yamaha 1.1.1 → 1.2.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 +10 -4
- package/admin/i18n/de.json +2 -0
- package/admin/i18n/en.json +2 -0
- package/admin/i18n/es.json +2 -0
- package/admin/i18n/fr.json +2 -0
- package/admin/i18n/it.json +2 -0
- package/admin/i18n/nl.json +2 -0
- package/admin/i18n/pl.json +2 -0
- package/admin/i18n/pt.json +2 -0
- package/admin/i18n/ru.json +2 -0
- package/admin/i18n/uk.json +2 -0
- package/admin/i18n/zh-cn.json +2 -0
- package/admin/jsonConfig.json +10 -0
- package/build/lib/catalog/groups.js +5 -1
- package/build/lib/catalog/groups.js.map +2 -2
- package/build/lib/catalog/owner-policy.js +2 -0
- package/build/lib/catalog/owner-policy.js.map +2 -2
- package/build/lib/catalog/value-coerce.js +12 -0
- package/build/lib/catalog/value-coerce.js.map +2 -2
- package/build/lib/ynca/catalog.js +97 -18
- package/build/lib/ynca/catalog.js.map +2 -2
- package/build/lib/yxc/capability.js +56 -4
- package/build/lib/yxc/capability.js.map +2 -2
- package/build/lib/yxc/catalog.js +21 -0
- package/build/lib/yxc/catalog.js.map +2 -2
- package/build/lib/yxc/client-contract.js.map +1 -1
- package/build/lib/yxc/command-mapper.js +241 -6
- package/build/lib/yxc/command-mapper.js.map +2 -2
- package/build/lib/yxc/device-controller.js +91 -1
- package/build/lib/yxc/device-controller.js.map +2 -2
- package/build/lib/yxc/http-client.js +64 -0
- package/build/lib/yxc/http-client.js.map +2 -2
- package/build/lib/yxc/object-mapper.js +204 -6
- package/build/lib/yxc/object-mapper.js.map +2 -2
- package/build/lib/yxc/push.js +10 -0
- package/build/lib/yxc/push.js.map +2 -2
- package/io-package.json +16 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,6 +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
|
|
23
|
+
- **Clock & alarm view** — MusicCast desk-audio devices show their clock and alarm settings
|
|
22
24
|
- **Capability-driven** — states are generated from what each device reports, no hardcoded model list
|
|
23
25
|
- **Automatic discovery** — an empty device list finds and sets up MusicCast devices at startup
|
|
24
26
|
- **Device manager** — receivers as admin cards with model, address, live protocol indicators and a device-type icon (receiver, stereo, speaker, soundbar, CD)
|
|
@@ -86,6 +88,14 @@ On the first connect the adapter asks the receiver which functions it supports
|
|
|
86
88
|
Placeholder for the next version (at the beginning of the line):
|
|
87
89
|
### **WORK IN PROGRESS**
|
|
88
90
|
-->
|
|
91
|
+
### 1.2.0 (2026-08-25)
|
|
92
|
+
|
|
93
|
+
- (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)
|
|
94
|
+
- (krobipd) Fixed: the FM frequency datapoint now shows MHz (it was mislabelled kHz) and accepts direct frequency writes in the form the tuner expects.
|
|
95
|
+
- (krobipd) New: preset selection — recall tuner presets by number with up/down stepping, and recall stored network or USB favourites per source on YNCA receivers (#613)
|
|
96
|
+
- (krobipd) New: MusicCast selection lists — stored favourites and tuner presets with names, a recently-played list with recall by number, and the device's own allowed values as dropdowns.
|
|
97
|
+
- (krobipd) New: more device detail — CD track and drive info, DAB and RDS station data, and a read-only clock and alarm view with its own datapoint group switch in the admin settings.
|
|
98
|
+
|
|
89
99
|
### 1.1.1 (2026-08-22)
|
|
90
100
|
|
|
91
101
|
- (krobipd) Changed: Internal cleanup. No user-facing changes.
|
|
@@ -111,10 +121,6 @@ On the first connect the adapter asks the receiver which functions it supports
|
|
|
111
121
|
|
|
112
122
|
- (foxriver76) updated packages
|
|
113
123
|
|
|
114
|
-
### 0.5.3 (2022-06-17)
|
|
115
|
-
|
|
116
|
-
- (Apollon77) Fix crash cases reported by Sentry
|
|
117
|
-
|
|
118
124
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
119
125
|
|
|
120
126
|
## History
|
package/admin/i18n/de.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "HDMI-Ausgänge und Lippensynchron-Versatz, für jede Zone",
|
|
34
34
|
"group_scene": "Szenen",
|
|
35
35
|
"group_scene_help": "Gespeicherte Szenen abrufen",
|
|
36
|
+
"group_clock": "Uhr & Wecker",
|
|
37
|
+
"group_clock_help": "Uhr- und Wecker-Datenpunkte",
|
|
36
38
|
"group_sound": "Klang",
|
|
37
39
|
"group_sound_help": "Klangmodi, Klangregler, Subwoofer, Balance und Equalizer",
|
|
38
40
|
"group_advanced": "Erweitert",
|
package/admin/i18n/en.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "HDMI outputs and lip-sync offset, for every zone",
|
|
34
34
|
"group_scene": "Scenes",
|
|
35
35
|
"group_scene_help": "Recall saved scenes",
|
|
36
|
+
"group_clock": "Clock & alarm",
|
|
37
|
+
"group_clock_help": "Clock and alarm datapoints",
|
|
36
38
|
"group_sound": "Sound",
|
|
37
39
|
"group_sound_help": "Sound-processing modes, tone controls, subwoofer, balance and equalizer",
|
|
38
40
|
"group_advanced": "Advanced",
|
package/admin/i18n/es.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "Salidas HDMI y desfase de sincronización labial, para cada zona",
|
|
34
34
|
"group_scene": "Escenas",
|
|
35
35
|
"group_scene_help": "Recuperar escenas guardadas",
|
|
36
|
+
"group_clock": "Reloj y alarma",
|
|
37
|
+
"group_clock_help": "Puntos de datos de reloj y alarma",
|
|
36
38
|
"group_sound": "Sonido",
|
|
37
39
|
"group_sound_help": "Modos de procesamiento de sonido, control de tono, subwoofer, balance y ecualizador",
|
|
38
40
|
"group_advanced": "Avanzado",
|
package/admin/i18n/fr.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "Sorties HDMI et décalage de synchronisation labiale, pour chaque zone",
|
|
34
34
|
"group_scene": "Scènes",
|
|
35
35
|
"group_scene_help": "Rappeler les scènes enregistrées",
|
|
36
|
+
"group_clock": "Horloge et réveil",
|
|
37
|
+
"group_clock_help": "Points de données horloge et réveil",
|
|
36
38
|
"group_sound": "Son",
|
|
37
39
|
"group_sound_help": "Modes de traitement du son, réglages de tonalité, caisson de basses, balance et égaliseur",
|
|
38
40
|
"group_advanced": "Avancé",
|
package/admin/i18n/it.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "Uscite HDMI e sfasamento lip-sync, per ogni zona",
|
|
34
34
|
"group_scene": "Scene",
|
|
35
35
|
"group_scene_help": "Richiama le scene salvate",
|
|
36
|
+
"group_clock": "Orologio e sveglia",
|
|
37
|
+
"group_clock_help": "Punti dati di orologio e sveglia",
|
|
36
38
|
"group_sound": "Audio",
|
|
37
39
|
"group_sound_help": "Modalità di elaborazione audio, toni, subwoofer, bilanciamento ed equalizzatore",
|
|
38
40
|
"group_advanced": "Avanzate",
|
package/admin/i18n/nl.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "HDMI-uitgangen en lip-sync-offset, voor elke zone",
|
|
34
34
|
"group_scene": "Scènes",
|
|
35
35
|
"group_scene_help": "Opgeslagen scènes oproepen",
|
|
36
|
+
"group_clock": "Klok & wekker",
|
|
37
|
+
"group_clock_help": "Klok- en wekkerdatapunten",
|
|
36
38
|
"group_sound": "Geluid",
|
|
37
39
|
"group_sound_help": "Geluidsmodi, klankregeling, subwoofer, balans en equalizer",
|
|
38
40
|
"group_advanced": "Geavanceerd",
|
package/admin/i18n/pl.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "Wyjścia HDMI i przesunięcie synchronizacji ust, dla każdej strefy",
|
|
34
34
|
"group_scene": "Sceny",
|
|
35
35
|
"group_scene_help": "Przywoływanie zapisanych scen",
|
|
36
|
+
"group_clock": "Zegar i budzik",
|
|
37
|
+
"group_clock_help": "Punkty danych zegara i budzika",
|
|
36
38
|
"group_sound": "Dźwięk",
|
|
37
39
|
"group_sound_help": "Tryby przetwarzania dźwięku, tony, subwoofer, balans i korektor",
|
|
38
40
|
"group_advanced": "Zaawansowane",
|
package/admin/i18n/pt.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "Saídas HDMI e desvio de sincronização labial, para cada zona",
|
|
34
34
|
"group_scene": "Cenas",
|
|
35
35
|
"group_scene_help": "Recuperar cenas guardadas",
|
|
36
|
+
"group_clock": "Relógio e alarme",
|
|
37
|
+
"group_clock_help": "Pontos de dados de relógio e alarme",
|
|
36
38
|
"group_sound": "Som",
|
|
37
39
|
"group_sound_help": "Modos de processamento de som, controlo de tom, subwoofer, balanço e equalizador",
|
|
38
40
|
"group_advanced": "Avançado",
|
package/admin/i18n/ru.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "Выходы HDMI и смещение синхронизации звука и видео для каждой зоны",
|
|
34
34
|
"group_scene": "Сцены",
|
|
35
35
|
"group_scene_help": "Вызов сохранённых сцен",
|
|
36
|
+
"group_clock": "Часы и будильник",
|
|
37
|
+
"group_clock_help": "Точки данных часов и будильника",
|
|
36
38
|
"group_sound": "Звук",
|
|
37
39
|
"group_sound_help": "Режимы обработки звука, тембр, сабвуфер, баланс и эквалайзер",
|
|
38
40
|
"group_advanced": "Расширенные",
|
package/admin/i18n/uk.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "Виходи HDMI та зсув синхронізації звуку й відео для кожної зони",
|
|
34
34
|
"group_scene": "Сцени",
|
|
35
35
|
"group_scene_help": "Виклик збережених сцен",
|
|
36
|
+
"group_clock": "Годинник і будильник",
|
|
37
|
+
"group_clock_help": "Точки даних годинника та будильника",
|
|
36
38
|
"group_sound": "Звук",
|
|
37
39
|
"group_sound_help": "Режими обробки звуку, тембр, сабвуфер, баланс і еквалайзер",
|
|
38
40
|
"group_advanced": "Розширені",
|
package/admin/i18n/zh-cn.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"group_hdmi_help": "每个区域的 HDMI 输出和唇音同步偏移",
|
|
34
34
|
"group_scene": "场景",
|
|
35
35
|
"group_scene_help": "调用已保存的场景",
|
|
36
|
+
"group_clock": "时钟与闹钟",
|
|
37
|
+
"group_clock_help": "时钟和闹钟数据点",
|
|
36
38
|
"group_sound": "音效",
|
|
37
39
|
"group_sound_help": "音效处理模式、音调、低音炮、平衡和均衡器",
|
|
38
40
|
"group_advanced": "高级",
|
package/admin/jsonConfig.json
CHANGED
|
@@ -110,6 +110,16 @@
|
|
|
110
110
|
"lg": 4,
|
|
111
111
|
"xl": 3
|
|
112
112
|
},
|
|
113
|
+
"group_clock": {
|
|
114
|
+
"type": "checkbox",
|
|
115
|
+
"label": "group_clock",
|
|
116
|
+
"help": "group_clock_help",
|
|
117
|
+
"xs": 12,
|
|
118
|
+
"sm": 6,
|
|
119
|
+
"md": 4,
|
|
120
|
+
"lg": 4,
|
|
121
|
+
"xl": 3
|
|
122
|
+
},
|
|
113
123
|
"group_sound": {
|
|
114
124
|
"type": "checkbox",
|
|
115
125
|
"label": "group_sound",
|
|
@@ -30,7 +30,8 @@ const SWITCHABLE_GROUPS = [
|
|
|
30
30
|
"hdmi",
|
|
31
31
|
"scene",
|
|
32
32
|
"sound",
|
|
33
|
-
"advanced"
|
|
33
|
+
"advanced",
|
|
34
|
+
"clock"
|
|
34
35
|
];
|
|
35
36
|
function groupOf(stateId) {
|
|
36
37
|
const seg = stateId.includes(".") ? stateId.slice(0, stateId.indexOf(".")) : stateId;
|
|
@@ -55,6 +56,9 @@ function groupOf(stateId) {
|
|
|
55
56
|
if (seg === "scene") {
|
|
56
57
|
return "scene";
|
|
57
58
|
}
|
|
59
|
+
if (seg === "clock") {
|
|
60
|
+
return "clock";
|
|
61
|
+
}
|
|
58
62
|
return "amp";
|
|
59
63
|
}
|
|
60
64
|
function isGroupEnabled(stateId, config) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/catalog/groups.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Datapoint groups: the thematic buckets that are both the object-tree structure and the\n * admin on/off switches. Every state id maps to exactly one group by its (zone-stripped) first\n * path segment (its channel). A disabled group's objects are never created and any existing ones\n * are pruned, the same way beszel gates its metric categories. `amp` is the always-on core \u2014 no\n * switch exists for it, exactly like beszel's unconditional `info.online`/`info.status`.\n *\n * Every group is a real channel prefix in the tree (`player.*`, `tuner.*`, `hdmi.*`,\n * `multiroom.*`, `scene.*`, `sound.*`, `advanced.*`) \u2014 the toggle and the folder a datapoint\n * visually sits in are the same thing, never decoupled. Zone 2/3/4, Zone B, masterPower, party\n * and distribution all live under `multiroom.*`. Legacy flat pre-v0.15.0 ids need no handling\n * here: the start-up cleanup deletes them via `RENAMED_CHANNELS` before anything queries them.\n */\n\n/** The switchable groups. `amp` is the amplifier core and can never be turned off. */\nexport type GroupId = \"amp\" | \"player\" | \"tuner\" | \"multiroom\" | \"hdmi\" | \"scene\" | \"sound\" | \"advanced\";\n\n/** The groups a user can switch off, in display order (amp is always on and not listed here). */\nexport const SWITCHABLE_GROUPS: readonly GroupId[] = [\n \"player\",\n \"tuner\",\n \"multiroom\",\n \"hdmi\",\n \"scene\",\n \"sound\",\n \"advanced\",\n];\n\n/**\n * The datapoint group a state id belongs to, decided by its first path segment. Every group is a\n * real folder prefix in the tree, so the first segment of the id determines the group directly:\n * `multiroom.*` (including zones, masterPower, party), `player.*`, `tuner.*`, `hdmi.*`, `sound.*`,\n * `advanced.*`, `scene.*`. Anything not matched \u2014 the amplifier core (power, volume, input,\n * sleep \u2026), `info.*` \u2014 is the always-on `amp` group.\n *\n * @param stateId the device-relative state id (e.g. \"multiroom.zone2.power\", \"player.spotify.playback\")\n * @returns the group the state belongs to\n */\nexport function groupOf(stateId: string): GroupId {\n const seg = stateId.includes(\".\") ? stateId.slice(0, stateId.indexOf(\".\")) : stateId;\n\n if (seg === \"multiroom\") {\n return \"multiroom\";\n }\n if (seg === \"hdmi\" || seg === \"lipSync\") {\n return \"hdmi\";\n }\n if (seg === \"player\") {\n return \"player\";\n }\n if (seg === \"tuner\") {\n return \"tuner\";\n }\n if (seg === \"sound\") {\n return \"sound\";\n }\n if (seg === \"advanced\") {\n return \"advanced\";\n }\n if (seg === \"scene\") {\n return \"scene\";\n }\n return \"amp\";\n}\n\n/**\n * Whether a state's group is switched on. The amplifier core is always on; every other group is\n * on unless its `group_<id>` flag is explicitly false \u2014 default-on, so a fresh install and every\n * existing install keep all groups until the user turns one off.\n *\n * @param stateId the device-relative state id\n * @param config the adapter native config (carries `group_player`, `group_tuner`, \u2026 booleans)\n * @returns true if the state's group is enabled\n */\nexport function isGroupEnabled(stateId: string, config: Record<string, unknown>): boolean {\n const group = groupOf(stateId);\n if (group === \"amp\") {\n return true;\n }\n return config[`group_${group}`] !== false;\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBO,MAAM,oBAAwC;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAYO,SAAS,QAAQ,SAA0B;AAChD,QAAM,MAAM,QAAQ,SAAS,GAAG,IAAI,QAAQ,MAAM,GAAG,QAAQ,QAAQ,GAAG,CAAC,IAAI;AAE7E,MAAI,QAAQ,aAAa;AACvB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,UAAU,QAAQ,WAAW;AACvC,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,UAAU;AACpB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS;AACnB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS;AACnB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,YAAY;AACtB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS;AACnB,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAWO,SAAS,eAAe,SAAiB,QAA0C;AACxF,QAAM,QAAQ,QAAQ,OAAO;AAC7B,MAAI,UAAU,OAAO;AACnB,WAAO;AAAA,EACT;AACA,SAAO,OAAO,SAAS,KAAK,EAAE,MAAM;AACtC;",
|
|
4
|
+
"sourcesContent": ["/**\n * Datapoint groups: the thematic buckets that are both the object-tree structure and the\n * admin on/off switches. Every state id maps to exactly one group by its (zone-stripped) first\n * path segment (its channel). A disabled group's objects are never created and any existing ones\n * are pruned, the same way beszel gates its metric categories. `amp` is the always-on core \u2014 no\n * switch exists for it, exactly like beszel's unconditional `info.online`/`info.status`.\n *\n * Every group is a real channel prefix in the tree (`player.*`, `tuner.*`, `hdmi.*`,\n * `multiroom.*`, `scene.*`, `sound.*`, `advanced.*`) \u2014 the toggle and the folder a datapoint\n * visually sits in are the same thing, never decoupled. Zone 2/3/4, Zone B, masterPower, party\n * and distribution all live under `multiroom.*`. Legacy flat pre-v0.15.0 ids need no handling\n * here: the start-up cleanup deletes them via `RENAMED_CHANNELS` before anything queries them.\n */\n\n/** The switchable groups. `amp` is the amplifier core and can never be turned off. */\nexport type GroupId = \"amp\" | \"player\" | \"tuner\" | \"multiroom\" | \"hdmi\" | \"scene\" | \"sound\" | \"advanced\" | \"clock\";\n\n/** The groups a user can switch off, in display order (amp is always on and not listed here). */\nexport const SWITCHABLE_GROUPS: readonly GroupId[] = [\n \"player\",\n \"tuner\",\n \"multiroom\",\n \"hdmi\",\n \"scene\",\n \"sound\",\n \"advanced\",\n \"clock\",\n];\n\n/**\n * The datapoint group a state id belongs to, decided by its first path segment. Every group is a\n * real folder prefix in the tree, so the first segment of the id determines the group directly:\n * `multiroom.*` (including zones, masterPower, party), `player.*`, `tuner.*`, `hdmi.*`, `sound.*`,\n * `advanced.*`, `scene.*`. Anything not matched \u2014 the amplifier core (power, volume, input,\n * sleep \u2026), `info.*` \u2014 is the always-on `amp` group.\n *\n * @param stateId the device-relative state id (e.g. \"multiroom.zone2.power\", \"player.spotify.playback\")\n * @returns the group the state belongs to\n */\nexport function groupOf(stateId: string): GroupId {\n const seg = stateId.includes(\".\") ? stateId.slice(0, stateId.indexOf(\".\")) : stateId;\n\n if (seg === \"multiroom\") {\n return \"multiroom\";\n }\n if (seg === \"hdmi\" || seg === \"lipSync\") {\n return \"hdmi\";\n }\n if (seg === \"player\") {\n return \"player\";\n }\n if (seg === \"tuner\") {\n return \"tuner\";\n }\n if (seg === \"sound\") {\n return \"sound\";\n }\n if (seg === \"advanced\") {\n return \"advanced\";\n }\n if (seg === \"scene\") {\n return \"scene\";\n }\n if (seg === \"clock\") {\n return \"clock\";\n }\n return \"amp\";\n}\n\n/**\n * Whether a state's group is switched on. The amplifier core is always on; every other group is\n * on unless its `group_<id>` flag is explicitly false \u2014 default-on, so a fresh install and every\n * existing install keep all groups until the user turns one off.\n *\n * @param stateId the device-relative state id\n * @param config the adapter native config (carries `group_player`, `group_tuner`, \u2026 booleans)\n * @returns true if the state's group is enabled\n */\nexport function isGroupEnabled(stateId: string, config: Record<string, unknown>): boolean {\n const group = groupOf(stateId);\n if (group === \"amp\") {\n return true;\n }\n return config[`group_${group}`] !== false;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBO,MAAM,oBAAwC;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAYO,SAAS,QAAQ,SAA0B;AAChD,QAAM,MAAM,QAAQ,SAAS,GAAG,IAAI,QAAQ,MAAM,GAAG,QAAQ,QAAQ,GAAG,CAAC,IAAI;AAE7E,MAAI,QAAQ,aAAa;AACvB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,UAAU,QAAQ,WAAW;AACvC,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,UAAU;AACpB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS;AACnB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS;AACnB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,YAAY;AACtB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS;AACnB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS;AACnB,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAWO,SAAS,eAAe,SAAiB,QAA0C;AACxF,QAAM,QAAQ,QAAQ,OAAO;AAC7B,MAAI,UAAU,OAAO;AACnB,WAAO;AAAA,EACT;AACA,SAAO,OAAO,SAAS,KAAK,EAAE,MAAM;AACtC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -30,6 +30,8 @@ const OWNER_OVERRIDES = {
|
|
|
30
30
|
volume: ["ynca", "xml", "yxc"],
|
|
31
31
|
// §3c write loss — YXC is read-only for these, YNCA (and often XML) is writable.
|
|
32
32
|
"advanced.maxVolume": ["ynca", "xml", "yxc"],
|
|
33
|
+
// The DAB preset is a writable recall on YNCA but display-only in the YXC play info.
|
|
34
|
+
"tuner.dab.preset": ["ynca", "yxc"],
|
|
33
35
|
"sound.extraBass": ["ynca", "xml", "yxc"],
|
|
34
36
|
"sound.adaptiveDrc": ["ynca", "xml", "yxc"],
|
|
35
37
|
"sound.surroundDecoder": ["ynca", "yxc"],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/catalog/owner-policy.ts"],
|
|
4
|
-
"sourcesContent": ["/** The transports the adapter speaks. Ordered most-modern-first \u2014 the default ownership rank. */\nexport type Transport = \"yxc\" | \"ynca\" | \"xml\";\n\n/**\n * Ownership preference by modernity, used when a shared capability is equally good on each\n * transport (krobi: \"wenn mehrere Protokolle dasselbe k\u00F6nnen, das modernste nutzen\"). YXC is\n * push + structured JSON, YNCA the text-poll base, XML the pre-2010 fallback.\n */\nconst MODERNITY: readonly Transport[] = [\"yxc\", \"ynca\", \"xml\"];\n\n/**\n * Per-capability ownership preference that OVERRIDES pure modernity \u2014 from the capability\n * census (`Ressourcen/yamaha/capability-census-2026-08-11.md` \u00A73). These are the shared keys\n * where YXC is present but NOT equivalent: wrong scale, read-only, or a poorer type. Each list\n * is the preferred owner order for that key; the first present transport wins.\n */\nconst OWNER_OVERRIDES: Record<string, readonly Transport[]> = {\n // \u00A73a scale conflict \u2014 YXC volume is the raw 0..161 device scale, YNCA/XML are dB. Keep dB.\n volume: [\"ynca\", \"xml\", \"yxc\"],\n // \u00A73c write loss \u2014 YXC is read-only for these, YNCA (and often XML) is writable.\n \"advanced.maxVolume\": [\"ynca\", \"xml\", \"yxc\"],\n \"sound.extraBass\": [\"ynca\", \"xml\", \"yxc\"],\n \"sound.adaptiveDrc\": [\"ynca\", \"xml\", \"yxc\"],\n \"sound.surroundDecoder\": [\"ynca\", \"yxc\"],\n \"sound.dialogueLift\": [\"xml\", \"yxc\"],\n // \u00A73d richness loss \u2014 YNCA carries an enum dropdown that YXC/XML flatten to a free string.\n input: [\"ynca\", \"yxc\", \"xml\"],\n soundProgram: [\"ynca\", \"yxc\", \"xml\"],\n sleep: [\"ynca\", \"xml\", \"yxc\"],\n \"tuner.band\": [\"ynca\", \"yxc\"],\n};\n\n/**\n * Per-transport state-id \u2192 canonical capability key, for the ids that drift between transports\n * (census \u00A73f, verified against the catalogs). The canonical key is the census left column; the\n * transports not listed already use the canonical id. Zone prefixes are stripped separately.\n */\nconst ID_DRIFT: Partial<Record<Transport, Readonly<Record<string, string>>>> = {\n yxc: { subwooferVolume: \"sound.subwooferTrim\", \"multiroom.partyEnable\": \"multiroom.party\" },\n xml: { hdmiOut1: \"hdmi.out1\", hdmiOut2: \"hdmi.out2\" },\n};\n\n/**\n * The transport-neutral capability key for a transport's state id: strip a `zone2/3/4.` prefix\n * to the per-zone template, then resolve any id drift to the canonical key. Two transports that\n * express the same capability under different ids therefore map to the same key.\n *\n * @param transport the transport the state id comes from\n * @param stateId the transport's own state id (may carry a zone prefix)\n * @returns the canonical capability key\n */\nexport function capabilityKeyOf(transport: Transport, stateId: string): string {\n const template = stateId.replace(/^(?:multiroom\\.)?zone[234]\\./, \"\");\n return ID_DRIFT[transport]?.[template] ?? template;\n}\n\n/**\n * The drift-resolved, transport-neutral OBJECT id \u2014 like {@link capabilityKeyOf} but KEEPS the\n * zone prefix. capabilityKeyOf drops the zone to decide ownership per template; canonicalIdOf\n * keeps it to place the per-zone node in the unified tree. Two transports' zoned ids for the\n * same capability therefore collapse to one node id.\n *\n * @param transport the transport the state id comes from\n * @param stateId the transport's own state id\n * @returns the canonical object id (zone prefix kept, drift resolved)\n */\nexport function canonicalIdOf(transport: Transport, stateId: string): string {\n const zone = /^(?:multiroom\\.)?zone[234]\\./.exec(stateId)?.[0] ?? \"\";\n const template = stateId.slice(zone.length);\n const resolved = ID_DRIFT[transport]?.[template] ?? template;\n if (zone && !zone.startsWith(\"multiroom.\")) {\n return `multiroom.${zone}${resolved}`;\n }\n return zone + resolved;\n}\n\n/**\n * Decide which transport owns a capability, given the transports that actually offer it on\n * this device. Default is the most modern; a census-driven override wins where the modern\n * transport would be lossy. An override that lists none of the present candidates falls back\n * to modernity, so any non-empty candidate set resolves.\n *\n * @param key the transport-neutral capability key (the unified state id)\n * @param candidates the present transports that offer this key (non-empty)\n * @returns the owning transport\n */\nexport function pickOwner(key: string, candidates: readonly Transport[]): Transport {\n const preference = OWNER_OVERRIDES[key] ?? MODERNITY;\n // The MODERNITY fallback is defence for a FUTURE override: every entry in the\n // current table lists at least two transports, so an override can never miss all\n // present candidates while more than one is present \u2014 today it is unobservable.\n const owner = preference.find(t => candidates.includes(t)) ?? MODERNITY.find(t => candidates.includes(t));\n return owner ?? candidates[0];\n}\n\n/**\n * Build the owner map for a device: given which capability keys each present transport offers,\n * resolve every key to exactly one owning transport (via {@link pickOwner}). This is the\n * dedup decision behind the unified object tree \u2014 each state is written by one transport only.\n *\n * @param offered the capability keys each present transport offers (absent transport = none)\n * @returns a map from capability key to its owning transport\n */\nexport function resolveOwnership(offered: Partial<Record<Transport, readonly string[]>>): Map<string, Transport> {\n const candidates = new Map<string, Transport[]>();\n // Iterate in modernity order so the candidate lists are deterministic.\n for (const transport of MODERNITY) {\n for (const key of offered[transport] ?? []) {\n const list = candidates.get(key);\n if (list) {\n list.push(transport);\n } else {\n candidates.set(key, [transport]);\n }\n }\n }\n const owners = new Map<string, Transport>();\n for (const [key, cands] of candidates) {\n owners.set(key, pickOwner(key, cands));\n }\n return owners;\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,MAAM,YAAkC,CAAC,OAAO,QAAQ,KAAK;AAQ7D,MAAM,kBAAwD;AAAA;AAAA,EAE5D,QAAQ,CAAC,QAAQ,OAAO,KAAK;AAAA;AAAA,EAE7B,sBAAsB,CAAC,QAAQ,OAAO,KAAK;AAAA,
|
|
4
|
+
"sourcesContent": ["/** The transports the adapter speaks. Ordered most-modern-first \u2014 the default ownership rank. */\nexport type Transport = \"yxc\" | \"ynca\" | \"xml\";\n\n/**\n * Ownership preference by modernity, used when a shared capability is equally good on each\n * transport (krobi: \"wenn mehrere Protokolle dasselbe k\u00F6nnen, das modernste nutzen\"). YXC is\n * push + structured JSON, YNCA the text-poll base, XML the pre-2010 fallback.\n */\nconst MODERNITY: readonly Transport[] = [\"yxc\", \"ynca\", \"xml\"];\n\n/**\n * Per-capability ownership preference that OVERRIDES pure modernity \u2014 from the capability\n * census (`Ressourcen/yamaha/capability-census-2026-08-11.md` \u00A73). These are the shared keys\n * where YXC is present but NOT equivalent: wrong scale, read-only, or a poorer type. Each list\n * is the preferred owner order for that key; the first present transport wins.\n */\nconst OWNER_OVERRIDES: Record<string, readonly Transport[]> = {\n // \u00A73a scale conflict \u2014 YXC volume is the raw 0..161 device scale, YNCA/XML are dB. Keep dB.\n volume: [\"ynca\", \"xml\", \"yxc\"],\n // \u00A73c write loss \u2014 YXC is read-only for these, YNCA (and often XML) is writable.\n \"advanced.maxVolume\": [\"ynca\", \"xml\", \"yxc\"],\n // The DAB preset is a writable recall on YNCA but display-only in the YXC play info.\n \"tuner.dab.preset\": [\"ynca\", \"yxc\"],\n \"sound.extraBass\": [\"ynca\", \"xml\", \"yxc\"],\n \"sound.adaptiveDrc\": [\"ynca\", \"xml\", \"yxc\"],\n \"sound.surroundDecoder\": [\"ynca\", \"yxc\"],\n \"sound.dialogueLift\": [\"xml\", \"yxc\"],\n // \u00A73d richness loss \u2014 YNCA carries an enum dropdown that YXC/XML flatten to a free string.\n input: [\"ynca\", \"yxc\", \"xml\"],\n soundProgram: [\"ynca\", \"yxc\", \"xml\"],\n sleep: [\"ynca\", \"xml\", \"yxc\"],\n \"tuner.band\": [\"ynca\", \"yxc\"],\n};\n\n/**\n * Per-transport state-id \u2192 canonical capability key, for the ids that drift between transports\n * (census \u00A73f, verified against the catalogs). The canonical key is the census left column; the\n * transports not listed already use the canonical id. Zone prefixes are stripped separately.\n */\nconst ID_DRIFT: Partial<Record<Transport, Readonly<Record<string, string>>>> = {\n yxc: { subwooferVolume: \"sound.subwooferTrim\", \"multiroom.partyEnable\": \"multiroom.party\" },\n xml: { hdmiOut1: \"hdmi.out1\", hdmiOut2: \"hdmi.out2\" },\n};\n\n/**\n * The transport-neutral capability key for a transport's state id: strip a `zone2/3/4.` prefix\n * to the per-zone template, then resolve any id drift to the canonical key. Two transports that\n * express the same capability under different ids therefore map to the same key.\n *\n * @param transport the transport the state id comes from\n * @param stateId the transport's own state id (may carry a zone prefix)\n * @returns the canonical capability key\n */\nexport function capabilityKeyOf(transport: Transport, stateId: string): string {\n const template = stateId.replace(/^(?:multiroom\\.)?zone[234]\\./, \"\");\n return ID_DRIFT[transport]?.[template] ?? template;\n}\n\n/**\n * The drift-resolved, transport-neutral OBJECT id \u2014 like {@link capabilityKeyOf} but KEEPS the\n * zone prefix. capabilityKeyOf drops the zone to decide ownership per template; canonicalIdOf\n * keeps it to place the per-zone node in the unified tree. Two transports' zoned ids for the\n * same capability therefore collapse to one node id.\n *\n * @param transport the transport the state id comes from\n * @param stateId the transport's own state id\n * @returns the canonical object id (zone prefix kept, drift resolved)\n */\nexport function canonicalIdOf(transport: Transport, stateId: string): string {\n const zone = /^(?:multiroom\\.)?zone[234]\\./.exec(stateId)?.[0] ?? \"\";\n const template = stateId.slice(zone.length);\n const resolved = ID_DRIFT[transport]?.[template] ?? template;\n if (zone && !zone.startsWith(\"multiroom.\")) {\n return `multiroom.${zone}${resolved}`;\n }\n return zone + resolved;\n}\n\n/**\n * Decide which transport owns a capability, given the transports that actually offer it on\n * this device. Default is the most modern; a census-driven override wins where the modern\n * transport would be lossy. An override that lists none of the present candidates falls back\n * to modernity, so any non-empty candidate set resolves.\n *\n * @param key the transport-neutral capability key (the unified state id)\n * @param candidates the present transports that offer this key (non-empty)\n * @returns the owning transport\n */\nexport function pickOwner(key: string, candidates: readonly Transport[]): Transport {\n const preference = OWNER_OVERRIDES[key] ?? MODERNITY;\n // The MODERNITY fallback is defence for a FUTURE override: every entry in the\n // current table lists at least two transports, so an override can never miss all\n // present candidates while more than one is present \u2014 today it is unobservable.\n const owner = preference.find(t => candidates.includes(t)) ?? MODERNITY.find(t => candidates.includes(t));\n return owner ?? candidates[0];\n}\n\n/**\n * Build the owner map for a device: given which capability keys each present transport offers,\n * resolve every key to exactly one owning transport (via {@link pickOwner}). This is the\n * dedup decision behind the unified object tree \u2014 each state is written by one transport only.\n *\n * @param offered the capability keys each present transport offers (absent transport = none)\n * @returns a map from capability key to its owning transport\n */\nexport function resolveOwnership(offered: Partial<Record<Transport, readonly string[]>>): Map<string, Transport> {\n const candidates = new Map<string, Transport[]>();\n // Iterate in modernity order so the candidate lists are deterministic.\n for (const transport of MODERNITY) {\n for (const key of offered[transport] ?? []) {\n const list = candidates.get(key);\n if (list) {\n list.push(transport);\n } else {\n candidates.set(key, [transport]);\n }\n }\n }\n const owners = new Map<string, Transport>();\n for (const [key, cands] of candidates) {\n owners.set(key, pickOwner(key, cands));\n }\n return owners;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,MAAM,YAAkC,CAAC,OAAO,QAAQ,KAAK;AAQ7D,MAAM,kBAAwD;AAAA;AAAA,EAE5D,QAAQ,CAAC,QAAQ,OAAO,KAAK;AAAA;AAAA,EAE7B,sBAAsB,CAAC,QAAQ,OAAO,KAAK;AAAA;AAAA,EAE3C,oBAAoB,CAAC,QAAQ,KAAK;AAAA,EAClC,mBAAmB,CAAC,QAAQ,OAAO,KAAK;AAAA,EACxC,qBAAqB,CAAC,QAAQ,OAAO,KAAK;AAAA,EAC1C,yBAAyB,CAAC,QAAQ,KAAK;AAAA,EACvC,sBAAsB,CAAC,OAAO,KAAK;AAAA;AAAA,EAEnC,OAAO,CAAC,QAAQ,OAAO,KAAK;AAAA,EAC5B,cAAc,CAAC,QAAQ,OAAO,KAAK;AAAA,EACnC,OAAO,CAAC,QAAQ,OAAO,KAAK;AAAA,EAC5B,cAAc,CAAC,QAAQ,KAAK;AAC9B;AAOA,MAAM,WAAyE;AAAA,EAC7E,KAAK,EAAE,iBAAiB,uBAAuB,yBAAyB,kBAAkB;AAAA,EAC1F,KAAK,EAAE,UAAU,aAAa,UAAU,YAAY;AACtD;AAWO,SAAS,gBAAgB,WAAsB,SAAyB;AArD/E;AAsDE,QAAM,WAAW,QAAQ,QAAQ,gCAAgC,EAAE;AACnE,UAAO,oBAAS,SAAS,MAAlB,mBAAsB,cAAtB,YAAmC;AAC5C;AAYO,SAAS,cAAc,WAAsB,SAAyB;AApE7E;AAqEE,QAAM,QAAO,0CAA+B,KAAK,OAAO,MAA3C,mBAA+C,OAA/C,YAAqD;AAClE,QAAM,WAAW,QAAQ,MAAM,KAAK,MAAM;AAC1C,QAAM,YAAW,oBAAS,SAAS,MAAlB,mBAAsB,cAAtB,YAAmC;AACpD,MAAI,QAAQ,CAAC,KAAK,WAAW,YAAY,GAAG;AAC1C,WAAO,aAAa,IAAI,GAAG,QAAQ;AAAA,EACrC;AACA,SAAO,OAAO;AAChB;AAYO,SAAS,UAAU,KAAa,YAA6C;AAxFpF;AAyFE,QAAM,cAAa,qBAAgB,GAAG,MAAnB,YAAwB;AAI3C,QAAM,SAAQ,gBAAW,KAAK,OAAK,WAAW,SAAS,CAAC,CAAC,MAA3C,YAAgD,UAAU,KAAK,OAAK,WAAW,SAAS,CAAC,CAAC;AACxG,SAAO,wBAAS,WAAW,CAAC;AAC9B;AAUO,SAAS,iBAAiB,SAAgF;AAzGjH;AA0GE,QAAM,aAAa,oBAAI,IAAyB;AAEhD,aAAW,aAAa,WAAW;AACjC,eAAW,QAAO,aAAQ,SAAS,MAAjB,YAAsB,CAAC,GAAG;AAC1C,YAAM,OAAO,WAAW,IAAI,GAAG;AAC/B,UAAI,MAAM;AACR,aAAK,KAAK,SAAS;AAAA,MACrB,OAAO;AACL,mBAAW,IAAI,KAAK,CAAC,SAAS,CAAC;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AACA,QAAM,SAAS,oBAAI,IAAuB;AAC1C,aAAW,CAAC,KAAK,KAAK,KAAK,YAAY;AACrC,WAAO,IAAI,KAAK,UAAU,KAAK,KAAK,CAAC;AAAA,EACvC;AACA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -20,6 +20,7 @@ var value_coerce_exports = {};
|
|
|
20
20
|
__export(value_coerce_exports, {
|
|
21
21
|
decode: () => decode,
|
|
22
22
|
encode: () => encode,
|
|
23
|
+
formatWireNumber: () => formatWireNumber,
|
|
23
24
|
isWritableValue: () => isWritableValue,
|
|
24
25
|
specToCommon: () => specToCommon
|
|
25
26
|
});
|
|
@@ -99,11 +100,21 @@ function isWritableValue(value, numeric) {
|
|
|
99
100
|
}
|
|
100
101
|
return Number.isFinite(Number(value));
|
|
101
102
|
}
|
|
103
|
+
function formatWireNumber(value, decimals, step) {
|
|
104
|
+
const snapped = step ? Math.round(value / step) * step : value;
|
|
105
|
+
const magnitude = Math.abs(snapped).toFixed(decimals);
|
|
106
|
+
const sign = snapped < 0 && Number(magnitude) !== 0 ? "-" : "";
|
|
107
|
+
return sign + magnitude;
|
|
108
|
+
}
|
|
102
109
|
function encode(spec, value) {
|
|
103
110
|
switch (spec.kind) {
|
|
104
111
|
case "onoff":
|
|
105
112
|
return value ? spec.on : spec.off;
|
|
106
113
|
case "number":
|
|
114
|
+
if (spec.decimals !== void 0) {
|
|
115
|
+
return formatWireNumber(Number(value), spec.decimals, spec.step);
|
|
116
|
+
}
|
|
117
|
+
return String(value);
|
|
107
118
|
case "enum":
|
|
108
119
|
case "text":
|
|
109
120
|
return String(value);
|
|
@@ -119,6 +130,7 @@ function encode(spec, value) {
|
|
|
119
130
|
0 && (module.exports = {
|
|
120
131
|
decode,
|
|
121
132
|
encode,
|
|
133
|
+
formatWireNumber,
|
|
122
134
|
isWritableValue,
|
|
123
135
|
specToCommon
|
|
124
136
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/catalog/value-coerce.ts"],
|
|
4
|
-
"sourcesContent": ["/** An on/off value \u2192 a boolean state. */\nexport interface OnOffSpec {\n /** Discriminant. */\n kind: \"onoff\";\n /** Wire value that maps to `true`. */\n on: string;\n /** Wire value that maps to `false`. */\n off: string;\n}\n\n/** A fixed multi-value choice \u2192 a string state with a `states` dropdown. */\nexport interface EnumSpec {\n /** Discriminant. */\n kind: \"enum\";\n /** Wire value \u2192 display label (usually identical for YNCA). */\n states: Record<string, string>;\n}\n\n/** A numeric value \u2192 a number state with an optional unit and range. */\nexport interface NumberSpec {\n /** Discriminant. */\n kind: \"number\";\n /** Unit for the numeric state (e.g. \"dB\"). */\n unit?: string;\n /** Minimum value. */\n min?: number;\n /** Maximum value. */\n max?: number;\n /** Step size. */\n step?: number;\n}\n\n/** A free-text value \u2192 a string state. */\nexport interface TextSpec {\n /** Discriminant. */\n kind: \"text\";\n}\n\n/**\n * A coded value: a fixed set of wire tokens maps to numeric codes and back, so a player\n * datum whose wire form is text (\"Play\"/\"Pause\"/\"Stop\") becomes a real `number` state\n * carrying its labels \u2014 the form the type-detector's `media.state` / `media.mode.repeat`\n * slots require. The code table lives on the spec (per state), so the write path stays\n * value-based, not a fragile global name\u2192wire guess.\n */\nexport interface CodeSpec {\n /** Discriminant. */\n kind: \"code\";\n /** Wire token \u2192 numeric code; the reverse encodes a written code back to the wire. */\n codes: Record<string, number>;\n /** Numeric code \u2192 display label for the states dropdown. */\n labels: Record<number, string>;\n}\n\n/**\n * A write-only action button \u2192 a boolean state that, when written, sends one fixed wire\n * value supplied by the entry's `wireEncode`. `read:false` so it never mirrors device state\n * (e.g. a \"next track\" button that puts `Skip Fwd` on the wire).\n */\nexport interface ButtonSpec {\n /** Discriminant. */\n kind: \"button\";\n}\n\n/**\n * The value semantics of a device function, protocol-agnostic. A catalog entry\n * carries one of these so the adapter can turn a raw protocol value into a\n * properly typed, user-friendly ioBroker state instead of a bare string.\n */\nexport type ValueSpec = OnOffSpec | EnumSpec | NumberSpec | TextSpec | CodeSpec | ButtonSpec;\n\n/** The ioBroker `common` fields this layer derives from a {@link ValueSpec}. */\nexport interface StateCommon {\n /** ioBroker value type. */\n type: \"boolean\" | \"number\" | \"string\";\n /** ioBroker role. */\n role: string;\n /** Always readable. */\n read: boolean;\n /** Whether the state accepts writes. */\n write: boolean;\n /** Unit for numeric states. */\n unit?: string;\n /** Minimum for numeric states. */\n min?: number;\n /** Maximum for numeric states. */\n max?: number;\n /** Step for numeric states. */\n step?: number;\n /** Predefined value \u2192 label map for a dropdown. */\n states?: Record<string, string>;\n}\n\n/**\n * Derive the ioBroker `common` for a value spec \u2014 the core of the intelligent\n * typing: an on/off value becomes a real boolean, not the raw \"On\"/\"Off\" text.\n *\n * @param spec the value semantics\n * @param opts write flag and an optional role override\n * @param opts.write whether the state accepts writes\n * @param opts.role optional role override (else derived from the spec kind)\n * @returns the derived common fields\n */\nexport function specToCommon(spec: ValueSpec, opts: { write?: boolean; role?: string } = {}): StateCommon {\n const write = opts.write ?? false;\n switch (spec.kind) {\n case \"onoff\":\n return { type: \"boolean\", role: opts.role ?? \"switch\", read: true, write };\n case \"enum\":\n return { type: \"string\", role: opts.role ?? \"state\", read: true, write, states: spec.states };\n case \"number\": {\n // Only set the range fields the spec actually carries \u2014 a stray\n // `{ min: undefined }` would break the object-tree fixture comparisons.\n const common: StateCommon = { type: \"number\", role: opts.role ?? (write ? \"level\" : \"value\"), read: true, write };\n if (spec.unit !== undefined) {\n common.unit = spec.unit;\n }\n if (spec.min !== undefined) {\n common.min = spec.min;\n }\n if (spec.max !== undefined) {\n common.max = spec.max;\n }\n if (spec.step !== undefined) {\n common.step = spec.step;\n }\n return common;\n }\n case \"text\":\n return { type: \"string\", role: opts.role ?? \"text\", read: true, write };\n case \"code\": {\n const states: Record<string, string> = {};\n for (const [code, label] of Object.entries(spec.labels)) {\n states[code] = label;\n }\n return { type: \"number\", role: opts.role ?? \"value\", read: true, write, states };\n }\n case \"button\":\n return { type: \"boolean\", role: opts.role ?? \"button\", read: false, write: true };\n }\n}\n\n/**\n * Strict decimal: only a plain finite decimal counts as a number, so garbage\n * (\"N/A\", \"12abc\") never lands in a number state (same line as the nut adapter).\n */\nconst DECIMAL_RE = /^-?\\d+(\\.\\d+)?$/;\n\n/**\n * Turn a raw protocol value into the typed ioBroker state value for a spec: an\n * on/off value becomes a real boolean, a numeric string a finite number (or\n * `undefined` if it is not a clean decimal), enum/text stay strings.\n *\n * @param spec the value semantics\n * @param wire the raw value received from the device\n * @returns the typed value, or undefined when a numeric value cannot be parsed\n */\nexport function decode(spec: ValueSpec, wire: string): boolean | number | string | undefined {\n switch (spec.kind) {\n case \"onoff\":\n // A third, unexpected wire value must not silently become `false` \u2014 that would\n // write back an explicit \"off\" command. Report it as unknown (undefined).\n if (wire === spec.on) {\n return true;\n }\n if (wire === spec.off) {\n return false;\n }\n return undefined;\n case \"number\": {\n const trimmed = wire.trim();\n return DECIMAL_RE.test(trimmed) ? Number(trimmed) : undefined;\n }\n case \"enum\":\n case \"text\":\n return wire;\n case \"code\": {\n // A wire token the device does not know maps to no code \u2014 report unknown (undefined)\n // rather than a bogus 0, the same way onoff rejects a third wire value.\n // (The ternary is for readability \u2014 a missing key already yields undefined.)\n const code = spec.codes[wire];\n return code === undefined ? undefined : code;\n }\n case \"button\":\n return undefined; // write-only action \u2014 never read back into a state\n }\n}\n\n/**\n * Whether a written state value may be sent to the device: never `null`/`undefined`\n * (both are regular ioBroker state values but not valid commands), and a finite\n * number for a numeric state \u2014 so a stray `null` or `\"abc\"` is dropped rather than\n * turned into a bogus command (`@TUN:AMFREQ=null`, `<Val>NaN</Val>`).\n *\n * @param value the value written to the state\n * @param numeric whether the target state is numeric\n * @returns true if the value is safe to encode and send\n */\nexport function isWritableValue(value: unknown, numeric: boolean): boolean {\n if (value === null || value === undefined) {\n return false;\n }\n if (!numeric) {\n return true;\n }\n // Reject an empty/whitespace string explicitly: Number(\"\") is 0 (finite), so it\n // would otherwise slip through and put an empty value on the wire.\n if (typeof value === \"string\" && value.trim() === \"\") {\n return false;\n }\n return Number.isFinite(Number(value));\n}\n\n/**\n * Turn a typed ioBroker state value back into the raw protocol value: a boolean\n * becomes the on/off wire token, everything else its string form.\n *\n * @param spec the value semantics\n * @param value the ioBroker state value written by the user\n * @returns the wire value to send to the device\n */\nexport function encode(spec: ValueSpec, value: boolean | number | string): string {\n switch (spec.kind) {\n case \"onoff\":\n return value ? spec.on : spec.off;\n case \"number\":\n case \"enum\":\n case \"text\":\n return String(value);\n case \"code\": {\n // Reverse the code table: the written numeric code back to its wire token.\n const token = Object.keys(spec.codes).find(w => spec.codes[w] === value);\n return token ?? String(value);\n }\n case \"button\":\n // Unreachable: a button entry always carries a wireEncode that supplies the fixed\n // command, so yncaCommand never falls back to encode() here.\n return \"\";\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
4
|
+
"sourcesContent": ["/** An on/off value \u2192 a boolean state. */\nexport interface OnOffSpec {\n /** Discriminant. */\n kind: \"onoff\";\n /** Wire value that maps to `true`. */\n on: string;\n /** Wire value that maps to `false`. */\n off: string;\n}\n\n/** A fixed multi-value choice \u2192 a string state with a `states` dropdown. */\nexport interface EnumSpec {\n /** Discriminant. */\n kind: \"enum\";\n /** Wire value \u2192 display label (usually identical for YNCA). */\n states: Record<string, string>;\n}\n\n/** A numeric value \u2192 a number state with an optional unit and range. */\nexport interface NumberSpec {\n /** Discriminant. */\n kind: \"number\";\n /** Unit for the numeric state (e.g. \"dB\"). */\n unit?: string;\n /** Minimum value. */\n min?: number;\n /** Maximum value. */\n max?: number;\n /** Step size. */\n step?: number;\n /**\n * Fixed decimal count the WIRE value must carry (YNCA float/int functions). Without\n * the mandatory decimal point the receiver reads the digits as tenths \u2014 \"VOL=-38\"\n * arrived as -3.8 dB (#612). When set, {@link encode} snaps to {@link step} (if any)\n * and formats with exactly this many decimals, mirroring ynca-python's\n * number_to_string_with_stepsize. Unset keeps the plain String(value) form.\n */\n decimals?: number;\n}\n\n/** A free-text value \u2192 a string state. */\nexport interface TextSpec {\n /** Discriminant. */\n kind: \"text\";\n}\n\n/**\n * A coded value: a fixed set of wire tokens maps to numeric codes and back, so a player\n * datum whose wire form is text (\"Play\"/\"Pause\"/\"Stop\") becomes a real `number` state\n * carrying its labels \u2014 the form the type-detector's `media.state` / `media.mode.repeat`\n * slots require. The code table lives on the spec (per state), so the write path stays\n * value-based, not a fragile global name\u2192wire guess.\n */\nexport interface CodeSpec {\n /** Discriminant. */\n kind: \"code\";\n /** Wire token \u2192 numeric code; the reverse encodes a written code back to the wire. */\n codes: Record<string, number>;\n /** Numeric code \u2192 display label for the states dropdown. */\n labels: Record<number, string>;\n}\n\n/**\n * A write-only action button \u2192 a boolean state that, when written, sends one fixed wire\n * value supplied by the entry's `wireEncode`. `read:false` so it never mirrors device state\n * (e.g. a \"next track\" button that puts `Skip Fwd` on the wire).\n */\nexport interface ButtonSpec {\n /** Discriminant. */\n kind: \"button\";\n}\n\n/**\n * The value semantics of a device function, protocol-agnostic. A catalog entry\n * carries one of these so the adapter can turn a raw protocol value into a\n * properly typed, user-friendly ioBroker state instead of a bare string.\n */\nexport type ValueSpec = OnOffSpec | EnumSpec | NumberSpec | TextSpec | CodeSpec | ButtonSpec;\n\n/** The ioBroker `common` fields this layer derives from a {@link ValueSpec}. */\nexport interface StateCommon {\n /** ioBroker value type. */\n type: \"boolean\" | \"number\" | \"string\";\n /** ioBroker role. */\n role: string;\n /** Always readable. */\n read: boolean;\n /** Whether the state accepts writes. */\n write: boolean;\n /** Unit for numeric states. */\n unit?: string;\n /** Minimum for numeric states. */\n min?: number;\n /** Maximum for numeric states. */\n max?: number;\n /** Step for numeric states. */\n step?: number;\n /** Predefined value \u2192 label map for a dropdown. */\n states?: Record<string, string>;\n}\n\n/**\n * Derive the ioBroker `common` for a value spec \u2014 the core of the intelligent\n * typing: an on/off value becomes a real boolean, not the raw \"On\"/\"Off\" text.\n *\n * @param spec the value semantics\n * @param opts write flag and an optional role override\n * @param opts.write whether the state accepts writes\n * @param opts.role optional role override (else derived from the spec kind)\n * @returns the derived common fields\n */\nexport function specToCommon(spec: ValueSpec, opts: { write?: boolean; role?: string } = {}): StateCommon {\n const write = opts.write ?? false;\n switch (spec.kind) {\n case \"onoff\":\n return { type: \"boolean\", role: opts.role ?? \"switch\", read: true, write };\n case \"enum\":\n return { type: \"string\", role: opts.role ?? \"state\", read: true, write, states: spec.states };\n case \"number\": {\n // Only set the range fields the spec actually carries \u2014 a stray\n // `{ min: undefined }` would break the object-tree fixture comparisons.\n const common: StateCommon = { type: \"number\", role: opts.role ?? (write ? \"level\" : \"value\"), read: true, write };\n if (spec.unit !== undefined) {\n common.unit = spec.unit;\n }\n if (spec.min !== undefined) {\n common.min = spec.min;\n }\n if (spec.max !== undefined) {\n common.max = spec.max;\n }\n if (spec.step !== undefined) {\n common.step = spec.step;\n }\n return common;\n }\n case \"text\":\n return { type: \"string\", role: opts.role ?? \"text\", read: true, write };\n case \"code\": {\n const states: Record<string, string> = {};\n for (const [code, label] of Object.entries(spec.labels)) {\n states[code] = label;\n }\n return { type: \"number\", role: opts.role ?? \"value\", read: true, write, states };\n }\n case \"button\":\n return { type: \"boolean\", role: opts.role ?? \"button\", read: false, write: true };\n }\n}\n\n/**\n * Strict decimal: only a plain finite decimal counts as a number, so garbage\n * (\"N/A\", \"12abc\") never lands in a number state (same line as the nut adapter).\n */\nconst DECIMAL_RE = /^-?\\d+(\\.\\d+)?$/;\n\n/**\n * Turn a raw protocol value into the typed ioBroker state value for a spec: an\n * on/off value becomes a real boolean, a numeric string a finite number (or\n * `undefined` if it is not a clean decimal), enum/text stay strings.\n *\n * @param spec the value semantics\n * @param wire the raw value received from the device\n * @returns the typed value, or undefined when a numeric value cannot be parsed\n */\nexport function decode(spec: ValueSpec, wire: string): boolean | number | string | undefined {\n switch (spec.kind) {\n case \"onoff\":\n // A third, unexpected wire value must not silently become `false` \u2014 that would\n // write back an explicit \"off\" command. Report it as unknown (undefined).\n if (wire === spec.on) {\n return true;\n }\n if (wire === spec.off) {\n return false;\n }\n return undefined;\n case \"number\": {\n const trimmed = wire.trim();\n return DECIMAL_RE.test(trimmed) ? Number(trimmed) : undefined;\n }\n case \"enum\":\n case \"text\":\n return wire;\n case \"code\": {\n // A wire token the device does not know maps to no code \u2014 report unknown (undefined)\n // rather than a bogus 0, the same way onoff rejects a third wire value.\n // (The ternary is for readability \u2014 a missing key already yields undefined.)\n const code = spec.codes[wire];\n return code === undefined ? undefined : code;\n }\n case \"button\":\n return undefined; // write-only action \u2014 never read back into a state\n }\n}\n\n/**\n * Whether a written state value may be sent to the device: never `null`/`undefined`\n * (both are regular ioBroker state values but not valid commands), and a finite\n * number for a numeric state \u2014 so a stray `null` or `\"abc\"` is dropped rather than\n * turned into a bogus command (`@TUN:AMFREQ=null`, `<Val>NaN</Val>`).\n *\n * @param value the value written to the state\n * @param numeric whether the target state is numeric\n * @returns true if the value is safe to encode and send\n */\nexport function isWritableValue(value: unknown, numeric: boolean): boolean {\n if (value === null || value === undefined) {\n return false;\n }\n if (!numeric) {\n return true;\n }\n // Reject an empty/whitespace string explicitly: Number(\"\") is 0 (finite), so it\n // would otherwise slip through and put an empty value on the wire.\n if (typeof value === \"string\" && value.trim() === \"\") {\n return false;\n }\n return Number.isFinite(Number(value));\n}\n\n/**\n * Format a number the way the YNCA wire demands it: snapped to the step grid (when a\n * step is given) and with a FIXED decimal count \u2014 the reference behaviour of\n * ynca-python's number_to_string_with_stepsize, including \"never a signed zero\".\n *\n * @param value the written number\n * @param decimals the mandatory decimal count\n * @param step optional step grid to snap to\n * @returns the wire string (e.g. \"-38.0\")\n */\nexport function formatWireNumber(value: number, decimals: number, step?: number): string {\n const snapped = step ? Math.round(value / step) * step : value;\n const magnitude = Math.abs(snapped).toFixed(decimals);\n const sign = snapped < 0 && Number(magnitude) !== 0 ? \"-\" : \"\";\n return sign + magnitude;\n}\n\n/**\n * Turn a typed ioBroker state value back into the raw protocol value: a boolean\n * becomes the on/off wire token, everything else its string form.\n *\n * @param spec the value semantics\n * @param value the ioBroker state value written by the user\n * @returns the wire value to send to the device\n */\nexport function encode(spec: ValueSpec, value: boolean | number | string): string {\n switch (spec.kind) {\n case \"onoff\":\n return value ? spec.on : spec.off;\n case \"number\":\n if (spec.decimals !== undefined) {\n return formatWireNumber(Number(value), spec.decimals, spec.step);\n }\n return String(value);\n case \"enum\":\n case \"text\":\n return String(value);\n case \"code\": {\n // Reverse the code table: the written numeric code back to its wire token.\n const token = Object.keys(spec.codes).find(w => spec.codes[w] === value);\n return token ?? String(value);\n }\n case \"button\":\n // Unreachable: a button entry always carries a wireEncode that supplies the fixed\n // command, so yncaCommand never falls back to encode() here.\n return \"\";\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|