iobroker.data-solectrus 0.2.9 → 0.3.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/CHANGELOG.md +29 -0
- package/README.md +24 -12
- package/io-package.json +5 -1
- package/lib/services/stateRegistry.js +137 -41
- package/lib/services/tickRunner.js +63 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 - 2026-02-03
|
|
4
|
+
|
|
5
|
+
### BREAKING CHANGES
|
|
6
|
+
|
|
7
|
+
- **Diagnostics state reorganization**: States moved to hierarchical structure for better organization
|
|
8
|
+
- `info.itemsConfigured` → `info.diagnostics.itemsTotal`
|
|
9
|
+
- `info.itemsEnabled` → `info.itemsActive`
|
|
10
|
+
- `info.evalTimeMs` → `info.lastRunMs`
|
|
11
|
+
- `info.timeBudgetMs` → `info.diagnostics.evalBudgetMs`
|
|
12
|
+
- `info.skippedItems` → `info.diagnostics.evalSkipped`
|
|
13
|
+
- Old flat timing states → `info.diagnostics.timing.*`
|
|
14
|
+
- **Note**: Existing visualizations and scripts need to be updated to use new state paths
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- New hierarchical state structure with `info.diagnostics.*` and `info.diagnostics.timing.*` channels
|
|
19
|
+
- Enhanced timing diagnostics:
|
|
20
|
+
- Active vs sleeping source detection (30s threshold)
|
|
21
|
+
- `info.diagnostics.timing.sourcesActive` and `sourcesSleeping`
|
|
22
|
+
- Newest/oldest source tracking with IDs and age metrics
|
|
23
|
+
- `info.diagnostics.timing.newestAgeMs`, `newestId`, `oldestAgeMs`, `oldestId`
|
|
24
|
+
- Separate gap calculations for all sources vs active sources only
|
|
25
|
+
- `info.diagnostics.timing.gapActiveMs` and `gapActiveOk`
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Improved state organization: all timing-related states now grouped under `info.diagnostics.timing.*`
|
|
30
|
+
- Better state naming for clarity (e.g., `lastRunMs` instead of `evalTimeMs`)
|
|
31
|
+
|
|
3
32
|
## 0.2.9 - 2026-02-03
|
|
4
33
|
|
|
5
34
|
### Added
|
package/README.md
CHANGED
|
@@ -71,18 +71,30 @@ Direktlinks (Auswahl):
|
|
|
71
71
|
Unter `data-solectrus.0.info.*` werden Status/Diagnosewerte gepflegt:
|
|
72
72
|
|
|
73
73
|
- `info.status`: `starting`, `ok`, `no_items_enabled`
|
|
74
|
-
- `info.
|
|
75
|
-
- `info.lastError
|
|
76
|
-
- `info.lastRun
|
|
77
|
-
- `info.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
- `info.
|
|
82
|
-
- `info.
|
|
83
|
-
- `info.
|
|
84
|
-
|
|
85
|
-
|
|
74
|
+
- `info.itemsActive`: Anzahl aktiver Items
|
|
75
|
+
- `info.lastError`: Letzter Fehler
|
|
76
|
+
- `info.lastRun`: Zeitstempel des letzten Ticks (ISO)
|
|
77
|
+
- `info.lastRunMs`: Dauer des letzten Ticks (ms)
|
|
78
|
+
|
|
79
|
+
Unter `info.diagnostics.*` liegen erweiterte Diagnose-Informationen:
|
|
80
|
+
|
|
81
|
+
- `info.diagnostics.itemsTotal`: Gesamtzahl konfigurierter Items
|
|
82
|
+
- `info.diagnostics.evalBudgetMs`: Verfügbares Zeitbudget pro Tick (ms)
|
|
83
|
+
- `info.diagnostics.evalSkipped`: Anzahl übersprungener Items (bei Budget-Überschreitung)
|
|
84
|
+
|
|
85
|
+
Unter `info.diagnostics.timing.*` finden sich detaillierte Timing-Analysen (hilft bei kurzzeitig „unplausiblen" Kombinationen, wenn Quellen zeitversetzt updaten):
|
|
86
|
+
|
|
87
|
+
- `info.diagnostics.timing.gapMs`: Zeitdifferenz zwischen ältestem und neuestem Source-Timestamp (alle Quellen)
|
|
88
|
+
- `info.diagnostics.timing.gapOk`: `true/false` basierend auf Threshold
|
|
89
|
+
- `info.diagnostics.timing.gapActiveMs`: Zeitdifferenz nur für aktive Quellen (< 30s alt)
|
|
90
|
+
- `info.diagnostics.timing.gapActiveOk`: `true/false` für aktive Quellen
|
|
91
|
+
- `info.diagnostics.timing.newestAgeMs`: Alter der neuesten Quelle (ms)
|
|
92
|
+
- `info.diagnostics.timing.newestId`: State-ID der neuesten Quelle
|
|
93
|
+
- `info.diagnostics.timing.oldestAgeMs`: Alter der ältesten Quelle (ms)
|
|
94
|
+
- `info.diagnostics.timing.oldestId`: State-ID der ältesten Quelle
|
|
95
|
+
- `info.diagnostics.timing.sources`: Anzahl Quellen mit Timestamp
|
|
96
|
+
- `info.diagnostics.timing.sourcesActive`: Anzahl aktiver Quellen (< 30s alt)
|
|
97
|
+
- `info.diagnostics.timing.sourcesSleeping`: Anzahl inaktiver Quellen (≥ 30s alt)
|
|
86
98
|
|
|
87
99
|
Zusätzlich gibt es per Item Diagnose-States unter `data-solectrus.0.items.<outputId>.*`:
|
|
88
100
|
|
package/io-package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "data-solectrus",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"title": "Data-SOLECTRUS",
|
|
6
6
|
"icon": "data-solectrus.png",
|
|
7
7
|
"extIcon": "https://raw.githubusercontent.com/Felliglanz/ioBroker.data-solectrus/main/admin/data-solectrus.png",
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"readme": "https://github.com/Felliglanz/ioBroker.data-solectrus/blob/main/README.md",
|
|
32
32
|
"news": {
|
|
33
|
+
"0.3.0": {
|
|
34
|
+
"en": "BREAKING: Restructured diagnostics states with hierarchical organization. Enhanced timing analytics.",
|
|
35
|
+
"de": "BREAKING: Diagnose-States neu strukturiert mit hierarchischer Organisation. Erweiterte Timing-Analysen."
|
|
36
|
+
},
|
|
33
37
|
"0.2.9": {
|
|
34
38
|
"en": "Added package metadata (repository, bugs, homepage) and .npmignore",
|
|
35
39
|
"de": "Package-Metadaten (repository, bugs, homepage) und .npmignore hinzugefügt"
|
|
@@ -145,6 +145,7 @@ async function ensureItemInfoStatesForCompiled(adapter, compiled) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
async function createInfoStates(adapter) {
|
|
148
|
+
// === info.* (top-level status states) ===
|
|
148
149
|
await adapter.setObjectNotExistsAsync('info.status', {
|
|
149
150
|
type: 'state',
|
|
150
151
|
common: {
|
|
@@ -157,10 +158,10 @@ async function createInfoStates(adapter) {
|
|
|
157
158
|
native: {},
|
|
158
159
|
});
|
|
159
160
|
|
|
160
|
-
await adapter.setObjectNotExistsAsync('info.
|
|
161
|
+
await adapter.setObjectNotExistsAsync('info.itemsActive', {
|
|
161
162
|
type: 'state',
|
|
162
163
|
common: {
|
|
163
|
-
name: '
|
|
164
|
+
name: 'Active items',
|
|
164
165
|
type: 'number',
|
|
165
166
|
role: 'value',
|
|
166
167
|
read: true,
|
|
@@ -169,84 +170,123 @@ async function createInfoStates(adapter) {
|
|
|
169
170
|
native: {},
|
|
170
171
|
});
|
|
171
172
|
|
|
172
|
-
await adapter.setObjectNotExistsAsync('info.
|
|
173
|
+
await adapter.setObjectNotExistsAsync('info.lastError', {
|
|
173
174
|
type: 'state',
|
|
174
175
|
common: {
|
|
175
|
-
name: '
|
|
176
|
-
type: '
|
|
177
|
-
role: '
|
|
176
|
+
name: 'Last error',
|
|
177
|
+
type: 'string',
|
|
178
|
+
role: 'text',
|
|
178
179
|
read: true,
|
|
179
180
|
write: false,
|
|
180
181
|
},
|
|
181
182
|
native: {},
|
|
182
183
|
});
|
|
183
184
|
|
|
184
|
-
await adapter.setObjectNotExistsAsync('info.
|
|
185
|
+
await adapter.setObjectNotExistsAsync('info.lastRun', {
|
|
185
186
|
type: 'state',
|
|
186
187
|
common: {
|
|
187
|
-
name: 'Last
|
|
188
|
+
name: 'Last run',
|
|
188
189
|
type: 'string',
|
|
189
|
-
role: '
|
|
190
|
+
role: 'date',
|
|
190
191
|
read: true,
|
|
191
192
|
write: false,
|
|
192
193
|
},
|
|
193
194
|
native: {},
|
|
194
195
|
});
|
|
195
196
|
|
|
196
|
-
await adapter.setObjectNotExistsAsync('info.
|
|
197
|
+
await adapter.setObjectNotExistsAsync('info.lastRunMs', {
|
|
197
198
|
type: 'state',
|
|
198
199
|
common: {
|
|
199
|
-
name: 'Last
|
|
200
|
-
type: '
|
|
201
|
-
role: '
|
|
200
|
+
name: 'Last run duration',
|
|
201
|
+
type: 'number',
|
|
202
|
+
role: 'value',
|
|
203
|
+
unit: 'ms',
|
|
202
204
|
read: true,
|
|
203
205
|
write: false,
|
|
204
206
|
},
|
|
205
207
|
native: {},
|
|
206
208
|
});
|
|
207
209
|
|
|
208
|
-
|
|
210
|
+
// === info.diagnostics.* (diagnostics channel) ===
|
|
211
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics', {
|
|
212
|
+
type: 'channel',
|
|
213
|
+
common: { name: 'diagnostics' },
|
|
214
|
+
native: {},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics.evalBudgetMs', {
|
|
209
218
|
type: 'state',
|
|
210
219
|
common: {
|
|
211
|
-
name: 'Evaluation
|
|
220
|
+
name: 'Evaluation budget',
|
|
212
221
|
type: 'number',
|
|
213
222
|
role: 'value',
|
|
223
|
+
unit: 'ms',
|
|
214
224
|
read: true,
|
|
215
225
|
write: false,
|
|
216
226
|
},
|
|
217
227
|
native: {},
|
|
218
228
|
});
|
|
219
229
|
|
|
220
|
-
await adapter.setObjectNotExistsAsync('info.
|
|
230
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics.evalSkipped', {
|
|
221
231
|
type: 'state',
|
|
222
232
|
common: {
|
|
223
|
-
name: '
|
|
233
|
+
name: 'Skipped items (last tick)',
|
|
224
234
|
type: 'number',
|
|
225
235
|
role: 'value',
|
|
226
|
-
unit: 'ms',
|
|
227
236
|
read: true,
|
|
228
237
|
write: false,
|
|
229
238
|
},
|
|
230
239
|
native: {},
|
|
231
240
|
});
|
|
232
241
|
|
|
233
|
-
await adapter.setObjectNotExistsAsync('info.
|
|
242
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics.itemsTotal', {
|
|
234
243
|
type: 'state',
|
|
235
244
|
common: {
|
|
236
|
-
name: '
|
|
245
|
+
name: 'Total configured items',
|
|
246
|
+
type: 'number',
|
|
247
|
+
role: 'value',
|
|
248
|
+
read: true,
|
|
249
|
+
write: false,
|
|
250
|
+
},
|
|
251
|
+
native: {},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// === info.diagnostics.timing.* (timing sub-channel) ===
|
|
255
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics.timing', {
|
|
256
|
+
type: 'channel',
|
|
257
|
+
common: { name: 'timing' },
|
|
258
|
+
native: {},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics.timing.gapMs', {
|
|
262
|
+
type: 'state',
|
|
263
|
+
common: {
|
|
264
|
+
name: 'Timestamp gap (all sources)',
|
|
237
265
|
type: 'number',
|
|
238
266
|
role: 'value',
|
|
267
|
+
unit: 'ms',
|
|
268
|
+
read: true,
|
|
269
|
+
write: false,
|
|
270
|
+
},
|
|
271
|
+
native: {},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics.timing.gapOk', {
|
|
275
|
+
type: 'state',
|
|
276
|
+
common: {
|
|
277
|
+
name: 'Timestamp gap OK',
|
|
278
|
+
type: 'boolean',
|
|
279
|
+
role: 'indicator',
|
|
239
280
|
read: true,
|
|
240
281
|
write: false,
|
|
241
282
|
},
|
|
242
283
|
native: {},
|
|
243
284
|
});
|
|
244
285
|
|
|
245
|
-
|
|
246
|
-
await adapter.setObjectNotExistsAsync('info.inputTsGapMs', {
|
|
286
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics.timing.gapActiveMs', {
|
|
247
287
|
type: 'state',
|
|
248
288
|
common: {
|
|
249
|
-
name: '
|
|
289
|
+
name: 'Timestamp gap (active sources)',
|
|
250
290
|
type: 'number',
|
|
251
291
|
role: 'value',
|
|
252
292
|
unit: 'ms',
|
|
@@ -256,10 +296,10 @@ async function createInfoStates(adapter) {
|
|
|
256
296
|
native: {},
|
|
257
297
|
});
|
|
258
298
|
|
|
259
|
-
await adapter.setObjectNotExistsAsync('info.
|
|
299
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics.timing.gapActiveOk', {
|
|
260
300
|
type: 'state',
|
|
261
301
|
common: {
|
|
262
|
-
name: '
|
|
302
|
+
name: 'Timestamp gap (active) OK',
|
|
263
303
|
type: 'boolean',
|
|
264
304
|
role: 'indicator',
|
|
265
305
|
read: true,
|
|
@@ -268,10 +308,35 @@ async function createInfoStates(adapter) {
|
|
|
268
308
|
native: {},
|
|
269
309
|
});
|
|
270
310
|
|
|
271
|
-
await adapter.setObjectNotExistsAsync('info.
|
|
311
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics.timing.newestAgeMs', {
|
|
312
|
+
type: 'state',
|
|
313
|
+
common: {
|
|
314
|
+
name: 'Newest source age',
|
|
315
|
+
type: 'number',
|
|
316
|
+
role: 'value',
|
|
317
|
+
unit: 'ms',
|
|
318
|
+
read: true,
|
|
319
|
+
write: false,
|
|
320
|
+
},
|
|
321
|
+
native: {},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics.timing.newestId', {
|
|
325
|
+
type: 'state',
|
|
326
|
+
common: {
|
|
327
|
+
name: 'Newest source ID',
|
|
328
|
+
type: 'string',
|
|
329
|
+
role: 'text',
|
|
330
|
+
read: true,
|
|
331
|
+
write: false,
|
|
332
|
+
},
|
|
333
|
+
native: {},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics.timing.oldestAgeMs', {
|
|
272
337
|
type: 'state',
|
|
273
338
|
common: {
|
|
274
|
-
name: '
|
|
339
|
+
name: 'Oldest source age',
|
|
275
340
|
type: 'number',
|
|
276
341
|
role: 'value',
|
|
277
342
|
unit: 'ms',
|
|
@@ -281,10 +346,34 @@ async function createInfoStates(adapter) {
|
|
|
281
346
|
native: {},
|
|
282
347
|
});
|
|
283
348
|
|
|
284
|
-
await adapter.setObjectNotExistsAsync('info.
|
|
349
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics.timing.oldestId', {
|
|
350
|
+
type: 'state',
|
|
351
|
+
common: {
|
|
352
|
+
name: 'Oldest source ID',
|
|
353
|
+
type: 'string',
|
|
354
|
+
role: 'text',
|
|
355
|
+
read: true,
|
|
356
|
+
write: false,
|
|
357
|
+
},
|
|
358
|
+
native: {},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics.timing.sources', {
|
|
362
|
+
type: 'state',
|
|
363
|
+
common: {
|
|
364
|
+
name: 'Sources with timestamps',
|
|
365
|
+
type: 'number',
|
|
366
|
+
role: 'value',
|
|
367
|
+
read: true,
|
|
368
|
+
write: false,
|
|
369
|
+
},
|
|
370
|
+
native: {},
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics.timing.sourcesActive', {
|
|
285
374
|
type: 'state',
|
|
286
375
|
common: {
|
|
287
|
-
name: '
|
|
376
|
+
name: 'Active sources',
|
|
288
377
|
type: 'number',
|
|
289
378
|
role: 'value',
|
|
290
379
|
read: true,
|
|
@@ -293,10 +382,10 @@ async function createInfoStates(adapter) {
|
|
|
293
382
|
native: {},
|
|
294
383
|
});
|
|
295
384
|
|
|
296
|
-
await adapter.setObjectNotExistsAsync('info.
|
|
385
|
+
await adapter.setObjectNotExistsAsync('info.diagnostics.timing.sourcesSleeping', {
|
|
297
386
|
type: 'state',
|
|
298
387
|
common: {
|
|
299
|
-
name: '
|
|
388
|
+
name: 'Sleeping sources',
|
|
300
389
|
type: 'number',
|
|
301
390
|
role: 'value',
|
|
302
391
|
read: true,
|
|
@@ -305,19 +394,26 @@ async function createInfoStates(adapter) {
|
|
|
305
394
|
native: {},
|
|
306
395
|
});
|
|
307
396
|
|
|
397
|
+
// Initialize all states with default values
|
|
308
398
|
await adapter.setStateAsync('info.status', 'starting', true);
|
|
309
|
-
await adapter.setStateAsync('info.
|
|
310
|
-
await adapter.setStateAsync('info.itemsEnabled', 0, true);
|
|
399
|
+
await adapter.setStateAsync('info.itemsActive', 0, true);
|
|
311
400
|
await adapter.setStateAsync('info.lastError', '', true);
|
|
312
401
|
await adapter.setStateAsync('info.lastRun', '', true);
|
|
313
|
-
await adapter.setStateAsync('info.
|
|
314
|
-
await adapter.setStateAsync('info.
|
|
315
|
-
await adapter.setStateAsync('info.
|
|
316
|
-
await adapter.setStateAsync('info.
|
|
317
|
-
await adapter.setStateAsync('info.
|
|
318
|
-
await adapter.setStateAsync('info.
|
|
319
|
-
await adapter.setStateAsync('info.
|
|
320
|
-
await adapter.setStateAsync('info.
|
|
402
|
+
await adapter.setStateAsync('info.lastRunMs', 0, true);
|
|
403
|
+
await adapter.setStateAsync('info.diagnostics.evalBudgetMs', 0, true);
|
|
404
|
+
await adapter.setStateAsync('info.diagnostics.evalSkipped', 0, true);
|
|
405
|
+
await adapter.setStateAsync('info.diagnostics.itemsTotal', 0, true);
|
|
406
|
+
await adapter.setStateAsync('info.diagnostics.timing.gapMs', 0, true);
|
|
407
|
+
await adapter.setStateAsync('info.diagnostics.timing.gapOk', true, true);
|
|
408
|
+
await adapter.setStateAsync('info.diagnostics.timing.gapActiveMs', 0, true);
|
|
409
|
+
await adapter.setStateAsync('info.diagnostics.timing.gapActiveOk', true, true);
|
|
410
|
+
await adapter.setStateAsync('info.diagnostics.timing.newestAgeMs', 0, true);
|
|
411
|
+
await adapter.setStateAsync('info.diagnostics.timing.newestId', '', true);
|
|
412
|
+
await adapter.setStateAsync('info.diagnostics.timing.oldestAgeMs', 0, true);
|
|
413
|
+
await adapter.setStateAsync('info.diagnostics.timing.oldestId', '', true);
|
|
414
|
+
await adapter.setStateAsync('info.diagnostics.timing.sources', 0, true);
|
|
415
|
+
await adapter.setStateAsync('info.diagnostics.timing.sourcesActive', 0, true);
|
|
416
|
+
await adapter.setStateAsync('info.diagnostics.timing.sourcesSleeping', 0, true);
|
|
321
417
|
}
|
|
322
418
|
|
|
323
419
|
module.exports = {
|
|
@@ -60,11 +60,11 @@ async function runTick(adapter) {
|
|
|
60
60
|
|
|
61
61
|
// Keep status in sync even if config changes without a restart
|
|
62
62
|
try {
|
|
63
|
-
await adapter.setStateAsync('info.
|
|
64
|
-
await adapter.setStateAsync('info.
|
|
63
|
+
await adapter.setStateAsync('info.diagnostics.itemsTotal', items.filter(it => it && typeof it === 'object').length, true);
|
|
64
|
+
await adapter.setStateAsync('info.itemsActive', enabledItems.length, true);
|
|
65
65
|
await adapter.setStateAsync('info.status', enabledItems.length ? 'ok' : 'no_items_enabled', true);
|
|
66
|
-
await adapter.setStateAsync('info.
|
|
67
|
-
await adapter.setStateAsync('info.
|
|
66
|
+
await adapter.setStateAsync('info.diagnostics.evalBudgetMs', timeBudgetMs, true);
|
|
67
|
+
await adapter.setStateAsync('info.diagnostics.evalSkipped', 0, true);
|
|
68
68
|
} catch {
|
|
69
69
|
// ignore
|
|
70
70
|
}
|
|
@@ -84,33 +84,83 @@ async function runTick(adapter) {
|
|
|
84
84
|
}
|
|
85
85
|
adapter.currentSnapshot = snapshot;
|
|
86
86
|
|
|
87
|
-
// Publish
|
|
87
|
+
// Publish comprehensive timing diagnostics.
|
|
88
88
|
// This helps explain "impossible" transient combinations when snapshot is off or sources update slightly offset.
|
|
89
89
|
try {
|
|
90
90
|
const ids = snapshot && typeof snapshot.keys === 'function' ? Array.from(snapshot.keys()) : [];
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const sleepThresholdMs = 30000; // Sources not updated in 30s are considered "sleeping"
|
|
93
|
+
|
|
91
94
|
let minTs = Infinity;
|
|
92
95
|
let maxTs = -Infinity;
|
|
96
|
+
let minActiveTs = Infinity;
|
|
97
|
+
let maxActiveTs = -Infinity;
|
|
98
|
+
let newestTs = -Infinity;
|
|
99
|
+
let oldestTs = Infinity;
|
|
100
|
+
let newestId = '';
|
|
101
|
+
let oldestId = '';
|
|
93
102
|
let withTs = 0;
|
|
103
|
+
let activeSources = 0;
|
|
104
|
+
let sleepingSources = 0;
|
|
94
105
|
let total = 0;
|
|
106
|
+
|
|
95
107
|
for (const id of ids) {
|
|
96
108
|
if (!id) continue;
|
|
97
109
|
total++;
|
|
98
110
|
const ts = adapter.cacheTs.get(id);
|
|
99
111
|
if (typeof ts !== 'number' || !Number.isFinite(ts)) continue;
|
|
112
|
+
|
|
100
113
|
withTs++;
|
|
114
|
+
const age = now - ts;
|
|
115
|
+
const isActive = age < sleepThresholdMs;
|
|
116
|
+
|
|
117
|
+
// Track overall min/max timestamps
|
|
101
118
|
if (ts < minTs) minTs = ts;
|
|
102
119
|
if (ts > maxTs) maxTs = ts;
|
|
120
|
+
|
|
121
|
+
// Track active sources min/max
|
|
122
|
+
if (isActive) {
|
|
123
|
+
activeSources++;
|
|
124
|
+
if (ts < minActiveTs) minActiveTs = ts;
|
|
125
|
+
if (ts > maxActiveTs) maxActiveTs = ts;
|
|
126
|
+
} else {
|
|
127
|
+
sleepingSources++;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Track newest (most recent)
|
|
131
|
+
if (ts > newestTs) {
|
|
132
|
+
newestTs = ts;
|
|
133
|
+
newestId = id;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Track oldest (least recent)
|
|
137
|
+
if (ts < oldestTs) {
|
|
138
|
+
oldestTs = ts;
|
|
139
|
+
oldestId = id;
|
|
140
|
+
}
|
|
103
141
|
}
|
|
142
|
+
|
|
104
143
|
const gapMs = withTs >= 2 ? Math.max(0, Math.round(maxTs - minTs)) : 0;
|
|
144
|
+
const gapActiveMs = activeSources >= 2 ? Math.max(0, Math.round(maxActiveTs - minActiveTs)) : 0;
|
|
105
145
|
const intervalMs = getTickIntervalMs(adapter);
|
|
106
146
|
const thresholdMs = Math.max(200, Math.min(5000, Math.floor(intervalMs * 0.2)));
|
|
107
|
-
const
|
|
147
|
+
const gapOk = withTs <= 1 ? true : gapMs <= thresholdMs;
|
|
148
|
+
const gapActiveOk = activeSources <= 1 ? true : gapActiveMs <= thresholdMs;
|
|
149
|
+
const newestAgeMs = newestTs > -Infinity ? Math.max(0, now - newestTs) : 0;
|
|
150
|
+
const oldestAgeMs = oldestTs < Infinity ? Math.max(0, now - oldestTs) : 0;
|
|
108
151
|
|
|
109
|
-
|
|
110
|
-
await adapter.setStateAsync('info.
|
|
111
|
-
await adapter.setStateAsync('info.
|
|
112
|
-
await adapter.setStateAsync('info.
|
|
113
|
-
await adapter.setStateAsync('info.
|
|
152
|
+
// Write all timing diagnostics
|
|
153
|
+
await adapter.setStateAsync('info.diagnostics.timing.gapMs', gapMs, true);
|
|
154
|
+
await adapter.setStateAsync('info.diagnostics.timing.gapOk', gapOk, true);
|
|
155
|
+
await adapter.setStateAsync('info.diagnostics.timing.gapActiveMs', gapActiveMs, true);
|
|
156
|
+
await adapter.setStateAsync('info.diagnostics.timing.gapActiveOk', gapActiveOk, true);
|
|
157
|
+
await adapter.setStateAsync('info.diagnostics.timing.newestAgeMs', newestAgeMs, true);
|
|
158
|
+
await adapter.setStateAsync('info.diagnostics.timing.newestId', newestId, true);
|
|
159
|
+
await adapter.setStateAsync('info.diagnostics.timing.oldestAgeMs', oldestAgeMs, true);
|
|
160
|
+
await adapter.setStateAsync('info.diagnostics.timing.oldestId', oldestId, true);
|
|
161
|
+
await adapter.setStateAsync('info.diagnostics.timing.sources', withTs, true);
|
|
162
|
+
await adapter.setStateAsync('info.diagnostics.timing.sourcesActive', activeSources, true);
|
|
163
|
+
await adapter.setStateAsync('info.diagnostics.timing.sourcesSleeping', sleepingSources, true);
|
|
114
164
|
} catch {
|
|
115
165
|
// ignore
|
|
116
166
|
}
|
|
@@ -194,9 +244,9 @@ async function runTick(adapter) {
|
|
|
194
244
|
adapter.currentSnapshot = null;
|
|
195
245
|
|
|
196
246
|
try {
|
|
197
|
-
await adapter.setStateAsync('info.
|
|
247
|
+
await adapter.setStateAsync('info.diagnostics.evalSkipped', skippedItems, true);
|
|
198
248
|
await adapter.setStateAsync('info.lastRun', new Date().toISOString(), true);
|
|
199
|
-
await adapter.setStateAsync('info.
|
|
249
|
+
await adapter.setStateAsync('info.lastRunMs', Date.now() - start, true);
|
|
200
250
|
} catch {
|
|
201
251
|
// ignore
|
|
202
252
|
}
|