iobroker.alias-workbench 0.11.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.
@@ -10,20 +10,30 @@
10
10
  der ganze Unterschied zum Neu-Anlegen, das den Alias aus der Vorlage
11
11
  heraus neu baut und dabei Namen, Rollen und Formeln mitbringt.
12
12
 
13
- Zugeordnet wird in drei Stufen, absteigend nach Verlaesslichkeit:
13
+ Zugeordnet wird in vier Stufen, absteigend nach Verlaesslichkeit:
14
14
  1. gleicher relativer Pfad (…ABC.1.STATE → …XYZ.1.STATE)
15
15
  2. ueber die Vorlage (der Punkt weiss aus `native.ausZustand`,
16
16
  aus welcher Zeile er stammt)
17
+ 4. ueber den Alias-Punkt (sein eigener Name und seine eigene
18
+ Rolle - nicht die seiner alten Quelle)
17
19
  3. ueber Rolle und Namen (nur als Vorschlag, sichtbar markiert)
18
- Was nichts findet, bleibt leer - und laesst sich dann entfernen. */
20
+ Was nichts findet, bleibt leer - und laesst sich dann entfernen.
21
+
22
+ Die Nummern sind die Reihenfolge ihrer Entstehung, nicht die der
23
+ Anwendung: Stufe 4 greift VOR dem Raten, weil sie belegter ist. Die
24
+ drei alten Nummern umzuhaengen haette jeden Pruefpunkt und jede
25
+ gespeicherte Messung ungueltig gemacht, ohne irgendetwas besser zu
26
+ machen. */
19
27
 
20
28
  import { S } from './zustand.js';
21
29
  import { $, el } from './basis.js';
22
- import { tr, txt } from './sprache.js';
30
+ import { tr, txt, sprachtext } from './sprache.js';
23
31
  import { socket } from './verbindung.js';
24
- import { aliasQuellen, kindZustaende } from './werte.js';
32
+ import { pruefeVorlage, vorschlag } from './vorlagen.js';
33
+ import { erkenneEntwurf } from './erkennung.js';
34
+ import { aliasQuellen, kindZustaende, holeWerte, auswerten } from './werte.js';
25
35
  import { holeZweig, indexNeu } from './objekte.js';
26
- import { waehle, knotenDa } from './entwurf.js';
36
+ import { waehle, knotenDa, rohEntwurf } from './entwurf.js';
27
37
  import { zeichneErgebnis } from './ergebnis.js';
28
38
 
29
39
  /* Die Punkte des Alias, die ueberhaupt eine Quelle haben. */
@@ -120,10 +130,296 @@ function geraten(altQuelle, neuKanal) {
120
130
  return treffer ? { id: treffer, ueber: 'rolle', rolle: rolle } : null;
121
131
  }
122
132
 
