iobroker.alias-workbench 0.9.11 → 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
@@ -109,7 +126,32 @@ export function baueEntwurf(kanal) {
109
126
  nichts abzuholen ist (K23). Dieselbe Ablesung wie in
110
127
  `zeileAusAlias` — sonst zeigte „Alias bearbeiten" eine Quelle,
111
128
  die „Alias anlegen" nie eingetragen hat. */
112
- srcR: istAlias ? ((c.read === false && q.einfach) ? '' : (q.read || '')) : id,
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),
113
155
  /* Steht in `common.alias.id` ein einzelner Text, gilt er fuer Lesen
114
156
  UND Schreiben - dann ist die Schreibquelle dieselbe wie die
115
157
  Lesequelle. Hier stand frueher eine leere Zeichenkette, gedacht
@@ -565,8 +607,30 @@ export function steuerKanaele(dev) {
565
607
  if (!S.objects[id] || S.objects[id].type !== 'channel') { return; }
566
608
  var e2 = baueEntwurf(id);
567
609
  if (!e2.states.length) { return; }
568
- var f = erkenneEntwurf(e2, null).filter(function (x) { return x.type !== 'info'; });
569
- 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; }
570
634
  /* Nur zaehlen, was einen Pflichtplatz wirklich besetzt — sonst
571
635
  gilt jeder Kanal mit irgendeinem Messwert als Geraet. */
572
636
  /* Alles, was der Kanal an Plaetzen belegt — nicht nur die
@@ -590,92 +654,131 @@ export function steuerKanaele(dev) {
590
654
  return raus;
591
655
  }
592
656
 
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);
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);
612
694
  });
613
- return raus;
695
+ if (eigen.length) {
696
+ gruppen.unshift({ id: e.kanal, kurz: '', name: '', kennung: '',
697
+ eigen: true, zeilen: eigen });
698
+ }
699
+ return gruppen;
614
700
  }
615
701
 
616
- export function entwurfFuer(id, gemeinsam, gemAn) {
617
- var v = vorschlag(id);
618
- 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) {
619
719
  var e2 = baueEntwurf(id);
620
720
  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
721
  var f = erkenneEntwurf(e2, null);
651
722
  e2.wantAuto = f.length ? f[0].type : null;
652
723
  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
724
  return merkeHakenVorgabe(e2);
677
725
  }
678
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
+
679
782
  /* Alle laufenden Abos abmelden.
680
783
 
681
784
  Stand nur in `waehle` — und damit meldete kein einziger der Wege ab,
@@ -690,6 +793,108 @@ export function aboLoesen() {
690
793
  zeigeAbozahl();
691
794
  }
692
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
+
693
898
  export function waehle(id, fertig) {
694
899
  /* Wer links etwas anderes anklickt, will es rechts sehen - auch wenn
695
900
  der Fokus noch in einem Feld der rechten Seite steht. `tipptGerade`
@@ -743,109 +948,28 @@ export function waehle(id, fertig) {
743
948
  `alias.0.Solar.Balkon.Energie`, wo sich im Sekundentakt etwas
744
949
  aendert, fiel es auf (Ricardo, 08.09.2026).
745
950
 
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. */
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. */
752
959
  aboLoesen();
753
- var muster = [id + '.*'];
960
+ S.abo = [id + '.*'];
961
+ socket.emit('subscribe', id + '.*');
754
962
  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 = [];
963
+ var ausAlias = [];
768
964
  kindZustaende(id).forEach(function (kid) {
769
965
  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. */
839
- kindZustaende(id).forEach(function (kid) {
840
- 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
- });
966
+ if (q.read) { ausAlias.push(q.read); }
967
+ if (q.write) { ausAlias.push(q.write); }
844
968
  });
969
+ aboErgaenzen(ausAlias);
970
+ } else {
971
+ zeigeAbozahl();
845
972
  }
846
- S.abo = muster;
847
- muster.forEach(function (m) { socket.emit('subscribe', m); });
848
- zeigeAbozahl();
849
973
 
850
974
  /* Erst die Werte, dann der Vorschlag. Die Erkennung schaut in den
851
975
  Rohwert von tele.SENSOR hinein — ohne Werte haelt sie ein Geraet
@@ -922,10 +1046,14 @@ export function waehle(id, fertig) {
922
1046
  if (fest) { v.vonHandGewaehlt = false; v.ausBestand = true; }
923
1047
  /* Gibt es den Alias schon, gilt sein Stand - in beiden Ansichten
924
1048
  dasselbe Bild (Ricardo, 25.08.2026). */
925
- 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); }
926
1053
  S.entwurf = v;
927
1054
  }
928
1055
  }
1056
+ aboErgaenzen(quellenVonEntwurf(S.entwurf));
929
1057
  var nach = [];
930
1058
  S.entwurf.states.forEach(function (s) {
931
1059
  if (s.srcR && !S.werte[s.srcR]) { nach.push(s.srcR); }
@@ -1123,7 +1251,75 @@ function alsQuelleKarten() {
1123
1251
  return alsQuelleKarte;
1124
1252
  }
1125
1253
 
1126
- 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
+ }
1127
1323
  var karte = alsQuelleKarten();
1128
1324
  var treffer = karte.fest[quelle] || null;
1129
1325
  if (treffer) { return treffer; }
@@ -1170,6 +1366,22 @@ export function aliasFuer(quelle) {
1170
1366
  export function bestandVorrang(e, aliasId) {
1171
1367
  if (!e || !e.states || !aliasId) { return e; }
1172
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);
1173
1385
  e.states.forEach(function (s) {
1174
1386
  if (!s.n) { return; }
1175
1387
  var o = S.objects[kanal + '.' + s.n];
@@ -1179,22 +1391,26 @@ export function bestandVorrang(e, aliasId) {
1179
1391
  anlegen (Ricardo, 25.08.2026: UNREACH und RSSI standen angehakt,
1180
1392
  obwohl sie im Alias fehlen). Anhaken kann man sie weiterhin, und
1181
1393
  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 };
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
+ }
1185
1399
  s.on = false;
1186
1400
  return;
1187
1401
  }
1188
1402
  var c = o.common;
1189
1403
  var q = aliasQuellen(o);
1190
1404
  /* 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
- };
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
+ }
1198
1414
  var ist = zeileAusAlias(o, s.n);
1199
1415
  /* Rolle und Typ NUR, wenn sie im Alias ueberhaupt stehen.
1200
1416