@enyo-energy/sunspec-sdk 0.0.50 → 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 +448 -272
- package/dist/cjs/sunspec-modbus-client.d.cts +104 -46
- 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 +104 -46
- package/dist/sunspec-modbus-client.js +446 -272
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -20,165 +20,351 @@ 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
|
-
|
|
90
|
+
// Serializes connection-state transitions so concurrent callers cannot open duplicate
|
|
91
|
+
// sockets for the same unit ID while one is still alive.
|
|
92
|
+
operationChain = Promise.resolve();
|
|
36
93
|
constructor(energyApp) {
|
|
37
94
|
this.energyApp = energyApp;
|
|
38
95
|
this.connectionHealth = new EnergyAppModbusConnectionHealth();
|
|
39
96
|
this.modbusDataTypeConverter = new EnergyAppModbusDataTypeConverter();
|
|
40
97
|
}
|
|
41
98
|
/**
|
|
42
|
-
* 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.
|
|
43
103
|
* @param host Primary host (hostname) to connect to
|
|
44
104
|
* @param port Modbus port (default 502)
|
|
45
105
|
* @param unitId Modbus unit ID (default 1)
|
|
46
106
|
* @param secondaryHost Optional secondary host (ipAddress) for fallback during reconnection
|
|
47
107
|
*/
|
|
48
108
|
async connect(host, port = 502, unitId = 1, secondaryHost) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
109
|
+
return this.withConnectionLock(async () => {
|
|
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
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
this.connectionParams = { primaryHost: host, secondaryHost, port };
|
|
121
|
+
}
|
|
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})`);
|
|
59
129
|
});
|
|
60
|
-
// Create fault-tolerant reader with connection health monitoring
|
|
61
|
-
if (this.modbusClient) {
|
|
62
|
-
this.faultTolerantReader = new EnergyAppModbusFaultTolerantReader(this.modbusClient, this.connectionHealth);
|
|
63
|
-
}
|
|
64
|
-
this.connected = true;
|
|
65
|
-
this.connectionHealth.recordSuccess();
|
|
66
|
-
this.recordOpen();
|
|
67
|
-
console.log(`Connected to Sunspec device at ${host}:${port} unit ${unitId} (opens=${this.openCount}, closes=${this.closeCount}, current=${this.currentlyOpen})`);
|
|
68
130
|
}
|
|
69
131
|
/**
|
|
70
|
-
* Disconnect from
|
|
132
|
+
* Disconnect from all units of this network device.
|
|
71
133
|
*
|
|
72
|
-
* Note: connection parameters are preserved so reconnect() can
|
|
73
|
-
* 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.
|
|
74
136
|
*/
|
|
75
137
|
async disconnect() {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.connected = false;
|
|
81
|
-
this.discoveredModels.clear();
|
|
82
|
-
this.recordClose();
|
|
138
|
+
return this.withConnectionLock(async () => {
|
|
139
|
+
if (this.modbusInstances.size === 0) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
83
142
|
const host = this.connectionParams?.primaryHost ?? 'unknown';
|
|
84
143
|
const port = this.connectionParams?.port ?? 0;
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
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})`);
|
|
162
|
+
});
|
|
88
163
|
}
|
|
89
164
|
/**
|
|
90
|
-
* Reconnect using stored connection
|
|
91
|
-
* First tries primaryHost (hostname), then falls back to secondaryHost
|
|
92
|
-
* 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.
|
|
93
168
|
*/
|
|
94
169
|
async reconnect() {
|
|
95
|
-
|
|
96
|
-
|
|
170
|
+
return this.withConnectionLock(async () => {
|
|
171
|
+
if (!this.connectionParams) {
|
|
172
|
+
console.error('Cannot reconnect: no connection parameters stored. Call connect() first.');
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
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);
|
|
183
|
+
if (primarySuccess) {
|
|
184
|
+
console.log(`Successfully reconnected to primary host ${primaryHost}:${port} units [${units.join(',')}]`);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (secondaryHost && secondaryHost !== primaryHost) {
|
|
188
|
+
console.log(`Primary host failed, attempting secondary host ${secondaryHost}:${port} units [${units.join(',')}]...`);
|
|
189
|
+
const secondarySuccess = await this.attemptConnection(secondaryHost, port, units);
|
|
190
|
+
if (secondarySuccess) {
|
|
191
|
+
console.log(`Successfully reconnected to secondary host ${secondaryHost}:${port} units [${units.join(',')}]`);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
console.error(`Reconnection failed to all available hosts`);
|
|
196
|
+
return false;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
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`);
|
|
97
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.
|
|
235
|
+
*/
|
|
236
|
+
async attemptConnection(host, port, units) {
|
|
237
|
+
for (const unitId of units) {
|
|
238
|
+
if (this.modbusInstances.has(unitId)) {
|
|
239
|
+
await this._closeUnit(unitId);
|
|
240
|
+
}
|
|
98
241
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
console.log(`Successfully reconnected to primary host ${primaryHost}:${port} unit ${unitId}`);
|
|
242
|
+
try {
|
|
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})`);
|
|
105
247
|
return true;
|
|
106
248
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const secondarySuccess = await this.attemptConnection(secondaryHost, port, unitId);
|
|
111
|
-
if (secondarySuccess) {
|
|
112
|
-
console.log(`Successfully reconnected to secondary host ${secondaryHost}:${port} unit ${unitId}`);
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
console.error(`Connection attempt to ${host}:${port} failed: ${error}`);
|
|
251
|
+
return false;
|
|
115
252
|
}
|
|
116
|
-
console.error(`Reconnection failed to all available hosts`);
|
|
117
|
-
this.connected = false;
|
|
118
|
-
return false;
|
|
119
253
|
}
|
|
120
254
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
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.
|
|
123
258
|
*/
|
|
124
|
-
async
|
|
259
|
+
async _openUnit(host, port, unitId) {
|
|
260
|
+
let candidate = null;
|
|
125
261
|
try {
|
|
126
|
-
|
|
127
|
-
if (this.modbusClient) {
|
|
128
|
-
const wasConnected = this.connected;
|
|
129
|
-
try {
|
|
130
|
-
await this.modbusClient.disconnect();
|
|
131
|
-
}
|
|
132
|
-
catch (e) {
|
|
133
|
-
// Ignore disconnect errors during reconnection
|
|
134
|
-
}
|
|
135
|
-
this.modbusClient = null;
|
|
136
|
-
this.faultTolerantReader = null;
|
|
137
|
-
if (wasConnected) {
|
|
138
|
-
this.connected = false;
|
|
139
|
-
this.recordClose();
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// Attempt connection
|
|
143
|
-
this.modbusClient = await this.energyApp.useModbus().connect({
|
|
262
|
+
candidate = await this.energyApp.useModbus().connect({
|
|
144
263
|
host,
|
|
145
264
|
port,
|
|
146
265
|
unitId,
|
|
147
266
|
timeout: 5000
|
|
148
267
|
});
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.faultTolerantReader = new EnergyAppModbusFaultTolerantReader(this.modbusClient, this.connectionHealth);
|
|
268
|
+
if (!candidate) {
|
|
269
|
+
throw new Error(`useModbus().connect returned null for ${host}:${port} unit ${unitId}`);
|
|
152
270
|
}
|
|
153
|
-
|
|
271
|
+
const reader = new EnergyAppModbusFaultTolerantReader(candidate, this.connectionHealth);
|
|
272
|
+
this.modbusInstances.set(unitId, candidate);
|
|
273
|
+
this.faultTolerantReaders.set(unitId, reader);
|
|
154
274
|
this.connectionHealth.recordSuccess();
|
|
155
275
|
this.recordOpen();
|
|
156
|
-
console.log(`Connection attempt to ${host}:${port} unit ${unitId} succeeded (opens=${this.openCount}, closes=${this.closeCount}, current=${this.currentlyOpen})`);
|
|
157
|
-
return true;
|
|
158
276
|
}
|
|
159
|
-
catch (
|
|
160
|
-
|
|
161
|
-
|
|
277
|
+
catch (err) {
|
|
278
|
+
if (candidate) {
|
|
279
|
+
try {
|
|
280
|
+
await candidate.disconnect();
|
|
281
|
+
}
|
|
282
|
+
catch { /* ignore */ }
|
|
283
|
+
}
|
|
284
|
+
this.modbusInstances.delete(unitId);
|
|
285
|
+
this.faultTolerantReaders.delete(unitId);
|
|
286
|
+
throw err;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Close the Modbus instance for a single unit. Idempotent. Caller must hold the
|
|
291
|
+
* connection lock.
|
|
292
|
+
*/
|
|
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) {
|
|
300
|
+
try {
|
|
301
|
+
await instance.disconnect();
|
|
302
|
+
}
|
|
303
|
+
catch { /* ignore */ }
|
|
162
304
|
}
|
|
305
|
+
if (wasOpen)
|
|
306
|
+
this.recordClose();
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Run `fn` with exclusive access to connection-state transitions. Subsequent calls queue
|
|
310
|
+
* behind any in-flight one. A rejected `fn` does not poison the chain for later callers.
|
|
311
|
+
*/
|
|
312
|
+
withConnectionLock(fn) {
|
|
313
|
+
const run = this.operationChain.then(fn, fn);
|
|
314
|
+
this.operationChain = run.catch(() => undefined);
|
|
315
|
+
return run;
|
|
163
316
|
}
|
|
164
317
|
recordOpen() {
|
|
165
318
|
this.openCount++;
|
|
166
|
-
this.
|
|
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}. ` +
|
|
324
|
+
`This indicates a code path bypassing the connection lock — please investigate.`);
|
|
325
|
+
}
|
|
167
326
|
}
|
|
168
327
|
recordClose() {
|
|
169
328
|
this.closeCount++;
|
|
170
|
-
if (this.
|
|
171
|
-
this.
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
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`);
|
|
175
331
|
}
|
|
176
332
|
}
|
|
177
333
|
/**
|
|
178
|
-
* 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.
|
|
179
336
|
*/
|
|
180
337
|
getConnectionStats() {
|
|
181
|
-
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;
|
|
182
368
|
}
|
|
183
369
|
/**
|
|
184
370
|
* Enable or disable automatic reconnection
|
|
@@ -196,16 +382,14 @@ export class SunspecModbusClient {
|
|
|
196
382
|
/**
|
|
197
383
|
* Detect the base address and addressing mode (0-based or 1-based) for SunSpec
|
|
198
384
|
*/
|
|
199
|
-
async detectSunspecBaseAddress(customBaseAddress) {
|
|
200
|
-
|
|
201
|
-
throw new Error('Modbus client not initialized');
|
|
202
|
-
}
|
|
385
|
+
async detectSunspecBaseAddress(unitId, customBaseAddress) {
|
|
386
|
+
const instance = this.getInstance(unitId);
|
|
203
387
|
// If custom base address provided, try it first (1-based, then 0-based variant)
|
|
204
388
|
if (customBaseAddress !== undefined) {
|
|
205
389
|
console.log(`Detect models for custom base address '${customBaseAddress}' ...`);
|
|
206
390
|
// Try 0-based at custom address
|
|
207
391
|
try {
|
|
208
|
-
const sunspecId = await
|
|
392
|
+
const sunspecId = await instance.readRegisterStringValue(customBaseAddress, 2);
|
|
209
393
|
if (sunspecId.includes('SunS')) {
|
|
210
394
|
console.log(`Detected 0-based addressing mode (base address: ${customBaseAddress})`);
|
|
211
395
|
return {
|
|
@@ -220,7 +404,7 @@ export class SunspecModbusClient {
|
|
|
220
404
|
}
|
|
221
405
|
// Try 1-based at custom address (customBaseAddress + 1)
|
|
222
406
|
try {
|
|
223
|
-
const sunspecId = await
|
|
407
|
+
const sunspecId = await instance.readRegisterStringValue(customBaseAddress + 1, 2);
|
|
224
408
|
if (sunspecId.includes('SunS')) {
|
|
225
409
|
console.log(`Detected 1-based addressing mode (base address: ${customBaseAddress + 1})`);
|
|
226
410
|
return {
|
|
@@ -237,7 +421,7 @@ export class SunspecModbusClient {
|
|
|
237
421
|
else {
|
|
238
422
|
// Try 1-based addressing first (most common)
|
|
239
423
|
try {
|
|
240
|
-
const sunspecId = await
|
|
424
|
+
const sunspecId = await instance.readRegisterStringValue(40001, 2);
|
|
241
425
|
if (sunspecId.includes('SunS')) {
|
|
242
426
|
console.log('Detected 1-based addressing mode (base address: 40001)');
|
|
243
427
|
return {
|
|
@@ -252,7 +436,7 @@ export class SunspecModbusClient {
|
|
|
252
436
|
}
|
|
253
437
|
// Try 0-based addressing
|
|
254
438
|
try {
|
|
255
|
-
const sunspecId = await
|
|
439
|
+
const sunspecId = await instance.readRegisterStringValue(40000, 2);
|
|
256
440
|
if (sunspecId.includes('SunS')) {
|
|
257
441
|
console.log('Detected 0-based addressing mode (base address: 40000)');
|
|
258
442
|
return {
|
|
@@ -272,27 +456,22 @@ export class SunspecModbusClient {
|
|
|
272
456
|
throw new Error(`Device is not SunSpec compliant - "SunS" identifier not found at addresses ${addressesChecked}`);
|
|
273
457
|
}
|
|
274
458
|
/**
|
|
275
|
-
* Discover all available Sunspec models
|
|
459
|
+
* Discover all available Sunspec models for a unit
|
|
276
460
|
* Automatically detects base address (40000 or 40001) and scans from there
|
|
277
461
|
*/
|
|
278
|
-
async discoverModels(customBaseAddress) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
this.discoveredModels.clear();
|
|
462
|
+
async discoverModels(unitId, customBaseAddress) {
|
|
463
|
+
const instance = this.getInstance(unitId);
|
|
464
|
+
const models = this.getModelsMap(unitId);
|
|
465
|
+
models.clear();
|
|
283
466
|
const maxAddress = 50000; // Safety limit
|
|
284
467
|
let currentAddress = 0;
|
|
285
|
-
console.log(
|
|
468
|
+
console.log(`Starting Sunspec model discovery for unit ${unitId}...`);
|
|
286
469
|
try {
|
|
287
470
|
// Detect the base address and addressing mode
|
|
288
|
-
const addressInfo = await this.detectSunspecBaseAddress(customBaseAddress);
|
|
471
|
+
const addressInfo = await this.detectSunspecBaseAddress(unitId, customBaseAddress);
|
|
289
472
|
currentAddress = addressInfo.nextAddress;
|
|
290
473
|
while (currentAddress < maxAddress) {
|
|
291
|
-
|
|
292
|
-
if (!this.modbusClient) {
|
|
293
|
-
throw new Error('Modbus client not initialized');
|
|
294
|
-
}
|
|
295
|
-
const buffer = await this.modbusClient.readHoldingRegisters(currentAddress, 2);
|
|
474
|
+
const buffer = await instance.readHoldingRegisters(currentAddress, 2);
|
|
296
475
|
const modelData = [buffer.readUInt16BE(0), buffer.readUInt16BE(2)];
|
|
297
476
|
if (!modelData || modelData.length < 2) {
|
|
298
477
|
console.log(`No data at address ${currentAddress}, ending discovery`);
|
|
@@ -311,23 +490,23 @@ export class SunspecModbusClient {
|
|
|
311
490
|
address: currentAddress,
|
|
312
491
|
length: modelLength
|
|
313
492
|
};
|
|
314
|
-
|
|
315
|
-
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})`);
|
|
316
495
|
// Jump to next model: current address + 2 (header) + model length
|
|
317
496
|
currentAddress = currentAddress + 2 + modelLength;
|
|
318
497
|
}
|
|
319
498
|
}
|
|
320
499
|
catch (error) {
|
|
321
|
-
console.error(`Error during model discovery at address ${currentAddress}: ${error}`);
|
|
500
|
+
console.error(`Error during model discovery at address ${currentAddress} (unit ${unitId}): ${error}`);
|
|
322
501
|
}
|
|
323
|
-
console.log(`Discovery complete. Found ${
|
|
324
|
-
return
|
|
502
|
+
console.log(`Discovery complete for unit ${unitId}. Found ${models.size} models`);
|
|
503
|
+
return models;
|
|
325
504
|
}
|
|
326
505
|
/**
|
|
327
|
-
* Find a specific model by ID
|
|
506
|
+
* Find a specific model by ID for a given unit
|
|
328
507
|
*/
|
|
329
|
-
findModel(modelId) {
|
|
330
|
-
return this.
|
|
508
|
+
findModel(unitId, modelId) {
|
|
509
|
+
return this.discoveredModelsByUnit.get(unitId)?.get(modelId);
|
|
331
510
|
}
|
|
332
511
|
/**
|
|
333
512
|
* Check if a value is "unimplemented" according to Sunspec specification
|
|
@@ -403,17 +582,15 @@ export class SunspecModbusClient {
|
|
|
403
582
|
* Read an entire model's register block in a single Modbus call.
|
|
404
583
|
* Returns a Buffer containing all registers for the model.
|
|
405
584
|
*/
|
|
406
|
-
async readModelBlock(model) {
|
|
407
|
-
|
|
408
|
-
throw new Error('Fault-tolerant reader not initialized');
|
|
409
|
-
}
|
|
585
|
+
async readModelBlock(unitId, model) {
|
|
586
|
+
const reader = this.getReader(unitId);
|
|
410
587
|
// Read model.length + 2 registers: the 2-register header (ID + length) plus all data registers.
|
|
411
588
|
// This way buffer offsets match the convention used throughout: offset 0 = model ID,
|
|
412
589
|
// offset 1 = model length, offset 2 = first data register, etc.
|
|
413
590
|
const totalRegisters = model.length + 2;
|
|
414
|
-
const result = await
|
|
591
|
+
const result = await reader.readHoldingRegisters(model.address, totalRegisters);
|
|
415
592
|
if (!result.success || !result.value) {
|
|
416
|
-
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'}`);
|
|
417
594
|
}
|
|
418
595
|
this.connectionHealth.recordSuccess();
|
|
419
596
|
return result.value;
|
|
@@ -438,12 +615,10 @@ export class SunspecModbusClient {
|
|
|
438
615
|
/**
|
|
439
616
|
* Helper to read register value(s) using the fault-tolerant reader with data type conversion
|
|
440
617
|
*/
|
|
441
|
-
async readRegisterValue(address, quantity = 1, dataType) {
|
|
442
|
-
|
|
443
|
-
throw new Error('Fault-tolerant reader not initialized');
|
|
444
|
-
}
|
|
618
|
+
async readRegisterValue(unitId, address, quantity = 1, dataType) {
|
|
619
|
+
const reader = this.getReader(unitId);
|
|
445
620
|
try {
|
|
446
|
-
const result = await
|
|
621
|
+
const result = await reader.readHoldingRegisters(address, quantity);
|
|
447
622
|
// Check if the read was successful
|
|
448
623
|
if (!result.success || !result.value) {
|
|
449
624
|
throw new Error(`Failed to read register at address ${address}: ${result.error?.message || 'Unknown error'}`);
|
|
@@ -464,10 +639,8 @@ export class SunspecModbusClient {
|
|
|
464
639
|
/**
|
|
465
640
|
* Helper to write register value(s)
|
|
466
641
|
*/
|
|
467
|
-
async writeRegisterValue(address, value, dataType = 'uint16') {
|
|
468
|
-
|
|
469
|
-
throw new Error('Modbus client not initialized');
|
|
470
|
-
}
|
|
642
|
+
async writeRegisterValue(unitId, address, value, dataType = 'uint16') {
|
|
643
|
+
const instance = this.getInstance(unitId);
|
|
471
644
|
try {
|
|
472
645
|
// Convert value to array of register values
|
|
473
646
|
let registerValues;
|
|
@@ -496,9 +669,9 @@ export class SunspecModbusClient {
|
|
|
496
669
|
}
|
|
497
670
|
}
|
|
498
671
|
// Write to holding registers
|
|
499
|
-
await
|
|
672
|
+
await instance.writeMultipleRegisters(address, registerValues);
|
|
500
673
|
this.connectionHealth.recordSuccess();
|
|
501
|
-
console.log(`Successfully wrote value ${value} to register ${address}`);
|
|
674
|
+
console.log(`Successfully wrote value ${value} to register ${address} (unit ${unitId})`);
|
|
502
675
|
return true;
|
|
503
676
|
}
|
|
504
677
|
catch (error) {
|
|
@@ -510,23 +683,23 @@ export class SunspecModbusClient {
|
|
|
510
683
|
/**
|
|
511
684
|
* Read inverter data from Model 101 (Single Phase) / Model 103 (Three Phase)
|
|
512
685
|
*/
|
|
513
|
-
async readInverterData() {
|
|
514
|
-
const model = this.findModel(SunspecModelId.Inverter3Phase);
|
|
686
|
+
async readInverterData(unitId) {
|
|
687
|
+
const model = this.findModel(unitId, SunspecModelId.Inverter3Phase);
|
|
515
688
|
if (!model) {
|
|
516
|
-
console.
|
|
517
|
-
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);
|
|
518
691
|
if (!singlePhaseModel) {
|
|
519
|
-
console.error(
|
|
692
|
+
console.error(`No inverter model found on unit ${unitId}`);
|
|
520
693
|
return null;
|
|
521
694
|
}
|
|
522
695
|
console.warn('IMPORTANT: Working with single-phase inverter, but 3-phase is expected!');
|
|
523
|
-
return this.readSinglePhaseInverterData(singlePhaseModel);
|
|
696
|
+
return this.readSinglePhaseInverterData(unitId, singlePhaseModel);
|
|
524
697
|
}
|
|
525
|
-
console.
|
|
698
|
+
console.debug(`Found 3-phase inverter model 103 at address ${model.address} with length ${model.length} (unit ${unitId})`);
|
|
526
699
|
try {
|
|
527
700
|
// Read entire model block in a single Modbus call
|
|
528
|
-
console.
|
|
529
|
-
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);
|
|
530
703
|
// Extract all scale factors from buffer
|
|
531
704
|
const scaleFactors = this.extractInverterScaleFactors(buffer);
|
|
532
705
|
// Extract raw values from buffer
|
|
@@ -607,11 +780,11 @@ export class SunspecModbusClient {
|
|
|
607
780
|
/**
|
|
608
781
|
* Read single phase inverter data (Model 101)
|
|
609
782
|
*/
|
|
610
|
-
async readSinglePhaseInverterData(model) {
|
|
783
|
+
async readSinglePhaseInverterData(unitId, model) {
|
|
611
784
|
try {
|
|
612
|
-
console.
|
|
785
|
+
console.debug(`Reading Single-Phase Inverter Data from Model 101 at base address: ${model.address} (unit ${unitId})`);
|
|
613
786
|
// Read entire model block in a single Modbus call
|
|
614
|
-
const buffer = await this.readModelBlock(model);
|
|
787
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
615
788
|
// Extract scale factors from buffer
|
|
616
789
|
const scaleFactors = {
|
|
617
790
|
A_SF: this.extractValue(buffer, 6, 'int16'),
|
|
@@ -760,18 +933,18 @@ export class SunspecModbusClient {
|
|
|
760
933
|
this.logRegisterRead(160, 5, 'DCWH_SF', scaleFactors.DCWH_SF, 'int16');
|
|
761
934
|
return scaleFactors;
|
|
762
935
|
}
|
|
763
|
-
async readMPPTScaleFactors() {
|
|
764
|
-
const model = this.findModel(SunspecModelId.MPPT);
|
|
936
|
+
async readMPPTScaleFactors(unitId) {
|
|
937
|
+
const model = this.findModel(unitId, SunspecModelId.MPPT);
|
|
765
938
|
if (!model) {
|
|
766
|
-
console.
|
|
939
|
+
console.debug(`MPPT model 160 not found on unit ${unitId}`);
|
|
767
940
|
return null;
|
|
768
941
|
}
|
|
769
942
|
try {
|
|
770
|
-
const buffer = await this.readModelBlock(model);
|
|
943
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
771
944
|
return this.extractMPPTScaleFactors(buffer);
|
|
772
945
|
}
|
|
773
946
|
catch (error) {
|
|
774
|
-
console.error(`Error reading MPPT scale factors: ${error}`);
|
|
947
|
+
console.error(`Error reading MPPT scale factors (unit ${unitId}): ${error}`);
|
|
775
948
|
return null;
|
|
776
949
|
}
|
|
777
950
|
}
|
|
@@ -797,7 +970,7 @@ export class SunspecModbusClient {
|
|
|
797
970
|
this.isUnimplementedValue(dcCurrentRaw, 'uint16') &&
|
|
798
971
|
this.isUnimplementedValue(dcVoltageRaw, 'uint16') &&
|
|
799
972
|
this.isUnimplementedValue(dcPowerRaw, 'uint16')) {
|
|
800
|
-
console.
|
|
973
|
+
console.debug(`MPPT module ${moduleId} appears to be unconnected (all values are 0xFFFF)`);
|
|
801
974
|
return null;
|
|
802
975
|
}
|
|
803
976
|
const temperatureScaleFactor = -1;
|
|
@@ -832,52 +1005,52 @@ export class SunspecModbusClient {
|
|
|
832
1005
|
/**
|
|
833
1006
|
* Read MPPT data from Model 160
|
|
834
1007
|
*/
|
|
835
|
-
async readMPPTData(moduleId = 1) {
|
|
836
|
-
const model = this.findModel(SunspecModelId.MPPT);
|
|
1008
|
+
async readMPPTData(unitId, moduleId = 1) {
|
|
1009
|
+
const model = this.findModel(unitId, SunspecModelId.MPPT);
|
|
837
1010
|
if (!model) {
|
|
838
|
-
console.
|
|
1011
|
+
console.debug(`MPPT model 160 not found on unit ${unitId}`);
|
|
839
1012
|
return null;
|
|
840
1013
|
}
|
|
841
1014
|
try {
|
|
842
1015
|
// Read entire model block in a single Modbus call
|
|
843
|
-
const buffer = await this.readModelBlock(model);
|
|
1016
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
844
1017
|
const scaleFactors = this.extractMPPTScaleFactors(buffer);
|
|
845
1018
|
return this.extractMPPTModuleData(buffer, model, moduleId, scaleFactors);
|
|
846
1019
|
}
|
|
847
1020
|
catch (error) {
|
|
848
|
-
console.error(`Error reading MPPT data for module ${moduleId}: ${error}`);
|
|
1021
|
+
console.error(`Error reading MPPT data for module ${moduleId} (unit ${unitId}): ${error}`);
|
|
849
1022
|
return null;
|
|
850
1023
|
}
|
|
851
1024
|
}
|
|
852
1025
|
/**
|
|
853
1026
|
* Read all MPPT strings from Model 160 (Multiple MPPT)
|
|
854
1027
|
*/
|
|
855
|
-
async readAllMPPTData() {
|
|
1028
|
+
async readAllMPPTData(unitId) {
|
|
856
1029
|
const mpptData = [];
|
|
857
|
-
const model = this.findModel(SunspecModelId.MPPT);
|
|
1030
|
+
const model = this.findModel(unitId, SunspecModelId.MPPT);
|
|
858
1031
|
if (!model) {
|
|
859
|
-
console.
|
|
1032
|
+
console.debug(`MPPT model 160 not found on unit ${unitId}`);
|
|
860
1033
|
return [];
|
|
861
1034
|
}
|
|
862
1035
|
try {
|
|
863
1036
|
// Read entire model block in a single Modbus call
|
|
864
|
-
const buffer = await this.readModelBlock(model);
|
|
1037
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
865
1038
|
const scaleFactors = this.extractMPPTScaleFactors(buffer);
|
|
866
1039
|
// Read the module count from register 8
|
|
867
1040
|
let moduleCount = 4; // Default fallback value
|
|
868
1041
|
const count = this.extractValue(buffer, 8, 'uint16');
|
|
869
1042
|
if (!this.isUnimplementedValue(count, 'uint16') && count > 0 && count <= 20) {
|
|
870
1043
|
moduleCount = count;
|
|
871
|
-
console.
|
|
1044
|
+
console.debug(`MPPT module count from register 8: ${moduleCount}`);
|
|
872
1045
|
}
|
|
873
1046
|
else {
|
|
874
|
-
console.
|
|
1047
|
+
console.debug(`Invalid or unimplemented module count (${count}), using default: ${moduleCount}`);
|
|
875
1048
|
}
|
|
876
1049
|
// Extract each MPPT module from the same buffer
|
|
877
1050
|
for (let i = 1; i <= moduleCount; i++) {
|
|
878
1051
|
try {
|
|
879
1052
|
const data = this.extractMPPTModuleData(buffer, model, i, scaleFactors);
|
|
880
|
-
console.
|
|
1053
|
+
console.debug(`MPPT ${i} has id ${data?.id} (${data?.stringId}) with ${data?.dcPower}W`);
|
|
881
1054
|
if (data &&
|
|
882
1055
|
(data.dcCurrent !== undefined ||
|
|
883
1056
|
data.dcVoltage !== undefined ||
|
|
@@ -1022,16 +1195,16 @@ export class SunspecModbusClient {
|
|
|
1022
1195
|
/**
|
|
1023
1196
|
* Read battery base data from Model 802 (Battery Base)
|
|
1024
1197
|
*/
|
|
1025
|
-
async readBatteryBaseData() {
|
|
1026
|
-
const model = this.findModel(SunspecModelId.BatteryBase);
|
|
1198
|
+
async readBatteryBaseData(unitId) {
|
|
1199
|
+
const model = this.findModel(unitId, SunspecModelId.BatteryBase);
|
|
1027
1200
|
if (!model) {
|
|
1028
|
-
console.
|
|
1201
|
+
console.debug(`Battery Base model 802 not found on unit ${unitId}`);
|
|
1029
1202
|
return null;
|
|
1030
1203
|
}
|
|
1031
|
-
console.
|
|
1204
|
+
console.debug(`Reading Battery Base Data from Model 802 at base address: ${model.address} (unit ${unitId})`);
|
|
1032
1205
|
try {
|
|
1033
1206
|
// Read entire model block in a single Modbus call
|
|
1034
|
-
const buffer = await this.readModelBlock(model);
|
|
1207
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1035
1208
|
// Extract scale factors from buffer (offsets 52-63)
|
|
1036
1209
|
const sf = this.extractBatteryBaseScaleFactors(buffer);
|
|
1037
1210
|
// Extract raw values from buffer
|
|
@@ -1160,27 +1333,27 @@ export class SunspecModbusClient {
|
|
|
1160
1333
|
/**
|
|
1161
1334
|
* Read battery data from Model 124 (Basic Storage) with fallback to Model 802 / Model 803
|
|
1162
1335
|
*/
|
|
1163
|
-
async readBatteryData() {
|
|
1336
|
+
async readBatteryData(unitId) {
|
|
1164
1337
|
// Try Model 124 first (Basic Storage Controls)
|
|
1165
|
-
let model = this.findModel(SunspecModelId.Battery);
|
|
1338
|
+
let model = this.findModel(unitId, SunspecModelId.Battery);
|
|
1166
1339
|
// Fall back to other battery models if needed
|
|
1167
1340
|
if (!model) {
|
|
1168
|
-
model = this.findModel(SunspecModelId.BatteryBase);
|
|
1341
|
+
model = this.findModel(unitId, SunspecModelId.BatteryBase);
|
|
1169
1342
|
}
|
|
1170
1343
|
if (!model) {
|
|
1171
|
-
model = this.findModel(SunspecModelId.BatteryControl);
|
|
1344
|
+
model = this.findModel(unitId, SunspecModelId.BatteryControl);
|
|
1172
1345
|
}
|
|
1173
1346
|
if (!model) {
|
|
1174
|
-
console.
|
|
1347
|
+
console.debug(`No battery model found on unit ${unitId}`);
|
|
1175
1348
|
return null;
|
|
1176
1349
|
}
|
|
1177
|
-
console.
|
|
1350
|
+
console.debug(`Reading Battery Data from Model ${model.id} at base address: ${model.address} (unit ${unitId})`);
|
|
1178
1351
|
try {
|
|
1179
1352
|
if (model.id === 124) {
|
|
1180
1353
|
// Model 124: Basic Storage Controls
|
|
1181
|
-
console.
|
|
1354
|
+
console.debug('Using Model 124 (Basic Storage Controls)');
|
|
1182
1355
|
// Read entire model block in a single Modbus call
|
|
1183
|
-
const buffer = await this.readModelBlock(model);
|
|
1356
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1184
1357
|
// Extract scale factors from buffer (offsets 18-25)
|
|
1185
1358
|
const scaleFactors = {
|
|
1186
1359
|
WChaMax_SF: this.extractValue(buffer, 18, 'int16'),
|
|
@@ -1268,20 +1441,20 @@ export class SunspecModbusClient {
|
|
|
1268
1441
|
// Calculate charge/discharge power if rates are available
|
|
1269
1442
|
if (data.inWRte !== undefined && data.wChaMax !== undefined) {
|
|
1270
1443
|
data.chargePower = (data.inWRte / 100) * data.wChaMax;
|
|
1271
|
-
console.
|
|
1444
|
+
console.debug(`Calculated Charge Power: (inWRte: ${data.inWRte}, wChaMax: ${data.wChaMax}) ${data.chargePower?.toFixed(2)} W`);
|
|
1272
1445
|
}
|
|
1273
1446
|
if (data.outWRte !== undefined && data.wChaMax !== undefined) {
|
|
1274
1447
|
// Assuming WDisChaMax is similar to WChaMax for simplicity
|
|
1275
1448
|
data.dischargePower = Math.abs((data.outWRte / 100) * data.wChaMax);
|
|
1276
|
-
console.
|
|
1449
|
+
console.debug(`Calculated Discharge Power (inWRte: ${data.outWRte}, wChaMax: ${data.wChaMax}): ${data.dischargePower?.toFixed(2)} W`);
|
|
1277
1450
|
}
|
|
1278
1451
|
console.debug('[Model 124] Battery Data:', data);
|
|
1279
1452
|
return data;
|
|
1280
1453
|
}
|
|
1281
1454
|
else if (model.id === 802) {
|
|
1282
1455
|
// Model 802: Battery Base
|
|
1283
|
-
console.
|
|
1284
|
-
const baseData = await this.readBatteryBaseData();
|
|
1456
|
+
console.debug('Using Model 802 (Battery Base)');
|
|
1457
|
+
const baseData = await this.readBatteryBaseData(unitId);
|
|
1285
1458
|
if (!baseData) {
|
|
1286
1459
|
return null;
|
|
1287
1460
|
}
|
|
@@ -1317,7 +1490,7 @@ export class SunspecModbusClient {
|
|
|
1317
1490
|
}
|
|
1318
1491
|
else {
|
|
1319
1492
|
// Handle other battery models (803) if needed
|
|
1320
|
-
console.
|
|
1493
|
+
console.debug(`Battery Model ${model.id} reading not yet implemented`);
|
|
1321
1494
|
return {
|
|
1322
1495
|
blockNumber: model.id,
|
|
1323
1496
|
blockAddress: model.address,
|
|
@@ -1333,55 +1506,55 @@ export class SunspecModbusClient {
|
|
|
1333
1506
|
/**
|
|
1334
1507
|
* Write battery control settings to Model 124
|
|
1335
1508
|
*/
|
|
1336
|
-
async writeBatteryControls(controls) {
|
|
1337
|
-
const model = this.findModel(SunspecModelId.Battery);
|
|
1509
|
+
async writeBatteryControls(unitId, controls) {
|
|
1510
|
+
const model = this.findModel(unitId, SunspecModelId.Battery);
|
|
1338
1511
|
if (!model) {
|
|
1339
|
-
console.error(
|
|
1512
|
+
console.error(`Battery model 124 not found on unit ${unitId}`);
|
|
1340
1513
|
return false;
|
|
1341
1514
|
}
|
|
1342
1515
|
const baseAddr = model.address;
|
|
1343
|
-
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})`);
|
|
1344
1517
|
try {
|
|
1345
1518
|
// Write storage control mode (Register 5)
|
|
1346
1519
|
if (controls.storCtlMod !== undefined) {
|
|
1347
|
-
await this.writeRegisterValue(baseAddr + 5, controls.storCtlMod, 'uint16');
|
|
1520
|
+
await this.writeRegisterValue(unitId, baseAddr + 5, controls.storCtlMod, 'uint16');
|
|
1348
1521
|
console.log(`Set storage control mode to 0x${controls.storCtlMod.toString(16)}`);
|
|
1349
1522
|
}
|
|
1350
1523
|
// Write charge source setting (Register 17)
|
|
1351
1524
|
if (controls.chaGriSet !== undefined) {
|
|
1352
|
-
await this.writeRegisterValue(baseAddr + 17, controls.chaGriSet, 'uint16');
|
|
1525
|
+
await this.writeRegisterValue(unitId, baseAddr + 17, controls.chaGriSet, 'uint16');
|
|
1353
1526
|
console.log(`Set charge source to ${controls.chaGriSet === SunspecChargeSource.GRID ? 'GRID' : 'PV'}`);
|
|
1354
1527
|
}
|
|
1355
1528
|
// Write maximum charge power (Register 2) - needs scale factor
|
|
1356
1529
|
if (controls.wChaMax !== undefined) {
|
|
1357
1530
|
const scaleFactorAddr = baseAddr + 18;
|
|
1358
|
-
const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
|
|
1531
|
+
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1359
1532
|
const scaledValue = Math.round(controls.wChaMax / Math.pow(10, scaleFactor));
|
|
1360
|
-
await this.writeRegisterValue(baseAddr + 2, scaledValue, 'uint16');
|
|
1533
|
+
await this.writeRegisterValue(unitId, baseAddr + 2, scaledValue, 'uint16');
|
|
1361
1534
|
console.log(`Set max charge power to ${controls.wChaMax}W (scaled: ${scaledValue})`);
|
|
1362
1535
|
}
|
|
1363
1536
|
// Write charge rate (Register 13) - needs scale factor
|
|
1364
1537
|
if (controls.inWRte !== undefined) {
|
|
1365
1538
|
const scaleFactorAddr = baseAddr + 25;
|
|
1366
|
-
const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
|
|
1539
|
+
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1367
1540
|
const scaledValue = Math.round(controls.inWRte / Math.pow(10, scaleFactor));
|
|
1368
|
-
await this.writeRegisterValue(baseAddr + 13, scaledValue, 'int16');
|
|
1541
|
+
await this.writeRegisterValue(unitId, baseAddr + 13, scaledValue, 'int16');
|
|
1369
1542
|
console.log(`Set charge rate to ${controls.inWRte}% (scaled: ${scaledValue})`);
|
|
1370
1543
|
}
|
|
1371
1544
|
// Write discharge rate (Register 12) - needs scale factor
|
|
1372
1545
|
if (controls.outWRte !== undefined) {
|
|
1373
1546
|
const scaleFactorAddr = baseAddr + 25;
|
|
1374
|
-
const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
|
|
1547
|
+
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1375
1548
|
const scaledValue = Math.round(controls.outWRte / Math.pow(10, scaleFactor));
|
|
1376
|
-
await this.writeRegisterValue(baseAddr + 12, scaledValue, 'int16');
|
|
1549
|
+
await this.writeRegisterValue(unitId, baseAddr + 12, scaledValue, 'int16');
|
|
1377
1550
|
console.log(`Set discharge rate to ${controls.outWRte}% (scaled: ${scaledValue})`);
|
|
1378
1551
|
}
|
|
1379
1552
|
// Write minimum reserve percentage (Register 7) - needs scale factor
|
|
1380
1553
|
if (controls.minRsvPct !== undefined) {
|
|
1381
1554
|
const scaleFactorAddr = baseAddr + 21;
|
|
1382
|
-
const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
|
|
1555
|
+
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1383
1556
|
const scaledValue = Math.round(controls.minRsvPct / Math.pow(10, scaleFactor));
|
|
1384
|
-
await this.writeRegisterValue(baseAddr + 7, scaledValue, 'uint16');
|
|
1557
|
+
await this.writeRegisterValue(unitId, baseAddr + 7, scaledValue, 'uint16');
|
|
1385
1558
|
console.log(`Set minimum reserve to ${controls.minRsvPct}% (scaled: ${scaledValue})`);
|
|
1386
1559
|
}
|
|
1387
1560
|
console.log('Battery controls written successfully');
|
|
@@ -1395,7 +1568,7 @@ export class SunspecModbusClient {
|
|
|
1395
1568
|
/**
|
|
1396
1569
|
* Set battery storage mode (simplified interface)
|
|
1397
1570
|
*/
|
|
1398
|
-
async setStorageMode(mode) {
|
|
1571
|
+
async setStorageMode(unitId, mode) {
|
|
1399
1572
|
let storCtlMod;
|
|
1400
1573
|
switch (mode) {
|
|
1401
1574
|
case SunspecStorageMode.CHARGE:
|
|
@@ -1419,29 +1592,29 @@ export class SunspecModbusClient {
|
|
|
1419
1592
|
return false;
|
|
1420
1593
|
}
|
|
1421
1594
|
console.log(`Setting storage mode to ${mode} (control bits: 0x${storCtlMod.toString(16)})`);
|
|
1422
|
-
return this.writeBatteryControls({ storCtlMod });
|
|
1595
|
+
return this.writeBatteryControls(unitId, { storCtlMod });
|
|
1423
1596
|
}
|
|
1424
1597
|
/**
|
|
1425
1598
|
* Enable or disable grid charging
|
|
1426
1599
|
*/
|
|
1427
|
-
async enableGridCharging(enable) {
|
|
1600
|
+
async enableGridCharging(unitId, enable) {
|
|
1428
1601
|
const chaGriSet = enable ? SunspecChargeSource.GRID : SunspecChargeSource.PV;
|
|
1429
|
-
console.log(`${enable ? 'Enabling' : 'Disabling'} grid charging`);
|
|
1430
|
-
return this.writeBatteryControls({ chaGriSet });
|
|
1602
|
+
console.log(`${enable ? 'Enabling' : 'Disabling'} grid charging on unit ${unitId}`);
|
|
1603
|
+
return this.writeBatteryControls(unitId, { chaGriSet });
|
|
1431
1604
|
}
|
|
1432
1605
|
/**
|
|
1433
1606
|
* Read battery control settings from Model 124 (Basic Storage Controls)
|
|
1434
1607
|
*/
|
|
1435
|
-
async readBatteryControls() {
|
|
1436
|
-
const model = this.findModel(SunspecModelId.Battery);
|
|
1608
|
+
async readBatteryControls(unitId) {
|
|
1609
|
+
const model = this.findModel(unitId, SunspecModelId.Battery);
|
|
1437
1610
|
if (!model) {
|
|
1438
|
-
console.log(
|
|
1611
|
+
console.log(`Battery model 124 not found on unit ${unitId}`);
|
|
1439
1612
|
return null;
|
|
1440
1613
|
}
|
|
1441
|
-
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})`);
|
|
1442
1615
|
try {
|
|
1443
1616
|
// Read entire model block in a single Modbus call
|
|
1444
|
-
const buffer = await this.readModelBlock(model);
|
|
1617
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1445
1618
|
// Extract scale factors from buffer
|
|
1446
1619
|
const scaleFactors = {
|
|
1447
1620
|
WChaMax_SF: this.extractValue(buffer, 18, 'int16'),
|
|
@@ -1480,22 +1653,22 @@ export class SunspecModbusClient {
|
|
|
1480
1653
|
/**
|
|
1481
1654
|
* Read meter data from Model 201 (Single Phase) / Model 203 (Three Phase) / Model 204 (Split Phase)
|
|
1482
1655
|
*/
|
|
1483
|
-
async readMeterData() {
|
|
1484
|
-
let model = this.findModel(SunspecModelId.Meter3Phase);
|
|
1656
|
+
async readMeterData(unitId) {
|
|
1657
|
+
let model = this.findModel(unitId, SunspecModelId.Meter3Phase);
|
|
1485
1658
|
if (!model) {
|
|
1486
|
-
model = this.findModel(SunspecModelId.MeterWye);
|
|
1659
|
+
model = this.findModel(unitId, SunspecModelId.MeterWye);
|
|
1487
1660
|
}
|
|
1488
1661
|
if (!model) {
|
|
1489
|
-
model = this.findModel(SunspecModelId.MeterSinglePhase);
|
|
1662
|
+
model = this.findModel(unitId, SunspecModelId.MeterSinglePhase);
|
|
1490
1663
|
}
|
|
1491
1664
|
if (!model) {
|
|
1492
|
-
console.
|
|
1665
|
+
console.debug(`No meter model found on unit ${unitId}`);
|
|
1493
1666
|
return null;
|
|
1494
1667
|
}
|
|
1495
|
-
console.
|
|
1668
|
+
console.debug(`Reading Meter Data from Model ${model.id} at base address: ${model.address} (unit ${unitId})`);
|
|
1496
1669
|
try {
|
|
1497
1670
|
// Different meter models have different register offsets
|
|
1498
|
-
console.
|
|
1671
|
+
console.debug(`Meter is Model ${model.id}`);
|
|
1499
1672
|
let powerOffset;
|
|
1500
1673
|
let powerSFOffset;
|
|
1501
1674
|
let freqOffset;
|
|
@@ -1505,7 +1678,7 @@ export class SunspecModbusClient {
|
|
|
1505
1678
|
let energySFOffset;
|
|
1506
1679
|
switch (model.id) {
|
|
1507
1680
|
case 203: // 3-phase meter (Delta)
|
|
1508
|
-
console.
|
|
1681
|
+
console.debug('Using Model 203 (3-Phase Meter Delta) offsets');
|
|
1509
1682
|
powerOffset = 18; // W - Total Real Power (int16)
|
|
1510
1683
|
powerSFOffset = 22; // W_SF - Power scale factor
|
|
1511
1684
|
freqOffset = 16; // Hz - Frequency (uint16)
|
|
@@ -1515,7 +1688,7 @@ export class SunspecModbusClient {
|
|
|
1515
1688
|
energySFOffset = 54; // TotWh_SF - Total Energy scale factor
|
|
1516
1689
|
break;
|
|
1517
1690
|
case 204: // 3-phase meter (Wye)
|
|
1518
|
-
console.
|
|
1691
|
+
console.debug('Using Model 204 (3-Phase Meter Wye) offsets');
|
|
1519
1692
|
powerOffset = 18; // W - Total Real Power (int16)
|
|
1520
1693
|
powerSFOffset = 22; // W_SF - Power scale factor
|
|
1521
1694
|
freqOffset = 16; // Hz - Frequency (uint16)
|
|
@@ -1526,7 +1699,7 @@ export class SunspecModbusClient {
|
|
|
1526
1699
|
break;
|
|
1527
1700
|
case 201: // Single-phase meter
|
|
1528
1701
|
default:
|
|
1529
|
-
console.
|
|
1702
|
+
console.debug('Using Model 201 (Single-Phase Meter) offsets');
|
|
1530
1703
|
powerOffset = 18; // W - Total Real Power (int16)
|
|
1531
1704
|
powerSFOffset = 22; // W_SF - Power scale factor
|
|
1532
1705
|
freqOffset = 16; // Hz - Frequency (uint16)
|
|
@@ -1536,7 +1709,7 @@ export class SunspecModbusClient {
|
|
|
1536
1709
|
energySFOffset = 54; // TotWh_SF - Total Energy scale factor
|
|
1537
1710
|
}
|
|
1538
1711
|
// Read entire model block in a single Modbus call
|
|
1539
|
-
const buffer = await this.readModelBlock(model);
|
|
1712
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1540
1713
|
// Extract scale factors from buffer
|
|
1541
1714
|
const powerSF = this.extractValue(buffer, powerSFOffset, 'int16');
|
|
1542
1715
|
const freqSF = this.extractValue(buffer, freqSFOffset, 'int16');
|
|
@@ -1574,16 +1747,16 @@ export class SunspecModbusClient {
|
|
|
1574
1747
|
/**
|
|
1575
1748
|
* Read common block data (Model 1)
|
|
1576
1749
|
*/
|
|
1577
|
-
async readCommonBlock() {
|
|
1578
|
-
const model = this.findModel(SunspecModelId.Common);
|
|
1750
|
+
async readCommonBlock(unitId) {
|
|
1751
|
+
const model = this.findModel(unitId, SunspecModelId.Common);
|
|
1579
1752
|
if (!model) {
|
|
1580
|
-
console.error(
|
|
1753
|
+
console.error(`Common block model not found on unit ${unitId}`);
|
|
1581
1754
|
return null;
|
|
1582
1755
|
}
|
|
1583
|
-
console.log(`Reading Common Block - Model address: ${model.address}`);
|
|
1756
|
+
console.log(`Reading Common Block - Model address: ${model.address} (unit ${unitId})`);
|
|
1584
1757
|
try {
|
|
1585
1758
|
// Read entire model block in a single Modbus call
|
|
1586
|
-
const buffer = await this.readModelBlock(model);
|
|
1759
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1587
1760
|
// Common block offsets are relative to the model start (after ID and Length header,
|
|
1588
1761
|
// but readModelBlock reads from model.address which includes the data area).
|
|
1589
1762
|
// The offsets below are relative to the data start within the model block.
|
|
@@ -1620,24 +1793,24 @@ export class SunspecModbusClient {
|
|
|
1620
1793
|
/**
|
|
1621
1794
|
* Get serial number from device
|
|
1622
1795
|
*/
|
|
1623
|
-
async getSerialNumber() {
|
|
1624
|
-
const commonData = await this.readCommonBlock();
|
|
1796
|
+
async getSerialNumber(unitId) {
|
|
1797
|
+
const commonData = await this.readCommonBlock(unitId);
|
|
1625
1798
|
return commonData?.serialNumber;
|
|
1626
1799
|
}
|
|
1627
1800
|
/**
|
|
1628
|
-
* Check if connected
|
|
1801
|
+
* Check if a specific unit is connected on this network device
|
|
1629
1802
|
*/
|
|
1630
|
-
isConnected() {
|
|
1631
|
-
return this.
|
|
1803
|
+
isConnected(unitId) {
|
|
1804
|
+
return this.modbusInstances.has(unitId);
|
|
1632
1805
|
}
|
|
1633
1806
|
/**
|
|
1634
|
-
* Check if connection is healthy
|
|
1807
|
+
* Check if a specific unit's connection is healthy
|
|
1635
1808
|
*/
|
|
1636
|
-
isHealthy() {
|
|
1637
|
-
return this.
|
|
1809
|
+
isHealthy(unitId) {
|
|
1810
|
+
return this.modbusInstances.has(unitId) && this.connectionHealth.isHealthy();
|
|
1638
1811
|
}
|
|
1639
1812
|
/**
|
|
1640
|
-
* Get connection health details
|
|
1813
|
+
* Get connection health details (shared across all units on this network device)
|
|
1641
1814
|
*/
|
|
1642
1815
|
getConnectionHealth() {
|
|
1643
1816
|
return this.connectionHealth;
|
|
@@ -1645,15 +1818,15 @@ export class SunspecModbusClient {
|
|
|
1645
1818
|
/**
|
|
1646
1819
|
* Read inverter settings from Model 121 (Inverter Settings)
|
|
1647
1820
|
*/
|
|
1648
|
-
async readInverterSettings() {
|
|
1649
|
-
const model = this.findModel(SunspecModelId.Settings);
|
|
1821
|
+
async readInverterSettings(unitId) {
|
|
1822
|
+
const model = this.findModel(unitId, SunspecModelId.Settings);
|
|
1650
1823
|
if (!model) {
|
|
1651
|
-
console.
|
|
1824
|
+
console.debug(`Settings model 121 not found on unit ${unitId}`);
|
|
1652
1825
|
return null;
|
|
1653
1826
|
}
|
|
1654
1827
|
try {
|
|
1655
1828
|
// Read entire model block in a single Modbus call
|
|
1656
|
-
const buffer = await this.readModelBlock(model);
|
|
1829
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1657
1830
|
// Extract scale factors from buffer (offsets 22-31)
|
|
1658
1831
|
const scaleFactors = {
|
|
1659
1832
|
WMax_SF: this.extractValue(buffer, 22, 'int16'),
|
|
@@ -1737,15 +1910,15 @@ export class SunspecModbusClient {
|
|
|
1737
1910
|
/**
|
|
1738
1911
|
* Read inverter controls from Model 123 (Immediate Inverter Controls)
|
|
1739
1912
|
*/
|
|
1740
|
-
async readInverterControls() {
|
|
1741
|
-
const model = this.findModel(SunspecModelId.Controls);
|
|
1913
|
+
async readInverterControls(unitId) {
|
|
1914
|
+
const model = this.findModel(unitId, SunspecModelId.Controls);
|
|
1742
1915
|
if (!model) {
|
|
1743
|
-
console.log(
|
|
1916
|
+
console.log(`Controls model 123 not found on unit ${unitId}`);
|
|
1744
1917
|
return null;
|
|
1745
1918
|
}
|
|
1746
1919
|
try {
|
|
1747
1920
|
// Read entire model block in a single Modbus call
|
|
1748
|
-
const buffer = await this.readModelBlock(model);
|
|
1921
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1749
1922
|
// Extract scale factors from buffer (offsets 21-23)
|
|
1750
1923
|
const scaleFactors = {
|
|
1751
1924
|
WMaxLimPct_SF: this.extractValue(buffer, 21, 'int16'),
|
|
@@ -1832,27 +2005,28 @@ export class SunspecModbusClient {
|
|
|
1832
2005
|
/**
|
|
1833
2006
|
* Write Block 121 - Inverter Basic Settings
|
|
1834
2007
|
*/
|
|
1835
|
-
async writeInverterSettings(settings) {
|
|
1836
|
-
const model = this.findModel(SunspecModelId.Settings);
|
|
2008
|
+
async writeInverterSettings(unitId, settings) {
|
|
2009
|
+
const model = this.findModel(unitId, SunspecModelId.Settings);
|
|
1837
2010
|
if (!model) {
|
|
1838
|
-
console.error(
|
|
2011
|
+
console.error(`Settings model 121 not found on unit ${unitId}`);
|
|
1839
2012
|
return false;
|
|
1840
2013
|
}
|
|
1841
2014
|
const baseAddr = model.address;
|
|
1842
2015
|
try {
|
|
2016
|
+
const instance = this.getInstance(unitId);
|
|
1843
2017
|
// For each setting, write the value if provided
|
|
1844
2018
|
// Note: This is a simplified implementation. In production, you'd batch writes
|
|
1845
|
-
if (settings.WMax !== undefined
|
|
2019
|
+
if (settings.WMax !== undefined) {
|
|
1846
2020
|
// Need to read scale factor first if not provided
|
|
1847
|
-
const sfBuffer = await
|
|
2021
|
+
const sfBuffer = await instance.readHoldingRegisters(baseAddr + 22, 1);
|
|
1848
2022
|
const scaleFactor = sfBuffer.readInt16BE(0);
|
|
1849
2023
|
const scaledValue = Math.round(settings.WMax / Math.pow(10, scaleFactor));
|
|
1850
2024
|
// Writing registers needs to be implemented in EnergyAppModbusInstance
|
|
1851
2025
|
// For now, log the write operation
|
|
1852
2026
|
console.log(`Would write value ${scaledValue} to register ${baseAddr}`);
|
|
1853
2027
|
}
|
|
1854
|
-
if (settings.VRef !== undefined
|
|
1855
|
-
const sfBuffer = await
|
|
2028
|
+
if (settings.VRef !== undefined) {
|
|
2029
|
+
const sfBuffer = await instance.readHoldingRegisters(baseAddr + 23, 1);
|
|
1856
2030
|
const scaleFactor = sfBuffer.readInt16BE(0);
|
|
1857
2031
|
const scaledValue = Math.round(settings.VRef / Math.pow(10, scaleFactor));
|
|
1858
2032
|
console.log(`Would write value ${scaledValue} to register ${baseAddr + 1}`);
|
|
@@ -1869,41 +2043,41 @@ export class SunspecModbusClient {
|
|
|
1869
2043
|
/**
|
|
1870
2044
|
* Write inverter controls to Model 123 (Immediate Inverter Controls)
|
|
1871
2045
|
*/
|
|
1872
|
-
async writeInverterControls(controls) {
|
|
1873
|
-
const model = this.findModel(SunspecModelId.Controls);
|
|
2046
|
+
async writeInverterControls(unitId, controls) {
|
|
2047
|
+
const model = this.findModel(unitId, SunspecModelId.Controls);
|
|
1874
2048
|
if (!model) {
|
|
1875
|
-
console.error(
|
|
2049
|
+
console.error(`Controls model 123 not found on unit ${unitId}`);
|
|
1876
2050
|
return false;
|
|
1877
2051
|
}
|
|
1878
2052
|
const baseAddr = model.address;
|
|
1879
2053
|
try {
|
|
1880
2054
|
// Connection control (Register 2)
|
|
1881
2055
|
if (controls.Conn !== undefined) {
|
|
1882
|
-
await this.writeRegisterValue(baseAddr + 2, controls.Conn, 'uint16');
|
|
2056
|
+
await this.writeRegisterValue(unitId, baseAddr + 2, controls.Conn, 'uint16');
|
|
1883
2057
|
console.log(`Set connection control to ${controls.Conn}`);
|
|
1884
2058
|
}
|
|
1885
2059
|
// Power limit control (Register 3) - needs scale factor
|
|
1886
2060
|
if (controls.WMaxLimPct !== undefined) {
|
|
1887
|
-
const scaleFactor = await this.readRegisterValue(baseAddr + 21, 1, 'int16');
|
|
2061
|
+
const scaleFactor = await this.readRegisterValue(unitId, baseAddr + 21, 1, 'int16');
|
|
1888
2062
|
const scaledValue = Math.round(controls.WMaxLimPct / Math.pow(10, scaleFactor));
|
|
1889
|
-
await this.writeRegisterValue(baseAddr + 3, scaledValue, 'uint16');
|
|
2063
|
+
await this.writeRegisterValue(unitId, baseAddr + 3, scaledValue, 'uint16');
|
|
1890
2064
|
console.log(`Set power limit to ${controls.WMaxLimPct}% (scaled: ${scaledValue})`);
|
|
1891
2065
|
}
|
|
1892
2066
|
// Throttle enable/disable (Register 7)
|
|
1893
2067
|
if (controls.WMaxLim_Ena !== undefined) {
|
|
1894
|
-
await this.writeRegisterValue(baseAddr + 7, controls.WMaxLim_Ena, 'uint16');
|
|
2068
|
+
await this.writeRegisterValue(unitId, baseAddr + 7, controls.WMaxLim_Ena, 'uint16');
|
|
1895
2069
|
console.log(`Set throttle enable to ${controls.WMaxLim_Ena}`);
|
|
1896
2070
|
}
|
|
1897
2071
|
// Power factor control (Register 8) - needs scale factor
|
|
1898
2072
|
if (controls.OutPFSet !== undefined) {
|
|
1899
|
-
const scaleFactor = await this.readRegisterValue(baseAddr + 22, 1, 'int16');
|
|
2073
|
+
const scaleFactor = await this.readRegisterValue(unitId, baseAddr + 22, 1, 'int16');
|
|
1900
2074
|
const scaledValue = Math.round(controls.OutPFSet / Math.pow(10, scaleFactor));
|
|
1901
|
-
await this.writeRegisterValue(baseAddr + 8, scaledValue, 'int16');
|
|
2075
|
+
await this.writeRegisterValue(unitId, baseAddr + 8, scaledValue, 'int16');
|
|
1902
2076
|
console.log(`Set power factor to ${controls.OutPFSet} (scaled: ${scaledValue})`);
|
|
1903
2077
|
}
|
|
1904
2078
|
// Power factor enable/disable (Register 12)
|
|
1905
2079
|
if (controls.OutPFSet_Ena !== undefined) {
|
|
1906
|
-
await this.writeRegisterValue(baseAddr + 12, controls.OutPFSet_Ena, 'uint16');
|
|
2080
|
+
await this.writeRegisterValue(unitId, baseAddr + 12, controls.OutPFSet_Ena, 'uint16');
|
|
1907
2081
|
console.log(`Set PF enable to ${controls.OutPFSet_Ena}`);
|
|
1908
2082
|
}
|
|
1909
2083
|
console.log('Inverter controls written successfully');
|
|
@@ -1924,21 +2098,21 @@ export class SunspecModbusClient {
|
|
|
1924
2098
|
* @param limitW - Power limit in Watts, or null to remove the limit
|
|
1925
2099
|
* @returns true if successful, false otherwise
|
|
1926
2100
|
*/
|
|
1927
|
-
async setFeedInLimit(limitW) {
|
|
2101
|
+
async setFeedInLimit(unitId, limitW) {
|
|
1928
2102
|
if (limitW === null) {
|
|
1929
2103
|
// Remove limit: disable WMaxLim_Ena
|
|
1930
|
-
console.log(
|
|
1931
|
-
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 });
|
|
1932
2106
|
}
|
|
1933
2107
|
// Read WMax from Model 121 to compute percentage
|
|
1934
|
-
const settings = await this.readInverterSettings();
|
|
2108
|
+
const settings = await this.readInverterSettings(unitId);
|
|
1935
2109
|
if (!settings || !settings.WMax) {
|
|
1936
|
-
console.error(
|
|
2110
|
+
console.error(`Cannot set feed-in limit on unit ${unitId}: unable to read WMax from Model 121`);
|
|
1937
2111
|
return false;
|
|
1938
2112
|
}
|
|
1939
2113
|
const pct = (limitW / settings.WMax) * 100;
|
|
1940
|
-
console.log(`Setting feed-in limit to ${limitW}W (${pct.toFixed(2)}% of WMax ${settings.WMax}W)`);
|
|
1941
|
-
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, {
|
|
1942
2116
|
WMaxLimPct: pct,
|
|
1943
2117
|
WMaxLim_Ena: SunspecEnableControl.ENABLED
|
|
1944
2118
|
});
|