iobroker.alias-workbench 0.9.0 → 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.
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
  });
@@ -112,6 +112,16 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
112
112
  var markeCache = {};
113
113
  function bewerte(m) {
114
114
  if (markeCache[m] !== undefined) { return markeCache[m]; }
115
+ /* Die angebotene Liste ist gefiltert — der gewaehlte Typ nicht:
116
+ `mBeschriften` ruft `markeFuer(e.want)` direkt, und `e.want` kommt
117
+ ungeprueft aus `v.geraetetyp`. Eine eingelesene Vorlage mit dem
118
+ Musternamen eines neueren type-detector liess `musterVon(m).states`
119
+ werfen, und der Entwurf wurde gar nicht erst gezeichnet — die
120
+ rechte Seite blieb leer (gemessen 09.09.2026). */
121
+ if (!musterVon(m)) {
122
+ markeCache[m] = { mark: tr('pattern.doesNotFit'), passt: false, belegt: 0 };
123
+ return markeCache[m];
124
+ }
115
125
  /* Die Vorschau muss vorhersagen, was das Auswaehlen wirklich tut.
116
126
  Beim Vorschlag werden Lampe und Steckdose samt Rollen neu gebaut -
117
127
  also auch hier gegen den umgebauten Entwurf pruefen. Sonst stuende
@@ -81,6 +81,19 @@ export function nachziehenErledigt() {
81
81
  nachziehBaum = false;
82
82
  }
83
83
 
84
+ /* Nach einem Abbruch sind beim Server alle Abos weg.
85
+
86
+ `subscribeObjects` und `subscribe` gelten nur fuer die Sitzung, die
87
+ sie gestellt hat — der WebSocket-Shim des Admin erneuert von sich aus
88
+ nichts (das Wort `subscribe` kommt in seinem Quelltext kein einziges
89
+ Mal vor). Der `objectChange`-Horcher wird hier ausdruecklich NICHT
90
+ noch einmal angemeldet: der haengt am Socket-Objekt und hat den
91
+ Abbruch ueberlebt. Nur die Anmeldung beim Server fehlt. */
92
+ export function abosErneuern() {
93
+ if (horcht) { socket.emit('subscribeObjects', '*'); }
94
+ (S.abo || []).forEach(function (m) { socket.emit('subscribe', m); });
95
+ }
96
+
84
97
  export function horcheAufObjekte() {
85
98
  if (horcht) { return; }
86
99
  horcht = true;
@@ -95,11 +108,20 @@ export function horcheAufObjekte() {
95
108
  var kern = function (o) {
96
109
  return o ? JSON.stringify([o.type, o.common, o.native]) : null;
97
110
  };
98
- var vorher = kern(S.objects[id]);
99
111
  var nachher = kern(obj);
100
- if (vorher === nachher) { return; }
101
112
 
102
- /* Aufzaehlungen gehen einen eigenen Weg.
113
+ /* Aufzaehlungen zuerst — und zwar VOR dem Vergleich mit `S.objects`.
114
+
115
+ Der Vergleich unten fragt `S.objects[id]`, und dort liegen
116
+ Aufzaehlungen nie. Beim Loeschen einer Aufzaehlung sind alter und
117
+ neuer Wert deshalb beide `null`, der Frueheinstieg griff, und
118
+ `delete enums[id]` wurde nie erreicht. Die geloeschte Aufzaehlung
119
+ blieb im Vorrat stehen, und der naechste Alias mit diesem Raum
120
+ kopierte sie samt aller alten Mitglieder zurueck an ihren Platz.
121
+ Gemessen 09.09.2026 am Testsystem: `enum.rooms.WBP7` war am System
122
+ geloescht und waere mit zwei fremden Mitgliedern wieder entstanden.
123
+
124
+ Aufzaehlungen gehen einen eigenen Weg.
103
125
 
104
126
  Sie gehoeren nicht in `objects` - dort stuenden sie im Geraetebaum,
105
127
  wo sie nichts zu suchen haben. Nachziehen muss man sie aber, und
@@ -128,6 +150,10 @@ export function horcheAufObjekte() {
128
150
  return;
129
151
  }
130
152
 
153
+ /* Alles Uebrige: nur was zaehlt, nicht jeder Schreibvorgang. */
154
+ var vorher = kern(S.objects[id]);
155
+ if (vorher === nachher) { return; }
156
+
131
157
  var gabEs = S.objects[id] !== undefined;
132
158
  if (obj) { S.objects[id] = obj; } else { delete S.objects[id]; }
133
159
  if (gabEs !== (S.objects[id] !== undefined)) { nachziehBaum = true; }
@@ -68,7 +68,7 @@ export function pruefungDopplung(e) {
68
68
  export function pruefungSenden(e) {
69
69
  var ziele = e.states.filter(function (x) { return x.on && x.srcW; });
70
70
  if (!ziele.length) { return { s: 'mut', t: tr('check.publish'), d: tr('check.noWritePath') }; }
71
- var mqtt = [], stumm = [], fehlt = [];
71
+ var mqtt = [], stumm = [], fehlt = [], unklar = [];
72
72
  ziele.forEach(function (x) {
73
73
  /* Drei Faelle, nicht zwei: den Punkt gibt es gar nicht, es gibt ihn
74
74
  und er ist stumm, oder er sendet. Der erste kam dazu, seit die
@@ -77,11 +77,19 @@ export function pruefungSenden(e) {
77
77
  var m = mqttEinstellung(x.srcW);
78
78
  if (!m) { return; }
79
79
  mqtt.push(x);
80
- if (!m.publish) { stumm.push(x.n); }
80
+ /* Nur ein ausdrueckliches Nein ist ein Nein. `null` heisst
81
+ unbekannt: Objekte von ioBroker.mqtt und sonoff tragen das Thema
82
+ nur in `native`, ohne `custom`-Block — ueber ihre Sendefaehigkeit
83
+ sagt das nichts. Vorher stand dort fest `false`, und die Pruefung
84
+ meldete rot „kann nicht senden", ohne dass sich etwas daran
85
+ aendern liess (gemessen 09.09.2026). */
86
+ if (m.publish === false) { stumm.push(x.n); }
87
+ else if (m.publish === null) { unklar.push(x.n); }
81
88
  });
82
89
  if (fehlt.length) { return { s: 'bad', t: tr('check.publish'), d: tr('check.sendPointMissing', fehlt.join(', ')) }; }
83
90
  if (!mqtt.length) { return { s: 'mut', t: tr('check.publish'), d: tr('check.notMqtt') }; }
84
91
  if (stumm.length) { return { s: 'bad', t: tr('check.publish'), d: tr('check.cannotSend', stumm.join(', ')) }; }
92
+ if (unklar.length) { return { s: 'mut', t: tr('check.publish'), d: tr('check.sendUnknown', unklar.join(', ')) }; }
85
93
  return { s: 'ok', t: tr('check.publish'), d: tr('check.canSend', mqtt.length) };
86
94
  }
87
95
 
@@ -162,7 +162,15 @@ export function tauschPlan(kanal, neuKanal) {
162
162
  }
163
163
  if (!zeile.neuW && zeile.altW) {
164
164
  var gw = geraten(zeile.altW, neuKanal);
165
- if (gw) { zeile.neuW = gw; }
165
+ /* Was hier geraten wird, muss ein anderer Punkt sein als die
166
+ Lesequelle. Bei getrennten Quellen heissen Melder und Befehl oft
167
+ gleich - `stat.POWER` und `cmnd.POWER` -, und das Raten greift
168
+ nach dem Namen: aus der Schreibquelle `cmnd.POWER` wurde am
169
+ neuen Geraet `stat.POWER`, also genau der Melder. Der Alias
170
+ haette danach seinen Befehl ins Status-Thema geschickt; das
171
+ Geraet schaltet nicht und die Rueckmeldung ist verfaelscht
172
+ (gemessen 09.09.2026). Lieber kein Treffer als dieser. */
173
+ if (gw && gw !== zeile.neuR) { zeile.neuW = gw; }
166
174
  }
167
175
  /* Bei einfacher Quelle gilt die Lesequelle fuer beides. */
168
176
  if (zeile.einfach && zeile.neuR) { zeile.neuW = zeile.neuR; }
@@ -191,7 +199,19 @@ export function tauschObjekte(kanal, neuKanal, plan, weg) {
191
199
  delete neu.ts; delete neu.from; delete neu.user; delete neu.acl; delete neu._id;
192
200
  var a = neu.common.alias || {};
193
201
  if (z.neuW && z.neuW !== z.neuR) { a.id = { read: z.neuR, write: z.neuW }; }
194
- else { a.id = z.neuR; }
202
+ else if (z.einfach) { a.id = z.neuR; }
203
+ else {
204
+ /* Getrennte Quellen ohne Schreibtreffer: die Objektform bleibt.
205
+
206
+ Eine blosse Zeichenkette heisst fuer den js-controller „lesen
207
+ und schreiben auf dieselbe Kennung". Aus einem Alias, der den
208
+ Zustand liest und den Befehl woanders hinschickt, wurde damit
209
+ einer, der auf den Melder schreibt - und aus einem bewusst nur
210
+ lesenden Punkt bei `common.write: true` sogar ein schreibender.
211
+ Ohne Treffer bleibt `write` einfach weg: dann schreibt der Alias
212
+ nirgendwohin, und das ist die ehrliche Auskunft. */
213
+ a.id = { read: z.neuR };
214
+ }
195
215
  neu.common.alias = a;
196
216
  raus.push({ id: z.id, neu: false, obj: neu });
197
217
  });
@@ -461,6 +481,15 @@ function zeichneTausch() {
461
481
  rechts.appendChild(paarZeile(tr('write.diffNew'),
462
482
  z.neuR + (z.neuW && z.neuW !== z.neuR ? ' → ' + z.neuW : ''),
463
483
  'sn', gleich ? 'var(--ink-3)' : 'var(--ok)'));
484
+ /* Lesequelle gefunden, Schreibquelle nicht — das gehoert gesagt.
485
+ Sonst steht nur die neue Lesequelle in Gruen da, und dass der
486
+ Punkt danach nirgendwohin schreibt, merkt man erst am Geraet. */
487
+ if (z.altW && z.altW !== z.altR && !z.neuW) {
488
+ var kw = el('div');
489
+ kw.style.color = 'var(--bad)';
490
+ kw.textContent = tr('swap.noWriteMatch');
491
+ rechts.appendChild(kw);
492
+ }
464
493
  } else {
465
494
  var fehlt = el('div');
466
495
  fehlt.style.color = 'var(--bad)';