iobroker.alias-workbench 0.8.3 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -3
- package/admin/css/werkbank.css +57 -8
- package/admin/detector.js +1 -1
- package/admin/geraetewissen/tasmota-befehle.json +3 -1
- package/admin/i18n/de.json +30 -6
- package/admin/i18n/en.json +30 -6
- package/admin/js/aufzaehlungen.js +11 -1
- package/admin/js/baum.js +118 -37
- package/admin/js/detail.js +76 -9
- package/admin/js/entwurf.js +313 -43
- package/admin/js/enums.js +50 -7
- package/admin/js/ergebnis.js +161 -37
- package/admin/js/erkennung.js +54 -5
- package/admin/js/katalog.js +12 -3
- package/admin/js/mqtt.js +76 -13
- package/admin/js/musternamen.js +91 -26
- package/admin/js/musterwahl.js +10 -0
- package/admin/js/objekte.js +29 -3
- package/admin/js/pruefungen.js +10 -2
- package/admin/js/quellentausch.js +31 -2
- package/admin/js/schreiben.js +209 -24
- package/admin/js/sprache.js +54 -7
- package/admin/js/start.js +32 -2
- package/admin/js/vorlagen.js +114 -6
- package/admin/js/vorlagenblatt.js +195 -26
- package/admin/js/werte.js +113 -6
- package/admin/js/zuordnung.js +71 -17
- package/admin/js/zustand.js +7 -1
- package/admin/js/zustandsliste.js +157 -26
- package/admin/tab.html +25 -3
- package/admin/vorlagen/hm-dimmer.json +1 -1
- package/admin/vorlagen/hm-schaltaktor-lampe.json +2 -2
- package/admin/vorlagen/hm-schaltaktor-mehrfach.json +2 -2
- package/admin/vorlagen/hm-schaltaktor-steckdose.json +2 -2
- package/io-package.json +105 -1
- package/package.json +1 -1
package/admin/i18n/en.json
CHANGED
|
@@ -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",
|
|
@@ -129,6 +132,7 @@
|
|
|
129
132
|
"enums.room": "Room",
|
|
130
133
|
"enums.roomHint": "Assigns the alias to a room. This is not stored on the object but in the \"enum.rooms\" enumeration — Alexa, Google and the dashboards group by it.",
|
|
131
134
|
"enums.suggested": "suggested",
|
|
135
|
+
"enums.unknown": "Enumerations not loaded — room and function stay untouched.",
|
|
132
136
|
"enums.unused": "empty",
|
|
133
137
|
"enums.willCreate": "create new",
|
|
134
138
|
"folder.aliasMissing": "missing",
|
|
@@ -162,10 +166,9 @@
|
|
|
162
166
|
"help.aliasesAdd": "To add points to an alias, the way via “to the source →” (top right, next to the alias name) is easier: the template applies there, and every data point of the device is offered with a ready role — here you would have to enter each one by hand.",
|
|
163
167
|
"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
168
|
"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
|
-
"info.
|
|
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.",
|
|
167
|
-
"info.
|
|
168
|
-
"info.sameDatapoints": "{0} — ",
|
|
171
|
+
"info.more": "What this means",
|
|
169
172
|
"leave.body": "There are changes on {0} that have not been written yet. Switching now discards them.",
|
|
170
173
|
"leave.discard": "Discard and switch",
|
|
171
174
|
"leave.stay": "Stay here",
|
|
@@ -187,9 +190,12 @@
|
|
|
187
190
|
"list.onlyPattern": "only what fits the pattern",
|
|
188
191
|
"list.onlyPatternHint": "Checks exactly the points that get a slot in the chosen pattern — everything else is unchecked and not created.",
|
|
189
192
|
"list.onlyWithValue": "only with a value",
|
|
193
|
+
"list.otherSource": "from {0}",
|
|
194
|
+
"list.otherSourceHint": "This point does not read from {0} but from {1}.",
|
|
190
195
|
"list.reset": "reset",
|
|
191
196
|
"list.resetHint": "Discards every change made here — checkboxes, roles, formulas, sources — and shows the state as stored again.",
|
|
192
197
|
"list.sendPointMissing": "send datapoint not created yet",
|
|
198
|
+
"list.sourceHint": "This point reads from {0}.",
|
|
193
199
|
"list.sourceMissing": "source missing",
|
|
194
200
|
"list.tpl": "template",
|
|
195
201
|
"list.tplDiffers": "differs from the template",
|
|
@@ -200,6 +206,7 @@
|
|
|
200
206
|
"move.exists": "{0} already exists. Remove it first or pick another name.",
|
|
201
207
|
"move.from": "from",
|
|
202
208
|
"move.needName": "A name is required.",
|
|
209
|
+
"move.nested": "The target sits below the alias itself (or the other way round). Moving would delete its own parent node — please pick a place beside it.",
|
|
203
210
|
"move.newFolder": "new folder",
|
|
204
211
|
"move.noFolder": "(no folder — directly under alias.0)",
|
|
205
212
|
"move.noScripts": "The ID does not appear in any script.",
|
|
@@ -276,7 +283,7 @@
|
|
|
276
283
|
"pattern.manualInstead": "by hand instead of {0}",
|
|
277
284
|
"pattern.missing": "{0} missing",
|
|
278
285
|
"pattern.noMatch": "no pattern matches \"{0}\"",
|
|
279
|
-
"pattern.
|
|
286
|
+
"pattern.noPlaceLegend": "Rows shaded like this have no slot in the {0} pattern",
|
|
280
287
|
"pattern.noPlaceLong": "no slot in the {0} pattern — the point is still created and fully usable, only Alexa and friends count it separately",
|
|
281
288
|
"pattern.notDetected": "not detected",
|
|
282
289
|
"pattern.required": "REQUIRED — missing",
|
|
@@ -292,7 +299,10 @@
|
|
|
292
299
|
"result.header": "What the type-detector makes of it",
|
|
293
300
|
"result.newlyCreated": "will be created",
|
|
294
301
|
"result.noObjectShort": "no object",
|
|
302
|
+
"result.pickSource": "Jump where?",
|
|
295
303
|
"result.pointsFromBranches": "{0} datapoints from {1} branches. ",
|
|
304
|
+
"result.sourceCount": "{0} sources",
|
|
305
|
+
"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
306
|
"result.sourceGone": "The source {0} no longer exists.",
|
|
297
307
|
"result.states": "{0} states",
|
|
298
308
|
"result.statesDirect": "{0} states ({1} directly)",
|
|
@@ -323,6 +333,7 @@
|
|
|
323
333
|
"swap.mindHint": "The alias formulas stay as they are — if the new source scales differently, the values will be wrong afterwards. The formula sits in the point's detail row.",
|
|
324
334
|
"swap.noDevice": "No device found.",
|
|
325
335
|
"swap.noMatch": "no matching point found",
|
|
336
|
+
"swap.noWriteMatch": "no write target found — this point will not write anywhere",
|
|
326
337
|
"swap.now": "Swap now",
|
|
327
338
|
"swap.removeIt": "remove this point while swapping",
|
|
328
339
|
"swap.same": "That is the same source as before.",
|
|
@@ -412,7 +423,7 @@
|
|
|
412
423
|
"tpls.id": "Id",
|
|
413
424
|
"tpls.json": "This is what gets stored",
|
|
414
425
|
"tpls.loadFailed": "own templates not loaded",
|
|
415
|
-
"tpls.loadFailedHint": "The instance configuration did not answer
|
|
426
|
+
"tpls.loadFailedHint": "The instance configuration did not answer or reported an error. Your own templates are not gone — they are only missing from this view. While this notice stands, saving may overwrite an existing template: reload first.",
|
|
416
427
|
"tpls.losesTo": "loses to {0}",
|
|
417
428
|
"tpls.multiple": "This device has several outputs",
|
|
418
429
|
"tpls.multipleHelp": "The {0} in the paths is replaced by %N% — only at the end of a segment, so ENERGY.Power stays intact.",
|
|
@@ -454,6 +465,7 @@
|
|
|
454
465
|
"tpls.wissen": "+ {0} more states as role knowledge — used in the “All data points of the device” view",
|
|
455
466
|
"tree.aliases": "Edit alias",
|
|
456
467
|
"tree.collapseAll": "collapse all",
|
|
468
|
+
"tree.emptyNode": "no datapoints — the node exists in the database but is empty",
|
|
457
469
|
"tree.expandAll": "expand all",
|
|
458
470
|
"tree.filter": "filter …",
|
|
459
471
|
"tree.filterAria": "Filter objects",
|
|
@@ -536,6 +548,7 @@
|
|
|
536
548
|
"write.dryTitleFailed": "Not everything went through",
|
|
537
549
|
"write.dryTitleUpdate": "Update alias — this is what would appear",
|
|
538
550
|
"write.duplicateName": "{0}: name appears twice.",
|
|
551
|
+
"write.enumsUnknown": "Room and function will not be written: the enumerations could not be loaded. The alias itself is created as shown. Without a reliable state an existing enumeration would be replaced by a fresh one holding just this single member.",
|
|
539
552
|
"write.failed": "{0} operations failed:",
|
|
540
553
|
"write.new": "new",
|
|
541
554
|
"write.noName": "One point has no name.",
|
|
@@ -562,5 +575,16 @@
|
|
|
562
575
|
"write.willKeep": "kept: {0}",
|
|
563
576
|
"write.willRemove": "removed: {0}",
|
|
564
577
|
"write.writeSourceMissing": "{0}: the send datapoint {1} does not exist yet",
|
|
565
|
-
"write.writing": "writing …"
|
|
578
|
+
"write.writing": "writing …",
|
|
579
|
+
"tpls.idTaken": "This id already belongs to “{0}” (v{1}). Saving would replace it.",
|
|
580
|
+
"tpls.nameHintBad": "Not a usable expression — it counts as “no match”.",
|
|
581
|
+
"tv.badName": "The name must be a string or a language object.",
|
|
582
|
+
"tv.badRecog": "“erkennung” must be an object.",
|
|
583
|
+
"tv.badList": "“{0}” must be a list of points.",
|
|
584
|
+
"tv.badContent": "“erkennung.inhalt” must be an object.",
|
|
585
|
+
"tv.badNameHint": "The name hint is not a usable expression.",
|
|
586
|
+
"tv.badType": "This detector does not know the device type “{0}”.",
|
|
587
|
+
"check.sendUnknown": "Whether {0} may publish is not stated in the object — the topic sits in `native` only. Normal for ioBroker.mqtt and sonoff.",
|
|
588
|
+
"del.channelsBelow": "Careful: {0} channels of their own sit below this and will be deleted too.",
|
|
589
|
+
"detail.formulaTooSlow": "This formula took {0} ms and is no longer executed. Reloading the tab gives it one more try."
|
|
566
590
|
}
|
|
@@ -221,8 +221,18 @@ export function raumVorschlag(e) {
|
|
|
221
221
|
|
|
222
222
|
/* 3. Aus dem Pfad der Quelle */
|
|
223
223
|
if (quelle) {
|
|
224
|
+
/* Von hinten, das LETZTE Segment eingeschlossen.
|
|
225
|
+
|
|
226
|
+
Frueher begann die Schleife eins davor — gedacht fuer
|
|
227
|
+
`…Wohnzimmer.Lampe`, wo hinten das Geraet steht. Bei einem flachen
|
|
228
|
+
Adapter wie `sonoff.0.Wohnzimmer` IST das letzte Segment aber der
|
|
229
|
+
Raum, und die Schleife lief kein einziges Mal: kein Vorschlag,
|
|
230
|
+
obwohl der Raum im Namen steht (gemessen 09.09.2026 —
|
|
231
|
+
`sonoff.0.Wohnzimmer.Lampe` ergab `enum.rooms.living_room`,
|
|
232
|
+
`sonoff.0.Wohnzimmer` nichts). Der Weg ueber den Geraetenamen
|
|
233
|
+
faengt es nicht auf: der geht bei Name gleich Segment leer aus. */
|
|
224
234
|
var qt = quelle.split('.');
|
|
225
|
-
for (var j = qt.length -
|
|
235
|
+
for (var j = qt.length - 1; j >= 2; j--) {
|
|
226
236
|
var qr = suche(qt[j]);
|
|
227
237
|
if (qr) { return qr; }
|
|
228
238
|
}
|
package/admin/js/baum.js
CHANGED
|
@@ -8,7 +8,7 @@ import { S } from './zustand.js';
|
|
|
8
8
|
import { $, el } from './basis.js';
|
|
9
9
|
import { tr, txt, expertenVorgabe } from './sprache.js';
|
|
10
10
|
import './enums.js';
|
|
11
|
-
import { mitNachfrage } from './entwurf.js';
|
|
11
|
+
import { mitNachfrage, aboLoesen } from './entwurf.js';
|
|
12
12
|
import { zeichneErgebnis } from './ergebnis.js';
|
|
13
13
|
import { zeichneVorlagenListe } from './vorlagenblatt.js';
|
|
14
14
|
|
|
@@ -70,7 +70,7 @@ window.addEventListener('storage', function (e) {
|
|
|
70
70
|
/* Steht die Auswahl in einem Zweig, den es gleich nicht mehr gibt, muss
|
|
71
71
|
sie weg — sonst zeigt die rechte Seite ein Geraet, das links fehlt. */
|
|
72
72
|
if (S.current && !experte() && nurFuerExperten(S.current, S.objects[S.current])) {
|
|
73
|
-
S.current = null;
|
|
73
|
+
S.current = null; aboLoesen();
|
|
74
74
|
S.entwurf = null;
|
|
75
75
|
}
|
|
76
76
|
zeichneBaum();
|
|
@@ -94,10 +94,6 @@ export function zusatzName(id) {
|
|
|
94
94
|
return (n === String(id).split('.').pop()) ? '' : n;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
/* Wonach die Zeile gelesen und sortiert wird. */
|
|
98
|
-
export function anzeigeText(k) {
|
|
99
|
-
return zusatzName(k.id) || k.name;
|
|
100
|
-
}
|
|
101
97
|
|
|
102
98
|
function baueBaum() {
|
|
103
99
|
var wurzel = { kinder: {}, id: '', name: '' };
|
|
@@ -152,6 +148,33 @@ function zaehleZustaende(k, eltern) {
|
|
|
152
148
|
}
|
|
153
149
|
|
|
154
150
|
|
|
151
|
+
/* Ist dieser Knoten aufgeklappt?
|
|
152
|
+
|
|
153
|
+
Stand frueher mitten im Zeichnen einer Zeile — und damit erst,
|
|
154
|
+
nachdem der ganze Teilbaum schon als DOM entstanden war. Fuer
|
|
155
|
+
„zugeklapptes gar nicht erst zeichnen" muss die Antwort vorher da
|
|
156
|
+
sein, also hier. */
|
|
157
|
+
function klappStand(k, tiefe) {
|
|
158
|
+
if ((($('#q') || {}).value || '').trim()) { return true; }
|
|
159
|
+
var vorgabe = (S.alleKlapp === null) ? (tiefe <= 1) : S.alleKlapp;
|
|
160
|
+
/* Im Aliasbaum gibt es unterhalb der Wurzel nur eigene Ordner - ganz
|
|
161
|
+
zugeklappt bliebe eine einzige Zeile `alias` uebrig, und jeder Weg
|
|
162
|
+
begaenne mit zwei Pflichtklicks. Bis `alias.0` bleibt deshalb offen.
|
|
163
|
+
Das steht hier und nicht im Zuklapp-Knopf: sonst gilt es nur, wenn
|
|
164
|
+
man IM Aliasmodus zuklappt - wer in den Quellen zuklappt und dann
|
|
165
|
+
herueberwechselt, fand den Baum zu weit zu (Ricardo, 25.08.2026).
|
|
166
|
+
Einzeln zuklappen darf man sie weiterhin, `S.aufgeklappt` gewinnt. */
|
|
167
|
+
if (S.baumModus === 'aliase' && (k.id === 'alias' || k.id === 'alias.0')) {
|
|
168
|
+
vorgabe = true;
|
|
169
|
+
}
|
|
170
|
+
return S.aufgeklappt[k.id] !== undefined ? S.aufgeklappt[k.id] : vorgabe;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Ein Vergleicher fuer den ganzen Baum. `localeCompare` mit
|
|
174
|
+
Optionsobjekt baut bei jedem Aufruf einen neuen — bei 24.800 Elementen
|
|
175
|
+
sind das je Sortierlauf zehntausende. */
|
|
176
|
+
var VERGLEICH = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
|
|
177
|
+
|
|
155
178
|
export function zeichneBaum() {
|
|
156
179
|
if (S.baumModus === 'vorlagen') { return zeichneVorlagenListe(); }
|
|
157
180
|
S.baum = baueBaum();
|
|
@@ -232,6 +255,23 @@ export function istWaehlbar(k) {
|
|
|
232
255
|
/* Notbehelf nur dort, wo im ganzen Zweig keine Typen gepflegt sind.
|
|
233
256
|
Wo es getypte Kanäle gibt, sollen die das Gerät sein. */
|
|
234
257
|
if (k._hatTypen) { return false; }
|
|
258
|
+
/* Aber niemals die Wurzel selbst.
|
|
259
|
+
|
|
260
|
+
`mqtt-client` und `mqtt-client.0` sind kein Geraet, sondern der
|
|
261
|
+
Adapter und seine Instanz. Waehlbar waren sie trotzdem, sobald im
|
|
262
|
+
Zweig keine Typen gepflegt sind — und die Auswahl abonniert
|
|
263
|
+
`<id>.*` und holt die Werte des ganzen Namensraums (am Testsystem
|
|
264
|
+
322 Objekte, in einer gewachsenen Anlage ein Vielfaches). Aufklappen
|
|
265
|
+
ja, auswaehlen nein.
|
|
266
|
+
|
|
267
|
+
Gemeint ist die STELLE im Baum, nicht `_tiefe`: Das ist die Hoehe
|
|
268
|
+
des Teilbaums UNTER dem Knoten, nicht seine Position darin. In
|
|
269
|
+
0.9.6 stand hier `k._tiefe <= 1`, und das schloss am Testsystem 131
|
|
270
|
+
Knoten aus — genau einen davon zu Recht. Die anderen 130 waren
|
|
271
|
+
Sparten wie `…Decklenlicht_RGB.cmnd` und Ordner wie `iot.0.smart`,
|
|
272
|
+
also Knoten, deren Datenpunkte direkt darunter liegen (gefunden
|
|
273
|
+
10.09.2026 beim Nachmessen von B23). */
|
|
274
|
+
if (k.id.split('.').length <= 2) { return false; }
|
|
235
275
|
/* Sonst: alles waehlbar. Was ein Geraet ist, weiss nur der Mensch —
|
|
236
276
|
bei Duneweaver ist es der Knoten darueber, bei Tasmota der darunter.
|
|
237
277
|
Statt zu sperren wird geraten (siehe wohlGeraet) und gewarnt. */
|
|
@@ -271,17 +311,37 @@ function renderKinder(knoten, filter, tiefe) {
|
|
|
271
311
|
return c._zust && !(c.art === 'state' && !Object.keys(c.kinder).length);
|
|
272
312
|
});
|
|
273
313
|
}
|
|
314
|
+
/* Einmal je Reihe, nicht bei jedem Vergleich.
|
|
315
|
+
|
|
316
|
+
`klappbar` laeuft ueber alle Enkel, und ein Sortiervergleich ruft es
|
|
317
|
+
zweimal — bei n Reihen also 2·n·log(n) Enkel-Schleifen statt n.
|
|
318
|
+
Dasselbe gilt fuer `localeCompare` mit Optionsobjekt: Das baut je
|
|
319
|
+
Aufruf einen neuen Vergleicher. Ein `Intl.Collator` tut dasselbe
|
|
320
|
+
einmal. */
|
|
321
|
+
reihen.forEach(function (r) { r.kl = klappbar(r.k) ? 0 : 1; r.txt = String(r.zus || r.k.name); });
|
|
274
322
|
reihen.sort(function (a, b) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (ba !== bb) { return ba - bb; }
|
|
278
|
-
return String(a.zus || a.k.name).localeCompare(String(b.zus || b.k.name),
|
|
279
|
-
undefined, { numeric: true, sensitivity: 'base' });
|
|
323
|
+
if (a.kl !== b.kl) { return a.kl - b.kl; }
|
|
324
|
+
return VERGLEICH.compare(a.txt, b.txt);
|
|
280
325
|
});
|
|
281
326
|
|
|
282
327
|
reihen.forEach(function (reihe) {
|
|
283
328
|
var k = reihe.k;
|
|
284
|
-
|
|
329
|
+
/* Leere Knoten ausgrauen statt weglassen.
|
|
330
|
+
|
|
331
|
+
Ein Kanal ohne Datenpunkte gibt keinen Alias her - deshalb blieb
|
|
332
|
+
er bisher ganz weg. Nur sieht das nicht nach „leer" aus, sondern
|
|
333
|
+
nach „gibt es nicht": Ricardos Heizungsgruppen standen im Admin
|
|
334
|
+
mit sieben Kanaelen, in der Werkbank mit zweien, und es sah wie
|
|
335
|
+
ein Anzeigefehler aus, waehrend in Wahrheit hm-rpc die
|
|
336
|
+
Datenpunkte nicht angelegt hatte (08.09.2026). Wer den Bestand
|
|
337
|
+
sucht, soll die Luecke sehen.
|
|
338
|
+
|
|
339
|
+
Nur echte Objekte, keine blossen Kennungsknoten: `channel`,
|
|
340
|
+
`device`, `folder`. Waehlbar sind sie nicht (`istWaehlbar` steigt
|
|
341
|
+
bei `_zust === 0` aus), also fuehrt auch kein Klick ins Leere.
|
|
342
|
+
Produktiv sind das 194 Zeilen zu 3432 - gut sechs Prozent. */
|
|
343
|
+
var leer = !k._zust;
|
|
344
|
+
if (leer && k.art !== 'channel' && k.art !== 'device' && k.art !== 'folder') { return; }
|
|
285
345
|
/* Zustaende zeigt die rechte Seite, nicht der Baum - es sei denn,
|
|
286
346
|
unter dem Zustand liegt noch etwas. Dann ist er ein Behaelter und
|
|
287
347
|
muss sichtbar sein, sonst hilft es nichts, dass `istWaehlbar` ihn
|
|
@@ -299,30 +359,36 @@ function renderKinder(knoten, filter, tiefe) {
|
|
|
299
359
|
|
|
300
360
|
/* Der Filter sucht auch im Namen. Wer „Badezimmer" tippt, meint
|
|
301
361
|
FK_Badezimmer — und nicht die Seriennummer, die er nicht kennt. */
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
var
|
|
305
|
-
|
|
362
|
+
/* Beim Filtern zaehlen leere Knoten nicht als Treffer - sonst
|
|
363
|
+
findet die Suche Zeilen, mit denen sich nichts anfangen laesst. */
|
|
364
|
+
var passt = !filter
|
|
365
|
+
? true
|
|
366
|
+
: (!leer && (k.id.toLowerCase().indexOf(filter) !== -1 ||
|
|
367
|
+
(reihe.zus && reihe.zus.toLowerCase().indexOf(filter) !== -1)));
|
|
368
|
+
/* Zugeklappte Knoten gar nicht erst zeichnen.
|
|
369
|
+
|
|
370
|
+
Vorher entstand IMMER der ganze Teilbaum als DOM — auch der eines
|
|
371
|
+
zugeklappten Knotens, der gleich darauf weggeworfen wurde. Bei
|
|
372
|
+
einer gewachsenen Anlage ist das der ganz ueberwiegende Teil der
|
|
373
|
+
Arbeit.
|
|
374
|
+
|
|
375
|
+
Zwei Ausnahmen, in denen der Teilbaum gebraucht wird: beim Filtern
|
|
376
|
+
(ein Treffer weiter unten macht den Elternknoten sichtbar) und
|
|
377
|
+
wenn der Knoten aufgeklappt ist. Sonst genuegt die Frage, OB es
|
|
378
|
+
zeigbare Kinder gibt — und die beantwortet `klappbar` ohne DOM. */
|
|
379
|
+
var aufJetzt = klappStand(k, tiefe);
|
|
380
|
+
var brauchtKinder = !!filter || aufJetzt;
|
|
381
|
+
var kinderUl = brauchtKinder ? renderKinder(k, filter, tiefe + 1) : null;
|
|
382
|
+
var kinderPassen = kinderUl ? kinderUl.childNodes.length > 0 : klappbar(k);
|
|
306
383
|
if (!passt && !kinderPassen) { return; }
|
|
307
384
|
|
|
308
385
|
var li = el('li');
|
|
309
386
|
var waehlbar = istWaehlbar(k);
|
|
310
|
-
var d = el('div', 'node' + (waehlbar ? ' pick' : ' grp') + (
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
var
|
|
314
|
-
|
|
315
|
-
zugeklappt bliebe eine einzige Zeile `alias` uebrig, und jeder Weg
|
|
316
|
-
begaenne mit zwei Pflichtklicks. Bis `alias.0` bleibt deshalb offen.
|
|
317
|
-
Das steht hier und nicht im Zuklapp-Knopf: sonst gilt es nur, wenn
|
|
318
|
-
man IM Aliasmodus zuklappt - wer in den Quellen zuklappt und dann
|
|
319
|
-
herueberwechselt, fand den Baum zu weit zu (Ricardo, 25.08.2026).
|
|
320
|
-
Einzeln zuklappen darf man sie weiterhin, `S.aufgeklappt` gewinnt. */
|
|
321
|
-
if (S.baumModus === 'aliase' && (k.id === 'alias' || k.id === 'alias.0')) {
|
|
322
|
-
vorgabe = true;
|
|
323
|
-
}
|
|
324
|
-
var auf = filter ? true
|
|
325
|
-
: (S.aufgeklappt[k.id] !== undefined ? S.aufgeklappt[k.id] : vorgabe);
|
|
387
|
+
var d = el('div', 'node' + (waehlbar ? ' pick' : ' grp') + (leer ? ' leer' : '') +
|
|
388
|
+
(k.id === S.current ? ' sel' : ''));
|
|
389
|
+
|
|
390
|
+
var hatKinder = kinderPassen;
|
|
391
|
+
var auf = aufJetzt;
|
|
326
392
|
var tw = el('span', 'tw', hatKinder ? (auf ? '▾' : '▸') : '');
|
|
327
393
|
if (hatKinder) {
|
|
328
394
|
tw.addEventListener('click', function (e) {
|
|
@@ -342,6 +408,9 @@ function renderKinder(knoten, filter, tiefe) {
|
|
|
342
408
|
d.appendChild(nm2);
|
|
343
409
|
d.title = k.id + ' · ' + reihe.zus;
|
|
344
410
|
}
|
|
411
|
+
/* Der Hinweis kommt hinter den Zusatznamen, nicht davor - sonst
|
|
412
|
+
ueberschreibt ihn die Zeile darueber wieder. */
|
|
413
|
+
if (leer) { d.title = (d.title ? d.title + ' · ' : '') + tr('tree.emptyNode'); }
|
|
345
414
|
if (waehlbar) {
|
|
346
415
|
if (wohlGeraet(k)) {
|
|
347
416
|
var pk = el('span', 'geraetepunkt', '●');
|
|
@@ -371,13 +440,25 @@ function renderKinder(knoten, filter, tiefe) {
|
|
|
371
440
|
});
|
|
372
441
|
}
|
|
373
442
|
li.appendChild(d);
|
|
374
|
-
if (hatKinder && auf) { li.appendChild(kinderUl); }
|
|
443
|
+
if (hatKinder && auf && kinderUl) { li.appendChild(kinderUl); }
|
|
375
444
|
ul.appendChild(li);
|
|
376
445
|
});
|
|
377
446
|
return ul;
|
|
378
447
|
}
|
|
379
448
|
|
|
380
|
-
|
|
449
|
+
/* Entprellt, nicht bei jedem Anschlag.
|
|
450
|
+
|
|
451
|
+
Der Baum wurde bei JEDEM Tastendruck komplett neu gebaut. Am
|
|
452
|
+
Produktivsystem mit 24.786 Objekten gemessen (10.09.2026): 236 ms je
|
|
453
|
+
Anschlag, acht Anschlaege 1,9 Sekunden — die Eingabe hinkte sichtbar
|
|
454
|
+
hinterher. 180 ms sind kurz genug, dass es nach dem Tippen sofort
|
|
455
|
+
dasteht, und lang genug, dass eine fluessig getippte Folge nur einmal
|
|
456
|
+
zeichnet. */
|
|
457
|
+
var suchTimer = null;
|
|
458
|
+
$('#q').addEventListener('input', function () {
|
|
459
|
+
if (suchTimer) { clearTimeout(suchTimer); }
|
|
460
|
+
suchTimer = setTimeout(function () { suchTimer = null; zeichneBaum(); }, 180);
|
|
461
|
+
});
|
|
381
462
|
|
|
382
463
|
/* Alles auf, alles zu. Die einzeln gemerkten Knoten fallen dabei weg -
|
|
383
464
|
sonst bliebe ein Zweig zu, den man eben ausdruecklich zugeklappt hat,
|
|
@@ -393,7 +474,7 @@ function klappeAlle(auf) {
|
|
|
393
474
|
Abschalten des Expertenmodus. Geprueft wird an der gezeichneten
|
|
394
475
|
Liste, nicht an der Datenlage: sichtbar ist, was im Baum steht. */
|
|
395
476
|
if (S.current && !auf && !imBaumSichtbar()) {
|
|
396
|
-
S.current = null;
|
|
477
|
+
S.current = null; aboLoesen();
|
|
397
478
|
S.entwurf = null;
|
|
398
479
|
S.openRow = null;
|
|
399
480
|
zeichneErgebnis();
|
|
@@ -425,10 +506,10 @@ export function setzeModus(m) {
|
|
|
425
506
|
Der Vorlagenbaum ist ausgenommen: er hat eine eigene Ansicht, und
|
|
426
507
|
wer von dort zurueckwechselt, findet seine Auswahl wieder vor. */
|
|
427
508
|
if (m === 'quellen' && S.current && S.current.indexOf('alias.') === 0) {
|
|
428
|
-
S.current = null; S.entwurf = null; S.openRow = null;
|
|
509
|
+
S.current = null; aboLoesen(); S.entwurf = null; S.openRow = null;
|
|
429
510
|
}
|
|
430
511
|
if (m === 'aliase' && S.current && S.current.indexOf('alias.') !== 0) {
|
|
431
|
-
S.current = null; S.entwurf = null; S.openRow = null;
|
|
512
|
+
S.current = null; aboLoesen(); S.entwurf = null; S.openRow = null;
|
|
432
513
|
}
|
|
433
514
|
/* Beim Moduswechsel den Filter fallen lassen und die Geraeteknoepfe
|
|
434
515
|
zurueckholen. Sonst stand man im Vorlagenbaum vor „nichts gefunden"
|
package/admin/js/detail.js
CHANGED
|
@@ -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,
|
|
@@ -97,7 +149,14 @@ export function detailZeile(e, s, idx) {
|
|
|
97
149
|
Eine Zeile kann beides tragen — etwa nach einem Musterwechsel, der
|
|
98
150
|
die Rolle anpasst: Die neue Rolle weicht dann vom Alias ab, und je
|
|
99
151
|
nachdem auch von der Vorlage (Ricardo, 06.09.2026). */
|
|
100
|
-
|
|
152
|
+
/* `e.ziel || e.kanal` — wie bei den beiden Nachbarn.
|
|
153
|
+
|
|
154
|
+
Im Aliasmodus ist `e.ziel` nie gesetzt: Man steht ja schon auf dem
|
|
155
|
+
Alias, das Ziel waehlt man erst im Quellenmodus. Die Marke „weicht vom
|
|
156
|
+
Alias ab" erschien dort also NIE — ausgerechnet dort, wo sie am
|
|
157
|
+
meisten sagt (gemessen 09.09.2026: mit `e.ziel` null Abweichungen, mit
|
|
158
|
+
`e.ziel || e.kanal` eine). */
|
|
159
|
+
var bstListe = bestandsAbweichung(s, e.ziel || e.kanal);
|
|
101
160
|
var bstNach = {};
|
|
102
161
|
bstListe.forEach(function (x) { bstNach[x.feld] = x; });
|
|
103
162
|
|
|
@@ -165,7 +224,7 @@ export function detailZeile(e, s, idx) {
|
|
|
165
224
|
var selR = el('select', 'tx');
|
|
166
225
|
selR.style.width = '260px';
|
|
167
226
|
selR.appendChild(opt('', quellen.length ? tr('detail.pickOne') : tr('detail.noSource')));
|
|
168
|
-
|
|
227
|
+
quellenIn(selR, quellen);
|
|
169
228
|
if (s.srcR && quellen.indexOf(s.srcR) === -1) { selR.appendChild(opt(s.srcR, s.srcR + ' ' + tr('detail.elsewhere'))); }
|
|
170
229
|
selR.appendChild(opt('__frei__', tr('detail.otherObject')));
|
|
171
230
|
selR.value = s.srcR || '';
|
|
@@ -175,8 +234,16 @@ export function detailZeile(e, s, idx) {
|
|
|
175
234
|
if (selR.value === '__frei__') { s.frei = true; s.srcR = ''; }
|
|
176
235
|
else { s.frei = false; s.srcR = selR.value; }
|
|
177
236
|
if (s.srcR && !S.werte[s.srcR]) {
|
|
178
|
-
|
|
179
|
-
|
|
237
|
+
/* Die Kennung festhalten, bevor gefragt wird.
|
|
238
|
+
|
|
239
|
+
Der Rueckruf legte den Wert unter der Quelle ab, die BEIM
|
|
240
|
+
EINTREFFEN eingestellt war. Schaltet man in der Zwischenzeit
|
|
241
|
+
weiter, landet der Wert der alten Quelle unter der neuen — und
|
|
242
|
+
da nur geholt wird, was noch fehlt, bleibt es dabei: die neue
|
|
243
|
+
gilt als bekannt (gemessen 09.09.2026). */
|
|
244
|
+
var geholt = s.srcR;
|
|
245
|
+
socket.emit('getState', geholt, function (err, st) {
|
|
246
|
+
if (!err && st) { S.werte[geholt] = st; zeichneErgebnis(); }
|
|
180
247
|
});
|
|
181
248
|
}
|
|
182
249
|
neu();
|
|
@@ -215,7 +282,7 @@ export function detailZeile(e, s, idx) {
|
|
|
215
282
|
lF.appendChild(selF);
|
|
216
283
|
qf.appendChild(lF);
|
|
217
284
|
}
|
|
218
|
-
zeigeBeide(zeile(tr('detail.readsFrom'), qf), ['srcR']);
|
|
285
|
+
zeigeBeide(zeile(tr('detail.readsFrom'), mitVollId(qf, s.srcR)), ['srcR']);
|
|
219
286
|
|
|
220
287
|
if (s.frei) {
|
|
221
288
|
var iFrei = el('input', 'tx w');
|
|
@@ -238,7 +305,7 @@ export function detailZeile(e, s, idx) {
|
|
|
238
305
|
ein, damit im Feld steht, worauf es hinausgeht. */
|
|
239
306
|
selW.appendChild(opt('', tr('detail.noWrite')));
|
|
240
307
|
if (s.srcR && s.srcW !== s.srcR) { selW.appendChild(opt('=lesen', tr('detail.sameAsRead'))); }
|
|
241
|
-
|
|
308
|
+
quellenIn(selW, quellen);
|
|
242
309
|
if (s.srcW && quellen.indexOf(s.srcW) === -1) { selW.appendChild(opt(s.srcW, s.srcW)); }
|
|
243
310
|
selW.value = s.srcW || '';
|
|
244
311
|
selW.addEventListener('click', function (ev) { ev.stopPropagation(); });
|
|
@@ -255,7 +322,7 @@ export function detailZeile(e, s, idx) {
|
|
|
255
322
|
if (s.srcW && s.srcW !== s.srcR) {
|
|
256
323
|
wf.appendChild(el('div', 'sugg', tr('detail.separateHint')));
|
|
257
324
|
}
|
|
258
|
-
zeigeBeide(zeile(tr('detail.writesTo'), wf), ['srcW']);
|
|
325
|
+
zeigeBeide(zeile(tr('detail.writesTo'), mitVollId(wf, s.srcW)), ['srcW']);
|
|
259
326
|
|
|
260
327
|
/* --- Leseformel --- */
|
|
261
328
|
var fb = el('div');
|
|
@@ -311,7 +378,7 @@ export function detailZeile(e, s, idx) {
|
|
|
311
378
|
if (must) {
|
|
312
379
|
var treffer = null;
|
|
313
380
|
must.states.forEach(function (p) {
|
|
314
|
-
if (!treffer && p.role &&
|
|
381
|
+
if (!treffer && p.role && rolleTrifft(p.role, s.role)) { treffer = p; }
|
|
315
382
|
});
|
|
316
383
|
var hin = el('div', 'sugg');
|
|
317
384
|
if (treffer) {
|