iobroker.alias-workbench 0.9.10 → 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.
@@ -11,7 +11,7 @@ import { katalogFehlt } from './katalog.js';
11
11
  import { funktionsAuswahl, kennungZuFunktion } from './aufzaehlungen.js';
12
12
  import { musterVon, platzFuerRolle, typenFuerRolle } from './erkennung.js';
13
13
  import { musterZeile, musterName } from './musternamen.js';
14
- import { ladeVorlagen, aendereVorlagen, setzeMeta, pruefeVorlage, hinweisTaugt, INSTANZ_ID } from './vorlagen.js';
14
+ import { ladeVorlagen, aendereVorlagen, setzeMeta, pruefeVorlage, wendeAn, 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';
@@ -287,7 +287,17 @@ export function zeichneVorlagenBlatt(host) {
287
287
  Musterschluessel. musterwahl.js loest es seit laengerem so. */
288
288
  (D ? Object.keys(D.patterns).sort() : []).forEach(function (m) {
289
289
  var typ = D.patterns[m].type || m;
290
- var o = opt(typ, musterZeile(m));
290
+ /* Beschriftet wird nach dem TYP, nicht nach dem Musterschluessel.
291
+
292
+ Der Wert stand seit dem 11.09.2026 richtig da, die Beschriftung
293
+ nicht: `musterZeile(m)` schlug den Schluessel nach, und fuer die
294
+ drei Muster, die anders heissen als ihr Typ (`blinds`,
295
+ `mediaPlayer`, `levelSlider`), kennt der Admin keinen Beinamen.
296
+ Im Feld stand am Rollladen deshalb „blinds" statt „Jalousien ·
297
+ blind" - der Musterschluessel, den der Nutzer nirgends sonst
298
+ sieht. Die Musterwahl (musterwahl.js) geht seit jeher ueber den
299
+ Typ. Gefunden produktiv am 15.09.2026 (I37). */
300
+ var o = opt(typ, musterZeile(typ));
291
301
  if (typ === v.geraetetyp) { o.selected = true; }
292
302
  st.appendChild(o);
293
303
  });
@@ -465,6 +475,18 @@ export function zeichneVorlagenBlatt(host) {
465
475
  ? probelauf(ohneUnterstrich(v), v._quelle === 'benutzer') : [];
466
476
  var gew = treffer.filter(function (t) { return t.gewinnt; }).length;
467
477
  ph2.appendChild(el('span', 'chip ' + (gew ? 'ok' : 'warn'), tr('tpls.hits', treffer.length, gew)));
478
+ /* „Getroffen" heisst nur, dass die Erkennung anspringt. Wie oft die
479
+ Vorlage danach gar nicht antreten kann, weil eine Pflichtzeile ihre
480
+ Quelle nicht findet, gehoert daneben — das ist die Zahl, an der man
481
+ beim Bauen einer Vorlage merkt, dass ihre Erkennung breiter greift
482
+ als ihre Zeilen. Neutral gefaerbt: eine Spezialvorlage SOLL auf die
483
+ meisten Geraete nicht passen. */
484
+ var passtNicht = treffer.filter(function (t) { return !t.greift; }).length;
485
+ if (passtNicht) {
486
+ var cl = el('span', 'chip mut', tr('tpls.fitsNot', passtNicht));
487
+ cl.title = tr('tpls.needsRowHint');
488
+ ph2.appendChild(cl);
489
+ }
468
490
  pk.appendChild(ph2);
469
491
  var pb2 = el('div');
470
492
  pb2.style.padding = '9px 12px 11px';
@@ -473,14 +495,35 @@ export function zeichneVorlagenBlatt(host) {
473
495
  } else {
474
496
  treffer.slice(0, 40).forEach(function (t) {
475
497
  var r = el('div', 'slot');
476
- r.style.gridTemplateColumns = '1fr 110px 150px';
498
+ /* Die Kennung darf schrumpfen, die Marke rechts nicht: ihr Text
499
+ ist nicht umbrechbar (`.chip` steht auf nowrap) und wuerde in
500
+ einer festen Spalte abgeschnitten. */
501
+ r.style.gridTemplateColumns = 'minmax(0,1fr) 72px 64px auto';
477
502
  var idl = el('span', 'sn', t.id);
478
503
  idl.style.fontFamily = 'var(--mono)';
504
+ idl.style.wordBreak = 'break-all';
479
505
  r.appendChild(idl);
480
506
  r.appendChild(el('span', 'rx', tr('tpls.evidence', t.eigen.belege)));
481
- r.appendChild(el('span', 'chip ' + (t.gewinnt ? 'ok' : 'bad'),
482
- t.gewinnt ? (t.gegner ? tr('tpls.beats', textIn(t.gegner.vorlage.name, sprache)) : tr('tpls.onlyOne'))
483
- : tr('tpls.losesTo', textIn(t.gegner.vorlage.name, sprache))));
507
+ /* Wie viele Zeilen dort wirklich entstehen. Die Belege sagen, wie
508
+ gut die Erkennung greift; erst die Zeilen sagen, was dabei
509
+ herauskommt. */
510
+ r.appendChild(el('span', 'rx',
511
+ t.zeilen === 1 ? tr('tpls.rowBuilt') : tr('tpls.rowsBuilt', t.zeilen)));
512
+ /* Drei Ausgaenge, nicht zwei: gewonnen und getragen, duenn
513
+ gewonnen, verloren. Der mittlere sah ohne eigene Marke aus wie
514
+ ein Erfolg. */
515
+ if (!t.greift) {
516
+ /* Passt hier nicht — und es steht dabei, woran es liegt. Ohne
517
+ den Namen der Zeile raet man, welches Feld am Geraet fehlt. */
518
+ var cn = el('span', 'chip bad', t.fehlendeZeile
519
+ ? tr('tpls.needsRow', t.fehlendeZeile) : tr('tpls.buildsNone'));
520
+ cn.title = tr('tpls.needsRowHint');
521
+ r.appendChild(cn);
522
+ } else {
523
+ r.appendChild(el('span', 'chip ' + (t.gewinnt ? 'ok' : 'bad'),
524
+ t.gewinnt ? (t.gegner ? tr('tpls.beats', textIn(t.gegner.vorlage.name, sprache)) : tr('tpls.onlyOne'))
525
+ : tr('tpls.losesTo', textIn(t.gegner.vorlage.name, sprache))));
526
+ }
484
527
  pb2.appendChild(r);
485
528
  });
486
529
  if (treffer.length > 40) {
@@ -1540,16 +1583,41 @@ var probeGeholt = {};
1540
1583
 
1541
1584
  function probeWerteHolen(v, danach) {
1542
1585
  var erk = v.erkennung || {};
1543
- if (!erk.inhalt) { return false; }
1544
1586
  var ph = v.mehrfach ? (v.mehrfach.platzhalter || '%N%') : null;
1587
+
1588
+ /* Nicht nur die Erkennungspunkte, sondern auch die, aus denen die
1589
+ ZEILEN lesen — samt ihrer Ersatzwege.
1590
+
1591
+ Sonst urteilt der Probelauf ueber Felder in Werten, die er gar nicht
1592
+ hat: am `Brenner` lag `tele.STATE` im Speicher und `tele.SENSOR`
1593
+ nicht, und derselbe Probelauf meldete je nachdem, was man vorher
1594
+ angeklickt hatte, „1 statt 11" oder „1 statt 20" (Ricardo,
1595
+ 14.09.2026). Eine Zahl, die von der Vorgeschichte abhaengt, ist
1596
+ keine. Hier soll dasselbe herauskommen wie beim „Alias anlegen",
1597
+ und dort sind alle Werte des Geraets geladen. */
1598
+ var pfade = Object.keys(erk.inhalt || {});
1599
+ (v.zustaende || []).forEach(function (z) {
1600
+ if (z.absolut) { return; }
1601
+ if (z.lesen) { pfade.push(z.lesen); }
1602
+ var sonst = z.lesenSonst;
1603
+ if (!sonst) { return; }
1604
+ (Array.isArray(sonst) ? sonst : [sonst]).forEach(function (x) {
1605
+ var pf = (typeof x === 'string') ? x : x.punkt;
1606
+ if (pf) { pfade.push(pf); }
1607
+ });
1608
+ });
1609
+ pfade = pfade.filter(function (x, i, a) { return x && a.indexOf(x) === i; });
1610
+ if (!pfade.length) { return false; }
1611
+
1545
1612
  var fehlt = [];
1546
1613
  kandidatenFuer(v).forEach(function (kanal) {
1547
- Object.keys(erk.inhalt).forEach(function (p0) {
1614
+ pfade.forEach(function (p0) {
1548
1615
  var pfad = ph ? String(p0).split(ph).join('1') : p0;
1549
1616
  var id = hatPunkt(kanal, pfad);
1550
1617
  if (id && S.werte[id] === undefined && !probeGeholt[id]) { fehlt.push(id); }
1551
1618
  });
1552
1619
  });
1620
+ fehlt = fehlt.filter(function (x, i, a) { return a.indexOf(x) === i; });
1553
1621
  if (!fehlt.length) { return false; }
1554
1622
  var offen = fehlt.length;
1555
1623
  fehlt.forEach(function (id) {
@@ -1581,19 +1649,57 @@ export function probelauf(vorlage, alsEigene) {
1581
1649
 
1582
1650
  /* Wer gewinnt hier heute, mit dem Bestand wie er ist? Beim
1583
1651
  Aktualisieren ist das oft die Vorlage selbst — dann wechselt
1584
- nichts, auch wenn sie den Vergleich gegen alle anderen gewinnt. */
1585
- var jetzt = null, andere = null;
1652
+ nichts, auch wenn sie den Vergleich gegen alle anderen gewinnt.
1653
+
1654
+ Die Punktbeste ist nicht zwingend die Gewinnerin: greift sie am
1655
+ Geraet nicht, ruecken die naechsten nach — genau wie in
1656
+ `vorschlag`. Vorher stand hier nur DIE eine punktbeste andere, und
1657
+ griff die nicht, erklaerte sich die gepruefte Vorlage zur
1658
+ Siegerin. An `PC_Bastelzimmer` meldete deshalb „Reiner Messpunkt"
1659
+ einen Sieg, obwohl `tasmota-steckdose` dazwischenliegt und dort
1660
+ wirklich greift (Ricardo, 14.09.2026). */
1661
+ var rang = [];
1586
1662
  S.VORLAGEN.forEach(function (v) {
1587
1663
  var x = pruefeVorlage(v, p);
1588
- if (!x.passt) { return; }
1589
- if (!jetzt || x.punkte > jetzt.punkte) { jetzt = x; }
1590
- if (v.id !== neu.id && (!andere || x.punkte > andere.punkte)) { andere = x; }
1664
+ if (x.passt) { rang.push(x); }
1591
1665
  });
1666
+ rang.sort(function (a, b) { return b.punkte - a.punkte; });
1667
+ var ersteGreifende = function (ausser) {
1668
+ for (var i = 0; i < rang.length; i++) {
1669
+ if (ausser && rang[i].vorlage.id === ausser) { continue; }
1670
+ var x = wendeAn(rang[i].vorlage, p, rang[i].gruende, rang[i].instanz, true);
1671
+ if (x && x.states.length && !x.pflichtFehlt) { return rang[i]; }
1672
+ }
1673
+ return null;
1674
+ };
1675
+ var jetzt = ersteGreifende(null);
1676
+ var andere = ersteGreifende(neu.id);
1677
+ /* Gewinnen ist nicht dasselbe wie greifen.
1678
+
1679
+ `pruefeVorlage` fragt, ob die genannten Punkte da sind; `wendeAn`
1680
+ braucht bestimmte Felder darin. Eine Vorlage, deren Zeilen
1681
+ `SML.Total_Summe` lesen, gewinnt an jeder Tasmota mit
1682
+ `tele.SENSOR` — und baut dort dann keine einzige Zeile.
1683
+
1684
+ Das laesst sich nicht vorhersagen, ohne zu raten. Also wird es
1685
+ gemessen: derselbe Aufruf, der beim Anlegen auch laeuft. Kostet
1686
+ nichts, weil die Kandidatenliste ohnehin schon durchgegangen
1687
+ wird (gefunden 14.09.2026, C3). */
1688
+ /* Mit `auchOhneTreffer`, damit auch der gescheiterte Fall zurueckkommt
1689
+ — sonst wuesste man nur „null", nicht warum. */
1690
+ var gebaut = wendeAn(neu, p, t.gruende, t.instanz, true);
1691
+ var greift = !!(gebaut && gebaut.states.length && !gebaut.pflichtFehlt);
1692
+
1693
+ /* Gewonnen hat nur, wer hier auch greift — und `andere` ist bereits
1694
+ die beste, die das tut. */
1695
+ var vorn = greift && (!andere || t.punkte > andere.punkte);
1696
+
1592
1697
  raus.push({
1593
1698
  id: p, eigen: t, gegner: andere,
1594
- gewinnt: !andere || t.punkte > andere.punkte,
1595
- wechsel: !!(jetzt && jetzt.vorlage.id !== neu.id &&
1596
- (!andere || t.punkte > andere.punkte))
1699
+ greift: greift, fehlendeZeile: gebaut ? gebaut.pflichtFehlt : null,
1700
+ zeilen: gebaut ? gebaut.states.length : 0,
1701
+ gewinnt: vorn,
1702
+ wechsel: !!(jetzt && jetzt.vorlage.id !== neu.id && vorn)
1597
1703
  });
1598
1704
  });
1599
1705
  return raus;
@@ -1738,7 +1844,17 @@ export function zeichneVorlagenDialog() {
1738
1844
  "- keiner -", obwohl "blind" gesetzt war. */
1739
1845
  (D ? Object.keys(D.patterns).sort() : []).forEach(function (m) {
1740
1846
  var typ = D.patterns[m].type || m;
1741
- var o = opt(typ, musterZeile(m));
1847
+ /* Beschriftet wird nach dem TYP, nicht nach dem Musterschluessel.
1848
+
1849
+ Der Wert stand seit dem 11.09.2026 richtig da, die Beschriftung
1850
+ nicht: `musterZeile(m)` schlug den Schluessel nach, und fuer die
1851
+ drei Muster, die anders heissen als ihr Typ (`blinds`,
1852
+ `mediaPlayer`, `levelSlider`), kennt der Admin keinen Beinamen.
1853
+ Im Feld stand am Rollladen deshalb „blinds" statt „Jalousien ·
1854
+ blind" - der Musterschluessel, den der Nutzer nirgends sonst
1855
+ sieht. Die Musterwahl (musterwahl.js) geht seit jeher ueber den
1856
+ Typ. Gefunden produktiv am 15.09.2026 (I37). */
1857
+ var o = opt(typ, musterZeile(typ));
1742
1858
  if (typ === k.geraetetyp) { o.selected = true; }
1743
1859
  st.appendChild(o);
1744
1860
  });
@@ -2026,6 +2142,7 @@ export function zeichneVorlagenDialog() {
2026
2142
  r.style.gridTemplateColumns = '1fr 150px 130px';
2027
2143
  var idl = el('span', 'sn', t.id);
2028
2144
  idl.style.fontFamily = 'var(--mono)';
2145
+ idl.style.wordBreak = 'break-all';
2029
2146
  if (t.id === (z.e.kanal || '')) { idl.style.fontWeight = '700'; }
2030
2147
  r.appendChild(idl);
2031
2148
  r.appendChild(el('span', 'rx', tr('tpls.evidence', t.eigen.belege)));
@@ -96,7 +96,7 @@ function kernWoerter(name) {
96
96
  ueberhaupt etwas anzubieten haette. */
97
97
  function rateLage(e) {
98
98
  var mu = musterVon(e.want);
99
- if (!mu) { return null; }
99
+ if (!mu) { return { grund: 'keinMuster', frei: [], handWeg: 0 }; }
100
100
 
101
101
  /* 1. Was schon belegt ist, bleibt unangetastet - in beide
102
102
  Richtungen: weder der Platz noch die Zeile. */
@@ -120,7 +120,11 @@ function rateLage(e) {
120
120
  gesehen[pl.name] = true;
121
121
  freiePlaetze.push(pl);
122
122
  });
123
- if (!freiePlaetze.length) { return null; }
123
+ /* Die Namen der freien Plaetze werden auch dann gebraucht, wenn nichts
124
+ zu holen ist - die Meldung soll sagen, WAS frei bleibt, statt nur
125
+ „nichts vorzuschlagen". */
126
+ var freiNamen = freiePlaetze.map(function (pl) { return pl.name; });
127
+ if (!freiePlaetze.length) { return { grund: 'voll', frei: [], handWeg: 0 }; }
124
128
 
125
129
  /* Ein Quellpunkt gehoert an genau eine Stelle.
126
130
 
@@ -142,30 +146,58 @@ function rateLage(e) {
142
146
  if (st.srcW) { quelleBelegt[st.srcW] = true; }
143
147
  });
144
148
 
149
+ /* Was von Hand entschieden ist, bleibt liegen (U15, Ricardo,
150
+ 15.09.2026).
151
+
152
+ Vorher fand der zweite Druck den Musterplatz wieder frei - die Rolle
153
+ stand ja nicht mehr auf dem, was das Muster verlangt - und bog
154
+ DIESELBE Zeile zurueck: aus der Handentscheidung `level.speed` wurde
155
+ wieder das vermutete `level.mode.fan`, samt Marke. Die Zeile war
156
+ damit doppelt bezeichnet („geaendert" UND „vermutet"), was sich
157
+ ausschliesst: was frisch geraten ist, kann nicht vorher geaendert
158
+ worden sein.
159
+
160
+ Eine zweite Zeile daneben zu legen waere die Alternative gewesen -
161
+ dann haette derselbe Quellpunkt aber zwei Plaetze, und die achte
162
+ Pruefung („Kein Punkt doppelt") meldete es zu Recht. Also bleibt der
163
+ Platz frei. Die Meldung unter der Leiste sagt, dass und warum. */
164
+ var handWeg = 0;
145
165
  var freieZeilen = e.states.filter(function (st) {
146
166
  if (!st.n || !st.srcR || zeileBelegt[st.n]) { return false; }
147
167
  if (quelleBelegt[st.srcR]) { return false; }
148
168
  if (st.srcW && quelleBelegt[st.srcW]) { return false; }
169
+ if (st.geaendert) { handWeg++; return false; }
149
170
  return true;
150
171
  });
151
- if (!freieZeilen.length) { return null; }
152
- return { mu: mu, namenDa: namenDa, freiePlaetze: freiePlaetze,
153
- freieZeilen: freieZeilen };
154
- }
155
-
156
- /* Gibt es ueberhaupt etwas, das der Knopf vorschlagen koennte? Die
157
- komplette Rechnung, nur ohne anzuwenden - eine grobe Vorpruefung
158
- liess den Knopf stehen, obwohl die Schwelle hinterher alles verwarf
159
- (Ricardos Thermostat: 3 Zeilen, 22 freie Plaetze, nichts passt).
160
- Der Preis ist ein zusaetzlicher Bewertungslauf je Zeichnen; die
161
- teuren Erkennungslaeufe macht das Zeichnen ohnehin mehrfach. */
162
- export function rateMoeglich(e) {
163
- return ratePlaetze(e, true) > 0;
172
+ if (!freieZeilen.length) {
173
+ return { grund: 'vergeben', frei: freiNamen, handWeg: handWeg };
174
+ }
175
+ return { grund: null, mu: mu, namenDa: namenDa, freiePlaetze: freiePlaetze,
176
+ freieZeilen: freieZeilen, frei: freiNamen, handWeg: handWeg };
164
177
  }
165
178
 
179
+ /* Der Rateschritt - mit `trocken` rechnet er nur, ohne den Entwurf
180
+ anzufassen. Zurueck kommt, wie viele Plaetze belegt wurden (`anzahl`),
181
+ und wenn keiner: warum nicht.
182
+
183
+ `grund` ist einer von vier Faellen - `keinMuster`, `voll` (kein Platz
184
+ frei), `vergeben` (Plaetze frei, aber kein uebriger Punkt mehr da),
185
+ `keinTreffer` (beides da, nichts kam ueber die Schwelle). `frei` nennt
186
+ die Plaetze, die frei bleiben, `handWeg` die Zeilen, die als
187
+ Handentscheidung liegenblieben. Der Text dazu wird dort gebaut, wo die
188
+ Sprachdatei liegt (musterwahl.js); hier steht nur der Befund.
189
+
190
+ Der Trockenlauf rechnet alles bis zur Schwelle durch. Eine grobe
191
+ Vorpruefung genuegte nicht: sie liess den Knopf stehen, obwohl die
192
+ Schwelle hinterher alles verwarf (Ricardos Thermostat, 3 Zeilen, 22
193
+ freie Plaetze, nichts passt). Seit dem 15.09.2026 laeuft er nur noch
194
+ einmal je Zeichnen - vorher fragte erst `rateMoeglich`, ob der Knopf
195
+ ueberhaupt hingehoert, und der Klick rechnete dasselbe noch einmal. */
166
196
  export function ratePlaetze(e, trocken) {
167
197
  var lage = rateLage(e);
168
- if (!lage) { return 0; }
198
+ if (lage.grund) {
199
+ return { anzahl: 0, grund: lage.grund, frei: lage.frei, handWeg: lage.handWeg };
200
+ }
169
201
  var namenDa = lage.namenDa;
170
202
  var freiePlaetze = lage.freiePlaetze, freieZeilen = lage.freieZeilen;
171
203
 
@@ -355,7 +387,8 @@ export function ratePlaetze(e, trocken) {
355
387
  st.geratenPartner = partner.n;
356
388
  }
357
389
  });
358
- return gesetzt;
390
+ return { anzahl: gesetzt, grund: gesetzt ? null : 'keinTreffer',
391
+ frei: lage.frei, handWeg: lage.handWeg };
359
392
  }
360
393
 
361
394
  /* Eine einzelne Vermutung zuruecknehmen. */
package/admin/js/werte.js CHANGED
@@ -166,7 +166,12 @@ export function zeileAusAlias(o, zeilenname) {
166
166
  unit: c.unit || '',
167
167
  states: c.states || undefined,
168
168
  wr: !!c.write,
169
- srcR: q.read || '',
169
+ /* Ein Taster meldet nichts, und der Alias sagt es mit `read: false`.
170
+ Die Kennung steht trotzdem in `alias.id` — ioBroker verlangt sie —,
171
+ aber als Lesequelle gilt sie nicht: sonst zeigte „Alias bearbeiten"
172
+ eine Quelle, die „Alias anlegen" nie eingetragen hat, und beim
173
+ naechsten Aktualisieren kippte `read` still auf `true` zurueck. */
174
+ srcR: (c.read === false && q.einfach) ? '' : (q.read || ''),
170
175
  srcW: schreibQuelle(o),
171
176
  f: (typeof a.read === 'string') ? a.read : '',
172
177
  fw: (typeof a.write === 'string') ? a.write : '',
@@ -348,21 +353,58 @@ export function fmt(s) {
348
353
  `JSON.parse(val)?.ENERGY?.Power ?? null`. Beide Schreibweisen muessen
349
354
  als Feldzugriff durchgehen - sonst zeigt die Feldauswahl an einem
350
355
  Punkt, den die Werkbank selbst gebaut hat, „eigene Formel". */
351
- var FELDPFAD = /^JSON\.parse\(val\)\??\.([A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*)*)(?:\s*\?\?\s*null)?$/;
356
+ /* ---- Feldpfade: Anfang (die Vorpruefung schneidet diesen Block heraus) ---- */
357
+
358
+ /* Ein Schluessel darf nur dann hinter den Punkt, wenn er ein gueltiger
359
+ JS-Bezeichner ist. Sonst entsteht kein Zugriff, sondern etwas anderes:
360
+ aus `?.DS18B20-1` wird die Rechnung `?.DS18B20 - 1?.…` (ergibt NaN),
361
+ aus `?./dev/shm` und `?.1111740` wird ein Syntaxfehler. Gemessen an
362
+ einem Frigate-Telegramm: von 233 angebotenen Feldern liessen sich so
363
+ nur 27 benutzen. Deshalb Klammern, wo der Name keiner ist. */
364
+ var NAME_OK = /^[A-Za-z_$][\w$]*$/;
365
+
366
+ function feldSegment(name) {
367
+ if (NAME_OK.test(name)) { return '?.' + name; }
368
+ return "?.['" + String(name).replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "']";
369
+ }
370
+
371
+ /* Ein Wegstueck ist entweder `?.name` oder `?.['name']` — die zweite
372
+ Form auch mit doppelten Anfuehrungszeichen, falls sie jemand von Hand
373
+ so geschrieben hat. */
374
+ var FELDTEIL = "\\??\\.(?:[A-Za-z_$][\\w$]*|\\[(?:'(?:[^'\\\\]|\\\\.)*'|\"(?:[^\"\\\\]|\\\\.)*\")\\])";
375
+ var FELDPFAD = new RegExp('^JSON\\.parse\\(val\\)((?:' + FELDTEIL + ')+)(?:\\s*\\?\\?\\s*null)?$');
376
+ var FELDSTUECK = new RegExp(FELDTEIL, 'g');
352
377
 
353
378
  export function feldAusFormel(f) {
354
379
  var m = FELDPFAD.exec(f || '');
355
- /* Der Feldname selbst traegt keine Fragezeichen. */
356
- return m ? m[1].split('?.').join('.') : '';
380
+ if (!m) { return ''; }
381
+ var teile = [];
382
+ var t;
383
+ FELDSTUECK.lastIndex = 0;
384
+ while ((t = FELDSTUECK.exec(m[1])) !== null) {
385
+ var s = t[0].slice(t[0].indexOf('.') + 1);
386
+ if (s.charAt(0) === '[') {
387
+ /* Nur die Fluchtzeichen zurueckdrehen, die feldSegment setzt. */
388
+ s = s.slice(2, -2).replace(/\\(.)/g, '$1');
389
+ /* Ein Punkt im Schluessel selbst laesst sich im Pfadformat von
390
+ `jsonFelder` nicht wieder auseinanderhalten — dann lieber
391
+ „eigene Formel" zeigen als einen Pfad, der nicht zurueckfuehrt. */
392
+ if (s.indexOf('.') > -1) { return ''; }
393
+ }
394
+ teile.push(s);
395
+ }
396
+ return teile.join('.');
357
397
  }
358
398
 
359
399
  /* Der abgesicherte Zugriff auf ein JSON-Feld. An einer Stelle, damit die
360
400
  Vorlagen-Abkuerzung und die Feldauswahl im Detail dasselbe bauen. */
361
401
  export function feldFormel(feld) {
362
402
  if (!feld) { return ''; }
363
- return 'JSON.parse(val)?.' + String(feld).split('.').join('?.') + ' ?? null';
403
+ return 'JSON.parse(val)' + String(feld).split('.').map(feldSegment).join('') + ' ?? null';
364
404
  }
365
405
 
406
+ /* ---- Feldpfade: Ende ---- */
407
+
366
408
  export function jsonFelder(id) {
367
409
  var st = S.werte[id];
368
410
  if (!st || typeof st.val !== 'string') { return null; }
@@ -380,3 +422,51 @@ export function jsonFelder(id) {
380
422
  })(o, '', 0);
381
423
  return raus;
382
424
  }
425
+
426
+
427
+ /* ---- Was das Ziel kann ---------------------------------------------
428
+
429
+ Die Werkbank schreibt auf fremde Punkte und liest aus fremden Punkten.
430
+ Ob die koennen, was von ihnen verlangt wird, stand nirgends: weder
431
+ `common.read` noch `common.write` noch der Typ des Ziels wurden je
432
+ angesehen (Ricardo, 15.09.2026).
433
+
434
+ Drei Arten, wie es auseinandergeht, alle drei still:
435
+
436
+ `formelBlind` Die Schreibformel liefert Text, das Ziel nimmt keinen.
437
+ Der js-controller rechnet ihn auf den Zieltyp zurueck,
438
+ die Formel war umsonst. Aus `"ON"` wird wieder `true`.
439
+ `nichtSchreibbar` Das Ziel traegt `write: false`. ioBroker laesst den
440
+ Schreibvorgang trotzdem durch — gemessen —, der Adapter
441
+ dahinter reicht ihn aber nicht ans Geraet weiter.
442
+ Ein stiller Blindgaenger.
443
+ `meldetNichts` Die Lesequelle traegt `read: false`. Ein Tastendruck
444
+ hat keinen Zustand; gelesen wird dort nie etwas.
445
+
446
+ Gibt eine Liste zurueck, nicht ein Urteil — wer sie anzeigt, entscheidet
447
+ ueber die Schaerfe. */
448
+ export function zielKann(s) {
449
+ var raus = [];
450
+ if (!s) { return raus; }
451
+
452
+ if (s.srcW) {
453
+ var w = S.objects[s.srcW];
454
+ var wc = (w && w.common) || null;
455
+ if (wc) {
456
+ if (wc.write === false) { raus.push({ art: 'nichtSchreibbar', punkt: s.srcW }); }
457
+ /* Anfuehrungszeichen im Ausdruck sind der Beleg, dass die Formel Text
458
+ liefert; ohne sie rechnet sie mit Zahlen oder Wahrheitswerten und
459
+ passt zum Ziel. */
460
+ if (s.fw && /['"`]/.test(s.fw) && (wc.type === 'boolean' || wc.type === 'number')) {
461
+ raus.push({ art: 'formelBlind', punkt: s.srcW, typ: wc.type });
462
+ }
463
+ }
464
+ }
465
+
466
+ if (s.srcR) {
467
+ var r = S.objects[s.srcR];
468
+ var rc = (r && r.common) || null;
469
+ if (rc && rc.read === false) { raus.push({ art: 'meldetNichts', punkt: s.srcR }); }
470
+ }
471
+ return raus;
472
+ }
@@ -610,7 +610,9 @@ export function uebernehmeBestand(e) {
610
610
  Gleicht die Beschriftung dem Zeilennamen, bleibt sie leer - so
611
611
  haelt es der Zweig darunter, der neue Zeilen anlegt, auch. */
612
612
  z.caption = ist.caption;
613
- if (q2.read) { z.srcR = q2.read; }
613
+ /* Wie im Bestandsvorrang: `ist.srcR` kennt den Taster, `q2.read`
614
+ nicht. */
615
+ if (q2.read) { z.srcR = ist.srcR; }
614
616
  /* Und dieselbe Schreibregel wie im Bestandsvorrang.
615
617
 
616
618
  Hier stand die Schreibquelle nur fuer das getrennte Paar; sonst
@@ -14,6 +14,9 @@ export var S = {
14
14
  objects: {},
15
15
  keysSorted: [],
16
16
  kleinIndex: {},
17
+ /* Alias-Kanal -> Zahl der Punkte, deren Quelle es nicht (mehr) gibt.
18
+ Einmal beim Laden gerechnet; der Baum schlaegt nur nach. */
19
+ aliasKaputt: {},
17
20
  werte: {},
18
21
  current: null,
19
22
  openRow: null,
@@ -17,6 +17,7 @@ import { detailZeile } from './detail.js';
17
17
  import { rateAnzahl, rateZurueck, rateZurueckEine, rateBehalten } from './vorschlagen.js';
18
18
  import { mqttEinzeln } from './mqtt.js';
19
19
  import { zeichneErgebnis, entwurfAngefasst } from './ergebnis.js';
20
+ import { taster } from './schreiben.js';
20
21
 
21
22
  export function berechnePlaetze(e, haupt) {
22
23
  /* --- welcher Punkt sitzt auf welchem Platz --- */
@@ -375,7 +376,22 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
375
376
  /* --- Zustandsliste --- */
376
377
  e.states.forEach(function (s, i) {
377
378
  var pl = platzVon[s.n];
378
- var unfertig = (!s.n || !s.srcR);
379
+ /* „Quelle eingetragen" und „Quelle vorhanden" sind zweierlei. Bis
380
+ zum 14.09.2026 fragte die Zeile nur das erste - ein Alias, dessen
381
+ Geraet aus dem System verschwunden ist (Ricardo: der Fensterkontakt
382
+ zeigte noch auf den laengst abgeloesten zigbee-Adapter), stand
383
+ deshalb ohne jedes Zeichen da. Gesagt wurde es nur im aufgeklappten
384
+ Detail und im Pruefungen-Kasten. */
385
+ var quelleWeg = !!(s.srcR && !S.objects[s.srcR]);
386
+ /* Ein Taster hat keine Lesequelle, und das ist kein Mangel: das Ziel
387
+ sagt selbst, dass dort nichts abzuholen ist (`read: false`). Ohne
388
+ diese Ausnahme stand an der STOP-Zeile weiter das rote `!` mit
389
+ „Quelle fehlt", obwohl der Alias sich laengst schreiben liess —
390
+ zwei Ansichten, zwei Urteile (Ricardo, 15.09.2026, K23). */
391
+ var istTaster = taster(s);
392
+ var unfertig = (!s.n || (!s.srcR && !istTaster) || quelleWeg);
393
+ var wasFehlt = !s.n ? tr('list.nameMissing')
394
+ : (quelleWeg ? tr('list.sourceGone') : tr('list.sourceMissing'));
379
395
  /* Angehakt, vollstaendig, aber ohne Platz im Muster. Frueher stand
380
396
  dazu in JEDER betroffenen Zeile derselbe Satz — bei einem
381
397
  Thermostat mit 42 Punkten zwanzigmal dasselbe, und die Spalte
@@ -425,7 +441,7 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
425
441
  Ueberfliegen wirklich sucht: wo klemmt es. */
426
442
  var mk = el('span', 'mk2 bad');
427
443
  mk.textContent = (s.on && unfertig) ? '!' : '';
428
- if (mk.textContent) { mk.title = !s.n ? tr('list.nameMissing') : tr('list.sourceMissing'); }
444
+ if (mk.textContent) { mk.title = wasFehlt; }
429
445
  row.appendChild(mk);
430
446
 
431
447
  var n3 = el('span', 'nm3', s.n || tr('list.newPoint'));
@@ -567,7 +583,10 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
567
583
  n3.appendChild(fb2);
568
584
  }
569
585
  if (s.on) {
570
- if (unfertig) { n3.appendChild(el('em', null, !s.n ? tr('list.nameMissing') : tr('list.sourceMissing'))); }
586
+ if (unfertig) { n3.appendChild(el('em', null, wasFehlt)); }
587
+ /* Beim Taster gehoert hin, WARUM keine Lesequelle dasteht — sonst
588
+ sieht die leere Spalte aus wie ein vergessener Eintrag. */
589
+ else if (istTaster) { n3.appendChild(el('em', null, tr('list.buttonOnly'))); }
571
590
  else if (pl && pl !== s.n && platzAnzahl[pl] === 1) {
572
591
  var pe = el('em', null, pl);
573
592
  pe.title = tr('pattern.slotName', pl, musterName(e.want) || e.want || '?');