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.
- package/LICENSE +21 -0
- package/README.md +516 -0
- package/admin/alias-workbench.png +0 -0
- package/admin/css/werkbank.css +916 -0
- package/admin/detector.js +1 -0
- package/admin/geraetewissen/tasmota-befehle.json +116 -0
- package/admin/i18n/de.json +552 -0
- package/admin/i18n/en.json +552 -0
- package/admin/js/aufzaehlungen.js +332 -0
- package/admin/js/basis.js +36 -0
- package/admin/js/baum.js +453 -0
- package/admin/js/detail.js +335 -0
- package/admin/js/einstellungen.js +40 -0
- package/admin/js/entwurf.js +908 -0
- package/admin/js/enums.js +23 -0
- package/admin/js/ergebnis.js +934 -0
- package/admin/js/erkennung.js +126 -0
- package/admin/js/katalog.js +313 -0
- package/admin/js/mqtt.js +885 -0
- package/admin/js/musternamen.js +94 -0
- package/admin/js/musterwahl.js +334 -0
- package/admin/js/objekte.js +243 -0
- package/admin/js/pruefungen.js +130 -0
- package/admin/js/quellentausch.js +570 -0
- package/admin/js/rollenwahl.js +199 -0
- package/admin/js/schreiben.js +1537 -0
- package/admin/js/sprache.js +137 -0
- package/admin/js/start.js +39 -0
- package/admin/js/verbindung.js +10 -0
- package/admin/js/vorlagen.js +572 -0
- package/admin/js/vorlagenblatt.js +1907 -0
- package/admin/js/vorschlagen.js +391 -0
- package/admin/js/werte.js +193 -0
- package/admin/js/zielleiste.js +339 -0
- package/admin/js/zuordnung.js +695 -0
- package/admin/js/zustand.js +47 -0
- package/admin/js/zustandsliste.js +515 -0
- package/admin/jsonConfig.json +249 -0
- package/admin/tab.html +148 -0
- package/admin/vorlagen/hm-dimmer.json +204 -0
- package/admin/vorlagen/hm-fensterkontakt-hm.json +176 -0
- package/admin/vorlagen/hm-fensterkontakt-hmip.json +191 -0
- package/admin/vorlagen/hm-heizgruppe.json +214 -0
- package/admin/vorlagen/hm-heizkoerper.json +236 -0
- package/admin/vorlagen/hm-rollladen.json +190 -0
- package/admin/vorlagen/hm-schaltaktor-lampe.json +197 -0
- package/admin/vorlagen/hm-schaltaktor-mehrfach.json +203 -0
- package/admin/vorlagen/hm-schaltaktor-steckdose.json +197 -0
- package/admin/vorlagen/hm-temperatur-feuchte.json +166 -0
- package/admin/vorlagen/hm-wandthermostat.json +228 -0
- package/admin/vorlagen/index.json +21 -0
- package/admin/vorlagen/messpunkt.json +225 -0
- package/admin/vorlagen/tasmota-lampe.json +263 -0
- package/admin/vorlagen/tasmota-mehrfach.json +203 -0
- package/admin/vorlagen/tasmota-rgb.json +239 -0
- package/admin/vorlagen/tasmota-steckdose.json +265 -0
- package/io-package.json +122 -0
- 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
|
+
}
|