iobroker.alias-workbench 0.9.11 → 0.10.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.
@@ -9,10 +9,10 @@ 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, zeileAusAlias
12
+ mitVorspann, schreibQuelle, zeileAusAlias, istTaster as istTasterObjekt
13
13
  } from './werte.js';
14
14
  import { erkenneEntwurf } from './erkennung.js';
15
- import { vorschlag , pruefeVorlage, zeigeAbozahl } from './vorlagen.js';
15
+ import { vorschlag , pruefeVorlage, zeigeAbozahl, setzeInstanz } from './vorlagen.js';
16
16
 
17
17
  import { zeichneErgebnis } from './ergebnis.js';
18
18
 
@@ -75,6 +75,21 @@ export function baueEntwurf(kanal) {
75
75
  kindZustaende(kanal).forEach(function (id) {
76
76
  var o = S.objects[id], c = o.common || {}, a = c.alias || {}, q = aliasQuellen(o);
77
77
  var kurzName = id.slice(kanal.length + 1);
78
+ /* Ein Knopfdruck hat keinen Zustand — sonst hat auch ein
79
+ nur-schreibender Punkt einen.
80
+
81
+ `read: false` allein reicht als Merkmal nicht. Homematic vergibt es
82
+ fuer beides: fuer `OLD_LEVEL` und `RAMP_STOP` (Gerätebeschreibung
83
+ `TYPE: ACTION` — ein Tastendruck) ebenso wie fuer `ON_TIME` und
84
+ `RAMP_TIME` (`TYPE: FLOAT` — eine Vorgabe fuer den naechsten
85
+ Schaltbefehl, die stehen bleibt). Nur beim ersten ist eine
86
+ Lesequelle sinnlos.
87
+
88
+ Nachgemessen am 17.09.2026: einen Alias auf `1.ON_TIME` gelegt, die
89
+ QUELLE direkt auf 30 gesetzt — der Alias zeigte 30. `read: false`
90
+ sperrt das Lesen also nicht, es sagt nur, dass von allein nichts
91
+ kommt. Wer den Wert setzt, will ihn auch sehen. */
92
+ var istTaster = istTasterObjekt(o);
78
93
  e.states.push({
79
94
  /* Bei einer Quelle wird aus stat.POWER der Zustand stat_POWER —
80
95
  ein Punkt im Namen wuerde sonst eine Unterebene im Alias
@@ -93,7 +108,12 @@ export function baueEntwurf(kanal) {
93
108
  dann „keine Rolle" und „nichts erkannt". Typ und Einheit hat sie
94
109
  von jeher uebernommen; die Rolle auszunehmen war nicht zu
95
110
  begruenden. Ueberschreiben laesst sie sich weiterhin. */
96
- role: c.role || '',
111
+ /* Ein Tastendruck laut Geraetebeschreibung bekommt die Knopfrolle,
112
+ wenn der Adapter ihm keine gegeben hat - an ihr haengt `read:
113
+ false` am Alias (`knopfZeile`). hm-rpc gibt `OLD_LEVEL`
114
+ `value.dimmer` und `INSTALL_TEST` `indicator`; die uebrigen
115
+ (PRESS_SHORT, STOP, RAMP_STOP) tragen schon `button…`. */
116
+ role: (istTaster && !/^button/.test(String(c.role || ''))) ? 'button' : (c.role || ''),
97
117
  typ: c.type || '',
98
118
  unit: c.unit || '',
99
119
  /* Ohne die Werteliste bleibt jeder Platz mit statesDefined leer —
@@ -109,7 +129,37 @@ export function baueEntwurf(kanal) {
109
129
  nichts abzuholen ist (K23). Dieselbe Ablesung wie in
110
130
  `zeileAusAlias` — sonst zeigte „Alias bearbeiten" eine Quelle,
111
131
  die „Alias anlegen" nie eingetragen hat. */
112
- srcR: istAlias ? ((c.read === false && q.einfach) ? '' : (q.read || '')) : id,
132
+ /* An der Quelle ist der Punkt seine eigene Lesequelle — ausser er
133
+ sagt selbst, dass dort nichts ankommt.
134
+
135
+ `read: false` heisst nicht „gesperrt": lesen kann den Punkt
136
+ jeder, er liegt in der Datenbank. Es heisst, dass nie eine
137
+ Aktualisierung eintrifft. Was drinsteht, ist der Wert, den
138
+ zuletzt jemand hineingeschrieben hat — meist der Anfangswert
139
+ beim Adapterstart. Ein Alias, der daraus liest, zeigt fuer immer
140
+ diesen einen Wert und sieht dabei aus wie ein Messwert.
141
+
142
+ Bei Homematic steht es in der Geraetebeschreibung: `OPERATIONS`
143
+ ist eine Bitmaske (1 lesen, 2 schreiben, 4 meldet von selbst),
144
+ und `2.OLD_LEVEL` traegt 2 bei `TYPE: ACTION` — ein Knopfdruck,
145
+ kein Zustand. Am Wohnzimmer_Dimmer sind das 15 von 43 Zeilen,
146
+ jede davon las und schrieb bisher auf denselben Befehlspunkt
147
+ (Ricardo, 17.09.2026).
148
+
149
+ Ist der Punkt beschreibbar, wird daraus also ein Taster: keine
150
+ Lesequelle, nur ein Schreibziel. Genau die Form, die K23/K26 am
151
+ Rollladen-STOP beschreiben — der Alias bekommt `read: false`,
152
+ und vis, Alexa und matter zeichnen einen Knopf statt eines
153
+ Schalters, der ewig auf „aus" steht. Kann der Punkt auch nicht
154
+ schreiben, bleibt er, wie er ist: dann ist die fehlende Meldung
155
+ wirklich ein Mangel, und die Marke sagt es. */
156
+ /* Seit 19.09.2026 bekommt auch der Taster seine Lesequelle: der
157
+ Alias sieht dann jeden Druck, auch einen von aussen. Zum Knopf
158
+ wird er trotzdem - `read: false` haengt jetzt an der Rolle
159
+ (`knopfZeile` in werte.js), nicht mehr an der leeren Lesequelle.
160
+ Was oben ueber `read: false` an der Quelle steht, ist damit
161
+ Geschichte: gemessen liest ein solcher Alias tadellos. */
162
+ srcR: istAlias ? (q.read || '') : id,
113
163
  /* Steht in `common.alias.id` ein einzelner Text, gilt er fuer Lesen
114
164
  UND Schreiben - dann ist die Schreibquelle dieselbe wie die
115
165
  Lesequelle. Hier stand frueher eine leere Zeichenkette, gedacht
@@ -364,7 +414,11 @@ export function quelleVon(aliasId) {
364
414
  kindZustaende(aliasId).forEach(function (id) {
365
415
  var qq = aliasQuellen(S.objects[id]);
366
416
  [qq.read, qq.write].forEach(function (x) {
367
- if (!x) { return; }
417
+ /* Nur Quellen, die es gibt. Zeigt jeder Punkt ins Leere, fand der
418
+ Rueckfall darunter („eine Ebene hoeher") den naechsten VORHANDENEN
419
+ Ordner - und „zur Quelle" sprang auf `0_userdata.0.wbfake` mit 31
420
+ fremden Geraeten statt auf die verschwundene Quelle (D10). */
421
+ if (!x || !S.objects[x]) { return; }
368
422
  var t = String(x).split('.');
369
423
  t.pop();
370
424
  var eltern = t.join('.');
@@ -565,8 +619,30 @@ export function steuerKanaele(dev) {
565
619
  if (!S.objects[id] || S.objects[id].type !== 'channel') { return; }
566
620
  var e2 = baueEntwurf(id);
567
621
  if (!e2.states.length) { return; }
568
- var f = erkenneEntwurf(e2, null).filter(function (x) { return x.type !== 'info'; });
569
- if (!f.length) { return; }
622
+ /* Auch Informationskanaele zaehlen mit (Ricardo, 17.09.2026).
623
+
624
+ Vorher fielen sie hier heraus — und damit sagte der Knopf am
625
+ Geschirrspueler „3 Stueck", waehrend im Baum daneben sechs Kanaele
626
+ standen. Wer `general` einzeln anklickt, bekommt laengst einen
627
+ Entwurf („Information ✓ 7/14 belegt") und kann ihn anlegen; nur
628
+ der Sammelweg liess ihn aus. Zwei Wege, zwei Antworten am selben
629
+ Geraet.
630
+
631
+ Der Zweck dieses Wegs ist, die Struktur der Quelle abzubilden —
632
+ dann gehoert jeder Kanal hinein, der fuer sich einen Entwurf
633
+ ergibt, und nicht nur der, den der Detektor fuer ein Geraet haelt.
634
+ Nebenbei faellt damit das Verteilen der „gemeinsamen" Kanaele in
635
+ den meisten Faellen weg: was einen eigenen Ordner bekommt, muss
636
+ nicht mehr in jeden anderen hineinkopiert werden (produktiv
637
+ gemessen: beim Maeher wanderten 206 Rohdatenpunkte in jeden der
638
+ vier Entwuerfe, beim Shelly SHEM-3 galten die drei Emeter-Phasen
639
+ als „gemeinsam" statt als eigene Kanaele). */
640
+ var alle = erkenneEntwurf(e2, null);
641
+ if (!alle.length) { return; }
642
+ /* Ein Kanal, der ein Geraet ergibt, wird danach benannt; nur wenn er
643
+ nichts als Information hergibt, gilt der info-Fund. */
644
+ var f = alle.filter(function (x) { return x.type !== 'info'; });
645
+ if (!f.length) { f = alle; }
570
646
  /* Nur zaehlen, was einen Pflichtplatz wirklich besetzt — sonst
571
647
  gilt jeder Kanal mit irgendeinem Messwert als Geraet. */
572
648
  /* Alles, was der Kanal an Plaetzen belegt — nicht nur die
@@ -590,92 +666,131 @@ export function steuerKanaele(dev) {
590
666
  return raus;
591
667
  }
592
668
 
593
- /* Ein fertiger Entwurf fuer einen Knoten, so wie ihn `waehle` erzeugen
594
- wuerde — Vorlage, sonst Rohentwurf mit erkanntem Muster und der
595
- Vorbelegung „nur was einen Platz hat". Gebraucht wird das, wenn aus
596
- mehreren Kanaelen auf einmal Aliase entstehen sollen. */
597
- /* Welche Kanaele eines Geraets gehoeren keinem einzelnen Verbraucher,
598
- sondern allen? Bei Homematic ist das Kanal 0 mit Batterie, Empfang und
599
- Erreichbarkeit — ein Funkmodul, zwei Lampen. */
600
- export function gemeinsameKanaele(dev, kanalGeraete) {
601
- var eigen = {};
602
- (kanalGeraete || []).forEach(function (k) { eigen[k.id] = 1; });
603
- var pre = dev + '.';
604
- var raus = [];
605
- S.keysSorted.forEach(function (id) {
606
- if (id.indexOf(pre) !== 0) { return; }
607
- if (id.slice(pre.length).indexOf('.') !== -1) { return; }
608
- if (!S.objects[id] || S.objects[id].type !== 'channel') { return; }
609
- if (eigen[id]) { return; }
610
- if (!kindZustaende(id).length) { return; }
611
- raus.push(id);
669
+ /* Welche Unterordner entstehen, wenn die Struktur der Quelle abgebildet
670
+ wird — und zwar nach den HAKEN, nicht nach einer Vermutung.
671
+
672
+ Bis 17.09.2026 baute dieser Weg je Kanal einen frischen Entwurf und
673
+ hakte an, was das Muster des Kanals hergab. Damit entstand, was
674
+ niemand ausgewaehlt hatte: am `MISCHKANAL` bekam der Wartungskanal
675
+ `LOW_BAT`, `RSSI_DEVICE` und `UNREACH`, obwohl oben nur `UNREACH`
676
+ angehakt war. Umgekehrt fiel weg, was jemand von Hand angehakt hatte —
677
+ der Entwurf wurde ja gar nicht gelesen (Ricardo, 17.09.2026).
678
+
679
+ Jetzt gilt: was angehakt ist, bestimmt beides — welche Ordner es gibt
680
+ und was darin steht. Ein Kanal ohne angehakte Zeile kommt nicht vor.
681
+ Zeilen, deren Quelle direkt am Geraet haengt oder von woanders kommt,
682
+ bleiben im Behaelter selbst (`eigen`). */
683
+ export function kanalGruppen(e) {
684
+ if (!e || !e.kanal || e.kanal.indexOf('alias.') === 0) { return []; }
685
+ var basis = e.kanal + '.';
686
+ var gruppen = [], nach = {}, eigen = [];
687
+ (e.states || []).forEach(function (st) {
688
+ if (!st.on || !st.n) { return; }
689
+ var quelle = st.srcR || st.srcW || '';
690
+ var rest = (quelle.indexOf(basis) === 0) ? quelle.slice(basis.length) : '';
691
+ var p = rest.indexOf('.');
692
+ /* Ohne Punkt liegt der Punkt direkt am Geraet; ohne `rest` liest die
693
+ Zeile von woanders. Beides gehoert in den Behaelter, nicht in einen
694
+ erfundenen Unterordner. */
695
+ if (!rest || p < 0) { eigen.push(st); return; }
696
+ var kid = e.kanal + '.' + rest.slice(0, p);
697
+ if (!S.objects[kid]) { eigen.push(st); return; }
698
+ if (!nach[kid]) {
699
+ var kn = eigenerKanalName(kid);
700
+ var kurz = rest.slice(0, p);
701
+ nach[kid] = { id: kid, kurz: kurz, name: kn || kurz,
702
+ kennung: kennungtauglich(kn || '') || kurz, zeilen: [] };
703
+ gruppen.push(nach[kid]);
704
+ }
705
+ nach[kid].zeilen.push(st);
612
706
  });
613
- return raus;
707
+ if (eigen.length) {
708
+ gruppen.unshift({ id: e.kanal, kurz: '', name: '', kennung: '',
709
+ eigen: true, zeilen: eigen });
710
+ }
711
+ return gruppen;
614
712
  }
615
713
 
616
- export function entwurfFuer(id, gemeinsam, gemAn) {
617
- var v = vorschlag(id);
618
- if (v) { return v; }
714
+ /* Der Entwurf ohne jede Vorlage — alle Punkte des Geraets, alle
715
+ angehakt, dazu das erkannte Muster als Anhaltspunkt.
716
+
717
+ Gebraucht fuer „— keine Vorlage —" in der Vorlagenwahl. Bis zum
718
+ 17.09.2026 liess sich eine erkannte Vorlage nicht abwaehlen: der erste
719
+ Eintrag der Liste war eine blosse Ueberschrift, sein Wert leer, und der
720
+ Zweig stieg bei leerem Wert sofort aus. Wechseln ging, weglassen nicht
721
+ (Ricardo).
722
+
723
+ Zuerst hakte dieser Weg an, was im erkannten Muster einen Platz hat.
724
+ Das ging an zwei GLEICHARTIGEN Kanaelen schief: am `hm.ZWEIKANAL`
725
+ nahm der Detektor einen der beiden Ausgaenge — und zwar den zweiten —,
726
+ `Licht_Esstisch` stand angehakt da und `Licht_Bar` leer. Welcher es
727
+ wird, ist Zufall, und danach zu raten ist nicht die Aufgabe eines
728
+ Entwurfs OHNE Vorlage. Wer sie abwaehlt, will die Punkte selbst
729
+ aussuchen; also stehen alle da (Ricardo, 17.09.2026). */
730
+ export function rohEntwurf(id) {
619
731
  var e2 = baueEntwurf(id);
620
732
  if (!e2.states.length) { return null; }
621
-
622
- /* Die Wartungsdaten des gemeinsamen Kanals gehoeren in jedes Geraet,
623
- nicht in eines davon oder in einen Kanal daneben.
624
-
625
- Ein Alias-Punkt ist ein Verweis, kein Wert — zwei Geraete duerfen
626
- auf dieselbe Quelle zeigen, und der Batteriestand des Funkmoduls
627
- gilt fuer beide Lampen gleichermassen. Lagerte man ihn in einen
628
- eigenen `info`-Kanal aus, haette keine der beiden eine
629
- Batteriewarnung: der Detektor schaut nur innerhalb eines Kanals.
630
- Genau so macht es die Tasmota-Mehrfachvorlage mit RSSI — jeder der
631
- vier Ausgaenge bekommt seinen eigenen, alle lesen aus tele/STATE. */
632
- (gemeinsam || []).forEach(function (gid) {
633
- kindZustaende(gid).forEach(function (qid) {
634
- var o = S.objects[qid]; if (!o) { return; }
635
- var c = o.common || {};
636
- var kurz = qid.slice(gid.length + 1).replace(/\./g, '_');
637
- /* Traegt der Kanal selbst schon einen Punkt dieses Namens, den
638
- gemeinsamen unterscheidbar machen. */
639
- if (e2.states.some(function (x) { return x.n === kurz; })) {
640
- kurz = gid.split('.').pop() + '_' + kurz;
641
- }
642
- e2.states.push({
643
- n: kurz, on: false, role: c.role || '', typ: c.type || '',
644
- unit: c.unit || '', states: c.states || undefined, wr: false,
645
- srcR: qid, srcW: '', f: '', fw: '', caption: '', manuell: false,
646
- gemeinsam: true
647
- });
648
- });
649
- });
650
733
  var f = erkenneEntwurf(e2, null);
651
734
  e2.wantAuto = f.length ? f[0].type : null;
652
735
  e2.want = e2.wantAuto;
653
- var funde = e2.want ? erkenneEntwurf(e2, e2.want) : f;
654
- var haupt = funde.length ? funde[0] : null;
655
- var platz = {};
656
- if (haupt) {
657
- haupt.states.forEach(function (x) {
658
- if (x.id) { platz[x.id.slice(id.length + 1)] = x.name; }
659
- });
660
- }
661
- if (Object.keys(platz).length) {
662
- e2.states.forEach(function (st) { st.on = !!platz[st.n]; });
663
- }
664
- /* Zum Schluss die Wartungspunkte, die auf der Geraeteebene einen Platz
665
- gefunden haben. Warum nicht einfach den Detektor auf dem Kanal
666
- fragen? Weil er dort nur SET und WORKING zuordnet, obwohl dieselben
667
- Punkte mit denselben Rollen im Abbild stehen — auf der Geraeteebene
668
- findet er LOWBAT, UNREACH und RSSI. Die Zuordnung von dort ist die
669
- belastbare; sie stammt aus demselben Detektor, nur aus dem Lauf, der
670
- sie sieht. */
671
- if (gemAn) {
672
- e2.states.forEach(function (st) {
673
- if (st.gemeinsam && gemAn[st.srcR]) { st.on = true; }
674
- });
675
- }
676
736
  return merkeHakenVorgabe(e2);
677
737
  }
678
738
 
739
+ /* Wohin die Unterordner kommen — der Behaelter fuer das Geraet.
740
+
741
+ Beim ersten Mal ist das schlicht `zielOrdner` plus Geraetename. Nach
742
+ dem Aufteilen gibt es aber mehrere Aliase zu derselben Quelle, je Kanal
743
+ einen, und `aliasFuer` liefert einen davon — den mit den meisten
744
+ Punkten. Der Behaelter waere dann dieser Unterordner, und der zweite
745
+ Druck haengte alles eine Ebene tiefer: aus
746
+ `alias.0.Geschirrspueler.Befehle` wurde
747
+ `alias.0.Geschirrspueler.Allgemeine_Informationen.Befehle`, beim
748
+ dritten Druck noch eine Ebene tiefer (17.09.2026).
749
+
750
+ Woran man es erkennt: das gefundene Ziel fuehrt in `native.quelle`
751
+ einen KANAL der Quelle, nicht das Geraet. Dann ist es selbst ein
752
+ Unterordner aus einem frueheren Durchgang, und der Behaelter ist sein
753
+ Elternknoten. */
754
+ export function aufteilBehaelter(e) {
755
+ var ordner = (e.zielOrdner || 'alias.0') + '.' + e.zielName;
756
+ var ziel = e.ziel;
757
+ if (!ziel) { return ordner; }
758
+ var q = ((S.objects[ziel] && S.objects[ziel].native) || {}).quelle;
759
+ if (q && e.kanal && q.indexOf(e.kanal + '.') === 0) {
760
+ var p = ziel.lastIndexOf('.');
761
+ if (p > 0) { return ziel.slice(0, p); }
762
+ }
763
+ return ordner;
764
+ }
765
+
766
+ /* Aus einer Gruppe wird ein Entwurf: dieselben Zeilen, nur auf den Kanal
767
+ bezogen benannt. Was jemand an einer Zeile geaendert hat — Rolle, Typ,
768
+ Einheit, Formel, Beschriftung — wandert unveraendert mit; es waere
769
+ sonderbar, eine Handeingabe beim Aufteilen wegzuwerfen. */
770
+ export function teilEntwurf(e, g) {
771
+ var v = { kanal: g.id, states: [], want: null, wantAuto: null,
772
+ alleMuster: false, raum: e.raum, funktion: e.funktion };
773
+ g.zeilen.forEach(function (st) {
774
+ var k = JSON.parse(JSON.stringify(st));
775
+ /* Der Name wird auf den Kanal bezogen: aus `commands_BSH_…` wird im
776
+ Kanal `Befehle` schlicht `BSH_…`. Gekuerzt wird ueber die Quelle —
777
+ und bei einem Taster ist das die SCHREIBquelle, denn `srcR` ist
778
+ dort leer. Ohne diesen Zusatz behielt genau diese Zeile ihren
779
+ langen Namen: `…Befehle.commands_BSH_Common_Command_StopProgram`
780
+ neben `…Allgemeine_Informationen.brand` (17.09.2026). */
781
+ var quelle = st.srcR || st.srcW || '';
782
+ if (!g.eigen && quelle && quelle.indexOf(g.id + '.') === 0) {
783
+ k.n = quelle.slice(g.id.length + 1).replace(/\./g, '_');
784
+ }
785
+ k.on = true;
786
+ v.states.push(k);
787
+ });
788
+ var f = erkenneEntwurf(v, null);
789
+ v.wantAuto = f.length ? f[0].type : null;
790
+ v.want = v.wantAuto;
791
+ return merkeHakenVorgabe(v);
792
+ }
793
+
679
794
  /* Alle laufenden Abos abmelden.
680
795
 
681
796
  Stand nur in `waehle` — und damit meldete kein einziger der Wege ab,
@@ -690,6 +805,108 @@ export function aboLoesen() {
690
805
  zeigeAbozahl();
691
806
  }
692
807
 
808
+ /* Die Quellen eines Entwurfs - alles, woraus eine Zeile liest oder
809
+ wohin sie schreibt. */
810
+ export function quellenVonEntwurf(e) {
811
+ var raus = [];
812
+ ((e && e.states) || []).forEach(function (s) {
813
+ if (s.srcR) { raus.push(s.srcR); }
814
+ if (s.srcW) { raus.push(s.srcW); }
815
+ });
816
+ return raus;
817
+ }
818
+
819
+ /* Abonniert Quellen dazu, die rechts angezeigt werden.
820
+
821
+ Ergaenzt nur: was ein laufendes Muster schon deckt, kommt nicht noch
822
+ einmal dazu, doppelt rufen schadet also nicht. Die Regeln stammen aus
823
+ `waehle`, wo sie vorher fest verbaut waren und nur am Alias griffen.
824
+
825
+ `quellenVerteilung` fasst zusammen, was die rechte Seite als
826
+ „Quellen" zeigt — dafuer bildet sie den ELTERNknoten jeder Quelle.
827
+ Bei einem flachen MQTT-Geraet ist das der halbe Adapter: gemessen
828
+ 09.09.2026 wurde daraus `mqtt-client.0.*` mit 322 Objekten, an einem
829
+ einzigen Alias. Abonniert wird deshalb der Kanal jeder wirklich
830
+ gelesenen Quelle. */
831
+ export function aboErgaenzen(quellen) {
832
+ var muster = (S.abo || []).slice();
833
+ var vorher = muster.length;
834
+ var drinSchon = function (was) {
835
+ return muster.some(function (m0) {
836
+ var p0 = m0.slice(-2) === '.*' ? m0.slice(0, -2) : m0;
837
+ return was === p0 || was.indexOf(p0 + '.') === 0;
838
+ });
839
+ };
840
+ var offen = [];
841
+ (quellen || []).forEach(function (x) {
842
+ if (x && !drinSchon(x) && offen.indexOf(x) === -1) { offen.push(x); }
843
+ });
844
+ if (!offen.length) { zeigeAbozahl(); return; }
845
+
846
+ var kanaele = [];
847
+ offen.forEach(function (x) {
848
+ var t = String(x).split('.');
849
+ t.pop();
850
+ var kn = t.join('.');
851
+ if (kn && kanaele.indexOf(kn) === -1) { kanaele.push(kn); }
852
+ });
853
+ var SCHWELLE = 30;
854
+ var wieBreit = function (knoten) {
855
+ var n = 0;
856
+ S.keysSorted.forEach(function (k0) { if (k0.indexOf(knoten + '.') === 0) { n++; } });
857
+ return n;
858
+ };
859
+
860
+ /* Sparten desselben Geraets zu einem Muster zusammenfassen.
861
+
862
+ Ein Tasmota-Alias liest aus `…Licht.stat`, `…Licht.tele` und
863
+ `…Licht.cmnd` — drei Kanaele, ein Geraet, und in der Kopfleiste
864
+ gehoert dort „ein Abo" zu stehen, nicht drei. Zusammengefasst wird
865
+ aber nur, wo wirklich MEHRERE Kanaele unter demselben Knoten
866
+ haengen: Sonst waere aus `…Garten.Weihnachten_aussen` schlicht
867
+ `…Garten` geworden — breiter als noetig, ohne dass etwas
868
+ zusammenfaellt. */
869
+ for (var runde = 0; runde < 4; runde++) {
870
+ var unter = {};
871
+ kanaele.forEach(function (kn) {
872
+ var t = kn.split('.');
873
+ if (t.length <= 3) { return; }
874
+ var ob = t.slice(0, -1).join('.');
875
+ (unter[ob] = unter[ob] || []).push(kn);
876
+ });
877
+ var geaendert = false;
878
+ Object.keys(unter).forEach(function (ob) {
879
+ if (unter[ob].length < 2 || wieBreit(ob) > SCHWELLE) { return; }
880
+ kanaele = kanaele.filter(function (kn) { return unter[ob].indexOf(kn) === -1; });
881
+ if (kanaele.indexOf(ob) === -1) { kanaele.push(ob); }
882
+ geaendert = true;
883
+ });
884
+ if (!geaendert) { break; }
885
+ }
886
+
887
+ /* Ein schmaler Kanal wird als Muster abonniert, ein breiter nicht.
888
+
889
+ Bei einem flachen Geraet IST der Kanal der breite Knoten: Die
890
+ Quelle `mqtt-client.0.SmartHome.Lavalampe_POWER` liegt direkt
891
+ unter `SmartHome`, und `SmartHome.*` sind 320 Objekte (gemessen
892
+ 09.09.2026). Ab dieser Schwelle werden die Quellen einzeln
893
+ abonniert — ein paar Abos mehr, dafuer genau die, deren Werte auch
894
+ angezeigt werden. */
895
+ kanaele.forEach(function (kn) {
896
+ if (drinSchon(kn)) { return; }
897
+ if (wieBreit(kn) > SCHWELLE) { return; }
898
+ muster.push(kn + '.*');
899
+ });
900
+ offen.forEach(function (x) {
901
+ if (!drinSchon(x) && muster.indexOf(x) === -1) { muster.push(x); }
902
+ });
903
+
904
+ var neu = muster.slice(vorher);
905
+ S.abo = muster;
906
+ neu.forEach(function (m) { socket.emit('subscribe', m); });
907
+ zeigeAbozahl();
908
+ }
909
+
693
910
  export function waehle(id, fertig) {
694
911
  /* Wer links etwas anderes anklickt, will es rechts sehen - auch wenn
695
912
  der Fokus noch in einem Feld der rechten Seite steht. `tipptGerade`
@@ -743,109 +960,28 @@ export function waehle(id, fertig) {
743
960
  `alias.0.Solar.Balkon.Energie`, wo sich im Sekundentakt etwas
744
961
  aendert, fiel es auf (Ricardo, 08.09.2026).
745
962
 
746
- Deshalb am Alias zusaetzlich seine Quellen - `quellenVerteilung`
747
- weiss ja, welche das sind. Und deshalb eine Liste: sonst bliebe
748
- beim naechsten Wechsel die Haelfte abonniert und die Werkbank
749
- bekaeme mit der Zeit Ereignisse fuer das halbe System. Die Zahl der
750
- laufenden Abos steht in der Kopfleiste - dort sieht man sofort, ob
751
- sich etwas ansammelt. */
963
+ Deshalb am Alias zusaetzlich seine Quellen, und - seit dem
964
+ 18.09.2026 - an der Quelle ebenso: dort ergaenzt `uebernehmeBestand`
965
+ die Zeilen des vorhandenen Alias, und die lesen oft woanders
966
+ (`Stromzaehler` zeigt drei Zeilen „aus Solar.Netz"). Die standen
967
+ nach dem ersten Abholen still, gemessen 428 W gegen 22 W am System
968
+ (J9). `aboErgaenzen` wird darum von jeder Stelle gerufen, an der
969
+ Zeilen mit neuen Quellen dazukommen. Eine Liste, damit beim
970
+ naechsten Wechsel `aboLoesen` alles abmeldet. */
752
971
  aboLoesen();
753
- var muster = [id + '.*'];
972
+ S.abo = [id + '.*'];
973
+ socket.emit('subscribe', id + '.*');
754
974
  if (id.indexOf('alias.') === 0) {
755
- /* Aus den Quellen, nicht aus der Anzeige-Verteilung.
756
-
757
- `quellenVerteilung` fasst zusammen, was die rechte Seite als
758
- „Quellen" zeigt — dafuer bildet sie den ELTERNknoten jeder
759
- Quelle. Bei einem flachen MQTT-Geraet ist das der halbe Adapter:
760
- gemessen 09.09.2026 wurde daraus `mqtt-client.0.*` mit 322
761
- Objekten, an einem einzigen Alias. Und `S.werte` wird nie
762
- geleert, das sammelt sich also.
763
-
764
- Abonniert wird deshalb der Kanal jeder wirklich gelesenen Quelle.
765
- Wo die Quellen ohnehin gebuendelt liegen, bleibt es bei einem
766
- Muster; wo nicht, sind es ein paar mehr — aber jedes davon eng. */
767
- var kanaele = [];
768
- kindZustaende(id).forEach(function (kid) {
769
- var q = aliasQuellen(S.objects[kid]);
770
- [q.read, q.write].forEach(function (x) {
771
- if (!x) { return; }
772
- var t = String(x).split('.');
773
- t.pop();
774
- var kn = t.join('.');
775
- if (kn && kanaele.indexOf(kn) === -1) { kanaele.push(kn); }
776
- });
777
- });
778
- /* Ein schmaler Kanal wird als Muster abonniert, ein breiter nicht.
779
-
780
- Bei einem flachen Geraet IST der Kanal der breite Knoten: Die
781
- Quelle `mqtt-client.0.SmartHome.Lavalampe_POWER` liegt direkt
782
- unter `SmartHome`, und `SmartHome.*` sind 320 Objekte. Ab dieser
783
- Schwelle werden die Quellen einzeln abonniert — ein paar Abos
784
- mehr, dafuer genau die, deren Werte auch angezeigt werden. */
785
- var SCHWELLE = 30;
786
- var wieBreit = function (knoten) {
787
- var n = 0;
788
- S.keysSorted.forEach(function (k0) { if (k0.indexOf(knoten + '.') === 0) { n++; } });
789
- return n;
790
- };
791
- var drinSchon = function (was) {
792
- return muster.some(function (m0) {
793
- var p0 = m0.slice(-2) === '.*' ? m0.slice(0, -2) : m0;
794
- return was === p0 || was.indexOf(p0 + '.') === 0;
795
- });
796
- };
797
-
798
- /* Sparten desselben Geraets zu einem Muster zusammenfassen.
799
-
800
- Ein Tasmota-Alias liest aus `…Licht.stat`, `…Licht.tele` und
801
- `…Licht.cmnd` — drei Kanaele, ein Geraet, und in der Kopfleiste
802
- gehoert dort „ein Abo" zu stehen, nicht drei. Zusammengefasst wird
803
- aber nur, wo wirklich MEHRERE Kanaele unter demselben Knoten
804
- haengen: Sonst waere aus `…Garten.Weihnachten_aussen` schlicht
805
- `…Garten` geworden — breiter als noetig, ohne dass etwas
806
- zusammenfaellt. */
807
- for (var runde = 0; runde < 4; runde++) {
808
- var unter = {};
809
- kanaele.forEach(function (kn) {
810
- var t = kn.split('.');
811
- if (t.length <= 3) { return; }
812
- var ob = t.slice(0, -1).join('.');
813
- (unter[ob] = unter[ob] || []).push(kn);
814
- });
815
- var geaendert = false;
816
- Object.keys(unter).forEach(function (ob) {
817
- if (unter[ob].length < 2 || wieBreit(ob) > SCHWELLE) { return; }
818
- kanaele = kanaele.filter(function (kn) { return unter[ob].indexOf(kn) === -1; });
819
- if (kanaele.indexOf(ob) === -1) { kanaele.push(ob); }
820
- geaendert = true;
821
- });
822
- if (!geaendert) { break; }
823
- }
824
-
825
- /* Ein schmaler Kanal wird als Muster abonniert, ein breiter nicht.
826
-
827
- Bei einem flachen Geraet IST der Kanal der breite Knoten: Die
828
- Quelle `mqtt-client.0.SmartHome.Lavalampe_POWER` liegt direkt
829
- unter `SmartHome`, und `SmartHome.*` sind 320 Objekte (gemessen
830
- 09.09.2026; vorher wurde daraus sogar `mqtt-client.0.*` mit 322).
831
- Ab dieser Schwelle werden die Quellen einzeln abonniert — ein paar
832
- Abos mehr, dafuer genau die, deren Werte auch angezeigt werden. */
833
- kanaele.forEach(function (kn) {
834
- if (drinSchon(kn)) { return; }
835
- if (wieBreit(kn) > SCHWELLE) { return; }
836
- muster.push(kn + '.*');
837
- });
838
- /* Was jetzt noch von keinem Muster gedeckt ist, kommt einzeln. */
975
+ var ausAlias = [];
839
976
  kindZustaende(id).forEach(function (kid) {
840
977
  var q = aliasQuellen(S.objects[kid]);
841
- [q.read, q.write].forEach(function (x) {
842
- if (x && !drinSchon(x) && muster.indexOf(x) === -1) { muster.push(x); }
843
- });
978
+ if (q.read) { ausAlias.push(q.read); }
979
+ if (q.write) { ausAlias.push(q.write); }
844
980
  });
981
+ aboErgaenzen(ausAlias);
982
+ } else {
983
+ zeigeAbozahl();
845
984
  }
846
- S.abo = muster;
847
- muster.forEach(function (m) { socket.emit('subscribe', m); });
848
- zeigeAbozahl();
849
985
 
850
986
  /* Erst die Werte, dann der Vorschlag. Die Erkennung schaut in den
851
987
  Rohwert von tele.SENSOR hinein — ohne Werte haelt sie ein Geraet
@@ -922,10 +1058,14 @@ export function waehle(id, fertig) {
922
1058
  if (fest) { v.vonHandGewaehlt = false; v.ausBestand = true; }
923
1059
  /* Gibt es den Alias schon, gilt sein Stand - in beiden Ansichten
924
1060
  dasselbe Bild (Ricardo, 25.08.2026). */
925
- if (vorh && S.objects[vorh]) { bestandVorrang(v, vorh); }
1061
+ /* An einem Mehrfachgeraet zaehlt der Alias DIESES Ausgangs,
1062
+ nicht der mit den meisten Punkten (C13). */
1063
+ var vorhA = aliasFuer(id, ausgangsQuellen(v)) || (v.instanz === null || v.instanz === undefined ? vorh : null);
1064
+ if (vorhA && S.objects[vorhA]) { bestandVorrang(v, vorhA); }
926
1065
  S.entwurf = v;
927
1066
  }
928
1067
  }
1068
+ aboErgaenzen(quellenVonEntwurf(S.entwurf));
929
1069
  var nach = [];
930
1070
  S.entwurf.states.forEach(function (s) {
931
1071
  if (s.srcR && !S.werte[s.srcR]) { nach.push(s.srcR); }
@@ -1076,9 +1216,13 @@ export function geraeteDarunter(wurzel) {
1076
1216
  }
1077
1217
  });
1078
1218
 
1079
- geraete.sort(function (a, b) { return a.id < b.id ? -1 : 1; });
1219
+ /* Angezeigt wird wie im Baum: `seg_inf.10` hinter `seg_inf.9`, nicht
1220
+ hinter `seg_inf.1` (E45). Die Liste dient nur der Anzeige — die
1221
+ byteweise Ordnung von `keysSorted` bleibt unberuehrt (E42). */
1222
+ geraete.sort(function (a, b) { return GERAETE_VERGLEICH.compare(a.id, b.id); });
1080
1223
  return geraete;
1081
1224
  }
1225
+ var GERAETE_VERGLEICH = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
1082
1226
 
1083
1227
  /* Gibt es fuer diese Quelle schon einen Alias?
1084
1228
 
@@ -1097,11 +1241,17 @@ export function geraeteDarunter(wurzel) {
1097
1241
  `aliasFuer` lief zweimal ueber alle Kennungen — und wird je
1098
1242
  Neuzeichnen mehrfach gerufen. Die Karte entsteht bei der ersten Frage
1099
1243
  nach einem Indexwechsel und beantwortet alle weiteren aus dem
1100
- Gedaechtnis. Erkannt wird der Wechsel an Laenge und erster Kennung;
1101
- `indexNeu()` sortiert ohnehin bei jeder Aenderung neu. */
1102
- var alsQuelleKarte = null, alsQuelleStand = '';
1244
+ Gedaechtnis.
1245
+
1246
+ Erkannt wurde der Wechsel bis 19.09.2026 an Laenge und erster Kennung.
1247
+ Das reicht nicht: Verlegen loescht acht Objekte und legt acht neue an,
1248
+ die Zahl bleibt gleich, und ein Quellentausch aendert nur `alias.id`.
1249
+ Die Karte zeigte danach auf den geloeschten Alias, an der Quelle stand
1250
+ „wird neu angelegt", und ein Klick haette einen zweiten Alias erzeugt
1251
+ (T22). Jetzt zaehlt `S.objektStand` jede Aenderung an den Objekten. */
1252
+ var alsQuelleKarte = null, alsQuelleStand = -1;
1103
1253
  function alsQuelleKarten() {
1104
- var stand = S.keysSorted.length + '|' + (S.keysSorted[0] || '');
1254
+ var stand = S.objektStand;
1105
1255
  if (alsQuelleKarte && alsQuelleStand === stand) { return alsQuelleKarte; }
1106
1256
  var fest = {}, zweig = {};
1107
1257
  mitVorspann('alias.').forEach(function (id) {
@@ -1123,7 +1273,75 @@ function alsQuelleKarten() {
1123
1273
  return alsQuelleKarte;
1124
1274
  }
1125
1275
 
1126
- export function aliasFuer(quelle) {
1276
+ /* Welche Quellen gehoeren zu genau diesem Ausgang?
1277
+
1278
+ An einem Mehrfachgeraet ist "der Alias dieses Geraets" keine
1279
+ beantwortbare Frage - es gibt je Ausgang einen, dazu oft einen fuer
1280
+ die gemeinsame Messung. Beantwortbar ist nur: welcher gehoert zu dem
1281
+ Ausgang, der gerade gewaehlt ist.
1282
+
1283
+ Geraten wird dabei nichts an Namen herum. Gefragt wird die Vorlage:
1284
+ welche ihrer Pfade tragen den Platzhalter? Genau die unterscheiden die
1285
+ Ausgaenge (bei `tasmota-mehrfach` `stat.POWER%N%` und `cmnd.POWER%N%`),
1286
+ alles andere (`tele.SENSOR`, `tele.LWT`) ist allen gemeinsam. Damit
1287
+ faellt auch die Falle aus I31 weg: `tele.INFO2` traegt zwar eine Zwei,
1288
+ aber keinen Platzhalter - es gehoert zu keinem Ausgang.
1289
+
1290
+ Fuer Ausgang 1 kommt das blanke `cmnd.POWER` dazu. Bei Tasmota ist
1291
+ POWER ohne Nummer ein Zweitname fuer POWER1 - am 16.09.2026 an der
1292
+ Steckdosenleiste nachgemessen: eine Abfrage an `cmnd/POWER` wird mit
1293
+ `{"POWER1":"ON"}` beantwortet, und `stat/POWER` gibt es nicht. Ein
1294
+ Alias, der so gebaut wurde, schaltet wirklich Ausgang 1 und gehoert
1295
+ dorthin. */
1296
+ export function ausgangsQuellen(e) {
1297
+ if (!e || e.instanz === null || e.instanz === undefined || !e.kanal) { return null; }
1298
+ var v = null;
1299
+ S.VORLAGEN.forEach(function (x) { if (x.id === e.vorlage) { v = x; } });
1300
+ if (!v || !v.mehrfach) { return null; }
1301
+ var platz = (v.mehrfach && v.mehrfach.platzhalter) || '%N%';
1302
+ var raus = [];
1303
+ var dazu = function (p) {
1304
+ if (!p) { return; }
1305
+ var voll = e.kanal + '.' + p;
1306
+ if (raus.indexOf(voll) === -1) { raus.push(voll); }
1307
+ };
1308
+ (v.zustaende || []).forEach(function (z) {
1309
+ [z.lesen, z.schreiben].forEach(function (p) {
1310
+ if (!p || String(p).indexOf(platz) === -1) { return; }
1311
+ dazu(setzeInstanz(p, v, e.instanz));
1312
+ if (String(e.instanz) === '1') { dazu(String(p).split(platz).join('')); }
1313
+ });
1314
+ });
1315
+ return raus.length ? raus : null;
1316
+ }
1317
+
1318
+ /* Mit `ausgang` - der Liste aus `ausgangsQuellen` - fragt sie enger: nur
1319
+ ein Alias, der wirklich aus einer dieser Quellen liest oder in eine
1320
+ schreibt, gehoert zu diesem Ausgang. Ohne das Argument, und das sind
1321
+ alle anderen sieben Aufrufer, bleibt sie Zeile fuer Zeile wie sie war.
1322
+
1323
+ Die Mehrheitsregel unten ist an einem Geraet mit einem Ausgang richtig
1324
+ und an einem Mehrfachgeraet die falsche Frage: An Ricardos
1325
+ Steckdosenleiste gewann `Verbrauch` mit sechs Punkten aus
1326
+ `tele.SENSOR` gegen `RF1000` mit zwei Punkten aus `stat.POWER1`, und
1327
+ der Entwurf fuer Ausgang 1 uebernahm dessen Haken. Im Musterfeld stand
1328
+ danach "✕ SET fehlt" an einem Geraet mit funktionierendem SET
1329
+ (C13, produktiv gemessen 16.09.2026).
1330
+
1331
+ Auch die Abkuerzung ueber `native.quelle` faellt in diesem Fall weg:
1332
+ die zeigt auf das GERAET, nicht auf den Ausgang. */
1333
+ export function aliasFuer(quelle, ausgang) {
1334
+ if (ausgang && ausgang.length) {
1335
+ var karteA = alsQuelleKarten();
1336
+ var zaehlA = {}, bestA = null, vielA = 0;
1337
+ ausgang.forEach(function (q0) {
1338
+ (karteA.zweig[q0] || []).forEach(function (kanal) {
1339
+ zaehlA[kanal] = (zaehlA[kanal] || 0) + 1;
1340
+ if (zaehlA[kanal] > vielA) { vielA = zaehlA[kanal]; bestA = kanal; }
1341
+ });
1342
+ });
1343
+ return bestA;
1344
+ }
1127
1345
  var karte = alsQuelleKarten();
1128
1346
  var treffer = karte.fest[quelle] || null;
1129
1347
  if (treffer) { return treffer; }
@@ -1170,6 +1388,22 @@ export function aliasFuer(quelle) {
1170
1388
  export function bestandVorrang(e, aliasId) {
1171
1389
  if (!e || !e.states || !aliasId) { return e; }
1172
1390
  var kanal = aliasId;
1391
+ /* `vorlagenWert` heisst, was es heisst: was die VORLAGE sagen wuerde.
1392
+ Ist keine in Kraft — abgewaehlt oder nie gefunden —, gibt es nichts,
1393
+ wovon etwas abweichen koennte, und die Kopie darf nicht entstehen.
1394
+
1395
+ Bis zum 17.09.2026 wurde sie unbesehen gesetzt, und dann log die
1396
+ Zeile: an `hm-rpc.2.NEQ1660737` nach „keine Vorlage" stand an SET
1397
+ „weicht von der Vorlage ab" und darunter „Die Vorlage will hier:
1398
+ switch" — waehrend im Alias `switch.light` steht, was auf das Muster
1399
+ `light` sogar passt. Verglichen wurde in Wahrheit gegen den
1400
+ Rohentwurf, also gegen die blanke Rolle des Quellpunkts, und als
1401
+ abweichend markiert ausgerechnet der bessere Wert. Dasselbe an einem
1402
+ Geraet, zu dem NIE eine Vorlage gefunden wurde (nachgestellt an
1403
+ `echt.Warmwasser`): auch dort erschien die Marke. Ricardo: „es gibt
1404
+ ja gar keine Vorlage mehr, man kann hoechstens vom Muster
1405
+ abweichen." */
1406
+ var mitVorlage = !!(e.vorschlag && e.vorlage);
1173
1407
  e.states.forEach(function (s) {
1174
1408
  if (!s.n) { return; }
1175
1409
  var o = S.objects[kanal + '.' + s.n];
@@ -1179,22 +1413,26 @@ export function bestandVorrang(e, aliasId) {
1179
1413
  anlegen (Ricardo, 25.08.2026: UNREACH und RSSI standen angehakt,
1180
1414
  obwohl sie im Alias fehlen). Anhaken kann man sie weiterhin, und
1181
1415
  der Chip „Vorlage" holt die Vorgabe der Vorlage zurueck. */
1182
- s.vorlagenWert = { on: s.on, role: s.role, typ: s.typ, unit: s.unit,
1183
- caption: s.caption, f: s.f, fw: s.fw,
1184
- srcR: s.srcR, srcW: s.srcW, states: s.states };
1416
+ if (mitVorlage) {
1417
+ s.vorlagenWert = { on: s.on, role: s.role, typ: s.typ, unit: s.unit,
1418
+ caption: s.caption, f: s.f, fw: s.fw,
1419
+ srcR: s.srcR, srcW: s.srcW, states: s.states };
1420
+ }
1185
1421
  s.on = false;
1186
1422
  return;
1187
1423
  }
1188
1424
  var c = o.common;
1189
1425
  var q = aliasQuellen(o);
1190
1426
  /* Was die Vorlage wollte - fuer den Vergleich und den Knopf. */
1191
- s.vorlagenWert = {
1192
- role: s.role, typ: s.typ, unit: s.unit, caption: s.caption,
1193
- f: s.f, fw: s.fw, srcR: s.srcR, srcW: s.srcW, states: s.states,
1194
- /* Auch der Haken der Vorlage - der Chip „Vorlage" stellt ihn wieder
1195
- her, nachdem der Bestand ihn ueberschrieben hat. */
1196
- on: s.on
1197
- };
1427
+ if (mitVorlage) {
1428
+ s.vorlagenWert = {
1429
+ role: s.role, typ: s.typ, unit: s.unit, caption: s.caption,
1430
+ f: s.f, fw: s.fw, srcR: s.srcR, srcW: s.srcW, states: s.states,
1431
+ /* Auch der Haken der Vorlage - der Chip „Vorlage" stellt ihn wieder
1432
+ her, nachdem der Bestand ihn ueberschrieben hat. */
1433
+ on: s.on
1434
+ };
1435
+ }
1198
1436
  var ist = zeileAusAlias(o, s.n);
1199
1437
  /* Rolle und Typ NUR, wenn sie im Alias ueberhaupt stehen.
1200
1438
 
@@ -1297,7 +1535,15 @@ export function vorlageAnwenden(e) {
1297
1535
  var anders = vorlagenAbweichung(s).length > 0;
1298
1536
  s.role = v.role; s.typ = v.typ; s.unit = v.unit;
1299
1537
  s.f = v.f; s.fw = v.fw; s.srcR = v.srcR; s.srcW = v.srcW;
1300
- s.states = v.states;
1538
+ /* Die Werteliste nur, wenn die Vorlage selbst eine mitbringt. Sagt sie
1539
+ nichts dazu, gilt der Bestand — dieselbe Regel wie bei Rolle und Typ
1540
+ in `bestandVorrang`. Vorher stand hier `s.states = v.states`, und an
1541
+ einer Vorlagenzeile ohne Werteliste hiess das `undefined`: am
1542
+ Rollladen verlor DIRECTION {0: NONE, 1: UP, 2: DOWN}, ohne dass
1543
+ Zaehler oder Marke es zeigten — `vorlagenAbweichung` vergleicht die
1544
+ Werteliste nicht, und die Werkbank hat kein Feld dafuer (Lauf vom
1545
+ 19.09.2026, G10). */
1546
+ if (v.states !== undefined) { s.states = v.states; }
1301
1547
  /* caption bleibt: der Anzeigename gehoert dem Nutzer. */
1302
1548
  if (anders) { s.geaendert = true; }
1303
1549
  });