iobroker.alias-workbench 0.10.0 → 0.12.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.
Files changed (54) hide show
  1. package/README.md +58 -98
  2. package/admin/css/werkbank.css +88 -0
  3. package/admin/i18n/de.json +766 -1
  4. package/admin/i18n/en.json +766 -1
  5. package/admin/i18n/es.json +766 -1
  6. package/admin/i18n/fr.json +766 -1
  7. package/admin/i18n/it.json +766 -1
  8. package/admin/i18n/nl.json +766 -1
  9. package/admin/i18n/pl.json +766 -1
  10. package/admin/i18n/pt.json +766 -1
  11. package/admin/i18n/ru.json +766 -1
  12. package/admin/i18n/uk.json +766 -1
  13. package/admin/i18n/zh-cn.json +766 -1
  14. package/admin/js/ablage.js +118 -0
  15. package/admin/js/attrappe.js +749 -0
  16. package/admin/js/baum.js +52 -5
  17. package/admin/js/detail.js +64 -3
  18. package/admin/js/entwurf.js +246 -31
  19. package/admin/js/ergebnis.js +77 -2
  20. package/admin/js/katalog.js +235 -40
  21. package/admin/js/mqtt.js +297 -27
  22. package/admin/js/musternamen.js +70 -26
  23. package/admin/js/objekte.js +2 -0
  24. package/admin/js/quellentausch.js +823 -16
  25. package/admin/js/schreiben.js +3 -2
  26. package/admin/js/sprache.js +11 -8
  27. package/admin/js/vorlagen.js +8 -3
  28. package/admin/js/vorlagenblatt.js +228 -64
  29. package/admin/js/werte.js +294 -5
  30. package/admin/js/zielleiste.js +19 -10
  31. package/admin/js/zuordnung.js +8 -7
  32. package/admin/js/zustand.js +32 -0
  33. package/admin/js/zustandsliste.js +9 -2
  34. package/admin/tab.html +24 -0
  35. package/admin/vorlagen/hm-dimmer.json +32 -8
  36. package/admin/vorlagen/hm-fensterkontakt-hm.json +20 -5
  37. package/admin/vorlagen/hm-fensterkontakt-hmip.json +28 -7
  38. package/admin/vorlagen/hm-heizgruppe.json +38 -11
  39. package/admin/vorlagen/hm-heizkoerper.json +42 -12
  40. package/admin/vorlagen/hm-rollladen.json +28 -7
  41. package/admin/vorlagen/hm-schaltaktor-lampe.json +24 -6
  42. package/admin/vorlagen/hm-schaltaktor-mehrfach.json +32 -8
  43. package/admin/vorlagen/hm-schaltaktor-steckdose.json +24 -6
  44. package/admin/vorlagen/hm-temperatur-feuchte.json +8 -2
  45. package/admin/vorlagen/hm-wandthermostat.json +36 -9
  46. package/admin/vorlagen/messpunkt.json +40 -10
  47. package/admin/vorlagen/tasmota-lampe.json +48 -12
  48. package/admin/vorlagen/tasmota-mehrfach.json +40 -10
  49. package/admin/vorlagen/tasmota-rgb.json +36 -9
  50. package/admin/vorlagen/tasmota-steckdose.json +56 -14
  51. package/io-package.json +41 -28
  52. package/package.json +3 -3
  53. package/admin/sprachen/de.json +0 -685
  54. package/admin/sprachen/en.json +0 -685
package/admin/js/mqtt.js CHANGED
@@ -8,7 +8,7 @@ import { S } from './zustand.js';
8
8
  import { socket } from './verbindung.js';
9
9
  import { $, el, klappZeichen } from './basis.js';
10
10
  import { tr } from './sprache.js';
11
- import { hatPunkt, jsonVon, kindZustaende } from './werte.js';
11
+ import { hatPunkt, jsonVon, geraetePunkte, punktKennung, istZerlegt } from './werte.js';
12
12
  import './entwurf.js';
13
13
  import { zeichneErgebnis } from './ergebnis.js';
14
14
  import { holeZweig, holeObjekt, uebernimmObjekt } from './objekte.js';
@@ -81,21 +81,36 @@ export function mqttEinstellung(id) {
81
81
  roh: cu[k] };
82
82
  }
83
83
 
84
+ /* ---- mqttGeraet: Anfang (test/vorpruefung.js schneidet hier) ----
85
+
84
86
  /* Ist der Kanal ein MQTT-Geraet, und wie heisst sein Thema? Alles aus
85
87
  dem Bestand abgelesen — auch die Reihenfolge von Praefix und Thema,
86
88
  denn FullTopic ist frei einstellbar. */
