iobroker.alias-workbench 0.9.9 → 0.9.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Ricardo Hipp
3
+ Copyright (c) 2026 Ricardo Hipp <ricardo.hipp@googlemail.com>
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -564,6 +564,21 @@ imported, and that both language files carry the same keys.
564
564
 
565
565
  ## Changelog
566
566
 
567
+ ### 0.9.10 (2026-09-13)
568
+ * Four things from the full test run. **A channel identifier** was not sanitised in the multi-output path: from the channel name “Licht, Bar” the js-controller quietly made `Licht_ Bar`, while the workbench displayed “Licht, Bar” and wrote it into the enumerations — an identifier that never existed, and after deleting the alias it stayed there as a leftover. `ausgangName` now sanitises itself, and where two channels produce the same name the second gets a `_2`. **The caption** differed between the two views: where the point's name equals the row name, “Edit alias” showed it and “Create alias” an empty field. **The row list** now counts like the tree — `POWER2` before `POWER10`. And **an enumeration the workbench created itself** now also disappears when it becomes empty through a change of function, not only when the alias is deleted
569
+ * Tidying: "turn an alias object into a draft row" was written out five times — each with the same write-source rule and slightly different surroundings. Four bugs in this version came from exactly that: the lost write source on the roller shutter, a switching point that became a read-only one, the missing caption on `alias.0.Solar.Netz`, and a deviation the workbench had produced itself. The reading now lives in one place; what the four callers do with it — compare, create, overwrite — stays with them. Behaviour is unchanged
570
+ * Three small things spotted during the run. **RSSI** was pre-ticked in the five Tasmota and measuring-point templates but not in the eleven Homematic ones — the same device property, two answers depending on the vendor; it is now unticked everywhere, like the rest of the diagnostics. **The `MAP_URL` slot** in the vacuum pattern got no role because its expression starts without `^` and the derivation demanded both anchors; a missing start anchor no longer counts against a slot. **And the "guessed" tag** in the swap dialog looked like text stuck to the point name — its styling hung on three parent levels that do not exist there. It is now a pill like in the state list and says in its tooltip whether the guess came from the point name or from the role
571
+ * The save button in the template dialog now also checks when pressed, not only when drawn. It is re-evaluated when a field is left — typing an open bracket into the name hint and clicking "Save template" without leaving the field left you with a blue button and saved a template with a broken expression. The five lock reasons now live in one place that drawing and saving share
572
+ * Three places now say why they cannot. **The save button in the template dialog** was greyed out and silent — five reasons can lock it and none of them was shown; every applicable one now appears on the button and as a box in the dialog. **The error dialog when deleting** only gave a count ("1 operations failed") and not which id is stuck; writing has had that list all along. **And the reason a send point could not be created** hung on a button that the same callback threw away 120 ms later — it now sits at the top of the dry run and survives the rebuild
573
+ * If a point's type does not match its pattern slot, all three places now say so — not just the expanded row. If the slot was **required**, the pattern stops applying altogether, and the whole list showed up tinted: on the Bastelzimmer_Licht, switching SET from `boolean` to `mixed` was enough for eight of eight rows to count as "no slot", while the pattern field reported "✕ SET missing" — SET was right there. It now reads "✕ SET: type mixed instead of boolean", and above the list: "The light pattern does not apply right now: SET requires boolean, but mixed is set. That is why no row has a slot." Only the row that causes it is tinted now — the whole list used to light up, which showed the consequence instead of the cause.
574
+ * A late answer to `system.config` now really does carry the expert mode over. The tab gives up after 2.5 seconds and continues with the browser language; if the answer arrives later, the language stays as it is (switching after the labels are drawn would be worse), the expert mode is adopted and the tree is redrawn once. That had long been built — but above it sat a guard from the time before, which bailed out in exactly this case. Without the mode, every node under `system.*` and `enum.*` was missing, with nothing saying why
575
+ * The swap dialog notices when one level up fits better. The picker lets you choose any node with data points underneath — it has to, because on a flat adapter the channel is the device. With MQTT, though, `stat`, `tele` and `cmnd` are topic folders: landing there finds fewer points, and the lock would ask you to remove some. The workbench now also computes the parent level and says so — "One level up (…) finds a target for 7 of 7 points, here only 5" — with a button that switches the selection. Nothing is forbidden
576
+ * The device list in the swap dialog keeps its position. Scrolling down a long list and picking a device used to jump back to the top, with the freshly picked entry out of sight — picking rebuilds the dialog, and the tree is a different element afterwards. If the selected row still ends up outside, it is brought into view. A new search and a freshly opened dialog still start at the top
577
+ * When swapping the source, a write target no longer counts if it coincides with the read source. The search tries the bare point name as its last form, so `cmnd.POWER` found a status point that also ends in `POWER` — the alias would then have written to its own read source. Guessing already caught this, matching by path did not; it showed when the new source was picked as a sub-node instead of the device
578
+ * When swapping the source, a point that cannot come along now stops the swap instead of letting it fail halfway. A point with separate sources — reads from `stat.POWER`, writes to `cmnd.POWER` — needs both at the new source. If the write target is missing, ioBroker has **no** form that means "reads, writes nowhere": a plain id means "write to the status point", `{read: X}` without `write` is rejected by the js-controller, and `common.write: false` does not protect the source (measured: writing to such an alias overwrote the read source). So the user decides — the checkbox "remove this point when swapping" now appears in this case as well, and "Swap now" stays disabled until someone has decided; the reason shows on the button and as a line in the dialog. Before, the swap started, failed at the controller and left the alias half on the old and half on the new source
579
+ * If a write fails during the swap, **nothing** is deleted. The deletions used to run anyway, so an alias that was already half migrated lost points on top of it. Moving an alias has followed that rule for a long time
580
+ * "All data points of the device" no longer moves the storage location. For a device with several outputs the workbench creates a folder with one channel per output. The button rebuilds the draft and passed on only the **finished target**; `setzeZiel` reads its last segment as the device name — with several outputs that segment is the output, and it ended up in the folder as well. Every press pushed the location one level deeper, and that is where it was written, too: three clicks produced four folders instead of one (`…Garten-Ventilinsel.POWER2.POWER2.POWER2.POWER2`). The button now passes folder and name separately, the way the output selector next to it has always done. A single-output device was never affected
581
+
567
582
  ### 0.9.9
