iobroker.alias-workbench 0.8.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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +516 -0
  3. package/admin/alias-workbench.png +0 -0
  4. package/admin/css/werkbank.css +916 -0
  5. package/admin/detector.js +1 -0
  6. package/admin/geraetewissen/tasmota-befehle.json +116 -0
  7. package/admin/i18n/de.json +552 -0
  8. package/admin/i18n/en.json +552 -0
  9. package/admin/js/aufzaehlungen.js +332 -0
  10. package/admin/js/basis.js +36 -0
  11. package/admin/js/baum.js +453 -0
  12. package/admin/js/detail.js +335 -0
  13. package/admin/js/einstellungen.js +40 -0
  14. package/admin/js/entwurf.js +908 -0
  15. package/admin/js/enums.js +23 -0
  16. package/admin/js/ergebnis.js +934 -0
  17. package/admin/js/erkennung.js +126 -0
  18. package/admin/js/katalog.js +313 -0
  19. package/admin/js/mqtt.js +885 -0
  20. package/admin/js/musternamen.js +94 -0
  21. package/admin/js/musterwahl.js +334 -0
  22. package/admin/js/objekte.js +243 -0
  23. package/admin/js/pruefungen.js +130 -0
  24. package/admin/js/quellentausch.js +570 -0
  25. package/admin/js/rollenwahl.js +199 -0
  26. package/admin/js/schreiben.js +1537 -0
  27. package/admin/js/sprache.js +137 -0
  28. package/admin/js/start.js +39 -0
  29. package/admin/js/verbindung.js +10 -0
  30. package/admin/js/vorlagen.js +572 -0
  31. package/admin/js/vorlagenblatt.js +1907 -0
  32. package/admin/js/vorschlagen.js +391 -0
  33. package/admin/js/werte.js +193 -0
  34. package/admin/js/zielleiste.js +339 -0
  35. package/admin/js/zuordnung.js +695 -0
  36. package/admin/js/zustand.js +47 -0
  37. package/admin/js/zustandsliste.js +515 -0
  38. package/admin/jsonConfig.json +249 -0
  39. package/admin/tab.html +148 -0
  40. package/admin/vorlagen/hm-dimmer.json +204 -0
  41. package/admin/vorlagen/hm-fensterkontakt-hm.json +176 -0
  42. package/admin/vorlagen/hm-fensterkontakt-hmip.json +191 -0
  43. package/admin/vorlagen/hm-heizgruppe.json +214 -0
  44. package/admin/vorlagen/hm-heizkoerper.json +236 -0
  45. package/admin/vorlagen/hm-rollladen.json +190 -0
  46. package/admin/vorlagen/hm-schaltaktor-lampe.json +197 -0
  47. package/admin/vorlagen/hm-schaltaktor-mehrfach.json +203 -0
  48. package/admin/vorlagen/hm-schaltaktor-steckdose.json +197 -0
  49. package/admin/vorlagen/hm-temperatur-feuchte.json +166 -0
  50. package/admin/vorlagen/hm-wandthermostat.json +228 -0
  51. package/admin/vorlagen/index.json +21 -0
  52. package/admin/vorlagen/messpunkt.json +225 -0
  53. package/admin/vorlagen/tasmota-lampe.json +263 -0
  54. package/admin/vorlagen/tasmota-mehrfach.json +203 -0
  55. package/admin/vorlagen/tasmota-rgb.json +239 -0
  56. package/admin/vorlagen/tasmota-steckdose.json +265 -0
  57. package/io-package.json +122 -0
  58. package/package.json +48 -0
