iobroker.alias-workbench 0.8.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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +516 -0
  3. package/admin/alias-workbench.png +0 -0
  4. package/admin/css/werkbank.css +916 -0
  5. package/admin/detector.js +1 -0
  6. package/admin/geraetewissen/tasmota-befehle.json +116 -0
  7. package/admin/i18n/de.json +552 -0
  8. package/admin/i18n/en.json +552 -0
  9. package/admin/js/aufzaehlungen.js +332 -0
  10. package/admin/js/basis.js +36 -0
  11. package/admin/js/baum.js +453 -0
  12. package/admin/js/detail.js +335 -0
  13. package/admin/js/einstellungen.js +40 -0
  14. package/admin/js/entwurf.js +908 -0
  15. package/admin/js/enums.js +23 -0
  16. package/admin/js/ergebnis.js +934 -0
  17. package/admin/js/erkennung.js +126 -0
  18. package/admin/js/katalog.js +313 -0
  19. package/admin/js/mqtt.js +885 -0
  20. package/admin/js/musternamen.js +94 -0
  21. package/admin/js/musterwahl.js +334 -0
  22. package/admin/js/objekte.js +243 -0
  23. package/admin/js/pruefungen.js +130 -0
  24. package/admin/js/quellentausch.js +570 -0
  25. package/admin/js/rollenwahl.js +199 -0
  26. package/admin/js/schreiben.js +1537 -0
  27. package/admin/js/sprache.js +137 -0
  28. package/admin/js/start.js +39 -0
  29. package/admin/js/verbindung.js +10 -0
  30. package/admin/js/vorlagen.js +572 -0
  31. package/admin/js/vorlagenblatt.js +1907 -0
  32. package/admin/js/vorschlagen.js +391 -0
  33. package/admin/js/werte.js +193 -0
  34. package/admin/js/zielleiste.js +339 -0
  35. package/admin/js/zuordnung.js +695 -0
  36. package/admin/js/zustand.js +47 -0
  37. package/admin/js/zustandsliste.js +515 -0
  38. package/admin/jsonConfig.json +249 -0
  39. package/admin/tab.html +148 -0
  40. package/admin/vorlagen/hm-dimmer.json +204 -0
  41. package/admin/vorlagen/hm-fensterkontakt-hm.json +176 -0
  42. package/admin/vorlagen/hm-fensterkontakt-hmip.json +191 -0
  43. package/admin/vorlagen/hm-heizgruppe.json +214 -0
  44. package/admin/vorlagen/hm-heizkoerper.json +236 -0
  45. package/admin/vorlagen/hm-rollladen.json +190 -0
  46. package/admin/vorlagen/hm-schaltaktor-lampe.json +197 -0
  47. package/admin/vorlagen/hm-schaltaktor-mehrfach.json +203 -0
  48. package/admin/vorlagen/hm-schaltaktor-steckdose.json +197 -0
  49. package/admin/vorlagen/hm-temperatur-feuchte.json +166 -0
  50. package/admin/vorlagen/hm-wandthermostat.json +228 -0
  51. package/admin/vorlagen/index.json +21 -0
  52. package/admin/vorlagen/messpunkt.json +225 -0
  53. package/admin/vorlagen/tasmota-lampe.json +263 -0
  54. package/admin/vorlagen/tasmota-mehrfach.json +203 -0
  55. package/admin/vorlagen/tasmota-rgb.json +239 -0
  56. package/admin/vorlagen/tasmota-steckdose.json +265 -0
  57. package/io-package.json +122 -0
  58. package/package.json +48 -0
