iobroker.alias-workbench 0.8.3 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,8 @@ import {
27
27
  mitNachfrage,
28
28
  geraeteDarunter,
29
29
  aliasFuer,
30
+ quellenVerteilung,
31
+ quelleHier,
30
32
  tipptGerade,
31
33
  opt,
32
34
  vorlagenAbweichung,
@@ -77,6 +79,14 @@ var warumAuf = false;
77
79
  var werteTimer = null;
78
80
  function werteGezeichnet() {
79
81
  werteTimer = null;
82
+ /* Im Vorlagenmodus steht rechts das Vorlagenblatt, nicht die
83
+ Zustandsliste — ein eintreffender Wert hat dort nichts zu melden.
84
+ `setzeModus('vorlagen')` laesst `S.current` ausdruecklich stehen,
85
+ damit man seine Auswahl beim Zurueckwechseln wiederfindet; ohne
86
+ diese Zeile baute jeder Takt eines Solarzaehlers das ganze Blatt
87
+ neu, samt `probelauf` ueber alle Kandidaten und alle Vorlagen
88
+ (gemessen 09.09.2026: vier Werte, vier Neuaufbauten). */
89
+ if (S.baumModus === 'vorlagen') { return; }
80
90
  if (document.querySelector('dialog[open]')) { return; }
81
91
  if (tipptGerade()) { werteTimer = setTimeout(werteGezeichnet, 700); return; }
82
92
  if (S.current) { zeichneErgebnis(); }
@@ -331,18 +341,137 @@ var warumAuf = false;
331
341
  erschien dadurch an jeder Sparte und jedem Unterkanal, also an
332
342
  `…RGB.stat` und `…RGB.tele` ebenso wie am Geraet. Der Knopf
333
343
  gehoert nur dorthin, wo der Alias auch wirklich herkommt. */
334
- if (sprungZiel && quelleVon(sprungZiel) !== S.current) { sprungZiel = null; }
344
+ /* Rueckprobe, aber nicht zu streng.
345
+
346
+ Frueher galt nur: `quelleVon(alias) === S.current`. Bei einem
347
+ Alias aus zwei Zweigen fiel der Rueckweg damit weg - wer ueber
348
+ die neue Quellenauswahl auf `…Stromzaehler.stat` sprang, stand
349
+ dort ohne Knopf zurueck (Ricardo, 08.09.2026).
350
+
351
+ Jetzt gilt der Knopf auch an einer Nebenquelle - aber nur, wenn
352
+ sie GENAU in der Verteilung steht und einen eigenen Zweig
353
+ bildet. Sparten unterhalb der Hauptquelle bleiben damit weiter
354
+ aussen vor: `…Licht.stat` und `…Licht.tele` liegen beide unter
355
+ `…Bastelzimmer_Licht`, und der Knopf gehoert ans Geraet, nicht
356
+ an jede Sparte. Knoten weiter oben (`…Strom`) stehen gar nicht
357
+ erst in der Verteilung. */
358
+ /* Der Rueckweg gehoert an jede Quelle des Alias - an die groesste
359
+ wie an jede andere, und auch an das Geraet ueber einer Sparte.
360
+ `quelleHier` sagt, ob dieser Knoten zu einer von ihnen gehoert;
361
+ steht er zu weit oben oder gar nicht dabei, bleibt der Knopf
362
+ weg (Ricardo, 08.09.2026). */
363
+ if (sprungZiel && !quelleHier(sprungZiel, S.current)) { sprungZiel = null; }
335
364
  sprungModus = 'aliase';
336
365
  }
366
+ /* Liest der Alias aus mehr als einem Knoten, gehoert das in den Kopf.
367
+
368
+ `quelleVon` entscheidet nach Mehrheit und wirft den Rest weg. Das
369
+ ist als Sprungziel richtig, als Auskunft aber die halbe Wahrheit:
370
+ an `alias.0.Solar.Netz` - drei Punkte aus `0_userdata`, zwei aus
371
+ `mqtt-client` - stand nirgends, dass es zwei Quellen sind. Man
372
+ musste jede Zeile aufklappen, um es zu sehen (Ricardo,
373
+ 08.09.2026). */
374
+ /* Der Chip gilt fuer den Alias, gleich von welcher Seite man ihn
375
+ ansieht: am Alias selbst und an jeder seiner Quellen. Wer an einer
376
+ Nebenquelle steht, soll sehen, dass er nur einen Teil vor sich
377
+ hat. */
378
+ var quellAlias = (S.current.indexOf('alias.') === 0) ? S.current
379
+ : (sprungZiel && sprungModus === 'aliase' ? sprungZiel : null);
380
+ var verteilung = quellAlias ? quellenVerteilung(quellAlias) : [];
381
+ var mehrQuellen = verteilung.length > 1;
337
382
  if (knotenDa(sprungZiel)) {
338
383
  var zumAlias = (sprungModus === 'aliase');
339
384
  sprung = el('button', 'btn schmal sprungknopf',
340
385
  (zumAlias ? tr('result.toAlias') : tr('result.toSource')) + ' \u2192');
341
386
  sprung.title = (zumAlias ? tr('result.toAliasHint') : tr('result.toSourceHint'))
342
- + '\n' + sprungZiel;
387
+ + String.fromCharCode(10) + sprungZiel;
343
388
  var ziel0 = sprungZiel, modus0 = sprungModus;
344
- sprung.addEventListener('click', function () { springeZu(ziel0, modus0); });
345
- namensZeile.appendChild(sprung);
389
+ /* Die Auswahl gehoert nur an „zur Quelle" - dort gibt es mehrere
390
+ Ziele. „zum Alias" hat genau eines; eine Liste von QUELLEN unter
391
+ diesem Knopf ist sinnlos (Ricardo, 08.09.2026). Der Chip bleibt
392
+ an beiden Stellen: er sagt etwas ueber den Alias, nicht ueber
393
+ den Sprung. */
394
+ if (mehrQuellen && sprungModus === 'quellen') {
395
+ /* Die Auswahl haengt am Body, nicht am Kopf.
396
+
397
+ Erst stand sie neben dem Knopf - und war sofort wieder weg:
398
+ bei `alias.0.Solar.Netz` laeuft im Sekundentakt ein neuer
399
+ Messwert ein, jeder zeichnet die rechte Seite neu, und mit ihr
400
+ verschwand die eben geoeffnete Liste. Am Body ueberlebt sie
401
+ das; ihre Stelle bekommt sie aus der Lage des Knopfes.
402
+
403
+ `mousedown` statt `click`, wie bei den Vorschlagslisten der
404
+ Zielleiste: der Mausdruck kommt vor dem Neuzeichnen. */
405
+ /* Gefragt wird das Dokument, nicht die Erinnerung.
406
+
407
+ Die Liste haengt am `body`, damit sie ein automatisches
408
+ Neuzeichnen ueberlebt. Der Knopf aber ist danach ein anderer,
409
+ und mit ihm diese Merkstelle: sie stand wieder auf „keine",
410
+ waehrend die alte Liste weiter am Body hing. Der zweite Klick
411
+ legte eine zweite Liste ueber die erste, und der Zuhoerer am
412
+ Body raeumte nicht auf, weil der Knopf das Ereignis abfaengt
413
+ (gemessen 09.09.2026). */
414
+ var offeneListe = function () { return document.querySelector('.sprungliste'); };
415
+ var listeQ = null;
416
+ var zuQ = function () {
417
+ var da = offeneListe();
418
+ while (da) {
419
+ if (da.parentNode) { da.parentNode.removeChild(da); }
420
+ da = offeneListe();
421
+ }
422
+ listeQ = null;
423
+ };
424
+ sprung.addEventListener('mousedown', function (ev) {
425
+ ev.preventDefault();
426
+ ev.stopPropagation();
427
+ if (offeneListe()) { zuQ(); return; }
428
+ listeQ = el('div', 'vorschlaege sprungliste');
429
+ listeQ.appendChild(el('div', 'vzkopf', tr('result.pickSource')));
430
+ verteilung.forEach(function (q) {
431
+ /* Keine Zeile hervorheben. Die Hervorhebung meinte „das ist
432
+ die wichtigste" - eine Aussage, die bei drei zu drei
433
+ niemand treffen kann und die auch sonst niemand braucht:
434
+ alle Quellen des Alias sind gleichberechtigt (Ricardo,
435
+ 08.09.2026). */
436
+ var z = el('div', 'vz');
437
+ z.appendChild(el('span', null, q.id));
438
+ z.appendChild(el('span', 'zq', ' \u00b7 ' + q.n));
439
+ z.addEventListener('mousedown', function (ev2) {
440
+ ev2.preventDefault();
441
+ ev2.stopPropagation();
442
+ zuQ();
443
+ springeZu(q.id, modus0);
444
+ });
445
+ listeQ.appendChild(z);
446
+ });
447
+ var r = sprung.getBoundingClientRect();
448
+ listeQ.style.position = 'fixed';
449
+ listeQ.style.left = Math.round(r.left) + 'px';
450
+ listeQ.style.top = Math.round(r.bottom + 2) + 'px';
451
+ listeQ.hidden = false;
452
+ document.body.appendChild(listeQ);
453
+ });
454
+ /* Woanders hin gedrueckt heisst: doch nicht. Einmal registriert,
455
+ nicht bei jedem Neuzeichnen - sonst haeuft sich der Zuhoerer. */
456
+ if (!S.sprungZuhoerer) {
457
+ S.sprungZuhoerer = true;
458
+ document.addEventListener('mousedown', function () {
459
+ var offen = document.querySelector('.sprungliste');
460
+ if (offen && offen.parentNode) { offen.parentNode.removeChild(offen); }
461
+ });
462
+ }
463
+ namensZeile.appendChild(sprung);
464
+ } else {
465
+ sprung.addEventListener('click', function () { springeZu(ziel0, modus0); });
466
+ namensZeile.appendChild(sprung);
467
+ }
468
+ }
469
+ if (mehrQuellen) {
470
+ var chQ = el('span', 'chip mut quellenzahl', tr('result.sourceCount', verteilung.length));
471
+ chQ.title = tr('result.sourceCountHint') + String.fromCharCode(10, 10) +
472
+ verteilung.map(function (q) { return q.id + ' \u00b7 ' + q.n; })
473
+ .join(String.fromCharCode(10));
474
+ namensZeile.appendChild(chQ);
346
475
  }
347
476
  links.appendChild(sub);
348
477
  head.appendChild(links);
@@ -558,8 +687,20 @@ var warumAuf = false;
558
687
  var gid = S.objects[vorh].native.vorlage;
559
688
  if (S.VORLAGEN.some(function (x) { return x.id === gid; })) { fest = gid; }
560
689
  }
