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
@@ -0,0 +1,749 @@
1
+ /* Einen Teilbaum als Attrappe ausgeben.
2
+
3
+ Zweck ist nicht das Sichern, sondern das Nachbauen: aus der Datei
4
+ entsteht anderswo ein Geraet, das sich wie das Original verhaelt — ohne
5
+ dass das Original da ist. Damit laesst sich eine Vorlage fuer ein
6
+ Geraet bauen, das man nicht besitzt. Wer im Forum schreibt „fuer mein
7
+ Geraet gibt es keine Vorlage", gibt seinen Teilbaum aus und schickt die
8
+ Datei.
9
+
10
+ Was mitgeht, steht in einer POSITIVLISTE, nicht in einer Sperrliste.
11
+ Eine Sperrliste („password", „token", …) laesst immer ein Feld durch,
12
+ an das niemand gedacht hat. `native` faellt deshalb vollstaendig weg —
13
+ dort stehen IP-Adressen, Seriennummern und bei manchen Adaptern
14
+ Zugangsdaten, und fuer den Zweck ist davon nichts noetig.
15
+
16
+ Siehe plan_export_attrappe.md. */
17
+
18
+ import { S } from './zustand.js';
19
+ import { el } from './basis.js';
20
+ import { tr } from './sprache.js';
21
+ import { holeWerte, kindZustaende } from './werte.js';
22
+ /* Zirkulaer zu baum.js/ergebnis.js - das ist in Ordnung, weil nichts
23
+ davon beim Laden gerufen wird, nur in Rueckrufen. */
24
+ import { mitNachfrage, zweigUmschalten, zweigPfade } from './entwurf.js';
25
+ import { zeichneErgebnis } from './ergebnis.js';
26
+ import { socket } from './verbindung.js';
27
+ import { INSTANZ_ID } from './vorlagen.js';
28
+
29
+ /* Die Nummer steigt, sobald sich das Format aendert — sonst liesse sich
30
+ eine alte Datei nicht von einer neuen unterscheiden.
31
+
32
+ Dass der Wert vom eigenen Block an den Eintrag wanderte (12.10.2026),
33
+ hat sie NICHT erhoeht: zu dem Zeitpunkt war keine Datei im Umlauf, es
34
+ gab das Einlesen noch nicht, und gelesen hat das Format nur der eigene
35
+ Pruefpunkt Z2. Ab jetzt waere derselbe Schritt ein Versionssprung. */
36
+ export var FORMAT = 1;
37
+
38
+ /* Nur diese Felder aus `common` gehen mit.
39
+
40
+ `states` MUSS dabei sein: ohne die Werteliste erkennt der
41
+ type-detector die Plaetze mit `statesDefined` nicht, und genau die
42
+ Erkennung will man an der Attrappe pruefen. */
43
+ var COMMON_FELDER = ['name', 'role', 'type', 'unit', 'read', 'write', 'min', 'max', 'def', 'states'];
44
+
45
+ /* Ab wann ist ein Teilbaum zu gross fuer eine Attrappe?
46
+
47
+ Gemessen am 12.10.2026 an beiden Systemen. Produktiv, auf Geraeteebene:
48
+ Median 20 Objekte, p90 136, p99 2551 — die Ausreisser sind
49
+ `yr.0.forecastHourly` (3173), `dreame.0.852247104` (2857) und
50
+ `mercedesme.0....` (2551). Am Testsystem: Median 25, p90 63.
51
+
52
+ 200 liegt deutlich ueber p90, schweigt also bei normalen Geraeten, und
53
+ weit unter den Unfaellen. Bei 100 ginge die Warnung schon bei jedem
54
+ siebten Geraet an — und man gewoehnt sich das Wegklicken an.
55
+
56
+ Es ist eine Warnung, keine Sperre: wer 3000 Objekte ausgeben will,
57
+ darf das, er soll es nur nicht aus Versehen tun. (Ricardo hatte am
58
+ 12.10.2026 `0_userdata` ausgegeben und wieder eingelesen — 393
59
+ Objekte, darunter eine Attrappe in der Attrappe.) */
60
+ export var GROSS_AB = 200;
61
+
62
+ /* So viele Zeilen zeigt die Vorschau hoechstens. Bei 3000 Zeilen waere
63
+ der Dialog unbrauchbar, und gelesen wuerde er ohnehin nicht. */
64
+ var ZEILEN_MAX = 300;
65
+
66
+ /* Alles unterhalb der Wurzel, die Wurzel eingeschlossen. */
67
+ function unterhalb(wurzel) {
68
+ var pre = wurzel + '.';
69
+ return Object.keys(S.objects).filter(function (id) {
70
+ return id === wurzel || id.indexOf(pre) === 0;
71
+ }).sort();
72
+ }
73
+
74
+ /* Die Ebenen zwischen Wurzel und einem Objekt, die selbst kein Objekt
75
+ haben. ioBroker nennt so eine Stufe `virtual`; der Baum zeigt sie als
76
+ „VIR". Sie gehoeren in die Datei, sonst beschriebe sie den Baum
77
+ unvollstaendig — aber sie tragen kein `common`, und beim Einlesen soll
78
+ erkennbar bleiben, dass dort nie ein Objekt stand. */
79
+ function virtuelleEbenen(wurzel, ids) {
80
+ var da = {};
81
+ ids.forEach(function (id) { da[id] = 1; });
82
+ var raus = {};
83
+ var tiefe = wurzel.split('.').length;
84
+ ids.forEach(function (id) {
85
+ var teile = id.split('.');
86
+ for (var i = tiefe; i < teile.length; i++) {
87
+ var pfad = teile.slice(0, i).join('.');
88
+ if (!da[pfad] && !raus[pfad]) { raus[pfad] = 1; }
89
+ }
90
+ });
91
+ return Object.keys(raus).sort();
92
+ }
93
+
94
+ function gesiebtesCommon(c) {
95
+ var raus = {};
96
+ COMMON_FELDER.forEach(function (f) {
97
+ if (c && c[f] !== undefined) { raus[f] = JSON.parse(JSON.stringify(c[f])); }
98
+ });
99
+ return raus;
100
+ }
101
+
102
+ /* Der Pfad RELATIV zur Wurzel — der Grund, warum die Attrappe an einem
103
+ anderen Ort dieselben Vorlagen trifft. Die Wurzel selbst bekommt den
104
+ leeren Pfad. */
105
+ function relativ(wurzel, id) {
106
+ return id === wurzel ? '' : id.slice(wurzel.length + 1);
107
+ }
108
+
109
+ /* Die beiden Versionen, die in die Datei gehoeren.
110
+
111
+ `erzeugtVon` sagt, welche Werkbank die Datei geschrieben hat — wichtig,
112
+ wenn jemand eine Datei schickt und etwas fehlt, das eine neuere Fassung
113
+ mitschreibt.
114
+
115
+ `quelle` ist die wichtigere: der Aufbau der Objekte kommt vom
116
+ Quelladapter, und derselbe `tele.SENSOR` sieht unter zwei Fassungen des
117
+ mqtt-Adapters verschieden aus. Aus `wurzel` liest man die Instanz ab,
118
+ ihre Version nicht.
119
+
120
+ `system.adapter.*` steht NICHT in S.objects — der Reiter laedt die
121
+ Objekte des Baums, nicht die der Instanzen. Darum ueber den Socket. */
122
+ export function holeVersionen(wurzel, fertig) {
123
+ var teile = String(wurzel).split('.');
124
+ var quellInstanz = teile.length > 1 ? teile[0] + '.' + teile[1] : '';
125
+ var raus = { erzeugtVon: null, quelle: null };
126
+ var offen = 2;
127
+ var ab = function () { if (--offen <= 0) { fertig(raus); } };
128
+
129
+ socket.emit('getObject', INSTANZ_ID, function (err, o) {
130
+ if (!err && o && o.common) {
131
+ raus.erzeugtVon = { adapter: 'alias-workbench', version: o.common.version };
132
+ }
133
+ ab();
134
+ });
135
+ if (!quellInstanz) { ab(); return; }
136
+ socket.emit('getObject', 'system.adapter.' + quellInstanz, function (err, o) {
137
+ /* Kein Treffer ist kein Fehler: `0_userdata.0` und `alias.0` haben
138
+ keine eigene Instanz in diesem Sinn. */
139
+ if (!err && o && o.common) {
140
+ raus.quelle = { adapter: quellInstanz, version: o.common.version };
141
+ }
142
+ ab();
143
+ });
144
+ }
145
+
146
+ /* Aus dem, was geladen ist, die Datei bauen. Liest nichts nach — die
147
+ Werte muessen vorher in S.werte stehen und die Versionen uebergeben
148
+ sein (siehe `ausgeben`). */
149
+ export function sammle(wurzel, versionen) {
150
+ var ids = unterhalb(wurzel);
151
+ var objekte = [];
152
+
153
+ virtuelleEbenen(wurzel, ids).forEach(function (id) {
154
+ objekte.push({ pfad: relativ(wurzel, id), virtuell: true });
155
+ });
156
+
157
+ /* Der Wert steht AM Eintrag, nicht in einem eigenen Block.
158
+
159
+ Getrennt waere es die sauberere Trennung — die Objekte beschreiben
160
+ das Geraet, die Werte sind eine Momentaufnahme. Nur liest diese
161
+ Datei ein Mensch: man bekommt sie geschickt und will bei
162
+ `ENERGY.Power` sofort sehen, was dort steht, statt vierzig Zeilen
163
+ weiter unten nachzuschlagen. Ricardo, 12.10.2026.
164
+
165
+ Ein Punkt ohne Wert hat den Schluessel einfach nicht — es gibt ihn,
166
+ er traegt nur gerade nichts. */
167
+ ids.forEach(function (id) {
168
+ var o = S.objects[id] || {};
169
+ var eintrag = {
170
+ pfad: relativ(wurzel, id),
171
+ type: o.type,
172
+ common: gesiebtesCommon(o.common),
173
+ };
174
+ var st = S.werte[id];
175
+ if (st && st.val !== undefined && st.val !== null) { eintrag.wert = st.val; }
176
+ objekte.push(eintrag);
177
+ });
178
+ objekte.sort(function (a, b) { return a.pfad.localeCompare(b.pfad); });
179
+
180
+ var v = versionen || {};
181
+ return {
182
+ format: FORMAT,
183
+ wurzel: wurzel,
184
+ erzeugt: new Date().toISOString(),
185
+ erzeugtVon: v.erzeugtVon || null,
186
+ quelle: v.quelle || null,
187
+ nativeEnthalten: false,
188
+ objekte: objekte,
189
+ };
190
+ }
191
+
192
+ /* ================== Auffaelliges in den Werten ==================
193
+
194
+ Die Werte werden gebraucht — eine JSON-Quelle wie `tele/SENSOR` ist
195
+ ohne ihren Inhalt nutzlos —, koennen aber eine Adresse oder eine
196
+ Kennung tragen. Hier wird nichts entfernt, nur hervorgehoben: was
197
+ auffaellig ist, entscheidet der Mensch, der die Datei verschickt. */
198
+ var MUSTER = [
199
+ { art: 'ip', re: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/ },
200
+ { art: 'mac', re: /\b[0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5}\b/ },
201
+ ];
202
+
203
+ /* Wann ist ein Stueck eine Kennung?
204
+
205
+ Gesucht wird in Stuecken ab acht Zeichen aus `[0-9A-Za-z_]`. Der
206
+ Bindestrich gehoert NICHT dazu: sonst waere `HM-LC-Sw1PBU-FM` ein
207
+ Treffer, und eine Geraetebezeichnung ist keine Kennung. So zerfaellt
208
+ sie in kurze Stuecke und faellt durch.
209
+
210
+ Ein solches Stueck gilt als Kennung, wenn es
211
+ - Buchstaben UND Ziffern traegt und ziffernlastig ist (ab 40 Prozent), oder
212
+ - sehr lang ist (ab 20 Zeichen) und Ziffern traegt.
213
+
214
+ Die Regel entstand in drei Anlaeufen, jeder mit einer Messung:
215
+
216
+ 1. „ab zwanzig Zeichen": verfehlte `QEQ0695220` und `000A9D899204D8`
217
+ — genau die Seriennummern, um die es geht — und markierte
218
+ `countSystemVariables`, einen Datenpunktnamen.
219
+ 2. „Buchstaben UND Ziffern": fing die Seriennummern, markierte aber
220
+ `SetOption59`. Ein Tasmota-Befehl stuende damit in jeder
221
+ Tasmota-Attrappe als Kennung (Ricardo, 12.10.2026 am Bild).
222
+ 3. Der Ziffernanteil trennt beides: `SetOption59` hat 18 Prozent,
223
+ `QEQ0695220` 70, `000A9D899204D8` 79. Namen mit angehaengter Nummer
224
+ (`Status11`, `SetOption19`, `POWER1`) bleiben darunter.
225
+
226
+ Die zweite Bedingung, die Laenge, holt lange Schluessel herein, die
227
+ nicht ziffernlastig sind — ein Base64-Zeichen hat oft nur ein Drittel
228
+ Ziffern. Ohne Ziffern faellt auch ein langes Stueck durch, sonst waere
229
+ `countSystemVariables` wieder dabei.
230
+
231
+ 4. Dass die ziffernlastige Haelfte auch Buchstaben verlangt, kam aus
232
+ derselben Messung: `3.89.11.20260919` ist eine Firmware-Version, und
233
+ `20260919` ist zu hundert Prozent Ziffern. Daten, Versionen und
234
+ Zaehler sind reine Ziffern, Kennungen tragen fast immer Buchstaben.
235
+
236
+ Aufgegeben wird damit die rein numerische Seriennummer. Die ist von
237
+ einem Zaehler aber auch durch keine Regel zu unterscheiden — dort hilft
238
+ nur, dass die Werte ueberhaupt in der Vorschau stehen. */
239
+ var KENNUNG = /[0-9A-Za-z_]{8,}/g;
240
+ function istKennung(stueck) {
241
+ var ziffern = (stueck.match(/[0-9]/g) || []).length;
242
+ if (!ziffern) { return false; }
243
+ if (stueck.length >= 20) { return true; }
244
+ return /[A-Za-z]/.test(stueck) && (ziffern / stueck.length) >= 0.4;
245
+ }
246
+
247
+ export function auffaellig(wert) {
248
+ var t = typeof wert === 'string' ? wert : JSON.stringify(wert);
249
+ if (!t) { return []; }
250
+ var raus = MUSTER.filter(function (m) { return m.re.test(t); }).map(function (m) { return m.art; });
251
+ if ((t.match(KENNUNG) || []).some(istKennung)) { raus.push('kennung'); }
252
+ return raus;
253
+ }
254
+
255
+ /* Die Zeilen der Vorschau — dieselbe Quelle fuer den Dialog und fuer den
256
+ Pruefpunkt, damit „was die Vorschau zeigt, steht auch in der Datei"
257
+ nicht zwei Rechenwege hat. */
258
+ export function vorschauZeilen(daten) {
259
+ return daten.objekte.map(function (o) {
260
+ var c = o.common || {};
261
+ return {
262
+ pfad: o.pfad,
263
+ type: o.virtuell ? '(ohne Objekt)' : o.type,
264
+ virtuell: !!o.virtuell,
265
+ name: typeof c.name === 'string' ? c.name : (c.name ? JSON.stringify(c.name) : ''),
266
+ rolle: c.role || '',
267
+ datentyp: c.type || '',
268
+ einheit: c.unit || '',
269
+ hatWert: 'wert' in o,
270
+ wert: 'wert' in o ? o.wert : undefined,
271
+ auffaellig: 'wert' in o ? auffaellig(o.wert) : [],
272
+ };
273
+ });
274
+ }
275
+
276
+ /* Kennungen stehen nicht nur in den Werten.
277
+
278
+ Gemessen am 12.10.2026 an `hm-rega.0.QEQ0695220`: die Seriennummer
279
+ steht in `wurzel` und in zwoelf `common.name`-Werten, obwohl `native`
280
+ nachweislich fehlt. Das Weglassen von `native` entfernt also keine
281
+ Kennung, die auch im Namen oder im Pfad steht — und davon muss die
282
+ Vorschau sprechen, sonst verspricht sie mehr, als sie halten kann. */
283
+ export function auffaelligImGeruest(daten) {
284
+ var raus = [];
285
+ if (auffaellig(daten.wurzel).length) { raus.push({ wo: 'wurzel', text: daten.wurzel }); }
286
+ daten.objekte.forEach(function (o) {
287
+ var c = o.common || {};
288
+ var n = typeof c.name === 'string' ? c.name : (c.name ? JSON.stringify(c.name) : '');
289
+ if (n && auffaellig(n).length) { raus.push({ wo: o.pfad || '(Wurzel)', text: n }); }
290
+ });
291
+ return raus;
292
+ }
293
+
294
+ /* Gleiche Bauweise wie `vAusgeben` im Vorlagenblatt. */
295
+ function dateiGeben(daten) {
296
+ var name = String(daten.wurzel).split('.').pop() || 'attrappe';
297
+ var text = JSON.stringify(daten, null, 2) + '\n';
298
+ var a = document.createElement('a');
299
+ a.href = URL.createObjectURL(new Blob([text], { type: 'application/json' }));
300
+ a.download = name + '.attrappe.json';
301
+ document.body.appendChild(a);
302
+ a.click();
303
+ setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 2000);
304
+ }
305
+
306
+ /* Werte holen, dann ausgeben. `holeWerte` fragt `getForeignStates` fuer
307
+ alles unterhalb ab; die Wurzel selbst kommt als Einzelabfrage dazu,
308
+ weil das Muster `<wurzel>.*` sie nicht einschliesst. */
309
+ export function ausgeben(wurzel, fertig) {
310
+ holeWerte(wurzel, [wurzel], function () {
311
+ holeVersionen(wurzel, function (versionen) {
312
+ var daten = sammle(wurzel, versionen);
313
+ dateiGeben(daten);
314
+ if (typeof fertig === 'function') { fertig(daten); }
315
+ });
316
+ });
317
+ }
318
+
319
+ /* Abgewaehlte Werte herausnehmen.
320
+
321
+ Der Wert faellt weg, das Objekt bleibt — und `werteAbgewaehlt` sagt,
322
+ dass jemand ihn entfernt hat. Ohne diese Liste saehe ein entfernter
323
+ Wert aus wie ein Punkt, der gerade nichts liefert. */
324
+ export function ohneAbgewaehlte(daten, abgewaehlt) {
325
+ var raus = JSON.parse(JSON.stringify(daten));
326
+ var weg = [];
327
+ raus.objekte.forEach(function (o) {
328
+ if (abgewaehlt.indexOf(o.pfad) > -1 && 'wert' in o) { delete o.wert; weg.push(o.pfad); }
329
+ });
330
+ if (weg.length) { raus.werteAbgewaehlt = weg.sort(); }
331
+ return raus;
332
+ }
333
+
334
+ /* ================== Die Vorschau ================== */
335
+
336
+ export function zeigeVorschau(wurzel, fertig) {
337
+ holeWerte(wurzel, [wurzel], function () {
338
+ holeVersionen(wurzel, function (versionen) {
339
+ var daten = sammle(wurzel, versionen);
340
+ baueVorschau(daten);
341
+ if (typeof fertig === 'function') { fertig(daten); }
342
+ });
343
+ });
344
+ }
345
+
346
+ function baueVorschau(daten) {
347
+ var dlg = document.getElementById('dlg-mock');
348
+ var body = document.getElementById('mock-body');
349
+ var knopf = document.getElementById('btn-mock-go');
350
+ if (!dlg || !body || !knopf) { return; }
351
+ body.textContent = '';
352
+
353
+ var zeilen = vorschauZeilen(daten);
354
+ var mitWert = zeilen.filter(function (z) { return z.hatWert; });
355
+ var abgewaehlt = [];
356
+
357
+ var kopf = el('div', 'mk-kopf');
358
+ kopf.appendChild(el('div', 'mk-wurzel', daten.wurzel));
359
+ kopf.appendChild(el('div', 'mk-zahlen', tr('mock.counts',
360
+ zeilen.length, mitWert.length,
361
+ (daten.erzeugtVon && daten.erzeugtVon.version) || '?',
362
+ (daten.quelle && (daten.quelle.adapter + ' ' + daten.quelle.version)) || '—')));
363
+ kopf.appendChild(el('div', 'mk-ohne', tr('mock.noNative')));
364
+ body.appendChild(kopf);
365
+
366
+ /* Zu gross? Warnen, nicht sperren. */
367
+ if (zeilen.length > GROSS_AB) {
368
+ var gw = el('div', 'mk-warn mk-gross');
369
+ gw.appendChild(el('div', 'mk-warn-kopf', tr('mock.tooBig', zeilen.length, GROSS_AB)));
370
+ gw.appendChild(el('div', null, tr('mock.tooBigHint')));
371
+ body.appendChild(gw);
372
+ }
373
+
374
+ /* Was auch ohne `native` mitfaehrt — Pfad und Namen. */
375
+ var geruest = auffaelligImGeruest(daten);
376
+ if (geruest.length) {
377
+ var warn = el('div', 'mk-warn');
378
+ warn.appendChild(el('div', 'mk-warn-kopf', tr('mock.inNames', geruest.length)));
379
+ var ul = el('ul', 'mk-warn-liste');
380
+ geruest.slice(0, 8).forEach(function (g) {
381
+ ul.appendChild(el('li', null, g.wo + ' — ' + g.text));
382
+ });
383
+ warn.appendChild(ul);
384
+ body.appendChild(warn);
385
+ }
386
+
387
+ var tab = el('table', 'mk-tab');
388
+ var kz = el('tr');
389
+ [tr('mock.colValue'), tr('mock.colPath'), tr('mock.colType'),
390
+ tr('mock.colRole'), tr('mock.colName')].forEach(function (t) {
391
+ kz.appendChild(el('th', null, t));
392
+ });
393
+ tab.appendChild(kz);
394
+
395
+ zeilen.slice(0, ZEILEN_MAX).forEach(function (z) {
396
+ var tz = el('tr', z.virtuell ? 'mk-virt' : '');
397
+ var tdW = el('td', 'mk-wert');
398
+ if (z.hatWert) {
399
+ var lab = el('label');
400
+ var box = el('input');
401
+ box.type = 'checkbox';
402
+ box.checked = true;
403
+ box.addEventListener('change', function () {
404
+ var i = abgewaehlt.indexOf(z.pfad);
405
+ if (box.checked) { if (i > -1) { abgewaehlt.splice(i, 1); } }
406
+ else if (i === -1) { abgewaehlt.push(z.pfad); }
407
+ tz.classList.toggle('mk-aus', !box.checked);
408
+ });
409
+ lab.appendChild(box);
410
+ var wt = el('span', 'mk-wtext', String(z.wert));
411
+ if (z.auffaellig.length) {
412
+ wt.classList.add('auff');
413
+ wt.title = tr('mock.suspicious', z.auffaellig.join(', '));
414
+ }
415
+ lab.appendChild(wt);
416
+ tdW.appendChild(lab);
417
+ }
418
+ tz.appendChild(tdW);
419
+ tz.appendChild(el('td', 'mk-pfad', z.pfad || '·'));
420
+ tz.appendChild(el('td', null, z.type + (z.datentyp ? ' · ' + z.datentyp : '') +
421
+ (z.einheit ? ' ' + z.einheit : '')));
422
+ tz.appendChild(el('td', null, z.rolle));
423
+ tz.appendChild(el('td', 'mk-name', z.name));
424
+ tab.appendChild(tz);
425
+ });
426
+ body.appendChild(tab);
427
+ if (zeilen.length > ZEILEN_MAX) {
428
+ body.appendChild(el('div', 'mk-mehr', tr('mock.more', zeilen.length - ZEILEN_MAX)));
429
+ }
430
+
431
+ /* Den Knopf neu setzen, damit kein Zuhoerer eines frueheren Aufrufs
432
+ haengen bleibt und die Datei zweimal schreibt. */
433
+ var neu = knopf.cloneNode(true);
434
+ knopf.parentNode.replaceChild(neu, knopf);
435
+ neu.addEventListener('click', function () {
436
+ dateiGeben(ohneAbgewaehlte(daten, abgewaehlt));
437
+ dlg.close();
438
+ });
439
+
440
+ if (!dlg.open) { dlg.showModal(); }
441
+ }
442
+
443
+ /* ================== Das Menue am Baum ==================
444
+
445
+ Der Baum hatte bisher nur `click`. Ein eigenes Menue statt des
446
+ Browsermenues, weil dort nichts steht, was hier hilft. */
447
+
448
+ var offenesMenue = null;
449
+
450
+ /* Welche Zeile das Menue meint.
451
+
452
+ Ein Rechtsklick waehlt nicht aus — das bleibt dem Linksklick. Damit
453
+ stand aber nirgends, von WELCHER Zeile die Attrappe entstehen wuerde,
454
+ wenn man eine andere als die gewaehlte anklickt (Ricardo, 12.10.2026).
455
+ Das ist nicht nur Kosmetik: so erzeugt man die Attrappe vom falschen
456
+ Knoten. Die Marke ist bewusst eine andere als die Auswahl `sel`, sonst
457
+ saehen zwei Zeilen gleich aus. */
458
+ var markierteZeile = null;
459
+
460
+ export function menueZu() {
461
+ if (offenesMenue) { offenesMenue.remove(); offenesMenue = null; }
462
+ if (markierteZeile) { markierteZeile.classList.remove('menueziel'); markierteZeile = null; }
463
+ }
464
+
465
+ function beiKlickDaneben(ev) {
466
+ if (offenesMenue && !offenesMenue.contains(ev.target)) { menueZu(); }
467
+ }
468
+ document.addEventListener('mousedown', beiKlickDaneben);
469
+ document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape') { menueZu(); } });
470
+ window.addEventListener('scroll', menueZu, true);
471
+ window.addEventListener('resize', menueZu);
472
+
473
+ export function zeigeMenue(ev, knotenId, zeile) {
474
+ ev.preventDefault();
475
+ menueZu();
476
+
477
+ var ziel = zeile || (ev.currentTarget && ev.currentTarget.classList
478
+ && ev.currentTarget.classList.contains('node') ? ev.currentTarget : null);
479
+ if (ziel) { ziel.classList.add('menueziel'); markierteZeile = ziel; }
480
+
481
+ var m = el('div', 'baummenue');
482
+ var z = el('div', 'bm-zeile', tr('tree.exportMock'));
483
+ z.title = tr('tree.exportMockHint', knotenId);
484
+ z.addEventListener('click', function () {
485
+ menueZu();
486
+ /* Erst zeigen, dann schreiben — wie ueberall sonst in der Werkbank.
487
+ Hier geht nichts in die Objekte, aber die Datei verlaesst das
488
+ Haus, und was darin steht, soll vorher jemand gesehen haben. */
489
+ zeigeVorschau(knotenId);
490
+ });
491
+ m.appendChild(z);
492
+
493
+ /* Einlesen haengt nicht am geklickten Knoten — eine Attrappe entsteht
494
+ immer unter `0_userdata.0.wb-attrappe`. Der Eintrag steht trotzdem
495
+ hier: alles zur Attrappe an einer Stelle, und der Dialog sagt, wo
496
+ sie landet. */
497
+ var zi = el('div', 'bm-zeile', tr('tree.importMock'));
498
+ zi.title = tr('tree.importMockHint', ATTRAPPEN_WURZEL);
499
+ zi.addEventListener('click', function () {
500
+ menueZu();
501
+ var f = document.getElementById('datei-attrappe');
502
+ if (f) { f.click(); }
503
+ });
504
+ m.appendChild(zi);
505
+
506
+ /* Mehrere Zweige als ein Geraet - dasselbe wie Strg-Klick im Baum,
507
+ nur sichtbar. Ohne diesen Eintrag faende die Funktion nur, wer
508
+ weiss, dass es sie gibt; und am Tablet gibt es gar keine Taste.
509
+
510
+ Nur an waehlbaren Zweigen und nur, wenn schon etwas gewaehlt ist -
511
+ ohne eine erste Auswahl gibt es nichts, wozu man etwas dazunimmt. */
512
+ if (S.current && knotenId !== S.current && kindZustaende(knotenId).length) {
513
+ var drin = zweigPfade().indexOf(knotenId) !== -1;
514
+ var zm = el('div', 'bm-zeile', drin ? tr('tree.branchRemove') : tr('tree.branchAdd'));
515
+ zm.title = tr('tree.branchHint');
516
+ zm.addEventListener('click', function () {
517
+ menueZu();
518
+ mitNachfrage(knotenId, function () {
519
+ var grund = zweigUmschalten(knotenId);
520
+ if (grund === 'fremd') {
521
+ S.zweigMeldung = tr('tree.branchForeign');
522
+ zeichneErgebnis();
523
+ }
524
+ });
525
+ });
526
+ m.appendChild(zm);
527
+ }
528
+
529
+ /* Erst anhaengen, dann messen: vorher hat das Menue keine Groesse und
530
+ liesse sich nicht am Fensterrand ausrichten. */
531
+ m.style.left = '0px';
532
+ m.style.top = '0px';
533
+ document.body.appendChild(m);
534
+ var r = m.getBoundingClientRect();
535
+ var x = Math.min(ev.clientX, window.innerWidth - r.width - 6);
536
+ var y = Math.min(ev.clientY, window.innerHeight - r.height - 6);
537
+ m.style.left = Math.max(6, x) + 'px';
538
+ m.style.top = Math.max(6, y) + 'px';
539
+
540
+ offenesMenue = m;
541
+ }
542
+
543
+ /* ================== Einlesen ==================
544
+
545
+ Eingelesen wird NIE an den Originalpfad. Eine Attrappe landet immer
546
+ unter `0_userdata.0.wb-attrappe.<name>`:
547
+
548
+ - Der schlimme Fall kommt baulich nicht vor. Ein fremder Export von
549
+ `hm-rpc.0.ABC123` zurueckzuschreiben, wo er herkam, traefe fremde
550
+ Geraete; ein eigener ueberschriebe das Echte. Eine Einstellung
551
+ („Einlesen erlauben") leistet das nicht: wer sie einschaltet, laesst
552
+ sie an, und sie schuetzt nicht vor dem Ueberschreiben.
553
+ - Die Vorlagen greifen trotzdem, weil sie RELATIVE Namen pruefen
554
+ (`stat.POWER`, `1.LEVEL`), nicht die Wurzel. Der Pruefstand zeigt das
555
+ seit Monaten: `wbFake` liegt unter `0_userdata.0.wbfake.mqtt.…`, und
556
+ die mitgelieferten Vorlagen passen unveraendert.
557
+
558
+ `0_userdata.0` und nicht `alias-workbench.0`: dort gehoeren vom Nutzer
559
+ angelegte Objekte hin, der Pruefstand benutzt es schon so, und
560
+ `alias-workbench.0` ist ein `meta`-Objekt fuer die Dateiablage.
561
+
562
+ Geschrieben wird erst, wenn der Dialog gezeigt hat, was entstehen
563
+ wuerde — dieselbe Zusage wie beim Trockenlauf. Der Trockenlauf des
564
+ Alias selbst passt nicht: er haengt an `S.entwurf` und verlangt, dass
565
+ jedes Objekt unter `alias.` liegt. */
566
+
567
+ export var ATTRAPPEN_WURZEL = '0_userdata.0.wb-attrappe';
568
+
569
+ /* Die Datei pruefen, bevor irgendetwas daraus gebaut wird. Gibt null
570
+ zurueck, wenn sie taugt, sonst den Grund. */
571
+ export function pruefeDatei(roh) {
572
+ if (!roh || typeof roh !== 'object') { return tr('mockin.badJson'); }
573
+ if (roh.format !== FORMAT) { return tr('mockin.badFormat', String(roh.format), String(FORMAT)); }
574
+ if (!Array.isArray(roh.objekte) || !roh.objekte.length) { return tr('mockin.badObjects'); }
575
+ var schlecht = roh.objekte.filter(function (o) {
576
+ return !o || typeof o !== 'object' || typeof o.pfad !== 'string';
577
+ });
578
+ if (schlecht.length) { return tr('mockin.badEntry', schlecht.length); }
579
+ if (!roh.objekte.some(function (o) { return o.pfad === ''; })) { return tr('mockin.noRoot'); }
580
+ return null;
581
+ }
582
+
583
+ function nameAus(wurzel) {
584
+ var letzt = String(wurzel || '').split('.').pop() || 'attrappe';
585
+ return letzt.replace(/[^\w-]/g, '_');
586
+ }
587
+
588
+ /* Was entstehen wuerde. Virtuelle Ebenen bekommen KEIN Objekt: im
589
+ Original stand dort auch keins, und ioBroker zeigt die Stufe von selbst
590
+ als `VIR` an. */
591
+ export function baueAttrappe(daten, name) {
592
+ var basis = ATTRAPPEN_WURZEL + '.' + name;
593
+ return daten.objekte.filter(function (o) { return !o.virtuell; }).map(function (o) {
594
+ var id = o.pfad ? basis + '.' + o.pfad : basis;
595
+ var obj = {
596
+ type: o.type || 'folder',
597
+ common: JSON.parse(JSON.stringify(o.common || {})),
598
+ native: { angelegtVon: 'alias-workbench', attrappeAus: daten.wurzel },
599
+ };
600
+ if (!obj.common.name) { obj.common.name = o.pfad || name; }
601
+ return { id: id, obj: obj, hatWert: 'wert' in o, wert: o.wert, schonDa: !!S.objects[id] };
602
+ });
603
+ }
604
+
605
+ export function schreibeAttrappe(liste, fertig) {
606
+ var offen = liste.length;
607
+ if (!offen) { return fertig(); }
608
+ liste.forEach(function (x) {
609
+ socket.emit('setObject', x.id, x.obj, function () {
610
+ if (!x.hatWert) { if (--offen <= 0) { fertig(); } return; }
611
+ socket.emit('setState', x.id, { val: x.wert, ack: true }, function () {
612
+ if (--offen <= 0) { fertig(); }
613
+ });
614
+ });
615
+ });
616
+ }
617
+
618
+ export function einlesenDatei(datei) {
619
+ var leser = new FileReader();
620
+ leser.onload = function () {
621
+ var roh = null;
622
+ try { roh = JSON.parse(String(leser.result)); } catch { roh = null; }
623
+ var fehler = pruefeDatei(roh);
624
+ zeigeEinlesen(roh, fehler);
625
+ };
626
+ leser.readAsText(datei);
627
+ }
628
+
629
+ function zeigeEinlesen(daten, fehler) {
630
+ var dlg = document.getElementById('dlg-mockin');
631
+ var body = document.getElementById('mockin-body');
632
+ var knopf = document.getElementById('btn-mockin-go');
633
+ if (!dlg || !body || !knopf) { return; }
634
+ body.textContent = '';
635
+
636
+ var neuerKnopf = knopf.cloneNode(true);
637
+ knopf.parentNode.replaceChild(neuerKnopf, knopf);
638
+
639
+ if (fehler) {
640
+ var f = el('div', 'mk-fehler');
641
+ f.appendChild(el('div', 'mk-warn-kopf', tr('mockin.bad')));
642
+ f.appendChild(el('div', null, fehler));
643
+ body.appendChild(f);
644
+ neuerKnopf.disabled = true;
645
+ if (!dlg.open) { dlg.showModal(); }
646
+ return;
647
+ }
648
+ neuerKnopf.disabled = false;
649
+
650
+ var name = nameAus(daten.wurzel);
651
+ var kopf = el('div', 'mk-kopf');
652
+ kopf.appendChild(el('div', 'mk-ohne', tr('mockin.from', daten.wurzel,
653
+ (daten.erzeugtVon && daten.erzeugtVon.version) || '?',
654
+ (daten.quelle && (daten.quelle.adapter + ' ' + daten.quelle.version)) || '—')));
655
+
656
+ var zeile = el('div', 'mk-namenszeile');
657
+ zeile.appendChild(el('span', 'mk-lbl', tr('mockin.name')));
658
+ var feld = el('input', 'tx');
659
+ feld.type = 'text';
660
+ feld.value = name;
661
+ zeile.appendChild(feld);
662
+ kopf.appendChild(zeile);
663
+ var zielZeile = el('div', 'mk-wurzel');
664
+ kopf.appendChild(zielZeile);
665
+ body.appendChild(kopf);
666
+
667
+ var tab = el('table', 'mk-tab');
668
+ body.appendChild(tab);
669
+
670
+ function malen() {
671
+ name = nameAus(feld.value);
672
+ var liste = baueAttrappe(daten, name);
673
+ var basis = ATTRAPPEN_WURZEL + '.' + name;
674
+ zielZeile.textContent = basis;
675
+
676
+ /* Einen vergebenen Namen gibt es nicht zweimal: dann bleibt der
677
+ Knopf gesperrt, bis jemand einen anderen eintippt.
678
+
679
+ Der Grund steht in einer Messung vom 12.10.2026. Geschrieben wird
680
+ je Objekt, also wird nur ueberschrieben, wo der Pfad gleich ist.
681
+ Dieselbe Datei erneut einzulesen waere ein sauberer Ersatz — ein
682
+ ANDERES Geraet unter demselben Namen ergibt aber eine Mischung:
683
+ aus einem Geraet mit 46 Objekten und einem Kanal mit 16 wurden 61,
684
+ weil nur die Wurzel beiden gemeinsam war und 45 Objekte des ersten
685
+ stehenblieben. Dabei entsteht ein Geraet, das es nie gab — und
686
+ genau daran wuerde dann jemand eine Vorlage bauen.
687
+
688
+ Ricardo hat am 12.10.2026 entschieden: sperren, nicht warnen. */
689
+ var altWarn = kopf.querySelector('.mk-namewarn');
690
+ if (altWarn) { altWarn.remove(); }
691
+ var vorhanden = Object.keys(S.objects).filter(function (id) {
692
+ return id === basis || id.indexOf(basis + '.') === 0;
693
+ });
694
+ if (vorhanden.length) {
695
+ var nw = el('div', 'mk-fehler mk-namewarn');
696
+ nw.appendChild(el('div', 'mk-warn-kopf', tr('mockin.nameTaken', name, vorhanden.length)));
697
+ nw.appendChild(el('div', null, tr('mockin.nameTakenHint')));
698
+ kopf.appendChild(nw);
699
+ }
700
+ neuerKnopf.disabled = vorhanden.length > 0;
701
+
702
+ kopf.querySelector('.mk-zahlen') && kopf.querySelector('.mk-zahlen').remove();
703
+ kopf.appendChild(el('div', 'mk-zahlen', tr('mockin.counts', liste.length,
704
+ liste.filter(function (x) { return x.hatWert; }).length,
705
+ liste.filter(function (x) { return x.schonDa; }).length)));
706
+ tab.textContent = '';
707
+ var kz = el('tr');
708
+ [tr('mock.colPath'), tr('mock.colType'), tr('mock.colRole'), tr('mock.colValue')]
709
+ .forEach(function (t) { kz.appendChild(el('th', null, t)); });
710
+ tab.appendChild(kz);
711
+ liste.forEach(function (x) {
712
+ var tz = el('tr');
713
+ var tdId = el('td', 'mk-pfad', x.id);
714
+ if (x.schonDa) { tdId.appendChild(el('span', 'mk-schon', ' ' + tr('mockin.exists'))); }
715
+ tz.appendChild(tdId);
716
+ tz.appendChild(el('td', null, (x.obj.type || '') +
717
+ (x.obj.common.type ? ' · ' + x.obj.common.type : '')));
718
+ tz.appendChild(el('td', null, x.obj.common.role || ''));
719
+ tz.appendChild(el('td', 'mk-wert', x.hatWert ? String(x.wert) : ''));
720
+ tab.appendChild(tz);
721
+ });
722
+ return liste;
723
+ }
724
+ var liste = malen();
725
+ feld.addEventListener('input', function () { liste = malen(); });
726
+
727
+ neuerKnopf.addEventListener('click', function () {
728
+ neuerKnopf.disabled = true;
729
+ schreibeAttrappe(liste, function () {
730
+ dlg.close();
731
+ /* Der Baum weiss noch nichts von den neuen Objekten. */
732
+ if (typeof window.__wbNachEinlesen === 'function') { window.__wbNachEinlesen(); }
733
+ });
734
+ });
735
+
736
+ if (!dlg.open) { dlg.showModal(); }
737
+ }
738
+
739
+ /* Das Dateifeld anmelden. Dasselbe Muster wie beim Vorlagen-Einlesen in
740
+ ergebnis.js: der Wert wird geleert, damit dieselbe Datei erneut geht. */
741
+ (function () {
742
+ var f = document.getElementById('datei-attrappe');
743
+ if (!f) { return; }
744
+ f.addEventListener('change', function () {
745
+ var datei = this.files && this.files[0];
746
+ this.value = '';
747
+ if (datei) { einlesenDatei(datei); }
748
+ });
749
+ })();