iobroker.alias-workbench 0.9.0 → 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.
@@ -11,7 +11,7 @@ import { katalogFehlt } from './katalog.js';
11
11
  import { funktionsAuswahl, kennungZuFunktion } from './aufzaehlungen.js';
12
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);
@@ -909,12 +918,61 @@ export function vEinlesenDatei(datei) {
909
918
  leser.readAsText(datei);
910
919
  }
911
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. */
912
935
  function vPruefeEingelesen(o) {
913
936
  if (!o || typeof o !== 'object' || Array.isArray(o)) { return tr('tv.badNotObject'); }
914
937
  if (!o.id || typeof o.id !== 'string') { return tr('tv.badNoId'); }
915
938
  if (!Array.isArray(o.zustaende) || !o.zustaende.length) { return tr('tv.badNoStates'); }
916
939
  var schlecht = o.zustaende.filter(function (z) { return !z || !z.name || !z.lesen; });
917
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
+ }
918
976
  return null;
919
977
  }
920
978
 
@@ -998,35 +1056,96 @@ function relZu(id, kanal) {
998
1056
  /* stat.POWER1 → stat.POWER%N%. Ersetzt wird nur am Ende eines
999
1057
  Abschnitts und nur, wenn davor noch etwas steht — sonst wuerde aus
1000
1058
  ENERGY.Power beim Speichern Unsinn. */
1001
- function zuPlatzhalter(pfad, inst) {
1059
+ function zuPlatzhalter(pfad, inst, vorspaenne) {
1002
1060
  var i = (inst === null || inst === undefined) ? '' : String(inst);
1003
1061
  if (!i) { return pfad; }
1004
1062
  return String(pfad).split('.').map(function (t) {
1005
- 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%';
1006
1075
  }).join('.');
1007
1076
  }
1008
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
+
1009
1120
  /* Welcher Pfad steht am Ende in der Vorlage? Bei einem Mehrfachgeraet
1010
1121
  tritt der Platzhalter an die Stelle der Ausgangsnummer. */
1011
- function zPfad(r, k, schreibend) {
1122
+ function zPfad(r, k, schreibend, zeilen) {
1012
1123
  var roh = schreibend ? r.schreibenRoh : r.lesenRoh;
1013
1124
  var abs = schreibend ? r.schreibenAbs : r.lesenAbs;
1014
1125
  if (!roh) { return ''; }
1015
1126
  if (!k.mehrfach || abs || !k.instanz) { return roh; }
1016
- return zuPlatzhalter(roh, k.instanz);
1127
+ return zuPlatzhalter(roh, k.instanz,
1128
+ ausgangsVorspaenne(zeilen || (S.tplZ && S.tplZ.zeilen) || [], k));
1017
1129
  }
1018
1130
 
1019
1131
  /* Endet die Nummer eines Ausgangs in den Pfaden? Haeufigste Zahl am
1020
1132
  Ende eines Abschnitts gewinnt — cmnd.POWER1, stat.POWER1 sagen 1. */
1021
- function rateInstanz(zeilen, geraetName) {
1022
- var zaehler = {};
1133
+ function rateInstanz(zeilen, geraetName, vorspann) {
1134
+ var zaehler = {}, nurVorspann = {};
1023
1135
  zeilen.forEach(function (r) {
1024
1136
  if (r.lesenAbs) { return; }
1025
1137
  String(r.lesenRoh).split('.').forEach(function (t) {
1026
1138
  var m = /^(.*[^0-9])([0-9]{1,2})$/.exec(t);
1027
- 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; }
1028
1142
  });
1029
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; }
1030
1149
  var mn = /^(.*[^0-9])([0-9]{1,2})$/.exec(geraetName || '');
1031
1150
  if (mn) { zaehler[mn[2]] = (zaehler[mn[2]] || 0) + 1; }
1032
1151
  var beste = '', wie = 0;
@@ -1042,15 +1161,19 @@ function rateInstanz(zeilen, geraetName) {
1042
1161
  entstehen daraus. Zeigt der Entwurf noch auf POWER2, POWER3 …, waere
1043
1162
  die Vorlage an dieses eine Geraet gefesselt. Also einsammeln und
1044
1163
  sagen, statt still etwas Falsches zu speichern. */
1045
- function andereAusgaenge(zeilen, instanz) {
1164
+ function andereAusgaenge(zeilen, instanz, vorspaenne) {
1046
1165
  var raus = { punkte: [], nummern: [] };
1047
1166
  if (!instanz) { return raus; }
1167
+ var vs = vorspaenne || [];
1048
1168
  zeilen.forEach(function (r) {
1049
1169
  if (r.weg || r.lesenAbs) { return; }
1050
1170
  var fremd = null;
1051
1171
  String(r.lesenRoh).split('.').forEach(function (t) {
1052
1172
  var m = /^(.*[^0-9])([0-9]{1,2})$/.exec(t);
1053
- 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]; }
1054
1177
  });
1055
1178
  if (fremd) {
1056
1179
  raus.punkte.push(r.name);
@@ -1167,7 +1290,8 @@ export function vorlagenVorbereiten(e) {
1167
1290
  maxRang: maxRang,
1168
1291
  mehrfach: mehrfach,
1169
1292
  instanz: mehrfach ? String(inst === null || inst === undefined ? '' : inst)
1170
- : rateInstanz(zeilen, geraetName),
1293
+ : rateInstanz(zeilen, geraetName,
1294
+ String((quelle && quelle.kanalname) || 'POWER%N%').split('%N%')[0]),
1171
1295
  kanalname: (quelle && quelle.kanalname) ? quelle.kanalname : 'POWER%N%',
1172
1296
  verboten: quelle ? (((quelle.erkennung || {}).verboten) || []).join(', ') : '',
1173
1297
  geraetName: geraetName
@@ -1191,8 +1315,8 @@ export function baueVorlage(z) {
1191
1315
  var erf = [];
1192
1316
  z.zeilen.forEach(function (r) {
1193
1317
  if (r.weg || !r.pflicht || r.lesenAbs) { return; }
1194
- var lp = zPfad(r, k, false);
1195
- var sp = zPfad(r, k, true);
1318
+ var lp = zPfad(r, k, false, z.zeilen);
1319
+ var sp = zPfad(r, k, true, z.zeilen);
1196
1320
  if (erf.indexOf(lp) === -1) { erf.push(lp); }
1197
1321
  if (sp && !r.schreibenAbs && erf.indexOf(sp) === -1) { erf.push(sp); }
1198
1322
  });
@@ -1225,10 +1349,10 @@ export function baueVorlage(z) {
1225
1349
  rolle: s.role || undefined,
1226
1350
  typ: s.typ || undefined,
1227
1351
  einheit: s.unit || undefined,
1228
- lesen: zPfad(r, k, false)
1352
+ lesen: zPfad(r, k, false, z.zeilen)
1229
1353
  };
1230
1354
  if (r.lesenAbs) { zu.absolut = true; }
1231
- if (r.schreibenRoh) { zu.schreiben = zPfad(r, k, true); }
1355
+ if (r.schreibenRoh) { zu.schreiben = zPfad(r, k, true, z.zeilen); }
1232
1356
  if (f) { zu.feld = f; } else if (s.f) { zu.leseformel = s.f; }
1233
1357
  if (s.fw && r.schreibenRoh) { zu.schreibformel = s.fw; }
1234
1358
  if (!r.pflicht) { zu.optional = true; }
@@ -1244,7 +1368,15 @@ export function baueVorlage(z) {
1244
1368
  .map(function (w) {
1245
1369
  var o = { punkt: w.punkt };
1246
1370
  if (w.feld) { o.feld = w.feld; }
1247
- 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; }
1248
1380
  return o;
1249
1381
  });
1250
1382
  if (rest.length) { zu.lesenSonst = (rest.length === 1) ? rest[0] : rest; }
@@ -1450,6 +1582,24 @@ export function zeigeVorlagenDialog() {
1450
1582
  $('#dlg-tpl').showModal();
1451
1583
  }
1452
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
+
1453
1603
  function tplFeld(label, wert, aendern, breit) {
1454
1604
  var l = el('label', 'feld');
1455
1605
  l.appendChild(el('span', 'feldlabel', label));
@@ -1531,7 +1681,16 @@ export function zeichneVorlagenDialog() {
1531
1681
 
1532
1682
  kb.appendChild(tplFeld(tr('tpls.nameDe'), k.nameDe, function (x) { k.nameDe = x; }, '150px'));
1533
1683
  kb.appendChild(tplFeld(tr('tpls.nameEn'), k.nameEn, function (x) { k.nameEn = x; }, '150px'));
1534
- 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);
1535
1694
 
1536
1695
  var lt = el('label', 'feld');
1537
1696
  lt.appendChild(el('span', 'feldlabel', tr('tpls.deviceType')));
@@ -1577,6 +1736,13 @@ export function zeichneVorlagenDialog() {
1577
1736
  var nh = el('div');
1578
1737
  nh.style.padding = '0 12px 11px';
1579
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
+ }
1580
1746
  nh.appendChild(nf);
1581
1747
  var nt = el('div', 'hint', tr('tpls.nameHintHelp', k.geraetName));
1582
1748
  nt.style.marginTop = '4px';
@@ -1594,7 +1760,10 @@ export function zeichneVorlagenDialog() {
1594
1760
  mcb.checked = !!k.mehrfach;
1595
1761
  mcb.addEventListener('change', function () {
1596
1762
  k.mehrfach = mcb.checked;
1597
- 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
+ }
1598
1767
  zeichneVorlagenDialog();
1599
1768
  });
1600
1769
  ml.appendChild(mcb);
@@ -1614,7 +1783,7 @@ export function zeichneVorlagenDialog() {
1614
1783
  mf.appendChild(mz);
1615
1784
  mf.appendChild(el('div', 'hint',
1616
1785
  k.instanz ? tr('tpls.multipleHelp', k.instanz) : tr('tpls.multipleNoNumber')));
1617
- var fremd = andereAusgaenge(z.zeilen, k.instanz);
1786
+ var fremd = andereAusgaenge(z.zeilen, k.instanz, ausgangsVorspaenne(z.zeilen, k));
1618
1787
  if (fremd.punkte.length) {
1619
1788
  var fw2 = el('div', 'aside w');
1620
1789
  fw2.style.margin = '9px 0 0';
@@ -1740,9 +1909,9 @@ export function zeichneVorlagenDialog() {
1740
1909
 
1741
1910
  var pw = el('span', 'rx pf');
1742
1911
  pw.style.fontFamily = 'var(--mono)';
1743
- pw.title = zPfad(r, k, false) + (r.schreibenRoh ? ' → ' + zPfad(r, k, true) : '');
1744
- pw.appendChild(document.createTextNode(zPfad(r, k, false)));
1745
- 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))); }
1746
1915
  if (r.lesenAbs) { pw.appendChild(el('span', 'chip warn', tr('tpls.absolute'))); }
1747
1916
  row.appendChild(pw);
1748
1917
 
@@ -1848,7 +2017,8 @@ export function zeichneVorlagenDialog() {
1848
2017
 
1849
2018
  var b = $('#btn-tpl-save');
1850
2019
  if (b) {
1851
- b.disabled = fehlerErk || !k.id ||
2020
+ b.disabled = fehlerErk || !k.id || !!fremdeMitId(k) ||
2021
+ !hinweisTaugt(k.namenshinweis) ||
1852
2022
  !z.zeilen.filter(function (r) { return !r.weg; }).length;
1853
2023
  b.textContent = k.modus === 'update' ? tr('tpls.saveUpdate') : tr('tpls.saveNew');
1854
2024
  }
package/admin/js/werte.js CHANGED
@@ -52,10 +52,34 @@ export function feldWert(obj, pfad) {
52
52
  }
53
53
 
54
54
 
55
+ /* Alle Kennungen mit diesem Vorspann — ohne die Gesamtliste zu lesen.
56
+
57
+ `S.keysSorted` ist sortiert, die gesuchten Eintraege liegen also am
58
+ Stueck. Vorher ging jeder dieser Aufrufe die ganze Liste durch:
59
+ 24.786 Vergleiche fuer zehn Treffer, rund zwanzigmal je Neuzeichnen,
60
+ und bei laufenden Werten alle 700 ms (gemessen am Produktivsystem
61
+ 10.09.2026: Alias 51,9 ms, Quelle 15,8 ms je Zeichnen).
62
+
63
+ Die Binaersuche findet den Anfang in ~15 Schritten, danach wird nur
64
+ noch der Bereich selbst gelesen. */
65
+ export function mitVorspann(pre) {
66
+ var liste = S.keysSorted;
67
+ var lo = 0, hi = liste.length;
68
+ while (lo < hi) {
69
+ var m = (lo + hi) >> 1;
70
+ if (liste[m] < pre) { lo = m + 1; } else { hi = m; }
71
+ }
72
+ var raus = [];
73
+ for (var i = lo; i < liste.length && liste[i].indexOf(pre) === 0; i++) {
74
+ raus.push(liste[i]);
75
+ }
76
+ return raus;
77
+ }
78
+
55
79
  export function kindZustaende(kanal) {
56
80
  var pre = kanal + '.';
57
- return S.keysSorted.filter(function (k) {
58
- return k.indexOf(pre) === 0 && S.objects[k] && S.objects[k].type === 'state';
81
+ return mitVorspann(pre).filter(function (k) {
82
+ return S.objects[k] && S.objects[k].type === 'state';
59
83
  });
60
84
  }
61
85
 
@@ -64,6 +88,30 @@ export function direkteZustaende(kanal) {
64
88
  return kindZustaende(kanal).filter(function (k) { return k.slice(pre.length).indexOf('.') === -1; });
65
89
  }
66
90
 
91
+ /* Wohin schreibt dieser Aliaspunkt?
92
+
93
+ Fuenfmal woertlich gleich ausprogrammiert gewesen — dreimal in
94
+ `entwurf.js`, zweimal in `zuordnung.js` —, und die Kommentare an zwei
95
+ dieser Stellen dokumentieren je einen Fehler, der genau daraus
96
+ entstanden ist: einmal wurde aus einem schaltenden Punkt ein lesender
97
+ ohne Umrechnung, einmal verloren OPEN, SET und pct ihre Schreibquelle,
98
+ weil dort `q.einfach ? '' : …` stand und `common.write` gar nicht
99
+ angesehen wurde.
100
+
101
+ Die Regel selbst: Bei getrennten Quellen (`alias.id.read`/`.write`)
102
+ gilt, was unter `write` steht. Bei einer schlichten `alias.id` zeigen
103
+ Lesen und Schreiben auf denselben Punkt — geschrieben wird dorthin
104
+ aber nur, wenn der Alias das auch sagt: durch `common.write` oder
105
+ durch eine hinterlegte Schreibformel. Wer eine Schreibformel
106
+ hinterlegt hat, wollte schreiben. */
107
+ export function schreibQuelle(o) {
108
+ var c = (o && o.common) || {};
109
+ var a = c.alias || {};
110
+ var q = aliasQuellen(o);
111
+ if (!q.einfach) { return q.write || ''; }
112
+ return (c.write === true || typeof a.write === 'string') ? (q.write || '') : '';
113
+ }
114
+
67
115
  export function aliasQuellen(obj) {
68
116
  var a = obj && obj.common && obj.common.alias;
69
117
  if (!a) { return {}; }
@@ -114,17 +162,76 @@ export function holeEinzelne(ids, fertig) {
114
162
  }
115
163
 
116
164
  /* ================== Formel und Wert ================== */
165
+
166
+ /* Leseformeln sind fremder Code, der hier laeuft.
167
+
168
+ Sie stehen in `common.alias.read` jedes Alias und in `leseformel`
169
+ jeder Vorlage, und sie werden je Zeile bei JEDEM Zeichnen ausgefuehrt
170
+ — ungefragt, nicht auf Knopfdruck. Das ist keine neue
171
+ Rechteausweitung: Wer `common.alias.read` schreiben darf, hat
172
+ dieselbe Formel schon im js-controller laufen. Es ist aber ein
173
+ zusaetzlicher Ausfuehrungsort, und einer ohne Grenzen: `try/catch`
174
+ faengt nur Ausnahmen, kein `while(true){}`.
175
+
176
+ Zwei Schranken, beide billig:
177
+
178
+ 1. Gemerkt wird je (Formel, Rohwert). Dieselbe Formel auf demselben
179
+ Wert laeuft genau einmal — und genau das ist der Regelfall, wenn
180
+ alle 700 ms neu gezeichnet wird, ohne dass sich etwas geaendert
181
+ hat.
182
+ 2. Wer einmal zu lange gebraucht hat, laeuft nicht wieder. Die Zeile
183
+ sagt dann, warum. Den Reiter kann man damit noch einmal
184
+ einfrieren, aber nicht dauerhaft — und nach dem Neuladen steht
185
+ die Sperre wieder. */
186
+ var GEDULD_MS = 250;
187
+ var formelCache = new Map();
188
+ var formelGesperrt = {};
189
+
190
+ function cacheSchluessel(formel, roh) {
191
+ var t = typeof roh;
192
+ if (t === 'string') { return roh.length > 300 ? null : ('s:' + formel + '\u0000' + roh); }
193
+ if (t === 'number' || t === 'boolean') { return t.charAt(0) + ':' + formel + '\u0000' + roh; }
194
+ return null;
195
+ }
196
+
117
197
  export function auswerten(formel, roh) {
118
198
  if (roh === undefined || roh === null) { return { ok: false, txt: tr('detail.noValue') }; }
119
199
  if (!formel) { return { ok: true, val: roh }; }
200
+ if (formelGesperrt[formel]) {
201
+ return { ok: false, txt: tr('detail.formulaTooSlow', formelGesperrt[formel]), fehler: true };
202
+ }
203
+ var sch = cacheSchluessel(formel, roh);
204
+ if (sch !== null) {
205
+ var da = formelCache.get(sch);
206
+ if (da !== undefined) { return da; }
207
+ }
208
+ var raus;
209
+ var t0 = performance.now();
120
210
  try {
121
211
  var v = (new Function('val', 'return (' + formel + ');'))(roh);
122
- if (v === undefined) { return { ok: false, txt: 'undefined', leer: true }; }
123
- if (typeof v === 'number' && isNaN(v)) { return { ok: false, txt: 'NaN', leer: true }; }
124
- return { ok: true, val: v };
212
+ if (v === undefined) { raus = { ok: false, txt: 'undefined', leer: true }; }
213
+ else if (typeof v === 'number' && isNaN(v)) { raus = { ok: false, txt: 'NaN', leer: true }; }
214
+ else { raus = { ok: true, val: v }; }
125
215
  } catch (e) {
126
- return { ok: false, txt: tr('detail.formulaThrows', e.message), fehler: true };
216
+ raus = { ok: false, txt: tr('detail.formulaThrows', e.message), fehler: true };
127
217
  }
218
+ var gedauert = performance.now() - t0;
219
+ if (gedauert > GEDULD_MS) {
220
+ formelGesperrt[formel] = Math.round(gedauert);
221
+ console.warn('[alias-workbench] Formel gesperrt nach ' + Math.round(gedauert) + ' ms: ' + formel);
222
+ return { ok: false, txt: tr('detail.formulaTooSlow', Math.round(gedauert)), fehler: true };
223
+ }
224
+ if (sch !== null) {
225
+ /* Nicht unbegrenzt wachsen lassen: bei laufenden Werten entsteht je
226
+ neuem Rohwert ein Eintrag. Zweitausend reichen fuer ein Zeichnen
227
+ um ein Vielfaches; darueber wird der aelteste verworfen. */
228
+ if (formelCache.size >= 2000) {
229
+ var ersteR = formelCache.keys().next();
230
+ if (!ersteR.done) { formelCache.delete(ersteR.value); }
231
+ }
232
+ formelCache.set(sch, raus);
233
+ }
234
+ return raus;
128
235
  }
129
236
 
130
237
  var JA = ['ON', 'TRUE', 'YES', 'JA', '1'];
@@ -4,7 +4,7 @@
4
4
  import { S } from './zustand.js';
5
5
  import { el } from './basis.js';
6
6
  import { tr, txt } from './sprache.js';
7
- import { enums } from './enums.js';
7
+ import { enums, enumsUnbekannt } from './enums.js';
8
8
  import {
9
9
  enumListe,
10
10
  enumsVon,
@@ -14,7 +14,7 @@ import {
14
14
  } from './aufzaehlungen.js';
15
15
  import { enumVorlagen, katalogFehlt, ikonCache, ikonErlaubt, ikonNachtrag,
16
16
  ikonTaugt, vorlageZu, holeIkon } from './katalog.js';
17
- import { kindZustaende, aliasQuellen, holeEinzelne } from './werte.js';
17
+ import { kindZustaende, aliasQuellen, holeEinzelne, schreibQuelle } from './werte.js';
18
18
  import { aliasFuer, zielId, knotenDa } from './entwurf.js';
19
19
  import { zeichneErgebnis, entwurfAngefasst, angebotFrisch } from './ergebnis.js';
20
20
 
@@ -35,6 +35,14 @@ import { musterVon, rolleTrifft } from './erkennung.js';
35
35
  export function enumAenderungen(e) {
36
36
  var raus = [];
37
37
  if (!e || !e.kanal) { return raus; }
38
+ /* Ohne verlaesslichen Vorrat wird keine Aufzaehlung angefasst.
39
+
40
+ Was hier entstuende, waere aus der Vorlage gebaut und truege genau
41
+ ein Mitglied - `setObject` ersetzt das vorhandene Objekt damit
42
+ vollstaendig. Lieber gar kein Raum als ein Raum, der die anderen
43
+ Mitglieder verschluckt (09.09.2026). Die Oberflaeche sagt es am
44
+ Feld und im Trockenlauf. */
45
+ if (enumsUnbekannt()) { return raus; }
38
46
  /* Im Aliasmodus gibt es kein gewaehltes Ziel — der Alias *ist* das Ziel.
39
47
  Eine frueher hier stehende Abfrage brach in genau diesem Fall ab, und
40
48
  eine im Aliasmodus geaenderte Zuordnung wurde stillschweigend nicht
@@ -343,6 +351,11 @@ export function enumZeile(host, e) {
343
351
  });
344
352
  el2.appendChild(z);
345
353
  });
354
+ if (enumsUnbekannt()) {
355
+ var ef = el('div', 'vz zurueck');
356
+ ef.appendChild(el('span', null, tr('enums.unknown')));
357
+ el2.appendChild(ef);
358
+ }
346
359
  if (katalogFehlt) {
347
360
  var kf = el('div', 'vz zurueck');
348
361
  kf.appendChild(el('span', null, tr('catalog.missing')));
@@ -609,14 +622,24 @@ export function uebernehmeBestand(e) {
609
622
  Belegt ist das Schreiben durch `common.write` oder durch eine
610
623
  hinterlegte Schreibformel; bei getrennten Quellen zaehlt, was in
611
624
  `alias.id.write` steht. */
612
- z.srcW = q2.einfach
613
- ? ((c.write === true || typeof a.write === 'string') ? (q2.write || '') : '')
614
- : (q2.write || '');
625
+ z.srcW = schreibQuelle(o);
615
626
  if (q2.write && !q2.einfach && q2.write !== q2.read) {
616
627
  /* Die Gegenzeile eines Paars ist keine eigene mehr - ihre Quelle
617
628
  steckt jetzt als Schreibquelle in dieser Zeile. */
618
629
  e.states.forEach(function (x) {
619
- if (x !== z && x.srcR === q2.write) { x.on = false; }
630
+ if (x === z || x.srcR !== q2.write) { return; }
631
+ /* Aber nur, was nicht selbst im Alias steht.
632
+
633
+ Sonst haengt es an der Reihenfolge von `Object.keys`, ob ein
634
+ bestehender Punkt ueberlebt: wird er vor der Paarzeile
635
+ verarbeitet, bleibt er aus, die Aenderungs-Karte meldet „wird
636
+ entfernt", und das Schreiben loescht ihn — ohne dass ihn
637
+ jemand abgewaehlt haette. Heute rettet ihn die Schleife
638
+ darunter (`if (!s.on) s.on = true`), weil in allen 16
639
+ mitgelieferten Vorlagen die Stellzeile vor der Melderzeile
640
+ steht; das ist Glueck, keine Zusicherung (09.09.2026). */
641
+ if (x.n && S.objects[e.ziel + '.' + x.n]) { return; }
642
+ x.on = false;
620
643
  });
621
644
  }
622
645
  e.bestandZog = true;
@@ -695,9 +718,7 @@ export function uebernehmeBestand(e) {
695
718
  Schreibquelle, OPEN, SET und pct verloren sie - ein
696
719
  Aktualisieren haette den Rollladen ueber den Alias unfahrbar
697
720
  gemacht (Ricardo, 08.09.2026). */
698
- srcW: q.einfach
699
- ? ((c.write === true || typeof a.write === 'string') ? (q.write || '') : '')
700
- : (q.write || ''),
721
+ srcW: schreibQuelle(o),
701
722
  f: (typeof a.read === 'string') ? a.read : '',
702
723
  fw: (typeof a.write === 'string') ? a.write : '',
703
724
  caption: (nm && nm !== n) ? nm : '',
@@ -35,16 +35,13 @@ export function berechnePlaetze(e, haupt) {
35
35
  Object.keys(platzVon).forEach(function (n) {
36
36
  platzAnzahl[platzVon[n]] = (platzAnzahl[platzVon[n]] || 0) + 1;
37
37
  });
38
- var infoFund = erkenneEntwurf(e, 'info');
38
+ /* Hier stand ein voller Detektorlauf gegen das info-Muster. Sein
39
+ Ergebnis wanderte ueber `imInfo` und `infoNamen` in einen Parameter
40
+ von `setzeKnoepfe`, den dort niemand liest — bei JEDEM Zeichnen,
41
+ also bei laufenden Werten alle 700 ms (gefunden 09.09.2026).
42
+ `imInfo` bleibt als leeres Objekt stehen, damit die Aufrufer
43
+ unveraendert bleiben. */
39
44
  var imInfo = {};
40
- if (infoFund.length && haupt && haupt.type !== 'info') {
41
- infoFund[0].states.forEach(function (x) {
42
- if (x.id) {
43
- var kurz = x.id.slice(S.current.length + 1);
44
- if (!platzVon[kurz]) { imInfo[kurz] = true; }
45
- }
46
- });
47
- }
48
45
 
49
46
  /* Mehrere Kanaele, die fuer sich ein Geraet ergeben? Einmal rechnen,
50
47
  am Entwurf merken — der Hinweis in der Karte und die Knoepfe unten
@@ -460,7 +457,14 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
460
457
  Marken, jede mit eigener Bedeutung: „geaendert" heisst, der
461
458
  Nutzer war es; „weicht von der Vorlage ab" vergleicht mit der
462
459
  Vorlage; diese hier mit der Datenbank (Ricardo, 06.09.2026). */
463
- var bAbw = bestandsAbweichung(s, e.ziel);
460
+ /* `e.ziel || e.kanal` — wie bei den beiden Nachbarn.
461
+
462
+ Im Aliasmodus ist `e.ziel` nie gesetzt: Man steht ja schon auf dem
463
+ Alias, das Ziel waehlt man erst im Quellenmodus. Die Marke „weicht vom
464
+ Alias ab" erschien dort also NIE — ausgerechnet dort, wo sie am
465
+ meisten sagt (gemessen 09.09.2026: mit `e.ziel` null Abweichungen, mit
466
+ `e.ziel || e.kanal` eine). */
467
+ var bAbw = bestandsAbweichung(s, e.ziel || e.kanal);
464
468
  if (bAbw.length) {
465
469
  var bv = el('span', 'geae bstabw', tr('list.aliasDiffers'));
466
470
  bv.title = bAbw.map(function (x) {
package/admin/tab.html CHANGED
@@ -11,10 +11,24 @@
11
11
  Vorgabepalette auf. Ohne Parameter (Reiter direkt geoeffnet)
12
12
  entscheidet die Systemeinstellung hell/dunkel. */
13
13
  (function () {
14
+ /* Der Admin hat mehr Themen als vier.
15
+
16
+ `io-package.json` laesst Admin ab 7.6.20 zu, und dort gibt es
17
+ auch `blue` (dunkel) und `colored` (hell). Beide fielen durch
18
+ diese Pruefung, das Attribut blieb ungesetzt — und dann
19
+ entscheidet die SYSTEMeinstellung: ein dunkler Admin auf einem
20
+ hell eingestellten Rechner bekam die helle Palette mitten in
21
+ seine dunkle Umgebung. Entschieden wird deshalb nach der
22
+ Helligkeit, nicht nach dem Namen; unbekannte Namen fallen
23
+ weiterhin auf die Systemeinstellung zurueck, das ist die
24
+ ehrlichste Vermutung. */
25
+ var HELLIGKEIT = {
26
+ modernLight: 'modernLight', light: 'light', colored: 'modernLight',
27
+ modernDark: 'modernDark', dark: 'dark', blue: 'modernDark'
28
+ };
14
29
  var setze = function (name) {
15
- if (/^(modernLight|modernDark|light|dark)$/.test(name)) {
16
- document.documentElement.setAttribute('data-adminthema', name);
17
- }
30
+ var w = HELLIGKEIT[name];
31
+ if (w) { document.documentElement.setAttribute('data-adminthema', w); }
18
32
  };
19
33
  var m = /[?&]react=([A-Za-z]+)(?:&|$)/.exec(location.search);
20
34
  if (m) { setze(m[1]); }
@@ -68,7 +68,7 @@
68
68
  },
69
69
  {
70
70
  "name": "ON_TIME",
71
- "rolle": "level.timer",
71
+ "rolle": "level.timer.off",
72
72
  "typ": "number",
73
73
  "einheit": "s",
74
74
  "lesen": "1.ON_TIME",