iobroker.alias-workbench 0.9.10 → 0.9.12

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 : '',
@@ -280,7 +285,7 @@ export function auswerten(formel, roh) {
280
285
  var gedauert = performance.now() - t0;
281
286
  if (gedauert > GEDULD_MS) {
282
287
  formelGesperrt[formel] = Math.round(gedauert);
283
- console.warn('[alias-workbench] Formel gesperrt nach ' + Math.round(gedauert) + ' ms: ' + formel);
288
+ console.warn('[alias-workbench] Formula blocked after ' + Math.round(gedauert) + ' ms: ' + formel);
284
289
  return { ok: false, txt: tr('detail.formulaTooSlow', Math.round(gedauert)), fehler: true };
285
290
  }
286
291
  if (sch !== null) {
@@ -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
+ }
@@ -11,7 +11,7 @@ import { el } from './basis.js';
11
11
  import { tr } from './sprache.js';
12
12
  import { enumListe } from './aufzaehlungen.js';
13
13
  import { enumZeile, setzeZiel } from './zuordnung.js';
14
- import { aliasFuer, ordnerUnterAlias, kennungtauglich, knotenDa } from './entwurf.js';
14
+ import { aliasFuer, ausgangsQuellen, ordnerUnterAlias, kennungtauglich, knotenDa } from './entwurf.js';
15
15
  import { zeichneErgebnis, entwurfAngefasst, angebotFrisch } from './ergebnis.js';
16
16
  import { zeigeVerlegen } from './schreiben.js';
17
17
 
@@ -381,7 +381,13 @@ export function baueZielleiste(host, e) {
381
381
  irrt sie hier, irrt sie dort ebenso. Und der Hinweis haelt
382
382
  niemanden auf. */
383
383
  var zwilling = null;
384
- var vorhAlias = aliasFuer(e.kanal);
384
+ /* Dieselbe Frage wie die Vorbelegung, also auch dieselbe Antwort:
385
+ am Mehrfachgeraet der Alias DIESES Ausgangs. Sonst nennt der
386
+ Hinweis einen anderen Alias als den, dessen Stand oben in der
387
+ Liste steht - zwei Aussagen auf einem Bildschirm, die sich
388
+ widersprechen (C13). */
389
+ var vorhAlias = aliasFuer(e.kanal, ausgangsQuellen(e)) ||
390
+ ((e.instanz === null || e.instanz === undefined) ? aliasFuer(e.kanal) : null);
385
391
  if (vorhAlias && S.objects[vorhAlias] && vorhAlias !== e.ziel) {
386
392
  var zw = el('div', 'uebernahme zwilling');
387
393
  zw.appendChild(el('span', 'zq', tr('target.twinExists', vorhAlias)));
@@ -16,7 +16,8 @@ import { enumVorlagen, katalogFehlt, ikonCache, ikonErlaubt, ikonNachtrag,
16
16
  ikonTaugt, vorlageZu, holeIkon } from './katalog.js';
17
17
  import { kindZustaende, aliasQuellen, holeEinzelne,
18
18
  zeileAusAlias } from './werte.js';
19
- import { aliasFuer, zielId, knotenDa } from './entwurf.js';
19
+ import { aliasFuer, ausgangsQuellen, zielId, knotenDa,
20
+ aboErgaenzen, quellenVonEntwurf } from './entwurf.js';
20
21
  import { zeichneErgebnis, entwurfAngefasst, angebotFrisch } from './ergebnis.js';
21
22
 
22
23
 
@@ -475,18 +476,35 @@ export function setzeZiel(e) {
475
476
  /* Mehrere Ausgaenge: das Geraet wird zum Ordner, jeder Ausgang
476
477
  ein Kanal darin. Ein folder darueber, kein device — sonst
477
478
  rutscht die Erkennung hoch und macht aus dreien wieder eins. */
478
- if (vorhanden) {
479
- /* Der gefundene Kanal ist einer der Ausgaenge; der Ordner liegt
480
- darueber. Wie der einzelne Ausgang heisst, kann die Werkbank
481
- nicht wissen — wer sie umbenannt hat, sieht den Vorschlag in
482
- der Zielzeile und kann ihn anpassen. */
483
- var tm = vorhanden.split('.');
484
- tm.pop();
485
- e.zielOrdner = tm.join('.');
479
+ /* Gibt es den Alias fuer GENAU DIESEN Ausgang, ist er das Ziel —
480
+ mit seinem eigenen Namen, nicht mit dem der Ausgangsnummer.
481
+
482
+ Hier stand: „Wie der einzelne Ausgang heisst, kann die Werkbank
483
+ nicht wissen." Das galt, solange sie nur nach dem GERAET fragen
484
+ konnte. Seit `ausgangsQuellen` weiss sie es: Ricardos `RF1000`
485
+ liest aus `stat.POWER1` und ist damit Ausgang 1. Vorher schlug
486
+ die Zielzeile `…Steckdosenleiste.POWER1` vor — eine Kennung, die
487
+ es gar nicht gibt —, meldete „wird neu angelegt" und haette beim
488
+ Klick einen zweiten Alias neben `RF1000` gesetzt (C13,
489
+ 16.09.2026). */
490
+ var vorhAus = aliasFuer(S.current, ausgangsQuellen(e));
491
+ if (vorhAus) {
492
+ var ta = vorhAus.split('.');
493
+ e.zielName = ta.pop();
494
+ e.zielOrdner = ta.join('.');
486
495
  } else {
487
- e.zielOrdner = zt0.join('.') + '.' + geraet;
496
+ if (vorhanden) {
497
+ /* Kein Alias fuer diesen Ausgang, aber einer am Geraet: der gibt
498
+ wenigstens den Ordner vor. Der Name kommt aus der
499
+ Ausgangsnummer — hier entsteht wirklich etwas Neues. */
500
+ var tm = vorhanden.split('.');
501
+ tm.pop();
502
+ e.zielOrdner = tm.join('.');
503
+ } else {
504
+ e.zielOrdner = zt0.join('.') + '.' + geraet;
505
+ }
506
+ e.zielName = ausgangName(e, e.instanz);
488
507
  }
489
- e.zielName = ausgangName(e, e.instanz);
490
508
  } else if (vorhanden) {
491
509
  var te = vorhanden.split('.');
492
510
  e.zielName = te.pop();
@@ -583,7 +601,10 @@ export function uebernehmeBestand(e) {
583
601
  function vomAliasUebernehmen(z, o) {
584
602
  var c = o.common || {}, q2 = aliasQuellen(o);
585
603
  var ist = zeileAusAlias(o, z.n);
586
- if (!z.vorlagenWert) {
604
+ /* Dieselbe Frage wie in `bestandVorrang`: ohne Vorlage gibt es keinen
605
+ Vorlagenwert. Sonst verglich die Zeile gegen den Rohentwurf und
606
+ nannte das „weicht von der Vorlage ab" (17.09.2026). */
607
+ if (!z.vorlagenWert && e.vorschlag && e.vorlage) {
587
608
  z.vorlagenWert = {
588
609
  on: z.on, role: z.role, typ: z.typ, unit: z.unit, caption: z.caption,
589
610
  f: z.f, fw: z.fw, srcR: z.srcR, srcW: z.srcW, states: z.states
@@ -610,7 +631,9 @@ export function uebernehmeBestand(e) {
610
631
  Gleicht die Beschriftung dem Zeilennamen, bleibt sie leer - so
611
632
  haelt es der Zweig darunter, der neue Zeilen anlegt, auch. */
612
633
  z.caption = ist.caption;
613
- if (q2.read) { z.srcR = q2.read; }
634
+ /* Wie im Bestandsvorrang: `ist.srcR` kennt den Taster, `q2.read`
635
+ nicht. */
636
+ if (q2.read) { z.srcR = ist.srcR; }
614
637
  /* Und dieselbe Schreibregel wie im Bestandsvorrang.
615
638
 
616
639
  Hier stand die Schreibquelle nur fuer das getrennte Paar; sonst
@@ -766,6 +789,13 @@ export function uebernehmeBestand(e) {
766
789
  });
767
790
  }
768
791
  }
792
+ /* Geholt ist nur der erste Wert - mitlaufen tut er erst mit einem Abo.
793
+ Die Zeilen aus dem Alias lesen oft aus einem anderen Knoten als dem
794
+ angeklickten (J9, 18.09.2026: `Stromzaehler` zeigte drei Zeilen „aus
795
+ Solar.Netz" stundenlang auf dem Stand vom Anklicken). Ergaenzt wird
796
+ nur, was noch nicht laeuft; bei jedem Zeichnen zu rufen kostet also
797
+ nichts. */
798
+ if (S.entwurf === e && e.kanal === S.current) { aboErgaenzen(quellenVonEntwurf(e)); }
769
799
  }
770
800
 
771
801
  export function unterschiede(e) {
@@ -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,