568
583
  * The build source moved from `src/` to `src-admin/`. ioBroker reserves that name for frontend sources that are bundled into `admin/` and never reach the user, and the adapter checker skips such directories when it looks for missing dependencies. `@iobroker/type-detector` therefore stays in `devDependencies`, which is where it belongs: esbuild bakes it into `admin/detector.js` at build time and nothing loads it at runtime. Users receive the finished 71 KB file, not the 548 KB package (W5042)
569
584
  * The settings page now takes its translations from `admin/i18n/` — the dictionary ioBroker reads itself (`"i18n": true`) and that Weblate can work with. The eleven languages it already carried moved there unchanged; not a word was retranslated. The tab keeps its own dictionary, now at `admin/sprachen/`: 588 keys in German and English, with an unknown language falling back to English. Two dictionaries, two jobs — the name `i18n` is the one the adapter checker inspects, and it expects all eleven languages there (W5603, W5614, W5015)
@@ -553,9 +553,14 @@
553
553
  .erow .nm3 .geae { font-family:var(--sans); font-size:8px; font-weight:700; letter-spacing:.05em;
554
554
  text-transform:uppercase; background:var(--warn-soft); color:var(--warn);
555
555
  padding:0 6px; border-radius:999px; margin-left:6px; }
556
- .erow .nm3 .rat { font-family:var(--sans); font-size:8px; font-weight:700; letter-spacing:.05em;
557
- text-transform:uppercase; background:var(--accent-soft); color:var(--accent);
558
- padding:0 6px; border-radius:999px; margin-left:6px; }
556
+ /* Die Marke gilt ueberall gleich, nicht nur in der Zustandsliste.
557
+ Mit den drei Ebenen davor griff sie im Tausch-Dialog nicht: dort
558
+ haengt das span unter .sn, und "vermutet" stand als blosser Text
559
+ am Punktnamen - ohne Abstand, ohne Farbe, in 12 statt 8 px
560
+ (Ricardo, 12.09.2026, an alias.0.Garten.FK_Badezimmer). */
561
+ .rat { font-family:var(--sans); font-size:8px; font-weight:700; letter-spacing:.05em;
562
+ text-transform:uppercase; background:var(--accent-soft); color:var(--accent);
563
+ padding:0 6px; border-radius:999px; margin-left:6px; white-space:nowrap; }
559
564
  .erow .nm3 .ratknopf { font-family:var(--sans); font-size:11px; cursor:pointer; margin-left:5px;
560
565
  color:var(--ink-3); padding:0 4px; border-radius:var(--r-sm); }
561
566
  .erow .nm3 .ratknopf:hover { background:var(--surface-3); color:var(--ink); }
@@ -9,7 +9,7 @@ import { txt, tr } from './sprache.js';
9
9
  import './enums.js';
10
10
  import { zusatzName, setzeModus, zeichneBaum, merkeKlappstand } from './baum.js';
11
11
  import { kindZustaende, aliasQuellen, holeWerte,
12
- mitVorspann, schreibQuelle
12
+ mitVorspann, schreibQuelle, zeileAusAlias
13
13
  } from './werte.js';
14
14
  import { erkenneEntwurf } from './erkennung.js';
15
15
  import { vorschlag , pruefeVorlage, zeigeAbozahl } from './vorlagen.js';
@@ -58,6 +58,17 @@ export function merkeHakenVorgabe(e) {
58
58
  return e;
59
59
  }
60
60
 
61
+ /* Was von `common.name` als Beschriftung uebrig bleibt.
62
+
63
+ Gleicht der Name dem Zeilennamen, ist er keine eigene Beschriftung,
64
+ sondern die Vorgabe - dann bleibt das Feld leer. Dieselbe Regel gilt
65
+ in `zeileAusAlias` (werte.js) und damit an allen vier Stellen, die
66
+ eine Zeile aus einem Alias-Objekt bauen. */
67
+ function beschriftungAus(name, zeilenname) {
68
+ var nm = txt(name);
69
+ return (nm && nm !== zeilenname) ? nm : '';
70
+ }
71
+
61
72
  export function baueEntwurf(kanal) {
62
73
  var istAlias = (kanal.indexOf('alias.') === 0);
63
74
  var e = { kanal: kanal, states: [], want: null, wantAuto: null, alleMuster: false };
@@ -106,7 +117,18 @@ export function baueEntwurf(kanal) {
106
117
  srcW: istAlias ? schreibQuelle(o) : (c.write ? id : ''),
107
118
  f: (typeof a.read === 'string') ? a.read : '',
108
119
  fw: (typeof a.write === 'string') ? a.write : '',
109
- caption: istAlias ? txt(c.name) : kanalBeschriftung(kanal, kurzName),
120
+ /* Dieselbe Regel wie ueberall sonst: „leer, wenn sie dem
121
+ Zeilennamen gleicht".
122
+
123
+ Hier stand `istAlias ? txt(c.name) : …` ohne diese Pruefung.
124
+ Folge: Wo `common.name` dem Punktnamen gleicht - bei fast jedem
125
+ Alias, den die Werkbank selbst angelegt hat -, zeigte „Alias
126
+ bearbeiten" den Namen und „Alias anlegen" ein leeres Feld.
127
+ Derselbe Alias, zwei Ansichten, zwei Antworten (U21, gemessen
128
+ 12.09.2026). Geschrieben wurde in beiden Faellen dasselbe; zu
129
+ sehen war es trotzdem. */
130
+ caption: istAlias ? beschriftungAus(c.name, kurzName)
131
+ : kanalBeschriftung(kanal, kurzName),
110
132
  manuell: false
111
133
  });
