@enyo-energy/sunspec-sdk 0.0.27 → 0.0.29

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.
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ /**
3
+ * Connection Retry Manager with Exponential Backoff
4
+ *
5
+ * Manages automatic connection retries with configurable exponential backoff.
6
+ * Backoff sequence (with defaults): 1s → 1.5s → 2.25s → 3.375s → 5.06s → 7.59s → 11.39s → 17.09s → 25.63s → 30s (capped)
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.ConnectionRetryManager = void 0;
10
+ const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
11
+ class ConnectionRetryManager {
12
+ config;
13
+ currentAttempt = 0;
14
+ pendingRetryTimeout = null;
15
+ isRetrying = false;
16
+ constructor(config) {
17
+ this.config = { ...sunspec_interfaces_js_1.DEFAULT_RETRY_CONFIG, ...config };
18
+ }
19
+ /**
20
+ * Calculate the next delay using exponential backoff
21
+ * Formula: min(initialDelayMs * backoffFactor^attempt, maxDelayMs)
22
+ */
23
+ getNextDelay() {
24
+ const delay = this.config.initialDelayMs * Math.pow(this.config.backoffFactor, this.currentAttempt);
25
+ return Math.min(delay, this.config.maxDelayMs);
26
+ }
27
+ /**
28
+ * Reset the retry state on successful connection
29
+ */
30
+ reset() {
31
+ this.currentAttempt = 0;
32
+ this.isRetrying = false;
33
+ this.cancelPendingRetry();
34
+ }
35
+ /**
36
+ * Check if we should retry based on max attempts
37
+ * Returns true if we haven't exceeded maxAttempts or if maxAttempts is -1 (infinite)
38
+ */
39
+ shouldRetry() {
40
+ if (this.config.maxAttempts === -1) {
41
+ return true;
42
+ }
43
+ return this.currentAttempt < this.config.maxAttempts;
44
+ }
45
+ /**
46
+ * Get the current attempt number (0-indexed)
47
+ */
48
+ getCurrentAttempt() {
49
+ return this.currentAttempt;
50
+ }
51
+ /**
52
+ * Get the max attempts configuration
53
+ */
54
+ getMaxAttempts() {
55
+ return this.config.maxAttempts;
56
+ }
57
+ /**
58
+ * Check if a retry is currently in progress
59
+ */
60
+ isRetryInProgress() {
61
+ return this.isRetrying;
62
+ }
63
+ /**
64
+ * Schedule a reconnection attempt with exponential backoff delay
65
+ * Returns a promise that resolves when the callback completes
66
+ */
67
+ async scheduleRetry(callback) {
68
+ if (!this.shouldRetry()) {
69
+ console.log(`Max retry attempts (${this.config.maxAttempts}) reached. Giving up.`);
70
+ return false;
71
+ }
72
+ if (this.isRetrying) {
73
+ console.log('Retry already in progress, skipping duplicate request.');
74
+ return false;
75
+ }
76
+ this.isRetrying = true;
77
+ const delay = this.getNextDelay();
78
+ const attemptNumber = this.currentAttempt + 1;
79
+ const maxAttemptsStr = this.config.maxAttempts === -1 ? '∞' : this.config.maxAttempts.toString();
80
+ console.log(`Scheduling reconnection attempt ${attemptNumber}/${maxAttemptsStr} in ${delay}ms`);
81
+ return new Promise((resolve) => {
82
+ this.pendingRetryTimeout = setTimeout(async () => {
83
+ this.pendingRetryTimeout = null;
84
+ this.currentAttempt++;
85
+ try {
86
+ console.log(`Attempting reconnection (attempt ${attemptNumber}/${maxAttemptsStr})...`);
87
+ const success = await callback();
88
+ if (success) {
89
+ console.log('Reconnection successful!');
90
+ this.reset();
91
+ resolve(true);
92
+ }
93
+ else {
94
+ console.log('Reconnection attempt failed.');
95
+ this.isRetrying = false;
96
+ resolve(false);
97
+ }
98
+ }
99
+ catch (error) {
100
+ console.error(`Reconnection attempt failed with error: ${error}`);
101
+ this.isRetrying = false;
102
+ resolve(false);
103
+ }
104
+ }, delay);
105
+ });
106
+ }
107
+ /**
108
+ * Cancel any pending retry attempt
109
+ */
110
+ cancelPendingRetry() {
111
+ if (this.pendingRetryTimeout) {
112
+ clearTimeout(this.pendingRetryTimeout);
113
+ this.pendingRetryTimeout = null;
114
+ this.isRetrying = false;
115
+ console.log('Pending retry cancelled.');
116
+ }
117
+ }
118
+ /**
119
+ * Get the current configuration
120
+ */
121
+ getConfig() {
122
+ return { ...this.config };
123
+ }
124
+ }
125
+ exports.ConnectionRetryManager = ConnectionRetryManager;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Connection Retry Manager with Exponential Backoff
3
+ *
4
+ * Manages automatic connection retries with configurable exponential backoff.
5
+ * Backoff sequence (with defaults): 1s → 1.5s → 2.25s → 3.375s → 5.06s → 7.59s → 11.39s → 17.09s → 25.63s → 30s (capped)
6
+ */
7
+ import { IRetryConfig } from './sunspec-interfaces.cjs';
8
+ export declare class ConnectionRetryManager {
9
+ private config;
10
+ private currentAttempt;
11
+ private pendingRetryTimeout;
12
+ private isRetrying;
13
+ constructor(config?: Partial<IRetryConfig>);
14
+ /**
15
+ * Calculate the next delay using exponential backoff
16
+ * Formula: min(initialDelayMs * backoffFactor^attempt, maxDelayMs)
17
+ */
18
+ getNextDelay(): number;
19
+ /**
20
+ * Reset the retry state on successful connection
21
+ */
22
+ reset(): void;
23
+ /**
24
+ * Check if we should retry based on max attempts
25
+ * Returns true if we haven't exceeded maxAttempts or if maxAttempts is -1 (infinite)
26
+ */
27
+ shouldRetry(): boolean;
28
+ /**
29
+ * Get the current attempt number (0-indexed)
30
+ */
31
+ getCurrentAttempt(): number;
32
+ /**
33
+ * Get the max attempts configuration
34
+ */
35
+ getMaxAttempts(): number;
36
+ /**
37
+ * Check if a retry is currently in progress
38
+ */
39
+ isRetryInProgress(): boolean;
40
+ /**
41
+ * Schedule a reconnection attempt with exponential backoff delay
42
+ * Returns a promise that resolves when the callback completes
43
+ */
44
+ scheduleRetry(callback: () => Promise<boolean>): Promise<boolean>;
45
+ /**
46
+ * Cancel any pending retry attempt
47
+ */
48
+ cancelPendingRetry(): void;
49
+ /**
50
+ * Get the current configuration
51
+ */
52
+ getConfig(): IRetryConfig;
53
+ }
@@ -14,10 +14,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.getSdkVersion = exports.SDK_VERSION = void 0;
17
+ exports.getSdkVersion = exports.SDK_VERSION = exports.ConnectionRetryManager = void 0;
18
18
  __exportStar(require("./sunspec-interfaces.cjs"), exports);
