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,572 @@
1
+ /* Die Geraetevorlagen: laden, pruefen, anwenden, vorschlagen.
2
+
3
+ Eine Datei je Vorlage unter admin/vorlagen/, eigene in native der
4
+ Instanz. Die Verarbeitung kennt kein einziges Geraet - alles
5
+ Geraetewissen steckt in den Dateien. */
6
+
7
+ import { S } from './zustand.js';
8
+ import { D, $, el } from './basis.js';
9
+ import { hatPunkt, jsonVon, feldWert, kindZustaende, feldFormel } from './werte.js';
10
+ import { eigenerKanalName, merkeHakenVorgabe } from './entwurf.js';
11
+ import { tasmotaBefehle } from './mqtt.js';
12
+ import { socket } from './verbindung.js';
13
+
14
+ import { tr, sprachtext } from './sprache.js';
15
+ import { musterVon } from './erkennung.js';
16
+
17
+ /* Welche Instanz — der Reiter wird mit ?<instanz>&noFooter geladen. */
18
+ var INSTANZ = (function () {
19
+ var s = (location.search || '').replace(/^\?/, '').split('&')[0];
20
+ return /^\d+$/.test(s) ? s : '0';
21
+ })();
22
+ export var INSTANZ_ID = 'system.adapter.alias-workbench.' + INSTANZ;
23
+
24
+ /* Zieht die Objektzahl in der Kopfzeile nach. Solange setzeMeta noch
25
+ nicht lief, gibt es den Span nicht - dann zeigt setzeMeta die Zahl
26
+ spaeter selbst. */
27
+ export function zeigeObjektzahl() {
28
+ var z = $('#meta-objekte');
29
+ if (z) { z.textContent = tr('app.objectsCount', S.keysSorted.length); }
30
+ }
31
+
32
+ export function setzeMeta() {
33
+ var m = $('#meta');
34
+ if (!m) { return; }
35
+ m.textContent = tr('app.detector') + ' ' + (D ? D.version : '?') +
36
+ ' \u00b7 ' + tr('app.patternsCount', D ? Object.keys(D.patterns).length : 0) +
37
+ ' \u00b7 ' + tr('app.templatesCount', S.VORLAGEN.length) +
38
+ ' \u00b7 ';
39
+ /* Die Objektzahl stand frueher rechts unten in der Fussleiste - dort
40
+ fragte man sich, was sie soll (Ricardo, 25.08.2026). Hier oben
41
+ steht sie neben ihresgleichen; ein eigener Span, damit das Abo sie
42
+ nachfuehren kann, ohne die ganze Zeile neu zu bauen. */
43
+ m.appendChild(el('span', null, tr('app.objectsCount', S.keysSorted.length))).id = 'meta-objekte';
44
+ if (S.vorlagenUnvollstaendig) {
45
+ var w = el('span', 'chip bad', tr('tpls.loadFailed'));
46
+ w.style.marginLeft = '8px';
47
+ w.title = tr('tpls.loadFailedHint');
48
+ m.appendChild(w);
49
+ }
50
+ }
51
+
52
+ export function ladeVorlagen(fertig) {
53
+ fetch('./vorlagen/index.json').then(function (r) { return r.json(); }).then(function (ix) {
54
+ var offen = ix.vorlagen.length;
55
+ if (!offen) { return dazu([]); }
56
+ /* Reihenfolge aus index.json festhalten. Vorher entschied bei
57
+ gleichwertigen Vorlagen, welche Datei zufaellig zuerst fertig
58
+ geladen war — dasselbe Geraet wurde mal Lampe, mal Steckdose. */
59
+ var platz = new Array(ix.vorlagen.length);
60
+ ix.vorlagen.forEach(function (datei, i) {
61
+ fetch('./vorlagen/' + datei).then(function (r) { return r.json(); }).then(function (v) {
62
+ v._datei = datei;
63
+ platz[i] = v;
64
+ if (--offen === 0) { dazu(platz.filter(Boolean)); }
65
+ }).catch(function () { if (--offen === 0) { dazu(platz.filter(Boolean)); } });
66
+ });
67
+ }).catch(function () { dazu([]); });
68
+
69
+ /* Dazu die selbstgebauten aus der Instanzkonfiguration. Der Paketordner
70
+ wird von `iobroker upload` geleert, native bleibt und steckt im
71
+ Backup. Bei gleicher id gewinnt die eigene — sie steht dann an der
72
+ Stelle der Paketvorlage, damit die Reihenfolge fest bleibt. */
73
+ function dazu(paket) {
74
+ paket.forEach(function (v) { v._quelle = 'paket'; });
75
+ var durch = false;
76
+ var schluss = function (eigene, ueberlauf) {
77
+ if (durch) { return; }
78
+ durch = true;
79
+ S.vorlagenUnvollstaendig = !!ueberlauf;
80
+ var zus = paket.slice();
81
+ (eigene || []).forEach(function (v) {
82
+ if (!v || !v.id) { return; }
83
+ v._quelle = 'benutzer';
84
+ var i = -1;
85
+ zus.forEach(function (p, k) { if (p.id === v.id) { i = k; } });
86
+ if (i > -1) { zus[i] = v; } else { zus.push(v); }
87
+ });
88
+ S.VORLAGEN = zus;
89
+ fertig();
90
+ };
91
+ /* Wenn die Antwort ausbleibt, laeuft die Oberflaeche weiter — aber
92
+ sie behauptet dann nicht, es gaebe keine eigenen Vorlagen. */
93
+ setTimeout(function () { schluss([], true); }, 6000);
94
+ socket.emit('getObject', INSTANZ_ID, function (err, o) {
95
+ schluss((o && o.native && o.native.vorlagen) || []);
96
+ });
97
+ }
98
+ }
99
+
100
+ /* Eine einzelne Vorlage ablegen oder entfernen.
101
+
102
+ Zwei Entscheidungen stecken hier drin, beide teuer erkauft:
103
+
104
+ Erstens *nicht* extendObject. Das fuehrt Listen feldweise zusammen:
105
+ eine kuerzer gewordene Liste bliebe unveraendert, und schreibt man
106
+ an Platz 0 eine andere Vorlage, verschmelzen beide zu einem Zwitter
107
+ aus neuem Namen und alten Zustaenden. Gemessen, nicht vermutet.
108
+
109
+ Zweitens wird nur *ein Eintrag* geaendert, nie die ganze Liste aus
110
+ dem Arbeitsspeicher zurueckgeschrieben. Sonst haenge die Sicherheit
111
+ der gespeicherten Vorlagen daran, dass das Laden beim Seitenaufbau
112
+ geklappt hat — ein Zeitueberlauf haette aus einem Speichervorgang
113
+ ein stilles Loeschen aller anderen Vorlagen gemacht. */
114
+ export function aendereVorlagen(id, vorlage, fertig) {
115
+ socket.emit('getObject', INSTANZ_ID, function (err, o) {
116
+ if (err || !o) { return fertig(err || tr('tpls.noInstance', INSTANZ_ID)); }
117
+ o.native = o.native || {};
118
+ var liste = Array.isArray(o.native.vorlagen) ? o.native.vorlagen.slice() : [];
119
+ var i = -1;
120
+ liste.forEach(function (x, n) { if (x && x.id === id) { i = n; } });
121
+ if (vorlage === null) {
122
+ if (i === -1) { return fertig(null); }
123
+ liste.splice(i, 1);
124
+ } else if (i > -1) {
125
+ liste[i] = vorlage;
126
+ } else {
127
+ liste.push(vorlage);
128
+ }
129
+ o.native.vorlagen = liste;
130
+ socket.emit('setObject', INSTANZ_ID, o, function (e2) { fertig(e2 || null); });
131
+ });
132
+ }
133
+
134
+
135
+ /* Welche Ausgangsnummern gibt es? Aus dem Bestand gelesen, nicht
136
+ hochgezaehlt — die Ventilinsel hat POWER1, POWER3, POWER4. */
137
+ export function instanzenVon(v, kanal) {
138
+ var mf = v.mehrfach;
139
+ if (!mf) { return [null]; }
140
+ var ph = mf.platzhalter || '%N%';
141
+ var erf = (v.erkennung || {}).erforderlich || [];
142
+ var mitPh = erf.filter(function (e) { return e.indexOf(ph) > -1; });
143
+
144
+ var kurzNamen = kindZustaende(kanal).map(function (id) { return id.slice(kanal.length + 1); });
145
+
146
+ /* Die Nummern der Ausgaenge muessen nicht als eigene Objekte
147
+ dastehen. Eine Tasmota meldet POWER1..POWER4 in tele/STATE, lange
148
+ bevor es stat/POWER1 gibt — das entsteht erst beim ersten
149
+ Schalten. Wer nur nach Objekten sucht, findet an einem frisch
150
+ eingebundenen Mehrfachgeraet keinen einzigen Ausgang und laesst
151
+ die Vorlage ins Leere laufen. */
152
+ var ausJson = [];
153
+ var inh = (v.erkennung || {}).inhalt || {};
154
+ Object.keys(inh).forEach(function (punkt) {
155
+ if (String(inh[punkt]).indexOf(ph) === -1) { return; }
156
+ var id = hatPunkt(kanal, punkt);
157
+ var j = id ? jsonVon(id) : null;
158
+ if (!j) { return; }
159
+ var teile = String(inh[punkt]).split(ph);
160
+ var vor = teile[0].toLowerCase(), nach = (teile[1] || '').toLowerCase();
161
+ var raus = {};
162
+ Object.keys(j).forEach(function (feld) {
163
+ var k = feld.toLowerCase();
164
+ if (k.length <= vor.length + nach.length) { return; }
165
+ if (k.slice(0, vor.length) !== vor) { return; }
166
+ if (nach && k.slice(-nach.length) !== nach) { return; }
167
+ var mitte = nach ? feld.slice(vor.length, feld.length - nach.length) : feld.slice(vor.length);
168
+ if (mitte && /^[A-Za-z0-9_]+$/.test(mitte)) { raus[mitte] = 1; }
169
+ });
170
+ if (Object.keys(raus).length) { ausJson.push(raus); }
171
+ });
172
+
173
+ /* Ohne regulaeren Ausdruck: der Platzhalter teilt die Angabe in
174
+ Vorspann und Nachspann, dazwischen steht die Nummer. */
175
+ var mengen = mitPh.map(function (muster) {
176
+ var teile = muster.split(ph);
177
+ var vor = teile[0].toLowerCase(), nach = (teile[1] || '').toLowerCase();
178
+ var raus = {};
179
+ kurzNamen.forEach(function (n) {
180
+ /* Gross- und Kleinschreibung ignorieren: dasselbe Modell meldet
181
+ mal stat.POWER1 und cmnd.power1 — beides ist derselbe Ausgang. */
182
+ var k = n.toLowerCase();
183
+ if (k.length <= vor.length + nach.length) { return; }
184
+ if (k.slice(0, vor.length) !== vor) { return; }
185
+ if (nach && k.slice(-nach.length) !== nach) { return; }
186
+ var mitte = nach ? n.slice(vor.length, n.length - nach.length) : n.slice(vor.length);
187
+ if (mitte && /^[A-Za-z0-9_]+$/.test(mitte)) { raus[mitte] = 1; }
188
+ });
189
+ return raus;
190
+ });
191
+
192
+ /* Leere Mengen fliegen raus. Eine Pflichtangabe, zu der es kein
193
+ einziges Objekt gibt, sagt „hier weiss ich nichts" — nicht „hier
194
+ gibt es keine Ausgaenge". Frueher ging genau diese leere Menge in
195
+ den Schnitt ein und machte ihn immer leer: eine Ventilinsel, die
196
+ POWER1..4 in tele/STATE meldet, aber noch keinen cmnd-Punkt hat,
197
+ fand keinen einzigen Ausgang. Das ist derselbe Fall, fuer den die
198
+ Erkennung aus moeglichen Befehlen gebaut wurde. */
199
+ mengen = mengen.concat(ausJson).filter(function (m) {
200
+ return Object.keys(m).length > 0;
201
+ });
202
+ if (!mengen.length) { return [null]; }
203
+
204
+ /* nur Nummern, die in allen Pflichtangaben vorkommen */
205
+ var klein = mengen.map(function (m) {
206
+ var k = {};
207
+ Object.keys(m).forEach(function (n) { k[n.toLowerCase()] = n; });
208
+ return k;
209
+ });
210
+ return Object.keys(klein[0]).filter(function (n) {
211
+ return klein.every(function (k) { return k[n]; });
212
+ }).map(function (n) { return klein[0][n]; })
213
+ .sort(function (a, b) { return String(a).localeCompare(String(b), undefined, {numeric: true}); });
214
+ }
215
+
216
+ export function setzeInstanz(text, v, n) {
217
+ if (!v.mehrfach || n === null || n === undefined) { return text; }
218
+ return String(text).split(v.mehrfach.platzhalter || '%N%').join(n);
219
+ }
220
+
221
+ /* Aus welchem Geraet ist dieser Alias gebaut?
222
+
223
+ Zwei Wege, und der zweite ist der wichtigere. `native.quelle` steht am
224
+ Kanal, seit die Werkbank ihn schreibt - das ist die verlaessliche
225
+ Auskunft. Einen von Hand gebauten Alias hat aber niemand so vermerkt;
226
+ dort bleibt nur, den Punkten zu folgen: worauf ihre `alias.id` zeigt,
227
+ davon ist das gemeinsame Elternteil die Quelle. */
228
+
229
+ /* Wie der Ausgang mit der Nummer n heisst.
230
+
231
+ Bei Tasmota ist ein Ausgang kein Objekt — POWER1 steht als Feld in
232
+ tele/STATE, und wie er im Haus heisst, weiss nur der Nutzer. Da
233
+ bleibt nur das Muster aus der Vorlage.
234
+
235
+ Bei Homematic ist jeder Ausgang ein eigener Kanal und traegt einen
236
+ Namen: der Doppelaktor Licht_Bar_Esstisch hat die Kanaele „Licht_Bar"
237
+ und „Licht_Esstisch". Diesen Namen zu uebernehmen ist der einzige
238
+ Weg, die beiden Aliase spaeter auseinanderzuhalten — „1" und „2"
239
+ sagen nichts. Traegt der Kanal keinen eigenen Namen (Homematic
240
+ schreibt sonst „<Geraet>:<Nr>" hinein, was nichts hinzufuegt), bleibt
241
+ es beim Muster. */
242
+ export function ausgangName(e, n) {
243
+ if (e && e.kanal) {
244
+ var eigen = eigenerKanalName(e.kanal + '.' + n);
245
+ if (eigen) { return eigen.replace(/\./g, '_'); }
246
+ }
247
+ if (!e || !e.kanalnameRoh) { return 'POWER' + n; }
248
+ return String(e.kanalnameRoh).split(e.platzhalter || '%N%').join(n);
249
+ }
250
+
251
+ /* Einen cmnd-Punkt gibt es erst, wenn ihn jemand angelegt hat: eine
252
+ Tasmota sendet dorthin nie, das ist ihr Posteingang. Was sie kann,
253
+ steht aber in dem, was sie sendet — die JSON-Schluessel in tele/STATE
254
+ sind die Befehlsnamen.
255
+
256
+ Deshalb zaehlt fuer die Erkennung nicht, ob der Punkt schon da ist,
257
+ sondern ob das Geraet den Befehl kennt. Sonst muss man erst Punkte
258
+ anlegen, damit die Vorlage greift, obwohl die Werkbank langst weiss,
259
+ was vor ihr steht.
260
+
261
+ Nur fuer cmnd. stat und tele entstehen von selbst, sobald das Geraet
262
+ sendet — dort ist „fehlt" eine echte Aussage. Und nur, wenn das Geraet
263
+ den Befehl wirklich meldet: ein blosser Messpunkt, der ein Relais
264
+ nennt, soll nicht als Steckdose durchgehen. */
265
+ export function befehlMoeglich(kanal, pfad) {
266
+ if (String(pfad).indexOf('cmnd.') !== 0) { return false; }
267
+ var name = String(pfad).slice(5).toLowerCase();
268
+ var b = tasmotaBefehle(kanal);
269
+ if (!b) { return false; }
270
+ return b.befehle.some(function (x) { return String(x.name).toLowerCase() === name; });
271
+ }
272
+
273
+ function punktErfuellt(kanal, pfad) {
274
+ return !!hatPunkt(kanal, pfad) || befehlMoeglich(kanal, pfad);
275
+ }
276
+
277
+ /* Passt die Vorlage auf diesen Kanal? Liefert null oder eine Bewertung. */
278
+ export function pruefeVorlage(v, kanal, instanz) {
279
+ var erk = v.erkennung || {};
280
+ var gruende = [];
281
+ var fehler = [];
282
+
283
+ var inst = instanz;
284
+ if (v.mehrfach && (inst === undefined || inst === null)) {
285
+ var alle = instanzenVon(v, kanal);
286
+ if (!alle.length) {
287
+ return { vorlage: v, passt: false, fehler: [tr('tpl.noOutputs')], instanzen: [] };
288
+ }
289
+ inst = alle[0];
290
+ }
291
+ var auf = function (t) { return setzeInstanz(t, v, inst); };
292
+
293
+ /* Punkte, die es nicht geben darf. Das ist der ehrliche Unterschied
294
+ zwischen einer einfachen und einer mehrfachen Steckdose — vorher
295
+ liess sich das nur ueber den Rang erraten. Bewusst zaehlt ein
296
+ nicht vorhandener Punkt *nicht* als Beleg: sonst liesse sich die
297
+ Bewertung mit einer Liste erfundener Ausschluesse aufblasen. */
298
+ var verbotenDa = ((erk.verboten || []).map(auf)).filter(function (e) { return hatPunkt(kanal, e); });
299
+ verbotenDa.forEach(function (e) { fehler.push(tr('tpl.forbiddenThere', e)); });
300
+
301
+ var fehlt = (erk.erforderlich || []).map(auf).filter(function (e) { return !punktErfuellt(kanal, e); });
302
+ fehlt.forEach(function (e) { fehler.push(tr('tpl.missingPoint', e)); });
303
+ (erk.erforderlich || []).map(auf).forEach(function (e) {
304
+ if (fehlt.indexOf(e) !== -1) { return; }
305
+ /* Im „Warum" auseinanderhalten, worauf die Erkennung beruht: ein
306
+ vorhandener Punkt ist eine Tatsache, ein moeglicher eine
307
+ Schlussfolgerung. */
308
+ gruende.push(hatPunkt(kanal, e) ? tr('tpl.presentIn', e) : tr('tpl.possibleIn', e));
309
+ });
310
+ var ausgaenge = v.mehrfach ? instanzenVon(v, kanal) : [];
311
+ if (v.mehrfach) {
312
+ gruende.push(tr('tpl.outputsFound', ausgaenge.join(', ')));
313
+ }
314
+
315
+ if (erk.inhalt) {
316
+ Object.keys(erk.inhalt).forEach(function (punkt0) {
317
+ /* Auch hier den Platzhalter ersetzen — ein Mehrfachgeraet nennt
318
+ sein Feld POWER%N%, und ohne Ersetzung suchte die Pruefung nach
319
+ einem Feld, das woertlich so heisst. */
320
+ var punkt = auf(punkt0), feld = auf(erk.inhalt[punkt0]);
321
+ var id = hatPunkt(kanal, punkt);
322
+ var j = id ? jsonVon(id) : null;
323
+ if (!id) { fehler.push(tr('tpl.missingPoint', punkt)); }
324
+ else if (!j) { fehler.push(tr('tpl.noJson', punkt)); }
325
+ else if (feldWert(j, feld) === undefined) {
326
+ fehler.push(tr('tpl.withoutField', punkt, feld));
327
+ } else { gruende.push(tr('tpl.contains', punkt, feld)); }
328
+ });
329
+ }
330
+
331
+ if (fehler.length) {
332
+ /* Dieselbe Angabe kann aus erforderlich und inhalt kommen — einmal reicht. */
333
+ var einmal = [];
334
+ fehler.forEach(function (x) { if (einmal.indexOf(x) === -1) { einmal.push(x); } });
335
+ return { vorlage: v, passt: false, fehler: einmal };
336
+ }
337
+
338
+ /* Bewertung. Zuerst zaehlt, was die Vorlage am Geraet wirklich
339
+ nachgewiesen hat — jeder gefundene Pflichtpunkt, bei mehreren
340
+ Ausgaengen je Ausgang, dazu jede aufgegangene Inhaltspruefung.
341
+ Erst danach der Namenshinweis, und der entscheidet damit wirklich
342
+ nur zwischen gleich gut belegten Vorlagen. Frueher schlug er alles:
343
+ eine selbstgebaute Vorlage verlor gegen eine mitgelieferte, bloss
344
+ weil deren Wort zufaellig im Geraetenamen stand. Die Abstaende sind
345
+ so gross, dass sich die Stufen nie ueberholen koennen. */
346
+ var name = kanal.split('.').pop();
347
+ var belege = (erk.erforderlich || []).length *
348
+ (v.mehrfach ? Math.max(1, ausgaenge.length) : 1) +
349
+ Object.keys(erk.inhalt || {}).length;
350
+ var namenstreffer = !!(erk.namenshinweis && new RegExp(erk.namenshinweis, 'i').test(name));
351
+ if (namenstreffer) { gruende.push(tr('tpl.nameHint', sprachtext(v.name))); }
352
+ var eigen = v._quelle === 'benutzer';
353
+ var punkte = belege * 1000000 +
354
+ (namenstreffer ? 10000 : 0) +
355
+ (eigen ? 1000 : 0) +
356
+ Math.min(v.rang || 0, 999);
357
+ return { vorlage: v, passt: true, punkte: punkte, gruende: gruende, fehler: [],
358
+ belege: belege, namenstreffer: namenstreffer, eigen: eigen,
359
+ instanz: inst, instanzen: ausgaenge };
360
+ }
361
+
362
+ /* Wohin ein Zustand schreibt. Gibt es den Punkt, ist es seine ID; kennt
363
+ das Geraet den Befehl bloss, ist es die ID, die er bekommen wird. */
364
+ export function schreibZiel(z, kanal, auf) {
365
+ if (!z.schreiben) { return ''; }
366
+ var pfad = auf(z.schreiben);
367
+ if (z.schreibenAbs || z.absolut) { return S.objects[pfad] ? pfad : ''; }
368
+ var da = hatPunkt(kanal, pfad);
369
+ if (da) { return da; }
370
+ return befehlMoeglich(kanal, pfad) ? (kanal + '.' + pfad) : '';
371
+ }
372
+
373
+ /* Vorlage auf einen Kanal anwenden → Entwurf. */
374
+ export function wendeAn(v, kanal, gruende, instanz) {
375
+ var auf = function (t) { return setzeInstanz(t, v, instanz); };
376
+ var e = { kanal: kanal, states: [], want: v.geraetetyp, wantAuto: v.geraetetyp,
377
+ alleMuster: false, vorschlag: true, roh: false,
378
+ vorlage: v.id, vorlageVersion: v.version, tplName: sprachtext(v.name),
379
+ grund: (gruende || []).slice(), uebersprungen: [],
380
+ instanz: (instanz === undefined ? null : instanz),
381
+ instanzen: v.mehrfach ? instanzenVon(v, kanal) : [],
382
+ /* Wie der Kanal je Ausgang heisst — steht in der Vorlage,
383
+ nicht im Code. `kanalname` gilt fuer den gerade
384
+ bearbeiteten Ausgang, `kanalnameRoh` fuer jeden anderen:
385
+ die Ausgangswahl und die Sammelanlage muessen die Namen
386
+ der *uebrigen* Nummern bilden koennen. Frueher rechneten
387
+ sie die Nummer aus dem fertigen Namen zurueck, und wo das
388
+ nicht ging, stand 'POWER' + Nummer im Code — ein
389
+ Homematic-Doppelaktor bekam so Ausgaenge namens POWER1
390
+ und POWER2, obwohl er kein POWER kennt. */
391
+ kanalname: v.mehrfach ? auf(v.kanalname || 'POWER%N%') : null,
392
+ kanalnameRoh: v.mehrfach ? (v.kanalname || 'POWER%N%') : null,
393
+ platzhalter: v.mehrfach ? (v.mehrfach.platzhalter || '%N%') : null };
394
+
395
+ (v.zustaende || []).forEach(function (z) {
396
+ /* Ein absolut gespeicherter Punkt zeigt auf ein festes Objekt und
397
+ wird nicht unter dem Kanal gesucht — etwa ein gemeinsamer
398
+ Sollwert in 0_userdata. */
399
+ var finde = function (pfad) {
400
+ if (!pfad) { return null; }
401
+ if (z.absolut) { return S.objects[pfad] ? pfad : null; }
402
+ return hatPunkt(kanal, auf(pfad));
403
+ };
404
+ /* Ersatzquelle: dasselbe steht oft an zwei Stellen, und welche davon
405
+ ein Geraet gerade fuehrt, weiss man vorher nicht.
406
+
407
+ Der Schaltzustand liegt in stat/POWER und in tele/STATE — stat
408
+ entsteht erst beim ersten Schalten. Die IP steht in tele/INFO2,
409
+ das eine Tasmota beim Start retained hinterlegt, und ausserdem in
410
+ der ersten tele/STATE nach dem Boot, danach nicht mehr.
411
+
412
+ Geprueft wird deshalb beides, und zwar vollstaendig: Objekt da UND
413
+ Feld drin. Vorher wurde der Ersatz nur genommen, wenn das Objekt
414
+ ganz fehlte — gab es tele.INFO2 ohne die IP darin, fiel der Punkt
415
+ weg, obwohl tele.STATE sie gehabt haette. */
416
+ var wege = [{ punkt: z.lesen, feld: z.feld, leseformel: z.leseformel }];
417
+ /* Ein Ersatzweg, mehrere Ersatzwege oder blosser Punktname — alles
418
+ drei kommt in Vorlagen vor und soll gehen. */
419
+ var sonst = z.lesenSonst;
420
+ if (sonst) {
421
+ (Array.isArray(sonst) ? sonst : [sonst]).forEach(function (x) {
422
+ wege.push(typeof x === 'string'
423
+ ? { punkt: x, feld: z.feld, leseformel: z.leseformel }
424
+ : { punkt: x.punkt, feld: x.feld, leseformel: x.leseformel });
425
+ });
426
+ }
427
+
428
+ var quelle = null, feld = null, leseformel = null, gescheitert = [];
429
+ for (var wi = 0; wi < wege.length; wi++) {
430
+ var w = wege[wi];
431
+ var id0 = finde(w.punkt);
432
+ var wo = auf(w.punkt) + (w.feld ? ' · ' + auf(w.feld) : '');
433
+ if (!id0) { gescheitert.push(wo); continue; }
434
+ if (w.feld) {
435
+ var f0 = auf(w.feld);
436
+ var j0 = jsonVon(id0);
437
+ if (!j0 || feldWert(j0, f0) === undefined) { gescheitert.push(wo); continue; }
438
+ }
439
+ quelle = id0; feld = w.feld; leseformel = w.leseformel;
440
+ break;
441
+ }
442
+
443
+ if (!quelle) {
444
+ /* Beide Wege nennen, nicht nur den letzten — sonst sucht man an der
445
+ einen Stelle und die Vorlage hatte an zwei geschaut. */
446
+ e.uebersprungen.push(gescheitert.length > 1
447
+ ? tr('tpl.skippedNowhere', z.name, gescheitert.join(tr('tpl.nor')))
448
+ : tr('tpl.skippedSource', z.name, gescheitert[0] || auf(z.lesen)));
449
+ return;
450
+ }
451
+
452
+ /* Auch in der Formel steht der Platzhalter — ohne Ersetzung suchte
453
+ der Alias nach einem Feld namens POWER%N%. */
454
+ var formel = leseformel ? auf(leseformel) : '';
455
+ if (feld) {
456
+ feld = auf(feld);
457
+ /* Mit Fragezeichen-Zugriff: `tele.SENSOR` ist ein Sammeltopf, und
458
+ nicht jede Nachricht darin fuehrt jedes Feld. Ohne Schutz wirft
459
+ `JSON.parse(val).ENERGY.Total` bei einer Nachricht ohne ENERGY
460
+ einen Fehler, der Alias liefert nichts und der Controller
461
+ schreibt eine Warnung. Ricardos handgebaute Formel hatte den
462
+ Schutz laengst, unsere nicht (26.08.2026). */
463
+ formel = feldFormel(feld);
464
+ }
465
+
466
+ e.states.push({
467
+ n: z.name,
468
+ on: !z.vorgabeAus,
469
+ role: z.rolle || '',
470
+ typ: z.typ || '',
471
+ unit: z.einheit || '',
472
+ wr: !!z.schreiben,
473
+ srcR: quelle,
474
+ /* Kennt das Geraet den Befehl, ist das Ziel bekannt — auch wenn
475
+ der Punkt noch nicht angelegt ist. Sonst waere der Alias stumm,
476
+ obwohl jeder weiss, wohin er schreiben muesste. Ob es den Punkt
477
+ schon gibt, wird beim Zeichnen nachgeschaut, nicht hier vermerkt:
478
+ ein Vermerk veraltet, sobald der Punkt entsteht, und zwingt zum
479
+ Neuaufbau des ganzen Entwurfs — mitsamt allem, was der Nutzer
480
+ bis dahin eingestellt hat. */
481
+ srcW: schreibZiel(z, kanal, auf),
482
+ f: formel,
483
+ fw: z.schreibformel ? auf(z.schreibformel) : '',
484
+ caption: z.beschriftung || '',
485
+ dec: z.nachkommastellen === undefined ? undefined : String(z.nachkommastellen),
486
+ /* Plaetze mit statesDefined — EFFECT etwa — bleiben ohne
487
+ Werteliste leer, egal wie gut Rolle und Typ passen. */
488
+ states: z.werteliste || undefined,
489
+ manuell: false,
490
+ ausVorlage: z.name,
491
+ hinweis: z.hinweis || '',
492
+ /* Alle Lesewege mitnehmen, nicht nur den, der heute gegriffen hat.
493
+ Wer aus diesem Geraet eine Vorlage macht, soll die Ersatzquellen
494
+ behalten — sonst faellt die neue Vorlage bei jedem Geraet um, das
495
+ seine IP an einer anderen Stelle fuehrt. */
496
+ wege: wege.map(function (w) {
497
+ return { punkt: w.punkt, feld: w.feld, formel: w.leseformel };
498
+ })
499
+ });
500
+ });
501
+
502
+ return e.states.length ? e : null;
503
+ }
504
+
505
+ /* Beste Vorlage suchen und anwenden. */
506
+ export function vorschlag(kanal, vorlagenId, instanz) {
507
+ if (!S.VORLAGEN.length) { return null; }
508
+
509
+ var alle = S.VORLAGEN.map(function (v) { return pruefeVorlage(v, kanal, instanz); });
510
+ var treffer = alle.filter(function (t) { return t.passt; });
511
+
512
+ var gewaehlt = null;
513
+ if (vorlagenId) {
514
+ /* Von Hand gewaehlt gilt auch, wenn die Erkennung nicht anspringt —
515
+ die Verarbeitung laesst dann weg, wofuer die Quelle fehlt. */
516
+ alle.forEach(function (t) { if (t.vorlage.id === vorlagenId) { gewaehlt = t; } });
517
+ }
518
+ if (!gewaehlt) {
519
+ if (!treffer.length) { return null; }
520
+ treffer.sort(function (a, b) { return b.punkte - a.punkte; });
521
+ gewaehlt = treffer[0];
522
+ }
523
+
524
+ var e = wendeAn(gewaehlt.vorlage, kanal, gewaehlt.gruende, gewaehlt.instanz);
525
+ if (!e) { return null; }
526
+ if (!vorlagenId && gewaehlt.passt) {
527
+ var gleichAuf = treffer.filter(function (t) { return t.punkte === gewaehlt.punkte; });
528
+ var ohneHinweis = !gewaehlt.namenstreffer;
529
+ if (gleichAuf.length > 1 || (ohneHinweis && treffer.length > 1)) {
530
+ e.grund.push(tr('tpl.noNameHint', sprachtext(gewaehlt.vorlage.name)));
531
+ }
532
+ }
533
+ if (!gewaehlt.passt) {
534
+ e.erzwungen = true;
535
+ e.grund = e.grund.concat(gewaehlt.fehler.map(function (f) { return tr('tpl.wouldNeed', f); }));
536
+ }
537
+ e.andere = treffer.filter(function (t) { return t.vorlage.id !== gewaehlt.vorlage.id; })
538
+ .map(function (t) { return t.vorlage; });
539
+ if (vorlagenId) { e.vonHandGewaehlt = true; }
540
+ return merkeHakenVorgabe(e);
541
+ }
542
+
543
+ /* Rollen an ein anderes Muster anpassen — generisch ueber defaultRole,
544
+ nicht je Vorlage ausprogrammiert. */
545
+ export function anMusterAnpassen(e, neuTyp) {
546
+ var alt = musterVon(e.want), neu = musterVon(neuTyp);
547
+ if (!alt || !neu) { return; }
548
+
549
+ function slotVon(muster, rolle) {
550
+ var s = null;
551
+ muster.states.forEach(function (st) {
552
+ if (s || !st.role) { return; }
553
+ try { if (new RegExp(st.role.source || st.role).test(rolle)) { s = st; } } catch { /* dito: eine kaputte Rolle macht die Vorlage nicht passend */ }
554
+ });
555
+ return s;
556
+ }
557
+
558
+ e.states.forEach(function (st) {
559
+ var alterSlot = slotVon(alt, st.role);
560
+ if (!alterSlot) { return; }
561
+ var ziel = null;
562
+ neu.states.forEach(function (n) { if (!ziel && n.name === alterSlot.name) { ziel = n; } });
563
+ if (!ziel && /ACTUAL$/.test(alterSlot.name)) {
564
+ neu.states.forEach(function (n) { if (!ziel && /ACTUAL$/.test(n.name)) { ziel = n; } });
565
+ }
566
+ if (ziel && ziel.defaultRole && ziel.defaultRole !== st.role) {
567
+ st.role = ziel.defaultRole;
568
+ st.n = ziel.name;
569
+ }
570
+ });
571
+ e.want = neuTyp;
572
+ }