iobroker.alias-workbench 0.9.9 → 0.9.11

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.
@@ -0,0 +1,478 @@
1
+ /* Wer nennt diese Kennung sonst noch?
2
+
3
+ Ein Adapter kann eine fremde Kennung nicht kennen, ohne sie irgendwo
4
+ hingeschrieben zu haben. Dafuer gibt es vier Ablagen: ein eigenes
5
+ Objekt, eine Datei, eine Aufzaehlung, oder `common.custom` am Punkt
6
+ selbst. Alle vier sieht dieser Kasten nach.
7
+
8
+ Gemessen am produktiven System (23584 Objekte der vier geladenen
9
+ Arten, 10,1 MB): Alias-Kennungen nennen dort ausschliesslich die
10
+ javascript-Skripte, zwei matter-Bridges, `enum.functions` und
11
+ `enum.rooms` - dazu die vis-Ansichten im Dateibaum (71 Aliase in
12
+ `vis.0`, 38 in `vis-2.0`). Es sind also nicht hundert Adapter,
13
+ sondern eine Handvoll Orte.
14
+
15
+ Was draussen bleibt, und warum:
16
+
17
+ * Adapter-Oberflaechen (`*.admin`, Widgetsaetze, Icon-Pakete). Das
18
+ sind 160 der 177 MB Text im Dateibaum, auf jedem ioBroker dieselben
19
+ - und sie enthalten Beispiele, die wie echte Daten aussehen
20
+ (`alias.0.Floor.Room.MotionSensor` in `javascript.admin`). Ohne
21
+ diese Grenze faende die Werkbank ihre eigenen hochgeladenen
22
+ Quelldateien als Verwender ihrer selbst. Gesucht wird deshalb nur
23
+ in Ordnern, die auf `.<Zahl>` enden - dort liegt, was der Nutzer
24
+ selbst gebaut hat (bei Ricardo 66 Dateien, 15 MB).
25
+ * Zustandswerte. `energiefluss-erweitert.0.configuration` ist ein
26
+ Datenpunkt, dessen *Wert* ein JSON voller Kennungen ist; im Objekt
27
+ steht davon nichts. Dafuer muesste man Werte lesen - ein eigener
28
+ Griff, bewusst noch nicht getan.
29
+ * Zusammengebaute Kennungen (`'alias.0.' + raum + '.Licht'`) und
30
+ Wildcard-Abos. Die findet keine Textsuche, und der Kasten sagt das.
31
+
32
+ Bewusst kein Urteil: dass die Quelle noch irgendwo genannt wird, ist
33
+ kein Fehler. Ein Skript, das sie beschreibt, gehoert genau dorthin.
34
+ Gemessen: von 376 Aliasen mit Quelle wurde bei 61 die Quelle in einem
35
+ Skript weiter direkt genannt, und ein guter Teil davon zu Recht. Eine
36
+ Textsuche kann lesend und schreibend nicht unterscheiden - deshalb
37
+ steht hier eine Liste zum Nachsehen und keine Warnung. */
38
+
39
+ import { S } from './zustand.js';
40
+ import { socket } from './verbindung.js';
41
+ import { el } from './basis.js';
42
+ import { tr, txt } from './sprache.js';
43
+ import { enums } from './enums.js';
44
+
45
+ /* --- Skripte ---------------------------------------------------------
46
+
47
+ Einmal holen und behalten. Der Verlegen-Dialog suchte bisher bei jedem
48
+ Neuzeichnen neu - bei 67 Skripten ist das jedes Mal dieselbe Antwort.
49
+ Der Vorrat gilt fuer die Sitzung; „neu einlesen" am Kasten wirft ihn
50
+ weg. */
51
+ var skriptVorrat = null;
52
+
53
+ function holeSkripte(fertig) {
54
+ if (skriptVorrat) { return fertig(skriptVorrat); }
55
+ var fertigMit = function (rows) {
56
+ skriptVorrat = rows;
57
+ fertig(skriptVorrat);
58
+ };
59
+ try {
60
+ socket.emit('getObjectView', 'script', 'javascript',
61
+ { startkey: 'script.js.', endkey: 'script.js.香' }, function (err, doc) {
62
+ if (err || !doc || !doc.rows) { return fertigMit([]); }
63
+ fertigMit(doc.rows.map(function (r) {
64
+ var o = r.value || {};
65
+ return {
66
+ id: r.id,
67
+ name: txt((o.common || {}).name) || String(r.id).split('.').pop(),
68
+ an: !!(o.common && o.common.enabled),
69
+ quelle: String((o.common || {}).source || '')
70
+ };
71
+ }));
72
+ });
73
+ } catch { fertigMit([]); }
74
+ }
75
+
76
+ /* Die Auswertung ohne Rueckruf - der Vorrat liegt dann schon da. */
77
+ function trefferFuer(kennung) {
78
+ var treffer = [];
79
+ (skriptVorrat || []).forEach(function (s) {
80
+ if (!kennung || s.quelle.indexOf(kennung) === -1) { return; }
81
+ var stellen = [];
82
+ s.quelle.split('\n').forEach(function (z, i) {
83
+ if (z.indexOf(kennung) > -1) { stellen.push(i + 1); }
84
+ });
85
+ treffer.push({ id: s.id, name: s.name, an: s.an, zeilen: stellen });
86
+ });
87
+ return treffer;
88
+ }
89
+
90
+ /* Alle Skripte, in deren Quelltext die Kennung im Klartext steht. */
91
+ export function sucheInSkripten(kennung, fertig) {
92
+ if (!kennung) { return fertig([]); }
93
+ holeSkripte(function () { fertig(trefferFuer(kennung)); });
94
+ }
95
+
96
+ /* Mehrere Kennungen auf einmal - der Loeschdialog nimmt beim Haken
97
+ „alle Ausgaenge" mehrere Wurzeln mit. Ein Skript, das zwei davon
98
+ nennt, soll trotzdem einmal in der Liste stehen. */
99
+ export function sucheInSkriptenMehrere(kennungen, fertig) {
100
+ holeSkripte(function () {
101
+ var nach = {};
102
+ (kennungen || []).forEach(function (k) {
103
+ trefferFuer(k).forEach(function (t) {
104
+ var v = nach[t.id];
105
+ if (!v) { nach[t.id] = { id: t.id, name: t.name, an: t.an, zeilen: t.zeilen.slice() }; return; }
106
+ t.zeilen.forEach(function (z) { if (v.zeilen.indexOf(z) === -1) { v.zeilen.push(z); } });
107
+ });
108
+ });
109
+ fertig(Object.keys(nach).map(function (k) {
110
+ nach[k].zeilen.sort(function (a, b) { return a - b; });
111
+ return nach[k];
112
+ }));
113
+ });
114
+ }
115
+
116
+ function gehoertZu(id, praefix) {
117
+ return id === praefix || id.indexOf(praefix + '.') === 0;
118
+ }
119
+
120
+ /* --- Objekte und Dateien ---------------------------------------------
121
+
122
+ Beide liefern dasselbe: „an dieser Stelle steht diese Kennung". Also
123
+ ein gemeinsamer Index, gebaut aus kennungsartigen Zeichenketten.
124
+
125
+ Der Ausdruck endet von selbst an der ersten Nicht-Kennungsstelle -
126
+ damit ist der Treffer hinten verankert. Ohne das faende die Suche
127
+ nach `hm-rpc.2.NEQ1660737` auch ein `…NEQ16607370`. */
128
+ var KENNUNG = /[A-Za-z0-9_-]+\.\d+\.[A-Za-z0-9_.\-À-ɏ]+/g;
129
+
130
+ /* token -> [{ art, id, name, ort, pfad }] */
131
+ var fundIndex = null;
132
+
133
+ function tokenAus(text, eintrag, ziel) {
134
+ var t;
135
+ KENNUNG.lastIndex = 0;
136
+ while ((t = KENNUNG.exec(text)) !== null) {
137
+ var k = t[0];
138
+ var liste = ziel.get(k);
139
+ if (!liste) { liste = []; ziel.set(k, liste); }
140
+ if (liste.indexOf(eintrag) === -1) { liste.push(eintrag); }
141
+ }
142
+ }
143
+
144
+ /* Die Objekte liegen schon im Speicher - `ladeObjekte` holt device,
145
+ channel, folder und state samt `native`. Das matter-Objekt, das seine
146
+ Bridge-Mitglieder auffuehrt, ist ein `channel` und damit von Anfang an
147
+ dabei. Es kostet also keinen neuen Zugriff, nur einen Durchlauf. */
148
+ function objekteIndizieren(ziel) {
149
+ Object.keys(S.objects).forEach(function (id) {
150
+ /* Die Diagnosepunkte einer Instanz (`system.adapter.mqtt-client.0.cpu`)
151
+ nennen nie eine fremde Kennung - sie tragen nur eine, die wie eine
152
+ aussieht. */
153
+ if (id.indexOf('system.adapter.') === 0) { return; }
154
+ var o = S.objects[id];
155
+ var text;
156
+ try { text = JSON.stringify(o); } catch { return; }
157
+ tokenAus(text, {
158
+ art: 'obj',
159
+ id: id,
160
+ name: txt((o.common || {}).name) || id.split('.').pop()
161
+ }, ziel);
162
+ });
163
+ }
164
+
165
+ /* Dateien, die nie eine Kennung tragen - und wenn doch, dann als Bild. */
166
+ var BINAER = /\.(png|jpe?g|gif|bmp|ico|webp|mp3|mp4|wav|ogg|woff2?|ttf|eot|otf|zip|gz|tgz|7z|pdf)$/i;
167
+ var MAX_DATEI = 8 * 1024 * 1024;
168
+
169
+ function lies(ort, pfad) {
170
+ return new Promise(function (fertig) {
171
+ try {
172
+ socket.emit('readFile', ort, pfad, function (err, daten) {
173
+ fertig((!err && typeof daten === 'string') ? daten : null);
174
+ });
175
+ } catch { fertig(null); }
176
+ });
177
+ }
178
+
179
+ function liesOrdner(ort, pfad) {
180
+ return new Promise(function (fertig) {
181
+ try {
182
+ socket.emit('readDir', ort, pfad, function (err, liste) {
183
+ fertig((!err && Array.isArray(liste)) ? liste : []);
184
+ });
185
+ } catch { fertig([]); }
186
+ });
187
+ }
188
+
189
+ /* Nur Instanzordner: `vis.0`, `vis-2.0`, `web.0`, `0_userdata.0` … Was
190
+ nicht auf `.<Zahl>` endet, ist Lieferbestandteil eines Adapters -
191
+ siehe der Kopf dieser Datei. */
192
+ var INSTANZ = /^[A-Za-z0-9_-]+\.\d+$/;
193
+
194
+ async function dateienIndizieren(ziel) {
195
+ var wurzel = await liesOrdner('', '');
196
+ var orte = wurzel.filter(function (x) {
197
+ return x && x.isDir && INSTANZ.test(x.file);
198
+ }).map(function (x) { return x.file; });
199
+
200
+ for (var i = 0; i < orte.length; i++) {
201
+ var ort = orte[i];
202
+ var offen = [''];
203
+ var dateien = [];
204
+ while (offen.length) {
205
+ var hier = offen.shift();
206
+ var liste = await liesOrdner(ort, hier);
207
+ liste.forEach(function (x) {
208
+ if (!x || !x.file) { return; }
209
+ var p = hier ? hier + '/' + x.file : x.file;
210
+ if (x.isDir) { offen.push(p); return; }
211
+ if (BINAER.test(p)) { return; }
212
+ if (x.stats && x.stats.size > MAX_DATEI) { return; }
213
+ dateien.push(p);
214
+ });
215
+ }
216
+ /* In kleinen Buendeln lesen: nacheinander dauert bei 66 Dateien
217
+ spuerbar, alle auf einmal ueberfaehrt den Socket. */
218
+ for (var j = 0; j < dateien.length; j += 4) {
219
+ var teil = dateien.slice(j, j + 4);
220
+ var inhalte = await Promise.all(teil.map(function (p) { return lies(ort, p); }));
221
+ inhalte.forEach(function (text, k) {
222
+ if (text) { tokenAus(text, { art: 'datei', ort: ort, pfad: teil[k] }, ziel); }
223
+ });
224
+ }
225
+ }
226
+ }
227
+
228
+ function holeFundorte(fertig) {
229
+ if (fundIndex) { return fertig(fundIndex); }
230
+ var ziel = new Map();
231
+ objekteIndizieren(ziel);
232
+ var schluss = function () { fundIndex = ziel; fertig(fundIndex); };
233
+ dateienIndizieren(ziel).then(schluss, schluss);
234
+ }
235
+
236
+ /* Fundstellen zu einem Praefix - ohne den Punkt selbst, ohne alles
237
+ unter ihm, und ohne die Aliase: die stehen in einer eigenen Zeile. */
238
+ function fundstellen(praefix, art) {
239
+ if (!praefix || !fundIndex) { return []; }
240
+ var raus = [];
241
+ var gesehen = {};
242
+ fundIndex.forEach(function (liste, token) {
243
+ if (!gehoertZu(token, praefix)) { return; }
244
+ liste.forEach(function (e) {
245
+ if (e.art !== art) { return; }
246
+ if (art === 'obj') {
247
+ if (e.id.indexOf('alias.') === 0) { return; }
248
+ if (gehoertZu(e.id, praefix) || gehoertZu(praefix, e.id)) { return; }
249
+ }
250
+ var schluessel = art === 'obj' ? e.id : e.ort + '/' + e.pfad;
251
+ if (gesehen[schluessel]) { return; }
252
+ gesehen[schluessel] = true;
253
+ raus.push(e);
254
+ });
255
+ });
256
+ return raus;
257
+ }
258
+
259
+ /* --- Was ohne Suche schon dasteht ------------------------------------ */
260
+
261
+ /* Aufzaehlungen, die den Punkt oder einen darunter als Mitglied fuehren. */
262
+ function aufzaehlungenMit(praefix) {
263
+ if (!praefix) { return []; }
264
+ return Object.keys(enums).filter(function (k) {
265
+ return ((enums[k].common || {}).members || []).some(function (m) {
266
+ return gehoertZu(m, praefix);
267
+ });
268
+ }).map(function (k) {
269
+ return { id: k, name: txt((enums[k].common || {}).name) || k.split('.').pop() };
270
+ });
271
+ }
272
+
273
+ /* Was am Punkt selbst haengt: Aufzeichnung (history, influxdb, sql),
274
+ Smart-Namen (iot, alexa). Das steht im Objekt und ist deshalb gratis. */
275
+ function customAn(praefix) {
276
+ if (!praefix) { return []; }
277
+ var raus = [];
278
+ Object.keys(S.objects).forEach(function (id) {
279
+ if (!gehoertZu(id, praefix)) { return; }
280
+ var c = (S.objects[id].common || {}).custom;
281
+ if (!c) { return; }
282
+ var adapter = Object.keys(c);
283
+ if (adapter.length) { raus.push({ id: id, adapter: adapter.join(', ') }); }
284
+ });
285
+ return raus;
286
+ }
287
+
288
+ /* Weitere Aliase, die aus derselben Quelle lesen oder in sie schreiben. */
289
+ function aliaseAuf(quelle, ausser) {
290
+ if (!quelle) { return []; }
291
+ var kanaele = {};
292
+ Object.keys(S.objects).forEach(function (id) {
293
+ if (id.indexOf('alias.') !== 0) { return; }
294
+ var a = ((S.objects[id].common || {}).alias || {}).id;
295
+ if (!a) { return; }
296
+ var ziele = (typeof a === 'string') ? [a] : [a.read, a.write];
297
+ if (!ziele.some(function (z) { return z && gehoertZu(z, quelle); })) { return; }
298
+ var kanal = id.split('.').slice(0, -1).join('.');
299
+ if (ausser && gehoertZu(kanal, ausser)) { return; }
300
+ kanaele[kanal] = true;
301
+ });
302
+ return Object.keys(kanaele);
303
+ }
304
+
305
+ /* --- Anzeige --------------------------------------------------------- */
306
+
307
+ /* Der Ordner, in dem das Skript liegt: aus script.js.common.Raum.Name
308
+ bleibt „Raum". Bei 67 Skripten sagt der blosse Name oft nicht genug -
309
+ „Licht" gibt es dreimal, in drei Raeumen. */
310
+ function skriptOrdner(id) {
311
+ return String(id).split('.').slice(3, -1).join(' / ');
312
+ }
313
+
314
+ export function skriptListe(treffer) {
315
+ var ul = el('ul');
316
+ treffer.forEach(function (t) {
317
+ var li = el('li');
318
+ var ordner = skriptOrdner(t.id);
319
+ if (ordner) { li.appendChild(el('span', 'ord', ordner + ' / ')); }
320
+ li.appendChild(el('span', 'id', t.name));
321
+ li.appendChild(document.createTextNode(
322
+ (t.an ? '' : ' (' + tr('move.scriptOff') + ')') + ' — ' +
323
+ (t.zeilen.length === 1
324
+ ? tr('move.scriptLines', t.zeilen[0])
325
+ : tr('move.scriptLinesN', t.zeilen.join(', ')))));
326
+ ul.appendChild(li);
327
+ });
328
+ return ul;
329
+ }
330
+
331
+ function zweiteilig(treffer, vorn, hinten) {
332
+ var ul = el('ul');
333
+ treffer.forEach(function (t) {
334
+ var li = el('li');
335
+ li.appendChild(el('span', 'id', vorn(t)));
336
+ li.appendChild(document.createTextNode(' — '));
337
+ li.appendChild(el('span', 'ord', hinten(t)));
338
+ ul.appendChild(li);
339
+ });
340
+ return ul;
341
+ }
342
+
343
+ /* Was zuletzt nachgesehen wurde. `zeichneErgebnis` baut die Ansicht bei
344
+ jeder Aenderung neu auf - ohne dieses Gedaechtnis waere das Ergebnis
345
+ nach dem ersten Hakenklick wieder weg und der Knopf stuende erneut da. */
346
+ var zuletzt = { id: null, daten: null };
347
+
348
+ function sammle(alias, quelle) {
349
+ return {
350
+ alias: {
351
+ id: alias,
352
+ skripte: trefferFuer(alias),
353
+ enums: aufzaehlungenMit(alias),
354
+ objekte: fundstellen(alias, 'obj'),
355
+ dateien: fundstellen(alias, 'datei'),
356
+ custom: customAn(alias)
357
+ },
358
+ quelle: quelle ? {
359
+ id: quelle,
360
+ skripte: trefferFuer(quelle),
361
+ enums: aufzaehlungenMit(quelle),
362
+ objekte: fundstellen(quelle, 'obj'),
363
+ dateien: fundstellen(quelle, 'datei'),
364
+ aliase: aliaseAuf(quelle, alias)
365
+ } : null
366
+ };
367
+ }
368
+
369
+ function kennungen(liste) {
370
+ var w = document.createElement('div');
371
+ liste.forEach(function (x, i) {
372
+ if (i) { w.appendChild(document.createTextNode(', ')); }
373
+ w.appendChild(el('span', 'id', x));
374
+ });
375
+ return w;
376
+ }
377
+
378
+ function zeile(wo, was, inhalt) {
379
+ var r = el('div', 'vwzeile');
380
+ r.appendChild(el('div', 'k', was));
381
+ r.appendChild(inhalt || el('div', 'leer', tr('use.none')));
382
+ wo.appendChild(r);
383
+ }
384
+
385
+ function nennungen(wo, teil, kopf) {
386
+ var k = el('div', 'vwkopf');
387
+ k.appendChild(el('span', 't', kopf));
388
+ k.appendChild(el('span', 'id', teil.id));
389
+ wo.appendChild(k);
390
+
391
+ zeile(wo, tr('use.scripts'), teil.skripte.length ? skriptListe(teil.skripte) : null);
392
+ zeile(wo, tr('use.files'), teil.dateien.length
393
+ ? zweiteilig(teil.dateien, function (t) { return t.ort; }, function (t) { return t.pfad; })
394
+ : null);
395
+ zeile(wo, tr('use.objects'), teil.objekte.length
396
+ ? zweiteilig(teil.objekte, function (t) { return t.name; }, function (t) { return t.id; })
397
+ : null);
398
+ zeile(wo, tr('use.enums'), teil.enums.length
399
+ ? el('div', null, teil.enums.map(function (x) { return x.name; }).join(', '))
400
+ : null);
401
+ if (teil.custom) {
402
+ zeile(wo, tr('use.custom'), teil.custom.length
403
+ ? el('div', null, teil.custom.map(function (x) {
404
+ return x.id.split('.').pop() + ' → ' + x.adapter;
405
+ }).join(', '))
406
+ : null);
407
+ }
408
+ if (teil.aliase) {
409
+ zeile(wo, tr('use.otherAliases'), teil.aliase.length ? kennungen(teil.aliase) : null);
410
+ }
411
+ }
412
+
413
+ export function baueVerwendungKarte(host, alias, quelle) {
414
+ if (!alias) { return; }
415
+
416
+ var k = el('div', 'card');
417
+ var kopf = el('div', 'ch');
418
+ kopf.appendChild(el('span', 'typ', tr('use.head')));
419
+ var knopf = el('button', 'btn');
420
+ knopf.style.fontSize = '11.5px';
421
+ kopf.appendChild(knopf);
422
+ k.appendChild(kopf);
423
+
424
+ var koerper = el('div');
425
+ k.appendChild(koerper);
426
+ host.appendChild(k);
427
+
428
+ var hinweis = function (text, warnen) {
429
+ var w = el('div', 'vwnote');
430
+ w.appendChild(el('div', warnen ? 'aside w' : 'aside', text));
431
+ koerper.appendChild(w);
432
+ };
433
+
434
+ var malen = function (daten) {
435
+ koerper.textContent = '';
436
+ nennungen(koerper, daten.alias, tr('use.aliasHead'));
437
+ if (daten.quelle) {
438
+ nennungen(koerper, daten.quelle, tr('use.sourceHead'));
439
+ hinweis(tr('use.sourceHint'), true);
440
+ }
441
+ hinweis(tr('use.limits'), false);
442
+ knopf.textContent = tr('use.again');
443
+ knopf.disabled = false;
444
+ };
445
+
446
+ var nachsehen = function () {
447
+ knopf.disabled = true;
448
+ knopf.textContent = tr('use.searching');
449
+ holeSkripte(function () {
450
+ holeFundorte(function () {
451
+ zuletzt = { id: alias, daten: sammle(alias, quelle) };
452
+ malen(zuletzt.daten);
453
+ });
454
+ });
455
+ };
456
+
457
+ /* Nur der zweite Druck liest neu ein.
458
+
459
+ Der Vorrat gilt fuer die ganze Sitzung und fuer jeden Alias - wer
460
+ nach dem Nachsehen zum naechsten Alias wechselt, soll die 15 MB
461
+ nicht noch einmal holen. Gemessen am Produktivsystem: der erste
462
+ Lauf 1,6 s, jeder weitere aus dem Vorrat. Erst „neu einlesen" am
463
+ schon gefuellten Kasten wirft ihn weg. */
464
+ knopf.addEventListener('click', function () {
465
+ if (zuletzt.id === alias && zuletzt.daten) {
466
+ skriptVorrat = null;
467
+ fundIndex = null;
468
+ }
469
+ nachsehen();
470
+ });
471
+
472
+ if (zuletzt.id === alias && zuletzt.daten) {
473
+ malen(zuletzt.daten);
474
+ } else {
475
+ knopf.textContent = tr('use.look');
476
+ hinweis(tr('use.lookHint'), false);
477
+ }
478
+ }
@@ -7,7 +7,7 @@
7
7
  import { S } from './zustand.js';