19
19
  __exportStar(require("./sunspec-devices.cjs"), exports);
20
20
  __exportStar(require("./sunspec-modbus-client.cjs"), exports);
21
+ var connection_retry_manager_js_1 = require("./connection-retry-manager.cjs");
22
+ Object.defineProperty(exports, "ConnectionRetryManager", { enumerable: true, get: function () { return connection_retry_manager_js_1.ConnectionRetryManager; } });
21
23
  var version_js_1 = require("./version.cjs");
22
24
  Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return version_js_1.SDK_VERSION; } });
23
25
  Object.defineProperty(exports, "getSdkVersion", { enumerable: true, get: function () { return version_js_1.getSdkVersion; } });
@@ -1,4 +1,5 @@
1
1
  export * from './sunspec-interfaces.cjs';
2
2
  export * from './sunspec-devices.cjs';
3
3
  export * from './sunspec-modbus-client.cjs';
4
+ export { ConnectionRetryManager } from './connection-retry-manager.cjs';
4
5
  export { SDK_VERSION, getSdkVersion } from './version.cjs';
@@ -55,7 +55,9 @@ class BaseSunspecDevice {
55
55
  */
56
56
  async ensureConnected() {
57
57
  if (!this.sunspecClient.isConnected()) {
58
- await this.sunspecClient.connect(this.networkDevice.ipAddress, 502, this.unitId);
58
+ await this.sunspecClient.connect(this.networkDevice.hostname, // primary
59
+ 502, this.unitId, this.networkDevice.ipAddress // secondary fallback
60
+ );
59
61
  await this.sunspecClient.discoverModels();
60
62
  }
61
63
  }
@@ -68,7 +70,9 @@ class SunspecInverter extends BaseSunspecDevice {
68
70
  async connect() {
69
71
  // Ensure Sunspec client is connected
70
72
  if (!this.sunspecClient.isConnected()) {
71
- await this.sunspecClient.connect(this.networkDevice.ipAddress, 502, this.unitId);
73
+ await this.sunspecClient.connect(this.networkDevice.hostname, // primary
74
+ 502, this.unitId, this.networkDevice.ipAddress // secondary fallback
75
+ );
72
76
  await this.sunspecClient.discoverModels();
73
77
  }
74
78
  // Get device info from common block
@@ -130,6 +134,7 @@ class SunspecInverter extends BaseSunspecDevice {
130
134
  if (inverterData) {
131
135
  const totalAcPowerW = inverterData.acPower || 0;
132
136
  const purePvPowerW = Math.max(0, totalAcPowerW - batteryDischargePowerW);
137
+ console.log(`Got Battery Discharge power ${batteryDischargePowerW} and Inverter Power W ${totalAcPowerW} with pure PV Power ${purePvPowerW}`);
133
138
  const inverterMessage = {
134
139
  id: (0, node_crypto_1.randomUUID)(),
135
140
  message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.InverterValuesUpdateV1,
@@ -3,7 +3,13 @@
3
3
  * SunSpec block interfaces with block numbers
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SunspecStorageMode = exports.SunspecChargeSource = exports.SunspecVArPctMode = exports.SunspecEnableControl = exports.SunspecConnectionControl = exports.SunspecStorageControlMode = exports.SunspecBatteryChargeState = exports.SunspecMPPTOperatingState = exports.SunspecModelId = void 0;
6
+ exports.SunspecStorageMode = exports.SunspecChargeSource = exports.SunspecVArPctMode = exports.SunspecEnableControl = exports.SunspecConnectionControl = exports.SunspecStorageControlMode = exports.SunspecBatteryChargeState = exports.SunspecMPPTOperatingState = exports.SunspecModelId = exports.DEFAULT_RETRY_CONFIG = void 0;
7
+ exports.DEFAULT_RETRY_CONFIG = {
8
+ initialDelayMs: 1000,
9
+ maxDelayMs: 30000,
10
+ backoffFactor: 1.5,
11
+ maxAttempts: 10
12
+ };
7
13
  /**
8
14
  * Common Sunspec Model IDs
9
15
  */
@@ -1,6 +1,16 @@
1
1
  /**
2
2
  * SunSpec block interfaces with block numbers
3
3
  */
4
+ /**
5
+ * Configuration for connection retry with exponential backoff
6
+ */
7
+ export interface IRetryConfig {
8
+ initialDelayMs: number;
9
+ maxDelayMs: number;
10
+ backoffFactor: number;
11
+ maxAttempts: number;
12
+ }
13
+ export declare const DEFAULT_RETRY_CONFIG: IRetryConfig;
4
14
  /**
5
15
  * Base interface for all SunSpec blocks
6
16
  */
@@ -20,6 +20,7 @@ exports.SunspecModbusClient = void 0;
20
20
  * - string: all registers 0x0000 (NULL)
21
21
  */
22
22
  const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
23
+ const connection_retry_manager_js_1 = require("./connection-retry-manager.cjs");
23
24
  const EnergyAppModbusConnectionHealth_js_1 = require("@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusConnectionHealth.js");
24
25
  const EnergyAppModbusFaultTolerantReader_js_1 = require("@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusFaultTolerantReader.js");
25
26
  const EnergyAppModbusDataTypeConverter_js_1 = require("@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusDataTypeConverter.js");
@@ -32,18 +33,28 @@ class SunspecModbusClient {
32
33
  connectionHealth;
33
34
  faultTolerantReader = null;
34
35
  modbusDataTypeConverter;
35
- constructor(energyApp) {
36
+ connectionParams = null;
37
+ retryManager;
38
+ autoReconnectEnabled = true;
39
+ constructor(energyApp, retryConfig) {
36
40
  this.energyApp = energyApp;
37
41
  this.connectionHealth = new EnergyAppModbusConnectionHealth_js_1.EnergyAppModbusConnectionHealth();
38
42
  this.modbusDataTypeConverter = new EnergyAppModbusDataTypeConverter_js_1.EnergyAppModbusDataTypeConverter();
43
+ this.retryManager = new connection_retry_manager_js_1.ConnectionRetryManager(retryConfig);
39
44
  }
40
45
  /**
41
46
  * Connect to Modbus device
47
+ * @param host Primary host (hostname) to connect to
48
+ * @param port Modbus port (default 502)
49
+ * @param unitId Modbus unit ID (default 1)
50
+ * @param secondaryHost Optional secondary host (ipAddress) for fallback during reconnection
42
51
  */
43
- async connect(host, port = 502, unitId = 1) {
52
+ async connect(host, port = 502, unitId = 1, secondaryHost) {
44
53
  if (this.connected) {
45
54
  await this.disconnect();
46
55
  }
56
+ // Store connection parameters for potential reconnection
57
+ this.connectionParams = { primaryHost: host, secondaryHost, port, unitId };
47
58
  this.modbusClient = await this.energyApp.useModbus().connect({
48
59
  host,
49
60
  port,
@@ -56,12 +67,15 @@ class SunspecModbusClient {
56
67
  }
57
68
  this.connected = true;
58
69
  this.connectionHealth.recordSuccess();
70
+ this.retryManager.reset();
59
71
  console.log(`Connected to Sunspec device at ${host}:${port} unit ${unitId}`);
60
72
  }
61
73
  /**
62
74
  * Disconnect from Modbus device
63
75
  */
64
76
  async disconnect() {
77
+ // Cancel any pending retry attempts
78
+ this.retryManager.cancelPendingRetry();
65
79
  if (this.modbusClient && this.connected) {
66
80
  await this.modbusClient.disconnect();
67
81
  this.modbusClient = null;
@@ -69,6 +83,144 @@ class SunspecModbusClient {
69
83
  this.connected = false;
70
84
  this.discoveredModels.clear();
71
85
  }
86
+ // Clear stored connection params
87
+ this.connectionParams = null;
88
+ }
89
+ /**
90
+ * Reconnect using stored connection parameters
91
+ * First tries primaryHost (hostname), then falls back to secondaryHost (ipAddress) if available
92
+ * Returns true if reconnection was successful, false otherwise
93
+ */
94
+ async reconnect() {
95
+ if (!this.connectionParams) {
96
+ console.error('Cannot reconnect: no connection parameters stored. Call connect() first.');
97
+ return false;
98
+ }
99
+ const { primaryHost, secondaryHost, port, unitId } = this.connectionParams;
100
+ // Try primary host first
101
+ console.log(`Attempting to reconnect to primary host ${primaryHost}:${port} unit ${unitId}...`);
102
+ const primarySuccess = await this.attemptConnection(primaryHost, port, unitId);
103
+ if (primarySuccess) {
104
+ console.log(`Successfully reconnected to primary host ${primaryHost}:${port} unit ${unitId}`);
105
+ return true;
106
+ }
107
+ // If primary failed and secondary is available, try secondary
108
+ if (secondaryHost && secondaryHost !== primaryHost) {
109
+ console.log(`Primary host failed, attempting secondary host ${secondaryHost}:${port} unit ${unitId}...`);
110
+ const secondarySuccess = await this.attemptConnection(secondaryHost, port, unitId);
111
+ if (secondarySuccess) {
112
+ console.log(`Successfully reconnected to secondary host ${secondaryHost}:${port} unit ${unitId}`);
113
+ return true;
114
+ }
115
+ }
116
+ console.error(`Reconnection failed to all available hosts`);
117
+ this.connected = false;
118
+ return false;
119
+ }
120
+ /**
121
+ * Attempt to establish a connection to a specific host
122
+ * Returns true if successful, false otherwise
123
+ */
124
+ async attemptConnection(host, port, unitId) {
125
+ try {
126
+ // Disconnect existing connection if any
127
+ if (this.modbusClient) {
128
+ try {
129
+ await this.modbusClient.disconnect();
130
+ }
131
+ catch (e) {
132
+ // Ignore disconnect errors during reconnection
133
+ }
134
+ this.modbusClient = null;
135
+ this.faultTolerantReader = null;
136
+ }
137
+ // Attempt connection
138
+ this.modbusClient = await this.energyApp.useModbus().connect({
139
+ host,
140
+ port,
141
+ unitId,
142
+ timeout: 5000
143
+ });
144
+ // Create fault-tolerant reader with connection health monitoring
145
+ if (this.modbusClient) {
146
+ this.faultTolerantReader = new EnergyAppModbusFaultTolerantReader_js_1.EnergyAppModbusFaultTolerantReader(this.modbusClient, this.connectionHealth);
147
+ }
148
+ this.connected = true;
149
+ this.connectionHealth.recordSuccess();
150
+ return true;
151
+ }
152
+ catch (error) {
153
+ console.error(`Connection attempt to ${host}:${port} failed: ${error}`);
154
+ return false;
155
+ }
156
+ }
157
+ /**
158
+ * Check connection health and trigger automatic reconnection if unhealthy
159
+ * Returns true if connection is healthy or was successfully restored
160
+ */
161
+ async ensureHealthyConnection() {
162
+ // If already healthy, return immediately
163
+ if (this.isHealthy()) {
164
+ return true;
165
+ }
166
+ // If no connection params, we can't reconnect
167
+ if (!this.connectionParams) {
168
+ console.error('Connection unhealthy and no connection parameters stored for reconnection.');
169
+ return false;
170
+ }
171
+ // If auto-reconnect is disabled, just report the status
172
+ if (!this.autoReconnectEnabled) {
173
+ console.log('Connection unhealthy but auto-reconnect is disabled.');
174
+ return false;
175
+ }
176
+ // If a retry is already in progress, don't start another
177
+ if (this.retryManager.isRetryInProgress()) {
178
+ console.log('Retry already in progress...');
179
+ return false;
180
+ }
181
+ console.log('Connection unhealthy, initiating reconnection with exponential backoff...');
182
+ // Attempt reconnection with exponential backoff
183
+ let success = false;
184
+ while (this.retryManager.shouldRetry() && !success) {
185
+ success = await this.retryManager.scheduleRetry(() => this.reconnect());
186
+ if (success) {
187
+ // Re-discover models after successful reconnection
188
+ try {
189
+ await this.discoverModels();
190
+ console.log('Models re-discovered after reconnection.');
191
+ }
192
+ catch (e) {
193
+ console.warn('Failed to re-discover models after reconnection:', e);
194
+ }
195
+ return true;
196
+ }
197
+ }
198
+ if (!success) {
199
+ console.error('Failed to restore healthy connection after all retry attempts.');
200
+ }
201
+ return success;
202
+ }
203
+ /**
204
+ * Enable or disable automatic reconnection
205
+ */
206
+ setAutoReconnect(enabled) {
207
+ this.autoReconnectEnabled = enabled;
208
+ console.log(`Auto-reconnect ${enabled ? 'enabled' : 'disabled'}`);
209
+ if (!enabled) {
210
+ this.retryManager.cancelPendingRetry();
211
+ }
212
+ }
213
+ /**
214
+ * Check if auto-reconnect is enabled
215
+ */
216
+ isAutoReconnectEnabled() {
217
+ return this.autoReconnectEnabled;
218
+ }
219
+ /**
220
+ * Get the retry manager for advanced configuration
221
+ */
222
+ getRetryManager() {
223
+ return this.retryManager;
72
224
  }
73
225
  /**
74
226
  * Detect the base address and addressing mode (0-based or 1-based) for SunSpec
@@ -16,7 +16,8 @@
16
16
  * - pad: 0x8000 (always returns this value)
17
17
  * - string: all registers 0x0000 (NULL)
18
18
  */
19
- import { type SunspecInverterControls, type SunspecInverterData, type SunspecInverterSettings, type SunspecMeterData, type SunspecModel, type SunspecMPPTData, type SunspecBatteryData, type SunspecBatteryControls, SunspecStorageMode } from "./sunspec-interfaces.cjs";
19
+ import { type SunspecInverterControls, type SunspecInverterData, type SunspecInverterSettings, type SunspecMeterData, type SunspecModel, type SunspecMPPTData, type SunspecBatteryData, type SunspecBatteryControls, SunspecStorageMode, type IRetryConfig } from "./sunspec-interfaces.cjs";
20
+ import { ConnectionRetryManager } from "./connection-retry-manager.cjs";
20
21
  import { IConnectionHealth } from "@enyo-energy/energy-app-sdk/dist/implementations/modbus/interfaces.js";
21
22
  import { EnergyApp } from "@enyo-energy/energy-app-sdk";
22
23
  export declare class SunspecModbusClient {
@@ -28,15 +29,50 @@ export declare class SunspecModbusClient {
28
29
  private connectionHealth;
29
30
  private faultTolerantReader;
30
31
  private modbusDataTypeConverter;
31
- constructor(energyApp: EnergyApp);
32
+ private connectionParams;
33
+ private retryManager;
34
+ private autoReconnectEnabled;
35
+ constructor(energyApp: EnergyApp, retryConfig?: Partial<IRetryConfig>);
32
36
  /**
33
37
  * Connect to Modbus device
38
+ * @param host Primary host (hostname) to connect to
39
+ * @param port Modbus port (default 502)
40
+ * @param unitId Modbus unit ID (default 1)
41
+ * @param secondaryHost Optional secondary host (ipAddress) for fallback during reconnection
34
42
  */
35
- connect(host: string, port?: number, unitId?: number): Promise<void>;
43
+ connect(host: string, port?: number, unitId?: number, secondaryHost?: string): Promise<void>;
36
44
  /**
37
45
  * Disconnect from Modbus device
38
46
  */
39
47
  disconnect(): Promise<void>;
48
+ /**
49
+ * Reconnect using stored connection parameters
50
+ * First tries primaryHost (hostname), then falls back to secondaryHost (ipAddress) if available
51
+ * Returns true if reconnection was successful, false otherwise
52
+ */
53
+ reconnect(): Promise<boolean>;
54
+ /**
55
+ * Attempt to establish a connection to a specific host
56
+ * Returns true if successful, false otherwise
57
+ */
58
+ private attemptConnection;
59
+ /**
60
+ * Check connection health and trigger automatic reconnection if unhealthy
61
+ * Returns true if connection is healthy or was successfully restored
62
+ */
63
+ ensureHealthyConnection(): Promise<boolean>;
64
+ /**
65
+ * Enable or disable automatic reconnection
66
+ */
67
+ setAutoReconnect(enabled: boolean): void;
68
+ /**
69
+ * Check if auto-reconnect is enabled
70
+ */
71
+ isAutoReconnectEnabled(): boolean;
72
+ /**
73
+ * Get the retry manager for advanced configuration
74
+ */
75
+ getRetryManager(): ConnectionRetryManager;
40
76
  /**
41
77
  * Detect the base address and addressing mode (0-based or 1-based) for SunSpec
42
78
  */
@@ -9,7 +9,7 @@ exports.getSdkVersion = getSdkVersion;
9
9
  /**
10
10
  * Current version of the enyo Energy App SDK.
11
11
  */
12
- exports.SDK_VERSION = '0.0.27';
12
+ exports.SDK_VERSION = '0.0.29';
13
13
  /**
14
14
  * Gets the current SDK version.
15
15
  * @returns The semantic version string of the SDK
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "0.0.27";
8
+ export declare const SDK_VERSION = "0.0.29";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Connection Retry Manager with Exponential Backoff
3
+ *
4
+ * Manages automatic connection retries with configurable exponential backoff.
5
+ * Backoff sequence (with defaults): 1s → 1.5s → 2.25s → 3.375s → 5.06s → 7.59s → 11.39s → 17.09s → 25.63s → 30s (capped)
6
+ */
7
+ import { IRetryConfig } from './sunspec-interfaces.js';
8
+ export declare class ConnectionRetryManager {
9
+ private config;
10
+ private currentAttempt;
11
+ private pendingRetryTimeout;
12
+ private isRetrying;
13
+ constructor(config?: Partial<IRetryConfig>);
14
+ /**
15
+ * Calculate the next delay using exponential backoff
16
+ * Formula: min(initialDelayMs * backoffFactor^attempt, maxDelayMs)
17
+ */
18
+ getNextDelay(): number;
19
+ /**
20
+ * Reset the retry state on successful connection
21
+ */
22
+ reset(): void;
23
+ /**
24
+ * Check if we should retry based on max attempts
25
+ * Returns true if we haven't exceeded maxAttempts or if maxAttempts is -1 (infinite)
26
+ */
27
+ shouldRetry(): boolean;
28
+ /**
29
+ * Get the current attempt number (0-indexed)
30
+ */
31
+ getCurrentAttempt(): number;
32
+ /**
33
+ * Get the max attempts configuration
34
+ */
35
+ getMaxAttempts(): number;
36
+ /**
37
+ * Check if a retry is currently in progress
38
+ */
39
+ isRetryInProgress(): boolean;
40
+ /**
41
+ * Schedule a reconnection attempt with exponential backoff delay
42
+ * Returns a promise that resolves when the callback completes
43
+ */
44
+ scheduleRetry(callback: () => Promise<boolean>): Promise<boolean>;
45
+ /**
46
+ * Cancel any pending retry attempt
47
+ */
48
+ cancelPendingRetry(): void;
49
+ /**
50
+ * Get the current configuration
51
+ */
52
+ getConfig(): IRetryConfig;
53
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Connection Retry Manager with Exponential Backoff
3
+ *
4
+ * Manages automatic connection retries with configurable exponential backoff.
5
+ * Backoff sequence (with defaults): 1s → 1.5s → 2.25s → 3.375s → 5.06s → 7.59s → 11.39s → 17.09s → 25.63s → 30s (capped)
6
+ */
7
+ import { DEFAULT_RETRY_CONFIG } from './sunspec-interfaces.js';
8
+ export class ConnectionRetryManager {
9
+ config;
10
+ currentAttempt = 0;
11
+ pendingRetryTimeout = null;
12
+ isRetrying = false;
13
+ constructor(config) {
14
+ this.config = { ...DEFAULT_RETRY_CONFIG, ...config };
15
+ }
16
+ /**
17
+ * Calculate the next delay using exponential backoff
18
+ * Formula: min(initialDelayMs * backoffFactor^attempt, maxDelayMs)
19
+ */
20
+ getNextDelay() {
21
+ const delay = this.config.initialDelayMs * Math.pow(this.config.backoffFactor, this.currentAttempt);
22
+ return Math.min(delay, this.config.maxDelayMs);
23
+ }
24
+ /**
25
+ * Reset the retry state on successful connection
26
+ */
27
+ reset() {
28
+ this.currentAttempt = 0;
29
+ this.isRetrying = false;
30
+ this.cancelPendingRetry();
31
+ }
32
+ /**
33
+ * Check if we should retry based on max attempts
34
+ * Returns true if we haven't exceeded maxAttempts or if maxAttempts is -1 (infinite)
35
+ */
36
+ shouldRetry() {
37
+ if (this.config.maxAttempts === -1) {
38
+ return true;
39
+ }
40
+ return this.currentAttempt < this.config.maxAttempts;
41
+ }
42
+ /**
43
+ * Get the current attempt number (0-indexed)
44
+ */
45
+ getCurrentAttempt() {
46
+ return this.currentAttempt;
47
+ }
48
+ /**
49
+ * Get the max attempts configuration
50
+ */
51
+ getMaxAttempts() {
52
+ return this.config.maxAttempts;
53
+ }
54
+ /**
55
+ * Check if a retry is currently in progress
56
+ */
57
+ isRetryInProgress() {
58
+ return this.isRetrying;
59
+ }
60
+ /**
61
+ * Schedule a reconnection attempt with exponential backoff delay
62
+ * Returns a promise that resolves when the callback completes
63
+ */
64
+ async scheduleRetry(callback) {
65
+ if (!this.shouldRetry()) {
66
+ console.log(`Max retry attempts (${this.config.maxAttempts}) reached. Giving up.`);
67
+ return false;
68
+ }
69
+ if (this.isRetrying) {
70
+ console.log('Retry already in progress, skipping duplicate request.');
71
+ return false;
72
+ }
73
+ this.isRetrying = true;
74
+ const delay = this.getNextDelay();
75
+ const attemptNumber = this.currentAttempt + 1;
76
+ const maxAttemptsStr = this.config.maxAttempts === -1 ? '∞' : this.config.maxAttempts.toString();
77
+ console.log(`Scheduling reconnection attempt ${attemptNumber}/${maxAttemptsStr} in ${delay}ms`);
78
+ return new Promise((resolve) => {
79
+ this.pendingRetryTimeout = setTimeout(async () => {
80
+ this.pendingRetryTimeout = null;
81
+ this.currentAttempt++;
82
+ try {
83
+ console.log(`Attempting reconnection (attempt ${attemptNumber}/${maxAttemptsStr})...`);
84
+ const success = await callback();
85
+ if (success) {
86
+ console.log('Reconnection successful!');
87
+ this.reset();
88
+ resolve(true);
89
+ }
90
+ else {
91
+ console.log('Reconnection attempt failed.');
92
+ this.isRetrying = false;
93
+ resolve(false);
94
+ }
95
+ }
96
+ catch (error) {
97
+ console.error(`Reconnection attempt failed with error: ${error}`);
98
+ this.isRetrying = false;
99
+ resolve(false);
100
+ }
101
+ }, delay);
102
+ });
103
+ }
104
+ /**
105
+ * Cancel any pending retry attempt
106
+ */
107
+ cancelPendingRetry() {
108
+ if (this.pendingRetryTimeout) {
109
+ clearTimeout(this.pendingRetryTimeout);
110
+ this.pendingRetryTimeout = null;
111
+ this.isRetrying = false;
112
+ console.log('Pending retry cancelled.');
113
+ }
114
+ }
115
+ /**
116
+ * Get the current configuration
117
+ */
118
+ getConfig() {
119
+ return { ...this.config };
120
+ }
121
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './sunspec-interfaces.js';
2
2
  export * from './sunspec-devices.js';
3
3
  export * from './sunspec-modbus-client.js';
4
+ export { ConnectionRetryManager } from './connection-retry-manager.js';
4
5
  export { SDK_VERSION, getSdkVersion } from './version.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './sunspec-interfaces.js';
2
2
  export * from './sunspec-devices.js';
3
3
  export * from './sunspec-modbus-client.js';
4
+ export { ConnectionRetryManager } from './connection-retry-manager.js';
4
5
  export { SDK_VERSION, getSdkVersion } from './version.js';
@@ -52,7 +52,9 @@ export class BaseSunspecDevice {
52
52
  */
53
53
  async ensureConnected() {
54
54
  if (!this.sunspecClient.isConnected()) {
55
- await this.sunspecClient.connect(this.networkDevice.ipAddress, 502, this.unitId);
55
+ await this.sunspecClient.connect(this.networkDevice.hostname, // primary
56
+ 502, this.unitId, this.networkDevice.ipAddress // secondary fallback
57
+ );
56
58
  await this.sunspecClient.discoverModels();
57
59
  }
58
60
  }
@@ -64,7 +66,9 @@ export class SunspecInverter extends BaseSunspecDevice {
64
66
  async connect() {
65
67
  // Ensure Sunspec client is connected
66
68
  if (!this.sunspecClient.isConnected()) {
67
- await this.sunspecClient.connect(this.networkDevice.ipAddress, 502, this.unitId);
69
+ await this.sunspecClient.connect(this.networkDevice.hostname, // primary
70
+ 502, this.unitId, this.networkDevice.ipAddress // secondary fallback
71
+ );
68
72
  await this.sunspecClient.discoverModels();
69
73
  }
70
74
  // Get device info from common block
@@ -126,6 +130,7 @@ export class SunspecInverter extends BaseSunspecDevice {
126
130
  if (inverterData) {
127
131
  const totalAcPowerW = inverterData.acPower || 0;
128
132
  const purePvPowerW = Math.max(0, totalAcPowerW - batteryDischargePowerW);
133
+ console.log(`Got Battery Discharge power ${batteryDischargePowerW} and Inverter Power W ${totalAcPowerW} with pure PV Power ${purePvPowerW}`);
129
134
  const inverterMessage = {
130
135
  id: randomUUID(),
131
136
  message: EnyoDataBusMessageEnum.InverterValuesUpdateV1,
@@ -1,6 +1,16 @@
1
1
  /**
2
2
  * SunSpec block interfaces with block numbers
3
3
  */
4
+ /**
5
+ * Configuration for connection retry with exponential backoff
6
+ */
7
+ export interface IRetryConfig {
8
+ initialDelayMs: number;
9
+ maxDelayMs: number;
10
+ backoffFactor: number;
11
+ maxAttempts: number;
12
+ }
13
+ export declare const DEFAULT_RETRY_CONFIG: IRetryConfig;
4
14
  /**
5
15
  * Base interface for all SunSpec blocks
6
16
  */
@@ -1,6 +1,12 @@
1
1
  /**
2
2
  * SunSpec block interfaces with block numbers
3
3
  */
4
+ export const DEFAULT_RETRY_CONFIG = {
5
+ initialDelayMs: 1000,
6
+ maxDelayMs: 30000,
7
+ backoffFactor: 1.5,
8
+ maxAttempts: 10
9
+ };
4
10
  /**
5
11
  * Common Sunspec Model IDs
6
12
  */
@@ -16,7 +16,8 @@
16
16
  * - pad: 0x8000 (always returns this value)
17
17
  * - string: all registers 0x0000 (NULL)
18
18
  */
19
- import { type SunspecInverterControls, type SunspecInverterData, type SunspecInverterSettings, type SunspecMeterData, type SunspecModel, type SunspecMPPTData, type SunspecBatteryData, type SunspecBatteryControls, SunspecStorageMode } from "./sunspec-interfaces.js";
19
+ import { type SunspecInverterControls, type SunspecInverterData, type SunspecInverterSettings, type SunspecMeterData, type SunspecModel, type SunspecMPPTData, type SunspecBatteryData, type SunspecBatteryControls, SunspecStorageMode, type IRetryConfig } from "./sunspec-interfaces.js";
20
+ import { ConnectionRetryManager } from "./connection-retry-manager.js";
20
21
  import { IConnectionHealth } from "@enyo-energy/energy-app-sdk/dist/implementations/modbus/interfaces.js";
21
22
  import { EnergyApp } from "@enyo-energy/energy-app-sdk";
22
23
  export declare class SunspecModbusClient {
@@ -28,15 +29,50 @@ export declare class SunspecModbusClient {
28
29
  private connectionHealth;
29
30
  private faultTolerantReader;
30
31
  private modbusDataTypeConverter;
31
- constructor(energyApp: EnergyApp);
32
+ private connectionParams;
33
+ private retryManager;
34
+ private autoReconnectEnabled;
35
+ constructor(energyApp: EnergyApp, retryConfig?: Partial<IRetryConfig>);
32
36
  /**
33
37
  * Connect to Modbus device
38
+ * @param host Primary host (hostname) to connect to
39
+ * @param port Modbus port (default 502)
40
+ * @param unitId Modbus unit ID (default 1)
41
+ * @param secondaryHost Optional secondary host (ipAddress) for fallback during reconnection
34
42
  */
35
- connect(host: string, port?: number, unitId?: number): Promise<void>;
43
+ connect(host: string, port?: number, unitId?: number, secondaryHost?: string): Promise<void>;
36
44
  /**
37
45
  * Disconnect from Modbus device
38
46
  */
39
47
  disconnect(): Promise<void>;
48
+ /**
49
+ * Reconnect using stored connection parameters
50
+ * First tries primaryHost (hostname), then falls back to secondaryHost (ipAddress) if available
51
+ * Returns true if reconnection was successful, false otherwise
52
+ */
53
+ reconnect(): Promise<boolean>;
54
+ /**
55
+ * Attempt to establish a connection to a specific host
56
+ * Returns true if successful, false otherwise
57
+ */
58
+ private attemptConnection;
59
+ /**
60
+ * Check connection health and trigger automatic reconnection if unhealthy
61
+ * Returns true if connection is healthy or was successfully restored
62
+ */
63
+ ensureHealthyConnection(): Promise<boolean>;
64
+ /**
65
+ * Enable or disable automatic reconnection
66
+ */
67
+ setAutoReconnect(enabled: boolean): void;
68
+ /**
69
+ * Check if auto-reconnect is enabled
70
+ */
71
+ isAutoReconnectEnabled(): boolean;
72
+ /**
73
+ * Get the retry manager for advanced configuration
74
+ */
75
+ getRetryManager(): ConnectionRetryManager;
40
76
  /**
41
77
  * Detect the base address and addressing mode (0-based or 1-based) for SunSpec
42
78
  */
@@ -17,6 +17,7 @@
17
17
  * - string: all registers 0x0000 (NULL)
18
18
  */
19
19
  import { SunspecModelId, SunspecBatteryChargeState, SunspecStorageControlMode, SunspecChargeSource, SunspecStorageMode } from "./sunspec-interfaces.js";
20
+ import { ConnectionRetryManager } from "./connection-retry-manager.js";
20
21
  import { EnergyAppModbusConnectionHealth } from "@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusConnectionHealth.js";
21
22
  import { EnergyAppModbusFaultTolerantReader } from "@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusFaultTolerantReader.js";
22
23
  import { EnergyAppModbusDataTypeConverter } from "@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusDataTypeConverter.js";
@@ -29,18 +30,28 @@ export class SunspecModbusClient {
29
30
  connectionHealth;
30
31
  faultTolerantReader = null;
31
32
  modbusDataTypeConverter;
32
- constructor(energyApp) {
33
+ connectionParams = null;
34
+ retryManager;
35
+ autoReconnectEnabled = true;
36
+ constructor(energyApp, retryConfig) {
33
37
  this.energyApp = energyApp;
34
38
  this.connectionHealth = new EnergyAppModbusConnectionHealth();
35
39
  this.modbusDataTypeConverter = new EnergyAppModbusDataTypeConverter();
40
+ this.retryManager = new ConnectionRetryManager(retryConfig);
36
41
  }
37
42
  /**
38
43
  * Connect to Modbus device
44
+ * @param host Primary host (hostname) to connect to
45
+ * @param port Modbus port (default 502)
46
+ * @param unitId Modbus unit ID (default 1)
47
+ * @param secondaryHost Optional secondary host (ipAddress) for fallback during reconnection
39
48
  */
40
- async connect(host, port = 502, unitId = 1) {
49
+ async connect(host, port = 502, unitId = 1, secondaryHost) {
41
50
  if (this.connected) {
42
51
  await this.disconnect();
43
52
  }
53
+ // Store connection parameters for potential reconnection
54
+ this.connectionParams = { primaryHost: host, secondaryHost, port, unitId };
44
55
  this.modbusClient = await this.energyApp.useModbus().connect({
45
56
  host,
46
57
  port,
@@ -53,12 +64,15 @@ export class SunspecModbusClient {
53
64
  }
54
65
  this.connected = true;
55
66
  this.connectionHealth.recordSuccess();
67
+ this.retryManager.reset();
56
68
  console.log(`Connected to Sunspec device at ${host}:${port} unit ${unitId}`);
57
69
  }
58
70
  /**
59
71
  * Disconnect from Modbus device
60
72
  */
61
73
  async disconnect() {
74
+ // Cancel any pending retry attempts
75
+ this.retryManager.cancelPendingRetry();
62
76
  if (this.modbusClient && this.connected) {
63
77
  await this.modbusClient.disconnect();
64
78
  this.modbusClient = null;
@@ -66,6 +80,144 @@ export class SunspecModbusClient {
66
80
  this.connected = false;
67
81
  this.discoveredModels.clear();
68
82
  }
83
+ // Clear stored connection params
84
+ this.connectionParams = null;
85
+ }
86
+ /**
87
+ * Reconnect using stored connection parameters
88
+ * First tries primaryHost (hostname), then falls back to secondaryHost (ipAddress) if available
89
+ * Returns true if reconnection was successful, false otherwise
90
+ */
91
+ async reconnect() {
92
+ if (!this.connectionParams) {
93
+ console.error('Cannot reconnect: no connection parameters stored. Call connect() first.');
94
+ return false;
95
+ }
96
+ const { primaryHost, secondaryHost, port, unitId } = this.connectionParams;
97
+ // Try primary host first
98
+ console.log(`Attempting to reconnect to primary host ${primaryHost}:${port} unit ${unitId}...`);
99
+ const primarySuccess = await this.attemptConnection(primaryHost, port, unitId);
100
+ if (primarySuccess) {
101
+ console.log(`Successfully reconnected to primary host ${primaryHost}:${port} unit ${unitId}`);
102
+ return true;
103
+ }
104
+ // If primary failed and secondary is available, try secondary
105
+ if (secondaryHost && secondaryHost !== primaryHost) {
106
+ console.log(`Primary host failed, attempting secondary host ${secondaryHost}:${port} unit ${unitId}...`);
107
+ const secondarySuccess = await this.attemptConnection(secondaryHost, port, unitId);
108
+ if (secondarySuccess) {
109
+ console.log(`Successfully reconnected to secondary host ${secondaryHost}:${port} unit ${unitId}`);
110
+ return true;
111
+ }
112
+ }
113
+ console.error(`Reconnection failed to all available hosts`);
114
+ this.connected = false;
115
+ return false;
116
+ }
117
+ /**
118
+ * Attempt to establish a connection to a specific host
119
+ * Returns true if successful, false otherwise
120
+ */
121
+ async attemptConnection(host, port, unitId) {
122
+ try {
123
+ // Disconnect existing connection if any
124
+ if (this.modbusClient) {
125
+ try {
126
+ await this.modbusClient.disconnect();
127
+ }
128
+ catch (e) {
129
+ // Ignore disconnect errors during reconnection
130
+ }
131
+ this.modbusClient = null;
132
+ this.faultTolerantReader = null;
133
+ }
134
+ // Attempt connection
135
+ this.modbusClient = await this.energyApp.useModbus().connect({
136
+ host,
137
+ port,
138
+ unitId,
139
+ timeout: 5000
140
+ });
141
+ // Create fault-tolerant reader with connection health monitoring
142
+ if (this.modbusClient) {
143
+ this.faultTolerantReader = new EnergyAppModbusFaultTolerantReader(this.modbusClient, this.connectionHealth);
144
+ }
145
+ this.connected = true;
146
+ this.connectionHealth.recordSuccess();
147
+ return true;
148
+ }
149
+ catch (error) {
150
+ console.error(`Connection attempt to ${host}:${port} failed: ${error}`);
151
+ return false;
152
+ }
153
+ }
154
+ /**
155
+ * Check connection health and trigger automatic reconnection if unhealthy
156
+ * Returns true if connection is healthy or was successfully restored
157
+ */
158
+ async ensureHealthyConnection() {
159
+ // If already healthy, return immediately
160
+ if (this.isHealthy()) {
161
+ return true;
162
+ }
163
+ // If no connection params, we can't reconnect
164
+ if (!this.connectionParams) {
165
+ console.error('Connection unhealthy and no connection parameters stored for reconnection.');
166
+ return false;
167
+ }
168
+ // If auto-reconnect is disabled, just report the status
169
+ if (!this.autoReconnectEnabled) {
170
+ console.log('Connection unhealthy but auto-reconnect is disabled.');
171
+ return false;
172
+ }
173
+ // If a retry is already in progress, don't start another
174
+ if (this.retryManager.isRetryInProgress()) {
175
+ console.log('Retry already in progress...');
176
+ return false;
177
+ }
178
+ console.log('Connection unhealthy, initiating reconnection with exponential backoff...');
179
+ // Attempt reconnection with exponential backoff
180
+ let success = false;
181
+ while (this.retryManager.shouldRetry() && !success) {
182
+ success = await this.retryManager.scheduleRetry(() => this.reconnect());
183
+ if (success) {
184
+ // Re-discover models after successful reconnection
185
+ try {
186
+ await this.discoverModels();
187
+ console.log('Models re-discovered after reconnection.');
188
+ }
189
+ catch (e) {
190
+ console.warn('Failed to re-discover models after reconnection:', e);
191
+ }
192
+ return true;
193
+ }
194
+ }
195
+ if (!success) {
196
+ console.error('Failed to restore healthy connection after all retry attempts.');
197
+ }
198
+ return success;
199
+ }
200
+ /**
201
+ * Enable or disable automatic reconnection
202
+ */
203
+ setAutoReconnect(enabled) {
204
+ this.autoReconnectEnabled = enabled;
205
+ console.log(`Auto-reconnect ${enabled ? 'enabled' : 'disabled'}`);
206
+ if (!enabled) {
207
+ this.retryManager.cancelPendingRetry();
208
+ }
209
+ }
210
+ /**
211
+ * Check if auto-reconnect is enabled
212
+ */
213
+ isAutoReconnectEnabled() {
214
+ return this.autoReconnectEnabled;
215
+ }
216
+ /**
217
+ * Get the retry manager for advanced configuration
218
+ */
219
+ getRetryManager() {
220
+ return this.retryManager;
69
221
  }
70
222
  /**
71
223
  * Detect the base address and addressing mode (0-based or 1-based) for SunSpec
package/dist/version.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "0.0.27";
8
+ export declare const SDK_VERSION = "0.0.29";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/dist/version.js CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export const SDK_VERSION = '0.0.27';
8
+ export const SDK_VERSION = '0.0.29';
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enyo-energy/sunspec-sdk",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "description": "enyo Energy Sunspec SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,7 +37,7 @@
37
37
  "typescript": "^5.8.3"
38
38
  },
39
39
  "dependencies": {
40
- "@enyo-energy/energy-app-sdk": "^0.0.52"
40
+ "@enyo-energy/energy-app-sdk": "^0.0.58"
41
41
  },
42
42
  "volta": {
43
43
  "node": "22.17.0"