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.
- package/README.md +58 -98
- package/admin/css/werkbank.css +88 -0
- package/admin/i18n/de.json +766 -1
- package/admin/i18n/en.json +766 -1
- package/admin/i18n/es.json +766 -1
- package/admin/i18n/fr.json +766 -1
- package/admin/i18n/it.json +766 -1
- package/admin/i18n/nl.json +766 -1
- package/admin/i18n/pl.json +766 -1
- package/admin/i18n/pt.json +766 -1
- package/admin/i18n/ru.json +766 -1
- package/admin/i18n/uk.json +766 -1
- package/admin/i18n/zh-cn.json +766 -1
- package/admin/js/ablage.js +118 -0
- package/admin/js/attrappe.js +749 -0
- package/admin/js/baum.js +52 -5
- package/admin/js/detail.js +64 -3
- package/admin/js/entwurf.js +246 -31
- package/admin/js/ergebnis.js +77 -2
- package/admin/js/katalog.js +235 -40
- package/admin/js/mqtt.js +297 -27
- package/admin/js/musternamen.js +70 -26
- package/admin/js/objekte.js +2 -0
- package/admin/js/quellentausch.js +823 -16
- package/admin/js/schreiben.js +3 -2
- package/admin/js/sprache.js +11 -8
- package/admin/js/vorlagen.js +8 -3
- package/admin/js/vorlagenblatt.js +228 -64
- package/admin/js/werte.js +294 -5
- package/admin/js/zielleiste.js +19 -10
- package/admin/js/zuordnung.js +8 -7
- package/admin/js/zustand.js +32 -0
- package/admin/js/zustandsliste.js +9 -2
- package/admin/tab.html +24 -0
- package/admin/vorlagen/hm-dimmer.json +32 -8
- package/admin/vorlagen/hm-fensterkontakt-hm.json +20 -5
- package/admin/vorlagen/hm-fensterkontakt-hmip.json +28 -7
- package/admin/vorlagen/hm-heizgruppe.json +38 -11
- package/admin/vorlagen/hm-heizkoerper.json +42 -12
- package/admin/vorlagen/hm-rollladen.json +28 -7
- package/admin/vorlagen/hm-schaltaktor-lampe.json +24 -6
- package/admin/vorlagen/hm-schaltaktor-mehrfach.json +32 -8
- package/admin/vorlagen/hm-schaltaktor-steckdose.json +24 -6
- package/admin/vorlagen/hm-temperatur-feuchte.json +8 -2
- package/admin/vorlagen/hm-wandthermostat.json +36 -9
- package/admin/vorlagen/messpunkt.json +40 -10
- package/admin/vorlagen/tasmota-lampe.json +48 -12
- package/admin/vorlagen/tasmota-mehrfach.json +40 -10
- package/admin/vorlagen/tasmota-rgb.json +36 -9
- package/admin/vorlagen/tasmota-steckdose.json +56 -14
- package/io-package.json +41 -28
- package/package.json +3 -3
- package/admin/sprachen/de.json +0 -685
- 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,
|
|
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
|
|
89
|
-
if (!
|
|
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
|
-
|
|
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 =
|
|
110
|
+
var rel = pt.rel.split('.');
|
|
96
111
|
if (rel.length < 2) { return; }
|
|
97
|
-
var praefix = rel[0];
|
|
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?
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
196
|
-
|
|
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
|
-
|
|
298
|
-
var
|
|
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
|
|
461
|
-
if (
|
|
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
|
-
|
|
475
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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; }
|
package/admin/js/musternamen.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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(
|
|
183
|
+
.then(fertig);
|
|
140
184
|
}
|
|
141
185
|
|
|
142
186
|
/* Der Beiname eines Musters - leer nur, wenn der Katalog nichts weiss.
|
package/admin/js/objekte.js
CHANGED
|
@@ -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
|
}
|