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.
@@ -0,0 +1,462 @@
1
+ 'use strict';
2
+
3
+ // Creates/extends ioBroker objects for this adapter.
4
+ // Goal: keep state/object boilerplate out of the runtime logic.
5
+
6
+ const { getItemOutputId } = require('./itemIds');
7
+
8
+ function isDiagnosticsEnabled(raw, fallback = true) {
9
+ if (raw === undefined) return fallback;
10
+ if (raw === false || raw === 0 || raw === '0') return false;
11
+ if (typeof raw === 'string' && raw.trim().toLowerCase() === 'false') return false;
12
+ return true;
13
+ }
14
+
15
+ function getItemInfoBaseId(outputId) {
16
+ return `items.${String(outputId)}`;
17
+ }
18
+
19
+ async function ensureChannelPath(adapter, id) {
20
+ const raw = id ? String(id).trim() : '';
21
+ if (!raw) return;
22
+ const parts = raw.split('.').filter(Boolean);
23
+ if (parts.length <= 1) return;
24
+
25
+ let prefix = '';
26
+ for (let i = 0; i < parts.length - 1; i++) {
27
+ prefix = prefix ? `${prefix}.${parts[i]}` : parts[i];
28
+ await adapter.setObjectNotExistsAsync(prefix, {
29
+ type: 'channel',
30
+ common: { name: parts[i] },
31
+ native: {},
32
+ });
33
+ }
34
+ }
35
+
36
+ async function ensureOutputState(adapter, item) {
37
+ const id = getItemOutputId(item);
38
+ if (!id) return;
39
+
40
+ await ensureChannelPath(adapter, id);
41
+
42
+ const typeMap = {
43
+ number: 'number',
44
+ boolean: 'boolean',
45
+ string: 'string',
46
+ mixed: 'mixed',
47
+ };
48
+ const commonType = typeMap[item.type] || 'number';
49
+
50
+ /** @type {ioBroker.SettableStateObject} */
51
+ const obj = {
52
+ type: 'state',
53
+ common: {
54
+ name: item.name || id,
55
+ type: commonType,
56
+ role: item.role || 'value',
57
+ unit: item.unit || undefined,
58
+ read: true,
59
+ write: false,
60
+ },
61
+ native: {
62
+ mode: item.mode || 'formula',
63
+ },
64
+ };
65
+
66
+ const existing = await adapter.getObjectAsync(id);
67
+ if (!existing) {
68
+ await adapter.setObjectAsync(id, obj);
69
+ } else {
70
+ await adapter.extendObjectAsync(id, obj);
71
+ }
72
+ }
73
+
74
+ async function ensureItemInfoStatesForCompiled(adapter, compiled) {
75
+ if (!compiled || !compiled.outputId) return;
76
+ const base = getItemInfoBaseId(compiled.outputId);
77
+
78
+ await ensureChannelPath(adapter, `${base}.compiledOk`);
79
+
80
+ await adapter.setObjectNotExistsAsync(`${base}.compiledOk`, {
81
+ type: 'state',
82
+ common: {
83
+ name: 'Compiled OK',
84
+ type: 'boolean',
85
+ role: 'indicator',
86
+ read: true,
87
+ write: false,
88
+ },
89
+ native: {},
90
+ });
91
+
92
+ await adapter.setObjectNotExistsAsync(`${base}.compileError`, {
93
+ type: 'state',
94
+ common: {
95
+ name: 'Compile Error',
96
+ type: 'string',
97
+ role: 'text',
98
+ read: true,
99
+ write: false,
100
+ },
101
+ native: {},
102
+ });
103
+
104
+ await adapter.setObjectNotExistsAsync(`${base}.lastError`, {
105
+ type: 'state',
106
+ common: {
107
+ name: 'Last Error',
108
+ type: 'string',
109
+ role: 'text',
110
+ read: true,
111
+ write: false,
112
+ },
113
+ native: {},
114
+ });
115
+
116
+ await adapter.setObjectNotExistsAsync(`${base}.lastOkTs`, {
117
+ type: 'state',
118
+ common: {
119
+ name: 'Last OK Timestamp',
120
+ type: 'string',
121
+ role: 'date',
122
+ read: true,
123
+ write: false,
124
+ },
125
+ native: {},
126
+ });
127
+
128
+ await adapter.setObjectNotExistsAsync(`${base}.lastEvalMs`, {
129
+ type: 'state',
130
+ common: {
131
+ name: 'Last Evaluation Time (ms)',
132
+ type: 'number',
133
+ role: 'value',
134
+ read: true,
135
+ write: false,
136
+ unit: 'ms',
137
+ },
138
+ native: {},
139
+ });
140
+
141
+ await adapter.setObjectNotExistsAsync(`${base}.consecutiveErrors`, {
142
+ type: 'state',
143
+ common: {
144
+ name: 'Consecutive Errors',
145
+ type: 'number',
146
+ role: 'value',
147
+ read: true,
148
+ write: false,
149
+ },
150
+ native: {},
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Cleans up old info.* states to ensure a fresh structure after updates.
156
+ * Preserves info.connection (managed by ioBroker core).
157
+ */
158
+ async function cleanupOldInfoStates(adapter) {
159
+ try {
160
+ const objects = await adapter.getObjectViewAsync('system', 'state', {
161
+ startkey: `${adapter.namespace}.info.`,
162
+ endkey: `${adapter.namespace}.info.\u9999`,
163
+ });
164
+
165
+ if (objects && objects.rows) {
166
+ for (const row of objects.rows) {
167
+ const id = row.id.replace(`${adapter.namespace}.`, '');
168
+ // Preserve connection (ioBroker standard)
169
+ if (id === 'info.connection') continue;
170
+
171
+ try {
172
+ await adapter.delObjectAsync(id);
173
+ } catch (e) {
174
+ // Ignore delete errors (state might not exist)
175
+ }
176
+ }
177
+ }
178
+ } catch (e) {
179
+ adapter.log.debug(`Cleanup old info states: ${e && e.message ? e.message : e}`);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Creates info states according to Variante D:
185
+ * - info.* (6 core states for users)
186
+ * - info.diagnostics.* (evaluation stats)
187
+ * - info.diagnostics.timing.* (timestamp skew diagnostics)
188
+ */
189
+ async function createInfoStates(adapter) {
190
+ // Core info states (alphabetically ordered for readability)
191
+ await adapter.setObjectNotExistsAsync('info.itemsActive', {
192
+ type: 'state',
193
+ common: {
194
+ name: 'Active items',
195
+ type: 'number',
196
+ role: 'value',
197
+ read: true,
198
+ write: false,
199
+ },
200
+ native: {},
201
+ });
202
+
203
+ await adapter.setObjectNotExistsAsync('info.lastError', {
204
+ type: 'state',
205
+ common: {
206
+ name: 'Last error',
207
+ type: 'string',
208
+ role: 'text',
209
+ read: true,
210
+ write: false,
211
+ },
212
+ native: {},
213
+ });
214
+
215
+ await adapter.setObjectNotExistsAsync('info.lastRun', {
216
+ type: 'state',
217
+ common: {
218
+ name: 'Last run',
219
+ type: 'string',
220
+ role: 'date',
221
+ read: true,
222
+ write: false,
223
+ },
224
+ native: {},
225
+ });
226
+
227
+ await adapter.setObjectNotExistsAsync('info.lastRunMs', {
228
+ type: 'state',
229
+ common: {
230
+ name: 'Last run duration',
231
+ type: 'number',
232
+ role: 'value',
233
+ unit: 'ms',
234
+ read: true,
235
+ write: false,
236
+ },
237
+ native: {},
238
+ });
239
+
240
+ await adapter.setObjectNotExistsAsync('info.status', {
241
+ type: 'state',
242
+ common: {
243
+ name: 'Status',
244
+ type: 'string',
245
+ role: 'text',
246
+ read: true,
247
+ write: false,
248
+ },
249
+ native: {},
250
+ });
251
+
252
+ // Diagnostics channel
253
+ await ensureChannelPath(adapter, 'info.diagnostics.evalBudgetMs');
254
+
255
+ await adapter.setObjectNotExistsAsync('info.diagnostics.evalBudgetMs', {
256
+ type: 'state',
257
+ common: {
258
+ name: 'Evaluation budget',
259
+ type: 'number',
260
+ role: 'value',
261
+ unit: 'ms',
262
+ read: true,
263
+ write: false,
264
+ },
265
+ native: {},
266
+ });
267
+
268
+ await adapter.setObjectNotExistsAsync('info.diagnostics.evalSkipped', {
269
+ type: 'state',
270
+ common: {
271
+ name: 'Skipped items (last tick)',
272
+ type: 'number',
273
+ role: 'value',
274
+ read: true,
275
+ write: false,
276
+ },
277
+ native: {},
278
+ });
279
+
280
+ await adapter.setObjectNotExistsAsync('info.diagnostics.itemsTotal', {
281
+ type: 'state',
282
+ common: {
283
+ name: 'Total configured items',
284
+ type: 'number',
285
+ role: 'value',
286
+ read: true,
287
+ write: false,
288
+ },
289
+ native: {},
290
+ });
291
+
292
+ // Diagnostics timing channel (alphabetically ordered)
293
+ await ensureChannelPath(adapter, 'info.diagnostics.timing.gapMs');
294
+
295
+ await adapter.setObjectNotExistsAsync('info.diagnostics.timing.gapActiveMs', {
296
+ type: 'state',
297
+ common: {
298
+ name: 'Timestamp gap (active sources)',
299
+ type: 'number',
300
+ role: 'value',
301
+ unit: 'ms',
302
+ read: true,
303
+ write: false,
304
+ },
305
+ native: {},
306
+ });
307
+
308
+ await adapter.setObjectNotExistsAsync('info.diagnostics.timing.gapActiveOk', {
309
+ type: 'state',
310
+ common: {
311
+ name: 'Timestamp gap (active) OK',
312
+ type: 'boolean',
313
+ role: 'indicator',
314
+ read: true,
315
+ write: false,
316
+ },
317
+ native: {},
318
+ });
319
+
320
+ await adapter.setObjectNotExistsAsync('info.diagnostics.timing.gapMs', {
321
+ type: 'state',
322
+ common: {
323
+ name: 'Timestamp gap (all sources)',
324
+ type: 'number',
325
+ role: 'value',
326
+ unit: 'ms',
327
+ read: true,
328
+ write: false,
329
+ },
330
+ native: {},
331
+ });
332
+
333
+ await adapter.setObjectNotExistsAsync('info.diagnostics.timing.gapOk', {
334
+ type: 'state',
335
+ common: {
336
+ name: 'Timestamp gap OK',
337
+ type: 'boolean',
338
+ role: 'indicator',
339
+ read: true,
340
+ write: false,
341
+ },
342
+ native: {},
343
+ });
344
+
345
+ await adapter.setObjectNotExistsAsync('info.diagnostics.timing.newestAgeMs', {
346
+ type: 'state',
347
+ common: {
348
+ name: 'Newest source age',
349
+ type: 'number',
350
+ role: 'value',
351
+ unit: 'ms',
352
+ read: true,
353
+ write: false,
354
+ },
355
+ native: {},
356
+ });
357
+
358
+ await adapter.setObjectNotExistsAsync('info.diagnostics.timing.newestId', {
359
+ type: 'state',
360
+ common: {
361
+ name: 'Newest source ID',
362
+ type: 'string',
363
+ role: 'text',
364
+ read: true,
365
+ write: false,
366
+ },
367
+ native: {},
368
+ });
369
+
370
+ await adapter.setObjectNotExistsAsync('info.diagnostics.timing.oldestAgeMs', {
371
+ type: 'state',
372
+ common: {
373
+ name: 'Oldest source age',
374
+ type: 'number',
375
+ role: 'value',
376
+ unit: 'ms',
377
+ read: true,
378
+ write: false,
379
+ },
380
+ native: {},
381
+ });
382
+
383
+ await adapter.setObjectNotExistsAsync('info.diagnostics.timing.oldestId', {
384
+ type: 'state',
385
+ common: {
386
+ name: 'Oldest source ID',
387
+ type: 'string',
388
+ role: 'text',
389
+ read: true,
390
+ write: false,
391
+ },
392
+ native: {},
393
+ });
394
+
395
+ await adapter.setObjectNotExistsAsync('info.diagnostics.timing.sources', {
396
+ type: 'state',
397
+ common: {
398
+ name: 'Sources with timestamps',
399
+ type: 'number',
400
+ role: 'value',
401
+ read: true,
402
+ write: false,
403
+ },
404
+ native: {},
405
+ });
406
+
407
+ await adapter.setObjectNotExistsAsync('info.diagnostics.timing.sourcesActive', {
408
+ type: 'state',
409
+ common: {
410
+ name: 'Active sources',
411
+ type: 'number',
412
+ role: 'value',
413
+ read: true,
414
+ write: false,
415
+ },
416
+ native: {},
417
+ });
418
+
419
+ await adapter.setObjectNotExistsAsync('info.diagnostics.timing.sourcesSleeping', {
420
+ type: 'state',
421
+ common: {
422
+ name: 'Sleeping sources',
423
+ type: 'number',
424
+ role: 'value',
425
+ read: true,
426
+ write: false,
427
+ },
428
+ native: {},
429
+ });
430
+
431
+ // Initialize state values
432
+ await adapter.setStateAsync('info.status', 'starting', true);
433
+ await adapter.setStateAsync('info.itemsActive', 0, true);
434
+ await adapter.setStateAsync('info.lastError', '', true);
435
+ await adapter.setStateAsync('info.lastRun', '', true);
436
+ await adapter.setStateAsync('info.lastRunMs', 0, true);
437
+
438
+ await adapter.setStateAsync('info.diagnostics.itemsTotal', 0, true);
439
+ await adapter.setStateAsync('info.diagnostics.evalBudgetMs', 0, true);
440
+ await adapter.setStateAsync('info.diagnostics.evalSkipped', 0, true);
441
+
442
+ await adapter.setStateAsync('info.diagnostics.timing.gapMs', 0, true);
443
+ await adapter.setStateAsync('info.diagnostics.timing.gapOk', true, true);
444
+ await adapter.setStateAsync('info.diagnostics.timing.gapActiveMs', 0, true);
445
+ await adapter.setStateAsync('info.diagnostics.timing.gapActiveOk', true, true);
446
+ await adapter.setStateAsync('info.diagnostics.timing.sources', 0, true);
447
+ await adapter.setStateAsync('info.diagnostics.timing.sourcesActive', 0, true);
448
+ await adapter.setStateAsync('info.diagnostics.timing.sourcesSleeping', 0, true);
449
+ await adapter.setStateAsync('info.diagnostics.timing.oldestId', '', true);
450
+ await adapter.setStateAsync('info.diagnostics.timing.oldestAgeMs', 0, true);
451
+ await adapter.setStateAsync('info.diagnostics.timing.newestId', '', true);
452
+ await adapter.setStateAsync('info.diagnostics.timing.newestAgeMs', 0, true);
453
+ }
454
+
455
+ module.exports = {
456
+ getItemInfoBaseId,
457
+ ensureChannelPath,
458
+ ensureOutputState,
459
+ ensureItemInfoStatesForCompiled,
460
+ cleanupOldInfoStates,
461
+ createInfoStates,
462
+ };
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ // Computes which foreign ids should be subscribed/snapshotted based on enabled items.
4
+
5
+ const { getItemOutputId } = require('./itemIds');
6
+ const { collectSourceStatesFromItem } = require('./sourceDiscovery');
7
+
8
+ function getMaxTotalSourceIds(adapter) {
9
+ const raw = Number(adapter.MAX_TOTAL_SOURCE_IDS);
10
+ if (!Number.isFinite(raw) || raw <= 0) return 5000;
11
+ return Math.min(50000, Math.round(raw));
12
+ }
13
+
14
+ function getDesiredSourceIdsForItems(adapter, items) {
15
+ const enabledItems = Array.isArray(items)
16
+ ? items.filter(it => it && typeof it === 'object' && it.enabled)
17
+ : [];
18
+
19
+ const desired = new Set();
20
+ for (const item of enabledItems) {
21
+ const out = getItemOutputId(item);
22
+ const compiled = out ? adapter.compiledItems.get(out) : null;
23
+ if (compiled && compiled.sourceIds) {
24
+ for (const id of compiled.sourceIds) {
25
+ if (id) desired.add(String(id));
26
+ }
27
+ } else {
28
+ for (const id of collectSourceStatesFromItem(adapter, item)) {
29
+ if (id) desired.add(String(id));
30
+ }
31
+ }
32
+ }
33
+
34
+ const cap = getMaxTotalSourceIds(adapter);
35
+ if (desired.size > cap) {
36
+ const kept = new Set();
37
+ let n = 0;
38
+ for (const id of desired) {
39
+ kept.add(id);
40
+ n++;
41
+ if (n >= cap) break;
42
+ }
43
+ adapter.warnOnce(
44
+ `source_ids_cap|${cap}`,
45
+ `Too many source state ids (${desired.size}); limiting subscriptions/snapshot to first ${cap}. Please reduce configured items/inputs.`
46
+ );
47
+ return kept;
48
+ }
49
+ return desired;
50
+ }
51
+
52
+ function syncSubscriptions(adapter, desiredIds) {
53
+ const desired = desiredIds instanceof Set ? desiredIds : new Set();
54
+
55
+ // Unsubscribe stale ids
56
+ for (const id of Array.from(adapter.subscribedIds)) {
57
+ if (!desired.has(id)) {
58
+ try {
59
+ adapter.unsubscribeForeignStates(id);
60
+ } catch (e) {
61
+ adapter.log.debug(`Cannot unsubscribe ${id}: ${e && e.message ? e.message : e}`);
62
+ } finally {
63
+ adapter.subscribedIds.delete(id);
64
+ }
65
+ }
66
+ }
67
+
68
+ // Subscribe missing ids
69
+ for (const id of desired) {
70
+ if (adapter.subscribedIds.has(id)) continue;
71
+ try {
72
+ adapter.subscribeForeignStates(id);
73
+ adapter.subscribedIds.add(id);
74
+ } catch (e) {
75
+ adapter.log.warn(`Cannot subscribe ${id}: ${e && e.message ? e.message : e}`);
76
+ }
77
+ }
78
+ }
79
+
80
+ module.exports = {
81
+ getDesiredSourceIdsForItems,
82
+ syncSubscriptions,
83
+ };