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,908 @@
1
+ /* Der Entwurf: aus einem Kanal wird das, was rechts steht - samt
2
+ Auswahl (waehle), Navigation (springeZu) und der Frage, welcher Alias
3
+ zu welcher Quelle gehoert. */
4
+
5
+ import { S } from './zustand.js';
6
+ import { socket } from './verbindung.js';
7
+ import { $ } from './basis.js';
8
+ import { txt } from './sprache.js';
9
+ import './enums.js';
10
+ import { zusatzName, setzeModus, zeichneBaum, merkeKlappstand } from './baum.js';
11
+ import { kindZustaende, aliasQuellen, holeWerte } from './werte.js';
12
+ import { erkenneEntwurf } from './erkennung.js';
13
+ import { vorschlag , pruefeVorlage } from './vorlagen.js';
14
+
15
+ import { zeichneErgebnis } from './ergebnis.js';
16
+
17
+ /* Der Name eines Kanals, wenn ihm jemand einen gegeben hat.
18
+
19
+ `zusatzName` ist hier zu grosszuegig: hm-rpc benennt jeden Kanal
20
+ schematisch `<Geraet>:<Nummer>` — `Licht_Bar_Esstisch:0`. Das weicht
21
+ zwar von der Kennung `0` ab, sagt aber nichts, was nicht schon
22
+ danebensteht. Ein eigener Name ist erst `Licht_Bar`. */
23
+ export function eigenerKanalName(id) {
24
+ var n = zusatzName(id);
25
+ if (!n) { return ''; }
26
+ var nr = String(id).split('.').pop();
27
+ return (n.slice(-(nr.length + 1)) === ':' + nr) ? '' : n;
28
+ }
29
+
30
+ /* Woran man im fertigen Alias erkennt, welcher Kanal gemeint ist.
31
+
32
+ Die Kennung bleibt `1_STATE` — sie umzubenennen hiesse, bei jedem
33
+ bestehenden Alias den Punkt zu loeschen und neu anzulegen, und jedes
34
+ Widget und Skript, das darauf zeigt, liefe ins Leere. Die Beschriftung
35
+ dagegen ist frei: sie landet in `common.name`, steht im Objektbrowser
36
+ in der Spalte daneben und bricht nichts.
37
+
38
+ 1_STATE Licht_Bar STATE
39
+ 2_STATE Licht_Esstisch STATE
40
+
41
+ Bei einem Geraet mit nur einem benannten Kanal — dem Normalfall —
42
+ entsteht keine Beschriftung; dort gibt es nichts zu unterscheiden. */
43
+ export function kanalBeschriftung(kanal, kurzName) {
44
+ var t = String(kurzName).split('.');
45
+ if (t.length < 2) { return ''; }
46
+ var kn = eigenerKanalName(kanal + '.' + t[0]);
47
+ if (!kn) { return ''; }
48
+ return kn + ' ' + t.slice(1).join(' ');
49
+ }
50
+
51
+ /* Haken-Schnappschuss fuer den Chip „Standard“: so standen sie, als
52
+ der Entwurf entstand. Zeilen, die erst spaeter dazukommen (Alle-
53
+ Ansicht, von Hand), haben keine Vorgabe und fallen auf „ab“. */
54
+ export function merkeHakenVorgabe(e) {
55
+ if (e && e.states) { e.states.forEach(function (s) { s.onVorgabe = s.on; }); }
56
+ return e;
57
+ }
58
+
59
+ export function baueEntwurf(kanal) {
60
+ var istAlias = (kanal.indexOf('alias.') === 0);
61
+ var e = { kanal: kanal, states: [], want: null, wantAuto: null, alleMuster: false };
62
+ kindZustaende(kanal).forEach(function (id) {
63
+ var o = S.objects[id], c = o.common || {}, a = c.alias || {}, q = aliasQuellen(o);
64
+ var kurzName = id.slice(kanal.length + 1);
65
+ e.states.push({
66
+ /* Bei einer Quelle wird aus stat.POWER der Zustand stat_POWER —
67
+ ein Punkt im Namen wuerde sonst eine Unterebene im Alias
68
+ aufmachen statt eines Datenpunkts im Geraet. */
69
+ n: istAlias ? kurzName : kurzName.replace(/\./g, '_'),
70
+ urId: istAlias ? id : undefined,
71
+ on: true,
72
+ /* Die Rolle der Quelle uebernehmen, wenn es eine gibt.
73
+
74
+ Frueher blieb sie im Quellenmodus leer — gedacht fuer Tasmota,
75
+ wo `cmnd.POWER` schlicht „text" heisst und die richtige Rolle
76
+ erst aus der Vorlage kommt. Adapter mit gepflegten Rollen
77
+ bestraft das: hm-rpc liefert `level.temperature` fuer
78
+ SET_POINT_TEMPERATURE und `value.temperature` fuer
79
+ ACTUAL_TEMPERATURE, die Werkbank warf beides weg und meldete
80
+ dann „keine Rolle" und „nichts erkannt". Typ und Einheit hat sie
81
+ von jeher uebernommen; die Rolle auszunehmen war nicht zu
82
+ begruenden. Ueberschreiben laesst sie sich weiterhin. */
83
+ role: c.role || '',
84
+ typ: c.type || '',
85
+ unit: c.unit || '',
86
+ /* Ohne die Werteliste bleibt jeder Platz mit statesDefined leer —
87
+ EFFECT etwa. Die Werkbank meldete dann eine Luecke an einem
88
+ Alias, den ihr eigener Detektor tadellos findet. */
89
+ states: c.states || undefined,
90
+ wr: !!c.write,
91
+ /* Ohne common.alias ist der Punkt seine eigene Quelle. Vorher stand
92
+ hier nichts und man musste jede Zeile von Hand zuweisen. */
93
+ srcR: istAlias ? (q.read || '') : id,
94
+ /* Steht in `common.alias.id` ein einzelner Text, gilt er fuer Lesen
95
+ UND Schreiben - dann ist die Schreibquelle dieselbe wie die
96
+ Lesequelle. Hier stand frueher eine leere Zeichenkette, gedacht
97
+ als „muss man sich nicht extra merken". Der Schreibpfad leitet
98
+ daraus aber `write: false` ab und laesst die Schreibformel weg:
99
+ ein Aktualisieren machte aus einem schaltenden Punkt einen
100
+ lesenden ohne Umrechnung (Ricardo, 25.08.2026, an
101
+ alias.0.Badezimmer.Rollladen.CLOSE). Belegt ist das Schreiben
102
+ durch `common.write` oder durch eine vorhandene Schreibformel -
103
+ wer eine hinterlegt hat, wollte schreiben. */
104
+ srcW: istAlias
105
+ ? (q.einfach
106
+ ? ((c.write === true || typeof a.write === 'string') ? (q.write || '') : '')
107
+ : (q.write || ''))
108
+ : (c.write ? id : ''),
109
+ f: (typeof a.read === 'string') ? a.read : '',
110
+ fw: (typeof a.write === 'string') ? a.write : '',
111
+ caption: istAlias ? txt(c.name) : kanalBeschriftung(kanal, kurzName),
112
+ manuell: false
113
+ });
114
+ });
115
+ return merkeHakenVorgabe(e);
116
+ }
117
+
118
+
119
+ /* Alle Datenpunkte zeigen, ohne die Vorlage zu verlieren.
120
+
121
+ Der Knopf tauschte den Vorlagen-Vorschlag frueher gegen einen
122
+ Rohentwurf aus: alle Punkte da, aber Vorlage, Rollen und Haken weg.
123
+ Wer nur nachsehen wollte, was das Geraet sonst noch hat, stand
124
+ danach ohne Erkennung da und musste zurueckschalten.
125
+
126
+ Jetzt liegt beides uebereinander. Grundlage bleibt der Vorschlag mit
127
+ seinen Rollen, Namen und Haken; die Punkte, die er nicht kennt,
128
+ kommen ungehakt dazu. Abgeglichen wird ueber die Quelle, nicht ueber
129
+ den Namen: die Vorlage nennt einen Punkt SET, das Geraet nennt ihn
130
+ 1.STATE - derselbe Punkt, zwei Namen. */
131
+ /* `vorlagenId` nagelt die Vorlage fest, aus der ein vorhandener Alias
132
+ entstanden ist - dieselbe Regel wie in `waehle`. Ohne sie durfte eine
133
+ spaeter angelegte Vorlage beim Umschalten auf die Alle-Ansicht das
134
+ fertige Geraet still uebernehmen (26.08.2026). */
135
+ export function alleUndVorlage(kanal, vorlagenId) {
136
+ var v = vorschlag(kanal, vorlagenId);
137
+ var roh = baueEntwurf(kanal);
138
+ if (!v) { return roh; }
139
+
140
+ var bekannt = {};
141
+ (v.states || []).forEach(function (st) {
142
+ if (st.srcR) { bekannt[st.srcR] = true; }
143
+ if (st.srcW) { bekannt[st.srcW] = true; }
144
+ });
145
+
146
+ /* Das Rollenwissen der Vorlage: Punkte, die keine eigene Zeile
147
+ tragen, aber eine gepflegte Rolle haben. Ohne das stand an den
148
+ meisten Rohzeilen „keine Rolle" - hm-rpc laesst sie am Geraet
149
+ schlicht leer (Ricardos Heizgruppen-Bild, 25.08.2026). Nur die
150
+ Rohzeilen der Alle-Ansicht werden angereichert; an den sichtbaren
151
+ Vorlagenzeilen und an der Erkennung aendert sich nichts. */
152
+ var vq = null;
153
+ S.VORLAGEN.forEach(function (x) { if (x.id === v.vorlage) { vq = x; } });
154
+ var wissen = (vq && vq.weiterePunkte) || null;
155
+ var wissenRolle = function (rel) {
156
+ if (!wissen) { return ''; }
157
+ if (wissen[rel]) { return wissen[rel]; }
158
+ /* `*.NAME` gilt fuer jeden Kanal: die Wartungspunkte von Homematic
159
+ heissen auf Kanal 0 wie auf Kanal 7 gleich, und welche Kanaele
160
+ ein Geraet hat, weiss die Vorlage nicht. */
161
+ var teile = rel.split('.');
162
+ if (teile.length > 1) {
163
+ var stern = '*.' + teile.slice(1).join('.');
164
+ if (wissen[stern]) { return wissen[stern]; }
165
+ }
166
+ /* Mehrfach-Vorlagen fuehren %N% im Pfad. */
167
+ var inst = v.instanz;
168
+ if (inst !== undefined && inst !== null) {
169
+ var raus = '';
170
+ Object.keys(wissen).forEach(function (k) {
171
+ if (raus || k.indexOf('%N%') === -1) { return; }
172
+ if (k.split('%N%').join(String(inst)) === rel) { raus = wissen[k]; }
173
+ });
174
+ if (raus) { return raus; }
175
+ }
176
+ return '';
177
+ };
178
+
179
+ (roh.states || []).forEach(function (st) {
180
+ if (st.srcR && bekannt[st.srcR]) { return; }
181
+ var kopie = JSON.parse(JSON.stringify(st));
182
+ kopie.on = false;
183
+ if (kopie.srcR) {
184
+ var wr = wissenRolle(kopie.srcR.slice(kanal.length + 1));
185
+ if (wr) { kopie.role = wr; }
186
+ }
187
+ /* Nicht aus der Vorlage - beim Aktualisieren soll dieser Punkt
188
+ nicht ihr zugeschrieben werden. `ausAnsicht` haelt fest, dass er
189
+ nur deshalb in der Liste steht, weil gerade alle Datenpunkte
190
+ gezeigt werden; die Abweichungskarte darf ihn dann nicht als
191
+ „abgewaehlt" melden. */
192
+ kopie.ausVorlage = undefined;
193
+ kopie.ausAnsicht = true;
194
+ v.states.push(kopie);
195
+ });
196
+ /* Der Schnappschuss aus baueEntwurf sagt „alles an“ - die Rohkopien
197
+ haben ihn im Gepaeck. Der Standard-Chip soll aber den Zustand
198
+ wiederherstellen, in dem DIESE Ansicht oeffnet: Vorlagenzeilen wie
199
+ die Vorlage, Rohzeilen ab. Also hier neu stempeln. */
200
+ return merkeHakenVorgabe(v);
201
+ }
202
+
203
+ /* Ein Knoten muss kein Objekt sein.
204
+
205
+ MQTT legt fuer Zwischenebenen keine Objekte an: unter
206
+ `mqtt-client.0.SmartHome.Bastelzimmer.Bastelzimmer_Decklenlicht_RGB`
207
+ liegen Dutzende Datenpunkte, das Geraet selbst gibt es als Objekt
208
+ aber nicht. Der Baum zeigt es trotzdem, weil er sich aus den
209
+ Kennungen aufbaut. Wer hier auf `objects[...]` prueft, findet den
210
+ halben Bestand nicht - und genau daran fiel der Sprung zur Quelle bei
211
+ jedem Tasmota-Geraet aus. */
212
+ export function knotenDa(id) {
213
+ if (!id) { return false; }
214
+ if (S.objects[id]) { return true; }
215
+ return kindZustaende(id).length > 0;
216
+ }
217
+
218
+ export function quelleVon(aliasId) {
219
+ var o = S.objects[aliasId];
220
+ var q = o && o.native && o.native.quelle;
221
+ if (q && knotenDa(q)) { return q; }
222
+
223
+ var kandidaten = {};
224
+ kindZustaende(aliasId).forEach(function (id) {
225
+ var qq = aliasQuellen(S.objects[id]);
226
+ [qq.read, qq.write].forEach(function (x) {
227
+ if (!x) { return; }
228
+ var t = String(x).split('.');
229
+ t.pop();
230
+ var eltern = t.join('.');
231
+ if (eltern) { kandidaten[eltern] = (kandidaten[eltern] || 0) + 1; }
232
+ });
233
+ });
234
+ var beste = null, wieOft = 0;
235
+ Object.keys(kandidaten).forEach(function (k) {
236
+ if (kandidaten[k] > wieOft) { beste = k; wieOft = kandidaten[k]; }
237
+ });
238
+ /* Sitzen die Punkte in Unterkanaelen eines Geraets - bei Homematic der
239
+ Normalfall, 0.LOWBAT neben 1.STATE -, liegt die eigentliche Quelle
240
+ eine Ebene hoeher. */
241
+ if (beste && !S.objects[beste]) {
242
+ var t2 = beste.split('.'); t2.pop();
243
+ var hoeher = t2.join('.');
244
+ if (knotenDa(hoeher)) { return hoeher; }
245
+ }
246
+ if (beste && S.objects[beste] && S.objects[beste].type === 'channel') {
247
+ var t3 = beste.split('.'); t3.pop();
248
+ var dev = t3.join('.');
249
+ if (S.objects[dev] && S.objects[dev].type === 'device') { return dev; }
250
+ }
251
+ return knotenDa(beste) ? beste : null;
252
+ }
253
+
254
+ /* Der Sprung selbst: Baummodus umstellen, Filter fallen lassen, waehlen.
255
+ Ohne das Leeren des Filters landet man auf einem Knoten, den der
256
+ Baum gerade ausblendet. */
257
+ export function springeZu(id, modus) {
258
+ var q0 = $('#q');
259
+ if (q0) { q0.value = ''; }
260
+ if (S.baumModus !== modus) { setzeModus(modus); }
261
+ waehle(id);
262
+ }
263
+
264
+ /* Was in `common` der Werkbank gehoert - und was nicht.
265
+
266
+ Die Werkbank baut `common` beim Schreiben neu auf. Sie ist aber nur
267
+ fuer die Felder zustaendig, die den Alias ausmachen. Alles andere hat
268
+ jemand anderes dort abgelegt, und ein Aktualisieren hat es bisher
269
+ mitgeloescht:
270
+
271
+ custom InfluxDB, History, SQL - die ganze Aufzeichnung
272
+ smartName Alexa, Google Home
273
+ min, max, def, step
274
+ desc, icon, color
275
+
276
+ Am schwersten wiegt `custom`: Wer die Temperatur seines Thermostats
277
+ seit Monaten in InfluxDB schreibt und den Alias einmal aktualisiert,
278
+ verliert die Aufzeichnung, ohne dass ihm irgendwo etwas auffiele.
279
+ Fuer `native` galt derselbe Fehler schon einmal und wurde behoben -
280
+ `common` blieb uebersehen.
281
+
282
+ Deshalb: die eigenen Felder setzt die Werkbank, alle uebrigen
283
+ uebernimmt sie unveraendert aus dem, was schon dasteht. Setzt die
284
+ Vorlage eines der eigenen Felder nicht mehr - etwa `unit` -, faellt
285
+ es weg; das ist gewollt, dafuer ist sie zustaendig. */
286
+ /* Wo der Alias landen wuerde: alias.0.<Ordner>.<Geraet> */
287
+ /* Wohin ein neuer Alias vorgeschlagen wird.
288
+
289
+ Frueher immer das letzte Stueck der Kennung — bei einer Homematic also
290
+ `alias.0.NEQ0000000`, und bei einem Kanal davon schlicht `alias.0.1`.
291
+ Wo ein Objekt einen Namen traegt, ist der gemeint. */
292
+ /* Ein Name, der als Kennung taugt. Dieselbe Regel wie in den Feldern
293
+ der Zielleiste und des Verlegen-Dialogs - sie stand dort zweimal und
294
+ hier gar nicht: ein Geraetename wie „HM-Schaltaktor, Original" wurde
295
+ zu `HM-Schaltaktor_ Original`, das Komma ersetzt, das Leerzeichen
296
+ stehengelassen (gefunden 26.08.2026 an einem Fake-Geraet).
297
+ Umlaute bleiben, Punkte wuerden eine Unterebene aufmachen. */
298
+ export function kennungtauglich(t) {
299
+ return String(t).trim()
300
+ .replace(/[^\w.\- äöüÄÖÜß]/g, '_')
301
+ .replace(/\s+/g, '_')
302
+ .replace(/\./g, '_')
303
+ .replace(/_{2,}/g, '_')
304
+ .replace(/^_+|_+$/g, '');
305
+ }
306
+
307
+ export function zielId(kanal) {
308
+ var t = kanal.split('.');
309
+ var name = zusatzName(kanal) || t.pop();
310
+ if (zusatzName(kanal)) { t.pop(); }
311
+ var raum = t.length > 2 ? t[t.length - 1] : '';
312
+ name = kennungtauglich(name) || 'Alias';
313
+ return raum ? ('alias.0.' + kennungtauglich(raum) + '.' + name) : ('alias.0.' + name);
314
+ }
315
+
316
+ /* Alle Ordner, die es unter alias.0 schon gibt — als Ablagevorschlag.
317
+ Auch Zweige ohne eigenes Objekt zaehlen, MQTT legt die ja auch nicht an. */
318
+ export function ordnerUnterAlias() {
319
+ var m = {};
320
+ S.keysSorted.forEach(function (id) {
321
+ if (id.indexOf('alias.0.') !== 0) { return; }
322
+ var o = S.objects[id];
323
+ if (!o) { return; }
324
+ var teile = id.split('.');
325
+ if (o.type === 'state') { teile = teile.slice(0, -1); }
326
+ /* jede Ebene oberhalb des Geraets ist ein moeglicher Ablageort */
327
+ for (var i = 3; i < teile.length; i++) {
328
+ m[teile.slice(0, i).join('.')] = 1;
329
+ }
330
+ });
331
+ return Object.keys(m).sort();
332
+ }
333
+
334
+ /* ================== Auswahl ================== */
335
+ /* Mit „fertig" kann der Aufrufer warten, bis der Entwurf wirklich
336
+ steht: Werte geladen, Vorlage angewendet, Ziel gesetzt. Wer sofort
337
+ nach waehle weiterarbeitet, sieht den Rohentwurf — alle Punkte ohne
338
+ Rollen, und das Ziel noch leer. */
339
+ /* Steht der Fokus in einem Feld der rechten Seite, tippt oder waehlt
340
+ dort gerade jemand. Dann darf ihm die Seite nicht unter den Haenden
341
+ weggezogen werden: der Ordnervorschlag verschwand mitten im Klick,
342
+ weil spaet nachgeladene Werte ein zweites Mal zeichnen liessen. */
343
+ export function tipptGerade() {
344
+ var a = document.activeElement;
345
+ if (!a || !/^(INPUT|SELECT|TEXTAREA)$/.test(a.tagName)) { return false; }
346
+ return !!(a.closest && a.closest('.respane'));
347
+ }
348
+
349
+ /* Fuer alles, was nachtraeglich zeichnet — spaete Werte, Nachzieher.
350
+ Eine Aenderung, die der Nutzer selbst ausgeloest hat, zeichnet
351
+ weiterhin sofort. */
352
+ export function zeichneWennFrei() {
353
+ if (tipptGerade()) { setTimeout(zeichneWennFrei, 400); return; }
354
+ zeichneErgebnis();
355
+ }
356
+
357
+ /* Welche Kanaele eines Geraets ergaeben fuer sich ein Geraet?
358
+
359
+ Die Frage ist nicht zu umgehen und nicht sicher zu beantworten. Ein
360
+ Zweifach-Aktor hat zwei Kanaele mit je einem eigenen STATE — das sind
361
+ zwei Lampen. Ein Dimmer hat drei Kanaele mit je einem eigenen LEVEL —
362
+ das ist ein Dimmer, die anderen beiden sind virtuelle Kanaele
363
+ desselben Ausgangs. In den Daten sehen beide Faelle gleich aus.
364
+
365
+ Also wird nicht geraten, sondern gefragt: gibt es mehrere solcher
366
+ Kanaele, bietet die Fussleiste beide Wege an — genau wie bei einer
367
+ Tasmota mit mehreren Ausgaengen. Die Zahl steht am Knopf, die
368
+ Entscheidung trifft der Mensch.
369
+
370
+ Das Ergebnis wird gemerkt: `zeichneErgebnis` laeuft bei jedem
371
+ eintreffenden Wert, und der Detektor je Kanal ist zu teuer, um ihn
372
+ sekuendlich zu wiederholen. */
373
+ var kanalGeraeteCache = {};
374
+
375
+ export function steuerKanaele(dev) {
376
+ if (!S.objects[dev] || S.objects[dev].type !== 'device') { return []; }
377
+ if (kanalGeraeteCache[dev]) { return kanalGeraeteCache[dev]; }
378
+ var raus = [];
379
+ var pre = dev + '.';
380
+ S.keysSorted.forEach(function (id) {
381
+ if (id.indexOf(pre) !== 0) { return; }
382
+ if (id.slice(pre.length).indexOf('.') !== -1) { return; } /* nur direkte Kinder */
383
+ if (!S.objects[id] || S.objects[id].type !== 'channel') { return; }
384
+ var e2 = baueEntwurf(id);
385
+ if (!e2.states.length) { return; }
386
+ var f = erkenneEntwurf(e2, null).filter(function (x) { return x.type !== 'info'; });
387
+ if (!f.length) { return; }
388
+ /* Nur zaehlen, was einen Pflichtplatz wirklich besetzt — sonst
389
+ gilt jeder Kanal mit irgendeinem Messwert als Geraet. */
390
+ /* Alles, was der Kanal an Plaetzen belegt — nicht nur die
391
+ Pflichtplaetze. Sonst kaeme beim Zweifach-Aktor der Schalter des
392
+ zweiten Kanals mit, seine Rueckmeldung WORKING aber nicht: ein
393
+ Licht mit Schalter, das andere ohne Statusanzeige. */
394
+ var pflicht = f[0].states.filter(function (st) { return st.id; })
395
+ .map(function (st) { return st.id; });
396
+ if (!f[0].states.some(function (st) { return st.required && st.id; })) { return; }
397
+ raus.push({ id: id, typ: f[0].type, name: eigenerKanalName(id) || id.slice(pre.length),
398
+ pflicht: pflicht });
399
+ });
400
+ kanalGeraeteCache[dev] = raus;
401
+ return raus;
402
+ }
403
+
404
+ /* Ein fertiger Entwurf fuer einen Knoten, so wie ihn `waehle` erzeugen
405
+ wuerde — Vorlage, sonst Rohentwurf mit erkanntem Muster und der
406
+ Vorbelegung „nur was einen Platz hat". Gebraucht wird das, wenn aus
407
+ mehreren Kanaelen auf einmal Aliase entstehen sollen. */
408
+ /* Welche Kanaele eines Geraets gehoeren keinem einzelnen Verbraucher,
409
+ sondern allen? Bei Homematic ist das Kanal 0 mit Batterie, Empfang und
410
+ Erreichbarkeit — ein Funkmodul, zwei Lampen. */
411
+ export function gemeinsameKanaele(dev, kanalGeraete) {
412
+ var eigen = {};
413
+ (kanalGeraete || []).forEach(function (k) { eigen[k.id] = 1; });
414
+ var pre = dev + '.';
415
+ var raus = [];
416
+ S.keysSorted.forEach(function (id) {
417
+ if (id.indexOf(pre) !== 0) { return; }
418
+ if (id.slice(pre.length).indexOf('.') !== -1) { return; }
419
+ if (!S.objects[id] || S.objects[id].type !== 'channel') { return; }
420
+ if (eigen[id]) { return; }
421
+ if (!kindZustaende(id).length) { return; }
422
+ raus.push(id);
423
+ });
424
+ return raus;
425
+ }
426
+
427
+ export function entwurfFuer(id, gemeinsam, gemAn) {
428
+ var v = vorschlag(id);
429
+ if (v) { return v; }
430
+ var e2 = baueEntwurf(id);
431
+ if (!e2.states.length) { return null; }
432
+
433
+ /* Die Wartungsdaten des gemeinsamen Kanals gehoeren in jedes Geraet,
434
+ nicht in eines davon oder in einen Kanal daneben.
435
+
436
+ Ein Alias-Punkt ist ein Verweis, kein Wert — zwei Geraete duerfen
437
+ auf dieselbe Quelle zeigen, und der Batteriestand des Funkmoduls
438
+ gilt fuer beide Lampen gleichermassen. Lagerte man ihn in einen
439
+ eigenen `info`-Kanal aus, haette keine der beiden eine
440
+ Batteriewarnung: der Detektor schaut nur innerhalb eines Kanals.
441
+ Genau so macht es die Tasmota-Mehrfachvorlage mit RSSI — jeder der
442
+ vier Ausgaenge bekommt seinen eigenen, alle lesen aus tele/STATE. */
443
+ (gemeinsam || []).forEach(function (gid) {
444
+ kindZustaende(gid).forEach(function (qid) {
445
+ var o = S.objects[qid]; if (!o) { return; }
446
+ var c = o.common || {};
447
+ var kurz = qid.slice(gid.length + 1).replace(/\./g, '_');
448
+ /* Traegt der Kanal selbst schon einen Punkt dieses Namens, den
449
+ gemeinsamen unterscheidbar machen. */
450
+ if (e2.states.some(function (x) { return x.n === kurz; })) {
451
+ kurz = gid.split('.').pop() + '_' + kurz;
452
+ }
453
+ e2.states.push({
454
+ n: kurz, on: false, role: c.role || '', typ: c.type || '',
455
+ unit: c.unit || '', states: c.states || undefined, wr: false,
456
+ srcR: qid, srcW: '', f: '', fw: '', caption: '', manuell: false,
457
+ gemeinsam: true
458
+ });
459
+ });
460
+ });
461
+ var f = erkenneEntwurf(e2, null);
462
+ e2.wantAuto = f.length ? f[0].type : null;
463
+ e2.want = e2.wantAuto;
464
+ var funde = e2.want ? erkenneEntwurf(e2, e2.want) : f;
465
+ var haupt = funde.length ? funde[0] : null;
466
+ var platz = {};
467
+ if (haupt) {
468
+ haupt.states.forEach(function (x) {
469
+ if (x.id) { platz[x.id.slice(id.length + 1)] = x.name; }
470
+ });
471
+ }
472
+ if (Object.keys(platz).length) {
473
+ e2.states.forEach(function (st) { st.on = !!platz[st.n]; });
474
+ }
475
+ /* Zum Schluss die Wartungspunkte, die auf der Geraeteebene einen Platz
476
+ gefunden haben. Warum nicht einfach den Detektor auf dem Kanal
477
+ fragen? Weil er dort nur SET und WORKING zuordnet, obwohl dieselben
478
+ Punkte mit denselben Rollen im Abbild stehen — auf der Geraeteebene
479
+ findet er LOWBAT, UNREACH und RSSI. Die Zuordnung von dort ist die
480
+ belastbare; sie stammt aus demselben Detektor, nur aus dem Lauf, der
481
+ sie sieht. */
482
+ if (gemAn) {
483
+ e2.states.forEach(function (st) {
484
+ if (st.gemeinsam && gemAn[st.srcR]) { st.on = true; }
485
+ });
486
+ }
487
+ return merkeHakenVorgabe(e2);
488
+ }
489
+
490
+ export function waehle(id, fertig) {
491
+ /* Wer links etwas anderes anklickt, will es rechts sehen - auch wenn
492
+ der Fokus noch in einem Feld der rechten Seite steht. `tipptGerade`
493
+ haelt das Neuzeichnen zurueck, damit ein eintreffender Messwert
494
+ niemandem in die Eingabe faehrt; beim Wechsel der Auswahl ist das
495
+ falsch: das Feld gehoert zum alten Geraet und verschwindet ohnehin.
496
+
497
+ Ohne diesen Griff stand links das eine Geraet markiert und rechts das
498
+ andere aufgeschlagen - samt Fussleiste. Ein Klick auf „Alias
499
+ entfernen" haette dann den Alias des falschen Geraets getroffen
500
+ (gemessen 26.08.2026: Baum auf hm-rpc.0.000A1D89900003, Kopfzeile
501
+ weiter FK_Badezimmer, bis das Feld den Fokus abgab). */
502
+ if (id !== S.current) {
503
+ var a = document.activeElement;
504
+ if (a && a.blur && /^(INPUT|SELECT|TEXTAREA)$/.test(a.tagName) &&
505
+ a.closest && a.closest('.respane')) {
506
+ a.blur();
507
+ }
508
+ }
509
+ S.current = id;
510
+ S.openRow = null;
511
+ S.entwurf = baueEntwurf(id);
512
+ /* Bis die Werte da sind und die Vorlage gegriffen hat, steht rechts
513
+ der Rohentwurf — alle Punkte angehakt, ohne Rollen. Auf dem
514
+ Testsystem waren das 130 ms, auf einer grossen Anlage deutlich
515
+ mehr. In dieser Zeit darf der Schreibknopf nicht scharf sein. */
516
+ var bd0 = $('#btn-dry');
517
+ if (bd0) { bd0.disabled = true; }
518
+
519
+ /* Alle Ebenen darueber aufklappen, damit die Auswahl im Baum auch
520
+ sichtbar wird — sonst waehlt man rechts etwas aus und links
521
+ passiert nichts. */
522
+ var teile = id.split('.');
523
+ for (var i = 1; i <= teile.length; i++) {
524
+ S.aufgeklappt[teile.slice(0, i).join('.')] = true;
525
+ }
526
+ merkeKlappstand();
527
+ zeichneBaum();
528
+
529
+ var sel = document.querySelector('#tree .node.sel');
530
+ if (sel && sel.scrollIntoView) {
531
+ sel.scrollIntoView({ block: 'nearest' });
532
+ }
533
+
534
+ if (S.abo) { socket.emit('unsubscribe', S.abo); }
535
+ S.abo = id + '.*';
536
+ socket.emit('subscribe', S.abo);
537
+
538
+ /* Erst die Werte, dann der Vorschlag. Die Erkennung schaut in den
539
+ Rohwert von tele.SENSOR hinein — ohne Werte haelt sie ein Geraet
540
+ mit Messung faelschlich fuer eines ohne. */
541
+ var quellen = kindZustaende(id).slice();
542
+ S.entwurf.states.forEach(function (s) {
543
+ if (s.srcR) { quellen.push(s.srcR); }
544
+ if (s.srcW) { quellen.push(s.srcW); }
545
+ });
546
+ /* Vorlagen duerfen aus festen Objekten ausserhalb des Kanals lesen.
547
+ Deren Werte muessen vorliegen, bevor die Vorlage geprueft wird —
548
+ sonst faellt ein JSON-Feld durch, bloss weil noch nichts da war. */
549
+ S.VORLAGEN.forEach(function (v) {
550
+ (v.zustaende || []).forEach(function (z) {
551
+ if (z.absolut && z.lesen && S.objects[z.lesen]) { quellen.push(z.lesen); }
552
+ });
553
+ });
554
+
555
+ /* Nicht „fertig" nennen — so hiess der Rueckruf dieser Funktion, und
556
+ die lokale Marke ueberschattete ihn. Der Aufrufer wartete dann
557
+ vergeblich. */
558
+ var werteDa = false;
559
+ holeWerte(id, quellen, function () {
560
+ werteDa = true;
561
+ if (id !== S.current) { return; }
562
+
563
+ /* Wer schon Hand angelegt hat, behaelt seinen Entwurf.
564
+
565
+ Unten steht ein `entwurf = v` - der Vorschlag ersetzt den
566
+ Rohentwurf vollstaendig. Das ist richtig, solange niemand
567
+ dazwischenkommt. Es kann aber jemand dazwischenkommen: Nach
568
+ 150 ms zeichnet der Platzhalter, und `zeichneErgebnis` gibt dabei
569
+ den Trockenlaufknopf frei. Von da an ist die Oberflaeche bedienbar,
570
+ waehrend die Werte noch unterwegs sind. Wer in diesem Fenster einen
571
+ Raum waehlt, einen Namen tippt oder einen Haken setzt, verliert es
572
+ wieder, sobald die Werte eintreffen - ohne jede Meldung.
573
+
574
+ Auf dem Testsystem sind das gut 150 ms, an einem langsamen Tag
575
+ waren es 1000. Gefunden, als eine Raumwahl „Garten“ spurlos
576
+ verschwand und der Trockenlauf nur noch die Funktion zeigte.
577
+
578
+ Der Preis ist, dass die Vorlage dann nicht mehr greift. Das ist
579
+ die richtige Seite zum Nachgeben: ein nicht angewandter Vorschlag
580
+ faellt auf, eine verschluckte Eingabe nicht. */
581
+ if (S.entwurf && S.entwurf.angefasst) {
582
+ zeichneWennFrei();
583
+ if (typeof fertig === 'function') { fertig(); }
584
+ return;
585
+ }
586
+
587
+ if (id.indexOf('alias.') !== 0) {
588
+ /* Gibt es fuer diese Quelle schon einen Alias, bleibt es bei der
589
+ Vorlage, aus der er entstanden ist. Sonst koennte eine spaeter
590
+ angelegte Vorlage ein fertiges Geraet still uebernehmen. */
591
+ var fest = null;
592
+ var vorh = aliasFuer(id);
593
+ if (vorh && S.objects[vorh] && S.objects[vorh].native && S.objects[vorh].native.vorlage) {
594
+ var gid = S.objects[vorh].native.vorlage;
595
+ if (S.VORLAGEN.some(function (x) { return x.id === gid; })) { fest = gid; }
596
+ }
597
+ var v = vorschlag(id, fest);
598
+ if (v) {
599
+ v.ziel = zielId(id);
600
+ if (fest) { v.vonHandGewaehlt = false; v.ausBestand = true; }
601
+ /* Gibt es den Alias schon, gilt sein Stand - in beiden Ansichten
602
+ dasselbe Bild (Ricardo, 25.08.2026). */
603
+ if (vorh && S.objects[vorh]) { bestandVorrang(v, vorh); }
604
+ S.entwurf = v;
605
+ }
606
+ }
607
+ var nach = [];
608
+ S.entwurf.states.forEach(function (s) {
609
+ if (s.srcR && !S.werte[s.srcR]) { nach.push(s.srcR); }
610
+ });
611
+ if (nach.length) { holeWerte(id, nach, function () { zeichneWennFrei(); }); }
612
+ /* Auch dieser Durchgang kommt verzoegert — die Werte muessen erst da
613
+ sein. Wer in der Zwischenzeit schon ins Zielfeld geklickt hat, ist
614
+ schneller als die Oberflaeche; ihm gehoert der Vortritt. */
615
+ zeichneWennFrei();
616
+ if (typeof fertig === 'function') { fertig(); }
617
+ });
618
+
619
+ /* Der Rohentwurf — alle Punkte, keine Rollen — ist nur ein Platzhalter,
620
+ bis die Werte da sind und die Vorlage greift. Sofort gezeichnet
621
+ sprang die Liste bei jeder Auswahl sichtbar um. Also erst zeigen,
622
+ wenn das Laden wirklich dauert. */
623
+ if (!werteDa) {
624
+ setTimeout(function () {
625
+ if (!werteDa && id === S.current) { zeichneWennFrei(); }
626
+ }, 150);
627
+ }
628
+ }
629
+
630
+ export function opt(val, text) {
631
+ var o = document.createElement('option');
632
+ o.value = val; o.textContent = text;
633
+ return o;
634
+ }
635
+
636
+ /* Datenpunkte, die als Quelle in Frage kommen: alles unter denselben
637
+ Quellzweigen, aus denen die bisherigen Punkte lesen. */
638
+ export function quellenAuswahl(e) {
639
+ var liste = [];
640
+ function dazu(k) {
641
+ if (S.objects[k] && S.objects[k].type === 'state' && liste.indexOf(k) === -1) { liste.push(k); }
642
+ }
643
+
644
+ /* 1. Die Punkte des gewaehlten Knotens selbst — bei einer MQTT-Quelle
645
+ ist genau das die Auswahl. Vorher blieb die Liste hier leer und der
646
+ Kasten meldete „keine Quelle bekannt". */
647
+ kindZustaende(e.kanal).forEach(dazu);
648
+
649
+ /* 2. Zusaetzlich die Zweige, aus denen bestehende Punkte schon lesen —
650
+ bei einem fertigen Alias liegt die Quelle ja woanders. */
651
+ var wurzeln = {};
652
+ e.states.forEach(function (s) {
653
+ if (!s.srcR) { return; }
654
+ var t = s.srcR.split('.');
655
+ var w = t.slice(0, Math.max(2, t.length - 1));
656
+ wurzeln[w.join('.')] = 1;
657
+ /* Endet die Wurzel auf einer Sparte, gehoert das Geraet darueber
658
+ dazu. Sonst sammelt die Liste nur Geschwister derselben Sparte:
659
+ wer einen Punkt aus tele/STATE liest, bekam ausschliesslich
660
+ tele-Punkte angeboten und konnte kein Schreibziel in cmnd
661
+ waehlen — die acht cmnd-Punkte einer Farblampe fehlten
662
+ vollstaendig. */
663
+ if (w.length > 2 && SPARTEN.indexOf(w[w.length - 1]) > -1) {
664
+ wurzeln[w.slice(0, w.length - 1).join('.')] = 1;
665
+ }
666
+ });
667
+ Object.keys(wurzeln).forEach(function (w) {
668
+ S.keysSorted.forEach(function (k) { if (k.indexOf(w + '.') === 0) { dazu(k); } });
669
+ });
670
+
671
+ return liste;
672
+ }
673
+
674
+ /* Welche Geraete liegen unter diesem Knoten? Ein Knoten gilt als Geraet,
675
+ sobald eine Vorlage darauf passt; darunter wird nicht weitergesucht.
676
+ Findet sich keine Vorlage, zaehlt jeder Knoten mit eigenen Zustaenden. */
677
+ /* cmnd, stat und tele sind die Sparten eines Tasmota, keine eigenen
678
+ Geraete. Wer sie einzeln zaehlt, macht aus einem NSPanel drei. */
679
+ var SPARTEN = ['cmnd', 'stat', 'tele'];
680
+
681
+ export function geraeteDarunter(wurzel) {
682
+ var pre = wurzel + '.';
683
+ var tiefeWurzel = wurzel.split('.').length;
684
+
685
+ var kandidaten = {};
686
+ S.keysSorted.forEach(function (id) {
687
+ if (id.indexOf(pre) !== 0) { return; }
688
+ if (!S.objects[id] || S.objects[id].type !== 'state') { return; }
689
+ var teile = id.split('.');
690
+ for (var i = tiefeWurzel + 1; i < teile.length; i++) {
691
+ kandidaten[teile.slice(0, i).join('.')] = 1;
692
+ }
693
+ });
694
+
695
+ var pfade = Object.keys(kandidaten).sort(function (a, b) {
696
+ return a.split('.').length - b.split('.').length || (a < b ? -1 : 1);
697
+ });
698
+
699
+ var geraete = [];
700
+ var abgedeckt = [];
701
+ function schonDrin(p) {
702
+ return abgedeckt.some(function (g) { return p === g || p.indexOf(g + '.') === 0; });
703
+ }
704
+
705
+ /* Ein Objekt vom Typ `device` ist ein Geraet — was darunter liegt,
706
+ sind seine Kanaele, nicht eigene Geraete. Homematic verteilt einen
707
+ Heizkoerperthermostaten auf zwei davon: Wartung in :0, Funktion in
708
+ :1. Gemessen ueber 39 Geraete findet der Detektor auf der
709
+ Geraeteebene jedes Mal mehr als im besten einzelnen Kanal —
710
+ thermostat 7/13 gegen info 8/12. Wer die Kanaele einzeln zaehlt,
711
+ macht aus einem Thermostaten zwei Halbe. */
712
+ pfade.forEach(function (pfad) {
713
+ if (schonDrin(pfad)) { return; }
714
+ if (!S.objects[pfad] || S.objects[pfad].type !== 'device') { return; }
715
+ var b = null;
716
+ S.VORLAGEN.forEach(function (v) {
717
+ var t = pruefeVorlage(v, pfad);
718
+ if (t.passt && (!b || t.punkte > b.punkte)) { b = t; }
719
+ });
720
+ geraete.push({ id: pfad, vorlage: b ? b.vorlage : null, punkte: b ? b.punkte : 0 });
721
+ abgedeckt.push(pfad);
722
+ });
723
+
724
+ pfade.forEach(function (pfad) {
725
+ if (schonDrin(pfad)) { return; }
726
+ if (SPARTEN.indexOf(pfad.split('.').pop()) !== -1) { return; }
727
+ var beste = null;
728
+ S.VORLAGEN.forEach(function (v) {
729
+ var t = pruefeVorlage(v, pfad);
730
+ if (t.passt && (!beste || t.punkte > beste.punkte)) { beste = t; }
731
+ });
732
+ if (beste) {
733
+ geraete.push({ id: pfad, vorlage: beste.vorlage, punkte: beste.punkte });
734
+ abgedeckt.push(pfad);
735
+ }
736
+ });
737
+
738
+ /* Reste: Knoten mit eigenen Zustaenden, die keine Vorlage traf */
739
+ pfade.forEach(function (pfad) {
740
+ if (schonDrin(pfad)) { return; }
741
+ if (SPARTEN.indexOf(pfad.split('.').pop()) !== -1) { return; }
742
+ var eigene = S.keysSorted.filter(function (k) {
743
+ if (k.indexOf(pfad + '.') !== 0) { return false; }
744
+ if (!S.objects[k] || S.objects[k].type !== 'state') { return false; }
745
+ var rest = k.slice(pfad.length + 1);
746
+ if (rest.indexOf('.') === -1) { return true; }
747
+ /* Ein Zustand in einer Sparte gehoert dem Geraet darueber. */
748
+ var t = rest.split('.');
749
+ return t.length === 2 && SPARTEN.indexOf(t[0]) !== -1;
750
+ });
751
+ if (eigene.length) {
752
+ geraete.push({ id: pfad, vorlage: null, punkte: 0 });
753
+ abgedeckt.push(pfad);
754
+ }
755
+ });
756
+
757
+ geraete.sort(function (a, b) { return a.id < b.id ? -1 : 1; });
758
+ return geraete;
759
+ }
760
+
761
+ /* Gibt es fuer diese Quelle schon einen Alias?
762
+
763
+ Zwei Wege, und der zweite ist der wichtigere. Ueber native.quelle
764
+ findet man nur, was die Werkbank selbst angelegt hat. Von Hand
765
+ gebaute Aliase — bei einer gewachsenen Anlage die Mehrheit — tragen
766
+ den Vermerk nicht. Dann wird gesucht, wo es wirklich steht: in den
767
+ Aliasquellen der Datenpunkte.
768
+
769
+ Ohne das schlaegt die Werkbank ein freies Ziel vor, meldet „wird neu
770
+ angelegt" und erzeugt beim Klick ein zweites Geraet, waehrend das
771
+ erste verwaist stehenbleibt. Beim Karbonator lag der Alias unter
772
+ alias.0.Strom, vorgeschlagen wurde alias.0.SmartHome. */
773
+ export function aliasFuer(quelle) {
774
+ var treffer = null;
775
+ S.keysSorted.forEach(function (id) {
776
+ if (treffer || id.indexOf('alias.') !== 0) { return; }
777
+ var o = S.objects[id];
778
+ if (o && o.native && o.native.quelle === quelle) { treffer = id; }
779
+ });
780
+ if (treffer) { return treffer; }
781
+
782
+ /* Welcher Aliaskanal liest oder schreibt am haeufigsten in diesen
783
+ Zweig? Einzelne Punkte koennen anderswohin zeigen — ein
784
+ gemeinsamer Sollwert etwa —, deshalb die Mehrheit und nicht der
785
+ erste Treffer. */
786
+ var zaehler = {}, bester = null, wieviel = 0;
787
+ var pre = quelle + '.';
788
+ S.keysSorted.forEach(function (id) {
789
+ if (id.indexOf('alias.') !== 0) { return; }
790
+ var o = S.objects[id];
791
+ if (!o || o.type !== 'state') { return; }
792
+ var q = aliasQuellen(o);
793
+ var passt = (q.read && q.read.indexOf(pre) === 0) ||
794
+ (q.write && q.write.indexOf(pre) === 0);
795
+ if (!passt) { return; }
796
+ var eltern = id.slice(0, id.lastIndexOf('.'));
797
+ zaehler[eltern] = (zaehler[eltern] || 0) + 1;
798
+ if (zaehler[eltern] > wieviel) { wieviel = zaehler[eltern]; bester = eltern; }
799
+ });
800
+ return bester;
801
+ }
802
+
803
+ /* Was im Alias steht, gilt - auch in der Quellenansicht.
804
+
805
+ Bis 0.7.2 zeigte „Alias anlegen" bei einem vorhandenen Alias die Werte
806
+ der VORLAGE, „Alias bearbeiten" dagegen den Bestand. Dieselbe Zeile
807
+ trug also je nach Reiter eine andere Rolle, und ein Aktualisieren tat
808
+ je nach Reiter etwas anderes - an Ricardos PC-Steckdose stand im Alias
809
+ `state`, in der Quellenansicht `sensor.switch` (25.08.2026).
810
+
811
+ Jetzt gewinnt der Bestand in beiden Ansichten: Rolle, Typ, Einheit,
812
+ Werteliste, Beschriftung, Formeln, Quellen und Haken kommen aus dem
813
+ Alias, wenn es ihn gibt. Ein Aktualisieren aendert damit von sich aus
814
+ nichts mehr. Was die Vorlage anders will, bleibt sichtbar - je Zeile
815
+ als Vermerk und oben als Knopf „Vorlage anwenden".
816
+
817
+ `vorlagenWert` haelt fest, was die Vorlage sagen wuerde; ohne diese
818
+ Kopie koennte der Knopf spaeter nichts mehr anwenden. */
819
+ export function bestandVorrang(e, aliasId) {
820
+ if (!e || !e.states || !aliasId) { return e; }
821
+ var kanal = aliasId;
822
+ e.states.forEach(function (s) {
823
+ if (!s.n) { return; }
824
+ var o = S.objects[kanal + '.' + s.n];
825
+ if (!o || !o.common) {
826
+ /* Punkte, die es im Alias nicht gibt, sind auch nicht Teil seines
827
+ Bestands - der Haken der Vorlage darf sie nicht heimlich wieder
828
+ anlegen (Ricardo, 25.08.2026: UNREACH und RSSI standen angehakt,
829
+ obwohl sie im Alias fehlen). Anhaken kann man sie weiterhin, und
830
+ der Chip „Vorlage" holt die Vorgabe der Vorlage zurueck. */
831
+ s.vorlagenWert = { on: s.on, role: s.role, typ: s.typ, unit: s.unit,
832
+ caption: s.caption, f: s.f, fw: s.fw,
833
+ srcR: s.srcR, srcW: s.srcW, states: s.states };
834
+ s.on = false;
835
+ return;
836
+ }
837
+ var c = o.common;
838
+ var q = aliasQuellen(o);
839
+ /* Was die Vorlage wollte - fuer den Vergleich und den Knopf. */
840
+ s.vorlagenWert = {
841
+ role: s.role, typ: s.typ, unit: s.unit, caption: s.caption,
842
+ f: s.f, fw: s.fw, srcR: s.srcR, srcW: s.srcW, states: s.states,
843
+ /* Auch der Haken der Vorlage - der Chip „Vorlage" stellt ihn wieder
844
+ her, nachdem der Bestand ihn ueberschrieben hat. */
845
+ on: s.on
846
+ };
847
+ if (c.role !== undefined) { s.role = c.role; }
848
+ if (c.type !== undefined) { s.typ = c.type; }
849
+ s.unit = (c.unit !== undefined) ? c.unit : '';
850
+ s.states = c.states;
851
+ /* Die Beschriftung gehoert dem Nutzer - sie wird auch spaeter vom
852
+ Vorlagen-Abgleich nicht angefasst (Ricardo, 25.08.2026). */
853
+ if (c.name !== undefined) { s.caption = txt(c.name); }
854
+ var a = c.alias || {};
855
+ s.f = (typeof a.read === 'string') ? a.read : '';
856
+ s.fw = (typeof a.write === 'string') ? a.write : '';
857
+ if (q.read) { s.srcR = q.read; }
858
+ s.srcW = q.einfach
859
+ ? ((c.write === true || typeof a.write === 'string') ? (q.write || '') : '')
860
+ : (q.write || '');
861
+ s.on = true;
862
+ s.ausBestand = true;
863
+ });
864
+ /* Punkte, die es im Alias gibt, die Vorlage aber nicht kennt, sind
865
+ schon ueber `alleUndVorlage` bzw. den Bestandsabgleich drin - hier
866
+ wird nur ueberschrieben, was beide kennen. */
867
+ e.bestandGilt = true;
868
+ return e;
869
+ }
870
+
871
+ /* Wo weicht die Vorlage vom Bestand ab? Liefert je Zeile die Felder, die
872
+ „Vorlage anwenden" aendern wuerde - die Beschriftung ausgenommen. */
873
+ export function vorlagenAbweichung(s) {
874
+ var v = s && s.vorlagenWert;
875
+ if (!v) { return []; }
876
+ var raus = [];
877
+ var prüf = [['role', 'Rolle'], ['typ', 'Typ'], ['unit', 'Einheit'],
878
+ ['f', 'Leseformel'], ['fw', 'Schreibformel'],
879
+ ['srcR', 'liest aus'], ['srcW', 'schreibt auf']];
880
+ prüf.forEach(function (p) {
881
+ var alt = s[p[0]] || '', neu = v[p[0]] || '';
882
+ if (alt !== neu) { raus.push({ feld: p[0], jetzt: alt, vorlage: neu }); }
883
+ });
884
+ return raus;
885
+ }
886
+
887
+ /* Den Vorlagenstand herstellen - alles ausser der Beschriftung. */
888
+ export function vorlageAnwenden(e) {
889
+ if (!e || !e.states) { return; }
890
+ e.states.forEach(function (s) {
891
+ var v = s.vorlagenWert;
892
+ if (!v) { return; }
893
+ /* Nur was wirklich anders wird, bekommt die Marke. Vorher trug jede
894
+ angefasste Zeile „geaendert", auch die, an der sich nichts aenderte:
895
+ am Fensterkontakt standen drei Zeilen als geaendert da, waehrend der
896
+ Trockenlauf daneben „1 unveraendert" zaehlte (26.08.2026). Die Marke
897
+ behauptet sonst eine Abweichung, die es nicht gibt - genau der
898
+ Vorwurf, der schon fuer G21 galt. */
899
+ var anders = vorlagenAbweichung(s).length > 0;
900
+ s.role = v.role; s.typ = v.typ; s.unit = v.unit;
901
+ s.f = v.f; s.fw = v.fw; s.srcR = v.srcR; s.srcW = v.srcW;
902
+ s.states = v.states;
903
+ /* caption bleibt: der Anzeigename gehoert dem Nutzer. */
904
+ if (anders) { s.geaendert = true; }
905
+ });
906
+ e.bestandGilt = false;
907
+ e.angefasst = true;
908
+ }