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,47 @@
1
+ /* Der geteilte Zustand der Werkbank.
2
+
3
+ Ein Objekt statt loser Variablen, weil ES-Module importierte Bindungen
4
+ schreibschuetzen: `S.current = ...` geht ueberall, `current = ...`
5
+ ginge nur im eigenen Modul. Hier liegt nur, was wirklich mehreren
6
+ Modulen gehoert - was ein einzelnes Modul fuer sich behaelt, bleibt
7
+ dort als gewoehnliche Variable.
8
+
9
+ VORLAGEN sind die eigenen und mitgelieferten Geraetevorlagen; der
10
+ Katalog des Admin (Raum-/Funktionsnamen samt Bildern) liegt getrennt
11
+ in katalog.js. */
12
+
13
+ export var S = {
14
+ objects: {},
15
+ keysSorted: [],
16
+ kleinIndex: {},
17
+ werte: {},
18
+ current: null,
19
+ openRow: null,
20
+ entwurf: null,
21
+ baum: null,
22
+ baumModus: 'quellen',
23
+ baumModusVorher: 'quellen',
24
+ aufgeklappt: {},
25
+ alleKlapp: null,
26
+ abo: null,
27
+ trockenAlle: null,
28
+ /* Vergleich im Trockenlauf: Felder geordnet (Vorgabe) oder in der
29
+ Reihenfolge, in der sie wirklich im Speicher stehen. */
30
+ trockenRoh: false,
31
+ /* Quellentausch: welcher Alias, welche neue Quelle, die Zuordnung
32
+ und die Punkte, die dabei entfallen sollen. */
33
+ tauschZiel: null,
34
+ tauschNeu: '',
35
+ tauschPlan: null,
36
+ tauschWeg: {},
37
+ tauschFilter: '',
38
+ loeschListe: [],
39
+ loeschZiel: null,
40
+ loeschAlleAusgaenge: false,
41
+ verlegeZiel: null,
42
+ VORLAGEN: [],
43
+ vorlagenUnvollstaendig: false,
44
+ fokusOrdnerfeld: false,
45
+ fokusMusterfeld: false,
46
+ tplZ: null
47
+ };
@@ -0,0 +1,515 @@
1
+ /* Die Zustandsliste: erst die Rechnung, welcher Punkt auf welchem
2
+ Platz des Musters sitzt (samt Vorbelegung der Haken), dann die Karte
3
+ mit Filterleiste, Ratleiste, den Zeilen selbst, den freien Plaetzen
4
+ des Musters und dem Zufuegen von Hand.
5
+
6
+ Herausgeloest aus zeichneErgebnis. `rateKnopf` und `musterBlock`
7
+ werden oben bei der Musterwahl gebaut und hier nur eingehaengt. */
8
+
9
+ import { S } from './zustand.js';
10
+ import { el } from './basis.js';
11
+ import { tr } from './sprache.js';
12
+ import { erkenneEntwurf, musterVon, rolleVonPlatz } from './erkennung.js';
13
+ import { kindZustaende, wertVon, fmt } from './werte.js';
14
+ import { steuerKanaele, vorlagenAbweichung, waehle } from './entwurf.js';
15
+ import { musterName } from './musternamen.js';
16
+ import { detailZeile } from './detail.js';
17
+ import { rateAnzahl, rateZurueck, rateZurueckEine, rateBehalten } from './vorschlagen.js';
18
+ import { mqttEinzeln } from './mqtt.js';
19
+ import { zeichneErgebnis, entwurfAngefasst } from './ergebnis.js';
20
+
21
+ export function berechnePlaetze(e, haupt) {
22
+ /* --- welcher Punkt sitzt auf welchem Platz --- */
23
+ var platzVon = {};
24
+ if (haupt) {
25
+ haupt.states.forEach(function (x) {
26
+ if (x.id) { platzVon[x.id.slice(S.current.length + 1)] = x.name; }
27
+ });
28
+ }
29
+ /* Manche Muster haben einen Platz, der beliebig viele Punkte
30
+ aufnimmt — im info-Muster heisst jeder Punkt ACTUAL. Diesen Namen
31
+ an jede Zeile zu schreiben sagt nichts, es sieht nur aus wie ein
32
+ Uebersetzungsloch. Also nur zeigen, wenn er wirklich einen
33
+ bestimmten Punkt bezeichnet. */
34
+ var platzAnzahl = {};
35
+ Object.keys(platzVon).forEach(function (n) {
36
+ platzAnzahl[platzVon[n]] = (platzAnzahl[platzVon[n]] || 0) + 1;
37
+ });
38
+ var infoFund = erkenneEntwurf(e, 'info');
39
+ var imInfo = {};
40
+ if (infoFund.length && haupt && haupt.type !== 'info') {
41
+ infoFund[0].states.forEach(function (x) {
42
+ if (x.id) {
43
+ var kurz = x.id.slice(S.current.length + 1);
44
+ if (!platzVon[kurz]) { imInfo[kurz] = true; }
45
+ }
46
+ });
47
+ }
48
+
49
+ /* Mehrere Kanaele, die fuer sich ein Geraet ergeben? Einmal rechnen,
50
+ am Entwurf merken — der Hinweis in der Karte und die Knoepfe unten
51
+ lesen dasselbe Ergebnis. */
52
+ if (e.kanalGeraete === undefined) {
53
+ e.kanalGeraete = (!(e.instanzen && e.instanzen.length > 1) && S.current.indexOf('alias.') !== 0)
54
+ ? steuerKanaele(S.current) : [];
55
+ }
56
+
57
+ /* Welche Punkte aus den gemeinsamen Kanaelen haben hier, auf der
58
+ Geraeteebene, einen Platz gefunden? Diese Liste bekommen die
59
+ Kanal-Entwuerfe mit — dort erkennt der Detektor sie nicht. */
60
+ if (e.kanalGeraete && e.kanalGeraete.length > 1) {
61
+ var eigenerKanal = {};
62
+ e.kanalGeraete.forEach(function (k) { eigenerKanal[k.id] = 1; });
63
+ e.gemeinsamMitPlatz = {};
64
+ e.states.forEach(function (st) {
65
+ if (!st.srcR || !platzVon[st.n]) { return; }
66
+ var elt = st.srcR.slice(0, st.srcR.lastIndexOf('.'));
67
+ if (elt.indexOf(S.current + '.') === 0 && !eigenerKanal[elt]) {
68
+ e.gemeinsamMitPlatz[st.srcR] = platzVon[st.n];
69
+ }
70
+ });
71
+ }
72
+
73
+ /* Vorbelegung eines Rohentwurfs: angehakt ist, was im erkannten Muster
74
+ einen Platz findet.
75
+
76
+ Vorher war alles an. Bei einer Tasmota mit acht Punkten ging das
77
+ durch; ein Homematic-Fensterkontakt bringt 26 mit, ein Thermostat
78
+ 42, und die meisten davon sind Alarmspiegel und Wartungsinnereien,
79
+ die in keinem Muster vorkommen. Wer die alle in einen Alias schreibt,
80
+ hat das Geraet nicht abgebildet, sondern abgeschrieben.
81
+
82
+ Drei Faelle bleiben ausgenommen:
83
+ - **Ein bestehender Alias.** Dort ist jeder Haken eine Entscheidung,
84
+ die schon getroffen wurde; abwaehlen hiesse loeschen.
85
+ - **Eine Vorlage.** Die bestimmt selbst, was an ist (`vorgabeAus`).
86
+ - **Kein einziger Punkt mit Platz.** Dann bliebe die Liste leer, und
87
+ eine leere Liste hilft niemandem.
88
+
89
+ Einmalig, gemerkt in `vorbelegt` — sonst wuerde jedes Neuzeichnen
90
+ zurueckstellen, was der Nutzer gerade dazugewaehlt hat. Die drei
91
+ Knoepfe darueber bleiben, „alle" ist einen Klick entfernt. */
92
+ /* Steht der Alias schon da, ist er die Antwort - nicht der Vorschlag.
93
+
94
+ Die Vorbelegung hakt an, was im erkannten Muster einen Platz
95
+ findet. Das ist richtig fuer einen Alias, den es noch nicht gibt.
96
+ Gibt es ihn, hat `uebernehmeBestand` gerade angehakt, was drinsteht
97
+ - und die Vorbelegung machte es sofort wieder zunichte: die neun
98
+ uebernommenen Zeilen hatten im thermostat-Muster keinen Platz und
99
+ wurden abgewaehlt, uebrig blieben zwei. */
100
+ var bestandDa = e.ziel && S.objects[e.ziel] && kindZustaende(e.ziel).length > 0;
101
+ if (!e.vorbelegt && !e.vorlage && S.current.indexOf('alias.') !== 0) {
102
+ e.vorbelegt = true;
103
+ if (bestandDa) {
104
+ /* Angehakt ist, was im Alias steht - nicht mehr und nicht weniger.
105
+
106
+ Die Vorbelegung ganz zu ueberspringen war zu grob: der
107
+ Rohentwurf startet mit allem an, und dann standen bei der
108
+ Klimaanlage 49 Zeilen angehakt statt neun. Schlimmer noch, der
109
+ Detektor gab den ACTUAL-Platz danach an `status_outdoorTemperature`
110
+ - die Aussentemperatur, weil sie dieselbe Rolle traegt und
111
+ frueher in der Liste steht. Wer nur das Erwartete anhakt,
112
+ bekommt auch nur das Erwartete erkannt. */
113
+ e.states.forEach(function (s) { s.on = !!s.ausBestand; });
114
+ } else if (e.states.some(function (s) { return !!platzVon[s.n]; })) {
115
+ e.states.forEach(function (s) { s.on = !!platzVon[s.n]; });
116
+ }
117
+ /* „Als ein Geraet" muss halten, was es sagt: alles, was dieses Geraet
118
+ schaltet, gehoert hinein. Das Muster hat aber nur einen Platz dafuer
119
+ — bei einem Zweifach-Aktor bekaeme ihn Kanal 2, und Kanal 1 fiele
120
+ stillschweigend weg. Ein Alias, dem heimlich ein Licht fehlt, ist
121
+ schlimmer als einer mit einem Punkt zu viel. Also kommen die
122
+ Pflichtpunkte der anderen Kanaele mit dazu; dass sie keinen Platz
123
+ finden, steht als Hinweis darueber. */
124
+ (e.kanalGeraete || []).forEach(function (k) {
125
+ (k.pflicht || []).forEach(function (pid) {
126
+ var kurz = pid.slice(S.current.length + 1).replace(/\./g, '_');
127
+ e.states.forEach(function (st) { if (st.n === kurz) { st.on = true; } });
128
+ });
129
+ });
130
+ }
131
+
132
+ return { platzVon: platzVon, platzAnzahl: platzAnzahl, imInfo: imInfo };
133
+ }
134
+
135
+ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
136
+ var platzVon = pl.platzVon, platzAnzahl = pl.platzAnzahl;
137
+
138
+ /* --- Gerätekarte ---
139
+
140
+ Hier stand ein Streifen mit Schaltbild, Geraetenamen und bis zu drei
141
+ Messwerten. Er sagte nichts, was nicht schon dastand: der Name steht
142
+ als Ueberschrift darueber, die Messwerte stehen in der Liste
143
+ darunter mit Rolle und Einheit. Zwei Zeilen Hoehe fuer eine
144
+ Wiederholung - weg damit, in beiden Ansichten. */
145
+ var card = el('div', 'card');
146
+
147
+ /* Ein Satz dazu, warum unten zwei Knoepfe stehen. Ohne ihn klickt man
148
+ auf eine Zahl und weiss nicht, was sie bedeutet. */
149
+ if (e.kanalGeraete && e.kanalGeraete.length > 1) {
150
+ var hk = el('div', 'aside');
151
+ hk.style.margin = '0 0 9px';
152
+ hk.textContent = tr('result.channelsHint', e.kanalGeraete.length);
153
+ card.appendChild(hk);
154
+
155
+ /* Wie viele angehakte Punkte sind Schalter, fuer die das Muster
156
+ keinen Platz hat? Genau die, die eben dazugekommen sind. */
157
+ var ohnePlatz = 0;
158
+ e.kanalGeraete.forEach(function (k) {
159
+ (k.pflicht || []).forEach(function (pid) {
160
+ var kurz = pid.slice(S.current.length + 1).replace(/\./g, '_');
161
+ e.states.forEach(function (st) {
162
+ if (st.n === kurz && st.on && !platzVon[st.n]) { ohnePlatz++; }
163
+ });
164
+ });
165
+ });
166
+ if (ohnePlatz) {
167
+ var hw = el('div', 'aside w');
168
+ hw.style.margin = '0 0 9px';
169
+ hw.textContent = tr('result.extraSwitches', ohnePlatz, e.want || '?');
170
+ card.appendChild(hw);
171
+ }
172
+ }
173
+
174
+ /* --- Werkzeugleiste ---
175
+
176
+ Hier sitzt auch die Musterwahl. Sie stand vorher oben rechts im
177
+ Kopf, weit weg von der Liste, deren Inhalt sie bestimmt: welches
178
+ Muster gilt, entscheidet, welcher Punkt einen Platz hat und welcher
179
+ ins info-Geraet wandert. Jetzt steht sie direkt ueber dieser Liste,
180
+ in derselben Zeile wie die Filter. */
181
+ /* Angehaktes nach oben - einmal, beim ersten Aufbau.
182
+
183
+ Wer links einen Knoten anklickt, will sehen, was daraus entsteht.
184
+ Bei einem Fensterkontakt sind das vier Punkte von 26, bei der
185
+ Klimaanlage neun von 55 - und die standen bisher ueber die ganze
186
+ Liste verstreut, weil sortiert wird, wie das Geraet seine Punkte
187
+ fuehrt.
188
+
189
+ Einmal und nicht bei jedem Haken: eine Liste, die unter der Hand
190
+ umspringt, sobald man etwas abwaehlt, ist schlimmer als eine
191
+ unsortierte. Die Marke sitzt am Entwurf, und der Entwurf wird bei
192
+ jeder neuen Auswahl neu gebaut - beim naechsten Anklicken steht
193
+ also wieder das Angehakte oben.
194
+
195
+ Stabil innerhalb der beiden Gruppen: die Reihenfolge der
196
+ angehakten Punkte bleibt, wie sie war. Das ist nicht nur
197
+ Ordnungsliebe - `abbild` nimmt nur angehakte Zeilen, und welcher
198
+ von zwei gleich passenden Punkten einen Platz bekommt, entscheidet
199
+ der Detektor nach der Reihenfolge. Wuerde die sich verschieben,
200
+ verschoebe sich auch die Erkennung. */
201
+ if (!e.sortiert) {
202
+ e.sortiert = true;
203
+ var an0 = [], aus0 = [];
204
+ e.states.forEach(function (s) { (s.on ? an0 : aus0).push(s); });
205
+ e.states = an0.concat(aus0);
206
+ }
207
+
208
+ var bar = el('div', 'listbar');
209
+ var an = e.states.filter(function (s) { return s.on; }).length;
210
+ bar.appendChild(document.createTextNode(tr('list.willBeCreated')));
211
+ bar.appendChild(el('span', 'cnt', tr('list.countOf', an, e.states.length)));
212
+ bar.appendChild(document.createTextNode(tr('list.ofDatapoints')));
213
+ bar.appendChild(el('span', 'sp2'));
214
+ /* Mittig zwischen der Zahl und den drei Filtern - zwischen zwei
215
+ Dehnfugen steht er von selbst in der Mitte. */
216
+ if (rateKnopf) { bar.appendChild(rateKnopf); rateKnopf = null; }
217
+ bar.appendChild(el('span', 'sp2'));
218
+ /* Die vier Auswahlknoepfe nur an einer Quelle. Am fertigen Alias sind
219
+ sie keine Auswahl, sondern ein Loeschwerkzeug: ein Haken weg heisst
220
+ dort, dass der Punkt beim Aktualisieren verschwindet - „nur was ins
221
+ Muster passt" wuerde einen Punkt ohne Platz sofort abwaehlen
222
+ (Ricardo, 25.08.2026). Und „Standard"/„alle" haben am Alias nichts,
223
+ worauf sie sich beziehen koennten. */
224
+ var amAlias = S.current && S.current.indexOf('alias.') === 0;
225
+ /* Die Auswahlknoepfe stehen in einer eigenen zweiten Zeile. In derselben
226
+ Zeile wie Zaehler und Musterwahl brachen sie je nach Fensterbreite
227
+ unterschiedlich um, und die Leiste sprang bei jedem Neuzeichnen anders
228
+ (Ricardo, 26.08.2026). „Freie Plaetze vorschlagen" bleibt oben - er
229
+ gehoert zur Erkennung, nicht zur Auswahl. */
230
+ var chipzeile = el('div', 'chipzeile');
231
+ (amAlias ? [] :
232
+ [[tr('list.tpl'), function () {
233
+ /* Die Haken, die die Vorlage vorgibt. Gibt es keinen Vorlagenwert
234
+ (kein bestehender Alias), ist der Oeffnungszustand die Vorgabe -
235
+ der stammt dann ja aus der Vorlage. */
236
+ e.states.forEach(function (s) {
237
+ s.on = s.vorlagenWert ? !!s.vorlagenWert.on : !!s.onVorgabe;
238
+ });
239
+ }, tr('list.tplHint')],
240
+ [tr('list.all'), function () { e.states.forEach(function (s) { s.on = true; }); }],
241
+ [tr('list.onlyWithValue'), function () { e.states.forEach(function (s) { s.on = wertVon(s).ok; }); }],
242
+ [tr('list.onlyPattern'), function () {
243
+ /* Frueher hiess der Chip „nur was ins Geraet gehoert“ und behielt
244
+ bloss die aktuellen Platzhalter — als einziger der Reihe hatte
245
+ er damit ein Gedaechtnis: nach „alle“ lieferte er mehr als
246
+ direkt nach dem Oeffnen (Ricardo, 25.08.2026). Jetzt treten
247
+ alle Zeilen zur Platzvergabe an, unabhaengig vom Haken, und
248
+ angehakt wird genau, wer einen Platz bekommt. */
249
+ var kopie = JSON.parse(JSON.stringify({ kanal: e.kanal, states: e.states }));
250
+ kopie.states.forEach(function (s) { s.on = true; });
251
+ var f = erkenneEntwurf(kopie, e.want);
252
+ var drin = {};
253
+ if (f.length) {
254
+ f[0].states.forEach(function (x) {
255
+ if (x.id) { drin[x.id.slice(e.kanal.length + 1)] = true; }
256
+ });
257
+ }
258
+ e.states.forEach(function (s) { s.on = !!drin[s.n]; });
259
+ }, tr('list.onlyPatternHint')]]).forEach(function (pr) {
260
+ var b = el('button', null, pr[0]);
261
+ if (pr[2]) { b.title = pr[2]; }
262
+ b.addEventListener('click', function () {
263
+ pr[1](); entwurfAngefasst(); S.openRow = null; zeichneErgebnis();
264
+ });
265
+ chipzeile.appendChild(b);
266
+ });
267
+
268
+ /* Alles verwerfen und den Stand aus dem System neu holen. Steht nur da,
269
+ wenn es etwas zu verwerfen gibt - ein Knopf, der nichts tut, gehoert
270
+ nicht in die Leiste (Ricardo, 26.08.2026). Abgesetzt, weil er als
271
+ einziger nicht nur Haken setzt. */
272
+ if (e.angefasst) {
273
+ var bZurueck = el('button', 'weit', tr('list.reset'));
274
+ bZurueck.title = tr('list.resetHint');
275
+ bZurueck.addEventListener('click', function () {
276
+ var id = S.current;
277
+ S.entwurf = null;
278
+ S.openRow = null;
279
+ waehle(id);
280
+ });
281
+ chipzeile.appendChild(bZurueck);
282
+ }
283
+ if (chipzeile.childNodes.length) { bar.appendChild(chipzeile); }
284
+ if (musterBlock) {
285
+ var mspacer = el('span', 'sp2');
286
+ bar.appendChild(mspacer);
287
+ musterBlock.classList.add('inbar');
288
+ bar.appendChild(musterBlock);
289
+ musterBlock = null;
290
+ }
291
+ card.appendChild(bar);
292
+
293
+ /* Was geraten wurde, sagt es selbst - mit dem Schnellweg daneben, falls
294
+ alles stimmt. Sonst muesste man bei acht Plaetzen achtmal haken. */
295
+ if (rateAnzahl(e)) {
296
+ var rl = el('div', 'ratleiste');
297
+ rl.appendChild(el('span', 'zq', tr('guess.bar', rateAnzahl(e))));
298
+ var bAlle = el('button', 'btn mini wichtig', tr('guess.keepAll'));
299
+ bAlle.addEventListener('click', function () {
300
+ e.states.forEach(rateBehalten);
301
+ entwurfAngefasst(); zeichneErgebnis();
302
+ });
303
+ rl.appendChild(bAlle);
304
+ var bWeg = el('button', 'btn mini', tr('guess.undo'));
305
+ bWeg.addEventListener('click', function () {
306
+ rateZurueck(e); entwurfAngefasst(); S.openRow = null; zeichneErgebnis();
307
+ });
308
+ rl.appendChild(bWeg);
309
+ card.appendChild(rl);
310
+ }
311
+ if (e.rateMeldung) {
312
+ var rm = el('div', 'aside');
313
+ rm.style.margin = '8px 15px';
314
+ rm.textContent = e.rateMeldung;
315
+ card.appendChild(rm);
316
+ }
317
+
318
+ var cap = el('div', 'lhead');
319
+ [tr('list.colOn'), '', tr('list.colState'), tr('list.colRole'), tr('list.colValue'), ''].forEach(function (t, i) {
320
+ cap.appendChild(el('span', i === 4 ? 'r' : null, t));
321
+ });
322
+ card.appendChild(cap);
323
+
324
+ /* --- Zustandsliste --- */
325
+ e.states.forEach(function (s, i) {
326
+ var pl = platzVon[s.n];
327
+ var unfertig = (!s.n || !s.srcR);
328
+ var row = el('div', 'erow' + (s.on ? '' : ' skip') + (S.openRow === i ? ' open' : ''));
329
+ row.tabIndex = 0;
330
+
331
+ var cbw = el('span');
332
+ var cb = document.createElement('input');
333
+ cb.type = 'checkbox';
334
+ cb.checked = s.on;
335
+ cb.setAttribute('aria-label', s.n + ' anlegen');
336
+ cb.addEventListener('click', function (ev) { ev.stopPropagation(); });
337
+ cb.addEventListener('change', function () {
338
+ s.on = cb.checked; entwurfAngefasst(); zeichneErgebnis();
339
+ });
340
+ cbw.appendChild(cb);
341
+ row.appendChild(cbw);
342
+
343
+ /* Nur noch bei einem Problem ein Zeichen. Vorher stand hier ein
344
+ Haken, sobald der Punkt angehakt war und einen Platz im Muster
345
+ hatte — das las sich wie eine Wiederholung des Kaestchens
346
+ daneben, und den Platz nennt die Zeile ohnehin im Klartext
347
+ („ACTUAL" hinter dem Namen). Uebrig bleibt, was man beim
348
+ Ueberfliegen wirklich sucht: wo klemmt es. */
349
+ var mk = el('span', 'mk2 bad');
350
+ mk.textContent = (s.on && unfertig) ? '!' : '';
351
+ if (mk.textContent) { mk.title = !s.n ? tr('list.nameMissing') : tr('list.sourceMissing'); }
352
+ row.appendChild(mk);
353
+
354
+ var n3 = el('span', 'nm3', s.n || tr('list.newPoint'));
355
+ /* Die Beschriftung (kuenftiger Anzeigename) steht gedaempft hinter
356
+ der Kennung - wie die Namen im Baum. Vorher tippte man sie im
357
+ Detail und sah nirgends eine Wirkung. */
358
+ if (s.caption && s.caption !== s.n) { n3.appendChild(el('span', 'nam', s.caption)); }
359
+ if (s.manuell) { n3.appendChild(el('span', 'hand', tr('list.byHand'))); }
360
+ if (s.geaendert) { n3.appendChild(el('span', 'geae', tr('list.changed'))); }
361
+ /* Was die Vorlage anders will, steht an der Zeile - sonst sieht man
362
+ es erst im Trockenlauf (Ricardo, 25.08.2026). */
363
+ if (s.on && !s.geaendert) {
364
+ var abw = vorlagenAbweichung(s);
365
+ if (abw.length) {
366
+ /* Formeln koennen lang sein - die Marke soll die Zeile nicht
367
+ sprengen; der Tooltip zeigt alles. */
368
+ var wert = String(abw[0].vorlage || '—');
369
+ if (wert.length > 28) { wert = wert.slice(0, 27) + '…'; }
370
+ var av = el('span', 'geae tplabw', tr('list.tplDiffers', wert));
371
+ av.title = abw.map(function (x) {
372
+ return x.feld + ': ' + (x.jetzt || '—') + ' → ' + (x.vorlage || '—');
373
+ }).join(String.fromCharCode(10));
374
+ n3.appendChild(av);
375
+ }
376
+ }
377
+ /* Die Marke ist zugleich der Griff: solange sie steht, ist die Zeile
378
+ unbestaetigt und faellt beim Zuruecknehmen. Ein Haken laesst sie
379
+ verschwinden, ein Kreuz nimmt genau diese eine zurueck. */
380
+ if (s.geraten) {
381
+ n3.appendChild(el('span', 'rat', tr('guess.mark')));
382
+ var jaK = el('span', 'ratknopf', '\u2713');
383
+ jaK.title = tr('guess.keep');
384
+ jaK.addEventListener('click', function (ev) {
385
+ ev.stopPropagation(); rateBehalten(s);
386
+ entwurfAngefasst(); zeichneErgebnis();
387
+ });
388
+ n3.appendChild(jaK);
389
+ var neinK = el('span', 'ratknopf', '\u2715');
390
+ neinK.title = tr('guess.drop');
391
+ neinK.addEventListener('click', function (ev) {
392
+ ev.stopPropagation(); rateZurueckEine(e, s);
393
+ entwurfAngefasst(); S.openRow = null; zeichneErgebnis();
394
+ });
395
+ n3.appendChild(neinK);
396
+ }
397
+ /* Das Geraet kann den Befehl, der Punkt fehlt noch. Die Handlung
398
+ gehoert dorthin, wo das Problem sichtbar wird — nicht nur in die
399
+ MQTT-Karte weiter oben. */
400
+ /* Frisch nachgeschaut statt gemerkt — siehe wendeAn. */
401
+ if (s.on && s.srcW && !S.objects[s.srcW]) {
402
+ var fw = el('span', 'fehltnoch', tr('list.sendPointMissing'));
403
+ fw.title = s.srcW;
404
+ n3.appendChild(fw);
405
+ var fb2 = el('button', 'btn schmal');
406
+ fb2.textContent = tr('mq.rowCreate');
407
+ fb2.style.marginLeft = '7px';
408
+ fb2.addEventListener('click', function (ev) {
409
+ ev.stopPropagation();
410
+ var teile = s.srcW.split('.cmnd.');
411
+ if (teile.length === 2) { mqttEinzeln(teile[0], teile[1], 'neu', fb2); }
412
+ });
413
+ n3.appendChild(fb2);
414
+ }
415
+ if (s.on) {
416
+ if (unfertig) { n3.appendChild(el('em', null, !s.n ? tr('list.nameMissing') : tr('list.sourceMissing'))); }
417
+ else if (pl && pl !== s.n && platzAnzahl[pl] === 1) {
418
+ var pe = el('em', null, pl);
419
+ pe.title = tr('pattern.slotName', pl, musterName(e.want) || e.want || '?');
420
+ n3.appendChild(pe);
421
+ } else if (!pl) {
422
+ /* Frueher stand der Vermerk nur an Zeilen, die das info-Muster
423
+ aufnaehme (imInfo). SABOTAGE mit indicator.alarm nimmt aber
424
+ nicht mal das - solche Zeilen blieben stumm, und oben stand
425
+ „3/7 belegt“ bei vier Haken ohne jede Erklaerung (Ricardo,
426
+ 25.08.2026). Angehakt ohne Platz heisst immer: sagen. */
427
+ n3.appendChild(el('em', null, tr('pattern.noPlaceIn', musterName(e.want) || e.want || '?')));
428
+ }
429
+ }
430
+ row.appendChild(n3);
431
+
432
+ var warn = (s.n === 'ACTUAL' && s.on && !pl);
433
+ row.appendChild(el('span', 'ro2' + (warn ? ' warnrole' : ''), s.role || tr('list.noRole')));
434
+ row.appendChild(el('span', 'vl2', fmt(s)));
435
+ row.appendChild(el('span', 'ca2', S.openRow === i ? '▾' : '▸'));
436
+
437
+ var um = function () { S.openRow = (S.openRow === i ? null : i); zeichneErgebnis(); };
438
+ row.addEventListener('click', um);
439
+ row.addEventListener('keydown', function (ev) {
440
+ if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); um(); }
441
+ });
442
+ card.appendChild(row);
443
+
444
+ if (S.openRow === i) { card.appendChild(detailZeile(e, s, i)); }
445
+ });
446
+
447
+ /* --- Freie Plaetze des gewaehlten Musters --- */
448
+ var belegteSlots = {};
449
+ Object.keys(platzVon).forEach(function (n) { belegteSlots[platzVon[n]] = 1; });
450
+ var muster = e.want && musterVon(e.want);
451
+ var freie = muster ? muster.states.filter(function (st) {
452
+ return !belegteSlots[st.name] && !st.multiple;
453
+ }) : [];
454
+
455
+ if (freie.length) {
456
+ var fk = el('div', 'freikopf');
457
+ fk.appendChild(el('span', null, tr('pattern.freeSlots', musterName(e.want) || e.want)));
458
+ fk.appendChild(el('span', 'cnt2', tr('list.countOf', freie.length, muster.states.length)));
459
+ card.appendChild(fk);
460
+
461
+ freie.forEach(function (st) {
462
+ var fr = el('div', 'freirow' + (st.required ? ' pflicht' : ''));
463
+ fr.tabIndex = 0;
464
+ fr.setAttribute('role', 'button');
465
+ fr.appendChild(el('span', 'mk3', st.required ? '✕' : '+'));
466
+ fr.appendChild(el('span', 'sn2', st.name));
467
+ fr.appendChild(el('span', 'rl2', rolleVonPlatz(st) || String(st.role || '')));
468
+ fr.appendChild(el('span', 'rx2', st.required ? tr('pattern.required') : String(st.role || '')));
469
+
470
+ var nimm = function () {
471
+ e.states.push({
472
+ n: st.name,
473
+ on: true,
474
+ role: rolleVonPlatz(st),
475
+ /* Manche Plaetze lassen mehrere Typen zu und nennen sie als
476
+ Liste — light.EFFECT etwa [number, string]. Ungefiltert
477
+ landete die Liste in common.type, der Detektor lehnte den
478
+ Punkt ab, der Platz blieb frei und liess sich endlos
479
+ nochmal hinzufuegen. Der erste Eintrag ist der gemeinte. */
480
+ typ: (Array.isArray(st.type) ? st.type[0] : st.type) || '',
481
+ unit: st.defaultUnit || '',
482
+ /* statesDefined heisst: ohne Werteliste bleibt der Platz leer,
483
+ egal wie gut Rolle und Typ passen. Also die Vorgabe aus dem
484
+ Muster mitnehmen, sonst legt man den Punkt an und der Platz
485
+ gilt weiter als frei — beliebig oft. */
486
+ states: st.statesDefined ? (st.defaultStates || { 0: 'None' }) : undefined,
487
+ wr: !!st.write,
488
+ srcR: '', srcW: '', f: '', fw: '',
489
+ caption: '', manuell: true, ausPlatz: st.name
490
+ });
491
+ S.openRow = e.states.length - 1;
492
+ entwurfAngefasst();
493
+ zeichneErgebnis();
494
+ };
495
+ fr.addEventListener('click', nimm);
496
+ fr.addEventListener('keydown', function (ev) {
497
+ if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); nimm(); }
498
+ });
499
+ card.appendChild(fr);
500
+ });
501
+ }
502
+
503
+ var add = el('button', 'addrow');
504
+ add.appendChild(el('span', null, '+'));
505
+ add.appendChild(el('span', null, tr('list.addByHand')));
506
+ add.addEventListener('click', function () {
507
+ e.states.push({ n: '', on: true, role: 'value', typ: 'number', unit: '', wr: false,
508
+ srcR: '', srcW: '', f: '', fw: '', caption: '', manuell: true, geaendert: true });
509
+ S.openRow = e.states.length - 1;
510
+ entwurfAngefasst();
511
+ zeichneErgebnis();
512
+ });
513
+ card.appendChild(add);
514
+ host.appendChild(card);
515
+ }