561
- var neuE = rohJetzt ? alleUndVorlage(S.current, fest)
562
- : vorschlag(S.current, fest);
690
+ /* Ausgang und Handwahl gehoeren mit.
691
+
692
+ Ohne die Instanz kamen an einer Mehrfachvorlage die Zeilen von
693
+ Ausgang 1 zurueck, waehrend das Ziel weiter `…Ausgang_2` hiess.
694
+ Und `fest` allein reicht nicht: Es ist nur gesetzt, wenn schon
695
+ ein Alias mit Vorlage dasteht. Hatte die Automatik nichts
696
+ gefunden und man die Vorlage von Hand gewaehlt, suchte der
697
+ Neuaufbau wieder automatisch, fand nichts — und man landete im
698
+ Rohmodus, die Handwahl weg (gemessen 09.09.2026). Der
699
+ Ausgangswechsel eine Stelle weiter oben macht es laengst
700
+ richtig. */
701
+ var vid = fest || e.vorlage || null;
702
+ var neuE = rohJetzt ? alleUndVorlage(S.current, vid, e.instanz)
703
+ : vorschlag(S.current, vid, e.instanz);
563
704
  if (!neuE) { return; }
564
705
  neuE.roh = rohJetzt;
565
706
  neuE.vorschlag = true;