@@ -0,0 +1,934 @@
1
+ import { S } from './zustand.js';
2
+ import './start.js';
3
+ import { socket } from './verbindung.js';
4
+ import { D, $, el, klappZeichen } from './basis.js';
5
+ import { tr, sprachtext } from './sprache.js';
6
+ import './einstellungen.js';
7
+ import './enums.js';
8
+ import { ausgangName, pruefeVorlage, vorschlag } from './vorlagen.js';
9
+ import { rateAnzahl } from './vorschlagen.js';
10
+
11
+ import {
12
+ vEinlesenDatei,
13
+ zeichneVorlagenBlatt,
14
+ tplKnopf,
15
+ zeigeVorlagenDialog,
16
+ speichereVorlageAusDialog
17
+ } from './vorlagenblatt.js';
18
+ import { baueZielleiste } from './zielleiste.js';
19
+ import { baueMusterwahl } from './musterwahl.js';
20
+ import { berechnePlaetze, baueListe } from './zustandsliste.js';
21
+ import {
22
+ alleUndVorlage,
23
+ knotenDa,
24
+ quelleVon,
25
+ springeZu,
26
+ waehle,
27
+ geraeteDarunter,
28
+ aliasFuer,
29
+ tipptGerade,
30
+ opt,
31
+ vorlagenAbweichung,
32
+ vorlageAnwenden,
33
+ bestandVorrang
34
+ } from './entwurf.js';
35
+ import { setzeEnums, setzeZiel, unterschiede } from './zuordnung.js';
36
+ import './detail.js';
37
+ import { zeigeTausch, tauscheAus } from './quellentausch.js';
38
+
39
+ import { baueChecks } from './pruefungen.js';
40
+ import {
41
+ zeigeTrockenlauf,
42
+ zeigeLoeschen,
43
+ loescheAlias,
44
+ zeigeVerlegen,
45
+ delKnopf,
46
+ schreibeObjekte
47
+ } from './schreiben.js';
48
+
49
+ import { mqttKarte, mqttSchreiben } from './mqtt.js';
50
+ import { kindZustaende, direkteZustaende } from './werte.js';
51
+ import './aufzaehlungen.js';
52
+ import { erkenneEntwurf } from './erkennung.js';
53
+ import { zusatzName } from './baum.js';
54
+ import './objekte.js';
55
+ import './katalog.js';
56
+
57
+ /* Das Aufklapp-Flag von „Warum diese Vorlage“ - lag versehentlich im
58
+ MQTT-Block und wanderte beim Schnitt mit; es gehoert hierher. */
59
+ var warumAuf = false;
60
+
61
+ /* Das Ergebnis rechts: Entwurf aufbauen, waehlen, zeichnen - Kopf,
62
+ Zustandsliste, Raum/Funktion-Zeile, Ordneruebersicht, Sprungknoepfe,
63
+ Dialog-Verkabelung.
64
+
65
+ Frueher ein IIFE - als Modul ist der eigene Gueltigkeitsbereich
66
+ ohnehin da, und nur ohne Huelle lassen sich Funktionen exportieren. */
67
+
68
+ /* Der Klappzustand ist eine Ansichtssache, keine Konfiguration - er
69
+ gehoert in den Browser, nicht in die Objektdatenbank. Sonst schriebe
70
+ jeder Klick auf ein Dreieck in die Instanz. */
71
+ /* Nicht bei jedem einzelnen Wert die ganze rechte Seite neu bauen. Ein
72
+ Solarzaehler sendet im Sekundentakt — und mit jedem Takt verschwand,
73
+ was gerade offen war: die Ordnerliste beim Hineinklicken, ein
74
+ aufgeklapptes Detail, der Fokus im Eingabefeld. Gesammelt zeichnen,
75
+ und nur, wenn niemand tippt. */
76
+ var werteTimer = null;
77
+ function werteGezeichnet() {
78
+ werteTimer = null;
79
+ if (document.querySelector('dialog[open]')) { return; }
80
+ if (tipptGerade()) { werteTimer = setTimeout(werteGezeichnet, 700); return; }
81
+ if (S.current) { zeichneErgebnis(); }
82
+ }
83
+ socket.on('stateChange', function (id, state) {
84
+ if (!state) { return; }
85
+ S.werte[id] = state;
86
+ if (!S.current || werteTimer) { return; }
87
+ werteTimer = setTimeout(werteGezeichnet, 700);
88
+ });
89
+
90
+ /* Kopfzeile: die Vorlagenzahl kommt erst, wenn sie geladen sind —
91
+ hier steht S.VORLAGEN noch auf undefined. */
92
+ $('#meta').textContent = 'type-detector ' + (D ? D.version : '?');
93
+
94
+
95
+ /* Welches Angebot zuletzt zu sehen war und seit wann.
96
+
97
+ An ein einzelnes Zeichnen laesst sich das Pochen nicht haengen: nach
98
+ der Auswahl zeichnet die Werkbank ein zweites Mal, sobald die Werte
99
+ nachkommen, und beim zweiten Mal waere das Angebot schon „bekannt“ und
100
+ bliebe stumm. Also gilt ein Zeitfenster - drei Sekunden ab dem ersten
101
+ Erscheinen. Danach steht es ruhig da, solange es dasselbe bleibt. */
102
+ var letztesAngebot = '';
103
+ var angebotSeit = 0;
104
+
105
+ /* Dasselbe Zeitfenster fuer den Vorschlagsknopf.
106
+
107
+ Eine eigene Marke und nicht `angebotFrisch`: die fuehrt genau einen
108
+ Schluessel, und zwei Dinge, die sich einen merken, loeschen sich
109
+ gegenseitig aus - der Knopf wuerde das Pochen des Uebernahme-Angebots
110
+ abwuergen und umgekehrt. */
111
+ var letzterKnopf = '';
112
+ var knopfSeit = 0;
113
+
114
+ export function knopfFrisch(schluessel) {
115
+ if (schluessel !== letzterKnopf) {
116
+ letzterKnopf = schluessel;
117
+ knopfSeit = Date.now();
118
+ return true;
119
+ }
120
+ return (Date.now() - knopfSeit) < 3200;
121
+ }
122
+
123
+ export function angebotFrisch(schluessel) {
124
+ if (schluessel !== letztesAngebot) {
125
+ letztesAngebot = schluessel;
126
+ angebotSeit = Date.now();
127
+ return true;
128
+ }
129
+ return (Date.now() - angebotSeit) < 3200;
130
+ }
131
+
132
+ export function entwurfAngefasst() {
133
+ if (S.entwurf) { S.entwurf.angefasst = true; }
134
+ }
135
+
136
+ export function neuZeichnenOderAufbauen(danach) {
137
+ if (!S.current || S.baumModus === 'vorlagen') {
138
+ if (typeof danach === 'function') { danach(); }
139
+ return;
140
+ }
141
+ if (S.entwurf && S.entwurf.angefasst) {
142
+ zeichneErgebnis();
143
+ if (typeof danach === 'function') { danach(); }
144
+ return;
145
+ }
146
+ waehle(S.current, danach);
147
+ }
148
+
149
+ (function () {
150
+ var b = $('#btn-move');
151
+ if (b) { b.addEventListener('click', zeigeVerlegen); }
152
+ var bt = $('#btn-swap');
153
+ if (bt) { bt.addEventListener('click', zeigeTausch); }
154
+ var btg = $('#btn-swap-go');
155
+ if (btg) { btg.addEventListener('click', tauscheAus); }
156
+ })();
157
+
158
+ $('#btn-einlesen').addEventListener('click', function () { $('#datei-einlesen').click(); });
159
+ $('#datei-einlesen').addEventListener('change', function () {
160
+ var f = this.files && this.files[0];
161
+ this.value = ''; /* dieselbe Datei soll erneut gehen */
162
+ if (f) { vEinlesenDatei(f); }
163
+ });
164
+
165
+ /* ================== Entwurf ==================
166
+ Alles, was du aenderst, lebt hier im Speicher. Geschrieben wird nichts.
167
+ Der echte Detektor laeuft gegen ein Objekt-Abbild dieses Entwurfs —
168
+ deshalb reagiert der Bericht sofort auf jede Aenderung. */
169
+
170
+ /* ================== Ergebnis ================== */
171
+
172
+ /* Nichts gewaehlt: Hinweis statt Entwurf, und alle Knoepfe aus. */
173
+ function zeichneLeer(host) {
174
+ /* Was man in diesem Modus tun kann, steht dort, wo man es beim
175
+ ersten Oeffnen sucht: auf der leeren rechten Seite. Die Reiter
176
+ benennen die Absicht, hier steht der Weg (Ricardo, 25.08.2026). */
177
+ var k = el('div', 'empty leerhilfe');
178
+ var modus = S.baumModus === 'aliase' ? 'aliases'
179
+ : (S.baumModus === 'vorlagen' ? 'templates' : 'sources');
180
+ k.appendChild(el('div', 'lh1', tr('help.' + modus)));
181
+ if (modus === 'aliases') { k.appendChild(el('div', 'lh2', tr('help.aliasesAdd'))); }
182
+ host.appendChild(k);
183
+ /* Nichts gewaehlt heisst: keine Knoepfe fuer ein Geraet, das es
184
+ gerade nicht gibt. Nach dem Loeschen eines Alias und nach der
185
+ Rueckkehr aus dem Vorlagen-Tab stand hier weiter „Alias
186
+ aktualisieren", „Alle 4 erzeugen", „Pruefungen 7 ✓" und unten
187
+ die Objektzahl von vorhin samt Geraetetyp. Die Knoepfe waren
188
+ wirkungslos, gelesen hat man sie trotzdem. */
189
+ var bd0 = $('#btn-dry');
190
+ bd0.disabled = true;
191
+ /* Auch die Beschriftung zuruecksetzen: „Alias aktualisieren" stand
192
+ sonst noch da, obwohl es den Alias nicht mehr gibt. */
193
+ bd0.textContent = tr('write.createAlias');
194
+ var dAlle = $('#btn-dry-alle'); if (dAlle) { dAlle.hidden = true; }
195
+ tplKnopf(false);
196
+ delKnopf(false);
197
+ var bc0 = $('#btn-checks');
198
+ if (bc0) { bc0.disabled = true; }
199
+ var chip0 = $('#checkchip');
200
+ if (chip0) { chip0.textContent = ''; chip0.className = ''; }
201
+ }
202
+
203
+ /* Sehr viele Punkte ohne Vorlage: Uebersicht oder Warnung statt
204
+ ungefragt hunderter Zeilen. */
205
+ function zeichneZuViel(host, e) {
206
+ /* Sind darunter mehrere eigenstaendige Geraete, ist eine Uebersicht
207
+ nuetzlicher als die blosse Warnung: sie zeigt je Geraet, welche
208
+ Vorlage greift und ob es schon einen Alias gibt. Die Funktion gab
209
+ es laengst, sie wurde nur nie aufgerufen. */
210
+ var drunter = geraeteDarunter(S.current);
211
+ if (drunter.length > 1) {
212
+ zeichneOrdner(host, S.current);
213
+ var bt2 = el('div');
214
+ bt2.style.margin = '11px 0 0';
215
+ var btn2 = el('button', 'btn', tr('result.allAnyway') + e.states.length);
216
+ btn2.addEventListener('click', function () { e.trotzdem = true; zeichneErgebnis(); });
217
+ bt2.appendChild(btn2);
218
+ host.appendChild(bt2);
219
+ return;
220
+ }
221
+ var zweige = {};
222
+ e.states.forEach(function (st) {
223
+ zweige[(st.n.split('_')[0] || st.n)] = 1;
224
+ });
225
+ var wr = el('div', 'aside w');
226
+ wr.appendChild(el('b', null, tr('result.pointsFromBranches',
227
+ e.states.length, Object.keys(zweige).length)));
228
+ wr.appendChild(document.createTextNode(tr('result.tooManyStates', e.states.length)));
229
+ var bt = el('div');
230
+ bt.style.marginTop = '9px';
231
+ var btn = el('button', 'btn', tr('result.allAnyway') + e.states.length);
232
+ btn.addEventListener('click', function () { e.trotzdem = true; zeichneErgebnis(); });
233
+ bt.appendChild(btn);
234
+ wr.appendChild(bt);
235
+ host.appendChild(wr);
236
+ var bd0 = $('#btn-dry');
237
+ if (bd0) { bd0.disabled = true; }
238
+ tplKnopf(false);
239
+ var ba0 = $('#btn-dry-alle');
240
+ if (ba0) { ba0.hidden = true; }
241
+ delKnopf(false);
242
+ }
243
+
244
+ /* Erkennung samt Ziel und Aufzaehlungen - und dem zweiten Lauf,
245
+ wenn der Bestand den Entwurf gerade umgebaut hat. */
246
+ function ermittleErkennung(e) {
247
+ var frei = erkenneEntwurf(e, null);
248
+ if (!e.wantAuto) {
249
+ e.wantAuto = frei.length ? frei[0].type : null;
250
+ if (!e.want) { e.want = e.wantAuto; }
251
+ }
252
+ var funde = e.want ? erkenneEntwurf(e, e.want) : frei;
253
+ var haupt = funde.length ? funde[0] : null;
254
+
255
+ /* --- Kopf --- */
256
+ setzeZiel(e);
257
+
258
+ /* Hat `uebernehmeBestand` den Entwurf auf den Bestand gezogen, lief die
259
+ Erkennung von eben noch auf den Rollen der Quelle - und `wantAuto`
260
+ stuende danach fest. Also noch einmal.
261
+
262
+ Das kann nur beim ersten Zeichnen nach der Auswahl passieren
263
+ (`bestandGeprueft` laesst es genau einmal zu), und da hat noch
264
+ niemand ein Muster von Hand gewaehlt - `want` zurueckzusetzen nimmt
265
+ also keine Entscheidung weg. */
266
+ if (e.bestandZog) {
267
+ e.bestandZog = false;
268
+ e.wantAuto = null;
269
+ e.want = null;
270
+ frei = erkenneEntwurf(e, null);
271
+ e.wantAuto = frei.length ? frei[0].type : null;
272
+ e.want = e.wantAuto;
273
+ funde = e.want ? erkenneEntwurf(e, e.want) : frei;
274
+ haupt = funde.length ? funde[0] : null;
275
+ }
276
+
277
+ setzeEnums(e);
278
+ return haupt;
279
+ }
280
+
281
+ /* Der Kopf: was links angeklickt wurde - Name, Kennung, Bestand
282
+ und der Sprungknopf zum Gegenstueck. Nicht, was daraus werden
283
+ soll: das sagt die Zielzeile darunter. */
284
+ function baueKopf() {
285
+ var obj = S.objects[S.current] || {};
286
+ var alleZust = kindZustaende(S.current);
287
+ var direkt = direkteZustaende(S.current);
288
+ var head = el('div', 'rhead');
289
+ var links = el('div');
290
+ /* Oben steht, was links angeklickt wurde — und das ist jetzt der Name,
291
+ wo es einen gibt. Die Kennung steht unverkuerzt in der Zeile
292
+ darunter; doppelt braucht sie in der Unterzeile nicht zu stehen. */
293
+ var kopfName = zusatzName(S.current);
294
+ /* Name und Sprungknopf in einer Zeile: der Knopf gehoert zu dem, was
295
+ oben steht, nicht unter den Bestandszaehler. */
296
+ var namensZeile = el('div', 'namzeile');
297
+ namensZeile.appendChild(el('div', 'id', kopfName || S.current.split('.').pop()));
298
+ links.appendChild(namensZeile);
299
+ links.appendChild(el('div', 'pfad', S.current));
300
+
301
+ var sub = el('div', 'sub');
302
+ sub.appendChild(document.createTextNode((S.objects[S.current] ? obj.type : tr('result.noObjectShort')) + ' · ' +
303
+ (direkt.length !== alleZust.length
304
+ ? tr('result.statesDirect', alleZust.length, direkt.length)
305
+ : tr('result.states', alleZust.length))));
306
+ /* Ein Weg zurueck zum Geraet. Im Aliasmodus laesst sich ein Alias
307
+ pflegen, aber nicht neu aus einer Vorlage aufbauen - Vorlagen
308
+ greifen nur an der Quelle. Wer von einem Alias aus weiterarbeiten
309
+ will, musste ihn bisher im Quellenbaum von Hand wiederfinden.
310
+ Umgekehrt gilt dasselbe: steht man am Geraet und will nachsehen,
311
+ was daraus geworden ist, fuehrt der zweite Knopf hin. */
312
+ var sprung = null, sprungZiel = null, sprungModus = null;
313
+ if (S.current.indexOf('alias.') === 0) {
314
+ sprungZiel = quelleVon(S.current);
315
+ sprungModus = 'quellen';
316
+ if (!sprungZiel) {
317
+ var vermerkt = S.objects[S.current] && S.objects[S.current].native && S.objects[S.current].native.quelle;
318
+ if (vermerkt) { sub.appendChild(el('span', 'chip warn', tr('result.sourceGone', vermerkt))); }
319
+ }
320
+ } else {
321
+ sprungZiel = aliasFuer(S.current);
322
+ /* Rueckprobe: fuehrt der Weg von diesem Alias wieder hierher?
323
+
324
+ `aliasFuer` sucht grosszuegig - es findet auch einen Alias, dessen
325
+ Punkte irgendwo *unterhalb* des Knotens lesen. Fuer die
326
+ Ordneruebersicht ist das richtig, fuer diesen Knopf zu weit: er
327
+ erschien dadurch an jeder Sparte und jedem Unterkanal, also an
328
+ `…RGB.stat` und `…RGB.tele` ebenso wie am Geraet. Der Knopf
329
+ gehoert nur dorthin, wo der Alias auch wirklich herkommt. */
330
+ if (sprungZiel && quelleVon(sprungZiel) !== S.current) { sprungZiel = null; }
331
+ sprungModus = 'aliase';
332
+ }
333
+ if (knotenDa(sprungZiel)) {
334
+ var zumAlias = (sprungModus === 'aliase');
335
+ sprung = el('button', 'btn schmal sprungknopf',
336
+ (zumAlias ? tr('result.toAlias') : tr('result.toSource')) + ' \u2192');
337
+ sprung.title = (zumAlias ? tr('result.toAliasHint') : tr('result.toSourceHint'))
338
+ + '\n' + sprungZiel;
339
+ var ziel0 = sprungZiel, modus0 = sprungModus;
340
+ sprung.addEventListener('click', function () { springeZu(ziel0, modus0); });
341
+ namensZeile.appendChild(sprung);
342
+ }
343
+ links.appendChild(sub);
344
+ head.appendChild(links);
345
+ return head;
346
+ }
347
+
348
+ /* Der Vorlagenkasten: welche Vorlage gegriffen hat, warum, ihre
349
+ Wahl und Knoepfe. Nimmt die Musterwahl bei sich auf; ohne Kasten
350
+ bleibt sie im Kopf - bis die Leiste sie sich holt. */
351
+ function baueVorlagenkasten(host, head, e, istQuelle, musterBlock) {
352
+ if (e.vorschlag || istQuelle) {
353
+ var sg = el('div', 'suggestbox');
354
+ if (!e.vorschlag) {
355
+ sg.style.borderColor = 'var(--line-strong)';
356
+ sg.style.background = 'var(--surface-2)';
357
+ }
358
+ var hd = el('div', 'hd', e.vorschlag
359
+ ? tr('tpl.identifiedAs', e.tplName)
360
+ : tr('tpl.noneDetected'));
361
+ if (e.vorlageVersion) {
362
+ var vv2 = el('span', null, ' · ' + e.vorlage + ' v' + e.vorlageVersion);
363
+ vv2.style.fontFamily = 'var(--mono)';
364
+ vv2.style.fontSize = '10.5px';
365
+ vv2.style.fontWeight = '400';
366
+ vv2.style.color = 'var(--ink-3)';
367
+ hd.appendChild(vv2);
368
+ }
369
+ if (e.vonHandGewaehlt) {
370
+ var vh = el('span', 'chip warn', tr('tpl.manuallyChosen'));
371
+ vh.style.marginLeft = '7px';
372
+ hd.appendChild(vh);
373
+ }
374
+ if (e.ausBestand) {
375
+ var ab = el('span', 'chip mut', tr('tpl.remembered'));
376
+ ab.style.marginLeft = '7px';
377
+ ab.title = tr('tpl.rememberedHint');
378
+ hd.appendChild(ab);
379
+ }
380
+ /* Woher die Vorlage kommt — mitgeliefert oder selbst gebaut. */
381
+ var vq = null;
382
+ S.VORLAGEN.forEach(function (v) { if (v.id === e.vorlage) { vq = v; } });
383
+ if (vq && vq._quelle === 'benutzer') {
384
+ var qc = el('span', 'chip ok', tr('tpl.own'));
385
+ qc.style.marginLeft = '7px';
386
+ hd.appendChild(qc);
387
+ }
388
+ sg.appendChild(hd);
389
+
390
+ /* Warum diese Vorlage, und was sie weggelassen hat — beides an einer
391
+ Stelle und zugeklappt. Vorher stand die Begruendung im Tooltip
392
+ eines kleinen i, wo sie niemand findet, und die Liste der
393
+ weggelassenen Punkte offen darunter, wo sie den halben Bildschirm
394
+ fuellte. Zugeklappt bleibt die Zahl sichtbar; wer es genau wissen
395
+ will, klappt auf. */
396
+ var gruende = [];
397
+ if (e.vorschlag) {
398
+ gruende = e.grund.slice();
399
+ } else {
400
+ S.VORLAGEN.forEach(function (v) {
401
+ var t = pruefeVorlage(v, S.current);
402
+ gruende.push(sprachtext(v.name) + ': ' + (t.passt ? tr('tpl.fits') : t.fehler.join(', ')));
403
+ });
404
+ }
405
+ var weggelassen = (e.uebersprungen || []).slice();
406
+
407
+ /* Der Aufklapper sitzt in der Kopfzeile, rechts aussen.
408
+
409
+ Zugeklappt braucht er dann keine eigene Zeile mehr - er teilt
410
+ sie sich mit „Identifiziert als …“, wo ohnehin Platz frei ist.
411
+ Das Zeichen steht rechts vom Text, weil es dort am Rand des
412
+ Kastens liegt und der Blick beim Lesen dorthin laeuft. Der Leib
413
+ klappt weiterhin unter der Kopfzeile auf; dort hat er die volle
414
+ Breite, die eine Liste braucht. */
415
+ var warumKopf = null, warumLeib = null;
416
+ if (gruende.length || weggelassen.length) {
417
+ warumKopf = el('div', 'wkopf');
418
+ /* Ohne Vorlage waere „Warum diese Vorlage" die falsche Frage —
419
+ dann steht hier, warum keine greift. */
420
+ warumKopf.appendChild(el('span', null, weggelassen.length
421
+ ? tr('tpl.skippedHead', weggelassen.length)
422
+ : tr(e.vorschlag ? 'tpl.whyThis' : 'tpl.whyNone')));
423
+ warumKopf.appendChild(klappZeichen(warumAuf));
424
+ warumKopf.tabIndex = 0;
425
+ var um2 = function () { warumAuf = !warumAuf; zeichneErgebnis(); };
426
+ warumKopf.addEventListener('click', um2);
427
+ warumKopf.addEventListener('keydown', function (ev) {
428
+ if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); um2(); }
429
+ });
430
+
431
+ if (warumAuf) {
432
+ warumLeib = el('div', 'wleib');
433
+ if (weggelassen.length) {
434
+ var ul2 = el('ul');
435
+ weggelassen.forEach(function (u) { ul2.appendChild(el('li', null, u)); });
436
+ warumLeib.appendChild(ul2);
437
+ }
438
+ if (gruende.length) {
439
+ if (weggelassen.length) { warumLeib.appendChild(el('div', 'wtrenn', tr(e.vorschlag ? 'tpl.whyThis' : 'tpl.whyNone'))); }
440
+ var ul3 = el('ul');
441
+ gruende.forEach(function (g2) { if (g2) { ul3.appendChild(el('li', null, g2)); } });
442
+ warumLeib.appendChild(ul3);
443
+ }
444
+ }
445
+ }
446
+ if (e.erzwungen) {
447
+ var ezc = el('span', 'chip warn', tr('tpl.forced'));
448
+ ezc.style.marginLeft = '7px';
449
+ ezc.title = tr('tpl.forcedHint');
450
+ hd.appendChild(ezc);
451
+ }
452
+ /* Zuletzt, damit die Marken davor stehen und das Zeichen ganz
453
+ rechts landet. */
454
+ if (warumKopf) { hd.appendChild(warumKopf); }
455
+ if (warumLeib) {
456
+ var wk = el('div', 'warum auf');
457
+ wk.appendChild(warumLeib);
458
+ sg.appendChild(wk);
459
+ }
460
+ var acts = el('div', 'acts');
461
+
462
+ /* Mehrere Ausgaenge: je Ausgang ein eigenes Geraet, weil ein socket
463
+ genau ein SET hat. Hier wird gewaehlt, welcher gerade bearbeitet wird. */
464
+ var ausgangFeld = null;
465
+ if (e.instanzen && e.instanzen.length > 1) {
466
+ var la = el('label', 'feld');
467
+ la.appendChild(el('span', 'feldlabel', tr('tpl.output')));
468
+ var selA = el('select', 'tx');
469
+ e.instanzen.forEach(function (n) {
470
+ var o = opt(n, ausgangName(e, n));
471
+ if (String(n) === String(e.instanz)) { o.selected = true; }
472
+ selA.appendChild(o);
473
+ });
474
+ selA.addEventListener('change', function () {
475
+ var v2 = vorschlag(S.current, e.vorlage, selA.value);
476
+ if (v2) {
477
+ v2.zielOrdner = e.zielOrdner;
478
+ v2.zielName = ausgangName(v2, selA.value);
479
+ v2.ziel = v2.zielOrdner + '.' + v2.zielName;
480
+ S.entwurf = v2; S.openRow = null; zeichneErgebnis();
481
+ }
482
+ });
483
+ la.appendChild(selA);
484
+ ausgangFeld = la;
485
+ }
486
+
487
+ /* Vorlagenwahl: alle geladenen, mit Vermerk ob sie passen. */
488
+ var lv = el('label', 'feld');
489
+ lv.appendChild(el('span', 'feldlabel', tr('tpl.label')));
490
+
491
+ /* Aussehen kommt aus werkbank.css (.suggestbox .acts select.tx) -
492
+ die Inline-Stile hier machten das Vorlagenfeld zum Einzelgaenger:
493
+ das Ausgangsfeld daneben blieb ungestylt in der Browserschrift. */
494
+ var selV = el('select', 'tx');
495
+
496
+ selV.appendChild(opt('', e.vorschlag ? tr('tpl.switch') : tr('tpl.choose')));
497
+ S.VORLAGEN.forEach(function (v) {
498
+ var t = pruefeVorlage(v, S.current);
499
+ /* Auch nicht passende bleiben waehlbar — mit Grund daneben. Sonst
500
+ steht man bei einem Geraet, das noch nichts gesendet hat, ohne
501
+ jede Moeglichkeit da. */
502
+ var o = opt(v.id, (v._quelle === 'benutzer' ? '★ ' : '') +
503
+ sprachtext(v.name) + ' v' + v.version + ' ' +
504
+ (t.passt ? tr('tpl.fits') : tr('tpl.fitsNot', t.fehler.join(', '))));
505
+ if (v.id === e.vorlage) { o.selected = true; }
506
+ selV.appendChild(o);
507
+ });
508
+ selV.addEventListener('change', function () {
509
+ if (!selV.value) { return; }
510
+ var v = vorschlag(S.current, selV.value);
511
+ if (v) {
512
+ v.ziel = e.ziel;
513
+ S.entwurf = v;
514
+ S.openRow = null;
515
+ zeichneErgebnis();
516
+ }
517
+ });
518
+ lv.appendChild(selV);
519
+ acts.appendChild(lv);
520
+
521
+ var bRoh = el('button', 'btn schmal',
522
+ e.roh ? tr('tpl.showProposal') : tr('tpl.showAllPoints'));
523
+ /* Der Knopf verlaesst den Vorschlag der Vorlage und zeigt den ganzen
524
+ Bestand des Geraets — nicht zu verwechseln mit dem Filter darunter,
525
+ der nur die vorgeschlagenen Zeilen ein- und ausblendet. Deshalb
526
+ steht das Geraet im Namen und der Rest im Hinweis. */
527
+ bRoh.title = e.roh ? tr('tpl.showProposalHelp') : tr('tpl.showAllPointsHelp');
528
+ if (!e.vorschlag) { bRoh.disabled = true; }
529
+ bRoh.addEventListener('click', function () {
530
+ var rohJetzt = !e.roh;
531
+ /* Dieselben zwei Schritte wie in `waehle`: die Vorlage, aus der der
532
+ Alias entstanden ist, bleibt festgenagelt, und was im Alias steht,
533
+ gilt. Ohne sie sprang die Liste beim Umschalten auf die
534
+ Vorlagenwerte zurueck - ACTUAL von `sensor.door` (so steht es im
535
+ Alias) auf `sensor.window` (so will es die Vorlage) -, der
536
+ Vermerk „Vorlage: …" verschwand, und der naechste Trockenlauf
537
+ meldete „3 aendern sich" und haette es geschrieben, ohne dass
538
+ jemand „Werte der Vorlage uebernehmen" gedrueckt hatte. Genau die
539
+ stille Aenderung, die der Bestandsvorrang abschaffen sollte
540
+ (26.08.2026). */
541
+ var fest = null;
542
+ var vorh = aliasFuer(S.current);
543
+ if (vorh && S.objects[vorh] && S.objects[vorh].native &&
544
+ S.objects[vorh].native.vorlage) {
545
+ var gid = S.objects[vorh].native.vorlage;
546
+ if (S.VORLAGEN.some(function (x) { return x.id === gid; })) { fest = gid; }
547
+ }
548
+ var neuE = rohJetzt ? alleUndVorlage(S.current, fest)
549
+ : vorschlag(S.current, fest);
550
+ if (!neuE) { return; }
551
+ neuE.roh = rohJetzt;
552
+ neuE.vorschlag = true;
553
+ neuE.tplName = e.tplName; neuE.grund = e.grund; neuE.ziel = e.ziel;
554
+ if (fest) { neuE.vonHandGewaehlt = false; neuE.ausBestand = true; }
555
+ if (vorh && S.objects[vorh]) { bestandVorrang(neuE, vorh); }
556
+ S.entwurf = neuE; S.openRow = null; zeichneErgebnis();
557
+ });
558
+ /* An der Quelle bleibt die Musterwahl im Vorlagenkasten, wo sie
559
+ hingehoert: dort steht daneben, welche Vorlage gegriffen hat, und
560
+ beides zusammen ist die Erkennung. Nur im Aliasmodus gibt es
561
+ diesen Kasten nicht - dann wandert sie weiter unten in die Leiste
562
+ ueber der Liste. */
563
+ acts.appendChild(musterBlock);
564
+ musterBlock = null;
565
+ if (ausgangFeld) { acts.appendChild(ausgangFeld); }
566
+ acts.appendChild(el('span', 'luecke'));
567
+ var knoepfe = el('div', 'knoepfe');
568
+ /* Gilt der Bestand (weil es den Alias schon gibt) und will die
569
+ Vorlage etwas anderes, steht hier der Weg dorthin - als bewusster
570
+ Klick, nicht als stille Aenderung (Ricardo, 25.08.2026). */
571
+ if (e.bestandGilt) {
572
+ var anders = e.states.filter(function (s0) {
573
+ return s0.on && vorlagenAbweichung(s0).length;
574
+ }).length;
575
+ if (anders) {
576
+ var bTpl = el('button', 'btn mini wichtig', tr('tpl.applyTemplate', anders));
577
+ bTpl.title = tr('tpl.applyTemplateHint');
578
+ bTpl.addEventListener('click', function () {
579
+ vorlageAnwenden(e);
580
+ S.openRow = null;
581
+ zeichneErgebnis();
582
+ });
583
+ knoepfe.appendChild(bTpl);
584
+ }
585
+ }
586
+ knoepfe.appendChild(bRoh);
587
+ acts.appendChild(knoepfe);
588
+ sg.appendChild(acts);
589
+ host.appendChild(sg);
590
+ }
591
+ /* Ohne Vorlagenkasten — etwa bei einem fertigen Alias — bleibt die
592
+ Musterwahl oben im Kopf stehen. */
593
+ if (musterBlock) {
594
+ head.appendChild(musterBlock);
595
+ }
596
+ return musterBlock;
597
+ }
598
+
599
+ /* Punkte, die im gewaehlten Muster keinen Platz haben, aber als
600
+ Messwerte durchgehen. */
601
+ function baueInfoKarte(host, e, infoNamen) {
602
+ if (infoNamen.length) {
603
+ var ic = el('div', 'card infod');
604
+ var ich = el('div', 'ch');
605
+ /* Hier stand „info" und „zaehlt der Detektor separat". Beides sind
606
+ unsere Woerter: „info" ist der interne Name des Sammeltyps, und
607
+ wer den Detektor nicht kennt, weiss nach dem Satz weniger als
608
+ vorher. Jetzt steht da, worum es geht - und der Zusatz sagt
609
+ gleich, ob man handeln muss. */
610
+ ich.appendChild(el('span', 'typ', tr('info.notPartOf', e.want || '')));
611
+ ich.appendChild(el('span', 'chip mut', tr('info.countedSeparately')));
612
+ ic.appendChild(ich);
613
+ var ie = el('div', 'infoerkl');
614
+ ie.style.padding = '10px 15px 12px';
615
+ ie.style.fontSize = '11.5px';
616
+ ie.style.color = 'var(--ink-3)';
617
+ ie.style.lineHeight = '1.6';
618
+ ie.appendChild(el('b', null, tr('info.sameDatapoints', infoNamen.join(', '))));
619
+ ie.appendChild(document.createTextNode(tr('info.explain', e.want || '')));
620
+ ic.appendChild(ie);
621
+ host.appendChild(ic);
622
+ }
623
+ }
624
+
625
+ /* Was sich gegenueber dem gespeicherten Stand aendern wuerde. */
626
+ function baueDiffKarte(host, e) {
627
+ var diff = unterschiede(e);
628
+ if (diff.length) {
629
+ var dc = el('div', 'card');
630
+ var dch = el('div', 'ch');
631
+ var gibtsSchon = !!S.objects[e.ziel || e.kanal];
632
+ dch.appendChild(el('span', 'typ', gibtsSchon ? tr('diff.changes') : tr('diff.deviations')));
633
+ dch.appendChild(el('span', 'chip warn', gibtsSchon ? tr('diff.open', diff.length) : tr('diff.points', diff.length)));
634
+ dch.appendChild(el('span', 'chip mut', gibtsSchon ? tr('diff.againstStored') : tr('diff.nothingExists')));
635
+ dc.appendChild(dch);
636
+ diff.forEach(function (z) {
637
+ var r = el('div', 'slot');
638
+ /* Der Name bekommt so viel Platz, wie er braucht, und der Text
639
+ steht danach immer an derselben Stelle. Vorher waren es feste
640
+ 150 px: ein langer Name wie 1_VALVE_STATE_ERROR_POSITION lief
641
+ darueber und schob sich in den Text daneben. */
642
+ r.style.gridTemplateColumns = '15px minmax(0, max-content) 1fr';
643
+ r.style.columnGap = '14px';
644
+ r.appendChild(el('span', 'mk', '•'));
645
+ r.appendChild(el('span', 'sn', z.n));
646
+ r.appendChild(el('span', 'who', z.was));
647
+ dc.appendChild(r);
648
+ });
649
+ host.appendChild(dc);
650
+ }
651
+ }
652
+
653
+ /* Pruefungen, Fusszeile und die Knopfleiste unten - beschriftet
654
+ nach dem, was der Klick tatsaechlich tut. */
655
+ function setzeKnoepfe(e, haupt, pflichtFehlt, _infoNamen) {
656
+ baueChecks(e, haupt, pflichtFehlt);
657
+
658
+ /* Der Knopf soll sagen, was er tut — nicht, wie er es tut. */
659
+ var zielId2 = e.ziel || e.kanal;
660
+ var bd = $('#btn-dry');
661
+ var ba = $('#btn-dry-alle');
662
+ var mehr = e.instanzen && e.instanzen.length > 1;
663
+ var mehrK = !mehr && (e.kanalGeraete || []).length > 1;
664
+ /* Die Knoepfe gehoeren zum Modus, nicht nur zum Objekt: Im Modus
665
+ „Alias anlegen" wird angelegt, nicht verwaltet — Verlegen, Quelle
666
+ tauschen und Entfernen haben dort nichts zu suchen. Im Modus
667
+ „Alias bearbeiten" wird bearbeitet, keine Vorlage gebaut
668
+ (Ricardo, 05.09.2026). */
669
+ var imAliasmodus = S.baumModus === 'aliase';
670
+ var bTpl = $('#btn-tpl');
671
+ if (bTpl) { bTpl.hidden = imAliasmodus; }
672
+ tplKnopf(true);
673
+ delKnopf(imAliasmodus && !!S.objects[zielId2] && zielId2.indexOf('alias.') === 0);
674
+ if (bd) {
675
+ bd.disabled = false;
676
+ /* Bei mehreren Ausgaengen den Ausgang beim Namen nennen — „für den
677
+ ausgewählten Ausgang" waere lang und sagt weniger. */
678
+ bd.textContent = mehr
679
+ ? (S.objects[zielId2] ? tr('write.updateNamed', e.zielName) : tr('write.createNamed', e.zielName))
680
+ : mehrK
681
+ ? (S.objects[zielId2] ? tr('write.updateAsOne') : tr('write.createAsOne'))
682
+ : (S.objects[zielId2] ? tr('write.updateAlias') : tr('write.createAlias'));
683
+ bd.title = zielId2;
684
+ }
685
+ if (ba) {
686
+ ba.hidden = !mehr && !mehrK;
687
+ if (mehr) {
688
+ ba.textContent = tr('write.createAll', e.instanzen.length);
689
+ ba.title = e.instanzen.map(function (n) {
690
+ return (e.zielOrdner || '') + '.' + ausgangName(e, n);
691
+ }).join(String.fromCharCode(10));
692
+ } else if (mehrK) {
693
+ ba.textContent = tr('write.createPerChannel', e.kanalGeraete.length);
694
+ /* Im Hinweis stehen die Namen, die entstehen wuerden — sonst
695
+ klickt man auf eine Zahl und weiss nicht, was danach dasteht. */
696
+ ba.title = e.kanalGeraete.map(function (k) {
697
+ return (e.zielOrdner || '') + '.' + k.name + ' (' + k.typ + ')';
698
+ }).join(String.fromCharCode(10));
699
+ }
700
+ ba.disabled = false;
701
+ }
702
+
703
+ /* Offene Vermutungen sperren Erzeugen und Vorlage-Speichern:
704
+ „bitte pruefen" soll eine echte Entscheidung sein, kein stilles
705
+ Ja beim Anlegen (Ricardo, 25.08.2026). Der Grund steht daneben,
706
+ sonst raetselt man, warum der Knopf grau ist. */
707
+ var offen = rateAnzahl(e);
708
+ var sg = $('#sperrgrund');
709
+ if (offen) {
710
+ var grund = tr('guess.locked', offen);
711
+ if (bd) { bd.disabled = true; bd.title = grund; }
712
+ if (ba && !ba.hidden) { ba.disabled = true; ba.title = grund; }
713
+ var bt = $('#btn-tpl');
714
+ if (bt) { bt.disabled = true; bt.title = grund; }
715
+ if (sg) { sg.hidden = false; sg.textContent = grund; }
716
+ } else if (sg) {
717
+ sg.hidden = true;
718
+ sg.textContent = '';
719
+ }
720
+ }
721
+
722
+ export function zeichneErgebnis() {
723
+ /* Wandert weiter unten in die Leiste ueber der Zustandsliste. */
724
+ var rateKnopf = null;
725
+ var host = $('#res');
726
+ host.textContent = '';
727
+ /* Der Sperrgrund gehoert zum Entwurf — beim Wechsel auf Leer- oder
728
+ Vorlagenansicht wuerde er sonst stehen bleiben. */
729
+ var sg0 = $('#sperrgrund');
730
+ if (sg0) { sg0.hidden = true; sg0.textContent = ''; }
731
+ if (S.baumModus === 'vorlagen') { return zeichneVorlagenBlatt(host); }
732
+ var bck = $('#btn-checks');
733
+ if (bck) { bck.disabled = false; }
734
+ if (!S.current || !S.entwurf) { return zeichneLeer(host); }
735
+
736
+ var e = S.entwurf;
737
+
738
+ /* Ist das ueberhaupt ein Geraet? Ein Ordner mit vielen Geraeten darunter
739
+ ergibt keinen Entwurf, sondern eine Uebersicht. Vorher landeten beim
740
+ Klick auf mqtt-client.0 alle Messpunkte in einem einzigen Entwurf. */
741
+ /* Sehr viele Punkte heisst fast immer: falscher Knoten gewaehlt.
742
+ Nicht sperren, aber auch nicht ungefragt 1000 Zeilen zeichnen. */
743
+ var VIEL = 40;
744
+ /* Ein `device` ist die Ebene, auf der das Geraet gemeint ist — auch
745
+ wenn viele Punkte darunter haengen. Ohne diese Ausnahme landete
746
+ jedes Homematic-Geraet in der Uebersicht „2 Geraete darunter" und
747
+ liess sich nur kanalweise bearbeiten. */
748
+ var istGeraetObjekt = !!(S.objects[S.current] && S.objects[S.current].type === 'device');
749
+ if (!e.vorschlag && !istGeraetObjekt && e.states.length > VIEL && !e.trotzdem) {
750
+ return zeichneZuViel(host, e);
751
+ }
752
+
753
+ /* --- erkannt (frei) und erzwungen --- */
754
+ var haupt = ermittleErkennung(e);
755
+
756
+ var head = baueKopf();
757
+
758
+ /* --- Musterwahl --- */
759
+ var pflichtFehlt = haupt ? haupt.states.filter(function (s) { return s.required && !s.id; }) : [];
760
+ var mw = baueMusterwahl(e, haupt, pflichtFehlt);
761
+ rateKnopf = mw.rateKnopf;
762
+ var musterBlock = mw.block; /* wandert unten in den Vorlagenkasten */
763
+ host.appendChild(head);
764
+
765
+ var istQuelle = baueZielleiste(host, e);
766
+
767
+ musterBlock = baueVorlagenkasten(host, head, e, istQuelle, musterBlock);
768
+
769
+ /* Gleich unter die Erkennung: was hier fehlt, entscheidet mit
770
+ darueber, ob eine Vorlage ueberhaupt greift. */
771
+ mqttKarte(host, S.current);
772
+
773
+ if (!S.objects[S.current]) {
774
+ /* Hier stand eine Erklaerung, dass MQTT fuer Zwischenebenen keine
775
+ Objekte anlegt. Sie ist richtig, half aber niemandem: kein
776
+ Risiko, keine Handlung, nichts anders zu machen. Und sie
777
+ widersprach dem Erkennungskasten drei Zeilen darueber, der
778
+ gleichzeitig „Identifiziert als: Tasmota-Steckdose" meldete.
779
+ Wer nichts damit anfangen kann, soll es auch nicht lesen
780
+ muessen — das Wissen steht im README, nicht in der Oberflaeche. */
781
+ }
782
+
783
+ var pl = berechnePlaetze(e, haupt);
784
+ var imInfo = pl.imInfo;
785
+
786
+ baueListe(host, e, pl, rateKnopf, musterBlock);
787
+ rateKnopf = null;
788
+ musterBlock = null;
789
+
790
+ /* --- info-Karte --- */
791
+ var infoNamen = Object.keys(imInfo);
792
+ baueInfoKarte(host, e, infoNamen);
793
+
794
+ /* --- Änderungen gegenüber dem, was gespeichert ist --- */
795
+ baueDiffKarte(host, e);
796
+
797
+ /* --- Prüfungen, Fusszeile, Knopfleiste --- */
798
+ setzeKnoepfe(e, haupt, pflichtFehlt, infoNamen);
799
+ }
800
+
801
+ /* Uebersicht ueber alle Geraete unterhalb eines Ordners. */
802
+ function zeichneOrdner(host, wurzel) {
803
+ var geraete = geraeteDarunter(wurzel);
804
+
805
+ var head = el('div', 'rhead');
806
+ var links = el('div');
807
+ var ordnerName = zusatzName(wurzel);
808
+ links.appendChild(el('div', 'id', ordnerName || wurzel.split('.').pop()));
809
+ links.appendChild(el('div', 'pfad', wurzel));
810
+ links.appendChild(el('div', 'sub',
811
+ tr('folder.devicesBelow', geraete.length, kindZustaende(wurzel).length)));
812
+ head.appendChild(links);
813
+ host.appendChild(head);
814
+
815
+ if (!geraete.length) {
816
+ host.appendChild(el('div', 'empty', tr('folder.nothingHere')));
817
+ $('#btn-dry').disabled = true;
818
+ tplKnopf(false);
819
+ return;
820
+ }
821
+
822
+ var mitAlias = 0, ohneVorlage = 0;
823
+ var card = el('div', 'card');
824
+ var ch = el('div', 'ch');
825
+ ch.appendChild(el('span', 'typ', tr('folder.devices')));
826
+ var zaehler = el('span', 'chip mut', tr('folder.found', geraete.length));
827
+ ch.appendChild(zaehler);
828
+ card.appendChild(ch);
829
+
830
+ var cap = el('div', 'lhead');
831
+ cap.style.gridTemplateColumns = '1fr 190px 130px 12px';
832
+ [tr('folder.colSource'), tr('folder.colDetected'), tr('folder.colAlias'), ''].forEach(function (t) { cap.appendChild(el('span', null, t)); });
833
+ card.appendChild(cap);
834
+
835
+ geraete.forEach(function (g) {
836
+ var vorhanden = aliasFuer(g.id);
837
+ if (vorhanden) { mitAlias++; }
838
+ if (!g.vorlage) { ohneVorlage++; }
839
+
840
+ var row = el('div', 'erow');
841
+ row.style.gridTemplateColumns = '1fr 190px 130px 12px';
842
+ row.tabIndex = 0;
843
+
844
+ /* Ohne den Namen heissen die Kanaele eines Homematic-Geraets hier
845
+ schlicht „0" und „1". */
846
+ var gName = zusatzName(g.id);
847
+ var nm = el('span', 'nm3', g.id.slice(wurzel.length + 1));
848
+ if (gName) {
849
+ nm.appendChild(el('span', 'nam', gName));
850
+ nm.title = g.id + ' · ' + gName;
851
+ }
852
+ row.appendChild(nm);
853
+
854
+ var vl = el('span', 'ro2');
855
+ if (g.vorlage) { vl.textContent = sprachtext(g.vorlage.name); }
856
+ else { vl.textContent = tr('tpl.none'); vl.style.color = 'var(--warn)'; }
857
+ row.appendChild(vl);
858
+
859
+ var al = el('span');
860
+ al.style.textAlign = 'right';
861
+ al.appendChild(el('span', 'chip ' + (vorhanden ? 'ok' : 'mut'), vorhanden ? tr('folder.aliasThere') : tr('folder.aliasMissing')));
862
+ row.appendChild(al);
863
+ row.appendChild(el('span', 'ca2', '›'));
864
+
865
+ var geh = function () { waehle(g.id); };
866
+ row.addEventListener('click', geh);
867
+ row.addEventListener('keydown', function (ev) {
868
+ if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); geh(); }
869
+ });
870
+ card.appendChild(row);
871
+ });
872
+
873
+ ch.appendChild(el('span', 'chip ' + (mitAlias === geraete.length ? 'ok' : 'warn'),
874
+ tr('folder.withAlias', mitAlias)));
875
+ if (ohneVorlage) { ch.appendChild(el('span', 'chip warn', tr('folder.withoutTemplate', ohneVorlage))); }
876
+ host.appendChild(card);
877
+
878
+ var hin = el('div', 'aside');
879
+ hin.appendChild(el('b', null, tr('folder.clickHint')));
880
+ hin.appendChild(document.createTextNode(tr('folder.clickHintText')));
881
+ host.appendChild(hin);
882
+
883
+ var bd = $('#btn-dry');
884
+ if (bd) { bd.disabled = true; bd.textContent = tr('write.createAlias'); bd.title = tr('write.pickDeviceFirst'); }
885
+ tplKnopf(false);
886
+ var chip = $('#checkchip');
887
+ if (chip) { chip.className = 'chip mut'; chip.textContent = ''; }
888
+ }
889
+
890
+ /* Gibt es den Alias schon, sollen seine Punkte angehakt sein.
891
+
892
+ Vorher kamen die Haken allein aus der Vorlage. Wer ein fertiges
893
+ Geraet aufmachte, sah den Vorschlag fuer ein neues — und ein Punkt,
894
+ den er sich damals von Hand dazugeholt hatte, stand ungehakt da.
895
+ Ein Klick auf „Alias aktualisieren" haette ihn entfernt. Der
896
+ Trockenlauf haette es gezeigt, aber darauf darf man sich nicht
897
+ verlassen.
898
+
899
+ Vereinigung statt Ersetzung: angehakt wird, was im Alias steht
900
+ ODER was die Vorlage vorschlaegt. In die gefaehrliche Richtung —
901
+ etwas verlieren — kann damit nichts mehr passieren, und ein
902
+ Vorschlag aus einer neueren Vorlage geht trotzdem nicht verloren.
903
+ Was dadurch dazukommt, meldet die Aenderungs-Karte. */
904
+ /* Wohin der Alias gehoert. Muss feststehen, bevor die Kopfzeile
905
+ gezeichnet wird — sonst meldet die „wird neu angelegt", waehrend die
906
+ Zielzeile darunter schon „gibt es schon" sagt. */
907
+ /* ================== Dialoge ================== */
908
+ /* Die <dialog>-Blöcke stehen im HTML hinter diesem Skript. Zur Laufzeit
909
+ des Skripts gibt es sie noch nicht — erst nach DOMContentLoaded
910
+ verkabeln, sonst wirft der erste Zugriff und alles danach fällt aus. */
911
+ function verkabeleDialoge() {
912
+ var bc = $('#btn-checks'), bd = $('#btn-dry'), bw = $('#btn-dry-write');
913
+ if (bc) { bc.addEventListener('click', function () { $('#dlg-checks').showModal(); }); }
914
+ if (bd) { bd.addEventListener('click', function () { zeigeTrockenlauf(false); }); }
915
+ var ba2 = $('#btn-dry-alle');
916
+ if (ba2) { ba2.addEventListener('click', function () { zeigeTrockenlauf(true); }); }
917
+ if (bw) { bw.addEventListener('click', schreibeObjekte); }
918
+ var bdel = $('#btn-del'), bdg = $('#btn-del-go');
919
+ if (bdel) { bdel.addEventListener('click', zeigeLoeschen); }
920
+ if (bdg) { bdg.addEventListener('click', loescheAlias); }
921
+ var bmq = $('#btn-mqtt-write');
922
+ if (bmq) { bmq.addEventListener('click', mqttSchreiben); }
923
+ var bt = $('#btn-tpl'), bts = $('#btn-tpl-save');
924
+ if (bt) { bt.addEventListener('click', zeigeVorlagenDialog); }
925
+ if (bts) { bts.addEventListener('click', speichereVorlageAusDialog); }
926
+ document.querySelectorAll('dialog [data-close]').forEach(function (b) {
927
+ b.addEventListener('click', function () { b.closest('dialog').close(); });
928
+ });
929
+ }
930
+ if (document.readyState === 'loading') {
931
+ document.addEventListener('DOMContentLoaded', verkabeleDialoge);
932
+ } else {
933
+ verkabeleDialoge();
934
+ }