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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Manages item preparation: state creation, formula compilation, subscriptions and initial reads.
|
|
4
|
+
// This is the "setup" side; the actual evaluation happens in tickRunner/evaluator.
|
|
5
|
+
|
|
6
|
+
const { parseExpression } = require('../formula');
|
|
7
|
+
const { normalizeFormulaExpression: normalizeFormulaExpressionImpl } = require('../formula');
|
|
8
|
+
const { getItemOutputId, calcTitle } = require('./itemIds');
|
|
9
|
+
const { collectSourceStatesFromItem } = require('./sourceDiscovery');
|
|
10
|
+
const stateRegistry = require('./stateRegistry');
|
|
11
|
+
const subscriptions = require('./subscriptions');
|
|
12
|
+
|
|
13
|
+
function getItemsConfigSignature(_adapter, items) {
|
|
14
|
+
const arr = Array.isArray(items) ? items : [];
|
|
15
|
+
// Only include relevant fields; order is stable by array order.
|
|
16
|
+
const normalized = arr
|
|
17
|
+
.filter(it => it && typeof it === 'object')
|
|
18
|
+
.map(it => ({
|
|
19
|
+
enabled: !!it.enabled,
|
|
20
|
+
mode: it.mode || 'formula',
|
|
21
|
+
group: it.group || '',
|
|
22
|
+
targetId: it.targetId || '',
|
|
23
|
+
name: it.name || '',
|
|
24
|
+
type: it.type || '',
|
|
25
|
+
role: it.role || '',
|
|
26
|
+
unit: it.unit || '',
|
|
27
|
+
noNegative: !!it.noNegative,
|
|
28
|
+
clamp: !!it.clamp,
|
|
29
|
+
min: it.min,
|
|
30
|
+
max: it.max,
|
|
31
|
+
sourceState: it.sourceState || '',
|
|
32
|
+
jsonPath: it.jsonPath || '',
|
|
33
|
+
formula: it.formula || '',
|
|
34
|
+
inputs: Array.isArray(it.inputs)
|
|
35
|
+
? it.inputs
|
|
36
|
+
.filter(inp => inp && typeof inp === 'object')
|
|
37
|
+
.map(inp => ({
|
|
38
|
+
key: inp.key || '',
|
|
39
|
+
sourceState: inp.sourceState || '',
|
|
40
|
+
jsonPath: inp.jsonPath || '',
|
|
41
|
+
noNegative: !!inp.noNegative,
|
|
42
|
+
}))
|
|
43
|
+
: [],
|
|
44
|
+
}));
|
|
45
|
+
try {
|
|
46
|
+
return JSON.stringify(normalized);
|
|
47
|
+
} catch {
|
|
48
|
+
// Fallback: should never happen for plain objects
|
|
49
|
+
return String(Date.now());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function compileItem(adapter, item) {
|
|
54
|
+
const mode = item && item.mode ? String(item.mode) : 'formula';
|
|
55
|
+
const outputId = getItemOutputId(item);
|
|
56
|
+
const sourceIds = new Set(collectSourceStatesFromItem(adapter, item));
|
|
57
|
+
|
|
58
|
+
if (!outputId) {
|
|
59
|
+
return { ok: false, error: 'Missing/invalid targetId', item, outputId: '', mode, sourceIds };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (mode === 'source') {
|
|
63
|
+
return { ok: true, item, outputId, mode, sourceIds };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const exprRaw = item && item.formula !== undefined && item.formula !== null ? String(item.formula).trim() : '';
|
|
67
|
+
if (!exprRaw) {
|
|
68
|
+
// Treat empty formula as constant 0.
|
|
69
|
+
return { ok: true, item, outputId, mode, sourceIds, normalizedExpr: '', ast: null, constantValue: 0 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const normalized = normalizeFormulaExpressionImpl(exprRaw);
|
|
73
|
+
if (normalized && normalized.length > adapter.MAX_FORMULA_LENGTH) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
error: `Formula too long (>${adapter.MAX_FORMULA_LENGTH} chars)`,
|
|
77
|
+
item,
|
|
78
|
+
outputId,
|
|
79
|
+
mode,
|
|
80
|
+
sourceIds,
|
|
81
|
+
normalizedExpr: normalized,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const ast = parseExpression(String(normalized));
|
|
87
|
+
adapter.analyzeAst(ast);
|
|
88
|
+
return { ok: true, item, outputId, mode, sourceIds, normalizedExpr: normalized, ast };
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
error: e && e.message ? e.message : String(e),
|
|
93
|
+
item,
|
|
94
|
+
outputId,
|
|
95
|
+
mode,
|
|
96
|
+
sourceIds,
|
|
97
|
+
normalizedExpr: normalized,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ensureCompiledForCurrentConfig(adapter, items) {
|
|
103
|
+
const sig = getItemsConfigSignature(adapter, items);
|
|
104
|
+
if (sig !== adapter.itemsConfigSignature) {
|
|
105
|
+
return prepareItems(adapter);
|
|
106
|
+
}
|
|
107
|
+
return Promise.resolve();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function ensureItemTitlesInInstanceConfig(adapter) {
|
|
111
|
+
try {
|
|
112
|
+
const objId = `system.adapter.${adapter.namespace}`;
|
|
113
|
+
const obj = await adapter.getForeignObjectAsync(objId);
|
|
114
|
+
if (!obj || !obj.native) return;
|
|
115
|
+
|
|
116
|
+
const items = Array.isArray(obj.native.items) ? obj.native.items : [];
|
|
117
|
+
const itemsEditor = Array.isArray(obj.native.itemsEditor) ? obj.native.itemsEditor : [];
|
|
118
|
+
const active = items.length ? items : itemsEditor;
|
|
119
|
+
if (!Array.isArray(active)) return;
|
|
120
|
+
|
|
121
|
+
let changed = false;
|
|
122
|
+
active.forEach(it => {
|
|
123
|
+
if (!it || typeof it !== 'object') return;
|
|
124
|
+
const expectedTitle = calcTitle(it);
|
|
125
|
+
if (it._title !== expectedTitle) {
|
|
126
|
+
it._title = expectedTitle;
|
|
127
|
+
changed = true;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// If Admin stored items under itemsEditor, migrate them back into items
|
|
132
|
+
// so runtime + fallback table see the same config.
|
|
133
|
+
if (items.length === 0 && itemsEditor.length > 0) {
|
|
134
|
+
obj.native.items = itemsEditor;
|
|
135
|
+
changed = true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (changed) {
|
|
139
|
+
await adapter.setForeignObjectAsync(objId, obj);
|
|
140
|
+
}
|
|
141
|
+
} catch (e) {
|
|
142
|
+
adapter.log.debug(`Cannot migrate item titles: ${e}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function prepareItems(adapter) {
|
|
147
|
+
const items = Array.isArray(adapter.config.items) ? adapter.config.items : [];
|
|
148
|
+
const validItems = items.filter(it => it && typeof it === 'object');
|
|
149
|
+
const enabledItems = validItems.filter(it => !!it.enabled);
|
|
150
|
+
adapter.itemsConfigSignature = getItemsConfigSignature(adapter, items);
|
|
151
|
+
|
|
152
|
+
await adapter.setStateAsync('info.diagnostics.itemsTotal', validItems.length, true);
|
|
153
|
+
await adapter.setStateAsync('info.itemsActive', enabledItems.length, true);
|
|
154
|
+
|
|
155
|
+
for (const item of validItems) {
|
|
156
|
+
await stateRegistry.ensureOutputState(adapter, item);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Compile items once (AST + discovered sourceIds). Errors are stored per item and handled during tick.
|
|
160
|
+
const compiled = new Map();
|
|
161
|
+
for (const item of validItems) {
|
|
162
|
+
const c = compileItem(adapter, item);
|
|
163
|
+
if (c && c.outputId) {
|
|
164
|
+
compiled.set(c.outputId, c);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
adapter.compiledItems = compiled;
|
|
168
|
+
|
|
169
|
+
// Ensure per-item info states and publish compile status.
|
|
170
|
+
for (const c of adapter.compiledItems.values()) {
|
|
171
|
+
try {
|
|
172
|
+
await stateRegistry.ensureItemInfoStatesForCompiled(adapter, c);
|
|
173
|
+
const base = stateRegistry.getItemInfoBaseId(c.outputId);
|
|
174
|
+
await adapter.setStateAsync(`${base}.compiledOk`, !!c.ok, true);
|
|
175
|
+
await adapter.setStateAsync(`${base}.compileError`, c.ok ? '' : String(c.error || 'compile failed'), true);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
adapter.log.debug(`Cannot create/update item info states: ${e && e.message ? e.message : e}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const sourceIds = subscriptions.getDesiredSourceIdsForItems(adapter, items);
|
|
182
|
+
subscriptions.syncSubscriptions(adapter, sourceIds);
|
|
183
|
+
|
|
184
|
+
for (const id of sourceIds) {
|
|
185
|
+
try {
|
|
186
|
+
const obj = await adapter.getForeignObjectAsync(id);
|
|
187
|
+
if (!obj) {
|
|
188
|
+
adapter.log.warn(`Source state not found: ${id}`);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const state = await adapter.getForeignStateAsync(id);
|
|
193
|
+
if (state) {
|
|
194
|
+
adapter.cache.set(id, state.val);
|
|
195
|
+
adapter.cacheTs.set(id, typeof state.ts === 'number' ? state.ts : Date.now());
|
|
196
|
+
}
|
|
197
|
+
} catch (e) {
|
|
198
|
+
adapter.log.warn(`Cannot subscribe/read ${id}: ${e && e.message ? e.message : e}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (enabledItems.length === 0) {
|
|
203
|
+
const msg = 'No item is enabled. Please enable at least one item in the adapter configuration.';
|
|
204
|
+
adapter.log.warn(msg);
|
|
205
|
+
await adapter.setStateAsync('info.status', 'no_items_enabled', true);
|
|
206
|
+
} else {
|
|
207
|
+
await adapter.setStateAsync('info.status', 'ok', true);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = {
|
|
212
|
+
getItemsConfigSignature,
|
|
213
|
+
compileItem,
|
|
214
|
+
ensureCompiledForCurrentConfig,
|
|
215
|
+
ensureItemTitlesInInstanceConfig,
|
|
216
|
+
prepareItems,
|
|
217
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Builds a snapshot map for one tick, optionally refreshing input states first.
|
|
4
|
+
|
|
5
|
+
const { getDesiredSourceIdsForItems } = require('./subscriptions');
|
|
6
|
+
|
|
7
|
+
function getUseSnapshotReads(adapter) {
|
|
8
|
+
return !!(adapter.config && adapter.config.snapshotInputs);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getSnapshotDelayMs(adapter) {
|
|
12
|
+
const raw = adapter.config && adapter.config.snapshotDelayMs !== undefined ? adapter.config.snapshotDelayMs : 0;
|
|
13
|
+
const ms = Number(raw);
|
|
14
|
+
return Number.isFinite(ms) && ms >= 0 && ms <= 5000 ? Math.round(ms) : 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function buildSnapshotForTick(adapter, items) {
|
|
18
|
+
const sourceIds = getDesiredSourceIdsForItems(adapter, items);
|
|
19
|
+
|
|
20
|
+
if (getUseSnapshotReads(adapter)) {
|
|
21
|
+
const delay = getSnapshotDelayMs(adapter);
|
|
22
|
+
if (delay) {
|
|
23
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
24
|
+
}
|
|
25
|
+
await Promise.all(
|
|
26
|
+
Array.from(sourceIds).map(async id => {
|
|
27
|
+
try {
|
|
28
|
+
const st = await adapter.getForeignStateAsync(id);
|
|
29
|
+
if (st) {
|
|
30
|
+
adapter.cache.set(id, st.val);
|
|
31
|
+
adapter.cacheTs.set(id, typeof st.ts === 'number' ? st.ts : Date.now());
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// ignore per-id read errors
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @type {Map<string, any>} */
|
|
41
|
+
const snapshot = new Map();
|
|
42
|
+
for (const id of sourceIds) {
|
|
43
|
+
snapshot.set(id, adapter.cache.get(id));
|
|
44
|
+
}
|
|
45
|
+
return snapshot;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { buildSnapshotForTick };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Discovers foreign state ids referenced by an item (inputs + s()/v()/jp() in formulas).
|
|
4
|
+
// This is used to decide which ids to subscribe to / include in snapshot reads.
|
|
5
|
+
|
|
6
|
+
const { getItemDisplayId } = require('./itemIds');
|
|
7
|
+
|
|
8
|
+
function collectSourceStatesFromItem(adapter, item) {
|
|
9
|
+
const ids = [];
|
|
10
|
+
if (!item || typeof item !== 'object') return ids;
|
|
11
|
+
|
|
12
|
+
if ((item.mode || 'formula') === 'source') {
|
|
13
|
+
if (item.sourceState) ids.push(String(item.sourceState));
|
|
14
|
+
return ids;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (Array.isArray(item.inputs)) {
|
|
18
|
+
for (const inp of item.inputs) {
|
|
19
|
+
if (inp && inp.sourceState) ids.push(String(inp.sourceState));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Also allow s("...") / v("...") / jp("...", "...") in formula;
|
|
24
|
+
// discover these ids so snapshot/subscriptions can include them.
|
|
25
|
+
const expr = item.formula ? String(item.formula) : '';
|
|
26
|
+
if (!expr) return ids;
|
|
27
|
+
|
|
28
|
+
const max = adapter.MAX_DISCOVERED_STATE_IDS_PER_ITEM || 250;
|
|
29
|
+
let added = 0;
|
|
30
|
+
const re = /\b(?:s|v)\(\s*(['"])([^'"\n\r]+)\1\s*\)/g;
|
|
31
|
+
const reJp = /\bjp\(\s*(['"])([^'"\n\r]+)\1\s*,/g;
|
|
32
|
+
let m;
|
|
33
|
+
|
|
34
|
+
while ((m = re.exec(expr)) !== null) {
|
|
35
|
+
const sid = (m[2] || '').trim();
|
|
36
|
+
if (!sid) continue;
|
|
37
|
+
ids.push(sid);
|
|
38
|
+
added++;
|
|
39
|
+
if (added >= max) {
|
|
40
|
+
const itemId = getItemDisplayId(item) || (item && item.name ? String(item.name) : 'item');
|
|
41
|
+
adapter.warnOnce(
|
|
42
|
+
`discover_ids_limit|${itemId}`,
|
|
43
|
+
`Formula contains many s()/v() state reads; limiting discovered ids to ${max} for '${itemId}'`
|
|
44
|
+
);
|
|
45
|
+
return ids;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
while ((m = reJp.exec(expr)) !== null) {
|
|
50
|
+
const sid = (m[2] || '').trim();
|
|
51
|
+
if (!sid) continue;
|
|
52
|
+
ids.push(sid);
|
|
53
|
+
added++;
|
|
54
|
+
if (added >= max) {
|
|
55
|
+
const itemId = getItemDisplayId(item) || (item && item.name ? String(item.name) : 'item');
|
|
56
|
+
adapter.warnOnce(
|
|
57
|
+
`discover_ids_limit|${itemId}`,
|
|
58
|
+
`Formula contains many s()/v()/jp() state reads; limiting discovered ids to ${max} for '${itemId}'`
|
|
59
|
+
);
|
|
60
|
+
return ids;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return ids;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { collectSourceStatesFromItem };
|