iobroker.eos-admin 7.9.76 → 7.9.78
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/adminWww/assets/Objects-DPan0bzw-v76.js +1 -1
- package/adminWww/assets/index-D2ymscJA-v76.js +2 -2
- package/adminWww/css/eos-branding.css +25 -32
- package/adminWww/index.html +1 -1
- package/adminWww/js/eos-branding.js +1 -1
- package/adminWww/js/eos-console-quiet.js +1 -1
- package/adminWww/js/eos-manual-write-policy.js +255 -330
- package/adminWww/js/eos-objects-state-tools.js +1 -1
- package/adminWww/js/eos-policy-client.js +1 -1
- package/adminWww/js/eos-role-ui.js +1 -1
- package/adminWww/js/eos-security-ui.js +1 -1
- package/adminWww/mf-manifest.json +1 -1
- package/adminWww/remoteEntry.js +1 -1
- package/docs/MANUAL_DATAPOINT_WRITE_POLICY.md +16 -13
- package/io-package.json +7 -7
- package/package.json +2 -2
|
@@ -5,71 +5,91 @@
|
|
|
5
5
|
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
6
6
|
if (!root) return;
|
|
7
7
|
|
|
8
|
-
root.NEXOWATT_EOS_MANUAL_WRITE_POLICY_VERSION = 'v76-universal-manual-
|
|
8
|
+
root.NEXOWATT_EOS_MANUAL_WRITE_POLICY_VERSION = 'v76-universal-manual-state-editor';
|
|
9
|
+
root.NEXOWATT_EOS_MANUAL_WRITE_POLICY = api;
|
|
9
10
|
root.NEXOWATT_EOS_GET_WRITE_BEHAVIOR = api.getWriteBehavior;
|
|
11
|
+
root.NEXOWATT_EOS_GET_DIRECT_WRITE_VALUE = api.getDirectWriteValue;
|
|
12
|
+
root.NEXOWATT_EOS_GET_EXPERT_ONLY_REASON = api.getExpertOnlyReason;
|
|
10
13
|
root.NEXOWATT_EOS_IS_EXPERT_ONLY_STATE = api.isExpertOnlyState;
|
|
11
14
|
root.NEXOWATT_EOS_COERCE_BOOLEAN = api.toBoolean;
|
|
15
|
+
root.NEXOWATT_EOS_PREPARE_MANUAL_EDITOR = api.prepareEditor;
|
|
16
|
+
root.NEXOWATT_EOS_PARSE_MANUAL_VALUE = api.parseEditorValue;
|
|
12
17
|
root.NEXOWATT_EOS_WRITE_MANUAL_STATE = api.writeManualState;
|
|
13
|
-
root.NEXOWATT_EOS_RESOLVE_EDITOR_TYPE = api.resolveEditorType;
|
|
14
|
-
root.NEXOWATT_EOS_PREPARE_EDITOR_VALUE = api.prepareEditorValue;
|
|
15
|
-
root.NEXOWATT_EOS_COERCE_WRITE_VALUE = api.coerceWriteValue;
|
|
16
|
-
root.NEXOWATT_EOS_GET_DIRECT_WRITE_VALUE = api.getDirectWriteValue;
|
|
17
|
-
root.NEXOWATT_EOS_GET_WRITE_LABEL = api.getWriteLabel;
|
|
18
|
-
root.NEXOWATT_EOS_NORMALIZE_STATES = api.normalizeStates;
|
|
19
18
|
})(typeof window !== 'undefined' ? window : typeof globalThis !== 'undefined' ? globalThis : null, function () {
|
|
20
19
|
'use strict';
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
// instead of being silently dropped while the previous write is pending.
|
|
24
|
-
const writeQueues = new Map();
|
|
21
|
+
const pendingWrites = new Map();
|
|
25
22
|
|
|
26
23
|
const normalizeText = value => String(value == null ? '' : value)
|
|
27
24
|
.normalize('NFKD')
|
|
28
25
|
.replace(/[\u0300-\u036f]/g, '')
|
|
29
|
-
.toLowerCase()
|
|
30
|
-
.trim();
|
|
26
|
+
.toLowerCase();
|
|
31
27
|
|
|
32
28
|
const flattenTranslatedText = value => {
|
|
33
29
|
if (value == null) return '';
|
|
34
30
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
35
|
-
if (
|
|
31
|
+
if (Array.isArray(value)) return value.map(flattenTranslatedText).join(' ');
|
|
32
|
+
if (typeof value === 'object') return Object.values(value).map(flattenTranslatedText).join(' ');
|
|
36
33
|
return '';
|
|
37
34
|
};
|
|
38
35
|
|
|
39
|
-
const
|
|
40
|
-
|
|
36
|
+
const normalizeCommonType = (obj, value) => {
|
|
37
|
+
const common = obj && obj.common ? obj.common : {};
|
|
38
|
+
const type = Array.isArray(common.type) ? common.type[0] : common.type;
|
|
39
|
+
if (['number', 'string', 'boolean', 'array', 'object', 'mixed', 'file', 'json'].includes(type)) return type;
|
|
40
|
+
if (Array.isArray(value)) return 'array';
|
|
41
|
+
if (value && typeof value === 'object') return 'object';
|
|
42
|
+
if (typeof value === 'number') return 'number';
|
|
43
|
+
if (typeof value === 'boolean') return 'boolean';
|
|
44
|
+
return 'string';
|
|
45
|
+
};
|
|
41
46
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
const normalizeStates = states => {
|
|
48
|
+
if (!states) return [];
|
|
49
|
+
if (Array.isArray(states)) return states.map((label, index) => ({ key: String(index), label: String(label) }));
|
|
50
|
+
if (typeof states === 'string') {
|
|
51
|
+
return states
|
|
52
|
+
.split(';')
|
|
53
|
+
.map(part => part.trim())
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.map(part => {
|
|
56
|
+
const index = part.indexOf(':');
|
|
57
|
+
return index === -1
|
|
58
|
+
? { key: part, label: part }
|
|
59
|
+
: { key: part.slice(0, index).trim(), label: part.slice(index + 1).trim() };
|
|
60
|
+
});
|
|
56
61
|
}
|
|
57
|
-
|
|
62
|
+
if (typeof states === 'object') {
|
|
63
|
+
return Object.entries(states).map(([key, label]) => ({ key, label: flattenTranslatedText(label) || key }));
|
|
64
|
+
}
|
|
65
|
+
return [];
|
|
58
66
|
};
|
|
59
67
|
|
|
60
68
|
const getExplicitExpertOnly = obj => {
|
|
61
|
-
const
|
|
69
|
+
const common = obj && obj.common ? obj.common : {};
|
|
70
|
+
const native = obj && obj.native ? obj.native : {};
|
|
71
|
+
const custom = common.custom || {};
|
|
72
|
+
const candidates = [
|
|
73
|
+
common.manualWriteExpertOnly,
|
|
74
|
+
common.expertOnly,
|
|
75
|
+
native.manualWriteExpertOnly,
|
|
76
|
+
native.nexowatt && native.nexowatt.manualWriteExpertOnly,
|
|
77
|
+
custom.nexowatt && custom.nexowatt.manualWriteExpertOnly,
|
|
78
|
+
custom['nexowatt.eos'] && custom['nexowatt.eos'].manualWriteExpertOnly,
|
|
79
|
+
custom['eos-admin'] && custom['eos-admin'].manualWriteExpertOnly,
|
|
80
|
+
];
|
|
62
81
|
for (const value of candidates) {
|
|
63
82
|
if (typeof value === 'boolean') return value;
|
|
64
83
|
}
|
|
65
84
|
return undefined;
|
|
66
85
|
};
|
|
67
86
|
|
|
68
|
-
const
|
|
87
|
+
const getExpertOnlyReason = (id, obj) => {
|
|
69
88
|
const explicit = getExplicitExpertOnly(obj);
|
|
70
|
-
if (explicit
|
|
89
|
+
if (explicit === true) return 'explicit';
|
|
90
|
+
if (explicit === false) return '';
|
|
71
91
|
|
|
72
|
-
const common =
|
|
92
|
+
const common = obj && obj.common ? obj.common : {};
|
|
73
93
|
const role = normalizeText(common.role);
|
|
74
94
|
const text = normalizeText([
|
|
75
95
|
id,
|
|
@@ -80,10 +100,10 @@
|
|
|
80
100
|
].join(' '));
|
|
81
101
|
const compact = text.replace(/[^a-z0-9]+/g, '');
|
|
82
102
|
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
if (/^(?:level\.)?(?:power|current)(?:\.|$)/.test(role)) return
|
|
86
|
-
if (/^(?:value\.)?(?:power|current)\.setpoint(?:\.|$)/.test(role)) return
|
|
103
|
+
// High-confidence safety classes only. Normal setpoints such as room
|
|
104
|
+
// temperatures remain available outside expert mode.
|
|
105
|
+
if (/^(?:level\.)?(?:power|current)(?:\.|$)/.test(role)) return 'power-or-current-command';
|
|
106
|
+
if (/^(?:value\.)?(?:power|current)\.(?:setpoint|limit|target|command)(?:\.|$)/.test(role)) return 'power-or-current-command';
|
|
87
107
|
|
|
88
108
|
const exactRiskTokens = [
|
|
89
109
|
'hardreset', 'softreset', 'factoryreset', 'factorydefault', 'reboot', 'restartdevice', 'shutdown',
|
|
@@ -95,332 +115,237 @@
|
|
|
95
115
|
'ladeleistung', 'entladeleistung', 'ladestrom', 'stromlimit', 'leistungsgrenze',
|
|
96
116
|
'leistungssollwert', 'einspeiselimit', 'ruecksetzen', 'neustart',
|
|
97
117
|
];
|
|
98
|
-
if (exactRiskTokens.some(token => compact.includes(token))) return
|
|
118
|
+
if (exactRiskTokens.some(token => compact.includes(token))) return 'safety-command';
|
|
99
119
|
|
|
100
120
|
const powerCommand = /(?:charge|charging|discharge|export|feedin|grid|netz|pv|lade|entlade|einspeise)[\s._/-]{0,6}(?:power|current|leistung|strom).{0,28}(?:limit|setpoint|target|command|override|control|grenze|soll|vorgabe)/;
|
|
101
121
|
const inversePowerCommand = /(?:limit|setpoint|target|command|override|control|grenze|soll|vorgabe).{0,28}(?:charge|charging|discharge|export|feedin|grid|netz|pv|lade|entlade|einspeise|power|current|leistung|strom)/;
|
|
102
|
-
|
|
122
|
+
if (powerCommand.test(text) || inversePowerCommand.test(text)) return 'power-or-current-command';
|
|
123
|
+
return '';
|
|
103
124
|
};
|
|
104
125
|
|
|
126
|
+
const isExpertOnlyState = (id, obj) => Boolean(getExpertOnlyReason(id, obj));
|
|
127
|
+
|
|
105
128
|
const toBoolean = value => {
|
|
106
129
|
if (value === true || value === 1) return true;
|
|
107
130
|
if (value === false || value === 0 || value == null) return false;
|
|
108
|
-
const normalized = normalizeText(value);
|
|
109
|
-
if (['true', '1', 'on', 'yes', 'ja', 'enabled', '
|
|
110
|
-
if (['false', '0', 'off', 'no', 'nein', 'disabled', '
|
|
131
|
+
const normalized = normalizeText(value).trim();
|
|
132
|
+
if (['true', '1', 'on', 'yes', 'ja', 'enabled', 'active', 'open', 'start', 'ein'].includes(normalized)) return true;
|
|
133
|
+
if (['false', '0', 'off', 'no', 'nein', 'disabled', 'inactive', 'closed', 'stop', 'aus', '', 'null', '(null)'].includes(normalized)) return false;
|
|
111
134
|
return Boolean(value);
|
|
112
135
|
};
|
|
113
136
|
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (type === 'object' || type === 'array') return type;
|
|
119
|
-
if (type === 'json') return 'json';
|
|
120
|
-
if (type === 'number' || type === 'boolean' || type === 'string' || type === 'mixed' || type === 'file') return type;
|
|
121
|
-
return '';
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const normalizeCommonType = value => {
|
|
125
|
-
if (Array.isArray(value)) {
|
|
126
|
-
const types = [...new Set(value.map(normalizeSingleCommonType).filter(Boolean))];
|
|
127
|
-
return types.length === 1 ? types[0] : types.length ? 'mixed' : '';
|
|
137
|
+
const parseNumber = value => {
|
|
138
|
+
if (typeof value === 'number') {
|
|
139
|
+
if (!Number.isFinite(value)) throw new Error('Ungültiger Zahlenwert');
|
|
140
|
+
return value;
|
|
128
141
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (value !== null && typeof value === 'object') return 'object';
|
|
135
|
-
if (typeof value === 'boolean') return 'boolean';
|
|
136
|
-
if (typeof value === 'number') return 'number';
|
|
137
|
-
if (typeof value === 'string') return 'string';
|
|
138
|
-
return '';
|
|
142
|
+
const normalized = String(value == null ? '' : value).trim().replace(',', '.');
|
|
143
|
+
if (!normalized) throw new Error('Zahlenwert fehlt');
|
|
144
|
+
const parsed = Number(normalized);
|
|
145
|
+
if (!Number.isFinite(parsed)) throw new Error(`Ungültiger Zahlenwert: ${value}`);
|
|
146
|
+
return parsed;
|
|
139
147
|
};
|
|
140
148
|
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const result = {};
|
|
150
|
-
states.split(/[;,]/).map(part => part.trim()).filter(Boolean).forEach(part => {
|
|
151
|
-
const separator = part.includes(':') ? ':' : part.includes('=') ? '=' : '';
|
|
152
|
-
if (!separator) return;
|
|
153
|
-
const index = part.indexOf(separator);
|
|
154
|
-
const key = part.slice(0, index).trim();
|
|
155
|
-
const label = part.slice(index + 1).trim();
|
|
156
|
-
if (key) result[key] = label || key;
|
|
157
|
-
});
|
|
158
|
-
return Object.keys(result).length ? result : null;
|
|
159
|
-
}
|
|
160
|
-
if (typeof states === 'object') {
|
|
161
|
-
const result = {};
|
|
162
|
-
Object.keys(states).forEach(key => { result[String(key)] = flattenTranslatedText(states[key]); });
|
|
163
|
-
return result;
|
|
149
|
+
const parseJson = value => {
|
|
150
|
+
if (value && typeof value === 'object') return value;
|
|
151
|
+
const text = String(value == null ? '' : value).trim();
|
|
152
|
+
if (!text) throw new Error('JSON-Wert fehlt');
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(text);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
throw new Error(`Ungültiges JSON: ${error && error.message ? error.message : error}`);
|
|
164
157
|
}
|
|
165
|
-
return null;
|
|
166
158
|
};
|
|
167
159
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (declared === 'object' || declared === 'array' || declared === 'json') return 'json';
|
|
176
|
-
if (declared === 'number' || declared === 'boolean' || declared === 'string') return declared;
|
|
177
|
-
|
|
178
|
-
const candidate = currentValue !== undefined && currentValue !== null ? currentValue : common.def;
|
|
179
|
-
const inferred = inferPrimitiveType(candidate);
|
|
180
|
-
if (inferred === 'object' || inferred === 'array') return 'json';
|
|
181
|
-
if (inferred) return inferred;
|
|
182
|
-
return 'string';
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const stringifyJsonValue = value => {
|
|
186
|
-
if (typeof value === 'string') {
|
|
160
|
+
const castStateKey = (key, type) => {
|
|
161
|
+
if (type === 'number') return parseNumber(key);
|
|
162
|
+
if (type === 'boolean') return toBoolean(key);
|
|
163
|
+
if (type === 'mixed') {
|
|
164
|
+
const text = String(key).trim();
|
|
165
|
+
if (/^(true|false|on|off|yes|no|ja|nein|1|0)$/i.test(text)) return toBoolean(text);
|
|
166
|
+
if (/^[+-]?(?:\d+\.?\d*|\.\d+)$/.test(text.replace(',', '.'))) return parseNumber(text);
|
|
187
167
|
try {
|
|
188
|
-
return JSON.
|
|
168
|
+
if (/^[\[{]/.test(text)) return JSON.parse(text);
|
|
189
169
|
} catch {
|
|
190
|
-
|
|
170
|
+
// keep as text
|
|
191
171
|
}
|
|
172
|
+
return text;
|
|
192
173
|
}
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
return JSON.stringify(value, null, 2);
|
|
196
|
-
} catch {
|
|
197
|
-
return String(value);
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
const isNumberInRange = (value, common) => {
|
|
202
|
-
if (value === '') return false;
|
|
203
|
-
const number = Number(typeof value === 'string' ? value.trim().replace(',', '.') : value);
|
|
204
|
-
if (!Number.isFinite(number)) return false;
|
|
205
|
-
if (typeof common.min === 'number' && number < common.min) return false;
|
|
206
|
-
if (typeof common.max === 'number' && number > common.max) return false;
|
|
207
|
-
return true;
|
|
174
|
+
return String(key);
|
|
208
175
|
};
|
|
209
176
|
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const hasDefault = common.def !== undefined && common.def !== null;
|
|
215
|
-
let value = hasCurrentValue ? currentValue : hasDefault ? common.def : undefined;
|
|
216
|
-
let valid = true;
|
|
217
|
-
|
|
218
|
-
if (type === 'number') {
|
|
219
|
-
if (value === undefined || value === '') {
|
|
220
|
-
value = '';
|
|
221
|
-
valid = false;
|
|
222
|
-
} else {
|
|
223
|
-
const normalized = typeof value === 'string' ? value.trim().replace(',', '.') : value;
|
|
224
|
-
const parsed = Number(normalized);
|
|
225
|
-
if (Number.isFinite(parsed)) {
|
|
226
|
-
value = parsed;
|
|
227
|
-
valid = isNumberInRange(parsed, common);
|
|
228
|
-
} else {
|
|
229
|
-
value = String(value);
|
|
230
|
-
valid = false;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
} else if (type === 'boolean') {
|
|
234
|
-
value = toBoolean(value);
|
|
235
|
-
} else if (type === 'json') {
|
|
236
|
-
const declared = normalizeCommonType(common.type);
|
|
237
|
-
if (value === undefined) value = declared === 'array' ? '[]' : declared === 'object' ? '{}' : '';
|
|
238
|
-
value = stringifyJsonValue(value);
|
|
177
|
+
const sameValue = (left, right) => {
|
|
178
|
+
if (Object.is(left, right)) return true;
|
|
179
|
+
if (left == null || right == null) return left == null && right == null;
|
|
180
|
+
if (typeof left === 'object' || typeof right === 'object') {
|
|
239
181
|
try {
|
|
240
|
-
JSON.
|
|
241
|
-
valid = true;
|
|
182
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
242
183
|
} catch {
|
|
243
|
-
|
|
184
|
+
return false;
|
|
244
185
|
}
|
|
245
|
-
} else if (type === 'states') {
|
|
246
|
-
value = value === undefined ? '' : value;
|
|
247
|
-
const normalizedStates = normalizeStates(states || common.states) || {};
|
|
248
|
-
valid = value !== '' && Object.prototype.hasOwnProperty.call(normalizedStates, String(value));
|
|
249
|
-
} else {
|
|
250
|
-
value = value === undefined ? '' : String(value);
|
|
251
186
|
}
|
|
252
|
-
|
|
253
|
-
return { type, value, propsValue: value, valid };
|
|
187
|
+
return String(left) === String(right);
|
|
254
188
|
};
|
|
255
189
|
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
if (typeof common.min === 'number' && number < common.min) throw new Error(`Wert liegt unter dem Minimum ${common.min}.`);
|
|
263
|
-
if (typeof common.max === 'number' && number > common.max) throw new Error(`Wert liegt über dem Maximum ${common.max}.`);
|
|
264
|
-
return number;
|
|
190
|
+
const optionScore = (option, mode) => {
|
|
191
|
+
const text = normalizeText(`${option.key} ${option.label}`).replace(/[^a-z0-9]+/g, ' ');
|
|
192
|
+
const offWords = ['false', '0', 'off', 'no', 'nein', 'disabled', 'inactive', 'closed', 'stop', 'aus'];
|
|
193
|
+
const onWords = ['true', '1', 'on', 'yes', 'ja', 'enabled', 'active', 'open', 'start', 'ein'];
|
|
194
|
+
const words = mode === 'on' ? onWords : offWords;
|
|
195
|
+
return words.reduce((score, word) => score + (new RegExp(`(?:^|\\s)${word}(?:\\s|$)`).test(text) ? 10 : 0), 0);
|
|
265
196
|
};
|
|
266
197
|
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
if (
|
|
272
|
-
if (
|
|
273
|
-
|
|
198
|
+
const getBinaryOptions = obj => {
|
|
199
|
+
const common = obj && obj.common ? obj.common : {};
|
|
200
|
+
const type = normalizeCommonType(obj);
|
|
201
|
+
const entries = normalizeStates(common.states);
|
|
202
|
+
if (entries.length > 2) return null;
|
|
203
|
+
if (entries.length === 2) {
|
|
204
|
+
const typed = entries.map(entry => ({ ...entry, value: castStateKey(entry.key, type) }));
|
|
205
|
+
const firstOff = optionScore(typed[0], 'off');
|
|
206
|
+
const firstOn = optionScore(typed[0], 'on');
|
|
207
|
+
const secondOff = optionScore(typed[1], 'off');
|
|
208
|
+
const secondOn = optionScore(typed[1], 'on');
|
|
209
|
+
if (firstOn > firstOff && secondOff >= secondOn) return { off: typed[1].value, on: typed[0].value };
|
|
210
|
+
if (secondOn > secondOff && firstOff >= firstOn) return { off: typed[0].value, on: typed[1].value };
|
|
211
|
+
return { off: typed[0].value, on: typed[1].value };
|
|
212
|
+
}
|
|
213
|
+
if (type === 'boolean') return { off: false, on: true };
|
|
214
|
+
if (type === 'number') {
|
|
215
|
+
const off = typeof common.min === 'number' ? common.min : 0;
|
|
216
|
+
let on = typeof common.max === 'number' ? common.max : 1;
|
|
217
|
+
if (sameValue(off, on)) on = off === 0 ? 1 : 0;
|
|
218
|
+
return { off, on };
|
|
274
219
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return
|
|
220
|
+
if (type === 'string') return { off: 'false', on: 'true' };
|
|
221
|
+
if (type === 'mixed') return { off: false, on: true };
|
|
222
|
+
return null;
|
|
278
223
|
};
|
|
279
224
|
|
|
280
|
-
const
|
|
281
|
-
const common =
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (declared === 'number') return parseNumber(value, common);
|
|
286
|
-
if (declared === 'boolean') return value === null || value === 'null' ? null : toBoolean(value);
|
|
287
|
-
if (declared === 'string') return value === null ? '' : String(value);
|
|
288
|
-
if (declared === 'object' || declared === 'array' || declared === 'json') return parseJsonStateValue(value, declared);
|
|
289
|
-
|
|
290
|
-
// mixed or missing type: preserve the current/default primitive type where possible.
|
|
291
|
-
const candidate = currentValue !== undefined && currentValue !== null ? currentValue : common.def;
|
|
292
|
-
const inferred = inferPrimitiveType(candidate);
|
|
293
|
-
if (inferred === 'number') return parseNumber(value, common);
|
|
294
|
-
if (inferred === 'boolean') return value === null || value === 'null' ? null : toBoolean(value);
|
|
295
|
-
if (inferred === 'object' || inferred === 'array') return parseJsonStateValue(value, inferred);
|
|
296
|
-
if (typeof value === 'string' && value.trim() === 'null') return null;
|
|
297
|
-
return value == null ? '' : value;
|
|
225
|
+
const isButtonState = (obj, item) => {
|
|
226
|
+
const common = obj && obj.common ? obj.common : {};
|
|
227
|
+
const role = normalizeText(common.role);
|
|
228
|
+
const data = item && item.data ? item.data : {};
|
|
229
|
+
return Boolean(data.button) || role === 'button' || role.startsWith('button.') || role === 'action' || role.startsWith('action.');
|
|
298
230
|
};
|
|
299
231
|
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
|
|
232
|
+
const isSwitchState = (obj, item) => {
|
|
233
|
+
const common = obj && obj.common ? obj.common : {};
|
|
234
|
+
const role = normalizeText(common.role);
|
|
235
|
+
const data = item && item.data ? item.data : {};
|
|
236
|
+
const requested = Boolean(data.switch) || role === 'switch' || role.startsWith('switch.') || normalizeCommonType(obj) === 'boolean';
|
|
237
|
+
return requested && Boolean(getBinaryOptions(obj));
|
|
303
238
|
};
|
|
304
239
|
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (
|
|
310
|
-
if (
|
|
311
|
-
if (
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
return coerceByDeclaredType(obj, rawValue, currentValue);
|
|
240
|
+
const getWriteBehavior = (id, obj, item, expertMode) => {
|
|
241
|
+
if (!obj || obj.type !== 'state') return 'readonly';
|
|
242
|
+
const common = obj.common || {};
|
|
243
|
+
if (common.write === false) return 'readonly';
|
|
244
|
+
if (isExpertOnlyState(id, obj) && !expertMode) return 'expert-only';
|
|
245
|
+
if (normalizeCommonType(obj) === 'file') return 'file';
|
|
246
|
+
if (isButtonState(obj, item) && (common.def !== undefined || !['array', 'object'].includes(normalizeCommonType(obj)))) return 'button';
|
|
247
|
+
if (isSwitchState(obj, item)) return 'switch';
|
|
248
|
+
return 'dialog';
|
|
315
249
|
};
|
|
316
250
|
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
251
|
+
const getDirectWriteValue = (id, obj, item, currentValue, behavior) => {
|
|
252
|
+
const common = obj && obj.common ? obj.common : {};
|
|
253
|
+
const type = normalizeCommonType(obj, currentValue);
|
|
254
|
+
const mode = behavior || getWriteBehavior(id, obj, item, true);
|
|
255
|
+
if (mode === 'button') {
|
|
256
|
+
if (common.def !== undefined) return common.def;
|
|
257
|
+
const options = getBinaryOptions(obj);
|
|
258
|
+
if (options) return options.on;
|
|
259
|
+
if (type === 'boolean') return true;
|
|
260
|
+
if (type === 'number') return 1;
|
|
261
|
+
if (type === 'string') return 'true';
|
|
262
|
+
if (type === 'mixed') return true;
|
|
263
|
+
throw new Error(`Button-Datenpunkt ${id} benötigt einen expliziten Vorgabewert`);
|
|
264
|
+
}
|
|
265
|
+
if (mode === 'switch') {
|
|
266
|
+
const options = getBinaryOptions(obj);
|
|
267
|
+
if (!options) throw new Error(`Datenpunkt ${id} ist kein binärer Schalter`);
|
|
268
|
+
return sameValue(currentValue, options.on) ? options.off : options.on;
|
|
269
|
+
}
|
|
270
|
+
throw new Error(`Direktes Schreiben ist für ${mode} nicht vorgesehen`);
|
|
320
271
|
};
|
|
321
272
|
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const tokens = desired ? booleanTokens.true : booleanTokens.false;
|
|
331
|
-
for (const [key, label] of Object.entries(normalized)) {
|
|
332
|
-
if (tokens.includes(normalizeText(key)) || tokens.includes(normalizeText(label))) return key;
|
|
333
|
-
}
|
|
334
|
-
return undefined;
|
|
273
|
+
const inferEditorType = (obj, currentValue) => {
|
|
274
|
+
const common = obj && obj.common ? obj.common : {};
|
|
275
|
+
if (normalizeStates(common.states).length) return 'states';
|
|
276
|
+
const type = normalizeCommonType(obj, currentValue);
|
|
277
|
+
if (type === 'array' || type === 'object' || type === 'json') return 'json';
|
|
278
|
+
if (type === 'mixed') return 'json';
|
|
279
|
+
if (type === 'number' || type === 'boolean' || type === 'string') return type;
|
|
280
|
+
return 'string';
|
|
335
281
|
};
|
|
336
282
|
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
let
|
|
341
|
-
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
283
|
+
const prepareEditor = (obj, currentValue) => {
|
|
284
|
+
const editorType = inferEditorType(obj, currentValue);
|
|
285
|
+
const originalType = normalizeCommonType(obj, currentValue);
|
|
286
|
+
let value = currentValue;
|
|
287
|
+
if (editorType === 'json') {
|
|
288
|
+
if (typeof value === 'string') {
|
|
289
|
+
try {
|
|
290
|
+
value = JSON.stringify(JSON.parse(value), null, 2);
|
|
291
|
+
} catch {
|
|
292
|
+
value = originalType === 'mixed' ? JSON.stringify(value) : value;
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
value = JSON.stringify(value == null ? null : value, null, 2);
|
|
345
296
|
}
|
|
297
|
+
} else if (editorType === 'number') {
|
|
298
|
+
value = value == null ? '' : value;
|
|
299
|
+
} else if (editorType === 'boolean') {
|
|
300
|
+
value = toBoolean(value);
|
|
301
|
+
} else if (editorType === 'string') {
|
|
302
|
+
value = value == null ? '' : String(value);
|
|
346
303
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
304
|
+
return {
|
|
305
|
+
editorType,
|
|
306
|
+
value,
|
|
307
|
+
originalType,
|
|
308
|
+
};
|
|
352
309
|
};
|
|
353
310
|
|
|
354
|
-
const
|
|
355
|
-
const common =
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
311
|
+
const parseEditorValue = (obj, editorType, rawValue) => {
|
|
312
|
+
const common = obj && obj.common ? obj.common : {};
|
|
313
|
+
const originalType = normalizeCommonType(obj, rawValue);
|
|
314
|
+
let value = rawValue && typeof rawValue === 'object' && Object.prototype.hasOwnProperty.call(rawValue, 'value')
|
|
315
|
+
? rawValue.value
|
|
316
|
+
: rawValue;
|
|
360
317
|
|
|
361
|
-
|
|
362
|
-
if (stateKey !== undefined) return coerceStateKey(obj, stateKey, currentValue);
|
|
318
|
+
if (editorType !== 'string' && value === 'null') return null;
|
|
363
319
|
|
|
364
|
-
if (
|
|
365
|
-
|
|
320
|
+
if (editorType === 'states') {
|
|
321
|
+
value = castStateKey(value, originalType);
|
|
322
|
+
} else if (editorType === 'number') {
|
|
323
|
+
value = parseNumber(value);
|
|
324
|
+
} else if (editorType === 'boolean') {
|
|
325
|
+
value = toBoolean(value);
|
|
326
|
+
} else if (editorType === 'json') {
|
|
327
|
+
value = parseJson(value);
|
|
328
|
+
} else {
|
|
329
|
+
value = value == null ? '' : String(value);
|
|
366
330
|
}
|
|
367
331
|
|
|
368
|
-
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
return desired ? 1 : 0;
|
|
375
|
-
}
|
|
376
|
-
if (declared === 'string') return getStringToggleValue(currentValue, desired);
|
|
377
|
-
if (declared === 'object' || declared === 'array' || declared === 'json') {
|
|
378
|
-
throw new Error('Objekt-/Array-Befehle müssen über den Wertdialog gesetzt werden.');
|
|
332
|
+
if (originalType === 'number' && typeof value !== 'number') value = parseNumber(value);
|
|
333
|
+
if (originalType === 'boolean' && typeof value !== 'boolean') value = toBoolean(value);
|
|
334
|
+
if (originalType === 'string' && typeof value !== 'string') value = String(value);
|
|
335
|
+
if (originalType === 'array') {
|
|
336
|
+
if (typeof value === 'string') value = parseJson(value);
|
|
337
|
+
if (!Array.isArray(value)) throw new Error('Der Datenpunkt erwartet ein JSON-Array');
|
|
379
338
|
}
|
|
380
|
-
if (
|
|
381
|
-
if (typeof
|
|
382
|
-
if (typeof
|
|
339
|
+
if (originalType === 'object') {
|
|
340
|
+
if (typeof value === 'string') value = parseJson(value);
|
|
341
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Der Datenpunkt erwartet ein JSON-Objekt');
|
|
383
342
|
}
|
|
384
|
-
return desired;
|
|
385
|
-
};
|
|
386
343
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (normalizeCommonType(common.type) === 'file') return 'file';
|
|
393
|
-
|
|
394
|
-
const role = normalizeText(common.role);
|
|
395
|
-
const data = item && item.data ? item.data : {};
|
|
396
|
-
const declared = normalizeCommonType(common.type);
|
|
397
|
-
const explicitControl = normalizeText(
|
|
398
|
-
getExplicitValue(obj, 'manualWriteControl') !== undefined
|
|
399
|
-
? getExplicitValue(obj, 'manualWriteControl')
|
|
400
|
-
: getExplicitValue(obj, 'manualControl'),
|
|
401
|
-
);
|
|
402
|
-
if (['button', 'trigger', 'action'].includes(explicitControl) && !['object', 'array', 'json'].includes(declared)) return 'button';
|
|
403
|
-
if (['dialog', 'value', 'editor'].includes(explicitControl)) return 'dialog';
|
|
404
|
-
if (['switch', 'toggle'].includes(explicitControl)) return 'switch';
|
|
405
|
-
|
|
406
|
-
const normalizedStates = normalizeStates(common.states);
|
|
407
|
-
const stateCount = normalizedStates ? Object.keys(normalizedStates).length : 0;
|
|
408
|
-
const isButton = Boolean(data.button) || role === 'button' || role.startsWith('button.') || role === 'action' || role.startsWith('action.') || role === 'trigger' || role.startsWith('trigger.');
|
|
409
|
-
if (isButton && declared !== 'object' && declared !== 'array' && declared !== 'json') return 'button';
|
|
410
|
-
if (declared === 'boolean') return 'switch';
|
|
411
|
-
// A numeric/string mode with more than two states is a selector, not a
|
|
412
|
-
// toggle. Treating it as a switch would make only two of its values reachable.
|
|
413
|
-
if ((Boolean(data.switch) || role === 'switch' || role.startsWith('switch.')) && stateCount <= 2) return 'switch';
|
|
414
|
-
return 'dialog';
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
const getWriteLabel = (id, obj, behavior, expertMode, hasState) => {
|
|
418
|
-
if (behavior === 'readonly') return 'Nur lesbar';
|
|
419
|
-
if (behavior === 'expert-only') return 'Nur im Expertenmodus beschreibbar';
|
|
420
|
-
if (behavior === 'button') return 'Befehl auslösen';
|
|
421
|
-
if (behavior === 'switch') return 'Schalten';
|
|
422
|
-
if (!hasState) return 'Wert setzen (schreibender Datenpunkt ohne aktuellen Lesewert)';
|
|
423
|
-
return 'Wert bearbeiten';
|
|
344
|
+
if (typeof value === 'number') {
|
|
345
|
+
if (typeof common.min === 'number' && value < common.min) throw new Error(`Wert liegt unter dem Minimum ${common.min}`);
|
|
346
|
+
if (typeof common.max === 'number' && value > common.max) throw new Error(`Wert liegt über dem Maximum ${common.max}`);
|
|
347
|
+
}
|
|
348
|
+
return value;
|
|
424
349
|
};
|
|
425
350
|
|
|
426
351
|
const writeManualState = (socket, id, value) => {
|
|
@@ -429,41 +354,41 @@
|
|
|
429
354
|
}
|
|
430
355
|
const key = String(id || '');
|
|
431
356
|
if (!key) return Promise.reject(new Error('Missing datapoint ID'));
|
|
432
|
-
if (value === undefined) return Promise.reject(new Error(`Undefined is not a valid state value for ${key}`));
|
|
433
|
-
if (value !== null && !['string', 'number', 'boolean'].includes(typeof value)) {
|
|
434
|
-
return Promise.reject(new Error(`Unsupported state value type for ${key}: ${typeof value}`));
|
|
435
|
-
}
|
|
436
357
|
|
|
437
|
-
const previous =
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
358
|
+
const previous = pendingWrites.get(key);
|
|
359
|
+
if (previous && sameValue(previous.value, value)) return previous.promise;
|
|
360
|
+
|
|
361
|
+
const predecessor = previous ? previous.promise.catch(() => undefined) : Promise.resolve();
|
|
362
|
+
let timeoutHandle;
|
|
363
|
+
const operation = predecessor.then(() => {
|
|
364
|
+
const write = Promise.resolve(socket.setState(key, { val: value, ack: false, q: 0 }));
|
|
441
365
|
const timeout = new Promise((_, reject) => {
|
|
442
366
|
timeoutHandle = setTimeout(() => reject(new Error(`Write timeout for ${key}`)), 15000);
|
|
443
367
|
});
|
|
444
|
-
return Promise.race([write, timeout])
|
|
445
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
446
|
-
});
|
|
368
|
+
return Promise.race([write, timeout]);
|
|
447
369
|
});
|
|
448
|
-
const
|
|
449
|
-
if (
|
|
370
|
+
const promise = operation.finally(() => {
|
|
371
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
372
|
+
const active = pendingWrites.get(key);
|
|
373
|
+
if (active && active.promise === promise) pendingWrites.delete(key);
|
|
450
374
|
});
|
|
451
|
-
|
|
452
|
-
return
|
|
375
|
+
pendingWrites.set(key, { value, promise });
|
|
376
|
+
return promise;
|
|
453
377
|
};
|
|
454
378
|
|
|
455
379
|
return {
|
|
380
|
+
normalizeCommonType,
|
|
381
|
+
normalizeStates,
|
|
456
382
|
getExplicitExpertOnly,
|
|
383
|
+
getExpertOnlyReason,
|
|
457
384
|
isExpertOnlyState,
|
|
385
|
+
getBinaryOptions,
|
|
458
386
|
getWriteBehavior,
|
|
459
|
-
getWriteLabel,
|
|
460
|
-
resolveEditorType,
|
|
461
|
-
prepareEditorValue,
|
|
462
|
-
coerceWriteValue,
|
|
463
387
|
getDirectWriteValue,
|
|
388
|
+
inferEditorType,
|
|
389
|
+
prepareEditor,
|
|
390
|
+
parseEditorValue,
|
|
464
391
|
toBoolean,
|
|
465
392
|
writeManualState,
|
|
466
|
-
normalizeStates,
|
|
467
|
-
normalizeCommonType,
|
|
468
393
|
};
|
|
469
394
|
});
|