133
+ /* Stufe 5: dieselbe Frage wie beim Anklicken im Baum.
134
+
135
+ Der Tausch hat lange eigene Wege gesucht - Pfad, gemerkte Vorlage,
136
+ Raten, Alias-Punkt. Dabei stellt er dieselbe Frage, die die Werkbank
137
+ beim Anklicken eines Geraets ohnehin beantwortet: Welcher Punkt DORT
138
+ ist das `ELECTRIC_POWER`? Beim Anlegen entsteht genau diese Zuordnung
139
+ - Musterplatz zu Quellpunkt, samt Formel - und beim Tauschen wurde sie
140
+ bisher nicht gelesen (Ricardo, 13.10.2026: „so wie wenn ich im Baum
141
+ klicke, da wird ja auch versucht zu erkennen oder Vorlage").
142
+
143
+ Das ist der Weg, der in BEIDE Richtungen traegt:
144
+
145
+ - Zu einem Geraet, das seine Punkte beschriftet (Matter: `role:
146
+ value.power`, `unit: W`), antwortet der Detektor.
147
+ - Zu einem Geraet, das gar nichts beschriftet, antwortet die Vorlage.
148
+ Gemessen am Testsystem an `mqtt-client.0.SmartHome.PC_Jonas`: alle
149
+ zehn Punkte `role: (keine)`, `type: mixed`, der type-detector macht
150
+ daraus `info` - aber `tasmota-steckdose` passt mit Rang 2000020 und
151
+ weiss, dass `stat.POWER` der Melder ist und die Leistung in
152
+ `tele.SENSOR` unter `ENERGY.Power` steht.
153
+
154
+ Und die FORMELN kommen mit, weil sie zur Quelle gehoeren und nicht
155
+ zum Alias. Das war der offene Rest vom Vormittag: Beim Tausch von
156
+ Tasmota auf Matter blieb `JSON.parse(val)?.ENERGY?.Power` stehen und
157
+ machte aus Messwerten `null`; auf dem Rueckweg fehlte sie. Jetzt
158
+ bringt jede Richtung ihre eigene mit.
159
+
160
+ Gefragt wird ueber den PLATZNAMEN. Was im Alias zu keinem Musterplatz
161
+ gehoert - Handgebautes -, findet auch dieser Weg nicht; solche Punkte
162
+ bleiben uebrig und muessen entschieden werden, wie bisher. */
163
+ export function zielKarte(neuKanal) {
164
+ var karte = {};
165
+ if (!neuKanal) { return karte; }
166
+ /* Erst die Vorlage: sie weiss mehr als der Detektor - Formeln, Felder
167
+ in einem JSON, getrennte Lese- und Schreibquellen. */
168
+ var v = null;
169
+ try { v = vorschlag(neuKanal); } catch { v = null; }
170
+ if (v && v.states && v.states.length) {
171
+ v.states.forEach(function (st) {
172
+ if (!st.n || !st.srcR) { return; }
173
+ karte[String(st.n).toLowerCase()] = {
174
+ read: st.srcR, write: st.srcW || '', f: st.f || '', fw: st.fw || '',
175
+ her: 'vorlage', was: v.tplName || (v.vorlage && v.vorlage.id) || ''
176
+ };
177
+ });
178
+ return karte;
179
+ }
180
+ /* Sonst der Detektor am Rohentwurf - er benennt die Plaetze genauso
181
+ (`SET`, `ELECTRIC_POWER`), nur ohne Formeln. */
182
+ var roh = null;
183
+ try { roh = rohEntwurf(neuKanal); } catch { roh = null; }
184
+ if (!roh) { return karte; }
185
+ var erk = [];
186
+ try { erk = erkenneEntwurf(roh, null) || []; } catch { erk = []; }
187
+ /* Die Kennung, die der Detektor nennt, ist KEIN Objekt.
188
+
189
+ `abbild` baut ihm ein eigenes Bild des Entwurfs: ein Kanal, alle
190
+ Punkte flach darunter, benannt nach der Alias-Zeile. Aus
191
+ `…Plug.ElectricalSensor-2.ELECTRIC_POWER` wird dort
192
+ `…Plug.ElectricalSensor-2_ELECTRIC_POWER` - mit Unterstrich, weil
193
+ ein Punkt im Namen am Alias eine Unterebene aufmachen wuerde. Wer
194
+ diese Kennung als Quelle einträgt, legt einen Alias an, der ins
195
+ Leere zeigt (gemessen in W30, 13.10.2026).
196
+
197
+ Die echte Quelle steht in der Zeile selbst: `srcR`/`srcW`. Also
198
+ ueber den Zeilennamen zurueckrechnen. */
199
+ var zeilen = {};
200
+ (roh.states || []).forEach(function (st) { if (st.n) { zeilen[st.n] = st; } });
201
+ var vor = neuKanal + '.';
202
+ ((erk[0] && erk[0].states) || []).forEach(function (s) {
203
+ if (!s.id || !s.name) { return; }
204
+ var zeile = zeilen[String(s.id).indexOf(vor) === 0 ? String(s.id).slice(vor.length) : ''];
205
+ if (!zeile || !zeile.srcR) { return; }
206
+ karte[String(s.name).toLowerCase()] = {
207
+ read: zeile.srcR,
208
+ /* Schreiben nur, wo der Punkt es zulaesst - ein Melder bekommt
209
+ kein Schreibziel. */
210
+ write: zeile.srcW || '',
211
+ f: zeile.f || '', fw: zeile.fw || '', her: 'detektor', was: erk[0].type || ''
212
+ };
213
+ });
214
+ return karte;
215
+ }
216
+
217
+ /* Stufe 4: der Alias-Punkt selbst - sein Name und seine Rolle.
218
+
219
+ Die drei alten Stufen fragen alle, WOHER der Punkt bisher liest. Das
220
+ greift, solange beide Geraete aehnlich gebaut sind, und versagt
221
+ vollstaendig, sobald sie es nicht sind. Gemessen am 13.10.2026 am
222
+ Produktivsystem: eine Tasmota-Steckdose auf eine Matter-Steckdose
223
+ umhaengen ergab 0 von 8 Zuordnungen - bei zwei Geraeten, die dasselbe
224
+ tun.
225
+
226
+ Zweimal lag es an der Quelle, nie am Ziel:
227
+ - mqtt-client pflegt keine Rollen (`stat.POWER`, `cmnd.POWER` und
228
+ `tele.SENSOR` haben alle `role: undefined`), damit faellt der
229
+ Rollenvergleich in Stufe 3 ohne einen Treffer aus.
230
+ - Fuenf der acht Punkte lesen aus EINEM Punkt, dem Telemetrie-JSON
231
+ `tele.SENSOR`. Aus ihm laesst sich nichts ableiten; er heisst
232
+ `SENSOR` und traegt die Bedeutung erst in der Formel.
233
+
234
+ Dabei steht die Antwort fertig im Alias: `…PC.ELECTRIC_POWER` traegt
235
+ `role: value.power`, und am neuen Geraet liegt
236
+ `…ElectricalSensor-2.ELECTRIC_POWER` mit genau dieser Rolle. Die
237
+ Bedeutung wurde beim Anlegen in den Alias geschrieben - sie beim
238
+ Tauschen nicht wieder zu lesen, war die Luecke.
239
+
240
+ Zwei Guetegrade, und beide verlangen Eindeutigkeit:
241
+ - Name UND Rolle gleich, genau einmal -> starker Treffer, gilt wie
242
+ ein Pfadtreffer und bekommt eine Herkunftsmarke.
243
+ - nur die Rolle, und nur wenn sie unter dem neuen Geraet genau einmal
244
+ vorkommt -> Vorschlag wie Stufe 3, sichtbar als „vermutet".
245
+ Zwei Punkte mit `value.power` unter einem Knoten heben sich also auf,
246
+ statt dass einer von beiden gewaehlt wird. */
247
+ function ausAliasPunkt(punktId, name, neuKanal) {
248
+ var o = S.objects[punktId];
249
+ var rolle = ((o && o.common) || {}).role || '';
250
+ if (!rolle) { return null; }
251
+ /* Der blosse Punktname, auch wenn der Alias Unterebenen hat. */
252
+ var kurz = String(name).split('.').pop().toLowerCase();
253
+ var mitRolle = [], mitBeidem = [];
254
+ kindZustaende(neuKanal).forEach(function (id) {
255
+ var k = S.objects[id];
256
+ if (!k || !k.common || k.common.role !== rolle) { return; }
257
+ mitRolle.push(id);
258
+ if (id.split('.').pop().toLowerCase() === kurz) { mitBeidem.push(id); }
259
+ });
260
+ if (mitBeidem.length === 1) { return { id: mitBeidem[0], stark: true, rolle: rolle }; }
261
+ if (!mitBeidem.length && mitRolle.length === 1) {
262
+ return { id: mitRolle[0], stark: false, rolle: rolle };
263
+ }
264
+ return null;
265
+ }
266
+
267
+ /* Die gemerkte Vorlage gegen die NEUE Quelle halten.
268
+
269
+ Ein Alias merkt sich, aus welcher Vorlage er entstanden ist - am Kanal
270
+ `native.vorlage`, an jedem Punkt zusaetzlich `native.ausZustand`. Die
271
+ Merkung ist stark: „sie bleibt, bis du sie ausdruecklich umstellst",
272
+ und der Reiter wendet sie auch dann an, wenn die Erkennung sie nicht
273
+ mehr findet (dann steht „erzwungen" daneben).
274
+
275
+ Beim Tausch auf ein BAUGLEICHES Geraet ist das richtig und soll so
276
+ bleiben. Beim Tausch auf ein anders gebautes Geraet ist es falsch, und
277
+ zwar unsichtbar falsch. Gemessen am 13.10.2026 an Ricardos
278
+ `alias.0.Bastelzimmer.PC`, nach dem Tausch von einer Tasmota-Steckdose
279
+ auf eine Matter-Steckdose:
280
+ - Der Kanal trug weiter `vorlage: tasmota-steckdose`, der Reiter
281
+ meldete „GEMERKT / ERZWUNGEN ✕ cmnd.POWER" und „21 ohne Quelle".
282
+ - Schlimmer sind die FORMELN, die aus derselben Vorlage stammen und an
283
+ den Punkten stehen: `JSON.parse(val)?.ENERGY?.Power ?? null` passte
284
+ zum Telemetrie-JSON einer Tasmota. Matter liefert eine blanke Zahl,
285
+ `JSON.parse(37)` ergibt 37, `37?.ENERGY` ist undefined - und der
286
+ Punkt stand auf `null`. Vier Messwerte waren still leer, ohne
287
+ Fehlermeldung.
288
+ - Und an `SET` blieb die Schreibformel `val ? "ON" : "OFF"` stehen,
289
+ die einen String auf einen boolean-Punkt geschrieben haette.
290
+
291
+ Die Formel gehoert zur QUELLE, nicht zum Alias - sie beschreibt, wie
292
+ man aus dieser Bauart einen Wert holt. Wechselt die Bauart, ist sie in
293
+ beide Richtungen falsch (Ricardo: „wenn ich von matter auf mqtt
294
+ wechsle, muessten die ja wieder dazu"). Entschieden wird das nicht
295
+ automatisch: die Werkbank sagt es und bietet es an. */
296
+ export function merkungPruefen(kanal, neuKanal) {
297
+ var k = S.objects[kanal];
298
+ var id = k && k.native && k.native.vorlage;
299
+ if (!id || !neuKanal) { return null; }
300
+ var v = null;
301
+ (S.VORLAGEN || []).forEach(function (x) { if (x.id === id) { v = x; } });
302
+ /* Eine Merkung auf eine Vorlage, die es nicht mehr gibt, ist ebenso
303
+ ueberholt wie eine, die nicht mehr passt. */
304
+ if (!v) { return { id: id, name: id, passt: false, fehler: [tr('swap.tplGone')], ersatz: null }; }
305
+ var t = pruefeVorlage(v, neuKanal);
306
+ if (t.passt) { return { id: id, name: sprachtext(v.name), passt: true }; }
307
+ /* Passt eine ANDERE auf die neue Quelle, gehoert sie genannt - dann
308
+ weiss der Nutzer, dass es dort Formeln und Namen zu holen gibt. Die
309
+ Werkbank wendet sie hier nicht an: das ist „Alias aktualisieren",
310
+ ein eigener Weg mit eigener Vorschau. */
311
+ var ersatz = null;
312
+ (S.VORLAGEN || []).forEach(function (x) {
313
+ if (x.id === id) { return; }
314
+ var p = pruefeVorlage(x, neuKanal);
315
+ if (p.passt && (!ersatz || p.punkte > ersatz.punkte)) {
316
+ ersatz = { name: sprachtext(x.name), punkte: p.punkte };
317
+ }
318
+ });
319
+ return { id: id, name: sprachtext(v.name), passt: false,
320
+ fehler: (t.fehler || []).slice(0, 3), ersatz: ersatz };
321
+ }
322
+
323
+ /* Welche Punkte des Alias tragen eine Merkung oder eine Formel? */
324
+ export function merkungStellen(kanal) {
325
+ var raus = { kanal: false, punkte: [], formeln: [] };
326
+ var k = S.objects[kanal];
327
+ if (k && k.native && (k.native.vorlage || k.native.vorlageVersion)) { raus.kanal = true; }
328
+ aliasPunkte(kanal).forEach(function (id) {
329
+ var o = S.objects[id];
330
+ var n = (o && o.native) || {};
331
+ var a = (o.common && o.common.alias) || {};
332
+ if (n.vorlage || n.ausZustand) { raus.punkte.push(id); }
333
+ if (a.read || a.write) { raus.formeln.push(id); }
334
+ });
335
+ return raus;
336
+ }
337
+
338
+ /* ================== Einheiten ==================
339
+
340
+ Wechselt beim Tausch die Einheit, stimmt danach jeder Wert nicht mehr
341
+ - und zwar unsichtbar. Tasmota liefert `ENERGY.Total` in kWh, Matter
342
+ `CONSUMPTION` in Wh: derselbe Zaehler, Faktor 1000. Der Alias zeigt
343
+ danach 35,4 statt 35380 und niemand merkt es.
344
+
345
+ Automatisch umgerechnet wird trotzdem NICHTS. Am 13.10.2026 hat sich
346
+ gezeigt, warum: Matter schreibt an `CURRENT` die Einheit `mA`,
347
+ liefert aber Ampere (0,926 bei 245 W an 228 V). Eine Automatik, die
348
+ der Beschriftung glaubt, haette daraus 926 A gemacht - sie baut den
349
+ Fehler ein, statt ihn zu finden.
350
+
351
+ Stattdessen stehen BEIDE WERTE nebeneinander, der der neuen Quelle
352
+ und der des Alias. Daran sieht man in einer Sekunde, ob der Faktor
353
+ stimmt - und beim CURRENT-Fall sofort, dass die Beschriftung luegt,
354
+ weil 0,926 und 0,92 schon gleich sind. Das sagt keine Tabelle. */
355
+
356
+ /* Faktor zur Basiseinheit. Nur Paare, die eindeutig sind: Temperatur
357
+ fehlt mit Absicht, °C nach °F ist kein Faktor, sondern eine Gerade,
358
+ und eine halb richtige Umrechnung ist schlimmer als keine. */
359
+ var EINHEITEN = {
360
+ mA: { b: 'A', f: 0.001 }, A: { b: 'A', f: 1 },
361
+ mV: { b: 'V', f: 0.001 }, V: { b: 'V', f: 1 }, kV: { b: 'V', f: 1000 },
362
+ mW: { b: 'W', f: 0.001 }, W: { b: 'W', f: 1 }, kW: { b: 'W', f: 1000 },
363
+ Wh: { b: 'Wh', f: 1 }, kWh: { b: 'Wh', f: 1000 }, MWh: { b: 'Wh', f: 1000000 },
364
+ Pa: { b: 'Pa', f: 1 }, hPa: { b: 'Pa', f: 100 },
365
+ mbar: { b: 'Pa', f: 100 }, bar: { b: 'Pa', f: 100000 },
366
+ ms: { b: 's', f: 0.001 }, s: { b: 's', f: 1 },
367
+ min: { b: 's', f: 60 }, h: { b: 's', f: 3600 },
368
+ mm: { b: 'm', f: 0.001 }, cm: { b: 'm', f: 0.01 },
369
+ m: { b: 'm', f: 1 }, km: { b: 'm', f: 1000 },
370
+ g: { b: 'g', f: 1 }, kg: { b: 'g', f: 1000 },
371
+ l: { b: 'l', f: 1 }, ml: { b: 'l', f: 0.001 }
372
+ };
373
+
374
+ /* Womit die neue Quelle multipliziert werden muesste, damit der Alias in
375
+ SEINER Einheit bleibt. `null`, wenn die beiden nichts miteinander zu
376
+ tun haben oder eine davon unbekannt ist. */
377
+ export function umrechenFaktor(altEinheit, neuEinheit) {
378
+ var a = EINHEITEN[String(altEinheit || '').trim()];
379
+ var n = EINHEITEN[String(neuEinheit || '').trim()];
380
+ if (!a || !n || a.b !== n.b || a.f === n.f) { return null; }
381
+ return n.f / a.f;
382
+ }
383
+
384
+ /* Als Rechenschritt, wie ihn ein Mensch schreiben wuerde: lieber
385
+ `/ 1000` als `* 0.001`. */
386
+ export function faktorText(faktor) {
387
+ if (faktor >= 1) { return '* ' + (Math.round(faktor * 1e6) / 1e6); }
388
+ var kehr = 1 / faktor;
389
+ var rund = Math.round(kehr);
390
+ if (Math.abs(kehr - rund) < 1e-9) { return '/ ' + rund; }
391
+ return '* ' + (Math.round(faktor * 1e9) / 1e9);
392
+ }
393
+
394
+ /* Welche Leseformel am Ende dasteht - nach allem, was an dieser Zeile
395
+ entschieden ist.
396
+
397
+ EINE Stelle fuer die Anzeige und fuer das Schreiben. Vorher rechnete
398
+ die Zeile nur die Formel der Stufe 5 aus und zeigte „keine", waehrend
399
+ die Umrechnung daneben `(val) / 1000` geschrieben haette - Anzeige und
400
+ Ergebnis liefen auseinander (Ricardo, 13.10.2026). Was dasteht, muss
401
+ das sein, was passiert. */
402
+ export function endFormel(zeile) {
403
+ var o = S.objects[zeile.id] || {};
404
+ var alt = ((o.common || {}).alias || {}).read || '';
405
+ /* Stufe 5 bringt die Formel des neuen Geraets mit - auch „keine". */
406
+ var formel = zeile.formelNeu ? (zeile.neuF || '') : alt;
407
+ /* Der Faktor, den der Nutzer gesetzt hat. 1 und leer heissen beide
408
+ „nichts rechnen"; ein ungueltiger Wert ebenso - wer `abc` tippt,
409
+ soll keine kaputte Formel bekommen. */
410
+ var fk = Number(((S.tauschEinheit || {})[zeile.id] || {}).faktor);
411
+ if (zeile.neuR && fk && isFinite(fk) && fk !== 1) {
412
+ formel = '(' + (formel || 'val') + ') ' + faktorText(fk);
413
+ }
414
+ return formel;
415
+ }
416
+
123
417
  /* Die Zuordnungstabelle: je Alias-Punkt, was heute gilt und was danach
124
418
  gelten soll. `stufe` sagt, wie verlaesslich der Vorschlag ist. */
