@noforeignland/signalk-to-noforeignland 1.1.0 → 1.2.0-beta.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.
@@ -1,93 +0,0 @@
1
- class DataPathEmitter {
2
- constructor(app, pluginId) {
3
- this.app = app;
4
- this.pluginId = pluginId;
5
- this.currentError = null;
6
- }
7
-
8
- /**
9
- * Emit SignalK delta for a data path
10
- */
11
- emitDelta(path, value) {
12
- try {
13
- const delta = {
14
- context: 'vessels.self',
15
- updates: [{
16
- timestamp: new Date().toISOString(),
17
- values: [{
18
- path: path,
19
- value: value
20
- }]
21
- }]
22
- };
23
- this.app.handleMessage(this.pluginId, delta);
24
- } catch (err) {
25
- this.app.debug(`Failed to emit delta for ${path}:`, err.message);
26
- }
27
- }
28
-
29
- /**
30
- * Update status paths with current state
31
- */
32
- updateStatusPaths(options, lastPosition, lastSuccessfulTransfer, autoSelectedSource) {
33
- const hasError = this.currentError !== null;
34
-
35
- if (!hasError) {
36
- const activeSource = options.filterSource || autoSelectedSource || '';
37
- const saveTime = lastPosition
38
- ? new Date(lastPosition.currentTime).toLocaleTimeString()
39
- : 'None since start';
40
- const transferTime = lastSuccessfulTransfer
41
- ? lastSuccessfulTransfer.toLocaleTimeString()
42
- : 'None since start';
43
- const shortStatus = `Save: ${saveTime} | Transfer: ${transferTime}`;
44
-
45
- this.emitDelta('noforeignland.status', shortStatus);
46
- this.emitDelta('noforeignland.source', activeSource);
47
- } else {
48
- this.emitDelta('noforeignland.status', `ERROR: ${this.currentError}`);
49
- }
50
-
51
- this.emitDelta('noforeignland.status_boolean', hasError ? 1 : 0);
52
- }
53
-
54
- /**
55
- * Emit savepoint deltas
56
- */
57
- emitSavepoint() {
58
- const now = new Date();
59
- this.emitDelta('noforeignland.savepoint', now.toISOString());
60
- this.emitDelta('noforeignland.savepoint_local', now.toLocaleString());
61
- }
62
-
63
- /**
64
- * Emit API transfer deltas
65
- */
66
- emitApiTransfer(transferTime) {
67
- this.emitDelta('noforeignland.sent_to_api', transferTime.toISOString());
68
- this.emitDelta('noforeignland.sent_to_api_local', transferTime.toLocaleString());
69
- }
70
-
71
- /**
72
- * Set error state
73
- */
74
- setError(error) {
75
- this.currentError = error;
76
- }
77
-
78
- /**
79
- * Clear error state
80
- */
81
- clearError() {
82
- this.currentError = null;
83
- }
84
-
85
- /**
86
- * Get current error
87
- */
88
- getError() {
89
- return this.currentError;
90
- }
91
- }
92
-
93
- module.exports = DataPathEmitter;
@@ -1,38 +0,0 @@
1
- const fs = require('fs');
2
-
3
- class DirectoryUtils {
4
- /**
5
- * Create directory with proper error handling
6
- */
7
- static createDir(dir, app) {
8
- if (fs.existsSync(dir)) {
9
- try {
10
- fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK);
11
- return true;
12
- } catch (error) {
13
- app.debug('[createDir]', error.message);
14
- throw new Error(`No rights to directory ${dir}`);
15
- }
16
- } else {
17
- try {
18
- fs.mkdirSync(dir, { recursive: true });
19
- return true;
20
- } catch (error) {
21
- switch (error.code) {
22
- case 'EACCES':
23
- case 'EPERM':
24
- app.debug(`Failed to create ${dir} by Permission denied`);
25
- throw new Error(`Failed to create ${dir} by Permission denied`);
26
- case 'ETIMEDOUT':
27
- app.debug(`Failed to create ${dir} by Operation timed out`);
28
- throw new Error(`Failed to create ${dir} by Operation timed out`);
29
- default:
30
- app.debug(`Error creating directory ${dir}: ${error.message}`);
31
- throw new Error(`Error creating directory ${dir}: ${error.message}`);
32
- }
33
- }
34
- }
35
- }
36
- }
37
-
38
- module.exports = DirectoryUtils;
@@ -1,91 +0,0 @@
1
- class HealthMonitor {
2
- constructor(app, options) {
3
- this.app = app;
4
- this.options = options;
5
- this.checkInterval = null;
6
- }
7
-
8
- /**
9
- * Start position health monitoring
10
- */
11
- start(getLastPositionReceived, getAutoSelectedSource, onError, onHealthy) {
12
- // Periodic health check every 5 minutes
13
- this.checkInterval = setInterval(() => {
14
- this.performHealthCheck(
15
- getLastPositionReceived(),
16
- getAutoSelectedSource(),
17
- onError,
18
- onHealthy
19
- );
20
- }, 5 * 60 * 1000);
21
-
22
- // Initial check after 2 minutes of startup
23
- setTimeout(() => {
24
- this.performInitialCheck(
25
- getLastPositionReceived(),
26
- getAutoSelectedSource(),
27
- onError
28
- );
29
- }, 2 * 60 * 1000);
30
- }
31
-
32
- /**
33
- * Perform periodic health check
34
- */
35
- performHealthCheck(lastPositionReceived, autoSelectedSource, onError, onHealthy) {
36
- const now = new Date().getTime();
37
- const timeSinceLastPosition = lastPositionReceived
38
- ? (now - lastPositionReceived) / 1000
39
- : null;
40
-
41
- const activeSource = this.options.filterSource || autoSelectedSource || 'any';
42
- const filterMsg = activeSource !== 'any' ? ` from source '${activeSource}'` : '';
43
-
44
- if (!lastPositionReceived) {
45
- const errorMsg = this.options.filterSource
46
- ? `No GNSS position data received from filtered source '${this.options.filterSource}'. Check Expert Settings > Position source device, or leave empty to use any GNSS source.`
47
- : 'No GNSS position data received. Check that your GNSS is connected and SignalK is receiving navigation.position data.';
48
-
49
- onError(errorMsg);
50
- this.app.debug('Position health check: No position data ever received' + filterMsg);
51
- } else if (timeSinceLastPosition > 300) {
52
- const errorMsg = this.options.filterSource
53
- ? `No GNSS position data${filterMsg} for ${Math.floor(timeSinceLastPosition / 60)} minutes. Check that source '${this.options.filterSource}' is active, or change/clear Position source device in Expert Settings.`
54
- : `No GNSS position data${filterMsg} for ${Math.floor(timeSinceLastPosition / 60)} minutes. Check your GNSS connection.`;
55
-
56
- onError(errorMsg);
57
- this.app.debug(`Position health check: No position for ${timeSinceLastPosition.toFixed(0)} seconds` + filterMsg);
58
- } else {
59
- this.app.debug(`Position health check: OK (last position ${timeSinceLastPosition.toFixed(0)} seconds ago${filterMsg})`);
60
- onHealthy();
61
- }
62
- }
63
-
64
- /**
65
- * Perform initial health check after startup
66
- */
67
- performInitialCheck(lastPositionReceived, autoSelectedSource, onError) {
68
- if (!lastPositionReceived) {
69
- const activeSource = this.options.filterSource || autoSelectedSource || 'any';
70
- const errorMsg = this.options.filterSource
71
- ? `No GNSS position data received after 2 minutes from filtered source '${this.options.filterSource}'. Check Expert Settings > Position source device. You may need to leave it empty to use any available GNSS source.`
72
- : 'No GNSS position data received after 2 minutes. Check that your GNSS is connected and SignalK is receiving navigation.position data.';
73
-
74
- onError(errorMsg);
75
- this.app.debug('Initial position check: No position data received' +
76
- (activeSource !== 'any' ? ` from source '${activeSource}'` : ''));
77
- }
78
- }
79
-
80
- /**
81
- * Stop health monitoring
82
- */
83
- stop() {
84
- if (this.checkInterval) {
85
- clearInterval(this.checkInterval);
86
- this.checkInterval = null;
87
- }
88
- }
89
- }
90
-
91
- module.exports = HealthMonitor;
@@ -1,259 +0,0 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
-
4
- class PluginCleanup {
5
- constructor(app) {
6
- this.app = app;
7
- }
8
-
9
- /**
10
- * Cleanup old plugin versions (signalk-to-noforeignland and signalk-to-nfl)
11
- */
12
- async cleanup() {
13
- try {
14
- // Detect SignalK directory (standard or Victron Cerbo)
15
- const victronPath = '/data/conf/signalk';
16
- const standardPath = process.env.SIGNALK_NODE_CONFIG_DIR ||
17
- path.join(process.env.HOME || process.env.USERPROFILE, '.signalk');
18
-
19
- const configDir = fs.existsSync(victronPath) ? victronPath : standardPath;
20
- this.app.debug(`Using SignalK directory: ${configDir}`);
21
-
22
- const configPath = path.join(configDir, 'plugin-config-data');
23
-
24
- // 1. Config Migration - only from signalk-to-noforeignland
25
- await this.migrateConfig(configPath);
26
-
27
- // 2. Verify what plugins are actually present
28
- this.logInstalledPlugins(configDir);
29
-
30
- // 3. Check and remove old plugins
31
- return await this.removeOldPlugins(configDir);
32
-
33
- } catch (err) {
34
- this.app.debug('Error during old plugin cleanup:', err.message);
35
- return null;
36
- }
37
- }
38
-
39
- /**
40
- * Log which SignalK NFL plugins are currently installed (for debugging)
41
- */
42
- logInstalledPlugins(configDir) {
43
- const nodeModulesDir = path.join(configDir, 'node_modules');
44
- const pluginsToCheck = [
45
- '@noforeignland/signalk-to-noforeignland',
46
- 'signalk-to-noforeignland',
47
- 'signalk-to-nfl'
48
- ];
49
-
50
- const found = [];
51
- for (const pluginName of pluginsToCheck) {
52
- const pluginPath = path.join(nodeModulesDir, pluginName);
53
- if (fs.existsSync(pluginPath)) {
54
- const packageJsonPath = path.join(pluginPath, 'package.json');
55
- let version = 'unknown';
56
- try {
57
- if (fs.existsSync(packageJsonPath)) {
58
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
59
- version = packageJson.version;
60
- }
61
- } catch (e) {
62
- // Ignore version read errors
63
- }
64
- found.push(`${pluginName}@${version}`);
65
- }
66
- }
67
-
68
- if (found.length > 0) {
69
- this.app.debug(`Installed NFL plugins: ${found.join(', ')}`);
70
- }
71
- }
72
-
73
- /**
74
- * Migrate config from old plugin ID to new plugin ID
75
- *
76
- * Handles config migration from:
77
- * 1. Old unscoped plugin "signalk-to-noforeignland" (v0.1.x)
78
- * 2. Beta versions with wrong plugin ID (v1.1.0-beta.1/2/3)
79
- *
80
- * Both used config filename: "signalk-to-noforeignland.json"
81
- * New version uses: "@noforeignland-signalk-to-noforeignland.json"
82
- *
83
- * Since both sources use the same filename, we simply copy if it exists.
84
- */
85
- async migrateConfig(configPath) {
86
- const oldConfigFile = path.join(configPath, 'signalk-to-noforeignland.json');
87
- const newConfigFile = path.join(configPath, '@noforeignland-signalk-to-noforeignland.json');
88
-
89
- // Only migrate if old config exists and new config doesn't
90
- if (fs.existsSync(oldConfigFile) && !fs.existsSync(newConfigFile)) {
91
- this.app.debug('Migrating configuration from old plugin to new scoped plugin...');
92
- this.app.debug(` Source: ${oldConfigFile}`);
93
- this.app.debug(` Target: ${newConfigFile}`);
94
-
95
- try {
96
- // Copy to new location
97
- fs.copyFileSync(oldConfigFile, newConfigFile);
98
-
99
- // Create backup of old config
100
- const backupFile = `${oldConfigFile}.backup-${Date.now()}`;
101
- fs.copyFileSync(oldConfigFile, backupFile);
102
-
103
- this.app.debug('✓ Configuration successfully migrated');
104
- this.app.debug(` Backup saved: ${backupFile}`);
105
- } catch (err) {
106
- this.app.debug(`⨯ Config migration failed: ${err.message}`);
107
- this.app.debug(' You may need to reconfigure the plugin manually');
108
- }
109
- } else if (fs.existsSync(newConfigFile)) {
110
- this.app.debug('Configuration already in new location');
111
-
112
- // Check if config has incorrect "configuration" wrapper and fix it
113
- try {
114
- const configData = JSON.parse(fs.readFileSync(newConfigFile, 'utf8'));
115
- if (configData.configuration && typeof configData.configuration === 'object') {
116
- this.app.debug('Detected nested "configuration" wrapper, unwrapping...');
117
-
118
- // Unwrap: move properties from configuration object to root
119
- const unwrapped = {
120
- ...configData.configuration,
121
- enabled: configData.enabled,
122
- enableLogging: configData.enableLogging,
123
- enableDebug: configData.enableDebug
124
- };
125
-
126
- // Backup before fixing
127
- const fixBackupFile = `${newConfigFile}.backup-unwrap-${Date.now()}`;
128
- fs.copyFileSync(newConfigFile, fixBackupFile);
129
-
130
- // Write fixed config
131
- fs.writeFileSync(newConfigFile, JSON.stringify(unwrapped, null, 2));
132
- this.app.debug('✓ Configuration unwrapped successfully');
133
- this.app.debug(` Backup: ${fixBackupFile}`);
134
- }
135
- } catch (err) {
136
- this.app.debug(`⨯ Could not check/fix config structure: ${err.message}`);
137
- }
138
- } else {
139
- this.app.debug('No old configuration found, first-time setup');
140
- }
141
- }
142
-
143
- /**
144
- * Remove old plugin directories - with immediate and delayed attempts
145
- */
146
- async removeOldPlugins(configDir) {
147
- const oldPlugins = [
148
- { dir: path.join(configDir, 'node_modules', 'signalk-to-noforeignland'), name: 'signalk-to-noforeignland' },
149
- { dir: path.join(configDir, 'node_modules', 'signalk-to-nfl'), name: 'signalk-to-nfl' }
150
- ];
151
-
152
- const foundOldPlugins = oldPlugins.filter(plugin => fs.existsSync(plugin.dir));
153
-
154
- if (foundOldPlugins.length === 0) {
155
- return null;
156
- }
157
-
158
- const pluginNames = foundOldPlugins.map(p => `"${p.name}"`).join(' and ');
159
- const uninstallCmd = foundOldPlugins.map(p => p.name).join(' ');
160
-
161
- this.app.debug(`Old plugin(s) detected: ${pluginNames}`);
162
-
163
- // Immediate removal attempt
164
- let anyRemovedNow = false;
165
- const stillPresent = [];
166
-
167
- for (const plugin of foundOldPlugins) {
168
- try {
169
- this.app.debug(`Attempting immediate removal of: ${plugin.name}...`);
170
- await fs.remove(plugin.dir);
171
- this.app.debug(`✓ Old plugin "${plugin.name}" removed immediately`);
172
- anyRemovedNow = true;
173
- } catch (err) {
174
- this.app.debug(`Could not remove "${plugin.name}" immediately:`, err.message);
175
- stillPresent.push(plugin);
176
- }
177
- }
178
-
179
- if (stillPresent.length === 0) {
180
- this.app.debug('All old plugins removed successfully');
181
- return 'all_removed';
182
- }
183
-
184
- // Don't show error immediately - log that we're retrying
185
- const stillPresentNames = stillPresent.map(p => `"${p.name}"`).join(' and ');
186
- this.app.debug(`Old plugin(s) ${stillPresentNames} still present, will retry removal...`);
187
-
188
- // Delayed removal attempts (multiple tries with increasing delays)
189
- return new Promise((resolve) => {
190
- const delays = [5000, 15000, 30000]; // 5s, 15s, 30s
191
- let attemptIndex = 0;
192
-
193
- const attemptRemoval = async () => {
194
- if (attemptIndex >= delays.length) {
195
- // Final attempt failed - NOW show error only if plugins still exist
196
- const remaining = stillPresent.filter(p => fs.existsSync(p.dir));
197
- if (remaining.length > 0) {
198
- const remainingNames = remaining.map(p => `"${p.name}"`).join(' and ');
199
- const remainingCmd = remaining.map(p => p.name).join(' ');
200
-
201
- this.app.debug(`Could not remove old plugins after ${delays.length} attempts: ${remainingNames}`);
202
-
203
- // Show error with platform-specific commands
204
- const isVictronCerbo = configDir === '/data/conf/signalk';
205
- const cmdPrefix = isVictronCerbo ? 'On Victron Cerbo GX, ' : '';
206
-
207
- this.app.setPluginError(
208
- `${cmdPrefix}Old plugin(s) ${remainingNames} detected. ` +
209
- `Manual removal required: cd ${configDir} && npm uninstall ${remainingCmd}`
210
- );
211
- resolve('partial_removal');
212
- } else {
213
- this.app.debug('All old plugins eventually removed');
214
- resolve('all_removed');
215
- }
216
- return;
217
- }
218
-
219
- const delay = delays[attemptIndex];
220
- attemptIndex++;
221
-
222
- setTimeout(async () => {
223
- this.app.debug(`Delayed removal attempt ${attemptIndex}/${delays.length}...`);
224
-
225
- const remaining = [];
226
- for (const plugin of stillPresent) {
227
- if (!fs.existsSync(plugin.dir)) {
228
- this.app.debug(`Plugin "${plugin.name}" already removed`);
229
- continue;
230
- }
231
-
232
- try {
233
- await fs.remove(plugin.dir);
234
- this.app.debug(`✓ Old plugin "${plugin.name}" removed on attempt ${attemptIndex}`);
235
- } catch (err) {
236
- this.app.debug(`Still cannot remove "${plugin.name}":`, err.message);
237
- remaining.push(plugin);
238
- }
239
- }
240
-
241
- if (remaining.length === 0) {
242
- this.app.debug('All old plugins successfully removed');
243
- // Clear the error
244
- this.app.setPluginStatus('Started (old plugins cleaned up)');
245
- resolve('all_removed');
246
- } else {
247
- stillPresent.length = 0;
248
- stillPresent.push(...remaining);
249
- attemptRemoval(); // Next attempt
250
- }
251
- }, delay);
252
- };
253
-
254
- attemptRemoval();
255
- });
256
- }
257
- }
258
-
259
- module.exports = PluginCleanup;