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.
- package/LICENSE +21 -0
- package/README.md +516 -0
- package/admin/alias-workbench.png +0 -0
- package/admin/css/werkbank.css +916 -0
- package/admin/detector.js +1 -0
- package/admin/geraetewissen/tasmota-befehle.json +116 -0
- package/admin/i18n/de.json +552 -0
- package/admin/i18n/en.json +552 -0
- package/admin/js/aufzaehlungen.js +332 -0
- package/admin/js/basis.js +36 -0
- package/admin/js/baum.js +453 -0
- package/admin/js/detail.js +335 -0
- package/admin/js/einstellungen.js +40 -0
- package/admin/js/entwurf.js +908 -0
- package/admin/js/enums.js +23 -0
- package/admin/js/ergebnis.js +934 -0
- package/admin/js/erkennung.js +126 -0
- package/admin/js/katalog.js +313 -0
- package/admin/js/mqtt.js +885 -0
- package/admin/js/musternamen.js +94 -0
- package/admin/js/musterwahl.js +334 -0
- package/admin/js/objekte.js +243 -0
- package/admin/js/pruefungen.js +130 -0
- package/admin/js/quellentausch.js +570 -0
- package/admin/js/rollenwahl.js +199 -0
- package/admin/js/schreiben.js +1537 -0
- package/admin/js/sprache.js +137 -0
- package/admin/js/start.js +39 -0
- package/admin/js/verbindung.js +10 -0
- package/admin/js/vorlagen.js +572 -0
- package/admin/js/vorlagenblatt.js +1907 -0
- package/admin/js/vorschlagen.js +391 -0
- package/admin/js/werte.js +193 -0
- package/admin/js/zielleiste.js +339 -0
- package/admin/js/zuordnung.js +695 -0
- package/admin/js/zustand.js +47 -0
- package/admin/js/zustandsliste.js +515 -0
- package/admin/jsonConfig.json +249 -0
- package/admin/tab.html +148 -0
- package/admin/vorlagen/hm-dimmer.json +204 -0
- package/admin/vorlagen/hm-fensterkontakt-hm.json +176 -0
- package/admin/vorlagen/hm-fensterkontakt-hmip.json +191 -0
- package/admin/vorlagen/hm-heizgruppe.json +214 -0
- package/admin/vorlagen/hm-heizkoerper.json +236 -0
- package/admin/vorlagen/hm-rollladen.json +190 -0
- package/admin/vorlagen/hm-schaltaktor-lampe.json +197 -0
- package/admin/vorlagen/hm-schaltaktor-mehrfach.json +203 -0
- package/admin/vorlagen/hm-schaltaktor-steckdose.json +197 -0
- package/admin/vorlagen/hm-temperatur-feuchte.json +166 -0
- package/admin/vorlagen/hm-wandthermostat.json +228 -0
- package/admin/vorlagen/index.json +21 -0
- package/admin/vorlagen/messpunkt.json +225 -0
- package/admin/vorlagen/tasmota-lampe.json +263 -0
- package/admin/vorlagen/tasmota-mehrfach.json +203 -0
- package/admin/vorlagen/tasmota-rgb.json +239 -0
- package/admin/vorlagen/tasmota-steckdose.json +265 -0
- package/io-package.json +122 -0
- 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
|
+
}
|