iobroker.alias-workbench 0.9.12 → 0.10.0

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.
package/README.md CHANGED
@@ -592,6 +592,60 @@ imported, and that both language files carry the same keys.
592
592
 
593
593
  ## Changelog
594
594
 
595
+ ### 0.10.0 (2026-09-20)
596
+ **Detection and patterns**
597
+ * Picking another pattern re-ticks the rows and leaves them alone while the new pattern does not apply yet.
598
+ * Guessing gives every datapoint one slot only, and a contradicting unit rules a slot out — a start delay in seconds does not become an actual temperature.
599
+ * The pattern field names what is really missing, groups included (“SET / SET_HEATING / SET_COOLING”). The legend says why a pattern does not apply and offers the matching button.
600
+ * The hint “might fit better” only suggests patterns that apply afterwards.
601
+ * For “Information” the field reads “✓ 24 points” instead of “24/31 filled” — that pattern takes any number of points.
602
+ * The message “No remaining datapoint fits” no longer sits above the orange legend and names the rows decided by hand.
603
+
604
+ **Free slots**
605
+ * A slot can be created once, and every slot is listed once.
606
+ * “Suggest free slots” now sits in the blue box next to the pattern; the button for the whole device is called “Load all datapoints of the device” and sits above the list.
607
+
608
+ **The detail row of a point**
609
+ * New: “Name” and “Slot in the pattern” — the slot fills in role, type, unit and value list.
610
+ * “reads from” and “writes to” are wider, the formula is called “read formula” and sits below.
611
+ * If the slot already belongs to another row, the workbench says so instead of blaming the type. A pure measured value no longer gets a write target.
612
+
613
+ **Templates**
614
+ * “Apply template values” no longer deletes a value list.
615
+ * A template without a name can be imported and then named.
616
+ * A skipped row gives the reason: point missing, no JSON, or field missing.
617
+ * “Update template” leaves the detection as it was; an exported template can be imported again.
618
+
619
+ **Source swap**
620
+ * If one point fails, the note on the alias stays on the old source — the swap can be finished afterwards.
621
+ * The greyed-out button gives its own reason.
622
+ * The hint “one level up” only appears where it helps.
623
+
624
+ **Deleting and moving**
625
+ * The delete dialog only warns about channels that really lie below the alias.
626
+ * After a move the source knows its alias again right away; “to the source →” no longer jumps somewhere vague.
627
+
628
+ **Room and function**
629
+ * A room is found in the middle of a device name too (“Heizung_Badezimmer INT0000001”).
630
+ * Picking with the keyboard works like the mouse, and the offer “file it under … as well?” no longer opens a row of its own.
631
+ * If rooms and functions cannot be loaded, the fields say so themselves.
632
+
633
+ **Tasmota and MQTT**
634
+ * The IP is found on older firmware too (`tele.INFO2` in both forms).
635
+ * The card no longer claims a switched-off point sends; “allow sending” switches the link back on as well.
636
+ * Creating several send points at once yields the same objects as creating them one by one — `POWER`, `Fade` and `LedTable` as `mixed`, so the write formula takes effect.
637
+ * A HomeMatic actuator without a template no longer drags maintenance points along.
638
+ * “Ask” for SetOption59 only counts what arrived after sending; if no answer comes, “No answer …” is shown and the button stays for a re-check.
639
+ * The “MQTT device” card opens again on devices without a row from `tele/…`.
640
+
641
+ **Display**
642
+ * The number on a tree node counts what lies below it.
643
+ * A node without an object of its own is marked “VIR”, the device card sorts like the tree.
644
+ * A button reads along and stays a button; the misleading “never changes” badge is gone.
645
+ * JSON fields with a dot in their name can be selected.
646
+
647
+ All changes in detail, with cause and measurement: [docs/aenderungen-ausfuehrlich.md](docs/aenderungen-ausfuehrlich.md).
648
+
595
649
  ### 0.9.12 (2026-09-18)
596
650
  * The small **type badges in the tree** (DEV, CHA, FOL …) now all have the same width. A node without an object of its own shows just a dot there, and its badge was narrower — its name fell out of line.
597
651
  * **“Move …” did nothing on some aliases.** Creating alias points by hand in the admin often yields an alias without its own channel object — the node shows in the tree but is not an object itself. The workbench displayed the “Move …” button there, but clicking it opened no dialog and said nothing. Such an alias is now moved exactly like one with a channel; a missing channel is not created along the way.
@@ -959,6 +959,14 @@
959
959
  margin:0;
960
960
  }
961
961
  .uebernahme .zq { color:var(--ink-2); font-size:11.5px; }
962
+ /* Das Raum-Angebot steht unter dem Raumfeld, in der Zeile der Marken
963
+ „vorgeschlagen“ — nicht in einer eigenen. Mit voller Breite fand es
964
+ dort keinen Platz, sobald die Funktion ihre Marke trug, und oeffnete
965
+ eine dritte Zeile (Ricardo, 19.09.2026: „nimmt zu viel Platz weg“).
966
+ Platz ist genau dann: das Angebot kommt nur nach einer Raumwahl von
967
+ Hand, und dann traegt der Raum keine Marke. Die Spalte der
968
+ Funktionsbeschriftung darunter ist frei und darf mitgenutzt werden. */
969
+ .zielbar .uebernahme.raumangebot { grid-row:2; grid-column:2 / 4; margin-top:-2px; }
962
970
  .btn.mini { padding:2px 9px; font-size:11px; line-height:1.6; }
