iobroker.yamaha 2.9.1 → 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 CHANGED
@@ -74,6 +74,11 @@ 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
+
77
82
  ### 2.9.1 (2026-09-12)
78
83
 
79
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
@@ -106,12 +111,6 @@ Details on all settings, the object tree and the ports the adapter uses are in t
106
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.
107
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.
108
113
 
109
- ### 2.7.1 (2026-09-09)
110
-
111
- - (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.
112
- - (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.
113
- - (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.
114
-
115
114
  [Older changelogs can be found there](CHANGELOG_OLD.md)
116
115
 
117
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. Same switch as on the card one value,
58
- // two places to reach it.
57
+ // wanting percent says nothing about the others. THE place to set it2.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. Same switch as on the card \u2014 one value,\n // two places to reach it.\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,MAIA,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;",
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
- * One device's percent switch as a state, for the card control to show.
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 the switch position as an ioBroker state
65
+ * @returns true when this device is set to percent
66
66
  */
67
- async volumePercentState(deviceId) {
67
+ async volumeAsPercentOf(deviceId) {
68
68
  var _a;
69
69
  const node = await this.adapter.getForeignObjectAsync(`${this.adapter.namespace}.${deviceId}`);
70
- const on = ((_a = node == null ? void 0 : node.native) == null ? void 0 : _a.volumeAsPercent) === true;
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). All live values read from the device's own `info.*` states. A discovered
149
- * card has no edit action it is auto-found and re-found; it can only be removed.
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
- actions: [edit, del],
203
- // The percent switch, per device. It is a decision, not something switched around, so it
204
- // sits in the admin next to the device rather than in the object tree but it belongs to
205
- // THIS device, which is what 2.8.0's single instance checkbox could not express.
206
- controls: [
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 = (await this.volumePercentState(cardId)).val === true;
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": "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",
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
  }
package/io-package.json CHANGED
@@ -1,9 +1,22 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "yamaha",
4
- "version": "2.9.1",
4
+ "version": "2.9.2",
5
5
  "nogit": true,
6
6
  "news": {
7
+ "2.9.2": {
8
+ "en": "The card now shows a 0–100 % badge while a receiver's volume is in percent. The setting itself moved back to one place: the device's edit dialog.",
9
+ "de": "Die Karte zeigt jetzt ein 0–100-%-Abzeichen, solange die Lautstärke eines Receivers in Prozent läuft. Gesetzt wird die Einstellung nur noch im Bearbeiten-Dialog.",
10
+ "ru": "На карточке теперь есть значок 0–100 %, пока громкость ресивера в процентах. Сама настройка задаётся только в диалоге редактирования устройства.",
11
+ "pt": "O cartão mostra agora um emblema 0–100 % enquanto o volume de um recetor estiver em percentagem. A definição passa a ser feita só no diálogo de edição.",
12
+ "nl": "De kaart toont nu een 0–100 %-badge zolang het volume van een receiver in procenten staat. De instelling zelf zet u alleen nog in het bewerkvenster.",
13
+ "fr": "La carte affiche désormais un badge 0–100 % tant que le volume d'un ampli est en pourcentage. Le réglage se fait uniquement dans la boîte de dialogue de l'appareil.",
14
+ "it": "La scheda mostra ora un distintivo 0–100 % finché il volume di un ricevitore è in percentuale. L'impostazione si cambia solo nella finestra di modifica.",
15
+ "es": "La tarjeta muestra ahora una insignia 0–100 % mientras el volumen de un receptor esté en porcentaje. El ajuste se cambia solo en el diálogo de edición.",
16
+ "pl": "Kafelek pokazuje teraz odznakę 0–100 %, dopóki głośność amplitunera jest w procentach. Samo ustawienie zmienia się już tylko w oknie edycji.",
17
+ "uk": "Картка тепер показує позначку 0–100 %, поки гучність ресивера у відсотках. Саме налаштування задається лише в діалозі редагування пристрою.",
18
+ "zh-cn": "当功放音量以百分比显示时,卡片现在会显示一个 0–100 % 标记。该设置本身只在设备的编辑对话框中更改。"
19
+ },
7
20
  "2.9.1": {
8
21
  "en": "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 being deleted.",
9
22
  "de": "Ein per Netzsuche gefundener Receiver behält seine Datenpunkte, wenn Sie ein Gerät von Hand eintragen: Sie bleiben samt Historie und werden nur als offline gekennzeichnet.",
@@ -81,19 +94,6 @@
81
94
  "pl": "Listy oferują tylko to, co amplituner sam zgłasza lub udowadnia (#619); modele z 2008 r. odzyskują głośność i programy; dodano HD Radio, Sirius, strefę B i wiele punktów konfiguracji.",
82
95
  "uk": "Списки містять лише те, що ресивер сам повідомляє або підтверджує (#619); моделі 2008 р. знову мають гучність і програми; додано HD Radio, Sirius, зону B і багато точок налаштування.",
83
96
  "zh-cn": "下拉列表只提供功放自身声明或证实拥有的项(#619);2008 年机型恢复音量与音场程序;新增 HD Radio、Sirius、B 区和许多设置数据点。"
84
- },
85
- "2.5.2": {
86
- "en": "174 more datapoints now explain themselves — volume and tone say which scale they use, the stored lists say what is inside them, and Connected means reachable, not switched on.",
87
- "de": "174 weitere Datenpunkte erklären sich jetzt selbst — Lautstärke und Klang nennen ihre Skala, die gespeicherten Listen sagen, was drinsteht, und Verbunden heißt erreichbar, nicht eingeschaltet.",
88
- "ru": "Ещё 174 датапоинта теперь объясняют себя: громкость и тембр называют свою шкалу, сохранённые списки говорят, что внутри, а «Подключено» значит доступен, а не включён.",
89
- "pt": "Mais 174 pontos de dados explicam-se agora: volume e tom indicam a sua escala, as listas guardadas dizem o que contêm, e Ligado significa alcançável, não em funcionamento.",
90
- "nl": "174 datapunten meer leggen zichzelf nu uit: volume en toon noemen hun schaal, de opgeslagen lijsten zeggen wat erin staat, en Verbonden betekent bereikbaar, niet ingeschakeld.",
91
- "fr": "174 points de données de plus s’expliquent désormais : volume et tonalité indiquent leur échelle, les listes mémorisées disent ce qu’elles contiennent, et Connecté signifie joignable, pas allumé.",
92
- "it": "Altri 174 datapoint ora si spiegano da sé: volume e toni indicano la loro scala, gli elenchi memorizzati dicono cosa contengono, e Connesso significa raggiungibile, non acceso.",
93
- "es": "174 puntos de datos más se explican ahora: volumen y tono indican su escala, las listas guardadas dicen qué contienen, y Conectado significa accesible, no encendido.",
94
- "pl": "Kolejne 174 punkty danych same się objaśniają: głośność i barwa podają swoją skalę, zapisane listy mówią, co zawierają, a Połączono znaczy osiągalny, nie włączony.",
95
- "uk": "Ще 174 датапоінти тепер пояснюють себе: гучність і тембр називають свою шкалу, збережені списки кажуть, що всередині, а «З'єднано» означає доступний, а не ввімкнений.",
96
- "zh-cn": "另有 174 个数据点现在会自我说明:音量和音调标明所用刻度,已存列表说明其内容,“已连接”表示可达,而非已开机。"
97
97
  }
98
98
  },
99
99
  "titleLang": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.yamaha",
3
- "version": "2.9.1",
3
+ "version": "2.9.2",
4
4
  "description": "ioBroker adapter for Yamaha AV receivers and MusicCast devices",
5
5
  "author": {
6
6
  "name": "krobi",