iobroker.alias-workbench 0.8.3 → 0.9.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.
@@ -4,6 +4,9 @@
4
4
  "app.disconnected": "getrennt",
5
5
  "app.objectsCount": "{0} Objekte",
6
6
  "app.patternsCount": "{0} Muster",
7
+ "app.subsCount": "{0} Abos",
8
+ "app.subsCount1": "ein Abo",
9
+ "app.subsHint": "Ein Abo ist ein ganzer Zweig, kein einzelner Wert — ein Muster mit Stern deckt alle Punkte darunter ab. Beim Wechsel des Knotens werden alle alten abgemeldet; steigt die Zahl mit der Zeit, bleibt eines hängen. Gerade beobachtet:",
7
10
  "app.templatesCount": "{0} Vorlagen",
8
11
  "app.title": "Alias-Werkbank",
9
12
  "catalog.missing": "Katalog des Admin nicht erreichbar — nur Bestand",
@@ -163,7 +166,9 @@
163
166
  "help.sources": "Links ein Gerät oder einen Kanal wählen. Die Werkbank erkennt, worum es sich handelt, und schlägt einen fertigen Alias vor. Gibt es ihn schon, wird er stattdessen aktualisiert.",
164
167
  "help.templates": "Links eine Vorlage wählen. Mitgelieferte lassen sich ansehen und duplizieren, eigene bearbeiten. Der Probelauf zeigt, welche Geräte sie trifft.",
165
168
  "info.countedSeparately": "für die meisten ohne Folgen",
169
+ "info.dlgTitle": "Gehört nicht zum {0}-Muster",
166
170
  "info.explain": "funktionieren ganz normal und liegen da, wo du sie angelegt hast. Es entsteht dadurch kein zusätzliches Objekt — der Alias bleibt, wie er ist. Sie gehören nur nicht zu dem, was ioBroker unter einem {0} versteht; dafür passt ihre Rolle nicht. Bedeutung hat das erst, wenn du den Alias an Alexa, Google Home oder eine Gerätesteuerung hängst: die sehen dann ein {0} ohne diese Punkte. In VIS, in Skripten und im Verlauf benutzt du sie wie jeden anderen Datenpunkt.",
171
+ "info.more": "Was das bedeutet",
167
172
  "info.notPartOf": "Gehört nicht zum {0}",
168
173
  "info.sameDatapoints": "{0} — ",
169
174
  "leave.body": "An {0} sind Änderungen offen, die noch nicht geschrieben wurden. Wechselst du jetzt, sind sie weg.",
@@ -187,9 +192,12 @@
187
192
  "list.onlyPattern": "nur was ins Muster passt",
188
193
  "list.onlyPatternHint": "Hakt genau die Punkte an, die einen Platz im gewählten Muster bekommen — alles andere wird abgehakt und nicht mit angelegt.",
189
194
  "list.onlyWithValue": "nur mit Wert",
195
+ "list.otherSource": "aus {0}",
196
+ "list.otherSourceHint": "Dieser Punkt liest nicht aus {0}, sondern aus {1}.",
190
197
  "list.reset": "zurücksetzen",
191
198
  "list.resetHint": "Verwirft alle Änderungen dieser Sitzung — Haken, Rollen, Formeln, Quellen — und zeigt wieder den Stand, wie er im System steht.",
192
199
  "list.sendPointMissing": "Sendepunkt fehlt noch",
200
+ "list.sourceHint": "Dieser Punkt liest aus {0}.",
193
201
  "list.sourceMissing": "Quelle fehlt",
194
202
  "list.tpl": "Vorlage",
195
203
  "list.tplDiffers": "weicht von der Vorlage ab",
@@ -277,6 +285,7 @@
277
285
  "pattern.missing": "{0} fehlt",
278
286
  "pattern.noMatch": "kein Muster passt zu „{0}“",
279
287
  "pattern.noPlaceIn": "kein Platz im {0}-Muster",
288
+ "pattern.noPlaceLegend": "So hinterlegte Zeilen haben keinen Platz im {0}-Muster",
280
289
  "pattern.noPlaceLong": "kein Platz im {0}-Muster — der Punkt wird trotzdem angelegt und ist voll benutzbar, nur Alexa & Co. zählen ihn separat",
281
290
  "pattern.notDetected": "nicht erkannt",
282
291
  "pattern.required": "PFLICHT — fehlt",
@@ -292,7 +301,10 @@
292
301
  "result.header": "Was der type-detector daraus macht",
293
302
  "result.newlyCreated": "wird neu angelegt",
294
303
  "result.noObjectShort": "kein Objekt",
304
+ "result.pickSource": "Wohin springen?",
295
305
  "result.pointsFromBranches": "{0} Datenpunkte aus {1} Zweigen. ",
306
+ "result.sourceCount": "{0} Quellen",
307
+ "result.sourceCountHint": "Die Punkte dieses Alias lesen aus mehr als einem Knoten. „zur Quelle\" führt zu dem, aus dem die meisten stammen; ein Klick zeigt alle zur Auswahl.",
296
308
  "result.sourceGone": "Die Quelle {0} gibt es nicht mehr.",
297
309
  "result.states": "{0} Zustände",
298
310
  "result.statesDirect": "{0} Zustände ({1} direkt)",
@@ -454,6 +466,7 @@
454
466
  "tpls.wissen": "+ {0} weitere Punkte als Rollenwissen — greift in der Ansicht „Alle Datenpunkte des Geräts“",
