iobroker.alias-workbench 0.8.1 → 0.8.3

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.
@@ -22,6 +22,89 @@ export function rolleAusAusdruck(r) {
22
22
 
23
23
  export function rolleVonPlatz(st) { return st.defaultRole || rolleAusAusdruck(st.role); }
24
24
 
25
+ /* Alle Schreibweisen, die ein Platz annimmt — nicht nur seine Vorgabe.
26
+
27
+ Der LOWBAT-Platz nennt als Vorgabe `indicator.maintenance.lowbat`,
28
+ sein Ausdruck erlaubt aber vier Formen:
29
+
30
+ /^indicator(\.maintenance)?\.(lowbat|battery)$/
31
+
32
+ Nur die Vorgabe in die Auswahlliste zu stellen hiess: Wer die Rolle
33
+ sucht, die sein eigenes Geraet traegt, findet sie nicht. hm-rpc
34
+ schreibt `indicator.lowbat`, alle elf Homematic-Vorlagen benutzen sie,
35
+ und die ioBroker-Rollenliste fuehrt beide nebeneinander — nur die
36
+ Werkbank kannte eine davon nicht (Ricardo, 06.09.2026).
37
+
38
+ Ausmultipliziert werden genau zwei Formen, weil nur die im Detector
39
+ vorkommen: Alternativen `(a|b)` und optionale Gruppen `(…)?`. Alles
40
+ andere — Zeichenklassen, `.*`, Verschachtelung — gibt leer zurueck;
41
+ dann bleibt es bei der Vorgaberolle. Lieber eine Schreibweise zu wenig
42
+ als eine erfundene in der Liste. */
43
+ export function schreibweisenAus(r) {
44
+ if (!r) { return []; }
45
+ var q = (r.source !== undefined) ? r.source : String(r);
46
+ if (q.charAt(0) !== '^' || q.slice(-1) !== '$') { return []; }
47
+ q = q.slice(1, -1);
48
+
49
+ var teile = [''];
50
+ var rest = q;
51
+ var sicher = 0;
52
+ while (rest && sicher++ < 40) {
53
+ var g = /^\(([^()]*)\)(\?)?/.exec(rest);
54
+ if (g) {
55
+ var wahl = g[1].split('|');
56
+ if (g[2]) { wahl = wahl.concat(['']); } /* optional: auch weglassen */
57
+ var neu = [];
58
+ teile.forEach(function (v) {
59
+ wahl.forEach(function (w) { neu.push(v + w.split('\\').join('')); });
60
+ });
61
+ teile = neu;
62
+ rest = rest.slice(g[0].length);
63
+ continue;
64
+ }
65
+ var lit = /^(\\.|[A-Za-z0-9_.])+/.exec(rest);
66
+ if (!lit) { return []; } /* etwas Unbekanntes */
67
+ var text = lit[0].split('\\').join('');
68
+ teile = teile.map(function (v) { return v + text; });
69
+ rest = rest.slice(lit[0].length);
70
+ }
71
+ if (rest) { return []; }
72
+
73
+ var gut = [];
74
+ teile.forEach(function (t) {
75
+ if (/^[A-Za-z0-9_]+([.][A-Za-z0-9_]+)*$/.test(t) && gut.indexOf(t) === -1) { gut.push(t); }
76
+ });
77
+ return gut;
78
+ }
79
+
80
+ /* Rollen, die der Detector aus Ruecksicht auf alte Adapter noch annimmt,
81
+ die aber nicht mehr benutzt werden sollen. Aus der Tabelle „Deprecated
82
+ role aliases" in ioBroker.docs (docs/en/dev/stateroles.md, gelesen am
83
+ 06.09.2026) — dort steht zu jeder auch, was stattdessen gilt.
84
+
85
+ Sie stehen hier, weil die Zerlegung oben sonst gerade sie in die
86
+ Auswahlliste holt: `indicator(\.maintenance)?\.(lowbat|battery)`
87
+ erlaubt eben auch `indicator.battery`. Eine laengere Liste ist kein
88
+ Gewinn, wenn die Haelfte davon veraltet ist.
89
+
90
+ Vorgaberollen des Detectors werden NICHT gefiltert: was er selbst als
91
+ Vorgabe fuehrt, gilt, auch wenn die Doku es anders saehe. Gefiltert
92
+ wird nur, was diese Zerlegung zusaetzlich vorschlaegt. */
93
+ var VERALTET = {};
94
+ ('action.close action.close.blind action.close.tilt action.home action.next ' +
95
+ 'action.open action.open.blind action.open.tilt action.pause action.play ' +
96
+ 'action.prev action.stop action.stop.blind action.stop.tilt ' +
97
+ 'indicator.battery indicator.fire indicator.flood ' +
98
+ 'indicator.maintenance.battery indicator.unreach ' +
99
+ 'level.thermostat sensor.co sensor.fire sensor.flood ' +
100
+ 'state.active state.alarm.co state.alarm.fire state.alarm.flood state.co ' +
101
+ 'state.door state.fire state.flood state.light state.motion state.window ' +
102
+ 'switch.active switch.autofocus switch.autowhitebalance switch.boost ' +
103
+ 'switch.brightness switch.nightmode switch.party ' +
104
+ 'value.accuracy value.brush value.brush.side value.elevation ' +
105
+ 'value.latitude value.longitude value.radius value.sensors'
106
+ ).split(' ').forEach(function (r) { VERALTET[r] = 1; });
107
+
25
108
  export var ROLLEN = (function () {
26
109
  if (!D) { return []; }
27
110
  var m = {};
@@ -29,6 +112,10 @@ export var ROLLEN = (function () {
29
112
  D.patterns[t].states.forEach(function (st) {
30
113
  var r = rolleVonPlatz(st);
31
114
  if (r) { m[r] = 1; }
115
+ /* Dazu, was der Platz sonst noch annimmt — ohne das Veraltete. */
116
+ schreibweisenAus(st.role).forEach(function (x) {
117
+ if (!VERALTET[x]) { m[x] = 1; }
118
+ });
32
119
  });
33
120
  });
34
121
  ['state', 'text', 'json', 'value'].forEach(function (r) { m[r] = 1; });
@@ -23,7 +23,7 @@ import { tr, txt } from './sprache.js';
23
23
  import { socket } from './verbindung.js';
24
24
  import { aliasQuellen, kindZustaende } from './werte.js';
25
25
  import { holeZweig, indexNeu } from './objekte.js';
26
- import { waehle } from './entwurf.js';
26
+ import { waehle, knotenDa } from './entwurf.js';
27
27
  import { zeichneErgebnis } from './ergebnis.js';
28
28
 
29
29
  /* Die Punkte des Alias, die ueberhaupt eine Quelle haben. */
@@ -325,7 +325,7 @@ export function zeigeTausch() {
325
325
  /* Wie bei zeigeVerlegen und zeigeLoeschen: an der Quelle ist
326
326
  S.current die Quelle, gemeint ist das Ziel des Entwurfs (T1). */
327
327
  var ziel = (S.entwurf && (S.entwurf.ziel || S.entwurf.kanal)) || S.current;
328
- if (!ziel || ziel.indexOf('alias.') !== 0 || !S.objects[ziel]) { return; }
328
+ if (!ziel || ziel.indexOf('alias.') !== 0 || !knotenDa(ziel)) { return; }
329
329
  S.tauschZiel = ziel;
330
330
  S.tauschNeu = '';
331
331
  S.tauschWeg = {};
@@ -355,6 +355,22 @@ function zeichneTausch() {
355
355
  var z1 = el('div');
356
356
  z1.appendChild(el('b', null, tr('swap.from') + ' '));
357
357
  z1.appendChild(el('span', 'sn', alt || tr('swap.unknown')));
358
+ /* Den Namen dahinter, wo er mehr sagt als die Kennung.
359
+
360
+ `hm-rpc.0.0000DBE9A2B8AF` sagt einem nichts; woran man das Geraet
361
+ erkennt, steht allein im Namen (Ricardo, 06.09.2026). Dieselbe Regel
362
+ wie im Baum: gleicht der Name dem letzten Stueck der Kennung, bringt
363
+ er nichts und bleibt weg.
364
+
365
+ Ist das Geraet weg — der Normalfall bei „Geraet kaputt" —, gibt es
366
+ kein Objekt und damit keinen Namen mehr. Dann steht dort wie bisher
367
+ nur die Kennung, und die Zeile darunter sagt, dass es sie nicht mehr
368
+ gibt. */
369
+ var altObj = alt && S.objects[alt];
370
+ var altName = altObj ? txt(altObj.common && altObj.common.name).trim() : '';
371
+ if (altName && altName !== String(alt).split('.').pop()) {
372
+ z1.appendChild(el('span', 'leise', ' ' + altName));
373
+ }
358
374
  kopf.appendChild(z1);
359
375
  if (alt && !S.objects[alt] && !kindZustaende(alt).length) {
360
376
  kopf.appendChild(el('div', 'hint', tr('swap.gone')));
@@ -18,7 +18,9 @@ import {
18
18
  ordnerUnterAlias,
19
19
  gemeinsameKanaele,
20
20
  kennungtauglich,
21
- entwurfFuer
21
+ entwurfFuer,
22
+ aliasFuer,
23
+ knotenDa
22
24
  } from './entwurf.js';
23
25
  import { zeichneErgebnis } from './ergebnis.js';
24
26
 
@@ -162,8 +164,22 @@ export function objektText(o, geordnet) {
162
164
  return JSON.stringify(geordnet ? ordne(k, '') : k, null, 2);
163
165
  }
164
166
 
167
+ /* Der gespeicherte Stand zu einer Kennung.
168
+
169
+ Aufzaehlungen liegen nicht in `S.objects` - die Werkbank haelt sie in
170
+ einem eigenen Vorrat, weil der Admin sie getrennt ausliefert. Der
171
+ Trockenlauf las trotzdem nur `S.objects`, und so blieb die Spalte
172
+ „bisher" bei jeder Aufzaehlung leer: verglichen wurde gegen nichts,
173
+ also stand rechts jede Zeile als neu da - auch die zehn Mitglieder,
174
+ die laengst drin waren (Ricardo, 08.09.2026). */
175
+ export function bestandsObjekt(id) {
176
+ if (S.objects[id]) { return S.objects[id]; }
177
+ if (String(id).indexOf('enum.') === 0 && enums[id]) { return enums[id]; }
178
+ return null;
179
+ }
180
+
165
181
  export function unveraendert(id, obj) {
166
- var alt = S.objects[id];
182
+ var alt = bestandsObjekt(id);
167
183
  if (!alt) { return false; }
168
184
  var putz = function (x) {
169
185
  var k = JSON.parse(JSON.stringify(x || {}));
@@ -300,7 +316,9 @@ export function baueObjekte(e) {
300
316
  das wird nicht ungefragt geloescht. */
301
317
  export function verwaiste(e) {
302
318
  var ziel = e.ziel || e.kanal;
303
- if (!S.objects[ziel]) { return []; }
319
+ /* Auch ohne Kanalobjekt kann darunter ein Alias liegen - dann gibt es
320
+ dort auch verwaiste Punkte zu finden. */
321
+ if (!knotenDa(ziel)) { return []; }
304
322
  var behalten = {};
305
323
  e.states.forEach(function (st) {
306
324
  if (st.on && st.n) { behalten[ziel + '.' + st.n] = 1; }
@@ -472,7 +490,8 @@ export function zeigeTrockenlauf(alleAusgaenge) {
472
490
  verspraeche er eine Wirkung, die er nicht hat. */
473
491
  var vergleichbar = liste.filter(function (x) { return !x.neu && !x.gleich; });
474
492
  var rohAnders = vergleichbar.some(function (x) {
475
- return objektText(S.objects[x.id], false) !== objektText(S.objects[x.id], true)
493
+ var b0 = bestandsObjekt(x.id);
494
+ return objektText(b0, false) !== objektText(b0, true)
476
495
  || objektText(x.obj, false) !== objektText(x.obj, true);
477
496
  });
478
497
  if (vergleichbar.length && rohAnders) {
@@ -488,6 +507,20 @@ export function zeigeTrockenlauf(alleAusgaenge) {
488
507
  }
489
508
  body.appendChild(kopf);
490
509
 
510
+ /* Dieselbe Auskunft wie in der Zielleiste, noch einmal hier - der
511
+ Trockenlauf ist die letzte Stelle vor dem Schreiben, und wer den
512
+ Balken oben ueberliest, soll es spaetestens jetzt sehen: es gibt
513
+ schon einen Alias auf diese Quelle, und der bleibt stehen. */
514
+ var zwQuelle = S.entwurf && S.entwurf.kanal;
515
+ var zwZiel = S.entwurf && (S.entwurf.ziel || S.entwurf.kanal);
516
+ var zwAlt = zwQuelle ? aliasFuer(zwQuelle) : null;
517
+ if (zwAlt && S.objects[zwAlt] && zwAlt !== zwZiel) {
518
+ var zwK = el('div', 'aside w');
519
+ zwK.style.marginBottom = '11px';
520
+ zwK.appendChild(el('b', null, tr('target.twinExists', zwAlt)));
521
+ body.appendChild(zwK);
522
+ }
523
+
491
524
  if (weg.length) {
492
525
  var wk = el('div', 'card');
493
526
  wk.style.marginBottom = '11px';
@@ -657,7 +690,7 @@ export function zeigeTrockenlauf(alleAusgaenge) {
657
690
  var vgl = el('div', 'dvgl');
658
691
  vgl.appendChild(el('span', 'dkopf', tr('write.diffOld')));
659
692
  vgl.appendChild(el('span', 'dkopf', tr('write.diffNew')));
660
- zeilenVergleich(objektText(S.objects[x.id], !S.trockenRoh),
693
+ zeilenVergleich(objektText(bestandsObjekt(x.id), !S.trockenRoh),
661
694
  objektText(x.obj, !S.trockenRoh))
662
695
  .forEach(function (z) {
663
696
  vgl.appendChild(el('span', 'dz l ' + z.art, z.l === undefined ? '' : z.l));
@@ -678,7 +711,7 @@ export function zeigeTrockenlauf(alleAusgaenge) {
678
711
  var zt = $('#dry-titel');
679
712
  if (zt) {
680
713
  var zielName = S.entwurf.ziel || S.entwurf.kanal;
681
- zt.textContent = (S.objects[zielName] ? tr('write.dryTitleUpdate') : tr('write.dryTitleCreate')) +
714
+ zt.textContent = (knotenDa(zielName) ? tr('write.dryTitleUpdate') : tr('write.dryTitleCreate')) +
682
715
  (entwuerfe.length > 1 ? tr('write.dryOutputs', entwuerfe.length) : '');
683
716
  }
684
717
  /* Beim Oeffnen wieder herstellen: der Dialog wird wiederverwendet, und
@@ -714,7 +747,7 @@ function geschwisterVon(ziel) {
714
747
 
715
748
  export function zeigeLoeschen() {
716
749
  var ziel = (S.entwurf && (S.entwurf.ziel || S.entwurf.kanal)) || S.current;
717
- if (!ziel || ziel.indexOf('alias.') !== 0 || !S.objects[ziel]) { return; }
750
+ if (!ziel || ziel.indexOf('alias.') !== 0 || !knotenDa(ziel)) { return; }
718
751
  S.loeschZiel = ziel;
719
752
  S.loeschAlleAusgaenge = false;
720
753
  zeichneLoeschen();
@@ -986,17 +1019,27 @@ export function sucheInSkripten(kennung, fertig) {
986
1019
  } catch { fertigMit(); }
987
1020
  }
988
1021
 
989
- export function zeigeVerlegen() {
1022
+ export function zeigeVerlegen(altVorgabe, zielVorgabe) {
990
1023
  /* Der Knopf steht auch an der Quelle, sobald es den Alias gibt (T1).
991
1024
  Dort ist S.current die Quelle, nicht der Alias - gemeint ist immer
992
1025
  das Ziel des Entwurfs. Genauso haelt es zeigeLoeschen; zeigeTausch
993
1026
  tat es bis 05.09.2026 nicht, und beide Knoepfe waren an der Quelle
994
- sichtbar, aber wirkungslos: kein Dialog, keine Meldung. */
995
- var alt = (S.entwurf && (S.entwurf.ziel || S.entwurf.kanal)) || S.current;
1027
+ sichtbar, aber wirkungslos: kein Dialog, keine Meldung.
1028
+
1029
+ Die zwei Vorgaben kommen vom Knopf „stattdessen verlegen" in der
1030
+ Zielleiste: dort ist das Ziel des Entwurfs der NEUE Pfad, den es
1031
+ noch gar nicht gibt - ohne Vorgabe stiege der Dialog gleich in der
1032
+ ersten Zeile wieder aus. Verlegt wird der vorhandene Alias
1033
+ (`altVorgabe`), und die Felder stehen schon auf dem, was in der
1034
+ Zielleiste getippt wurde (`zielVorgabe`). */
1035
+ var alt = (typeof altVorgabe === 'string' && altVorgabe) ||
1036
+ (S.entwurf && (S.entwurf.ziel || S.entwurf.kanal)) || S.current;
996
1037
  if (!alt || alt.indexOf('alias.') !== 0 || !S.objects[alt]) { return; }
997
1038
  S.verlegeZiel = alt;
998
1039
 
999
- var teile = alt.split('.');
1040
+ var vorbelegt = (typeof zielVorgabe === 'string' && zielVorgabe &&
1041
+ zielVorgabe.indexOf('alias.') === 0) ? zielVorgabe : alt;
1042
+ var teile = vorbelegt.split('.');
1000
1043
  var name0 = teile.pop();
1001
1044
  var ordner0 = teile.join('.');
1002
1045
 
@@ -370,8 +370,17 @@ export function schreibZiel(z, kanal, auf) {
370
370
  return befehlMoeglich(kanal, pfad) ? (kanal + '.' + pfad) : '';
371
371
  }
372
372
 
373
- /* Vorlage auf einen Kanal anwenden → Entwurf. */
374
- export function wendeAn(v, kanal, gruende, instanz) {
373
+ /* Vorlage auf einen Kanal anwenden → Entwurf.
374
+
375
+ `auchOhneTreffer` laesst einen Entwurf auch dann entstehen, wenn keine
376
+ einzige Zeile der Vorlage eine Quelle am Geraet findet. Das gilt nur fuer
377
+ die Wahl von Hand: Genau dafuer ist das Auswahlfeld gedacht — „sonst steht
378
+ man bei einem Geraet, das noch nichts gesendet hat, ohne jede Moeglichkeit
379
+ da". Bis 06.09.2026 fiel gerade dieser Fall durch das Sieb unten, und der
380
+ Wechsel scheiterte still (Ricardo). Die automatische Erkennung ruft ohne
381
+ die Fahne auf und bleibt unveraendert — dort waere ein Entwurf ohne eine
382
+ einzige Zeile ein Fehlgriff. */
383
+ export function wendeAn(v, kanal, gruende, instanz, auchOhneTreffer) {
375
384
  var auf = function (t) { return setzeInstanz(t, v, instanz); };
376
385
  var e = { kanal: kanal, states: [], want: v.geraetetyp, wantAuto: v.geraetetyp,
377
386
  alleMuster: false, vorschlag: true, roh: false,
@@ -499,7 +508,7 @@ export function wendeAn(v, kanal, gruende, instanz) {
499
508
  });
500
509
  });
501
510
 
502
- return e.states.length ? e : null;
511
+ return (e.states.length || auchOhneTreffer) ? e : null;
503
512
  }
504
513
 
505
514
  /* Beste Vorlage suchen und anwenden. */
@@ -521,7 +530,7 @@ export function vorschlag(kanal, vorlagenId, instanz) {
521
530
  gewaehlt = treffer[0];
522
531
  }
523
532
 
524
- var e = wendeAn(gewaehlt.vorlage, kanal, gewaehlt.gruende, gewaehlt.instanz);
533
+ var e = wendeAn(gewaehlt.vorlage, kanal, gewaehlt.gruende, gewaehlt.instanz, !!vorlagenId);
525
534
  if (!e) { return null; }
526
535
  if (!vorlagenId && gewaehlt.passt) {
527
536
  var gleichAuf = treffer.filter(function (t) { return t.punkte === gewaehlt.punkte; });
package/admin/js/werte.js CHANGED
@@ -93,6 +93,26 @@ export function holeWerte(kanal, extraIds, fertig) {
93
93
  });
94
94
  }
95
95
 
96
+ /* Nur diese Kennungen, ohne den Rundumschlag ueber einen Kanal.
97
+
98
+ Gebraucht dort, wo Zeilen erst spaeter dazukommen und ihre Quelle
99
+ ausserhalb des angeklickten Knotens liegt - dann ist der Kanal
100
+ laengst abgefragt, und ein zweites `getForeignStates` darauf braechte
101
+ nichts als Last. */
102
+ export function holeEinzelne(ids, fertig) {
103
+ var eindeutig = {};
104
+ (ids || []).forEach(function (i) { if (i) { eindeutig[i] = 1; } });
105
+ var liste = Object.keys(eindeutig);
106
+ if (!liste.length) { if (typeof fertig === 'function') { fertig(false); } return; }
107
+ var offen = liste.length, etwas = false;
108
+ liste.forEach(function (id) {
109
+ socket.emit('getState', id, function (err, st) {
110
+ if (!err && st) { S.werte[id] = st; etwas = true; }
111
+ if (--offen <= 0 && typeof fertig === 'function') { fertig(etwas); }
112
+ });
113
+ });
114
+ }
115
+
96
116
  /* ================== Formel und Wert ================== */
97
117
  export function auswerten(formel, roh) {
98
118
  if (roh === undefined || roh === null) { return { ok: false, txt: tr('detail.noValue') }; }
@@ -11,8 +11,9 @@ 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, ordnerUnterAlias, kennungtauglich } from './entwurf.js';
14
+ import { aliasFuer, ordnerUnterAlias, kennungtauglich, knotenDa } from './entwurf.js';
15
15
  import { zeichneErgebnis, entwurfAngefasst, angebotFrisch } from './ergebnis.js';
16
+ import { zeigeVerlegen } from './schreiben.js';
16
17
 
17
18
  export function baueZielleiste(host, e) {
18
19
  var istQuelle = S.current.indexOf('alias.') !== 0;
@@ -224,7 +225,23 @@ export function baueZielleiste(host, e) {
224
225
  });
225
226
  iO.addEventListener('change', uebernehmen);
226
227
  iO.addEventListener('keydown', function (ev) {
227
- if (ev.key === 'Escape') { schliessen(); return; }
228
+ /* Escape heisst „doch nicht" — die Liste zu machen reicht dafuer
229
+ nicht, der getippte Text muss auch weg. Sonst steht er weiter im
230
+ Feld und wird beim Verlassen uebernommen: Wer Escape drueckt und
231
+ danebenklickt, legt den Alias unter einem Ordner an, den er
232
+ gerade verworfen zu haben glaubte.
233
+
234
+ Musterfeld (`musterwahl.js`, ruft mBeschriften) und
235
+ Aufzaehlungsfeld (`zuordnung.js`, setzt ei.value zurueck) machen
236
+ das laengst; hier und im Namensfeld daneben fehlte es
237
+ (Ricardo, 06.09.2026). */
238
+ if (ev.key === 'Escape') {
239
+ iO.value = (e.zielOrdner === 'alias.0') ? '' : e.zielOrdner.slice('alias.0.'.length);
240
+ xBtn.hidden = !iO.value;
241
+ schliessen();
242
+ iO.blur();
243
+ return;
244
+ }
228
245
  if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {
229
246
  if (vlist.hidden) { malen(true); return; }
230
247
  ev.preventDefault();
@@ -263,6 +280,14 @@ export function baueZielleiste(host, e) {
263
280
  var iN2 = el('input', 'tx');
264
281
  iN2.type = 'text';
265
282
  iN2.value = e.zielName;
283
+ /* Dasselbe fuer den Namen. Hier gab es bisher gar keinen
284
+ Tastenhandler — Escape tat nichts, der getippte Name blieb stehen
285
+ und wurde beim Verlassen uebernommen (Ricardo, 06.09.2026). */
286
+ iN2.addEventListener('keydown', function (ev) {
287
+ if (ev.key !== 'Escape') { return; }
288
+ iN2.value = e.zielName;
289
+ iN2.blur();
290
+ });
266
291
  iN2.addEventListener('change', function () {
267
292
  /* Dieselbe Regel wie im Vorschlag und im Verlege-Dialog. Die eigene
268
293
  Fassung hier liess Punkte und doppelte Unterstriche stehen - ein
@@ -278,7 +303,10 @@ export function baueZielleiste(host, e) {
278
303
  /* Beide Faelle, an einer Stelle. Vorher stand „wird neu angelegt"
279
304
  oben im Kopf und „gibt es schon" hier — zwei Formulierungen fuer
280
305
  dieselbe Auskunft, an zwei Orten. */
281
- var gibtsZiel = !!S.objects[e.ziel];
306
+ /* `knotenDa`, nicht `S.objects`: ein Alias, dessen Punkte von Hand
307
+ angelegt wurden, hat oft kein Kanalobjekt - der Chip meldete dort
308
+ „wird neu angelegt", obwohl geaendert wird (Ricardo, 08.09.2026). */
309
+ var gibtsZiel = knotenDa(e.ziel);
282
310
  var ch2 = el('span', 'chip ' + (gibtsZiel ? 'warn' : 'ok'),
283
311
  gibtsZiel ? tr('target.exists') : tr('result.newlyCreated'));
284
312
  ch2.style.marginLeft = '7px';
@@ -291,7 +319,13 @@ export function baueZielleiste(host, e) {
291
319
  Aenderung folgen, und der Blick ist beim angefassten Feld, nicht
292
320
  beim anderen. Der Text nennt deshalb die Wirkung woanders. */
293
321
  var angebot = null;
294
- if (e.zuletzt === 'ordner' && !aliasFuer(e.kanal)) {
322
+ /* Frueher hing hier zusaetzlich `!aliasFuer(e.kanal)` - an einem
323
+ Geraet, das schon einen Alias hat, kam die Frage also nie. Gedacht
324
+ war das als „am fertigen Alias wird nicht mehr geraten"; in
325
+ Wahrheit ist gerade das der Fall, in dem sie gebraucht wird: wer
326
+ den Ordner eines vorhandenen Alias umschreibt, zieht um, und der
327
+ Raum soll mit (Ricardo, 06.09.2026). */
328
+ if (e.zuletzt === 'ordner') {
295
329
  var otx = String(e.zielOrdner || '').slice('alias.0.'.length);
296
330
  if (otx && otx.indexOf('.') === -1) {
297
331
  var vorhandenerRaum = enumListe('rooms', true).filter(function (r) {
@@ -322,6 +356,42 @@ export function baueZielleiste(host, e) {
322
356
  setTimeout(function () { iO.focus(); }, 0);
323
357
  }
324
358
 
359
+ /* Der Zwilling: es gibt schon einen Alias auf diese Quelle, und das
360
+ Ziel zeigt woandershin.
361
+
362
+ Die Werkbank weiss das in diesem Moment - `aliasFuer` liefert den
363
+ alten Pfad -, sagte es aber nirgends. Am Chip stand nur „wird neu
364
+ angelegt", was stimmt und die Haelfte verschweigt: der alte bleibt
365
+ stehen, und aus einem Umbenennen des Ordners wird lautlos ein
366
+ zweiter Alias auf dieselbe Quelle (Ricardo, 06.09.2026, am
367
+ Bastelzimmer_Decklenlicht_RGB).
368
+
369
+ Verboten wird nichts. Zwei Aliase auf eine Quelle sind ein
370
+ ordentlicher Fall - einer fuers Licht, einer fuer den Verbrauch;
371
+ „alle anlegen" tut bei mehreren Ausgaengen nichts anderes. Es
372
+ fehlte die Auskunft, nicht die Sperre. Wer wirklich einen zweiten
373
+ will, drueckt denselben Knopf wie bisher und weiss jetzt, was er
374
+ tut; wer umziehen wollte, hat den Weg dafuer daneben.
375
+
376
+ Ohne Rueckprobe ueber `quelleVon`: an einem Homematic-Kanal
377
+ (…B8AF.0) zeigt die Quelle des Alias auf das Geraet (…B8AF), die
378
+ Probe schluege fehl und der Hinweis bliebe genau dort aus, wo er
379
+ gebraucht wird. `aliasFuer` ist dieselbe Funktion, mit der die
380
+ Werkbank auch „zum Alias →" und ihre Zielvorschlaege bestimmt -
381
+ irrt sie hier, irrt sie dort ebenso. Und der Hinweis haelt
382
+ niemanden auf. */
383
+ var zwilling = null;
384
+ var vorhAlias = aliasFuer(e.kanal);
385
+ if (vorhAlias && S.objects[vorhAlias] && vorhAlias !== e.ziel) {
386
+ var zw = el('div', 'uebernahme zwilling');
387
+ zw.appendChild(el('span', 'zq', tr('target.twinExists', vorhAlias)));
388
+ var vb = el('button', 'btn mini', tr('target.moveInstead'));
389
+ vb.title = tr('target.moveInsteadHint');
390
+ vb.addEventListener('click', function () { zeigeVerlegen(vorhAlias, e.ziel); });
391
+ zw.appendChild(vb);
392
+ zwilling = zw;
393
+ }
394
+
325
395
  /* Das Angebot ganz ans Ende der Leiste.
326
396
 
327
397
  Zuerst hing es direkt hinter dem Ordnerfeld - und weil es eine
@@ -329,6 +399,7 @@ export function baueZielleiste(host, e) {
329
399
  Namensfeld stand ploetzlich darunter statt daneben. Eine Zeile,
330
400
  die umbricht, gehoert hinter das, was in der Zeile bleiben soll. */
331
401
  if (angebot) { zb.appendChild(angebot); }
402
+ if (zwilling) { zb.appendChild(zwilling); }
332
403
 
333
404
  zb.appendChild(ergebnis);
334
405
 
@@ -14,8 +14,8 @@ import {
14
14
  } from './aufzaehlungen.js';
15
15
  import { enumVorlagen, katalogFehlt, ikonCache, ikonErlaubt, ikonNachtrag,
16
16
  ikonTaugt, vorlageZu, holeIkon } from './katalog.js';
17
- import { kindZustaende, aliasQuellen } from './werte.js';
18
- import { aliasFuer, zielId } from './entwurf.js';
17
+ import { kindZustaende, aliasQuellen, holeEinzelne } from './werte.js';
18
+ import { aliasFuer, zielId, knotenDa } from './entwurf.js';
19
19
  import { zeichneErgebnis, entwurfAngefasst, angebotFrisch } from './ergebnis.js';
20
20
 
21
21
 
@@ -394,7 +394,11 @@ export function enumZeile(host, e) {
394
394
  export function setzeEnums(e) {
395
395
  if (e.raum !== undefined && e.funktion !== undefined) { return; }
396
396
  var ziel = e.ziel || e.kanal;
397
- var da = !!S.objects[ziel];
397
+ /* Gibt es den Alias, gelten SEINE Aufzaehlungen - auch wenn sein Kanal
398
+ kein eigenes Objekt hat. Sonst wurden Raum und Funktion geraten und
399
+ als „vorgeschlagen" angezeigt, obwohl beide laengst zugeordnet
400
+ waren (Ricardo, 08.09.2026). */
401
+ var da = knotenDa(ziel);
398
402
 
399
403
  if (e.raum === undefined) {
400
404
  var r = da ? (enumsVon(ziel, 'rooms')[0] || '') : '';
@@ -499,7 +503,21 @@ export function setzeZiel(e) {
499
503
  export function uebernehmeBestand(e) {
500
504
  if (!e.ziel || e.bestandGeprueft === e.ziel) { return; }
501
505
  e.bestandGeprueft = e.ziel;
502
- if (!S.objects[e.ziel]) { return; }
506
+ /* Gefragt ist, ob es unter dem Ziel schon einen Alias gibt - nicht, ob
507
+ das Ziel selbst ein Objekt ist. Das ist nicht dasselbe: wer seine
508
+ Punkte von Hand im Admin anlegt, bekommt `alias.0.X.Y.PUNKT`, ohne
509
+ dass `alias.0.X.Y` als Kanal entsteht. Der Baum zeigt so einen
510
+ Knoten trotzdem, weil er sich aus den Kennungen aufbaut - `knotenDa`
511
+ ist genau dafuer da.
512
+
513
+ Mit der alten Frage stieg der ganze Bestandsabgleich dort aus:
514
+ `alias.0.Bastelzimmer.Klima` und `alias.0.Solar.Einstellungen.Bilanz`
515
+ haben kein Kanalobjekt, und ein Aktualisieren haette ihre Namen
516
+ durch die blossen Punktnamen ersetzt - „Strompreis fuer die
517
+ Ersparnisrechnung" waere zu „Strompreis_ct_kWh" geworden. Rollen,
518
+ Formeln und Schreibrichtung waeren ebenso durchgefallen
519
+ (Ricardo, 08.09.2026). */
520
+ if (!knotenDa(e.ziel)) { return; }
503
521
  /* Im Aliasmodus ist der Entwurf schon der Bestand. */
504
522
  if (e.kanal === e.ziel) { return; }
505
523
 
@@ -533,9 +551,35 @@ export function uebernehmeBestand(e) {
533
551
  z.states = c.states || undefined;
534
552
  z.f = (typeof a.read === 'string') ? a.read : '';
535
553
  z.fw = (typeof a.write === 'string') ? a.write : '';
554
+ /* Die Beschriftung steht im Alias und gehoert dem Nutzer.
555
+
556
+ Sie fehlte hier als einzige - `bestandVorrang` holt sie laengst
557
+ (`s.caption = txt(c.name)`), diese Zwillingsfunktion nicht. Also
558
+ blieb `caption` leer, im aufgeklappten Feld stand nichts, und ein
559
+ „Alias aktualisieren" haette den Namen durch den blossen
560
+ Zeilennamen ersetzt: aus „Netzbezug (+) / Einspeisung (-)" waere
561
+ „Bezug" geworden (Ricardo, 07.09.2026, an alias.0.Solar.Netz).
562
+ Gleicht die Beschriftung dem Zeilennamen, bleibt sie leer - so
563
+ haelt es der Zweig darunter, der neue Zeilen anlegt, auch. */
564
+ var nm2 = txt(c.name);
565
+ z.caption = (nm2 && nm2 !== z.n) ? nm2 : '';
536
566
  if (q2.read) { z.srcR = q2.read; }
567
+ /* Und dieselbe Schreibregel wie im Bestandsvorrang.
568
+
569
+ Hier stand die Schreibquelle nur fuer das getrennte Paar; sonst
570
+ blieb stehen, was der Vorschlag aus der Quelle abgeleitet hatte.
571
+ Bei einem Nur-Lese-Alias auf einen beschreibbaren Punkt hiess das:
572
+ die Quelle sagt `write: true`, der Alias sagt `write: false` - und
573
+ die Werkbank meldete eine Abweichung, die sie selbst erzeugt hatte,
574
+ und haette den Alias beim Aktualisieren schreibbar gemacht.
575
+
576
+ Belegt ist das Schreiben durch `common.write` oder durch eine
577
+ hinterlegte Schreibformel; bei getrennten Quellen zaehlt, was in
578
+ `alias.id.write` steht. */
579
+ z.srcW = q2.einfach
580
+ ? ((c.write === true || typeof a.write === 'string') ? (q2.write || '') : '')
581
+ : (q2.write || '');
537
582
  if (q2.write && !q2.einfach && q2.write !== q2.read) {
538
- z.srcW = q2.write;
539
583
  /* Die Gegenzeile eines Paars ist keine eigene mehr - ihre Quelle
540
584
  steckt jetzt als Schreibquelle in dieser Zeile. */
541
585
  e.states.forEach(function (x) {
@@ -577,6 +621,21 @@ export function uebernehmeBestand(e) {
577
621
  stehen; neue Punkte bekommen den neuen Namen. */
578
622
  if (q.read && kenntQuelle[q.read] && !S.objects[e.ziel + '.' + kenntQuelle[q.read].n]) {
579
623
  var vorhanden = kenntQuelle[q.read];
624
+ /* Die Zeile bekommt den Namen aus dem Alias — und damit einen
625
+ anderen, als die Erkennung eben gesehen hat. `platzVon` kennt
626
+ noch den Vorlagennamen, die Zeile heisst jetzt anders, und die
627
+ Platzpruefung findet nichts: Am `Bastelzimmer_Licht` stand nach
628
+ einem Wechsel auf die Steckdosen-Vorlage „kein Platz im
629
+ Steckdose-Muster" an ON_ACTUAL, obwohl der Platz ACTUAL frei
630
+ besetzt war. Nach jedem beliebigen Neuzeichnen war es weg
631
+ (Ricardo, 06.09.2026).
632
+
633
+ `bestandZog` loest genau dafuer das einmalige Nachzeichnen aus.
634
+ Gesetzt wurde es bisher nur in `vomAliasUebernehmen` — und das
635
+ steigt bei gesetzter Vorlage sofort aus, damit der Vorlagenstand
636
+ gilt. Die Umbenennung findet aber trotzdem statt, also gehoert
637
+ die Fahne hierher. */
638
+ if (vorhanden.n !== n) { e.bestandZog = true; }
580
639
  vorhanden.n = n;
581
640
  vorhanden.on = true;
582
641
  vorhanden.ausBestand = true;
@@ -590,7 +649,22 @@ export function uebernehmeBestand(e) {
590
649
  role: c.role || '', typ: c.type || '', unit: c.unit || '',
591
650
  states: c.states || undefined,
592
651
  wr: !!c.write,
593
- srcR: q.read || '', srcW: q.einfach ? '' : (q.write || ''),
652
+ srcR: q.read || '',
653
+ /* Dieselbe Schreibregel wie oben und im Bestandsvorrang.
654
+
655
+ Hier stand `q.einfach ? '' : (q.write || '')` - bei schlichtem
656
+ `alias.id` also immer leer, ohne `common.write` ueberhaupt
657
+ anzusehen. Es trifft jeden Alias, bei dem MEHRERE Punkte auf
658
+ denselben Quellpunkt zeigen: die eine Vorschlagszeile bekommt
659
+ der erste von ihnen, alle weiteren landen hier. Am
660
+ `alias.0.Badezimmer.Rollladen` zeigen OPEN, CLOSE, SET und pct
661
+ alle auf `…Bad.level`; CLOSE lief oben durch und behielt seine
662
+ Schreibquelle, OPEN, SET und pct verloren sie - ein
663
+ Aktualisieren haette den Rollladen ueber den Alias unfahrbar
664
+ gemacht (Ricardo, 08.09.2026). */
665
+ srcW: q.einfach
666
+ ? ((c.write === true || typeof a.write === 'string') ? (q.write || '') : '')
667
+ : (q.write || ''),
594
668
  f: (typeof a.read === 'string') ? a.read : '',
595
669
  fw: (typeof a.write === 'string') ? a.write : '',
596
670
  caption: (nm && nm !== n) ? nm : '',
@@ -612,6 +686,38 @@ export function uebernehmeBestand(e) {
612
686
  e.bestandNachgezeichnet = true;
613
687
  setTimeout(function () { if (S.entwurf === e) { zeichneErgebnis(); } }, 0);
614
688
  }
689
+
690
+ /* Und die Werte zu den eben ergaenzten Zeilen nachholen.
691
+
692
+ `waehle` holt die Werte, bevor es diese Zeilen ueberhaupt gibt: es
693
+ fragt den angeklickten Kanal ab und die Quellen, die der Entwurf zu
694
+ dem Zeitpunkt kennt. Was hier dazukommt, steht nur im Alias - und
695
+ seine Quelle kann irgendwo liegen. Solange sie unter demselben
696
+ Knoten liegt, faellt nichts auf, weil `getForeignStates` sie
697
+ mitgenommen hat. Liegt sie woanders, blieb die Zeile ohne Wert und
698
+ behauptete „Quelle liefert nichts", obwohl der Alias einwandfrei
699
+ arbeitet.
700
+
701
+ Gefunden am 07.09.2026 (Ricardo) an `alias.0.Solar.Netz`: drei
702
+ Punkte aus `0_userdata`, zwei aus `mqtt-client`. Nach einem
703
+ Reiterwechsel und dem ersten Klick auf den Knoten standen genau die
704
+ beiden mqtt-Zeilen leer; ein zweiter Klick heilte es, weil der Wert
705
+ dann schon im Vorrat lag. Der Mitschnitt zeigte, dass nach der
706
+ mqtt-Kennung nie gefragt wurde.
707
+
708
+ Einmal je Entwurf, und nur, wenn wirklich etwas fehlt. */
709
+ if (!e.bestandWerte) {
710
+ var fehlen = [];
711
+ e.states.forEach(function (s) {
712
+ if (s.srcR && !S.werte[s.srcR] && fehlen.indexOf(s.srcR) === -1) { fehlen.push(s.srcR); }
713
+ });
714
+ if (fehlen.length) {
715
+ e.bestandWerte = true;
716
+ holeEinzelne(fehlen, function (etwas) {
717
+ if (etwas && S.entwurf === e) { zeichneErgebnis(); }
718
+ });
719
+ }
720
+ }
615
721
  }
616
722
 
617
723
  export function unterschiede(e) {
@@ -652,7 +758,7 @@ export function unterschiede(e) {
652
758
  Grundlinie — dann zaehlt, was sich gegenueber dem Gespeicherten
653
759
  aendert. Gibt es ihn noch nicht, ist es der Vorschlag der
654
760
  Vorlage. */
655
- var aliasDa = !!S.objects[zielBasis];
761
+ var aliasDa = knotenDa(zielBasis);
656
762
 
657
763
  e.states.forEach(function (s) {
658
764
  if (s.manuell) { raus.push({ n: s.n || '(ohne Namen)', was: tr('diff.newByHand') }); return; }