112
134
  });
@@ -1167,18 +1189,33 @@ export function bestandVorrang(e, aliasId) {
1167
1189
  her, nachdem der Bestand ihn ueberschrieben hat. */
1168
1190
  on: s.on
1169
1191
  };
1192
+ var ist = zeileAusAlias(o, s.n);
1193
+ /* Rolle und Typ NUR, wenn sie im Alias ueberhaupt stehen.
1194
+
1195
+ `zeileAusAlias` liefert an dieser Stelle `''` - fuer die anderen
1196
+ drei Aufrufer richtig, hier nicht: ein Alias ohne eigene Rolle
1197
+ soll die der Vorlage behalten, statt sie gegen einen Leerstring zu
1198
+ tauschen. Das ist der eine Unterschied, der beim Zusammenlegen der
1199
+ fuenf Stellen erhalten bleiben musste (Y5) - wer ihn wegraeumt,
1200
+ nimmt jedem vorlagenbasierten Alias seine Rollen. */
1170
1201
  if (c.role !== undefined) { s.role = c.role; }
1171
1202
  if (c.type !== undefined) { s.typ = c.type; }
1172
- s.unit = (c.unit !== undefined) ? c.unit : '';
1203
+ s.unit = ist.unit;
1173
1204
  s.states = c.states;
1174
1205
  /* Die Beschriftung gehoert dem Nutzer - sie wird auch spaeter vom
1175
- Vorlagen-Abgleich nicht angefasst (Ricardo, 25.08.2026). */
1176
- if (c.name !== undefined) { s.caption = txt(c.name); }
1177
- var a = c.alias || {};
1178
- s.f = (typeof a.read === 'string') ? a.read : '';
1179
- s.fw = (typeof a.write === 'string') ? a.write : '';
1206
+ Vorlagen-Abgleich nicht angefasst (Ricardo, 25.08.2026).
1207
+
1208
+ Seit dem 12.09.2026 gilt hier dieselbe Regel wie ueberall sonst:
1209
+ gleicht sie dem Zeilennamen, bleibt das Feld leer. Vorher stand
1210
+ hier `if (c.name !== undefined) s.caption = txt(c.name)` - und
1211
+ weil `vomAliasUebernehmen` danach laeuft und verwirft, hing es
1212
+ an der Reihenfolge, welche der beiden Antworten man zu sehen
1213
+ bekam (U21). */
1214
+ if (c.name !== undefined) { s.caption = ist.caption; }
1215
+ s.f = ist.f;
1216
+ s.fw = ist.fw;
1180
1217
  if (q.read) { s.srcR = q.read; }
1181
- s.srcW = schreibQuelle(o);
1218
+ s.srcW = ist.srcW;
1182
1219
  s.on = true;
1183
1220
  s.ausBestand = true;
1184
1221
  });
