iobroker.alias-workbench 0.9.10 → 0.9.12

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.
@@ -12,7 +12,7 @@ import { kindZustaende, aliasQuellen, holeWerte,
12
12
  mitVorspann, schreibQuelle, zeileAusAlias
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,23 @@ 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 nat = o.native || {};
93
+ var istTaster = (c.read === false && !!c.write) &&
94
+ (nat.TYPE === 'ACTION' || /^button/.test(String(c.role || '')));
78
95
  e.states.push({
79
96
  /* Bei einer Quelle wird aus stat.POWER der Zustand stat_POWER —
80
97
  ein Punkt im Namen wuerde sonst eine Unterebene im Alias
@@ -102,8 +119,39 @@ export function baueEntwurf(kanal) {
102
119
  states: c.states || undefined,
103
120
  wr: !!c.write,
104
121
  /* Ohne common.alias ist der Punkt seine eigene Quelle. Vorher stand
105
- hier nichts und man musste jede Zeile von Hand zuweisen. */
106
- srcR: istAlias ? (q.read || '') : id,
122
+ hier nichts und man musste jede Zeile von Hand zuweisen.
123
+
124
+ Ein Taster hat keine Lesequelle, obwohl `alias.id` eine Kennung
125
+ nennt: ioBroker verlangt sie, `common.read: false` sagt, dass dort
126
+ nichts abzuholen ist (K23). Dieselbe Ablesung wie in
127
+ `zeileAusAlias` — sonst zeigte „Alias bearbeiten" eine Quelle,
128
+ die „Alias anlegen" nie eingetragen hat. */
129
+ /* An der Quelle ist der Punkt seine eigene Lesequelle — ausser er
130
+ sagt selbst, dass dort nichts ankommt.
131
+
132
+ `read: false` heisst nicht „gesperrt": lesen kann den Punkt
133
+ jeder, er liegt in der Datenbank. Es heisst, dass nie eine
134
+ Aktualisierung eintrifft. Was drinsteht, ist der Wert, den
135
+ zuletzt jemand hineingeschrieben hat — meist der Anfangswert
136
+ beim Adapterstart. Ein Alias, der daraus liest, zeigt fuer immer
137
+ diesen einen Wert und sieht dabei aus wie ein Messwert.
138
+
139
+ Bei Homematic steht es in der Geraetebeschreibung: `OPERATIONS`
140
+ ist eine Bitmaske (1 lesen, 2 schreiben, 4 meldet von selbst),
141
+ und `2.OLD_LEVEL` traegt 2 bei `TYPE: ACTION` — ein Knopfdruck,
142
+ kein Zustand. Am Wohnzimmer_Dimmer sind das 15 von 43 Zeilen,
143
+ jede davon las und schrieb bisher auf denselben Befehlspunkt
144
+ (Ricardo, 17.09.2026).
145
+
146
+ Ist der Punkt beschreibbar, wird daraus also ein Taster: keine
147
+ Lesequelle, nur ein Schreibziel. Genau die Form, die K23/K26 am
148
+ Rollladen-STOP beschreiben — der Alias bekommt `read: false`,
149
+ und vis, Alexa und matter zeichnen einen Knopf statt eines
150
+ Schalters, der ewig auf „aus" steht. Kann der Punkt auch nicht
151
+ schreiben, bleibt er, wie er ist: dann ist die fehlende Meldung
152
+ wirklich ein Mangel, und die Marke sagt es. */
153
+ srcR: istAlias ? ((c.read === false && q.einfach) ? '' : (q.read || ''))
154
+ : (istTaster ? '' : id),
107
155
  /* Steht in `common.alias.id` ein einzelner Text, gilt er fuer Lesen
108
156
  UND Schreiben - dann ist die Schreibquelle dieselbe wie die
109
157
  Lesequelle. Hier stand frueher eine leere Zeichenkette, gedacht
@@ -559,8 +607,30 @@ export function steuerKanaele(dev) {
559
607
  if (!S.objects[id] || S.objects[id].type !== 'channel') { return; }
560
608
  var e2 = baueEntwurf(id);
561
609
  if (!e2.states.length) { return; }
562
- var f = erkenneEntwurf(e2, null).filter(function (x) { return x.type !== 'info'; });
563
- if (!f.length) { return; }
610
+ /* Auch Informationskanaele zaehlen mit (Ricardo, 17.09.2026).
611
+
612
+ Vorher fielen sie hier heraus — und damit sagte der Knopf am
613
+ Geschirrspueler „3 Stueck", waehrend im Baum daneben sechs Kanaele
614
+ standen. Wer `general` einzeln anklickt, bekommt laengst einen
615
+ Entwurf („Information ✓ 7/14 belegt") und kann ihn anlegen; nur
616
+ der Sammelweg liess ihn aus. Zwei Wege, zwei Antworten am selben
617
+ Geraet.
618
+
619
+ Der Zweck dieses Wegs ist, die Struktur der Quelle abzubilden —
620
+ dann gehoert jeder Kanal hinein, der fuer sich einen Entwurf
621
+ ergibt, und nicht nur der, den der Detektor fuer ein Geraet haelt.
622
+ Nebenbei faellt damit das Verteilen der „gemeinsamen" Kanaele in
623
+ den meisten Faellen weg: was einen eigenen Ordner bekommt, muss
624
+ nicht mehr in jeden anderen hineinkopiert werden (produktiv
625
+ gemessen: beim Maeher wanderten 206 Rohdatenpunkte in jeden der
626
+ vier Entwuerfe, beim Shelly SHEM-3 galten die drei Emeter-Phasen
627
+ als „gemeinsam" statt als eigene Kanaele). */
628
+ var alle = erkenneEntwurf(e2, null);
629
+ if (!alle.length) { return; }
630
+ /* Ein Kanal, der ein Geraet ergibt, wird danach benannt; nur wenn er
631
+ nichts als Information hergibt, gilt der info-Fund. */
632
+ var f = alle.filter(function (x) { return x.type !== 'info'; });
633
+ if (!f.length) { f = alle; }
564
634
  /* Nur zaehlen, was einen Pflichtplatz wirklich besetzt — sonst
565
635
  gilt jeder Kanal mit irgendeinem Messwert als Geraet. */
566
636
  /* Alles, was der Kanal an Plaetzen belegt — nicht nur die
@@ -584,92 +654,131 @@ export function steuerKanaele(dev) {
584
654
  return raus;
585
655
  }
586
656
 
587
- /* Ein fertiger Entwurf fuer einen Knoten, so wie ihn `waehle` erzeugen
588
- wuerde — Vorlage, sonst Rohentwurf mit erkanntem Muster und der
589
- Vorbelegung „nur was einen Platz hat". Gebraucht wird das, wenn aus
590
- mehreren Kanaelen auf einmal Aliase entstehen sollen. */
591
- /* Welche Kanaele eines Geraets gehoeren keinem einzelnen Verbraucher,
592
- sondern allen? Bei Homematic ist das Kanal 0 mit Batterie, Empfang und
593
- Erreichbarkeit — ein Funkmodul, zwei Lampen. */
594
- export function gemeinsameKanaele(dev, kanalGeraete) {
595
- var eigen = {};
596
- (kanalGeraete || []).forEach(function (k) { eigen[k.id] = 1; });
597
- var pre = dev + '.';
598
- var raus = [];
599
- S.keysSorted.forEach(function (id) {
600
- if (id.indexOf(pre) !== 0) { return; }
601
- if (id.slice(pre.length).indexOf('.') !== -1) { return; }
602
- if (!S.objects[id] || S.objects[id].type !== 'channel') { return; }
603
- if (eigen[id]) { return; }
604
- if (!kindZustaende(id).length) { return; }
605
- raus.push(id);
657
+ /* Welche Unterordner entstehen, wenn die Struktur der Quelle abgebildet
658
+ wird — und zwar nach den HAKEN, nicht nach einer Vermutung.
659
+
660
+ Bis 17.09.2026 baute dieser Weg je Kanal einen frischen Entwurf und
661
+ hakte an, was das Muster des Kanals hergab. Damit entstand, was
662
+ niemand ausgewaehlt hatte: am `MISCHKANAL` bekam der Wartungskanal
663
+ `LOW_BAT`, `RSSI_DEVICE` und `UNREACH`, obwohl oben nur `UNREACH`
664
+ angehakt war. Umgekehrt fiel weg, was jemand von Hand angehakt hatte —
665
+ der Entwurf wurde ja gar nicht gelesen (Ricardo, 17.09.2026).
666
+
667
+ Jetzt gilt: was angehakt ist, bestimmt beides — welche Ordner es gibt
668
+ und was darin steht. Ein Kanal ohne angehakte Zeile kommt nicht vor.
669
+ Zeilen, deren Quelle direkt am Geraet haengt oder von woanders kommt,
670
+ bleiben im Behaelter selbst (`eigen`). */
671
+ export function kanalGruppen(e) {
672
+ if (!e || !e.kanal || e.kanal.indexOf('alias.') === 0) { return []; }
673
+ var basis = e.kanal + '.';
674
+ var gruppen = [], nach = {}, eigen = [];
675
+ (e.states || []).forEach(function (st) {
676
+ if (!st.on || !st.n) { return; }
677
+ var quelle = st.srcR || st.srcW || '';
678
+ var rest = (quelle.indexOf(basis) === 0) ? quelle.slice(basis.length) : '';
679
+ var p = rest.indexOf('.');
680
+ /* Ohne Punkt liegt der Punkt direkt am Geraet; ohne `rest` liest die
681
+ Zeile von woanders. Beides gehoert in den Behaelter, nicht in einen
682
+ erfundenen Unterordner. */
683
+ if (!rest || p < 0) { eigen.push(st); return; }
684
+ var kid = e.kanal + '.' + rest.slice(0, p);
685
+ if (!S.objects[kid]) { eigen.push(st); return; }
686
+ if (!nach[kid]) {
687
+ var kn = eigenerKanalName(kid);
688
+ var kurz = rest.slice(0, p);
689
+ nach[kid] = { id: kid, kurz: kurz, name: kn || kurz,
690
+ kennung: kennungtauglich(kn || '') || kurz, zeilen: [] };
691
+ gruppen.push(nach[kid]);
692
+ }
693
+ nach[kid].zeilen.push(st);
606
694
  });
607
- return raus;
695
+ if (eigen.length) {
696
+ gruppen.unshift({ id: e.kanal, kurz: '', name: '', kennung: '',
697
+ eigen: true, zeilen: eigen });
698
+ }
699
+ return gruppen;
608
700
  }
609
701
 
610
- export function entwurfFuer(id, gemeinsam, gemAn) {
611
- var v = vorschlag(id);
612
- if (v) { return v; }
702
+ /* Der Entwurf ohne jede Vorlage — alle Punkte des Geraets, alle
703
+ angehakt, dazu das erkannte Muster als Anhaltspunkt.
704
+
705
+ Gebraucht fuer „— keine Vorlage —" in der Vorlagenwahl. Bis zum
706
+ 17.09.2026 liess sich eine erkannte Vorlage nicht abwaehlen: der erste
707
+ Eintrag der Liste war eine blosse Ueberschrift, sein Wert leer, und der
708
+ Zweig stieg bei leerem Wert sofort aus. Wechseln ging, weglassen nicht
709
+ (Ricardo).
710
+
711
+ Zuerst hakte dieser Weg an, was im erkannten Muster einen Platz hat.
712
+ Das ging an zwei GLEICHARTIGEN Kanaelen schief: am `hm.ZWEIKANAL`
713
+ nahm der Detektor einen der beiden Ausgaenge — und zwar den zweiten —,
714
+ `Licht_Esstisch` stand angehakt da und `Licht_Bar` leer. Welcher es
715
+ wird, ist Zufall, und danach zu raten ist nicht die Aufgabe eines
716
+ Entwurfs OHNE Vorlage. Wer sie abwaehlt, will die Punkte selbst
717
+ aussuchen; also stehen alle da (Ricardo, 17.09.2026). */
718
+ export function rohEntwurf(id) {
613
719
  var e2 = baueEntwurf(id);
614
720
  if (!e2.states.length) { return null; }
615
-
616
- /* Die Wartungsdaten des gemeinsamen Kanals gehoeren in jedes Geraet,
617
- nicht in eines davon oder in einen Kanal daneben.
618
-
619
- Ein Alias-Punkt ist ein Verweis, kein Wert — zwei Geraete duerfen
620
- auf dieselbe Quelle zeigen, und der Batteriestand des Funkmoduls
621
- gilt fuer beide Lampen gleichermassen. Lagerte man ihn in einen
622
- eigenen `info`-Kanal aus, haette keine der beiden eine
623
- Batteriewarnung: der Detektor schaut nur innerhalb eines Kanals.
624
- Genau so macht es die Tasmota-Mehrfachvorlage mit RSSI — jeder der
625
- vier Ausgaenge bekommt seinen eigenen, alle lesen aus tele/STATE. */
626
- (gemeinsam || []).forEach(function (gid) {
627
- kindZustaende(gid).forEach(function (qid) {
628
- var o = S.objects[qid]; if (!o) { return; }
629
- var c = o.common || {};
630
- var kurz = qid.slice(gid.length + 1).replace(/\./g, '_');
631
- /* Traegt der Kanal selbst schon einen Punkt dieses Namens, den
632
- gemeinsamen unterscheidbar machen. */
633
- if (e2.states.some(function (x) { return x.n === kurz; })) {
634
- kurz = gid.split('.').pop() + '_' + kurz;
635
- }
636
- e2.states.push({
637
- n: kurz, on: false, role: c.role || '', typ: c.type || '',
638
- unit: c.unit || '', states: c.states || undefined, wr: false,
639
- srcR: qid, srcW: '', f: '', fw: '', caption: '', manuell: false,
640
- gemeinsam: true
641
- });
642
- });
643
- });
644
721
  var f = erkenneEntwurf(e2, null);
645
722
  e2.wantAuto = f.length ? f[0].type : null;
646
723
  e2.want = e2.wantAuto;
647
- var funde = e2.want ? erkenneEntwurf(e2, e2.want) : f;
648
- var haupt = funde.length ? funde[0] : null;
649
- var platz = {};
650
- if (haupt) {
651
- haupt.states.forEach(function (x) {
652
- if (x.id) { platz[x.id.slice(id.length + 1)] = x.name; }
653
- });
654
- }
655
- if (Object.keys(platz).length) {
656
- e2.states.forEach(function (st) { st.on = !!platz[st.n]; });
657
- }
658
- /* Zum Schluss die Wartungspunkte, die auf der Geraeteebene einen Platz
659
- gefunden haben. Warum nicht einfach den Detektor auf dem Kanal
660
- fragen? Weil er dort nur SET und WORKING zuordnet, obwohl dieselben
661
- Punkte mit denselben Rollen im Abbild stehen — auf der Geraeteebene
662
- findet er LOWBAT, UNREACH und RSSI. Die Zuordnung von dort ist die
663
- belastbare; sie stammt aus demselben Detektor, nur aus dem Lauf, der
664
- sie sieht. */
665
- if (gemAn) {
666
- e2.states.forEach(function (st) {
667
- if (st.gemeinsam && gemAn[st.srcR]) { st.on = true; }
668
- });
669
- }
670
724
  return merkeHakenVorgabe(e2);
671
725
  }
672
726
 
727
+ /* Wohin die Unterordner kommen — der Behaelter fuer das Geraet.
728
+
729
+ Beim ersten Mal ist das schlicht `zielOrdner` plus Geraetename. Nach
730
+ dem Aufteilen gibt es aber mehrere Aliase zu derselben Quelle, je Kanal
731
+ einen, und `aliasFuer` liefert einen davon — den mit den meisten
732
+ Punkten. Der Behaelter waere dann dieser Unterordner, und der zweite
733
+ Druck haengte alles eine Ebene tiefer: aus
734
+ `alias.0.Geschirrspueler.Befehle` wurde
735
+ `alias.0.Geschirrspueler.Allgemeine_Informationen.Befehle`, beim
736
+ dritten Druck noch eine Ebene tiefer (17.09.2026).
737
+
738
+ Woran man es erkennt: das gefundene Ziel fuehrt in `native.quelle`
739
+ einen KANAL der Quelle, nicht das Geraet. Dann ist es selbst ein
740
+ Unterordner aus einem frueheren Durchgang, und der Behaelter ist sein
741
+ Elternknoten. */
742
+ export function aufteilBehaelter(e) {
743
+ var ordner = (e.zielOrdner || 'alias.0') + '.' + e.zielName;
744
+ var ziel = e.ziel;
745
+ if (!ziel) { return ordner; }
746
+ var q = ((S.objects[ziel] && S.objects[ziel].native) || {}).quelle;
747
+ if (q && e.kanal && q.indexOf(e.kanal + '.') === 0) {
748
+ var p = ziel.lastIndexOf('.');
749
+ if (p > 0) { return ziel.slice(0, p); }
750
+ }
751
+ return ordner;
752
+ }
753
+
754
+ /* Aus einer Gruppe wird ein Entwurf: dieselben Zeilen, nur auf den Kanal
755
+ bezogen benannt. Was jemand an einer Zeile geaendert hat — Rolle, Typ,
756
+ Einheit, Formel, Beschriftung — wandert unveraendert mit; es waere
757
+ sonderbar, eine Handeingabe beim Aufteilen wegzuwerfen. */
758
+ export function teilEntwurf(e, g) {
759
+ var v = { kanal: g.id, states: [], want: null, wantAuto: null,
760
+ alleMuster: false, raum: e.raum, funktion: e.funktion };
761
+ g.zeilen.forEach(function (st) {
762
+ var k = JSON.parse(JSON.stringify(st));
763
+ /* Der Name wird auf den Kanal bezogen: aus `commands_BSH_…` wird im
764
+ Kanal `Befehle` schlicht `BSH_…`. Gekuerzt wird ueber die Quelle —
765
+ und bei einem Taster ist das die SCHREIBquelle, denn `srcR` ist
766
+ dort leer. Ohne diesen Zusatz behielt genau diese Zeile ihren
767
+ langen Namen: `…Befehle.commands_BSH_Common_Command_StopProgram`
768
+ neben `…Allgemeine_Informationen.brand` (17.09.2026). */
769
+ var quelle = st.srcR || st.srcW || '';
770
+ if (!g.eigen && quelle && quelle.indexOf(g.id + '.') === 0) {
771
+ k.n = quelle.slice(g.id.length + 1).replace(/\./g, '_');
772
+ }
773
+ k.on = true;
774
+ v.states.push(k);
775
+ });
776
+ var f = erkenneEntwurf(v, null);
777
+ v.wantAuto = f.length ? f[0].type : null;
778
+ v.want = v.wantAuto;
779
+ return merkeHakenVorgabe(v);
780
+ }
781
+
673
782
  /* Alle laufenden Abos abmelden.
674
783
 
675
784
  Stand nur in `waehle` — und damit meldete kein einziger der Wege ab,
@@ -684,6 +793,108 @@ export function aboLoesen() {
684
793
  zeigeAbozahl();
685
794
  }
686
795
 
796
+ /* Die Quellen eines Entwurfs - alles, woraus eine Zeile liest oder
797
+ wohin sie schreibt. */
798
+ export function quellenVonEntwurf(e) {
799
+ var raus = [];
800
+ ((e && e.states) || []).forEach(function (s) {
801
+ if (s.srcR) { raus.push(s.srcR); }
802
+ if (s.srcW) { raus.push(s.srcW); }
803
+ });
804
+ return raus;
805
+ }
806
+
807
+ /* Abonniert Quellen dazu, die rechts angezeigt werden.
808
+
809
+ Ergaenzt nur: was ein laufendes Muster schon deckt, kommt nicht noch
810
+ einmal dazu, doppelt rufen schadet also nicht. Die Regeln stammen aus
811
+ `waehle`, wo sie vorher fest verbaut waren und nur am Alias griffen.
812
+
813
+ `quellenVerteilung` fasst zusammen, was die rechte Seite als
814
+ „Quellen" zeigt — dafuer bildet sie den ELTERNknoten jeder Quelle.
815
+ Bei einem flachen MQTT-Geraet ist das der halbe Adapter: gemessen
816
+ 09.09.2026 wurde daraus `mqtt-client.0.*` mit 322 Objekten, an einem
817
+ einzigen Alias. Abonniert wird deshalb der Kanal jeder wirklich
818
+ gelesenen Quelle. */
819
+ export function aboErgaenzen(quellen) {
820
+ var muster = (S.abo || []).slice();
821
+ var vorher = muster.length;
822
+ var drinSchon = function (was) {
823
+ return muster.some(function (m0) {
824
+ var p0 = m0.slice(-2) === '.*' ? m0.slice(0, -2) : m0;
825
+ return was === p0 || was.indexOf(p0 + '.') === 0;
826
+ });
827
+ };
828
+ var offen = [];
829
+ (quellen || []).forEach(function (x) {
830
+ if (x && !drinSchon(x) && offen.indexOf(x) === -1) { offen.push(x); }
831
+ });
832
+ if (!offen.length) { zeigeAbozahl(); return; }
833
+
834
+ var kanaele = [];
835
+ offen.forEach(function (x) {
836
+ var t = String(x).split('.');
837
+ t.pop();
838
+ var kn = t.join('.');
839
+ if (kn && kanaele.indexOf(kn) === -1) { kanaele.push(kn); }
840
+ });
841
+ var SCHWELLE = 30;
842
+ var wieBreit = function (knoten) {
843
+ var n = 0;
844
+ S.keysSorted.forEach(function (k0) { if (k0.indexOf(knoten + '.') === 0) { n++; } });
845
+ return n;
846
+ };
847
+
848
+ /* Sparten desselben Geraets zu einem Muster zusammenfassen.
849
+
850
+ Ein Tasmota-Alias liest aus `…Licht.stat`, `…Licht.tele` und
851
+ `…Licht.cmnd` — drei Kanaele, ein Geraet, und in der Kopfleiste
852
+ gehoert dort „ein Abo" zu stehen, nicht drei. Zusammengefasst wird
853
+ aber nur, wo wirklich MEHRERE Kanaele unter demselben Knoten
854
+ haengen: Sonst waere aus `…Garten.Weihnachten_aussen` schlicht
855
+ `…Garten` geworden — breiter als noetig, ohne dass etwas
856
+ zusammenfaellt. */
857
+ for (var runde = 0; runde < 4; runde++) {
858
+ var unter = {};
859
+ kanaele.forEach(function (kn) {
860
+ var t = kn.split('.');
861
+ if (t.length <= 3) { return; }
862
+ var ob = t.slice(0, -1).join('.');
863
+ (unter[ob] = unter[ob] || []).push(kn);
864
+ });
865
+ var geaendert = false;
866
+ Object.keys(unter).forEach(function (ob) {
867
+ if (unter[ob].length < 2 || wieBreit(ob) > SCHWELLE) { return; }
868
+ kanaele = kanaele.filter(function (kn) { return unter[ob].indexOf(kn) === -1; });
869
+ if (kanaele.indexOf(ob) === -1) { kanaele.push(ob); }
870
+ geaendert = true;
871
+ });
872
+ if (!geaendert) { break; }
873
+ }
874
+
875
+ /* Ein schmaler Kanal wird als Muster abonniert, ein breiter nicht.
876
+
877
+ Bei einem flachen Geraet IST der Kanal der breite Knoten: Die
878
+ Quelle `mqtt-client.0.SmartHome.Lavalampe_POWER` liegt direkt
879
+ unter `SmartHome`, und `SmartHome.*` sind 320 Objekte (gemessen
880
+ 09.09.2026). Ab dieser Schwelle werden die Quellen einzeln
881
+ abonniert — ein paar Abos mehr, dafuer genau die, deren Werte auch
882
+ angezeigt werden. */
883
+ kanaele.forEach(function (kn) {
884
+ if (drinSchon(kn)) { return; }
885
+ if (wieBreit(kn) > SCHWELLE) { return; }
886
+ muster.push(kn + '.*');
887
+ });
888
+ offen.forEach(function (x) {
889
+ if (!drinSchon(x) && muster.indexOf(x) === -1) { muster.push(x); }
890
+ });
891
+
892
+ var neu = muster.slice(vorher);
893
+ S.abo = muster;
894
+ neu.forEach(function (m) { socket.emit('subscribe', m); });
895
+ zeigeAbozahl();
896
+ }
897
+
687
898
  export function waehle(id, fertig) {
688
899
  /* Wer links etwas anderes anklickt, will es rechts sehen - auch wenn
689
900
  der Fokus noch in einem Feld der rechten Seite steht. `tipptGerade`
@@ -737,109 +948,28 @@ export function waehle(id, fertig) {
737
948
  `alias.0.Solar.Balkon.Energie`, wo sich im Sekundentakt etwas
738
949
  aendert, fiel es auf (Ricardo, 08.09.2026).
739
950
 
740
- Deshalb am Alias zusaetzlich seine Quellen - `quellenVerteilung`
741
- weiss ja, welche das sind. Und deshalb eine Liste: sonst bliebe
742
- beim naechsten Wechsel die Haelfte abonniert und die Werkbank
743
- bekaeme mit der Zeit Ereignisse fuer das halbe System. Die Zahl der
744
- laufenden Abos steht in der Kopfleiste - dort sieht man sofort, ob
745
- sich etwas ansammelt. */
951
+ Deshalb am Alias zusaetzlich seine Quellen, und - seit dem
952
+ 18.09.2026 - an der Quelle ebenso: dort ergaenzt `uebernehmeBestand`
953
+ die Zeilen des vorhandenen Alias, und die lesen oft woanders
954
+ (`Stromzaehler` zeigt drei Zeilen „aus Solar.Netz"). Die standen
955
+ nach dem ersten Abholen still, gemessen 428 W gegen 22 W am System
956
+ (J9). `aboErgaenzen` wird darum von jeder Stelle gerufen, an der
957
+ Zeilen mit neuen Quellen dazukommen. Eine Liste, damit beim
958
+ naechsten Wechsel `aboLoesen` alles abmeldet. */
746
959
  aboLoesen();
747
- var muster = [id + '.*'];
960
+ S.abo = [id + '.*'];
961
+ socket.emit('subscribe', id + '.*');
748
962
  if (id.indexOf('alias.') === 0) {
749
- /* Aus den Quellen, nicht aus der Anzeige-Verteilung.
750
-
751
- `quellenVerteilung` fasst zusammen, was die rechte Seite als
752
- „Quellen" zeigt — dafuer bildet sie den ELTERNknoten jeder
753
- Quelle. Bei einem flachen MQTT-Geraet ist das der halbe Adapter:
754
- gemessen 09.09.2026 wurde daraus `mqtt-client.0.*` mit 322
755
- Objekten, an einem einzigen Alias. Und `S.werte` wird nie
756
- geleert, das sammelt sich also.
757
-
758
- Abonniert wird deshalb der Kanal jeder wirklich gelesenen Quelle.
759
- Wo die Quellen ohnehin gebuendelt liegen, bleibt es bei einem
760
- Muster; wo nicht, sind es ein paar mehr — aber jedes davon eng. */
761
- var kanaele = [];
963
+ var ausAlias = [];
762
964
  kindZustaende(id).forEach(function (kid) {
763
965
  var q = aliasQuellen(S.objects[kid]);
764
- [q.read, q.write].forEach(function (x) {
765
- if (!x) { return; }
766
- var t = String(x).split('.');
767
- t.pop();
768
- var kn = t.join('.');
769
- if (kn && kanaele.indexOf(kn) === -1) { kanaele.push(kn); }
770
- });
771
- });
772
- /* Ein schmaler Kanal wird als Muster abonniert, ein breiter nicht.
773
-
774
- Bei einem flachen Geraet IST der Kanal der breite Knoten: Die
775
- Quelle `mqtt-client.0.SmartHome.Lavalampe_POWER` liegt direkt
776
- unter `SmartHome`, und `SmartHome.*` sind 320 Objekte. Ab dieser
777
- Schwelle werden die Quellen einzeln abonniert — ein paar Abos
778
- mehr, dafuer genau die, deren Werte auch angezeigt werden. */
779
- var SCHWELLE = 30;
780
- var wieBreit = function (knoten) {
781
- var n = 0;
782
- S.keysSorted.forEach(function (k0) { if (k0.indexOf(knoten + '.') === 0) { n++; } });
783
- return n;
784
- };
785
- var drinSchon = function (was) {
786
- return muster.some(function (m0) {
787
- var p0 = m0.slice(-2) === '.*' ? m0.slice(0, -2) : m0;
788
- return was === p0 || was.indexOf(p0 + '.') === 0;
789
- });
790
- };
791
-
792
- /* Sparten desselben Geraets zu einem Muster zusammenfassen.
793
-
794
- Ein Tasmota-Alias liest aus `…Licht.stat`, `…Licht.tele` und
795
- `…Licht.cmnd` — drei Kanaele, ein Geraet, und in der Kopfleiste
796
- gehoert dort „ein Abo" zu stehen, nicht drei. Zusammengefasst wird
797
- aber nur, wo wirklich MEHRERE Kanaele unter demselben Knoten
798
- haengen: Sonst waere aus `…Garten.Weihnachten_aussen` schlicht
799
- `…Garten` geworden — breiter als noetig, ohne dass etwas
800
- zusammenfaellt. */
801
- for (var runde = 0; runde < 4; runde++) {
802
- var unter = {};
803
- kanaele.forEach(function (kn) {
804
- var t = kn.split('.');
805
- if (t.length <= 3) { return; }
806
- var ob = t.slice(0, -1).join('.');
807
- (unter[ob] = unter[ob] || []).push(kn);
808
- });
809
- var geaendert = false;
810
- Object.keys(unter).forEach(function (ob) {
811
- if (unter[ob].length < 2 || wieBreit(ob) > SCHWELLE) { return; }
812
- kanaele = kanaele.filter(function (kn) { return unter[ob].indexOf(kn) === -1; });
813
- if (kanaele.indexOf(ob) === -1) { kanaele.push(ob); }
814
- geaendert = true;
815
- });
816
- if (!geaendert) { break; }
817
- }
818
-
819
- /* Ein schmaler Kanal wird als Muster abonniert, ein breiter nicht.
820
-
821
- Bei einem flachen Geraet IST der Kanal der breite Knoten: Die
822
- Quelle `mqtt-client.0.SmartHome.Lavalampe_POWER` liegt direkt
823
- unter `SmartHome`, und `SmartHome.*` sind 320 Objekte (gemessen
824
- 09.09.2026; vorher wurde daraus sogar `mqtt-client.0.*` mit 322).
825
- Ab dieser Schwelle werden die Quellen einzeln abonniert — ein paar
826
- Abos mehr, dafuer genau die, deren Werte auch angezeigt werden. */
827
- kanaele.forEach(function (kn) {
828
- if (drinSchon(kn)) { return; }
829
- if (wieBreit(kn) > SCHWELLE) { return; }
830
- muster.push(kn + '.*');
831
- });
832
- /* Was jetzt noch von keinem Muster gedeckt ist, kommt einzeln. */
833
- kindZustaende(id).forEach(function (kid) {
834
- var q = aliasQuellen(S.objects[kid]);
835
- [q.read, q.write].forEach(function (x) {
836
- if (x && !drinSchon(x) && muster.indexOf(x) === -1) { muster.push(x); }
837
- });
966
+ if (q.read) { ausAlias.push(q.read); }
967
+ if (q.write) { ausAlias.push(q.write); }
838
968
  });
969
+ aboErgaenzen(ausAlias);
970
+ } else {
971
+ zeigeAbozahl();
839
972
  }
840
- S.abo = muster;
841
- muster.forEach(function (m) { socket.emit('subscribe', m); });
842
- zeigeAbozahl();
843
973
 
844
974
  /* Erst die Werte, dann der Vorschlag. Die Erkennung schaut in den
845
975
  Rohwert von tele.SENSOR hinein — ohne Werte haelt sie ein Geraet
@@ -916,10 +1046,14 @@ export function waehle(id, fertig) {
916
1046
  if (fest) { v.vonHandGewaehlt = false; v.ausBestand = true; }
917
1047
  /* Gibt es den Alias schon, gilt sein Stand - in beiden Ansichten
918
1048
  dasselbe Bild (Ricardo, 25.08.2026). */
919
- if (vorh && S.objects[vorh]) { bestandVorrang(v, vorh); }
1049
+ /* An einem Mehrfachgeraet zaehlt der Alias DIESES Ausgangs,
1050
+ nicht der mit den meisten Punkten (C13). */
1051
+ var vorhA = aliasFuer(id, ausgangsQuellen(v)) || (v.instanz === null || v.instanz === undefined ? vorh : null);
1052
+ if (vorhA && S.objects[vorhA]) { bestandVorrang(v, vorhA); }
920
1053
  S.entwurf = v;
921
1054
  }
922
1055
  }
1056
+ aboErgaenzen(quellenVonEntwurf(S.entwurf));
923
1057
  var nach = [];
924
1058
  S.entwurf.states.forEach(function (s) {
925
1059
  if (s.srcR && !S.werte[s.srcR]) { nach.push(s.srcR); }
@@ -1117,7 +1251,75 @@ function alsQuelleKarten() {
1117
1251
  return alsQuelleKarte;
1118
1252
  }
1119
1253
 
1120
- export function aliasFuer(quelle) {
1254
+ /* Welche Quellen gehoeren zu genau diesem Ausgang?
1255
+
1256
+ An einem Mehrfachgeraet ist "der Alias dieses Geraets" keine
1257
+ beantwortbare Frage - es gibt je Ausgang einen, dazu oft einen fuer
1258
+ die gemeinsame Messung. Beantwortbar ist nur: welcher gehoert zu dem
1259
+ Ausgang, der gerade gewaehlt ist.
1260
+
1261
+ Geraten wird dabei nichts an Namen herum. Gefragt wird die Vorlage:
1262
+ welche ihrer Pfade tragen den Platzhalter? Genau die unterscheiden die
1263
+ Ausgaenge (bei `tasmota-mehrfach` `stat.POWER%N%` und `cmnd.POWER%N%`),
1264
+ alles andere (`tele.SENSOR`, `tele.LWT`) ist allen gemeinsam. Damit
1265
+ faellt auch die Falle aus I31 weg: `tele.INFO2` traegt zwar eine Zwei,
1266
+ aber keinen Platzhalter - es gehoert zu keinem Ausgang.
1267
+
1268
+ Fuer Ausgang 1 kommt das blanke `cmnd.POWER` dazu. Bei Tasmota ist
1269
+ POWER ohne Nummer ein Zweitname fuer POWER1 - am 16.09.2026 an der
1270
+ Steckdosenleiste nachgemessen: eine Abfrage an `cmnd/POWER` wird mit
1271
+ `{"POWER1":"ON"}` beantwortet, und `stat/POWER` gibt es nicht. Ein
1272
+ Alias, der so gebaut wurde, schaltet wirklich Ausgang 1 und gehoert
1273
+ dorthin. */
1274
+ export function ausgangsQuellen(e) {
1275
+ if (!e || e.instanz === null || e.instanz === undefined || !e.kanal) { return null; }
1276
+ var v = null;
1277
+ S.VORLAGEN.forEach(function (x) { if (x.id === e.vorlage) { v = x; } });
1278
+ if (!v || !v.mehrfach) { return null; }
1279
+ var platz = (v.mehrfach && v.mehrfach.platzhalter) || '%N%';
1280
+ var raus = [];
1281
+ var dazu = function (p) {
1282
+ if (!p) { return; }
1283
+ var voll = e.kanal + '.' + p;
1284
+ if (raus.indexOf(voll) === -1) { raus.push(voll); }
1285
+ };
1286
+ (v.zustaende || []).forEach(function (z) {
1287
+ [z.lesen, z.schreiben].forEach(function (p) {
1288
+ if (!p || String(p).indexOf(platz) === -1) { return; }
1289
+ dazu(setzeInstanz(p, v, e.instanz));
1290
+ if (String(e.instanz) === '1') { dazu(String(p).split(platz).join('')); }
1291
+ });
1292
+ });
1293
+ return raus.length ? raus : null;
1294
+ }
1295
+
1296
+ /* Mit `ausgang` - der Liste aus `ausgangsQuellen` - fragt sie enger: nur
1297
+ ein Alias, der wirklich aus einer dieser Quellen liest oder in eine
1298
+ schreibt, gehoert zu diesem Ausgang. Ohne das Argument, und das sind
1299
+ alle anderen sieben Aufrufer, bleibt sie Zeile fuer Zeile wie sie war.
1300
+
1301
+ Die Mehrheitsregel unten ist an einem Geraet mit einem Ausgang richtig
1302
+ und an einem Mehrfachgeraet die falsche Frage: An Ricardos
1303
+ Steckdosenleiste gewann `Verbrauch` mit sechs Punkten aus
1304
+ `tele.SENSOR` gegen `RF1000` mit zwei Punkten aus `stat.POWER1`, und
1305
+ der Entwurf fuer Ausgang 1 uebernahm dessen Haken. Im Musterfeld stand
1306
+ danach "✕ SET fehlt" an einem Geraet mit funktionierendem SET
1307
+ (C13, produktiv gemessen 16.09.2026).
1308
+
1309
+ Auch die Abkuerzung ueber `native.quelle` faellt in diesem Fall weg:
1310
+ die zeigt auf das GERAET, nicht auf den Ausgang. */
1311
+ export function aliasFuer(quelle, ausgang) {
1312
+ if (ausgang && ausgang.length) {
1313
+ var karteA = alsQuelleKarten();
1314
+ var zaehlA = {}, bestA = null, vielA = 0;
1315
+ ausgang.forEach(function (q0) {
1316
+ (karteA.zweig[q0] || []).forEach(function (kanal) {
1317
+ zaehlA[kanal] = (zaehlA[kanal] || 0) + 1;
1318
+ if (zaehlA[kanal] > vielA) { vielA = zaehlA[kanal]; bestA = kanal; }
1319
+ });
1320
+ });
1321
+ return bestA;
1322
+ }
1121
1323
  var karte = alsQuelleKarten();
1122
1324
  var treffer = karte.fest[quelle] || null;
1123
1325
  if (treffer) { return treffer; }
@@ -1164,6 +1366,22 @@ export function aliasFuer(quelle) {
1164
1366
  export function bestandVorrang(e, aliasId) {
1165
1367
  if (!e || !e.states || !aliasId) { return e; }
1166
1368
  var kanal = aliasId;
1369
+ /* `vorlagenWert` heisst, was es heisst: was die VORLAGE sagen wuerde.
1370
+ Ist keine in Kraft — abgewaehlt oder nie gefunden —, gibt es nichts,
1371
+ wovon etwas abweichen koennte, und die Kopie darf nicht entstehen.
1372
+
1373
+ Bis zum 17.09.2026 wurde sie unbesehen gesetzt, und dann log die
1374
+ Zeile: an `hm-rpc.2.NEQ1660737` nach „keine Vorlage" stand an SET
1375
+ „weicht von der Vorlage ab" und darunter „Die Vorlage will hier:
1376
+ switch" — waehrend im Alias `switch.light` steht, was auf das Muster
1377
+ `light` sogar passt. Verglichen wurde in Wahrheit gegen den
1378
+ Rohentwurf, also gegen die blanke Rolle des Quellpunkts, und als
1379
+ abweichend markiert ausgerechnet der bessere Wert. Dasselbe an einem
1380
+ Geraet, zu dem NIE eine Vorlage gefunden wurde (nachgestellt an
1381
+ `echt.Warmwasser`): auch dort erschien die Marke. Ricardo: „es gibt
1382
+ ja gar keine Vorlage mehr, man kann hoechstens vom Muster
1383
+ abweichen." */
1384
+ var mitVorlage = !!(e.vorschlag && e.vorlage);
1167
1385
  e.states.forEach(function (s) {
1168
1386
  if (!s.n) { return; }
1169
1387
  var o = S.objects[kanal + '.' + s.n];
@@ -1173,22 +1391,26 @@ export function bestandVorrang(e, aliasId) {
1173
1391
  anlegen (Ricardo, 25.08.2026: UNREACH und RSSI standen angehakt,
1174
1392
  obwohl sie im Alias fehlen). Anhaken kann man sie weiterhin, und
1175
1393
  der Chip „Vorlage" holt die Vorgabe der Vorlage zurueck. */
1176
- s.vorlagenWert = { on: s.on, role: s.role, typ: s.typ, unit: s.unit,
1177
- caption: s.caption, f: s.f, fw: s.fw,
1178
- srcR: s.srcR, srcW: s.srcW, states: s.states };
1394
+ if (mitVorlage) {
1395
+ s.vorlagenWert = { on: s.on, role: s.role, typ: s.typ, unit: s.unit,
1396
+ caption: s.caption, f: s.f, fw: s.fw,
1397
+ srcR: s.srcR, srcW: s.srcW, states: s.states };
1398
+ }
1179
1399
  s.on = false;
1180
1400
  return;
1181
1401
  }
1182
1402
  var c = o.common;
1183
1403
  var q = aliasQuellen(o);
1184
1404
  /* Was die Vorlage wollte - fuer den Vergleich und den Knopf. */
1185
- s.vorlagenWert = {
1186
- role: s.role, typ: s.typ, unit: s.unit, caption: s.caption,
1187
- f: s.f, fw: s.fw, srcR: s.srcR, srcW: s.srcW, states: s.states,
1188
- /* Auch der Haken der Vorlage - der Chip „Vorlage" stellt ihn wieder
1189
- her, nachdem der Bestand ihn ueberschrieben hat. */
1190
- on: s.on
1191
- };
1405
+ if (mitVorlage) {
1406
+ s.vorlagenWert = {
1407
+ role: s.role, typ: s.typ, unit: s.unit, caption: s.caption,
1408
+ f: s.f, fw: s.fw, srcR: s.srcR, srcW: s.srcW, states: s.states,
1409
+ /* Auch der Haken der Vorlage - der Chip „Vorlage" stellt ihn wieder
1410
+ her, nachdem der Bestand ihn ueberschrieben hat. */
1411
+ on: s.on
1412
+ };
1413
+ }
1192
1414
  var ist = zeileAusAlias(o, s.n);
1193
1415
  /* Rolle und Typ NUR, wenn sie im Alias ueberhaupt stehen.
1194
1416
 
@@ -1214,7 +1436,11 @@ export function bestandVorrang(e, aliasId) {
1214
1436
  if (c.name !== undefined) { s.caption = ist.caption; }
1215
1437
  s.f = ist.f;
1216
1438
  s.fw = ist.fw;
1217
- if (q.read) { s.srcR = q.read; }
1439
+ /* `ist.srcR`, nicht `q.read`: bei einem Taster laesst
1440
+ `zeileAusAlias` die Lesequelle bewusst leer, obwohl `alias.id` eine
1441
+ Kennung nennt (K23). Die Wache auf `q.read` bleibt — sie verhindert,
1442
+ dass ein Alias ohne Quellangabe die vorhandene ueberschreibt. */
1443
+ if (q.read) { s.srcR = ist.srcR; }
1218
1444
  s.srcW = ist.srcW;
1219
1445
  s.on = true;
1220
1446
  s.ausBestand = true;