455
467
  "tree.aliases": "Alias bearbeiten",
456
468
  "tree.collapseAll": "alles zuklappen",
469
+ "tree.emptyNode": "keine Datenpunkte — der Knoten steht in der Datenbank, ist aber leer",
457
470
  "tree.expandAll": "alles aufklappen",
458
471
  "tree.filter": "filtern …",
459
472
  "tree.filterAria": "Objekte filtern",
@@ -4,6 +4,9 @@
4
4
  "app.disconnected": "disconnected",
5
5
  "app.objectsCount": "{0} objects",
6
6
  "app.patternsCount": "{0} patterns",
7
+ "app.subsCount": "{0} subscriptions",
8
+ "app.subsCount1": "one subscription",
9
+ "app.subsHint": "A subscription covers a whole branch, not a single value — one pattern with a star catches every point below it. Switching nodes unsubscribes all previous ones; if the number keeps growing, one is being left behind. Currently watching:",
7
10
  "app.templatesCount": "{0} templates",
8
11
  "app.title": "Alias Workbench",
9
12
  "catalog.missing": "Admin catalogue unavailable — existing entries only",
@@ -163,7 +166,9 @@
163
166
  "help.sources": "Pick a device or channel on the left. The workbench recognises what it is and proposes a finished alias. If one already exists, it is updated instead.",
164
167
  "help.templates": "Pick a template on the left. Bundled ones can be viewed and duplicated, your own edited. The trial run shows which devices it matches.",
165
168
  "info.countedSeparately": "no consequences for most setups",
169
+ "info.dlgTitle": "Not part of the {0} pattern",
166
170
  "info.explain": "work perfectly normally and stay where you created them. Nothing extra is written — the alias stays as it is. They simply are not part of what ioBroker considers a {0}; their role does not fit any of its slots. It only starts to matter once you hand the alias to Alexa, Google Home or a device UI: those will see a {0} without these points. In VIS, in scripts and in history you use them like any other datapoint.",
171
+ "info.more": "What this means",
167
172
  "info.notPartOf": "Not part of the {0}",
168
173
  "info.sameDatapoints": "{0} — ",
169
174
  "leave.body": "There are changes on {0} that have not been written yet. Switching now discards them.",
@@ -187,9 +192,12 @@
187
192
  "list.onlyPattern": "only what fits the pattern",
188
193
  "list.onlyPatternHint": "Checks exactly the points that get a slot in the chosen pattern — everything else is unchecked and not created.",
189
194
  "list.onlyWithValue": "only with a value",
195
+ "list.otherSource": "from {0}",
196
+ "list.otherSourceHint": "This point does not read from {0} but from {1}.",
190
197
  "list.reset": "reset",
191
198
  "list.resetHint": "Discards every change made here — checkboxes, roles, formulas, sources — and shows the state as stored again.",
192
199
  "list.sendPointMissing": "send datapoint not created yet",
200
+ "list.sourceHint": "This point reads from {0}.",
193
201
  "list.sourceMissing": "source missing",
194
202
  "list.tpl": "template",
195
203
  "list.tplDiffers": "differs from the template",
@@ -277,6 +285,7 @@
277
285
  "pattern.missing": "{0} missing",
278
286
  "pattern.noMatch": "no pattern matches \"{0}\"",
279
287
  "pattern.noPlaceIn": "no slot in the {0} pattern",
288
+ "pattern.noPlaceLegend": "Rows shaded like this have no slot in the {0} pattern",
280
289
  "pattern.noPlaceLong": "no slot in the {0} pattern — the point is still created and fully usable, only Alexa and friends count it separately",
281
290
  "pattern.notDetected": "not detected",
282
291
  "pattern.required": "REQUIRED — missing",
@@ -292,7 +301,10 @@
292
301
  "result.header": "What the type-detector makes of it",
293
302
  "result.newlyCreated": "will be created",
294
303
  "result.noObjectShort": "no object",
304
+ "result.pickSource": "Jump where?",
295
305
  "result.pointsFromBranches": "{0} datapoints from {1} branches. ",
306
+ "result.sourceCount": "{0} sources",
307
+ "result.sourceCountHint": "The points of this alias read from more than one node. \"to the source\" leads to the one most of them come from; a click shows all of them.",
296
308
  "result.sourceGone": "The source {0} no longer exists.",
297
309
  "result.states": "{0} states",
298
310
  "result.statesDirect": "{0} states ({1} directly)",
@@ -454,6 +466,7 @@
454
466
  "tpls.wissen": "+ {0} more states as role knowledge — used in the “All data points of the device” view",
455
467
  "tree.aliases": "Edit alias",
456
468
  "tree.collapseAll": "collapse all",
469
+ "tree.emptyNode": "no datapoints — the node exists in the database but is empty",
457
470
  "tree.expandAll": "expand all",
458
471
  "tree.filter": "filter …",
459
472
  "tree.filterAria": "Filter objects",
