@enyo-energy/sunspec-sdk 0.0.32 → 0.0.33
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 +63 -80
- package/dist/cjs/connection-retry-manager.d.cts +32 -27
- package/dist/cjs/sunspec-devices.cjs +175 -32
- package/dist/cjs/sunspec-devices.d.cts +47 -6
- package/dist/cjs/sunspec-interfaces.cjs +28 -5
- package/dist/cjs/sunspec-interfaces.d.cts +18 -5
- package/dist/cjs/sunspec-modbus-client.cjs +61 -91
- package/dist/cjs/sunspec-modbus-client.d.cts +27 -28
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/connection-retry-manager.d.ts +32 -27
- package/dist/connection-retry-manager.js +63 -80
- package/dist/sunspec-devices.d.ts +47 -6
- package/dist/sunspec-devices.js +175 -32
- package/dist/sunspec-interfaces.d.ts +18 -5
- package/dist/sunspec-interfaces.js +27 -4
- package/dist/sunspec-modbus-client.d.ts +27 -28
- package/dist/sunspec-modbus-client.js +62 -92
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -1,53 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Connection Retry Manager with
|
|
2
|
+
* Connection Retry Manager with Tiered Schedule
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Poll-based retry manager — no timers or setTimeout loops.
|
|
5
|
+
* Called on each readData() cycle; determines whether enough time has elapsed
|
|
6
|
+
* to attempt a reconnection based on the current retry phase.
|
|
7
|
+
*
|
|
8
|
+
* Default schedule:
|
|
9
|
+
* - Phase 1: every 10s for 1 minute
|
|
10
|
+
* - Phase 2: every 30s for 2 minutes
|
|
11
|
+
* - Phase 3: every 1m for 5 minutes
|
|
12
|
+
* - Phase 4: every 5m forever
|
|
6
13
|
*/
|
|
7
|
-
import { IRetryConfig } from './sunspec-interfaces.js';
|
|
14
|
+
import { IRetryConfig, IRetryPhase } from './sunspec-interfaces.js';
|
|
8
15
|
export declare class ConnectionRetryManager {
|
|
9
16
|
private config;
|
|
10
|
-
private
|
|
11
|
-
private
|
|
12
|
-
private
|
|
13
|
-
constructor(config?:
|
|
17
|
+
private disconnectedSince;
|
|
18
|
+
private lastAttemptTime;
|
|
19
|
+
private attemptCount;
|
|
20
|
+
constructor(config?: IRetryConfig);
|
|
14
21
|
/**
|
|
15
|
-
*
|
|
16
|
-
* Formula: min(initialDelayMs * backoffFactor^attempt, maxDelayMs)
|
|
22
|
+
* Mark the connection as disconnected. Sets the timestamp if not already set.
|
|
17
23
|
*/
|
|
18
|
-
|
|
24
|
+
markDisconnected(): void;
|
|
19
25
|
/**
|
|
20
|
-
* Reset
|
|
26
|
+
* Reset all retry state (call on successful reconnect).
|
|
21
27
|
*/
|
|
22
28
|
reset(): void;
|
|
23
29
|
/**
|
|
24
|
-
*
|
|
25
|
-
* Returns true if we haven't exceeded maxAttempts or if maxAttempts is -1 (infinite)
|
|
30
|
+
* Determine which retry phase we're currently in based on elapsed time since disconnect.
|
|
26
31
|
*/
|
|
27
|
-
|
|
32
|
+
getCurrentPhase(): IRetryPhase;
|
|
28
33
|
/**
|
|
29
|
-
*
|
|
34
|
+
* Check if we should attempt a reconnection now.
|
|
35
|
+
* Called on every readData() poll cycle.
|
|
30
36
|
*/
|
|
31
|
-
|
|
37
|
+
shouldAttemptNow(): boolean;
|
|
32
38
|
/**
|
|
33
|
-
*
|
|
39
|
+
* Record that an attempt was just made.
|
|
34
40
|
*/
|
|
35
|
-
|
|
41
|
+
recordAttempt(): void;
|
|
36
42
|
/**
|
|
37
|
-
*
|
|
43
|
+
* Get the number of reconnection attempts made.
|
|
38
44
|
*/
|
|
39
|
-
|
|
45
|
+
getAttemptCount(): number;
|
|
40
46
|
/**
|
|
41
|
-
*
|
|
42
|
-
* Returns a promise that resolves when the callback completes
|
|
47
|
+
* Get milliseconds elapsed since disconnect was first detected.
|
|
43
48
|
*/
|
|
44
|
-
|
|
49
|
+
getElapsedMs(): number;
|
|
45
50
|
/**
|
|
46
|
-
*
|
|
51
|
+
* Check if the manager is currently tracking a disconnection.
|
|
47
52
|
*/
|
|
48
|
-
|
|
53
|
+
isDisconnected(): boolean;
|
|
49
54
|
/**
|
|
50
|
-
* Get the current configuration
|
|
55
|
+
* Get the current configuration.
|
|
51
56
|
*/
|
|
52
57
|
getConfig(): IRetryConfig;
|
|
53
58
|
}
|
|
@@ -1,119 +1,102 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Connection Retry Manager with
|
|
2
|
+
* Connection Retry Manager with Tiered Schedule
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Poll-based retry manager — no timers or setTimeout loops.
|
|
5
|
+
* Called on each readData() cycle; determines whether enough time has elapsed
|
|
6
|
+
* to attempt a reconnection based on the current retry phase.
|
|
7
|
+
*
|
|
8
|
+
* Default schedule:
|
|
9
|
+
* - Phase 1: every 10s for 1 minute
|
|
10
|
+
* - Phase 2: every 30s for 2 minutes
|
|
11
|
+
* - Phase 3: every 1m for 5 minutes
|
|
12
|
+
* - Phase 4: every 5m forever
|
|
6
13
|
*/
|
|
7
14
|
import { DEFAULT_RETRY_CONFIG } from './sunspec-interfaces.js';
|
|
8
15
|
export class ConnectionRetryManager {
|
|
9
16
|
config;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
disconnectedSince = null;
|
|
18
|
+
lastAttemptTime = 0;
|
|
19
|
+
attemptCount = 0;
|
|
13
20
|
constructor(config) {
|
|
14
|
-
this.config =
|
|
21
|
+
this.config = config ?? DEFAULT_RETRY_CONFIG;
|
|
15
22
|
}
|
|
16
23
|
/**
|
|
17
|
-
*
|
|
18
|
-
* Formula: min(initialDelayMs * backoffFactor^attempt, maxDelayMs)
|
|
24
|
+
* Mark the connection as disconnected. Sets the timestamp if not already set.
|
|
19
25
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
markDisconnected() {
|
|
27
|
+
if (this.disconnectedSince === null) {
|
|
28
|
+
this.disconnectedSince = Date.now();
|
|
29
|
+
}
|
|
23
30
|
}
|
|
24
31
|
/**
|
|
25
|
-
* Reset
|
|
32
|
+
* Reset all retry state (call on successful reconnect).
|
|
26
33
|
*/
|
|
27
34
|
reset() {
|
|
28
|
-
this.
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
35
|
+
this.disconnectedSince = null;
|
|
36
|
+
this.lastAttemptTime = 0;
|
|
37
|
+
this.attemptCount = 0;
|
|
31
38
|
}
|
|
32
39
|
/**
|
|
33
|
-
*
|
|
34
|
-
* Returns true if we haven't exceeded maxAttempts or if maxAttempts is -1 (infinite)
|
|
40
|
+
* Determine which retry phase we're currently in based on elapsed time since disconnect.
|
|
35
41
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
getCurrentPhase() {
|
|
43
|
+
const elapsed = this.getElapsedMs();
|
|
44
|
+
let cumulativeDuration = 0;
|
|
45
|
+
for (const phase of this.config.phases) {
|
|
46
|
+
if (phase.durationMs === 0) {
|
|
47
|
+
// This is the final "forever" phase
|
|
48
|
+
return phase;
|
|
49
|
+
}
|
|
50
|
+
cumulativeDuration += phase.durationMs;
|
|
51
|
+
if (elapsed < cumulativeDuration) {
|
|
52
|
+
return phase;
|
|
53
|
+
}
|
|
39
54
|
}
|
|
40
|
-
return
|
|
55
|
+
// Fallback: return last phase
|
|
56
|
+
return this.config.phases[this.config.phases.length - 1];
|
|
41
57
|
}
|
|
42
58
|
/**
|
|
43
|
-
*
|
|
59
|
+
* Check if we should attempt a reconnection now.
|
|
60
|
+
* Called on every readData() poll cycle.
|
|
44
61
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
62
|
+
shouldAttemptNow() {
|
|
63
|
+
if (this.disconnectedSince === null) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const phase = this.getCurrentPhase();
|
|
68
|
+
return (now - this.lastAttemptTime) >= phase.intervalMs;
|
|
47
69
|
}
|
|
48
70
|
/**
|
|
49
|
-
*
|
|
71
|
+
* Record that an attempt was just made.
|
|
50
72
|
*/
|
|
51
|
-
|
|
52
|
-
|
|
73
|
+
recordAttempt() {
|
|
74
|
+
this.lastAttemptTime = Date.now();
|
|
75
|
+
this.attemptCount++;
|
|
53
76
|
}
|
|
54
77
|
/**
|
|
55
|
-
*
|
|
78
|
+
* Get the number of reconnection attempts made.
|
|
56
79
|
*/
|
|
57
|
-
|
|
58
|
-
return this.
|
|
80
|
+
getAttemptCount() {
|
|
81
|
+
return this.attemptCount;
|
|
59
82
|
}
|
|
60
83
|
/**
|
|
61
|
-
*
|
|
62
|
-
* Returns a promise that resolves when the callback completes
|
|
84
|
+
* Get milliseconds elapsed since disconnect was first detected.
|
|
63
85
|
*/
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
if (this.isRetrying) {
|
|
70
|
-
console.log('Retry already in progress, skipping duplicate request.');
|
|
71
|
-
return false;
|
|
86
|
+
getElapsedMs() {
|
|
87
|
+
if (this.disconnectedSince === null) {
|
|
88
|
+
return 0;
|
|
72
89
|
}
|
|
73
|
-
|
|
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
|
-
});
|
|
90
|
+
return Date.now() - this.disconnectedSince;
|
|
103
91
|
}
|
|
104
92
|
/**
|
|
105
|
-
*
|
|
93
|
+
* Check if the manager is currently tracking a disconnection.
|
|
106
94
|
*/
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
clearTimeout(this.pendingRetryTimeout);
|
|
110
|
-
this.pendingRetryTimeout = null;
|
|
111
|
-
this.isRetrying = false;
|
|
112
|
-
console.log('Pending retry cancelled.');
|
|
113
|
-
}
|
|
95
|
+
isDisconnected() {
|
|
96
|
+
return this.disconnectedSince !== null;
|
|
114
97
|
}
|
|
115
98
|
/**
|
|
116
|
-
* Get the current configuration
|
|
99
|
+
* Get the current configuration.
|
|
117
100
|
*/
|
|
118
101
|
getConfig() {
|
|
119
102
|
return { ...this.config };
|
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
import { type SunspecBatteryBaseData, type SunspecBatteryControls, SunspecStorageMode } from "./sunspec-interfaces.js";
|
|
1
|
+
import { type SunspecBatteryBaseData, type SunspecBatteryControls, SunspecStorageMode, SunspecInverterCapability, SunspecBatteryCapability, SunspecMeterCapability } from "./sunspec-interfaces.js";
|
|
2
2
|
import { ApplianceManager, EnergyApp } from "@enyo-energy/energy-app-sdk";
|
|
3
3
|
import { EnyoApplianceName } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
|
|
4
4
|
import { EnyoNetworkDevice } from "@enyo-energy/energy-app-sdk/dist/types/enyo-network-device.js";
|
|
5
5
|
import { SunspecModbusClient } from "./sunspec-modbus-client.js";
|
|
6
|
-
import {
|
|
6
|
+
import { ConnectionRetryManager } from "./connection-retry-manager.js";
|
|
7
|
+
import { type IRetryConfig } from "./sunspec-interfaces.js";
|
|
8
|
+
import { EnyoCommandAcknowledgeAnswerEnum, EnyoDataBusMessage, EnyoDataBusMessageEnum, EnyoDataBusMessageResolution } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
|
|
9
|
+
import { EnergyAppDataBus } from "@enyo-energy/energy-app-sdk/dist/packages/energy-app-data-bus.js";
|
|
10
|
+
export declare const ENYO_DATA_BUS_SET_INVERTER_FEED_IN_LIMIT_V1 = "SetInverterFeedInLimitV1";
|
|
11
|
+
export interface EnyoDataBusSetInverterFeedInLimitV1 {
|
|
12
|
+
id: string;
|
|
13
|
+
type: 'message';
|
|
14
|
+
message: string;
|
|
15
|
+
applianceId: string;
|
|
16
|
+
data: {
|
|
17
|
+
feedInLimitW: number | null;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
7
20
|
/**
|
|
8
21
|
* Base abstract class for all Sunspec devices
|
|
9
22
|
*/
|
|
@@ -18,7 +31,10 @@ export declare abstract class BaseSunspecDevice {
|
|
|
18
31
|
protected readonly baseAddress: number;
|
|
19
32
|
protected applianceId?: string;
|
|
20
33
|
protected lastUpdateTime: number;
|
|
21
|
-
|
|
34
|
+
protected dataBusListenerId?: string;
|
|
35
|
+
protected dataBus?: EnergyAppDataBus;
|
|
36
|
+
protected retryManager: ConnectionRetryManager;
|
|
37
|
+
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, retryConfig?: IRetryConfig);
|
|
22
38
|
/**
|
|
23
39
|
* Connect to the device and create/update the appliance
|
|
24
40
|
*/
|
|
@@ -42,11 +58,24 @@ export declare abstract class BaseSunspecDevice {
|
|
|
42
58
|
* Ensure the Sunspec client is connected and models are discovered
|
|
43
59
|
*/
|
|
44
60
|
protected ensureConnected(): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Attempt a reconnection if the tiered retry schedule allows it.
|
|
63
|
+
* Called from readData() when the device is disconnected.
|
|
64
|
+
* Returns true if reconnection succeeded.
|
|
65
|
+
*/
|
|
66
|
+
protected tryReconnect(): Promise<boolean>;
|
|
67
|
+
/**
|
|
68
|
+
* Mark the device as offline: update appliance state and start tracking disconnection.
|
|
69
|
+
*/
|
|
70
|
+
protected markOffline(): Promise<void>;
|
|
71
|
+
protected sendCommandAcknowledge(messageId: string, acknowledgeMessage: EnyoDataBusMessageEnum | string, answer: EnyoCommandAcknowledgeAnswerEnum, rejectionReason?: string): void;
|
|
45
72
|
}
|
|
46
73
|
/**
|
|
47
74
|
* Sunspec Inverter implementation using dynamic model discovery
|
|
48
75
|
*/
|
|
49
76
|
export declare class SunspecInverter extends BaseSunspecDevice {
|
|
77
|
+
private readonly capabilities;
|
|
78
|
+
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, capabilities?: SunspecInverterCapability[], retryConfig?: IRetryConfig);
|
|
50
79
|
connect(): Promise<void>;
|
|
51
80
|
disconnect(): Promise<void>;
|
|
52
81
|
isConnected(): boolean;
|
|
@@ -59,13 +88,24 @@ export declare class SunspecInverter extends BaseSunspecDevice {
|
|
|
59
88
|
private mapMPPTToStrings;
|
|
60
89
|
private mapMPPTOperatingState;
|
|
61
90
|
private mapDcStringToApplianceMetadata;
|
|
91
|
+
/**
|
|
92
|
+
* Start listening for inverter commands on the data bus.
|
|
93
|
+
* Idempotent — does nothing if already listening.
|
|
94
|
+
*/
|
|
95
|
+
startDataBusListening(): void;
|
|
96
|
+
/**
|
|
97
|
+
* Stop listening for inverter commands on the data bus.
|
|
98
|
+
*/
|
|
99
|
+
stopDataBusListening(): void;
|
|
100
|
+
private handleInverterCommand;
|
|
101
|
+
private handleSetFeedInLimit;
|
|
62
102
|
}
|
|
63
103
|
/**
|
|
64
104
|
* Sunspec Battery implementation
|
|
65
105
|
*/
|
|
66
106
|
export declare class SunspecBattery extends BaseSunspecDevice {
|
|
67
|
-
private
|
|
68
|
-
|
|
107
|
+
private readonly capabilities;
|
|
108
|
+
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, capabilities?: SunspecBatteryCapability[], retryConfig?: IRetryConfig);
|
|
69
109
|
/**
|
|
70
110
|
* Connect to the battery and create/update the appliance
|
|
71
111
|
*/
|
|
@@ -176,12 +216,13 @@ export declare class SunspecBattery extends BaseSunspecDevice {
|
|
|
176
216
|
private handleStartGridCharge;
|
|
177
217
|
private handleStopGridCharge;
|
|
178
218
|
private handleSetDischargeLimit;
|
|
179
|
-
private sendCommandAcknowledge;
|
|
180
219
|
}
|
|
181
220
|
/**
|
|
182
221
|
* Sunspec Meter implementation
|
|
183
222
|
*/
|
|
184
223
|
export declare class SunspecMeter extends BaseSunspecDevice {
|
|
224
|
+
private readonly capabilities;
|
|
225
|
+
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, capabilities?: SunspecMeterCapability[], retryConfig?: IRetryConfig);
|
|
185
226
|
/**
|
|
186
227
|
* Connect to the meter and create/update the appliance
|
|
187
228
|
*/
|