@@ -609,31 +750,16 @@ var warumAuf = false;
609
750
  return musterBlock;
610
751
  }
611
752
 
612
- /* Punkte, die im gewaehlten Muster keinen Platz haben, aber als
613
- Messwerte durchgehen. */
614
- function baueInfoKarte(host, e, infoNamen) {
615
- if (infoNamen.length) {
616
- var ic = el('div', 'card infod');
617
- var ich = el('div', 'ch');
618
- /* Hier stand „info" und „zaehlt der Detektor separat". Beides sind
619
- unsere Woerter: „info" ist der interne Name des Sammeltyps, und
620
- wer den Detektor nicht kennt, weiss nach dem Satz weniger als
621
- vorher. Jetzt steht da, worum es geht - und der Zusatz sagt
622
- gleich, ob man handeln muss. */
623
- ich.appendChild(el('span', 'typ', tr('info.notPartOf', e.want || '')));
624
- ich.appendChild(el('span', 'chip mut', tr('info.countedSeparately')));
625
- ic.appendChild(ich);
626
- var ie = el('div', 'infoerkl');
627
- ie.style.padding = '10px 15px 12px';
628
- ie.style.fontSize = '11.5px';
629
- ie.style.color = 'var(--ink-3)';
630
- ie.style.lineHeight = '1.6';
631
- ie.appendChild(el('b', null, tr('info.sameDatapoints', infoNamen.join(', '))));
632
- ie.appendChild(document.createTextNode(tr('info.explain', e.want || '')));
633
- ic.appendChild(ie);
634
- host.appendChild(ic);
635
- }
636
- }
753
+ /* Die Karte „Gehoert nicht zum <Muster>" stand hier bis zum
754
+ 08.09.2026 — weit unten, mit den Namen der betroffenen Punkte und
755
+ der langen Erklaerung. Sie ist weg: dieselbe Auskunft haengt jetzt
756
+ am Hinweis der Legende, und die steht dort, wo die getoenten Zeilen
757
+ stehen. Zweimal dasselbe, einmal oben und einmal ganz unten, war
758
+ eine zu viel (Ricardo, 08.09.2026). Der Text `info.explain` bleibt in
759
+ Gebrauch — die Legende nutzt ihn. `info.notPartOf` dagegen rief nach
760
+ dem Umbau niemand mehr; dieser Satz behauptete das Gegenteil, und
761
+ der Schluessel stand noch ein Jahr in beiden Sprachdateien, weil der
762
+ Test nur die Richtung Code → JSON prueft (09.09.2026 entfernt). */
637
763
 
