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.
@@ -9,7 +9,7 @@ import { txt, tr } from './sprache.js';
9
9
  import './enums.js';
10
10
  import { zusatzName, setzeModus, zeichneBaum, merkeKlappstand } from './baum.js';
11
11
  import { kindZustaende, aliasQuellen, holeWerte,
12
- mitVorspann, schreibQuelle, zeileAusAlias
12
+ mitVorspann, schreibQuelle, zeileAusAlias, istTaster as istTasterObjekt
13
13
  } from './werte.js';
14
14
  import { erkenneEntwurf } from './erkennung.js';
15
15
  import { vorschlag , pruefeVorlage, zeigeAbozahl, setzeInstanz } from './vorlagen.js';
@@ -89,9 +89,7 @@ export function baueEntwurf(kanal) {
89
89
  QUELLE direkt auf 30 gesetzt — der Alias zeigte 30. `read: false`
90
90
  sperrt das Lesen also nicht, es sagt nur, dass von allein nichts
91
91
  kommt. Wer den Wert setzt, will ihn auch sehen. */
92
- var nat = o.native || {};
93
- var istTaster = (c.read === false && !!c.write) &&
94
- (nat.TYPE === 'ACTION' || /^button/.test(String(c.role || '')));
92
+ var istTaster = istTasterObjekt(o);
95
93
  e.states.push({
96
94
  /* Bei einer Quelle wird aus stat.POWER der Zustand stat_POWER —
97
95
  ein Punkt im Namen wuerde sonst eine Unterebene im Alias
@@ -110,7 +108,12 @@ export function baueEntwurf(kanal) {
110
108
  dann „keine Rolle" und „nichts erkannt". Typ und Einheit hat sie
111
109
  von jeher uebernommen; die Rolle auszunehmen war nicht zu
112
110
  begruenden. Ueberschreiben laesst sie sich weiterhin. */
113
- role: c.role || '',
111
+ /* Ein Tastendruck laut Geraetebeschreibung bekommt die Knopfrolle,
112
+ wenn der Adapter ihm keine gegeben hat - an ihr haengt `read:
113
+ false` am Alias (`knopfZeile`). hm-rpc gibt `OLD_LEVEL`
114
+ `value.dimmer` und `INSTALL_TEST` `indicator`; die uebrigen
115
+ (PRESS_SHORT, STOP, RAMP_STOP) tragen schon `button…`. */
116
+ role: (istTaster && !/^button/.test(String(c.role || ''))) ? 'button' : (c.role || ''),
114
117
  typ: c.type || '',
115
118
  unit: c.unit || '',
116
119
  /* Ohne die Werteliste bleibt jeder Platz mit statesDefined leer —
@@ -150,8 +153,13 @@ export function baueEntwurf(kanal) {
150
153
  Schalters, der ewig auf „aus" steht. Kann der Punkt auch nicht
151
154
  schreiben, bleibt er, wie er ist: dann ist die fehlende Meldung
152
155
  wirklich ein Mangel, und die Marke sagt es. */
153
- srcR: istAlias ? ((c.read === false && q.einfach) ? '' : (q.read || ''))
154
- : (istTaster ? '' : id),
156
+ /* Seit 19.09.2026 bekommt auch der Taster seine Lesequelle: der
157
+ Alias sieht dann jeden Druck, auch einen von aussen. Zum Knopf
158
+ wird er trotzdem - `read: false` haengt jetzt an der Rolle
159
+ (`knopfZeile` in werte.js), nicht mehr an der leeren Lesequelle.
160
+ Was oben ueber `read: false` an der Quelle steht, ist damit
161
+ Geschichte: gemessen liest ein solcher Alias tadellos. */
162
+ srcR: istAlias ? (q.read || '') : id,
155
163
  /* Steht in `common.alias.id` ein einzelner Text, gilt er fuer Lesen
156
164
  UND Schreiben - dann ist die Schreibquelle dieselbe wie die
157
165
  Lesequelle. Hier stand frueher eine leere Zeichenkette, gedacht
@@ -406,7 +414,11 @@ export function quelleVon(aliasId) {
406
414
  kindZustaende(aliasId).forEach(function (id) {
407
415
  var qq = aliasQuellen(S.objects[id]);
408
416
  [qq.read, qq.write].forEach(function (x) {
409
- if (!x) { return; }
417
+ /* Nur Quellen, die es gibt. Zeigt jeder Punkt ins Leere, fand der
418
+ Rueckfall darunter („eine Ebene hoeher") den naechsten VORHANDENEN
419
+ Ordner - und „zur Quelle" sprang auf `0_userdata.0.wbfake` mit 31
420
+ fremden Geraeten statt auf die verschwundene Quelle (D10). */
421
+ if (!x || !S.objects[x]) { return; }
410
422
  var t = String(x).split('.');
411
423
  t.pop();
412
424
  var eltern = t.join('.');
@@ -1204,9 +1216,13 @@ export function geraeteDarunter(wurzel) {
1204
1216
  }
1205
1217
  });
1206
1218
 
1207
- geraete.sort(function (a, b) { return a.id < b.id ? -1 : 1; });
1219
+ /* Angezeigt wird wie im Baum: `seg_inf.10` hinter `seg_inf.9`, nicht
1220
+ hinter `seg_inf.1` (E45). Die Liste dient nur der Anzeige — die
1221
+ byteweise Ordnung von `keysSorted` bleibt unberuehrt (E42). */
1222
+ geraete.sort(function (a, b) { return GERAETE_VERGLEICH.compare(a.id, b.id); });
1208
1223
  return geraete;
1209
1224
  }
