@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,122 +1,105 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Connection Retry Manager with
|
|
3
|
+
* Connection Retry Manager with Tiered Schedule
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Poll-based retry manager — no timers or setTimeout loops.
|
|
6
|
+
* Called on each readData() cycle; determines whether enough time has elapsed
|
|
7
|
+
* to attempt a reconnection based on the current retry phase.
|
|
8
|
+
*
|
|
9
|
+
* Default schedule:
|
|
10
|
+
* - Phase 1: every 10s for 1 minute
|
|
11
|
+
* - Phase 2: every 30s for 2 minutes
|
|
12
|
+
* - Phase 3: every 1m for 5 minutes
|
|
13
|
+
* - Phase 4: every 5m forever
|
|
7
14
|
*/
|
|
8
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
16
|
exports.ConnectionRetryManager = void 0;
|
|
10
17
|
const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
|
|
11
18
|
class ConnectionRetryManager {
|
|
12
19
|
config;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
disconnectedSince = null;
|
|
21
|
+
lastAttemptTime = 0;
|
|
22
|
+
attemptCount = 0;
|
|
16
23
|
constructor(config) {
|
|
17
|
-
this.config =
|
|
24
|
+
this.config = config ?? sunspec_interfaces_js_1.DEFAULT_RETRY_CONFIG;
|
|
18
25
|
}
|
|
19
26
|
/**
|
|
20
|
-
*
|
|
21
|
-
* Formula: min(initialDelayMs * backoffFactor^attempt, maxDelayMs)
|
|
27
|
+
* Mark the connection as disconnected. Sets the timestamp if not already set.
|
|
22
28
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
markDisconnected() {
|
|
30
|
+
if (this.disconnectedSince === null) {
|
|
31
|
+
this.disconnectedSince = Date.now();
|
|
32
|
+
}
|
|
26
33
|
}
|
|
27
34
|
/**
|
|
28
|
-
* Reset
|
|
35
|
+
* Reset all retry state (call on successful reconnect).
|
|
29
36
|
*/
|
|
30
37
|
reset() {
|
|
31
|
-
this.
|
|
32
|
-
this.
|
|
33
|
-
this.
|
|
38
|
+
this.disconnectedSince = null;
|
|
39
|
+
this.lastAttemptTime = 0;
|
|
40
|
+
this.attemptCount = 0;
|
|
34
41
|
}
|
|
35
42
|
/**
|
|
36
|
-
*
|
|
37
|
-
* Returns true if we haven't exceeded maxAttempts or if maxAttempts is -1 (infinite)
|
|
43
|
+
* Determine which retry phase we're currently in based on elapsed time since disconnect.
|
|
38
44
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
getCurrentPhase() {
|
|
46
|
+
const elapsed = this.getElapsedMs();
|
|
47
|
+
let cumulativeDuration = 0;
|
|
48
|
+
for (const phase of this.config.phases) {
|
|
49
|
+
if (phase.durationMs === 0) {
|
|
50
|
+
// This is the final "forever" phase
|
|
51
|
+
return phase;
|
|
52
|
+
}
|
|
53
|
+
cumulativeDuration += phase.durationMs;
|
|
54
|
+
if (elapsed < cumulativeDuration) {
|
|
55
|
+
return phase;
|
|
56
|
+
}
|
|
42
57
|
}
|
|
43
|
-
return
|
|
58
|
+
// Fallback: return last phase
|
|
59
|
+
return this.config.phases[this.config.phases.length - 1];
|
|
44
60
|
}
|
|
45
61
|
/**
|
|
46
|
-
*
|
|
62
|
+
* Check if we should attempt a reconnection now.
|
|
63
|
+
* Called on every readData() poll cycle.
|
|
47
64
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
shouldAttemptNow() {
|
|
66
|
+
if (this.disconnectedSince === null) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const phase = this.getCurrentPhase();
|
|
71
|
+
return (now - this.lastAttemptTime) >= phase.intervalMs;
|
|
50
72
|
}
|
|
51
73
|
/**
|
|
52
|
-
*
|
|
74
|
+
* Record that an attempt was just made.
|
|
53
75
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
76
|
+
recordAttempt() {
|
|
77
|
+
this.lastAttemptTime = Date.now();
|
|
78
|
+
this.attemptCount++;
|
|
56
79
|
}
|
|
57
80
|
/**
|
|
58
|
-
*
|
|
81
|
+
* Get the number of reconnection attempts made.
|
|
59
82
|
*/
|
|
60
|
-
|
|
61
|
-
return this.
|
|
83
|
+
getAttemptCount() {
|
|
84
|
+
return this.attemptCount;
|
|
62
85
|
}
|
|
63
86
|
/**
|
|
64
|
-
*
|
|
65
|
-
* Returns a promise that resolves when the callback completes
|
|
87
|
+
* Get milliseconds elapsed since disconnect was first detected.
|
|
66
88
|
*/
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
if (this.isRetrying) {
|
|
73
|
-
console.log('Retry already in progress, skipping duplicate request.');
|
|
74
|
-
return false;
|
|
89
|
+
getElapsedMs() {
|
|
90
|
+
if (this.disconnectedSince === null) {
|
|
91
|
+
return 0;
|
|
75
92
|
}
|
|
76
|
-
|
|
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
|
-
});
|
|
93
|
+
return Date.now() - this.disconnectedSince;
|
|
106
94
|
}
|
|
107
95
|
/**
|
|
108
|
-
*
|
|
96
|
+
* Check if the manager is currently tracking a disconnection.
|
|
109
97
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
clearTimeout(this.pendingRetryTimeout);
|
|
113
|
-
this.pendingRetryTimeout = null;
|
|
114
|
-
this.isRetrying = false;
|
|
115
|
-
console.log('Pending retry cancelled.');
|
|
116
|
-
}
|
|
98
|
+
isDisconnected() {
|
|
99
|
+
return this.disconnectedSince !== null;
|
|
117
100
|
}
|
|
118
101
|
/**
|
|
119
|
-
* Get the current configuration
|
|
102
|
+
* Get the current configuration.
|
|
120
103
|
*/
|
|
121
104
|
getConfig() {
|
|
122
105
|
return { ...this.config };
|
|
@@ -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.cjs';
|
|
14
|
+
import { IRetryConfig, IRetryPhase } from './sunspec-interfaces.cjs';
|
|
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,13 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SunspecMeter = exports.SunspecBattery = exports.SunspecInverter = exports.BaseSunspecDevice = void 0;
|
|
3
|
+
exports.SunspecMeter = exports.SunspecBattery = exports.SunspecInverter = exports.BaseSunspecDevice = exports.ENYO_DATA_BUS_SET_INVERTER_FEED_IN_LIMIT_V1 = void 0;
|
|
4
4
|
const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
|
|
5
5
|
const node_crypto_1 = require("node:crypto");
|
|
6
6
|
const enyo_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js");
|
|
7
|
+
const connection_retry_manager_js_1 = require("./connection-retry-manager.cjs");
|
|
7
8
|
const enyo_data_bus_value_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js");
|
|
8
9
|
const enyo_source_enum_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-source.enum.js");
|
|
9
10
|
const enyo_meter_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-meter-appliance.js");
|
|
10
11
|
const enyo_battery_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js");
|
|
12
|
+
// TODO: Remove once added to @enyo-energy/energy-app-sdk EnyoDataBusMessageEnum
|
|
13
|
+
exports.ENYO_DATA_BUS_SET_INVERTER_FEED_IN_LIMIT_V1 = 'SetInverterFeedInLimitV1';
|
|
11
14
|
/**
|
|
12
15
|
* Extract battery discharge power from MPPT data.
|
|
13
16
|
* Returns the discharge power in Watts (positive value), or 0 if no discharge.
|
|
@@ -35,7 +38,10 @@ class BaseSunspecDevice {
|
|
|
35
38
|
baseAddress;
|
|
36
39
|
applianceId;
|
|
37
40
|
lastUpdateTime = 0;
|
|
38
|
-
|
|
41
|
+
dataBusListenerId;
|
|
42
|
+
dataBus;
|
|
43
|
+
retryManager;
|
|
44
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, retryConfig) {
|
|
39
45
|
this.energyApp = energyApp;
|
|
40
46
|
this.name = name;
|
|
41
47
|
this.networkDevice = networkDevice;
|
|
@@ -44,6 +50,7 @@ class BaseSunspecDevice {
|
|
|
44
50
|
this.unitId = unitId;
|
|
45
51
|
this.port = port;
|
|
46
52
|
this.baseAddress = baseAddress;
|
|
53
|
+
this.retryManager = new connection_retry_manager_js_1.ConnectionRetryManager(retryConfig);
|
|
47
54
|
}
|
|
48
55
|
/**
|
|
49
56
|
* Check if the device is connected
|
|
@@ -65,12 +72,87 @@ class BaseSunspecDevice {
|
|
|
65
72
|
await this.sunspecClient.discoverModels(this.baseAddress);
|
|
66
73
|
}
|
|
67
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Attempt a reconnection if the tiered retry schedule allows it.
|
|
77
|
+
* Called from readData() when the device is disconnected.
|
|
78
|
+
* Returns true if reconnection succeeded.
|
|
79
|
+
*/
|
|
80
|
+
async tryReconnect() {
|
|
81
|
+
this.retryManager.markDisconnected();
|
|
82
|
+
if (!this.retryManager.shouldAttemptNow()) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
this.retryManager.recordAttempt();
|
|
86
|
+
const phase = this.retryManager.getCurrentPhase();
|
|
87
|
+
const attempt = this.retryManager.getAttemptCount();
|
|
88
|
+
const elapsed = Math.round(this.retryManager.getElapsedMs() / 1000);
|
|
89
|
+
console.log(`${this.constructor.name} ${this.applianceId}: Reconnect attempt #${attempt} ` +
|
|
90
|
+
`(phase: ${phase.intervalMs / 1000}s interval, elapsed: ${elapsed}s)`);
|
|
91
|
+
try {
|
|
92
|
+
const success = await this.sunspecClient.reconnect();
|
|
93
|
+
if (success) {
|
|
94
|
+
// Re-discover models after reconnect
|
|
95
|
+
await this.sunspecClient.discoverModels(this.baseAddress);
|
|
96
|
+
this.retryManager.reset();
|
|
97
|
+
// Update appliance state to Connected
|
|
98
|
+
if (this.applianceId) {
|
|
99
|
+
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Connected);
|
|
100
|
+
}
|
|
101
|
+
console.log(`${this.constructor.name} ${this.applianceId}: Reconnection successful after ${attempt} attempt(s)`);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.error(`${this.constructor.name} ${this.applianceId}: Reconnect attempt #${attempt} failed: ${error}`);
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Mark the device as offline: update appliance state and start tracking disconnection.
|
|
112
|
+
*/
|
|
113
|
+
async markOffline() {
|
|
114
|
+
this.retryManager.markDisconnected();
|
|
115
|
+
if (this.applianceId) {
|
|
116
|
+
try {
|
|
117
|
+
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.error(`${this.constructor.name} ${this.applianceId}: Failed to mark appliance offline: ${error}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
sendCommandAcknowledge(messageId, acknowledgeMessage, answer, rejectionReason) {
|
|
125
|
+
if (!this.dataBus || !this.applianceId) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const ackMessage = {
|
|
129
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
130
|
+
message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.CommandAcknowledgeV1,
|
|
131
|
+
type: 'answer',
|
|
132
|
+
source: enyo_source_enum_js_1.EnyoSourceEnum.Device,
|
|
133
|
+
applianceId: this.applianceId,
|
|
134
|
+
timestampIso: new Date().toISOString(),
|
|
135
|
+
data: {
|
|
136
|
+
messageId,
|
|
137
|
+
acknowledgeMessage: acknowledgeMessage,
|
|
138
|
+
answer,
|
|
139
|
+
rejectionReason
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
console.log(`${this.constructor.name} ${this.applianceId}: sending ${answer} for ${acknowledgeMessage} (messageId=${messageId}${rejectionReason ? `, reason=${rejectionReason}` : ''})`);
|
|
143
|
+
this.dataBus.sendMessage([ackMessage]);
|
|
144
|
+
}
|
|
68
145
|
}
|
|
69
146
|
exports.BaseSunspecDevice = BaseSunspecDevice;
|
|
70
147
|
/**
|
|
71
148
|
* Sunspec Inverter implementation using dynamic model discovery
|
|
72
149
|
*/
|
|
73
150
|
class SunspecInverter extends BaseSunspecDevice {
|
|
151
|
+
capabilities;
|
|
152
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig) {
|
|
153
|
+
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig);
|
|
154
|
+
this.capabilities = capabilities;
|
|
155
|
+
}
|
|
74
156
|
async connect() {
|
|
75
157
|
// Ensure Sunspec client is connected
|
|
76
158
|
if (!this.sunspecClient.isConnected()) {
|
|
@@ -111,8 +193,10 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
111
193
|
if (mpptModel) {
|
|
112
194
|
console.log(`MPPT model found for inverter ${this.networkDevice.hostname}`);
|
|
113
195
|
}
|
|
196
|
+
this.startDataBusListening();
|
|
114
197
|
}
|
|
115
198
|
async disconnect() {
|
|
199
|
+
this.stopDataBusListening();
|
|
116
200
|
if (this.applianceId) {
|
|
117
201
|
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
|
|
118
202
|
}
|
|
@@ -123,7 +207,9 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
123
207
|
}
|
|
124
208
|
async readData(clockId, resolution) {
|
|
125
209
|
if (!this.isConnected()) {
|
|
126
|
-
|
|
210
|
+
await this.tryReconnect();
|
|
211
|
+
if (!this.isConnected())
|
|
212
|
+
return [];
|
|
127
213
|
}
|
|
128
214
|
const messages = [];
|
|
129
215
|
const timestamp = new Date();
|
|
@@ -173,6 +259,7 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
173
259
|
}
|
|
174
260
|
catch (error) {
|
|
175
261
|
console.error(`Error updating inverter data: ${error}`);
|
|
262
|
+
await this.markOffline();
|
|
176
263
|
}
|
|
177
264
|
return messages;
|
|
178
265
|
}
|
|
@@ -243,14 +330,73 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
243
330
|
mapDcStringToApplianceMetadata(mpptDataList) {
|
|
244
331
|
return mpptDataList.map(s => ({ index: s.index, name: s.name }));
|
|
245
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Start listening for inverter commands on the data bus.
|
|
335
|
+
* Idempotent — does nothing if already listening.
|
|
336
|
+
*/
|
|
337
|
+
startDataBusListening() {
|
|
338
|
+
if (this.dataBusListenerId) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
this.dataBus = this.energyApp.useDataBus();
|
|
342
|
+
this.dataBusListenerId = this.dataBus.listenForMessages([exports.ENYO_DATA_BUS_SET_INVERTER_FEED_IN_LIMIT_V1], (entry) => this.handleInverterCommand(entry));
|
|
343
|
+
console.log(`Inverter ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Stop listening for inverter commands on the data bus.
|
|
347
|
+
*/
|
|
348
|
+
stopDataBusListening() {
|
|
349
|
+
if (this.dataBusListenerId && this.dataBus) {
|
|
350
|
+
this.dataBus.unsubscribe(this.dataBusListenerId);
|
|
351
|
+
console.log(`Inverter ${this.applianceId}: stopped data bus listening (listener ${this.dataBusListenerId})`);
|
|
352
|
+
}
|
|
353
|
+
this.dataBusListenerId = undefined;
|
|
354
|
+
this.dataBus = undefined;
|
|
355
|
+
}
|
|
356
|
+
handleInverterCommand(entry) {
|
|
357
|
+
if (entry.applianceId !== this.applianceId) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
void (async () => {
|
|
361
|
+
try {
|
|
362
|
+
if (entry.message === exports.ENYO_DATA_BUS_SET_INVERTER_FEED_IN_LIMIT_V1) {
|
|
363
|
+
await this.handleSetFeedInLimit(entry);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
console.error(`Inverter ${this.applianceId}: error handling ${entry.message}:`, error);
|
|
368
|
+
}
|
|
369
|
+
})();
|
|
370
|
+
}
|
|
371
|
+
async handleSetFeedInLimit(msg) {
|
|
372
|
+
// Check capability
|
|
373
|
+
if (!this.capabilities.includes(sunspec_interfaces_js_1.SunspecInverterCapability.FeedInLimit)) {
|
|
374
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported, 'FeedInLimit capability not enabled');
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (!this.isConnected() || !this.applianceId) {
|
|
378
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
console.log(`Inverter ${this.applianceId}: handling SetInverterFeedInLimitV1 (feedInLimitW=${msg.data.feedInLimitW})`);
|
|
382
|
+
const success = await this.sunspecClient.setFeedInLimit(msg.data.feedInLimitW);
|
|
383
|
+
if (!success) {
|
|
384
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set feed-in limit');
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
388
|
+
}
|
|
246
389
|
}
|
|
247
390
|
exports.SunspecInverter = SunspecInverter;
|
|
248
391
|
/**
|
|
249
392
|
* Sunspec Battery implementation
|
|
250
393
|
*/
|
|
251
394
|
class SunspecBattery extends BaseSunspecDevice {
|
|
252
|
-
|
|
253
|
-
|
|
395
|
+
capabilities;
|
|
396
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig) {
|
|
397
|
+
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig);
|
|
398
|
+
this.capabilities = capabilities;
|
|
399
|
+
}
|
|
254
400
|
/**
|
|
255
401
|
* Connect to the battery and create/update the appliance
|
|
256
402
|
*/
|
|
@@ -337,7 +483,9 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
337
483
|
*/
|
|
338
484
|
async readData(clockId, resolution) {
|
|
339
485
|
if (!this.isConnected()) {
|
|
340
|
-
|
|
486
|
+
await this.tryReconnect();
|
|
487
|
+
if (!this.isConnected())
|
|
488
|
+
return [];
|
|
341
489
|
}
|
|
342
490
|
const messages = [];
|
|
343
491
|
const timestamp = new Date();
|
|
@@ -395,6 +543,7 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
395
543
|
}
|
|
396
544
|
catch (error) {
|
|
397
545
|
console.error(`Error updating battery data: ${error}`);
|
|
546
|
+
await this.markOffline();
|
|
398
547
|
}
|
|
399
548
|
return messages;
|
|
400
549
|
}
|
|
@@ -711,45 +860,36 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
711
860
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported);
|
|
712
861
|
return;
|
|
713
862
|
}
|
|
714
|
-
console.log(`Battery ${this.applianceId}: handling SetStorageDischargeLimitV1 (
|
|
715
|
-
// Read current state for
|
|
863
|
+
console.log(`Battery ${this.applianceId}: handling SetStorageDischargeLimitV1 (dischargeLimitW=${msg.data.dischargeLimitW})`);
|
|
864
|
+
// Read current state to get wChaMax for percentage conversion
|
|
716
865
|
const controls = await this.getBatteryControls();
|
|
717
|
-
console.log(`Battery ${this.applianceId}: current state - outWRte=${controls?.outWRte}`);
|
|
866
|
+
console.log(`Battery ${this.applianceId}: current state - outWRte=${controls?.outWRte}, wChaMax=${controls?.wChaMax}`);
|
|
867
|
+
if (!controls?.wChaMax) {
|
|
868
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read wChaMax for discharge limit conversion');
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
// Convert watts to percentage of WDisChaMax (using wChaMax), clamped to 0-100%
|
|
872
|
+
const dischargeLimitPercent = Math.min(100, Math.max(0, (msg.data.dischargeLimitW / controls.wChaMax) * 100));
|
|
873
|
+
console.log(`Battery ${this.applianceId}: calculated discharge limit: ${dischargeLimitPercent.toFixed(1)}% (${msg.data.dischargeLimitW}W / ${controls.wChaMax}W)`);
|
|
718
874
|
// Set discharge limit (Register 12: outWRte)
|
|
719
|
-
const success = await this.writeBatteryControls({ outWRte:
|
|
875
|
+
const success = await this.writeBatteryControls({ outWRte: dischargeLimitPercent });
|
|
720
876
|
if (!success) {
|
|
721
877
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set discharge limit');
|
|
722
878
|
return;
|
|
723
879
|
}
|
|
724
880
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
725
881
|
}
|
|
726
|
-
sendCommandAcknowledge(messageId, acknowledgeMessage, answer, rejectionReason) {
|
|
727
|
-
if (!this.dataBus || !this.applianceId) {
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
const ackMessage = {
|
|
731
|
-
id: (0, node_crypto_1.randomUUID)(),
|
|
732
|
-
message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.CommandAcknowledgeV1,
|
|
733
|
-
type: 'answer',
|
|
734
|
-
source: enyo_source_enum_js_1.EnyoSourceEnum.Device,
|
|
735
|
-
applianceId: this.applianceId,
|
|
736
|
-
timestampIso: new Date().toISOString(),
|
|
737
|
-
data: {
|
|
738
|
-
messageId,
|
|
739
|
-
acknowledgeMessage,
|
|
740
|
-
answer,
|
|
741
|
-
rejectionReason
|
|
742
|
-
}
|
|
743
|
-
};
|
|
744
|
-
console.log(`Battery ${this.applianceId}: sending ${answer} for ${acknowledgeMessage} (messageId=${messageId}${rejectionReason ? `, reason=${rejectionReason}` : ''})`);
|
|
745
|
-
this.dataBus.sendMessage([ackMessage]);
|
|
746
|
-
}
|
|
747
882
|
}
|
|
748
883
|
exports.SunspecBattery = SunspecBattery;
|
|
749
884
|
/**
|
|
750
885
|
* Sunspec Meter implementation
|
|
751
886
|
*/
|
|
752
887
|
class SunspecMeter extends BaseSunspecDevice {
|
|
888
|
+
capabilities;
|
|
889
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig) {
|
|
890
|
+
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig);
|
|
891
|
+
this.capabilities = capabilities;
|
|
892
|
+
}
|
|
753
893
|
/**
|
|
754
894
|
* Connect to the meter and create/update the appliance
|
|
755
895
|
*/
|
|
@@ -806,7 +946,9 @@ class SunspecMeter extends BaseSunspecDevice {
|
|
|
806
946
|
*/
|
|
807
947
|
async readData(clockId, resolution) {
|
|
808
948
|
if (!this.isConnected()) {
|
|
809
|
-
|
|
949
|
+
await this.tryReconnect();
|
|
950
|
+
if (!this.isConnected())
|
|
951
|
+
return [];
|
|
810
952
|
}
|
|
811
953
|
const messages = [];
|
|
812
954
|
const timestamp = new Date();
|
|
@@ -837,6 +979,7 @@ class SunspecMeter extends BaseSunspecDevice {
|
|
|
837
979
|
}
|
|
838
980
|
catch (error) {
|
|
839
981
|
console.error(`Error updating meter data: ${error}`);
|
|
982
|
+
await this.markOffline();
|
|
840
983
|
}
|
|
841
984
|
return messages;
|
|
842
985
|
}
|