iobroker.alias-workbench 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -98
- package/admin/css/werkbank.css +88 -0
- package/admin/i18n/de.json +766 -1
- package/admin/i18n/en.json +766 -1
- package/admin/i18n/es.json +766 -1
- package/admin/i18n/fr.json +766 -1
- package/admin/i18n/it.json +766 -1
- package/admin/i18n/nl.json +766 -1
- package/admin/i18n/pl.json +766 -1
- package/admin/i18n/pt.json +766 -1
- package/admin/i18n/ru.json +766 -1
- package/admin/i18n/uk.json +766 -1
- package/admin/i18n/zh-cn.json +766 -1
- package/admin/js/ablage.js +118 -0
- package/admin/js/attrappe.js +749 -0
- package/admin/js/baum.js +52 -5
- package/admin/js/detail.js +64 -3
- package/admin/js/entwurf.js +246 -31
- package/admin/js/ergebnis.js +77 -2
- package/admin/js/katalog.js +235 -40
- package/admin/js/mqtt.js +297 -27
- package/admin/js/musternamen.js +70 -26
- package/admin/js/objekte.js +2 -0
- package/admin/js/quellentausch.js +823 -16
- package/admin/js/schreiben.js +3 -2
- package/admin/js/sprache.js +11 -8
- package/admin/js/vorlagen.js +8 -3
- package/admin/js/vorlagenblatt.js +228 -64
- package/admin/js/werte.js +294 -5
- package/admin/js/zielleiste.js +19 -10
- package/admin/js/zuordnung.js +8 -7
- package/admin/js/zustand.js +32 -0
- package/admin/js/zustandsliste.js +9 -2
- package/admin/tab.html +24 -0
- package/admin/vorlagen/hm-dimmer.json +32 -8
- package/admin/vorlagen/hm-fensterkontakt-hm.json +20 -5
- package/admin/vorlagen/hm-fensterkontakt-hmip.json +28 -7
- package/admin/vorlagen/hm-heizgruppe.json +38 -11
- package/admin/vorlagen/hm-heizkoerper.json +42 -12
- package/admin/vorlagen/hm-rollladen.json +28 -7
- package/admin/vorlagen/hm-schaltaktor-lampe.json +24 -6
- package/admin/vorlagen/hm-schaltaktor-mehrfach.json +32 -8
- package/admin/vorlagen/hm-schaltaktor-steckdose.json +24 -6
- package/admin/vorlagen/hm-temperatur-feuchte.json +8 -2
- package/admin/vorlagen/hm-wandthermostat.json +36 -9
- package/admin/vorlagen/messpunkt.json +40 -10
- package/admin/vorlagen/tasmota-lampe.json +48 -12
- package/admin/vorlagen/tasmota-mehrfach.json +40 -10
- package/admin/vorlagen/tasmota-rgb.json +36 -9
- package/admin/vorlagen/tasmota-steckdose.json +56 -14
- package/io-package.json +41 -28
- package/package.json +3 -3
- package/admin/sprachen/de.json +0 -685
- package/admin/sprachen/en.json +0 -685
package/admin/js/werte.js
CHANGED
|
@@ -15,14 +15,17 @@ import './entwurf.js';
|
|
|
15
15
|
/* MQTT uebernimmt die Schreibweise des Themas. Dasselbe Geraetemodell
|
|
16
16
|
meldet mal cmnd.POWER1, mal cmnd.power1 — deshalb erst genau suchen,
|
|
17
17
|
dann ohne Ruecksicht auf Gross- und Kleinschreibung. */
|
|
18
|
+
/* Die Namen kommen aus `geraetePunkte`, nicht aus dem Pfad: bei einem
|
|
19
|
+
zerlegten MQTT-Geraet liegen die Punkte in Geschwisterzweigen, heissen
|
|
20
|
+
relativ aber genauso wie bei der anderen Bauform. Damit findet
|
|
21
|
+
`hatPunkt('cmnd.POWER')` sie in beiden Faellen — und die
|
|
22
|
+
Geraetevorlagen, die nur relativ adressieren, brauchen nichts davon zu
|
|
23
|
+
wissen. */
|
|
18
24
|
export function kleinKarte(kanal) {
|
|
19
25
|
if (S.kleinIndex[kanal]) { return S.kleinIndex[kanal]; }
|
|
20
26
|
var karte = {};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (k.indexOf(pre) !== 0) { return; }
|
|
24
|
-
if (!S.objects[k] || S.objects[k].type !== 'state') { return; }
|
|
25
|
-
karte[k.slice(pre.length).toLowerCase()] = k;
|
|
27
|
+
geraetePunkte(kanal).forEach(function (p) {
|
|
28
|
+
karte[p.rel.toLowerCase()] = p.id;
|
|
26
29
|
});
|
|
27
30
|
S.kleinIndex[kanal] = karte;
|
|
28
31
|
return karte;
|
|
@@ -101,6 +104,292 @@ export function direkteZustaende(kanal) {
|
|
|
101
104
|
return kindZustaende(kanal).filter(function (k) { return k.slice(pre.length).indexOf('.') === -1; });
|
|
102
105
|
}
|
|
103
106
|
|
|
107
|
+
/* ---- Werteliste: Anfang (test/vorpruefung.js schneidet hier) ----
|
|
108
|
+
|
|
109
|
+
`common.states` ist eine Karte Wert → Text: `{"0":"CLOSED","1":"OPEN"}`.
|
|
110
|
+
In der Oberflaeche steht sie als eine Zeile je Eintrag, `wert = text`,
|
|
111
|
+
weil neun Eintraege (HmIP-Ventilzustand) in kein einzeiliges Feld
|
|
112
|
+
passen.
|
|
113
|
+
|
|
114
|
+
Beim Lesen wird grosszuegig verfahren, beim Schreiben streng: ein `=`
|
|
115
|
+
trennt, Leerraum aussen faellt weg, leere Zeilen werden uebergangen.
|
|
116
|
+
Ein Schluessel ohne Text ist keiner — sonst entstuende `{"0":""}`, und
|
|
117
|
+
der Admin zeigte ein leeres Auswahlfeld. */
|
|
118
|
+
export function statesText(states) {
|
|
119
|
+
if (!states || typeof states !== 'object') { return ''; }
|
|
120
|
+
return Object.keys(states).map(function (k) { return k + ' = ' + states[k]; }).join('\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function textStates(text) {
|
|
124
|
+
var raus = {};
|
|
125
|
+
var leer = true;
|
|
126
|
+
String(text === null || text === undefined ? '' : text).split('\n').forEach(function (z) {
|
|
127
|
+
var i = z.indexOf('=');
|
|
128
|
+
if (i < 0) { return; }
|
|
129
|
+
var k = z.slice(0, i).trim();
|
|
130
|
+
var t = z.slice(i + 1).trim();
|
|
131
|
+
if (!k || !t) { return; }
|
|
132
|
+
raus[k] = t;
|
|
133
|
+
leer = false;
|
|
134
|
+
});
|
|
135
|
+
/* Nichts drin heisst: keine Werteliste. `undefined`, nicht `{}` —
|
|
136
|
+
ein leeres Objekt wuerde geschrieben und ergaebe am Alias eine
|
|
137
|
+
Auswahlliste ohne Eintraege. */
|
|
138
|
+
return leer ? undefined : raus;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Zwei Wertelisten vergleichen — fuer die Abweichungsanzeige. */
|
|
142
|
+
export function statesGleich(a, b) {
|
|
143
|
+
var ka = a ? Object.keys(a) : [];
|
|
144
|
+
var kb = b ? Object.keys(b) : [];
|
|
145
|
+
if (ka.length !== kb.length) { return false; }
|
|
146
|
+
return ka.every(function (k) { return String(a[k]) === String(b ? b[k] : undefined); });
|
|
147
|
+
}
|
|
148
|
+
/* ---- Werteliste: Ende ---- */
|
|
149
|
+
|
|
150
|
+
/* ---- Geraetepunkte: Anfang (test/vorpruefung.js schneidet hier) ----
|
|
151
|
+
|
|
152
|
+
/* ================== Ein Geraet, auch wenn es in Zweigen liegt ==================
|
|
153
|
+
|
|
154
|
+
Tasmotas `FullTopic` bestimmt, in welcher Reihenfolge Praefix (cmnd,
|
|
155
|
+
stat, tele) und Geraetename im Thema stehen — und weil mqtt und
|
|
156
|
+
mqtt-client das Thema eins zu eins auf den Objektpfad abbilden,
|
|
157
|
+
bestimmt es auch den Bau des Objektbaums:
|
|
158
|
+
|
|
159
|
+
%topic%/%prefix%/ → …Bastelzimmer_Licht.tele.STATE ein Knoten
|
|
160
|
+
%prefix%/%topic%/ → …SmartHome_Test.tele.Weihnachten_aussen.STATE
|
|
161
|
+
…SmartHome_Test.stat.Weihnachten_aussen.POWER
|
|
162
|
+
…SmartHome_Test.cmnd.Weihnachten_aussen.POWER
|
|
163
|
+
|
|
164
|
+
Die zweite Form ist Tasmotas Werkseinstellung. Dort gibt es keinen
|
|
165
|
+
Knoten, unter dem das Geraet als Ganzes steht: es liegt in zwei bis
|
|
166
|
+
drei Geschwisterzweigen, die nur der gleichlautende Namensteil in der
|
|
167
|
+
Mitte verbindet.
|
|
168
|
+
|
|
169
|
+
Diese Funktion beantwortet als einzige Stelle, welche Punkte zu einem
|
|
170
|
+
Geraet gehoeren und wie sie relativ heissen. Beide Formen ergeben
|
|
171
|
+
dieselben relativen Namen (`tele.STATE`, `cmnd.POWER`) — und damit
|
|
172
|
+
greifen die Geraetevorlagen unveraendert, denn sie adressieren nur
|
|
173
|
+
relativ. Gemessen am 30.09.2026 an einem umgestellten Tasmota
|
|
174
|
+
(Weihnachten_aussen): vorher fand `mqttGeraet()` am Geraeteknoten
|
|
175
|
+
nichts und legte am Knoten darueber `…SmartHome_Test.cmnd.POWER` an —
|
|
176
|
+
eine Kennung ohne Geraeteebene, die sich das zweite Geraet am selben
|
|
177
|
+
Praefix mit dem ersten geteilt haette. */
|
|
178
|
+
|
|
179
|
+
/* Praefixe, wie Tasmota sie ab Werk vergibt. Nur als Saatkorn fuer den
|
|
180
|
+
Fall, dass erst ein einziger Zweig da ist: sie lassen sich am Geraet
|
|
181
|
+
umbenennen (Prefix1-3), deshalb entscheidet zuerst der Bau des Baums
|
|
182
|
+
und erst danach diese Liste. */
|
|
183
|
+
var TASMOTA_PRAEFIX = /^(cmnd|stat|tele)$/i;
|
|
184
|
+
|
|
185
|
+
/* Das MQTT-Thema eines Punktes. Dieselbe Ablesung wie `mqttEinstellung`,
|
|
186
|
+
hier noch einmal in Kurzform: mqtt.js importiert dieses Modul, der
|
|
187
|
+
umgekehrte Weg ergaebe einen Ring. Gebraucht wird hier nur das Thema,
|
|
188
|
+
nicht die Frage, ob gesendet werden darf. */
|
|
189
|
+
function themaVon(id) {
|
|
190
|
+
var o = S.objects[id];
|
|
191
|
+
if (!o || !o.common) { return ''; }
|
|
192
|
+
var cu = o.common.custom || {};
|
|
193
|
+
var t = '';
|
|
194
|
+
Object.keys(cu).forEach(function (x) {
|
|
195
|
+
if (!t && /^mqtt/.test(x) && cu[x] && cu[x].topic) { t = cu[x].topic; }
|
|
196
|
+
});
|
|
197
|
+
return t || ((o.native || {}).topic || '');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Die Punkte unmittelbar unterhalb eines Knotens, mit ihrem relativen
|
|
201
|
+
Namen. */
|
|
202
|
+
function eigenePunkte(kanal) {
|
|
203
|
+
var pre = kanal + '.';
|
|
204
|
+
var raus = [];
|
|
205
|
+
mitVorspann(pre).forEach(function (k) {
|
|
206
|
+
if (!S.objects[k] || S.objects[k].type !== 'state') { return; }
|
|
207
|
+
raus.push({ id: k, rel: k.slice(pre.length) });
|
|
208
|
+
});
|
|
209
|
+
return raus;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Steht `[praefix, ...geraetPfad]` im Thema unmittelbar vor dem
|
|
213
|
+
Datenpunkt? Das ist der Beleg dafuer, dass die Ebene ueber dem Geraet
|
|
214
|
+
wirklich ein Praefix ist und nicht bloss ein Ordner.
|
|
215
|
+
|
|
216
|
+
Ohne diese Pruefung wuerde die Zusammenfassung auch dort greifen, wo
|
|
217
|
+
nichts zusammengehoert: `hm-rpc.0.ABC123.1` und `hm-rpc.0.DEF456.1`
|
|
218
|
+
sehen im Baum genauso aus wie zwei Praefixzweige desselben Geraets.
|
|
219
|
+
Homematic-Punkte tragen aber kein Thema, und damit ist die Frage
|
|
220
|
+
entschieden, bevor sie gestellt wird. */
|
|
221
|
+
function themaPasst(punkte, praefix, geraetPfad) {
|
|
222
|
+
var muster = [praefix].concat(geraetPfad);
|
|
223
|
+
return punkte.length > 0 && punkte.every(function (pt) {
|
|
224
|
+
var t = themaVon(pt.id);
|
|
225
|
+
if (!t) { return false; }
|
|
226
|
+
var tt = t.split('/');
|
|
227
|
+
var ab = tt.length - 1 - muster.length;
|
|
228
|
+
if (ab < 0) { return false; }
|
|
229
|
+
/* Der letzte Teil des Themas ist der Datenpunkt. Bei mqtt-client ist
|
|
230
|
+
das genau eine Ebene; liegt im Objektbaum mehr darunter, passt die
|
|
231
|
+
Form nicht und wir fassen nichts zusammen. */
|
|
232
|
+
if (tt[tt.length - 1] !== pt.rel) { return false; }
|
|
233
|
+
for (var i = 0; i < muster.length; i++) {
|
|
234
|
+
if (tt[ab + i] !== muster[i]) { return false; }
|
|
235
|
+
}
|
|
236
|
+
return true;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* Alle Zweige unter `eltern`, die denselben Geraetepfad tragen, je
|
|
241
|
+
Praefix gesammelt. */
|
|
242
|
+
function zweigePunkte(eltern, geraetPfad) {
|
|
243
|
+
var pre = eltern + '.';
|
|
244
|
+
var n = geraetPfad.length;
|
|
245
|
+
var schwanz = geraetPfad.join('.');
|
|
246
|
+
var raus = {};
|
|
247
|
+
mitVorspann(pre).forEach(function (k) {
|
|
248
|
+
if (!S.objects[k] || S.objects[k].type !== 'state') { return; }
|
|
249
|
+
var rest = k.slice(pre.length).split('.');
|
|
250
|
+
if (rest.length < n + 2) { return; }
|
|
251
|
+
if (rest.slice(1, 1 + n).join('.') !== schwanz) { return; }
|
|
252
|
+
var x = rest[0];
|
|
253
|
+
var punkt = rest.slice(1 + n).join('.');
|
|
254
|
+
(raus[x] = raus[x] || []).push({ id: k, rel: punkt });
|
|
255
|
+
});
|
|
256
|
+
return raus;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* Liegt dieses Geraet zerlegt in mehreren Praefixzweigen? Dann die
|
|
260
|
+
Punkte aller Zweige mit ihrem zusammengesetzten Namen. */
|
|
261
|
+
function zerlegtesGeraet(kanal) {
|
|
262
|
+
var eigene = eigenePunkte(kanal);
|
|
263
|
+
if (!eigene.length) { return null; }
|
|
264
|
+
/* Liegt unter dem Knoten noch eine Ebene, ist er selbst schon das
|
|
265
|
+
Geraet (Form „Geraet zuerst") — da gibt es nichts einzusammeln. */
|
|
266
|
+
if (eigene.some(function (p) { return p.rel.indexOf('.') > -1; })) { return null; }
|
|
267
|
+
|
|
268
|
+
var teile = kanal.split('.');
|
|
269
|
+
/* Das Praefix muss nicht unmittelbar ueber dem Geraet sitzen:
|
|
270
|
+
`tele/haus/og/lampe/STATE` ist ein gueltiges FullTopic. Also jede
|
|
271
|
+
Ebene daraufhin ansehen — und unter den moeglichen die richtige
|
|
272
|
+
waehlen, nicht die erstbeste. */
|
|
273
|
+
var kandidaten = [];
|
|
274
|
+
for (var p = 2; p <= teile.length - 2; p++) {
|
|
275
|
+
var praefix = teile[p];
|
|
276
|
+
var geraetPfad = teile.slice(p + 1);
|
|
277
|
+
var eltern = teile.slice(0, p).join('.');
|
|
278
|
+
/* Ein Werksname im Geraetepfad heisst: hier ist etwas verrutscht.
|
|
279
|
+
Wer im Baum den Praefixknoten selbst anklickt
|
|
280
|
+
(`…Bastelzimmer_Licht.tele`), bekaeme sonst `geraetPfad = ['tele']`
|
|
281
|
+
und als „Zweige" alle Geraete des Raums — `3D_Drucker`,
|
|
282
|
+
`Bastelzimmer_Licht`, `Steckdosenleiste_Drucker` sehen von dort aus
|
|
283
|
+
aus wie Praefixe. Der Entwurf haette dann die Punkte eines halben
|
|
284
|
+
Zimmers in einem Geraet. Ein Geraet heisst nicht cmnd, stat oder
|
|
285
|
+
tele. */
|
|
286
|
+
if (geraetPfad.some(function (x) { return TASMOTA_PRAEFIX.test(x); })) { continue; }
|
|
287
|
+
if (!themaPasst(eigene, praefix, geraetPfad)) { continue; }
|
|
288
|
+
var zweige = zweigePunkte(eltern, geraetPfad);
|
|
289
|
+
var namen = Object.keys(zweige).filter(function (x) {
|
|
290
|
+
return themaPasst(zweige[x], x, geraetPfad);
|
|
291
|
+
});
|
|
292
|
+
if (!namen.length) { continue; }
|
|
293
|
+
kandidaten.push({ praefix: praefix, geraetPfad: geraetPfad, eltern: eltern,
|
|
294
|
+
zweige: zweige, namen: namen });
|
|
295
|
+
}
|
|
296
|
+
if (!kandidaten.length) { return null; }
|
|
297
|
+
|
|
298
|
+
/* Zuerst die Werksnamen. Sie entscheiden, sobald einer passt — und das
|
|
299
|
+
tun sie fast immer, denn `cmnd/stat/tele` aendert kaum jemand.
|
|
300
|
+
|
|
301
|
+
Ohne diesen Vorrang griff am 30.09.2026 die falsche Ebene: das
|
|
302
|
+
umgestellte Weihnachtsgeraet lag noch als Leiche unter seiner alten
|
|
303
|
+
Wurzel, `…SmartHome.tele.Weihnachten_aussen` und
|
|
304
|
+
`…SmartHome_Test.tele.Weihnachten_aussen` enden gleich, und damit
|
|
305
|
+
galten die beiden Themenwurzeln als Praefixzweige eines Geraets.
|
|
306
|
+
Gemessen kam `basis: "tele/Weihnachten_aussen"` heraus und ein
|
|
307
|
+
Sendepunkt haette `mqtt-client.0.cmnd.tele.…` heissen sollen. */
|
|
308
|
+
var gewaehlt = null;
|
|
309
|
+
kandidaten.forEach(function (k) {
|
|
310
|
+
if (!gewaehlt && TASMOTA_PRAEFIX.test(k.praefix)) { gewaehlt = k; }
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
/* Und sonst gar nichts.
|
|
314
|
+
|
|
315
|
+
Hier stand bis zum 02.10.2026 ein struktureller Rueckfall: zwei
|
|
316
|
+
Zweige mit demselben Geraetepfad darunter, die noch ein zweites
|
|
317
|
+
Geraet teilen, galten auch ohne Werksnamen als Praefixe. Gedacht war
|
|
318
|
+
er fuer umbenannte `Prefix1-3`.
|
|
319
|
+
|
|
320
|
+
Am Produktivbestand gemessen hat er 50 Knoten falsch zusammengefasst
|
|
321
|
+
— durchweg Victron und Wechselrichter, keine einzige Tasmota:
|
|
322
|
+
`…system.0.Ac.Grid.L1` hat zwei eigene Punkte (Current, Power), der
|
|
323
|
+
Entwurf bekam **achtzehn**, naemlich die L1-Phase von Grid, Genset,
|
|
324
|
+
Consumption, PvOnGrid und allen uebrigen in einem Geraet. `Grid`
|
|
325
|
+
galt als Praefix, weil die Nachbarzweige ebenfalls ein `L1` fuehren
|
|
326
|
+
und das Thema dazu passt.
|
|
327
|
+
|
|
328
|
+
Die Regel laesst sich nicht so verschaerfen, dass sie Victron
|
|
329
|
+
aussperrt und umbenannte Tasmota-Praefixe einlaesst — beide sehen im
|
|
330
|
+
Baum gleich aus. Also entscheidet nur, was belegt ist: die
|
|
331
|
+
Werksnamen. Wer seine Praefixe umbenannt hat, bekommt die
|
|
332
|
+
Zusammenfassung nicht; das ist derselbe Stand wie vorher und nimmt
|
|
333
|
+
niemandem etwas weg, der sie schon hatte. */
|
|
334
|
+
if (!gewaehlt) { return null; }
|
|
335
|
+
|
|
336
|
+
var raus = [];
|
|
337
|
+
gewaehlt.namen.forEach(function (x) {
|
|
338
|
+
gewaehlt.zweige[x].forEach(function (pt) { raus.push({ id: pt.id, rel: x + '.' + pt.rel }); });
|
|
339
|
+
});
|
|
340
|
+
raus.sort(function (a, b) { return ZEILEN_VERGLEICH.compare(a.rel, b.rel); });
|
|
341
|
+
return { punkte: raus, eltern: gewaehlt.eltern, geraetPfad: gewaehlt.geraetPfad };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
/* Punkte und Bauform in einem, je Knoten einmal gerechnet. */
|
|
346
|
+
function geraeteBild(kanal) {
|
|
347
|
+
if (S.geraeteIndex[kanal]) { return S.geraeteIndex[kanal]; }
|
|
348
|
+
var bild = zerlegtesGeraet(kanal);
|
|
349
|
+
if (!bild) {
|
|
350
|
+
var pre = kanal + '.';
|
|
351
|
+
bild = {
|
|
352
|
+
punkte: kindZustaende(kanal).map(function (id) {
|
|
353
|
+
return { id: id, rel: id.slice(pre.length) };
|
|
354
|
+
}),
|
|
355
|
+
eltern: null, geraetPfad: null
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
S.geraeteIndex[kanal] = bild;
|
|
359
|
+
return bild;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* Die Punkte eines Geraets mit ihrem relativen Namen — egal, in welcher
|
|
363
|
+
Form der Adapter sie abgelegt hat. */
|
|
364
|
+
export function geraetePunkte(kanal) {
|
|
365
|
+
return geraeteBild(kanal).punkte;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* Liegt dieses Geraet zerlegt vor? Fuer die Stellen, die es sagen
|
|
369
|
+
wollen, statt es nur stillschweigend zu koennen. */
|
|
370
|
+
export function istZerlegt(kanal) {
|
|
371
|
+
return !!geraeteBild(kanal).eltern;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* Welche Kennung haette ein Punkt mit diesem relativen Namen an diesem
|
|
375
|
+
Geraet — auch wenn es ihn noch nicht gibt?
|
|
376
|
+
|
|
377
|
+
Genau daran scheiterte das Anlegen: `kanal + '.cmnd.' + name` traf bei
|
|
378
|
+
der zerlegten Form neben das Geraet. Zwei Tasmotas unter demselben
|
|
379
|
+
Praefix bekamen dieselbe Kennung, die zweite haette die erste samt
|
|
380
|
+
ihrem Thema ueberschrieben. */
|
|
381
|
+
export function punktKennung(kanal, rel) {
|
|
382
|
+
var bild = geraeteBild(kanal);
|
|
383
|
+
if (!bild.eltern) { return kanal + '.' + rel; }
|
|
384
|
+
var t = String(rel).split('.');
|
|
385
|
+
if (t.length < 2) { return kanal + '.' + rel; }
|
|
386
|
+
var praefix = t[0];
|
|
387
|
+
var punkt = t.slice(1).join('.');
|
|
388
|
+
return bild.eltern + '.' + praefix + '.' + bild.geraetPfad.join('.') + '.' + punkt;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* ---- Geraetepunkte: Ende ---- */
|
|
392
|
+
|
|
104
393
|
/* Wohin schreibt dieser Aliaspunkt?
|
|
105
394
|
|
|
106
395
|
Fuenfmal woertlich gleich ausprogrammiert gewesen — dreimal in
|
package/admin/js/zielleiste.js
CHANGED
|
@@ -11,7 +11,7 @@ import { el } from './basis.js';
|
|
|
11
11
|
import { tr } from './sprache.js';
|
|
12
12
|
import { enumListe } from './aufzaehlungen.js';
|
|
13
13
|
import { enumZeile, setzeZiel } from './zuordnung.js';
|
|
14
|
-
import { aliasFuer, ausgangsQuellen, ordnerUnterAlias, kennungtauglich, knotenDa } from './entwurf.js';
|
|
14
|
+
import { aliasFuer, ausgangsQuellen, ordnerUnterAlias, kennungtauglich, knotenDa, ohneUmlaute } from './entwurf.js';
|
|
15
15
|
import { zeichneErgebnis, entwurfAngefasst, angebotFrisch } from './ergebnis.js';
|
|
16
16
|
import { zeigeVerlegen } from './schreiben.js';
|
|
17
17
|
|
|
@@ -63,6 +63,7 @@ export function baueZielleiste(host, e) {
|
|
|
63
63
|
iO.value = '';
|
|
64
64
|
S.fokusOrdnerfeld = true;
|
|
65
65
|
e.zielOrdner = 'alias.0';
|
|
66
|
+
e.ordnerRoh = '';
|
|
66
67
|
e.ordnerVonHand = true;
|
|
67
68
|
e.zuletzt = 'ordner';
|
|
68
69
|
entwurfAngefasst();
|
|
@@ -76,16 +77,21 @@ export function baueZielleiste(host, e) {
|
|
|
76
77
|
zb.appendChild(wrap);
|
|
77
78
|
|
|
78
79
|
var saeubern = function (t) {
|
|
79
|
-
return
|
|
80
|
-
.replace(/[^\w.\-
|
|
80
|
+
return ohneUmlaute(t).trim()
|
|
81
|
+
.replace(/[^\w.\- ]/g, '_')
|
|
81
82
|
.replace(/\s+/g, '_')
|
|
82
83
|
.replace(/\.{2,}/g, '.')
|
|
83
84
|
.replace(/^\.+|\.+$/g, '');
|
|
84
85
|
};
|
|
85
86
|
var uebernehmen = function () {
|
|
86
|
-
|
|
87
|
+
/* Der Ordner ist eine Kennung, der Raum nicht. „Küche" wird als
|
|
88
|
+
Ablageort zu Kueche, als Raumvorschlag bleibt es Küche. */
|
|
89
|
+
var roh = String(iO.value || '').trim();
|
|
90
|
+
var t = saeubern(roh);
|
|
87
91
|
var neuerOrdner = t ? ('alias.0.' + t) : 'alias.0';
|
|
88
|
-
|
|
92
|
+
var rohNeu = (roh && roh.indexOf('.') === -1) ? roh : '';
|
|
93
|
+
if (neuerOrdner === e.zielOrdner && rohNeu === (e.ordnerRoh || '')) { return; }
|
|
94
|
+
e.ordnerRoh = rohNeu;
|
|
89
95
|
e.zielOrdner = neuerOrdner;
|
|
90
96
|
/* Ab jetzt gehoert der Ordner dem Nutzer - der Raum schreibt ihn
|
|
91
97
|
nicht mehr um. Angeboten wird die Uebernahme trotzdem, aber in
|
|
@@ -328,17 +334,20 @@ export function baueZielleiste(host, e) {
|
|
|
328
334
|
if (e.zuletzt === 'ordner') {
|
|
329
335
|
var otx = String(e.zielOrdner || '').slice('alias.0.'.length);
|
|
330
336
|
if (otx && otx.indexOf('.') === -1) {
|
|
337
|
+
/* Getippt wurde vielleicht „Küche", in der Kennung steht
|
|
338
|
+
Kueche. Der Raum bekommt den getippten Namen. */
|
|
339
|
+
var nameFuerRaum = e.ordnerRoh || otx;
|
|
331
340
|
var vorhandenerRaum = enumListe('rooms', true).filter(function (r) {
|
|
332
|
-
return r.name.toLowerCase() ===
|
|
333
|
-
r.kurz.toLowerCase() ===
|
|
341
|
+
return r.name.toLowerCase() === nameFuerRaum.toLowerCase() ||
|
|
342
|
+
r.kurz.toLowerCase() === nameFuerRaum.toLowerCase();
|
|
334
343
|
})[0];
|
|
335
|
-
var raumId = vorhandenerRaum ? vorhandenerRaum.id : ('enum.rooms.' +
|
|
344
|
+
var raumId = vorhandenerRaum ? vorhandenerRaum.id : ('enum.rooms.' + nameFuerRaum);
|
|
336
345
|
if (raumId !== e.raum) {
|
|
337
346
|
var az = el('div', 'uebernahme' +
|
|
338
347
|
(angebotFrisch('ordner:' + raumId) ? ' frisch' : ''));
|
|
339
348
|
az.appendChild(el('span', 'zq', vorhandenerRaum
|
|
340
|
-
? tr('target.alsoRoom',
|
|
341
|
-
: tr('target.alsoRoomNew',
|
|
349
|
+
? tr('target.alsoRoom', nameFuerRaum)
|
|
350
|
+
: tr('target.alsoRoomNew', nameFuerRaum)));
|
|
342
351
|
var ab = el('button', 'btn mini wichtig', tr('target.yes'));
|
|
343
352
|
ab.addEventListener('click', function () {
|
|
344
353
|
e.raum = raumId;
|
package/admin/js/zuordnung.js
CHANGED
|
@@ -17,7 +17,7 @@ import { enumVorlagen, katalogFehlt, ikonCache, ikonErlaubt, ikonNachtrag,
|
|
|
17
17
|
import { kindZustaende, aliasQuellen, holeEinzelne,
|
|
18
18
|
zeileAusAlias } from './werte.js';
|
|
19
19
|
import { aliasFuer, ausgangsQuellen, zielId, knotenDa,
|
|
20
|
-
aboErgaenzen, quellenVonEntwurf } from './entwurf.js';
|
|
20
|
+
kennungtauglich, aboErgaenzen, quellenVonEntwurf } from './entwurf.js';
|
|
21
21
|
import { zeichneErgebnis, entwurfAngefasst, angebotFrisch } from './ergebnis.js';
|
|
22
22
|
|
|
23
23
|
|
|
@@ -452,11 +452,12 @@ export function setzeEnums(e) {
|
|
|
452
452
|
}
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
/* Der Name einer Aufzaehlung, so wie er als Ordner
|
|
455
|
+
/* Der Name einer Aufzaehlung, so wie er als Ordner unter alias.0 taugt.
|
|
456
456
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
457
|
+
Dieselbe Regel wie beim Alias-Namen: kennungtauglich. Umlaute werden
|
|
458
|
+
zu ae, oe, ue und ss, ein Punkt wuerde sonst eine weitere Ebene
|
|
459
|
+
aufmachen. Der Raum bleibt „Wohnküche“, der Ordner heisst
|
|
460
|
+
„Wohnkueche“. */
|
|
460
461
|
export function enumAlsOrdner(id) {
|
|
461
462
|
if (!id) { return ''; }
|
|
462
463
|
var o = enums[id];
|
|
@@ -468,7 +469,7 @@ export function enumAlsOrdner(id) {
|
|
|
468
469
|
n = v ? (txt(v.name) || '') : '';
|
|
469
470
|
}
|
|
470
471
|
if (!n) { n = id.split('.').pop(); }
|
|
471
|
-
return
|
|
472
|
+
return kennungtauglich(n);
|
|
472
473
|
}
|
|
473
474
|
|
|
474
475
|
export function setzeZiel(e) {
|
|
@@ -862,7 +863,7 @@ export function unterschiede(e) {
|
|
|
862
863
|
var aliasDa = knotenDa(zielBasis);
|
|
863
864
|
|
|
864
865
|
e.states.forEach(function (s) {
|
|
865
|
-
if (s.manuell) { raus.push({ n: s.n || '
|
|
866
|
+
if (s.manuell) { raus.push({ n: s.n || tr('diff.noName'), was: tr('diff.newByHand') }); return; }
|
|
866
867
|
|
|
867
868
|
var imAlias = !!S.objects[zielBasis + '.' + s.n];
|
|
868
869
|
var grundlinieAn = aliasDa ? imAlias : warVorgabeAn(s);
|
package/admin/js/zustand.js
CHANGED
|
@@ -14,6 +14,10 @@ export var S = {
|
|
|
14
14
|
objects: {},
|
|
15
15
|
keysSorted: [],
|
|
16
16
|
kleinIndex: {},
|
|
17
|
+
/* Je Knoten die Punkte des Geraets samt relativem Namen — bei
|
|
18
|
+
zerlegten MQTT-Geraeten aus mehreren Zweigen zusammengetragen.
|
|
19
|
+
Verfaellt zusammen mit `kleinIndex`. */
|
|
20
|
+
geraeteIndex: {},
|
|
17
21
|
/* Zaehlt jede Aenderung an S.objects. Wer sich etwas aus den Objekten
|
|
18
22
|
merkt (die Karte Quelle -> Alias in entwurf.js), vergleicht hiermit
|
|
19
23
|
statt mit der Zahl der Schluessel — beim Verlegen verschwinden genau
|
|
@@ -24,6 +28,25 @@ export var S = {
|
|
|
24
28
|
aliasKaputt: {},
|
|
25
29
|
werte: {},
|
|
26
30
|
current: null,
|
|
31
|
+
/* Mehrere Zweige als EIN Geraet.
|
|
32
|
+
|
|
33
|
+
Manche Adapter verteilen ein Geraet auf Geschwisterzweige: Matter
|
|
34
|
+
legt eine Steckdose als Ordner mit zwei `device` darunter ab - der
|
|
35
|
+
Schalter in `OnOffPlugInUnit-1`, die Messwerte in
|
|
36
|
+
`ElectricalSensor-2` (gemessen am Produktivsystem 13.10.2026). Der
|
|
37
|
+
Ordner selbst traegt keine Datenpunkte und ist damit nicht
|
|
38
|
+
waehlbar, und jeder Endpoint einzeln ergibt ein halbes Geraet.
|
|
39
|
+
|
|
40
|
+
`S.current` bleibt EIN Knoten - hier der gemeinsame Elternknoten -,
|
|
41
|
+
und `S.zweige` sagt, welche seiner Zweige dazugehoeren. `null`
|
|
42
|
+
heisst „alle", also genau das bisherige Verhalten. Eine Liste
|
|
43
|
+
daraus zu machen haette `S.current` an 91 Stellen beruehrt, ohne
|
|
44
|
+
dass irgendeine davon besser geworden waere.
|
|
45
|
+
|
|
46
|
+
Die Auswahl ist fluechtig: sie ueberlebt keinen Seitenneustart. */
|
|
47
|
+
zweige: null,
|
|
48
|
+
/* Einmalige Rueckmeldung aus dem Baum an die rechte Seite. */
|
|
49
|
+
zweigMeldung: '',
|
|
27
50
|
openRow: null,
|
|
28
51
|
entwurf: null,
|
|
29
52
|
baum: null,
|
|
@@ -46,6 +69,15 @@ export var S = {
|
|
|
46
69
|
tauschPlan: null,
|
|
47
70
|
tauschWeg: {},
|
|
48
71
|
tauschFilter: '',
|
|
72
|
+
/* Fuer welches Ziel die Werte schon geholt sind - Stufe 5 braucht sie. */
|
|
73
|
+
tauschWerte: null,
|
|
74
|
+
/* Von Hand benannte Ziele beim Tausch: Alias-Punkt -> Punkt am neuen Geraet. */
|
|
75
|
+
tauschHand: {},
|
|
76
|
+
/* Was beim Tausch mit einer gewechselten Einheit geschehen soll:
|
|
77
|
+
Alias-Punkt -> nichts | einheit | rechnen. */
|
|
78
|
+
tauschEinheit: {},
|
|
79
|
+
/* Soll der Tausch die Formeln des neuen Geraets uebernehmen? */
|
|
80
|
+
tauschMerkung: null,
|
|
49
81
|
loeschListe: [],
|
|
50
82
|
loeschZiel: null,
|
|
51
83
|
loeschAlleAusgaenge: false,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { S } from './zustand.js';
|
|
10
10
|
import { el, $ } from './basis.js';
|
|
11
|
-
import { tr } from './sprache.js';
|
|
11
|
+
import { tr, sprachtext } from './sprache.js';
|
|
12
12
|
import { erkenneEntwurf, musterVon, rolleVonPlatz, typKonflikte, plaetzeFuerRolle, typPasstZuPlatz, fehlendePflicht } from './erkennung.js';
|
|
13
13
|
import { kindZustaende, wertVon, fmt } from './werte.js';
|
|
14
14
|
import { steuerKanaele, vorlagenAbweichung, bestandsAbweichung, waehle, quelleHier, quellenVerteilung } from './entwurf.js';
|
|
@@ -505,7 +505,14 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
|
|
|
505
505
|
„geaendert", „weicht ab") wurde sie damit zu voll, und Rolle und
|
|
506
506
|
Wert rutschten nach rechts weg (Ricardo, 08.09.2026). Die Kennung
|
|
507
507
|
traegt jetzt den Hinweis; sichtbar bleibt sie im Detail. */
|
|
508
|
-
|
|
508
|
+
/* Beschriftung und der Hinweis aus der Vorlage. Der Hinweis ist
|
|
509
|
+
eine Zeichenkette oder { de, en }; sprachtext nimmt die
|
|
510
|
+
eingestellte Sprache, sonst Englisch. */
|
|
511
|
+
var titel = [];
|
|
512
|
+
if (s.caption && s.caption !== s.n) { titel.push(s.caption); }
|
|
513
|
+
var zeilenHinweis = sprachtext(s.hinweis);
|
|
514
|
+
if (zeilenHinweis) { titel.push(zeilenHinweis); }
|
|
515
|
+
if (titel.length) { n3.title = titel.join('\n\n'); }
|
|
509
516
|
if (s.manuell) { n3.appendChild(el('span', 'hand', tr('list.byHand'))); }
|
|
510
517
|
if (s.geaendert) { n3.appendChild(el('span', 'geae', tr('list.changed'))); }
|
|
511
518
|
/* Was die Vorlage anders will, steht an der Zeile - sonst sieht man
|
package/admin/tab.html
CHANGED
|
@@ -166,6 +166,30 @@
|
|
|
166
166
|
<div class="dfoot"><button class="btn" data-close>Close</button></div>
|
|
167
167
|
</dialog>
|
|
168
168
|
|
|
169
|
+
<!-- Vor dem Ausgeben einer Attrappe: was geht in die Datei. Die Texte
|
|
170
|
+
werden zur Laufzeit gesetzt. -->
|
|
171
|
+
<dialog id="dlg-mock" style="max-width:880px;width:calc(100% - 32px);">
|
|
172
|
+
<h3 id="mock-titel">Export as mock device</h3>
|
|
173
|
+
<div class="dbody" id="mock-body"></div>
|
|
174
|
+
<div class="dfoot">
|
|
175
|
+
<button class="btn" data-close>Close</button>
|
|
176
|
+
<button class="btn primary" id="btn-mock-go">Save file</button>
|
|
177
|
+
</div>
|
|
178
|
+
</dialog>
|
|
179
|
+
|
|
180
|
+
<!-- Eine Attrappe einlesen: was entstehen wuerde, steht vorher da. Ein
|
|
181
|
+
eigener Dialog, nicht der Trockenlauf des Alias — der verlangt
|
|
182
|
+
`alias.` als Ziel, die Attrappe landet unter 0_userdata.0. -->
|
|
183
|
+
<dialog id="dlg-mockin" style="max-width:880px;width:calc(100% - 32px);">
|
|
184
|
+
<h3 id="mockin-titel">Import mock device</h3>
|
|
185
|
+
<div class="dbody" id="mockin-body"></div>
|
|
186
|
+
<div class="dfoot">
|
|
187
|
+
<button class="btn" data-close>Close</button>
|
|
188
|
+
<button class="btn primary" id="btn-mockin-go">Create now</button>
|
|
189
|
+
</div>
|
|
190
|
+
</dialog>
|
|
191
|
+
<input type="file" id="datei-attrappe" accept=".json,application/json" hidden>
|
|
192
|
+
|
|
169
193
|
<!-- Wer im Baum weiterklickt, verlor bis zum 06.09.2026 seine Aenderungen
|
|
170
194
|
ohne ein Wort (Ricardo). Der Text wird zur Laufzeit gesetzt. -->
|
|
171
195
|
<dialog id="dlg-leave">
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
"1.LEVEL",
|
|
18
18
|
"1.LEVEL_REAL"
|
|
19
19
|
],
|
|
20
|
-
"hinweis":
|
|
20
|
+
"hinweis": {
|
|
21
|
+
"de": "1.LEVEL_REAL ist der Unterschied zum Rollladenaktor, der ebenfalls ein 1.LEVEL hat, aber keine Ruecklesung - und zur Zentrale HM-RCV-50, die fuenfzig blanke LEVEL-Kanaele fuehrt.",
|
|
22
|
+
"en": "1.LEVEL_REAL is what sets this apart from the roller-shutter actuator, which also has a 1.LEVEL but no position feedback — and from the HM-RCV-50 central unit, which carries fifty bare LEVEL channels."
|
|
23
|
+
}
|
|
21
24
|
},
|
|
22
25
|
"zustaende": [
|
|
23
26
|
{
|
|
@@ -27,7 +30,10 @@
|
|
|
27
30
|
"einheit": "%",
|
|
28
31
|
"lesen": "1.LEVEL",
|
|
29
32
|
"schreiben": "1.LEVEL",
|
|
30
|
-
"hinweis":
|
|
33
|
+
"hinweis": {
|
|
34
|
+
"de": "Die Quelle steht schon auf 0 bis 100, also keine Umrechnung noetig.",
|
|
35
|
+
"en": "The source is already on a 0 to 100 scale, so no conversion is needed."
|
|
36
|
+
}
|
|
31
37
|
},
|
|
32
38
|
{
|
|
33
39
|
"name": "ACTUAL",
|
|
@@ -36,7 +42,10 @@
|
|
|
36
42
|
"einheit": "%",
|
|
37
43
|
"lesen": "1.LEVEL_REAL",
|
|
38
44
|
"optional": true,
|
|
39
|
-
"hinweis":
|
|
45
|
+
"hinweis": {
|
|
46
|
+
"de": "Der tatsaechlich erreichte Wert. Waehrend einer Rampe laeuft er dem Sollwert hinterher.",
|
|
47
|
+
"en": "The value actually reached. During a ramp it lags behind the setpoint."
|
|
48
|
+
}
|
|
40
49
|
},
|
|
41
50
|
{
|
|
42
51
|
"name": "WORKING",
|
|
@@ -44,7 +53,10 @@
|
|
|
44
53
|
"typ": "boolean",
|
|
45
54
|
"lesen": "1.WORKING",
|
|
46
55
|
"optional": true,
|
|
47
|
-
"hinweis":
|
|
56
|
+
"hinweis": {
|
|
57
|
+
"de": "true, solange die Rampe laeuft.",
|
|
58
|
+
"en": "true while the ramp is running."
|
|
59
|
+
}
|
|
48
60
|
},
|
|
49
61
|
{
|
|
50
62
|
"name": "DIRECTION",
|
|
@@ -53,7 +65,10 @@
|
|
|
53
65
|
"lesen": "1.DIRECTION",
|
|
54
66
|
"optional": true,
|
|
55
67
|
"vorgabeAus": true,
|
|
56
|
-
"hinweis":
|
|
68
|
+
"hinweis": {
|
|
69
|
+
"de": "0 steht, 1 heller, 2 dunkler, 3 unbekannt.",
|
|
70
|
+
"en": "0 is idle, 1 brighter, 2 darker, 3 unknown."
|
|
71
|
+
}
|
|
57
72
|
},
|
|
58
73
|
{
|
|
59
74
|
"name": "RAMP_TIME",
|
|
@@ -64,7 +79,10 @@
|
|
|
64
79
|
"schreiben": "1.RAMP_TIME",
|
|
65
80
|
"optional": true,
|
|
66
81
|
"vorgabeAus": true,
|
|
67
|
-
"hinweis":
|
|
82
|
+
"hinweis": {
|
|
83
|
+
"de": "Wie lange der naechste Uebergang dauern soll. Vor dem Setzen von LEVEL schreiben.",
|
|
84
|
+
"en": "How long the next transition should take. Write this before setting LEVEL."
|
|
85
|
+
}
|
|
68
86
|
},
|
|
69
87
|
{
|
|
70
88
|
"name": "ON_TIME",
|
|
@@ -84,7 +102,10 @@
|
|
|
84
102
|
"schreiben": "1.INHIBIT",
|
|
85
103
|
"optional": true,
|
|
86
104
|
"vorgabeAus": true,
|
|
87
|
-
"hinweis":
|
|
105
|
+
"hinweis": {
|
|
106
|
+
"de": "Sperrt den Aktor. Solange true, nimmt er keine Befehle an - auch nicht vom Taster.",
|
|
107
|
+
"en": "Locks the actuator. While true it accepts no commands — not even from the wall switch."
|
|
108
|
+
}
|
|
88
109
|
},
|
|
89
110
|
{
|
|
90
111
|
"name": "UNREACH",
|
|
@@ -104,7 +125,10 @@
|
|
|
104
125
|
"lesenSonst": {
|
|
105
126
|
"punkt": "0.RSSI_PEER"
|
|
106
127
|
},
|
|
107
|
-
"hinweis":
|
|
128
|
+
"hinweis": {
|
|
129
|
+
"de": "RSSI_DEVICE steht zuerst: es ist der Pegel, den die Zentrale vom Geraet empfaengt. RSSI_PEER ist bei Batteriegeraeten oft unbelegt und traegt dann Grenzwerte wie -2147483648.",
|
|
130
|
+
"en": "RSSI_DEVICE comes first: it is the level the central unit receives from the device. RSSI_PEER is often empty on battery devices and then carries sentinel values such as -2147483648."
|
|
131
|
+
}
|
|
108
132
|
}
|
|
109
133
|
],
|
|
110
134
|
"weiterePunkte": {
|