data-solectrus 0.2.10
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/.github/workflows/npm-publish.yml +32 -0
- package/CHANGELOG.md +138 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/admin/custom/customComponents.js +2691 -0
- package/admin/data-solectrus.png +0 -0
- package/admin/i18n/de/translations.json +94 -0
- package/admin/i18n/en/translations.json +94 -0
- package/admin/jsonConfig.json +59 -0
- package/io-package.json +39 -0
- package/lib/formula.js +249 -0
- package/lib/jsonpath.js +202 -0
- package/lib/services/evaluator.js +266 -0
- package/lib/services/itemIds.js +56 -0
- package/lib/services/itemManager.js +217 -0
- package/lib/services/snapshot.js +48 -0
- package/lib/services/sourceDiscovery.js +67 -0
- package/lib/services/stateRegistry.js +462 -0
- package/lib/services/subscriptions.js +83 -0
- package/lib/services/tickRunner.js +282 -0
- package/main.js +253 -0
- package/package.json +36 -0
package/lib/jsonpath.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal JSONPath subset evaluator for typical IoT payloads.
|
|
5
|
+
* Supported examples:
|
|
6
|
+
* - $.apower
|
|
7
|
+
* - $.aenergy.by_minute[2]
|
|
8
|
+
* - $['temperature']['tC']
|
|
9
|
+
*
|
|
10
|
+
* Not supported: filters, wildcards, unions, recursive descent, functions.
|
|
11
|
+
*/
|
|
12
|
+
function applyJsonPath(obj, path) {
|
|
13
|
+
if (!path) return undefined;
|
|
14
|
+
let p = String(path).trim();
|
|
15
|
+
if (!p) return undefined;
|
|
16
|
+
|
|
17
|
+
// Accept both "$.x" and ".x" as a convenience.
|
|
18
|
+
if (p.startsWith('.')) {
|
|
19
|
+
p = `$${p}`;
|
|
20
|
+
}
|
|
21
|
+
if (!p.startsWith('$')) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let cur = obj;
|
|
26
|
+
let i = 1; // skip '$'
|
|
27
|
+
const len = p.length;
|
|
28
|
+
const isDangerousKey = k => k === '__proto__' || k === 'prototype' || k === 'constructor';
|
|
29
|
+
while (i < len) {
|
|
30
|
+
const ch = p[i];
|
|
31
|
+
if (ch === '.') {
|
|
32
|
+
i++;
|
|
33
|
+
let start = i;
|
|
34
|
+
while (i < len && /[A-Za-z0-9_]/.test(p[i])) i++;
|
|
35
|
+
const key = p.slice(start, i);
|
|
36
|
+
if (!key) return undefined;
|
|
37
|
+
if (isDangerousKey(key)) return undefined;
|
|
38
|
+
if (cur === null || cur === undefined) return undefined;
|
|
39
|
+
cur = cur[key];
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (ch === '[') {
|
|
43
|
+
i++;
|
|
44
|
+
while (i < len && /\s/.test(p[i])) i++;
|
|
45
|
+
if (i >= len) return undefined;
|
|
46
|
+
const quote = p[i] === '"' || p[i] === "'" ? p[i] : null;
|
|
47
|
+
if (quote) {
|
|
48
|
+
i++;
|
|
49
|
+
let str = '';
|
|
50
|
+
while (i < len) {
|
|
51
|
+
const c = p[i];
|
|
52
|
+
if (c === '\\') {
|
|
53
|
+
if (i + 1 < len) {
|
|
54
|
+
str += p[i + 1];
|
|
55
|
+
i += 2;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (c === quote) {
|
|
61
|
+
i++;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
str += c;
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
while (i < len && /\s/.test(p[i])) i++;
|
|
68
|
+
if (p[i] !== ']') return undefined;
|
|
69
|
+
i++;
|
|
70
|
+
if (isDangerousKey(str)) return undefined;
|
|
71
|
+
if (cur === null || cur === undefined) return undefined;
|
|
72
|
+
cur = cur[str];
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// array index
|
|
77
|
+
let start = i;
|
|
78
|
+
while (i < len && /[0-9]/.test(p[i])) i++;
|
|
79
|
+
const numStr = p.slice(start, i);
|
|
80
|
+
while (i < len && /\s/.test(p[i])) i++;
|
|
81
|
+
if (p[i] !== ']') return undefined;
|
|
82
|
+
i++;
|
|
83
|
+
const idx = Number(numStr);
|
|
84
|
+
if (!Number.isInteger(idx)) return undefined;
|
|
85
|
+
if (!Array.isArray(cur)) return undefined;
|
|
86
|
+
cur = cur[idx];
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Unknown token
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return cur;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getNumericFromJsonPath(rawValue, jsonPath, opts) {
|
|
97
|
+
const options = opts || {};
|
|
98
|
+
const safeNum = typeof options.safeNum === 'function' ? options.safeNum : v => {
|
|
99
|
+
const n = Number(v);
|
|
100
|
+
return Number.isFinite(n) ? n : 0;
|
|
101
|
+
};
|
|
102
|
+
const warnOnce = typeof options.warnOnce === 'function' ? options.warnOnce : () => {};
|
|
103
|
+
const debugOnce = typeof options.debugOnce === 'function' ? options.debugOnce : () => {};
|
|
104
|
+
const warnKeyPrefix = options.warnKeyPrefix || '';
|
|
105
|
+
|
|
106
|
+
const jp = jsonPath !== undefined && jsonPath !== null ? String(jsonPath).trim() : '';
|
|
107
|
+
if (!jp) {
|
|
108
|
+
return safeNum(rawValue);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Be forgiving: if the value is already numeric-ish, just use it.
|
|
112
|
+
// This allows mixed setups where a state sometimes is numeric and sometimes JSON-string.
|
|
113
|
+
if (typeof rawValue === 'number' || typeof rawValue === 'boolean') {
|
|
114
|
+
debugOnce(
|
|
115
|
+
`jsonpath_skipped_numeric|${warnKeyPrefix || ''}`,
|
|
116
|
+
`JSONPath '${jp}' skipped because source value is already ${typeof rawValue} (${warnKeyPrefix || 'no-prefix'})`
|
|
117
|
+
);
|
|
118
|
+
return safeNum(rawValue);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let obj = null;
|
|
122
|
+
if (rawValue && typeof rawValue === 'object') {
|
|
123
|
+
obj = rawValue;
|
|
124
|
+
} else if (typeof rawValue === 'string') {
|
|
125
|
+
const s = rawValue.trim();
|
|
126
|
+
if (!s) {
|
|
127
|
+
warnOnce(`${warnKeyPrefix}|empty`, `JSONPath configured but source value is empty (${jp})`);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
obj = JSON.parse(s);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
warnOnce(
|
|
134
|
+
`${warnKeyPrefix}|parse`,
|
|
135
|
+
`Cannot parse JSON for JSONPath ${jp}: ${e && e.message ? e.message : e}`
|
|
136
|
+
);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
warnOnce(`${warnKeyPrefix}|type`, `JSONPath configured but source value is not JSON (${typeof rawValue}) (${jp})`);
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const extracted = applyJsonPath(obj, jp);
|
|
145
|
+
if (extracted === undefined) {
|
|
146
|
+
warnOnce(`${warnKeyPrefix}|path`, `JSONPath did not match any value: ${jp}`);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
return safeNum(extracted);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getValueFromJsonPath(rawValue, jsonPath, opts) {
|
|
153
|
+
const options = opts || {};
|
|
154
|
+
const warnOnce = typeof options.warnOnce === 'function' ? options.warnOnce : () => {};
|
|
155
|
+
const warnKeyPrefix = options.warnKeyPrefix || '';
|
|
156
|
+
|
|
157
|
+
const jp = jsonPath !== undefined && jsonPath !== null ? String(jsonPath).trim() : '';
|
|
158
|
+
if (!jp) {
|
|
159
|
+
return rawValue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let obj = null;
|
|
163
|
+
if (rawValue && typeof rawValue === 'object') {
|
|
164
|
+
obj = rawValue;
|
|
165
|
+
} else if (typeof rawValue === 'string') {
|
|
166
|
+
const s = rawValue.trim();
|
|
167
|
+
if (!s) {
|
|
168
|
+
warnOnce(`${warnKeyPrefix}|empty`, `JSONPath configured but source value is empty (${jp})`);
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
obj = JSON.parse(s);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
warnOnce(
|
|
175
|
+
`${warnKeyPrefix}|parse`,
|
|
176
|
+
`Cannot parse JSON for JSONPath ${jp}: ${e && e.message ? e.message : e}`
|
|
177
|
+
);
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
warnOnce(`${warnKeyPrefix}|type`, `JSONPath configured but source value is not JSON (${typeof rawValue}) (${jp})`);
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const extracted = applyJsonPath(obj, jp);
|
|
186
|
+
if (extracted === undefined) {
|
|
187
|
+
warnOnce(`${warnKeyPrefix}|path`, `JSONPath did not match any value: ${jp}`);
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
if (extracted === null) return null;
|
|
191
|
+
const t = typeof extracted;
|
|
192
|
+
if (t === 'string' || t === 'number' || t === 'boolean') return extracted;
|
|
193
|
+
if (extracted instanceof Date && typeof extracted.toISOString === 'function') return extracted.toISOString();
|
|
194
|
+
// Keep formulas deterministic: do not expose objects/arrays.
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
applyJsonPath,
|
|
200
|
+
getNumericFromJsonPath,
|
|
201
|
+
getValueFromJsonPath,
|
|
202
|
+
};
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Evaluates item values (source mode + formula mode) and applies output shaping.
|
|
4
|
+
// Intentionally kept pure-ish: it only reads via the provided adapter.
|
|
5
|
+
|
|
6
|
+
const { getItemOutputId, getItemDisplayId } = require('./itemIds');
|
|
7
|
+
|
|
8
|
+
function createFormulaFunctions(adapter) {
|
|
9
|
+
return {
|
|
10
|
+
min: Math.min,
|
|
11
|
+
max: Math.max,
|
|
12
|
+
pow: Math.pow,
|
|
13
|
+
abs: Math.abs,
|
|
14
|
+
round: Math.round,
|
|
15
|
+
floor: Math.floor,
|
|
16
|
+
ceil: Math.ceil,
|
|
17
|
+
// IF(condition, valueIfTrue, valueIfFalse)
|
|
18
|
+
IF: (condition, valueIfTrue, valueIfFalse) => (condition ? valueIfTrue : valueIfFalse),
|
|
19
|
+
if: (condition, valueIfTrue, valueIfFalse) => (condition ? valueIfTrue : valueIfFalse),
|
|
20
|
+
clamp: (value, min, max) => {
|
|
21
|
+
const v = Number(value);
|
|
22
|
+
const lo = Number(min);
|
|
23
|
+
const hi = Number(max);
|
|
24
|
+
if (!Number.isFinite(v)) return 0;
|
|
25
|
+
if (Number.isFinite(lo) && v < lo) return lo;
|
|
26
|
+
if (Number.isFinite(hi) && v > hi) return hi;
|
|
27
|
+
return v;
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// Read a foreign state value by id from cache/snapshot (raw, but restricted to primitives).
|
|
31
|
+
v: id => {
|
|
32
|
+
const key = String(id);
|
|
33
|
+
const val = (adapter.currentSnapshot && typeof adapter.currentSnapshot.get === 'function')
|
|
34
|
+
? adapter.currentSnapshot.get(key)
|
|
35
|
+
: adapter.cache.get(key);
|
|
36
|
+
if (val === null || val === undefined) return val;
|
|
37
|
+
const t = typeof val;
|
|
38
|
+
if (t === 'string' || t === 'number' || t === 'boolean') return val;
|
|
39
|
+
if (val instanceof Date && typeof val.toISOString === 'function') return val.toISOString();
|
|
40
|
+
adapter.debugOnce(
|
|
41
|
+
`v_non_primitive|${key}`,
|
|
42
|
+
`v("${key}") returned non-primitive (${t}); treating as empty string`
|
|
43
|
+
);
|
|
44
|
+
return '';
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Extract a primitive value from a JSON payload using the adapter's minimal JSONPath subset.
|
|
48
|
+
jp: (id, jsonPath) => {
|
|
49
|
+
const key = String(id);
|
|
50
|
+
const raw = (adapter.currentSnapshot && typeof adapter.currentSnapshot.get === 'function')
|
|
51
|
+
? adapter.currentSnapshot.get(key)
|
|
52
|
+
: adapter.cache.get(key);
|
|
53
|
+
const jp = jsonPath !== undefined && jsonPath !== null ? String(jsonPath).trim() : '';
|
|
54
|
+
if (!jp) return undefined;
|
|
55
|
+
|
|
56
|
+
let obj = null;
|
|
57
|
+
if (raw && typeof raw === 'object') {
|
|
58
|
+
obj = raw;
|
|
59
|
+
} else if (typeof raw === 'string') {
|
|
60
|
+
const s = raw.trim();
|
|
61
|
+
if (!s) return undefined;
|
|
62
|
+
try {
|
|
63
|
+
obj = JSON.parse(s);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
adapter.debugOnce(
|
|
66
|
+
`jp_parse_failed|${key}|${jp}`,
|
|
67
|
+
`jp("${key}", "${jp}") cannot parse JSON: ${e && e.message ? e.message : e}`
|
|
68
|
+
);
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const extracted = adapter.applyJsonPath(obj, jp);
|
|
76
|
+
if (extracted === undefined || extracted === null) return extracted;
|
|
77
|
+
const t = typeof extracted;
|
|
78
|
+
if (t === 'string' || t === 'number' || t === 'boolean') return extracted;
|
|
79
|
+
if (extracted instanceof Date && typeof extracted.toISOString === 'function') return extracted.toISOString();
|
|
80
|
+
return undefined;
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Read a foreign state value by id from cache (sync, safe numeric).
|
|
84
|
+
s: id => {
|
|
85
|
+
const key = String(id);
|
|
86
|
+
if (adapter.currentSnapshot && typeof adapter.currentSnapshot.get === 'function') {
|
|
87
|
+
return adapter.safeNum(adapter.currentSnapshot.get(key));
|
|
88
|
+
}
|
|
89
|
+
return adapter.safeNum(adapter.cache.get(key));
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isNumericOutputItem(item) {
|
|
95
|
+
const t = item && item.type ? String(item.type) : '';
|
|
96
|
+
// Only these should be forced numeric and get clamping/noNegative rules.
|
|
97
|
+
return t === '' || t === 'number';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getZeroValueForItem(item) {
|
|
101
|
+
const t = item && item.type ? String(item.type) : '';
|
|
102
|
+
if (t === 'string') return '';
|
|
103
|
+
if (t === 'boolean') return false;
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function computeItemValue(adapter, item, snapshot) {
|
|
108
|
+
const mode = item.mode || 'formula';
|
|
109
|
+
if (mode === 'source') {
|
|
110
|
+
const id = item.sourceState ? String(item.sourceState) : '';
|
|
111
|
+
const raw = snapshot ? snapshot.get(id) : adapter.cache.get(id);
|
|
112
|
+
|
|
113
|
+
// Source-mode supports both numeric (default) and non-numeric outputs.
|
|
114
|
+
// For numeric outputs we keep the existing behavior (numeric extraction + optional output clamp).
|
|
115
|
+
if (isNumericOutputItem(item)) {
|
|
116
|
+
let v = adapter.getNumericFromJsonPath(
|
|
117
|
+
raw,
|
|
118
|
+
item && item.jsonPath,
|
|
119
|
+
`item|${id}|${item && item.targetId ? item.targetId : ''}`
|
|
120
|
+
);
|
|
121
|
+
// item.noNegative is an OUTPUT rule; for source-mode numeric it's equivalent to clamping the mirrored value.
|
|
122
|
+
if (item && item.noNegative && v < 0) {
|
|
123
|
+
v = 0;
|
|
124
|
+
}
|
|
125
|
+
return v;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const hasJsonPath = item && item.jsonPath !== undefined && item.jsonPath !== null && String(item.jsonPath).trim() !== '';
|
|
129
|
+
const value = hasJsonPath
|
|
130
|
+
? adapter.getValueFromJsonPath(raw, item && item.jsonPath, `item|${id}|${item && item.targetId ? item.targetId : ''}`)
|
|
131
|
+
: raw;
|
|
132
|
+
|
|
133
|
+
// Keep output values safe and predictable: only allow primitives.
|
|
134
|
+
if (value === null || value === undefined) return value;
|
|
135
|
+
const t = typeof value;
|
|
136
|
+
if (t === 'string' || t === 'number' || t === 'boolean') return value;
|
|
137
|
+
if (value instanceof Date && typeof value.toISOString === 'function') return value.toISOString();
|
|
138
|
+
adapter.debugOnce(
|
|
139
|
+
`source_non_primitive|${id}|${item && item.targetId ? item.targetId : ''}`,
|
|
140
|
+
`Source state '${id}' returned non-primitive (${t}); treating as empty string`
|
|
141
|
+
);
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const inputs = Array.isArray(item.inputs) ? item.inputs : [];
|
|
146
|
+
/** @type {Record<string, any>} */
|
|
147
|
+
const vars = Object.create(null);
|
|
148
|
+
|
|
149
|
+
for (const inp of inputs) {
|
|
150
|
+
if (!inp || typeof inp !== 'object') continue;
|
|
151
|
+
const keyRaw = inp.key ? String(inp.key).trim() : '';
|
|
152
|
+
const key = keyRaw.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
153
|
+
if (key === '__proto__' || key === 'prototype' || key === 'constructor') {
|
|
154
|
+
const itemId = getItemDisplayId(item) || ((item && item.name) ? String(item.name) : '');
|
|
155
|
+
adapter.debugOnce(
|
|
156
|
+
`blocked_input_key|${itemId}|${key}`,
|
|
157
|
+
`Blocked dangerous input key '${keyRaw}' (sanitized to '${key}') for item '${itemId}'`
|
|
158
|
+
);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!key) continue;
|
|
162
|
+
|
|
163
|
+
const id = inp.sourceState ? String(inp.sourceState) : '';
|
|
164
|
+
const raw = snapshot ? snapshot.get(id) : adapter.cache.get(id);
|
|
165
|
+
let value;
|
|
166
|
+
|
|
167
|
+
const hasJsonPath = inp && inp.jsonPath !== undefined && inp.jsonPath !== null && String(inp.jsonPath).trim() !== '';
|
|
168
|
+
if (hasJsonPath) {
|
|
169
|
+
const extracted = adapter.getValueFromJsonPath(raw, inp && inp.jsonPath, `input|${id}|${key}`);
|
|
170
|
+
if (typeof extracted === 'string') {
|
|
171
|
+
const n = Number(extracted);
|
|
172
|
+
value = Number.isFinite(n) ? n : extracted;
|
|
173
|
+
} else {
|
|
174
|
+
value = extracted;
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
// Backwards compatible default: inputs without JSONPath are treated as numeric.
|
|
178
|
+
value = adapter.safeNum(raw);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Clamp negative inputs BEFORE formula evaluation (only if numeric).
|
|
182
|
+
// Important: item.noNegative is an OUTPUT rule and must not affect inputs.
|
|
183
|
+
// Use per-input noNegative to clamp only those specific sources.
|
|
184
|
+
if (typeof value === 'number' && (inp && inp.noNegative) && value < 0) {
|
|
185
|
+
value = 0;
|
|
186
|
+
}
|
|
187
|
+
vars[key] = value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const expr = item.formula ? String(item.formula).trim() : '';
|
|
191
|
+
if (!expr) return 0;
|
|
192
|
+
|
|
193
|
+
// Prefer compiled AST if available.
|
|
194
|
+
const targetId = getItemOutputId(item);
|
|
195
|
+
const compiled = targetId ? adapter.compiledItems.get(targetId) : null;
|
|
196
|
+
if (compiled && compiled.ok) {
|
|
197
|
+
if (compiled.constantValue !== undefined) {
|
|
198
|
+
return compiled.constantValue;
|
|
199
|
+
}
|
|
200
|
+
if (compiled.ast) {
|
|
201
|
+
return adapter.evalFormulaAst(compiled.ast, vars);
|
|
202
|
+
}
|
|
203
|
+
return adapter.evalFormula(expr, vars);
|
|
204
|
+
}
|
|
205
|
+
if (compiled && !compiled.ok) {
|
|
206
|
+
throw new Error(compiled.error || 'Formula compile failed');
|
|
207
|
+
}
|
|
208
|
+
return adapter.evalFormula(expr, vars);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function applyResultRules(adapter, item, value) {
|
|
212
|
+
let v = adapter.safeNum(value);
|
|
213
|
+
|
|
214
|
+
const toOptionalNumber = val => {
|
|
215
|
+
if (val === undefined || val === null) return NaN;
|
|
216
|
+
if (typeof val === 'string' && val.trim() === '') return NaN;
|
|
217
|
+
const n = Number(val);
|
|
218
|
+
return Number.isFinite(n) ? n : NaN;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (item && item.noNegative && v < 0) {
|
|
222
|
+
v = 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (item && item.clamp) {
|
|
226
|
+
const min = toOptionalNumber(item.min);
|
|
227
|
+
const max = toOptionalNumber(item.max);
|
|
228
|
+
if (Number.isFinite(min) && v < min) v = min;
|
|
229
|
+
if (Number.isFinite(max) && v > max) v = max;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return v;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function castValueForItemType(adapter, item, value) {
|
|
236
|
+
const t = item && item.type ? String(item.type) : '';
|
|
237
|
+
if (t === 'boolean') {
|
|
238
|
+
if (typeof value === 'boolean') return value;
|
|
239
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value !== 0 : false;
|
|
240
|
+
if (typeof value === 'string') {
|
|
241
|
+
const s = value.trim().toLowerCase();
|
|
242
|
+
if (s === 'true' || s === 'on' || s === 'yes' || s === '1') return true;
|
|
243
|
+
if (s === 'false' || s === 'off' || s === 'no' || s === '0' || s === '') return false;
|
|
244
|
+
const n = Number(value);
|
|
245
|
+
return Number.isFinite(n) ? n !== 0 : false;
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
if (t === 'string') {
|
|
250
|
+
if (value === undefined || value === null) return '';
|
|
251
|
+
return String(value);
|
|
252
|
+
}
|
|
253
|
+
if (t === 'mixed') {
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
return adapter.safeNum(value);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
createFormulaFunctions,
|
|
261
|
+
isNumericOutputItem,
|
|
262
|
+
getZeroValueForItem,
|
|
263
|
+
computeItemValue,
|
|
264
|
+
applyResultRules,
|
|
265
|
+
castValueForItemType,
|
|
266
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Helpers for computing adapter-owned ids derived from the user config.
|
|
4
|
+
// Kept separate so other modules can stay free of adapter-class details.
|
|
5
|
+
|
|
6
|
+
function calcTitle(item) {
|
|
7
|
+
const enabled = !!(item && item.enabled);
|
|
8
|
+
const displayId = getItemDisplayId(item);
|
|
9
|
+
const name = (item && item.name) ? String(item.name) : (displayId || 'Item');
|
|
10
|
+
return `${enabled ? '🟢 ' : '⚪ '}${name}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getItemDisplayId(item) {
|
|
14
|
+
const group = item && item.group ? String(item.group).trim() : '';
|
|
15
|
+
const targetId = item && item.targetId ? String(item.targetId).trim() : '';
|
|
16
|
+
if (group && targetId) return `${group}.${targetId}`;
|
|
17
|
+
return targetId || group;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isValidRelativeId(id) {
|
|
21
|
+
if (!id) return false;
|
|
22
|
+
const raw = String(id).trim();
|
|
23
|
+
if (!raw) return false;
|
|
24
|
+
// No absolute IDs; must be relative within this adapter
|
|
25
|
+
if (raw.includes('..') || raw.startsWith('.') || raw.endsWith('.')) return false;
|
|
26
|
+
// Keep it conservative: segments of [a-zA-Z0-9_-] separated by dots
|
|
27
|
+
return /^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/.test(raw);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getItemTargetId(item) {
|
|
31
|
+
const raw = item && item.targetId ? String(item.targetId).trim() : '';
|
|
32
|
+
if (!raw) return '';
|
|
33
|
+
return isValidRelativeId(raw) ? raw : '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getItemGroupId(item) {
|
|
37
|
+
const raw = item && item.group ? String(item.group).trim() : '';
|
|
38
|
+
if (!raw) return '';
|
|
39
|
+
return isValidRelativeId(raw) ? raw : '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getItemOutputId(item) {
|
|
43
|
+
const group = getItemGroupId(item);
|
|
44
|
+
const targetId = getItemTargetId(item);
|
|
45
|
+
if (!targetId) return '';
|
|
46
|
+
return group ? `${group}.${targetId}` : targetId;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
calcTitle,
|
|
51
|
+
getItemDisplayId,
|
|
52
|
+
isValidRelativeId,
|
|
53
|
+
getItemTargetId,
|
|
54
|
+
getItemGroupId,
|
|
55
|
+
getItemOutputId,
|
|
56
|
+
};
|