iobroker.yamaha 2.9.0 → 2.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -15
- package/build/device-management-helpers.js +3 -2
- package/build/device-management-helpers.js.map +2 -2
- package/build/device-management.js +36 -30
- package/build/device-management.js.map +2 -2
- package/build/lib/pure-helpers.js +2 -2
- package/build/lib/pure-helpers.js.map +2 -2
- package/build/main.js +59 -3
- package/build/main.js.map +2 -2
- package/io-package.json +27 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,15 @@ Details on all settings, the object tree and the ports the adapter uses are in t
|
|
|
74
74
|
Placeholder for the next version (at the beginning of the line):
|
|
75
75
|
### **WORK IN PROGRESS**
|
|
76
76
|
-->
|
|
77
|
+
### 2.9.2 (2026-09-12)
|
|
78
|
+
|
|
79
|
+
- (krobipd) New: The device card shows a 0–100 % badge while that receiver's volume is in percent, so you can tell the two scales apart at a glance
|
|
80
|
+
- (krobipd) Fixed: The percent setting is made in one place again — the device's edit dialog; the extra switch on the card showed the wrong position and is gone
|
|
81
|
+
|
|
82
|
+
### 2.9.1 (2026-09-12)
|
|
83
|
+
|
|
84
|
+
- (krobipd) Fixed: A receiver the network search found keeps its datapoints when you add a device by hand — they stay with their history and are marked offline instead of deleted
|
|
85
|
+
|
|
77
86
|
### 2.9.0 (2026-09-12)
|
|
78
87
|
|
|
79
88
|
- (krobipd) New: Devices you enter by hand and devices the network search finds now run side by side — entering one receiver no longer takes every found one out of the instance
|
|
@@ -102,21 +111,6 @@ Details on all settings, the object tree and the ports the adapter uses are in t
|
|
|
102
111
|
- (krobipd) New: Inputs now appear under the names the receiver carries for them, so a socket named “Apple TV” reads that way instead of HDMI1.
|
|
103
112
|
- (krobipd) Changed: After this update every receiver is asked about its abilities once more, so the first start takes a little longer than usual.
|
|
104
113
|
|
|
105
|
-
### 2.7.1 (2026-09-09)
|
|
106
|
-
|
|
107
|
-
- (krobipd) Improved: The first connection asks a receiver only what its generation can answer, so zones, trigger sockets and per-input settings follow the device, not a catalogue.
|
|
108
|
-
- (krobipd) New: A datapoint the receiver reveals later now appears at once — a function it starts answering, a value it reports for the first time, a status field it begins delivering.
|
|
109
|
-
- (krobipd) Changed: A datapoint that never carried a value is removed only after two starts confirm it, so a receiver left in standby no longer loses datapoints it still has.
|
|
110
|
-
|
|
111
|
-
### 2.6.0 (2026-09-09)
|
|
112
|
-
|
|
113
|
-
- (krobipd) Fixed: input and sound program lists now offer only what the receiver itself declares or proves it has, instead of every value any Yamaha may have (#619)
|
|
114
|
-
- (krobipd) Fixed: the 2008 receiver generation gets volume, mute and sound program back; HDMI output, aspect, resolution and decoder lists carry the values the receiver reports
|
|
115
|
-
- (krobipd) New: HD Radio and Sirius on the US models, zone balance, pre-out mode and zone scenes, party volume keys, HDMI video mode, lip sync, a second trigger output and speaker pattern
|
|
116
|
-
- (krobipd) New: on older XML receivers the enhancer, CINEMA DSP 3D, speaker A/B, Zone B, a zone-wide cursor pad, transport keys and zone names; MusicCast gains standby-through and speaker pattern
|
|
117
|
-
- (krobipd) Improved: a receiver is set up from its own declaration of zones and inputs, so it comes online faster and is learned again by itself after an update that changes how it is read
|
|
118
|
-
- (krobipd) Improved: the first connection to a YNCA receiver asks fewer questions, so its datapoints appear sooner
|
|
119
|
-
|
|
120
114
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
121
115
|
|
|
122
116
|
## History
|
|
@@ -54,8 +54,9 @@ function buildDeviceForm(usedIps) {
|
|
|
54
54
|
md: 6
|
|
55
55
|
},
|
|
56
56
|
// Per device, not per instance: the adapter serves several receivers, and one of them
|
|
57
|
-
// wanting percent says nothing about the others.
|
|
58
|
-
//
|
|
57
|
+
// wanting percent says nothing about the others. THE place to set it — 2.9.1 also had a
|
|
58
|
+
// switch control on the card itself, and one value reachable from two independently-read
|
|
59
|
+
// places is how the card came to show "off" while this checkbox showed "on".
|
|
59
60
|
volumeAsPercent: {
|
|
60
61
|
newLine: true,
|
|
61
62
|
type: "checkbox",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/device-management-helpers.ts"],
|
|
4
|
-
"sourcesContent": ["import type { JsonFormSchema } from \"@iobroker/dm-utils\";\nimport { t } from \"./lib/i18n\";\nimport { sanitizeId } from \"./lib/pure-helpers\";\nimport { TRANSPORT_LABELS } from \"./lib/ready-line\";\n\n/** Object-id segments the adapter reserves for its own tree \u2014 a device may not take them. */\nconst RESERVED_IDS = new Set([\"info\"]);\n/** IPv4 dotted-quad \u2014 the single source for both the frontend validator and the backend check. */\nconst IP_RE = /^(\\d{1,3}\\.){3}\\d{1,3}$/;\n\n/** The transports shown as card indicators \u2014 the single source shared with the ready-log line. */\nexport const TRANSPORTS = TRANSPORT_LABELS;\n\n/** One raw manual device row from `native.devices` (the name is optional). */\nexport interface ManualRow {\n /** Display name, or empty/absent when the user left it blank. */\n name?: string;\n /** The device IP address. */\n ip: string;\n}\n\n/** A running device as shown on a card, plus which source it came from (routes edit/delete). */\nexport interface CardDevice {\n /** The id-safe device id (object-tree path segment). */\n id: string;\n /** The device IP address. */\n ip: string;\n /** The card header name. */\n name: string;\n /** Where it lives: the manual `native.devices` table, or the auto-discovery store. */\n source: \"manual\" | \"discovered\";\n}\n\n/**\n * The id the object tree uses for a manual row: its name, or the ip when the name is blank.\n *\n * @param row the manual device row\n * @returns the id-safe device id\n */\nexport function rowId(row: ManualRow): string {\n return sanitizeId(row.name && row.name.length > 0 ? row.name : row.ip);\n}\n\n/**\n * The add/edit form for one receiver: a display name, the IP address and whether its volume\n * datapoints read 0\u2013100 %. The IP field\n * carries a live validator against a valid dotted-quad that is not already in use (the OK\n * button greys out on a clash). Labels are resolved translation objects so the embedded\n * form is language-correct.\n *\n * @param usedIps the IPs taken by OTHER devices (the edited device excluded)\n * @returns the jsonConfig panel schema for one device\n */\nexport function buildDeviceForm(usedIps: readonly string[]): JsonFormSchema {\n const ipList = JSON.stringify([...usedIps]);\n return {\n type: \"panel\",\n items: {\n name: {\n type: \"text\",\n label: t(\"columnName\"),\n sm: 12,\n md: 6,\n },\n ip: {\n type: \"text\",\n label: t(\"columnIp\"),\n validator: `!!(data.ip && ${IP_RE.toString()}.test(data.ip)) && !${ipList}.includes(data.ip)`,\n validatorErrorText: t(\"invalidIp\"),\n validatorNoSaveOnError: true,\n sm: 12,\n md: 6,\n },\n // Per device, not per instance: the adapter serves several receivers, and one of them\n // wanting percent says nothing about the others.
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,kBAAkB;AAClB,0BAA2B;AAC3B,wBAAiC;AAGjC,MAAM,eAAe,oBAAI,IAAI,CAAC,MAAM,CAAC;AAErC,MAAM,QAAQ;AAGP,MAAM,aAAa;AA4BnB,SAAS,MAAM,KAAwB;AAC5C,aAAO,gCAAW,IAAI,QAAQ,IAAI,KAAK,SAAS,IAAI,IAAI,OAAO,IAAI,EAAE;AACvE;AAYO,SAAS,gBAAgB,SAA4C;AAC1E,QAAM,SAAS,KAAK,UAAU,CAAC,GAAG,OAAO,CAAC;AAC1C,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,MACL,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,WAAO,eAAE,YAAY;AAAA,QACrB,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,MACA,IAAI;AAAA,QACF,MAAM;AAAA,QACN,WAAO,eAAE,UAAU;AAAA,QACnB,WAAW,iBAAiB,MAAM,SAAS,CAAC,uBAAuB,MAAM;AAAA,QACzE,wBAAoB,eAAE,WAAW;AAAA,QACjC,wBAAwB;AAAA,QACxB,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA;AAAA;AAAA;AAAA,
|
|
4
|
+
"sourcesContent": ["import type { JsonFormSchema } from \"@iobroker/dm-utils\";\nimport { t } from \"./lib/i18n\";\nimport { sanitizeId } from \"./lib/pure-helpers\";\nimport { TRANSPORT_LABELS } from \"./lib/ready-line\";\n\n/** Object-id segments the adapter reserves for its own tree \u2014 a device may not take them. */\nconst RESERVED_IDS = new Set([\"info\"]);\n/** IPv4 dotted-quad \u2014 the single source for both the frontend validator and the backend check. */\nconst IP_RE = /^(\\d{1,3}\\.){3}\\d{1,3}$/;\n\n/** The transports shown as card indicators \u2014 the single source shared with the ready-log line. */\nexport const TRANSPORTS = TRANSPORT_LABELS;\n\n/** One raw manual device row from `native.devices` (the name is optional). */\nexport interface ManualRow {\n /** Display name, or empty/absent when the user left it blank. */\n name?: string;\n /** The device IP address. */\n ip: string;\n}\n\n/** A running device as shown on a card, plus which source it came from (routes edit/delete). */\nexport interface CardDevice {\n /** The id-safe device id (object-tree path segment). */\n id: string;\n /** The device IP address. */\n ip: string;\n /** The card header name. */\n name: string;\n /** Where it lives: the manual `native.devices` table, or the auto-discovery store. */\n source: \"manual\" | \"discovered\";\n}\n\n/**\n * The id the object tree uses for a manual row: its name, or the ip when the name is blank.\n *\n * @param row the manual device row\n * @returns the id-safe device id\n */\nexport function rowId(row: ManualRow): string {\n return sanitizeId(row.name && row.name.length > 0 ? row.name : row.ip);\n}\n\n/**\n * The add/edit form for one receiver: a display name, the IP address and whether its volume\n * datapoints read 0\u2013100 %. The IP field\n * carries a live validator against a valid dotted-quad that is not already in use (the OK\n * button greys out on a clash). Labels are resolved translation objects so the embedded\n * form is language-correct.\n *\n * @param usedIps the IPs taken by OTHER devices (the edited device excluded)\n * @returns the jsonConfig panel schema for one device\n */\nexport function buildDeviceForm(usedIps: readonly string[]): JsonFormSchema {\n const ipList = JSON.stringify([...usedIps]);\n return {\n type: \"panel\",\n items: {\n name: {\n type: \"text\",\n label: t(\"columnName\"),\n sm: 12,\n md: 6,\n },\n ip: {\n type: \"text\",\n label: t(\"columnIp\"),\n validator: `!!(data.ip && ${IP_RE.toString()}.test(data.ip)) && !${ipList}.includes(data.ip)`,\n validatorErrorText: t(\"invalidIp\"),\n validatorNoSaveOnError: true,\n sm: 12,\n md: 6,\n },\n // Per device, not per instance: the adapter serves several receivers, and one of them\n // wanting percent says nothing about the others. THE place to set it \u2014 2.9.1 also had a\n // switch control on the card itself, and one value reachable from two independently-read\n // places is how the card came to show \"off\" while this checkbox showed \"on\".\n volumeAsPercent: {\n newLine: true,\n type: \"checkbox\",\n label: t(\"volumeAsPercent\"),\n help: t(\"volumeAsPercent_help\"),\n sm: 12,\n md: 12,\n },\n },\n } as unknown as JsonFormSchema;\n}\n\n/**\n * A duplicate-IP/name or invalid-value clash against the other rows, as a ready-to-show\n * message \u2014 the backend safety net behind the form validator (the dialog validator may not\n * fire in every admin version; this never lets a bad row through). Each failure mode gets\n * its own message: a malformed IP is not the same problem as a name that is reserved or\n * already taken \u2014 reporting \"invalid IP\" for a name clash sends the user hunting in the\n * wrong field.\n *\n * @param rows the current manual rows\n * @param candidate the row being added/edited\n * @param exceptIndex the row position to ignore (the row being edited), or -1\n * @returns a translated clash message, or null when the row is fine\n */\nexport function findClash(\n rows: readonly ManualRow[],\n candidate: ManualRow,\n exceptIndex: number,\n): ioBroker.StringOrTranslated | null {\n if (!IP_RE.test(candidate.ip)) {\n return t(\"invalidIp\");\n }\n const id = rowId(candidate);\n if (id === \"\" || RESERVED_IDS.has(id)) {\n return t(\"invalidName\");\n }\n for (let i = 0; i < rows.length; i++) {\n if (i === exceptIndex) {\n continue;\n }\n if (rows[i].ip === candidate.ip || rowId(rows[i]) === id) {\n return t(\"duplicateDevice\");\n }\n }\n return null;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,kBAAkB;AAClB,0BAA2B;AAC3B,wBAAiC;AAGjC,MAAM,eAAe,oBAAI,IAAI,CAAC,MAAM,CAAC;AAErC,MAAM,QAAQ;AAGP,MAAM,aAAa;AA4BnB,SAAS,MAAM,KAAwB;AAC5C,aAAO,gCAAW,IAAI,QAAQ,IAAI,KAAK,SAAS,IAAI,IAAI,OAAO,IAAI,EAAE;AACvE;AAYO,SAAS,gBAAgB,SAA4C;AAC1E,QAAM,SAAS,KAAK,UAAU,CAAC,GAAG,OAAO,CAAC;AAC1C,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,MACL,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,WAAO,eAAE,YAAY;AAAA,QACrB,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,MACA,IAAI;AAAA,QACF,MAAM;AAAA,QACN,WAAO,eAAE,UAAU;AAAA,QACnB,WAAW,iBAAiB,MAAM,SAAS,CAAC,uBAAuB,MAAM;AAAA,QACzE,wBAAoB,eAAE,WAAW;AAAA,QACjC,wBAAwB;AAAA,QACxB,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA;AAAA;AAAA;AAAA;AAAA,MAKA,iBAAiB;AAAA,QACf,SAAS;AAAA,QACT,MAAM;AAAA,QACN,WAAO,eAAE,iBAAiB;AAAA,QAC1B,UAAM,eAAE,sBAAsB;AAAA,QAC9B,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,EACF;AACF;AAeO,SAAS,UACd,MACA,WACA,aACoC;AACpC,MAAI,CAAC,MAAM,KAAK,UAAU,EAAE,GAAG;AAC7B,eAAO,eAAE,WAAW;AAAA,EACtB;AACA,QAAM,KAAK,MAAM,SAAS;AAC1B,MAAI,OAAO,MAAM,aAAa,IAAI,EAAE,GAAG;AACrC,eAAO,eAAE,aAAa;AAAA,EACxB;AACA,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,QAAI,MAAM,aAAa;AACrB;AAAA,IACF;AACA,QAAI,KAAK,CAAC,EAAE,OAAO,UAAU,MAAM,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI;AACxD,iBAAO,eAAE,iBAAiB;AAAA,IAC5B;AAAA,EACF;AACA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -59,17 +59,15 @@ class YamahaDeviceManagement extends import_dm_utils.DeviceManagement {
|
|
|
59
59
|
await this.adapter.extendForeignObjectAsync(this.objId, { native: { devices: rows } });
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
62
|
+
* Whether one device's volume datapoints read 0–100 %, for the edit dialog to prefill.
|
|
63
63
|
*
|
|
64
64
|
* @param deviceId the id-safe device id
|
|
65
|
-
* @returns
|
|
65
|
+
* @returns true when this device is set to percent
|
|
66
66
|
*/
|
|
67
|
-
async
|
|
67
|
+
async volumeAsPercentOf(deviceId) {
|
|
68
68
|
var _a;
|
|
69
69
|
const node = await this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${deviceId}`);
|
|
70
|
-
|
|
71
|
-
const now = Date.now();
|
|
72
|
-
return { val: on, ack: true, ts: now, lc: now, from: `system.adapter.${this.adapter.namespace}` };
|
|
70
|
+
return ((_a = node == null ? void 0 : node.native) == null ? void 0 : _a.volumeAsPercent) === true;
|
|
73
71
|
}
|
|
74
72
|
/**
|
|
75
73
|
* Set one device's percent switch. The value lives at the DEVICE object — one place the card,
|
|
@@ -127,17 +125,19 @@ class YamahaDeviceManagement extends import_dm_utils.DeviceManagement {
|
|
|
127
125
|
* @param context the load context
|
|
128
126
|
*/
|
|
129
127
|
async loadDevices(context) {
|
|
130
|
-
var _a;
|
|
128
|
+
var _a, _b;
|
|
131
129
|
for (const card of await this.cards()) {
|
|
132
130
|
const [model, node] = await Promise.all([
|
|
133
131
|
this.adapter.getForeignStateAsync(`${this.adapter.namespace}.${card.id}.info.model`),
|
|
134
132
|
this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${card.id}`)
|
|
135
133
|
]);
|
|
136
134
|
const label = typeof ((_a = node == null ? void 0 : node.common) == null ? void 0 : _a.name) === "string" ? node.common.name : void 0;
|
|
135
|
+
const percent = ((_b = node == null ? void 0 : node.native) == null ? void 0 : _b.volumeAsPercent) === true;
|
|
137
136
|
context.addDevice(
|
|
138
137
|
this.toDeviceInfo(
|
|
139
138
|
label && label !== card.id ? { ...card, name: label } : card,
|
|
140
|
-
typeof (model == null ? void 0 : model.val) === "string" ? model.val : void 0
|
|
139
|
+
typeof (model == null ? void 0 : model.val) === "string" ? model.val : void 0,
|
|
140
|
+
percent
|
|
141
141
|
)
|
|
142
142
|
);
|
|
143
143
|
}
|
|
@@ -145,14 +145,16 @@ class YamahaDeviceManagement extends import_dm_utils.DeviceManagement {
|
|
|
145
145
|
/**
|
|
146
146
|
* Build one device card: the live model, the IP as the identifier line, a connection
|
|
147
147
|
* status, and one indicator per connected protocol (hidden while that protocol is not
|
|
148
|
-
* connected).
|
|
149
|
-
*
|
|
148
|
+
* connected). The live values read from the device's own `info.*` states. Every card can be
|
|
149
|
+
* edited, a discovered one included: that is how a found receiver is given the fixed address
|
|
150
|
+
* the user just assigned it (see {@link editDevice}).
|
|
150
151
|
*
|
|
151
152
|
* @param card the running device
|
|
152
153
|
* @param model the device's reported model name, for the device-class icon
|
|
154
|
+
* @param percent whether this device's volume datapoints read 0–100 %, for the badge
|
|
153
155
|
* @returns the card descriptor
|
|
154
156
|
*/
|
|
155
|
-
toDeviceInfo(card, model) {
|
|
157
|
+
toDeviceInfo(card, model, percent = false) {
|
|
156
158
|
const base = `${this.adapter.namespace}.${card.id}`;
|
|
157
159
|
const del = {
|
|
158
160
|
id: "delete",
|
|
@@ -195,27 +197,31 @@ class YamahaDeviceManagement extends import_dm_utils.DeviceManagement {
|
|
|
195
197
|
text: tr.label,
|
|
196
198
|
colorOn: "ok",
|
|
197
199
|
hideIfEmpty: true
|
|
198
|
-
}))
|
|
200
|
+
})),
|
|
201
|
+
// The percent setting is SET in the edit dialog, but it has to be READABLE at a glance —
|
|
202
|
+
// otherwise the only way to find out what a receiver's volume datapoints carry is to open
|
|
203
|
+
// a dialog. Shown only while it is on (`hideIfEmpty`), so a card in the default state
|
|
204
|
+
// stays as quiet as before; deliberately not clickable, the dialog stays the one place
|
|
205
|
+
// that sets it.
|
|
206
|
+
{
|
|
207
|
+
id: "volume-percent",
|
|
208
|
+
value: percent,
|
|
209
|
+
icon: "fa-percent",
|
|
210
|
+
text: "0\u2013100 %",
|
|
211
|
+
colorOn: "primary",
|
|
212
|
+
tooltip: (0, import_i18n.t)("volumeAsPercent"),
|
|
213
|
+
hideIfEmpty: true,
|
|
214
|
+
order: 20
|
|
215
|
+
}
|
|
199
216
|
],
|
|
200
217
|
// Edit on every card: a device the search found can be given the fixed address the user
|
|
201
218
|
// just assigned it, which makes it a manual device (see editDevice).
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
id: "volumeAsPercent",
|
|
209
|
-
type: "switch",
|
|
210
|
-
label: (0, import_i18n.t)("volumeAsPercent"),
|
|
211
|
-
description: (0, import_i18n.t)("volumeAsPercent_help"),
|
|
212
|
-
getStateHandler: async (deviceId) => this.volumePercentState(deviceId),
|
|
213
|
-
handler: async (deviceId, _controlId, state) => {
|
|
214
|
-
await this.applyVolumePercent(deviceId, state === true);
|
|
215
|
-
return this.volumePercentState(deviceId);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
]
|
|
219
|
+
// The percent switch is NOT a second control on the card: it lives in the edit dialog,
|
|
220
|
+
// next to name and address, because it is a decision about the device and not something
|
|
221
|
+
// flipped in passing. 2.9.1 had it in both places — the card control showed the wrong
|
|
222
|
+
// position while the dialog showed the right one, and two ways to set one value is one
|
|
223
|
+
// too many (krobi 2026-09-12: "für was hast du den das doppelt gemoppelt?").
|
|
224
|
+
actions: [edit, del]
|
|
219
225
|
};
|
|
220
226
|
}
|
|
221
227
|
/**
|
|
@@ -290,7 +296,7 @@ class YamahaDeviceManagement extends import_dm_utils.DeviceManagement {
|
|
|
290
296
|
}
|
|
291
297
|
const node = await this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${cardId}`);
|
|
292
298
|
const shownName = typeof ((_a = node == null ? void 0 : node.common) == null ? void 0 : _a.name) === "string" && node.common.name !== cardId ? node.common.name : card.name;
|
|
293
|
-
const percent =
|
|
299
|
+
const percent = await this.volumeAsPercentOf(cardId);
|
|
294
300
|
const data = await context.showForm(
|
|
295
301
|
(0, import_device_management_helpers.buildDeviceForm)(cards.filter((entry) => entry.id !== cardId).map((entry) => entry.ip)),
|
|
296
302
|
{ title: (0, import_i18n.t)("dmEditTitle"), data: { name: shownName, ip: card.ip, volumeAsPercent: percent } }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/device-management.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n DeviceManagement,\n type ActionContext,\n type DeviceInfo,\n type ControlState,\n type DeviceLoadContext,\n type InstanceDetails,\n} from \"@iobroker/dm-utils\";\nimport { t } from \"./lib/i18n\";\nimport { iconForModel } from \"./lib/device-type\";\nimport { readDiscovered, readIgnored, writeDiscovered, writeIgnored } from \"./lib/discovered-store\";\nimport { discoveredStoreDeps, ignoredStoreDeps } from \"./lib/discovered-store-deps\";\nimport type { DeviceRecord } from \"./lib/types\";\nimport { unionDevices } from \"./lib/pure-helpers\";\nimport {\n TRANSPORTS,\n buildDeviceForm,\n findClash,\n rowId,\n type CardDevice,\n type ManualRow,\n} from \"./device-management-helpers\";\n\n/** The one adapter method this backend needs beyond the plain ioBroker surface. */\ninterface DeviceOwner {\n /** Stop supervising a device and delete its object tree. */\n removeDevice(deviceId: string): Promise<void>;\n /** Switch one device's volume datapoints to percent (or back) and rebuild them at once. */\n setVolumePercent(deviceId: string, on: boolean): Promise<void>;\n}\n\n/**\n * ioBroker device-manager backend: the Yamaha receivers as cards showing the live model,\n * the IP, and which protocols (YNCA/MusicCast/XML) are connected right now, with a manual\n * add-by-IP dialog. The card list follows the running set \u2014 the manual `native.devices`\n * table when it is filled, otherwise the auto-discovered devices \u2014 so it matches exactly\n * what the adapter runs. \"Yamaha\" is never a card line: it is the whole adapter.\n */\nexport class YamahaDeviceManagement extends DeviceManagement {\n /** The instance object id whose `native` holds the manual device table. */\n private get objId(): string {\n return `system.adapter.${this.adapter.namespace}`;\n }\n\n /** The running adapter, for the one action that has to reach into it (delete a device). */\n private get owner(): DeviceOwner | undefined {\n const candidate = this.adapter as unknown as Partial<DeviceOwner>;\n return typeof candidate.removeDevice === \"function\" && typeof candidate.setVolumePercent === \"function\"\n ? (candidate as DeviceOwner)\n : undefined;\n }\n\n /** Read the manual device table (`native.devices`) as raw rows, keeping the name. */\n private async readManual(): Promise<ManualRow[]> {\n const obj = await this.adapter.getForeignObjectAsync(this.objId);\n const devices = (obj?.native as { devices?: unknown } | undefined)?.devices;\n if (!Array.isArray(devices)) {\n return [];\n }\n return devices.filter(\n (d): d is ManualRow => !!d && typeof (d as ManualRow).ip === \"string\" && (d as ManualRow).ip.length > 0,\n );\n }\n\n /**\n * Persist the manual device table; writing `native.*` restarts the adapter with the new set.\n *\n * @param rows the manual rows to store\n */\n private async writeManual(rows: ManualRow[]): Promise<void> {\n await this.adapter.extendForeignObjectAsync(this.objId, { native: { devices: rows } });\n }\n\n /**\n * One device's percent switch as a state, for the card control to show.\n *\n * @param deviceId the id-safe device id\n * @returns the switch position as an ioBroker state\n */\n private async volumePercentState(deviceId: string): Promise<ioBroker.State> {\n const node = await this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${deviceId}`);\n const on = (node?.native as { volumeAsPercent?: unknown } | undefined)?.volumeAsPercent === true;\n const now = Date.now();\n return { val: on, ack: true, ts: now, lc: now, from: `system.adapter.${this.adapter.namespace}` };\n }\n\n /**\n * Set one device's percent switch. The value lives at the DEVICE object \u2014 one place the card,\n * the dialog and the running adapter all read, and writing it restarts nothing (an instance\n * object's native would).\n *\n * @param deviceId the id-safe device id\n * @param on whether its volume datapoints should read 0\u2013100 %\n */\n private async applyVolumePercent(deviceId: string, on: boolean): Promise<void> {\n const id = `${this.adapter.namespace}.${deviceId}`;\n const existing = await this.adapter.getForeignObjectAsync(id);\n const owner = this.owner;\n if (existing && owner) {\n // The adapter is running this device: it writes the value AND rebuilds the volume\n // datapoints on the spot, so the change is visible without a restart.\n await owner.setVolumePercent(deviceId, on);\n return;\n }\n // A device just added through the dialog has no object yet \u2014 seed the shape\n // `ensureDeviceHeader` completes on the next start, so the answer has somewhere to live.\n await this.adapter.extendForeignObjectAsync(id, {\n type: \"device\",\n common: { name: deviceId },\n native: { volumeAsPercent: on },\n });\n }\n\n /**\n * The running device set as cards: the manual table AND the discovery store, exactly the\n * union the adapter itself runs (`unionDevices`), so the list matches what is live.\n *\n * @returns the cards, each tagged with where its address came from\n */\n private async cards(): Promise<CardDevice[]> {\n const names = new Map<string, string>();\n const manualRecords: DeviceRecord[] = [];\n for (const row of await this.readManual()) {\n const id = rowId(row);\n // \"info\" is the adapter's own channel \u2014 a device may never claim it.\n if (id === \"info\" || names.has(id)) {\n continue;\n }\n names.set(id, row.name && row.name.length > 0 ? row.name : row.ip);\n manualRecords.push({ id, ip: row.ip });\n }\n const discovered = await readDiscovered(discoveredStoreDeps(this.adapter));\n return unionDevices(manualRecords, discovered).map(device => ({\n id: device.id,\n ip: device.ip,\n name: names.get(device.id) ?? device.id,\n source: device.source ?? \"discovered\",\n }));\n }\n\n /**\n * Populate the manager with one card per running device.\n *\n * @param context the load context\n */\n protected async loadDevices(context: DeviceLoadContext<string>): Promise<void> {\n for (const card of await this.cards()) {\n // Model and object are independent reads \u2014 fetch them together so a card with\n // several devices does not add up their round-trips.\n const [model, node] = await Promise.all([\n this.adapter.getForeignStateAsync(`${this.adapter.namespace}.${card.id}.info.model`),\n this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${card.id}`),\n ]);\n // The card title follows the device object's name, not the table entry. On an\n // instance upgraded from the previous adapter the table entry is the receiver's\n // ip \u2014 the object carries the readable name the adapter learned from the device.\n // The table entry itself must stay put: the object id is derived from it, and\n // changing that would move the whole tree.\n const label = typeof node?.common?.name === \"string\" ? node.common.name : undefined;\n context.addDevice(\n this.toDeviceInfo(\n label && label !== card.id ? { ...card, name: label } : card,\n typeof model?.val === \"string\" ? model.val : undefined,\n ),\n );\n }\n }\n\n /**\n * Build one device card: the live model, the IP as the identifier line, a connection\n * status, and one indicator per connected protocol (hidden while that protocol is not\n * connected). All live values read from the device's own `info.*` states. A discovered\n * card has no edit action \u2014 it is auto-found and re-found; it can only be removed.\n *\n * @param card the running device\n * @param model the device's reported model name, for the device-class icon\n * @returns the card descriptor\n */\n private toDeviceInfo(card: CardDevice, model?: string): DeviceInfo<string> {\n const base = `${this.adapter.namespace}.${card.id}`;\n const del = {\n id: \"delete\",\n icon: \"delete\",\n description: t(\"dmDelete\"),\n handler: async (id: string, ctx: ActionContext): Promise<{ refresh: \"devices\" }> => this.deleteDevice(id, ctx),\n };\n const edit = {\n id: \"edit\",\n icon: \"edit\",\n description: t(\"dmEdit\"),\n handler: async (id: string, ctx: ActionContext): Promise<{ refresh: \"devices\" }> => this.editDevice(id, ctx),\n };\n return {\n id: card.id,\n name: card.name,\n icon: iconForModel(model),\n model: { stateId: `${base}.info.model` },\n identifier: card.ip,\n status: {\n connection: { stateId: `${base}.info.connection`, mapping: { true: \"connected\", false: \"disconnected\" } },\n },\n // No icon on the transports: the indicator icon accepts only a reserved/`fa-*`/`data:`/URL\n // name, so a plain \"wifi\" rendered as a \"?\". The transport label as text plus a green \"on\"\n // colour carries it; `hideIfEmpty` shows only the protocols this device is connected over.\n indicators: [\n // Where the address came from. The adapter treats the two differently \u2014 only a device\n // the search found is looked for again when it drops off \u2014 so the card says which it is.\n {\n id: \"device-source\",\n value: true,\n icon: card.source === \"manual\" ? \"fa-pencil\" : \"fa-search\",\n tooltip: t(card.source === \"manual\" ? \"sourceManual\" : \"sourceDiscovered\"),\n color: \"primary\",\n order: 10,\n },\n ...TRANSPORTS.map(tr => ({\n id: `transport-${tr.id}`,\n value: { stateId: `${base}.info.transports.${tr.id}` },\n text: tr.label,\n colorOn: \"ok\" as const,\n hideIfEmpty: true,\n })),\n ],\n // Edit on every card: a device the search found can be given the fixed address the user\n // just assigned it, which makes it a manual device (see editDevice).\n actions: [edit, del],\n // The percent switch, per device. It is a decision, not something switched around, so it\n // sits in the admin next to the device rather than in the object tree \u2014 but it belongs to\n // THIS device, which is what 2.8.0's single instance checkbox could not express.\n controls: [\n {\n id: \"volumeAsPercent\",\n type: \"switch\",\n label: t(\"volumeAsPercent\"),\n description: t(\"volumeAsPercent_help\"),\n getStateHandler: async (deviceId: string): Promise<ioBroker.State> => this.volumePercentState(deviceId),\n handler: async (deviceId: string, _controlId: string, state: ControlState): Promise<ioBroker.State> => {\n await this.applyVolumePercent(deviceId, state === true);\n return this.volumePercentState(deviceId);\n },\n },\n ],\n };\n }\n\n /**\n * The \"+ add\" action above the list and the label of the identifier line (the IP).\n *\n * @returns the instance action descriptor\n */\n protected getInstanceInfo(): InstanceDetails {\n return {\n apiVersion: \"v3\",\n identifierLabel: t(\"ipLabel\"),\n actions: [{ id: \"add\", icon: \"add\", description: t(\"dmAdd\"), handler: async ctx => this.addDevice(ctx) }],\n };\n }\n\n /**\n * Manual add: show the name+IP form, then append the device to `native.devices` (which\n * restarts the adapter and switches it to manual mode).\n *\n * @param context the action context\n * @returns a directive to reload the manager\n */\n private async addDevice(context: ActionContext): Promise<{ refresh: boolean }> {\n const manual = await this.readManual();\n const data = await context.showForm(buildDeviceForm(manual.map(r => r.ip)), { title: t(\"dmAdd\") });\n if (data && typeof data.ip === \"string\" && data.ip.trim()) {\n const row: ManualRow = { name: typeof data.name === \"string\" ? data.name.trim() : \"\", ip: data.ip.trim() };\n const clash = findClash(manual, row, -1);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: true };\n }\n manual.push(row);\n // Adding a device by hand undoes an earlier delete of the same id \u2014 otherwise the\n // exclusion would silently outlive the decision that created it.\n const ignoredDeps = ignoredStoreDeps(this.adapter);\n const ignored = await readIgnored(ignoredDeps);\n const id = rowId(row);\n if (ignored.includes(id)) {\n await writeIgnored(\n ignoredDeps,\n ignored.filter(entry => entry !== id),\n );\n }\n await this.writeManual(manual);\n // Written down right away, so the device starts with the answer the user gave instead of\n // inheriting whatever the instance-wide switch of 2.8.0 was left on.\n await this.applyVolumePercent(id, data.volumeAsPercent === true);\n }\n return { refresh: true };\n }\n\n /**\n * Edit one device: its display name and its address. Offered on every card.\n *\n * Two rules make this safe, and both were learned from what the adapter does elsewhere:\n *\n * 1. **The object id never changes.** It is derived from the table row's name, and a changed\n * id would leave the whole object tree behind for `cleanupStaleObjects` to delete \u2014\n * history and VIS bindings with it. The row therefore carries the id as its name, and what\n * the user typed becomes the DISPLAY name at the device object, where `nextDeviceLabel`\n * already defends a user's own name against every name the device reports.\n * 2. **A discovered device whose address changes MOVES into the table.** The user gave the\n * receiver a fixed address and entered it; that is what a manual device is. Copying it\n * instead would lose the edit at the next search \u2014 `mergeDiscovered` carries the found\n * address onto a known id.\n *\n * @param cardId the card id (= the object-tree device id)\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async editDevice(cardId: string, context: ActionContext): Promise<{ refresh: \"devices\" }> {\n const cards = await this.cards();\n const card = cards.find(entry => entry.id === cardId);\n if (!card) {\n return { refresh: \"devices\" };\n }\n // Prefill what the CARD shows, by the same rule loadDevices titles it: the device object's\n // name when it carries one, otherwise the table entry.\n const node = await this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${cardId}`);\n const shownName =\n typeof node?.common?.name === \"string\" && node.common.name !== cardId ? node.common.name : card.name;\n const percent = (await this.volumePercentState(cardId)).val === true;\n const data = await context.showForm(\n buildDeviceForm(cards.filter(entry => entry.id !== cardId).map(entry => entry.ip)),\n { title: t(\"dmEditTitle\"), data: { name: shownName, ip: card.ip, volumeAsPercent: percent } },\n );\n if (!data || typeof data.ip !== \"string\" || !data.ip.trim()) {\n return { refresh: \"devices\" };\n }\n const ip = data.ip.trim();\n const name = typeof data.name === \"string\" ? data.name.trim() : \"\";\n const manual = await this.readManual();\n const index = manual.findIndex(entry => rowId(entry) === cardId);\n const row: ManualRow = { name: cardId, ip };\n const clash = findClash(manual, row, index);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: \"devices\" };\n }\n if (index >= 0) {\n manual[index] = row;\n await this.writeManual(manual);\n } else if (ip !== card.ip) {\n const store = discoveredStoreDeps(this.adapter);\n const discovered = await readDiscovered(store);\n await writeDiscovered(\n store,\n discovered.filter((entry: DeviceRecord) => entry.id !== cardId),\n );\n manual.push(row);\n await this.writeManual(manual);\n }\n if (name !== shownName) {\n await this.adapter.extendForeignObjectAsync(`${this.adapter.namespace}.${cardId}`, {\n common: { name: name || cardId },\n });\n }\n if ((data.volumeAsPercent === true) !== percent) {\n await this.applyVolumePercent(cardId, data.volumeAsPercent === true);\n }\n return { refresh: \"devices\" };\n }\n\n /**\n * Delete a device after confirmation, from whichever source it came from: a manual card\n * from `native.devices`, a discovered card from the discovery store \u2014 the latter so the\n * standby-protection merge does not resurrect it on the next start.\n *\n * @param cardId the card id (= the object-tree device id)\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async deleteDevice(cardId: string, context: ActionContext): Promise<{ refresh: \"devices\" }> {\n const manual = await this.readManual();\n // Which store this card lives in, not which store is non-empty: with a mixed set a filled\n // table no longer means every card is manual, and the old shape silently did nothing when a\n // discovered card was deleted next to a typed one.\n const index = manual.findIndex(r => rowId(r) === cardId);\n if (index >= 0) {\n const confirmed = await context.showConfirmation(t(\"dmDeleteConfirm\", manual[index].name || manual[index].ip));\n if (confirmed) {\n manual.splice(index, 1);\n await this.writeManual(manual);\n }\n return { refresh: \"devices\" };\n }\n const store = discoveredStoreDeps(this.adapter);\n const discovered = await readDiscovered(store);\n const remaining = discovered.filter((d: DeviceRecord) => d.id !== cardId);\n if (remaining.length !== discovered.length) {\n const confirmed = await context.showConfirmation(t(\"dmDeleteConfirm\", cardId));\n if (confirmed) {\n await writeDiscovered(store, remaining);\n // Writing this file restarts nothing, so the delete has to do the two things a restart\n // would otherwise do much later: stop talking to the device and take its tree away.\n await this.owner?.removeDevice(cardId);\n // And it has to STAY deleted \u2014 without this the next network search finds the receiver\n // and puts the card straight back.\n const ignoredDeps = ignoredStoreDeps(this.adapter);\n await writeIgnored(ignoredDeps, [...(await readIgnored(ignoredDeps)), cardId]);\n }\n }\n return { refresh: \"devices\" };\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,
|
|
4
|
+
"sourcesContent": ["import {\n DeviceManagement,\n type ActionContext,\n type DeviceInfo,\n type DeviceLoadContext,\n type InstanceDetails,\n} from \"@iobroker/dm-utils\";\nimport { t } from \"./lib/i18n\";\nimport { iconForModel } from \"./lib/device-type\";\nimport { readDiscovered, readIgnored, writeDiscovered, writeIgnored } from \"./lib/discovered-store\";\nimport { discoveredStoreDeps, ignoredStoreDeps } from \"./lib/discovered-store-deps\";\nimport type { DeviceRecord } from \"./lib/types\";\nimport { unionDevices } from \"./lib/pure-helpers\";\nimport {\n TRANSPORTS,\n buildDeviceForm,\n findClash,\n rowId,\n type CardDevice,\n type ManualRow,\n} from \"./device-management-helpers\";\n\n/** The one adapter method this backend needs beyond the plain ioBroker surface. */\ninterface DeviceOwner {\n /** Stop supervising a device and delete its object tree. */\n removeDevice(deviceId: string): Promise<void>;\n /** Switch one device's volume datapoints to percent (or back) and rebuild them at once. */\n setVolumePercent(deviceId: string, on: boolean): Promise<void>;\n}\n\n/**\n * ioBroker device-manager backend: the Yamaha receivers as cards showing the live model,\n * the IP, and which protocols (YNCA/MusicCast/XML) are connected right now, with a manual\n * add-by-IP dialog. The card list follows the running set \u2014 the manual `native.devices`\n * table when it is filled, otherwise the auto-discovered devices \u2014 so it matches exactly\n * what the adapter runs. \"Yamaha\" is never a card line: it is the whole adapter.\n */\nexport class YamahaDeviceManagement extends DeviceManagement {\n /** The instance object id whose `native` holds the manual device table. */\n private get objId(): string {\n return `system.adapter.${this.adapter.namespace}`;\n }\n\n /** The running adapter, for the one action that has to reach into it (delete a device). */\n private get owner(): DeviceOwner | undefined {\n const candidate = this.adapter as unknown as Partial<DeviceOwner>;\n return typeof candidate.removeDevice === \"function\" && typeof candidate.setVolumePercent === \"function\"\n ? (candidate as DeviceOwner)\n : undefined;\n }\n\n /** Read the manual device table (`native.devices`) as raw rows, keeping the name. */\n private async readManual(): Promise<ManualRow[]> {\n const obj = await this.adapter.getForeignObjectAsync(this.objId);\n const devices = (obj?.native as { devices?: unknown } | undefined)?.devices;\n if (!Array.isArray(devices)) {\n return [];\n }\n return devices.filter(\n (d): d is ManualRow => !!d && typeof (d as ManualRow).ip === \"string\" && (d as ManualRow).ip.length > 0,\n );\n }\n\n /**\n * Persist the manual device table; writing `native.*` restarts the adapter with the new set.\n *\n * @param rows the manual rows to store\n */\n private async writeManual(rows: ManualRow[]): Promise<void> {\n await this.adapter.extendForeignObjectAsync(this.objId, { native: { devices: rows } });\n }\n\n /**\n * Whether one device's volume datapoints read 0\u2013100 %, for the edit dialog to prefill.\n *\n * @param deviceId the id-safe device id\n * @returns true when this device is set to percent\n */\n private async volumeAsPercentOf(deviceId: string): Promise<boolean> {\n const node = await this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${deviceId}`);\n return (node?.native as { volumeAsPercent?: unknown } | undefined)?.volumeAsPercent === true;\n }\n\n /**\n * Set one device's percent switch. The value lives at the DEVICE object \u2014 one place the card,\n * the dialog and the running adapter all read, and writing it restarts nothing (an instance\n * object's native would).\n *\n * @param deviceId the id-safe device id\n * @param on whether its volume datapoints should read 0\u2013100 %\n */\n private async applyVolumePercent(deviceId: string, on: boolean): Promise<void> {\n const id = `${this.adapter.namespace}.${deviceId}`;\n const existing = await this.adapter.getForeignObjectAsync(id);\n const owner = this.owner;\n if (existing && owner) {\n // The adapter is running this device: it writes the value AND rebuilds the volume\n // datapoints on the spot, so the change is visible without a restart.\n await owner.setVolumePercent(deviceId, on);\n return;\n }\n // A device just added through the dialog has no object yet \u2014 seed the shape\n // `ensureDeviceHeader` completes on the next start, so the answer has somewhere to live.\n await this.adapter.extendForeignObjectAsync(id, {\n type: \"device\",\n common: { name: deviceId },\n native: { volumeAsPercent: on },\n });\n }\n\n /**\n * The running device set as cards: the manual table AND the discovery store, exactly the\n * union the adapter itself runs (`unionDevices`), so the list matches what is live.\n *\n * @returns the cards, each tagged with where its address came from\n */\n private async cards(): Promise<CardDevice[]> {\n const names = new Map<string, string>();\n const manualRecords: DeviceRecord[] = [];\n for (const row of await this.readManual()) {\n const id = rowId(row);\n // \"info\" is the adapter's own channel \u2014 a device may never claim it.\n if (id === \"info\" || names.has(id)) {\n continue;\n }\n names.set(id, row.name && row.name.length > 0 ? row.name : row.ip);\n manualRecords.push({ id, ip: row.ip });\n }\n const discovered = await readDiscovered(discoveredStoreDeps(this.adapter));\n return unionDevices(manualRecords, discovered).map(device => ({\n id: device.id,\n ip: device.ip,\n name: names.get(device.id) ?? device.id,\n source: device.source ?? \"discovered\",\n }));\n }\n\n /**\n * Populate the manager with one card per running device.\n *\n * @param context the load context\n */\n protected async loadDevices(context: DeviceLoadContext<string>): Promise<void> {\n for (const card of await this.cards()) {\n // Model and object are independent reads \u2014 fetch them together so a card with\n // several devices does not add up their round-trips.\n const [model, node] = await Promise.all([\n this.adapter.getForeignStateAsync(`${this.adapter.namespace}.${card.id}.info.model`),\n this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${card.id}`),\n ]);\n // The card title follows the device object's name, not the table entry. On an\n // instance upgraded from the previous adapter the table entry is the receiver's\n // ip \u2014 the object carries the readable name the adapter learned from the device.\n // The table entry itself must stay put: the object id is derived from it, and\n // changing that would move the whole tree.\n const label = typeof node?.common?.name === \"string\" ? node.common.name : undefined;\n // The percent answer comes from the object read above \u2014 no extra round-trip for the badge.\n const percent = (node?.native as { volumeAsPercent?: unknown } | undefined)?.volumeAsPercent === true;\n context.addDevice(\n this.toDeviceInfo(\n label && label !== card.id ? { ...card, name: label } : card,\n typeof model?.val === \"string\" ? model.val : undefined,\n percent,\n ),\n );\n }\n }\n\n /**\n * Build one device card: the live model, the IP as the identifier line, a connection\n * status, and one indicator per connected protocol (hidden while that protocol is not\n * connected). The live values read from the device's own `info.*` states. Every card can be\n * edited, a discovered one included: that is how a found receiver is given the fixed address\n * the user just assigned it (see {@link editDevice}).\n *\n * @param card the running device\n * @param model the device's reported model name, for the device-class icon\n * @param percent whether this device's volume datapoints read 0\u2013100 %, for the badge\n * @returns the card descriptor\n */\n private toDeviceInfo(card: CardDevice, model?: string, percent = false): DeviceInfo<string> {\n const base = `${this.adapter.namespace}.${card.id}`;\n const del = {\n id: \"delete\",\n icon: \"delete\",\n description: t(\"dmDelete\"),\n handler: async (id: string, ctx: ActionContext): Promise<{ refresh: \"devices\" }> => this.deleteDevice(id, ctx),\n };\n const edit = {\n id: \"edit\",\n icon: \"edit\",\n description: t(\"dmEdit\"),\n handler: async (id: string, ctx: ActionContext): Promise<{ refresh: \"devices\" }> => this.editDevice(id, ctx),\n };\n return {\n id: card.id,\n name: card.name,\n icon: iconForModel(model),\n model: { stateId: `${base}.info.model` },\n identifier: card.ip,\n status: {\n connection: { stateId: `${base}.info.connection`, mapping: { true: \"connected\", false: \"disconnected\" } },\n },\n // No icon on the transports: the indicator icon accepts only a reserved/`fa-*`/`data:`/URL\n // name, so a plain \"wifi\" rendered as a \"?\". The transport label as text plus a green \"on\"\n // colour carries it; `hideIfEmpty` shows only the protocols this device is connected over.\n indicators: [\n // Where the address came from. The adapter treats the two differently \u2014 only a device\n // the search found is looked for again when it drops off \u2014 so the card says which it is.\n {\n id: \"device-source\",\n value: true,\n icon: card.source === \"manual\" ? \"fa-pencil\" : \"fa-search\",\n tooltip: t(card.source === \"manual\" ? \"sourceManual\" : \"sourceDiscovered\"),\n color: \"primary\",\n order: 10,\n },\n ...TRANSPORTS.map(tr => ({\n id: `transport-${tr.id}`,\n value: { stateId: `${base}.info.transports.${tr.id}` },\n text: tr.label,\n colorOn: \"ok\" as const,\n hideIfEmpty: true,\n })),\n // The percent setting is SET in the edit dialog, but it has to be READABLE at a glance \u2014\n // otherwise the only way to find out what a receiver's volume datapoints carry is to open\n // a dialog. Shown only while it is on (`hideIfEmpty`), so a card in the default state\n // stays as quiet as before; deliberately not clickable, the dialog stays the one place\n // that sets it.\n {\n id: \"volume-percent\",\n value: percent,\n icon: \"fa-percent\",\n text: \"0\u2013100 %\",\n colorOn: \"primary\" as const,\n tooltip: t(\"volumeAsPercent\"),\n hideIfEmpty: true,\n order: 20,\n },\n ],\n // Edit on every card: a device the search found can be given the fixed address the user\n // just assigned it, which makes it a manual device (see editDevice).\n // The percent switch is NOT a second control on the card: it lives in the edit dialog,\n // next to name and address, because it is a decision about the device and not something\n // flipped in passing. 2.9.1 had it in both places \u2014 the card control showed the wrong\n // position while the dialog showed the right one, and two ways to set one value is one\n // too many (krobi 2026-09-12: \"f\u00FCr was hast du den das doppelt gemoppelt?\").\n actions: [edit, del],\n };\n }\n\n /**\n * The \"+ add\" action above the list and the label of the identifier line (the IP).\n *\n * @returns the instance action descriptor\n */\n protected getInstanceInfo(): InstanceDetails {\n return {\n apiVersion: \"v3\",\n identifierLabel: t(\"ipLabel\"),\n actions: [{ id: \"add\", icon: \"add\", description: t(\"dmAdd\"), handler: async ctx => this.addDevice(ctx) }],\n };\n }\n\n /**\n * Manual add: show the name+IP form, then append the device to `native.devices` (which\n * restarts the adapter and switches it to manual mode).\n *\n * @param context the action context\n * @returns a directive to reload the manager\n */\n private async addDevice(context: ActionContext): Promise<{ refresh: boolean }> {\n const manual = await this.readManual();\n const data = await context.showForm(buildDeviceForm(manual.map(r => r.ip)), { title: t(\"dmAdd\") });\n if (data && typeof data.ip === \"string\" && data.ip.trim()) {\n const row: ManualRow = { name: typeof data.name === \"string\" ? data.name.trim() : \"\", ip: data.ip.trim() };\n const clash = findClash(manual, row, -1);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: true };\n }\n manual.push(row);\n // Adding a device by hand undoes an earlier delete of the same id \u2014 otherwise the\n // exclusion would silently outlive the decision that created it.\n const ignoredDeps = ignoredStoreDeps(this.adapter);\n const ignored = await readIgnored(ignoredDeps);\n const id = rowId(row);\n if (ignored.includes(id)) {\n await writeIgnored(\n ignoredDeps,\n ignored.filter(entry => entry !== id),\n );\n }\n await this.writeManual(manual);\n // Written down right away, so the device starts with the answer the user gave instead of\n // inheriting whatever the instance-wide switch of 2.8.0 was left on.\n await this.applyVolumePercent(id, data.volumeAsPercent === true);\n }\n return { refresh: true };\n }\n\n /**\n * Edit one device: its display name and its address. Offered on every card.\n *\n * Two rules make this safe, and both were learned from what the adapter does elsewhere:\n *\n * 1. **The object id never changes.** It is derived from the table row's name, and a changed\n * id would leave the whole object tree behind for `cleanupStaleObjects` to delete \u2014\n * history and VIS bindings with it. The row therefore carries the id as its name, and what\n * the user typed becomes the DISPLAY name at the device object, where `nextDeviceLabel`\n * already defends a user's own name against every name the device reports.\n * 2. **A discovered device whose address changes MOVES into the table.** The user gave the\n * receiver a fixed address and entered it; that is what a manual device is. Copying it\n * instead would lose the edit at the next search \u2014 `mergeDiscovered` carries the found\n * address onto a known id.\n *\n * @param cardId the card id (= the object-tree device id)\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async editDevice(cardId: string, context: ActionContext): Promise<{ refresh: \"devices\" }> {\n const cards = await this.cards();\n const card = cards.find(entry => entry.id === cardId);\n if (!card) {\n return { refresh: \"devices\" };\n }\n // Prefill what the CARD shows, by the same rule loadDevices titles it: the device object's\n // name when it carries one, otherwise the table entry.\n const node = await this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${cardId}`);\n const shownName =\n typeof node?.common?.name === \"string\" && node.common.name !== cardId ? node.common.name : card.name;\n const percent = await this.volumeAsPercentOf(cardId);\n const data = await context.showForm(\n buildDeviceForm(cards.filter(entry => entry.id !== cardId).map(entry => entry.ip)),\n { title: t(\"dmEditTitle\"), data: { name: shownName, ip: card.ip, volumeAsPercent: percent } },\n );\n if (!data || typeof data.ip !== \"string\" || !data.ip.trim()) {\n return { refresh: \"devices\" };\n }\n const ip = data.ip.trim();\n const name = typeof data.name === \"string\" ? data.name.trim() : \"\";\n const manual = await this.readManual();\n const index = manual.findIndex(entry => rowId(entry) === cardId);\n const row: ManualRow = { name: cardId, ip };\n const clash = findClash(manual, row, index);\n if (clash) {\n await context.showMessage(clash);\n return { refresh: \"devices\" };\n }\n if (index >= 0) {\n manual[index] = row;\n await this.writeManual(manual);\n } else if (ip !== card.ip) {\n const store = discoveredStoreDeps(this.adapter);\n const discovered = await readDiscovered(store);\n await writeDiscovered(\n store,\n discovered.filter((entry: DeviceRecord) => entry.id !== cardId),\n );\n manual.push(row);\n await this.writeManual(manual);\n }\n if (name !== shownName) {\n await this.adapter.extendForeignObjectAsync(`${this.adapter.namespace}.${cardId}`, {\n common: { name: name || cardId },\n });\n }\n if ((data.volumeAsPercent === true) !== percent) {\n await this.applyVolumePercent(cardId, data.volumeAsPercent === true);\n }\n return { refresh: \"devices\" };\n }\n\n /**\n * Delete a device after confirmation, from whichever source it came from: a manual card\n * from `native.devices`, a discovered card from the discovery store \u2014 the latter so the\n * standby-protection merge does not resurrect it on the next start.\n *\n * @param cardId the card id (= the object-tree device id)\n * @param context the action context\n * @returns a directive to reload the list\n */\n private async deleteDevice(cardId: string, context: ActionContext): Promise<{ refresh: \"devices\" }> {\n const manual = await this.readManual();\n // Which store this card lives in, not which store is non-empty: with a mixed set a filled\n // table no longer means every card is manual, and the old shape silently did nothing when a\n // discovered card was deleted next to a typed one.\n const index = manual.findIndex(r => rowId(r) === cardId);\n if (index >= 0) {\n const confirmed = await context.showConfirmation(t(\"dmDeleteConfirm\", manual[index].name || manual[index].ip));\n if (confirmed) {\n manual.splice(index, 1);\n await this.writeManual(manual);\n }\n return { refresh: \"devices\" };\n }\n const store = discoveredStoreDeps(this.adapter);\n const discovered = await readDiscovered(store);\n const remaining = discovered.filter((d: DeviceRecord) => d.id !== cardId);\n if (remaining.length !== discovered.length) {\n const confirmed = await context.showConfirmation(t(\"dmDeleteConfirm\", cardId));\n if (confirmed) {\n await writeDiscovered(store, remaining);\n // Writing this file restarts nothing, so the delete has to do the two things a restart\n // would otherwise do much later: stop talking to the device and take its tree away.\n await this.owner?.removeDevice(cardId);\n // And it has to STAY deleted \u2014 without this the next network search finds the receiver\n // and puts the card straight back.\n const ignoredDeps = ignoredStoreDeps(this.adapter);\n await writeIgnored(ignoredDeps, [...(await readIgnored(ignoredDeps)), cardId]);\n }\n }\n return { refresh: \"devices\" };\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -112,13 +112,13 @@ function mergeDiscovered(known, found, onCollision) {
|
|
|
112
112
|
}
|
|
113
113
|
return [...byId.values()];
|
|
114
114
|
}
|
|
115
|
-
function staleObjects(existing, deviceIds, namespace) {
|
|
115
|
+
function staleObjects(existing, deviceIds, namespace, remembered = /* @__PURE__ */ new Set()) {
|
|
116
116
|
if (deviceIds.size === 0) {
|
|
117
117
|
return [];
|
|
118
118
|
}
|
|
119
119
|
const isKept = (fullId) => {
|
|
120
120
|
const top = stripNamespace(fullId, namespace).split(".")[0];
|
|
121
|
-
return top === "info" || deviceIds.has(top);
|
|
121
|
+
return top === "info" || deviceIds.has(top) || remembered.has(top);
|
|
122
122
|
};
|
|
123
123
|
return existing.filter((id) => !isKept(id)).sort((a, b) => b.length - a.length);
|
|
124
124
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/pure-helpers.ts"],
|
|
4
|
-
"sourcesContent": ["import type { DeviceRecord } from \"./types\";\nimport type { DiscoveredDevice } from \"./discovery\";\n\ninterface ConfiguredDevice {\n name?: string;\n ip: string;\n}\n\n/**\n * True when a raw config row carries a non-empty ip (the name is optional \u2014 a row\n * with an ip but no name is valid and falls back to the ip as its id, so a device\n * is never silently dropped just because its name was left blank).\n *\n * @param entry a raw config row from the admin device table\n * @returns whether the row is a valid configured device\n */\nfunction isConfiguredDevice(entry: unknown): entry is ConfiguredDevice {\n if (typeof entry !== \"object\" || entry === null) {\n return false;\n }\n const candidate = entry as { name?: unknown; ip?: unknown };\n return (\n typeof candidate.ip === \"string\" &&\n candidate.ip.length > 0 &&\n (candidate.name === undefined || typeof candidate.name === \"string\")\n );\n}\n\n/**\n * Make a string safe for use as an ioBroker object id segment.\n *\n * @param raw the raw string (e.g. a device name)\n * @returns the string with id-unsafe characters replaced by underscores\n */\nexport function sanitizeId(raw: string): string {\n return raw.replace(/[^A-Za-z0-9\\-_]/g, \"_\");\n}\n\n/**\n * Strip the adapter namespace (e.g. `yamaha.0`) from a full state id, leaving the\n * device-relative path (e.g. `living.power`).\n *\n * @param fullId the full state id\n * @param namespace the adapter namespace\n * @returns the id relative to the adapter instance\n */\nexport function stripNamespace(fullId: string, namespace: string): string {\n return fullId.slice(namespace.length + 1);\n}\n\n/**\n * Turn the admin device table (untrusted native config) into device records.\n * Invalid rows are dropped, as are rows whose id collides with the adapter's own\n * reserved `info` branch or with an id already taken \u2014 two names that sanitise to\n * the same id (e.g. \"Living Room\" and \"Living.Room\") would otherwise share one\n * object tree. A dropped collision is reported via `onCollision` so the device does\n * not just silently \"not exist\" for the user.\n *\n * @param raw the raw `native.devices` value\n * @param onCollision called with the dropped row's name/ip and the clashing id\n * @returns validated, de-duplicated device records\n */\nexport function parseDevices(raw: unknown, onCollision?: (dropped: string, takenId: string) => void): DeviceRecord[] {\n if (!Array.isArray(raw)) {\n return [];\n }\n const records: DeviceRecord[] = [];\n const taken = new Set<string>([\"info\"]); // reserved: the adapter's own info channel\n for (const entry of raw) {\n if (!isConfiguredDevice(entry)) {\n continue;\n }\n // Fall back to the ip as the id when the name is blank, so the device still appears\n // instead of vanishing silently.\n const id = sanitizeId(entry.name && entry.name.length > 0 ? entry.name : entry.ip);\n if (taken.has(id)) {\n onCollision?.(entry.name || entry.ip, id);\n continue;\n }\n taken.add(id);\n records.push({ id, ip: entry.ip, source: \"manual\" });\n }\n return records;\n}\n\n/**\n * The set of devices the adapter runs: the manual table plus the discovered store, keyed by id.\n *\n * A manual entry WINS over a discovered one of the same id \u2014 the user typed that address, and\n * the network search must not overwrite it. Before 2.9.0 the two were mutually exclusive (a\n * filled table turned the search off entirely), so making one discovered device manual dropped\n * every other one from the run and `cleanupStaleObjects` deleted their trees.\n *\n * @param manual the rows from the instance's device table\n * @param discovered the records remembered from the network search\n * @returns one record per id, manual first, each tagged with where it came from\n */\nexport function unionDevices(manual: readonly DeviceRecord[], discovered: readonly DeviceRecord[]): DeviceRecord[] {\n const byId = new Map<string, DeviceRecord>();\n for (const device of manual) {\n byId.set(device.id, { ...device, source: \"manual\" });\n }\n for (const device of discovered) {\n if (byId.has(device.id)) {\n continue;\n }\n // An address a manual device already occupies would mean two records talking to one\n // receiver \u2014 the manual one owns it.\n if ([...byId.values()].some(known => known.ip === device.ip)) {\n continue;\n }\n byId.set(device.id, { ...device, source: \"discovered\" });\n }\n return [...byId.values()];\n}\n\n/**\n * Merge freshly discovered devices into the set already known from earlier runs\n * (the auto-discovery standby protection). A known device is kept even when this\n * run's scan did not find it \u2014 a receiver in deep standby answers no SSDP, and its\n * object tree must survive.\n *\n * Identity is the device ID (derived from the friendly name it advertises, or from its\n * address when it advertises none) \u2014 NOT its address. Keyed by address, a receiver that\n * moved to a new address by DHCP was lost for good: the remembered record kept the old\n * address, the same receiver found at the new one produced the same id, and the id clash\n * dropped it. It stayed offline with no way back, because the id is what the whole object\n * tree hangs off. Now the same id simply carries the new address over.\n *\n * A genuine clash remains a clash: a DIFFERENT device sitting on an address another\n * record already claims is skipped and reported through `onCollision`, so a missing\n * device is explainable rather than silent.\n *\n * @param known the device records remembered from earlier runs\n * @param found the devices discovered this run\n * @param onCollision called with the dropped device's name/ip and the clashing id\n * @returns the merged records, one per device id\n */\nexport function mergeDiscovered(\n known: DeviceRecord[],\n found: DiscoveredDevice[],\n onCollision?: (dropped: string, takenId: string) => void,\n): DeviceRecord[] {\n const byId = new Map<string, DeviceRecord>();\n for (const device of known) {\n // \"info\" is the adapter's own channel \u2014 a device may never claim it.\n if (device.id === \"info\" || byId.has(device.id)) {\n continue;\n }\n byId.set(device.id, { ...device });\n }\n for (const device of found) {\n const label = device.name || device.ip;\n const id = sanitizeId(label);\n const remembered = byId.get(id);\n if (remembered) {\n // Same device, possibly at a new address \u2014 carry the address over, keep the id.\n remembered.ip = device.ip;\n continue;\n }\n const ipOwner = [...byId.values()].find(record => record.ip === device.ip);\n if (id === \"info\" || ipOwner) {\n onCollision?.(label, ipOwner?.id ?? id);\n continue;\n }\n byId.set(id, { id, ip: device.ip });\n }\n return [...byId.values()];\n}\n\n/**\n * From all object ids under the instance, pick the stale ones to delete on start:\n * everything that does not belong to a configured device and is outside the\n * adapter's own `info` branch. Removes the previous adapter's whole object tree\n * and any device dropped from the config. Keyed on the configured device ids\n * (not on what was created this run), so it works with the async connect where a\n * device's tree may only appear later \u2014 a configured device's subtree is kept\n * regardless of whether it has connected yet. Deepest first, so children go\n * before their parents.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the stale ids to delete, deepest first\n */\nexport function staleObjects(existing: string[], deviceIds: Set<string>, namespace: string): string[] {\n // No configured devices \u2192 never wipe the whole tree (a user who cleared the\n // device table by accident would otherwise lose every object in one pass).\n if (deviceIds.size === 0) {\n return [];\n }\n const isKept = (fullId: string): boolean => {\n const top = stripNamespace(fullId, namespace).split(\".\")[0];\n return top === \"info\" || deviceIds.has(top);\n };\n return existing.filter(id => !isKept(id)).sort((a, b) => b.length - a.length);\n}\n\n/**\n * Relative state ids an earlier version created under a different path and that this\n * version has renamed or moved. On start-up the old object is deleted so it does not\n * linger orphaned beside the new one \u2014 {@link staleObjects} only removes whole\n * non-configured device trees, not renamed states inside a device that is kept.\n */\n/**\n * The per-source playback-block states of the pre-2.0.0 tree \u2014 the same 19 states\n * used to sit under every player source folder, almost all permanently empty. In\n * 2.0.0 ONE flat block per zone replaces them; these copies are removed on the\n * first start (the sources that keep genuinely own states keep those untouched).\n */\nconst V2_PLAYER_BLOCK_STATES = [\n \"playback\",\n \"artist\",\n \"album\",\n \"track\",\n \"station\",\n \"channelName\",\n \"totalTime\",\n \"elapsedTime\",\n \"repeat\",\n \"shuffle\",\n \"albumArt\",\n \"source\",\n \"play\",\n \"pause\",\n \"stop\",\n \"next\",\n \"prev\",\n \"repeatToggle\",\n \"shuffleToggle\",\n];\n\n/** The source folders that stay in 2.0.0 (own states remain) but lose their block copy. */\nconst V2_SLIMMED_SOURCES = [\n \"netPlayer\",\n \"cd\",\n \"netRadio\",\n \"server\",\n \"usb\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"pc\",\n \"airplay\",\n \"bluetooth\",\n // Folders that came back with own content after the 2.0.0 cut (iPod/iPod USB: MODE, 2026-09-06;\n // Spotify: the RX-A850 presets, 2026-09-09) \u2014 their 1.x playback copies still have to go.\n \"ipod\",\n \"ipodUsb\",\n \"spotify\",\n];\n\nexport const RENAMED_STATE_IDS = [\n // v2.8.0: the volume datapoint now carries the scale the receiver itself displays, so the two\n // derived states are gone \u2014 `actualVolume` was an exact duplicate of `volume` on every device\n // declaring a single scale, and `actualVolumeMode` a dropdown with one entry there. `inputText`\n // repeated the label the `input` dropdown carries since 2.7.2, and only on MusicCast devices.\n \"actualVolume\",\n \"actualVolumeMode\",\n \"inputText\",\n \"hdmiOut\",\n \"directMode\",\n \"masterPower\",\n \"party\",\n \"partyMute\",\n \"distributionEnable\",\n \"partyEnable\",\n \"remoteCode\",\n // v1.0.0 multiroom regroup: the MusicCast-Link states moved into their own\n // multiroom.group folder, so the tree itself tells device-group from all-zones scope.\n \"multiroom.distributionEnable\",\n // Short-lived v1.0.0 pre-cut id \u2014 mislabeled \"active\", the field means \"enabled for use\".\n \"multiroom.group.streamingActive\",\n \"multiroom.role\",\n \"multiroom.groupId\",\n \"multiroom.groupName\",\n \"multiroom.serverZone\",\n \"multiroom.clientList\",\n \"multiroom.linkClient\",\n \"multiroom.leaveGroup\",\n // ---- v2.0.0 tree rework ------------------------------------------------------\n // Player unification: the per-source copies of the playback block are gone (the\n // slimmed folders keep only their own preset/pairing/drive states).\n ...V2_SLIMMED_SOURCES.flatMap(source => V2_PLAYER_BLOCK_STATES.map(state => `player.${source}.${state}`)),\n // Scenes: the twelve per-name datapoints became the recall dropdown + scene.list.\n ...Array.from({ length: 12 }, (_unused, i) => `scene.name${i + 1}`),\n // Tuner unification: ONE band/frequency/preset; the DAB subunit's FM half moved\n // onto the flat tuner ids, the two per-band frequencies became tuner.frequency.\n \"tuner.amFrequency\",\n \"tuner.fmFrequency\",\n \"tuner.dab.band\",\n \"tuner.dab.preset\",\n \"tuner.dab.fmPreset\",\n \"tuner.dab.fmFrequency\",\n \"tuner.dab.fmSearchMode\",\n \"tuner.dab.fmRdsService\",\n \"tuner.dab.fmRdsProgramType\",\n \"tuner.dab.fmRdsText\",\n \"tuner.dab.fmRdsClock\",\n \"tuner.dab.fmStereo\",\n \"tuner.dab.fmTuned\",\n \"tuner.dab.audioMode\",\n // Sound polish: equalizer and signal info each moved into their own subfolder.\n \"sound.equalizerMode\",\n \"sound.equalizerLow\",\n \"sound.equalizerMid\",\n \"sound.equalizerHigh\",\n \"sound.signalFormat\",\n \"sound.signalSampling\",\n \"sound.signalBits\",\n \"sound.signalBitrate\",\n // HDMI polish: the lip-sync offsets moved into the hdmi folder (the lipSync\n // channel itself is in RENAMED_CHANNELS); the A/B toggles joined the speakers.\n \"advanced.speakerA\",\n \"advanced.speakerB\",\n];\n\n/**\n * Old channel prefixes whose whole subtree this version moved out \u2014 the `system`\n * grab-bag is gone (model/firmware \u2192 info, HDMI outputs \u2192 hdmi, speaker patterns \u2192\n * speakers, input names \u2192 inputNames, master power \u2192 multiroom.masterPower). The channel and\n * every state under it are removed.\n */\nexport const RENAMED_CHANNELS = [\n // pre-0.11 system folder\n \"system\",\n // v0.18.1 multiroom regroup: zone2/3/4, zoneB, flat multiroom states moved under multiroom/.\n \"zone2\",\n \"zone3\",\n \"zone4\",\n \"zoneB\",\n // v1.0.0: stray per-zone copies of the device-global YXC states (the zone loop used to\n // prefix them too, yielding multiroom.zoneN.multiroom.* junk) are swept away.\n \"multiroom.zone2.multiroom\",\n \"multiroom.zone3.multiroom\",\n \"multiroom.zone4.multiroom\",\n // Regrouping: media sources moved under player/, DAB under tuner/, dist \u2192 multiroom. The old\n // flat channels (and their whole subtree) are deleted so the new grouped ones do not sit\n // beside orphaned copies on an upgraded instance.\n \"netRadio\",\n \"server\",\n \"usb\",\n \"spotify\",\n \"deezer\",\n \"tidal\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"airplay\",\n \"bluetooth\",\n \"pc\",\n \"musicCastLink\",\n \"ipod\",\n \"ipodUsb\",\n \"netPlayer\",\n \"cd\",\n \"dab\",\n \"dist\",\n // Sound/Advanced regroup: DSP/tone-tuning states moved under sound.*, setup-only states\n // (+ the speakers/initialVolume/inputNames subtrees, already dotted before) under advanced.*.\n // {@link renamedObjectIds} also checks these against a stripped zone2/3/4 prefix, so one\n // entry here catches a MAIN state and its zoned copies (e.g. \"straight\" and \"zone2.straight\").\n \"straight\",\n \"enhancer\",\n \"pureDirect\",\n \"direct\",\n \"adaptiveDrc\",\n \"surroundAI\",\n \"surroundDecoder\",\n \"cinemaDsp3d\",\n \"extraBass\",\n \"bass\",\n \"treble\",\n \"subwooferTrim\",\n \"balance\",\n \"dialogueLevel\",\n \"dialogueLift\",\n \"dtsDialogueControl\",\n \"monaural\",\n \"surround3d\",\n \"adaptiveDspLevel\",\n \"audioSelect\",\n \"linkControl\",\n \"linkAudioDelay\",\n \"linkAudioQuality\",\n \"contentsDisplay\",\n \"equalizerLow\",\n \"equalizerMid\",\n \"equalizerHigh\",\n \"clearVoice\",\n \"bassExtension\",\n \"ypaoVolume\",\n \"maxVolume\",\n \"speakerA\",\n \"speakerB\",\n \"speakers\",\n \"initialVolume\",\n \"inputNames\",\n // ---- v2.0.0 tree rework: the always-empty source channels are gone entirely\n // (their playback lives in the flat per-zone block), lip sync moved into hdmi.\n // `player.ipod`/`player.ipodUsb` came BACK on 2026-09-06: the official command list gives\n // both sources an own `MODE` (Normal/Extended), so the folders carry genuine content again\n // and must not be deleted on every start \u2014 the migration-table guard in pure-helpers.test.ts\n // is what caught the contradiction. `player.spotify` followed on 2026-09-09: the RX-A850 list\n // gives Spotify a preset recall and store, so its folder carries content again.\n \"player.deezer\",\n \"player.tidal\",\n \"player.musicCastLink\",\n \"lipSync\",\n];\n\n/**\n * Read-capable state objects under a configured device whose state NEVER carried a\n * value (no value, no last-change). They are over-declarations of an earlier adapter\n * version \u2014 today's claim-with-proof creation would not make them \u2014 and deleting them\n * is lossless: there is no value and no history to lose, and anything a transport\n * legitimately claims is recreated right after, when the device connects. Runs once\n * per adapter version (the caller keeps a marker), so a freshly created state that is\n * merely waiting for its first value does not flap on every start. Excluded: buttons\n * and other write-only states (naturally valueless) and the `info.` header the adapter\n * itself maintains for every device, connected or not. A recording setting\n * (`common.custom`) is deliberately NO factor: whether anyone records a datapoint is\n * the user's business and says nothing about whether the datapoint belongs in the\n * tree \u2014 that is solely the adapter's responsibility (krobi, 2.0.3). On a never-filled\n * state there is nothing recorded to lose anyway.\n *\n * @param objects all objects under the instance, keyed by full id\n * @param states all states under the instance, keyed by full id\n * @param deviceIds the ids of the devices to sweep\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full ids of never-filled read-capable states\n */\nexport function neverWrittenStateIds(\n objects: Record<string, { type?: string; common?: unknown } | undefined>,\n states: Record<string, { val?: unknown; lc?: number } | null | undefined>,\n deviceIds: Set<string>,\n namespace: string,\n): string[] {\n const ids: string[] = [];\n for (const [fullId, object] of Object.entries(objects)) {\n const common = object?.common as { read?: boolean } | undefined;\n if (object?.type !== \"state\" || common?.read === false) {\n continue;\n }\n const relative = stripNamespace(fullId, namespace);\n const top = relative.split(\".\")[0];\n if (!deviceIds.has(top) || relative.slice(top.length + 1).startsWith(\"info.\")) {\n continue;\n }\n const state = states[fullId];\n if (!state || ((state.val === null || state.val === undefined) && !state.lc)) {\n ids.push(fullId);\n }\n }\n return ids;\n}\n\n/**\n * Channels under a connected device that carry no datapoint anywhere below them \u2014 the folders a\n * tree rework empties but never removes.\n *\n * Both existing sweeps miss these on purpose: {@link staleObjects} only knows whole device trees\n * and named paths, and {@link neverWrittenStateIds} filters on `type === \"state\"`. So a folder\n * whose datapoints the v2.0.0 migration deleted (`player.server` \u2014 the SERVER source keeps no own\n * datapoint in the new tree) survives as an empty promise in the object tree, and every future\n * rework that empties a folder instead of listing it would leave the same kind of leftover.\n *\n * Nested empties resolve by themselves: a channel counts as filled only when a STATE exists\n * somewhere beneath it, so a folder holding nothing but other empty folders goes too. Deepest\n * first, so children are deleted before their parents.\n *\n * @param objects all objects currently under the instance\n * @param deviceIds the devices that have connected in this run (an offline device is left alone)\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full channel ids to delete, deepest first\n */\nexport function childlessChannelIds(\n objects: Record<string, { type?: string } | undefined>,\n deviceIds: Set<string>,\n namespace: string,\n): string[] {\n // Every path segment that has a datapoint below it, collected once over all objects.\n const filled = new Set<string>();\n for (const [fullId, object] of Object.entries(objects)) {\n if (object?.type !== \"state\") {\n continue;\n }\n for (let cut = fullId.lastIndexOf(\".\"); cut > 0; cut = fullId.lastIndexOf(\".\", cut - 1)) {\n filled.add(fullId.slice(0, cut));\n }\n }\n const ids: string[] = [];\n for (const [fullId, object] of Object.entries(objects)) {\n if (object?.type !== \"channel\" || filled.has(fullId)) {\n continue;\n }\n const relative = stripNamespace(fullId, namespace);\n if (deviceIds.has(relative.split(\".\")[0])) {\n ids.push(fullId);\n }\n }\n return ids.sort((a, b) => b.length - a.length);\n}\n\n/**\n * The full ids of renamed old states (and old channel subtrees) that still exist\n * under a configured device, to be deleted on start-up so no orphan lingers beside\n * the new object. Deepest first, so children go before their parents.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full old ids to delete, deepest first\n */\nexport function renamedObjectIds(existing: string[], deviceIds: Set<string>, namespace: string): string[] {\n const stale: string[] = [];\n for (const deviceId of deviceIds) {\n const base = `${namespace}.${deviceId}.`;\n for (const full of existing) {\n if (!full.startsWith(base)) {\n continue;\n }\n const rel = full.slice(base.length);\n // Strip an optional zone prefix before matching too, so one RENAMED_CHANNELS entry\n // (e.g. \"straight\") catches both the MAIN state and its zoned copies \u2014 the old flat\n // \"zone2.\" form and today's \"multiroom.zone2.\" form alike (v2.0.0 renames live in\n // zoned folders, e.g. multiroom.zone2.scene.name1).\n const zone = /^(?:multiroom\\.)?zone[234]\\./.exec(rel)?.[0] ?? \"\";\n const template = rel.slice(zone.length);\n const renamedState = RENAMED_STATE_IDS.includes(rel) || RENAMED_STATE_IDS.includes(template);\n const underRenamedChannel = RENAMED_CHANNELS.some(\n ch => rel === ch || rel.startsWith(`${ch}.`) || template === ch || template.startsWith(`${ch}.`),\n );\n if (renamedState || underRenamedChannel) {\n stale.push(full);\n }\n }\n }\n return stale.sort((a, b) => b.length - a.length);\n}\n\n/**\n * The device row to carry over from the previous adapter's single-device config,\n * or undefined if nothing needs migrating. The old yamaha stored one receiver as\n * `config.ip` (older installs: `config.IP`); the new adapter uses a `devices`\n * table. Only migrates when the table is still empty, so it runs once.\n *\n * The old value could be a hostname (its HTTP client resolved names) and could\n * carry a `:port` suffix (its HTTP library split host:port). A port suffix would\n * break every transport here \u2014 YNCA is TCP :50000, YXC/XML build `http://<ip>\u2026`\n * \u2014 so it is stripped; the host/IP itself is carried over as-is.\n *\n * @param config the instance's native config\n * @returns the row to add to the devices table, or undefined\n */\nexport function legacyDeviceRow(config: Record<string, unknown>): { name: string; ip: string } | undefined {\n if (Array.isArray(config.devices) && config.devices.length > 0) {\n return undefined;\n }\n const raw =\n typeof config.ip === \"string\" && config.ip\n ? config.ip\n : typeof config.IP === \"string\" && config.IP\n ? config.IP\n : undefined;\n if (!raw) {\n return undefined;\n }\n const ip = raw.trim().replace(/:\\d+$/, \"\");\n return ip ? { name: ip, ip } : undefined;\n}\n\n/**\n * How trustworthy a display-name candidate is. A name the device carries for itself\n * (the MusicCast zone name a user typed in the app) beats its model designation.\n */\nexport const LABEL_RANK = { model: 1, deviceName: 2 } as const;\n\n/** Rank of a display-name candidate \u2014 see {@link LABEL_RANK}. */\nexport type LabelRank = (typeof LABEL_RANK)[keyof typeof LABEL_RANK];\n\n/**\n * Zone names that say nothing about the device \u2014 a receiver ships with these and a\n * user who never renamed the zone would end up with \"Main Zone\" as the device name.\n */\nconst GENERIC_ZONE_NAMES = new Set([\"main\", \"main zone\", \"mainzone\", \"zone\", \"zone 1\", \"zone1\"]);\n\n/**\n * Is this candidate worth showing as a device name?\n *\n * @param candidate the reported name\n * @returns true when it carries information about this particular device\n */\nexport function isUsefulDeviceName(candidate: string | undefined): boolean {\n const trimmed = (candidate ?? \"\").trim();\n return trimmed.length > 0 && !GENERIC_ZONE_NAMES.has(trimmed.toLowerCase());\n}\n\n/**\n * The display name to write onto a device object, or undefined to leave it alone.\n *\n * An upgraded instance carries the IP as its device name: the previous adapter knew\n * only an IP, so the migration had nothing else to call the device, and the object id\n * \u2014 which must not change, every history and visualisation binding hangs off it \u2014\n * became that IP. This decides when the adapter may replace that placeholder with\n * something a user recognises.\n *\n * Two things must never be overwritten: a name the user typed, and a better name by a\n * weaker source. The adapter therefore only writes over its own placeholder (the id\n * itself) or over what it wrote last, and only when the new candidate ranks at least\n * as high as the one behind the current name.\n *\n * @param current the device object's present `common.name`\n * @param deviceId the object id, which is also the placeholder name\n * @param candidate the newly reported name\n * @param rank how trustworthy the candidate is\n * @param ownName the name this adapter wrote last for the device, if any\n * @param ownRank the rank behind {@link ownName}\n * @returns the name to write, or undefined when the current name stays\n */\nexport function nextDeviceLabel(\n current: string | undefined,\n deviceId: string,\n candidate: string | undefined,\n rank: LabelRank,\n ownName?: string,\n ownRank?: LabelRank,\n): string | undefined {\n const wanted = (candidate ?? \"\").trim();\n if (!isUsefulDeviceName(wanted) || wanted === current) {\n return undefined;\n }\n const isPlaceholder = current === undefined || current === deviceId;\n const isOurs = ownName !== undefined && current === ownName;\n if (!isPlaceholder && !isOurs) {\n return undefined; // the user named this device \u2014 theirs wins\n }\n if (isOurs && ownRank !== undefined && rank < ownRank) {\n return undefined; // do not fall back from a device name to its model\n }\n return wanted;\n}\n\n/** The numeric bounds an ioBroker state object can carry. */\nexport interface BoundFields {\n /** The smallest value the datapoint declares. */\n min?: number;\n /** The largest value the datapoint declares. */\n max?: number;\n /** The grid the datapoint's values sit on. */\n step?: number;\n}\n\n/**\n * The bound fields, in the order a clearing write lists them. Kept as one list so the snapshot,\n * the comparison and the clearing write can never drift apart.\n */\nexport const BOUND_FIELDS = [\"min\", \"max\", \"step\"] as const;\n\n/**\n * The bounds a stored object carries, with anything that is not a number treated as absent \u2014 an\n * object written by an older version, or by hand in the admin, can hold a string there.\n *\n * @param common the stored object's `common` part\n * @returns its numeric bounds\n */\nexport function boundsOfCommon(common: { min?: unknown; max?: unknown; step?: unknown } | undefined): BoundFields {\n const bounds: BoundFields = {};\n for (const field of BOUND_FIELDS) {\n const value = common?.[field];\n if (typeof value === \"number\") {\n bounds[field] = value;\n }\n }\n return bounds;\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,SAAS,mBAAmB,OAA2C;AACrE,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,WAAO;AAAA,EACT;AACA,QAAM,YAAY;AAClB,SACE,OAAO,UAAU,OAAO,YACxB,UAAU,GAAG,SAAS,MACrB,UAAU,SAAS,UAAa,OAAO,UAAU,SAAS;AAE/D;AAQO,SAAS,WAAW,KAAqB;AAC9C,SAAO,IAAI,QAAQ,oBAAoB,GAAG;AAC5C;AAUO,SAAS,eAAe,QAAgB,WAA2B;AACxE,SAAO,OAAO,MAAM,UAAU,SAAS,CAAC;AAC1C;AAcO,SAAS,aAAa,KAAc,aAA0E;AACnH,MAAI,CAAC,MAAM,QAAQ,GAAG,GAAG;AACvB,WAAO,CAAC;AAAA,EACV;AACA,QAAM,UAA0B,CAAC;AACjC,QAAM,QAAQ,oBAAI,IAAY,CAAC,MAAM,CAAC;AACtC,aAAW,SAAS,KAAK;AACvB,QAAI,CAAC,mBAAmB,KAAK,GAAG;AAC9B;AAAA,IACF;AAGA,UAAM,KAAK,WAAW,MAAM,QAAQ,MAAM,KAAK,SAAS,IAAI,MAAM,OAAO,MAAM,EAAE;AACjF,QAAI,MAAM,IAAI,EAAE,GAAG;AACjB,iDAAc,MAAM,QAAQ,MAAM,IAAI;AACtC;AAAA,IACF;AACA,UAAM,IAAI,EAAE;AACZ,YAAQ,KAAK,EAAE,IAAI,IAAI,MAAM,IAAI,QAAQ,SAAS,CAAC;AAAA,EACrD;AACA,SAAO;AACT;AAcO,SAAS,aAAa,QAAiC,YAAqD;AACjH,QAAM,OAAO,oBAAI,IAA0B;AAC3C,aAAW,UAAU,QAAQ;AAC3B,SAAK,IAAI,OAAO,IAAI,EAAE,GAAG,QAAQ,QAAQ,SAAS,CAAC;AAAA,EACrD;AACA,aAAW,UAAU,YAAY;AAC/B,QAAI,KAAK,IAAI,OAAO,EAAE,GAAG;AACvB;AAAA,IACF;AAGA,QAAI,CAAC,GAAG,KAAK,OAAO,CAAC,EAAE,KAAK,WAAS,MAAM,OAAO,OAAO,EAAE,GAAG;AAC5D;AAAA,IACF;AACA,SAAK,IAAI,OAAO,IAAI,EAAE,GAAG,QAAQ,QAAQ,aAAa,CAAC;AAAA,EACzD;AACA,SAAO,CAAC,GAAG,KAAK,OAAO,CAAC;AAC1B;AAwBO,SAAS,gBACd,OACA,OACA,aACgB;AA9IlB;AA+IE,QAAM,OAAO,oBAAI,IAA0B;AAC3C,aAAW,UAAU,OAAO;AAE1B,QAAI,OAAO,OAAO,UAAU,KAAK,IAAI,OAAO,EAAE,GAAG;AAC/C;AAAA,IACF;AACA,SAAK,IAAI,OAAO,IAAI,EAAE,GAAG,OAAO,CAAC;AAAA,EACnC;AACA,aAAW,UAAU,OAAO;AAC1B,UAAM,QAAQ,OAAO,QAAQ,OAAO;AACpC,UAAM,KAAK,WAAW,KAAK;AAC3B,UAAM,aAAa,KAAK,IAAI,EAAE;AAC9B,QAAI,YAAY;AAEd,iBAAW,KAAK,OAAO;AACvB;AAAA,IACF;AACA,UAAM,UAAU,CAAC,GAAG,KAAK,OAAO,CAAC,EAAE,KAAK,YAAU,OAAO,OAAO,OAAO,EAAE;AACzE,QAAI,OAAO,UAAU,SAAS;AAC5B,iDAAc,QAAO,wCAAS,OAAT,YAAe;AACpC;AAAA,IACF;AACA,SAAK,IAAI,IAAI,EAAE,IAAI,IAAI,OAAO,GAAG,CAAC;AAAA,EACpC;AACA,SAAO,CAAC,GAAG,KAAK,OAAO,CAAC;AAC1B;
|
|
4
|
+
"sourcesContent": ["import type { DeviceRecord } from \"./types\";\nimport type { DiscoveredDevice } from \"./discovery\";\n\ninterface ConfiguredDevice {\n name?: string;\n ip: string;\n}\n\n/**\n * True when a raw config row carries a non-empty ip (the name is optional \u2014 a row\n * with an ip but no name is valid and falls back to the ip as its id, so a device\n * is never silently dropped just because its name was left blank).\n *\n * @param entry a raw config row from the admin device table\n * @returns whether the row is a valid configured device\n */\nfunction isConfiguredDevice(entry: unknown): entry is ConfiguredDevice {\n if (typeof entry !== \"object\" || entry === null) {\n return false;\n }\n const candidate = entry as { name?: unknown; ip?: unknown };\n return (\n typeof candidate.ip === \"string\" &&\n candidate.ip.length > 0 &&\n (candidate.name === undefined || typeof candidate.name === \"string\")\n );\n}\n\n/**\n * Make a string safe for use as an ioBroker object id segment.\n *\n * @param raw the raw string (e.g. a device name)\n * @returns the string with id-unsafe characters replaced by underscores\n */\nexport function sanitizeId(raw: string): string {\n return raw.replace(/[^A-Za-z0-9\\-_]/g, \"_\");\n}\n\n/**\n * Strip the adapter namespace (e.g. `yamaha.0`) from a full state id, leaving the\n * device-relative path (e.g. `living.power`).\n *\n * @param fullId the full state id\n * @param namespace the adapter namespace\n * @returns the id relative to the adapter instance\n */\nexport function stripNamespace(fullId: string, namespace: string): string {\n return fullId.slice(namespace.length + 1);\n}\n\n/**\n * Turn the admin device table (untrusted native config) into device records.\n * Invalid rows are dropped, as are rows whose id collides with the adapter's own\n * reserved `info` branch or with an id already taken \u2014 two names that sanitise to\n * the same id (e.g. \"Living Room\" and \"Living.Room\") would otherwise share one\n * object tree. A dropped collision is reported via `onCollision` so the device does\n * not just silently \"not exist\" for the user.\n *\n * @param raw the raw `native.devices` value\n * @param onCollision called with the dropped row's name/ip and the clashing id\n * @returns validated, de-duplicated device records\n */\nexport function parseDevices(raw: unknown, onCollision?: (dropped: string, takenId: string) => void): DeviceRecord[] {\n if (!Array.isArray(raw)) {\n return [];\n }\n const records: DeviceRecord[] = [];\n const taken = new Set<string>([\"info\"]); // reserved: the adapter's own info channel\n for (const entry of raw) {\n if (!isConfiguredDevice(entry)) {\n continue;\n }\n // Fall back to the ip as the id when the name is blank, so the device still appears\n // instead of vanishing silently.\n const id = sanitizeId(entry.name && entry.name.length > 0 ? entry.name : entry.ip);\n if (taken.has(id)) {\n onCollision?.(entry.name || entry.ip, id);\n continue;\n }\n taken.add(id);\n records.push({ id, ip: entry.ip, source: \"manual\" });\n }\n return records;\n}\n\n/**\n * The set of devices the adapter runs: the manual table plus the discovered store, keyed by id.\n *\n * A manual entry WINS over a discovered one of the same id \u2014 the user typed that address, and\n * the network search must not overwrite it. Before 2.9.0 the two were mutually exclusive (a\n * filled table turned the search off entirely), so making one discovered device manual dropped\n * every other one from the run and `cleanupStaleObjects` deleted their trees.\n *\n * @param manual the rows from the instance's device table\n * @param discovered the records remembered from the network search\n * @returns one record per id, manual first, each tagged with where it came from\n */\nexport function unionDevices(manual: readonly DeviceRecord[], discovered: readonly DeviceRecord[]): DeviceRecord[] {\n const byId = new Map<string, DeviceRecord>();\n for (const device of manual) {\n byId.set(device.id, { ...device, source: \"manual\" });\n }\n for (const device of discovered) {\n if (byId.has(device.id)) {\n continue;\n }\n // An address a manual device already occupies would mean two records talking to one\n // receiver \u2014 the manual one owns it.\n if ([...byId.values()].some(known => known.ip === device.ip)) {\n continue;\n }\n byId.set(device.id, { ...device, source: \"discovered\" });\n }\n return [...byId.values()];\n}\n\n/**\n * Merge freshly discovered devices into the set already known from earlier runs\n * (the auto-discovery standby protection). A known device is kept even when this\n * run's scan did not find it \u2014 a receiver in deep standby answers no SSDP, and its\n * object tree must survive.\n *\n * Identity is the device ID (derived from the friendly name it advertises, or from its\n * address when it advertises none) \u2014 NOT its address. Keyed by address, a receiver that\n * moved to a new address by DHCP was lost for good: the remembered record kept the old\n * address, the same receiver found at the new one produced the same id, and the id clash\n * dropped it. It stayed offline with no way back, because the id is what the whole object\n * tree hangs off. Now the same id simply carries the new address over.\n *\n * A genuine clash remains a clash: a DIFFERENT device sitting on an address another\n * record already claims is skipped and reported through `onCollision`, so a missing\n * device is explainable rather than silent.\n *\n * @param known the device records remembered from earlier runs\n * @param found the devices discovered this run\n * @param onCollision called with the dropped device's name/ip and the clashing id\n * @returns the merged records, one per device id\n */\nexport function mergeDiscovered(\n known: DeviceRecord[],\n found: DiscoveredDevice[],\n onCollision?: (dropped: string, takenId: string) => void,\n): DeviceRecord[] {\n const byId = new Map<string, DeviceRecord>();\n for (const device of known) {\n // \"info\" is the adapter's own channel \u2014 a device may never claim it.\n if (device.id === \"info\" || byId.has(device.id)) {\n continue;\n }\n byId.set(device.id, { ...device });\n }\n for (const device of found) {\n const label = device.name || device.ip;\n const id = sanitizeId(label);\n const remembered = byId.get(id);\n if (remembered) {\n // Same device, possibly at a new address \u2014 carry the address over, keep the id.\n remembered.ip = device.ip;\n continue;\n }\n const ipOwner = [...byId.values()].find(record => record.ip === device.ip);\n if (id === \"info\" || ipOwner) {\n onCollision?.(label, ipOwner?.id ?? id);\n continue;\n }\n byId.set(id, { id, ip: device.ip });\n }\n return [...byId.values()];\n}\n\n/**\n * From all object ids under the instance, pick the stale ones to delete on start:\n * everything that does not belong to a configured device and is outside the\n * adapter's own `info` branch. Removes the previous adapter's whole object tree\n * and any device dropped from the config. Keyed on the configured device ids\n * (not on what was created this run), so it works with the async connect where a\n * device's tree may only appear later \u2014 a configured device's subtree is kept\n * regardless of whether it has connected yet. Deepest first, so children go\n * before their parents.\n *\n * A device the discovery store still remembers is kept too, even when it is not part of\n * this run: switching the network search off is not a delete. Only the card's delete\n * button removes a device \u2014 it takes the record out of the store, and the id stops being\n * remembered here at the same moment. Without that a single hand-entered receiver used to\n * take every found one's tree with it, history and VIS bindings included.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @param remembered ids the discovery store still holds that are not running this time\n * @returns the stale ids to delete, deepest first\n */\nexport function staleObjects(\n existing: string[],\n deviceIds: Set<string>,\n namespace: string,\n remembered: ReadonlySet<string> = new Set(),\n): string[] {\n // No configured devices \u2192 never wipe the whole tree (a user who cleared the\n // device table by accident would otherwise lose every object in one pass).\n // Anchored on the RUNNING set: a run with nothing to run deletes nothing at all.\n if (deviceIds.size === 0) {\n return [];\n }\n const isKept = (fullId: string): boolean => {\n const top = stripNamespace(fullId, namespace).split(\".\")[0];\n return top === \"info\" || deviceIds.has(top) || remembered.has(top);\n };\n return existing.filter(id => !isKept(id)).sort((a, b) => b.length - a.length);\n}\n\n/**\n * Relative state ids an earlier version created under a different path and that this\n * version has renamed or moved. On start-up the old object is deleted so it does not\n * linger orphaned beside the new one \u2014 {@link staleObjects} only removes whole\n * non-configured device trees, not renamed states inside a device that is kept.\n */\n/**\n * The per-source playback-block states of the pre-2.0.0 tree \u2014 the same 19 states\n * used to sit under every player source folder, almost all permanently empty. In\n * 2.0.0 ONE flat block per zone replaces them; these copies are removed on the\n * first start (the sources that keep genuinely own states keep those untouched).\n */\nconst V2_PLAYER_BLOCK_STATES = [\n \"playback\",\n \"artist\",\n \"album\",\n \"track\",\n \"station\",\n \"channelName\",\n \"totalTime\",\n \"elapsedTime\",\n \"repeat\",\n \"shuffle\",\n \"albumArt\",\n \"source\",\n \"play\",\n \"pause\",\n \"stop\",\n \"next\",\n \"prev\",\n \"repeatToggle\",\n \"shuffleToggle\",\n];\n\n/** The source folders that stay in 2.0.0 (own states remain) but lose their block copy. */\nconst V2_SLIMMED_SOURCES = [\n \"netPlayer\",\n \"cd\",\n \"netRadio\",\n \"server\",\n \"usb\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"pc\",\n \"airplay\",\n \"bluetooth\",\n // Folders that came back with own content after the 2.0.0 cut (iPod/iPod USB: MODE, 2026-09-06;\n // Spotify: the RX-A850 presets, 2026-09-09) \u2014 their 1.x playback copies still have to go.\n \"ipod\",\n \"ipodUsb\",\n \"spotify\",\n];\n\nexport const RENAMED_STATE_IDS = [\n // v2.8.0: the volume datapoint now carries the scale the receiver itself displays, so the two\n // derived states are gone \u2014 `actualVolume` was an exact duplicate of `volume` on every device\n // declaring a single scale, and `actualVolumeMode` a dropdown with one entry there. `inputText`\n // repeated the label the `input` dropdown carries since 2.7.2, and only on MusicCast devices.\n \"actualVolume\",\n \"actualVolumeMode\",\n \"inputText\",\n \"hdmiOut\",\n \"directMode\",\n \"masterPower\",\n \"party\",\n \"partyMute\",\n \"distributionEnable\",\n \"partyEnable\",\n \"remoteCode\",\n // v1.0.0 multiroom regroup: the MusicCast-Link states moved into their own\n // multiroom.group folder, so the tree itself tells device-group from all-zones scope.\n \"multiroom.distributionEnable\",\n // Short-lived v1.0.0 pre-cut id \u2014 mislabeled \"active\", the field means \"enabled for use\".\n \"multiroom.group.streamingActive\",\n \"multiroom.role\",\n \"multiroom.groupId\",\n \"multiroom.groupName\",\n \"multiroom.serverZone\",\n \"multiroom.clientList\",\n \"multiroom.linkClient\",\n \"multiroom.leaveGroup\",\n // ---- v2.0.0 tree rework ------------------------------------------------------\n // Player unification: the per-source copies of the playback block are gone (the\n // slimmed folders keep only their own preset/pairing/drive states).\n ...V2_SLIMMED_SOURCES.flatMap(source => V2_PLAYER_BLOCK_STATES.map(state => `player.${source}.${state}`)),\n // Scenes: the twelve per-name datapoints became the recall dropdown + scene.list.\n ...Array.from({ length: 12 }, (_unused, i) => `scene.name${i + 1}`),\n // Tuner unification: ONE band/frequency/preset; the DAB subunit's FM half moved\n // onto the flat tuner ids, the two per-band frequencies became tuner.frequency.\n \"tuner.amFrequency\",\n \"tuner.fmFrequency\",\n \"tuner.dab.band\",\n \"tuner.dab.preset\",\n \"tuner.dab.fmPreset\",\n \"tuner.dab.fmFrequency\",\n \"tuner.dab.fmSearchMode\",\n \"tuner.dab.fmRdsService\",\n \"tuner.dab.fmRdsProgramType\",\n \"tuner.dab.fmRdsText\",\n \"tuner.dab.fmRdsClock\",\n \"tuner.dab.fmStereo\",\n \"tuner.dab.fmTuned\",\n \"tuner.dab.audioMode\",\n // Sound polish: equalizer and signal info each moved into their own subfolder.\n \"sound.equalizerMode\",\n \"sound.equalizerLow\",\n \"sound.equalizerMid\",\n \"sound.equalizerHigh\",\n \"sound.signalFormat\",\n \"sound.signalSampling\",\n \"sound.signalBits\",\n \"sound.signalBitrate\",\n // HDMI polish: the lip-sync offsets moved into the hdmi folder (the lipSync\n // channel itself is in RENAMED_CHANNELS); the A/B toggles joined the speakers.\n \"advanced.speakerA\",\n \"advanced.speakerB\",\n];\n\n/**\n * Old channel prefixes whose whole subtree this version moved out \u2014 the `system`\n * grab-bag is gone (model/firmware \u2192 info, HDMI outputs \u2192 hdmi, speaker patterns \u2192\n * speakers, input names \u2192 inputNames, master power \u2192 multiroom.masterPower). The channel and\n * every state under it are removed.\n */\nexport const RENAMED_CHANNELS = [\n // pre-0.11 system folder\n \"system\",\n // v0.18.1 multiroom regroup: zone2/3/4, zoneB, flat multiroom states moved under multiroom/.\n \"zone2\",\n \"zone3\",\n \"zone4\",\n \"zoneB\",\n // v1.0.0: stray per-zone copies of the device-global YXC states (the zone loop used to\n // prefix them too, yielding multiroom.zoneN.multiroom.* junk) are swept away.\n \"multiroom.zone2.multiroom\",\n \"multiroom.zone3.multiroom\",\n \"multiroom.zone4.multiroom\",\n // Regrouping: media sources moved under player/, DAB under tuner/, dist \u2192 multiroom. The old\n // flat channels (and their whole subtree) are deleted so the new grouped ones do not sit\n // beside orphaned copies on an upgraded instance.\n \"netRadio\",\n \"server\",\n \"usb\",\n \"spotify\",\n \"deezer\",\n \"tidal\",\n \"napster\",\n \"pandora\",\n \"rhapsody\",\n \"sirius\",\n \"airplay\",\n \"bluetooth\",\n \"pc\",\n \"musicCastLink\",\n \"ipod\",\n \"ipodUsb\",\n \"netPlayer\",\n \"cd\",\n \"dab\",\n \"dist\",\n // Sound/Advanced regroup: DSP/tone-tuning states moved under sound.*, setup-only states\n // (+ the speakers/initialVolume/inputNames subtrees, already dotted before) under advanced.*.\n // {@link renamedObjectIds} also checks these against a stripped zone2/3/4 prefix, so one\n // entry here catches a MAIN state and its zoned copies (e.g. \"straight\" and \"zone2.straight\").\n \"straight\",\n \"enhancer\",\n \"pureDirect\",\n \"direct\",\n \"adaptiveDrc\",\n \"surroundAI\",\n \"surroundDecoder\",\n \"cinemaDsp3d\",\n \"extraBass\",\n \"bass\",\n \"treble\",\n \"subwooferTrim\",\n \"balance\",\n \"dialogueLevel\",\n \"dialogueLift\",\n \"dtsDialogueControl\",\n \"monaural\",\n \"surround3d\",\n \"adaptiveDspLevel\",\n \"audioSelect\",\n \"linkControl\",\n \"linkAudioDelay\",\n \"linkAudioQuality\",\n \"contentsDisplay\",\n \"equalizerLow\",\n \"equalizerMid\",\n \"equalizerHigh\",\n \"clearVoice\",\n \"bassExtension\",\n \"ypaoVolume\",\n \"maxVolume\",\n \"speakerA\",\n \"speakerB\",\n \"speakers\",\n \"initialVolume\",\n \"inputNames\",\n // ---- v2.0.0 tree rework: the always-empty source channels are gone entirely\n // (their playback lives in the flat per-zone block), lip sync moved into hdmi.\n // `player.ipod`/`player.ipodUsb` came BACK on 2026-09-06: the official command list gives\n // both sources an own `MODE` (Normal/Extended), so the folders carry genuine content again\n // and must not be deleted on every start \u2014 the migration-table guard in pure-helpers.test.ts\n // is what caught the contradiction. `player.spotify` followed on 2026-09-09: the RX-A850 list\n // gives Spotify a preset recall and store, so its folder carries content again.\n \"player.deezer\",\n \"player.tidal\",\n \"player.musicCastLink\",\n \"lipSync\",\n];\n\n/**\n * Read-capable state objects under a configured device whose state NEVER carried a\n * value (no value, no last-change). They are over-declarations of an earlier adapter\n * version \u2014 today's claim-with-proof creation would not make them \u2014 and deleting them\n * is lossless: there is no value and no history to lose, and anything a transport\n * legitimately claims is recreated right after, when the device connects. Runs once\n * per adapter version (the caller keeps a marker), so a freshly created state that is\n * merely waiting for its first value does not flap on every start. Excluded: buttons\n * and other write-only states (naturally valueless) and the `info.` header the adapter\n * itself maintains for every device, connected or not. A recording setting\n * (`common.custom`) is deliberately NO factor: whether anyone records a datapoint is\n * the user's business and says nothing about whether the datapoint belongs in the\n * tree \u2014 that is solely the adapter's responsibility (krobi, 2.0.3). On a never-filled\n * state there is nothing recorded to lose anyway.\n *\n * @param objects all objects under the instance, keyed by full id\n * @param states all states under the instance, keyed by full id\n * @param deviceIds the ids of the devices to sweep\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full ids of never-filled read-capable states\n */\nexport function neverWrittenStateIds(\n objects: Record<string, { type?: string; common?: unknown } | undefined>,\n states: Record<string, { val?: unknown; lc?: number } | null | undefined>,\n deviceIds: Set<string>,\n namespace: string,\n): string[] {\n const ids: string[] = [];\n for (const [fullId, object] of Object.entries(objects)) {\n const common = object?.common as { read?: boolean } | undefined;\n if (object?.type !== \"state\" || common?.read === false) {\n continue;\n }\n const relative = stripNamespace(fullId, namespace);\n const top = relative.split(\".\")[0];\n if (!deviceIds.has(top) || relative.slice(top.length + 1).startsWith(\"info.\")) {\n continue;\n }\n const state = states[fullId];\n if (!state || ((state.val === null || state.val === undefined) && !state.lc)) {\n ids.push(fullId);\n }\n }\n return ids;\n}\n\n/**\n * Channels under a connected device that carry no datapoint anywhere below them \u2014 the folders a\n * tree rework empties but never removes.\n *\n * Both existing sweeps miss these on purpose: {@link staleObjects} only knows whole device trees\n * and named paths, and {@link neverWrittenStateIds} filters on `type === \"state\"`. So a folder\n * whose datapoints the v2.0.0 migration deleted (`player.server` \u2014 the SERVER source keeps no own\n * datapoint in the new tree) survives as an empty promise in the object tree, and every future\n * rework that empties a folder instead of listing it would leave the same kind of leftover.\n *\n * Nested empties resolve by themselves: a channel counts as filled only when a STATE exists\n * somewhere beneath it, so a folder holding nothing but other empty folders goes too. Deepest\n * first, so children are deleted before their parents.\n *\n * @param objects all objects currently under the instance\n * @param deviceIds the devices that have connected in this run (an offline device is left alone)\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full channel ids to delete, deepest first\n */\nexport function childlessChannelIds(\n objects: Record<string, { type?: string } | undefined>,\n deviceIds: Set<string>,\n namespace: string,\n): string[] {\n // Every path segment that has a datapoint below it, collected once over all objects.\n const filled = new Set<string>();\n for (const [fullId, object] of Object.entries(objects)) {\n if (object?.type !== \"state\") {\n continue;\n }\n for (let cut = fullId.lastIndexOf(\".\"); cut > 0; cut = fullId.lastIndexOf(\".\", cut - 1)) {\n filled.add(fullId.slice(0, cut));\n }\n }\n const ids: string[] = [];\n for (const [fullId, object] of Object.entries(objects)) {\n if (object?.type !== \"channel\" || filled.has(fullId)) {\n continue;\n }\n const relative = stripNamespace(fullId, namespace);\n if (deviceIds.has(relative.split(\".\")[0])) {\n ids.push(fullId);\n }\n }\n return ids.sort((a, b) => b.length - a.length);\n}\n\n/**\n * The full ids of renamed old states (and old channel subtrees) that still exist\n * under a configured device, to be deleted on start-up so no orphan lingers beside\n * the new object. Deepest first, so children go before their parents.\n *\n * @param existing all object ids currently under the instance\n * @param deviceIds the ids of the currently configured devices\n * @param namespace the adapter namespace (e.g. `yamaha.0`)\n * @returns the full old ids to delete, deepest first\n */\nexport function renamedObjectIds(existing: string[], deviceIds: Set<string>, namespace: string): string[] {\n const stale: string[] = [];\n for (const deviceId of deviceIds) {\n const base = `${namespace}.${deviceId}.`;\n for (const full of existing) {\n if (!full.startsWith(base)) {\n continue;\n }\n const rel = full.slice(base.length);\n // Strip an optional zone prefix before matching too, so one RENAMED_CHANNELS entry\n // (e.g. \"straight\") catches both the MAIN state and its zoned copies \u2014 the old flat\n // \"zone2.\" form and today's \"multiroom.zone2.\" form alike (v2.0.0 renames live in\n // zoned folders, e.g. multiroom.zone2.scene.name1).\n const zone = /^(?:multiroom\\.)?zone[234]\\./.exec(rel)?.[0] ?? \"\";\n const template = rel.slice(zone.length);\n const renamedState = RENAMED_STATE_IDS.includes(rel) || RENAMED_STATE_IDS.includes(template);\n const underRenamedChannel = RENAMED_CHANNELS.some(\n ch => rel === ch || rel.startsWith(`${ch}.`) || template === ch || template.startsWith(`${ch}.`),\n );\n if (renamedState || underRenamedChannel) {\n stale.push(full);\n }\n }\n }\n return stale.sort((a, b) => b.length - a.length);\n}\n\n/**\n * The device row to carry over from the previous adapter's single-device config,\n * or undefined if nothing needs migrating. The old yamaha stored one receiver as\n * `config.ip` (older installs: `config.IP`); the new adapter uses a `devices`\n * table. Only migrates when the table is still empty, so it runs once.\n *\n * The old value could be a hostname (its HTTP client resolved names) and could\n * carry a `:port` suffix (its HTTP library split host:port). A port suffix would\n * break every transport here \u2014 YNCA is TCP :50000, YXC/XML build `http://<ip>\u2026`\n * \u2014 so it is stripped; the host/IP itself is carried over as-is.\n *\n * @param config the instance's native config\n * @returns the row to add to the devices table, or undefined\n */\nexport function legacyDeviceRow(config: Record<string, unknown>): { name: string; ip: string } | undefined {\n if (Array.isArray(config.devices) && config.devices.length > 0) {\n return undefined;\n }\n const raw =\n typeof config.ip === \"string\" && config.ip\n ? config.ip\n : typeof config.IP === \"string\" && config.IP\n ? config.IP\n : undefined;\n if (!raw) {\n return undefined;\n }\n const ip = raw.trim().replace(/:\\d+$/, \"\");\n return ip ? { name: ip, ip } : undefined;\n}\n\n/**\n * How trustworthy a display-name candidate is. A name the device carries for itself\n * (the MusicCast zone name a user typed in the app) beats its model designation.\n */\nexport const LABEL_RANK = { model: 1, deviceName: 2 } as const;\n\n/** Rank of a display-name candidate \u2014 see {@link LABEL_RANK}. */\nexport type LabelRank = (typeof LABEL_RANK)[keyof typeof LABEL_RANK];\n\n/**\n * Zone names that say nothing about the device \u2014 a receiver ships with these and a\n * user who never renamed the zone would end up with \"Main Zone\" as the device name.\n */\nconst GENERIC_ZONE_NAMES = new Set([\"main\", \"main zone\", \"mainzone\", \"zone\", \"zone 1\", \"zone1\"]);\n\n/**\n * Is this candidate worth showing as a device name?\n *\n * @param candidate the reported name\n * @returns true when it carries information about this particular device\n */\nexport function isUsefulDeviceName(candidate: string | undefined): boolean {\n const trimmed = (candidate ?? \"\").trim();\n return trimmed.length > 0 && !GENERIC_ZONE_NAMES.has(trimmed.toLowerCase());\n}\n\n/**\n * The display name to write onto a device object, or undefined to leave it alone.\n *\n * An upgraded instance carries the IP as its device name: the previous adapter knew\n * only an IP, so the migration had nothing else to call the device, and the object id\n * \u2014 which must not change, every history and visualisation binding hangs off it \u2014\n * became that IP. This decides when the adapter may replace that placeholder with\n * something a user recognises.\n *\n * Two things must never be overwritten: a name the user typed, and a better name by a\n * weaker source. The adapter therefore only writes over its own placeholder (the id\n * itself) or over what it wrote last, and only when the new candidate ranks at least\n * as high as the one behind the current name.\n *\n * @param current the device object's present `common.name`\n * @param deviceId the object id, which is also the placeholder name\n * @param candidate the newly reported name\n * @param rank how trustworthy the candidate is\n * @param ownName the name this adapter wrote last for the device, if any\n * @param ownRank the rank behind {@link ownName}\n * @returns the name to write, or undefined when the current name stays\n */\nexport function nextDeviceLabel(\n current: string | undefined,\n deviceId: string,\n candidate: string | undefined,\n rank: LabelRank,\n ownName?: string,\n ownRank?: LabelRank,\n): string | undefined {\n const wanted = (candidate ?? \"\").trim();\n if (!isUsefulDeviceName(wanted) || wanted === current) {\n return undefined;\n }\n const isPlaceholder = current === undefined || current === deviceId;\n const isOurs = ownName !== undefined && current === ownName;\n if (!isPlaceholder && !isOurs) {\n return undefined; // the user named this device \u2014 theirs wins\n }\n if (isOurs && ownRank !== undefined && rank < ownRank) {\n return undefined; // do not fall back from a device name to its model\n }\n return wanted;\n}\n\n/** The numeric bounds an ioBroker state object can carry. */\nexport interface BoundFields {\n /** The smallest value the datapoint declares. */\n min?: number;\n /** The largest value the datapoint declares. */\n max?: number;\n /** The grid the datapoint's values sit on. */\n step?: number;\n}\n\n/**\n * The bound fields, in the order a clearing write lists them. Kept as one list so the snapshot,\n * the comparison and the clearing write can never drift apart.\n */\nexport const BOUND_FIELDS = [\"min\", \"max\", \"step\"] as const;\n\n/**\n * The bounds a stored object carries, with anything that is not a number treated as absent \u2014 an\n * object written by an older version, or by hand in the admin, can hold a string there.\n *\n * @param common the stored object's `common` part\n * @returns its numeric bounds\n */\nexport function boundsOfCommon(common: { min?: unknown; max?: unknown; step?: unknown } | undefined): BoundFields {\n const bounds: BoundFields = {};\n for (const field of BOUND_FIELDS) {\n const value = common?.[field];\n if (typeof value === \"number\") {\n bounds[field] = value;\n }\n }\n return bounds;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|