iobroker.alias-workbench 0.8.3 → 0.9.7

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.
@@ -12,7 +12,7 @@ import { tasmotaBefehle } from './mqtt.js';
12
12
  import { socket } from './verbindung.js';
13
13
 
14
14
  import { tr, sprachtext } from './sprache.js';
15
- import { musterVon } from './erkennung.js';
15
+ import { musterVon, rolleTrifft } from './erkennung.js';
16
16
 
17
17
  /* Welche Instanz — der Reiter wird mit ?<instanz>&noFooter geladen. */
18
18
  var INSTANZ = (function () {
@@ -29,6 +29,24 @@ export function zeigeObjektzahl() {
29
29
  if (z) { z.textContent = tr('app.objectsCount', S.keysSorted.length); }
30
30
  }
31
31
 
32
+ /* Wie viele Wertabos laufen gerade? Steht neben der Objektzahl.
33
+
34
+ Nicht aus Neugier: die Abos werden beim Wechsel des Knotens abgemeldet
35
+ und neu gesetzt, und ob dabei wirklich alle wieder verschwinden, sieht
36
+ man sonst nirgends. Bleibt die Zahl beim Durchklicken stabil, stimmt
37
+ es; waechst sie, sammelt sich etwas an (Ricardo, 08.09.2026). */
38
+ export function zeigeAbozahl() {
39
+ var z = $('#meta-abos');
40
+ if (!z) { return; }
41
+ var n = (S.abo || []).length;
42
+ z.textContent = ' \u00b7 ' + (n === 1 ? tr('app.subsCount1') : tr('app.subsCount', n));
43
+ /* Die Muster gehoeren dazu, sonst ist die Zahl ein Raetsel: „2 Abos"
44
+ bei zwanzig sichtbaren Werten liest sich falsch, bis man sieht, dass
45
+ ein Muster einen ganzen Zweig deckt (Ricardo, 08.09.2026). */
46
+ z.title = tr('app.subsHint') + String.fromCharCode(10) +
47
+ (S.abo || []).map(function (mu) { return ' ' + mu; }).join(String.fromCharCode(10));
48
+ }
49
+
32
50
  export function setzeMeta() {
33
51
  var m = $('#meta');
34
52
  if (!m) { return; }
@@ -41,6 +59,8 @@ export function setzeMeta() {
41
59
  steht sie neben ihresgleichen; ein eigener Span, damit das Abo sie
42
60
  nachfuehren kann, ohne die ganze Zeile neu zu bauen. */
43
61
  m.appendChild(el('span', null, tr('app.objectsCount', S.keysSorted.length))).id = 'meta-objekte';
62
+ m.appendChild(el('span', null, '')).id = 'meta-abos';
63
+ zeigeAbozahl();
44
64
  if (S.vorlagenUnvollstaendig) {
45
65
  var w = el('span', 'chip bad', tr('tpls.loadFailed'));
46
66
  w.style.marginLeft = '8px';
@@ -92,6 +112,15 @@ export function ladeVorlagen(fertig) {
92
112
  sie behauptet dann nicht, es gaebe keine eigenen Vorlagen. */
93
113
  setTimeout(function () { schluss([], true); }, 6000);
94
114
  socket.emit('getObject', INSTANZ_ID, function (err, o) {
115
+ /* Denselben Weg wie der Timeout eine Zeile darueber: ein Fehler
116
+ heisst nicht „keine eigenen Vorlagen".
117
+
118
+ `err` wurde vorher nicht einmal angesehen. Zwei Folgen: in der
119
+ Kopfzeile fehlte der Warnchip, und `freierSchluessel` hielt
120
+ vergebene Kennungen fuer frei — beim naechsten Speichern haette
121
+ `aendereVorlagen` eine vorhandene Vorlage mit Version 1
122
+ ersetzt. */
123
+ if (err) { return schluss([], true); }
95
124
  schluss((o && o.native && o.native.vorlagen) || []);
96
125
  });
97
126
  }
@@ -275,6 +304,47 @@ function punktErfuellt(kanal, pfad) {
275
304
  }
276
305
 
277
306
  /* Passt die Vorlage auf diesen Kanal? Liefert null oder eine Bewertung. */
307
+ /* Der Namenshinweis als Ausdruck — und der wirft nicht.
308
+
309
+ Der Text kommt frei aus dem Vorlagenblatt und aus dem
310
+ Speichern-Dialog; keins der beiden Felder prueft ihn. Ein `Lampe (Bad`
311
+ liess `pruefeVorlage` bei JEDER Geraeteauswahl werfen, kein Aufrufer
312
+ fing das, und die rechte Seite blieb leer — zu reparieren nur ueber
313
+ die Instanzkonfiguration von Hand (gemessen 09.09.2026). `rolleTrifft`
314
+ in `erkennung.js` faengt denselben Fall laengst ab; hier fehlte es.
315
+
316
+ Ein unbrauchbarer Ausdruck heisst „trifft nicht": der Hinweis ist nur
317
+ ein Bonus in der Bewertung, keine Bedingung. Die Vorlage bleibt damit
318
+ brauchbar, statt das ganze Werkzeug mitzureissen. */
319
+ export function hinweisTrifft(hinweis, name) {
320
+ if (!hinweisTaugt(hinweis)) { return false; }
321
+ try { return new RegExp(hinweis, 'i').test(name); }
322
+ catch { return false; }
323
+ }
324
+
325
+ /* Verschachtelte Wiederholungen — `(a+)+`, `(a*)*`, `(a+){2,}`.
326
+
327
+ Die klassische Form, an der eine RegExp-Maschine in die Knie geht:
328
+ `(a+)+$` gegen neunundzwanzig "a" brauchte hier gemessene 3196 ms,
329
+ und der ganze Reiter stand solange (09.09.2026). Ein try/catch faengt
330
+ das nicht — die Syntax ist ja in Ordnung, es dauert nur ewig.
331
+
332
+ Das ist eine Heuristik, kein Beweis: Sie deckt die Form ab, die man
333
+ sich versehentlich baut, nicht jeden denkbaren Fall. Fuer einen
334
+ Namenshinweis reicht das — er soll ein Wort im Geraetenamen finden,
335
+ nicht eine Sprache parsen. */
336
+ function verschachtelt(hinweis) {
337
+ return /\([^()]*[+*][^()]*\)\s*([+*]|\{\s*[0-9]+\s*,)/.test(String(hinweis));
338
+ }
339
+
340
+ /* Taugt der Text als Ausdruck? Fuer die Felder, die ihn aufnehmen. */
341
+ export function hinweisTaugt(hinweis) {
342
+ if (!hinweis) { return true; }
343
+ if (verschachtelt(hinweis)) { return false; }
344
+ try { new RegExp(hinweis, 'i'); return true; }
345
+ catch { return false; }
346
+ }
347
+
278
348
  export function pruefeVorlage(v, kanal, instanz) {
279
349
  var erk = v.erkennung || {};
280
350
  var gruende = [];
@@ -347,7 +417,7 @@ export function pruefeVorlage(v, kanal, instanz) {
347
417
  var belege = (erk.erforderlich || []).length *
348
418
  (v.mehrfach ? Math.max(1, ausgaenge.length) : 1) +
349
419
  Object.keys(erk.inhalt || {}).length;
350
- var namenstreffer = !!(erk.namenshinweis && new RegExp(erk.namenshinweis, 'i').test(name));
420
+ var namenstreffer = !!(erk.namenshinweis && hinweisTrifft(erk.namenshinweis, name));
351
421
  if (namenstreffer) { gruende.push(tr('tpl.nameHint', sprachtext(v.name))); }
352
422
  var eigen = v._quelle === 'benutzer';
353
423
  var punkte = belege * 1000000 +
@@ -449,6 +519,34 @@ export function wendeAn(v, kanal, gruende, instanz, auchOhneTreffer) {
449
519
  break;
450
520
  }
451
521
 
522
+ /* Ein Tastendruck hat keinen Zustand zum Lesen.
523
+
524
+ `STOP` am Rollladen traegt nur `schreiben` — genau so steht es in
525
+ der Vorlage, mit Begruendung. Diesen Fall kannte das Anwenden
526
+ nicht: Ohne `lesen` fiel der Punkt mit „STOP: undefined fehlt"
527
+ heraus, und KEIN EINZIGER Rollladen-Alias bekam je einen STOP
528
+ (gefunden 09.09.2026). Der Platz im Muster verlangt auch nichts
529
+ weiter als `write: true`. */
530
+ var nurSchreiben = !z.lesen && !!z.schreiben;
531
+ if (nurSchreiben && !quelle) {
532
+ var ziel0 = schreibZiel(z, kanal, auf);
533
+ if (!ziel0) {
534
+ e.uebersprungen.push(tr('tpl.skippedSource', z.name, auf(z.schreiben)));
535
+ return;
536
+ }
537
+ e.states.push({
538
+ n: z.name, on: !z.vorgabeAus, role: z.rolle || '', typ: z.typ || '',
539
+ unit: z.einheit || '', wr: true,
540
+ srcR: '', srcW: ziel0,
541
+ f: '', fw: z.schreibformel ? auf(z.schreibformel) : '',
542
+ caption: z.beschriftung || '',
543
+ dec: z.nachkommastellen === undefined ? undefined : String(z.nachkommastellen),
544
+ states: z.werteliste || undefined,
545
+ manuell: false, ausVorlage: z.name, hinweis: z.hinweis || '', wege: []
546
+ });
547
+ return;
548
+ }
549
+
452
550
  if (!quelle) {
453
551
  /* Beide Wege nennen, nicht nur den letzten — sonst sucht man an der
454
552
  einen Stelle und die Vorlage hatte an zwei geschaut. */
@@ -461,7 +559,15 @@ export function wendeAn(v, kanal, gruende, instanz, auchOhneTreffer) {
461
559
  /* Auch in der Formel steht der Platzhalter — ohne Ersetzung suchte
462
560
  der Alias nach einem Feld namens POWER%N%. */
463
561
  var formel = leseformel ? auf(leseformel) : '';
464
- if (feld) {
562
+ /* Wer eine Formel hinschreibt, bekommt sie auch.
563
+
564
+ `tasmota-rgb` fuehrt an FADE beides: `feld` UND `leseformel`. Die
565
+ Formel wurde verworfen und aus dem Feld eine neue gebaut — bei
566
+ FADE faellt es nicht auf, weil der Zieltyp die Umwandlung
567
+ nachholt, an der naechsten Zeile mit eigener Rechnung aber schon
568
+ (gefunden 09.09.2026). Das Feld bleibt der bequeme Weg; die
569
+ ausdrueckliche Angabe schlaegt ihn. */
570
+ if (feld && !formel) {
465
571
  feld = auf(feld);
466
572
  /* Mit Fragezeichen-Zugriff: `tele.SENSOR` ist ein Sammeltopf, und
467
573
  nicht jede Nachricht darin fuehrt jedes Feld. Ohne Schutz wirft
@@ -543,8 +649,10 @@ export function vorschlag(kanal, vorlagenId, instanz) {
543
649
  e.erzwungen = true;
544
650
  e.grund = e.grund.concat(gewaehlt.fehler.map(function (f) { return tr('tpl.wouldNeed', f); }));
545
651
  }
546
- e.andere = treffer.filter(function (t) { return t.vorlage.id !== gewaehlt.vorlage.id; })
547
- .map(function (t) { return t.vorlage; });
652
+ /* Hier stand `e.andere` — die Liste der unterlegenen Vorlagen. Sie
653
+ wurde bei jedem Vorschlag gebaut und von niemandem gelesen
654
+ (gefunden 09.09.2026). Was der Nutzer wirklich braucht, steht im
655
+ Musterwahl-Feld und im Vorlagenblatt. */
548
656
  if (vorlagenId) { e.vonHandGewaehlt = true; }
549
657
  return merkeHakenVorgabe(e);
550
658
  }
@@ -559,7 +667,7 @@ export function anMusterAnpassen(e, neuTyp) {
559
667
  var s = null;
560
668
  muster.states.forEach(function (st) {
561
669
  if (s || !st.role) { return; }
562
- try { if (new RegExp(st.role.source || st.role).test(rolle)) { s = st; } } catch { /* dito: eine kaputte Rolle macht die Vorlage nicht passend */ }
670
+ if (rolleTrifft(st.role, rolle)) { s = st; } /* eine kaputte Rolle macht die Vorlage nicht passend */
563
671
  });
564
672
  return s;
565
673
  }
@@ -9,9 +9,9 @@ import { D, $, el, kurz } from './basis.js';
9
9
  import { tr, sprachtext, sprache } from './sprache.js';
10
10
  import { katalogFehlt } from './katalog.js';
11
11
  import { funktionsAuswahl, kennungZuFunktion } from './aufzaehlungen.js';
12
- import { musterVon } from './erkennung.js';
12
+ import { musterVon, rolleTrifft } from './erkennung.js';
13
13
  import { musterZeile, musterName } from './musternamen.js';
14
- import { ladeVorlagen, aendereVorlagen, setzeMeta, pruefeVorlage, INSTANZ_ID } from './vorlagen.js';
14
+ import { ladeVorlagen, aendereVorlagen, setzeMeta, pruefeVorlage, hinweisTaugt, INSTANZ_ID } from './vorlagen.js';
15
15
  import { kindZustaende, hatPunkt, feldAusFormel } from './werte.js';
16
16
  import { opt } from './entwurf.js';
17
17
  import { zeichneErgebnis } from './ergebnis.js';
@@ -326,6 +326,15 @@ export function zeichneVorlagenBlatt(host) {
326
326
  nb.style.padding = '0 12px 11px';
327
327
  nb.appendChild(feld(tr('tpls.nameHint'), (v.erkennung || {}).namenshinweis || '',
328
328
  function (x, w) { x.erkennung = x.erkennung || {}; if (w) { x.erkennung.namenshinweis = w; } else { delete x.erkennung.namenshinweis; } }, '260px'));
329
+ /* Der Text ist ein Ausdruck, und ein unbrauchbarer Ausdruck faellt
330
+ sonst nirgends auf: Er wird still zu „trifft nicht", und die Vorlage
331
+ verliert leise ihren Namensbonus. Also hier sagen, wo er steht. */
332
+ if (!hinweisTaugt((v.erkennung || {}).namenshinweis || '')) {
333
+ var nbw = el('div', 'hint');
334
+ nbw.style.color = 'var(--bad)';
335
+ nbw.textContent = tr('tpls.nameHintBad');
336
+ nb.appendChild(nbw);
337
+ }
329
338
  nb.appendChild(el('div', 'hint', tr('tv.nameHintShort')));
330
339
  kk.appendChild(nb);
331
340
  host.appendChild(kk);
@@ -551,8 +560,7 @@ function rollenBlock(rolle, geraetetyp, setzen) {
551
560
  if (must) {
552
561
  var treffer = null;
553
562
  must.states.forEach(function (pp) {
554
- if (!treffer && pp.role && rolle &&
555
- new RegExp(pp.role.source ? pp.role.source : pp.role).test(rolle)) { treffer = pp; }
563
+ if (!treffer && pp.role && rolleTrifft(pp.role, rolle)) { treffer = pp; }
556
564
  });
557
565
  var hin = el('div', 'sugg');
558
566
  if (treffer) {
@@ -910,12 +918,61 @@ export function vEinlesenDatei(datei) {
910
918
  leser.readAsText(datei);
911
919
  }
912
920
 
921
+ /* Was eingelesen wird, wird gespeichert — und danach an jedem Geraet
922
+ ausgewertet. Geprueft wurden bisher nur `id` und `zustaende`.
923
+
924
+ Zwei Wege in den Totalschaden: `erkennung.erforderlich` als Text
925
+ statt als Liste laesst `pruefeVorlage` an JEDEM Geraet werfen
926
+ („map is not a function", gemessen 09.09.2026), ein fehlendes `name`
927
+ wirft schon beim Oeffnen im Blatt. Die Knopfleiste „Einlesen und
928
+ speichern" steht zu diesem Zeitpunkt laengst im DOM — man konnte
929
+ also eine Vorlage speichern, die den Reiter danach unbrauchbar
930
+ machte.
931
+
932
+ Listenfelder werden nicht abgewiesen, wenn ein einzelner Text
933
+ gemeint sein kann: `erforderlich: "tele.STATE"` ist eindeutig und
934
+ wird zur Liste gemacht. Abgewiesen wird nur, was keinen Sinn ergibt. */
913
935
  function vPruefeEingelesen(o) {
914
936
  if (!o || typeof o !== 'object' || Array.isArray(o)) { return tr('tv.badNotObject'); }
915
937
  if (!o.id || typeof o.id !== 'string') { return tr('tv.badNoId'); }
916
938
  if (!Array.isArray(o.zustaende) || !o.zustaende.length) { return tr('tv.badNoStates'); }
917
939
  var schlecht = o.zustaende.filter(function (z) { return !z || !z.name || !z.lesen; });
918
940
  if (schlecht.length) { return tr('tv.badState', schlecht.length); }
941
+
942
+ if (o.name !== undefined &&
943
+ !(typeof o.name === 'string' ||
944
+ (o.name && typeof o.name === 'object' && !Array.isArray(o.name)))) {
945
+ return tr('tv.badName');
946
+ }
947
+ if (o.erkennung !== undefined &&
948
+ (!o.erkennung || typeof o.erkennung !== 'object' || Array.isArray(o.erkennung))) {
949
+ return tr('tv.badRecog');
950
+ }
951
+ var erk = o.erkennung || {};
952
+ var listen = ['erforderlich', 'verboten'];
953
+ for (var i = 0; i < listen.length; i++) {
954
+ var w = erk[listen[i]];
955
+ if (w === undefined) { continue; }
956
+ if (typeof w === 'string') { erk[listen[i]] = [w]; continue; }
957
+ if (!Array.isArray(w) || w.some(function (x) { return typeof x !== 'string'; })) {
958
+ return tr('tv.badList', listen[i]);
959
+ }
960
+ }
961
+ if (erk.inhalt !== undefined &&
962
+ (!erk.inhalt || typeof erk.inhalt !== 'object' || Array.isArray(erk.inhalt))) {
963
+ return tr('tv.badContent');
964
+ }
965
+ if (erk.namenshinweis !== undefined &&
966
+ (typeof erk.namenshinweis !== 'string' || !hinweisTaugt(erk.namenshinweis))) {
967
+ return tr('tv.badNameHint');
968
+ }
969
+ /* Ein Musternamen, den dieser Detector nicht kennt, wirft spaeter in
970
+ der Musterwahl — und der Entwurf wird dann gar nicht erst
971
+ gezeichnet. Lieber hier abweisen, wo noch jemand zusieht. */
972
+ if (o.geraetetyp !== undefined &&
973
+ (typeof o.geraetetyp !== 'string' || (o.geraetetyp && !musterVon(o.geraetetyp)))) {
974
+ return tr('tv.badType', String(o.geraetetyp));
975
+ }
919
976
  return null;
920
977
  }
921
978
 
@@ -999,35 +1056,96 @@ function relZu(id, kanal) {
999
1056
  /* stat.POWER1 → stat.POWER%N%. Ersetzt wird nur am Ende eines
1000
1057
  Abschnitts und nur, wenn davor noch etwas steht — sonst wuerde aus
1001
1058
  ENERGY.Power beim Speichern Unsinn. */
1002
- function zuPlatzhalter(pfad, inst) {
1059
+ function zuPlatzhalter(pfad, inst, vorspaenne) {
1003
1060
  var i = (inst === null || inst === undefined) ? '' : String(inst);
1004
1061
  if (!i) { return pfad; }
1005
1062
  return String(pfad).split('.').map(function (t) {
1006
- return (t.length > i.length && t.slice(-i.length) === i) ? t.slice(0, -i.length) + '%N%' : t;
1063
+ if (!(t.length > i.length && t.slice(-i.length) === i)) { return t; }
1064
+ /* Nicht jede Endziffer ist eine Ausgangsnummer.
1065
+
1066
+ Ersetzt wurde rein nach Endziffer: an einem Mehrfachgeraet mit
1067
+ Ausgang 2 wurde damit auch die IP-Zeile aus `tele.INFO2` zu
1068
+ `tele.INFO%N%` (gemessen 09.09.2026). Der Schaden zeigt sich erst
1069
+ am naechsten Geraet — und an Ausgang 1 meldete `andereAusgaenge`
1070
+ dieselbe Zeile als fremden Ausgang, „Andere weglassen" warf sie
1071
+ hinaus. `INFO2` heisst bei Tasmota die zweite Info-Seite, an
1072
+ jedem Ausgang gleich. */
1073
+ return (vorspaenne.indexOf(t.slice(0, -i.length)) === -1)
1074
+ ? t : t.slice(0, -i.length) + '%N%';
1007
1075
  }).join('.');
1008
1076
  }
1009
1077
 
1078
+ /* Welche Vorspaenne tragen wirklich die Ausgangsnummer?
1079
+
1080
+ Drei Quellen, in dieser Reihenfolge belastbar: was die Vorlage im
1081
+ Original schon mit `%N%` fuehrte, der Vorspann aus `kanalname`
1082
+ (`POWER%N%` → `POWER`), und was mit derselben Nummer mehrfach
1083
+ vorkommt — `cmnd.POWER2` und `stat.POWER2` sagen gemeinsam mehr als
1084
+ ein einzelnes `tele.INFO2`. */
1085
+ function ausgangsVorspaenne(zeilen, k) {
1086
+ var i = (k.instanz === null || k.instanz === undefined) ? '' : String(k.instanz);
1087
+ var raus = [];
1088
+ var dazu = function (v) { if (v && raus.indexOf(v) === -1) { raus.push(v); } };
1089
+
1090
+ var kn = String(k.kanalname || 'POWER%N%');
1091
+ if (kn.indexOf('%N%') !== -1) { dazu(kn.split('%N%')[0]); }
1092
+
1093
+ ((k.quelle && k.quelle.zustaende) || []).forEach(function (z) {
1094
+ [z.lesen, z.schreiben].forEach(function (pf) {
1095
+ String(pf || '').split('.').forEach(function (t) {
1096
+ var n = t.indexOf('%N%');
1097
+ if (n > 0) { dazu(t.slice(0, n)); }
1098
+ });
1099
+ });
1100
+ });
1101
+
1102
+ if (i) {
1103
+ var wie = {};
1104
+ (zeilen || []).forEach(function (r) {
1105
+ if (r.weg) { return; }
1106
+ [r.lesenAbs ? '' : r.lesenRoh, r.schreibenAbs ? '' : r.schreibenRoh].forEach(function (pf) {
1107
+ String(pf || '').split('.').forEach(function (t) {
1108
+ if (t.length > i.length && t.slice(-i.length) === i) {
1109
+ var v = t.slice(0, -i.length);
1110
+ wie[v] = (wie[v] || 0) + 1;
1111
+ }
1112
+ });
1113
+ });
1114
+ });
1115
+ Object.keys(wie).forEach(function (v) { if (wie[v] > 1) { dazu(v); } });
1116
+ }
1117
+ return raus;
1118
+ }
1119
+
1010
1120
  /* Welcher Pfad steht am Ende in der Vorlage? Bei einem Mehrfachgeraet
1011
1121
  tritt der Platzhalter an die Stelle der Ausgangsnummer. */
1012
- function zPfad(r, k, schreibend) {
1122
+ function zPfad(r, k, schreibend, zeilen) {
1013
1123
  var roh = schreibend ? r.schreibenRoh : r.lesenRoh;
1014
1124
  var abs = schreibend ? r.schreibenAbs : r.lesenAbs;
1015
1125
  if (!roh) { return ''; }
1016
1126
  if (!k.mehrfach || abs || !k.instanz) { return roh; }
1017
- return zuPlatzhalter(roh, k.instanz);
1127
+ return zuPlatzhalter(roh, k.instanz,
1128
+ ausgangsVorspaenne(zeilen || (S.tplZ && S.tplZ.zeilen) || [], k));
1018
1129
  }
1019
1130
 
1020
1131
  /* Endet die Nummer eines Ausgangs in den Pfaden? Haeufigste Zahl am
1021
1132
  Ende eines Abschnitts gewinnt — cmnd.POWER1, stat.POWER1 sagen 1. */
1022
- function rateInstanz(zeilen, geraetName) {
1023
- var zaehler = {};
1133
+ function rateInstanz(zeilen, geraetName, vorspann) {
1134
+ var zaehler = {}, nurVorspann = {};
1024
1135
  zeilen.forEach(function (r) {
1025
1136
  if (r.lesenAbs) { return; }
1026
1137
  String(r.lesenRoh).split('.').forEach(function (t) {
1027
1138
  var m = /^(.*[^0-9])([0-9]{1,2})$/.exec(t);
1028
- if (m) { zaehler[m[2]] = (zaehler[m[2]] || 0) + 1; }
1139
+ if (!m) { return; }
1140
+ zaehler[m[2]] = (zaehler[m[2]] || 0) + 1;
1141
+ if (vorspann && m[1] === vorspann) { nurVorspann[m[2]] = (nurVorspann[m[2]] || 0) + 1; }
1029
1142
  });
1030
1143
  });
1144
+ /* Zaehlt der Vorspann des Ausgangs mit (`POWER%N%` → `POWER`), gilt
1145
+ allein er: sonst hob ein `tele.INFO2` die Nummer eines Geraets an,
1146
+ das gar keinen zweiten Ausgang hat. Findet sich damit nichts,
1147
+ bleibt es beim alten Weg — besser eine geratene Nummer als keine. */
1148
+ if (Object.keys(nurVorspann).length) { zaehler = nurVorspann; }
1031
1149
  var mn = /^(.*[^0-9])([0-9]{1,2})$/.exec(geraetName || '');
1032
1150
  if (mn) { zaehler[mn[2]] = (zaehler[mn[2]] || 0) + 1; }
1033
1151
  var beste = '', wie = 0;
@@ -1043,15 +1161,19 @@ function rateInstanz(zeilen, geraetName) {
1043
1161
  entstehen daraus. Zeigt der Entwurf noch auf POWER2, POWER3 …, waere
1044
1162
  die Vorlage an dieses eine Geraet gefesselt. Also einsammeln und
1045
1163
  sagen, statt still etwas Falsches zu speichern. */
1046
- function andereAusgaenge(zeilen, instanz) {
1164
+ function andereAusgaenge(zeilen, instanz, vorspaenne) {
1047
1165
  var raus = { punkte: [], nummern: [] };
1048
1166
  if (!instanz) { return raus; }
1167
+ var vs = vorspaenne || [];
1049
1168
  zeilen.forEach(function (r) {
1050
1169
  if (r.weg || r.lesenAbs) { return; }
1051
1170
  var fremd = null;
1052
1171
  String(r.lesenRoh).split('.').forEach(function (t) {
1053
1172
  var m = /^(.*[^0-9])([0-9]{1,2})$/.exec(t);
1054
- if (m && m[2] !== String(instanz)) { fremd = m[2]; }
1173
+ /* Dieselbe Regel wie beim Platzhalter: nur was den Ausgang traegt.
1174
+ Sonst stand `tele.INFO2` an Ausgang 1 als fremder Ausgang da,
1175
+ und „Andere weglassen" warf die IP-Zeile hinaus. */
1176
+ if (m && m[2] !== String(instanz) && vs.indexOf(m[1]) !== -1) { fremd = m[2]; }
1055
1177
  });
1056
1178
  if (fremd) {
1057
1179
  raus.punkte.push(r.name);
@@ -1168,7 +1290,8 @@ export function vorlagenVorbereiten(e) {
1168
1290
  maxRang: maxRang,
1169
1291
  mehrfach: mehrfach,
1170
1292
  instanz: mehrfach ? String(inst === null || inst === undefined ? '' : inst)
1171
- : rateInstanz(zeilen, geraetName),
1293
+ : rateInstanz(zeilen, geraetName,
1294
+ String((quelle && quelle.kanalname) || 'POWER%N%').split('%N%')[0]),
1172
1295
  kanalname: (quelle && quelle.kanalname) ? quelle.kanalname : 'POWER%N%',
1173
1296
  verboten: quelle ? (((quelle.erkennung || {}).verboten) || []).join(', ') : '',
1174
1297
  geraetName: geraetName
@@ -1192,8 +1315,8 @@ export function baueVorlage(z) {
1192
1315
  var erf = [];
1193
1316
  z.zeilen.forEach(function (r) {
1194
1317
  if (r.weg || !r.pflicht || r.lesenAbs) { return; }
1195
- var lp = zPfad(r, k, false);
1196
- var sp = zPfad(r, k, true);
1318
+ var lp = zPfad(r, k, false, z.zeilen);
1319
+ var sp = zPfad(r, k, true, z.zeilen);
1197
1320
  if (erf.indexOf(lp) === -1) { erf.push(lp); }
1198
1321
  if (sp && !r.schreibenAbs && erf.indexOf(sp) === -1) { erf.push(sp); }
1199
1322
  });
@@ -1226,10 +1349,10 @@ export function baueVorlage(z) {
1226
1349
  rolle: s.role || undefined,
1227
1350
  typ: s.typ || undefined,
1228
1351
  einheit: s.unit || undefined,
1229
- lesen: zPfad(r, k, false)
1352
+ lesen: zPfad(r, k, false, z.zeilen)
1230
1353
  };
1231
1354
  if (r.lesenAbs) { zu.absolut = true; }
1232
- if (r.schreibenRoh) { zu.schreiben = zPfad(r, k, true); }
1355
+ if (r.schreibenRoh) { zu.schreiben = zPfad(r, k, true, z.zeilen); }
1233
1356
  if (f) { zu.feld = f; } else if (s.f) { zu.leseformel = s.f; }
1234
1357
  if (s.fw && r.schreibenRoh) { zu.schreibformel = s.fw; }
1235
1358
  if (!r.pflicht) { zu.optional = true; }
@@ -1245,7 +1368,15 @@ export function baueVorlage(z) {
1245
1368
  .map(function (w) {
1246
1369
  var o = { punkt: w.punkt };
1247
1370
  if (w.feld) { o.feld = w.feld; }
1248
- if (w.formel) { o.formel = w.formel; }
1371
+ /* `leseformel`, nicht `formel`. `wendeAn` legt die Wege mit
1372
+ dem Schluessel `formel` ab, jeder Leser sucht in
1373
+ `lesenSonst` aber `leseformel` (vorlagen.js:462,
1374
+ vorlagenblatt.js:720). Die Formel jeder Ersatzquelle ging
1375
+ beim Aktualisieren still verloren: bei tasmota-steckdose
1376
+ landete der boolesche Alias an Geraeten ohne `stat.POWER`
1377
+ danach auf dem rohen JSON-Text von `tele.STATE` statt auf
1378
+ `POWER` (gemessen 09.09.2026). */
1379
+ if (w.formel) { o.leseformel = w.formel; }
1249
1380
  return o;
1250
1381
  });
1251
1382
  if (rest.length) { zu.lesenSonst = (rest.length === 1) ? rest[0] : rest; }
@@ -1451,6 +1582,24 @@ export function zeigeVorlagenDialog() {
1451
1582
  $('#dlg-tpl').showModal();
1452
1583
  }
1453
1584
 
1585
+ /* Zeigt die Kennung auf eine fremde Vorlage?
1586
+
1587
+ `freierSchluessel` laeuft beim Radio-Wechsel und beim Vorbereiten,
1588
+ nicht nach der Handeingabe im Kennungsfeld. `aendereVorlagen` ersetzt
1589
+ bei gleicher Kennung ohne Rueckfrage (`liste[i] = vorlage`), im Modus
1590
+ „neu" sogar mit Version 1: Wer die Kennung einer anderen eigenen
1591
+ Vorlage eintippte, ueberschrieb sie still — gemessen 09.09.2026, aus
1592
+ „I30 Opfer" v7 mit einem Zustand wurde „Tasmota-Lampe" v1 mit
1593
+ vierzehn. Die eigene Quelle ist ausgenommen: sie zu ersetzen ist ja
1594
+ gerade der Sinn von „aktualisieren". */
1595
+ function fremdeMitId(k) {
1596
+ if (!k || !k.id) { return null; }
1597
+ var t = vorlageMitId(k.id);
1598
+ if (!t) { return null; }
1599
+ if (k.modus === 'update' && k.quelle && k.quelle.id === k.id) { return null; }
1600
+ return t;
1601
+ }
1602
+
1454
1603
  function tplFeld(label, wert, aendern, breit) {
1455
1604
  var l = el('label', 'feld');
1456
1605
  l.appendChild(el('span', 'feldlabel', label));
@@ -1532,7 +1681,16 @@ export function zeichneVorlagenDialog() {
1532
1681
 
1533
1682
  kb.appendChild(tplFeld(tr('tpls.nameDe'), k.nameDe, function (x) { k.nameDe = x; }, '150px'));
1534
1683
  kb.appendChild(tplFeld(tr('tpls.nameEn'), k.nameEn, function (x) { k.nameEn = x; }, '150px'));
1535
- kb.appendChild(tplFeld(tr('tpls.id'), k.id, function (x) { k.id = schluesselAus(x); }, '150px'));
1684
+ var kf = tplFeld(tr('tpls.id'), k.id, function (x) { k.id = schluesselAus(x); }, '150px');
1685
+ var stoert = fremdeMitId(k);
1686
+ if (stoert) {
1687
+ var kw = el('span', 'hint');
1688
+ kw.style.color = 'var(--bad)';
1689
+ kw.style.flexBasis = '100%';
1690
+ kw.textContent = tr('tpls.idTaken', textIn(stoert.name, sprache), stoert.version || 1);
1691
+ kf.appendChild(kw);
1692
+ }
1693
+ kb.appendChild(kf);
1536
1694
 
1537
1695
  var lt = el('label', 'feld');
1538
1696
  lt.appendChild(el('span', 'feldlabel', tr('tpls.deviceType')));
@@ -1578,6 +1736,13 @@ export function zeichneVorlagenDialog() {
1578
1736
  var nh = el('div');
1579
1737
  nh.style.padding = '0 12px 11px';
1580
1738
  var nf = tplFeld(tr('tpls.nameHint'), k.namenshinweis, function (x) { k.namenshinweis = x; }, '260px');
1739
+ if (!hinweisTaugt(k.namenshinweis)) {
1740
+ var nw = el('span', 'hint');
1741
+ nw.style.color = 'var(--bad)';
1742
+ nw.style.flexBasis = '100%';
1743
+ nw.textContent = tr('tpls.nameHintBad');
1744
+ nf.appendChild(nw);
1745
+ }
1581
1746
  nh.appendChild(nf);
1582
1747
  var nt = el('div', 'hint', tr('tpls.nameHintHelp', k.geraetName));
1583
1748
  nt.style.marginTop = '4px';
@@ -1595,7 +1760,10 @@ export function zeichneVorlagenDialog() {
1595
1760
  mcb.checked = !!k.mehrfach;
1596
1761
  mcb.addEventListener('change', function () {
1597
1762
  k.mehrfach = mcb.checked;
1598
- if (k.mehrfach && !k.instanz) { k.instanz = rateInstanz(z.zeilen, k.geraetName); }
1763
+ if (k.mehrfach && !k.instanz) {
1764
+ k.instanz = rateInstanz(z.zeilen, k.geraetName,
1765
+ String(k.kanalname || 'POWER%N%').split('%N%')[0]);
1766
+ }
1599
1767
  zeichneVorlagenDialog();
1600
1768
  });
1601
1769
  ml.appendChild(mcb);
@@ -1615,7 +1783,7 @@ export function zeichneVorlagenDialog() {
1615
1783
  mf.appendChild(mz);
1616
1784
  mf.appendChild(el('div', 'hint',
1617
1785
  k.instanz ? tr('tpls.multipleHelp', k.instanz) : tr('tpls.multipleNoNumber')));
1618
- var fremd = andereAusgaenge(z.zeilen, k.instanz);
1786
+ var fremd = andereAusgaenge(z.zeilen, k.instanz, ausgangsVorspaenne(z.zeilen, k));
1619
1787
  if (fremd.punkte.length) {
1620
1788
  var fw2 = el('div', 'aside w');
1621
1789
  fw2.style.margin = '9px 0 0';
@@ -1741,9 +1909,9 @@ export function zeichneVorlagenDialog() {
1741
1909
 
1742
1910
  var pw = el('span', 'rx pf');
1743
1911
  pw.style.fontFamily = 'var(--mono)';
1744
- pw.title = zPfad(r, k, false) + (r.schreibenRoh ? ' → ' + zPfad(r, k, true) : '');
1745
- pw.appendChild(document.createTextNode(zPfad(r, k, false)));
1746
- if (r.schreibenRoh) { pw.appendChild(el('span', 'chip mut', '→ ' + zPfad(r, k, true))); }
1912
+ pw.title = zPfad(r, k, false, z.zeilen) + (r.schreibenRoh ? ' → ' + zPfad(r, k, true, z.zeilen) : '');
1913
+ pw.appendChild(document.createTextNode(zPfad(r, k, false, z.zeilen)));
1914
+ if (r.schreibenRoh) { pw.appendChild(el('span', 'chip mut', '→ ' + zPfad(r, k, true, z.zeilen))); }
1747
1915
  if (r.lesenAbs) { pw.appendChild(el('span', 'chip warn', tr('tpls.absolute'))); }
1748
1916
  row.appendChild(pw);
1749
1917
 
@@ -1849,7 +2017,8 @@ export function zeichneVorlagenDialog() {
1849
2017
 
1850
2018
  var b = $('#btn-tpl-save');
1851
2019
  if (b) {
1852
- b.disabled = fehlerErk || !k.id ||
2020
+ b.disabled = fehlerErk || !k.id || !!fremdeMitId(k) ||
2021
+ !hinweisTaugt(k.namenshinweis) ||
1853
2022
  !z.zeilen.filter(function (r) { return !r.weg; }).length;
1854
2023
  b.textContent = k.modus === 'update' ? tr('tpls.saveUpdate') : tr('tpls.saveNew');
1855
2024
  }