@@ -1209,14 +1246,7 @@ export function bestandsAbweichung(s, aliasId) {
1209
1246
  if (!s || !s.n || !s.on || !aliasId) { return []; }
1210
1247
  var o = S.objects[aliasId + '.' + s.n];
1211
1248
  if (!o || !o.common) { return []; }
1212
- var c = o.common, a = c.alias || {}, q = aliasQuellen(o);
1213
- var ist = {
1214
- role: c.role || '', typ: c.type || '', unit: c.unit || '',
1215
- f: (typeof a.read === 'string') ? a.read : '',
1216
- fw: (typeof a.write === 'string') ? a.write : '',
1217
- srcR: q.read || '',
1218
- srcW: schreibQuelle(o)
1219
- };
1249
+ var ist = zeileAusAlias(o, s.n);
1220
1250
  var raus = [];
1221
1251
  ['role', 'typ', 'unit', 'f', 'fw', 'srcR', 'srcW'].forEach(function (k) {
1222
1252
  var jetzt = s[k] || '';
@@ -705,6 +705,27 @@ var warumAuf = false;
705
705
  neuE.roh = rohJetzt;
706
706
  neuE.vorschlag = true;
707
707
  neuE.tplName = e.tplName; neuE.grund = e.grund; neuE.ziel = e.ziel;
708
+ /* Ordner und Name getrennt mitgeben, nicht nur das fertige Ziel.
709
+
710
+ `setzeZiel` rechnet den Ordner nur aus, solange keiner dasteht —
711
+ und dabei liest es das letzte Segment von `e.ziel` als
712
+ Geraetenamen. Bei einem Geraet mit mehreren Ausgaengen ist das
713
+ letzte Segment aber der AUSGANG (`…Garten-Ventilinsel.POWER2`),
714
+ und der Ordner bekam ihn angehaengt. Jeder Druck auf diesen
715
+ Knopf schob den Ablageort eine Ebene tiefer, und geschrieben
716
+ wurde er auch so: gemessen 12.09.2026 am Testsystem entstanden
717
+ nach drei Klicks vier Ordner statt einem
718
+ (`…Garten-Ventilinsel.POWER2.POWER2.POWER2.POWER2`).
719
+
720
+ Der Ausgangswechsel eine Stelle weiter oben macht es seit jeher
721
+ richtig — hier dieselbe Form. Im Aliasmodus sind beide Felder
722
+ `undefined`; dann bleibt alles wie bisher, `setzeZiel` steigt
723
+ dort ohnehin sofort aus. `ordnerVonHand` gehoert mit, sonst
724
+ verliert der Raumvorschlag die Entscheidung, wer den Ordner
725
+ gesetzt hat (E41). */
726
+ neuE.zielOrdner = e.zielOrdner;
727
+ neuE.zielName = e.zielName;
728
+ neuE.ordnerVonHand = e.ordnerVonHand;
708
729
  if (fest) { neuE.vonHandGewaehlt = false; neuE.ausBestand = true; }
709
730
  if (vorh && S.objects[vorh]) { bestandVorrang(neuE, vorh); }
710
731
  S.entwurf = neuE; S.openRow = null; zeichneErgebnis();
@@ -827,15 +848,32 @@ var warumAuf = false;
827
848
  ba.hidden = !mehr && !mehrK;
828
849
  if (mehr) {
829
850
  ba.textContent = tr('write.createAll', e.instanzen.length);
851
+ /* Dieselbe Rechnung wie beim Schreiben, samt Kollisionsschutz.
852
+
853
+ Zwei Kanaele koennen nach der Saeuberung denselben Namen
854
+ ergeben („Licht Bar" und „Licht, Bar" beide `Licht_Bar`); der
855
+ Schreibpfad haengt dann `_2` an. Ohne dieselbe Rechnung stuende
856
+ im Hinweis zweimal derselbe Name und danach etwas anderes am
857
+ System - genau die Luecke, die G31 beschreibt (12.09.2026). */
858
+ var belegt = {};
830
859
  ba.title = e.instanzen.map(function (n) {
831
- return (e.zielOrdner || '') + '.' + ausgangName(e, n);
860
+ var nm = (String(n) === String(e.instanz)) ? e.zielName : ausgangName(e, n);
861
+ if (belegt[nm]) {
862
+ var z = 1, n2;
863
+ do { z++; n2 = nm + '_' + z; } while (belegt[n2]);
864
+ nm = n2;
865
+ }
866
+ belegt[nm] = 1;
867
+ return (e.zielOrdner || '') + '.' + nm;
832
868
  }).join(String.fromCharCode(10));
833
869
  } else if (mehrK) {
834
870
  ba.textContent = tr('write.createPerChannel', e.kanalGeraete.length);
835
871
  /* Im Hinweis stehen die Namen, die entstehen wuerden — sonst
836
872
  klickt man auf eine Zahl und weiss nicht, was danach dasteht. */
837
873
  ba.title = e.kanalGeraete.map(function (k) {
838
- return (e.zielOrdner || '') + '.' + k.name + ' (' + k.typ + ')';
874
+ /* Die Kennung, nicht den Anzeigenamen - so heisst das Objekt
875
+ hinterher wirklich (G31). */
876
+ return (e.zielOrdner || '') + '.' + (k.kennung || k.name) + ' (' + k.typ + ')';
839
877
  }).join(String.fromCharCode(10));
840
878
  }
841
879
  ba.disabled = false;
@@ -1050,9 +1088,16 @@ var warumAuf = false;
1050
1088
  function verkabeleDialoge() {
1051
1089
  var bc = $('#btn-checks'), bd = $('#btn-dry'), bw = $('#btn-dry-write');
1052
1090
  if (bc) { bc.addEventListener('click', function () { $('#dlg-checks').showModal(); }); }
1053
- if (bd) { bd.addEventListener('click', function () { zeigeTrockenlauf(false); }); }
1091
+ /* Beim Oeffnen von Hand faengt der Dialog frisch an - ein alter
1092
+ Sendepunkt-Fehler von vorhin hat dann nichts mehr zu sagen (H17).
1093
+ Aus dem Rueckruf heraus wird zeigeTrockenlauf direkt gerufen, dort
1094
+ bleibt die Meldung also stehen. */
1095
+ var frisch = function (alle) {
1096
+ return function () { S.mqFehler = null; zeigeTrockenlauf(alle); };
1097
+ };
1098
+ if (bd) { bd.addEventListener('click', frisch(false)); }
1054
1099
  var ba2 = $('#btn-dry-alle');
1055
- if (ba2) { ba2.addEventListener('click', function () { zeigeTrockenlauf(true); }); }
1100
+ if (ba2) { ba2.addEventListener('click', frisch(true)); }
1056
1101
  if (bw) { bw.addEventListener('click', schreibeObjekte); }
1057
1102
  var bdel = $('#btn-del'), bdg = $('#btn-del-go');
1058
1103
  if (bdel) { bdel.addEventListener('click', zeigeLoeschen); }
@@ -61,11 +61,25 @@ export function rolleTrifft(r, rolle) {
61
61
  Ableitung bekam so ein Punkt gar keine: er belegte seinen Platz nie,
62
62
  liess sich beliebig oft hinzufuegen, und die Rolle fehlte auch in der
63
63
  Auswahlliste — der Platz war auf keinem Weg zu befuellen. */
64
+ /* Der fehlende Anfangsanker zaehlt nicht gegen den Platz.
65
+
66
+ `vacuumCleaner.MAP_URL` schreibt `/vacuum\.map\.url$/` - ohne `^`.
67
+ Gemeint ist derselbe eine Rollenname wie bei den anderen; der Platz
68
+ blieb aber ohne Rolle, weil hier beide Anker verlangt wurden
69
+ (gemessen 12.09.2026 ueber alle 53 Muster: ausser MAP_URL traf es
70
+ nur `media.COVER`, und dessen Ausdruck `/^media\.cover(\..*)$/`
71
+ beschreibt wirklich keinen einen Namen - der Platz hat dafuer eine
72
+ `defaultRole`).
73
+
74
+ Das Endezeichen bleibt Pflicht: ohne `$` waere `/^level\.dimmer/`
75
+ auch von `level.dimmer.irgendwas` erfuellt, und dann ist es kein
76
+ Name mehr, sondern ein Praefix. */
64
77
  export function rolleAusAusdruck(r) {
65
78
  var q = ausdruckText(r);
66
79
  if (!q) { return ''; }
67
- if (q.charAt(0) !== '^' || q.slice(-1) !== '$') { return ''; }
68
- var t = q.slice(1, -1).split(String.fromCharCode(92)).join('');
80
+ if (q.slice(-1) !== '$') { return ''; }
81
+ if (q.charAt(0) === '^') { q = q.slice(1); }
82
+ var t = q.slice(0, -1).split(String.fromCharCode(92)).join('');
69
83
  return /^[A-Za-z0-9_]+([.][A-Za-z0-9_]+)*$/.test(t) ? t : '';
70
84
  }
71
85
 
@@ -260,6 +274,36 @@ export function typenFuerRolle(musterName, rolle) {
260
274
  return raus;
261
275
  }
262
276
 
277
+ /* Welche angehakten Zeilen treffen ihren Platz in der Rolle, aber nicht
278
+ im Datentyp?
279
+
280
+ Ein solcher Punkt faellt lautlos aus dem Muster - und wenn es ein
281
+ Pflichtplatz war, greift das Muster gar nicht mehr. Dann hat auf
282
+ einmal KEINE Zeile einen Platz, und die Oberflaeche beschuldigte alle:
283
+ am Bastelzimmer_Licht standen nach einem Typwechsel an SET alle acht
284
+ Zeilen getoent da, das Musterfeld sagte „✕ SET fehlt", und die
285
+ Wahrheit stand nur in der aufgeklappten SET-Zeile (Ricardo,
286
+ 12.09.2026).
287
+
288
+ Hier steht die Frage einmal, damit Musterfeld, Legende und Zeile
289
+ dieselbe Antwort geben. `pflicht` sagt, ob der betroffene Platz ein
290
+ Pflichtplatz ist - nur dann kippt das ganze Muster. */
291
+ export function typKonflikte(states, musterName) {
292
+ var mu = musterName && musterVon(musterName);
293
+ if (!mu) { return []; }
294
+ var raus = [];
295
+ (states || []).forEach(function (s) {
296
+ if (!s.on || !s.role || !s.typ) { return; }
297
+ var moegliche = typenFuerRolle(musterName, s.role);
298
+ if (!moegliche.length || moegliche.indexOf(s.typ) > -1) { return; }
299
+ var platz = platzFuerRolle(musterName, s.role);
300
+ if (!platz) { return; }
301
+ raus.push({ n: s.n, typ: s.typ, erwartet: moegliche, platz: platz.name,
302
+ pflicht: !!platz.required });
303
+ });
304
+ return raus;
305
+ }
306
+
263
307
  /* Die Datentypen, die ein Platz zulaesst - als Liste.
264
308
 
265
309
  Drei Faelle, gemessen am type-detector 6.0.1: 622 Plaetze nennen
@@ -10,7 +10,7 @@
10
10
  import { S } from './zustand.js';
11
11
  import { D, el, $ } from './basis.js';
12
12
  import { tr, sprachtext } from './sprache.js';
13
- import { VORSCHAU, musterVon, erkenneEntwurf, platzFuerRolle, typenFuerRolle } from './erkennung.js';
13
+ import { VORSCHAU, musterVon, erkenneEntwurf, platzFuerRolle, typenFuerRolle, typKonflikte } from './erkennung.js';
14
14
  import { anMusterAnpassen } from './vorlagen.js';
15
15
  import { ratePlaetze, rateZurueck, rateAnzahl, rateMoeglich } from './vorschlagen.js';
16
16
  import { zeichneErgebnis, entwurfAngefasst, knopfFrisch } from './ergebnis.js';
@@ -138,7 +138,17 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
138
138
  if (!r.length) {
139
139
  var fehlend = musterVon(m).states.filter(function (x) { return x.required; })
140
140
  .map(function (x) { return x.name; });
141
- if (fehlend.length) {
141
+ /* Ein Pflichtplatz kann auch deshalb leer sein, weil die Zeile
142
+ dafuer im falschen Datentyp steht - die Rolle stimmt, der Typ
143
+ nicht, und der Punkt faellt aus dem Muster. „SET fehlt" ist dann
144
+ die falsche Auskunft: SET ist da. Der Typ-Grund geht vor, wo es
145
+ einen gibt (Ricardo, 12.09.2026, am Bastelzimmer_Licht). */
146
+ var konflikt = typKonflikte(pruefE.states, m)
147
+ .filter(function (x) { return fehlend.indexOf(x.platz) > -1; })[0];
148
+ if (konflikt) {
149
+ mark = tr('pattern.fitsNotType', konflikt.platz, konflikt.typ,
150
+ konflikt.erwartet.join(tr('pattern.typeOr')));
151
+ } else if (fehlend.length) {
142
152
  mark = '✕ ' + tr('pattern.missing', fehlend.join(', '));
143
153
  } else {
144
154
  /* Kein Pflichtplatz fehlt und trotzdem passt nichts - dann liegt
@@ -92,6 +92,15 @@ function ausVorlage(punktId, neuKanal) {
92
92
 
93
93
  /* Stufe 3: gleicher Punktname irgendwo unter dem neuen Geraet, oder
94
94
  gleiche Rolle. Bewusst schwach - deshalb nur als Vorschlag. */
95
+ /* Und WORUEBER geraten wurde, wird mitgegeben.
96
+
97
+ „vermutet" allein sagt nur, dass die Zuordnung schwach ist - nicht,
98
+ woran sie haengt. Ueber den Namen geraten heisst: irgendein Punkt
99
+ heisst hinten gleich; das kann weit danebenliegen (ein Fensterkontakt
100
+ `…1.STATE` landete so auf `…tele.STATE`, dem Telemetrie-JSON einer
101
+ Tasmota). Ueber die Rolle geraten ist etwas anderes und muss anders
102
+ geprueft werden. Der Tooltip an der Marke sagt es jetzt (Ricardo,
103
+ 12.09.2026). */
95
104
  function geraten(altQuelle, neuKanal) {
96
105
  var name = altQuelle.split('.').pop();
97
106
  var kandidaten = kindZustaende(neuKanal);
@@ -99,7 +108,7 @@ function geraten(altQuelle, neuKanal) {
99
108
  kandidaten.forEach(function (id) {
100
109
  if (!treffer && id.split('.').pop() === name) { treffer = id; }
101
110
  });
102
- if (treffer) { return treffer; }
111
+ if (treffer) { return { id: treffer, ueber: 'name' }; }
103
112
  var altO = S.objects[altQuelle];
104
113
  var rolle = altO && altO.common && altO.common.role;
105
114
  if (rolle) {
@@ -108,7 +117,7 @@ function geraten(altQuelle, neuKanal) {
108
117
  if (!treffer && o && o.common && o.common.role === rolle) { treffer = id; }
109
118
  });
110
119
  }
111
- return treffer;
120
+ return treffer ? { id: treffer, ueber: 'rolle', rolle: rolle } : null;
112
121
  }
113
122
 
114
123
  /* Die Zuordnungstabelle: je Alias-Punkt, was heute gilt und was danach
@@ -158,10 +167,14 @@ export function tauschPlan(kanal, neuKanal) {
158
167
  /* 3. geraten */
159
168
  if (!zeile.neuR && zeile.altR) {
160
169
  var g = geraten(zeile.altR, neuKanal);
161
- if (g) { zeile.neuR = g; zeile.stufe = 3; }
170
+ if (g) {
171
+ zeile.neuR = g.id; zeile.stufe = 3;
172
+ zeile.geratenUeber = g.ueber; zeile.geratenRolle = g.rolle || '';
173
+ }
162
174
  }
163
175
  if (!zeile.neuW && zeile.altW) {
164
- var gw = geraten(zeile.altW, neuKanal);
176
+ var gwT = geraten(zeile.altW, neuKanal);
177
+ var gw = gwT && gwT.id;
165
178
  /* Was hier geraten wird, muss ein anderer Punkt sein als die
166
179
  Lesequelle. Bei getrennten Quellen heissen Melder und Befehl oft
167
180
  gleich - `stat.POWER` und `cmnd.POWER` -, und das Raten greift
@@ -172,6 +185,21 @@ export function tauschPlan(kanal, neuKanal) {
172
185
  (gemessen 09.09.2026). Lieber kein Treffer als dieser. */
173
186
  if (gw && gw !== zeile.neuR) { zeile.neuW = gw; }
174
187
  }
188
+ /* Bei getrennten Quellen darf das Schreibziel nie die Lesequelle
189
+ sein - egal, aus welcher der drei Stufen es kam.
190
+
191
+ Beim Raten stand diese Regel schon (siehe oben), beim PFADTREFFER
192
+ fehlte sie: `suchePfad` probiert als letzte Form den blossen
193
+ Punktnamen, und `cmnd.POWER` findet so `…Stumm.stat.POWER` - den
194
+ Melder. Sichtbar wird es, wenn man beim Tauschen nicht das Geraet,
195
+ sondern einen Unterknoten waehlt (`…Stumm.stat` statt `…Stumm`):
196
+ dann verschwand der rote Hinweis, der Knopf wurde frei, und
197
+ geschrieben haette der Alias auf seine eigene Lesequelle - genau
198
+ der Fehler, den W20 abschaffen sollte (Ricardo, 12.09.2026).
199
+
200
+ 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 = ''; }
175
203
  /* Bei einfacher Quelle gilt die Lesequelle fuer beides. */
176
204
  if (zeile.einfach && zeile.neuR) { zeile.neuW = zeile.neuR; }
177
205
  return zeile;
@@ -339,6 +367,25 @@ function zeichneAuswahlbaum(host, filter, beiWahl) {
339
367
  if (!host.childNodes.length) {
340
368
  host.appendChild(el('div', 'empty', tr('swap.noDevice')));
341
369
  }
370
+
371
+ /* Die Liste bleibt stehen, wo sie stand.
372
+
373
+ Beim Aufklappen fiel das nicht auf: derselbe Behaelter, nur neu
374
+ gefuellt, und `scrollTop` ueberlebt das. Beim WAEHLEN schon -
375
+ `zeichneTausch` baut den ganzen Dialog neu, der Baum ist danach ein
376
+ anderes Element und faengt bei 0 an. Wer weit unten ein Geraet
377
+ anklickte, sah danach den Anfang der Liste und seinen eben
378
+ gewaehlten Eintrag gar nicht mehr (Ricardo, 12.09.2026, an einer
379
+ Liste mit 42 Zeilen: scrollTop 700 -> 0).
380
+
381
+ Gemerkt wird an `S`, nicht am Element: das Element gibt es beim
382
+ naechsten Zeichnen nicht mehr. `block: 'nearest'` schiebt nur, wenn
383
+ die Zeile wirklich draussen ist - sonst springt die Liste bei jedem
384
+ Klick ein Stueck, und das waere die naechste Unruhe. */
385
+ if (S.tauschScroll) { host.scrollTop = S.tauschScroll; }
386
+ var gewaehlteZeile = host.querySelector('.node.sel');
387
+ if (gewaehlteZeile) { gewaehlteZeile.scrollIntoView({ block: 'nearest' }); }
388
+ host.addEventListener('scroll', function () { S.tauschScroll = host.scrollTop; });
342
389
  }
343
390
 
344
391
  export function zeigeTausch() {
@@ -355,6 +402,9 @@ export function zeigeTausch() {
355
402
  schlimmsten Fall genau dann, wenn im Feld noch das eben getauschte,
356
403
  inzwischen geloeschte Geraet stand (W13, gemessen 26.08.2026). */
357
404
  S.tauschFilter = '';
405
+ /* Und die Liste faengt oben an - sonst startet der naechste Tausch
406
+ dort, wo der letzte aufgehoert hat. */
407
+ S.tauschScroll = 0;
358
408
  var b = $('#btn-swap-go');
359
409
  if (b) { b.hidden = false; b.textContent = tr('swap.now'); }
360
410
  zeichneTausch();
@@ -420,6 +470,10 @@ function zeichneTausch() {
420
470
  zeichneAuswahlbaum(baumHost, S.tauschFilter, gewaehlt);
421
471
  feld.addEventListener('input', function () {
422
472
  S.tauschFilter = feld.value;
473
+ /* Andere Suche, andere Liste - die alte Scrollhoehe passt nicht mehr
474
+ und zeigte sonst ins Leere. */
475
+ S.tauschScroll = 0;
476
+ baumHost.scrollTop = 0;
423
477
  zeichneAuswahlbaum(baumHost, S.tauschFilter, gewaehlt);
424
478
  });
425
479
 
@@ -434,6 +488,40 @@ function zeichneTausch() {
434
488
  /* Die Zuordnung, Zeile fuer Zeile. */
435
489
  if (!S.tauschPlan) { S.tauschPlan = tauschPlan(kanal, S.tauschNeu); }
436
490
  var plan = S.tauschPlan;
491
+
492
+ /* Steht die Wahl eine Ebene zu tief?
493
+
494
+ Der Baum laesst jeden Knoten waehlen, unter dem Datenpunkte haengen -
495
+ absichtlich, denn welche Ebene beim fremden Adapter das Geraet ist,
496
+ kann die Werkbank nicht wissen; bei einem flachen Adapter ist der
497
+ Kanal die richtige (Kommentar in `tauschPlan`). Bei MQTT sind
498
+ `stat`, `tele` und `cmnd` aber Themenordner, keine Geraete, und wer
499
+ dort landet, bekommt schlechtere Treffer: am RGB-Deckenlicht fanden
500
+ ueber das Geraet alle sieben Punkte ein Ziel, ueber `…Licht.tele`
501
+ nur fuenf - die beiden aus `stat` gar nichts (12.09.2026).
502
+
503
+ Verboten wird nichts. Aber die Werkbank kann nachrechnen, was eine
504
+ Ebene hoeher herauskaeme, und es sagen - sonst hakt man zwei Punkte
505
+ zum Entfernen an und opfert seinen Schalter, statt den Knoten
506
+ darueber zu nehmen (Ricardos Frage, 12.09.2026). */
507
+ var eltern = S.tauschNeu.split('.').slice(0, -1).join('.');
508
+ if (eltern.split('.').length > 2 && S.tauschNeu !== alt) {
509
+ var trefferHier = plan.filter(function (z) { return z.neuR; }).length;
510
+ var planOben = tauschPlan(kanal, eltern);
511
+ var trefferOben = planOben.filter(function (z) { return z.neuR; }).length;
512
+ if (trefferOben > trefferHier) {
513
+ var eb = el('div', 'aside w');
514
+ eb.style.borderLeftColor = 'var(--warn)';
515
+ eb.appendChild(el('b', null, tr('swap.higherLevel',
516
+ eltern, trefferOben, plan.length, trefferHier)));
517
+ var knopf = el('button', 'btn mini wichtig', tr('swap.goHigher'));
518
+ knopf.style.marginTop = '5px';
519
+ knopf.addEventListener('click', function () { gewaehlt(eltern); });
520
+ eb.appendChild(el('div', null, ''));
521
+ eb.appendChild(knopf);
522
+ body.appendChild(eb);
523
+ }
524
+ }
437
525
  var k = el('div', 'card');
438
526
  k.style.marginTop = '11px';
439
527
  var ch = el('div', 'ch');
@@ -460,9 +548,49 @@ function zeichneTausch() {
460
548
  && (z.einfach || (z.neuW || '') === (z.altW || ''));
461
549
  var nm = el('span', 'sn', z.name);
462
550
  if (gleich) { nm.appendChild(el('span', 'rat', tr('swap.unchanged'))); }
463
- else if (z.stufe === 3) { nm.appendChild(el('span', 'rat', tr('guess.mark'))); }
551
+ else if (z.stufe === 3) {
552
+ /* Die Marke sagt jetzt auch, WORUEBER geraten wurde - ueber den
553
+ blossen Punktnamen oder ueber die Rolle. Das sind zwei sehr
554
+ verschiedene Vermutungen, und sie pruefen sich verschieden. */
555
+ var rm = el('span', 'rat', tr('guess.mark'));
556
+ rm.title = z.geratenUeber === 'rolle'
557
+ ? tr('swap.guessedByRole', z.geratenRolle || '?')
558
+ : tr('swap.guessedByName', (z.altR || '').split('.').pop());
559
+ nm.appendChild(rm);
560
+ }
464
561
  r.appendChild(nm);
465
562
  var rechts = el('span');
563
+ /* Das Kaestchen „entfernen" - an zwei Stellen gebraucht, deshalb hier.
564
+
565
+ Es gab es bisher nur, wenn GAR NICHTS gefunden wurde. Der zweite
566
+ Fall braucht es genauso: Lesequelle gefunden, Schreibziel nicht.
567
+ Ein Punkt mit Schreibrecht ohne Schreibziel laesst sich in ioBroker
568
+ nicht ausdruecken - jede Form, die der js-controller annimmt,
569
+ schreibt irgendwohin, und `{read: X}` ohne `write` lehnt er ab
570
+ („The id is empty", gemessen 12.09.2026). Also entscheidet der
571
+ Nutzer: mitnehmen geht nicht, entfernen schon (Ricardo). */
572
+ var entfernenKaestchen = function () {
573
+ var lb = document.createElement('label');
574
+ lb.style.display = 'flex';
575
+ lb.style.gap = '6px';
576
+ lb.style.alignItems = 'center';
577
+ lb.style.marginTop = '3px';
578
+ lb.style.cursor = 'pointer';
579
+ var cb = document.createElement('input');
580
+ cb.type = 'checkbox';
581
+ cb.checked = !!S.tauschWeg[z.id];
582
+ cb.addEventListener('change', function () {
583
+ S.tauschWeg[z.id] = cb.checked;
584
+ /* Der Knopf haengt daran - also neu rechnen, nicht erst beim
585
+ naechsten Oeffnen. `zeichneTausch`, nicht `zeigeTausch`: das
586
+ zweite setzt Auswahl, Plan und Haken zurueck und faengt von
587
+ vorn an. */
588
+ zeichneTausch();
589
+ });
590
+ lb.appendChild(cb);
591
+ lb.appendChild(el('span', 'hint', tr('swap.removeIt')));
592
+ return lb;
593
+ };
466
594
  /* Beschriftet, sonst raet man, welche der beiden Zeilen die neue ist. */
467
595
  var paarZeile = function (beschriftung, text, klasse, farbe) {
468
596
  var zz = el('div');
@@ -489,6 +617,7 @@ function zeichneTausch() {
489
617
  kw.style.color = 'var(--bad)';
490
618
  kw.textContent = tr('swap.noWriteMatch');
491
619
  rechts.appendChild(kw);
620
+ rechts.appendChild(entfernenKaestchen());
492
621
  }
493
622
  } else {
494
623
  var fehlt = el('div');
@@ -497,19 +626,7 @@ function zeichneTausch() {
497
626
  rechts.appendChild(fehlt);
498
627
  /* Was nichts findet, laesst sich mit entfernen - sonst bliebe ein
499
628
  Punkt zurueck, der ins Leere zeigt (Ricardos Wahl, 25.08.2026). */
500
- var lb = document.createElement('label');
501
- lb.style.display = 'flex';
502
- lb.style.gap = '6px';
503
- lb.style.alignItems = 'center';
504
- lb.style.marginTop = '3px';
505
- lb.style.cursor = 'pointer';
506
- var cb = document.createElement('input');
507
- cb.type = 'checkbox';
508
- cb.checked = !!S.tauschWeg[z.id];
509
- cb.addEventListener('change', function () { S.tauschWeg[z.id] = cb.checked; });
510
- lb.appendChild(cb);
511
- lb.appendChild(el('span', 'hint', tr('swap.removeIt')));
512
- rechts.appendChild(lb);
629
+ rechts.appendChild(entfernenKaestchen());
513
630
  }
514
631
  r.appendChild(rechts);
515
632
  k.appendChild(r);
@@ -556,7 +673,39 @@ function zeichneTausch() {
556
673
  if (plan.some(function (z) { return z.stufe === 3; })) {
557
674
  body.appendChild(el('div', 'aside', tr('swap.guessHint')));
558
675
  }
559
- if (go) { go.disabled = false; }
676
+
677
+ /* Punkte, die nicht mitkoennen, halten den Tausch auf - bis der Nutzer
678
+ sagt, was mit ihnen geschehen soll.
679
+
680
+ Zwei Faelle, beide mit demselben Ausgang (das Kaestchen daneben):
681
+ gar kein Treffer, oder getrennte Quellen mit Lesetreffer und ohne
682
+ Schreibziel. Frueher lief der Tausch in beiden Faellen los. Der
683
+ zweite endete dabei im Fehler des js-controllers, und weil
684
+ `tauscheAus` Objekt fuer Objekt schrieb, stand der Alias danach
685
+ halb auf der alten und halb auf der neuen Quelle (W20, gemessen
686
+ 12.09.2026). Der erste lief still durch und liess den Punkt auf der
687
+ alten Quelle stehen - derselbe Mischzustand, nur ohne Meldung.
688
+
689
+ Gesperrt wird mit Begruendung am Knopf, nicht bloss grau: ein
690
+ Knopf, der nicht sagt warum, ist der Fehler aus I32. */
691
+ var offeneStolper = plan.filter(function (z) {
692
+ if (S.tauschWeg[z.id]) { return false; }
693
+ if (!z.neuR) { return true; }
694
+ return !!(z.altW && z.altW !== z.altR && !z.neuW);
695
+ });
696
+ if (go) {
697
+ if (offeneStolper.length) {
698
+ go.disabled = true;
699
+ go.title = tr('swap.blocked', offeneStolper.map(function (z) { return z.name; }).join(', '));
700
+ var sp = el('div', 'aside w');
701
+ sp.style.borderLeftColor = 'var(--bad)';
702
+ sp.appendChild(el('b', null, go.title));
703
+ body.appendChild(sp);
704
+ } else {
705
+ go.disabled = false;
706
+ go.title = '';
707
+ }
708
+ }
560
709
  }
561
710
 
562
711
  export function tauscheAus() {
@@ -585,6 +734,17 @@ export function tauscheAus() {
585
734
  };
586
735
 
587
736
  var weiter = function () {
737
+ /* Schlug beim Umschreiben etwas fehl, wird nichts geloescht.
738
+
739
+ Dieselbe Regel wie beim Verlegen (T11): erst alles Neue, und geht
740
+ dabei etwas schief, bleibt der Rest unberuehrt. Hier fehlte sie -
741
+ `weiter` loeschte auch dann, wenn die Haelfte der Punkte noch auf
742
+ der alten Quelle stand. Zurueck kann die Werkbank nichts nehmen,
743
+ aber sie kann aufhoeren, es schlimmer zu machen: der Nutzer sieht
744
+ die Fehler und drueckt „Noch einmal versuchen", und die schon
745
+ umgeschriebenen Punkte stoeren dabei nicht - sie stehen ja bereits
746
+ richtig (12.09.2026). */
747
+ if (fehler.length) { return fertig(); }
588
748
  if (!loeschen.length) { return fertig(); }
589
749
  var offen2 = loeschen.length;
590
750
  loeschen.forEach(function (id) {