@enyo-energy/sunspec-sdk 0.0.51 → 0.0.52
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/sunspec-devices.cjs +45 -42
- package/dist/cjs/sunspec-modbus-client.cjs +374 -241
- package/dist/cjs/sunspec-modbus-client.d.cts +94 -52
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/sunspec-devices.js +45 -42
- package/dist/sunspec-modbus-client.d.ts +94 -52
- package/dist/sunspec-modbus-client.js +372 -241
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SunspecModbusClient = void 0;
|
|
4
|
+
exports.getOrCreateSunspecClient = getOrCreateSunspecClient;
|
|
5
|
+
exports.releaseSunspecClient = releaseSunspecClient;
|
|
4
6
|
/**
|
|
5
7
|
* Sunspec Modbus Client Implementation
|
|
6
8
|
*
|
|
@@ -23,21 +25,75 @@ const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
|
|
|
23
25
|
const EnergyAppModbusConnectionHealth_js_1 = require("@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusConnectionHealth.js");
|
|
24
26
|
const EnergyAppModbusFaultTolerantReader_js_1 = require("@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusFaultTolerantReader.js");
|
|
25
27
|
const EnergyAppModbusDataTypeConverter_js_1 = require("@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusDataTypeConverter.js");
|
|
28
|
+
/**
|
|
29
|
+
* Module-level registry: one SunspecModbusClient per network device (host:port).
|
|
30
|
+
* Refcounted so the underlying connection only tears down when the last consumer releases.
|
|
31
|
+
*/
|
|
32
|
+
const CLIENT_REGISTRY = new Map();
|
|
33
|
+
const registryKey = (host, port) => `${host}:${port}`;
|
|
34
|
+
/**
|
|
35
|
+
* Get (or create) the singleton SunspecModbusClient for this network device.
|
|
36
|
+
* Increments the refcount; pair with `releaseSunspecClient` on teardown.
|
|
37
|
+
*/
|
|
38
|
+
function getOrCreateSunspecClient(energyApp, host, port = 502) {
|
|
39
|
+
const key = registryKey(host, port);
|
|
40
|
+
let entry = CLIENT_REGISTRY.get(key);
|
|
41
|
+
if (!entry) {
|
|
42
|
+
entry = { client: new SunspecModbusClient(energyApp), refcount: 0 };
|
|
43
|
+
CLIENT_REGISTRY.set(key, entry);
|
|
44
|
+
}
|
|
45
|
+
entry.refcount++;
|
|
46
|
+
return entry.client;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Release a refcount on the network-device client. When the last consumer releases,
|
|
50
|
+
* the client is fully disconnected (all units closed) and removed from the registry.
|
|
51
|
+
*
|
|
52
|
+
* Releasing more times than acquired is a programming error and is logged; the count
|
|
53
|
+
* is clamped at zero so repeat releases are no-ops.
|
|
54
|
+
*/
|
|
55
|
+
async function releaseSunspecClient(host, port = 502) {
|
|
56
|
+
const key = registryKey(host, port);
|
|
57
|
+
const entry = CLIENT_REGISTRY.get(key);
|
|
58
|
+
if (!entry)
|
|
59
|
+
return;
|
|
60
|
+
if (entry.refcount <= 0) {
|
|
61
|
+
console.warn(`releaseSunspecClient(${host}:${port}): refcount already 0 — extra release ignored`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
entry.refcount--;
|
|
65
|
+
if (entry.refcount === 0) {
|
|
66
|
+
// Disconnect first, THEN remove from registry, so a concurrent getOrCreate
|
|
67
|
+
// doesn't bind a fresh client to the same host:port while sockets are still
|
|
68
|
+
// closing on the old one.
|
|
69
|
+
try {
|
|
70
|
+
await entry.client.disconnect();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
// Guard against the very-unlikely case where someone re-acquired during await.
|
|
74
|
+
if (CLIENT_REGISTRY.get(key) === entry && entry.refcount === 0) {
|
|
75
|
+
CLIENT_REGISTRY.delete(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
26
80
|
class SunspecModbusClient {
|
|
27
81
|
energyApp;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
82
|
+
// Per-unit Modbus instance/reader/discovered-models maps. One client = one network device,
|
|
83
|
+
// many unit IDs share that client (each unit ID is one EnergyAppModbusInstance underneath).
|
|
84
|
+
modbusInstances = new Map();
|
|
85
|
+
faultTolerantReaders = new Map();
|
|
86
|
+
discoveredModelsByUnit = new Map();
|
|
31
87
|
connectionHealth;
|
|
32
|
-
faultTolerantReader = null;
|
|
33
88
|
modbusDataTypeConverter;
|
|
34
89
|
connectionParams = null;
|
|
90
|
+
// Per-unit known parameters so reconnect() can replay every unit ID we've seen.
|
|
91
|
+
knownUnits = new Set();
|
|
35
92
|
autoReconnectEnabled = true;
|
|
36
93
|
openCount = 0;
|
|
37
94
|
closeCount = 0;
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
// TCP socket while the first is still alive (some Modbus devices allow only one client).
|
|
95
|
+
// Serializes connection-state transitions so concurrent callers cannot open duplicate
|
|
96
|
+
// sockets for the same unit ID while one is still alive.
|
|
41
97
|
operationChain = Promise.resolve();
|
|
42
98
|
constructor(energyApp) {
|
|
43
99
|
this.energyApp = energyApp;
|
|
@@ -45,7 +101,10 @@ class SunspecModbusClient {
|
|
|
45
101
|
this.modbusDataTypeConverter = new EnergyAppModbusDataTypeConverter_js_1.EnergyAppModbusDataTypeConverter();
|
|
46
102
|
}
|
|
47
103
|
/**
|
|
48
|
-
* Connect to Modbus device
|
|
104
|
+
* Connect to a Modbus device unit. Multiple unit IDs on the same network device share this
|
|
105
|
+
* client; calling connect() with different unit IDs adds each as a separately-managed unit.
|
|
106
|
+
* The first call locks in the host/port for this client; later calls for a different host
|
|
107
|
+
* are rejected.
|
|
49
108
|
* @param host Primary host (hostname) to connect to
|
|
50
109
|
* @param port Modbus port (default 502)
|
|
51
110
|
* @param unitId Modbus unit ID (default 1)
|
|
@@ -53,45 +112,64 @@ class SunspecModbusClient {
|
|
|
53
112
|
*/
|
|
54
113
|
async connect(host, port = 502, unitId = 1, secondaryHost) {
|
|
55
114
|
return this.withConnectionLock(async () => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
115
|
+
if (this.connectionParams) {
|
|
116
|
+
if (this.connectionParams.primaryHost !== host || this.connectionParams.port !== port) {
|
|
117
|
+
throw new Error(`SunspecModbusClient is bound to ${this.connectionParams.primaryHost}:${this.connectionParams.port}; ` +
|
|
118
|
+
`cannot reuse it for ${host}:${port}. Use getOrCreateSunspecClient() per (host, port).`);
|
|
119
|
+
}
|
|
120
|
+
if (secondaryHost && !this.connectionParams.secondaryHost) {
|
|
121
|
+
this.connectionParams.secondaryHost = secondaryHost;
|
|
122
|
+
}
|
|
64
123
|
}
|
|
65
|
-
|
|
66
|
-
|
|
124
|
+
else {
|
|
125
|
+
this.connectionParams = { primaryHost: host, secondaryHost, port };
|
|
67
126
|
}
|
|
68
|
-
this.
|
|
69
|
-
|
|
70
|
-
|
|
127
|
+
this.knownUnits.add(unitId);
|
|
128
|
+
if (this.modbusInstances.has(unitId)) {
|
|
129
|
+
console.debug(`connect(): unit ${unitId} on ${host}:${port} already open, skipping`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await this._openUnit(host, port, unitId);
|
|
133
|
+
console.log(`Connected to Sunspec device at ${host}:${port} unit ${unitId} (opens=${this.openCount}, closes=${this.closeCount}, openUnits=${this.modbusInstances.size})`);
|
|
71
134
|
});
|
|
72
135
|
}
|
|
73
136
|
/**
|
|
74
|
-
* Disconnect from
|
|
137
|
+
* Disconnect from all units of this network device.
|
|
75
138
|
*
|
|
76
|
-
* Note: connection parameters are preserved so reconnect() can
|
|
77
|
-
* They will be overwritten by the next connect() call anyway.
|
|
139
|
+
* Note: connection parameters and the set of known units are preserved so reconnect() can
|
|
140
|
+
* be called afterwards. They will be overwritten by the next connect() call anyway.
|
|
78
141
|
*/
|
|
79
142
|
async disconnect() {
|
|
80
143
|
return this.withConnectionLock(async () => {
|
|
81
|
-
if (
|
|
144
|
+
if (this.modbusInstances.size === 0) {
|
|
82
145
|
return;
|
|
83
146
|
}
|
|
84
147
|
const host = this.connectionParams?.primaryHost ?? 'unknown';
|
|
85
148
|
const port = this.connectionParams?.port ?? 0;
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
149
|
+
const units = [...this.modbusInstances.keys()];
|
|
150
|
+
for (const unitId of units) {
|
|
151
|
+
await this._closeUnit(unitId);
|
|
152
|
+
}
|
|
153
|
+
console.log(`Disconnected from Sunspec device at ${host}:${port} units [${units.join(',')}] (opens=${this.openCount}, closes=${this.closeCount})`);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Disconnect a single unit on this network device. Other units on the same device stay open.
|
|
158
|
+
*/
|
|
159
|
+
async disconnectUnit(unitId) {
|
|
160
|
+
return this.withConnectionLock(async () => {
|
|
161
|
+
if (!this.modbusInstances.has(unitId))
|
|
162
|
+
return;
|
|
163
|
+
const host = this.connectionParams?.primaryHost ?? 'unknown';
|
|
164
|
+
const port = this.connectionParams?.port ?? 0;
|
|
165
|
+
await this._closeUnit(unitId);
|
|
166
|
+
console.log(`Disconnected unit ${unitId} from Sunspec device at ${host}:${port} (opens=${this.openCount}, closes=${this.closeCount})`);
|
|
89
167
|
});
|
|
90
168
|
}
|
|
91
169
|
/**
|
|
92
|
-
* Reconnect using stored connection
|
|
93
|
-
* First tries primaryHost (hostname), then falls back to secondaryHost
|
|
94
|
-
* Returns true if
|
|
170
|
+
* Reconnect every previously-known unit on this network device using stored connection
|
|
171
|
+
* parameters. First tries primaryHost (hostname), then falls back to secondaryHost
|
|
172
|
+
* (ipAddress) if available. Returns true only if every known unit reconnected successfully.
|
|
95
173
|
*/
|
|
96
174
|
async reconnect() {
|
|
97
175
|
return this.withConnectionLock(async () => {
|
|
@@ -99,18 +177,23 @@ class SunspecModbusClient {
|
|
|
99
177
|
console.error('Cannot reconnect: no connection parameters stored. Call connect() first.');
|
|
100
178
|
return false;
|
|
101
179
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
180
|
+
if (this.knownUnits.size === 0) {
|
|
181
|
+
console.error('Cannot reconnect: no known unit IDs. Call connect(unitId) at least once first.');
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
const { primaryHost, secondaryHost, port } = this.connectionParams;
|
|
185
|
+
const units = [...this.knownUnits];
|
|
186
|
+
console.log(`Attempting to reconnect to primary host ${primaryHost}:${port} units [${units.join(',')}]...`);
|
|
187
|
+
const primarySuccess = await this.attemptConnection(primaryHost, port, units);
|
|
105
188
|
if (primarySuccess) {
|
|
106
|
-
console.log(`Successfully reconnected to primary host ${primaryHost}:${port}
|
|
189
|
+
console.log(`Successfully reconnected to primary host ${primaryHost}:${port} units [${units.join(',')}]`);
|
|
107
190
|
return true;
|
|
108
191
|
}
|
|
109
192
|
if (secondaryHost && secondaryHost !== primaryHost) {
|
|
110
|
-
console.log(`Primary host failed, attempting secondary host ${secondaryHost}:${port}
|
|
111
|
-
const secondarySuccess = await this.attemptConnection(secondaryHost, port,
|
|
193
|
+
console.log(`Primary host failed, attempting secondary host ${secondaryHost}:${port} units [${units.join(',')}]...`);
|
|
194
|
+
const secondarySuccess = await this.attemptConnection(secondaryHost, port, units);
|
|
112
195
|
if (secondarySuccess) {
|
|
113
|
-
console.log(`Successfully reconnected to secondary host ${secondaryHost}:${port}
|
|
196
|
+
console.log(`Successfully reconnected to secondary host ${secondaryHost}:${port} units [${units.join(',')}]`);
|
|
114
197
|
return true;
|
|
115
198
|
}
|
|
116
199
|
}
|
|
@@ -119,16 +202,53 @@ class SunspecModbusClient {
|
|
|
119
202
|
});
|
|
120
203
|
}
|
|
121
204
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
205
|
+
* Reconnect a single previously-known unit on this network device. Other units on the
|
|
206
|
+
* same client stay open. Useful when one device's polling loop detects a dropped
|
|
207
|
+
* connection and only needs to recover its own unit, not thrash siblings.
|
|
208
|
+
*
|
|
209
|
+
* Returns true if the unit reconnected successfully (on primary or secondary host).
|
|
210
|
+
*/
|
|
211
|
+
async reconnectUnit(unitId) {
|
|
212
|
+
return this.withConnectionLock(async () => {
|
|
213
|
+
if (!this.connectionParams) {
|
|
214
|
+
console.error(`Cannot reconnectUnit(${unitId}): no connection parameters stored. Call connect() first.`);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
const { primaryHost, secondaryHost, port } = this.connectionParams;
|
|
218
|
+
console.log(`Attempting to reconnect unit ${unitId} on primary host ${primaryHost}:${port}...`);
|
|
219
|
+
const primarySuccess = await this.attemptConnection(primaryHost, port, [unitId]);
|
|
220
|
+
if (primarySuccess) {
|
|
221
|
+
console.log(`Successfully reconnected unit ${unitId} on primary host ${primaryHost}:${port}`);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (secondaryHost && secondaryHost !== primaryHost) {
|
|
225
|
+
console.log(`Primary host failed for unit ${unitId}, attempting secondary host ${secondaryHost}:${port}...`);
|
|
226
|
+
const secondarySuccess = await this.attemptConnection(secondaryHost, port, [unitId]);
|
|
227
|
+
if (secondarySuccess) {
|
|
228
|
+
console.log(`Successfully reconnected unit ${unitId} on secondary host ${secondaryHost}:${port}`);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
console.error(`Reconnection failed for unit ${unitId} on all available hosts`);
|
|
233
|
+
return false;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Attempt to (re)open every requested unit on a specific host. Caller must hold the
|
|
238
|
+
* connection lock. Closes any pre-existing per-unit instances first. Returns true only
|
|
239
|
+
* if every unit succeeded.
|
|
124
240
|
*/
|
|
125
|
-
async attemptConnection(host, port,
|
|
126
|
-
|
|
127
|
-
|
|
241
|
+
async attemptConnection(host, port, units) {
|
|
242
|
+
for (const unitId of units) {
|
|
243
|
+
if (this.modbusInstances.has(unitId)) {
|
|
244
|
+
await this._closeUnit(unitId);
|
|
245
|
+
}
|
|
128
246
|
}
|
|
129
247
|
try {
|
|
130
|
-
|
|
131
|
-
|
|
248
|
+
for (const unitId of units) {
|
|
249
|
+
await this._openUnit(host, port, unitId);
|
|
250
|
+
}
|
|
251
|
+
console.log(`Connection attempt to ${host}:${port} units [${units.join(',')}] succeeded (opens=${this.openCount}, closes=${this.closeCount}, openUnits=${this.modbusInstances.size})`);
|
|
132
252
|
return true;
|
|
133
253
|
}
|
|
134
254
|
catch (error) {
|
|
@@ -137,11 +257,11 @@ class SunspecModbusClient {
|
|
|
137
257
|
}
|
|
138
258
|
}
|
|
139
259
|
/**
|
|
140
|
-
* Open a new Modbus
|
|
141
|
-
* just-opened
|
|
142
|
-
* lock and ensure
|
|
260
|
+
* Open a new Modbus instance for one unit ID and wire up its fault-tolerant reader.
|
|
261
|
+
* On any failure, the just-opened instance (if any) is closed so we never leak. Caller must
|
|
262
|
+
* hold the connection lock and ensure the unit is not already open.
|
|
143
263
|
*/
|
|
144
|
-
async
|
|
264
|
+
async _openUnit(host, port, unitId) {
|
|
145
265
|
let candidate = null;
|
|
146
266
|
try {
|
|
147
267
|
candidate = await this.energyApp.useModbus().connect({
|
|
@@ -153,9 +273,9 @@ class SunspecModbusClient {
|
|
|
153
273
|
if (!candidate) {
|
|
154
274
|
throw new Error(`useModbus().connect returned null for ${host}:${port} unit ${unitId}`);
|
|
155
275
|
}
|
|
156
|
-
|
|
157
|
-
this.
|
|
158
|
-
this.
|
|
276
|
+
const reader = new EnergyAppModbusFaultTolerantReader_js_1.EnergyAppModbusFaultTolerantReader(candidate, this.connectionHealth);
|
|
277
|
+
this.modbusInstances.set(unitId, candidate);
|
|
278
|
+
this.faultTolerantReaders.set(unitId, reader);
|
|
159
279
|
this.connectionHealth.recordSuccess();
|
|
160
280
|
this.recordOpen();
|
|
161
281
|
}
|
|
@@ -166,31 +286,28 @@ class SunspecModbusClient {
|
|
|
166
286
|
}
|
|
167
287
|
catch { /* ignore */ }
|
|
168
288
|
}
|
|
169
|
-
this.
|
|
170
|
-
this.
|
|
171
|
-
this.connected = false;
|
|
289
|
+
this.modbusInstances.delete(unitId);
|
|
290
|
+
this.faultTolerantReaders.delete(unitId);
|
|
172
291
|
throw err;
|
|
173
292
|
}
|
|
174
293
|
}
|
|
175
294
|
/**
|
|
176
|
-
* Close the
|
|
295
|
+
* Close the Modbus instance for a single unit. Idempotent. Caller must hold the
|
|
296
|
+
* connection lock.
|
|
177
297
|
*/
|
|
178
|
-
async
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
this.
|
|
184
|
-
|
|
185
|
-
this.connected = false;
|
|
186
|
-
this.discoveredModels.clear();
|
|
187
|
-
if (client) {
|
|
298
|
+
async _closeUnit(unitId) {
|
|
299
|
+
const instance = this.modbusInstances.get(unitId);
|
|
300
|
+
const wasOpen = instance !== undefined;
|
|
301
|
+
this.modbusInstances.delete(unitId);
|
|
302
|
+
this.faultTolerantReaders.delete(unitId);
|
|
303
|
+
this.discoveredModelsByUnit.delete(unitId);
|
|
304
|
+
if (instance) {
|
|
188
305
|
try {
|
|
189
|
-
await
|
|
306
|
+
await instance.disconnect();
|
|
190
307
|
}
|
|
191
308
|
catch { /* ignore */ }
|
|
192
309
|
}
|
|
193
|
-
if (
|
|
310
|
+
if (wasOpen)
|
|
194
311
|
this.recordClose();
|
|
195
312
|
}
|
|
196
313
|
/**
|
|
@@ -204,27 +321,55 @@ class SunspecModbusClient {
|
|
|
204
321
|
}
|
|
205
322
|
recordOpen() {
|
|
206
323
|
this.openCount++;
|
|
207
|
-
this.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
324
|
+
const expected = this.modbusInstances.size;
|
|
325
|
+
const delta = this.openCount - this.closeCount;
|
|
326
|
+
if (delta > expected) {
|
|
327
|
+
console.error(`SunspecModbusClient: open/close imbalance — ` +
|
|
328
|
+
`opens=${this.openCount}, closes=${this.closeCount}, instances=${expected}. ` +
|
|
211
329
|
`This indicates a code path bypassing the connection lock — please investigate.`);
|
|
212
330
|
}
|
|
213
331
|
}
|
|
214
332
|
recordClose() {
|
|
215
333
|
this.closeCount++;
|
|
216
|
-
if (this.
|
|
217
|
-
this.
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
console.warn(`SunspecModbusClient: closeCount incremented while currentlyOpen was 0 — possible double-close`);
|
|
334
|
+
if (this.closeCount > this.openCount) {
|
|
335
|
+
console.warn(`SunspecModbusClient: closeCount=${this.closeCount} exceeded openCount=${this.openCount} — possible double-close`);
|
|
221
336
|
}
|
|
222
337
|
}
|
|
223
338
|
/**
|
|
224
|
-
* Get cumulative open/close counts for this client. Useful for
|
|
339
|
+
* Get cumulative open/close counts for this client (across all unit IDs). Useful for
|
|
340
|
+
* spotting connection leaks.
|
|
225
341
|
*/
|
|
226
342
|
getConnectionStats() {
|
|
227
|
-
return { opens: this.openCount, closes: this.closeCount,
|
|
343
|
+
return { opens: this.openCount, closes: this.closeCount, openUnits: this.modbusInstances.size };
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Get the EnergyAppModbusInstance for a unit, throwing if it isn't open.
|
|
347
|
+
*/
|
|
348
|
+
getInstance(unitId) {
|
|
349
|
+
const inst = this.modbusInstances.get(unitId);
|
|
350
|
+
if (!inst)
|
|
351
|
+
throw new Error(`Modbus instance for unit ${unitId} not initialized — call connect(host, port, ${unitId}) first`);
|
|
352
|
+
return inst;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Get the fault-tolerant reader for a unit, throwing if it isn't open.
|
|
356
|
+
*/
|
|
357
|
+
getReader(unitId) {
|
|
358
|
+
const reader = this.faultTolerantReaders.get(unitId);
|
|
359
|
+
if (!reader)
|
|
360
|
+
throw new Error(`Fault-tolerant reader for unit ${unitId} not initialized — call connect(host, port, ${unitId}) first`);
|
|
361
|
+
return reader;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Get (or create) the discovered-models map for a unit.
|
|
365
|
+
*/
|
|
366
|
+
getModelsMap(unitId) {
|
|
367
|
+
let m = this.discoveredModelsByUnit.get(unitId);
|
|
368
|
+
if (!m) {
|
|
369
|
+
m = new Map();
|
|
370
|
+
this.discoveredModelsByUnit.set(unitId, m);
|
|
371
|
+
}
|
|
372
|
+
return m;
|
|
228
373
|
}
|
|
229
374
|
/**
|
|
230
375
|
* Enable or disable automatic reconnection
|
|
@@ -242,16 +387,14 @@ class SunspecModbusClient {
|
|
|
242
387
|
/**
|
|
243
388
|
* Detect the base address and addressing mode (0-based or 1-based) for SunSpec
|
|
244
389
|
*/
|
|
245
|
-
async detectSunspecBaseAddress(customBaseAddress) {
|
|
246
|
-
|
|
247
|
-
throw new Error('Modbus client not initialized');
|
|
248
|
-
}
|
|
390
|
+
async detectSunspecBaseAddress(unitId, customBaseAddress) {
|
|
391
|
+
const instance = this.getInstance(unitId);
|
|
249
392
|
// If custom base address provided, try it first (1-based, then 0-based variant)
|
|
250
393
|
if (customBaseAddress !== undefined) {
|
|
251
394
|
console.log(`Detect models for custom base address '${customBaseAddress}' ...`);
|
|
252
395
|
// Try 0-based at custom address
|
|
253
396
|
try {
|
|
254
|
-
const sunspecId = await
|
|
397
|
+
const sunspecId = await instance.readRegisterStringValue(customBaseAddress, 2);
|
|
255
398
|
if (sunspecId.includes('SunS')) {
|
|
256
399
|
console.log(`Detected 0-based addressing mode (base address: ${customBaseAddress})`);
|
|
257
400
|
return {
|
|
@@ -266,7 +409,7 @@ class SunspecModbusClient {
|
|
|
266
409
|
}
|
|
267
410
|
// Try 1-based at custom address (customBaseAddress + 1)
|
|
268
411
|
try {
|
|
269
|
-
const sunspecId = await
|
|
412
|
+
const sunspecId = await instance.readRegisterStringValue(customBaseAddress + 1, 2);
|
|
270
413
|
if (sunspecId.includes('SunS')) {
|
|
271
414
|
console.log(`Detected 1-based addressing mode (base address: ${customBaseAddress + 1})`);
|
|
272
415
|
return {
|
|
@@ -283,7 +426,7 @@ class SunspecModbusClient {
|
|
|
283
426
|
else {
|
|
284
427
|
// Try 1-based addressing first (most common)
|
|
285
428
|
try {
|
|
286
|
-
const sunspecId = await
|
|
429
|
+
const sunspecId = await instance.readRegisterStringValue(40001, 2);
|
|
287
430
|
if (sunspecId.includes('SunS')) {
|
|
288
431
|
console.log('Detected 1-based addressing mode (base address: 40001)');
|
|
289
432
|
return {
|
|
@@ -298,7 +441,7 @@ class SunspecModbusClient {
|
|
|
298
441
|
}
|
|
299
442
|
// Try 0-based addressing
|
|
300
443
|
try {
|
|
301
|
-
const sunspecId = await
|
|
444
|
+
const sunspecId = await instance.readRegisterStringValue(40000, 2);
|
|
302
445
|
if (sunspecId.includes('SunS')) {
|
|
303
446
|
console.log('Detected 0-based addressing mode (base address: 40000)');
|
|
304
447
|
return {
|
|
@@ -318,27 +461,22 @@ class SunspecModbusClient {
|
|
|
318
461
|
throw new Error(`Device is not SunSpec compliant - "SunS" identifier not found at addresses ${addressesChecked}`);
|
|
319
462
|
}
|
|
320
463
|
/**
|
|
321
|
-
* Discover all available Sunspec models
|
|
464
|
+
* Discover all available Sunspec models for a unit
|
|
322
465
|
* Automatically detects base address (40000 or 40001) and scans from there
|
|
323
466
|
*/
|
|
324
|
-
async discoverModels(customBaseAddress) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
this.discoveredModels.clear();
|
|
467
|
+
async discoverModels(unitId, customBaseAddress) {
|
|
468
|
+
const instance = this.getInstance(unitId);
|
|
469
|
+
const models = this.getModelsMap(unitId);
|
|
470
|
+
models.clear();
|
|
329
471
|
const maxAddress = 50000; // Safety limit
|
|
330
472
|
let currentAddress = 0;
|
|
331
|
-
console.log(
|
|
473
|
+
console.log(`Starting Sunspec model discovery for unit ${unitId}...`);
|
|
332
474
|
try {
|
|
333
475
|
// Detect the base address and addressing mode
|
|
334
|
-
const addressInfo = await this.detectSunspecBaseAddress(customBaseAddress);
|
|
476
|
+
const addressInfo = await this.detectSunspecBaseAddress(unitId, customBaseAddress);
|
|
335
477
|
currentAddress = addressInfo.nextAddress;
|
|
336
478
|
while (currentAddress < maxAddress) {
|
|
337
|
-
|
|
338
|
-
if (!this.modbusClient) {
|
|
339
|
-
throw new Error('Modbus client not initialized');
|
|
340
|
-
}
|
|
341
|
-
const buffer = await this.modbusClient.readHoldingRegisters(currentAddress, 2);
|
|
479
|
+
const buffer = await instance.readHoldingRegisters(currentAddress, 2);
|
|
342
480
|
const modelData = [buffer.readUInt16BE(0), buffer.readUInt16BE(2)];
|
|
343
481
|
if (!modelData || modelData.length < 2) {
|
|
344
482
|
console.log(`No data at address ${currentAddress}, ending discovery`);
|
|
@@ -357,23 +495,23 @@ class SunspecModbusClient {
|
|
|
357
495
|
address: currentAddress,
|
|
358
496
|
length: modelLength
|
|
359
497
|
};
|
|
360
|
-
|
|
361
|
-
console.log(`Discovered Model ${modelId} at address ${currentAddress} with length ${modelLength}`);
|
|
498
|
+
models.set(modelId, model);
|
|
499
|
+
console.log(`Discovered Model ${modelId} at address ${currentAddress} with length ${modelLength} (unit ${unitId})`);
|
|
362
500
|
// Jump to next model: current address + 2 (header) + model length
|
|
363
501
|
currentAddress = currentAddress + 2 + modelLength;
|
|
364
502
|
}
|
|
365
503
|
}
|
|
366
504
|
catch (error) {
|
|
367
|
-
console.error(`Error during model discovery at address ${currentAddress}: ${error}`);
|
|
505
|
+
console.error(`Error during model discovery at address ${currentAddress} (unit ${unitId}): ${error}`);
|
|
368
506
|
}
|
|
369
|
-
console.log(`Discovery complete. Found ${
|
|
370
|
-
return
|
|
507
|
+
console.log(`Discovery complete for unit ${unitId}. Found ${models.size} models`);
|
|
508
|
+
return models;
|
|
371
509
|
}
|
|
372
510
|
/**
|
|
373
|
-
* Find a specific model by ID
|
|
511
|
+
* Find a specific model by ID for a given unit
|
|
374
512
|
*/
|
|
375
|
-
findModel(modelId) {
|
|
376
|
-
return this.
|
|
513
|
+
findModel(unitId, modelId) {
|
|
514
|
+
return this.discoveredModelsByUnit.get(unitId)?.get(modelId);
|
|
377
515
|
}
|
|
378
516
|
/**
|
|
379
517
|
* Check if a value is "unimplemented" according to Sunspec specification
|
|
@@ -449,17 +587,15 @@ class SunspecModbusClient {
|
|
|
449
587
|
* Read an entire model's register block in a single Modbus call.
|
|
450
588
|
* Returns a Buffer containing all registers for the model.
|
|
451
589
|
*/
|
|
452
|
-
async readModelBlock(model) {
|
|
453
|
-
|
|
454
|
-
throw new Error('Fault-tolerant reader not initialized');
|
|
455
|
-
}
|
|
590
|
+
async readModelBlock(unitId, model) {
|
|
591
|
+
const reader = this.getReader(unitId);
|
|
456
592
|
// Read model.length + 2 registers: the 2-register header (ID + length) plus all data registers.
|
|
457
593
|
// This way buffer offsets match the convention used throughout: offset 0 = model ID,
|
|
458
594
|
// offset 1 = model length, offset 2 = first data register, etc.
|
|
459
595
|
const totalRegisters = model.length + 2;
|
|
460
|
-
const result = await
|
|
596
|
+
const result = await reader.readHoldingRegisters(model.address, totalRegisters);
|
|
461
597
|
if (!result.success || !result.value) {
|
|
462
|
-
throw new Error(`Failed to read model block ${model.id} at address ${model.address}: ${result.error?.message || 'Unknown error'}`);
|
|
598
|
+
throw new Error(`Failed to read model block ${model.id} at address ${model.address} (unit ${unitId}): ${result.error?.message || 'Unknown error'}`);
|
|
463
599
|
}
|
|
464
600
|
this.connectionHealth.recordSuccess();
|
|
465
601
|
return result.value;
|
|
@@ -484,12 +620,10 @@ class SunspecModbusClient {
|
|
|
484
620
|
/**
|
|
485
621
|
* Helper to read register value(s) using the fault-tolerant reader with data type conversion
|
|
486
622
|
*/
|
|
487
|
-
async readRegisterValue(address, quantity = 1, dataType) {
|
|
488
|
-
|
|
489
|
-
throw new Error('Fault-tolerant reader not initialized');
|
|
490
|
-
}
|
|
623
|
+
async readRegisterValue(unitId, address, quantity = 1, dataType) {
|
|
624
|
+
const reader = this.getReader(unitId);
|
|
491
625
|
try {
|
|
492
|
-
const result = await
|
|
626
|
+
const result = await reader.readHoldingRegisters(address, quantity);
|
|
493
627
|
// Check if the read was successful
|
|
494
628
|
if (!result.success || !result.value) {
|
|
495
629
|
throw new Error(`Failed to read register at address ${address}: ${result.error?.message || 'Unknown error'}`);
|
|
@@ -510,10 +644,8 @@ class SunspecModbusClient {
|
|
|
510
644
|
/**
|
|
511
645
|
* Helper to write register value(s)
|
|
512
646
|
*/
|
|
513
|
-
async writeRegisterValue(address, value, dataType = 'uint16') {
|
|
514
|
-
|
|
515
|
-
throw new Error('Modbus client not initialized');
|
|
516
|
-
}
|
|
647
|
+
async writeRegisterValue(unitId, address, value, dataType = 'uint16') {
|
|
648
|
+
const instance = this.getInstance(unitId);
|
|
517
649
|
try {
|
|
518
650
|
// Convert value to array of register values
|
|
519
651
|
let registerValues;
|
|
@@ -542,9 +674,9 @@ class SunspecModbusClient {
|
|
|
542
674
|
}
|
|
543
675
|
}
|
|
544
676
|
// Write to holding registers
|
|
545
|
-
await
|
|
677
|
+
await instance.writeMultipleRegisters(address, registerValues);
|
|
546
678
|
this.connectionHealth.recordSuccess();
|
|
547
|
-
console.log(`Successfully wrote value ${value} to register ${address}`);
|
|
679
|
+
console.log(`Successfully wrote value ${value} to register ${address} (unit ${unitId})`);
|
|
548
680
|
return true;
|
|
549
681
|
}
|
|
550
682
|
catch (error) {
|
|
@@ -556,23 +688,23 @@ class SunspecModbusClient {
|
|
|
556
688
|
/**
|
|
557
689
|
* Read inverter data from Model 101 (Single Phase) / Model 103 (Three Phase)
|
|
558
690
|
*/
|
|
559
|
-
async readInverterData() {
|
|
560
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Inverter3Phase);
|
|
691
|
+
async readInverterData(unitId) {
|
|
692
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Inverter3Phase);
|
|
561
693
|
if (!model) {
|
|
562
|
-
console.debug(
|
|
563
|
-
const singlePhaseModel = this.findModel(sunspec_interfaces_js_1.SunspecModelId.InverterSinglePhase);
|
|
694
|
+
console.debug(`Inverter model 103 not found on unit ${unitId}, trying single phase model 101`);
|
|
695
|
+
const singlePhaseModel = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.InverterSinglePhase);
|
|
564
696
|
if (!singlePhaseModel) {
|
|
565
|
-
console.error(
|
|
697
|
+
console.error(`No inverter model found on unit ${unitId}`);
|
|
566
698
|
return null;
|
|
567
699
|
}
|
|
568
700
|
console.warn('IMPORTANT: Working with single-phase inverter, but 3-phase is expected!');
|
|
569
|
-
return this.readSinglePhaseInverterData(singlePhaseModel);
|
|
701
|
+
return this.readSinglePhaseInverterData(unitId, singlePhaseModel);
|
|
570
702
|
}
|
|
571
|
-
console.debug(`Found 3-phase inverter model 103 at address ${model.address} with length ${model.length}`);
|
|
703
|
+
console.debug(`Found 3-phase inverter model 103 at address ${model.address} with length ${model.length} (unit ${unitId})`);
|
|
572
704
|
try {
|
|
573
705
|
// Read entire model block in a single Modbus call
|
|
574
|
-
console.debug(`Reading Inverter Data from Model ${model.id} at base address: ${model.address}`);
|
|
575
|
-
const buffer = await this.readModelBlock(model);
|
|
706
|
+
console.debug(`Reading Inverter Data from Model ${model.id} at base address: ${model.address} (unit ${unitId})`);
|
|
707
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
576
708
|
// Extract all scale factors from buffer
|
|
577
709
|
const scaleFactors = this.extractInverterScaleFactors(buffer);
|
|
578
710
|
// Extract raw values from buffer
|
|
@@ -653,11 +785,11 @@ class SunspecModbusClient {
|
|
|
653
785
|
/**
|
|
654
786
|
* Read single phase inverter data (Model 101)
|
|
655
787
|
*/
|
|
656
|
-
async readSinglePhaseInverterData(model) {
|
|
788
|
+
async readSinglePhaseInverterData(unitId, model) {
|
|
657
789
|
try {
|
|
658
|
-
console.debug(`Reading Single-Phase Inverter Data from Model 101 at base address: ${model.address}`);
|
|
790
|
+
console.debug(`Reading Single-Phase Inverter Data from Model 101 at base address: ${model.address} (unit ${unitId})`);
|
|
659
791
|
// Read entire model block in a single Modbus call
|
|
660
|
-
const buffer = await this.readModelBlock(model);
|
|
792
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
661
793
|
// Extract scale factors from buffer
|
|
662
794
|
const scaleFactors = {
|
|
663
795
|
A_SF: this.extractValue(buffer, 6, 'int16'),
|
|
@@ -806,18 +938,18 @@ class SunspecModbusClient {
|
|
|
806
938
|
this.logRegisterRead(160, 5, 'DCWH_SF', scaleFactors.DCWH_SF, 'int16');
|
|
807
939
|
return scaleFactors;
|
|
808
940
|
}
|
|
809
|
-
async readMPPTScaleFactors() {
|
|
810
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MPPT);
|
|
941
|
+
async readMPPTScaleFactors(unitId) {
|
|
942
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MPPT);
|
|
811
943
|
if (!model) {
|
|
812
|
-
console.debug(
|
|
944
|
+
console.debug(`MPPT model 160 not found on unit ${unitId}`);
|
|
813
945
|
return null;
|
|
814
946
|
}
|
|
815
947
|
try {
|
|
816
|
-
const buffer = await this.readModelBlock(model);
|
|
948
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
817
949
|
return this.extractMPPTScaleFactors(buffer);
|
|
818
950
|
}
|
|
819
951
|
catch (error) {
|
|
820
|
-
console.error(`Error reading MPPT scale factors: ${error}`);
|
|
952
|
+
console.error(`Error reading MPPT scale factors (unit ${unitId}): ${error}`);
|
|
821
953
|
return null;
|
|
822
954
|
}
|
|
823
955
|
}
|
|
@@ -878,36 +1010,36 @@ class SunspecModbusClient {
|
|
|
878
1010
|
/**
|
|
879
1011
|
* Read MPPT data from Model 160
|
|
880
1012
|
*/
|
|
881
|
-
async readMPPTData(moduleId = 1) {
|
|
882
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MPPT);
|
|
1013
|
+
async readMPPTData(unitId, moduleId = 1) {
|
|
1014
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MPPT);
|
|
883
1015
|
if (!model) {
|
|
884
|
-
console.debug(
|
|
1016
|
+
console.debug(`MPPT model 160 not found on unit ${unitId}`);
|
|
885
1017
|
return null;
|
|
886
1018
|
}
|
|
887
1019
|
try {
|
|
888
1020
|
// Read entire model block in a single Modbus call
|
|
889
|
-
const buffer = await this.readModelBlock(model);
|
|
1021
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
890
1022
|
const scaleFactors = this.extractMPPTScaleFactors(buffer);
|
|
891
1023
|
return this.extractMPPTModuleData(buffer, model, moduleId, scaleFactors);
|
|
892
1024
|
}
|
|
893
1025
|
catch (error) {
|
|
894
|
-
console.error(`Error reading MPPT data for module ${moduleId}: ${error}`);
|
|
1026
|
+
console.error(`Error reading MPPT data for module ${moduleId} (unit ${unitId}): ${error}`);
|
|
895
1027
|
return null;
|
|
896
1028
|
}
|
|
897
1029
|
}
|
|
898
1030
|
/**
|
|
899
1031
|
* Read all MPPT strings from Model 160 (Multiple MPPT)
|
|
900
1032
|
*/
|
|
901
|
-
async readAllMPPTData() {
|
|
1033
|
+
async readAllMPPTData(unitId) {
|
|
902
1034
|
const mpptData = [];
|
|
903
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MPPT);
|
|
1035
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MPPT);
|
|
904
1036
|
if (!model) {
|
|
905
|
-
console.debug(
|
|
1037
|
+
console.debug(`MPPT model 160 not found on unit ${unitId}`);
|
|
906
1038
|
return [];
|
|
907
1039
|
}
|
|
908
1040
|
try {
|
|
909
1041
|
// Read entire model block in a single Modbus call
|
|
910
|
-
const buffer = await this.readModelBlock(model);
|
|
1042
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
911
1043
|
const scaleFactors = this.extractMPPTScaleFactors(buffer);
|
|
912
1044
|
// Read the module count from register 8
|
|
913
1045
|
let moduleCount = 4; // Default fallback value
|
|
@@ -1068,16 +1200,16 @@ class SunspecModbusClient {
|
|
|
1068
1200
|
/**
|
|
1069
1201
|
* Read battery base data from Model 802 (Battery Base)
|
|
1070
1202
|
*/
|
|
1071
|
-
async readBatteryBaseData() {
|
|
1072
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.BatteryBase);
|
|
1203
|
+
async readBatteryBaseData(unitId) {
|
|
1204
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.BatteryBase);
|
|
1073
1205
|
if (!model) {
|
|
1074
|
-
console.debug(
|
|
1206
|
+
console.debug(`Battery Base model 802 not found on unit ${unitId}`);
|
|
1075
1207
|
return null;
|
|
1076
1208
|
}
|
|
1077
|
-
console.debug(`Reading Battery Base Data from Model 802 at base address: ${model.address}`);
|
|
1209
|
+
console.debug(`Reading Battery Base Data from Model 802 at base address: ${model.address} (unit ${unitId})`);
|
|
1078
1210
|
try {
|
|
1079
1211
|
// Read entire model block in a single Modbus call
|
|
1080
|
-
const buffer = await this.readModelBlock(model);
|
|
1212
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1081
1213
|
// Extract scale factors from buffer (offsets 52-63)
|
|
1082
1214
|
const sf = this.extractBatteryBaseScaleFactors(buffer);
|
|
1083
1215
|
// Extract raw values from buffer
|
|
@@ -1206,27 +1338,27 @@ class SunspecModbusClient {
|
|
|
1206
1338
|
/**
|
|
1207
1339
|
* Read battery data from Model 124 (Basic Storage) with fallback to Model 802 / Model 803
|
|
1208
1340
|
*/
|
|
1209
|
-
async readBatteryData() {
|
|
1341
|
+
async readBatteryData(unitId) {
|
|
1210
1342
|
// Try Model 124 first (Basic Storage Controls)
|
|
1211
|
-
let model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Battery);
|
|
1343
|
+
let model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Battery);
|
|
1212
1344
|
// Fall back to other battery models if needed
|
|
1213
1345
|
if (!model) {
|
|
1214
|
-
model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.BatteryBase);
|
|
1346
|
+
model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.BatteryBase);
|
|
1215
1347
|
}
|
|
1216
1348
|
if (!model) {
|
|
1217
|
-
model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.BatteryControl);
|
|
1349
|
+
model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.BatteryControl);
|
|
1218
1350
|
}
|
|
1219
1351
|
if (!model) {
|
|
1220
|
-
console.debug(
|
|
1352
|
+
console.debug(`No battery model found on unit ${unitId}`);
|
|
1221
1353
|
return null;
|
|
1222
1354
|
}
|
|
1223
|
-
console.debug(`Reading Battery Data from Model ${model.id} at base address: ${model.address}`);
|
|
1355
|
+
console.debug(`Reading Battery Data from Model ${model.id} at base address: ${model.address} (unit ${unitId})`);
|
|
1224
1356
|
try {
|
|
1225
1357
|
if (model.id === 124) {
|
|
1226
1358
|
// Model 124: Basic Storage Controls
|
|
1227
1359
|
console.debug('Using Model 124 (Basic Storage Controls)');
|
|
1228
1360
|
// Read entire model block in a single Modbus call
|
|
1229
|
-
const buffer = await this.readModelBlock(model);
|
|
1361
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1230
1362
|
// Extract scale factors from buffer (offsets 18-25)
|
|
1231
1363
|
const scaleFactors = {
|
|
1232
1364
|
WChaMax_SF: this.extractValue(buffer, 18, 'int16'),
|
|
@@ -1327,7 +1459,7 @@ class SunspecModbusClient {
|
|
|
1327
1459
|
else if (model.id === 802) {
|
|
1328
1460
|
// Model 802: Battery Base
|
|
1329
1461
|
console.debug('Using Model 802 (Battery Base)');
|
|
1330
|
-
const baseData = await this.readBatteryBaseData();
|
|
1462
|
+
const baseData = await this.readBatteryBaseData(unitId);
|
|
1331
1463
|
if (!baseData) {
|
|
1332
1464
|
return null;
|
|
1333
1465
|
}
|
|
@@ -1379,55 +1511,55 @@ class SunspecModbusClient {
|
|
|
1379
1511
|
/**
|
|
1380
1512
|
* Write battery control settings to Model 124
|
|
1381
1513
|
*/
|
|
1382
|
-
async writeBatteryControls(controls) {
|
|
1383
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Battery);
|
|
1514
|
+
async writeBatteryControls(unitId, controls) {
|
|
1515
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Battery);
|
|
1384
1516
|
if (!model) {
|
|
1385
|
-
console.error(
|
|
1517
|
+
console.error(`Battery model 124 not found on unit ${unitId}`);
|
|
1386
1518
|
return false;
|
|
1387
1519
|
}
|
|
1388
1520
|
const baseAddr = model.address;
|
|
1389
|
-
console.log(`Writing Battery Controls to Model 124 at base address: ${baseAddr}`);
|
|
1521
|
+
console.log(`Writing Battery Controls to Model 124 at base address: ${baseAddr} (unit ${unitId})`);
|
|
1390
1522
|
try {
|
|
1391
1523
|
// Write storage control mode (Register 5)
|
|
1392
1524
|
if (controls.storCtlMod !== undefined) {
|
|
1393
|
-
await this.writeRegisterValue(baseAddr + 5, controls.storCtlMod, 'uint16');
|
|
1525
|
+
await this.writeRegisterValue(unitId, baseAddr + 5, controls.storCtlMod, 'uint16');
|
|
1394
1526
|
console.log(`Set storage control mode to 0x${controls.storCtlMod.toString(16)}`);
|
|
1395
1527
|
}
|
|
1396
1528
|
// Write charge source setting (Register 17)
|
|
1397
1529
|
if (controls.chaGriSet !== undefined) {
|
|
1398
|
-
await this.writeRegisterValue(baseAddr + 17, controls.chaGriSet, 'uint16');
|
|
1530
|
+
await this.writeRegisterValue(unitId, baseAddr + 17, controls.chaGriSet, 'uint16');
|
|
1399
1531
|
console.log(`Set charge source to ${controls.chaGriSet === sunspec_interfaces_js_1.SunspecChargeSource.GRID ? 'GRID' : 'PV'}`);
|
|
1400
1532
|
}
|
|
1401
1533
|
// Write maximum charge power (Register 2) - needs scale factor
|
|
1402
1534
|
if (controls.wChaMax !== undefined) {
|
|
1403
1535
|
const scaleFactorAddr = baseAddr + 18;
|
|
1404
|
-
const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
|
|
1536
|
+
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1405
1537
|
const scaledValue = Math.round(controls.wChaMax / Math.pow(10, scaleFactor));
|
|
1406
|
-
await this.writeRegisterValue(baseAddr + 2, scaledValue, 'uint16');
|
|
1538
|
+
await this.writeRegisterValue(unitId, baseAddr + 2, scaledValue, 'uint16');
|
|
1407
1539
|
console.log(`Set max charge power to ${controls.wChaMax}W (scaled: ${scaledValue})`);
|
|
1408
1540
|
}
|
|
1409
1541
|
// Write charge rate (Register 13) - needs scale factor
|
|
1410
1542
|
if (controls.inWRte !== undefined) {
|
|
1411
1543
|
const scaleFactorAddr = baseAddr + 25;
|
|
1412
|
-
const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
|
|
1544
|
+
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1413
1545
|
const scaledValue = Math.round(controls.inWRte / Math.pow(10, scaleFactor));
|
|
1414
|
-
await this.writeRegisterValue(baseAddr + 13, scaledValue, 'int16');
|
|
1546
|
+
await this.writeRegisterValue(unitId, baseAddr + 13, scaledValue, 'int16');
|
|
1415
1547
|
console.log(`Set charge rate to ${controls.inWRte}% (scaled: ${scaledValue})`);
|
|
1416
1548
|
}
|
|
1417
1549
|
// Write discharge rate (Register 12) - needs scale factor
|
|
1418
1550
|
if (controls.outWRte !== undefined) {
|
|
1419
1551
|
const scaleFactorAddr = baseAddr + 25;
|
|
1420
|
-
const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
|
|
1552
|
+
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1421
1553
|
const scaledValue = Math.round(controls.outWRte / Math.pow(10, scaleFactor));
|
|
1422
|
-
await this.writeRegisterValue(baseAddr + 12, scaledValue, 'int16');
|
|
1554
|
+
await this.writeRegisterValue(unitId, baseAddr + 12, scaledValue, 'int16');
|
|
1423
1555
|
console.log(`Set discharge rate to ${controls.outWRte}% (scaled: ${scaledValue})`);
|
|
1424
1556
|
}
|
|
1425
1557
|
// Write minimum reserve percentage (Register 7) - needs scale factor
|
|
1426
1558
|
if (controls.minRsvPct !== undefined) {
|
|
1427
1559
|
const scaleFactorAddr = baseAddr + 21;
|
|
1428
|
-
const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
|
|
1560
|
+
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1429
1561
|
const scaledValue = Math.round(controls.minRsvPct / Math.pow(10, scaleFactor));
|
|
1430
|
-
await this.writeRegisterValue(baseAddr + 7, scaledValue, 'uint16');
|
|
1562
|
+
await this.writeRegisterValue(unitId, baseAddr + 7, scaledValue, 'uint16');
|
|
1431
1563
|
console.log(`Set minimum reserve to ${controls.minRsvPct}% (scaled: ${scaledValue})`);
|
|
1432
1564
|
}
|
|
1433
1565
|
console.log('Battery controls written successfully');
|
|
@@ -1441,7 +1573,7 @@ class SunspecModbusClient {
|
|
|
1441
1573
|
/**
|
|
1442
1574
|
* Set battery storage mode (simplified interface)
|
|
1443
1575
|
*/
|
|
1444
|
-
async setStorageMode(mode) {
|
|
1576
|
+
async setStorageMode(unitId, mode) {
|
|
1445
1577
|
let storCtlMod;
|
|
1446
1578
|
switch (mode) {
|
|
1447
1579
|
case sunspec_interfaces_js_1.SunspecStorageMode.CHARGE:
|
|
@@ -1465,29 +1597,29 @@ class SunspecModbusClient {
|
|
|
1465
1597
|
return false;
|
|
1466
1598
|
}
|
|
1467
1599
|
console.log(`Setting storage mode to ${mode} (control bits: 0x${storCtlMod.toString(16)})`);
|
|
1468
|
-
return this.writeBatteryControls({ storCtlMod });
|
|
1600
|
+
return this.writeBatteryControls(unitId, { storCtlMod });
|
|
1469
1601
|
}
|
|
1470
1602
|
/**
|
|
1471
1603
|
* Enable or disable grid charging
|
|
1472
1604
|
*/
|
|
1473
|
-
async enableGridCharging(enable) {
|
|
1605
|
+
async enableGridCharging(unitId, enable) {
|
|
1474
1606
|
const chaGriSet = enable ? sunspec_interfaces_js_1.SunspecChargeSource.GRID : sunspec_interfaces_js_1.SunspecChargeSource.PV;
|
|
1475
|
-
console.log(`${enable ? 'Enabling' : 'Disabling'} grid charging`);
|
|
1476
|
-
return this.writeBatteryControls({ chaGriSet });
|
|
1607
|
+
console.log(`${enable ? 'Enabling' : 'Disabling'} grid charging on unit ${unitId}`);
|
|
1608
|
+
return this.writeBatteryControls(unitId, { chaGriSet });
|
|
1477
1609
|
}
|
|
1478
1610
|
/**
|
|
1479
1611
|
* Read battery control settings from Model 124 (Basic Storage Controls)
|
|
1480
1612
|
*/
|
|
1481
|
-
async readBatteryControls() {
|
|
1482
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Battery);
|
|
1613
|
+
async readBatteryControls(unitId) {
|
|
1614
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Battery);
|
|
1483
1615
|
if (!model) {
|
|
1484
|
-
console.log(
|
|
1616
|
+
console.log(`Battery model 124 not found on unit ${unitId}`);
|
|
1485
1617
|
return null;
|
|
1486
1618
|
}
|
|
1487
|
-
console.log(`Reading Battery Controls from Model 124 at base address: ${model.address}`);
|
|
1619
|
+
console.log(`Reading Battery Controls from Model 124 at base address: ${model.address} (unit ${unitId})`);
|
|
1488
1620
|
try {
|
|
1489
1621
|
// Read entire model block in a single Modbus call
|
|
1490
|
-
const buffer = await this.readModelBlock(model);
|
|
1622
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1491
1623
|
// Extract scale factors from buffer
|
|
1492
1624
|
const scaleFactors = {
|
|
1493
1625
|
WChaMax_SF: this.extractValue(buffer, 18, 'int16'),
|
|
@@ -1526,19 +1658,19 @@ class SunspecModbusClient {
|
|
|
1526
1658
|
/**
|
|
1527
1659
|
* Read meter data from Model 201 (Single Phase) / Model 203 (Three Phase) / Model 204 (Split Phase)
|
|
1528
1660
|
*/
|
|
1529
|
-
async readMeterData() {
|
|
1530
|
-
let model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Meter3Phase);
|
|
1661
|
+
async readMeterData(unitId) {
|
|
1662
|
+
let model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Meter3Phase);
|
|
1531
1663
|
if (!model) {
|
|
1532
|
-
model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MeterWye);
|
|
1664
|
+
model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MeterWye);
|
|
1533
1665
|
}
|
|
1534
1666
|
if (!model) {
|
|
1535
|
-
model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MeterSinglePhase);
|
|
1667
|
+
model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MeterSinglePhase);
|
|
1536
1668
|
}
|
|
1537
1669
|
if (!model) {
|
|
1538
|
-
console.debug(
|
|
1670
|
+
console.debug(`No meter model found on unit ${unitId}`);
|
|
1539
1671
|
return null;
|
|
1540
1672
|
}
|
|
1541
|
-
console.debug(`Reading Meter Data from Model ${model.id} at base address: ${model.address}`);
|
|
1673
|
+
console.debug(`Reading Meter Data from Model ${model.id} at base address: ${model.address} (unit ${unitId})`);
|
|
1542
1674
|
try {
|
|
1543
1675
|
// Different meter models have different register offsets
|
|
1544
1676
|
console.debug(`Meter is Model ${model.id}`);
|
|
@@ -1582,7 +1714,7 @@ class SunspecModbusClient {
|
|
|
1582
1714
|
energySFOffset = 54; // TotWh_SF - Total Energy scale factor
|
|
1583
1715
|
}
|
|
1584
1716
|
// Read entire model block in a single Modbus call
|
|
1585
|
-
const buffer = await this.readModelBlock(model);
|
|
1717
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1586
1718
|
// Extract scale factors from buffer
|
|
1587
1719
|
const powerSF = this.extractValue(buffer, powerSFOffset, 'int16');
|
|
1588
1720
|
const freqSF = this.extractValue(buffer, freqSFOffset, 'int16');
|
|
@@ -1620,16 +1752,16 @@ class SunspecModbusClient {
|
|
|
1620
1752
|
/**
|
|
1621
1753
|
* Read common block data (Model 1)
|
|
1622
1754
|
*/
|
|
1623
|
-
async readCommonBlock() {
|
|
1624
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Common);
|
|
1755
|
+
async readCommonBlock(unitId) {
|
|
1756
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Common);
|
|
1625
1757
|
if (!model) {
|
|
1626
|
-
console.error(
|
|
1758
|
+
console.error(`Common block model not found on unit ${unitId}`);
|
|
1627
1759
|
return null;
|
|
1628
1760
|
}
|
|
1629
|
-
console.log(`Reading Common Block - Model address: ${model.address}`);
|
|
1761
|
+
console.log(`Reading Common Block - Model address: ${model.address} (unit ${unitId})`);
|
|
1630
1762
|
try {
|
|
1631
1763
|
// Read entire model block in a single Modbus call
|
|
1632
|
-
const buffer = await this.readModelBlock(model);
|
|
1764
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1633
1765
|
// Common block offsets are relative to the model start (after ID and Length header,
|
|
1634
1766
|
// but readModelBlock reads from model.address which includes the data area).
|
|
1635
1767
|
// The offsets below are relative to the data start within the model block.
|
|
@@ -1666,24 +1798,24 @@ class SunspecModbusClient {
|
|
|
1666
1798
|
/**
|
|
1667
1799
|
* Get serial number from device
|
|
1668
1800
|
*/
|
|
1669
|
-
async getSerialNumber() {
|
|
1670
|
-
const commonData = await this.readCommonBlock();
|
|
1801
|
+
async getSerialNumber(unitId) {
|
|
1802
|
+
const commonData = await this.readCommonBlock(unitId);
|
|
1671
1803
|
return commonData?.serialNumber;
|
|
1672
1804
|
}
|
|
1673
1805
|
/**
|
|
1674
|
-
* Check if connected
|
|
1806
|
+
* Check if a specific unit is connected on this network device
|
|
1675
1807
|
*/
|
|
1676
|
-
isConnected() {
|
|
1677
|
-
return this.
|
|
1808
|
+
isConnected(unitId) {
|
|
1809
|
+
return this.modbusInstances.has(unitId);
|
|
1678
1810
|
}
|
|
1679
1811
|
/**
|
|
1680
|
-
* Check if connection is healthy
|
|
1812
|
+
* Check if a specific unit's connection is healthy
|
|
1681
1813
|
*/
|
|
1682
|
-
isHealthy() {
|
|
1683
|
-
return this.
|
|
1814
|
+
isHealthy(unitId) {
|
|
1815
|
+
return this.modbusInstances.has(unitId) && this.connectionHealth.isHealthy();
|
|
1684
1816
|
}
|
|
1685
1817
|
/**
|
|
1686
|
-
* Get connection health details
|
|
1818
|
+
* Get connection health details (shared across all units on this network device)
|
|
1687
1819
|
*/
|
|
1688
1820
|
getConnectionHealth() {
|
|
1689
1821
|
return this.connectionHealth;
|
|
@@ -1691,15 +1823,15 @@ class SunspecModbusClient {
|
|
|
1691
1823
|
/**
|
|
1692
1824
|
* Read inverter settings from Model 121 (Inverter Settings)
|
|
1693
1825
|
*/
|
|
1694
|
-
async readInverterSettings() {
|
|
1695
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Settings);
|
|
1826
|
+
async readInverterSettings(unitId) {
|
|
1827
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Settings);
|
|
1696
1828
|
if (!model) {
|
|
1697
|
-
console.debug(
|
|
1829
|
+
console.debug(`Settings model 121 not found on unit ${unitId}`);
|
|
1698
1830
|
return null;
|
|
1699
1831
|
}
|
|
1700
1832
|
try {
|
|
1701
1833
|
// Read entire model block in a single Modbus call
|
|
1702
|
-
const buffer = await this.readModelBlock(model);
|
|
1834
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1703
1835
|
// Extract scale factors from buffer (offsets 22-31)
|
|
1704
1836
|
const scaleFactors = {
|
|
1705
1837
|
WMax_SF: this.extractValue(buffer, 22, 'int16'),
|
|
@@ -1783,15 +1915,15 @@ class SunspecModbusClient {
|
|
|
1783
1915
|
/**
|
|
1784
1916
|
* Read inverter controls from Model 123 (Immediate Inverter Controls)
|
|
1785
1917
|
*/
|
|
1786
|
-
async readInverterControls() {
|
|
1787
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Controls);
|
|
1918
|
+
async readInverterControls(unitId) {
|
|
1919
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Controls);
|
|
1788
1920
|
if (!model) {
|
|
1789
|
-
console.log(
|
|
1921
|
+
console.log(`Controls model 123 not found on unit ${unitId}`);
|
|
1790
1922
|
return null;
|
|
1791
1923
|
}
|
|
1792
1924
|
try {
|
|
1793
1925
|
// Read entire model block in a single Modbus call
|
|
1794
|
-
const buffer = await this.readModelBlock(model);
|
|
1926
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1795
1927
|
// Extract scale factors from buffer (offsets 21-23)
|
|
1796
1928
|
const scaleFactors = {
|
|
1797
1929
|
WMaxLimPct_SF: this.extractValue(buffer, 21, 'int16'),
|
|
@@ -1878,27 +2010,28 @@ class SunspecModbusClient {
|
|
|
1878
2010
|
/**
|
|
1879
2011
|
* Write Block 121 - Inverter Basic Settings
|
|
1880
2012
|
*/
|
|
1881
|
-
async writeInverterSettings(settings) {
|
|
1882
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Settings);
|
|
2013
|
+
async writeInverterSettings(unitId, settings) {
|
|
2014
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Settings);
|
|
1883
2015
|
if (!model) {
|
|
1884
|
-
console.error(
|
|
2016
|
+
console.error(`Settings model 121 not found on unit ${unitId}`);
|
|
1885
2017
|
return false;
|
|
1886
2018
|
}
|
|
1887
2019
|
const baseAddr = model.address;
|
|
1888
2020
|
try {
|
|
2021
|
+
const instance = this.getInstance(unitId);
|
|
1889
2022
|
// For each setting, write the value if provided
|
|
1890
2023
|
// Note: This is a simplified implementation. In production, you'd batch writes
|
|
1891
|
-
if (settings.WMax !== undefined
|
|
2024
|
+
if (settings.WMax !== undefined) {
|
|
1892
2025
|
// Need to read scale factor first if not provided
|
|
1893
|
-
const sfBuffer = await
|
|
2026
|
+
const sfBuffer = await instance.readHoldingRegisters(baseAddr + 22, 1);
|
|
1894
2027
|
const scaleFactor = sfBuffer.readInt16BE(0);
|
|
1895
2028
|
const scaledValue = Math.round(settings.WMax / Math.pow(10, scaleFactor));
|
|
1896
2029
|
// Writing registers needs to be implemented in EnergyAppModbusInstance
|
|
1897
2030
|
// For now, log the write operation
|
|
1898
2031
|
console.log(`Would write value ${scaledValue} to register ${baseAddr}`);
|
|
1899
2032
|
}
|
|
1900
|
-
if (settings.VRef !== undefined
|
|
1901
|
-
const sfBuffer = await
|
|
2033
|
+
if (settings.VRef !== undefined) {
|
|
2034
|
+
const sfBuffer = await instance.readHoldingRegisters(baseAddr + 23, 1);
|
|
1902
2035
|
const scaleFactor = sfBuffer.readInt16BE(0);
|
|
1903
2036
|
const scaledValue = Math.round(settings.VRef / Math.pow(10, scaleFactor));
|
|
1904
2037
|
console.log(`Would write value ${scaledValue} to register ${baseAddr + 1}`);
|
|
@@ -1915,41 +2048,41 @@ class SunspecModbusClient {
|
|
|
1915
2048
|
/**
|
|
1916
2049
|
* Write inverter controls to Model 123 (Immediate Inverter Controls)
|
|
1917
2050
|
*/
|
|
1918
|
-
async writeInverterControls(controls) {
|
|
1919
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Controls);
|
|
2051
|
+
async writeInverterControls(unitId, controls) {
|
|
2052
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Controls);
|
|
1920
2053
|
if (!model) {
|
|
1921
|
-
console.error(
|
|
2054
|
+
console.error(`Controls model 123 not found on unit ${unitId}`);
|
|
1922
2055
|
return false;
|
|
1923
2056
|
}
|
|
1924
2057
|
const baseAddr = model.address;
|
|
1925
2058
|
try {
|
|
1926
2059
|
// Connection control (Register 2)
|
|
1927
2060
|
if (controls.Conn !== undefined) {
|
|
1928
|
-
await this.writeRegisterValue(baseAddr + 2, controls.Conn, 'uint16');
|
|
2061
|
+
await this.writeRegisterValue(unitId, baseAddr + 2, controls.Conn, 'uint16');
|
|
1929
2062
|
console.log(`Set connection control to ${controls.Conn}`);
|
|
1930
2063
|
}
|
|
1931
2064
|
// Power limit control (Register 3) - needs scale factor
|
|
1932
2065
|
if (controls.WMaxLimPct !== undefined) {
|
|
1933
|
-
const scaleFactor = await this.readRegisterValue(baseAddr + 21, 1, 'int16');
|
|
2066
|
+
const scaleFactor = await this.readRegisterValue(unitId, baseAddr + 21, 1, 'int16');
|
|
1934
2067
|
const scaledValue = Math.round(controls.WMaxLimPct / Math.pow(10, scaleFactor));
|
|
1935
|
-
await this.writeRegisterValue(baseAddr + 3, scaledValue, 'uint16');
|
|
2068
|
+
await this.writeRegisterValue(unitId, baseAddr + 3, scaledValue, 'uint16');
|
|
1936
2069
|
console.log(`Set power limit to ${controls.WMaxLimPct}% (scaled: ${scaledValue})`);
|
|
1937
2070
|
}
|
|
1938
2071
|
// Throttle enable/disable (Register 7)
|
|
1939
2072
|
if (controls.WMaxLim_Ena !== undefined) {
|
|
1940
|
-
await this.writeRegisterValue(baseAddr + 7, controls.WMaxLim_Ena, 'uint16');
|
|
2073
|
+
await this.writeRegisterValue(unitId, baseAddr + 7, controls.WMaxLim_Ena, 'uint16');
|
|
1941
2074
|
console.log(`Set throttle enable to ${controls.WMaxLim_Ena}`);
|
|
1942
2075
|
}
|
|
1943
2076
|
// Power factor control (Register 8) - needs scale factor
|
|
1944
2077
|
if (controls.OutPFSet !== undefined) {
|
|
1945
|
-
const scaleFactor = await this.readRegisterValue(baseAddr + 22, 1, 'int16');
|
|
2078
|
+
const scaleFactor = await this.readRegisterValue(unitId, baseAddr + 22, 1, 'int16');
|
|
1946
2079
|
const scaledValue = Math.round(controls.OutPFSet / Math.pow(10, scaleFactor));
|
|
1947
|
-
await this.writeRegisterValue(baseAddr + 8, scaledValue, 'int16');
|
|
2080
|
+
await this.writeRegisterValue(unitId, baseAddr + 8, scaledValue, 'int16');
|
|
1948
2081
|
console.log(`Set power factor to ${controls.OutPFSet} (scaled: ${scaledValue})`);
|
|
1949
2082
|
}
|
|
1950
2083
|
// Power factor enable/disable (Register 12)
|
|
1951
2084
|
if (controls.OutPFSet_Ena !== undefined) {
|
|
1952
|
-
await this.writeRegisterValue(baseAddr + 12, controls.OutPFSet_Ena, 'uint16');
|
|
2085
|
+
await this.writeRegisterValue(unitId, baseAddr + 12, controls.OutPFSet_Ena, 'uint16');
|
|
1953
2086
|
console.log(`Set PF enable to ${controls.OutPFSet_Ena}`);
|
|
1954
2087
|
}
|
|
1955
2088
|
console.log('Inverter controls written successfully');
|
|
@@ -1970,21 +2103,21 @@ class SunspecModbusClient {
|
|
|
1970
2103
|
* @param limitW - Power limit in Watts, or null to remove the limit
|
|
1971
2104
|
* @returns true if successful, false otherwise
|
|
1972
2105
|
*/
|
|
1973
|
-
async setFeedInLimit(limitW) {
|
|
2106
|
+
async setFeedInLimit(unitId, limitW) {
|
|
1974
2107
|
if (limitW === null) {
|
|
1975
2108
|
// Remove limit: disable WMaxLim_Ena
|
|
1976
|
-
console.log(
|
|
1977
|
-
return this.writeInverterControls({ WMaxLim_Ena: sunspec_interfaces_js_1.SunspecEnableControl.DISABLED });
|
|
2109
|
+
console.log(`Removing feed-in limit (disabling WMaxLim_Ena) on unit ${unitId}`);
|
|
2110
|
+
return this.writeInverterControls(unitId, { WMaxLim_Ena: sunspec_interfaces_js_1.SunspecEnableControl.DISABLED });
|
|
1978
2111
|
}
|
|
1979
2112
|
// Read WMax from Model 121 to compute percentage
|
|
1980
|
-
const settings = await this.readInverterSettings();
|
|
2113
|
+
const settings = await this.readInverterSettings(unitId);
|
|
1981
2114
|
if (!settings || !settings.WMax) {
|
|
1982
|
-
console.error(
|
|
2115
|
+
console.error(`Cannot set feed-in limit on unit ${unitId}: unable to read WMax from Model 121`);
|
|
1983
2116
|
return false;
|
|
1984
2117
|
}
|
|
1985
2118
|
const pct = (limitW / settings.WMax) * 100;
|
|
1986
|
-
console.log(`Setting feed-in limit to ${limitW}W (${pct.toFixed(2)}% of WMax ${settings.WMax}W)`);
|
|
1987
|
-
return this.writeInverterControls({
|
|
2119
|
+
console.log(`Setting feed-in limit to ${limitW}W (${pct.toFixed(2)}% of WMax ${settings.WMax}W) on unit ${unitId}`);
|
|
2120
|
+
return this.writeInverterControls(unitId, {
|
|
1988
2121
|
WMaxLimPct: pct,
|
|
1989
2122
|
WMaxLim_Ena: sunspec_interfaces_js_1.SunspecEnableControl.ENABLED
|
|
1990
2123
|
});
|