iobroker.tidy 0.0.1

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/admin/tidy.svg ADDED
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg width="256" height="256" viewBox="0 0 256 256" version="1.1" id="svg14" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><defs id="defs2"><linearGradient id="bg" x1="0" y1="0" x2="256" y2="256" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-color="#2c3e50" id="stop1" /><stop offset="100%" stop-color="#4ca1af" id="stop2" /></linearGradient></defs><!-- Hintergrund --><rect x="0" y="0" width="256" height="256" rx="40" fill="url(#bg)" id="rect2" style="fill:url(#bg)" /><!-- Datenpunkte --><g id="g16" transform="translate(-5.6420517,-4)"><rect x="42.403702" y="21.998047" width="30" height="30" rx="6" id="rect3" style="opacity:0.9;fill:white" /><rect x="82.403702" y="21.998047" width="142.4767" height="30" id="rect7-9" style="opacity:0.9;fill:white;stroke-width:1.05371" rx="6.3222833" /><rect x="42.403702" y="58.233414" width="30" height="30" rx="6" id="rect3-8" style="opacity:0.9;fill:white" /><rect x="82.403702" y="58.233414" width="142.4767" height="30" rx="6.3222833" id="rect7-9-2" style="opacity:0.9;fill:white;fill-opacity:1;stroke-width:1.05371" /><rect x="42.403702" y="94.468781" width="30" height="30" rx="6" id="rect3-5" style="opacity:0.9;fill:white" /><rect x="82.403702" y="94.468781" width="142.4767" height="30" rx="6.3222833" id="rect7-9-1" style="opacity:0.9;fill:white;fill-opacity:0.5;stroke-width:1.05371" /></g><g id="g18" transform="matrix(1.1304153,0,0,1.1304153,-30.735776,-18.061617)"><path style="fill:#0d4049;stroke-width:0.227137" d="m 193.04979,217.21126 c 2.18052,2.18052 3.08907,4.72446 5.08788,2.54394 l 36.70541,-36.5237 c 2.18052,-2.18052 -0.36342,-2.90736 -2.54394,-5.08788 l -11.08431,-3.27078 c -2.18052,-2.18052 -5.63301,-2.18052 -7.63182,0 l -23.804,23.804 c -2.18052,2.18052 -2.18052,5.63301 0,7.63182 z" id="path1" /><path style="fill:#073135;stroke-width:0.227137" d="m 193.04979,217.21126 c 2.18052,2.18052 3.08907,4.72446 5.08788,2.54394 l 36.70541,-36.5237 c 2.18052,-2.18052 -0.36342,-2.90736 -2.54394,-5.08788" id="path2" /><g id="g4" transform="matrix(-0.22713746,0,0,0.22713746,235.67622,107.82186)">
3
+ <path style="fill:#c19a6b" d="m 187.668,331.2 c -6.4,6.4 -16,6.4 -22.4,0 v 0 c -6.4,-6.4 -6.4,-16 0,-22.4 l 304,-304 c 6.4,-6.4 16,-6.4 22.4,0 v 0 c 6.4,6.4 6.4,16 0,22.4 z" id="path3" />
4
+ <path style="fill:#c19a6b" d="m 212.468,400.8 c -2.4,2.4 -7.2,2.4 -9.6,0 L 95.668,293.6 c -2.4,-2.4 -2.4,-7.2 0,-9.6 l 16.8,-16.8 c 2.4,-2.4 7.2,-2.4 9.6,0 l 106.4,106.4 c 2.4,2.4 2.4,7.2 0,9.6 z" id="path4" />
5
+ </g><path style="fill:#af895f;stroke-width:0.227137" d="m 183.78258,192.86213 c -0.54513,0.54512 -0.54513,1.63538 0,2.18051 l 3.81591,3.81591 c 0.54513,0.54513 1.63539,0.54513 2.18052,0 l 24.16742,-24.34913 c 0.54513,-0.54513 0.54513,-1.63539 0,-2.18052" id="path5" /></g><!-- BESEN (realistischer) --><!-- Checkmark --><!-- Text --><path d="M 66.247429,136.91536 112.18185,182.84977 222.42446,63.42029" stroke="#2ecc71" stroke-width="18.3738" fill="none" stroke-linecap="round" stroke-linejoin="round" id="path14" /><text xml:space="preserve" style="font-size:84.0845px;line-height:1.25;font-family:sans-serif;fill:white;fill-opacity:0.8;stroke-width:2.10211" x="37.016853" y="235.58179" id="text16"><tspan id="tspan16" x="37.016853" y="235.58179" style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'Samsung Sharp Sans';-inkscape-font-specification:'Samsung Sharp Sans Medium';stroke-width:2.10211">T</tspan></text><text xml:space="preserve" style="font-size:53.4011px;line-height:1.25;font-family:sans-serif;fill:white;fill-opacity:0.8;stroke-width:1.33502" x="70.553688" y="235.58179" id="text16-5"><tspan id="tspan16-1" x="70.553688" y="235.58179" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'Samsung Sharp Sans';-inkscape-font-specification:'Samsung Sharp Sans Bold';stroke-width:1.33502">idy</tspan></text></svg>
@@ -0,0 +1,112 @@
1
+ {
2
+ "common": {
3
+ "name": "tidy",
4
+ "version": "0.0.1",
5
+ "news": {
6
+ "0.0.1": {
7
+ "en": "initial release",
8
+ "de": "Erstveröffentlichung",
9
+ "ru": "Начальная версия",
10
+ "pt": "lançamento inicial",
11
+ "nl": "Eerste uitgave",
12
+ "fr": "Première version",
13
+ "it": "Versione iniziale",
14
+ "es": "Versión inicial",
15
+ "pl": "Pierwsze wydanie",
16
+ "uk": "Початкова версія",
17
+ "zh-cn": "首次出版"
18
+ }
19
+ },
20
+ "titleLang": {
21
+ "en": "Tidy",
22
+ "de": "Tidy",
23
+ "ru": "Tidy",
24
+ "pt": "Tidy",
25
+ "nl": "Tidy",
26
+ "fr": "Tidy",
27
+ "it": "Tidy",
28
+ "es": "Tidy",
29
+ "pl": "Tidy",
30
+ "uk": "Tidy",
31
+ "zh-cn": "Tidy"
32
+ },
33
+ "desc": {
34
+ "en": "Helps to find unused objects and states to clean up your system.",
35
+ "de": "Hilft dabei, ungenutzte Objekte und Zustände zu finden, um dein System aufzuräumen.",
36
+ "ru": "Помогает находить неиспользуемые объекты и состояния для очистки системы.",
37
+ "pt": "Ajuda você a encontrar objetos e estados não utilizados para limpar seu sistema.",
38
+ "nl": "Helpt je ongebruikte objecten en statussen te vinden om je systeem op te schonen.",
39
+ "fr": "Vous aide à trouver les objets et états inutilisés afin de nettoyer votre système.",
40
+ "it": "Ti aiuta a trovare oggetti e stati inutilizzati per ripulire il sistema.",
41
+ "es": "Te ayuda a encontrar objetos y estados no utilizados para limpiar tu sistema.",
42
+ "pl": "Pomaga znaleźć nieużywane obiekty i stany, dzięki czemu możesz oczyścić system.",
43
+ "uk": "Допомагає знайти невикористовувані об'єкти та стани для очищення системи.",
44
+ "zh-cn": "帮助您查找未使用的对象和状态,以清理您的系统。"
45
+ },
46
+ "authors": [
47
+ "skvarel <skvarel@inventwo.com>"
48
+ ],
49
+ "keywords": [
50
+ "cleanup",
51
+ "tidy",
52
+ "unused"
53
+ ],
54
+ "licenseInformation": {
55
+ "type": "free",
56
+ "license": "MIT"
57
+ },
58
+ "platform": "Javascript/Node.js",
59
+ "icon": "tidy.svg",
60
+ "enabled": true,
61
+ "extIcon": "https://raw.githubusercontent.com/inventwo/ioBroker.tidy/main/admin/tidy.svg",
62
+ "readme": "https://github.com/inventwo/ioBroker.tidy/blob/main/README.md",
63
+ "loglevel": "info",
64
+ "tier": 3,
65
+ "mode": "daemon",
66
+ "type": "storage",
67
+ "compact": true,
68
+ "connectionType": "local",
69
+ "dataSource": "poll",
70
+ "adminUI": {
71
+ "config": "json"
72
+ },
73
+ "dependencies": [
74
+ {
75
+ "js-controller": ">=6.0.11"
76
+ }
77
+ ],
78
+ "globalDependencies": [
79
+ {
80
+ "admin": ">=7.6.20"
81
+ }
82
+ ]
83
+ },
84
+ "native": {
85
+ "autoScan": false,
86
+ "scanInterval": 24,
87
+ "daysUntilStale": 90,
88
+ "daysUntilDead": 365,
89
+ "paths": [
90
+ {
91
+ "enabled": true,
92
+ "path": "0_userdata.0",
93
+ "name": "userdata",
94
+ "checkAliasTargets": false
95
+ }
96
+ ],
97
+ "fieldDocs": [
98
+ {"pos": "01", "field": "id", "description": "Full datapoint path", "purpose": "Unique identification"},
99
+ {"pos": "02", "field": "name", "description": "common.name or last part of ID", "purpose": "User-friendly name"},
100
+ {"pos": "03", "field": "last_ts", "description": "Unix timestamp (ms) or null", "purpose": "Sorting in background"},
101
+ {"pos": "04", "field": "last_ts_iso", "description": "Formatted date string", "purpose": "Display in table"},
102
+ {"pos": "05", "field": "value", "description": "Current datapoint value", "purpose": "Final check before deletion"},
103
+ {"pos": "06", "field": "status", "description": "active, dead, stale, undefined, orphaned", "purpose": "Classification (English)"},
104
+ {"pos": "07", "field": "status_de", "description": "aktiv, inaktiv, veraltet, undefiniert, verwaist", "purpose": "Classification (German)"},
105
+ {"pos": "08", "field": "issue", "description": "dead, stale, orphaned_alias, or null", "purpose": "Filter criterion (null = OK)"},
106
+ {"pos": "09", "field": "issue_de", "description": "inaktiv, veraltet, verwaistes Alias, or null", "purpose": "Filter criterion (German)"},
107
+ {"pos": "10", "field": "size", "description": "JSON.stringify(val).length", "purpose": "Finds \"storage hogs\""}
108
+ ]
109
+ },
110
+ "objects": [],
111
+ "instanceObjects": []
112
+ }
@@ -0,0 +1,19 @@
1
+ // This file extends the AdapterConfig type from "@iobroker/types"
2
+ // using the actual properties present in io-package.json
3
+ // in order to provide typings for adapter.config properties
4
+
5
+ import { native } from '../io-package.json';
6
+
7
+ type _AdapterConfig = typeof native;
8
+
9
+ // Augment the globally declared type ioBroker.AdapterConfig
10
+ declare global {
11
+ namespace ioBroker {
12
+ interface AdapterConfig extends _AdapterConfig {
13
+ // Do not enter anything here!
14
+ }
15
+ }
16
+ }
17
+
18
+ // this is required so the above AdapterConfig is found by TypeScript / type checking
19
+ export {};
package/main.js ADDED
@@ -0,0 +1,439 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * Created with @iobroker/create-adapter v3.1.2
5
+ */
6
+
7
+ // The adapter-core module gives you access to the core ioBroker functions
8
+ // you need to create an adapter
9
+ const utils = require('@iobroker/adapter-core');
10
+
11
+ class Tidy extends utils.Adapter {
12
+ /**
13
+ * @param {Partial<utils.AdapterOptions>} [options] - Adapter options
14
+ */
15
+ constructor(options) {
16
+ super({
17
+ ...options,
18
+ name: 'tidy',
19
+ });
20
+ this.on('ready', this.onReady.bind(this));
21
+ this.on('stateChange', this.onStateChange.bind(this));
22
+ this.on('unload', this.onUnload.bind(this));
23
+
24
+ this.scanInterval = undefined;
25
+ }
26
+
27
+ /**
28
+ * Is called when databases are connected and adapter received configuration.
29
+ */
30
+ async onReady() {
31
+ this.log.info('Starting Tidy adapter...');
32
+
33
+ // Validate configuration
34
+ if (!this.config.paths || !Array.isArray(this.config.paths)) {
35
+ this.log.error('No paths configured! Please configure at least one path to scan.');
36
+ return;
37
+ }
38
+
39
+ // Create objects for each configured path
40
+ await this.createPathObjects();
41
+
42
+ // Subscribe to trigger states
43
+ this.subscribeStates('*.trigger');
44
+
45
+ // Run initial scan for all enabled paths
46
+ await this.scanAllPaths();
47
+
48
+ // Setup automatic scanning if enabled
49
+ if (this.config.autoScan && this.config.scanInterval > 0) {
50
+ const intervalMs = this.config.scanInterval * 60 * 60 * 1000; // Convert hours to milliseconds
51
+ this.log.info(`Automatic scanning enabled: Every ${this.config.scanInterval} hour(s)`);
52
+ this.scanInterval = setInterval(async () => {
53
+ this.log.info('Running automatic scan...');
54
+ await this.scanAllPaths();
55
+ }, intervalMs);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Is called when adapter shuts down - callback has to be called under any circumstances!
61
+ *
62
+ * @param {() => void} callback - Callback function
63
+ */
64
+ onUnload(callback) {
65
+ try {
66
+ // Clear automatic scan interval
67
+ if (this.scanInterval) {
68
+ clearInterval(this.scanInterval);
69
+ this.scanInterval = undefined;
70
+ }
71
+
72
+ callback();
73
+ } catch (error) {
74
+ this.log.error(`Error during unloading: ${error.message}`);
75
+ callback();
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Create channel and states for each configured path
81
+ */
82
+ async createPathObjects() {
83
+ for (const pathConfig of this.config.paths) {
84
+ if (!pathConfig.enabled || !pathConfig.name) {
85
+ continue;
86
+ }
87
+
88
+ const channelId = this.sanitizeName(pathConfig.name);
89
+
90
+ // Create channel
91
+ await this.setObjectNotExistsAsync(channelId, {
92
+ type: 'channel',
93
+ common: {
94
+ name: `Scan results for ${pathConfig.path}`,
95
+ },
96
+ native: {},
97
+ });
98
+
99
+ // Create trigger state
100
+ await this.setObjectNotExistsAsync(`${channelId}.trigger`, {
101
+ type: 'state',
102
+ common: {
103
+ name: 'Trigger scan',
104
+ type: 'boolean',
105
+ role: 'button',
106
+ read: true,
107
+ write: true,
108
+ def: false,
109
+ },
110
+ native: {},
111
+ });
112
+
113
+ // Create result state
114
+ await this.setObjectNotExistsAsync(`${channelId}.result`, {
115
+ type: 'state',
116
+ common: {
117
+ name: 'Scan result (JSON table)',
118
+ type: 'string',
119
+ role: 'json',
120
+ read: true,
121
+ write: false,
122
+ def: '[]',
123
+ },
124
+ native: {},
125
+ });
126
+
127
+ // Create last scan timestamp
128
+ await this.setObjectNotExistsAsync(`${channelId}.lastScan`, {
129
+ type: 'state',
130
+ common: {
131
+ name: 'Last scan timestamp',
132
+ type: 'number',
133
+ role: 'value.time',
134
+ read: true,
135
+ write: false,
136
+ def: 0,
137
+ },
138
+ native: {},
139
+ });
140
+
141
+ // Create count states
142
+ await this.setObjectNotExistsAsync(`${channelId}.count`, {
143
+ type: 'state',
144
+ common: {
145
+ name: 'Total datapoints found',
146
+ type: 'number',
147
+ role: 'value',
148
+ read: true,
149
+ write: false,
150
+ def: 0,
151
+ },
152
+ native: {},
153
+ });
154
+
155
+ await this.setObjectNotExistsAsync(`${channelId}.deadCount`, {
156
+ type: 'state',
157
+ common: {
158
+ name: 'Dead datapoints',
159
+ type: 'number',
160
+ role: 'value',
161
+ read: true,
162
+ write: false,
163
+ def: 0,
164
+ },
165
+ native: {},
166
+ });
167
+
168
+ await this.setObjectNotExistsAsync(`${channelId}.staleCount`, {
169
+ type: 'state',
170
+ common: {
171
+ name: 'Stale datapoints',
172
+ type: 'number',
173
+ role: 'value',
174
+ read: true,
175
+ write: false,
176
+ def: 0,
177
+ },
178
+ native: {},
179
+ });
180
+
181
+ await this.setObjectNotExistsAsync(`${channelId}.orphanedCount`, {
182
+ type: 'state',
183
+ common: {
184
+ name: 'Orphaned aliases',
185
+ type: 'number',
186
+ role: 'value',
187
+ read: true,
188
+ write: false,
189
+ def: 0,
190
+ },
191
+ native: {},
192
+ });
193
+ }
194
+ }
195
+
196
+ // If you need to react to object changes, uncomment the following block and the corresponding line in the constructor.
197
+ // You also need to subscribe to the objects with `this.subscribeObjects`, similar to `this.subscribeStates`.
198
+ // /**
199
+ // * Is called if a subscribed object changes
200
+ // * @param {string} id
201
+ // * @param {ioBroker.Object | null | undefined} obj
202
+ // */
203
+ // onObjectChange(id, obj) {
204
+ // if (obj) {
205
+ // // The object was changed
206
+ // this.log.info(`object ${id} changed: ${JSON.stringify(obj)}`);
207
+ // } else {
208
+ // // The object was deleted
209
+ // this.log.info(`object ${id} deleted`);
210
+ // }
211
+ // }
212
+
213
+ /**
214
+ * Is called if a subscribed state changes
215
+ *
216
+ * @param {string} id - State ID
217
+ * @param {ioBroker.State | null | undefined} state - State object
218
+ */
219
+ async onStateChange(id, state) {
220
+ if (state && !state.ack && state.val === true && id.endsWith('.trigger')) {
221
+ // Trigger button was pressed
222
+ this.log.info(`Manual scan triggered for ${id}`);
223
+
224
+ // Find the corresponding path config
225
+ const channelId = id.replace(`${this.namespace}.`, '').replace('.trigger', '');
226
+ const pathConfig = this.config.paths.find(p => this.sanitizeName(p.name) === channelId);
227
+
228
+ if (pathConfig && pathConfig.enabled) {
229
+ await this.scanPath(pathConfig);
230
+ }
231
+
232
+ // Reset trigger
233
+ await this.setStateAsync(id, false, true);
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Scan all enabled paths
239
+ */
240
+ async scanAllPaths() {
241
+ for (const pathConfig of this.config.paths) {
242
+ if (pathConfig.enabled) {
243
+ await this.scanPath(pathConfig);
244
+ }
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Scan a single path for datapoints
250
+ *
251
+ * @param {object} pathConfig - Path configuration object
252
+ */
253
+ async scanPath(pathConfig) {
254
+ const startTime = Date.now();
255
+ this.log.info(`Scanning path: ${pathConfig.path}`);
256
+
257
+ try {
258
+ const channelId = this.sanitizeName(pathConfig.name);
259
+ const results = [];
260
+
261
+ // Get all objects under the specified path
262
+ const pattern = `${pathConfig.path}.*`;
263
+ const objects = await this.getForeignObjectsAsync(pattern, 'state');
264
+
265
+ this.log.debug(`Found ${Object.keys(objects).length} objects under ${pathConfig.path}`);
266
+
267
+ // Analyze each object
268
+ for (const [id, obj] of Object.entries(objects)) {
269
+ if (!obj || obj.type !== 'state') {
270
+ continue;
271
+ }
272
+
273
+ const state = await this.getForeignStateAsync(id);
274
+ const analysis = await this.analyzeDatapoint(id, obj, state, pathConfig);
275
+
276
+ if (analysis) {
277
+ results.push(analysis);
278
+ }
279
+ }
280
+
281
+ // Sort results by timestamp (oldest first)
282
+ results.sort((a, b) => {
283
+ if (a.last_ts === null) {
284
+ return -1;
285
+ }
286
+ if (b.last_ts === null) {
287
+ return 1;
288
+ }
289
+ return a.last_ts - b.last_ts;
290
+ });
291
+
292
+ // Count issues
293
+ const counts = {
294
+ total: results.length,
295
+ dead: results.filter(r => r.issue === 'dead').length,
296
+ stale: results.filter(r => r.issue === 'stale').length,
297
+ orphaned: results.filter(r => r.issue === 'orphaned_alias').length,
298
+ };
299
+
300
+ // Store results
301
+ await this.setStateAsync(`${channelId}.result`, JSON.stringify(results), true);
302
+ await this.setStateAsync(`${channelId}.lastScan`, Date.now(), true);
303
+ await this.setStateAsync(`${channelId}.count`, counts.total, true);
304
+ await this.setStateAsync(`${channelId}.deadCount`, counts.dead, true);
305
+ await this.setStateAsync(`${channelId}.staleCount`, counts.stale, true);
306
+ await this.setStateAsync(`${channelId}.orphanedCount`, counts.orphaned, true);
307
+
308
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
309
+ this.log.info(
310
+ `Scan completed for ${pathConfig.path}: ${counts.total} datapoints ` +
311
+ `(${counts.dead} dead, ${counts.stale} stale, ${counts.orphaned} orphaned) in ${duration}s`,
312
+ );
313
+ } catch (error) {
314
+ this.log.error(`Error scanning path ${pathConfig.path}: ${error.message}`);
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Analyze a single datapoint
320
+ *
321
+ * @param {string} id - Datapoint ID
322
+ * @param {ioBroker.Object} obj - Object definition
323
+ * @param {ioBroker.State | null | undefined} state - State object
324
+ * @param {object} pathConfig - Path configuration
325
+ * @returns {Promise<object|null>} Analysis result
326
+ */
327
+ async analyzeDatapoint(id, obj, state, pathConfig) {
328
+ const now = Date.now();
329
+ const daysUntilStale = this.config.daysUntilStale || 90;
330
+ const daysUntilDead = this.config.daysUntilDead || 365;
331
+
332
+ const result = {
333
+ id: id,
334
+ name: obj.common?.name || id.split('.').pop(),
335
+ last_ts: null,
336
+ last_ts_iso: 'undefined',
337
+ value: null,
338
+ status: 'active',
339
+ status_de: 'aktiv',
340
+ issue: null,
341
+ issue_de: null,
342
+ size: 0,
343
+ };
344
+
345
+ // Get timestamp
346
+ if (state && state.ts) {
347
+ result.last_ts = state.ts;
348
+ result.last_ts_iso = new Date(state.ts).toLocaleString('de-DE');
349
+ result.value = state.val;
350
+
351
+ // Calculate age in days
352
+ const ageMs = now - state.ts;
353
+ const ageDays = ageMs / (1000 * 60 * 60 * 24);
354
+
355
+ if (ageDays > daysUntilDead) {
356
+ result.status = 'dead';
357
+ result.status_de = 'inaktiv';
358
+ result.issue = 'dead';
359
+ result.issue_de = 'inaktiv';
360
+ } else if (ageDays > daysUntilStale) {
361
+ result.status = 'stale';
362
+ result.status_de = 'veraltet';
363
+ result.issue = 'stale';
364
+ result.issue_de = 'veraltet';
365
+ } else {
366
+ // Active datapoint - no issue
367
+ result.status = 'active';
368
+ result.status_de = 'aktiv';
369
+ result.issue = null;
370
+ result.issue_de = null;
371
+ }
372
+ } else {
373
+ // No timestamp = never written
374
+ result.status = 'undefined';
375
+ result.status_de = 'undefiniert';
376
+ result.issue = 'dead';
377
+ result.issue_de = 'inaktiv';
378
+ }
379
+
380
+ // Calculate size
381
+ if (state && state.val !== null && state.val !== undefined) {
382
+ result.size = JSON.stringify(state.val).length;
383
+ }
384
+
385
+ // Check for orphaned aliases
386
+ if (pathConfig.checkAliasTargets && id.startsWith('alias.')) {
387
+ const targetId = obj.common?.alias?.id;
388
+ if (targetId) {
389
+ const targetExists = await this.getForeignObjectAsync(targetId);
390
+ if (!targetExists) {
391
+ result.status = 'orphaned';
392
+ result.status_de = 'verwaist';
393
+ result.issue = 'orphaned_alias';
394
+ result.issue_de = 'verwaistes Alias';
395
+ }
396
+ }
397
+ }
398
+
399
+ return result;
400
+ }
401
+
402
+ /**
403
+ * Sanitize name for use as object ID
404
+ *
405
+ * @param {string} name - Name to sanitize
406
+ * @returns {string} Sanitized name
407
+ */
408
+ sanitizeName(name) {
409
+ return name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
410
+ }
411
+ // If you need to accept messages in your adapter, uncomment the following block and the corresponding line in the constructor.
412
+ // /**
413
+ // * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ...
414
+ // * Using this method requires "common.messagebox" property to be set to true in io-package.json
415
+ // * @param {ioBroker.Message} obj
416
+ // */
417
+ // onMessage(obj) {
418
+ // if (typeof obj === 'object' && obj.message) {
419
+ // if (obj.command === 'send') {
420
+ // // e.g. send email or pushover or whatever
421
+ // this.log.info('send command');
422
+
423
+ // // Send response in callback if required
424
+ // if (obj.callback) this.sendTo(obj.from, obj.command, 'Message received', obj.callback);
425
+ // }
426
+ // }
427
+ // }
428
+ }
429
+
430
+ if (require.main !== module) {
431
+ // Export the constructor in compact mode
432
+ /**
433
+ * @param {Partial<utils.AdapterOptions>} [options] - Adapter options
434
+ */
435
+ module.exports = options => new Tidy(options);
436
+ } else {
437
+ // otherwise start the instance directly
438
+ new Tidy();
439
+ }
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "iobroker.tidy",
3
+ "version": "0.0.1",
4
+ "description": "Analyzes ioBroker objects for unused datapoints and helps you clean up your instance",
5
+ "author": {
6
+ "name": "skvarel",
7
+ "email": "skvarel@inventwo.com"
8
+ },
9
+ "contributors": [
10
+ {
11
+ "name": "skvarel"
12
+ }
13
+ ],
14
+ "homepage": "https://github.com/inventwo/ioBroker.tidy",
15
+ "license": "MIT",
16
+ "keywords": [
17
+ "cleanup",
18
+ "tidy",
19
+ "unused",
20
+ "ioBroker",
21
+ "objects"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/inventwo/ioBroker.tidy.git"
26
+ },
27
+ "engines": {
28
+ "node": ">= 20"
29
+ },
30
+ "dependencies": {
31
+ "@iobroker/adapter-core": "^3.3.2"
32
+ },
33
+ "devDependencies": {
34
+ "@alcalzone/release-script": "^5.1.1",
35
+ "@alcalzone/release-script-plugin-iobroker": "^5.1.2",
36
+ "@alcalzone/release-script-plugin-license": "^5.1.1",
37
+ "@alcalzone/release-script-plugin-manual-review": "^5.1.1",
38
+ "@iobroker/adapter-dev": "^1.5.0",
39
+ "@iobroker/dev-server": "^0.8.0",
40
+ "@iobroker/eslint-config": "^2.2.0",
41
+ "@iobroker/testing": "^5.2.2",
42
+ "@tsconfig/node20": "^20.1.9",
43
+ "@types/iobroker": "npm:@iobroker/types@^7.1.1",
44
+ "@types/node": "^25.5.2",
45
+ "typescript": "~6.0.2"
46
+ },
47
+ "main": "main.js",
48
+ "files": [
49
+ "admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).{json,json5}",
50
+ "admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}",
51
+ "lib/",
52
+ "www/",
53
+ "io-package.json",
54
+ "LICENSE",
55
+ "main.js"
56
+ ],
57
+ "scripts": {
58
+ "release-patch": "release-script patch --yes",
59
+ "release-minor": "release-script minor --yes",
60
+ "release-major": "release-script major --yes",
61
+ "test:js": "mocha --config test/mocharc.custom.json \"{!(node_modules|test)/**/*.test.js,*.test.js,test/**/test!(PackageFiles|Startup).js}\"",
62
+ "test:package": "mocha test/package --exit",
63
+ "test:integration": "mocha test/integration --exit",
64
+ "test": "npm run test:js && npm run test:package",
65
+ "check": "tsc --noEmit -p tsconfig.check.json",
66
+ "lint": "eslint -c eslint.config.mjs .",
67
+ "lint-fix": "eslint -c eslint.config.mjs . --fix",
68
+ "translate": "translate-adapter",
69
+ "dev-server": "dev-server"
70
+ },
71
+ "bugs": {
72
+ "url": "https://github.com/skvarel/ioBroker.tidy/issues"
73
+ },
74
+ "readmeFilename": "README.md"
75
+ }