iobroker.autodoc 0.9.35
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/LICENSE +21 -0
- package/README.md +126 -0
- package/admin/autodoc.png +0 -0
- package/admin/i18n/de.json +244 -0
- package/admin/i18n/en.json +241 -0
- package/admin/i18n/es.json +229 -0
- package/admin/i18n/fr.json +235 -0
- package/admin/i18n/it.json +229 -0
- package/admin/i18n/nl.json +229 -0
- package/admin/i18n/pl.json +229 -0
- package/admin/i18n/pt.json +229 -0
- package/admin/i18n/ru.json +229 -0
- package/admin/i18n/uk.json +229 -0
- package/admin/i18n/zh-cn.json +229 -0
- package/admin/jsonConfig.json +1490 -0
- package/io-package.json +253 -0
- package/lib/adapter-config.d.ts +19 -0
- package/lib/aiEnhancer.js +2114 -0
- package/lib/autoHostTopologyMermaid.js +195 -0
- package/lib/dependencyAnalyzer.js +83 -0
- package/lib/diagnosisSnapshot.js +32 -0
- package/lib/discovery.js +953 -0
- package/lib/docTemplateConfig.js +422 -0
- package/lib/documentModel.js +640 -0
- package/lib/forumCard.js +70 -0
- package/lib/guestHelpContent.js +93 -0
- package/lib/guestScriptPrivacy.js +14 -0
- package/lib/hostDisplay.js +19 -0
- package/lib/htmlRenderer.js +4108 -0
- package/lib/htmlThemePresets.js +79 -0
- package/lib/htmlToPdf.js +99 -0
- package/lib/i18n.js +1309 -0
- package/lib/markdownRenderer.js +2025 -0
- package/lib/mermaidAutodocPalette.js +165 -0
- package/lib/mermaidServerSvg.js +252 -0
- package/lib/notifier.js +124 -0
- package/lib/quickStartGuide.js +180 -0
- package/lib/roleMapper.js +90 -0
- package/lib/scriptGroups.js +78 -0
- package/lib/versionTracker.js +312 -0
- package/main.js +1368 -0
- package/package.json +88 -0
package/lib/discovery.js
ADDED
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
const { execSync } = require('node:child_process');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* State roles used for opt-in live values in onboarding (order = pick preference when several match).
|
|
5
|
+
* Room enums often reference a channel/device id without role; actual `level.temperature` etc. sit on child states.
|
|
6
|
+
*/
|
|
7
|
+
const LIVE_STATE_ROLE_PRIORITY = [
|
|
8
|
+
'level.temperature',
|
|
9
|
+
'value.temperature',
|
|
10
|
+
'level.blind',
|
|
11
|
+
'level.dimmer',
|
|
12
|
+
'switch.light',
|
|
13
|
+
'switch',
|
|
14
|
+
'value.brightness',
|
|
15
|
+
'value.humidity',
|
|
16
|
+
'value.power',
|
|
17
|
+
'switch.lock',
|
|
18
|
+
'sensor.door',
|
|
19
|
+
'sensor.window',
|
|
20
|
+
'sensor.contact',
|
|
21
|
+
'sensor.motion',
|
|
22
|
+
'alarm',
|
|
23
|
+
'sensor.alarm',
|
|
24
|
+
];
|
|
25
|
+
const LIVE_STATE_ROLES = new Set(LIVE_STATE_ROLE_PRIORITY);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Roles that match by prefix / pattern (ioBroker adapters vary). `pri` = tie-break bucket in LIVE_STATE_ROLE_PRIORITY.
|
|
29
|
+
*
|
|
30
|
+
*/
|
|
31
|
+
const LIVE_STATE_ROLE_REGEX = [
|
|
32
|
+
{ re: /^level\.blind|^blind\./, pri: 'level.blind' },
|
|
33
|
+
{ re: /^level\.dimmer/, pri: 'level.dimmer' },
|
|
34
|
+
{ re: /^switch\.light/, pri: 'switch.light' },
|
|
35
|
+
{ re: /^switch$|^switch\.plug|^switch\.socket/, pri: 'switch' },
|
|
36
|
+
{ re: /^value\.brightness/, pri: 'value.brightness' },
|
|
37
|
+
{ re: /^value\.(power|current|voltage)/, pri: 'value.power' },
|
|
38
|
+
{ re: /^switch\.lock|^lock\./, pri: 'switch.lock' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} role - ioBroker state role
|
|
43
|
+
* @returns {boolean} whether live values may be read for this role
|
|
44
|
+
*/
|
|
45
|
+
function isReadableLiveRole(role) {
|
|
46
|
+
if (!role) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (LIVE_STATE_ROLES.has(role)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return LIVE_STATE_ROLE_REGEX.some(x => x.re.test(role));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} role - ioBroker state role
|
|
57
|
+
* @returns {number} sort key for display ordering (lower first)
|
|
58
|
+
*/
|
|
59
|
+
function liveRoleSortIndex(role) {
|
|
60
|
+
const idx = LIVE_STATE_ROLE_PRIORITY.indexOf(role);
|
|
61
|
+
if (idx !== -1) {
|
|
62
|
+
return idx;
|
|
63
|
+
}
|
|
64
|
+
for (const x of LIVE_STATE_ROLE_REGEX) {
|
|
65
|
+
if (x.re.test(role)) {
|
|
66
|
+
return LIVE_STATE_ROLE_PRIORITY.indexOf(x.pri);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return 999;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* AutoDoc Discovery Module
|
|
74
|
+
* Handles automatic discovery of adapter instances, hosts, and system metadata
|
|
75
|
+
*/
|
|
76
|
+
class Discovery {
|
|
77
|
+
/**
|
|
78
|
+
* @param {object} adapter ioBroker adapter instance
|
|
79
|
+
*/
|
|
80
|
+
constructor(adapter) {
|
|
81
|
+
this.adapter = adapter;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve a multilingual ioBroker string to a plain string.
|
|
86
|
+
* common.desc and common.titleLang can be either a plain string
|
|
87
|
+
* or an object like { en: "...", de: "..." }.
|
|
88
|
+
*
|
|
89
|
+
* @param {string|object} value The raw value from common
|
|
90
|
+
* @param {string} lang Preferred language code
|
|
91
|
+
* @returns {string} Resolved string or empty string
|
|
92
|
+
*/
|
|
93
|
+
resolveI18nString(value, lang) {
|
|
94
|
+
if (!value) {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
if (typeof value === 'string') {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
if (typeof value === 'object') {
|
|
101
|
+
return value[lang] || value.en || Object.values(value)[0] || '';
|
|
102
|
+
}
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Read all adapter instances from the system
|
|
108
|
+
*
|
|
109
|
+
* @returns {Promise<Array>} Array of adapter instance objects
|
|
110
|
+
*/
|
|
111
|
+
async readAdapterInstances() {
|
|
112
|
+
try {
|
|
113
|
+
const instances = await this.adapter.getObjectViewAsync('system', 'instance', {});
|
|
114
|
+
const lang = this.adapter.config.language || 'en';
|
|
115
|
+
const result = [];
|
|
116
|
+
|
|
117
|
+
for (const obj of instances.rows) {
|
|
118
|
+
const instance = obj.value;
|
|
119
|
+
// Extract adapter name from common.name (e.g. "admin") or from _id (e.g. "system.adapter.admin.0" → "admin")
|
|
120
|
+
const adapterName = instance.common.name || instance._id.split('.')[2] || instance._id;
|
|
121
|
+
|
|
122
|
+
// Skip our own adapter instance
|
|
123
|
+
if (adapterName === 'autodoc') {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const scheduleCron = (instance.common && instance.common.schedule) || '';
|
|
128
|
+
const restartSchedule = (instance.common && instance.common.restartSchedule) || '';
|
|
129
|
+
|
|
130
|
+
result.push({
|
|
131
|
+
id: instance._id,
|
|
132
|
+
name: instance.common.name,
|
|
133
|
+
adapter: adapterName,
|
|
134
|
+
title:
|
|
135
|
+
this.resolveI18nString(instance.common.titleLang || instance.common.title, lang) || adapterName,
|
|
136
|
+
desc: this.resolveI18nString(instance.common.desc, lang),
|
|
137
|
+
enabled: instance.common.enabled,
|
|
138
|
+
host: instance.common.host,
|
|
139
|
+
mode: instance.common.mode,
|
|
140
|
+
/** ioBroker: CRON when common.mode === 'schedule' (elsewhere: state instanceId.schedule) */
|
|
141
|
+
scheduleCron,
|
|
142
|
+
/** ioBroker: optional CRON to restart daemon adapters */
|
|
143
|
+
restartSchedule,
|
|
144
|
+
version: instance.common.version,
|
|
145
|
+
config: this.filterNative(instance.native),
|
|
146
|
+
connectionType: instance.common.connectionType || '',
|
|
147
|
+
dataSource: instance.common.dataSource || '',
|
|
148
|
+
tier: instance.common.tier || 0,
|
|
149
|
+
type: instance.common.type || '',
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// If mode is schedule but common.schedule is empty, try the companion state (some setups keep CRON only there).
|
|
154
|
+
const needScheduleFromState = result.filter(
|
|
155
|
+
i => i.mode === 'schedule' && (!i.scheduleCron || !String(i.scheduleCron).trim()),
|
|
156
|
+
);
|
|
157
|
+
if (needScheduleFromState.length > 0) {
|
|
158
|
+
await Promise.all(
|
|
159
|
+
needScheduleFromState.map(async inst => {
|
|
160
|
+
try {
|
|
161
|
+
const st = await this.adapter.getForeignStateAsync(`${inst.id}.schedule`);
|
|
162
|
+
if (st && st.val != null && String(st.val).trim()) {
|
|
163
|
+
inst.scheduleCron = String(st.val).trim();
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// ignore
|
|
167
|
+
}
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return result;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
this.adapter.log.error(`Error reading adapter instances: ${error.message}`);
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* True when the object DB’s Couch-style design `system` defines a `schedule` search.
|
|
181
|
+
* Calling getObjectView without this triggers a controller/objects log on many installs (view added in newer js-controller).
|
|
182
|
+
*
|
|
183
|
+
* @returns {Promise<boolean>} whether the `schedule` object view is available
|
|
184
|
+
*/
|
|
185
|
+
async systemDesignHasScheduleView() {
|
|
186
|
+
try {
|
|
187
|
+
const d = await this.adapter.getForeignObjectAsync('_design/system');
|
|
188
|
+
if (!d || typeof d !== 'object') {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const views = d.views || (d.native && d.native.views);
|
|
192
|
+
return !!(views && typeof views === 'object' && views.schedule);
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Objects of ioBroker type "schedule" (design system / search "schedule"), if the object DB exposes this view.
|
|
200
|
+
* Separate from JavaScript scripts and from adapter instances in mode "schedule".
|
|
201
|
+
*
|
|
202
|
+
* @returns {Promise<Array<{id: string, name: string, desc: string, enabled: boolean}>>} schedule objects from the object DB
|
|
203
|
+
*/
|
|
204
|
+
async readScheduleDesignObjects() {
|
|
205
|
+
try {
|
|
206
|
+
if (!(await this.systemDesignHasScheduleView())) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
const res = await this.adapter.getObjectViewAsync('system', 'schedule', {});
|
|
210
|
+
if (!res || !Array.isArray(res.rows) || res.rows.length === 0) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
const lang = this.adapter.config.language || 'en';
|
|
214
|
+
const out = [];
|
|
215
|
+
for (const row of res.rows) {
|
|
216
|
+
const o = row.value;
|
|
217
|
+
const id = (o && o._id) || row.id;
|
|
218
|
+
if (!id) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const name =
|
|
222
|
+
(o && o.common && (o.common.name || o.common.title)) || id.split('.').filter(Boolean).pop() || id;
|
|
223
|
+
const descRaw = o && o.common && o.common.desc;
|
|
224
|
+
const desc = this.resolveI18nString(descRaw, lang);
|
|
225
|
+
const enabled = !o || !o.common || o.common.enabled !== false;
|
|
226
|
+
out.push({ id, name: String(name), desc, enabled });
|
|
227
|
+
}
|
|
228
|
+
if (out.length > 0) {
|
|
229
|
+
this.adapter.log.debug(
|
|
230
|
+
`readScheduleDesignObjects: ${out.length} row(s) from getObjectView(system, schedule)`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
} catch (e) {
|
|
235
|
+
this.adapter.log.debug(`getObjectView(system, schedule) not available or empty: ${e.message}`);
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Filter native config object: remove sensitive keys, keep only scalar values.
|
|
242
|
+
*
|
|
243
|
+
* @param {object} native Raw native config from instance
|
|
244
|
+
* @returns {object} Filtered config object
|
|
245
|
+
*/
|
|
246
|
+
filterNative(native) {
|
|
247
|
+
if (!native || typeof native !== 'object') {
|
|
248
|
+
return {};
|
|
249
|
+
}
|
|
250
|
+
const SENSITIVE = /password|passwd|token|secret|apikey|api_key|pass|key|auth|credential/i;
|
|
251
|
+
const result = {};
|
|
252
|
+
for (const [k, v] of Object.entries(native)) {
|
|
253
|
+
if (SENSITIVE.test(k)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
257
|
+
result[k] = v;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Read state objects summary
|
|
265
|
+
*
|
|
266
|
+
* @returns {Promise<object>} State objects statistics
|
|
267
|
+
*/
|
|
268
|
+
async readStateObjectsSummary() {
|
|
269
|
+
try {
|
|
270
|
+
const states = await this.adapter.getObjectViewAsync('system', 'state', {});
|
|
271
|
+
let total = 0;
|
|
272
|
+
let writable = 0;
|
|
273
|
+
let readonly = 0;
|
|
274
|
+
|
|
275
|
+
for (const obj of states.rows) {
|
|
276
|
+
const state = obj.value;
|
|
277
|
+
total++;
|
|
278
|
+
|
|
279
|
+
if (state.common.write) {
|
|
280
|
+
writable++;
|
|
281
|
+
} else {
|
|
282
|
+
readonly++;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
total,
|
|
288
|
+
writable,
|
|
289
|
+
readonly,
|
|
290
|
+
};
|
|
291
|
+
} catch (error) {
|
|
292
|
+
this.adapter.log.error(`Error reading state objects: ${error.message}`);
|
|
293
|
+
return { total: 0, writable: 0, readonly: 0 };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Read host information
|
|
299
|
+
*
|
|
300
|
+
* @returns {Promise<Array>} Array of host objects
|
|
301
|
+
*/
|
|
302
|
+
async readHosts() {
|
|
303
|
+
try {
|
|
304
|
+
const hosts = await this.adapter.getObjectViewAsync('system', 'host', {});
|
|
305
|
+
const result = [];
|
|
306
|
+
let localNpmProbed = false;
|
|
307
|
+
let localNpmVersion = '';
|
|
308
|
+
|
|
309
|
+
const probeLocalNpmOnce = () => {
|
|
310
|
+
if (localNpmProbed) {
|
|
311
|
+
return localNpmVersion;
|
|
312
|
+
}
|
|
313
|
+
localNpmProbed = true;
|
|
314
|
+
try {
|
|
315
|
+
localNpmVersion = execSync('npm -v', {
|
|
316
|
+
encoding: 'utf8',
|
|
317
|
+
windowsHide: true,
|
|
318
|
+
timeout: 8000,
|
|
319
|
+
}).trim();
|
|
320
|
+
} catch {
|
|
321
|
+
localNpmVersion = '';
|
|
322
|
+
}
|
|
323
|
+
return localNpmVersion;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const adapterHost = this.adapter.host || '';
|
|
327
|
+
|
|
328
|
+
for (const obj of hosts.rows) {
|
|
329
|
+
const host = obj.value;
|
|
330
|
+
// getObjectViewAsync may return a stripped native — fetch the full object
|
|
331
|
+
let fullNative = host.native || {};
|
|
332
|
+
try {
|
|
333
|
+
const full = await this.adapter.getForeignObjectAsync(host._id);
|
|
334
|
+
if (full && full.native) {
|
|
335
|
+
fullNative = full.native;
|
|
336
|
+
}
|
|
337
|
+
} catch {
|
|
338
|
+
// fall back to whatever getObjectViewAsync returned
|
|
339
|
+
}
|
|
340
|
+
const osInfo = fullNative.os || {};
|
|
341
|
+
const processInfo = fullNative.process || {};
|
|
342
|
+
const nodeVersion =
|
|
343
|
+
processInfo.version ||
|
|
344
|
+
(processInfo.versions && processInfo.versions.node ? `v${processInfo.versions.node}` : '') ||
|
|
345
|
+
host.common.nodeVersion ||
|
|
346
|
+
'';
|
|
347
|
+
const versions =
|
|
348
|
+
processInfo.versions && typeof processInfo.versions === 'object' ? processInfo.versions : {};
|
|
349
|
+
let npmVersion =
|
|
350
|
+
(versions.npm && String(versions.npm)) ||
|
|
351
|
+
(host.common.npmVersion && String(host.common.npmVersion)) ||
|
|
352
|
+
'';
|
|
353
|
+
const hostMatchesThisMachine =
|
|
354
|
+
(host.common.hostname && host.common.hostname === adapterHost) ||
|
|
355
|
+
(host.common.name && host.common.name === adapterHost);
|
|
356
|
+
// Probe locally if: host matches, OR only one host exists (must be this machine)
|
|
357
|
+
if (!npmVersion && (hostMatchesThisMachine || hosts.rows.length === 1)) {
|
|
358
|
+
npmVersion = probeLocalNpmOnce();
|
|
359
|
+
}
|
|
360
|
+
result.push({
|
|
361
|
+
id: host._id,
|
|
362
|
+
name: host.common.name,
|
|
363
|
+
hostname: host.common.hostname,
|
|
364
|
+
platform: host.common.platform,
|
|
365
|
+
type: host.common.type,
|
|
366
|
+
version: host.common.installedVersion,
|
|
367
|
+
nodeVersion,
|
|
368
|
+
npmVersion,
|
|
369
|
+
osRelease: osInfo.release || '',
|
|
370
|
+
osArch: osInfo.arch || '',
|
|
371
|
+
osType: osInfo.type || osInfo.platform || '',
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return result;
|
|
376
|
+
} catch (error) {
|
|
377
|
+
this.adapter.log.error(`Error reading hosts: ${error.message}`);
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Read rooms (enum.rooms) with their assigned member IDs
|
|
384
|
+
*
|
|
385
|
+
* @returns {Promise<Array>} Array of room objects
|
|
386
|
+
*/
|
|
387
|
+
async readRooms() {
|
|
388
|
+
try {
|
|
389
|
+
const enums = await this.adapter.getObjectViewAsync('system', 'enum', {
|
|
390
|
+
startkey: 'enum.rooms.',
|
|
391
|
+
endkey: 'enum.rooms.\u9999',
|
|
392
|
+
});
|
|
393
|
+
const lang = this.adapter.config.language || 'en';
|
|
394
|
+
const result = [];
|
|
395
|
+
|
|
396
|
+
for (const obj of enums.rows) {
|
|
397
|
+
const room = obj.value;
|
|
398
|
+
result.push({
|
|
399
|
+
id: room._id,
|
|
400
|
+
name: this.resolveI18nString(room.common.name, lang),
|
|
401
|
+
members: room.common.members || [],
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return result;
|
|
406
|
+
} catch (error) {
|
|
407
|
+
this.adapter.log.error(`Error reading rooms: ${error.message}`);
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Read functions (enum.functions) with their assigned member IDs
|
|
414
|
+
*
|
|
415
|
+
* @returns {Promise<Array>} Array of function objects
|
|
416
|
+
*/
|
|
417
|
+
async readFunctions() {
|
|
418
|
+
try {
|
|
419
|
+
const enums = await this.adapter.getObjectViewAsync('system', 'enum', {
|
|
420
|
+
startkey: 'enum.functions.',
|
|
421
|
+
endkey: 'enum.functions.\u9999',
|
|
422
|
+
});
|
|
423
|
+
const lang = this.adapter.config.language || 'en';
|
|
424
|
+
const result = [];
|
|
425
|
+
|
|
426
|
+
for (const obj of enums.rows) {
|
|
427
|
+
const fn = obj.value;
|
|
428
|
+
result.push({
|
|
429
|
+
id: fn._id,
|
|
430
|
+
name: this.resolveI18nString(fn.common.name, lang),
|
|
431
|
+
members: fn.common.members || [],
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return result;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
this.adapter.log.error(`Error reading functions: ${error.message}`);
|
|
438
|
+
return [];
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Read system.config for location and language settings
|
|
444
|
+
*
|
|
445
|
+
* @returns {Promise<object>} System config subset
|
|
446
|
+
*/
|
|
447
|
+
async readSystemConfig() {
|
|
448
|
+
try {
|
|
449
|
+
const obj = await this.adapter.getForeignObjectAsync('system.config');
|
|
450
|
+
if (!obj || !obj.common) {
|
|
451
|
+
return {};
|
|
452
|
+
}
|
|
453
|
+
// activeRepo can be a string ("stable") or array (["stable","beta"]) in newer js-controller
|
|
454
|
+
const rawRepo = obj.common.activeRepo;
|
|
455
|
+
const activeRepo = Array.isArray(rawRepo) ? rawRepo.join(', ') : typeof rawRepo === 'string' ? rawRepo : '';
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
city: obj.common.city || '',
|
|
459
|
+
country: obj.common.country || '',
|
|
460
|
+
language: obj.common.language || 'en',
|
|
461
|
+
latitude: obj.common.latitude || null,
|
|
462
|
+
longitude: obj.common.longitude || null,
|
|
463
|
+
timezone: obj.common.timezone || '',
|
|
464
|
+
activeRepo,
|
|
465
|
+
};
|
|
466
|
+
} catch (e) {
|
|
467
|
+
this.adapter.log.warn(`Could not read system.config: ${e.message}`);
|
|
468
|
+
return {};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Read live resource states for all hosts (RAM, CPU, uptime).
|
|
474
|
+
*
|
|
475
|
+
* @param {Array} hosts Host objects from readHosts()
|
|
476
|
+
* @returns {Promise<object>} Map of hostName → { totalMem, freeMem, cpu, uptime }
|
|
477
|
+
*/
|
|
478
|
+
async readHostResources(hosts) {
|
|
479
|
+
const result = {};
|
|
480
|
+
for (const host of hosts) {
|
|
481
|
+
const hostId = host.id.replace('system.host.', '');
|
|
482
|
+
try {
|
|
483
|
+
const [freemem, totalmem, memRss, memHeapUsed, cpu, uptime] = await Promise.all([
|
|
484
|
+
this.adapter.getForeignStateAsync(`system.host.${hostId}.freemem`).catch(() => null),
|
|
485
|
+
this.adapter.getForeignStateAsync(`system.host.${hostId}.totalmem`).catch(() => null),
|
|
486
|
+
this.adapter.getForeignStateAsync(`system.host.${hostId}.memRss`).catch(() => null),
|
|
487
|
+
this.adapter.getForeignStateAsync(`system.host.${hostId}.memHeapUsed`).catch(() => null),
|
|
488
|
+
this.adapter.getForeignStateAsync(`system.host.${hostId}.cpu`).catch(() => null),
|
|
489
|
+
this.adapter.getForeignStateAsync(`system.host.${hostId}.uptime`).catch(() => null),
|
|
490
|
+
]);
|
|
491
|
+
|
|
492
|
+
// freemem/totalmem/memRss/memHeapUsed are all in MB in ioBroker JS-controller
|
|
493
|
+
const sysFreeMb =
|
|
494
|
+
freemem && freemem.val !== null && freemem.val !== undefined ? Number(freemem.val) : null;
|
|
495
|
+
const sysTotalMb =
|
|
496
|
+
totalmem && totalmem.val !== null && totalmem.val !== undefined && totalmem.val > 0
|
|
497
|
+
? Number(totalmem.val)
|
|
498
|
+
: null;
|
|
499
|
+
// js-controller process RSS — MB, safety check for byte values
|
|
500
|
+
const rawProcMb =
|
|
501
|
+
memRss && memRss.val > 0 ? memRss.val : memHeapUsed && memHeapUsed.val > 0 ? memHeapUsed.val : null;
|
|
502
|
+
const procMb =
|
|
503
|
+
rawProcMb !== null
|
|
504
|
+
? rawProcMb > 100000
|
|
505
|
+
? Math.round(rawProcMb / 1048576)
|
|
506
|
+
: Math.round(rawProcMb)
|
|
507
|
+
: null;
|
|
508
|
+
this.adapter.log.debug(
|
|
509
|
+
`Host ${hostId} resources: freemem=${sysFreeMb} totalmem=${sysTotalMb} procMb=${procMb} cpu=${cpu && cpu.val}`,
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
// Sum memRss of all running adapter instances on this host
|
|
513
|
+
let adapterTotalMb = null;
|
|
514
|
+
try {
|
|
515
|
+
const adapterObjs = await this.adapter
|
|
516
|
+
.getForeignObjectsAsync(`system.adapter.*.*.memRss`, 'state')
|
|
517
|
+
.catch(() => null);
|
|
518
|
+
if (adapterObjs) {
|
|
519
|
+
const ids = Object.keys(adapterObjs);
|
|
520
|
+
let sum = 0;
|
|
521
|
+
let count = 0;
|
|
522
|
+
for (const id of ids) {
|
|
523
|
+
try {
|
|
524
|
+
const s = await this.adapter.getForeignStateAsync(id).catch(() => null);
|
|
525
|
+
if (s && s.val !== null && s.val !== undefined && Number(s.val) > 0) {
|
|
526
|
+
const mb =
|
|
527
|
+
Number(s.val) > 100000
|
|
528
|
+
? Math.round(Number(s.val) / 1048576)
|
|
529
|
+
: Math.round(Number(s.val));
|
|
530
|
+
sum += mb;
|
|
531
|
+
count++;
|
|
532
|
+
}
|
|
533
|
+
} catch {
|
|
534
|
+
/* ignore individual state errors */
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (count > 0) {
|
|
538
|
+
adapterTotalMb = sum;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
} catch {
|
|
542
|
+
/* adapter memory sum optional */
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
result[host.name] = {
|
|
546
|
+
sysFreeMb,
|
|
547
|
+
sysTotalMb,
|
|
548
|
+
procMb, // js-controller process RAM
|
|
549
|
+
adapterTotalMb, // sum of all adapter instance RSS (incl. js-controller)
|
|
550
|
+
cpu: cpu && cpu.val !== null && cpu.val !== undefined ? Number(cpu.val) : null,
|
|
551
|
+
uptime: uptime && uptime.val ? uptime.val : null,
|
|
552
|
+
};
|
|
553
|
+
} catch {
|
|
554
|
+
result[host.name] = {};
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return result;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Read user-defined variables from the 0_userdata.0 namespace.
|
|
562
|
+
* Groups them by folder. Skips objects without common.name.
|
|
563
|
+
*
|
|
564
|
+
* @returns {Promise<Array>} Array of { id, name, folder, type, unit, desc, value }
|
|
565
|
+
*/
|
|
566
|
+
async readUserData() {
|
|
567
|
+
try {
|
|
568
|
+
const objs = await this.adapter.getForeignObjectsAsync('0_userdata.0.*', 'state');
|
|
569
|
+
if (!objs) {
|
|
570
|
+
return [];
|
|
571
|
+
}
|
|
572
|
+
const result = [];
|
|
573
|
+
const lang = this.adapter.config.language || 'en';
|
|
574
|
+
|
|
575
|
+
// Also try to read current values for context
|
|
576
|
+
const ids = Object.keys(objs);
|
|
577
|
+
const states = {};
|
|
578
|
+
// Read in batches of 50 to avoid overload
|
|
579
|
+
for (let i = 0; i < ids.length; i += 50) {
|
|
580
|
+
const batch = ids.slice(i, i + 50);
|
|
581
|
+
try {
|
|
582
|
+
const batchStates = await this.adapter.getForeignStatesAsync(batch.join(','));
|
|
583
|
+
Object.assign(states, batchStates || {});
|
|
584
|
+
} catch {
|
|
585
|
+
// ignore batch errors
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
for (const [id, obj] of Object.entries(objs)) {
|
|
590
|
+
if (!obj || !obj.common) {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const nameParts = id.replace('0_userdata.0.', '').split('.');
|
|
594
|
+
const name = this.resolveI18nString(obj.common.name, lang) || nameParts[nameParts.length - 1];
|
|
595
|
+
const folder = nameParts.length > 1 ? nameParts.slice(0, -1).join('/') : null;
|
|
596
|
+
const stateVal = states[id];
|
|
597
|
+
result.push({
|
|
598
|
+
id,
|
|
599
|
+
name,
|
|
600
|
+
folder,
|
|
601
|
+
type: obj.common.type || 'mixed',
|
|
602
|
+
unit: obj.common.unit || '',
|
|
603
|
+
desc: this.resolveI18nString(obj.common.desc, lang) || '',
|
|
604
|
+
role: obj.common.role || '',
|
|
605
|
+
value: stateVal && stateVal.val !== undefined ? stateVal.val : null,
|
|
606
|
+
lastChange: stateVal && stateVal.ts ? new Date(stateVal.ts).toISOString() : null,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
// Sort by folder then name
|
|
610
|
+
result.sort((a, b) => {
|
|
611
|
+
const fa = a.folder || '';
|
|
612
|
+
const fb = b.folder || '';
|
|
613
|
+
if (fa !== fb) {
|
|
614
|
+
return fa.localeCompare(fb);
|
|
615
|
+
}
|
|
616
|
+
return a.name.localeCompare(b.name);
|
|
617
|
+
});
|
|
618
|
+
return result;
|
|
619
|
+
} catch (e) {
|
|
620
|
+
this.adapter.log.debug(`Could not read 0_userdata.0: ${e.message}`);
|
|
621
|
+
return [];
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Read alias datapoints from the alias.0 namespace.
|
|
627
|
+
* Extracts name, folder, type, and the read/write target IDs.
|
|
628
|
+
*
|
|
629
|
+
* @returns {Promise<Array>} Array of { id, name, folder, type, readTarget, writeTarget, desc }
|
|
630
|
+
*/
|
|
631
|
+
async readAliases() {
|
|
632
|
+
try {
|
|
633
|
+
const objs = await this.adapter.getForeignObjectsAsync('alias.0.*', 'state');
|
|
634
|
+
if (!objs) {
|
|
635
|
+
return [];
|
|
636
|
+
}
|
|
637
|
+
const result = [];
|
|
638
|
+
const lang = this.adapter.config.language || 'en';
|
|
639
|
+
|
|
640
|
+
for (const [id, obj] of Object.entries(objs)) {
|
|
641
|
+
if (!obj || !obj.common) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const nameParts = id.replace('alias.0.', '').split('.');
|
|
645
|
+
const name = this.resolveI18nString(obj.common.name, lang) || nameParts[nameParts.length - 1];
|
|
646
|
+
const folder = nameParts.length > 1 ? nameParts.slice(0, -1).join('/') : null;
|
|
647
|
+
|
|
648
|
+
// common.alias.id can be a string or { read, write }
|
|
649
|
+
let readTarget = null;
|
|
650
|
+
let writeTarget = null;
|
|
651
|
+
if (obj.common.alias) {
|
|
652
|
+
const aliasId = obj.common.alias.id;
|
|
653
|
+
if (typeof aliasId === 'string') {
|
|
654
|
+
readTarget = aliasId;
|
|
655
|
+
writeTarget = aliasId;
|
|
656
|
+
} else if (aliasId && typeof aliasId === 'object') {
|
|
657
|
+
readTarget = aliasId.read || null;
|
|
658
|
+
writeTarget = aliasId.write || null;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
result.push({
|
|
663
|
+
id,
|
|
664
|
+
name,
|
|
665
|
+
folder,
|
|
666
|
+
type: obj.common.type || '—',
|
|
667
|
+
unit: obj.common.unit || '',
|
|
668
|
+
desc: this.resolveI18nString(obj.common.desc, lang) || '',
|
|
669
|
+
role: obj.common.role || '',
|
|
670
|
+
readTarget,
|
|
671
|
+
writeTarget,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
result.sort((a, b) => {
|
|
676
|
+
const fa = a.folder || '';
|
|
677
|
+
const fb = b.folder || '';
|
|
678
|
+
if (fa !== fb) {
|
|
679
|
+
return fa.localeCompare(fb);
|
|
680
|
+
}
|
|
681
|
+
return a.name.localeCompare(b.name);
|
|
682
|
+
});
|
|
683
|
+
return result;
|
|
684
|
+
} catch (e) {
|
|
685
|
+
this.adapter.log.debug(`Could not read alias.0: ${e.message}`);
|
|
686
|
+
return [];
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Resolve member objects for all rooms and return a map of memberId → device info
|
|
692
|
+
*
|
|
693
|
+
* @param {Array} rooms Array of room objects from readRooms()
|
|
694
|
+
* @returns {Promise<object>} Map of memberId → { deviceId, deviceName, role, type, unit }
|
|
695
|
+
*/
|
|
696
|
+
async resolveRoomDevices(rooms) {
|
|
697
|
+
// collect unique member IDs
|
|
698
|
+
const allMembers = new Set();
|
|
699
|
+
for (const room of rooms) {
|
|
700
|
+
for (const memberId of room.members) {
|
|
701
|
+
allMembers.add(memberId);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const result = {};
|
|
706
|
+
for (const memberId of allMembers) {
|
|
707
|
+
try {
|
|
708
|
+
// Try the member object itself first
|
|
709
|
+
const obj = await this.adapter.getForeignObjectAsync(memberId);
|
|
710
|
+
if (obj && obj.common) {
|
|
711
|
+
const name =
|
|
712
|
+
this.resolveI18nString(obj.common.name, this.adapter.config.language || 'en') ||
|
|
713
|
+
memberId.split('.').pop();
|
|
714
|
+
result[memberId] = {
|
|
715
|
+
deviceId: memberId,
|
|
716
|
+
deviceName: name,
|
|
717
|
+
role: obj.common.role || '',
|
|
718
|
+
type: obj.common.type || '',
|
|
719
|
+
unit: obj.common.unit || '',
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
} catch {
|
|
723
|
+
// silently skip unresolvable members
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Find a state id under a room member (channel/device/state) that carries a live-relevant role.
|
|
731
|
+
*
|
|
732
|
+
* @param {string} memberId Room enum member id
|
|
733
|
+
* @param {{ role?: string }} device Entry from resolveRoomDevices()
|
|
734
|
+
* @returns {Promise<string|null>} State id to pass to getForeignStateAsync, or null
|
|
735
|
+
*/
|
|
736
|
+
async resolveLiveDatapointForRoomMember(memberId, device) {
|
|
737
|
+
const role = device && device.role ? device.role : '';
|
|
738
|
+
if (isReadableLiveRole(role)) {
|
|
739
|
+
return memberId;
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
const res = await this.adapter.getObjectViewAsync('system', 'state', {
|
|
743
|
+
startkey: `${memberId}.`,
|
|
744
|
+
endkey: `${memberId}.\u9999`,
|
|
745
|
+
});
|
|
746
|
+
const candidates = [];
|
|
747
|
+
for (const row of res.rows || []) {
|
|
748
|
+
const o = row.value;
|
|
749
|
+
if (!o || o.type !== 'state') {
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
const id = row.id || o._id;
|
|
753
|
+
const r = o.common && o.common.role ? o.common.role : '';
|
|
754
|
+
if (!id || !isReadableLiveRole(r)) {
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
candidates.push({ id, role: r });
|
|
758
|
+
}
|
|
759
|
+
if (candidates.length === 0) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
candidates.sort((a, b) => liveRoleSortIndex(a.role) - liveRoleSortIndex(b.role));
|
|
763
|
+
return candidates[0].id;
|
|
764
|
+
} catch (e) {
|
|
765
|
+
this.adapter.log.debug(`resolveLiveDatapointForRoomMember(${memberId}): ${e.message}`);
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Optionally read live state values for key roles (opt-in via config.readLiveStates)
|
|
772
|
+
*
|
|
773
|
+
* @param {object} deviceMap Map from resolveRoomDevices()
|
|
774
|
+
* @returns {Promise<object>} Map of memberId → { val, ts }
|
|
775
|
+
*/
|
|
776
|
+
async readLiveStates(deviceMap) {
|
|
777
|
+
if (!this.adapter.config.readLiveStates) {
|
|
778
|
+
return {};
|
|
779
|
+
}
|
|
780
|
+
const result = {};
|
|
781
|
+
const resolvedCache = new Map();
|
|
782
|
+
|
|
783
|
+
for (const [memberId, device] of Object.entries(deviceMap)) {
|
|
784
|
+
try {
|
|
785
|
+
if (!resolvedCache.has(memberId)) {
|
|
786
|
+
const sid = await this.resolveLiveDatapointForRoomMember(memberId, device);
|
|
787
|
+
resolvedCache.set(memberId, sid || null);
|
|
788
|
+
}
|
|
789
|
+
const stateId = resolvedCache.get(memberId);
|
|
790
|
+
if (!stateId) {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const state = await this.adapter.getForeignStateAsync(stateId);
|
|
794
|
+
if (state !== null && state !== undefined) {
|
|
795
|
+
result[memberId] = { val: state.val, ts: state.ts };
|
|
796
|
+
}
|
|
797
|
+
} catch {
|
|
798
|
+
// skip
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const n = Object.keys(result).length;
|
|
803
|
+
if (n > 0) {
|
|
804
|
+
this.adapter.log.debug(`readLiveStates: ${n} room member(s) with live values (opt-in)`);
|
|
805
|
+
}
|
|
806
|
+
return result;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Read scripts from script.js.* namespace
|
|
811
|
+
*
|
|
812
|
+
* @returns {Promise<Array>} Array of script objects
|
|
813
|
+
*/
|
|
814
|
+
async readScripts() {
|
|
815
|
+
try {
|
|
816
|
+
const scripts = await this.adapter.getObjectViewAsync('script', 'javascript', {});
|
|
817
|
+
const result = [];
|
|
818
|
+
|
|
819
|
+
for (const obj of scripts.rows) {
|
|
820
|
+
const script = obj.value;
|
|
821
|
+
if (!script || !script._id) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Derive a readable name from the object id: script.js.Folder.MyScript → Folder / MyScript
|
|
826
|
+
const idParts = script._id.replace('script.js.', '').split('.');
|
|
827
|
+
const name = script.common.name || idParts[idParts.length - 1] || script._id;
|
|
828
|
+
const folder = idParts.length > 1 ? idParts.slice(0, -1).join('/') : null;
|
|
829
|
+
|
|
830
|
+
result.push({
|
|
831
|
+
id: script._id,
|
|
832
|
+
name,
|
|
833
|
+
folder,
|
|
834
|
+
enabled: script.common.enabled !== false,
|
|
835
|
+
engineType: script.common.engineType || 'Javascript/js',
|
|
836
|
+
/** e.g. javascript.0 — which script engine runs this script */
|
|
837
|
+
engine: script.common.engine || '',
|
|
838
|
+
schedule: script.common.schedule || '',
|
|
839
|
+
/** ioBroker object schema: optional "group purpose description", not a required per-script user manual */
|
|
840
|
+
desc: script.common.desc || '',
|
|
841
|
+
source: script.common.source || '',
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return result;
|
|
846
|
+
} catch (error) {
|
|
847
|
+
this.adapter.log.warn(`Error reading scripts (script adapter may not be installed): ${error.message}`);
|
|
848
|
+
return [];
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Check how many adapters have updates available.
|
|
854
|
+
* Reads system.adapter.<name> objects and compares installedVersion to version from npm (if available).
|
|
855
|
+
* Fallback: counts adapters where common.installedVersion differs from common.version.
|
|
856
|
+
*
|
|
857
|
+
* @param {Array} instances Adapter instances already discovered
|
|
858
|
+
* @returns {Promise<number>} Count of adapters with pending updates
|
|
859
|
+
*/
|
|
860
|
+
async readPendingUpdates(instances) {
|
|
861
|
+
try {
|
|
862
|
+
const adapterNames = [...new Set(instances.map(i => i.adapter))];
|
|
863
|
+
let updateCount = 0;
|
|
864
|
+
for (const name of adapterNames) {
|
|
865
|
+
try {
|
|
866
|
+
const obj = await this.adapter.getForeignObjectAsync(`system.adapter.${name}`);
|
|
867
|
+
if (obj && obj.common) {
|
|
868
|
+
const installed = obj.common.installedVersion || obj.common.version || '';
|
|
869
|
+
const available = obj.common.latestVersion || '';
|
|
870
|
+
if (available && installed && available !== installed) {
|
|
871
|
+
updateCount++;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
} catch {
|
|
875
|
+
// ignore per-adapter errors
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return updateCount;
|
|
879
|
+
} catch (e) {
|
|
880
|
+
this.adapter.log.debug(`Could not read pending updates: ${e.message}`);
|
|
881
|
+
return 0;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Try to read BackItUp last backup timestamp.
|
|
887
|
+
*
|
|
888
|
+
* @returns {Promise<string|null>} ISO string of last backup or null
|
|
889
|
+
*/
|
|
890
|
+
async readLastBackup() {
|
|
891
|
+
try {
|
|
892
|
+
const state = await this.adapter.getForeignStateAsync('backitup.0.info.lastBackup');
|
|
893
|
+
if (state && state.val) {
|
|
894
|
+
return String(state.val);
|
|
895
|
+
}
|
|
896
|
+
} catch {
|
|
897
|
+
// BackItUp may not be installed
|
|
898
|
+
}
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Collect all raw system data
|
|
904
|
+
*
|
|
905
|
+
* @returns {Promise<object>} Raw system data
|
|
906
|
+
*/
|
|
907
|
+
async collectRawData() {
|
|
908
|
+
const [instances, stateSummary, hosts, rooms, functions, scripts, systemConfig] = await Promise.all([
|
|
909
|
+
this.readAdapterInstances(),
|
|
910
|
+
this.readStateObjectsSummary(),
|
|
911
|
+
this.readHosts(),
|
|
912
|
+
this.readRooms(),
|
|
913
|
+
this.readFunctions(),
|
|
914
|
+
this.readScripts(),
|
|
915
|
+
this.readSystemConfig(),
|
|
916
|
+
]);
|
|
917
|
+
|
|
918
|
+
// Device resolution and live states depend on rooms → sequential
|
|
919
|
+
const deviceMap = await this.resolveRoomDevices(rooms);
|
|
920
|
+
const liveStates = await this.readLiveStates(deviceMap);
|
|
921
|
+
|
|
922
|
+
// Optional extras — soft failures allowed, all in parallel
|
|
923
|
+
const [pendingUpdates, lastBackup, hostResources, userData, aliases, scheduleObjects] = await Promise.all([
|
|
924
|
+
this.readPendingUpdates(instances),
|
|
925
|
+
this.readLastBackup(),
|
|
926
|
+
this.readHostResources(hosts),
|
|
927
|
+
this.readUserData(),
|
|
928
|
+
this.readAliases(),
|
|
929
|
+
this.readScheduleDesignObjects(),
|
|
930
|
+
]);
|
|
931
|
+
|
|
932
|
+
return {
|
|
933
|
+
instances,
|
|
934
|
+
stateSummary,
|
|
935
|
+
hosts,
|
|
936
|
+
rooms,
|
|
937
|
+
functions,
|
|
938
|
+
scripts,
|
|
939
|
+
systemConfig,
|
|
940
|
+
deviceMap,
|
|
941
|
+
liveStates,
|
|
942
|
+
pendingUpdates,
|
|
943
|
+
lastBackup,
|
|
944
|
+
hostResources,
|
|
945
|
+
userData,
|
|
946
|
+
aliases,
|
|
947
|
+
scheduleObjects,
|
|
948
|
+
collectedAt: new Date().toISOString(),
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
module.exports = Discovery;
|