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