iobroker.alias-workbench 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +58 -98
  2. package/admin/css/werkbank.css +88 -0
  3. package/admin/i18n/de.json +766 -1
  4. package/admin/i18n/en.json +766 -1
  5. package/admin/i18n/es.json +766 -1
  6. package/admin/i18n/fr.json +766 -1
  7. package/admin/i18n/it.json +766 -1
  8. package/admin/i18n/nl.json +766 -1
  9. package/admin/i18n/pl.json +766 -1
  10. package/admin/i18n/pt.json +766 -1
  11. package/admin/i18n/ru.json +766 -1
  12. package/admin/i18n/uk.json +766 -1
  13. package/admin/i18n/zh-cn.json +766 -1
  14. package/admin/js/ablage.js +118 -0
  15. package/admin/js/attrappe.js +749 -0
  16. package/admin/js/baum.js +52 -5
  17. package/admin/js/detail.js +64 -3
  18. package/admin/js/entwurf.js +246 -31
  19. package/admin/js/ergebnis.js +77 -2
  20. package/admin/js/katalog.js +235 -40
  21. package/admin/js/mqtt.js +297 -27
  22. package/admin/js/musternamen.js +70 -26
  23. package/admin/js/objekte.js +2 -0
  24. package/admin/js/quellentausch.js +823 -16
  25. package/admin/js/schreiben.js +3 -2
  26. package/admin/js/sprache.js +11 -8
  27. package/admin/js/vorlagen.js +8 -3
  28. package/admin/js/vorlagenblatt.js +228 -64
  29. package/admin/js/werte.js +294 -5
  30. package/admin/js/zielleiste.js +19 -10
  31. package/admin/js/zuordnung.js +8 -7
  32. package/admin/js/zustand.js +32 -0
  33. package/admin/js/zustandsliste.js +9 -2
  34. package/admin/tab.html +24 -0
  35. package/admin/vorlagen/hm-dimmer.json +32 -8
  36. package/admin/vorlagen/hm-fensterkontakt-hm.json +20 -5
  37. package/admin/vorlagen/hm-fensterkontakt-hmip.json +28 -7
  38. package/admin/vorlagen/hm-heizgruppe.json +38 -11
  39. package/admin/vorlagen/hm-heizkoerper.json +42 -12
  40. package/admin/vorlagen/hm-rollladen.json +28 -7
  41. package/admin/vorlagen/hm-schaltaktor-lampe.json +24 -6
  42. package/admin/vorlagen/hm-schaltaktor-mehrfach.json +32 -8
  43. package/admin/vorlagen/hm-schaltaktor-steckdose.json +24 -6
  44. package/admin/vorlagen/hm-temperatur-feuchte.json +8 -2
  45. package/admin/vorlagen/hm-wandthermostat.json +36 -9
  46. package/admin/vorlagen/messpunkt.json +40 -10
  47. package/admin/vorlagen/tasmota-lampe.json +48 -12
  48. package/admin/vorlagen/tasmota-mehrfach.json +40 -10
  49. package/admin/vorlagen/tasmota-rgb.json +36 -9
  50. package/admin/vorlagen/tasmota-steckdose.json +56 -14
  51. package/io-package.json +41 -28
  52. package/package.json +3 -3
  53. package/admin/sprachen/de.json +0 -685
  54. package/admin/sprachen/en.json +0 -685
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
- var pre = kanal + '.';
22
- S.keysSorted.forEach(function (k) {
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
@@ -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 String(t).trim()
80
- .replace(/[^\w.\- äöüÄÖÜß]/g, '_')
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
- var t = saeubern(iO.value);
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
- if (neuerOrdner === e.zielOrdner) { return; }
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() === otx.toLowerCase() ||
333
- r.kurz.toLowerCase() === otx.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.' + otx);
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', otx)
341
- : tr('target.alsoRoomNew', otx)));
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;
@@ -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 taugen wuerde.
455
+ /* Der Name einer Aufzaehlung, so wie er als Ordner unter alias.0 taugt.
456
456
 
457
- Ein Ordner unter `alias.0` darf keine Punkte enthalten - sie wuerden
458
- eine weitere Ebene aufmachen. Alles andere bleibt, wie der Nutzer es
459
- kennt: „Wohnkueche“ heisst auch im Pfad „Wohnkueche“. */
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 String(n).replace(/\./g, '_').trim();
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 || '(ohne Namen)', was: tr('diff.newByHand') }); return; }
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);
@@ -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
- if (s.caption && s.caption !== s.n) { n3.title = s.caption; }
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": "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."
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": "Die Quelle steht schon auf 0 bis 100, also keine Umrechnung noetig."
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": "Der tatsaechlich erreichte Wert. Waehrend einer Rampe laeuft er dem Sollwert hinterher."
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": "true, solange die Rampe laeuft."
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": "0 steht, 1 heller, 2 dunkler, 3 unbekannt."
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": "Wie lange der naechste Uebergang dauern soll. Vor dem Setzen von LEVEL schreiben."
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": "Sperrt den Aktor. Solange true, nimmt er keine Befehle an - auch nicht vom Taster."
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": "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."
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": {