8
8
  import { D, $, el } from './basis.js';
9
9
  import { hatPunkt, jsonVon, feldWert, kindZustaende, feldFormel } from './werte.js';
10
- import { eigenerKanalName, merkeHakenVorgabe } from './entwurf.js';
10
+ import { eigenerKanalName, merkeHakenVorgabe, kennungtauglich } from './entwurf.js';
11
11
  import { tasmotaBefehle } from './mqtt.js';
12
12
  import { socket } from './verbindung.js';
13
13
 
@@ -271,7 +271,17 @@ export function setzeInstanz(text, v, n) {
271
271
  export function ausgangName(e, n) {
272
272
  if (e && e.kanal) {
273
273
  var eigen = eigenerKanalName(e.kanal + '.' + n);
274
- if (eigen) { return eigen.replace(/\./g, '_'); }
274
+ /* Der Kanalname wird hier zur KENNUNG - also durch dieselbe
275
+ Saeuberung wie ueberall sonst.
276
+
277
+ Vorher stand hier nur `replace(/\./g, '_')`. Leerzeichen und
278
+ Kommas blieben damit stehen, und der js-controller lehnt so etwas
279
+ nicht ab, er saeubert still: aus dem Kanalnamen „Licht, Bar" wurde
280
+ `alias.0.…Licht_ Bar`, waehrend die Werkbank „Licht, Bar" anzeigte
281
+ und in die Aufzaehlungen eintrug - eine Kennung, die es nie gab
282
+ (G31, gemessen 12.09.2026). Der Name selbst bleibt unberuehrt: er
283
+ steht als Beschriftung im Objekt. */
284
+ if (eigen) { return kennungtauglich(eigen) || eigen.replace(/\./g, '_'); }
275
285
  }
276
286
  if (!e || !e.kanalnameRoh) { return 'POWER' + n; }
277
287
  return String(e.kanalnameRoh).split(e.platzhalter || '%N%').join(n);
@@ -452,7 +462,8 @@ export function schreibZiel(z, kanal, auf) {
452
462
  einzige Zeile ein Fehlgriff. */
453
463
  export function wendeAn(v, kanal, gruende, instanz, auchOhneTreffer) {
454
464
  var auf = function (t) { return setzeInstanz(t, v, instanz); };
455
- var e = { kanal: kanal, states: [], want: v.geraetetyp, wantAuto: v.geraetetyp,
465
+ var e = { kanal: kanal, states: [], pflichtFehlt: null,
466
+ want: v.geraetetyp, wantAuto: v.geraetetyp,
456
467
  alleMuster: false, vorschlag: true, roh: false,
457
468
  vorlage: v.id, vorlageVersion: v.version, tplName: sprachtext(v.name),
458
469
  grund: (gruende || []).slice(), uebersprungen: [],
@@ -532,6 +543,7 @@ export function wendeAn(v, kanal, gruende, instanz, auchOhneTreffer) {
532
543
  var ziel0 = schreibZiel(z, kanal, auf);
533
544
  if (!ziel0) {
534
545
  e.uebersprungen.push(tr('tpl.skippedSource', z.name, auf(z.schreiben)));
546
+ if (!z.optional) { e.pflichtFehlt = z.name; }
535
547
  return;
536
548
  }
537
549
  e.states.push({
@@ -553,6 +565,18 @@ export function wendeAn(v, kanal, gruende, instanz, auchOhneTreffer) {
553
565
  e.uebersprungen.push(gescheitert.length > 1
554
566
  ? tr('tpl.skippedNowhere', z.name, gescheitert.join(tr('tpl.nor')))
555
567
  : tr('tpl.skippedSource', z.name, gescheitert[0] || auf(z.lesen)));
568
+ /* Eine Pflichtzeile, die ihre Quelle nicht findet, heisst: die
569
+ Vorlage passt zu diesem Geraet nicht. Die Erkennung sieht das
570
+ nicht — sie prueft, ob die genannten *Punkte* da sind, nicht ob
571
+ die Zeilen ihre *Felder darin* finden. Genau daran gewann eine
572
+ von Hand gebaute Stromzaehler-Vorlage an siebzehn Tasmotas und
573
+ lieferte dort eine einzige, obendrein abgewaehlte Zeile, waehrend
574
+ die Steckdosen-Vorlage elf gebaut haette (Ricardo, 14.09.2026).
575
+
576
+ Optionale Zeilen zaehlen nicht: sie duerfen fehlen, dafuer sind
577
+ sie optional. Gemessen am Testsystem betrifft die Regel keine
578
+ einzige der fuenfzehn uebrigen Vorlagen. */
579
+ if (!z.optional) { e.pflichtFehlt = z.name; }
556
580
  return;
557
581
  }
558
582
 
@@ -614,9 +638,34 @@ export function wendeAn(v, kanal, gruende, instanz, auchOhneTreffer) {
614
638
  });
615
639
  });
616
640
 
617
- return (e.states.length || auchOhneTreffer) ? e : null;
641
+ /* `auchOhneTreffer` gilt beim Von-Hand-Waehlen: wer eine Vorlage
642
+ ausdruecklich auswaehlt, bekommt sie auch dann, wenn eine
643
+ Pflichtzeile fehlt — samt der Liste in „uebersprungen". */
644
+ return ((e.states.length && !e.pflichtFehlt) || auchOhneTreffer) ? e : null;
618
645
  }
619
646
 
647
+ /* ---- Rueckfall: Anfang ---------------------------------------------
648
+
649
+ Die erste Vorlage nehmen, die wirklich etwas baut.
650
+
651
+ Bewusst ohne jede Abhaengigkeit: `anwenden` wird hereingereicht. So
652
+ laesst sich die Reihenfolge ohne Browser und ohne Objektdatenbank
653
+ pruefen, und test/vorpruefung.js schneidet diesen Block heraus.
654
+
655
+ `uebergangen` sammelt die Vorlagen, die vorne lagen und trotzdem
656
+ nichts ergaben — sie gehoeren ins „Warum", sonst wundert man sich,
657
+ warum die punktbeste nicht griff. */
658
+ export function ersteAnwendbare(treffer, anwenden) {
659
+ var uebergangen = [];
660
+ for (var i = 0; i < treffer.length; i++) {
661
+ var e = anwenden(treffer[i]);
662
+ if (e) { return { gewaehlt: treffer[i], e: e, uebergangen: uebergangen }; }
663
+ uebergangen.push(treffer[i]);
664
+ }
665
+ return { gewaehlt: null, e: null, uebergangen: uebergangen };
666
+ }
667
+ /* ---- Rueckfall: Ende ---------------------------------------------- */
668
+
620
669
  /* Beste Vorlage suchen und anwenden. */
621
670
  export function vorschlag(kanal, vorlagenId, instanz) {
622
671
  if (!S.VORLAGEN.length) { return null; }
@@ -624,20 +673,50 @@ export function vorschlag(kanal, vorlagenId, instanz) {
624
673
  var alle = S.VORLAGEN.map(function (v) { return pruefeVorlage(v, kanal, instanz); });
625
674
  var treffer = alle.filter(function (t) { return t.passt; });
626
675
 
627
- var gewaehlt = null;
676
+ var gewaehlt = null, e = null, uebergangen = [];
628
677
  if (vorlagenId) {
629
678
  /* Von Hand gewaehlt gilt auch, wenn die Erkennung nicht anspringt —
630
- die Verarbeitung laesst dann weg, wofuer die Quelle fehlt. */
679
+ die Verarbeitung laesst dann weg, wofuer die Quelle fehlt. Hier
680
+ gibt es auch keinen Rueckfall: wer eine Vorlage ausdruecklich
681
+ auswaehlt, meint genau diese und keine andere. */
631
682
  alle.forEach(function (t) { if (t.vorlage.id === vorlagenId) { gewaehlt = t; } });
683
+ if (gewaehlt) {
684
+ e = wendeAn(gewaehlt.vorlage, kanal, gewaehlt.gruende, gewaehlt.instanz, true);
685
+ }
632
686
  }
633
687
  if (!gewaehlt) {
634
688
  if (!treffer.length) { return null; }
635
689
  treffer.sort(function (a, b) { return b.punkte - a.punkte; });
636
- gewaehlt = treffer[0];
690
+
691
+ /* Die Erkennung und das Anwenden stellen zwei verschiedene Fragen.
692
+ `pruefeVorlage` prueft, ob die genannten *Punkte* da sind;
693
+ `wendeAn` braucht bestimmte *Felder darin*. Eine Vorlage kann die
694
+ Erkennung also gewinnen und danach keine einzige Zeile bauen —
695
+ `wendeAn` gibt dann `null` zurueck.
696
+
697
+ Vorher war an dieser Stelle Schluss: `treffer[0]` anwenden, und
698
+ wenn das nichts ergab, gar keine Vorlage. Am Geraet stand dann
699
+ „Keine Vorlage erkannt", waehrend die Auswahlliste daneben
700
+ „Tasmota-Steckdose v6 passt" meldete — zwei Urteile ueber zwei
701
+ verschiedene Fragen, nebeneinander in derselben Ansicht
702
+ (gefunden 14.09.2026 am Testsystem, C3).
703
+
704
+ Jetzt kommt die naechstbeste an die Reihe. Was uebergangen wurde,
705
+ steht danach im „Warum" — sonst wundert man sich, warum
706
+ ausgerechnet die eigene Vorlage nicht griff. */
707
+ var reihe = ersteAnwendbare(treffer, function (t) {
708
+ return wendeAn(t.vorlage, kanal, t.gruende, t.instanz, false);
709
+ });
710
+ if (!reihe.gewaehlt) { return null; }
711
+ gewaehlt = reihe.gewaehlt;
712
+ e = reihe.e;
713
+ uebergangen = reihe.uebergangen;
637
714
  }
638
715
 
639
- var e = wendeAn(gewaehlt.vorlage, kanal, gewaehlt.gruende, gewaehlt.instanz, !!vorlagenId);
640
716
  if (!e) { return null; }
717
+ uebergangen.forEach(function (t) {
718
+ e.grund.push(tr('tpl.skippedEmpty', sprachtext(t.vorlage.name)));
719
+ });
641
720
  if (!vorlagenId && gewaehlt.passt) {
642
721
  var gleichAuf = treffer.filter(function (t) { return t.punkte === gewaehlt.punkte; });
643
722
  var ohneHinweis = !gewaehlt.namenstreffer;