package/admin/js/baum.js CHANGED
@@ -281,7 +281,22 @@ function renderKinder(knoten, filter, tiefe) {
281
281
 
282
282
  reihen.forEach(function (reihe) {
283
283
  var k = reihe.k;
284
- if (!k._zust) { return; }
284
+ /* Leere Knoten ausgrauen statt weglassen.
285
+
286
+ Ein Kanal ohne Datenpunkte gibt keinen Alias her - deshalb blieb
287
+ er bisher ganz weg. Nur sieht das nicht nach „leer" aus, sondern
288
+ nach „gibt es nicht": Ricardos Heizungsgruppen standen im Admin
289
+ mit sieben Kanaelen, in der Werkbank mit zweien, und es sah wie
290
+ ein Anzeigefehler aus, waehrend in Wahrheit hm-rpc die
291
+ Datenpunkte nicht angelegt hatte (08.09.2026). Wer den Bestand
292
+ sucht, soll die Luecke sehen.
293
+
294
+ Nur echte Objekte, keine blossen Kennungsknoten: `channel`,
295
+ `device`, `folder`. Waehlbar sind sie nicht (`istWaehlbar` steigt
296
+ bei `_zust === 0` aus), also fuehrt auch kein Klick ins Leere.
297
+ Produktiv sind das 194 Zeilen zu 3432 - gut sechs Prozent. */
298
+ var leer = !k._zust;
299
+ if (leer && k.art !== 'channel' && k.art !== 'device' && k.art !== 'folder') { return; }
285
300
  /* Zustaende zeigt die rechte Seite, nicht der Baum - es sei denn,
286
301
  unter dem Zustand liegt noch etwas. Dann ist er ein Behaelter und
287
302
  muss sichtbar sein, sonst hilft es nichts, dass `istWaehlbar` ihn
@@ -299,15 +314,20 @@ function renderKinder(knoten, filter, tiefe) {
299
314
 
300
315
  /* Der Filter sucht auch im Namen. Wer „Badezimmer" tippt, meint
301
316
  FK_Badezimmer — und nicht die Seriennummer, die er nicht kennt. */
302
- var passt = !filter || k.id.toLowerCase().indexOf(filter) !== -1 ||
303
- (reihe.zus && reihe.zus.toLowerCase().indexOf(filter) !== -1);
317
+ /* Beim Filtern zaehlen leere Knoten nicht als Treffer - sonst
318
+ findet die Suche Zeilen, mit denen sich nichts anfangen laesst. */
319
+ var passt = !filter
320
+ ? true
321
+ : (!leer && (k.id.toLowerCase().indexOf(filter) !== -1 ||
322
+ (reihe.zus && reihe.zus.toLowerCase().indexOf(filter) !== -1)));
304
323
  var kinderUl = renderKinder(k, filter, tiefe + 1);
305
324
  var kinderPassen = kinderUl.childNodes.length > 0;
306
325
  if (!passt && !kinderPassen) { return; }
307
326
 
308
327
  var li = el('li');
309
328
  var waehlbar = istWaehlbar(k);
310
- var d = el('div', 'node' + (waehlbar ? ' pick' : ' grp') + (k.id === S.current ? ' sel' : ''));
329
+ var d = el('div', 'node' + (waehlbar ? ' pick' : ' grp') + (leer ? ' leer' : '') +
330
+ (k.id === S.current ? ' sel' : ''));
311
331
 
312
332
  var hatKinder = kinderUl.childNodes.length > 0;
313
333
  var vorgabe = (S.alleKlapp === null) ? (tiefe <= 1) : S.alleKlapp;
@@ -342,6 +362,9 @@ function renderKinder(knoten, filter, tiefe) {
342
362
  d.appendChild(nm2);
343
363
  d.title = k.id + ' · ' + reihe.zus;
344
364
  }
365
+ /* Der Hinweis kommt hinter den Zusatznamen, nicht davor - sonst
366
+ ueberschreibt ihn die Zeile darueber wieder. */
367
+ if (leer) { d.title = (d.title ? d.title + ' · ' : '') + tr('tree.emptyNode'); }
345
368
  if (waehlbar) {
346
369
  if (wohlGeraet(k)) {
347
370
  var pk = el('span', 'geraetepunkt', '●');
@@ -5,7 +5,7 @@ import { socket } from './verbindung.js';
5
5
  import { el } from './basis.js';
6
6
  import { tr } from './sprache.js';
7
7
  import { wertVon, jsonFelder, feldAusFormel, feldFormel, feldWert, jsonVon } from './werte.js';
8
- import { musterVon, erkenneEntwurf } from './erkennung.js';
8
+ import { musterVon, erkenneEntwurf, rolleTrifft } from './erkennung.js';
9
9
  import { musterName } from './musternamen.js';
10
10
  import { rollenFeld } from './rollenwahl.js';
11
11
  import { opt , quellenAuswahl, vorlagenAbweichung, bestandsAbweichung } from './entwurf.js';
@@ -28,6 +28,58 @@ export function detailZeile(e, s, idx) {
28
28
  keine mehr und faellt beim Zuruecknehmen nicht mehr weg. */
29
29
  function neu() { s.geaendert = true; rateBehalten(s); entwurfAngefasst(); zeichneErgebnis(); }
30
30
 
31
+ /* ---- Die Auswahl der Datenpunkte ---------------------------------
32
+
33
+ Im Eintrag stehen die ersten beiden Glieder nicht: eine volle
34
+ Kennung wird hier bis zu 78 Zeichen lang (gemessen am Testsystem,
35
+ Median 36) und passte in kein Feld dieser Spalte. Ein `select`
36
+ kuerzt hinten ab — weg waere also gerade das, was den Punkt
37
+ unterscheidet.
38
+
39
+ Nur: ohne den Instanzteil sehen `Solar.Balkon.Einspeisung.Deckel`
40
+ (ein Alias-Punkt) und `SmartHome.Solar_Balkon.tele.SENSOR` (die
41
+ echte Quelle) gleich aus, und wer den ersten waehlt, baut aus
42
+ Versehen einen Alias, der aus einem Alias liest (Ricardo,
43
+ 08.09.2026). Deshalb traegt jede Gruppe ihre Instanz als
44
+ Ueberschrift, und `alias.0` steht ganz unten: daraus zu lesen ist
45
+ der Sonderfall, nicht der Regelfall. */
46
+ function quellenIn(sel, ids) {
47
+ var gruppen = {}, folge = [];
48
+ ids.forEach(function (id) {
49
+ var inst = id.split('.').slice(0, 2).join('.');
50
+ if (!gruppen[inst]) { gruppen[inst] = []; folge.push(inst); }
51
+ gruppen[inst].push(id);
52
+ });
53
+ folge.sort(function (a, b) {
54
+ var aa = a.indexOf('alias.') === 0, bb = b.indexOf('alias.') === 0;
55
+ return aa === bb ? 0 : (aa ? 1 : -1);
56
+ });
57
+ folge.forEach(function (inst) {
58
+ var g = document.createElement('optgroup');
59
+ g.label = inst;
60
+ gruppen[inst].forEach(function (id) {
61
+ var o = opt(id, id.split('.').slice(2).join('.'));
62
+ o.title = id;
63
+ g.appendChild(o);
64
+ });
65
+ sel.appendChild(g);
66
+ });
67
+ }
68
+
69
+ /* Und weil die Ueberschrift nur im aufgeklappten Zustand zu sehen ist
70
+ — ein `select` zeigt zugeklappt allein den Text seiner Option —,
71
+ steht die volle Kennung als eigene Zeile unter dem Feld. Dieselbe
72
+ Bauart wie „Die Vorlage will hier: …": sie darf umbrechen und hat
73
+ damit keine Breitengrenze. */
74
+ function mitVollId(felder, id) {
75
+ var w = el('div', 'feldstapel');
76
+ w.appendChild(felder);
77
+ var v = el('div', 'vollid');
78
+ if (id) { v.textContent = id; v.title = id; }
79
+ w.appendChild(v);
80
+ return w;
81
+ }
82
+
31
83
  /* ---- Wo die Vorlage etwas anderes will --------------------------
32
84
 
33
85
  Die zugeklappte Zeile sagt nur, DASS etwas abweicht. Welches Feld,
@@ -165,7 +217,7 @@ export function detailZeile(e, s, idx) {
165
217
  var selR = el('select', 'tx');
166
218
  selR.style.width = '260px';
167
219
  selR.appendChild(opt('', quellen.length ? tr('detail.pickOne') : tr('detail.noSource')));
168
- quellen.forEach(function (id) { selR.appendChild(opt(id, id.split('.').slice(2).join('.'))); });
220
+ quellenIn(selR, quellen);
169
221
  if (s.srcR && quellen.indexOf(s.srcR) === -1) { selR.appendChild(opt(s.srcR, s.srcR + ' ' + tr('detail.elsewhere'))); }
170
222
  selR.appendChild(opt('__frei__', tr('detail.otherObject')));
171
223
  selR.value = s.srcR || '';
@@ -215,7 +267,7 @@ export function detailZeile(e, s, idx) {
215
267
  lF.appendChild(selF);
216
268
  qf.appendChild(lF);
217
269
  }
218
- zeigeBeide(zeile(tr('detail.readsFrom'), qf), ['srcR']);
270
+ zeigeBeide(zeile(tr('detail.readsFrom'), mitVollId(qf, s.srcR)), ['srcR']);
219
271
 
220
272
  if (s.frei) {
221
273
  var iFrei = el('input', 'tx w');
@@ -238,7 +290,7 @@ export function detailZeile(e, s, idx) {
238
290
  ein, damit im Feld steht, worauf es hinausgeht. */
239
291
  selW.appendChild(opt('', tr('detail.noWrite')));
240
292
  if (s.srcR && s.srcW !== s.srcR) { selW.appendChild(opt('=lesen', tr('detail.sameAsRead'))); }
241
- quellen.forEach(function (id) { selW.appendChild(opt(id, id.split('.').slice(2).join('.'))); });
293
+ quellenIn(selW, quellen);
242
294
  if (s.srcW && quellen.indexOf(s.srcW) === -1) { selW.appendChild(opt(s.srcW, s.srcW)); }
243
295
  selW.value = s.srcW || '';
244
296
  selW.addEventListener('click', function (ev) { ev.stopPropagation(); });
@@ -255,7 +307,7 @@ export function detailZeile(e, s, idx) {
255
307
  if (s.srcW && s.srcW !== s.srcR) {
256
308
  wf.appendChild(el('div', 'sugg', tr('detail.separateHint')));
257
309
  }
258
- zeigeBeide(zeile(tr('detail.writesTo'), wf), ['srcW']);
310
+ zeigeBeide(zeile(tr('detail.writesTo'), mitVollId(wf, s.srcW)), ['srcW']);
259
311
 
260
312
  /* --- Leseformel --- */
261
313
  var fb = el('div');
@@ -311,7 +363,7 @@ export function detailZeile(e, s, idx) {
311
363
  if (must) {
312
364
  var treffer = null;
313
365
  must.states.forEach(function (p) {
314
- if (!treffer && p.role && new RegExp(p.role.source ? p.role.source : p.role).test(s.role)) { treffer = p; }
366
+ if (!treffer && p.role && rolleTrifft(p.role, s.role)) { treffer = p; }
315
367
  });
316
368
  var hin = el('div', 'sugg');
317
369
  if (treffer) {
@@ -10,7 +10,7 @@ import './enums.js';
10
10
  import { zusatzName, setzeModus, zeichneBaum, merkeKlappstand } from './baum.js';
11
11
  import { kindZustaende, aliasQuellen, holeWerte } from './werte.js';
12
12
  import { erkenneEntwurf } from './erkennung.js';
13
- import { vorschlag , pruefeVorlage } from './vorlagen.js';
13
+ import { vorschlag , pruefeVorlage, zeigeAbozahl } from './vorlagen.js';
14
14
 
15
15
  import { zeichneErgebnis } from './ergebnis.js';
16
16
 
@@ -215,6 +215,120 @@ export function knotenDa(id) {
215
215
  return kindZustaende(id).length > 0;
216
216
  }
217
217
 
218
+ /* Aus welchen Knoten liest dieser Alias, und wie oft aus jedem?
219
+
220
+ Dieselbe Zaehlung wie in `quelleVon` - nur dass die dort das Ergebnis
221
+ auf einen Sieger eindampft. Fuer die Anzeige zaehlt aber gerade das,
222
+ was dabei verlorengeht: dass es mehr als einen gibt. Ricardos
223
+ `alias.0.Solar.Netz` liest drei Punkte aus `0_userdata` und zwei aus
224
+ `mqtt-client`; der Sprungknopf fuehrte zur Mehrheit und sagte kein
225
+ Wort ueber den Rest (Ricardo, 08.09.2026).
226
+
227
+ Absteigend nach Anzahl, bei Gleichstand alphabetisch - sonst
228
+ wechselte die Reihenfolge bei jedem Neuzeichnen. */
229
+ /* Zu welcher Quelle des Alias gehoert der Knoten, an dem ich stehe?
230
+
231
+ Die Antwort ist nicht „die Mehrheit", sondern „die hier". Sie traegt
232
+ zwei Dinge: den Rueckweg zum Alias (er gehoert an jede Quelle, nicht
233
+ nur an die groesste) und den Bezugspunkt der Zeilenmarken - markiert
234
+ wird, was NICHT von hier kommt (Ricardo, 08.09.2026).
235
+
236
+ Getroffen wird ein Eintrag der Verteilung auf drei Weisen: genau er,
237
+ ein Knoten unter ihm (`…Stromzaehler.stat` unter der zusammengefassten
238
+ Sparte) oder sein unmittelbarer Elternknoten - dort steht man am
239
+ Geraet, waehrend die Quelle eine Ebene tiefer liegt. Nur eine Ebene:
240
+ sonst bekaeme `mqtt-client.0.SmartHome` den Knopf und jede Ebene
241
+ darueber gleich mit. */
242
+ export function quelleHier(aliasId, knoten) {
243
+ if (!aliasId || !knoten) { return null; }
244
+ var treffer = null;
245
+ quellenVerteilung(aliasId).forEach(function (q) {
246
+ if (treffer) { return; }
247
+ if (q.id === knoten) { treffer = q.id; return; }
248
+ if (knoten.indexOf(q.id + '.') === 0) { treffer = q.id; return; }
249
+ var t = q.id.split('.');
250
+ t.pop();
251
+ if (t.join('.') === knoten) { treffer = q.id; }
252
+ });
253
+ return treffer;
254
+ }
255
+
256
+ export function quellenVerteilung(aliasId) {
257
+ var zaehler = {};
258
+ kindZustaende(aliasId).forEach(function (id) {
259
+ var q = aliasQuellen(S.objects[id]);
260
+ /* Einmal je DATENPUNKT, nicht je Quellangabe. `quelleVon` zaehlt
261
+ Lese- und Schreibquelle getrennt - fuer den Mehrheitsentscheid
262
+ gleichgueltig, fuer eine Anzeige nicht: an einem Alias mit fuenf
263
+ Punkten stand „6 und 4" statt „3 und 2". Wo Lesen und Schreiben
264
+ auseinandergehen, zaehlt die Lesequelle: aus ihr kommt der Wert,
265
+ den man sieht. */
266
+ var x = q.read || q.write;
267
+ if (!x) { return; }
268
+ var t = String(x).split('.');
269
+ t.pop();
270
+ var eltern = t.join('.');
271
+ if (eltern) { zaehler[eltern] = (zaehler[eltern] || 0) + 1; }
272
+ });
273
+ var sortiere = function (liste) {
274
+ return liste.sort(function (a, b) { return (b.n - a.n) || (a.id < b.id ? -1 : 1); });
275
+ };
276
+ var liste = sortiere(Object.keys(zaehler).map(function (k) { return { id: k, n: zaehler[k] }; }));
277
+
278
+ /* Sparten desselben Geraets sind keine zweite Quelle.
279
+
280
+ Ein Tasmota-Alias liest aus `…Licht.stat` und `…Licht.tele` - das
281
+ sind zwei Knoten, aber ein Geraet, und „2 Quellen" waere dort
282
+ schlicht falsch. Alles, was unterhalb der Hauptquelle liegt, zaehlt
283
+ deshalb zu ihr. Uebrig bleiben nur eigene Zweige - bei Ricardos
284
+ `alias.0.Solar.Netz` `0_userdata` und `mqtt-client` (08.09.2026). */
285
+ var haupt = quelleVon(aliasId);
286
+ if (!haupt) { return liste; }
287
+ var raus = [], hauptN = 0;
288
+ liste.forEach(function (q) {
289
+ if (q.id === haupt || q.id.indexOf(haupt + '.') === 0) { hauptN += q.n; return; }
290
+ raus.push(q);
291
+ });
292
+ if (hauptN) { raus.push({ id: haupt, n: hauptN }); }
293
+
294
+ /* Dasselbe noch einmal unter den Nebenquellen.
295
+
296
+ Der Schritt darueber fasst nur gegen die HAUPTquelle zusammen. Bei
297
+ `alias.0.Solar.Balkon.Einspeisung` liegen aber zwei der Nebenquellen
298
+ ebenfalls beieinander - `…Solar_Balkon.stat` und `…Solar_Balkon.tele`
299
+ sind ein Tasmota, und es standen „4 Quellen" statt drei (Ricardo,
300
+ 08.09.2026).
301
+
302
+ Zusammengefasst wird nur, was wirklich zusammengehoert: mehrere
303
+ Knoten unter demselben Elternknoten, und das auch nur, wenn sie
304
+ blosse Kennungsebenen ohne eigenes Objekt sind (so legt MQTT seine
305
+ Sparten an) oder ihr Elternknoten ein `device` ist (so liegen die
306
+ Kanaele bei Homematic). Zwei `channel` in einem Ordner - etwa
307
+ `0_userdata.0.Solar.Netz` und `…Solar.Balkon` - bleiben getrennt:
308
+ das sind zwei Geraete, keine zwei Sparten. */
309
+ var nachEltern = {};
310
+ raus.forEach(function (q) {
311
+ var t = q.id.split('.');
312
+ t.pop();
313
+ var eltern = t.join('.');
314
+ (nachEltern[eltern] = nachEltern[eltern] || []).push(q);
315
+ });
316
+ var zusammen = [];
317
+ Object.keys(nachEltern).forEach(function (eltern) {
318
+ var gruppe = nachEltern[eltern];
319
+ var ohneObjekt = gruppe.every(function (q) { return !S.objects[q.id]; });
320
+ var elternGeraet = !!(S.objects[eltern] && S.objects[eltern].type === 'device');
321
+ if (gruppe.length < 2 || !eltern || (!ohneObjekt && !elternGeraet)) {
322
+ zusammen = zusammen.concat(gruppe);
323
+ return;
324
+ }
325
+ var summe = 0;
326
+ gruppe.forEach(function (q) { summe += q.n; });
327
+ zusammen.push({ id: eltern, n: summe });
328
+ });
329
+ return sortiere(zusammen);
330
+ }
331
+
218
332
  export function quelleVon(aliasId) {
219
333
  var o = S.objects[aliasId];
220
334
  var q = o && o.native && o.native.quelle;
@@ -573,9 +687,31 @@ export function waehle(id, fertig) {
573
687
  sel.scrollIntoView({ block: 'nearest' });
574
688
  }
575
689
 
576
- if (S.abo) { socket.emit('unsubscribe', S.abo); }
577
- S.abo = id + '.*';
578
- socket.emit('subscribe', S.abo);
690
+ /* Abonniert wird, was auch angezeigt wird.
691
+
692
+ Bisher nur `<knoten>.*`. An einer Quelle stimmt das - die Werte in
693
+ der Liste kommen von dort. Am Alias nicht: dort stehen die Werte der
694
+ QUELLEN, und die bekamen kein Ereignis. Die Zahlen wurden einmal
695
+ beim Auswaehlen geholt und standen dann still; an
696
+ `alias.0.Solar.Balkon.Energie`, wo sich im Sekundentakt etwas
697
+ aendert, fiel es auf (Ricardo, 08.09.2026).
698
+
699
+ Deshalb am Alias zusaetzlich seine Quellen - `quellenVerteilung`
700
+ weiss ja, welche das sind. Und deshalb eine Liste: sonst bliebe
701
+ beim naechsten Wechsel die Haelfte abonniert und die Werkbank
702
+ bekaeme mit der Zeit Ereignisse fuer das halbe System. Die Zahl der
703
+ laufenden Abos steht in der Kopfleiste - dort sieht man sofort, ob
704
+ sich etwas ansammelt. */
705
+ (S.abo || []).forEach(function (m) { socket.emit('unsubscribe', m); });
706
+ var muster = [id + '.*'];
707
+ if (id.indexOf('alias.') === 0) {
708
+ quellenVerteilung(id).forEach(function (q) {
709
+ if (muster.indexOf(q.id + '.*') === -1) { muster.push(q.id + '.*'); }
710
+ });
711
+ }
712
+ S.abo = muster;
713
+ muster.forEach(function (m) { socket.emit('subscribe', m); });
714
+ zeigeAbozahl();
579
715
 
580
716
  /* Erst die Werte, dann der Vorschlag. Die Erkennung schaut in den
581
717
  Rohwert von tele.SENSOR hinein — ohne Werte haelt sie ein Geraet
@@ -27,6 +27,8 @@ import {
27
27
  mitNachfrage,
28
28
  geraeteDarunter,
29
29
  aliasFuer,
30
+ quellenVerteilung,
31
+ quelleHier,
30
32
  tipptGerade,
31
33
  opt,
32
34
  vorlagenAbweichung,
@@ -331,18 +333,123 @@ var warumAuf = false;
331
333
  erschien dadurch an jeder Sparte und jedem Unterkanal, also an
332
334
  `…RGB.stat` und `…RGB.tele` ebenso wie am Geraet. Der Knopf
333
335
  gehoert nur dorthin, wo der Alias auch wirklich herkommt. */
334
- if (sprungZiel && quelleVon(sprungZiel) !== S.current) { sprungZiel = null; }
336
+ /* Rueckprobe, aber nicht zu streng.
337
+
338
+ Frueher galt nur: `quelleVon(alias) === S.current`. Bei einem
339
+ Alias aus zwei Zweigen fiel der Rueckweg damit weg - wer ueber
340
+ die neue Quellenauswahl auf `…Stromzaehler.stat` sprang, stand
341
+ dort ohne Knopf zurueck (Ricardo, 08.09.2026).
342
+
343
+ Jetzt gilt der Knopf auch an einer Nebenquelle - aber nur, wenn
344
+ sie GENAU in der Verteilung steht und einen eigenen Zweig
345
+ bildet. Sparten unterhalb der Hauptquelle bleiben damit weiter
346
+ aussen vor: `…Licht.stat` und `…Licht.tele` liegen beide unter
347
+ `…Bastelzimmer_Licht`, und der Knopf gehoert ans Geraet, nicht
348
+ an jede Sparte. Knoten weiter oben (`…Strom`) stehen gar nicht
349
+ erst in der Verteilung. */
350
+ /* Der Rueckweg gehoert an jede Quelle des Alias - an die groesste
351
+ wie an jede andere, und auch an das Geraet ueber einer Sparte.
352
+ `quelleHier` sagt, ob dieser Knoten zu einer von ihnen gehoert;
353
+ steht er zu weit oben oder gar nicht dabei, bleibt der Knopf
354
+ weg (Ricardo, 08.09.2026). */
355
+ if (sprungZiel && !quelleHier(sprungZiel, S.current)) { sprungZiel = null; }
335
356
  sprungModus = 'aliase';
336
357
  }
358
+ /* Liest der Alias aus mehr als einem Knoten, gehoert das in den Kopf.
359
+
360
+ `quelleVon` entscheidet nach Mehrheit und wirft den Rest weg. Das
361
+ ist als Sprungziel richtig, als Auskunft aber die halbe Wahrheit:
362
+ an `alias.0.Solar.Netz` - drei Punkte aus `0_userdata`, zwei aus
363
+ `mqtt-client` - stand nirgends, dass es zwei Quellen sind. Man
364
+ musste jede Zeile aufklappen, um es zu sehen (Ricardo,
365
+ 08.09.2026). */
366
+ /* Der Chip gilt fuer den Alias, gleich von welcher Seite man ihn
367
+ ansieht: am Alias selbst und an jeder seiner Quellen. Wer an einer
368
+ Nebenquelle steht, soll sehen, dass er nur einen Teil vor sich
369
+ hat. */
370
+ var quellAlias = (S.current.indexOf('alias.') === 0) ? S.current
371
+ : (sprungZiel && sprungModus === 'aliase' ? sprungZiel : null);
372
+ var verteilung = quellAlias ? quellenVerteilung(quellAlias) : [];
373
+ var mehrQuellen = verteilung.length > 1;
337
374
  if (knotenDa(sprungZiel)) {
338
375
  var zumAlias = (sprungModus === 'aliase');
339
376
  sprung = el('button', 'btn schmal sprungknopf',
340
377
  (zumAlias ? tr('result.toAlias') : tr('result.toSource')) + ' \u2192');
341
378
  sprung.title = (zumAlias ? tr('result.toAliasHint') : tr('result.toSourceHint'))
342
- + '\n' + sprungZiel;
379
+ + String.fromCharCode(10) + sprungZiel;
343
380
  var ziel0 = sprungZiel, modus0 = sprungModus;
344
- sprung.addEventListener('click', function () { springeZu(ziel0, modus0); });
345
- namensZeile.appendChild(sprung);
381
+ /* Die Auswahl gehoert nur an „zur Quelle" - dort gibt es mehrere
382
+ Ziele. „zum Alias" hat genau eines; eine Liste von QUELLEN unter
383
+ diesem Knopf ist sinnlos (Ricardo, 08.09.2026). Der Chip bleibt
384
+ an beiden Stellen: er sagt etwas ueber den Alias, nicht ueber
385
+ den Sprung. */
386
+ if (mehrQuellen && sprungModus === 'quellen') {
387
+ /* Die Auswahl haengt am Body, nicht am Kopf.
388
+
389
+ Erst stand sie neben dem Knopf - und war sofort wieder weg:
390
+ bei `alias.0.Solar.Netz` laeuft im Sekundentakt ein neuer
391
+ Messwert ein, jeder zeichnet die rechte Seite neu, und mit ihr
392
+ verschwand die eben geoeffnete Liste. Am Body ueberlebt sie
393
+ das; ihre Stelle bekommt sie aus der Lage des Knopfes.
394
+
395
+ `mousedown` statt `click`, wie bei den Vorschlagslisten der
396
+ Zielleiste: der Mausdruck kommt vor dem Neuzeichnen. */
397
+ var listeQ = null;
398
+ var zuQ = function () {
399
+ if (listeQ && listeQ.parentNode) { listeQ.parentNode.removeChild(listeQ); }
400
+ listeQ = null;
401
+ };
402
+ sprung.addEventListener('mousedown', function (ev) {
403
+ ev.preventDefault();
404
+ ev.stopPropagation();
405
+ if (listeQ) { zuQ(); return; }
406
+ listeQ = el('div', 'vorschlaege sprungliste');
407
+ listeQ.appendChild(el('div', 'vzkopf', tr('result.pickSource')));
408
+ verteilung.forEach(function (q) {
409
+ /* Keine Zeile hervorheben. Die Hervorhebung meinte „das ist
410
+ die wichtigste" - eine Aussage, die bei drei zu drei
411
+ niemand treffen kann und die auch sonst niemand braucht:
412
+ alle Quellen des Alias sind gleichberechtigt (Ricardo,
413
+ 08.09.2026). */
414
+ var z = el('div', 'vz');
415
+ z.appendChild(el('span', null, q.id));
416
+ z.appendChild(el('span', 'zq', ' \u00b7 ' + q.n));
417
+ z.addEventListener('mousedown', function (ev2) {
418
+ ev2.preventDefault();
419
+ ev2.stopPropagation();
420
+ zuQ();
421
+ springeZu(q.id, modus0);
422
+ });
423
+ listeQ.appendChild(z);
424
+ });
425
+ var r = sprung.getBoundingClientRect();
426
+ listeQ.style.position = 'fixed';
427
+ listeQ.style.left = Math.round(r.left) + 'px';
428
+ listeQ.style.top = Math.round(r.bottom + 2) + 'px';
429
+ listeQ.hidden = false;
430
+ document.body.appendChild(listeQ);
431
+ });
432
+ /* Woanders hin gedrueckt heisst: doch nicht. Einmal registriert,
433
+ nicht bei jedem Neuzeichnen - sonst haeuft sich der Zuhoerer. */
434
+ if (!S.sprungZuhoerer) {
435
+ S.sprungZuhoerer = true;
436
+ document.addEventListener('mousedown', function () {
437
+ var offen = document.querySelector('.sprungliste');
438
+ if (offen && offen.parentNode) { offen.parentNode.removeChild(offen); }
439
+ });
440
+ }
441
+ namensZeile.appendChild(sprung);
442
+ } else {
443
+ sprung.addEventListener('click', function () { springeZu(ziel0, modus0); });
444
+ namensZeile.appendChild(sprung);
445
+ }
446
+ }
447
+ if (mehrQuellen) {
448
+ var chQ = el('span', 'chip mut quellenzahl', tr('result.sourceCount', verteilung.length));
449
+ chQ.title = tr('result.sourceCountHint') + String.fromCharCode(10, 10) +
450
+ verteilung.map(function (q) { return q.id + ' \u00b7 ' + q.n; })
451
+ .join(String.fromCharCode(10));
452
+ namensZeile.appendChild(chQ);
346
453
  }
347
454
  links.appendChild(sub);
348
455
  head.appendChild(links);
@@ -609,31 +716,13 @@ var warumAuf = false;
609
716
  return musterBlock;
610
717
  }
611
718
 
612
- /* Punkte, die im gewaehlten Muster keinen Platz haben, aber als
613
- Messwerte durchgehen. */
614
- function baueInfoKarte(host, e, infoNamen) {
615
- if (infoNamen.length) {
616
- var ic = el('div', 'card infod');
617
- var ich = el('div', 'ch');
618
- /* Hier stand „info" und „zaehlt der Detektor separat". Beides sind
619
- unsere Woerter: „info" ist der interne Name des Sammeltyps, und
620
- wer den Detektor nicht kennt, weiss nach dem Satz weniger als
621
- vorher. Jetzt steht da, worum es geht - und der Zusatz sagt
622
- gleich, ob man handeln muss. */
623
- ich.appendChild(el('span', 'typ', tr('info.notPartOf', e.want || '')));
624
- ich.appendChild(el('span', 'chip mut', tr('info.countedSeparately')));
625
- ic.appendChild(ich);
626
- var ie = el('div', 'infoerkl');
627
- ie.style.padding = '10px 15px 12px';
628
- ie.style.fontSize = '11.5px';
629
- ie.style.color = 'var(--ink-3)';
630
- ie.style.lineHeight = '1.6';
631
- ie.appendChild(el('b', null, tr('info.sameDatapoints', infoNamen.join(', '))));
632
- ie.appendChild(document.createTextNode(tr('info.explain', e.want || '')));
633
- ic.appendChild(ie);
634
- host.appendChild(ic);
635
- }
636
- }
719
+ /* Die Karte „Gehoert nicht zum <Muster>" stand hier bis zum
720
+ 08.09.2026 — weit unten, mit den Namen der betroffenen Punkte und
721
+ der langen Erklaerung. Sie ist weg: dieselbe Auskunft haengt jetzt
722
+ am Hinweis der Legende, und die steht dort, wo die getoenten Zeilen
723
+ stehen. Zweimal dasselbe, einmal oben und einmal ganz unten, war
724
+ eine zu viel (Ricardo, 08.09.2026). Die Texte `info.explain` und
725
+ `info.notPartOf` bleiben in Gebrauch — die Legende nutzt sie. */
637
726
 
638
727
  /* Was sich gegenueber dem gespeicherten Stand aendern wuerde. */
639
728
  function baueDiffKarte(host, e) {
@@ -802,9 +891,7 @@ var warumAuf = false;
802
891
  rateKnopf = null;
803
892
  musterBlock = null;
804
893
 
805
- /* --- info-Karte --- */
806
894
  var infoNamen = Object.keys(imInfo);
807
- baueInfoKarte(host, e, infoNamen);
808
895
 
809
896
  /* --- Änderungen gegenüber dem, was gespeichert ist --- */
810
897
  baueDiffKarte(host, e);