@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.
- package/dist/cjs/connection-retry-manager.cjs +125 -0
- package/dist/cjs/connection-retry-manager.d.cts +53 -0
- package/dist/cjs/index.cjs +3 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/sunspec-devices.cjs +7 -2
- package/dist/cjs/sunspec-interfaces.cjs +7 -1
- package/dist/cjs/sunspec-interfaces.d.cts +10 -0
- package/dist/cjs/sunspec-modbus-client.cjs +154 -2
- package/dist/cjs/sunspec-modbus-client.d.cts +39 -3
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/connection-retry-manager.d.ts +53 -0
- package/dist/connection-retry-manager.js +121 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/sunspec-devices.js +7 -2
- package/dist/sunspec-interfaces.d.ts +10 -0
- package/dist/sunspec-interfaces.js +6 -0
- package/dist/sunspec-modbus-client.d.ts +39 -3
- package/dist/sunspec-modbus-client.js +154 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -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
|
+
}
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -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; } });
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
*/
|
package/dist/cjs/version.cjs
CHANGED
|
@@ -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.
|
|
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
|
package/dist/cjs/version.d.cts
CHANGED
|
@@ -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
package/dist/index.js
CHANGED
package/dist/sunspec-devices.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
*/
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enyo-energy/sunspec-sdk",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
40
|
+
"@enyo-energy/energy-app-sdk": "^0.0.58"
|
|
41
41
|
},
|
|
42
42
|
"volta": {
|
|
43
43
|
"node": "22.17.0"
|