125
419
  export function tauschPlan(kanal, neuKanal) {
126
420
  var altK = alteQuelle(kanal);
421
+ /* Einmal je Plan, nicht je Zeile: `vorschlag` prueft alle Vorlagen. */
422
+ var karte = zielKarte(neuKanal);
127
423
  return aliasPunkte(kanal).map(function (id) {
128
424
  var q = aliasQuellen(S.objects[id]);
129
425
  var zeile = { id: id, name: id.slice(kanal.length + 1),
@@ -159,11 +455,79 @@ export function tauschPlan(kanal, neuKanal) {
159
455
  var trW = suchePfad(zeile.altW);
160
456
  if (trW) { zeile.neuW = trW; }
161
457
  }
458
+ /* 5. wie beim Anklicken im Baum - Vorlage oder Detektor am ZIEL.
459
+
460
+ Gleich nach dem Pfadtreffer, denn das ist die belegteste Auskunft,
461
+ die es ueber das neue Geraet gibt. Nur der gleiche relative Pfad
462
+ (Stufe 1) geht noch davor: beim baugleichen Ersatz ist er exakt
463
+ und braucht keine Erkennung. */
464
+ if (!zeile.neuR) {
465
+ var zk = karte[String(zeile.name).split('.').pop().toLowerCase()];
466
+ if (zk && zk.read) {
467
+ /* Der Detektor ist auch nur eine Heuristik: Liegen am Ziel zwei
468
+ Punkte mit derselben Rolle, waehlt er einen davon - gemessen
469
+ in W26, wo ein zweiter `value.power` daneben lag und
470
+ `ELECTRIC_POWER` auf `ELECTRIC_POWER_B` landete. Heisst ein
471
+ Punkt dort GENAU wie der Platz und traegt dessen Rolle, ist er
472
+ gemeint; das ist keine Vermutung mehr.
473
+
474
+ Nur gegen den Detektor, nicht gegen die Vorlage: die hat ein
475
+ Mensch geschrieben und weiss es besser als jede Namensregel
476
+ (bei Tasmota heisst kein Punkt `ACTUAL`). */
477
+ var genau = (zk.her === 'detektor') ? ausAliasPunkt(id, zeile.name, neuKanal) : null;
478
+ if (genau && genau.stark && genau.id !== zk.read) {
479
+ var gc = (S.objects[genau.id] || {}).common || {};
480
+ zk = { read: genau.id, write: (gc.write ? genau.id : ''),
481
+ f: '', fw: '', her: zk.her, was: zk.was };
482
+ }
483
+ zeile.neuR = zk.read;
484
+ /* Ein Schreibziel bekommt nur, wer schreiben soll. Massgeblich
485
+ ist der ALIAS-Punkt, nicht das Ziel: `LOWBAT` ist ein Melder,
486
+ auch wenn der zigbee-Punkt `battery_low` beschreibbar waere
487
+ (gemessen 13.10.2026). */
488
+ var ac5 = (S.objects[id] || {}).common || {};
489
+ zeile.neuW = ac5.write ? (zk.write || '') : '';
490
+ zeile.stufe = 5;
491
+ zeile.zielHer = zk.her;
492
+ zeile.zielWas = zk.was;
493
+ /* Die Formeln gehoeren zur Quelle: die alte beschreibt das alte
494
+ Geraet und ist nach dem Tausch falsch. Leer heisst „keine" -
495
+ auch das ist eine Auskunft (Matter liefert blanke Zahlen). */
496
+ zeile.neuF = zk.f || '';
497
+ zeile.neuFw = zk.fw || '';
498
+ zeile.formelNeu = true;
499
+ if (zk.write && zk.write === zk.read) { zeile.einPunkt = true; }
500
+ }
501
+ }
162
502
  /* 2. ueber die Vorlage */
163
503
  if (!zeile.neuR) {
164
504
  var v = ausVorlage(id, neuKanal);
165
505
  if (v && v.read) { zeile.neuR = v.read; zeile.stufe = 2; if (v.write) { zeile.neuW = v.write; } }
166
506
  }
507
+ /* 4. ueber den Alias-Punkt - vor dem Raten, weil belegter.
508
+
509
+ Das Schreibziel darf hier auf der Lesequelle liegen: Wo ein
510
+ einziger Punkt liest und schreibt, ist das die richtige Form und
511
+ nicht die Falle aus der Regel weiter unten. Verlangt werden drei
512
+ Dinge - ein starker Treffer, ein Alias-Punkt, der ueberhaupt
513
+ schreibt, und ein Ziel, das sich beschreiben laesst. Ein Melder
514
+ wie `ACTUAL` bekommt so nie ein Schreibziel. */
515
+ if (!zeile.neuR) {
516
+ var ap = ausAliasPunkt(id, zeile.name, neuKanal);
517
+ if (ap && ap.stark) {
518
+ zeile.neuR = ap.id;
519
+ zeile.stufe = 4;
520
+ zeile.ausAlias = ap.rolle;
521
+ var ac = (S.objects[id] || {}).common || {};
522
+ var zc = (S.objects[ap.id] || {}).common || {};
523
+ if (ac.write && zc.write) { zeile.neuW = ap.id; zeile.einPunkt = true; }
524
+ } else if (ap) {
525
+ zeile.neuR = ap.id;
526
+ zeile.stufe = 3;
527
+ zeile.geratenUeber = 'aliasRolle';
528
+ zeile.geratenRolle = ap.rolle;
529
+ }
530
+ }
167
531
  /* 3. geraten */
168
532
  if (!zeile.neuR && zeile.altR) {
169
533
  var g = geraten(zeile.altR, neuKanal);
@@ -198,17 +562,66 @@ export function tauschPlan(kanal, neuKanal) {
198
562
  der Fehler, den W20 abschaffen sollte (Ricardo, 12.09.2026).
199
563
 
200
564
  Danach steht `neuW` leer, und die Sperre aus W20 greift: der
201
- Nutzer entscheidet, ob der Punkt entfernt wird. */
202
- if (!zeile.einfach && zeile.neuW && zeile.neuW === zeile.neuR) { zeile.neuW = ''; }
203
- /* Bei einfacher Quelle gilt die Lesequelle fuer beides. */
204
- if (zeile.einfach && zeile.neuR) { zeile.neuW = zeile.neuR; }
565
+ Nutzer entscheidet, ob der Punkt entfernt wird.
566
+
567
+ Ausgenommen ist nur Stufe 4 mit `einPunkt`: dort ist die Gleichheit
568
+ nicht geraten, sondern gepruefte Absicht - ein Punkt, der liest und
569
+ schreibt. Matter legt die Steckdose so an (`…OnOffPlugInUnit-1.SET`,
570
+ `role: switch`, lesbar und beschreibbar), hm-rpc mit `1.STATE`
571
+ ebenso. Ohne diese Ausnahme meldete die Zeile „kein Schreibziel
572
+ gefunden" an einem Punkt, der genau dafuer da ist. */
573
+ if (!zeile.einfach && !zeile.einPunkt &&
574
+ zeile.neuW && zeile.neuW === zeile.neuR) { zeile.neuW = ''; }
575
+ /* 6. Von Hand gewaehlt - das letzte Wort hat der Mensch.
576
+
577
+ Was keine Stufe findet, liess sich bisher nur entfernen oder
578
+ stehenlassen. Das ist zu wenig: Wer weiss, welcher Punkt am neuen
579
+ Geraet gemeint ist, soll ihn benennen koennen (Ricardo,
580
+ 13.10.2026, an `info_connection`).
581
+
582
+ Die Formel wird dabei NICHT angefasst. Woher sollte die Werkbank
583
+ wissen, wie aus diesem Punkt der Wert wird? Das steht in keiner
584
+ Vorlage und in keiner Rolle - also bleibt stehen, was dasteht, und
585
+ die Zeile sagt, dass es zu pruefen ist. */
586
+ var hand = (S.tauschHand || {})[id];
587
+ if (hand && S.objects[hand]) {
588
+ zeile.neuR = hand;
589
+ zeile.stufe = 6;
590
+ zeile.vonHand = true;
591
+ zeile.formelNeu = false;
592
+ zeile.einPunkt = false;
593
+ var ah = (S.objects[id] || {}).common || {};
594
+ var zh = (S.objects[hand] || {}).common || {};
595
+ /* Schreibziel nur, wenn beide Seiten es hergeben - und dann auf
596
+ denselben Punkt, denn einen zweiten hat niemand benannt. */
597
+ zeile.neuW = (ah.write && zh.write) ? hand : '';
598
+ if (zeile.neuW) { zeile.einPunkt = true; }
599
+ return zeile;
600
+ }
601
+
602
+ /* Bei einfacher Quelle gilt die Lesequelle fuer beides - ausser das
603
+ ZIEL verlangt es anders.
604
+
605
+ Ob ein Alias-Punkt eine oder zwei Quellen hat, ist keine
606
+ Eigenschaft des Alias, sondern des Geraets, an dem er haengt.
607
+ Matter hat einen Punkt, der liest und schreibt (`SET`); Tasmota
608
+ hat zwei, `stat.POWER` meldet und `cmnd.POWER` befiehlt. Beim
609
+ Tausch von dem einen auf das andere muss die Bauform mitwandern.
610
+
611
+ Ohne diese Ausnahme ueberschrieb die Regel genau das, was Stufe 5
612
+ aus der Vorlage geholt hatte: Der Befehl waere auf dem Melder
613
+ gelandet - der Fehler, den W20 und W22 abschaffen (gemessen in
614
+ W30, 13.10.2026). */
615
+ if (zeile.einfach && zeile.neuR && !(zeile.stufe === 5 && zeile.neuW)) {
616
+ zeile.neuW = zeile.neuR;
617
+ }
205
618
  return zeile;
206
619
  });
207
620
  }
208
621
 
209
622
  /* Was der Tausch am Ende schreibt - dieselbe Form wie `baueObjekte`,
210
623
  damit der Trockenlauf-Dialog sie unveraendert anzeigen kann. */
211
- export function tauschObjekte(kanal, neuKanal, plan, weg) {
624
+ export function tauschObjekte(kanal, neuKanal, plan, weg, merkungLoesen) {
212
625
  var raus = [];
213
626
  var k = S.objects[kanal];
214
627
  if (k) {
@@ -216,6 +629,10 @@ export function tauschObjekte(kanal, neuKanal, plan, weg) {
216
629
  delete kn.ts; delete kn.from; delete kn.user; delete kn.acl; delete kn._id;
217
630
  kn.native = kn.native || {};
218
631
  kn.native.quelle = neuKanal;
632
+ /* `geraetetyp` bleibt: der sagt, was das Geraet IST (socket), und das
633
+ aendert sich beim Tausch der Quelle nicht. Weg muss nur, was auf
634
+ die alte Bauart zeigt. */
635
+ if (merkungLoesen) { delete kn.native.vorlage; delete kn.native.vorlageVersion; }
219
636
  raus.push({ id: kanal, neu: false, obj: kn });
220
637
  }
221
638
  plan.forEach(function (z) {
@@ -227,7 +644,11 @@ export function tauschObjekte(kanal, neuKanal, plan, weg) {
227
644
  delete neu.ts; delete neu.from; delete neu.user; delete neu.acl; delete neu._id;
228
645
  var a = neu.common.alias || {};
229
646
  if (z.neuW && z.neuW !== z.neuR) { a.id = { read: z.neuR, write: z.neuW }; }
230
- else if (z.einfach) { a.id = z.neuR; }
647
+ /* `einPunkt`: Lesen und Schreiben fallen am neuen Geraet auf einen
648
+ Punkt zusammen (Stufe 4). Dann ist die einfache Form richtig -
649
+ die Objektform unten wuerde `write` weglassen und aus einem
650
+ Schalter einen Melder machen. */
651
+ else if (z.einfach || z.einPunkt) { a.id = z.neuR; }
231
652
  else {
232
653
  /* Getrennte Quellen ohne Schreibtreffer: die Objektform bleibt.
233
654
 
@@ -240,6 +661,41 @@ export function tauschObjekte(kanal, neuKanal, plan, weg) {
240
661
  nirgendwohin, und das ist die ehrliche Auskunft. */
241
662
  a.id = { read: z.neuR };
242
663
  }
664
+ /* Kam die Zuordnung vom Ziel (Stufe 5), kommt auch die Formel von
665
+ dort - das ist der Kern der Sache: die Formel beschreibt, wie man
666
+ aus DIESER Bauart einen Wert holt.
667
+
668
+ Leer setzen heisst loeschen, und das ist richtig: Matter liefert
669
+ eine blanke Zahl, auf die keine JSON-Formel passt. Umgekehrt
670
+ bringt der Weg zurueck zu Tasmota `JSON.parse(val)?.ENERGY?.Power`
671
+ wieder mit. */
672
+ if (z.formelNeu) {
673
+ if (z.neuFw) { a.write = z.neuFw; } else { delete a.write; }
674
+ }
675
+ /* Die Leseformel kommt aus `endFormel` - derselben Rechnung, die
676
+ die Zeile anzeigt. Zwei Rechenwege waeren zwei Wahrheiten. */
677
+ var endF = endFormel(z);
678
+ if (endF) { a.read = endF; } else { delete a.read; }
679
+ /* Mit der Merkung gehen die Formeln. Sie stammen aus derselben
680
+ Vorlage und beschreiben die alte Bauart; stehen sie weiter da,
681
+ steht der Punkt still auf `null` (siehe `merkungPruefen`). */
682
+ if (merkungLoesen) {
683
+ delete a.read;
684
+ delete a.write;
685
+ neu.native = neu.native || {};
686
+ delete neu.native.vorlage;
687
+ delete neu.native.vorlageVersion;
688
+ delete neu.native.ausZustand;
689
+ }
690
+ /* Die Einheit - was der Nutzer dazu entschieden hat.
691
+
692
+ Hier steht nur noch `einheit`: der Wert bleibt roh, die
693
+ Beschriftung zieht nach. Das Umrechnen steckt in `endFormel`
694
+ (oben), damit Anzeige und Ergebnis dieselbe Rechnung benutzen. */
695
+ var eW = (S.tauschEinheit || {})[z.id] || {};
696
+ if (eW.unit !== undefined) {
697
+ if (eW.unit) { neu.common.unit = eW.unit; } else { delete neu.common.unit; }
698
+ }
243
699
  neu.common.alias = a;
244
700
  raus.push({ id: z.id, neu: false, obj: neu });
245
701
  });
@@ -397,6 +853,10 @@ export function zeigeTausch() {
397
853
  S.tauschNeu = '';
398
854
  S.tauschWeg = {};
399
855
  S.tauschPlan = null;
856
+ S.tauschMerkung = null;
857
+ S.tauschWerte = null;
858
+ S.tauschHand = {};
859
+ S.tauschEinheit = {};
400
860
  /* Auch die Suche faengt neu an. Sie blieb sonst stehen, und beim
401
861
  naechsten Oeffnen zeigte der Baum „Kein Geraet gefunden" — im
402
862
  schlimmsten Fall genau dann, wenn im Feld noch das eben getauschte,
@@ -465,6 +925,12 @@ function zeichneTausch() {
465
925
  S.tauschNeu = id;
466
926
  S.tauschPlan = null;
467
927
  S.tauschWeg = {};
928
+ S.tauschWerte = null;
929
+ S.tauschHand = {};
930
+ S.tauschEinheit = {};
931
+ /* Andere Quelle, andere Frage: ob die gemerkte Vorlage passt, haengt
932
+ am Ziel. Die Vorgabe wird in `zeichneTausch` neu gesetzt. */
933
+ S.tauschMerkung = null;
468
934
  zeichneTausch();
469
935
  };
470
936
  zeichneAuswahlbaum(baumHost, S.tauschFilter, gewaehlt);
@@ -493,6 +959,31 @@ function zeichneTausch() {
493
959
  return;
494
960
  }
495
961
 
962
+ /* Erst die Werte des neuen Geraets, dann die Zuordnung.
963
+
964
+ Stufe 5 fragt dieselbe Erkennung wie der Baum, und die schaut bei
965
+ einem Tasmota in den Rohwert von `tele.SENSOR` hinein: ohne Werte
966
+ faellt die Vorlage durch, und der Tausch zu einem MQTT-Geraet faende
967
+ wieder nichts. Einmal je Ziel - `S.tauschWerte` merkt, welches
968
+ schon geholt ist. */
969
+ if (S.tauschWerte !== S.tauschNeu) {
970
+ var zielPunkte = kindZustaende(S.tauschNeu);
971
+ var fehlt = zielPunkte.some(function (id) { return S.werte[id] === undefined; });
972
+ if (fehlt) {
973
+ S.tauschWerte = S.tauschNeu;
974
+ holeWerte(S.tauschNeu, zielPunkte, function () {
975
+ /* Noch im selben Dialog und am selben Ziel? Sonst ist das
976
+ Ergebnis von gestern. */
977
+ if (S.tauschNeu !== S.tauschWerte) { return; }
978
+ S.tauschPlan = null;
979
+ zeichneTausch();
980
+ });
981
+ body.appendChild(el('div', 'aside w', tr('swap.loadingValues')));
982
+ return;
983
+ }
984
+ S.tauschWerte = S.tauschNeu;
985
+ }
986
+
496
987
  /* Die Zuordnung, Zeile fuer Zeile. */
497
988
  if (!S.tauschPlan) { S.tauschPlan = tauschPlan(kanal, S.tauschNeu); }
498
989
  var plan = S.tauschPlan;
@@ -575,17 +1066,48 @@ function zeichneTausch() {
575
1066
  neue Quelle zeigte). */
576
1067
  var gleich = z.neuR && z.neuR === z.altR
577
1068
  && (z.einfach || (z.neuW || '') === (z.altW || ''));
578
- var nm = el('span', 'sn', z.name);
1069
+ /* Der Name und seine Marke untereinander, nicht nebeneinander: Die
1070
+ Namensspalte ist 120 Pixel breit, und „ERKANNT" hinter einem
1071
+ langen Namen wie `ELECTRIC_POWER` lief in die Pfade der Nachbarn
1072
+ hinein (Ricardo, 13.10.2026, am Bild zu sehen). Eine Zeile ist
1073
+ hier ohnehin immer mindestens zweizeilig. */
1074
+ var nm = el('span', 'sn zuname', z.name);
579
1075
  if (gleich) { nm.appendChild(el('span', 'rat', tr('swap.unchanged'))); }
580
1076
  else if (z.stufe === 3) {
581
1077
  /* Die Marke sagt jetzt auch, WORUEBER geraten wurde - ueber den
582
1078
  blossen Punktnamen oder ueber die Rolle. Das sind zwei sehr
583
1079
  verschiedene Vermutungen, und sie pruefen sich verschieden. */
584
1080
  var rm = el('span', 'rat', tr('guess.mark'));
585
- rm.title = z.geratenUeber === 'rolle'
586
- ? tr('swap.guessedByRole', z.geratenRolle || '?')
587
- : tr('swap.guessedByName', (z.altR || '').split('.').pop());
1081
+ rm.title = z.geratenUeber === 'aliasRolle'
1082
+ ? tr('swap.guessedByAliasRole', z.geratenRolle || '?')
1083
+ : z.geratenUeber === 'rolle'
1084
+ ? tr('swap.guessedByRole', z.geratenRolle || '?')
1085
+ : tr('swap.guessedByName', (z.altR || '').split('.').pop());
588
1086
  nm.appendChild(rm);
1087
+ } else if (z.stufe === 6) {
1088
+ var hm2 = el('span', 'rat', tr('swap.byHandMark'));
1089
+ hm2.title = tr('swap.byHandHint');
1090
+ nm.appendChild(hm2);
1091
+ } else if (z.stufe === 5) {
1092
+ /* Woher die Zuordnung kommt - aus der Vorlage des neuen Geraets
1093
+ oder aus seinen Rollen. Das ist dieselbe Auskunft, die der
1094
+ Reiter beim Anklicken gibt, und sie gehoert hierher: wer
1095
+ zurueck auf einen Tasmota tauscht, soll sehen, dass die
1096
+ Tasmota-Vorlage dahintersteht. */
1097
+ var zh = el('span', 'herk', z.zielHer === 'vorlage'
1098
+ ? tr('swap.fromTplMark') : tr('swap.fromDetectMark'));
1099
+ zh.title = z.zielHer === 'vorlage'
1100
+ ? tr('swap.fromTpl', z.zielWas || '?')
1101
+ : tr('swap.fromDetect', z.zielWas || '?');
1102
+ nm.appendChild(zh);
1103
+ } else if (z.stufe === 4) {
1104
+ /* Kein „vermutet", aber auch kein stiller Treffer: dass die
1105
+ Zuordnung vom Alias-Punkt kommt und nicht vom Pfad, erklaert,
1106
+ warum ein Tausch zwischen zwei ganz verschiedenen Adaptern
1107
+ ueberhaupt aufgeht - und woran er haengt, wenn er danebenliegt. */
1108
+ var hm = el('span', 'herk', tr('swap.fromAliasMark'));
1109
+ hm.title = tr('swap.fromAlias', z.name.split('.').pop(), z.ausAlias || '?');
1110
+ nm.appendChild(hm);
589
1111
  }
590
1112
  r.appendChild(nm);
591
1113
  var rechts = el('span');
@@ -620,6 +1142,211 @@ function zeichneTausch() {
620
1142
  lb.appendChild(el('span', 'hint', tr('swap.removeIt')));
621
1143
  return lb;
622
1144
  };
1145
+ /* Einen Punkt des neuen Geraets von Hand benennen.
1146
+
1147
+ Die Liste zeigt die relativen Namen - `1.STATE`, `tele.SENSOR` -,
1148
+ weil der volle Pfad an jeder Zeile gleich anfaengt und nur Platz
1149
+ kostet. Sortiert wie im Baum. */
1150
+ var handAuswahl = function (zeile) {
1151
+ var huelle = el('div');
1152
+ huelle.style.marginTop = '4px';
1153
+ var vor = S.tauschNeu + '.';
1154
+ var sel = document.createElement('select');
1155
+ sel.className = 'tx';
1156
+ sel.style.maxWidth = '100%';
1157
+ var leer = document.createElement('option');
1158
+ leer.value = '';
1159
+ leer.textContent = tr('swap.pickByHand');
1160
+ sel.appendChild(leer);
1161
+ kindZustaende(S.tauschNeu).forEach(function (pid) {
1162
+ var o = document.createElement('option');
1163
+ o.value = pid;
1164
+ var c = (S.objects[pid] || {}).common || {};
1165
+ o.textContent = pid.slice(vor.length) + (c.role ? ' · ' + c.role : '') +
1166
+ (c.unit ? ' · ' + c.unit : '');
1167
+ if ((S.tauschHand || {})[zeile.id] === pid) { o.selected = true; }
1168
+ sel.appendChild(o);
1169
+ });
1170
+ sel.addEventListener('change', function () {
1171
+ S.tauschHand = S.tauschHand || {};
1172
+ if (sel.value) { S.tauschHand[zeile.id] = sel.value; }
1173
+ else { delete S.tauschHand[zeile.id]; }
1174
+ S.tauschPlan = null;
1175
+ zeichneTausch();
1176
+ });
1177
+ huelle.appendChild(sel);
1178
+ return huelle;
1179
+ };
1180
+
1181
+ /* Wechselt die Einheit, stehen beide Werte nebeneinander und man
1182
+ waehlt, was damit geschehen soll. Vorbelegt ist „nichts aendern" -
1183
+ empfohlen wird, aber nicht entschieden. */
1184
+ /* Ein Kuerzel neben dem Feld: „so waere es, wenn du der Beschriftung
1185
+ glaubst". Ein Klick setzt es ein - entschieden ist damit nichts,
1186
+ und genau deshalb steht es neben dem Feld und nicht darin. */
1187
+ var vorschlagKnopf = function (feld, wert, danach, beschriftung) {
1188
+ var b = el('button', 'btn klein', beschriftung || wert);
1189
+ b.title = tr('swap.suggestionHint');
1190
+ b.addEventListener('click', function (ev) {
1191
+ ev.preventDefault();
1192
+ feld.value = wert;
1193
+ danach();
1194
+ });
1195
+ return b;
1196
+ };
1197
+
1198
+ /* Einheit und Faktor - beide frei, beide auf dem Ist-Zustand.
1199
+
1200
+ Die erste Fassung bot drei feste Knoepfe: nichts, Einheit des
1201
+ Ziels uebernehmen, mit dem bekannten Faktor rechnen. Das greift
1202
+ zu kurz, sobald das Ziel GAR KEINE Einheit traegt - und das ist
1203
+ kein Randfall, sondern der Normalfall bei einem JSON-Sammelpunkt.
1204
+ `tele.SENSOR` hat keine Einheit; die steckt im Feld, nicht im
1205
+ Objekt: `ENERGY.Total` IST kWh, `CONSUMPTION` bei Matter IST Wh.
1206
+ Dass ioBroker das nicht weiss, macht den Wechsel nicht kleiner -
1207
+ er wird dadurch unsichtbar (Ricardo, 13.10.2026).
1208
+
1209
+ Also zwei Felder statt Knoepfe. Wer nichts anfasst, aendert
1210
+ nichts; was die Werkbank weiss, steht als Vorschlag daneben. Das
1211
+ deckt auch den Fall ab, in dem die Beschriftung LUEGT: Matter
1212
+ schreibt an `CURRENT` die Einheit mA und liefert Ampere, der
1213
+ angebotene Faktor waere also falsch - und die beiden Werte
1214
+ darueber zeigen es. */
1215
+ var einheitenWahl = function (zeile) {
1216
+ if (!zeile.neuR) { return document.createDocumentFragment(); }
1217
+ var altO = S.objects[zeile.id] || {}, neuO = S.objects[zeile.neuR] || {};
1218
+ var altE = ((altO.common || {}).unit || '').trim();
1219
+ var neuE = ((neuO.common || {}).unit || '').trim();
1220
+
1221
+ /* Verglichen werden die beiden QUELLEN, nicht der Alias mit dem
1222
+ Ziel.
1223
+
1224
+ Vorher stand hier `altE === neuE` mit der Einheit des ALIAS auf
1225
+ der einen Seite - und damit erschien der Kasten auch beim Tausch
1226
+ zwischen zwei gleichartigen Geraeten: Ein Alias in kWh, der aus
1227
+ `…PC_Bastelzimmer.tele.SENSOR` liest, sollte auf
1228
+ `…PC_Jonas.tele.SENSOR` umziehen. Beide JSON-Punkte tragen keine
1229
+ Einheit, es aendert sich also nichts - trotzdem stand da
1230
+ „am Alias kWh · neue Quelle: keine" samt Feldern und dem Hinweis
1231
+ auf die Aufzeichnung (Ricardo, 14.10.2026).
1232
+
1233
+ Mit dem Vergleich der Quellen verschwindet das von selbst, ohne
1234
+ Sonderregel fuer „identisches Geraet": Liefert die neue Quelle
1235
+ dasselbe wie die alte, gibt es nichts zu entscheiden. */
1236
+ var quellE = '';
1237
+ var altQ = zeile.altR && S.objects[zeile.altR];
1238
+ if (altQ) { quellE = ((altQ.common || {}).unit || '').trim(); }
1239
+ else { quellE = altE; } /* Quelle weg (W13): dann der Alias */
1240
+ if (quellE === neuE) { return document.createDocumentFragment(); }
1241
+
1242
+ S.tauschEinheit = S.tauschEinheit || {};
1243
+ var stand = S.tauschEinheit[zeile.id] || {};
1244
+
1245
+ var huelle = el('div');
1246
+ huelle.style.marginTop = '5px';
1247
+ huelle.style.paddingLeft = '8px';
1248
+ huelle.style.borderLeft = '2px solid var(--warn)';
1249
+ huelle.appendChild(el('div', 'hint',
1250
+ tr('swap.unitChanged', altE || tr('swap.unitNone'), neuE || tr('swap.unitNone'))));
1251
+
1252
+ /* Die beiden Werte - der eigentliche Punkt. Was der Alias heute
1253
+ zeigt und was nach dem Tausch dastuende. Beim Rohwert eines
1254
+ JSON-Punktes waere die ganze Nachricht unlesbar, deshalb wird
1255
+ er durch die neue Formel gereicht. */
1256
+ var wertVon = function (id) {
1257
+ var w = S.werte[id];
1258
+ if (!w || w.val === undefined || w.val === null) { return null; }
1259
+ return w.val;
1260
+ };
1261
+ var wRoh = wertVon(zeile.neuR), wAlt = wertVon(zeile.id);
1262
+ if (wAlt !== null) {
1263
+ huelle.appendChild(el('div', 'hint',
1264
+ tr('swap.unitValOld', String(wAlt), altE || '')));
1265
+ }
1266
+
1267
+ var ergebnisZeile = el('div', 'sn');
1268
+ ergebnisZeile.style.marginTop = '3px';
1269
+
1270
+ var feldZeile = function (beschriftung, feld, knopf) {
1271
+ var zz = el('div');
1272
+ zz.style.display = 'flex';
1273
+ zz.style.gap = '6px';
1274
+ zz.style.alignItems = 'center';
1275
+ zz.style.marginTop = '4px';
1276
+ zz.appendChild(el('span', 'rx', beschriftung));
1277
+ zz.appendChild(feld);
1278
+ if (knopf) { zz.appendChild(knopf); }
1279
+ return zz;
1280
+ };
1281
+
1282
+ var eFeld = document.createElement('input');
1283
+ eFeld.className = 'tx';
1284
+ eFeld.type = 'text';
1285
+ eFeld.style.width = '90px';
1286
+ eFeld.value = (stand.unit !== undefined) ? stand.unit : altE;
1287
+ var fFeld = document.createElement('input');
1288
+ fFeld.className = 'tx';
1289
+ fFeld.type = 'text';
1290
+ fFeld.style.width = '90px';
1291
+ fFeld.value = (stand.faktor !== undefined) ? String(stand.faktor) : '1';
1292
+
1293
+ /* Nur rechnen und die eine Zeile neu schreiben - NICHT den Dialog
1294
+ neu zeichnen. Sonst springt der Fokus bei jedem Anschlag aus dem
1295
+ Feld, und tippen wird unmoeglich. */
1296
+ var frisch = function () {
1297
+ var e = eFeld.value.trim();
1298
+ var f = fFeld.value.trim();
1299
+ S.tauschEinheit[zeile.id] = {
1300
+ unit: (e === altE) ? undefined : e,
1301
+ faktor: (f === '' || f === '1') ? undefined : f
1302
+ };
1303
+ S.tauschPlan = null;
1304
+ ergebnisZeile.textContent = '';
1305
+ var neuPlan = tauschPlan(kanal, S.tauschNeu);
1306
+ S.tauschPlan = neuPlan;
1307
+ var zz = neuPlan.find(function (x) { return x.id === zeile.id; }) || zeile;
1308
+ var fz = endFormel(zz);
1309
+ var txtE = (e !== '') ? e : '';
1310
+ if (wRoh === null) {
1311
+ ergebnisZeile.textContent = tr('swap.unitResult', '—', txtE);
1312
+ return;
1313
+ }
1314
+ var r = auswerten(fz, wRoh);
1315
+ ergebnisZeile.textContent = tr('swap.unitResult',
1316
+ r.ok ? String(r.val) : (r.txt || '—'), txtE);
1317
+ formelNach(fz);
1318
+ };
1319
+ /* Die Formelzeile derselben Reihe nachziehen. Sie steht ausserhalb
1320
+ dieses Kastens, gehoert aber zur selben Entscheidung - bliebe
1321
+ sie stehen, zeigte sie eine Formel, die so nicht geschrieben
1322
+ wird. Genau der Fehler, den Ricardo vorhin an der Pfeilzeile
1323
+ gefunden hat, nur eine Ebene weiter. */
1324
+ var formelNach = function (fz) {
1325
+ var reihe = huelle.closest ? huelle.closest('.slot') : null;
1326
+ var ziel = reihe && reihe.querySelector('.formel-neu span:last-child');
1327
+ if (ziel) { ziel.textContent = fz || tr('swap.formulaNone'); }
1328
+ };
1329
+ eFeld.addEventListener('input', frisch);
1330
+ fFeld.addEventListener('input', frisch);
1331
+
1332
+ huelle.appendChild(feldZeile(tr('swap.unitField'), eFeld,
1333
+ neuE ? vorschlagKnopf(eFeld, neuE, frisch) : null));
1334
+ var faktor = umrechenFaktor(altE, neuE);
1335
+ huelle.appendChild(feldZeile(tr('swap.factorField'), fFeld,
1336
+ faktor ? vorschlagKnopf(fFeld, String(faktor), frisch, faktorText(faktor)) : null));
1337
+ if (!faktor && altE && neuE) {
1338
+ huelle.appendChild(el('div', 'hint', tr('swap.unitNoFactor')));
1339
+ }
1340
+ huelle.appendChild(ergebnisZeile);
1341
+ /* Haengt eine Aufzeichnung dran, gehoert der Hinweis dazu - die
1342
+ Historie liegt in der alten Einheit. */
1343
+ if (altO.common && altO.common.custom && Object.keys(altO.common.custom).length) {
1344
+ huelle.appendChild(el('div', 'hint', tr('swap.unitHistWarn')));
1345
+ }
1346
+ frisch();
1347
+ return huelle;
1348
+ };
1349
+
623
1350
  /* Beschriftet, sonst raet man, welche der beiden Zeilen die neue ist. */
624
1351
  var paarZeile = function (beschriftung, text, klasse, farbe) {
625
1352
  var zz = el('div');
@@ -638,9 +1365,45 @@ function zeichneTausch() {
638
1365
  rechts.appendChild(paarZeile(tr('write.diffNew'),
639
1366
  z.neuR + (z.neuW && z.neuW !== z.neuR ? ' → ' + z.neuW : ''),
640
1367
  'sn', gleich ? 'var(--ink-3)' : 'var(--ok)'));
1368
+ /* Von Hand benannt heisst: die Werkbank weiss nichts ueber diesen
1369
+ Punkt. Formel, Einheit und Wertebereich bleiben, wie sie am
1370
+ Alias stehen - und passen vielleicht nicht mehr. */
1371
+ if (z.vonHand) {
1372
+ var hw = el('div', 'hint');
1373
+ hw.style.color = 'var(--warn)';
1374
+ hw.textContent = tr('swap.byHandCheck');
1375
+ rechts.appendChild(hw);
1376
+ rechts.appendChild(handAuswahl(z));
1377
+ }
641
1378
  /* Lesequelle gefunden, Schreibquelle nicht — das gehoert gesagt.
642
1379
  Sonst steht nur die neue Lesequelle in Gruen da, und dass der
643
1380
  Punkt danach nirgendwohin schreibt, merkt man erst am Geraet. */
1381
+ rechts.appendChild(einheitenWahl(z));
1382
+ /* Wechselt die Formel mit, gehoert das in die Zeile - sie
1383
+ entscheidet mit darueber, was am Alias ankommt, und man sieht es
1384
+ dem Pfad nicht an. */
1385
+ /* Was am Ende dasteht - einschliesslich einer gewaehlten
1386
+ Umrechnung. `endFormel` rechnet dasselbe wie `tauschObjekte`. */
1387
+ var altF = (S.objects[z.id] && S.objects[z.id].common &&
1388
+ S.objects[z.id].common.alias || {}).read || '';
1389
+ var endF = endFormel(z);
1390
+ if (altF !== endF) {
1391
+ /* Beschriftet wie die Pfade darueber. Vorher stand dort nur ein
1392
+ Pfeil zwischen zwei Formeln, und man musste raten, welche die
1393
+ neue ist - Ricardo hat zweimal nachgefragt (13.10.2026). */
1394
+ var fb = el('div');
1395
+ fb.style.marginTop = '3px';
1396
+ fb.appendChild(el('div', 'rx', tr('swap.formula')));
1397
+ fb.appendChild(paarZeile(tr('write.diffOld'),
1398
+ altF || tr('swap.formulaNone'), 'zq'));
1399
+ var fneu = paarZeile(tr('write.diffNew'),
1400
+ endF || tr('swap.formulaNone'), 'sn');
1401
+ /* Markiert, damit die Einheitenfelder sie beim Tippen
1402
+ mitschreiben koennen - sie zeichnen den Dialog nicht neu. */
1403
+ fneu.classList.add('formel-neu');
1404
+ fb.appendChild(fneu);
1405
+ rechts.appendChild(fb);
1406
+ }
644
1407
  if (z.altW && z.altW !== z.altR && !z.neuW) {
645
1408
  var kw = el('div');
646
1409
  kw.style.color = 'var(--bad)';
@@ -656,12 +1419,56 @@ function zeichneTausch() {
656
1419
  /* Was nichts findet, laesst sich mit entfernen - sonst bliebe ein
657
1420
  Punkt zurueck, der ins Leere zeigt (Ricardos Wahl, 25.08.2026). */
658
1421
  rechts.appendChild(entfernenKaestchen());
1422
+ /* Oder von Hand benennen. Wer sein Geraet kennt, weiss oft genau,
1423
+ welcher Punkt gemeint ist - die Werkbank kann es nur nicht
1424
+ ableiten (Ricardo, 13.10.2026). */
1425
+ rechts.appendChild(handAuswahl(z));
659
1426
  }
660
1427
  r.appendChild(rechts);
661
1428
  k.appendChild(r);
662
1429
  });
663
1430
  body.appendChild(k);
664
1431
 
1432
+ /* --- Die gemerkte Vorlage, gegen die neue Quelle gehalten ---
1433
+
1434
+ Nur wenn sie NICHT mehr passt. Beim baugleichen Ersatz steht hier
1435
+ nichts, und das ist der haeufigere Fall. */
1436
+ var merk = merkungPruefen(kanal, S.tauschNeu);
1437
+ if (merk && !merk.passt) {
1438
+ var stellen = merkungStellen(kanal);
1439
+ if (S.tauschMerkung === null || S.tauschMerkung === undefined) { S.tauschMerkung = true; }
1440
+ var mk = el('div', 'aside w');
1441
+ mk.style.borderLeftColor = 'var(--warn)';
1442
+ mk.appendChild(el('b', null, tr('swap.tplStale', merk.name)));
1443
+ (merk.fehler || []).forEach(function (f) { mk.appendChild(el('div', 'hint', f)); });
1444
+ /* Die Formeln sind der eigentliche Grund, warum das nicht still
1445
+ stehenbleiben darf - sie machen aus Messwerten ein `null`, ohne
1446
+ dass irgendwo ein Fehler steht. */
1447
+ if (stellen.formeln.length) {
1448
+ mk.appendChild(el('div', 'hint', tr('swap.tplFormulas', stellen.formeln.length)));
1449
+ }
1450
+ if (merk.ersatz) {
1451
+ mk.appendChild(el('div', 'hint', tr('swap.tplOther', merk.ersatz.name)));
1452
+ }
1453
+ var ml = document.createElement('label');
1454
+ ml.style.display = 'flex';
1455
+ ml.style.gap = '6px';
1456
+ ml.style.alignItems = 'center';
1457
+ ml.style.marginTop = '7px';
1458
+ ml.style.cursor = 'pointer';
1459
+ var mcb = document.createElement('input');
1460
+ mcb.type = 'checkbox';
1461
+ mcb.checked = !!S.tauschMerkung;
1462
+ mcb.addEventListener('change', function () { S.tauschMerkung = mcb.checked; zeichneTausch(); });
1463
+ ml.appendChild(mcb);
1464
+ ml.appendChild(el('span', null, tr('swap.tplDrop',
1465
+ stellen.punkte.length + (stellen.kanal ? 1 : 0))));
1466
+ mk.appendChild(ml);
1467
+ body.appendChild(mk);
1468
+ } else {
1469
+ S.tauschMerkung = false;
1470
+ }
1471
+
665
1472
  /* Wo die neue Quelle anders tickt als die alte. Die Formel des Alias
666
1473
  bleibt beim Tausch stehen - rechnet die alte Quelle 0..100 und die
667
1474
  neue 0..255, stimmt danach jeder Wert nicht mehr. Das sieht man dem
@@ -744,7 +1551,7 @@ export function tauscheAus() {
744
1551
  b.disabled = true;
745
1552
  b.textContent = tr('write.writing');
746
1553
 
747
- var arbeit = tauschObjekte(kanal, neu, S.tauschPlan, S.tauschWeg);
1554
+ var arbeit = tauschObjekte(kanal, neu, S.tauschPlan, S.tauschWeg, !!S.tauschMerkung);
748
1555
  var loeschen = Object.keys(S.tauschWeg).filter(function (id) { return S.tauschWeg[id]; });
749
1556
  var fehler = [];
750
1557