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,391 @@
1
+ /* Freie Plaetze vorschlagen - ein Versuch, kein Wissen.
2
+
3
+ Setzt nur Rollen im Entwurf; entscheiden laesst es den Detektor. */
4
+
5
+ import { musterVon, erkenneEntwurf } from './erkennung.js';
6
+
7
+ /* ================== Plaetze raten ==================
8
+
9
+ Ein Geraet, das kein Muster trifft, ist nicht unbedingt ein
10
+ unbekanntes Geraet - oft fehlt nur eine Rolle. Ricardos Klimaanlage
11
+ (midea) bringt alles mit, was `airCondition` verlangt, aber
12
+ `control.operationalMode` traegt die Rolle `switch` statt
13
+ `level.mode.airconditioner`. `MODE` ist der einzige Pflichtplatz des
14
+ Musters - also passt nichts, und weil die Erkennung Alles-oder-nichts
15
+ ist, fallen auch die drei Plaetze weg, die sehr wohl gepasst haetten.
16
+
17
+ Wichtig fuer das Verstaendnis: die Rolle steht am **Alias**-Punkt,
18
+ nicht an der Quelle. Am Geraet aendert das Raten nichts. Es setzt nur
19
+ Rollen im Entwurf - entscheiden laesst es weiter den Detektor. Passt
20
+ die Vermutung, bestaetigt er sie und vergibt die Platznamen selbst;
21
+ passt sie nicht, faellt sie durch und man sieht es sofort. Deshalb
22
+ gibt es hier keinen zweiten Erkennungsweg neben dem bestehenden.
23
+
24
+ Geraten wird nur auf Knopfdruck. Bei der automatischen Erkennung
25
+ nicht - sonst wuesste man nie mehr, was gemessen und was vermutet
26
+ ist. */
27
+
28
+ /* Woerter eines Namens: camelCase getrennt, Trenner weg, klein.
29
+ `control_operationalMode` -> ['control','operational','mode'] */
30
+ function wortListe(t) {
31
+ return String(t || '')
32
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
33
+ .replace(/[_.-]+/g, ' ')
34
+ .toLowerCase().trim().split(/\s+/).filter(Boolean);
35
+ }
36
+
37
+ /* Wortbruecken zwischen den Platznamen des Detektors und dem, was
38
+ Adapter tatsaechlich schreiben. Die Platznamen sind ioBroker-Jargon,
39
+ die Adapternamen englische Produktwoerter - dazwischen fehlt sonst
40
+ die Bruecke.
41
+
42
+ Das ist die einzige gepflegte Liste, und sie enthaelt mit Absicht
43
+ **kein Geraetewissen**, nur Wortgleichungen. Eine Geraeteliste
44
+ altert mit jedem Adapter; Woerter tun das nicht.
45
+
46
+ Verglichen wird als Wortanfang, damit „operation" auch
47
+ „operational" trifft. Umkehrungen bleiben draussen: `reachable` ist
48
+ nicht `UNREACH`, und wer das raet, dreht einem Nutzer die
49
+ Ausfallmeldung um. */
50
+ var WORTBRUECKE = {
51
+ SET: ['target', 'soll', 'setpoint'],
52
+ SET_HEATING: ['heating', 'heiz'],
53
+ SET_COOLING: ['cooling', 'kuehl'],
54
+ ACTUAL: ['indoor', 'current', 'measured', 'room', 'inside', 'ist'],
55
+ MODE: ['operation', 'betrieb'],
56
+ WORKING_MODE: ['operation', 'betrieb'],
57
+ SPEED: ['fan', 'luefter'],
58
+ SPEED_LEVEL: ['fan', 'speed'],
59
+ POWER: ['onoff', 'ein'],
60
+ BOOST: ['turbo', 'boost'],
61
+ SWING: ['schwenk'],
62
+ HUMIDITY: ['humid', 'feuchte'],
63
+ UNREACH: ['unreach', 'offline'],
64
+ LOWBAT: ['lowbat', 'lowbattery'],
65
+ ERROR: ['error', 'fault', 'fehler', 'stoerung'],
66
+ MAINTAIN: ['maintenance', 'wartung'],
67
+ RSSI: ['rssi'],
68
+ BATTERY: ['battery', 'batterie'],
69
+ CONSUMPTION: ['consumption', 'verbrauch', 'energy'],
70
+ CURRENT: ['strom'],
71
+ VOLTAGE: ['voltage', 'spannung'],
72
+ FREQUENCY: ['frequency', 'frequenz'],
73
+ WORKING: ['working', 'busy'],
74
+ DIRECTION: ['direction', 'richtung']
75
+ };
76
+
77
+ function bruecken(platzName) {
78
+ var b = WORTBRUECKE[platzName] || [];
79
+ return b.concat(wortListe(platzName));
80
+ }
81
+
82
+ /* Vorsilben, die Adapter zum Trennen von Stellen und Melden benutzen.
83
+ midea schreibt `control.X` und `status.X`, Tasmota `cmnd` und `stat`. */
84
+ var VORSILBE = /^(control|status|cmnd|cmd|stat|state|set|get|soll|ist|steuerung)$/;
85
+
86
+ function kernWoerter(name) {
87
+ var w = wortListe(name);
88
+ if (w.length > 1 && VORSILBE.test(w[0])) { w = w.slice(1); }
89
+ return w;
90
+ }
91
+
92
+ /* Der eigentliche Rateschritt. Gibt zurueck, wie viele Plaetze belegt
93
+ wurden - null heisst: es gab nichts zu holen. */
94
+ /* Der nicht veraendernde Vorbau des Ratens: was ist frei, was kaeme in
95
+ Frage? Geteilt zwischen dem Raten selbst und der Frage, ob der Knopf
96
+ ueberhaupt etwas anzubieten haette. */
97
+ function rateLage(e) {
98
+ var mu = musterVon(e.want);
99
+ if (!mu) { return null; }
100
+
101
+ /* 1. Was schon belegt ist, bleibt unangetastet - in beide
102
+ Richtungen: weder der Platz noch die Zeile. */
103
+ var fund = erkenneEntwurf(e, e.want);
104
+ var platzBelegt = {}, zeileBelegt = {};
105
+ if (fund.length) {
106
+ fund[0].states.forEach(function (x) {
107
+ if (!x.id) { return; }
108
+ platzBelegt[x.name] = true;
109
+ zeileBelegt[x.id.slice(e.kanal.length + 1)] = true;
110
+ });
111
+ }
112
+ var namenDa = {};
113
+ e.states.forEach(function (st) { namenDa[st.n] = true; });
114
+
115
+ var freiePlaetze = [];
116
+ var gesehen = {};
117
+ mu.states.forEach(function (pl) {
118
+ if (!pl.name || !pl.defaultRole) { return; }
119
+ if (platzBelegt[pl.name] || gesehen[pl.name]) { return; }
120
+ gesehen[pl.name] = true;
121
+ freiePlaetze.push(pl);
122
+ });
123
+ if (!freiePlaetze.length) { return null; }
124
+
125
+ /* Ein Quellpunkt gehoert an genau eine Stelle.
126
+
127
+ Gezaehlt wird ueber die Quelle, nicht ueber den Zeilennamen. Wer als
128
+ Schreib- oder Lesequelle in einer Zeile mit Platz steckt, ist
129
+ vergeben - auch wenn seine eigene Zeile unangehakt danebensteht.
130
+ Genau das fehlte: nach dem Speichern und Wiedereinlesen sah
131
+ `status.operationalMode` frei aus, obwohl es die Lesequelle von MODE
132
+ ist, und wanderte beim naechsten Vorschlag auf WORKING_MODE - den
133
+ Leseplatz zum selben Modus. Jeder Durchgang legte einen drauf, und
134
+ ueber Alias und zurueck sogar zwei.
135
+
136
+ Zeilen ohne Platz bleiben Kandidaten, auch wenn sie angehakt sind -
137
+ wer „alle" gewaehlt hat, soll trotzdem Vorschlaege bekommen. */
138
+ var quelleBelegt = {};
139
+ e.states.forEach(function (st) {
140
+ if (!zeileBelegt[st.n]) { return; }
141
+ if (st.srcR) { quelleBelegt[st.srcR] = true; }
142
+ if (st.srcW) { quelleBelegt[st.srcW] = true; }
143
+ });
144
+
145
+ var freieZeilen = e.states.filter(function (st) {
146
+ if (!st.n || !st.srcR || zeileBelegt[st.n]) { return false; }
147
+ if (quelleBelegt[st.srcR]) { return false; }
148
+ if (st.srcW && quelleBelegt[st.srcW]) { return false; }
149
+ return true;
150
+ });
151
+ if (!freieZeilen.length) { return null; }
152
+ return { mu: mu, namenDa: namenDa, freiePlaetze: freiePlaetze,
153
+ freieZeilen: freieZeilen };
154
+ }
155
+
156
+ /* Gibt es ueberhaupt etwas, das der Knopf vorschlagen koennte? Die
157
+ komplette Rechnung, nur ohne anzuwenden - eine grobe Vorpruefung
158
+ liess den Knopf stehen, obwohl die Schwelle hinterher alles verwarf
159
+ (Ricardos Thermostat: 3 Zeilen, 22 freie Plaetze, nichts passt).
160
+ Der Preis ist ein zusaetzlicher Bewertungslauf je Zeichnen; die
161
+ teuren Erkennungslaeufe macht das Zeichnen ohnehin mehrfach. */
162
+ export function rateMoeglich(e) {
163
+ return ratePlaetze(e, true) > 0;
164
+ }
165
+
166
+ export function ratePlaetze(e, trocken) {
167
+ var lage = rateLage(e);
168
+ if (!lage) { return 0; }
169
+ var namenDa = lage.namenDa;
170
+ var freiePlaetze = lage.freiePlaetze, freieZeilen = lage.freieZeilen;
171
+
172
+ /* 2. Stellen und Melden zusammenfuehren.
173
+
174
+ Bleibt nach dem Abstreifen der Vorsilbe derselbe Name uebrig und
175
+ ist einer schreibbar, der andere nicht, gehoeren sie zusammen:
176
+ geschrieben wird auf den einen, gelesen aus dem anderen. Das ist
177
+ keine Eigenheit von midea - Tasmota macht es mit cmnd und stat
178
+ genauso, und die Trennung „liest aus / schreibt auf" gibt es in
179
+ der Werkbank ohnehin. */
180
+ var gruppen = {};
181
+ freieZeilen.forEach(function (st) {
182
+ var k = kernWoerter(st.n).join('');
183
+ if (!k) { return; }
184
+ (gruppen[k] = gruppen[k] || []).push(st);
185
+ });
186
+ var partnerVon = {};
187
+ var stillgelegt = {};
188
+ Object.keys(gruppen).forEach(function (k) {
189
+ var g = gruppen[k];
190
+ if (g.length !== 2) { return; }
191
+ var schreibt = g.filter(function (x) { return !!x.srcW; });
192
+ var meldet = g.filter(function (x) { return !x.srcW; });
193
+ if (schreibt.length !== 1 || meldet.length !== 1) { return; }
194
+ partnerVon[schreibt[0].n] = meldet[0];
195
+ stillgelegt[meldet[0].n] = true;
196
+ });
197
+ var kandidaten = freieZeilen.filter(function (st) { return !stillgelegt[st.n]; });
198
+
199
+ /* 3. Wie haeufig ist ein Wort auf diesem Geraet?
200
+
201
+ „mode" steht bei der Klimaanlage in ecoMode, operationalMode,
202
+ swingMode, turboMode, childSleepMode und timerMode. Ein Wort,
203
+ das fast ueberall steht, unterscheidet nichts - es zaehlt also
204
+ weniger als eines, das nur einmal vorkommt. Das erspart eine
205
+ Tabelle voller Sonderfaelle. */
206
+ var haeufig = {};
207
+ kandidaten.forEach(function (st) {
208
+ var w = {};
209
+ kernWoerter(st.n).forEach(function (x) { w[x] = 1; });
210
+ Object.keys(w).forEach(function (x) { haeufig[x] = (haeufig[x] || 0) + 1; });
211
+ });
212
+
213
+ /* Welche Woerter gehoeren einem *anderen* Platz? Wer `swingMode`
214
+ heisst, ist der Schwenk und nicht der Betriebsmodus - auch wenn
215
+ „mode" darin steht. */
216
+ var fremdWort = {};
217
+ freiePlaetze.forEach(function (pl) {
218
+ bruecken(pl.name).forEach(function (b) {
219
+ (fremdWort[b] = fremdWort[b] || {})[pl.name] = true;
220
+ });
221
+ });
222
+
223
+ function typPasst(pl, st) {
224
+ if (!pl.type || !st.typ) { return true; }
225
+ var t = Array.isArray(pl.type) ? pl.type : [pl.type];
226
+ return t.indexOf(st.typ) > -1;
227
+ }
228
+
229
+ function punkte(pl, st) {
230
+ /* Was unmoeglich ist, kommt gar nicht erst in die Bewertung. */
231
+ if (pl.write === true && !st.srcW && !partnerVon[st.n]) { return -1; }
232
+ if (!typPasst(pl, st)) { return -1; }
233
+
234
+ var w = kernWoerter(st.n);
235
+ var wSet = {};
236
+ w.forEach(function (x) { wSet[x] = 1; });
237
+ var pw = wortListe(pl.name);
238
+ var punkt = 0, beleg = false;
239
+
240
+ /* Gemeinsame Woerter, gewichtet nach Seltenheit. */
241
+ pw.forEach(function (x) {
242
+ if (!wSet[x]) { return; }
243
+ beleg = true;
244
+ punkt += Math.round(40 / Math.max(1, haeufig[x] || 1));
245
+ });
246
+ if (pw.join('') === w.join('')) { punkt += 45; beleg = true; }
247
+
248
+ /* Wortbruecken, als Wortanfang verglichen.
249
+
250
+ Eine Bruecke, die schon im Platznamen steht, zaehlt nicht noch
251
+ einmal - sonst gewinnt der Platz mit dem laengeren Namen. Genau
252
+ daran bekam die Klimaanlage `SPEED_LEVEL` statt `SPEED`: „speed"
253
+ zaehlte dort als Namenswort **und** als Bruecke. */
254
+ (WORTBRUECKE[pl.name] || []).forEach(function (b) {
255
+ if (pw.indexOf(b) > -1) { return; }
256
+ if (w.some(function (x) { return x.indexOf(b) === 0 || b.indexOf(x) === 0; })) {
257
+ punkt += 30; beleg = true;
258
+ }
259
+ });
260
+
261
+ /* Traegt der Name das Kennwort eines anderen Platzes, gehoert er
262
+ wahrscheinlich dorthin - aber nur, wenn dieses Wort ueberhaupt
263
+ etwas unterscheidet.
264
+
265
+ Die Strafe wird nach derselben Seltenheitsregel gewichtet wie die
266
+ Belohnung. „swing" steht bei der Klimaanlage nur an einem Punkt,
267
+ also spricht es deutlich gegen jeden anderen Platz. „mode" steht
268
+ an sechsen; wer daraus eine volle Strafe macht, wirft
269
+ `turboMode` aus BOOST heraus, obwohl „turbo" eindeutig ist. */
270
+ w.forEach(function (x) {
271
+ var andere = fremdWort[x];
272
+ if (!andere) { return; }
273
+ if (Object.keys(andere).some(function (n) { return n !== pl.name; }) && !andere[pl.name]) {
274
+ punkt -= Math.round(35 / Math.max(1, haeufig[x] || 1));
275
+ }
276
+ });
277
+
278
+ /* Eine Werteliste ist ein starkes Zeichen fuer einen Modusplatz -
279
+ und ihr Fehlen ein ebenso starkes dagegen. */
280
+ var modus = /mode|swing/.test(pl.defaultRole || '');
281
+ if (modus && st.states) { punkt += 25; }
282
+ if (modus && !st.states) { punkt -= 15; }
283
+
284
+ /* Einheit und Typ bestaetigen, mehr nicht. */
285
+ if (/temperature/.test(pl.defaultRole || '') && st.unit === '\u00b0C') { punkt += 15; }
286
+ if (/humidity/.test(pl.defaultRole || '') && st.unit === '%') { punkt += 15; }
287
+ if (pl.type && st.typ && typPasst(pl, st)) { punkt += 8; }
288
+
289
+ /* Rollenfamilie: auch eine falsche Rolle traegt Auskunft. */
290
+ var fam = String(pl.defaultRole || '').split('.')[0];
291
+ if (fam && String(st.role || '').split('.')[0] === fam) { punkt += 8; }
292
+
293
+ /* Die Schreibrichtung entscheidet, wo Gleichstand herrscht.
294
+
295
+ `airCondition` hat zu jedem Modus zwei Plaetze: MODE stellt
296
+ (`level.mode.airconditioner`), WORKING_MODE meldet nur
297
+ (`value.mode.airconditioner`). Nach Namen und Werteliste sind
298
+ beide gleich gut - der Unterschied ist, ob der Punkt etwas
299
+ stellen kann. Ohne diese Regel entschied die Reihenfolge im
300
+ Muster, und die Klimaanlage bekam den Melder statt des
301
+ Stellglieds. */
302
+ var kannStellen = !!(st.srcW || partnerVon[st.n]);
303
+ if (pl.write === true && kannStellen) { punkt += 12; }
304
+ if (pl.write === false && !kannStellen) { punkt += 6; }
305
+ if (pl.write === false && kannStellen) { punkt -= 6; }
306
+
307
+ /* Ohne einen Namensbeleg wird nicht geraten. Typ, Einheit und
308
+ Rollenfamilie allein passen auf zu vieles. */
309
+ return beleg ? punkt : -1;
310
+ }
311
+
312
+ /* 4. Alle Paare bewerten, das beste zuerst nehmen. */
313
+ var SCHWELLE = 30;
314
+ var paare = [];
315
+ freiePlaetze.forEach(function (pl) {
316
+ kandidaten.forEach(function (st, i) {
317
+ var pkt = punkte(pl, st);
318
+ /* Die Zeile wird ueber ihre Stelle gemerkt, nicht ueber ihren
319
+ Namen - den benennt das Zuordnen ja selbst um. Vorher stand
320
+ hier `zeileWeg[pa.st.n]`, und weil die Zeile nach dem ersten
321
+ Treffer schon MODE hiess, kannte der Schutz sie beim zweiten
322
+ Durchgang nicht wieder: derselbe Punkt bekam MODE und gleich
323
+ darauf WORKING_MODE, und am Ende blieb der Leseplatz stehen. */
324
+ if (pkt >= SCHWELLE) { paare.push({ pl: pl, st: st, nr: i, pkt: pkt }); }
325
+ });
326
+ });
327
+ paare.sort(function (a, b) { return b.pkt - a.pkt; });
328
+
329
+ var platzWeg = {}, zeileWeg = {}, gesetzt = 0;
330
+ paare.forEach(function (pa) {
331
+ if (platzWeg[pa.pl.name] || zeileWeg[pa.nr]) { return; }
332
+ /* Traegt schon eine andere Zeile diesen Namen, waere der Alias
333
+ doppelt belegt. Dann lieber nichts. */
334
+ if (namenDa[pa.pl.name] && pa.st.n !== pa.pl.name) { return; }
335
+ platzWeg[pa.pl.name] = true;
336
+ zeileWeg[pa.nr] = true;
337
+ gesetzt++;
338
+ /* Im Trockenlauf zaehlt nur, was vergeben wuerde - namenDa gehoert
339
+ zur lokalen Lage und muss mitziehen, damit die Doppelnamen-Regel
340
+ genauso greift wie beim echten Lauf. */
341
+ if (trocken) { namenDa[pa.pl.name] = true; return; }
342
+
343
+ var st = pa.st, partner = partnerVon[st.n];
344
+ st.ratenVorher = { n: st.n, role: st.role, on: st.on, srcR: st.srcR };
345
+ st.n = pa.pl.name;
346
+ st.role = pa.pl.defaultRole;
347
+ st.on = true;
348
+ st.geraten = true;
349
+ namenDa[pa.pl.name] = true;
350
+ if (partner) {
351
+ /* Schreiben auf den Stellpunkt, lesen vom Melder. */
352
+ st.ratenVorher.partner = { n: partner.n, on: partner.on };
353
+ st.srcR = partner.srcR;
354
+ partner.on = false;
355
+ st.geratenPartner = partner.n;
356
+ }
357
+ });
358
+ return gesetzt;
359
+ }
360
+
361
+ /* Eine einzelne Vermutung zuruecknehmen. */
362
+ export function rateZurueckEine(e, st) {
363
+ if (!st.geraten || !st.ratenVorher) { return false; }
364
+ var v = st.ratenVorher;
365
+ if (v.partner) {
366
+ e.states.forEach(function (x) {
367
+ if (x.n === v.partner.n) { x.on = v.partner.on; }
368
+ });
369
+ }
370
+ st.n = v.n; st.role = v.role; st.on = v.on; st.srcR = v.srcR;
371
+ delete st.geraten; delete st.ratenVorher; delete st.geratenPartner;
372
+ return true;
373
+ }
374
+
375
+ /* Alles zuruecknehmen, was noch die Marke traegt. Bestaetigtes und von
376
+ Hand Geaendertes bleibt stehen - dort ist die Marke schon gefallen. */
377
+ export function rateZurueck(e) {
378
+ var weg = 0;
379
+ e.states.slice().forEach(function (st) {
380
+ if (rateZurueckEine(e, st)) { weg++; }
381
+ });
382
+ return weg;
383
+ }
384
+
385
+ export function rateBehalten(st) {
386
+ delete st.geraten; delete st.ratenVorher; delete st.geratenPartner;
387
+ }
388
+
389
+ export function rateAnzahl(e) {
390
+ return e && e.states ? e.states.filter(function (s) { return s.geraten; }).length : 0;
391
+ }
@@ -0,0 +1,193 @@
1
+ /* Werte und Quellen: nachschlagen, holen, Formeln auswerten, formatieren.
2
+
3
+ S.werte ist der Vorrat der zuletzt gesehenen Zustaende; `wertVon`
4
+ rechnet daraus, was eine Entwurfszeile anzeigen wuerde - samt
5
+ Leseformel und Zieltyp. */
6
+
7
+ import { S } from './zustand.js';
8
+ import { socket } from './verbindung.js';
9
+ import { tr } from './sprache.js';
10
+ import './entwurf.js';
11
+
12
+ /* Direkt nachschlagen — die Vorlagen nennen den Pfad ja vollstaendig
13
+ ("stat.POWER"). Vorher lief das ueber alle Objekte, was beim Pruefen
14
+ ganzer Ordner unbezahlbar wird. */
15
+ /* MQTT uebernimmt die Schreibweise des Themas. Dasselbe Geraetemodell
16
+ meldet mal cmnd.POWER1, mal cmnd.power1 — deshalb erst genau suchen,
17
+ dann ohne Ruecksicht auf Gross- und Kleinschreibung. */
18
+ export function kleinKarte(kanal) {
19
+ if (S.kleinIndex[kanal]) { return S.kleinIndex[kanal]; }
20
+ var karte = {};
21
+ var pre = kanal + '.';
22
+ S.keysSorted.forEach(function (k) {
23
+ if (k.indexOf(pre) !== 0) { return; }
24
+ if (!S.objects[k] || S.objects[k].type !== 'state') { return; }
25
+ karte[k.slice(pre.length).toLowerCase()] = k;
26
+ });
27
+ S.kleinIndex[kanal] = karte;
28
+ return karte;
29
+ }
30
+
31
+ export function hatPunkt(kanal, endung) {
32
+ var id = kanal + '.' + endung;
33
+ if (S.objects[id] && S.objects[id].type === 'state') { return id; }
34
+ var treffer = kleinKarte(kanal)[String(endung).toLowerCase()];
35
+ return treffer || null;
36
+ }
37
+
38
+ export function jsonVon(id) {
39
+ var st = S.werte[id];
40
+ if (!st || typeof st.val !== 'string') { return null; }
41
+ try { return JSON.parse(st.val); } catch { return null; }
42
+ }
43
+
44
+ export function feldWert(obj, pfad) {
45
+ var t = pfad.split('.');
46
+ var x = obj;
47
+ for (var i = 0; i < t.length; i++) {
48
+ if (x === null || x === undefined || typeof x !== 'object') { return undefined; }
49
+ x = x[t[i]];
50
+ }
51
+ return x;
52
+ }
53
+
54
+
55
+ export function kindZustaende(kanal) {
56
+ var pre = kanal + '.';
57
+ return S.keysSorted.filter(function (k) {
58
+ return k.indexOf(pre) === 0 && S.objects[k] && S.objects[k].type === 'state';
59
+ });
60
+ }
61
+
62
+ export function direkteZustaende(kanal) {
63
+ var pre = kanal + '.';
64
+ return kindZustaende(kanal).filter(function (k) { return k.slice(pre.length).indexOf('.') === -1; });
65
+ }
66
+
67
+ export function aliasQuellen(obj) {
68
+ var a = obj && obj.common && obj.common.alias;
69
+ if (!a) { return {}; }
70
+ if (typeof a.id === 'string') { return { read: a.id, write: a.id, einfach: true }; }
71
+ if (a.id && typeof a.id === 'object') { return { read: a.id.read, write: a.id.write }; }
72
+ return {};
73
+ }
74
+
75
+ /* Der Admin-Socket kennt kein getForeignState. getForeignStates(muster)
76
+ und getState(id) funktionieren. */
77
+ export function holeWerte(kanal, extraIds, fertig) {
78
+ var eindeutig = {};
79
+ (extraIds || []).forEach(function (i) { if (i) { eindeutig[i] = 1; } });
80
+ var liste = Object.keys(eindeutig);
81
+ var offen = 1 + liste.length;
82
+ var ab = function () { if (--offen <= 0) { fertig(); } };
83
+
84
+ socket.emit('getForeignStates', kanal + '.*', function (err, res) {
85
+ if (!err && res) { Object.keys(res).forEach(function (k) { if (res[k]) { S.werte[k] = res[k]; } }); }
86
+ ab();
87
+ });
88
+ liste.forEach(function (id) {
89
+ socket.emit('getState', id, function (err, st) {
90
+ if (!err && st) { S.werte[id] = st; }
91
+ ab();
92
+ });
93
+ });
94
+ }
95
+
96
+ /* ================== Formel und Wert ================== */
97
+ export function auswerten(formel, roh) {
98
+ if (roh === undefined || roh === null) { return { ok: false, txt: tr('detail.noValue') }; }
99
+ if (!formel) { return { ok: true, val: roh }; }
100
+ try {
101
+ var v = (new Function('val', 'return (' + formel + ');'))(roh);
102
+ if (v === undefined) { return { ok: false, txt: 'undefined', leer: true }; }
103
+ if (typeof v === 'number' && isNaN(v)) { return { ok: false, txt: 'NaN', leer: true }; }
104
+ return { ok: true, val: v };
105
+ } catch (e) {
106
+ return { ok: false, txt: tr('detail.formulaThrows', e.message), fehler: true };
107
+ }
108
+ }
109
+
110
+ var JA = ['ON', 'TRUE', 'YES', 'JA', '1'];
111
+ var NEIN = ['OFF', 'FALSE', 'NO', 'NEIN', '0'];
112
+
113
+ export function alsZieltyp(v, typ) {
114
+ if (typ === 'boolean' && typeof v === 'string') {
115
+ var t = v.trim().toUpperCase();
116
+ if (JA.indexOf(t) > -1) { return { val: true, gewandelt: true }; }
117
+ if (NEIN.indexOf(t) > -1) { return { val: false, gewandelt: true }; }
118
+ }
119
+ if (typ === 'number' && typeof v === 'string' && v.trim() !== '' && !isNaN(Number(v))) {
120
+ return { val: Number(v), gewandelt: true };
121
+ }
122
+ return { val: v, gewandelt: false };
123
+ }
124
+
125
+ export function wertVon(s) {
126
+ var st = S.werte[s.srcR];
127
+ var roh = st ? st.val : undefined;
128
+ var aus = auswerten(s.f, roh);
129
+ if (aus.ok) {
130
+ var um = alsZieltyp(aus.val, s.typ);
131
+ aus.val = um.val;
132
+ aus.gewandelt = um.gewandelt;
133
+ }
134
+ aus.roh = roh;
135
+ return aus;
136
+ }
137
+
138
+ export function fmt(s) {
139
+ var a = wertVon(s);
140
+ if (!a.ok) { return '—'; }
141
+ var v = a.val;
142
+ if (typeof v === 'boolean') { return v ? 'an' : 'aus'; }
143
+ if (typeof v === 'number') {
144
+ var t = (s.dec === undefined || s.dec === '') ? String(v) : Number(v).toFixed(Number(s.dec));
145
+ return t.replace('.', ',') + (s.unit ? ' ' + s.unit : '');
146
+ }
147
+ if (typeof v === 'object') { return JSON.stringify(v).slice(0, 30); }
148
+ return String(v).slice(0, 30) + (s.unit ? ' ' + s.unit : '');
149
+ }
150
+
151
+ /* Steckt in der Formel nichts als ein ausgepacktes JSON-Feld? Dann ist
152
+ es eines — sonst ist es eine Formel und bleibt eine.
153
+
154
+ `(.+)` nahm frueher alles bis zum Zeilenende mit. Aus
155
+ `JSON.parse(val).POWER === "ON"` wurde damit das „Feld"
156
+ `POWER === "ON"`; eine daraus abgeleitete Vorlage trug den Vergleich
157
+ im Feldnamen, und die Feldauswahl im Detail zeigte ihn als Eintrag. */
158
+ /* Seit 26.08.2026 schreibt die Werkbank den Zugriff abgesichert:
159
+ `JSON.parse(val)?.ENERGY?.Power ?? null`. Beide Schreibweisen muessen
160
+ als Feldzugriff durchgehen - sonst zeigt die Feldauswahl an einem
161
+ Punkt, den die Werkbank selbst gebaut hat, „eigene Formel". */
162
+ var FELDPFAD = /^JSON\.parse\(val\)\??\.([A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*)*)(?:\s*\?\?\s*null)?$/;
163
+
164
+ export function feldAusFormel(f) {
165
+ var m = FELDPFAD.exec(f || '');
166
+ /* Der Feldname selbst traegt keine Fragezeichen. */
167
+ return m ? m[1].split('?.').join('.') : '';
168
+ }
169
+
170
+ /* Der abgesicherte Zugriff auf ein JSON-Feld. An einer Stelle, damit die
171
+ Vorlagen-Abkuerzung und die Feldauswahl im Detail dasselbe bauen. */
172
+ export function feldFormel(feld) {
173
+ if (!feld) { return ''; }
174
+ return 'JSON.parse(val)?.' + String(feld).split('.').join('?.') + ' ?? null';
175
+ }
176
+
177
+ export function jsonFelder(id) {
178
+ var st = S.werte[id];
179
+ if (!st || typeof st.val !== 'string') { return null; }
180
+ var o;
181
+ try { o = JSON.parse(st.val); } catch { return null; }
182
+ if (!o || typeof o !== 'object') { return null; }
183
+ var raus = [];
184
+ (function geh(x, pfad, tiefe) {
185
+ if (tiefe > 3) { return; }
186
+ Object.keys(x).forEach(function (k) {
187
+ var p = pfad ? pfad + '.' + k : k;
188
+ if (x[k] && typeof x[k] === 'object' && !Array.isArray(x[k])) { geh(x[k], p, tiefe + 1); }
189
+ else { raus.push(p); }
190
+ });
191
+ })(o, '', 0);
192
+ return raus;
193
+ }