87
89
  export function mqttGeraet(kanal) {
88
- var kinder = kindZustaende(kanal);
89
- if (!kinder.length) { return null; }
90
+ var punkte = geraetePunkte(kanal);
91
+ if (!punkte.length) { return null; }
92
+ /* Welches erste Stueck taugt als Praefix?
93
+
94
+ Bei einem zerlegten Geraet hat `geraetePunkte` das schon am Thema
95
+ entschieden — dort gilt jedes. Bei der gewohnten Bauform ist der
96
+ relative Name einfach der Pfad, und ein Geraet unter einem Geraet
97
+ saehe von hier aus wie ein Praefix aus: `…Wohnkueche.Ambilight`
98
+ traegt `Ambilight.tele.LWT` des Untergeraets, und ohne diese
99
+ Unterscheidung kam als Basispfad `SmartHome/Wohnkueche/Ambilight/tele`
100
+ heraus (gemessen 30.09.2026 am Produktivbestand, 1 von 30 Geraeten).
101
+ Wer seine Praefixe umbenannt hat, wird ueber die Zusammenfassung
102
+ erkannt, nicht hier. */
103
+ var zerlegt = istZerlegt(kanal);
90
104
  var fund = null;
91
- kinder.forEach(function (id) {
105
+ punkte.forEach(function (pt) {
92
106
  if (fund) { return; }
107
+ var id = pt.id;
93
108
  var e = mqttEinstellung(id);
94
109
  if (!e || !e.topic) { return; }
95
- var rel = id.slice(kanal.length + 1).split('.');
110
+ var rel = pt.rel.split('.');
96
111
  if (rel.length < 2) { return; }
97
- var praefix = rel[0]; /* stat | tele | cmnd */
98
- if (!/^(stat|tele|cmnd)$/i.test(praefix)) { return; }
112
+ var praefix = rel[0];
113
+ if (!zerlegt && !/^(stat|tele|cmnd)$/i.test(praefix)) { return; }
99
114
  var teile = e.topic.split('/');
100
115
  var pi = -1;
101
116
  teile.forEach(function (t, i) { if (t.toLowerCase() === praefix.toLowerCase()) { pi = i; } });
@@ -114,6 +129,8 @@ export function mqttGeraet(kanal) {
114
129
  return fund;
115
130
  }
116
131
 
132
+ /* ---- mqttGeraet: Ende ---- */
133
+
117
134
  /* Das Thema fuer einen bestimmten Punkt bauen, mit der abgelesenen
118
135
  Reihenfolge. */
119
136
  function mqttThema(g, praefix, name) {
@@ -175,6 +192,88 @@ export function befehlsWissen(name) {
175
192
  der Karte und die Pruefung meldete "alle in Ordnung", als waere etwas
176
193
  geprueft worden. Beides ist jetzt an diese Frage gebunden
177
194
  (Ricardo, 16.09.2026). */
195
+ /* ---- Erreichbarkeit: Anfang (test/vorpruefung.js schneidet hier) ----
196
+
197
+ Meldet sich das Geraet noch? Drei Antworten, nicht zwei.
198
+
199
+ `Offline` schreibt der **Broker**, in dem Moment, in dem er die
200
+ Verbindung verliert (Keepalive abgelaufen, Stecker raus) — das ist
201
+ eine Tatsache. `Online` schreibt das **Geraet selbst**, einmal beim
202
+ Verbinden, und die Nachricht bleibt danach als retained beim Broker
203
+ stehen. Sie kann beliebig alt sein: ein Geraet, das ein Jahr
204
+ durchlaeuft, wiederholt sie nie.
205
+
206
+ Deshalb ist „Online" nur eine Behauptung und wird gegen die
207
+ Telemetrie geprueft. Gemessen am 30.09.2026 an Ricardos Bestand:
208
+ `SmartHome.Waschkueche.tasmota` steht auf `Online`, zuletzt geaendert
209
+ am 29.05.2022, hat kein einziges `tele/STATE` und antwortet auf
210
+ seiner letzten bekannten IP nicht. Eine zweiwertige Anzeige haette
211
+ das gruen gemeldet.
212
+
213
+ Umgekehrt wird nur als offline gewertet, was ausdruecklich offline
214
+ sagt. OpenBeken, ESPHome und Zigbee2MQTT benutzen andere Woerter; ein
215
+ unbekanntes Wort heisst „weiss ich nicht", nicht „weg". Eine falsch
216
+ gelesene Auskunft ist schlimmer als keine. */
217
+ export function lwtWert(roh) {
218
+ var w = String(roh === null || roh === undefined ? '' : roh).trim().toLowerCase();
219
+ if (w === 'offline' || w === 'false' || w === '0' || w === 'disconnected') { return false; }
220
+ if (w === 'online' || w === 'true' || w === '1' || w === 'connected') { return true; }
221
+ return null;
222
+ }
223
+
224
+ /* Woher die Auskunft kommt: das LWT-Thema des Geraets, sonst ein Punkt
225
+ mit der ioBroker-Rolle `indicator.reachable` — so legt ioBroker.sonoff
226
+ seinen `alive`-Punkt an, und so halten es Adapter, die sich an die
227
+ Konvention halten. Nach der Rolle suchen, nicht nach dem Namen: der
228
+ heisst je Adapter anders. */
229
+ export function erreichbarkeitsQuelle(kanal) {
230
+ var lwt = null, rolle = null;
231
+ geraetePunkte(kanal).forEach(function (p) {
232
+ if (!lwt && /(^|\.)LWT$/i.test(p.rel)) { lwt = p; return; }
233
+ var o = S.objects[p.id];
234
+ if (!rolle && o && o.common && o.common.role === 'indicator.reachable') { rolle = p; }
235
+ });
236
+ if (lwt) { return { punkt: lwt, art: 'lwt' }; }
237
+ if (rolle) { return { punkt: rolle, art: 'rolle' }; }
238
+ return null;
239
+ }
240
+
241
+ /* Wann kam zuletzt eine Meldung, die das Geraet von sich aus schickt?
242
+ `STATE` und `SENSOR` — nicht `stat/...`, das kommt nur auf Befehl,
243
+ und nicht das LWT selbst, das ja gerade geprueft wird. */
244
+ function letzteTelemetrie(kanal) {
245
+ var neuste = 0;
246
+ geraetePunkte(kanal).forEach(function (p) {
247
+ if (!/(^|\.)(STATE|SENSOR)$/i.test(p.rel)) { return; }
248
+ var w = S.werte[p.id];
249
+ if (w && w.ts && w.ts > neuste) { neuste = w.ts; }
250
+ });
251
+ return neuste;
252
+ }
253
+
254
+ /* → { stand: 'offline' | 'da' | 'still' | null, id, seit, takt }
255
+
256
+ `null` heisst: keine Auskunft, und dann wird auch keine behauptet. */
257
+ export function geraetErreichbar(kanal, jetztMs) {
258
+ var q = erreichbarkeitsQuelle(kanal);
259
+ if (!q) { return null; }
260
+ var w = S.werte[q.punkt.id];
261
+ if (!w) { return null; }
262
+ var ja = lwtWert(w.val);
263
+ if (ja === null) { return null; }
264
+ var seit = w.lc || w.ts || 0;
265
+ if (!ja) { return { stand: 'offline', id: q.punkt.id, seit: seit, takt: null }; }
266
+ /* Ein Adapter, der die Erreichbarkeit selbst pflegt, braucht keine
267
+ Gegenprobe — er hat sie schon gemacht. */
268
+ if (q.art === 'rolle') { return { stand: 'da', id: q.punkt.id, seit: seit, takt: null }; }
269
+ var takt = telePeriode(kanal) || 300;
270
+ var neuste = letzteTelemetrie(kanal);
271
+ var jetzt = jetztMs || new Date().getTime();
272
+ var still = !neuste || (jetzt - neuste) > takt * 3000;
273
+ return { stand: still ? 'still' : 'da', id: q.punkt.id, seit: still ? (neuste || seit) : seit, takt: takt };
274
+ }
275
+ /* ---- Erreichbarkeit: Ende ---- */
276
+
178
277
  export function telegesteuert(e) {
179
278
  if (!e || !e.states) { return []; }
180
279
  return e.states.filter(function (x) {
@@ -183,17 +282,54 @@ export function telegesteuert(e) {
183
282
  });
184
283
  }
185
284
 
186
- /* Wie oft meldet das Geraet von sich aus? Steht in tele/STATE.
285
+ /* Wie oft meldet das Geraet von sich aus?
286
+
287
+ Gesucht wurde das frueher in `tele/STATE` — da steht es nicht.
288
+ Nachgemessen am 30.09.2026 ueber den ganzen Bestand: von 36 Geraeten,
289
+ die `tele/STATE` veroeffentlichen, trug **kein einziges** ein Feld
290
+ `TelePeriod` darin; in `stat/STATUS11` (StatusSTS) steht es ebenso
291
+ wenig. Diese Funktion gab also an allen 42 Geraeten `null` zurueck,
292
+ und der Hinweis zu SetOption59 nannte nie eine Zahl, obwohl genau
293
+ dafuer gebaut.
187
294
 
188
- Ohne diese Zahl stand im Hinweis "minutenlang" — das unterstellt die
189
- Tasmota-Vorgabe von 300 Sekunden. An Ricardos Leiste sind es zehn
190
- (gemessen 16.09.2026: tele/STATE im Zehnsekundentakt). Wer eine Zahl
191
- nennt, soll die richtige nennen. */
192
- export function telePeriode(kanal) {
193
- var id = hatPunkt(kanal, 'tele.STATE');
295
+ Zu holen ist die Zahl wie SetOption59: eine leere Nutzlast an
296
+ `cmnd/TelePeriod` ist eine Abfrage, die Antwort steht in
297
+ `stat/RESULT` als `{"TelePeriod":10}`. Direkt am Geraet gegengeprueft
298
+ (`cm?cmnd=TelePeriod` → `{"TelePeriod":10}`).
299
+
300
+ `merken` wie bei `sofortRueckmeldung`: nur die Abfrage schreibt den
301
+ Wert fort, der Zeichenpfad liest bloss. */
302
+ export function telePeriode(kanal, merken) {
303
+ var id = hatPunkt(kanal, 'stat.RESULT');
194
304
  var j = id ? jsonVon(id) : null;
195
- var n = j && Number(j.TelePeriod);
196
- return (n && isFinite(n)) ? n : null;
305
+ var n = j ? Number(j.TelePeriod) : NaN;
306
+ if (j && j.TelePeriod !== undefined && isFinite(n) && n > 0) {
307
+ if (merken) { merkeTakt(kanal, n); }
308
+ return n;
309
+ }
310
+ var g = gemerkterTakt(kanal);
311
+ return g ? g.wert : null;
312
+ }
313
+
314
+ /* Gemerkt wird am Abfragepunkt selbst — dort, wo gefragt wurde. Kein
315
+ eigenes Objekt, und beim Aufraeumen verschwindet es mit. Dieselbe
316
+ Bauart wie `gemerktesSo59`. */
317
+ export function gemerkterTakt(kanal) {
318
+ var id = hatPunkt(kanal, 'cmnd.TelePeriod');
319
+ var n = id && S.objects[id] && S.objects[id].native;
320
+ if (!n || n.takt === undefined) { return null; }
321
+ return { wert: n.takt, zeit: n.taktZeit || 0 };
322
+ }
323
+
324
+ function merkeTakt(kanal, wert) {
325
+ var id = hatPunkt(kanal, 'cmnd.TelePeriod');
326
+ if (!id || !S.objects[id]) { return; }
327
+ var n = S.objects[id].native || (S.objects[id].native = {});
328
+ if (n.takt === wert) { return; }
329
+ n.takt = wert;
330
+ n.taktZeit = neueZeit();
331
+ var kopie = JSON.parse(JSON.stringify(S.objects[id]));
332
+ socket.emit('setObject', id, kopie, function () {});
197
333
  }
198
334
 
199
335
  /* Meldet das Geraet Aenderungen sofort? Das haengt an SetOption59.
@@ -294,8 +430,8 @@ export function mqttLage(kanal) {
294
430
  }
295
431
  /* Auch cmnd-Punkte, die es schon gibt, ohne dass sie im Zustand
296
432
  auftauchen — Status etwa. Stumm sind sie genauso. */
297
- kindZustaende(kanal).forEach(function (id) {
298
- var rel = id.slice(kanal.length + 1);
433
+ geraetePunkte(kanal).forEach(function (pt) {
434
+ var id = pt.id, rel = pt.rel;
299
435
  if (rel.indexOf('cmnd.') !== 0) { return; }
300
436
  var name = rel.slice(5);
301
437
  if (stumm.some(function (x) { return x.name === name; })) { return; }
@@ -308,6 +444,15 @@ export function mqttLage(kanal) {
308
444
  sofort: sofortRueckmeldung(kanal) };
309
445
  }
310
446
 
447
+ /* Ueber wie viele Zweige ist dieses Geraet verteilt? `0`, wenn es an
448
+ einem Stueck liegt. */
449
+ function zerlegtZweige(kanal) {
450
+ if (!istZerlegt(kanal)) { return 0; }
451
+ var zweige = {};
452
+ geraetePunkte(kanal).forEach(function (p) { zweige[p.rel.split('.')[0]] = 1; });
453
+ return Object.keys(zweige).length;
454
+ }
455
+
311
456
  export function mqttKarte(host, kanal) {
312
457
  var l = mqttLage(kanal);
313
458
  if (!l) { return; }
@@ -331,6 +476,21 @@ export function mqttKarte(host, kanal) {
331
476
  th.style.fontFamily = 'var(--mono)';
332
477
  th.style.fontSize = '11px';
333
478
  ch.appendChild(th);
479
+ /* Ein zerlegtes Geraet sieht im Baum nicht wie eines aus. Wer hier
480
+ Punkte anlegt, soll wissen, dass die Werkbank mehrere Zweige
481
+ zusammengefasst hat — sonst sucht er sie hinterher am falschen
482
+ Ort. */
483
+ var zweigZahl = zerlegtZweige(kanal);
484
+ if (zweigZahl) { ch.appendChild(el('span', 'chip mut', tr('mq.split', zweigZahl))); }
485
+ /* Erreichbarkeit nur, wenn es etwas zu sagen gibt. Kein gruener Chip
486
+ fuer den Normalfall: die Kopfzeile traegt schon drei, ein vierter,
487
+ der fast immer dasteht, macht die anderen unsichtbar. */
488
+ var err = geraetErreichbar(kanal);
489
+ if (err && err.stand === 'offline') {
490
+ ch.appendChild(el('span', 'chip bad', tr('mq.offline')));
491
+ } else if (err && err.stand === 'still') {
492
+ ch.appendChild(el('span', 'chip warn', tr('mq.silent')));
493
+ }
334
494
  if (l.fehlen.length) { ch.appendChild(el('span', 'chip warn', tr('mq.missing', l.fehlen.length))); }
335
495
  if (l.stumm.length) { ch.appendChild(el('span', 'chip bad', tr('mq.mute', l.stumm.length))); }
336
496
  /* Nur warnen, wo es jemanden trifft: Ohne eine Zeile, die aus tele
@@ -358,6 +518,13 @@ export function mqttKarte(host, kanal) {
358
518
  einl.textContent = tr('mq.intro');
359
519
  b.appendChild(einl);
360
520
 
521
+ if (zweigZahl) {
522
+ var zh = el('div', 'hint');
523
+ zh.style.marginBottom = '9px';
524
+ zh.textContent = tr('mq.splitHint', zweigZahl);
525
+ b.appendChild(zh);
526
+ }
527
+
361
528
  if (l.befehle) {
362
529
  b.appendChild(el('div', 'hint', tr('mq.commandsFrom', l.befehle.woher)));
363
530
 
@@ -457,8 +624,8 @@ export function mqttKarte(host, kanal) {
457
624
  Antwort auf den letzten Befehl enthaelt und nicht den ganzen
458
625
  Zustand. Steht die Liste vollstaendig da, gibt es den Knopf nicht —
459
626
  er koennte nichts hinzufuegen. */
460
- var luecke = !l.befehle || l.befehle.woher === 'stat.RESULT';
461
- if (luecke) {
627
+ var fehlt = abfrageLohnt(kanal, l);
628
+ if (fehlt.length) {
462
629
  var kasten = el('div');
463
630
  kasten.style.marginTop = '11px';
464
631
  kasten.style.paddingTop = '10px';
@@ -469,10 +636,23 @@ export function mqttKarte(host, kanal) {
469
636
  if (abfrageLaeuft[kanal]) { bf.disabled = true; }
470
637
  bf.addEventListener('click', function () { mqttFragen(kanal, bf); });
471
638
  kasten.appendChild(bf);
639
+ /* Sagen, was die Abfrage holen wuerde — sonst steht da ein Knopf
640
+ an einem Geraet, dessen Befehlsliste vollstaendig ist, und
641
+ niemand weiss, wofuer. */
472
642
  var warum = el('div', 'hint');
473
643
  warum.style.marginTop = '6px';
474
- warum.textContent = (l.befehle ? tr('mq.onlyResult') : tr('mq.neverReported')) +
475
- ' ' + tr('mq.askHint');
644
+ var teile = [];
645
+ if (fehlt.indexOf('befehle') > -1) {
646
+ teile.push(l.befehle ? tr('mq.onlyResult') : tr('mq.neverReported'));
647
+ }
648
+ if (fehlt.indexOf('ip') > -1) { teile.push(tr('mq.missingIp')); }
649
+ if (fehlt.indexOf('takt') > -1) { teile.push(tr('mq.missingPeriod')); }
650
+ teile.push(tr('mq.askHint'));
651
+ /* Wer hier klickt, wartet sonst auf eine Antwort, die nicht kommen
652
+ kann. Gesperrt wird trotzdem nichts — das Geraet kann seit einer
653
+ Sekunde wieder da sein. */
654
+ if (err && err.stand !== 'da') { teile.push(tr('mq.askOffline')); }
655
+ warum.textContent = teile.join(' ');
476
656
  kasten.appendChild(warum);
477
657
  if (mqttAbfrage[kanal] !== undefined) {
478
658
  var ah = el('div', 'hint');
@@ -559,6 +739,10 @@ function so59Kasten(b, kanal, l, telZ) {
559
739
  /* Woher der Befund stammt, gehoert dazu — sonst weiss niemand, ob er
560
740
  von eben ist oder von vorgestern. */
561
741
  var alter = (l.sofort === null) ? '' : so59Alter(kanal);
742
+ /* Dieselbe Warnung wie am Abfrageknopf: auch SetOption59 wartet auf
743
+ eine Antwort aus `stat/RESULT`. */
744
+ var errS = geraetErreichbar(kanal);
745
+ if (errS && errS.stand !== 'da') { hinweis += ' ' + tr('mq.askOffline'); }
562
746
  sk.appendChild(el('div', 'hint', hinweis + (alter ? ' · ' + alter : '')));
563
747
  b.appendChild(sk);
564
748
  }
@@ -623,7 +807,7 @@ function so59(kanal, knopf, wert) {
623
807
  naechsten Nachzieher, Sekunden spaeter (Ricardo, 25.08.2026). */
624
808
  zeichneErgebnis();
625
809
  var thema = mqttThema(g, 'cmnd', 'SetOption59');
626
- var id = hatPunkt(kanal, 'cmnd.SetOption59') || (kanal + '.cmnd.SetOption59');
810
+ var id = hatPunkt(kanal, 'cmnd.SetOption59') || punktKennung(kanal, 'cmnd.SetOption59');
627
811
  var obj = {
628
812
  type: 'state',
629
813
  common: { name: 'SetOption59', type: 'mixed', read: true, write: true, role: 'text',
@@ -667,7 +851,7 @@ function mqttFragen(kanal, _knopf) {
667
851
  delete mqttAbfrage[kanal];
668
852
  zeichneErgebnis();
669
853
  var thema = mqttThema(g, 'cmnd', 'Status');
670
- var id = kanal + '.cmnd.Status';
854
+ var id = punktKennung(kanal, 'cmnd.Status');
671
855
  var vorhanden = hatPunkt(kanal, 'cmnd.Status');
672
856
  if (vorhanden) { id = vorhanden; }
673
857
 
@@ -705,7 +889,30 @@ function mqttFragen(kanal, _knopf) {
705
889
  var qid = hatPunkt(kanal, q);
706
890
  if (qid && S.werte[qid]) { vorTs[q] = S.werte[qid].ts || 0; }
707
891
  });
892
+ /* `Status 5` genauso zweimal wie `Status 11`, und nicht im
893
+ selben Zug: beim ersten Mal legt der mqtt-client nur das
894
+ Antwortobjekt `stat/STATUS5` an und wirft den Wert weg.
895
+
896
+ Am 01.10.2026 am Broker mitgeschnitten (`mosquitto_sub` auf
897
+ `SmartHome_Test/#`): `cmnd/Status 5` ging hinaus,
898
+ `stat/STATUS5` mit der IP kam zurueck — und in ioBroker
899
+ stand hinterher ein Objekt ohne Wert. Eine zweite Abfrage
900
+ 17 Sekunden spaeter trug ihn ein. Dasselbe erklaert die
901
+ INFO-Punkte, die am Testsystem seit jeher leer stehen: die
902
+ kommen nur beim Geraetestart, ein zweites Mal gibt es
903
+ nicht. */
708
904
  socket.emit('setState', id, { val: '5', ack: false }, function () {});
905
+ setTimeout(function () {
906
+ socket.emit('setState', id, { val: '5', ack: false }, function () {});
907
+ }, 2500);
908
+ /* Den Telemetrietakt gleich mitfragen. Er steht in keiner der
909
+ Meldungen, die das Geraet von sich aus schickt (gemessen an
910
+ 36 Geraeten: kein `TelePeriod` in `tele/STATE`, keines in
911
+ `StatusSTS`) — nur eine leere Nutzlast an
912
+ `cmnd/TelePeriod` holt ihn, Antwort in `stat/RESULT`.
913
+ Zuletzt gesendet, damit seine Antwort dort auch stehen
914
+ bleibt und nicht von einer spaeteren ueberschrieben wird. */
915
+ frageTakt(kanal, g);
709
916
  socket.emit('setState', id, { val: '11', ack: false }, function () {
710
917
  setTimeout(function () {
711
918
  /* Kam eine Antwort? Nur dann hat die Abfrage etwas
@@ -724,6 +931,13 @@ function mqttFragen(kanal, _knopf) {
724
931
  var jetzt = (qid && S.werte[qid]) ? (S.werte[qid].ts || 0) : 0;
725
932
  var frisch = bb && jetzt > (vorTs[bb.woher] || 0);
726
933
  mqttAbfrage[kanal] = (frisch && bb.befehle.length) ? bb.befehle.length : 0;
934
+ /* Den Takt nur werten, wenn `stat/RESULT` nach dem
935
+ Senden frisch geworden ist — sonst stuende ein
936
+ liegengebliebenes `{"TelePeriod":…}` von vorgestern
937
+ als frische Antwort da (dieselbe Regel wie H15). */
938
+ var rid3 = hatPunkt(kanal, 'stat.RESULT');
939
+ var rjetzt = (rid3 && S.werte[rid3]) ? (S.werte[rid3].ts || 0) : 0;
940
+ if (rjetzt > (vorTs['stat.RESULT'] || 0)) { telePeriode(kanal, true); }
727
941
  zeichneErgebnis();
728
942
  });
729
943
  }, 5000);
@@ -734,6 +948,62 @@ function mqttFragen(kanal, _knopf) {
734
948
  });
735
949
  }
736
950
 
951
+ /* Leere Nutzlast an `cmnd/TelePeriod` — eine Abfrage, sie aendert
952
+ nichts. Der Punkt wird dabei angelegt, denn an ihm haengt auch der
953
+ gemerkte Wert. */
954
+ function frageTakt(kanal, g) {
955
+ var thema = mqttThema(g, 'cmnd', 'TelePeriod');
956
+ var id = hatPunkt(kanal, 'cmnd.TelePeriod') || punktKennung(kanal, 'cmnd.TelePeriod');
957
+ var alt = S.objects[id];
958
+ var obj = {
959
+ type: 'state',
960
+ common: { name: 'TelePeriod', type: 'mixed', read: true, write: true, role: 'level',
961
+ unit: 's', desc: tr('mq.createdBy'), custom: mqttCustom(g, thema) },
962
+ /* Einen schon gemerkten Takt nicht wegwerfen, nur weil erneut
963
+ gefragt wird. */
964
+ native: { topic: thema, takt: alt && alt.native ? alt.native.takt : undefined,
965
+ taktZeit: alt && alt.native ? alt.native.taktZeit : undefined }
966
+ };
967
+ socket.emit('setObject', id, obj, function (err) {
968
+ if (err) { return; }
969
+ uebernimmObjekt(id, obj);
970
+ /* Zweimal, aus demselben Grund wie bei `Status 5`: existiert
971
+ `stat/RESULT` noch nicht, legt der mqtt-client es beim ersten Mal
972
+ nur an und verwirft den Wert. An einem eingefahrenen Geraet ist
973
+ der Punkt laengst da und das zweite Mal ist folgenlos. */
974
+ setTimeout(function () { socket.emit('setState', id, { val: '', ack: false }, function () {}); }, 300);
975
+ setTimeout(function () { socket.emit('setState', id, { val: '', ack: false }, function () {}); }, 3300);
976
+ });
977
+ }
978
+
979
+ /* Hat das Geraet seine IP schon irgendwo hinterlegt? Dieselben Quellen,
980
+ die die Tasmota-Vorlagen der Reihe nach durchgehen. */
981
+ export function hatAdresse(kanal) {
982
+ var s5 = jsonVon(hatPunkt(kanal, 'stat.STATUS5') || '');
983
+ if (s5 && s5.StatusNET && s5.StatusNET.IPAddress) { return true; }
984
+ var i2 = jsonVon(hatPunkt(kanal, 'tele.INFO2') || '');
985
+ if (i2 && (i2.IPAddress || (i2.Info2 && i2.Info2.IPAddress))) { return true; }
986
+ return false;
987
+ }
988
+
989
+ /* Was koennte eine Abfrage noch holen?
990
+
991
+ Frueher haftete der Knopf allein an der Befehlsliste — stand die
992
+ vollstaendig da, gab es ihn nicht. Genau dann fehlen aber oft die
993
+ beiden anderen Angaben, und es gibt keinen Weg mehr, sie zu holen:
994
+ die IP steht nur in `tele/INFO2` (kommt einmal beim Start) oder in
995
+ `stat/STATUS5` (nur auf Abruf), der Takt ueberhaupt nur auf Abruf.
996
+ Gemessen am 30.09.2026 an Ricardos Ambilight: Befehlsliste komplett,
997
+ kein Knopf, IP seit Monaten unbekannt — und wer sie per HTTP holen
998
+ wollte, brauchte dafuer die IP. */
999
+ export function abfrageLohnt(kanal, l) {
1000
+ var raus = [];
1001
+ if (!l || !l.befehle || l.befehle.woher === 'stat.RESULT') { raus.push('befehle'); }
1002
+ if (!hatAdresse(kanal)) { raus.push('ip'); }
1003
+ if (telePeriode(kanal) === null) { raus.push('takt'); }
1004
+ return raus;
1005
+ }
1006
+
737
1007
  function mqttCustom(g, thema) {
738
1008
  var c = {};
739
1009
  var inst = g.instanz || 'mqtt-client.0';
@@ -813,7 +1083,7 @@ function mqttPunktBauen(kanal, name, was) {
813
1083
  } else {
814
1084
  var w0 = befehlsWissen(name) || {};
815
1085
  var thema0 = mqttThema(g, 'cmnd', name);
816
- id = kanal + '.cmnd.' + name;
1086
+ id = punktKennung(kanal, 'cmnd.' + name);
817
1087
  var common0 = {
818
1088
  name: name,
819
1089
  role: w0.rolle || 'state',
@@ -847,7 +1117,7 @@ export function mqttEinzeln(kanal, name, was, knopf) {
847
1117
  } else {
848
1118
  var w = befehlsWissen(name) || {};
849
1119
  var thema = mqttThema(g, 'cmnd', name);
850
- id = kanal + '.cmnd.' + name;
1120
+ id = punktKennung(kanal, 'cmnd.' + name);
851
1121
  var common = {
852
1122
  name: name,
853
1123
  role: w.rolle || 'state',
@@ -1032,7 +1302,7 @@ function mqttZuSchreiben() {
1032
1302
  };
1033
1303
  if (w.einheit) { common.unit = w.einheit; }
1034
1304
  if (w.werteliste) { common.states = w.werteliste; }
1035
- raus.push({ id: kanal + '.cmnd.' + x.name, obj: { type: 'state', common: common, native: { topic: thema } }, neu: true });
1305
+ raus.push({ id: punktKennung(kanal, 'cmnd.' + x.name), obj: { type: 'state', common: common, native: { topic: thema } }, neu: true });
1036
1306
  });
1037
1307
  l.stumm.forEach(function (x) {
1038
1308
  if (!mqttAuswahl['fix:' + x.name]) { return; }
@@ -27,10 +27,18 @@
27
27
  Bricht die Kette - Aufbau geaendert, Datei zu gross -, bleibt die
28
28
  Karte leer und ueberall steht der rohe Name. Kein Fehler, nur weniger
29
29
  Komfort. Beide Schreibweisen werden verstanden: Werte in Backticks
30
- (Admin 8) und als JSON-Strings (Admin 7). */
30
+ (Admin 8) und als JSON-Strings (Admin 7).
31
31
 
32
- import { sprache } from './sprache.js';
32
+ Gesucht wird nur noch, wenn die Ablage nichts hergibt (ablage.js).
33
+ Gemessen am 07.10.2026: das Buendel, in dem die Karte bei Admin 8
34
+ steckt, kostete 1 727 KB und 854 ms — bei jedem Oeffnen des Reiters,
35
+ fuer eine Karte von gut zwei Kilobyte. Gezogen werden jetzt beide
36
+ Sprachen auf einmal, damit ein Sprachwechsel nicht denselben Abruf
37
+ noch einmal ausloest. */
38
+
39
+ import { sprache, SPRACHEN } from './sprache.js';
33
40
  import { holeText } from './basis.js';
41
+ import { leseAblage, ergaenzeAblage, stempelAus } from './ablage.js';
34
42
 
35
43
  var musterNamen = {};
36
44
  var geholt = false;
@@ -39,8 +47,8 @@ var geholt = false;
39
47
  kennt die Werkbank nicht (SPRACHEN in sprache.js). */
40
48
  var ANKER = { de: 'Ger\u00e4tetyp', en: 'Device type' };
41
49
 
42
- function ziehe(src) {
43
- var anker = ANKER[sprache] || ANKER.en;
50
+ function ziehe(src, spr) {
51
+ var anker = ANKER[spr || sprache] || ANKER.en;
44
52
  var re = /"type-Device type":(?:`([^`]*)`|"((?:[^"\\]|\\.)*)")/g;
45
53
  var m, start = -1;
46
54
  while ((m = re.exec(src))) {
@@ -62,6 +70,22 @@ function ziehe(src) {
62
70
  /* Welche Dateien der Admin ueberhaupt ausliefert. Zwei Verzeichnisse,
63
71
  zusammengelegt und ohne Doppel - siehe Kopf: keines allein reicht
64
72
  fuer beide Admin-Fassungen. */
73
+ /* Alle Sprachen auf einmal, nicht nur die eingestellte.
74
+
75
+ Die Karte wird abgelegt und beim naechsten Mal von dort genommen.
76
+ Wer dann die Sprache umstellt, haette ohne das hier eine Ablage, die
77
+ seine Sprache nicht enthaelt — und muesste das 7,9-MB-Buendel noch
78
+ einmal holen. Es sind zwei Sprachen und zusammen gut zwei Kilobyte;
79
+ beide mitzunehmen kostet nichts. */
80
+ function alleZiehen(src) {
81
+ var raus = {}, hatWas = false;
82
+ SPRACHEN.forEach(function (spr) {
83
+ var k = ziehe(src, spr);
84
+ if (k) { raus[spr] = k; hatWas = true; }
85
+ });
86
+ return hatWas ? raus : null;
87
+ }
88
+
65
89
  function adminDateien() {
66
90
  var raus = [];
67
91
  function dazu(f) {
@@ -114,29 +138,49 @@ function adminDateien() {
114
138
  export function ladeMusterNamen(nachher) {
115
139
  if (geholt) { return; }
116
140
  geholt = true;
117
- adminDateien().then(function (dateien) {
118
- if (!dateien.length) { throw new Error('kein Bundle'); }
119
- return dateien.reduce(function (kette, f) {
120
- return kette.then(function (fertig) {
121
- if (fertig) { return true; }
122
- return holeText('/' + f).then(function (src) {
123
- /* Die Grenze liegt hoeher als beim Aufzaehlungskatalog (3 MB):
124
- bei Admin 8 steckt die Karte ausgerechnet im grossen
125
- GUI-Buendel, gemessen 8,4 MB (08.09.2026). Mit der alten
126
- Grenze wurde genau diese Datei uebersprungen und produktiv
127
- blieb jeder Beiname roh. Teuer ist es nicht: derselbe Admin
128
- hat das Buendel gerade selbst geladen, der zweite Griff
129
- kommt aus dem Zwischenspeicher des Browsers. Alles darueber
130
- ist kein Buendel mehr, sondern ein Versehen. */
131
- if (src.length > 12000000) { return false; }
132
- var karte = ziehe(src);
133
- if (karte) { musterNamen = karte; return true; }
134
- return false;
135
- })['catch'](function () { return false; });
136
- });
137
- }, Promise.resolve(false));
141
+
142
+ var stempel = null;
143
+
144
+ function fertig() { if (typeof nachher === 'function') { nachher(); } }
145
+
146
+ /* Suchen gibt es nur, wenn die Ablage nichts hergibt. Vorher hing
147
+ hier ein Abruf von 1 727 KB an jedem Oeffnen des Reiters, fuer eine
148
+ Karte von gut zwei Kilobyte (gemessen 07.10.2026). */
149
+ function suchen() {
150
+ return adminDateien().then(function (dateien) {
151
+ if (!dateien.length) { throw new Error('kein Bundle'); }
152
+ return dateien.reduce(function (kette, f) {
153
+ return kette.then(function (gefunden) {
154
+ if (gefunden) { return gefunden; }
155
+ return holeText('/' + f).then(function (src) {
156
+ /* Die Grenze liegt hoeher als beim Aufzaehlungskatalog:
157
+ bei Admin 8 steckt die Karte ausgerechnet im grossen
158
+ GUI-Buendel, gemessen 8,4 MB (08.09.2026). Alles darueber
159
+ ist kein Buendel mehr, sondern ein Versehen. */
160
+ if (src.length > 12000000) { return null; }
161
+ return alleZiehen(src);
162
+ })['catch'](function () { return null; });
163
+ });
164
+ }, Promise.resolve(null));
165
+ })['catch'](function () { return null; });
166
+ }
167
+
168
+ holeText('/index.html').then(function (html) {
169
+ stempel = stempelAus(html);
170
+ return leseAblage();
171
+ }).then(function (abgelegt) {
172
+ if (abgelegt && abgelegt.stempel === stempel && abgelegt.musternamen &&
173
+ abgelegt.musternamen[sprache]) {
174
+ musterNamen = abgelegt.musternamen[sprache];
175
+ return null;
176
+ }
177
+ return suchen().then(function (karten) {
178
+ if (!karten) { return null; }
179
+ musterNamen = karten[sprache] || karten.en || {};
180
+ return ergaenzeAblage(stempel, { musternamen: karten });
181
+ });
138
182
  })['catch'](function () { /* still bleiben: rohe Namen genuegen */ })
139
- .then(function () { if (typeof nachher === 'function') { nachher(); } });
183
+ .then(fertig);
140
184
  }
141
185
 
142
186
  /* Der Beiname eines Musters - leer nur, wenn der Katalog nichts weiss.
@@ -41,6 +41,7 @@ export function ladeObjekte(danach) {
41
41
  if (--offen === 0) {
42
42
  S.keysSorted = Object.keys(S.objects).sort();
43
43
  S.kleinIndex = {};
44
+ S.geraeteIndex = {};
44
45
  kaputteAliaseNeu();
45
46
  ladeEnums();
46
47
  /* Erst die Einstellungen, dann die Vorlagen - sonst holt die
@@ -255,6 +256,7 @@ export function indexNeu() {
255
256
  S.objektStand++;
256
257
  S.keysSorted = Object.keys(S.objects).sort();
257
258
  S.kleinIndex = {};
259
+ S.geraeteIndex = {};
258
260
  kaputteAliaseNeu();
259
261
  zeigeObjektzahl();
260
262
  }