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,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
|
+
}
|