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/LICENSE +21 -0
- package/README.md +176 -0
- package/admin/i18n/de.json +36 -0
- package/admin/i18n/en.json +33 -0
- package/admin/i18n/es.json +62 -0
- package/admin/i18n/fr.json +62 -0
- package/admin/i18n/it.json +62 -0
- package/admin/i18n/nl.json +62 -0
- package/admin/i18n/pl.json +62 -0
- package/admin/i18n/pt.json +62 -0
- package/admin/i18n/ru.json +62 -0
- package/admin/i18n/uk.json +62 -0
- package/admin/i18n/zh-cn.json +62 -0
- package/admin/img/inventwo.svg +2 -0
- package/admin/img/tidy_de.PNG +0 -0
- package/admin/img/tidy_en.PNG +0 -0
- package/admin/jsonConfig.json +229 -0
- package/admin/tidy.svg +5 -0
- package/io-package.json +112 -0
- package/lib/adapter-config.d.ts +19 -0
- package/main.js +439 -0
- package/package.json +75 -0
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>
|
package/io-package.json
ADDED
|
@@ -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
|
+
}
|