@@ -0,0 +1,94 @@
1
+ /* Die uebersetzten Namen der Muster, angezapft beim Geraete-Adapter.
2
+
3
+ Der type-detector kennt nur rohe Typnamen (socket, thermostat). Der
4
+ Geraete-Adapter fuehrt fuer genau diese Typen fertige Uebersetzungen
5
+ in elf Sprachen - in seinem Bundle, ein Block je Sprache, und jeder
6
+ Block gibt sich am Eintrag "type-Device type" selbst zu erkennen.
7
+ Wir holen den Block der Oberflaechensprache und schlagen darin nach;
8
+ eine eigene Liste wuerde bei jedem type-detector-Update altern
9
+ (Ricardos Punkt 13, 25.08.2026 - Weg 2, wie der Raum/Funktion-Katalog).
10
+
11
+ Bricht die Kette - Adapter fehlt, Bundle umgebaut - bleibt die Karte
12
+ leer und ueberall steht der rohe Name. Kein Fehler, nur weniger
13
+ Komfort. Beide Bundle-Fassungen werden verstanden: die neue (4.x)
14
+ schreibt Werte in Backticks, die alte (1.x, produktiv) als
15
+ JSON-Strings. */
16
+
17
+ import { sprache } from './sprache.js';
18
+ import { holeText } from './basis.js';
19
+
20
+ var musterNamen = {};
21
+ var geholt = false;
22
+
23
+ /* Woran sich der gesuchte Sprachblock zu erkennen gibt. Mehr Sprachen
24
+ kennt die Werkbank nicht (SPRACHEN in sprache.js). */
25
+ var ANKER = { de: 'Ger\u00e4tetyp', en: 'Device type' };
26
+
27
+ function ziehe(src) {
28
+ var anker = ANKER[sprache] || ANKER.en;
29
+ var re = /"type-Device type":(?:`([^`]*)`|"((?:[^"\\]|\\.)*)")/g;
30
+ var m, start = -1;
31
+ while ((m = re.exec(src))) {
32
+ if ((m[1] !== undefined ? m[1] : m[2]) === anker) { start = m.index; break; }
33
+ }
34
+ if (start < 0) { return null; }
35
+ /* Der Block enthaelt nur type-Schluessel - das naechste } beendet ihn. */
36
+ var teil = src.slice(start, src.indexOf('}', start));
37
+ var raus = {};
38
+ var re2 = /"type-([^"]+)":(?:`([^`]*)`|"((?:[^"\\]|\\.)*)")/g;
39
+ var m2;
40
+ while ((m2 = re2.exec(teil))) {
41
+ var wert = m2[2] !== undefined ? m2[2] : m2[3].replace(/\\(.)/g, '$1');
42
+ if (m2[1] !== 'Device type') { raus[m2[1]] = wert; }
43
+ }
44
+ return Object.keys(raus).length ? raus : null;
45
+ }
46
+
47
+ export function ladeMusterNamen(nachher) {
48
+ if (geholt) { return; }
49
+ geholt = true;
50
+ holeText('/adapter/devices/tab.html').then(function (html) {
51
+ /* Die tab.html nennt mehrere Bausteine (Bootstrap-Haeppchen,
52
+ Vendor, Hauptbundle). Die Karte steckt im Hauptbundle - das
53
+ heisst in beiden bekannten Fassungen index-<hash>.js, also
54
+ dieses zuerst, die uebrigen als Rueckfall der Reihe nach. */
55
+ var dateien = (html.match(/assets\/[A-Za-z0-9_\-.]+\.js/g) || [])
56
+ .filter(function (x, i, a) { return a.indexOf(x) === i; })
57
+ .sort(function (a, b) {
58
+ return (b.indexOf('assets/index-') === 0 ? 1 : 0)
59
+ - (a.indexOf('assets/index-') === 0 ? 1 : 0);
60
+ })
61
+ .slice(0, 8);
62
+ if (!dateien.length) { throw new Error('kein Bundle'); }
63
+ return dateien.reduce(function (kette, f) {
64
+ return kette.then(function (fertig) {
65
+ if (fertig) { return true; }
66
+ return holeText('/adapter/devices/' + f).then(function (src) {
67
+ var karte = ziehe(src);
68
+ if (karte) { musterNamen = karte; return true; }
69
+ return false;
70
+ })['catch'](function () { return false; });
71
+ });
72
+ }, Promise.resolve(false));
73
+ })['catch'](function () { /* still bleiben: rohe Namen genuegen */ })
74
+ .then(function () { if (typeof nachher === 'function') { nachher(); } });
75
+ }
76
+
77
+ /* Der Beiname eines Musters - leer nur, wenn der Katalog nichts weiss.
78
+ Auch die blosse Grossschreibung („Dimmer“ zu dimmer) zaehlt: sonst
79
+ standen in der Liste grosse Beinamen und kleine rohe Namen gemischt
80
+ (Ricardo, 25.08.2026). Ob der rohe Name dazugehoert, entscheidet die
81
+ Anzeige - sagt der Beiname nur die Schreibung, waere „Dimmer ·
82
+ dimmer“ doppelt gemoppelt. */
83
+ export function musterName(typ) {
84
+ return musterNamen[typ] || '';
85
+ }
86
+
87
+ /* Beiname und roher Name fuer Listen, immer als Paar: „Fenstersensor
88
+ · window“ und auch „Dimmer · dimmer“ - einheitlich lesbar, selbst
89
+ wenn Deutsch und roher Name fast gleich sind (Ricardo, 25.08.2026).
90
+ Nur ohne Katalogeintrag steht der rohe Name allein. */
91
+ export function musterZeile(typ) {
92
+ var n = musterNamen[typ];
93
+ return n ? (n + ' · ' + typ) : typ;
94
+ }
@@ -0,0 +1,334 @@
1
+ /* Die Musterwahl: das Suchfeld mit eigener Liste, die Bewertung je
2
+ Muster (passt / n von m belegt) und der Knopf, der freie Plaetze
3
+ vorschlaegt. Gebaut wird beides hier, eingehaengt woanders: der Block
4
+ je nach Ansicht im Vorlagenkasten oder in der Leiste ueber der Liste,
5
+ der Knopf immer in der Leiste.
6
+
7
+ Herausgeloest aus zeichneErgebnis - der Abschnitt hing an `e`, dem
8
+ Erkennungsergebnis und sonst an nichts. */
9
+
10
+ import { S } from './zustand.js';
11
+ import { D, el, $ } from './basis.js';
12
+ import { tr, sprachtext } from './sprache.js';
13
+ import { VORSCHAU, musterVon, erkenneEntwurf } from './erkennung.js';
14
+ import { anMusterAnpassen } from './vorlagen.js';
15
+ import { ratePlaetze, rateZurueck, rateAnzahl, rateMoeglich } from './vorschlagen.js';
16
+ import { zeichneErgebnis, entwurfAngefasst, knopfFrisch } from './ergebnis.js';
17
+ import { musterName, musterZeile } from './musternamen.js';
18
+
19
+ export function baueMusterwahl(e, haupt, pflichtFehlt) {
20
+ var vw = el('div', 'verdictwrap' + (haupt && !pflichtFehlt.length ? '' : ' bad'));
21
+ var vlbl = el('span', 'feldlabel', tr('pattern.label'));
22
+ vw.appendChild(vlbl);
23
+ /* Musterwahl als Eingabefeld mit eigener Liste statt als <select>.
24
+ Ein <select> laesst sich nicht durchsuchen, und die 51 Muster
25
+ brauchten deshalb einen Knopf daneben, der die Liste umschaltet.
26
+ Jetzt ist beides im Feld: tippen sucht ueber alle Muster, und die
27
+ letzte Zeile schaltet zwischen den ueblichen und allen um. Kein
28
+ <datalist> - dessen Fenster malt das Betriebssystem, und in
29
+ eingebetteten Ansichten landet es neben dem Fenster. */
30
+ var mwrap = el('div', 'feldwrap');
31
+ var sel = el('input', 'tx musterwahl');
32
+ sel.type = 'text';
33
+ sel.setAttribute('autocomplete', 'off');
34
+ sel.placeholder = tr('pattern.search');
35
+ mwrap.appendChild(sel);
36
+ /* Kein eigenes Pfeilzeichen mehr: das Feld traegt denselben
37
+ SVG-Pfeil wie jedes Auswahlfeld (werkbank.css). Das Zeichen ▾
38
+ ist in der Schrift ein bewusst kleines Dreieck und fiel neben den
39
+ Selects sichtbar ab. */
40
+ var mlist = el('div', 'vorschlaege musterliste');
41
+ mlist.hidden = true;
42
+ mwrap.appendChild(mlist);
43
+ vw.appendChild(mwrap);
44
+
45
+ /* Der Knopf, der die freien Plaetze belegt.
46
+
47
+ Ein Knopf und nicht der Musterwechsel als Ausloeser: sonst haette
48
+ ausgerechnet ein erkanntes Geraet keinen. Ricardos Klimaanlage wird
49
+ als `thermostat` erkannt, man wechselt also gar nichts, und ohne
50
+ Knopf bliebe es dabei.
51
+
52
+ Er wird hier gebaut, weil er zum Muster gehoert - haengt aber unten
53
+ in der Leiste ueber der Zustandsliste. Neben dem Musterfeld stand er
54
+ am Rand; die Liste, die er veraendert, faengt darunter an. */
55
+ /* Und nur, wenn er etwas anzubieten haette: ohne freie Plaetze oder
56
+ freie Zeilen ist der Knopf ein leeres Versprechen — dann faellt er
57
+ weg, statt beim Klick „nichts gefunden" zu melden. Zuruecknehmen
58
+ bleibt immer moeglich, solange Vermutungen im Entwurf stehen. */
59
+ var rateKnopf = null;
60
+ if (rateAnzahl(e) || rateMoeglich(e)) {
61
+ /* Mit passender Vorlage ist Raten nicht der normale Weg - die
62
+ Zuordnung steht ja schon. Der Knopf bleibt, tritt aber leise auf
63
+ und fragt vor dem Lauf nach (Ricardo, 25.08.2026). */
64
+ var beilaeufig = !!e.vorlage && !rateAnzahl(e);
65
+ rateKnopf = el('button', 'btn mini' + (rateAnzahl(e) ? ' wichtig' : '')
66
+ + (beilaeufig ? ' leise' : '')
67
+ /* Ein leiser Knopf pocht nicht - das Pochen wuerde genau die
68
+ Aufmerksamkeit holen, die er nicht verdient. */
69
+ + (!beilaeufig && knopfFrisch('raten:' + S.current + ':' + (rateAnzahl(e) ? 'zurueck' : 'vor')) ? ' frisch' : ''),
70
+ rateAnzahl(e) ? tr('guess.undo') : tr('guess.button'));
71
+ rateKnopf.title = beilaeufig ? tr('guess.tplHint') : tr('guess.hint');
72
+ var rateLos = function () {
73
+ e.rateMeldung = ratePlaetze(e) ? null : tr('guess.none');
74
+ entwurfAngefasst();
75
+ S.openRow = null;
76
+ zeichneErgebnis();
77
+ };
78
+ rateKnopf.addEventListener('click', function () {
79
+ if (rateAnzahl(e)) {
80
+ rateZurueck(e); e.rateMeldung = null;
81
+ entwurfAngefasst();
82
+ S.openRow = null;
83
+ zeichneErgebnis();
84
+ return;
85
+ }
86
+ if (!beilaeufig) { rateLos(); return; }
87
+ var vq = null;
88
+ S.VORLAGEN.forEach(function (v) { if (v.id === e.vorlage) { vq = v; } });
89
+ var body = $('#guess-body');
90
+ body.textContent = '';
91
+ body.appendChild(el('div', 'aside w',
92
+ tr('guess.tplAsk', vq ? sprachtext(vq.name) : e.vorlage)));
93
+ /* onclick statt addEventListener: der Dialog ist einer fuer alle
94
+ Geraete, der Griff muss beim Oeffnen den alten ersetzen. */
95
+ $('#btn-guess-go').onclick = function () {
96
+ $('#dlg-guess').close();
97
+ rateLos();
98
+ };
99
+ $('#dlg-guess').showModal();
100
+ });
101
+ }
102
+
103
+ /* Typen, nicht Schluessel: wer „blinds" waehlte, bekam einen
104
+ Entwurf fuer einen Typ, den der Detektor nicht kennt. */
105
+ var alleTypen = Object.keys(D.patterns)
106
+ .map(function (k) { return D.patterns[k].type || k; })
107
+ .filter(function (t, n, arr) { return arr.indexOf(t) === n; }).sort();
108
+
109
+ /* Die Belegung je Muster auszurechnen kostet einen Erkennungslauf.
110
+ Beim Tippen wird die Liste bei jedem Anschlag neu gemalt, deshalb
111
+ einmal gemerkt statt jedes Mal neu gerechnet. */
112
+ var markeCache = {};
113
+ function bewerte(m) {
114
+ if (markeCache[m] !== undefined) { return markeCache[m]; }
115
+ /* Die Vorschau muss vorhersagen, was das Auswaehlen wirklich tut.
116
+ Beim Vorschlag werden Lampe und Steckdose samt Rollen neu gebaut -
117
+ also auch hier gegen den umgebauten Entwurf pruefen. Sonst stuende
118
+ hier „SET fehlt", und nach dem Klick passte trotzdem alles. */
119
+ var pruefE = e;
120
+ if (e.vorschlag && !e.roh && m !== e.want && musterVon(m)) {
121
+ var kopie = JSON.parse(JSON.stringify({ kanal: e.kanal, states: e.states, want: e.want }));
122
+ anMusterAnpassen(kopie, m);
123
+ if (kopie.states.some(function (x, n) { return x.role !== e.states[n].role; })) {
124
+ pruefE = kopie;
125
+ }
126
+ }
127
+ var r = erkenneEntwurf(pruefE, m), mark;
128
+ if (!r.length) {
129
+ var fehlend = musterVon(m).states.filter(function (x) { return x.required; })
130
+ .map(function (x) { return x.name; });
131
+ mark = fehlend.length ? ('✕ ' + tr('pattern.missing', fehlend.join(', ')))
132
+ : tr('pattern.doesNotFit');
133
+ } else {
134
+ var g = r[0].states.filter(function (x) { return x.id; }).length;
135
+ mark = tr('pattern.slotsTaken', g, r[0].states.length);
136
+ /* Hier stand frueher „(Rollen werden angepasst)" hinter der
137
+ Belegung. Der Vermerk erschien an jeder Zeile ausser der gerade
138
+ gewaehlten — an einem Fensterkontakt also an sieben von acht.
139
+ Was fast ueberall steht, unterscheidet nichts; es verdraengte
140
+ nur Name und Belegung, also genau das, wonach man in der Liste
141
+ sucht. Dass beim Wechsel die Rollen passend gesetzt werden, ist
142
+ ohnehin das gewollte Verhalten und keine Abwaegung, die man vor
143
+ der Auswahl treffen muesste. */
144
+ }
145
+ markeCache[m] = { mark: mark, passt: !!r.length, belegt: r.length ? r[0].states.filter(function (x) { return x.id; }).length : 0 };
146
+ return markeCache[m];
147
+ }
148
+ function markeFuer(m) { return bewerte(m).mark; }
149
+
150
+ /* Welche Muster ohne Suchtext angeboten werden. Das erkannte und das
151
+ gerade gewaehlte gehoeren immer dazu - sonst fiel rgbSingle heraus,
152
+ sobald man einmal auf light gewechselt hatte, und der Weg zurueck
153
+ fuehrte nur ueber die ganze Liste. */
154
+ function grundListe() {
155
+ var l = e.alleMuster ? alleTypen.slice() : VORSCHAU.slice();
156
+ [e.wantAuto, e.want].forEach(function (m0) {
157
+ if (m0 && musterVon(m0) && l.indexOf(m0) === -1) { l.unshift(m0); }
158
+ });
159
+ return l.filter(function (m) { return !!musterVon(m); });
160
+ }
161
+
162
+ /* Gesucht wird ueber den Namen des Musters und ueber die Rollen seiner
163
+ Plaetze. „state" findet so auch light und window, deren Plaetze
164
+ state.light und state.window annehmen - der Name allein haette dazu
165
+ nichts gemeldet. */
166
+ function rollenVon(m) {
167
+ var mu = musterVon(m);
168
+ if (!mu) { return []; }
169
+ return mu.states.map(function (x) { return x.defaultRole || ''; })
170
+ .filter(Boolean).filter(function (r0, n, arr) { return arr.indexOf(r0) === n; });
171
+ }
172
+
173
+ var mMarkiert = -1;
174
+ var mEintraege = [];
175
+ var mSchliessen = function () { mlist.hidden = true; mMarkiert = -1; };
176
+
177
+ var mWaehlen = function (neuTyp) {
178
+ if (!musterVon(neuTyp)) { return; }
179
+ mSchliessen();
180
+ /* Beim Vorschlag reicht es nicht, das Muster umzustellen - die Rollen
181
+ entscheiden. switch.light passt auf kein socket-SET. Also den
182
+ Vorschlag mit den passenden Rollen neu bauen. */
183
+ if (e.vorschlag && !e.roh) {
184
+ anMusterAnpassen(e, neuTyp);
185
+ } else {
186
+ e.want = neuTyp;
187
+ }
188
+ S.openRow = null;
189
+ /* Kein Fokus zurueck ins Feld: der Fokus oeffnete die Liste sofort
190
+ wieder, und nach der Wahl stand sie erneut offen — man musste
191
+ eigens daneben klicken. Die Wahl ist getroffen; wer ein anderes
192
+ Muster will, klickt neu ins Feld. */
193
+ zeichneErgebnis();
194
+ };
195
+
196
+ var mMalen = function () {
197
+ var such = String(sel.value || '').trim().toLowerCase();
198
+ var treffer = [];
199
+ if (!such) {
200
+ treffer = grundListe().map(function (m) { return { m: m, via: null }; });
201
+ } else {
202
+ /* Mit Suchtext wird immer der ganze Bestand durchsucht, nicht nur
203
+ die uebliche Auswahl - sonst muesste man erst umschalten, um
204
+ ueberhaupt finden zu koennen. */
205
+ alleTypen.forEach(function (m) {
206
+ if (!musterVon(m)) { return; }
207
+ /* Wer „Steckdose“ tippt, will socket finden - die Suche geht
208
+ ueber rohen Namen UND Beinamen. */
209
+ if (m.toLowerCase().indexOf(such) > -1
210
+ || musterName(m).toLowerCase().indexOf(such) > -1) { treffer.push({ m: m, via: null }); return; }
211
+ var r0 = rollenVon(m).filter(function (x) { return x.toLowerCase().indexOf(such) > -1; });
212
+ if (r0.length) { treffer.push({ m: m, via: r0[0] }); }
213
+ });
214
+ }
215
+ /* Was passt, steht oben. Ein Muster, dem eine Pflichtangabe fehlt,
216
+ ist keine Wahl, die man gerade trifft — es soll die Liste nicht
217
+ anfuehren, nur weil es alphabetisch vorn steht. Unter den
218
+ passenden gewinnt, wer mehr Plaetze belegt; darunter bleibt die
219
+ Reihenfolge, in der die Liste zusammengestellt wurde. */
220
+ treffer.forEach(function (t0, n) { t0.rang = n; });
221
+ treffer.sort(function (a, b) {
222
+ var pa = bewerte(a.m), pb = bewerte(b.m);
223
+ if (pa.passt !== pb.passt) { return pa.passt ? -1 : 1; }
224
+ if (pa.passt && pb.belegt !== pa.belegt) { return pb.belegt - pa.belegt; }
225
+ return a.rang - b.rang;
226
+ });
227
+ mEintraege = treffer;
228
+
229
+ mlist.textContent = '';
230
+ if (!treffer.length) {
231
+ var leer = el('div', 'vz zurueck');
232
+ leer.appendChild(el('span', null, tr('pattern.noMatch', sel.value)));
233
+ mlist.appendChild(leer);
234
+ }
235
+ treffer.forEach(function (t0, n) {
236
+ var z = el('div', 'vz' + (n === mMarkiert ? ' an' : '')
237
+ + (t0.m === e.want ? ' gewaehlt' : ''));
238
+ z.appendChild(el('span', null, musterZeile(t0.m)));
239
+ z.appendChild(el('span', 'tiefer',
240
+ (t0.via ? tr('pattern.viaRole', t0.via) + ' ' : '') + markeFuer(t0.m)));
241
+ /* mousedown statt click: sonst greift der Fokusverlust zuerst und
242
+ die Liste ist weg, bevor der Klick ankommt. */
243
+ z.addEventListener('mousedown', function (ev) {
244
+ ev.preventDefault();
245
+ mWaehlen(t0.m);
246
+ });
247
+ mlist.appendChild(z);
248
+ });
249
+
250
+ /* Der Umschalter, der frueher als Knopf daneben stand. Beim Suchen
251
+ ist er ueberfluessig - dann laeuft die Suche ohnehin ueber alles. */
252
+ if (!such && alleTypen.length > VORSCHAU.length) {
253
+ var um = el('div', 'vz mehr');
254
+ um.appendChild(el('span', null, (e.alleMuster ? '▴ ' : '▾ ')
255
+ + (e.alleMuster ? tr('pattern.usualOnly')
256
+ : tr('pattern.allPatterns', alleTypen.length))));
257
+ um.addEventListener('mousedown', function (ev) {
258
+ ev.preventDefault();
259
+ e.alleMuster = !e.alleMuster;
260
+ mMarkiert = -1;
261
+ mMalen();
262
+ sel.focus();
263
+ });
264
+ mlist.appendChild(um);
265
+ }
266
+ mlist.hidden = false;
267
+ };
268
+
269
+ /* Solange nichts getippt ist, steht im Feld das gewaehlte Muster mit
270
+ seiner Belegung - dieselbe Auskunft wie frueher im zugeklappten
271
+ Auswahlfeld. */
272
+ var mBeschriften = function () {
273
+ /* Im Feld der Beiname, der rohe Name im Tooltip - fuer beides
274
+ nebeneinander ist das Feld zu schmal. */
275
+ sel.value = e.want ? ((musterName(e.want) || e.want) + ' ' + markeFuer(e.want)) : '';
276
+ sel.title = e.want || '';
277
+ };
278
+ mBeschriften();
279
+
280
+ sel.addEventListener('focus', function () {
281
+ sel.value = '';
282
+ mMarkiert = -1;
283
+ mMalen();
284
+ });
285
+ sel.addEventListener('input', function () { mMarkiert = -1; mMalen(); });
286
+ sel.addEventListener('blur', function () {
287
+ setTimeout(function () { mSchliessen(); mBeschriften(); }, 120);
288
+ });
289
+ sel.addEventListener('keydown', function (ev) {
290
+ if (ev.key === 'Escape') { mSchliessen(); mBeschriften(); sel.blur(); return; }
291
+ if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {
292
+ if (mlist.hidden) { mMalen(); return; }
293
+ ev.preventDefault();
294
+ if (!mEintraege.length) { return; }
295
+ mMarkiert += (ev.key === 'ArrowDown' ? 1 : -1);
296
+ if (mMarkiert < 0) { mMarkiert = mEintraege.length - 1; }
297
+ if (mMarkiert >= mEintraege.length) { mMarkiert = 0; }
298
+ mMalen();
299
+ var akt = mlist.children[mMarkiert];
300
+ if (akt && akt.scrollIntoView) { akt.scrollIntoView({ block: 'nearest' }); }
301
+ return;
302
+ }
303
+ if (ev.key === 'Enter') {
304
+ ev.preventDefault();
305
+ if (!mlist.hidden && mEintraege.length) {
306
+ mWaehlen(mEintraege[mMarkiert >= 0 ? mMarkiert : 0].m);
307
+ }
308
+ }
309
+ });
310
+ if (S.fokusMusterfeld) {
311
+ S.fokusMusterfeld = false;
312
+ setTimeout(function () { sel.focus(); }, 0);
313
+ }
314
+
315
+ /* Kein Fliesstext unter dem Auswahlfeld: die Belegung steht schon in
316
+ jeder Zeile des Auswahlfelds, und eine zweite Zeile verschoebe die
317
+ Felder daneben gegeneinander. Nur Meldungen bleiben. */
318
+ var meta = el('div', 'vmeta');
319
+ if (!haupt) {
320
+ meta.appendChild(el('span', 'chip bad', tr('pattern.notDetected')));
321
+ } else if (pflichtFehlt.length) {
322
+ meta.appendChild(el('span', 'chip bad',
323
+ tr('pattern.missing', pflichtFehlt.map(function (x) { return x.name; }).join(', '))));
324
+ }
325
+ if (e.want && e.wantAuto && e.want !== e.wantAuto) {
326
+ meta.appendChild(el('span', 'chip warn', tr('pattern.manualInstead', e.wantAuto)));
327
+ }
328
+ if (meta.childNodes.length) { vw.appendChild(meta); }
329
+ /* Der Knopf „alle 51 Muster" stand frueher neben dem Auswahlfeld und
330
+ schaltete die Liste um. Er sitzt jetzt als letzte Zeile in der Liste
331
+ selbst - dort, wo man ihn braucht, statt daneben. */
332
+
333
+ return { block: vw, rateKnopf: rateKnopf };
334
+ }
@@ -0,0 +1,243 @@
1
+ /* Das Objektlager: Laden, Abo, Index, gezieltes Nachladen.
2
+
3
+ Was hier lebt, ist die eine Wahrheit ueber die Objektdatenbank, wie der
4
+ Reiter sie kennt: S.objects samt sortiertem Index. Alles Zeichnen und
5
+ Entwerfen liest daraus.
6
+
7
+ Die Rueckrufe nach oben (zeichnen, waehlen) kommen als Kreis-Import aus
8
+ werkbank.js - beim Laden der Module ist das unkritisch, weil sie erst
9
+ zur Laufzeit gerufen werden. */
10
+
11
+ import { S } from './zustand.js';
12
+ import { socket } from './verbindung.js';
13
+ import './basis.js';
14
+ import './sprache.js';
15
+ import { ladeEinstellungen } from './einstellungen.js';
16
+ import { zeigeObjektzahl } from './vorlagen.js';
17
+ import { enums, ladeEnums } from './enums.js';
18
+ import { ladeEnumVorlagen } from './katalog.js';
19
+ import { zeichneBaum } from './baum.js';
20
+ import { waehle , tipptGerade } from './entwurf.js';
21
+ import { zeichneErgebnis, neuZeichnenOderAufbauen } from './ergebnis.js';
22
+
23
+
24
+ /* ================== Objekte holen ================== */
25
+ export var ARTEN = ['device', 'channel', 'folder', 'state'];
26
+
27
+ /* Nach dem Neuladen soll der Bericht stimmen. Die Erkennung haengt an
28
+ den vorhandenen Objekten — legt man einen cmnd-Punkt an, kann eine
29
+ Vorlage greifen, die vorher nicht griff. Vorher zeichnete das Laden
30
+ nur den Baum neu und liess rechts den alten Entwurf stehen. */
31
+ export function ladeObjekte(danach) {
32
+ var offen = ARTEN.length;
33
+ S.objects = {};
34
+ ARTEN.forEach(function (art) {
35
+ socket.emit('getObjectView', 'system', art, { startkey: '', endkey: '香' }, function (err, doc) {
36
+ if (!err && doc && doc.rows) {
37
+ doc.rows.forEach(function (r) {
38
+ if (r.value && r.id) { S.objects[r.id] = r.value; }
39
+ });
40
+ }
41
+ if (--offen === 0) {
42
+ S.keysSorted = Object.keys(S.objects).sort();
43
+ S.kleinIndex = {};
44
+ ladeEnums();
45
+ /* Erst die Einstellungen, dann die Vorlagen - sonst holt die
46
+ Werkbank sie auch dann, wenn sie abgeschaltet sind. */
47
+ ladeEinstellungen(function () {
48
+ /* Kam der Katalog spaeter als das erste Zeichnen, fehlte dem
49
+ Feld der Vorschlag - nachzeichnen, sobald er da ist. */
50
+ ladeEnumVorlagen(function () {
51
+ if (S.current) { zeichneErgebnis(); }
52
+ });
53
+ });
54
+ /* Kopfzeile nachziehen - je nach Ladereihenfolge stand dort
55
+ noch die Null von vor dem Einlesen. */
56
+ zeigeObjektzahl();
57
+ zeichneBaum();
58
+ if (S.current && S.baumModus !== 'vorlagen') { waehle(S.current); }
59
+ horcheAufObjekte();
60
+ if (typeof danach === 'function') { danach(); }
61
+ }
62
+ });
63
+ });
64
+ }
65
+
66
+ /* Objekte entstehen auch ohne Zutun der Werkbank. Der uebliche Weg ist
67
+ genau der: cmnd.POWER anlegen, einmal schalten — und mqtt-client legt
68
+ stat.POWER an. Ohne Abo zeigte die Werkbank danach weiter „stat.POWER
69
+ fehlt", bis man den Reiter neu lud. Man erreichte den Zustand, in dem
70
+ die Vorlage greift, mit dem Werkzeug allein nie.
71
+
72
+ Nachgezogen wird immer, neu gezeichnet nur, wenn es den gerade
73
+ gewaehlten Zweig betrifft und kein Dialog offensteht — sonst
74
+ verschwindet einem die Zeile unter dem Finger. */
75
+ var horcht = false, nachziehTimer = null, nachziehBaum = false, enumTimer = null;
76
+
77
+ /* Wer selbst frisch nachgeladen hat, macht den Nachzieher des Abos
78
+ ueberfluessig. */
79
+ export function nachziehenErledigt() {
80
+ if (nachziehTimer) { clearTimeout(nachziehTimer); nachziehTimer = null; }
81
+ nachziehBaum = false;
82
+ }
83
+
84
+ export function horcheAufObjekte() {
85
+ if (horcht) { return; }
86
+ horcht = true;
87
+ socket.emit('subscribeObjects', '*');
88
+ socket.on('objectChange', function (id, obj) {
89
+ if (!id) { return; }
90
+ /* Nur was zaehlt, nicht jeder Schreibvorgang: ts und from aendern
91
+ sich bei jedem setObject, auch wenn inhaltlich alles gleich
92
+ bleibt. Ohne diesen Vergleich zeichnete die Werkbank ein zweites
93
+ Mal neu, nachdem sie selbst geschrieben und schon nachgezogen
94
+ hatte — der Baum sprang sichtbar zweimal. */
95
+ var kern = function (o) {
96
+ return o ? JSON.stringify([o.type, o.common, o.native]) : null;
97
+ };
98
+ var vorher = kern(S.objects[id]);
99
+ var nachher = kern(obj);
100
+ if (vorher === nachher) { return; }
101
+
102
+ /* Aufzaehlungen gehen einen eigenen Weg.
103
+
104
+ Sie gehoeren nicht in `objects` - dort stuenden sie im Geraetebaum,
105
+ wo sie nichts zu suchen haben. Nachziehen muss man sie aber, und
106
+ das war der Fehler: Die Werkbank las sie einmal beim Start und nie
107
+ wieder. Nach dem ersten Schreiben arbeitete sie mit einem
108
+ veralteten Stand weiter, kannte die eben gesetzte Zuordnung nicht
109
+ und trug sie beim naechsten Mal nicht aus. Die neue Funktion kam
110
+ hinzu, statt die alte abzuloesen - ein Geraet stand am Ende in
111
+ zweien. Gemessen an `alias.0.FK_Badezimmer`, das in „Licht“ und
112
+ „Verschluss“ gleichzeitig hing. */
113
+ if (id.indexOf('enum.') === 0) {
114
+ if (kern(enums[id]) === nachher) { return; }
115
+ if (obj) { enums[id] = obj; } else { delete enums[id]; }
116
+ if (enumTimer) { clearTimeout(enumTimer); }
117
+ var enumNachziehen = function () {
118
+ enumTimer = null;
119
+ /* Wer gerade im Trockenlauf steht oder tippt, wird nicht
120
+ unterbrochen - gleich nochmal. */
121
+ if (document.querySelector('dialog[open]') || tipptGerade()) {
122
+ enumTimer = setTimeout(enumNachziehen, 400);
123
+ return;
124
+ }
125
+ if (S.current) { zeichneErgebnis(); }
126
+ };
127
+ enumTimer = setTimeout(enumNachziehen, 300);
128
+ return;
129
+ }
130
+
131
+ var gabEs = S.objects[id] !== undefined;
132
+ if (obj) { S.objects[id] = obj; } else { delete S.objects[id]; }
133
+ if (gabEs !== (S.objects[id] !== undefined)) { nachziehBaum = true; }
134
+
135
+ var betrifft = S.current && (id === S.current || id.indexOf(S.current + '.') === 0);
136
+ if (!betrifft && !nachziehBaum) { return; }
137
+ if (nachziehTimer) { clearTimeout(nachziehTimer); }
138
+ var nachziehen = function () {
139
+ nachziehTimer = null;
140
+ /* Offener Dialog: spaeter nochmal, nicht ersatzlos fallenlassen.
141
+
142
+ Vorher stand hier ein blankes `return`. Wer einen Alias ueber den
143
+ Loeschdialog entfernte, bekam waehrend der offenen Meldung die
144
+ `objectChange`-Meldungen: `objects` verlor die Eintraege,
145
+ `keysSorted` behielt sie, und der Nachzieher, der beides wieder
146
+ in Deckung gebracht haette, wurde verworfen. Der naechste
147
+ Baumaufbau griff dann auf ein Objekt zu, das es nicht mehr gab -
148
+ `Cannot read properties of undefined (reading 'type')`. */
149
+ if (document.querySelector('dialog[open]')) {
150
+ nachziehTimer = setTimeout(nachziehen, 400);
151
+ return;
152
+ }
153
+ /* Wer gerade tippt, behaelt seine Eingabe — spaeter nochmal. */
154
+ if (tipptGerade()) { nachziehTimer = setTimeout(nachziehen, 400); return; }
155
+ if (nachziehBaum || indexStimmtNicht()) {
156
+ indexNeu();
157
+ nachziehBaum = false;
158
+ zeichneBaum();
159
+ }
160
+ neuZeichnenOderAufbauen();
161
+ };
162
+ nachziehTimer = setTimeout(nachziehen, 600);
163
+ });
164
+ }
165
+
166
+ /* Nach einer eigenen Aktion nur nachziehen, was sich geaendert hat.
167
+ Vorher rief jede angelegte cmnd-Zeile ladeObjekte() und las die ganze
168
+ Datenbank neu — auf einer gewachsenen Anlage sind das 24 500 Objekte
169
+ fuer einen einzigen Datenpunkt, und der Baum sprang zweimal. */
170
+ /* Sobald jemand am Entwurf etwas eingestellt hat, darf ihn kein
171
+ Nachladen mehr wegwerfen. Vorher genuegte ein angelegter Datenpunkt,
172
+ und Zielordner, Haken und von Hand angelegte Punkte waren weg. */
173
+
174
+ export function uebernimmObjekt(id, o) {
175
+ var gabEs = S.objects[id] !== undefined;
176
+ if (o) { S.objects[id] = o; } else { delete S.objects[id]; }
177
+ return gabEs !== (S.objects[id] !== undefined);
178
+ }
179
+
180
+ /* Der Baum liest aus S.keysSorted, nicht aus S.objects. Wer einen Punkt
181
+ hinzufuegt oder entfernt, muss den Index neu bauen — sonst kennt der
182
+ Baum ihn nicht, egal wie oft man zeichnet. */
183
+ export function indexNeu() {
184
+ S.keysSorted = Object.keys(S.objects).sort();
185
+ S.kleinIndex = {};
186
+ zeigeObjektzahl();
187
+ }
188
+
189
+ /* Stimmt die Zahl der Schluessel nicht mit dem Index ueberein, hat
190
+ jemand anders an S.objects gearbeitet — das Abo etwa, das eintraegt und
191
+ das Aufraeumen seinem Zeitgeber ueberlaesst. Ohne diese Probe fiel
192
+ genau der uebliche Fall durch: Abo traegt ein, das gezielte Nachladen
193
+ sieht keine Aenderung mehr, bricht den Zeitgeber ab, und der Baum
194
+ bleibt auf altem Stand. */
195
+ export function indexStimmtNicht() {
196
+ return S.keysSorted.length !== Object.keys(S.objects).length;
197
+ }
198
+
199
+ export function holeObjekt(id, danach) {
200
+ socket.emit('getObject', id, function (err, o) {
201
+ var anders = uebernimmObjekt(id, err ? null : o);
202
+ if (anders || nachziehBaum || indexStimmtNicht()) {
203
+ indexNeu();
204
+ zeichneBaum();
205
+ }
206
+ /* Das Abo hat auf dieselbe Aenderung schon einen Nachzieher
207
+ angesetzt — der ist jetzt erledigt. Sonst zeichnet die Werkbank
208
+ ein zweites Mal, eine halbe Sekunde spaeter, und alles springt. */
209
+ nachziehenErledigt();
210
+ neuZeichnenOderAufbauen(danach);
211
+ });
212
+ }
213
+
214
+ /* Nach einer Abfrage am Geraet weiss man nicht, welche Punkte dabei
215
+ entstanden sind — dann eben der ganze Zweig, aber nicht mehr. */
216
+ export function holeZweig(praefix, danach) {
217
+ /* `baumNeu` ist ein lokales Flag - nicht der Baum in S. Beim Umzug in
218
+ das Zustandsmodul geriet es einmal faelschlich unter die Raeder,
219
+ weil es in einer Komma-Deklaration versteckt war. */
220
+ var offen = ARTEN.length, baumNeu = false;
221
+ ARTEN.forEach(function (art) {
222
+ socket.emit('getObjectView', 'system', art,
223
+ { startkey: praefix, endkey: praefix + '香' }, function (err, doc) {
224
+ if (!err && doc && doc.rows) {
225
+ doc.rows.forEach(function (r) {
226
+ if (r.value && r.id && uebernimmObjekt(r.id, r.value)) { baumNeu = true; }
227
+ });
228
+ }
229
+ if (--offen === 0) {
230
+ if (baumNeu || nachziehBaum || indexStimmtNicht()) {
231
+ indexNeu();
232
+ zeichneBaum();
233
+ }
234
+ nachziehenErledigt();
235
+ neuZeichnenOderAufbauen(danach);
236
+ }
237
+ });
238
+ });
239
+ }
240
+
241
+ /* ================== Baum ================== */
242
+ /* Aus den flachen IDs einen Baum bauen. Nur Zweige, die irgendwo
243
+ Zustaende enthalten — der Rest ist fuer uns Laerm. */