638
764
  /* Was sich gegenueber dem gespeicherten Stand aendern wuerde. */
639
765
  function baueDiffKarte(host, e) {
@@ -665,7 +791,7 @@ var warumAuf = false;
665
791
 
666
792
  /* Pruefungen, Fusszeile und die Knopfleiste unten - beschriftet
667
793
  nach dem, was der Klick tatsaechlich tut. */
668
- function setzeKnoepfe(e, haupt, pflichtFehlt, _infoNamen) {
794
+ function setzeKnoepfe(e, haupt, pflichtFehlt) {
669
795
  baueChecks(e, haupt, pflichtFehlt);
670
796
 
671
797
  /* Der Knopf soll sagen, was er tut — nicht, wie er es tut. */
@@ -796,21 +922,19 @@ var warumAuf = false;
796
922
  }
797
923
 
798
924
  var pl = berechnePlaetze(e, haupt);
799
- var imInfo = pl.imInfo;
925
+
800
926
 
801
927
  baueListe(host, e, pl, rateKnopf, musterBlock);
802
928
  rateKnopf = null;
803
929
  musterBlock = null;
804
930
 
805
- /* --- info-Karte --- */
806
- var infoNamen = Object.keys(imInfo);
807
- baueInfoKarte(host, e, infoNamen);
931
+
808
932
 
809
933
  /* --- Änderungen gegenüber dem, was gespeichert ist --- */
810
934
  baueDiffKarte(host, e);
811
935
 
812
936
  /* --- Prüfungen, Fusszeile, Knopfleiste --- */
813
- setzeKnoepfe(e, haupt, pflichtFehlt, infoNamen);
937
+ setzeKnoepfe(e, haupt, pflichtFehlt);
814
938
  }
815
939
 
816
940
  /* Uebersicht ueber alle Geraete unterhalb eines Ordners. */
@@ -4,6 +4,55 @@
4
4
  import { S } from './zustand.js';
5
5
  import { D } from './basis.js';
6
6
 
7
+ /* Der Ausdruck eines Platzes als blosser Text — ohne Schraegstriche.
8
+
9
+ Der Detector liefert seine Muster ueber `ChannelDetector.getPatterns()`
10
+ (so haengt `src/detector-bundle.js` sie an `window`), und diese Funktion
11
+ gibt `role` nicht als RegExp weiter, sondern als `role.toString()` —
12
+ also MIT den Schraegstrichen:
13
+
14
+ /^indicator(\.maintenance)?\.(lowbat|battery)$/
15
+
16
+ Das ist bei uns der Normalfall, nicht die Ausnahme: ein RegExp bekommen
17
+ wir nur, wenn jemand die Muster direkt aus `typePatterns` liest, etwa
18
+ ein Test. Wer bloss `.source` abfragt und sonst `String(r)` nimmt, haelt
19
+ die Schraegstriche fuer Teil des Ausdrucks und findet danach weder den
20
+ Anker `^` noch das Ende `$`.
21
+
22
+ Gefunden am 08.09.2026 am Produktivsystem, und der eine Fehler zog weit:
23
+ `ROLLEN` hatte 210 statt 232 Eintraege (E27) — `indicator.lowbat` fehlte,
24
+ die Rolle, die hm-rpc schreibt —, `BRIGHTNESS` bekam in sechs Mustern gar
25
+ keine Rolle (E6), `plaetzeFuerRollen` fand zu keiner einzigen Rolle einen
26
+ Platz, und die Abweichungskarte behauptete „haette ohnehin nicht
27
+ gezaehlt" ueber Punkte mit Platz. Die Node-Tests sahen nichts davon: sie
28
+ fuettern die Zerlegung mit echten RegExp-Literalen.
29
+
30
+ Deshalb geht seither jede Stelle, die einen Platzausdruck liest, hier
31
+ durch — es sind sieben. */
32
+ export function ausdruckText(r) {
33
+ if (!r) { return ''; }
34
+ if (r.source !== undefined) { return r.source; }
35
+ var q = String(r);
36
+ var m = /^\/(.*)\/[a-z]*$/.exec(q);
37
+ return m ? m[1] : q;
38
+ }
39
+
40
+ /* Der Ausdruck als RegExp — fuer alle, die damit pruefen wollen. Wirft
41
+ nicht: ein unbrauchbarer Ausdruck gibt `null`, und der Aufrufer
42
+ entscheidet, was das heisst. */
43
+ export function ausdruckRegExp(r) {
44
+ var q = ausdruckText(r);
45
+ if (!q) { return null; }
46
+ try { return new RegExp(q); } catch { return null; }
47
+ }
48
+
49
+ /* Passt eine Rolle auf den Ausdruck eines Platzes? */
50
+ export function rolleTrifft(r, rolle) {
51
+ if (!rolle) { return false; }
52
+ var re = ausdruckRegExp(r);
53
+ return re ? re.test(rolle) : false;
54
+ }
55
+
7
56
  /* Die Rollen kommen aus dem Paket, nicht aus einer eigenen Liste —
8
57
  sonst pflegt man eine zweite Wahrheit, die auseinanderlaeuft. */
9
58
  /* Manche Plaetze nennen keine Standardrolle, sondern nur einen
@@ -13,8 +62,8 @@ import { D } from './basis.js';
13
62
  liess sich beliebig oft hinzufuegen, und die Rolle fehlte auch in der
14
63
  Auswahlliste — der Platz war auf keinem Weg zu befuellen. */
15
64
  export function rolleAusAusdruck(r) {
16
- if (!r) { return ''; }
17
- var q = (r.source !== undefined) ? r.source : String(r);
65
+ var q = ausdruckText(r);
66
+ if (!q) { return ''; }
18
67
  if (q.charAt(0) !== '^' || q.slice(-1) !== '$') { return ''; }
19
68
  var t = q.slice(1, -1).split(String.fromCharCode(92)).join('');
20
69
  return /^[A-Za-z0-9_]+([.][A-Za-z0-9_]+)*$/.test(t) ? t : '';
@@ -41,8 +90,8 @@ export function rolleVonPlatz(st) { return st.defaultRole || rolleAusAusdruck(st
41
90
  dann bleibt es bei der Vorgaberolle. Lieber eine Schreibweise zu wenig
42
91
  als eine erfundene in der Liste. */
43
92
  export function schreibweisenAus(r) {
44
- if (!r) { return []; }
45
- var q = (r.source !== undefined) ? r.source : String(r);
93
+ var q = ausdruckText(r);
94
+ if (!q) { return []; }
46
95
  if (q.charAt(0) !== '^' || q.slice(-1) !== '$') { return []; }
47
96
  q = q.slice(1, -1);
48
97
 
@@ -160,7 +209,7 @@ export function plaetzeFuerRollen(musterName) {
160
209
  ROLLEN.forEach(function (r) {
161
210
  mu.states.forEach(function (st) {
162
211
  if (raus[r] || !st.role) { return; }
163
- try { if (new RegExp(st.role.source || st.role).test(r)) { raus[r] = st.name; } } catch { /* unbrauchbarer Ausdruck in der Vorlage: zaehlt nicht */ }
212
+ if (rolleTrifft(st.role, r)) { raus[r] = st.name; }
164
213
  });
165
214
  });
166
215
  return raus;
@@ -1,5 +1,5 @@
1
1
  import { holeText } from './basis.js';
2
- import { txt } from './sprache.js';
2
+ import { txt, alleTexte } from './sprache.js';
3
3
  import { einst } from './einstellungen.js';
4
4
  import './enums.js';
5
5
 
@@ -304,10 +304,19 @@ export function vorlageZu(id, art, name) {
304
304
  var v = null;
305
305
  liste.forEach(function (x) { if (!v && x._id === kurz) { v = x; } });
306
306
  if (v) { return v; }
307
- var n = String(name || '').toLowerCase();
307
+ var n = String(name || '').trim().toLowerCase();
308
308
  if (!n) { return null; }
309
+ /* Alle Sprachfassungen, nicht nur die eingestellte.
310
+
311
+ Im Bestand steht, was jemand getippt hat — „Light" etwa, waehrend
312
+ die Vorlage auf einem deutschen Admin „Licht" heisst. Verglichen
313
+ wurde aber nur die eine Fassung, und ohne Treffer entsteht beim
314
+ naechsten Schreiben ein Doppelgaenger `enum.functions.light` neben
315
+ der vorhandenen Funktion (gefunden 09.09.2026). Der Vergleich war
316
+ schon immer unabhaengig von Gross- und Kleinschreibung; was fehlte,
317
+ waren die anderen Sprachen. */
309
318
  liste.forEach(function (x) {
310
- if (!v && String(txt(x.name) || '').toLowerCase() === n) { v = x; }
319
+ if (!v && alleTexte(x.name).indexOf(n) !== -1) { v = x; }
311
320
  });
312
321
  return v;
313
322
  }
package/admin/js/mqtt.js CHANGED
@@ -38,10 +38,28 @@ export function mqttEinstellung(id) {
38
38
  if (!o || !o.common) { return null; }
39
39
  var cu = o.common.custom || {};
40
40
  var k = null;
41
- Object.keys(cu).forEach(function (x) { if (!k && /^mqtt/.test(x)) { k = x; } });
41
+ /* Drei Bedingungen, nicht eine.
42
+
43
+ `cu[x]` kann `null` sein — ioBroker raeumt den Eintrag nicht weg,
44
+ wenn eine Instanz verschwindet. Der Zugriff auf `.topic` riss dann
45
+ die ganze Ergebnisansicht mit (gemessen 09.09.2026).
46
+
47
+ Und `enabled` kam hier gar nicht vor: `{ enabled: false,
48
+ publish: true }` galt als sendefaehig, obwohl die Instanz diesen
49
+ Punkt nicht bedient. Fehlt das Feld, gilt der Eintrag als an — so
50
+ legt der Admin es an, wenn man die Verknuepfung setzt. */
51
+ Object.keys(cu).forEach(function (x) {
52
+ if (!k && /^mqtt/.test(x) && cu[x] && cu[x].enabled !== false) { k = x; }
53
+ });
42
54
  if (!k) {
43
- /* Manche Objekte tragen das Thema nur in native. */
44
- if (o.native && o.native.topic) { return { instanz: null, topic: o.native.topic, publish: false }; }
55
+ /* Manche Objekte tragen das Thema nur in native — ioBroker.mqtt und
56
+ sonoff tun das. Fuer sie ist `publish` schlicht UNBEKANNT, nicht
57
+ nein: Frueher stand hier `false`, die Pruefung meldete „kann nicht
58
+ senden", der Chip stand rot, und der Knopf „senden erlauben"
59
+ schrieb das Objekt unveraendert zurueck — eine Meldung, die sich
60
+ nicht abstellen liess (gemessen 09.09.2026). `null` heisst: wir
61
+ wissen es nicht, also nicht behaupten. */
62
+ if (o.native && o.native.topic) { return { instanz: null, topic: o.native.topic, publish: null }; }
45
63
  return null;
46
64
  }
47
65
  return { instanz: k, topic: cu[k].topic || (o.native || {}).topic || '', publish: !!cu[k].publish,
@@ -110,6 +128,12 @@ export function tasmotaBefehle(kanal) {
110
128
  var liste = [];
111
129
  Object.keys(aus).forEach(function (f) {
112
130
  if (weg.indexOf(f) > -1) { return; }
131
+ /* Aus dem Schluessel wird eine Kennung UND ein MQTT-Thema. Ein `#`
132
+ oder `+` darin ergaebe ein Wildcard-Abo, ein Punkt eine
133
+ zusaetzliche Ebene im Objektbaum. Der Inhalt kommt vom Broker,
134
+ also von aussen — erreichbar ueber jeden fremden Publisher auf
135
+ demselben Broker. Tasmota-Befehle heissen ohnehin alle so. */
136
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(f)) { return; }
113
137
  if (aus[f] !== null && typeof aus[f] === 'object' && !Array.isArray(aus[f])) { return; }
114
138
  liste.push({ name: f, wert: aus[f] });
115
139
  });
@@ -143,13 +167,21 @@ export function befehlsWissen(name) {
143
167
  naechste Befehl die Antwort. Gemessen: nachfragen → „eingeschaltet",
144
168
  einmal schalten → wieder „unbekannt", obwohl sich am Geraet nichts
145
169
  geaendert hatte. Der Befund war praktisch nie stabil zu sehen. */
146
- export function sofortRueckmeldung(kanal) {
170
+ export function sofortRueckmeldung(kanal, merken) {
147
171
  var id = hatPunkt(kanal, 'stat.RESULT');
148
172
  if (id) {
149
173
  var j = jsonVon(id);
150
174
  if (j && j.SetOption59 !== undefined) {
151
175
  var w = String(j.SetOption59).toUpperCase() === 'ON';
152
- merkeSo59(kanal, w);
176
+ /* Gemerkt wird nur nach einer Abfrage, nicht beim Zeichnen.
177
+
178
+ Vorher schrieb diese Funktion aus dem Zeichenpfad heraus ein
179
+ Objekt — ein Waechter verhinderte nur das Mehrfachschreiben,
180
+ der Fehler wurde verschluckt, und der Wert stand lokal schon.
181
+ Das widerspricht der Zusage, dass die Pruefungen nichts
182
+ anfassen: Wer die rechte Seite ansieht, aendert nichts.
183
+ `so59()` ruft mit `merken = true`, alle anderen lesen nur. */
184
+ if (merken) { merkeSo59(kanal, w); }
153
185
  return w;
154
186
  }
155
187
  }
@@ -203,7 +235,10 @@ export function mqttLage(kanal) {
203
235
  var id = hatPunkt(kanal, 'cmnd.' + x.name);
204
236
  if (!id) { fehlen.push(x); return; }
205
237
  var m = mqttEinstellung(id);
206
- if (m && !m.publish) { stumm.push({ name: x.name, id: id }); } else { da.push(x.name); }
238
+ /* `publish === null` heisst unbekannt (Thema nur in `native`) —
239
+ so ein Punkt kommt zu den vorhandenen, nicht zu den stummen.
240
+ „Senden erlauben" haette an ihm ohnehin nichts zu schreiben. */
241
+ if (m && m.publish === false) { stumm.push({ name: x.name, id: id }); } else { da.push(x.name); }
207
242
  });
208
243
  }
209
244
  /* Auch cmnd-Punkte, die es schon gibt, ohne dass sie im Zustand
@@ -215,7 +250,7 @@ export function mqttLage(kanal) {
215
250
  if (stumm.some(function (x) { return x.name === name; })) { return; }
216
251
  if (da.indexOf(name) > -1) { return; }
217
252
  var m = mqttEinstellung(id);
218
- if (m && !m.publish) { stumm.push({ name: name, id: id }); }
253
+ if (m && m.publish === false) { stumm.push({ name: name, id: id }); }
219
254
  });
220
255
  return { geraet: g, befehle: b, fehlen: fehlen, stumm: stumm, da: da,
221
256
  sofort: sofortRueckmeldung(kanal) };
@@ -509,7 +544,7 @@ function so59(kanal, knopf, wert) {
509
544
  setTimeout(function () {
510
545
  holeZweig(kanal + '.', function () {
511
546
  delete so59Laeuft[kanal];
512
- so59Stumm[kanal] = (sofortRueckmeldung(kanal) === null);
547
+ so59Stumm[kanal] = (sofortRueckmeldung(kanal, true) === null);
513
548
  zeichneErgebnis();
514
549
  });
515
550
  }, 4000);
@@ -556,6 +591,18 @@ function mqttFragen(kanal, _knopf) {
556
591
  /* Status 5 gleich mit: dort steht die IP, und die fehlt an
557
592
  jedem Geraet, das seit dem Einbinden nicht neu gestartet
558
593
  ist — tele/INFO2 kommt nur beim Start. */
594
+ /* Wie alt sind die Quellen JETZT?
595
+
596
+ Vorher wurde nur die Merkstelle geleert. Ein `stat.RESULT`,
597
+ das retained von vorgestern dasteht, reichte danach fuer
598
+ die gruene Rueckmeldung — auch wenn das Geraet aus ist und
599
+ gar nicht antwortet (gemessen 09.09.2026). Gewertet wird
600
+ jetzt nur, was nach dem Senden hereinkam. */
601
+ var vorTs = {};
602
+ ['stat.STATUS11', 'tele.STATE', 'stat.RESULT'].forEach(function (q) {
603
+ var qid = hatPunkt(kanal, q);
604
+ if (qid && S.werte[qid]) { vorTs[q] = S.werte[qid].ts || 0; }
605
+ });
559
606
  socket.emit('setState', id, { val: '5', ack: false }, function () {});
560
607
  socket.emit('setState', id, { val: '11', ack: false }, function () {
561
608
  setTimeout(function () {
@@ -569,7 +616,12 @@ function mqttFragen(kanal, _knopf) {
569
616
  holeZweig(kanal + '.', function () {
570
617
  delete abfrageLaeuft[kanal];
571
618
  var bb = tasmotaBefehle(kanal);
572
- mqttAbfrage[kanal] = (bb && bb.befehle.length) ? bb.befehle.length : 0;
619
+ /* Nur wenn die Quelle, aus der die Liste stammt, seit
620
+ dem Senden frisch geworden ist. */
621
+ var qid = bb && hatPunkt(kanal, bb.woher);
622
+ var jetzt = (qid && S.werte[qid]) ? (S.werte[qid].ts || 0) : 0;
623
+ var frisch = bb && jetzt > (vorTs[bb.woher] || 0);
624
+ mqttAbfrage[kanal] = (frisch && bb.befehle.length) ? bb.befehle.length : 0;
573
625
  zeichneErgebnis();
574
626
  });
575
627
  }, 5000);
@@ -606,7 +658,18 @@ function mqttCustom(g, thema) {
606
658
  export function mqttEinzelnStill(kanal, name, fertig) {
607
659
  var bau = mqttPunktBauen(kanal, name, 'neu');
608
660
  if (!bau) { return fertig && fertig(); }
609
- socket.emit('setObject', bau.id, bau.obj, function () {
661
+ socket.emit('setObject', bau.id, bau.obj, function (err) {
662
+ /* Ein abgelehntes Schreiben ist kein Erfolg.
663
+
664
+ Der Rueckruf nahm `err` frueher nicht einmal entgegen und trug das
665
+ Objekt trotzdem in den eigenen Bestand ein. Danach hielt
666
+ `pruefeSchreiben` den Sendepunkt fuer vorhanden, die Sperre griff
667
+ nicht mehr, und „Jetzt anlegen" schrieb `alias.id.write` auf eine
668
+ Kennung, die es in der Datenbank nie gab — genau der Fehler, den
669
+ der js-controller sich dauerhaft merkt und nur durch Loeschen und
670
+ Neuanlegen vergisst. Der Zwilling `mqttEinzeln` prueft ihn seit
671
+ jeher richtig (gemessen 09.09.2026). */
672
+ if (err) { return fertig && fertig(err); }
610
673
  uebernimmObjekt(bau.id, bau.obj);
611
674
  if (fertig) { fertig(); }
612
675
  });
@@ -622,7 +685,7 @@ function mqttPunktBauen(kanal, name, was) {
622
685
  if (!alt0) { return null; }
623
686
  obj = JSON.parse(JSON.stringify(alt0));
624
687
  var cu0 = obj.common.custom || {};
625
- Object.keys(cu0).forEach(function (i) { if (/^mqtt/.test(i)) { cu0[i].publish = true; } });
688
+ Object.keys(cu0).forEach(function (i) { if (/^mqtt/.test(i) && cu0[i]) { cu0[i].publish = true; } });
626
689
  obj.common.custom = cu0;
627
690
  } else {
628
691
  var w0 = befehlsWissen(name) || {};
@@ -656,7 +719,7 @@ export function mqttEinzeln(kanal, name, was, knopf) {
656
719
  if (!alt) { return; }
657
720
  obj = JSON.parse(JSON.stringify(alt));
658
721
  var cu = obj.common.custom || {};
659
- Object.keys(cu).forEach(function (i) { if (/^mqtt/.test(i)) { cu[i].publish = true; } });
722
+ Object.keys(cu).forEach(function (i) { if (/^mqtt/.test(i) && cu[i]) { cu[i].publish = true; } });
660
723
  obj.common.custom = cu;
661
724
  } else {
662
725
  var w = befehlsWissen(name) || {};
@@ -846,7 +909,7 @@ function mqttZuSchreiben() {
846
909
  if (!alt) { return; }
847
910
  var kopie = JSON.parse(JSON.stringify(alt));
848
911
  var cu = kopie.common.custom || {};
849
- Object.keys(cu).forEach(function (i) { if (/^mqtt/.test(i)) { cu[i].publish = true; } });
912
+ Object.keys(cu).forEach(function (i) { if (/^mqtt/.test(i) && cu[i]) { cu[i].publish = true; } });
850
913
  kopie.common.custom = cu;
851
914
  raus.push({ id: x.id, obj: kopie, neu: false });
852
915
  });