1225
+ var GERAETE_VERGLEICH = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
1210
1226
 
1211
1227
  /* Gibt es fuer diese Quelle schon einen Alias?
1212
1228
 
@@ -1225,11 +1241,17 @@ export function geraeteDarunter(wurzel) {
1225
1241
  `aliasFuer` lief zweimal ueber alle Kennungen — und wird je
1226
1242
  Neuzeichnen mehrfach gerufen. Die Karte entsteht bei der ersten Frage
1227
1243
  nach einem Indexwechsel und beantwortet alle weiteren aus dem
1228
- Gedaechtnis. Erkannt wird der Wechsel an Laenge und erster Kennung;
1229
- `indexNeu()` sortiert ohnehin bei jeder Aenderung neu. */
1230
- var alsQuelleKarte = null, alsQuelleStand = '';
1244
+ Gedaechtnis.
1245
+
1246
+ Erkannt wurde der Wechsel bis 19.09.2026 an Laenge und erster Kennung.
1247
+ Das reicht nicht: Verlegen loescht acht Objekte und legt acht neue an,
1248
+ die Zahl bleibt gleich, und ein Quellentausch aendert nur `alias.id`.
1249
+ Die Karte zeigte danach auf den geloeschten Alias, an der Quelle stand
1250
+ „wird neu angelegt", und ein Klick haette einen zweiten Alias erzeugt
1251
+ (T22). Jetzt zaehlt `S.objektStand` jede Aenderung an den Objekten. */
1252
+ var alsQuelleKarte = null, alsQuelleStand = -1;
1231
1253
  function alsQuelleKarten() {
1232
- var stand = S.keysSorted.length + '|' + (S.keysSorted[0] || '');
1254
+ var stand = S.objektStand;
1233
1255
  if (alsQuelleKarte && alsQuelleStand === stand) { return alsQuelleKarte; }
1234
1256
  var fest = {}, zweig = {};
1235
1257
  mitVorspann('alias.').forEach(function (id) {
@@ -1513,7 +1535,15 @@ export function vorlageAnwenden(e) {
1513
1535
  var anders = vorlagenAbweichung(s).length > 0;
1514
1536
  s.role = v.role; s.typ = v.typ; s.unit = v.unit;
1515
1537
  s.f = v.f; s.fw = v.fw; s.srcR = v.srcR; s.srcW = v.srcW;
1516
- s.states = v.states;
1538
+ /* Die Werteliste nur, wenn die Vorlage selbst eine mitbringt. Sagt sie
1539
+ nichts dazu, gilt der Bestand — dieselbe Regel wie bei Rolle und Typ
1540
+ in `bestandVorrang`. Vorher stand hier `s.states = v.states`, und an
1541
+ einer Vorlagenzeile ohne Werteliste hiess das `undefined`: am
1542
+ Rollladen verlor DIRECTION {0: NONE, 1: UP, 2: DOWN}, ohne dass
1543
+ Zaehler oder Marke es zeigten — `vorlagenAbweichung` vergleicht die
1544
+ Werteliste nicht, und die Werkbank hat kein Feld dafuer (Lauf vom
1545
+ 19.09.2026, G10). */
1546
+ if (v.states !== undefined) { s.states = v.states; }
1517
1547
  /* caption bleibt: der Anzeigename gehoert dem Nutzer. */
1518
1548
  if (anders) { s.geaendert = true; }
1519
1549
  });
package/admin/js/enums.js CHANGED
@@ -32,6 +32,11 @@ export function enumsUnbekannt() { return unbekannt; }
32
32
  var versuch = 0;
33
33
  var wiederTimer = null;
34
34
 
35
+ /* Wer wissen will, wann ein Nachversuch geklappt hat - die Felder fuer
36
+ Raum und Funktion zeichnen sich dann neu (P8). */
37
+ var geladenHoerer = [];
38
+ export function beiEnumsGeladen(fn) { geladenHoerer.push(fn); }
39
+
35
40
  export function ladeEnums(danach) {
36
41
  if (wiederTimer) { clearTimeout(wiederTimer); wiederTimer = null; }
37
42
  socket.emit('getObjectView', 'system', 'enum',
@@ -59,8 +64,10 @@ export function ladeEnums(danach) {
59
64
  if (r.value && r.id) { frisch[r.id] = r.value; }
60
65
  });
61
66
  enums = frisch;
67
+ var warUnbekannt = unbekannt;
62
68
  unbekannt = false;
63
69
  versuch = 0;
70
+ if (warUnbekannt) { geladenHoerer.forEach(function (fn) { fn(); }); }
64
71
  if (typeof danach === 'function') { danach(null); }
65
72
  });
66
73
  }