963
971
  .btn.mini.wichtig {
964
972
  background:var(--accent); border-color:var(--accent); color:#fff; font-weight:600;
@@ -238,15 +238,31 @@ export function raumVorschlag(e) {
238
238
  }
239
239
  }
240
240
 
241
- /* 4. Aus dem Geraetenamen: „HK_Bastelzimmer" -> Bastelzimmer.
242
- Von hinten, weil der Raum am Ende steht, und stueckweise, weil
243
- „Max_Spielzimmer" aus zwei Teilen besteht. */
241
+ /* 4. Aus dem Geraetenamen: „HK_Bastelzimmer" -> Bastelzimmer. */
244
242
  var name = zusatzName(e.kanal) || (quelle ? zusatzName(quelle) : '') || '';
245
- if (name) {
246
- var nt = String(name).split(/[_ .]+/);
247
- for (var k = 0; k < nt.length; k++) {
248
- var rest = nt.slice(k).join('_');
249
- var nr = suche(rest);
243
+ if (name) { return raumAusNamen(name, suche); }
244
+ return '';
245
+ }
246
+
247
+ /* Einen Raum im Geraetenamen finden.
248
+
249
+ Probiert wird jedes zusammenhaengende Wortstueck: laengere vor
250
+ kuerzeren, damit „Jonas_Schlafzimmer" vor „Schlafzimmer" gewinnt, und
251
+ bei gleicher Laenge das weiter hinten stehende zuerst.
252
+
253
+ Bis 19.09.2026 kamen nur Stuecke in Frage, die bis zum Ende reichen.
254
+ An „Heizung_Badezimmer INT0000001" hing dann immer der Anhang mit dran,
255
+ „Badezimmer" allein wurde nie probiert — vier Heizgruppen am
256
+ Testsystem und alle sieben am Produktivsystem blieben ohne Raum (Lauf
257
+ vom 19.09.2026, N5). Ein Raum mitten im Namen ist die Regel, nicht die
258
+ Ausnahme („Kueche_Licht", „Wohnzimmer_Decke_Links"); und es bleibt ein
259
+ Vorschlag, sichtbar markiert und vor dem Schreiben zu aendern (Ricardo,
260
+ 19.09.2026). */
261
+ export function raumAusNamen(name, suche) {
262
+ var nt = String(name).split(/[_ .]+/).filter(function (x) { return x; });
263
+ for (var laenge = nt.length; laenge >= 1; laenge--) {
264
+ for (var start = nt.length - laenge; start >= 0; start--) {
265
+ var nr = suche(nt.slice(start, start + laenge).join('_'));
250
266
  if (nr) { return nr; }
251
267
  }
252
268
  }
package/admin/js/baum.js CHANGED
@@ -127,7 +127,14 @@ function baueBaum() {
127
127
 
128
128
  function zaehleZustaende(k, eltern) {
129
129
  k._eltern = eltern || null;
130
- var n = (k.art === 'state') ? 1 : 0;
130
+ /* Ein `state` mit eigenen Kindern ist ein Behaelter und zaehlt sich
131
+ selbst nicht mit — die Zahl am Knoten nennt, was DARUNTER liegt, wie
132
+ rechts die Liste. Vorher stand an `0_userdata.0.Bastelzimmer` eine 7,
133
+ waehrend rechts „6 Zustaende" und sechs Zeilen standen (Ricardo,
134
+ 20.09.2026; Lauf vom 19.09.2026, B1). Ein Blatt zaehlt weiter sich
135
+ selbst, sonst zaehlte kein Elternknoten mehr etwas. */
136
+ var hatKinder = Object.keys(k.kinder).length > 0;
137
+ var n = (k.art === 'state' && !hatKinder) ? 1 : 0;
131
138
  var tiefe = -1;
132
139
  Object.keys(k.kinder).forEach(function (name) {
133
140
  var kind = k.kinder[name];
@@ -399,7 +406,11 @@ function renderKinder(knoten, filter, tiefe) {
399
406
  });
400
407
  }
401
408
  d.appendChild(tw);
402
- var kk = el('span', 'kind', k.art ? k.art.slice(0, 3) : '·');
409
+ /* Ohne eigenes Objekt: „VIR“ statt eines Punkts. ioBroker selbst nennt
410
+ so eine Stufe `type: "virtual"` (web-Adapter, `/object` mit `depth`).
411
+ Gleich gestaltet wie DEV/CHA/FOL — die Reihe sieht einheitlich aus,
412
+ und wer es wissen will, liest es trotzdem ab (Ricardo, 19.09.2026). */
413
+ var kk = el('span', 'kind', k.art ? k.art.slice(0, 3) : 'vir');
403
414
  if (!k.art) { kk.title = tr('tree.noObjectHint'); }
404
415
  d.appendChild(kk);
405
416
  d.appendChild(el('span', 'idstueck', k.name));
@@ -4,8 +4,8 @@ import { S } from './zustand.js';
4
4
  import { socket } from './verbindung.js';
5
5
  import { el } from './basis.js';
6
6
  import { tr } from './sprache.js';
7
- import { wertVon, jsonFelder, feldAusFormel, feldFormel, feldWert, jsonVon, zielKann } from './werte.js';
8
- import { musterVon, erkenneEntwurf, platzFuerRolle, typenFuerRolle } from './erkennung.js';
7
+ import { wertVon, jsonFelder, feldAusFormel, feldFormel, feldWert, jsonVon, zielKann, knopfZeile } from './werte.js';
8
+ import { musterVon, erkenneEntwurf, platzFuerRolle, typenFuerRolle, rolleVonPlatz } from './erkennung.js';
9
9
  import { musterName } from './musternamen.js';
10
10
  import { rollenFeld } from './rollenwahl.js';
11
11
  import { opt , quellenAuswahl, vorlagenAbweichung, bestandsAbweichung, aboErgaenzen } from './entwurf.js';
@@ -106,9 +106,7 @@ export function detailZeile(e, s, idx) {
106
106
  if (!o || !o.common) { return null; }
107
107
  var c = o.common;
108
108
 
109
- var maengel = zielKann(s).filter(function (x) {
110
- return seite === 'w' ? x.art !== 'meldetNichts' : x.art === 'meldetNichts';
111
- });
109
+ var maengel = seite === 'w' ? zielKann(s) : [];
112
110
 
113
111
  var text, hinweis;
114
112
  if (seite === 'w') {
@@ -120,9 +118,13 @@ export function detailZeile(e, s, idx) {
120
118
  : (bl ? tr('detail.targetTypeVoid', c.type) : tr('detail.targetTypeHint', c.type));
121
119
  if (nw) { text = c.type + ' \u00b7 ' + tr('detail.notWritable'); }
122
120
  } else {
123
- if (!maengel.length) { return null; }
124
- text = tr('detail.reportsNothing');
125
- hinweis = tr('detail.targetNoRead');
121
+ /* Nur noch der Knopf, und ruhig: kein Mangel, eine Auskunft -
122
+ was `read: false` am Alias bewirkt und warum die Lesequelle
123
+ trotzdem da ist. Die Marke „aendert sich nie" an jeder Quelle
124
+ mit `read: false` ist seit 19.09.2026 weg (siehe `zielKann`). */
125
+ if (!knopfZeile(s)) { return null; }
126
+ text = tr('detail.readIsButton');
127
+ hinweis = tr('detail.readIsButtonHint');
126
128
  }
127
129
 
128
130
  var m = el('span', 'chip ' + (maengel.length ? 'warn' : 'mut'), text);
@@ -249,6 +251,121 @@ export function detailZeile(e, s, idx) {
249
251
  zeigeBestand(r, felder);
250
252
  }
251
253
 
254
+ /* Welcher Platz ist schon vergeben, und von welcher Zeile? Das steht
255
+ als Vermerk am Gruppenkopf der Rollenliste. */
256
+ var belegtVon = {};
257
+ /* Eigener Platz direkt aus dem Befund — ueber `belegtVon` allein fand
258
+ ihn nur die LETZTE Zeile eines mehrfach vergebenen Platzes (ACTUAL
259
+ bei `info`), alle anderen standen auf „kein Platz“. */
260
+ var eigenerPlatz = null;
261
+ if (e.want) {
262
+ var fundR = erkenneEntwurf(e, e.want);
263
+ if (fundR.length) {
264
+ fundR[0].states.forEach(function (x) {
265
+ if (!x.id) { return; }
266
+ var nx = x.id.slice(e.kanal.length + 1);
267
+ belegtVon[x.name] = nx;
268
+ if (nx === s.n) { eigenerPlatz = x.name; }
269
+ });
270
+ }
271
+ }
272
+ /* --- Platz im Muster ---
273
+
274
+ Umgekehrt zur Rolle: erst den Platz waehlen, und die Werkbank traegt
275
+ ein, was das Muster dafuer vorgibt — Rolle, Typ, Einheit, Werteliste.
276
+ Wer eine Zeile auf SET setzen wollte, musste bisher wissen, dass SET
277
+ `switch` heisst (Ricardo, 19.09.2026). Datenpunkt und Name bleiben;
278
+ dieselbe Vorgabe nimmt der Klick auf einen freien Platz unten, nur
279
+ dass der eine neue Zeile anlegt. */
280
+ var platzBox = null;
281
+ var mPl = e.want && musterVon(e.want);
282
+ if (mPl) {
283
+ var meinPlatz = eigenerPlatz;
284
+ var pb = el('div');
285
+ var selP = el('select', 'tx');
286
+ selP.style.width = '240px';
287
+ var oKein = opt('', tr('detail.slotNone'));
288
+ if (meinPlatz) { oKein.disabled = true; }
289
+ selP.appendChild(oKein);
290
+ var gesehen = {};
291
+ mPl.states.forEach(function (st) {
292
+ if (gesehen[st.name]) { return; }
293
+ gesehen[st.name] = 1;
294
+ var fremd = belegtVon[st.name] && belegtVon[st.name] !== s.n && !st.multiple;
295
+ var txt = st.name + (st.required ? ' · ' + tr('detail.slotRequired') : '') +
296
+ (fremd ? ' · ' + tr('roles.slotTaken', belegtVon[st.name]) : '');
297
+ var o = opt(st.name, txt);
298
+ if (fremd) { o.disabled = true; }
299
+ selP.appendChild(o);
300
+ });
301
+ /* Gewaehlt, aber nicht bekommen (etwa weil die Schreibrichtung nicht
302
+ passt): die Wahl bleibt im Feld stehen, darunter steht warum. */
303
+ var gewuenscht = !meinPlatz && s.platzWahl && gesehen[s.platzWahl] ? s.platzWahl : '';
304
+ selP.value = meinPlatz || gewuenscht;
305
+ selP.addEventListener('click', function (ev) { ev.stopPropagation(); });
306
+ selP.addEventListener('change', function () {
307
+ var st = null;
308
+ mPl.states.forEach(function (x) { if (!st && x.name === selP.value) { st = x; } });
309
+ if (!st) { return; }
310
+ s.platzWahl = st.name;
311
+ s.role = rolleVonPlatz(st);
312
+ var ty = Array.isArray(st.type) ? st.type[0] : st.type;
313
+ if (ty) { s.typ = ty; }
314
+ if (st.defaultUnit) { s.unit = st.defaultUnit; }
315
+ /* Ohne Werteliste bleibt so ein Platz leer, egal wie gut Rolle und
316
+ Typ passen — wie beim Klick auf einen freien Platz. */
317
+ if (st.statesDefined && !s.states) { s.states = st.defaultStates || { 0: 'None' }; }
318
+ /* Ein Platz, den das Muster nur lesen laesst (ELECTRIC_POWER,
319
+ ACTUAL …), bekommt kein Schreibziel. mqtt-client legt jeden Punkt
320
+ beschreibbar an, und die Werkbank trug deshalb die Quelle auch
321
+ bei „schreibt auf" ein — am 3D-Drucker schrieb die Leistung auf
322
+ `tele.SENSOR` (Ricardo, 19.09.2026). Weggenommen wird nur, nie
323
+ geraten: ein fehlendes Schreibziel bei SET setzt das Feld nicht. */
324
+ if (st.write === false) { s.srcW = ''; s.wr = false; }
325
+ neu();
326
+ });
327
+ pb.appendChild(selP);
328
+ /* Neben dem Namen ist kein Platz fuer einen Satz darunter — er steht
329
+ als Tooltip am Feld. */
330
+ selP.title = tr('detail.slotHint');
331
+ /* Gewaehlt, aber nicht bekommen — warum?
332
+
333
+ Die Schreibrichtung ist es fast nie: traegt ein Punkt genau die
334
+ Vorgaberolle des Platzes, prueft der type-detector `read`/`write`
335
+ gar nicht (ChannelDetector: „When the default role is assigned …
336
+ we can be a bit more laxe"). Und das Feld setzt immer die
337
+ Vorgaberolle. Der erste Hinweis an dieser Stelle behauptete
338
+ deshalb etwas Falsches (Ricardo, 19.09.2026: an tele.SENSOR mit
339
+ Schreibziel blieb ELECTRIC_POWER vergeben, ohne Hinweis — zu
340
+ Recht). Der haeufige Grund ist ein anderer: das Muster greift als
341
+ Ganzes nicht, weil ein Pflichtplatz fehlt — dann vergibt der
342
+ Detektor gar keinen Platz, auch diesen nicht. */
343
+ /* Greift das Muster als Ganzes nicht, steht das in der Legende ueber
344
+ der Liste — hier waere es an der falschen Zeile. */
345
+ if (gewuenscht && Object.keys(belegtVon).length > 0) {
346
+ pb.appendChild(el('div', 'aside w', tr('detail.slotNotGiven', gewuenscht)));
347
+ }
348
+ platzBox = pb;
349
+ }
350
+
351
+ /* Der Platz steht rechts neben dem Namen, mit seiner Bezeichnung links
352
+ daneben wie eine Zeilenbeschriftung: man waehlt ihn am Anfang, und
353
+ weiter unten musste man ihn suchen (Ricardo, 19.09.2026). */
354
+ function mitPlatz(inhalt) {
355
+ if (!platzBox) { return inhalt; }
356
+ var w = el('div', 'fields');
357
+ w.style.alignItems = 'flex-start';
358
+ w.appendChild(inhalt);
359
+ var pf = el('div');
360
+ pf.style.display = 'flex';
361
+ pf.style.gap = '8px';
362
+ pf.style.alignItems = 'flex-start';
363
+ pf.appendChild(el('div', 'k', tr('detail.slot')));
364
+ pf.appendChild(platzBox);
365
+ w.appendChild(pf);
366
+ return w;
367
+ }
368
+
252
369
  /* --- Name, nur bei selbst angelegten --- */
253
370
  if (s.manuell) {
254
371
  var iN = el('input', 'tx w');
@@ -261,24 +378,76 @@ export function detailZeile(e, s, idx) {
261
378
  s.n = iN.value.trim().toUpperCase().replace(/\s+/g, '_');
262
379
  neu();
263
380
  });
264
- zeile(tr('detail.name'), iN);
381
+ zeile(tr('detail.name'), mitPlatz(iN));
265
382
  } else if (s.urId) {
266
- zeile(tr('detail.objectId'), s.urId);
383
+ zeile(tr('detail.objectId'), mitPlatz(el('div', null, s.urId)));
267
384
  } else {
268
- zeile(tr('detail.willBecome'), (e.ziel || e.kanal) + '.' + s.n);
385
+ /* Der letzte Teil ist ein Feld: der Name bestimmt nur die Kennung,
386
+ nicht was der Punkt tut — das macht die Rolle. Wer `cmnd_POWER`
387
+ lieber `POWER` oder `SET` nennen will, konnte das bisher nirgends
388
+ (Ricardo, 19.09.2026). Nur hier, beim Neuanlegen: an einem
389
+ bestehenden Punkt (`urId`, oben) haengen Skripte und vis am Namen.
390
+
391
+ Bereinigt wie die Zielkennung (zielleiste.js), aber ohne Punkt —
392
+ ein Punkt machte aus dem Namen einen Unterordner. Gross- und
393
+ Kleinschreibung bleiben, wie sie getippt wurden. */
394
+ /* Aufgebaut wie „liest aus": oben das Feld, darunter klein die
395
+ volle Kennung, die daraus entsteht (Ricardo, 19.09.2026). */
396
+ var vorspann = (e.ziel || e.kanal) + '.';
397
+ var iNa = el('input', 'tx');
398
+ iNa.type = 'text';
399
+ iNa.value = s.n;
400
+ iNa.style.width = '260px';
401
+ iNa.style.maxWidth = '100%';
402
+ iNa.style.fontFamily = 'var(--mono)';
403
+ var nameFelder = el('div', 'fields');
404
+ nameFelder.appendChild(iNa);
405
+ var nameStapel = mitVollId(nameFelder, vorspann + s.n);
406
+ var nameVoll = nameStapel.querySelector('.vollid');
407
+ var bereinigt = function (v) {
408
+ return v.trim().replace(/\s+/g, '_').replace(/[^\w\-äöüÄÖÜß]/g, '_');
409
+ };
410
+ iNa.addEventListener('click', function (ev) { ev.stopPropagation(); });
411
+ /* Beim Tippen zieht die Kennung darunter mit — man sieht, was
412
+ entsteht, bevor man das Feld verlaesst. */
413
+ iNa.addEventListener('input', function () {
414
+ var v = bereinigt(iNa.value) || s.n;
415
+ nameVoll.textContent = vorspann + v;
416
+ nameVoll.title = vorspann + v;
417
+ });
418
+ iNa.addEventListener('change', function () {
419
+ var neuN = bereinigt(iNa.value);
420
+ if (!neuN) { iNa.value = s.n; return; }
421
+ if (neuN !== s.n && e.states.some(function (x) { return x !== s && x.n === neuN; })) {
422
+ iNa.value = s.n;
423
+ iNa.title = tr('detail.nameTaken', neuN);
424
+ iNa.classList.add('miss');
425
+ nameVoll.textContent = vorspann + s.n;
426
+ return;
427
+ }
428
+ s.n = neuN;
429
+ neu();
430
+ });
431
+ zeile(tr('detail.name'), mitPlatz(nameStapel));
269
432
  }
270
433
 
271
434
  /* --- Quelle lesen + JSON-Feld --- */
272
435
  var qf = el('div', 'fields');
273
436
 
274
437
  var selR = el('select', 'tx');
275
- selR.style.width = '260px';
438
+ /* 420 statt 260 px: gekuerzt wird hinten, und dort steht, was den
439
+ Punkt unterscheidet — `…3D_Drucker.` ohne `cmnd.POWER` sagte nichts
440
+ (Ricardo, 19.09.2026). `maxWidth` haelt es in schmalen Fenstern. */
441
+ selR.style.width = '420px';
442
+ selR.style.maxWidth = '100%';
276
443
  selR.appendChild(opt('', quellen.length ? tr('detail.pickOne') : tr('detail.noSource')));
277
444
  quellenIn(selR, quellen);
278
445
  if (s.srcR && quellen.indexOf(s.srcR) === -1) { selR.appendChild(opt(s.srcR, s.srcR + ' ' + tr('detail.elsewhere'))); }
279
446
  selR.appendChild(opt('__frei__', tr('detail.otherObject')));
280
447
  selR.value = s.srcR || '';
281
- if (!s.srcR) { selR.className += ' miss'; }
448
+ /* Ein Knopf darf ohne Lesequelle sein - dann ist das leere Feld kein
449
+ Fehler und wird nicht rot (Ricardo, 19.09.2026). */
450
+ if (!s.srcR && !knopfZeile(s)) { selR.className += ' miss'; }
282
451
  selR.addEventListener('click', function (ev) { ev.stopPropagation(); });
283
452
  selR.addEventListener('change', function () {
284
453
  if (selR.value === '__frei__') { s.frei = true; s.srcR = ''; }
@@ -299,7 +468,6 @@ export function detailZeile(e, s, idx) {
299
468
  neu();
300
469
  });
301
470
  var lR = el('label', 'fld');
302
- lR.appendChild(el('span', null, tr('detail.datapoint')));
303
471
  lR.appendChild(selR);
304
472
  qf.appendChild(lR);
305
473
 
@@ -344,10 +512,31 @@ export function detailZeile(e, s, idx) {
344
512
  zeile('Objekt-ID', iFrei);
345
513
  }
346
514
 
515
+ /* --- Leseformel ---
516
+
517
+ Direkt unter „liest aus“, wie die Schreibformel unter „schreibt auf“:
518
+ jede Formel steht bei der Quelle, fuer die sie rechnet (Ricardo,
519
+ 19.09.2026). */
520
+ var fb = el('div');
521
+ var iF = el('input', 'tx w');
522
+ iF.type = 'text';
523
+ iF.value = s.f || '';
524
+ iF.placeholder = tr('detail.formulaPlaceholder');
525
+ iF.addEventListener('click', function (ev) { ev.stopPropagation(); });
526
+ iF.addEventListener('change', function () { s.f = iF.value.trim(); neu(); });
527
+ fb.appendChild(iF);
528
+ /* Der Hinweis gilt nur fuer Schaltpunkte. Bei einem Spannungswert
529
+ stand er auch — und ergab dort keinen Sinn. */
530
+ if ((s.typ || '') === 'boolean') {
531
+ fb.appendChild(el('div', 'sugg', tr('detail.formulaHint')));
532
+ }
533
+ zeigeBeide(zeile(tr('detail.formula'), fb), ['f']);
534
+
347
535
  /* --- Quelle schreiben --- */
348
536
  var wf = el('div', 'fields');
349
537
  var selW = el('select', 'tx');
350
- selW.style.width = '260px';
538
+ selW.style.width = '420px';
539
+ selW.style.maxWidth = '100%';
351
540
  /* Leer heisst „schreibt nicht" - und muss auch so heissen. Frueher
352
541
  stand hier „dieselbe wie lesen", also das Gegenteil dessen, was
353
542
  passierte (Ricardo, 25.08.2026). Wer wirklich auf die Lesequelle
@@ -361,34 +550,40 @@ export function detailZeile(e, s, idx) {
361
550
  selW.addEventListener('click', function (ev) { ev.stopPropagation(); });
362
551
  selW.addEventListener('change', function () {
363
552
  s.srcW = (selW.value === '=lesen') ? (s.srcR || '') : selW.value;
553
+ /* Wer hier waehlt, hat die Schreibrichtung entschieden. `wr` stammt
554
+ sonst vom Quellobjekt (`common.write`) und hielt die Zeile fuer den
555
+ Detektor beschreibbar, auch nach „nicht schreiben" — angelegt wird
556
+ aber nach `srcW` (`write: !!s.srcW`). Die Vorschau sagte also
557
+ „Platz bleibt frei", der fertige Alias haette ihn bekommen
558
+ (Ricardo, 19.09.2026, an 3D_Drucker tele.SENSOR). */
559
+ s.wr = !!s.srcW;
364
560
  neu();
365
561
  });
366
562
  var lW = el('label', 'fld');
367
- lW.appendChild(el('span', null, tr('detail.datapoint')));
368
563
  lW.appendChild(selW);
369
564
  wf.appendChild(lW);
370
565
  /* Der Hinweis gilt nur, wenn die beiden Quellen wirklich
371
566
  auseinandergehen - bei gleicher Quelle ist nichts getrennt. */
372
- if (s.srcW && s.srcW !== s.srcR) {
373
- wf.appendChild(el('div', 'sugg', tr('detail.separateHint')));
567
+ /* Unter die Kennung, nicht daneben: seit die Auswahl 420 px breit ist,
568
+ brach der Satz um und schob sich mit einer Luecke zwischen Auswahl und
569
+ Kennung (Ricardo, 19.09.2026). */
570
+ var wStapel = mitVollId(wf, s.srcW, zielMarke(s, 'w'));
571
+ /* Nur-lesen-Platz mit Schreibziel: der Detektor sieht es bei der
572
+ Vorgaberolle nicht so streng, gemeint ist es aber so — ein Messwert,
573
+ den jemand setzt, geht ans Geraet. */
574
+ var platzDef = null;
575
+ if (mPl && eigenerPlatz) {
576
+ mPl.states.forEach(function (x) { if (!platzDef && x.name === eigenerPlatz) { platzDef = x; } });
374
577
  }
375
- zeigeBeide(zeile(tr('detail.writesTo'), mitVollId(wf, s.srcW, zielMarke(s, 'w'))), ['srcW']);
376
-
377
- /* --- Leseformel --- */
378
- var fb = el('div');
379
- var iF = el('input', 'tx w');
380
- iF.type = 'text';
381
- iF.value = s.f || '';
382
- iF.placeholder = tr('detail.formulaPlaceholder');
383
- iF.addEventListener('click', function (ev) { ev.stopPropagation(); });
384
- iF.addEventListener('change', function () { s.f = iF.value.trim(); neu(); });
385
- fb.appendChild(iF);
386
- /* Der Hinweis gilt nur fuer Schaltpunkte. Bei einem Spannungswert
387
- stand er auch — und ergab dort keinen Sinn. */
388
- if ((s.typ || '') === 'boolean') {
389
- fb.appendChild(el('div', 'sugg', tr('detail.formulaHint')));
578
+ if (platzDef && platzDef.write === false && s.srcW) {
579
+ wStapel.appendChild(el('div', 'aside w', tr('detail.readOnlySlotWrites', platzDef.name)));
390
580
  }
391
- zeigeBeide(zeile(tr('detail.formula'), fb), ['f']);
581
+ /* Nur, wo wirklich woanders gelesen wird - ohne Lesequelle liest die
582
+ Zeile nirgends, und der Satz stimmte nicht. */
583
+ if (s.srcR && s.srcW && s.srcW !== s.srcR) {
584
+ wStapel.appendChild(el('div', 'sugg', tr('detail.separateHint')));
585
+ }
586
+ zeigeBeide(zeile(tr('detail.writesTo'), wStapel), ['srcW']);
392
587
 
393
588
  if (s.srcW) {
394
589
  var wb = el('div');
@@ -405,17 +600,7 @@ export function detailZeile(e, s, idx) {
405
600
 
406
601
  /* --- Rolle, mit Begründung --- */
407
602
  var rb = el('div');
408
- /* Welcher Platz ist schon vergeben, und von welcher Zeile? Das steht
409
- als Vermerk am Gruppenkopf der Rollenliste. */
410
- var belegtVon = {};
411
- if (e.want) {
412
- var fundR = erkenneEntwurf(e, e.want);
413
- if (fundR.length) {
414
- fundR[0].states.forEach(function (x) {
415
- if (x.id) { belegtVon[x.name] = x.id.slice(e.kanal.length + 1); }
416
- });
417
- }
418
- }
603
+
419
604
  rb.appendChild(rollenFeld({
420
605
  wert: s.role,
421
606
  muster: e.want,
@@ -428,6 +613,9 @@ export function detailZeile(e, s, idx) {
428
613
  die Rolle erneut wechselt. */
429
614
  beiWahl: function (r) {
430
615
  s.role = r;
616
+ /* Eine Rolle von Hand ersetzt eine fruehere Platzwahl — sonst
617
+ stuende ein Hinweis zu einem Platz da, den keiner mehr will. */
618
+ delete s.platzWahl;
431
619
  /* Nur wo die Rolle im Muster genau einen Datentyp zulaesst, gibt
432
620
  es etwas vorzugeben. Trifft sie mehrere Plaetze mit
433
621
  verschiedenen Typen - wie die Fahrtrichtung boolean und number -
@@ -443,22 +631,41 @@ export function detailZeile(e, s, idx) {
443
631
  var treffer = platzFuerRolle(e.want, s.role, s.typ);
444
632
  var erlaubt = typenFuerRolle(e.want, s.role);
445
633
  var hin = el('div', 'sugg');
634
+ /* Hat den Platz schon eine ANDERE Zeile, hilft weder der Typ noch die
635
+ Rolle — es gibt ihn nur einmal. Am KeinJson-Testgeraet hielt
636
+ `cmnd_POWER` SET, und unter `stat_POWER` (Rolle `switch` von
637
+ mqtt-client, Typ string) stand „SET verlangt boolean … so faellt der
638
+ Punkt aus dem Muster" — als muesste man nur den Typ stellen. Richtig
639
+ waere ACTUAL gewesen (Ricardo, 19.09.2026). */
640
+ var besetztVon = treffer && !treffer.multiple && belegtVon[treffer.name] &&
641
+ belegtVon[treffer.name] !== s.n ? belegtVon[treffer.name] : null;
446
642
  if (treffer) {
447
643
  hin.appendChild(document.createTextNode(tr('pattern.fitsOn')));
448
644
  hin.appendChild(el('b', null, String(treffer.role)));
449
645
  hin.appendChild(document.createTextNode(tr('pattern.toSlot', treffer.name, musterName(e.want) || e.want)));
450
- if (erlaubt.length) {
646
+ if (!besetztVon && erlaubt.length) {
451
647
  hin.appendChild(document.createTextNode(
452
648
  tr('pattern.expectsType', erlaubt.join(tr('pattern.typeOr')))));
453
649
  }
454
- } else {
650
+ } else if (!eigenerPlatz) {
455
651
  hin.textContent = tr('pattern.noPlaceLong', musterName(e.want) || e.want);
456
652
  }
457
- rb.appendChild(hin);
653
+ /* Findet die Rollenliste nichts, hat die Zeile aber doch einen Platz,
654
+ steht hier nichts: das sagt schon „Platz im Muster“ oben. Der Fall
655
+ ist der Auffangplatz ACTUAL bei `info` — er verlangt keine Rolle und
656
+ steht deshalb in keiner Rollenliste. Hier stand vorher „kein Platz im
657
+ Information-Muster … Alexa & Co. zählen ihn separat“, direkt unter
658
+ einem Feld, das ACTUAL zeigte (Ricardo, 19.09.2026). */
659
+ if (hin.childNodes.length) { rb.appendChild(hin); }
458
660
  /* Passt der Typ nicht zum Platz, faellt der Punkt aus dem Muster -
459
661
  lautlos, denn die Rolle stimmt ja. Genau das ist am 11.09.2026
460
662
  an einem Tasmota-Stromzaehler passiert (I33). */
461
- if (treffer && s.typ && erlaubt.length && erlaubt.indexOf(s.typ) === -1) {
663
+ /* Gelb wie die Typ-Warnung: in beiden Faellen bekommt die Zeile keinen
664
+ Platz. Als grauer Nachsatz ging es unter (Ricardo, 19.09.2026). */
665
+ if (besetztVon) {
666
+ rb.appendChild(el('div', 'aside w', tr('pattern.slotHeldBy', treffer.name, besetztVon)));
667
+ }
668
+ if (treffer && !besetztVon && s.typ && erlaubt.length && erlaubt.indexOf(s.typ) === -1) {
462
669
  rb.appendChild(el('div', 'aside w',
463
670
  tr('pattern.typeMismatch', treffer.name, erlaubt.join(tr('pattern.typeOr')), s.typ)));
464
671
  }
@@ -529,48 +736,56 @@ export function detailZeile(e, s, idx) {
529
736
 
530
737
  /* --- Rohwert und Ergebnis --- */
531
738
  var a = wertVon(s);
532
- zeile(tr('detail.rawValue'), a.roh === undefined ? tr('detail.rawNothing')
533
- : (typeof a.roh === 'object' ? JSON.stringify(a.roh) : String(a.roh)), 'raw');
534
-
535
- if (s.srcR && !S.objects[s.srcR]) {
536
- var wbox = el('div');
537
- wbox.appendChild(el('span', 'chip bad', tr('detail.sourceGone')));
538
- zeile(tr('detail.attention'), wbox);
739
+ /* Ein Knopf ohne Lesequelle hat nichts anzuzeigen - das ist gewollt
740
+ und kein „liefert nichts" in Rot. */
741
+ if (!s.srcR && knopfZeile(s)) {
742
+ zeile(tr('detail.yields'), el('span', 'chip mut', tr('detail.buttonNoRead')));
743
+ a = null;
539
744
  }
540
-
541
- var box = el('div');
542
- var rv = el('span', 'res', a.ok
543
- ? (typeof a.val === 'object' ? JSON.stringify(a.val) : String(a.val)) + (s.unit ? ' ' + s.unit : '')
544
- : a.txt);
545
- if (!a.ok) { rv.style.color = 'var(--bad)'; }
546
- box.appendChild(rv);
547
- box.appendChild(document.createTextNode(' '));
548
- if (a.ok) {
549
- var t = typeof a.val;
550
- var passt = !s.typ || s.typ === t || s.typ === 'mixed';
551
- box.appendChild(el('span', 'chip ' + (passt ? 'ok' : 'warn'),
552
- (passt ? '✓ ' : '≠ ') + (t === 'number' ? tr('detail.typeNumber') : t)));
553
- if (a.gewandelt) {
554
- box.appendChild(document.createTextNode(' '));
555
- box.appendChild(el('span', 'chip mut', tr('detail.converted')));
556
- }
557
- if (!passt) {
558
- box.appendChild(document.createTextNode(' '));
559
- box.appendChild(el('span', 'chip warn', tr('detail.expects', s.typ)));
745
+ if (a) {
746
+ zeile(tr('detail.rawValue'), a.roh === undefined ? tr('detail.rawNothing')
747
+ : (typeof a.roh === 'object' ? JSON.stringify(a.roh) : String(a.roh)), 'raw');
748
+
749
+ if (s.srcR && !S.objects[s.srcR]) {
750
+ var wbox = el('div');
751
+ wbox.appendChild(el('span', 'chip bad', tr('detail.sourceGone')));
752
+ zeile(tr('detail.attention'), wbox);
560
753
  }
561
- var ist = s.urId ? S.werte[s.urId] : null;
562
- if (ist && ist.val !== undefined && ist.val !== null) {
563
- var gleich = String(ist.val) === String(a.val);
564
- box.appendChild(document.createTextNode(' '));
565
- box.appendChild(el('span', 'chip ' + (gleich ? 'ok' : 'warn'),
566
- gleich ? tr('detail.matchesState') : tr('detail.stateSays', String(ist.val))));
754
+
755
+ var box = el('div');
756
+ var rv = el('span', 'res', a.ok
757
+ ? (typeof a.val === 'object' ? JSON.stringify(a.val) : String(a.val)) + (s.unit ? ' ' + s.unit : '')
758
+ : a.txt);
759
+ if (!a.ok) { rv.style.color = 'var(--bad)'; }
760
+ box.appendChild(rv);
761
+ box.appendChild(document.createTextNode(' '));
762
+ if (a.ok) {
763
+ var t = typeof a.val;
764
+ var passt = !s.typ || s.typ === t || s.typ === 'mixed';
765
+ box.appendChild(el('span', 'chip ' + (passt ? 'ok' : 'warn'),
766
+ (passt ? '✓ ' : '≠ ') + (t === 'number' ? tr('detail.typeNumber') : t)));
767
+ if (a.gewandelt) {
768
+ box.appendChild(document.createTextNode(' '));
769
+ box.appendChild(el('span', 'chip mut', tr('detail.converted')));
770
+ }
771
+ if (!passt) {
772
+ box.appendChild(document.createTextNode(' '));
773
+ box.appendChild(el('span', 'chip warn', tr('detail.expects', s.typ)));
774
+ }
775
+ var ist = s.urId ? S.werte[s.urId] : null;
776
+ if (ist && ist.val !== undefined && ist.val !== null) {
777
+ var gleich = String(ist.val) === String(a.val);
778
+ box.appendChild(document.createTextNode(' '));
779
+ box.appendChild(el('span', 'chip ' + (gleich ? 'ok' : 'warn'),
780
+ gleich ? tr('detail.matchesState') : tr('detail.stateSays', String(ist.val))));
781
+ }
782
+ } else if (a.fehler) {
783
+ box.appendChild(el('span', 'chip bad', tr('detail.formulaBroken')));
784
+ } else {
785
+ box.appendChild(el('span', 'chip bad', tr('detail.yieldsNothing')));
567
786
  }
568
- } else if (a.fehler) {
569
- box.appendChild(el('span', 'chip bad', tr('detail.formulaBroken')));
570
- } else {
571
- box.appendChild(el('span', 'chip bad', tr('detail.yieldsNothing')));
787
+ zeile(tr('detail.yields'), box);
572
788
  }
573
- zeile(tr('detail.yields'), box);
574
789
 
575
790
  if (s.manuell) {
576
791
  var db = el('div');