@@ -358,6 +358,15 @@ var warumAuf = false;
358
358
  if (!sprungZiel) {
359
359
  var vermerkt = S.objects[S.current] && S.objects[S.current].native && S.objects[S.current].native.quelle;
360
360
  if (vermerkt) { sub.appendChild(el('span', 'chip warn', tr('result.sourceGone', vermerkt))); }
361
+ /* Der Knopf bleibt sichtbar, aber gesperrt: verschwaende er, suchte
362
+ man ihn; klickbar sprang er frueher irgendwohin (D10). */
363
+ var wohin = vermerkt || (insLeere[0] && insLeere[0].srcR) || '';
364
+ if (wohin) {
365
+ sprung = el('button', 'btn schmal sprungknopf', tr('result.toSource') + ' →');
366
+ sprung.disabled = true;
367
+ sprung.title = tr('result.sourceGone', wohin);
368
+ namensZeile.appendChild(sprung);
369
+ }
361
370
  }
362
371
  } else {
363
372
  sprungZiel = aliasFuer(S.current);
@@ -509,7 +518,13 @@ var warumAuf = false;
509
518
  /* Der Vorlagenkasten: welche Vorlage gegriffen hat, warum, ihre
510
519
  Wahl und Knoepfe. Nimmt die Musterwahl bei sich auf; ohne Kasten
511
520
  bleibt sie im Kopf - bis die Leiste sie sich holt. */
512
- function baueVorlagenkasten(host, head, e, istQuelle, musterBlock) {
521
+ /* Zurueck kommt, was noch keinen Platz hat: der Musterblock (ohne
522
+ Kasten) und der Knopf fuer die Leiste ueber der Liste. Im Kasten
523
+ sitzt seit 19.09.2026 „Freie Plaetze vorschlagen"; der Knopf, der
524
+ den ganzen Bestand des Geraets laedt, wandert dafuer in die Leiste -
525
+ er aendert die Liste, nicht die Erkennung (Ricardo). */
526
+ function baueVorlagenkasten(host, head, e, istQuelle, musterBlock, rateKnopf) {
527
+ var leistenKnopf = rateKnopf;
513
528
  if (e.vorschlag || istQuelle) {
514
529
  var sg = el('div', 'suggestbox');
515
530
  if (!e.vorschlag) {
@@ -760,7 +775,6 @@ var warumAuf = false;
760
775
  der nur die vorgeschlagenen Zeilen ein- und ausblendet. Deshalb
761
776
  steht das Geraet im Namen und der Rest im Hinweis. */
762
777
  bRoh.title = e.roh ? tr('tpl.showProposalHelp') : tr('tpl.showAllPointsHelp');
763
- if (!e.vorschlag) { bRoh.disabled = true; }
764
778
  bRoh.addEventListener('click', function () {
765
779
  var rohJetzt = !e.roh;
766
780
  /* Dieselben zwei Schritte wie in `waehle`: die Vorlage, aus der der
@@ -855,8 +869,12 @@ var warumAuf = false;
855
869
  knoepfe.appendChild(bTpl);
856
870
  }
857
871
  }
858
- knoepfe.appendChild(bRoh);
859
- acts.appendChild(knoepfe);
872
+ if (rateKnopf) { knoepfe.appendChild(rateKnopf); }
873
+ if (knoepfe.childNodes.length) { acts.appendChild(knoepfe); }
874
+ /* Ohne Vorlagenvorschlag gibt es keinen Bestand, zu dem er
875
+ umschalten koennte - dann steht er gar nicht erst da, statt
876
+ ausgegraut in der Leiste. */
877
+ leistenKnopf = e.vorschlag ? bRoh : null;
860
878
  sg.appendChild(acts);
861
879
  host.appendChild(sg);
862
880
  }
@@ -865,7 +883,7 @@ var warumAuf = false;
865
883
  if (musterBlock) {
866
884
  head.appendChild(musterBlock);
867
885
  }
868
- return musterBlock;
886
+ return { musterBlock: musterBlock, leistenKnopf: leistenKnopf };
869
887
  }
870
888
 
871
889
  /* Die Karte „Gehoert nicht zum <Muster>" stand hier bis zum
@@ -1067,7 +1085,9 @@ var warumAuf = false;
1067
1085
 
1068
1086
  var istQuelle = baueZielleiste(host, e);
1069
1087
 
1070
- musterBlock = baueVorlagenkasten(host, head, e, istQuelle, musterBlock);
1088
+ var kasten = baueVorlagenkasten(host, head, e, istQuelle, musterBlock, rateKnopf);
1089
+ musterBlock = kasten.musterBlock;
1090
+ rateKnopf = kasten.leistenKnopf;
1071
1091
 
1072
1092
  /* Gleich unter die Erkennung: was hier fehlt, entscheidet mit
1073
1093
  darueber, ob eine Vorlage ueberhaupt greift. */
@@ -32,6 +32,11 @@ import { D } from './basis.js';
32
32
  export function ausdruckText(r) {
33
33
  if (!r) { return ''; }
34
34
  if (r.source !== undefined) { return r.source; }
35
+ /* Ein Objekt ohne `source` ist kein Ausdruck. `ignoreRole` kommt im
36
+ Browser als leeres `{}` an; `String({})` gab „[object Object]", und
37
+ das ist eine Zeichenklasse, die jede Rolle mit einem „e" trifft —
38
+ jeder Sollwert der Klimaanlage galt als ausgenommen (19.09.2026). */
39
+ if (typeof r === 'object') { return ''; }
35
40
  var q = String(r);
36
41
  var m = /^\/(.*)\/[a-z]*$/.exec(q);
37
42
  return m ? m[1] : q;
@@ -311,6 +316,46 @@ export function typKonflikte(states, musterName) {
311
316
  return raus;
312
317
  }
313
318
 
319
+ /* Welche Pflicht eines Musters der Entwurf wirklich nicht erfuellt.
320
+
321
+ Bis 19.09.2026 nannte das Musterfeld schlicht alle `required`-Plaetze:
322
+ an der Klimaanlage stand „✕ MODE fehlt", obwohl MODE angehakt und
323
+ bestaetigt war — gefehlt hat der Sollwert. Der steht im Muster nicht
324
+ als `required`, sondern als Gruppe (`requiredOneOf: 'setpoint'`):
325
+ einer aus SET und den beiden Doppel-Sollwerten muss da sein, und die
326
+ Gruppe kam in der Auskunft gar nicht vor (U13).
327
+
328
+ Geprueft wird grob, wie der Detektor: angehakte Zeile mit passender
329
+ Rolle, schreibbar wo der Platz es verlangt, im zugelassenen Typ.
330
+ Zurueck kommen Namen — eine Gruppe als „SET / SET_HEAT / …". Findet
331
+ die Grobpruefung nichts, gibt sie [] zurueck, und der Aufrufer faellt
332
+ auf die alte Auskunft zurueck. */
333
+ export function fehlendePflicht(states, musterName) {
334
+ var mu = musterName && musterVon(musterName);
335
+ if (!mu) { return []; }
336
+ function erfuellt(pl) {
337
+ return (states || []).some(function (s) {
338
+ if (!s.on || !s.role || !rolleTrifft(pl.role, s.role)) { return false; }
339
+ if (pl.ignoreRole && rolleTrifft(pl.ignoreRole, s.role)) { return false; }
340
+ if (pl.write === true && !(s.wr || s.srcW)) { return false; }
341
+ return typPasstZuPlatz(pl, s.typ);
342
+ });
343
+ }
344
+ var raus = [], gruppen = {};
345
+ mu.states.forEach(function (pl) {
346
+ if (pl.required && !erfuellt(pl)) { raus.push(pl.name); }
347
+ if (pl.requiredOneOf) {
348
+ var g = gruppen[pl.requiredOneOf] = gruppen[pl.requiredOneOf] || { namen: [], da: false };
349
+ if (g.namen.indexOf(pl.name) === -1) { g.namen.push(pl.name); }
350
+ if (erfuellt(pl)) { g.da = true; }
351
+ }
352
+ });
353
+ Object.keys(gruppen).forEach(function (k) {
354
+ if (!gruppen[k].da) { raus.push(gruppen[k].namen.join(' / ')); }
355
+ });
356
+ return raus;
357
+ }
358
+
314
359
  /* Die Datentypen, die ein Platz zulaesst - als Liste.
315
360
 
316
361
  Drei Faelle, gemessen am type-detector 6.0.1: 622 Plaetze nennen
package/admin/js/mqtt.js CHANGED
@@ -48,9 +48,24 @@ export function mqttEinstellung(id) {
48
48
  publish: true }` galt als sendefaehig, obwohl die Instanz diesen
49
49
  Punkt nicht bedient. Fehlt das Feld, gilt der Eintrag als an — so
50
50
  legt der Admin es an, wenn man die Verknuepfung setzt. */
51
+ var aus = null;
51
52
  Object.keys(cu).forEach(function (x) {
52
- if (!k && /^mqtt/.test(x) && cu[x] && cu[x].enabled !== false) { k = x; }
53
+ if (!/^mqtt/.test(x) || !cu[x]) { return; }
54
+ if (cu[x].enabled !== false) { if (!k) { k = x; } }
55
+ else if (!aus) { aus = x; }
53
56
  });
57
+ /* Eine abgeschaltete Verknuepfung ist eine Auskunft, kein Nichtwissen.
58
+
59
+ Steht das Thema zusaetzlich in `native` — bei mqtt-client der
60
+ Normalfall —, fiel der Punkt vorher in den Zweig darunter und galt
61
+ als „Thema da, Senden unbekannt"; die Karte zeigte dafuer gruen
62
+ „angelegt, sendet", obwohl die Instanz ihn nicht bedient (Ricardo,
63
+ 20.09.2026; Lauf vom 19.09.2026, H13). Wer den Schalter ausdruecklich
64
+ auf aus gestellt hat, bekommt das jetzt gesagt. */
65
+ if (!k && aus) {
66
+ return { instanz: aus, topic: cu[aus].topic || (o.native || {}).topic || '',
67
+ publish: false, abgeschaltet: true, roh: cu[aus] };
68
+ }
54
69
  if (!k) {
55
70
  /* Manche Objekte tragen das Thema nur in native — ioBroker.mqtt und
56
71
  sonoff tun das. Fuer sie ist `publish` schlicht UNBEKANNT, nicht
@@ -261,16 +276,20 @@ export function mqttLage(kanal) {
261
276
  var g = mqttGeraet(kanal);
262
277
  if (!g) { return null; }
263
278
  var b = tasmotaBefehle(kanal);
264
- var fehlen = [], stumm = [], da = [];
279
+ var fehlen = [], stumm = [], da = [], unklar = [];
265
280
  if (b) {
266
281
  b.befehle.forEach(function (x) {
267
282
  var id = hatPunkt(kanal, 'cmnd.' + x.name);
268
283
  if (!id) { fehlen.push(x); return; }
269
284
  var m = mqttEinstellung(id);
270
- /* `publish === null` heisst unbekannt (Thema nur in `native`) —
271
- so ein Punkt kommt zu den vorhandenen, nicht zu den stummen.
272
- „Senden erlauben" haette an ihm ohnehin nichts zu schreiben. */
273
- if (m && m.publish === false) { stumm.push({ name: x.name, id: id }); } else { da.push(x.name); }
285
+ /* `publish === null` heisst unbekannt (Thema nur in `native`, ohne
286
+ Verknuepfung — ioBroker.mqtt und sonoff). Frueher kam so ein
287
+ Punkt zu den vorhandenen und stand gruen als „angelegt, sendet"
288
+ da; behauptet ist damit mehr, als die Werkbank weiss. Er steht
289
+ jetzt eigens (Ricardo, 20.09.2026). */
290
+ if (m && m.publish === false) { stumm.push({ name: x.name, id: id }); }
291
+ else if (m && m.publish === null) { unklar.push(x.name); }
292
+ else { da.push(x.name); }
274
293
  });
275
294
  }
276
295
  /* Auch cmnd-Punkte, die es schon gibt, ohne dass sie im Zustand
@@ -283,8 +302,9 @@ export function mqttLage(kanal) {
283
302
  if (da.indexOf(name) > -1) { return; }
284
303
  var m = mqttEinstellung(id);
285
304
  if (m && m.publish === false) { stumm.push({ name: name, id: id }); }
305
+ else if (m && m.publish === null && unklar.indexOf(name) === -1) { unklar.push(name); }
286
306
  });
287
- return { geraet: g, befehle: b, fehlen: fehlen, stumm: stumm, da: da,
307
+ return { geraet: g, befehle: b, fehlen: fehlen, stumm: stumm, da: da, unklar: unklar,
288
308
  sofort: sofortRueckmeldung(kanal) };
289
309
  }
290
310
 
@@ -346,7 +366,11 @@ export function mqttKarte(host, kanal) {
346
366
  sah die Zahlen, aber nicht, welcher Befehl welcher Fall ist. */
347
367
  var zustand = {};
348
368
  l.fehlen.forEach(function (x) { zustand[x.name] = 'fehlt'; });
349
- l.stumm.forEach(function (x) { zustand[x.name] = 'stumm'; });
369
+ l.stumm.forEach(function (x) {
370
+ var m = mqttEinstellung(x.id);
371
+ zustand[x.name] = (m && m.abgeschaltet) ? 'aus' : 'stumm';
372
+ });
373
+ (l.unklar || []).forEach(function (n) { zustand[n] = 'unklar'; });
350
374
  l.da.forEach(function (n) { zustand[n] = 'da'; });
351
375
 
352
376
  var reihen = l.befehle.befehle.map(function (x) { return x.name; });
@@ -379,6 +403,8 @@ export function mqttKarte(host, kanal) {
379
403
  var c = el('span', 'mchip');
380
404
  if (z === 'fehlt') { c.appendChild(el('span', 'chip warn', tr('mq.stMissing'))); }
381
405
  else if (z === 'stumm') { c.appendChild(el('span', 'chip bad', tr('mq.stMute'))); }
406
+ else if (z === 'aus') { c.appendChild(el('span', 'chip bad', tr('mq.stOff'))); }
407
+ else if (z === 'unklar') { c.appendChild(el('span', 'chip mut', tr('mq.stUnknown'))); }
382
408
  else { c.appendChild(el('span', 'chip ok', tr('mq.stFine'))); }
383
409
  r.appendChild(c);
384
410
 
@@ -389,7 +415,7 @@ export function mqttKarte(host, kanal) {
389
415
  var bn = el('button', 'btn schmal', tr('mq.rowCreate'));
390
416
  bn.addEventListener('click', function () { mqttEinzeln(kanal, n, 'neu', bn); });
391
417
  h.appendChild(bn);
392
- } else if (z === 'stumm') {
418
+ } else if (z === 'stumm' || z === 'aus') {
393
419
  var bs2 = el('button', 'btn schmal', tr('mq.rowAllow'));
394
420
  bs2.addEventListener('click', function () { mqttEinzeln(kanal, n, 'frei', bs2); });
395
421
  h.appendChild(bs2);
@@ -421,7 +447,56 @@ export function mqttKarte(host, kanal) {
421
447
  Nur, wenn ueberhaupt eine Zeile am Telemetrietakt haengt. An einem
422
448
  Geraet, dessen Aliase alle aus `stat/...` lesen, ist SetOption59
423
449
  folgenlos, und ein Block darueber ist bloss Betrieb. */
424
- if (!telZ.length) { host.appendChild(k); return; }
450
+ /* Frueher stand hier ein vorzeitiges `return` — es haengte die Karte
451
+ an, bevor `b` in ihr steckte, und aufgeklappt blieb sie leer. */
452
+ if (telZ.length) { so59Kasten(b, kanal, l, telZ); }
453
+
454
+ /* Nachfragen liefert dasselbe wie tele/STATE — gemessen, Feld fuer
455
+ Feld. Der Knopf erscheint deshalb nur, wenn es etwas zu holen gibt:
456
+ gar nichts gelesen, oder nur stat.RESULT, das meist bloss die
457
+ Antwort auf den letzten Befehl enthaelt und nicht den ganzen
458
+ Zustand. Steht die Liste vollstaendig da, gibt es den Knopf nicht —
459
+ er koennte nichts hinzufuegen. */
460
+ var luecke = !l.befehle || l.befehle.woher === 'stat.RESULT';
461
+ if (luecke) {
462
+ var kasten = el('div');
463
+ kasten.style.marginTop = '11px';
464
+ kasten.style.paddingTop = '10px';
465
+ kasten.style.borderTop = '1px solid var(--line)';
466
+ var bf = el('button', 'btn' + (l.befehle ? '' : ' primary')
467
+ + (abfrageLaeuft[kanal] ? ' laedt' : ''),
468
+ abfrageLaeuft[kanal] ? tr('mq.asking') : tr('mq.ask'));
469
+ if (abfrageLaeuft[kanal]) { bf.disabled = true; }
470
+ bf.addEventListener('click', function () { mqttFragen(kanal, bf); });
471
+ kasten.appendChild(bf);
472
+ var warum = el('div', 'hint');
473
+ warum.style.marginTop = '6px';
474
+ warum.textContent = (l.befehle ? tr('mq.onlyResult') : tr('mq.neverReported')) +
475
+ ' ' + tr('mq.askHint');
476
+ kasten.appendChild(warum);
477
+ if (mqttAbfrage[kanal] !== undefined) {
478
+ var ah = el('div', 'hint');
479
+ ah.style.marginTop = '6px';
480
+ var ab = mqttAbfrage[kanal];
481
+ if (ab && ab.fehler) {
482
+ ah.textContent = tr('mq.askError', ab.fehler);
483
+ ah.style.color = 'var(--bad)';
484
+ } else {
485
+ ah.textContent = ab ? tr('mq.askGotAnswer', ab) : tr('mq.askNoAnswer');
486
+ ah.style.color = ab ? 'var(--ok)' : 'var(--warn)';
487
+ }
488
+ kasten.appendChild(ah);
489
+ }
490
+ b.appendChild(kasten);
491
+ }
492
+
493
+ k.appendChild(b);
494
+ host.appendChild(k);
495
+ }
496
+
497
+ /* Der Block zur sofortigen Rueckmeldung (SetOption59) — nur gerufen,
498
+ wenn eine Zeile am Telemetrietakt haengt. */
499
+ function so59Kasten(b, kanal, l, telZ) {
425
500
 
426
501
  var sk = el('div');
427
502
  sk.style.marginTop = '11px';
@@ -438,7 +513,9 @@ export function mqttKarte(host, kanal) {
438
513
  sz.appendChild(el('span', 'chip mut', tr('mq.instantUnknown')));
439
514
  }
440
515
  sk.appendChild(sz);
441
- if (so59Stumm[kanal] && l.sofort === null) {
516
+ /* Auch wenn noch ein alter Wert dasteht: die Frage eben blieb
517
+ unbeantwortet, und das gehoert gesagt. */
518
+ if (so59Stumm[kanal]) {
442
519
  var st59 = el('div', 'hint');
443
520
  st59.style.marginTop = '4px';
444
521
  st59.style.color = 'var(--warn)';
@@ -451,19 +528,24 @@ export function mqttKarte(host, kanal) {
451
528
  sl.style.display = 'flex';
452
529
  sl.style.gap = '8px';
453
530
  sl.style.flexWrap = 'wrap';
454
- if (l.sofort !== true) {
455
- /* Beide gleich gross - schmal neben normal las sich wie zwei
456
- verschiedene Schriften (Ricardo, 25.08.2026). */
457
- var lauf = so59Laeuft[kanal];
458
- var bp = el('button', 'btn schmal' + (lauf === 'pruefen' ? ' laedt' : ''),
459
- lauf === 'pruefen' ? tr('mq.instantChecking') : tr('mq.instantCheck'));
460
- bp.addEventListener('click', function () { so59(kanal, bp, ''); });
461
- sl.appendChild(bp);
531
+ /* „Nachfragen" steht immer da. Ein „eingeschaltet" kann von
532
+ vorgestern stammen — ein liegengebliebenes `stat.RESULT` —, und
533
+ ohne den Knopf liess es sich nicht nachpruefen (H15).
534
+ „Einschalten" nur, wo es noch nicht an ist oder die Frage eben
535
+ unbeantwortet blieb. Beide gleich gross - schmal neben normal las
536
+ sich wie zwei verschiedene Schriften (Ricardo, 25.08.2026). */
537
+ var lauf = so59Laeuft[kanal];
538
+ var bp = el('button', 'btn schmal' + (lauf === 'pruefen' ? ' laedt' : ''),
539
+ lauf === 'pruefen' ? tr('mq.instantChecking') : tr('mq.instantCheck'));
540
+ bp.addEventListener('click', function () { so59(kanal, bp, ''); });
541
+ sl.appendChild(bp);
542
+ if (lauf) { bp.disabled = true; }
543
+ if (l.sofort !== true || so59Stumm[kanal]) {
462
544
  var be = el('button', 'btn schmal' + (lauf === 'einschalten' ? ' laedt' : ''),
463
545
  lauf === 'einschalten' ? tr('mq.instantEnabling') : tr('mq.instantEnable'));
464
546
  be.addEventListener('click', function () { so59(kanal, be, '1'); });
465
547
  sl.appendChild(be);
466
- if (lauf) { bp.disabled = true; be.disabled = true; }
548
+ if (lauf) { be.disabled = true; }
467
549
  }
468
550
  sk.appendChild(sl);
469
551
  /* Der Hinweis nennt die betroffenen Zeilen und den wirklichen Takt,
@@ -479,48 +561,6 @@ export function mqttKarte(host, kanal) {
479
561
  var alter = (l.sofort === null) ? '' : so59Alter(kanal);
480
562
  sk.appendChild(el('div', 'hint', hinweis + (alter ? ' · ' + alter : '')));
481
563
  b.appendChild(sk);
482
-
483
- /* Nachfragen liefert dasselbe wie tele/STATE — gemessen, Feld fuer
484
- Feld. Der Knopf erscheint deshalb nur, wenn es etwas zu holen gibt:
485
- gar nichts gelesen, oder nur stat.RESULT, das meist bloss die
486
- Antwort auf den letzten Befehl enthaelt und nicht den ganzen
487
- Zustand. Steht die Liste vollstaendig da, gibt es den Knopf nicht —
488
- er koennte nichts hinzufuegen. */
489
- var luecke = !l.befehle || l.befehle.woher === 'stat.RESULT';
490
- if (luecke) {
491
- var kasten = el('div');
492
- kasten.style.marginTop = '11px';
493
- kasten.style.paddingTop = '10px';
494
- kasten.style.borderTop = '1px solid var(--line)';
495
- var bf = el('button', 'btn' + (l.befehle ? '' : ' primary')
496
- + (abfrageLaeuft[kanal] ? ' laedt' : ''),
497
- abfrageLaeuft[kanal] ? tr('mq.asking') : tr('mq.ask'));
498
- if (abfrageLaeuft[kanal]) { bf.disabled = true; }
499
- bf.addEventListener('click', function () { mqttFragen(kanal, bf); });
500
- kasten.appendChild(bf);
501
- var warum = el('div', 'hint');
502
- warum.style.marginTop = '6px';
503
- warum.textContent = (l.befehle ? tr('mq.onlyResult') : tr('mq.neverReported')) +
504
- ' ' + tr('mq.askHint');
505
- kasten.appendChild(warum);
506
- if (mqttAbfrage[kanal] !== undefined) {
507
- var ah = el('div', 'hint');
508
- ah.style.marginTop = '6px';
509
- var ab = mqttAbfrage[kanal];
510
- if (ab && ab.fehler) {
511
- ah.textContent = tr('mq.askError', ab.fehler);
512
- ah.style.color = 'var(--bad)';
513
- } else {
514
- ah.textContent = ab ? tr('mq.askGotAnswer', ab) : tr('mq.askNoAnswer');
515
- ah.style.color = ab ? 'var(--ok)' : 'var(--warn)';
516
- }
517
- kasten.appendChild(ah);
518
- }
519
- b.appendChild(kasten);
520
- }
521
-
522
- k.appendChild(b);
523
- host.appendChild(k);
524
564
  }
525
565
 
526
566
  var mqttStand = null;
@@ -571,6 +611,13 @@ function so59(kanal, knopf, wert) {
571
611
  if (!g) { return; }
572
612
  so59Laeuft[kanal] = wert ? 'einschalten' : 'pruefen';
573
613
  delete so59Stumm[kanal];
614
+ /* Wie alt ist die letzte Antwort JETZT? Gewertet wird nur, was nach
615
+ dem Senden kam — wie beim Abfrageweg. Vorher reichte ein
616
+ `stat.RESULT` von vorgestern mit `SetOption59` darin fuer
617
+ „eingeschaltet · gerade gefragt", auch wenn das Geraet gar nicht
618
+ antwortete (H15, gemessen 17.09.2026). */
619
+ var rid = hatPunkt(kanal, 'stat.RESULT');
620
+ var vorTs = (rid && S.werte[rid]) ? (S.werte[rid].ts || 0) : 0;
574
621
  /* Sofort neu zeichnen: erst damit sperren sich BEIDE Knoepfe und die
575
622
  alte Antwortzeile verschwindet - vorher passierte das erst mit dem
576
623
  naechsten Nachzieher, Sekunden spaeter (Ricardo, 25.08.2026). */
@@ -593,7 +640,13 @@ function so59(kanal, knopf, wert) {
593
640
  setTimeout(function () {
594
641
  holeZweig(kanal + '.', function () {
595
642
  delete so59Laeuft[kanal];
596
- so59Stumm[kanal] = (sofortRueckmeldung(kanal, true) === null);
643
+ var rid2 = hatPunkt(kanal, 'stat.RESULT');
644
+ var jetzt = (rid2 && S.werte[rid2]) ? (S.werte[rid2].ts || 0) : 0;
645
+ /* Nichts Neues: nicht merken — sonst stuende der alte
646
+ Wert mit frischer Uhrzeit da. */
647
+ so59Stumm[kanal] = jetzt > vorTs
648
+ ? (sofortRueckmeldung(kanal, true) === null)
649
+ : true;
597
650
  zeichneErgebnis();
598
651
  });
599
652
  }, 4000);
@@ -755,7 +808,7 @@ function mqttPunktBauen(kanal, name, was) {
755
808
  if (!alt0) { return null; }
756
809
  obj = JSON.parse(JSON.stringify(alt0));
757
810
  var cu0 = obj.common.custom || {};
758
- Object.keys(cu0).forEach(function (i) { if (/^mqtt/.test(i) && cu0[i]) { cu0[i].publish = true; } });
811
+ Object.keys(cu0).forEach(function (i) { if (/^mqtt/.test(i) && cu0[i]) { cu0[i].publish = true; cu0[i].enabled = true; } });
759
812
  obj.common.custom = cu0;
760
813
  } else {
761
814
  var w0 = befehlsWissen(name) || {};
@@ -789,7 +842,7 @@ export function mqttEinzeln(kanal, name, was, knopf) {
789
842
  if (!alt) { return; }
790
843
  obj = JSON.parse(JSON.stringify(alt));
791
844
  var cu = obj.common.custom || {};
792
- Object.keys(cu).forEach(function (i) { if (/^mqtt/.test(i) && cu[i]) { cu[i].publish = true; } });
845
+ Object.keys(cu).forEach(function (i) { if (/^mqtt/.test(i) && cu[i]) { cu[i].publish = true; cu[i].enabled = true; } });
793
846
  obj.common.custom = cu;
794
847
  } else {
795
848
  var w = befehlsWissen(name) || {};
@@ -961,10 +1014,18 @@ function mqttZuSchreiben() {
961
1014
  if (!mqttAuswahl['neu:' + x.name]) { return; }
962
1015
  var w = befehlsWissen(x.name) || {};
963
1016
  var thema = mqttThema(l.geraet, 'cmnd', x.name);
1017
+ /* Derselbe Typ wie auf dem Einzelweg — `punktTyp`, nicht `w.typ`.
1018
+
1019
+ Die Regel „Schreibformel heisst mixed" stand seit dem 15.09.2026 in
1020
+ `punktTyp` und wurde in `mqttPunktBauen` benutzt; hier blieb die
1021
+ alte Fassung stehen. Wer den Sammeldialog nahm, bekam `POWER`,
1022
+ `Fade` und `LedTable` als `boolean` statt `mixed` — der
1023
+ js-controller rechnete den Formelwert auf den Zieltyp zurueck, und
1024
+ auf der Leitung stand `true` statt `ON` (H18, gemessen 20.09.2026). */
964
1025
  var common = {
965
1026
  name: x.name,
966
1027
  role: w.rolle || 'state',
967
- type: w.typ || 'mixed',
1028
+ type: punktTyp(w),
968
1029
  read: true, write: true,
969
1030
  desc: tr('mq.createdBy'),
970
1031
  custom: mqttCustom(l.geraet, thema)
@@ -979,7 +1040,7 @@ function mqttZuSchreiben() {
979
1040
  if (!alt) { return; }
980
1041
  var kopie = JSON.parse(JSON.stringify(alt));
981
1042
  var cu = kopie.common.custom || {};
982
- Object.keys(cu).forEach(function (i) { if (/^mqtt/.test(i) && cu[i]) { cu[i].publish = true; } });
1043
+ Object.keys(cu).forEach(function (i) { if (/^mqtt/.test(i) && cu[i]) { cu[i].publish = true; cu[i].enabled = true; } });
983
1044
  kopie.common.custom = cu;
984
1045
  raus.push({ id: x.id, obj: kopie, neu: false });
985
1046
  });