@enyo-energy/sunspec-sdk 0.0.51 → 0.0.54

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.
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SunspecModbusClient = void 0;
4
+ exports.getOrCreateSunspecClient = getOrCreateSunspecClient;
5
+ exports.releaseSunspecClient = releaseSunspecClient;
4
6
  /**
5
7
  * Sunspec Modbus Client Implementation
6
8
  *
@@ -23,21 +25,75 @@ const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
23
25
  const EnergyAppModbusConnectionHealth_js_1 = require("@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusConnectionHealth.js");
24
26
  const EnergyAppModbusFaultTolerantReader_js_1 = require("@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusFaultTolerantReader.js");
25
27
  const EnergyAppModbusDataTypeConverter_js_1 = require("@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusDataTypeConverter.js");
28
+ /**
29
+ * Module-level registry: one SunspecModbusClient per network device (host:port).
30
+ * Refcounted so the underlying connection only tears down when the last consumer releases.
31
+ */
32
+ const CLIENT_REGISTRY = new Map();
33
+ const registryKey = (host, port) => `${host}:${port}`;
34
+ /**
35
+ * Get (or create) the singleton SunspecModbusClient for this network device.
36
+ * Increments the refcount; pair with `releaseSunspecClient` on teardown.
37
+ */
38
+ function getOrCreateSunspecClient(energyApp, host, port = 502) {
39
+ const key = registryKey(host, port);
40
+ let entry = CLIENT_REGISTRY.get(key);
41
+ if (!entry) {
42
+ entry = { client: new SunspecModbusClient(energyApp), refcount: 0 };
43
+ CLIENT_REGISTRY.set(key, entry);
44
+ }
45
+ entry.refcount++;
46
+ return entry.client;
47
+ }
48
+ /**
49
+ * Release a refcount on the network-device client. When the last consumer releases,
50
+ * the client is fully disconnected (all units closed) and removed from the registry.
51
+ *
52
+ * Releasing more times than acquired is a programming error and is logged; the count
53
+ * is clamped at zero so repeat releases are no-ops.
54
+ */
55
+ async function releaseSunspecClient(host, port = 502) {
56
+ const key = registryKey(host, port);
57
+ const entry = CLIENT_REGISTRY.get(key);
58
+ if (!entry)
59
+ return;
60
+ if (entry.refcount <= 0) {
61
+ console.warn(`releaseSunspecClient(${host}:${port}): refcount already 0 — extra release ignored`);
62
+ return;
63
+ }
64
+ entry.refcount--;
65
+ if (entry.refcount === 0) {
66
+ // Disconnect first, THEN remove from registry, so a concurrent getOrCreate
67
+ // doesn't bind a fresh client to the same host:port while sockets are still
68
+ // closing on the old one.
69
+ try {
70
+ await entry.client.disconnect();
71
+ }
72
+ finally {
73
+ // Guard against the very-unlikely case where someone re-acquired during await.
74
+ if (CLIENT_REGISTRY.get(key) === entry && entry.refcount === 0) {
75
+ CLIENT_REGISTRY.delete(key);
76
+ }
77
+ }
78
+ }
79
+ }
26
80
  class SunspecModbusClient {
27
81
  energyApp;
28
- modbusClient = null;
29
- discoveredModels = new Map();
30
- connected = false;
82
+ // Per-unit Modbus instance/reader/discovered-models maps. One client = one network device,
83
+ // many unit IDs share that client (each unit ID is one EnergyAppModbusInstance underneath).
84
+ modbusInstances = new Map();
85
+ faultTolerantReaders = new Map();
86
+ discoveredModelsByUnit = new Map();
31
87
  connectionHealth;
32
- faultTolerantReader = null;
33
88
  modbusDataTypeConverter;
34
89
  connectionParams = null;
90
+ // Per-unit known parameters so reconnect() can replay every unit ID we've seen.
91
+ knownUnits = new Set();
35
92
  autoReconnectEnabled = true;
36
93
  openCount = 0;
37
94
  closeCount = 0;
38
- currentlyOpen = 0;
39
- // Serializes connection-state transitions so concurrent callers cannot open a second
40
- // TCP socket while the first is still alive (some Modbus devices allow only one client).
95
+ // Serializes connection-state transitions so concurrent callers cannot open duplicate
96
+ // sockets for the same unit ID while one is still alive.
41
97
  operationChain = Promise.resolve();
42
98
  constructor(energyApp) {
43
99
  this.energyApp = energyApp;
@@ -45,7 +101,10 @@ class SunspecModbusClient {
45
101
  this.modbusDataTypeConverter = new EnergyAppModbusDataTypeConverter_js_1.EnergyAppModbusDataTypeConverter();
46
102
  }
47
103
  /**
48
- * Connect to Modbus device
104
+ * Connect to a Modbus device unit. Multiple unit IDs on the same network device share this
105
+ * client; calling connect() with different unit IDs adds each as a separately-managed unit.
106
+ * The first call locks in the host/port for this client; later calls for a different host
107
+ * are rejected.
49
108
  * @param host Primary host (hostname) to connect to
50
109
  * @param port Modbus port (default 502)
51
110
  * @param unitId Modbus unit ID (default 1)
@@ -53,45 +112,64 @@ class SunspecModbusClient {
53
112
  */
54
113
  async connect(host, port = 502, unitId = 1, secondaryHost) {
55
114
  return this.withConnectionLock(async () => {
56
- const sameParams = this.connected
57
- && this.connectionParams !== null
58
- && this.connectionParams.primaryHost === host
59
- && this.connectionParams.port === port
60
- && this.connectionParams.unitId === unitId;
61
- if (sameParams) {
62
- console.debug(`connect(): already connected to ${host}:${port} unit ${unitId}, skipping`);
63
- return;
115
+ if (this.connectionParams) {
116
+ if (this.connectionParams.primaryHost !== host || this.connectionParams.port !== port) {
117
+ throw new Error(`SunspecModbusClient is bound to ${this.connectionParams.primaryHost}:${this.connectionParams.port}; ` +
118
+ `cannot reuse it for ${host}:${port}. Use getOrCreateSunspecClient() per (host, port).`);
119
+ }
120
+ if (secondaryHost && !this.connectionParams.secondaryHost) {
121
+ this.connectionParams.secondaryHost = secondaryHost;
122
+ }
64
123
  }
65
- if (this.connected || this.modbusClient) {
66
- await this._closeSocket();
124
+ else {
125
+ this.connectionParams = { primaryHost: host, secondaryHost, port };
67
126
  }
68
- this.connectionParams = { primaryHost: host, secondaryHost, port, unitId };
69
- await this._openSocket(host, port, unitId);
70
- console.log(`Connected to Sunspec device at ${host}:${port} unit ${unitId} (opens=${this.openCount}, closes=${this.closeCount}, current=${this.currentlyOpen})`);
127
+ this.knownUnits.add(unitId);
128
+ if (this.modbusInstances.has(unitId)) {
129
+ console.debug(`connect(): unit ${unitId} on ${host}:${port} already open, skipping`);
130
+ return;
131
+ }
132
+ await this._openUnit(host, port, unitId);
133
+ console.log(`Connected to Sunspec device at ${host}:${port} unit ${unitId} (opens=${this.openCount}, closes=${this.closeCount}, openUnits=${this.modbusInstances.size})`);
71
134
  });
72
135
  }
73
136
  /**
74
- * Disconnect from Modbus device.
137
+ * Disconnect from all units of this network device.
75
138
  *
76
- * Note: connection parameters are preserved so reconnect() can be called afterwards.
77
- * They will be overwritten by the next connect() call anyway.
139
+ * Note: connection parameters and the set of known units are preserved so reconnect() can
140
+ * be called afterwards. They will be overwritten by the next connect() call anyway.
78
141
  */
79
142
  async disconnect() {
80
143
  return this.withConnectionLock(async () => {
81
- if (!this.modbusClient && !this.connected) {
144
+ if (this.modbusInstances.size === 0) {
82
145
  return;
83
146
  }
84
147
  const host = this.connectionParams?.primaryHost ?? 'unknown';
85
148
  const port = this.connectionParams?.port ?? 0;
86
- const unitId = this.connectionParams?.unitId ?? 0;
87
- await this._closeSocket();
88
- console.log(`Disconnected from Sunspec device at ${host}:${port} unit ${unitId} (opens=${this.openCount}, closes=${this.closeCount}, current=${this.currentlyOpen})`);
149
+ const units = [...this.modbusInstances.keys()];
150
+ for (const unitId of units) {
151
+ await this._closeUnit(unitId);
152
+ }
153
+ console.log(`Disconnected from Sunspec device at ${host}:${port} units [${units.join(',')}] (opens=${this.openCount}, closes=${this.closeCount})`);
154
+ });
155
+ }
156
+ /**
157
+ * Disconnect a single unit on this network device. Other units on the same device stay open.
158
+ */
159
+ async disconnectUnit(unitId) {
160
+ return this.withConnectionLock(async () => {
161
+ if (!this.modbusInstances.has(unitId))
162
+ return;
163
+ const host = this.connectionParams?.primaryHost ?? 'unknown';
164
+ const port = this.connectionParams?.port ?? 0;
165
+ await this._closeUnit(unitId);
166
+ console.log(`Disconnected unit ${unitId} from Sunspec device at ${host}:${port} (opens=${this.openCount}, closes=${this.closeCount})`);
89
167
  });
90
168
  }
91
169
  /**
92
- * Reconnect using stored connection parameters
93
- * First tries primaryHost (hostname), then falls back to secondaryHost (ipAddress) if available
94
- * Returns true if reconnection was successful, false otherwise
170
+ * Reconnect every previously-known unit on this network device using stored connection
171
+ * parameters. First tries primaryHost (hostname), then falls back to secondaryHost
172
+ * (ipAddress) if available. Returns true only if every known unit reconnected successfully.
95
173
  */
96
174
  async reconnect() {
97
175
  return this.withConnectionLock(async () => {
@@ -99,18 +177,23 @@ class SunspecModbusClient {
99
177
  console.error('Cannot reconnect: no connection parameters stored. Call connect() first.');
100
178
  return false;
101
179
  }
102
- const { primaryHost, secondaryHost, port, unitId } = this.connectionParams;
103
- console.log(`Attempting to reconnect to primary host ${primaryHost}:${port} unit ${unitId}...`);
104
- const primarySuccess = await this.attemptConnection(primaryHost, port, unitId);
180
+ if (this.knownUnits.size === 0) {
181
+ console.error('Cannot reconnect: no known unit IDs. Call connect(unitId) at least once first.');
182
+ return false;
183
+ }
184
+ const { primaryHost, secondaryHost, port } = this.connectionParams;
185
+ const units = [...this.knownUnits];
186
+ console.log(`Attempting to reconnect to primary host ${primaryHost}:${port} units [${units.join(',')}]...`);
187
+ const primarySuccess = await this.attemptConnection(primaryHost, port, units);
105
188
  if (primarySuccess) {
106
- console.log(`Successfully reconnected to primary host ${primaryHost}:${port} unit ${unitId}`);
189
+ console.log(`Successfully reconnected to primary host ${primaryHost}:${port} units [${units.join(',')}]`);
107
190
  return true;
108
191
  }
109
192
  if (secondaryHost && secondaryHost !== primaryHost) {
110
- console.log(`Primary host failed, attempting secondary host ${secondaryHost}:${port} unit ${unitId}...`);
111
- const secondarySuccess = await this.attemptConnection(secondaryHost, port, unitId);
193
+ console.log(`Primary host failed, attempting secondary host ${secondaryHost}:${port} units [${units.join(',')}]...`);
194
+ const secondarySuccess = await this.attemptConnection(secondaryHost, port, units);
112
195
  if (secondarySuccess) {
113
- console.log(`Successfully reconnected to secondary host ${secondaryHost}:${port} unit ${unitId}`);
196
+ console.log(`Successfully reconnected to secondary host ${secondaryHost}:${port} units [${units.join(',')}]`);
114
197
  return true;
115
198
  }
116
199
  }
@@ -119,16 +202,53 @@ class SunspecModbusClient {
119
202
  });
120
203
  }
121
204
  /**
122
- * Attempt to establish a connection to a specific host. Caller must hold the connection lock.
123
- * Returns true if successful, false otherwise.
205
+ * Reconnect a single previously-known unit on this network device. Other units on the
206
+ * same client stay open. Useful when one device's polling loop detects a dropped
207
+ * connection and only needs to recover its own unit, not thrash siblings.
208
+ *
209
+ * Returns true if the unit reconnected successfully (on primary or secondary host).
210
+ */
211
+ async reconnectUnit(unitId) {
212
+ return this.withConnectionLock(async () => {
213
+ if (!this.connectionParams) {
214
+ console.error(`Cannot reconnectUnit(${unitId}): no connection parameters stored. Call connect() first.`);
215
+ return false;
216
+ }
217
+ const { primaryHost, secondaryHost, port } = this.connectionParams;
218
+ console.log(`Attempting to reconnect unit ${unitId} on primary host ${primaryHost}:${port}...`);
219
+ const primarySuccess = await this.attemptConnection(primaryHost, port, [unitId]);
220
+ if (primarySuccess) {
221
+ console.log(`Successfully reconnected unit ${unitId} on primary host ${primaryHost}:${port}`);
222
+ return true;
223
+ }
224
+ if (secondaryHost && secondaryHost !== primaryHost) {
225
+ console.log(`Primary host failed for unit ${unitId}, attempting secondary host ${secondaryHost}:${port}...`);
226
+ const secondarySuccess = await this.attemptConnection(secondaryHost, port, [unitId]);
227
+ if (secondarySuccess) {
228
+ console.log(`Successfully reconnected unit ${unitId} on secondary host ${secondaryHost}:${port}`);
229
+ return true;
230
+ }
231
+ }
232
+ console.error(`Reconnection failed for unit ${unitId} on all available hosts`);
233
+ return false;
234
+ });
235
+ }
236
+ /**
237
+ * Attempt to (re)open every requested unit on a specific host. Caller must hold the
238
+ * connection lock. Closes any pre-existing per-unit instances first. Returns true only
239
+ * if every unit succeeded.
124
240
  */
125
- async attemptConnection(host, port, unitId) {
126
- if (this.connected || this.modbusClient) {
127
- await this._closeSocket();
241
+ async attemptConnection(host, port, units) {
242
+ for (const unitId of units) {
243
+ if (this.modbusInstances.has(unitId)) {
244
+ await this._closeUnit(unitId);
245
+ }
128
246
  }
129
247
  try {
130
- await this._openSocket(host, port, unitId);
131
- console.log(`Connection attempt to ${host}:${port} unit ${unitId} succeeded (opens=${this.openCount}, closes=${this.closeCount}, current=${this.currentlyOpen})`);
248
+ for (const unitId of units) {
249
+ await this._openUnit(host, port, unitId);
250
+ }
251
+ console.log(`Connection attempt to ${host}:${port} units [${units.join(',')}] succeeded (opens=${this.openCount}, closes=${this.closeCount}, openUnits=${this.modbusInstances.size})`);
132
252
  return true;
133
253
  }
134
254
  catch (error) {
@@ -137,11 +257,11 @@ class SunspecModbusClient {
137
257
  }
138
258
  }
139
259
  /**
140
- * Open a new Modbus socket and wire up the fault-tolerant reader. On any failure, the
141
- * just-opened socket (if any) is closed so we never leak. Caller must hold the connection
142
- * lock and ensure no prior socket is open (use _closeSocket first).
260
+ * Open a new Modbus instance for one unit ID and wire up its fault-tolerant reader.
261
+ * On any failure, the just-opened instance (if any) is closed so we never leak. Caller must
262
+ * hold the connection lock and ensure the unit is not already open.
143
263
  */
144
- async _openSocket(host, port, unitId) {
264
+ async _openUnit(host, port, unitId) {
145
265
  let candidate = null;
146
266
  try {
147
267
  candidate = await this.energyApp.useModbus().connect({
@@ -153,9 +273,9 @@ class SunspecModbusClient {
153
273
  if (!candidate) {
154
274
  throw new Error(`useModbus().connect returned null for ${host}:${port} unit ${unitId}`);
155
275
  }
156
- this.faultTolerantReader = new EnergyAppModbusFaultTolerantReader_js_1.EnergyAppModbusFaultTolerantReader(candidate, this.connectionHealth);
157
- this.modbusClient = candidate;
158
- this.connected = true;
276
+ const reader = new EnergyAppModbusFaultTolerantReader_js_1.EnergyAppModbusFaultTolerantReader(candidate, this.connectionHealth);
277
+ this.modbusInstances.set(unitId, candidate);
278
+ this.faultTolerantReaders.set(unitId, reader);
159
279
  this.connectionHealth.recordSuccess();
160
280
  this.recordOpen();
161
281
  }
@@ -166,31 +286,28 @@ class SunspecModbusClient {
166
286
  }
167
287
  catch { /* ignore */ }
168
288
  }
169
- this.modbusClient = null;
170
- this.faultTolerantReader = null;
171
- this.connected = false;
289
+ this.modbusInstances.delete(unitId);
290
+ this.faultTolerantReaders.delete(unitId);
172
291
  throw err;
173
292
  }
174
293
  }
175
294
  /**
176
- * Close the current Modbus socket if any. Idempotent. Caller must hold the connection lock.
295
+ * Close the Modbus instance for a single unit. Idempotent. Caller must hold the
296
+ * connection lock.
177
297
  */
178
- async _closeSocket() {
179
- if (!this.modbusClient && !this.connected)
180
- return;
181
- const client = this.modbusClient;
182
- const wasConnected = this.connected;
183
- this.modbusClient = null;
184
- this.faultTolerantReader = null;
185
- this.connected = false;
186
- this.discoveredModels.clear();
187
- if (client) {
298
+ async _closeUnit(unitId) {
299
+ const instance = this.modbusInstances.get(unitId);
300
+ const wasOpen = instance !== undefined;
301
+ this.modbusInstances.delete(unitId);
302
+ this.faultTolerantReaders.delete(unitId);
303
+ this.discoveredModelsByUnit.delete(unitId);
304
+ if (instance) {
188
305
  try {
189
- await client.disconnect();
306
+ await instance.disconnect();
190
307
  }
191
308
  catch { /* ignore */ }
192
309
  }
193
- if (wasConnected)
310
+ if (wasOpen)
194
311
  this.recordClose();
195
312
  }
196
313
  /**
@@ -204,27 +321,55 @@ class SunspecModbusClient {
204
321
  }
205
322
  recordOpen() {
206
323
  this.openCount++;
207
- this.currentlyOpen++;
208
- if (this.currentlyOpen > 1) {
209
- console.error(`SunspecModbusClient: currentlyOpen=${this.currentlyOpen} after open ` +
210
- `(opens=${this.openCount}, closes=${this.closeCount}). ` +
324
+ const expected = this.modbusInstances.size;
325
+ const delta = this.openCount - this.closeCount;
326
+ if (delta > expected) {
327
+ console.error(`SunspecModbusClient: open/close imbalance — ` +
328
+ `opens=${this.openCount}, closes=${this.closeCount}, instances=${expected}. ` +
211
329
  `This indicates a code path bypassing the connection lock — please investigate.`);
212
330
  }
213
331
  }
214
332
  recordClose() {
215
333
  this.closeCount++;
216
- if (this.currentlyOpen > 0) {
217
- this.currentlyOpen--;
218
- }
219
- else {
220
- console.warn(`SunspecModbusClient: closeCount incremented while currentlyOpen was 0 — possible double-close`);
334
+ if (this.closeCount > this.openCount) {
335
+ console.warn(`SunspecModbusClient: closeCount=${this.closeCount} exceeded openCount=${this.openCount} — possible double-close`);
221
336
  }
222
337
  }
223
338
  /**
224
- * Get cumulative open/close counts for this client. Useful for spotting connection leaks.
339
+ * Get cumulative open/close counts for this client (across all unit IDs). Useful for
340
+ * spotting connection leaks.
225
341
  */
226
342
  getConnectionStats() {
227
- return { opens: this.openCount, closes: this.closeCount, currentlyOpen: this.currentlyOpen };
343
+ return { opens: this.openCount, closes: this.closeCount, openUnits: this.modbusInstances.size };
344
+ }
345
+ /**
346
+ * Get the EnergyAppModbusInstance for a unit, throwing if it isn't open.
347
+ */
348
+ getInstance(unitId) {
349
+ const inst = this.modbusInstances.get(unitId);
350
+ if (!inst)
351
+ throw new Error(`Modbus instance for unit ${unitId} not initialized — call connect(host, port, ${unitId}) first`);
352
+ return inst;
353
+ }
354
+ /**
355
+ * Get the fault-tolerant reader for a unit, throwing if it isn't open.
356
+ */
357
+ getReader(unitId) {
358
+ const reader = this.faultTolerantReaders.get(unitId);
359
+ if (!reader)
360
+ throw new Error(`Fault-tolerant reader for unit ${unitId} not initialized — call connect(host, port, ${unitId}) first`);
361
+ return reader;
362
+ }
363
+ /**
364
+ * Get (or create) the discovered-models map for a unit.
365
+ */
366
+ getModelsMap(unitId) {
367
+ let m = this.discoveredModelsByUnit.get(unitId);
368
+ if (!m) {
369
+ m = new Map();
370
+ this.discoveredModelsByUnit.set(unitId, m);
371
+ }
372
+ return m;
228
373
  }
229
374
  /**
230
375
  * Enable or disable automatic reconnection
@@ -242,16 +387,14 @@ class SunspecModbusClient {
242
387
  /**
243
388
  * Detect the base address and addressing mode (0-based or 1-based) for SunSpec
244
389
  */
245
- async detectSunspecBaseAddress(customBaseAddress) {
246
- if (!this.modbusClient) {
247
- throw new Error('Modbus client not initialized');
248
- }
390
+ async detectSunspecBaseAddress(unitId, customBaseAddress) {
391
+ const instance = this.getInstance(unitId);
249
392
  // If custom base address provided, try it first (1-based, then 0-based variant)
250
393
  if (customBaseAddress !== undefined) {
251
394
  console.log(`Detect models for custom base address '${customBaseAddress}' ...`);
252
395
  // Try 0-based at custom address
253
396
  try {
254
- const sunspecId = await this.modbusClient.readRegisterStringValue(customBaseAddress, 2);
397
+ const sunspecId = await instance.readRegisterStringValue(customBaseAddress, 2);
255
398
  if (sunspecId.includes('SunS')) {
256
399
  console.log(`Detected 0-based addressing mode (base address: ${customBaseAddress})`);
257
400
  return {
@@ -266,7 +409,7 @@ class SunspecModbusClient {
266
409
  }
267
410
  // Try 1-based at custom address (customBaseAddress + 1)
268
411
  try {
269
- const sunspecId = await this.modbusClient.readRegisterStringValue(customBaseAddress + 1, 2);
412
+ const sunspecId = await instance.readRegisterStringValue(customBaseAddress + 1, 2);
270
413
  if (sunspecId.includes('SunS')) {
271
414
  console.log(`Detected 1-based addressing mode (base address: ${customBaseAddress + 1})`);
272
415
  return {
@@ -283,7 +426,7 @@ class SunspecModbusClient {
283
426
  else {
284
427
  // Try 1-based addressing first (most common)
285
428
  try {
286
- const sunspecId = await this.modbusClient.readRegisterStringValue(40001, 2);
429
+ const sunspecId = await instance.readRegisterStringValue(40001, 2);
287
430
  if (sunspecId.includes('SunS')) {
288
431
  console.log('Detected 1-based addressing mode (base address: 40001)');
289
432
  return {
@@ -298,7 +441,7 @@ class SunspecModbusClient {
298
441
  }
299
442
  // Try 0-based addressing
300
443
  try {
301
- const sunspecId = await this.modbusClient.readRegisterStringValue(40000, 2);
444
+ const sunspecId = await instance.readRegisterStringValue(40000, 2);
302
445
  if (sunspecId.includes('SunS')) {
303
446
  console.log('Detected 0-based addressing mode (base address: 40000)');
304
447
  return {
@@ -318,27 +461,22 @@ class SunspecModbusClient {
318
461
  throw new Error(`Device is not SunSpec compliant - "SunS" identifier not found at addresses ${addressesChecked}`);
319
462
  }
320
463
  /**
321
- * Discover all available Sunspec models
464
+ * Discover all available Sunspec models for a unit
322
465
  * Automatically detects base address (40000 or 40001) and scans from there
323
466
  */
324
- async discoverModels(customBaseAddress) {
325
- if (!this.connected) {
326
- throw new Error('Not connected to Modbus device');
327
- }
328
- this.discoveredModels.clear();
467
+ async discoverModels(unitId, customBaseAddress) {
468
+ const instance = this.getInstance(unitId);
469
+ const models = this.getModelsMap(unitId);
470
+ models.clear();
329
471
  const maxAddress = 50000; // Safety limit
330
472
  let currentAddress = 0;
331
- console.log('Starting Sunspec model discovery...');
473
+ console.log(`Starting Sunspec model discovery for unit ${unitId}...`);
332
474
  try {
333
475
  // Detect the base address and addressing mode
334
- const addressInfo = await this.detectSunspecBaseAddress(customBaseAddress);
476
+ const addressInfo = await this.detectSunspecBaseAddress(unitId, customBaseAddress);
335
477
  currentAddress = addressInfo.nextAddress;
336
478
  while (currentAddress < maxAddress) {
337
- // Read model ID and length
338
- if (!this.modbusClient) {
339
- throw new Error('Modbus client not initialized');
340
- }
341
- const buffer = await this.modbusClient.readHoldingRegisters(currentAddress, 2);
479
+ const buffer = await instance.readHoldingRegisters(currentAddress, 2);
342
480
  const modelData = [buffer.readUInt16BE(0), buffer.readUInt16BE(2)];
343
481
  if (!modelData || modelData.length < 2) {
344
482
  console.log(`No data at address ${currentAddress}, ending discovery`);
@@ -357,23 +495,23 @@ class SunspecModbusClient {
357
495
  address: currentAddress,
358
496
  length: modelLength
359
497
  };
360
- this.discoveredModels.set(modelId, model);
361
- console.log(`Discovered Model ${modelId} at address ${currentAddress} with length ${modelLength}`);
498
+ models.set(modelId, model);
499
+ console.log(`Discovered Model ${modelId} at address ${currentAddress} with length ${modelLength} (unit ${unitId})`);
362
500
  // Jump to next model: current address + 2 (header) + model length
363
501
  currentAddress = currentAddress + 2 + modelLength;
364
502
  }
365
503
  }
366
504
  catch (error) {
367
- console.error(`Error during model discovery at address ${currentAddress}: ${error}`);
505
+ console.error(`Error during model discovery at address ${currentAddress} (unit ${unitId}): ${error}`);
368
506
  }
369
- console.log(`Discovery complete. Found ${this.discoveredModels.size} models`);
370
- return this.discoveredModels;
507
+ console.log(`Discovery complete for unit ${unitId}. Found ${models.size} models: [${[...models.keys()].sort((a, b) => a - b).join(', ')}]`);
508
+ return models;
371
509
  }
372
510
  /**
373
- * Find a specific model by ID
511
+ * Find a specific model by ID for a given unit
374
512
  */
375
- findModel(modelId) {
376
- return this.discoveredModels.get(modelId);
513
+ findModel(unitId, modelId) {
514
+ return this.discoveredModelsByUnit.get(unitId)?.get(modelId);
377
515
  }
378
516
  /**
379
517
  * Check if a value is "unimplemented" according to Sunspec specification
@@ -449,17 +587,15 @@ class SunspecModbusClient {
449
587
  * Read an entire model's register block in a single Modbus call.
450
588
  * Returns a Buffer containing all registers for the model.
451
589
  */
452
- async readModelBlock(model) {
453
- if (!this.faultTolerantReader) {
454
- throw new Error('Fault-tolerant reader not initialized');
455
- }
590
+ async readModelBlock(unitId, model) {
591
+ const reader = this.getReader(unitId);
456
592
  // Read model.length + 2 registers: the 2-register header (ID + length) plus all data registers.
457
593
  // This way buffer offsets match the convention used throughout: offset 0 = model ID,
458
594
  // offset 1 = model length, offset 2 = first data register, etc.
459
595
  const totalRegisters = model.length + 2;
460
- const result = await this.faultTolerantReader.readHoldingRegisters(model.address, totalRegisters);
596
+ const result = await reader.readHoldingRegisters(model.address, totalRegisters);
461
597
  if (!result.success || !result.value) {
462
- throw new Error(`Failed to read model block ${model.id} at address ${model.address}: ${result.error?.message || 'Unknown error'}`);
598
+ throw new Error(`Failed to read model block ${model.id} at address ${model.address} (unit ${unitId}): ${result.error?.message || 'Unknown error'}`);
463
599
  }
464
600
  this.connectionHealth.recordSuccess();
465
601
  return result.value;
@@ -484,12 +620,10 @@ class SunspecModbusClient {
484
620
  /**
485
621
  * Helper to read register value(s) using the fault-tolerant reader with data type conversion
486
622
  */
487
- async readRegisterValue(address, quantity = 1, dataType) {
488
- if (!this.faultTolerantReader) {
489
- throw new Error('Fault-tolerant reader not initialized');
490
- }
623
+ async readRegisterValue(unitId, address, quantity = 1, dataType) {
624
+ const reader = this.getReader(unitId);
491
625
  try {
492
- const result = await this.faultTolerantReader.readHoldingRegisters(address, quantity);
626
+ const result = await reader.readHoldingRegisters(address, quantity);
493
627
  // Check if the read was successful
494
628
  if (!result.success || !result.value) {
495
629
  throw new Error(`Failed to read register at address ${address}: ${result.error?.message || 'Unknown error'}`);
@@ -510,10 +644,8 @@ class SunspecModbusClient {
510
644
  /**
511
645
  * Helper to write register value(s)
512
646
  */
513
- async writeRegisterValue(address, value, dataType = 'uint16') {
514
- if (!this.modbusClient) {
515
- throw new Error('Modbus client not initialized');
516
- }
647
+ async writeRegisterValue(unitId, address, value, dataType = 'uint16') {
648
+ const instance = this.getInstance(unitId);
517
649
  try {
518
650
  // Convert value to array of register values
519
651
  let registerValues;
@@ -542,9 +674,9 @@ class SunspecModbusClient {
542
674
  }
543
675
  }
544
676
  // Write to holding registers
545
- await this.modbusClient.writeMultipleRegisters(address, registerValues);
677
+ await instance.writeMultipleRegisters(address, registerValues);
546
678
  this.connectionHealth.recordSuccess();
547
- console.log(`Successfully wrote value ${value} to register ${address}`);
679
+ console.log(`Successfully wrote value ${value} to register ${address} (unit ${unitId})`);
548
680
  return true;
549
681
  }
550
682
  catch (error) {
@@ -554,28 +686,36 @@ class SunspecModbusClient {
554
686
  }
555
687
  }
556
688
  /**
557
- * Read inverter data from Model 101 (Single Phase) / Model 103 (Three Phase)
689
+ * Read inverter data. Detects which SunSpec inverter model the device exposes
690
+ * int+SF (101/103) or float (111/112/113) — by checking the discovered model directory,
691
+ * and dispatches to the appropriate decoder.
558
692
  */
559
- async readInverterData() {
560
- const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Inverter3Phase);
561
- if (!model) {
562
- console.debug('Inverter model 103 not found, trying single phase model 101');
563
- const singlePhaseModel = this.findModel(sunspec_interfaces_js_1.SunspecModelId.InverterSinglePhase);
564
- if (!singlePhaseModel) {
565
- console.error('No inverter model found');
566
- return null;
693
+ async readInverterData(unitId) {
694
+ const tryOrder = [
695
+ { id: 103, reader: m => this.readThreePhaseInverterData_IntSF(unitId, m) },
696
+ { id: 113, reader: m => this.readFloatInverterData(unitId, m, 113) },
697
+ { id: 112, reader: m => this.readFloatInverterData(unitId, m, 112) },
698
+ { id: 101, reader: m => this.readSinglePhaseInverterData(unitId, m) },
699
+ { id: 111, reader: m => this.readFloatInverterData(unitId, m, 111) },
700
+ ];
701
+ for (const { id, reader } of tryOrder) {
702
+ const model = this.findModel(unitId, id);
703
+ if (model) {
704
+ console.debug(`Using inverter Model ${id} at address ${model.address} (length ${model.length}) on unit ${unitId}`);
705
+ return reader(model);
567
706
  }
568
- console.warn('IMPORTANT: Working with single-phase inverter, but 3-phase is expected!');
569
- return this.readSinglePhaseInverterData(singlePhaseModel);
570
707
  }
571
- console.debug(`Found 3-phase inverter model 103 at address ${model.address} with length ${model.length}`);
708
+ console.debug(`No inverter model (101/103/111/112/113) on unit ${unitId}`);
709
+ return null;
710
+ }
711
+ /**
712
+ * Read three-phase inverter data from Model 103 (int + scale factor encoding).
713
+ */
714
+ async readThreePhaseInverterData_IntSF(unitId, model) {
572
715
  try {
573
- // Read entire model block in a single Modbus call
574
- console.debug(`Reading Inverter Data from Model ${model.id} at base address: ${model.address}`);
575
- const buffer = await this.readModelBlock(model);
576
- // Extract all scale factors from buffer
716
+ console.debug(`Reading Inverter Data from Model 103 at base address: ${model.address} (unit ${unitId})`);
717
+ const buffer = await this.readModelBlock(unitId, model);
577
718
  const scaleFactors = this.extractInverterScaleFactors(buffer);
578
- // Extract raw values from buffer
579
719
  const acCurrentRaw = this.extractValue(buffer, 2, 'uint16');
580
720
  const voltageANRaw = this.extractValue(buffer, 10, 'uint16');
581
721
  const voltageBNRaw = this.extractValue(buffer, 11, 'uint16');
@@ -627,7 +767,6 @@ class SunspecModbusClient {
627
767
  vendorEvents3: this.extractValue(buffer, 48, 'uint32', 2),
628
768
  vendorEvents4: this.extractValue(buffer, 50, 'uint32', 2)
629
769
  };
630
- // Log non-scaled fields
631
770
  this.logRegisterRead(103, 38, 'Operating State', data.operatingState, 'enum16');
632
771
  this.logRegisterRead(103, 39, 'Vendor State', data.vendorState, 'uint16');
633
772
  this.logRegisterRead(103, 40, 'Events', data.events, 'bitfield32');
@@ -636,7 +775,6 @@ class SunspecModbusClient {
636
775
  this.logRegisterRead(103, 46, 'Vendor Events 2', data.vendorEvents2, 'bitfield32');
637
776
  this.logRegisterRead(103, 48, 'Vendor Events 3', data.vendorEvents3, 'bitfield32');
638
777
  this.logRegisterRead(103, 50, 'Vendor Events 4', data.vendorEvents4, 'bitfield32');
639
- // Read AC Energy (32-bit accumulator) - Offset 24-25
640
778
  const acEnergy = this.extractValue(buffer, 24, 'uint32', 2);
641
779
  this.logRegisterRead(103, 24, 'AC Energy', acEnergy, 'acc32');
642
780
  data.acEnergy = !this.isUnimplementedValue(acEnergy, 'acc32')
@@ -653,49 +791,49 @@ class SunspecModbusClient {
653
791
  /**
654
792
  * Read single phase inverter data (Model 101)
655
793
  */
656
- async readSinglePhaseInverterData(model) {
794
+ async readSinglePhaseInverterData(unitId, model) {
657
795
  try {
658
- console.debug(`Reading Single-Phase Inverter Data from Model 101 at base address: ${model.address}`);
796
+ console.debug(`Reading Single-Phase Inverter Data from Model 101 at base address: ${model.address} (unit ${unitId})`);
659
797
  // Read entire model block in a single Modbus call
660
- const buffer = await this.readModelBlock(model);
661
- // Extract scale factors from buffer
798
+ const buffer = await this.readModelBlock(unitId, model);
799
+ // Extract scale factors from buffer (offsets aligned with Model 103 reader, which works on real
800
+ // hardware; differs from the published SunSpec Model 101 spec for DC fields).
662
801
  const scaleFactors = {
663
802
  A_SF: this.extractValue(buffer, 6, 'int16'),
664
803
  V_SF: this.extractValue(buffer, 13, 'int16'),
665
- W_SF: this.extractValue(buffer, 10, 'int16'),
666
- Hz_SF: this.extractValue(buffer, 12, 'int16'),
804
+ W_SF: this.extractValue(buffer, 15, 'int16'),
805
+ Hz_SF: this.extractValue(buffer, 17, 'int16'),
667
806
  WH_SF: this.extractValue(buffer, 26, 'int16'),
668
- DCA_SF: this.extractValue(buffer, 18, 'int16'),
669
- DCV_SF: this.extractValue(buffer, 19, 'int16'),
670
- DCW_SF: this.extractValue(buffer, 21, 'int16')
807
+ DCA_SF: this.extractValue(buffer, 28, 'int16'),
808
+ DCV_SF: this.extractValue(buffer, 29, 'int16'),
809
+ DCW_SF: this.extractValue(buffer, 31, 'int16')
671
810
  };
672
811
  this.logRegisterRead(101, 6, 'A_SF', scaleFactors.A_SF, 'int16');
673
812
  this.logRegisterRead(101, 13, 'V_SF', scaleFactors.V_SF, 'int16');
674
- this.logRegisterRead(101, 10, 'W_SF', scaleFactors.W_SF, 'int16');
675
- this.logRegisterRead(101, 12, 'Hz_SF', scaleFactors.Hz_SF, 'int16');
813
+ this.logRegisterRead(101, 15, 'W_SF', scaleFactors.W_SF, 'int16');
814
+ this.logRegisterRead(101, 17, 'Hz_SF', scaleFactors.Hz_SF, 'int16');
676
815
  this.logRegisterRead(101, 26, 'WH_SF', scaleFactors.WH_SF, 'int16');
677
- this.logRegisterRead(101, 18, 'DCA_SF', scaleFactors.DCA_SF, 'int16');
678
- this.logRegisterRead(101, 19, 'DCV_SF', scaleFactors.DCV_SF, 'int16');
679
- this.logRegisterRead(101, 21, 'DCW_SF', scaleFactors.DCW_SF, 'int16');
680
- // Extract raw values from buffer
816
+ this.logRegisterRead(101, 28, 'DCA_SF', scaleFactors.DCA_SF, 'int16');
817
+ this.logRegisterRead(101, 29, 'DCV_SF', scaleFactors.DCV_SF, 'int16');
818
+ this.logRegisterRead(101, 31, 'DCW_SF', scaleFactors.DCW_SF, 'int16');
681
819
  const acCurrentRaw = this.extractValue(buffer, 2, 'uint16');
682
- const voltageRaw = this.extractValue(buffer, 7, 'uint16');
683
- const acPowerRaw = this.extractValue(buffer, 9, 'int16');
684
- const freqRaw = this.extractValue(buffer, 11, 'uint16');
685
- const dcCurrentRaw = this.extractValue(buffer, 14, 'uint16');
686
- const dcVoltageRaw = this.extractValue(buffer, 15, 'uint16');
687
- const dcPowerRaw = this.extractValue(buffer, 20, 'int16');
688
- const stateRaw = this.extractValue(buffer, 24, 'uint16');
689
- this.logRegisterRead(101, 24, 'Operating State', stateRaw, 'enum16');
820
+ const voltageRaw = this.extractValue(buffer, 10, 'uint16');
821
+ const acPowerRaw = this.extractValue(buffer, 14, 'int16');
822
+ const freqRaw = this.extractValue(buffer, 16, 'uint16');
823
+ const dcCurrentRaw = this.extractValue(buffer, 27, 'uint16');
824
+ const dcVoltageRaw = this.extractValue(buffer, 28, 'uint16');
825
+ const dcPowerRaw = this.extractValue(buffer, 30, 'int16');
826
+ const stateRaw = this.extractValue(buffer, 38, 'uint16');
827
+ this.logRegisterRead(101, 38, 'Operating State', stateRaw, 'enum16');
690
828
  const data = {
691
829
  blockNumber: 101,
692
- voltageAN: this.applyScaleFactor(voltageRaw, scaleFactors.V_SF, 'uint16', 'Voltage AN', 7, 101),
830
+ voltageAN: this.applyScaleFactor(voltageRaw, scaleFactors.V_SF, 'uint16', 'Voltage AN', 10, 101),
693
831
  acCurrent: this.applyScaleFactor(acCurrentRaw, scaleFactors.A_SF, 'uint16', 'AC Current', 2, 101),
694
- acPower: this.applyScaleFactor(acPowerRaw, scaleFactors.W_SF, 'int16', 'AC Power', 9, 101),
695
- frequency: this.applyScaleFactor(freqRaw, scaleFactors.Hz_SF, 'uint16', 'Frequency', 11, 101),
696
- dcCurrent: this.applyScaleFactor(dcCurrentRaw, scaleFactors.DCA_SF, 'uint16', 'DC Current', 14, 101),
697
- dcVoltage: this.applyScaleFactor(dcVoltageRaw, scaleFactors.DCV_SF, 'uint16', 'DC Voltage', 15, 101),
698
- dcPower: this.applyScaleFactor(dcPowerRaw, scaleFactors.DCW_SF, 'int16', 'DC Power', 20, 101),
832
+ acPower: this.applyScaleFactor(acPowerRaw, scaleFactors.W_SF, 'int16', 'AC Power', 14, 101),
833
+ frequency: this.applyScaleFactor(freqRaw, scaleFactors.Hz_SF, 'uint16', 'Frequency', 16, 101),
834
+ dcCurrent: this.applyScaleFactor(dcCurrentRaw, scaleFactors.DCA_SF, 'uint16', 'DC Current', 27, 101),
835
+ dcVoltage: this.applyScaleFactor(dcVoltageRaw, scaleFactors.DCV_SF, 'uint16', 'DC Voltage', 28, 101),
836
+ dcPower: this.applyScaleFactor(dcPowerRaw, scaleFactors.DCW_SF, 'int16', 'DC Power', 30, 101),
699
837
  operatingState: stateRaw
700
838
  };
701
839
  // Read AC Energy (32-bit accumulator) - Offset 24-25
@@ -712,6 +850,70 @@ class SunspecModbusClient {
712
850
  return null;
713
851
  }
714
852
  }
853
+ /**
854
+ * Read inverter data from a float-variant model: 111 (single-phase), 112 (split-phase), or 113 (three-phase).
855
+ *
856
+ * All three models share the same SunSpec register layout — measurement fields are 32-bit IEEE 754 floats
857
+ * (no scale factors), status registers stay enum16, event registers stay bitfield32. Phase scope differs
858
+ * by model: 111 populates phase A only; 112 phases A+B; 113 all three. Unpopulated phase fields read NaN
859
+ * and are returned as undefined.
860
+ */
861
+ async readFloatInverterData(unitId, model, blockNumber) {
862
+ try {
863
+ console.debug(`Reading Float Inverter Data from Model ${blockNumber} at base address: ${model.address} (unit ${unitId})`);
864
+ const buffer = await this.readModelBlock(unitId, model);
865
+ const data = {
866
+ blockNumber,
867
+ blockAddress: model.address,
868
+ blockLength: model.length,
869
+ acCurrent: this.extractFloat32OrUndefined(buffer, 2, blockNumber, 'AC Current'),
870
+ phaseACurrent: this.extractFloat32OrUndefined(buffer, 4, blockNumber, 'Phase A Current'),
871
+ phaseBCurrent: this.extractFloat32OrUndefined(buffer, 6, blockNumber, 'Phase B Current'),
872
+ phaseCCurrent: this.extractFloat32OrUndefined(buffer, 8, blockNumber, 'Phase C Current'),
873
+ voltageAB: this.extractFloat32OrUndefined(buffer, 10, blockNumber, 'Voltage AB'),
874
+ voltageBC: this.extractFloat32OrUndefined(buffer, 12, blockNumber, 'Voltage BC'),
875
+ voltageCA: this.extractFloat32OrUndefined(buffer, 14, blockNumber, 'Voltage CA'),
876
+ voltageAN: this.extractFloat32OrUndefined(buffer, 16, blockNumber, 'Voltage AN'),
877
+ voltageBN: this.extractFloat32OrUndefined(buffer, 18, blockNumber, 'Voltage BN'),
878
+ voltageCN: this.extractFloat32OrUndefined(buffer, 20, blockNumber, 'Voltage CN'),
879
+ acPower: this.extractFloat32OrUndefined(buffer, 22, blockNumber, 'AC Power'),
880
+ frequency: this.extractFloat32OrUndefined(buffer, 24, blockNumber, 'Frequency'),
881
+ apparentPower: this.extractFloat32OrUndefined(buffer, 26, blockNumber, 'Apparent Power'),
882
+ reactivePower: this.extractFloat32OrUndefined(buffer, 28, blockNumber, 'Reactive Power'),
883
+ powerFactor: this.extractFloat32OrUndefined(buffer, 30, blockNumber, 'Power Factor'),
884
+ acEnergy: this.extractFloat32OrUndefined(buffer, 32, blockNumber, 'AC Energy'),
885
+ dcCurrent: this.extractFloat32OrUndefined(buffer, 34, blockNumber, 'DC Current'),
886
+ dcVoltage: this.extractFloat32OrUndefined(buffer, 36, blockNumber, 'DC Voltage'),
887
+ dcPower: this.extractFloat32OrUndefined(buffer, 38, blockNumber, 'DC Power'),
888
+ cabinetTemperature: this.extractFloat32OrUndefined(buffer, 40, blockNumber, 'Cabinet Temperature'),
889
+ heatSinkTemperature: this.extractFloat32OrUndefined(buffer, 42, blockNumber, 'Heat Sink Temperature'),
890
+ transformerTemperature: this.extractFloat32OrUndefined(buffer, 44, blockNumber, 'Transformer Temperature'),
891
+ otherTemperature: this.extractFloat32OrUndefined(buffer, 46, blockNumber, 'Other Temperature'),
892
+ operatingState: this.extractValue(buffer, 48, 'uint16'),
893
+ vendorState: this.extractValue(buffer, 49, 'uint16'),
894
+ events: this.extractValue(buffer, 50, 'uint32', 2),
895
+ events2: this.extractValue(buffer, 52, 'uint32', 2),
896
+ vendorEvents1: this.extractValue(buffer, 54, 'uint32', 2),
897
+ vendorEvents2: this.extractValue(buffer, 56, 'uint32', 2),
898
+ vendorEvents3: this.extractValue(buffer, 58, 'uint32', 2),
899
+ vendorEvents4: this.extractValue(buffer, 60, 'uint32', 2),
900
+ };
901
+ this.logRegisterRead(blockNumber, 48, 'Operating State', data.operatingState, 'enum16');
902
+ this.logRegisterRead(blockNumber, 49, 'Vendor State', data.vendorState, 'enum16');
903
+ this.logRegisterRead(blockNumber, 50, 'Events', data.events, 'bitfield32');
904
+ this.logRegisterRead(blockNumber, 52, 'Events2', data.events2, 'bitfield32');
905
+ this.logRegisterRead(blockNumber, 54, 'Vendor Events 1', data.vendorEvents1, 'bitfield32');
906
+ this.logRegisterRead(blockNumber, 56, 'Vendor Events 2', data.vendorEvents2, 'bitfield32');
907
+ this.logRegisterRead(blockNumber, 58, 'Vendor Events 3', data.vendorEvents3, 'bitfield32');
908
+ this.logRegisterRead(blockNumber, 60, 'Vendor Events 4', data.vendorEvents4, 'bitfield32');
909
+ console.debug(`[Model ${blockNumber}] Float Inverter Data:`, data);
910
+ return data;
911
+ }
912
+ catch (error) {
913
+ console.error(`Error reading float inverter data (Model ${blockNumber}): ${error}`);
914
+ return null;
915
+ }
916
+ }
715
917
  /**
716
918
  * Extract inverter scale factors from a pre-read model buffer
717
919
  */
@@ -779,6 +981,16 @@ class SunspecModbusClient {
779
981
  console.debug(`[Model ${modelId}] offset ${offset}: ${fieldName} = "${rawValue}"${typeInfo}`);
780
982
  }
781
983
  }
984
+ /**
985
+ * Read a float32 (IEEE 754, big-endian, 2 registers) from a model buffer.
986
+ * SunSpec uses NaN as the "unimplemented" sentinel for floats; returns undefined in that case.
987
+ */
988
+ extractFloat32OrUndefined(buffer, offset, modelId, fieldName) {
989
+ const value = this.extractValue(buffer, offset, 'float32', 2);
990
+ const display = Number.isNaN(value) ? 'NaN' : value;
991
+ console.debug(`[Model ${modelId}] offset ${offset}: ${fieldName} = ${display} (float32)`);
992
+ return Number.isNaN(value) ? undefined : value;
993
+ }
782
994
  /**
783
995
  * Read scale factors from Model 160 (MPPT)
784
996
  *
@@ -806,18 +1018,18 @@ class SunspecModbusClient {
806
1018
  this.logRegisterRead(160, 5, 'DCWH_SF', scaleFactors.DCWH_SF, 'int16');
807
1019
  return scaleFactors;
808
1020
  }
809
- async readMPPTScaleFactors() {
810
- const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MPPT);
1021
+ async readMPPTScaleFactors(unitId) {
1022
+ const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MPPT);
811
1023
  if (!model) {
812
- console.debug('MPPT model 160 not found');
1024
+ console.debug(`MPPT model 160 not found on unit ${unitId}`);
813
1025
  return null;
814
1026
  }
815
1027
  try {
816
- const buffer = await this.readModelBlock(model);
1028
+ const buffer = await this.readModelBlock(unitId, model);
817
1029
  return this.extractMPPTScaleFactors(buffer);
818
1030
  }
819
1031
  catch (error) {
820
- console.error(`Error reading MPPT scale factors: ${error}`);
1032
+ console.error(`Error reading MPPT scale factors (unit ${unitId}): ${error}`);
821
1033
  return null;
822
1034
  }
823
1035
  }
@@ -878,36 +1090,36 @@ class SunspecModbusClient {
878
1090
  /**
879
1091
  * Read MPPT data from Model 160
880
1092
  */
881
- async readMPPTData(moduleId = 1) {
882
- const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MPPT);
1093
+ async readMPPTData(unitId, moduleId = 1) {
1094
+ const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MPPT);
883
1095
  if (!model) {
884
- console.debug('MPPT model 160 not found');
1096
+ console.debug(`MPPT model 160 not found on unit ${unitId}`);
885
1097
  return null;
886
1098
  }
887
1099
  try {
888
1100
  // Read entire model block in a single Modbus call
889
- const buffer = await this.readModelBlock(model);
1101
+ const buffer = await this.readModelBlock(unitId, model);
890
1102
  const scaleFactors = this.extractMPPTScaleFactors(buffer);
891
1103
  return this.extractMPPTModuleData(buffer, model, moduleId, scaleFactors);
892
1104
  }
893
1105
  catch (error) {
894
- console.error(`Error reading MPPT data for module ${moduleId}: ${error}`);
1106
+ console.error(`Error reading MPPT data for module ${moduleId} (unit ${unitId}): ${error}`);
895
1107
  return null;
896
1108
  }
897
1109
  }
898
1110
  /**
899
1111
  * Read all MPPT strings from Model 160 (Multiple MPPT)
900
1112
  */
901
- async readAllMPPTData() {
1113
+ async readAllMPPTData(unitId) {
902
1114
  const mpptData = [];
903
- const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MPPT);
1115
+ const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MPPT);
904
1116
  if (!model) {
905
- console.debug('MPPT model 160 not found');
1117
+ console.debug(`MPPT model 160 not found on unit ${unitId}`);
906
1118
  return [];
907
1119
  }
908
1120
  try {
909
1121
  // Read entire model block in a single Modbus call
910
- const buffer = await this.readModelBlock(model);
1122
+ const buffer = await this.readModelBlock(unitId, model);
911
1123
  const scaleFactors = this.extractMPPTScaleFactors(buffer);
912
1124
  // Read the module count from register 8
913
1125
  let moduleCount = 4; // Default fallback value
@@ -1068,16 +1280,16 @@ class SunspecModbusClient {
1068
1280
  /**
1069
1281
  * Read battery base data from Model 802 (Battery Base)
1070
1282
  */
1071
- async readBatteryBaseData() {
1072
- const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.BatteryBase);
1283
+ async readBatteryBaseData(unitId) {
1284
+ const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.BatteryBase);
1073
1285
  if (!model) {
1074
- console.debug('Battery Base model 802 not found');
1286
+ console.debug(`Battery Base model 802 not found on unit ${unitId}`);
1075
1287
  return null;
1076
1288
  }
1077
- console.debug(`Reading Battery Base Data from Model 802 at base address: ${model.address}`);
1289
+ console.debug(`Reading Battery Base Data from Model 802 at base address: ${model.address} (unit ${unitId})`);
1078
1290
  try {
1079
1291
  // Read entire model block in a single Modbus call
1080
- const buffer = await this.readModelBlock(model);
1292
+ const buffer = await this.readModelBlock(unitId, model);
1081
1293
  // Extract scale factors from buffer (offsets 52-63)
1082
1294
  const sf = this.extractBatteryBaseScaleFactors(buffer);
1083
1295
  // Extract raw values from buffer
@@ -1206,27 +1418,27 @@ class SunspecModbusClient {
1206
1418
  /**
1207
1419
  * Read battery data from Model 124 (Basic Storage) with fallback to Model 802 / Model 803
1208
1420
  */
1209
- async readBatteryData() {
1421
+ async readBatteryData(unitId) {
1210
1422
  // Try Model 124 first (Basic Storage Controls)
1211
- let model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Battery);
1423
+ let model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Battery);
1212
1424
  // Fall back to other battery models if needed
1213
1425
  if (!model) {
1214
- model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.BatteryBase);
1426
+ model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.BatteryBase);
1215
1427
  }
1216
1428
  if (!model) {
1217
- model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.BatteryControl);
1429
+ model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.BatteryControl);
1218
1430
  }
1219
1431
  if (!model) {
1220
- console.debug('No battery model found');
1432
+ console.debug(`No battery model found on unit ${unitId}`);
1221
1433
  return null;
1222
1434
  }
1223
- console.debug(`Reading Battery Data from Model ${model.id} at base address: ${model.address}`);
1435
+ console.debug(`Reading Battery Data from Model ${model.id} at base address: ${model.address} (unit ${unitId})`);
1224
1436
  try {
1225
1437
  if (model.id === 124) {
1226
1438
  // Model 124: Basic Storage Controls
1227
1439
  console.debug('Using Model 124 (Basic Storage Controls)');
1228
1440
  // Read entire model block in a single Modbus call
1229
- const buffer = await this.readModelBlock(model);
1441
+ const buffer = await this.readModelBlock(unitId, model);
1230
1442
  // Extract scale factors from buffer (offsets 18-25)
1231
1443
  const scaleFactors = {
1232
1444
  WChaMax_SF: this.extractValue(buffer, 18, 'int16'),
@@ -1327,7 +1539,7 @@ class SunspecModbusClient {
1327
1539
  else if (model.id === 802) {
1328
1540
  // Model 802: Battery Base
1329
1541
  console.debug('Using Model 802 (Battery Base)');
1330
- const baseData = await this.readBatteryBaseData();
1542
+ const baseData = await this.readBatteryBaseData(unitId);
1331
1543
  if (!baseData) {
1332
1544
  return null;
1333
1545
  }
@@ -1379,55 +1591,55 @@ class SunspecModbusClient {
1379
1591
  /**
1380
1592
  * Write battery control settings to Model 124
1381
1593
  */
1382
- async writeBatteryControls(controls) {
1383
- const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Battery);
1594
+ async writeBatteryControls(unitId, controls) {
1595
+ const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Battery);
1384
1596
  if (!model) {
1385
- console.error('Battery model 124 not found');
1597
+ console.error(`Battery model 124 not found on unit ${unitId}`);
1386
1598
  return false;
1387
1599
  }
1388
1600
  const baseAddr = model.address;
1389
- console.log(`Writing Battery Controls to Model 124 at base address: ${baseAddr}`);
1601
+ console.log(`Writing Battery Controls to Model 124 at base address: ${baseAddr} (unit ${unitId})`);
1390
1602
  try {
1391
1603
  // Write storage control mode (Register 5)
1392
1604
  if (controls.storCtlMod !== undefined) {
1393
- await this.writeRegisterValue(baseAddr + 5, controls.storCtlMod, 'uint16');
1605
+ await this.writeRegisterValue(unitId, baseAddr + 5, controls.storCtlMod, 'uint16');
1394
1606
  console.log(`Set storage control mode to 0x${controls.storCtlMod.toString(16)}`);
1395
1607
  }
1396
1608
  // Write charge source setting (Register 17)
1397
1609
  if (controls.chaGriSet !== undefined) {
1398
- await this.writeRegisterValue(baseAddr + 17, controls.chaGriSet, 'uint16');
1610
+ await this.writeRegisterValue(unitId, baseAddr + 17, controls.chaGriSet, 'uint16');
1399
1611
  console.log(`Set charge source to ${controls.chaGriSet === sunspec_interfaces_js_1.SunspecChargeSource.GRID ? 'GRID' : 'PV'}`);
1400
1612
  }
1401
1613
  // Write maximum charge power (Register 2) - needs scale factor
1402
1614
  if (controls.wChaMax !== undefined) {
1403
1615
  const scaleFactorAddr = baseAddr + 18;
1404
- const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
1616
+ const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
1405
1617
  const scaledValue = Math.round(controls.wChaMax / Math.pow(10, scaleFactor));
1406
- await this.writeRegisterValue(baseAddr + 2, scaledValue, 'uint16');
1618
+ await this.writeRegisterValue(unitId, baseAddr + 2, scaledValue, 'uint16');
1407
1619
  console.log(`Set max charge power to ${controls.wChaMax}W (scaled: ${scaledValue})`);
1408
1620
  }
1409
1621
  // Write charge rate (Register 13) - needs scale factor
1410
1622
  if (controls.inWRte !== undefined) {
1411
1623
  const scaleFactorAddr = baseAddr + 25;
1412
- const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
1624
+ const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
1413
1625
  const scaledValue = Math.round(controls.inWRte / Math.pow(10, scaleFactor));
1414
- await this.writeRegisterValue(baseAddr + 13, scaledValue, 'int16');
1626
+ await this.writeRegisterValue(unitId, baseAddr + 13, scaledValue, 'int16');
1415
1627
  console.log(`Set charge rate to ${controls.inWRte}% (scaled: ${scaledValue})`);
1416
1628
  }
1417
1629
  // Write discharge rate (Register 12) - needs scale factor
1418
1630
  if (controls.outWRte !== undefined) {
1419
1631
  const scaleFactorAddr = baseAddr + 25;
1420
- const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
1632
+ const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
1421
1633
  const scaledValue = Math.round(controls.outWRte / Math.pow(10, scaleFactor));
1422
- await this.writeRegisterValue(baseAddr + 12, scaledValue, 'int16');
1634
+ await this.writeRegisterValue(unitId, baseAddr + 12, scaledValue, 'int16');
1423
1635
  console.log(`Set discharge rate to ${controls.outWRte}% (scaled: ${scaledValue})`);
1424
1636
  }
1425
1637
  // Write minimum reserve percentage (Register 7) - needs scale factor
1426
1638
  if (controls.minRsvPct !== undefined) {
1427
1639
  const scaleFactorAddr = baseAddr + 21;
1428
- const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
1640
+ const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
1429
1641
  const scaledValue = Math.round(controls.minRsvPct / Math.pow(10, scaleFactor));
1430
- await this.writeRegisterValue(baseAddr + 7, scaledValue, 'uint16');
1642
+ await this.writeRegisterValue(unitId, baseAddr + 7, scaledValue, 'uint16');
1431
1643
  console.log(`Set minimum reserve to ${controls.minRsvPct}% (scaled: ${scaledValue})`);
1432
1644
  }
1433
1645
  console.log('Battery controls written successfully');
@@ -1441,7 +1653,7 @@ class SunspecModbusClient {
1441
1653
  /**
1442
1654
  * Set battery storage mode (simplified interface)
1443
1655
  */
1444
- async setStorageMode(mode) {
1656
+ async setStorageMode(unitId, mode) {
1445
1657
  let storCtlMod;
1446
1658
  switch (mode) {
1447
1659
  case sunspec_interfaces_js_1.SunspecStorageMode.CHARGE:
@@ -1465,29 +1677,29 @@ class SunspecModbusClient {
1465
1677
  return false;
1466
1678
  }
1467
1679
  console.log(`Setting storage mode to ${mode} (control bits: 0x${storCtlMod.toString(16)})`);
1468
- return this.writeBatteryControls({ storCtlMod });
1680
+ return this.writeBatteryControls(unitId, { storCtlMod });
1469
1681
  }
1470
1682
  /**
1471
1683
  * Enable or disable grid charging
1472
1684
  */
1473
- async enableGridCharging(enable) {
1685
+ async enableGridCharging(unitId, enable) {
1474
1686
  const chaGriSet = enable ? sunspec_interfaces_js_1.SunspecChargeSource.GRID : sunspec_interfaces_js_1.SunspecChargeSource.PV;
1475
- console.log(`${enable ? 'Enabling' : 'Disabling'} grid charging`);
1476
- return this.writeBatteryControls({ chaGriSet });
1687
+ console.log(`${enable ? 'Enabling' : 'Disabling'} grid charging on unit ${unitId}`);
1688
+ return this.writeBatteryControls(unitId, { chaGriSet });
1477
1689
  }
1478
1690
  /**
1479
1691
  * Read battery control settings from Model 124 (Basic Storage Controls)
1480
1692
  */
1481
- async readBatteryControls() {
1482
- const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Battery);
1693
+ async readBatteryControls(unitId) {
1694
+ const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Battery);
1483
1695
  if (!model) {
1484
- console.log('Battery model 124 not found');
1696
+ console.log(`Battery model 124 not found on unit ${unitId}`);
1485
1697
  return null;
1486
1698
  }
1487
- console.log(`Reading Battery Controls from Model 124 at base address: ${model.address}`);
1699
+ console.log(`Reading Battery Controls from Model 124 at base address: ${model.address} (unit ${unitId})`);
1488
1700
  try {
1489
1701
  // Read entire model block in a single Modbus call
1490
- const buffer = await this.readModelBlock(model);
1702
+ const buffer = await this.readModelBlock(unitId, model);
1491
1703
  // Extract scale factors from buffer
1492
1704
  const scaleFactors = {
1493
1705
  WChaMax_SF: this.extractValue(buffer, 18, 'int16'),
@@ -1526,19 +1738,19 @@ class SunspecModbusClient {
1526
1738
  /**
1527
1739
  * Read meter data from Model 201 (Single Phase) / Model 203 (Three Phase) / Model 204 (Split Phase)
1528
1740
  */
1529
- async readMeterData() {
1530
- let model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Meter3Phase);
1741
+ async readMeterData(unitId) {
1742
+ let model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Meter3Phase);
1531
1743
  if (!model) {
1532
- model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MeterWye);
1744
+ model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MeterWye);
1533
1745
  }
1534
1746
  if (!model) {
1535
- model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MeterSinglePhase);
1747
+ model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MeterSinglePhase);
1536
1748
  }
1537
1749
  if (!model) {
1538
- console.debug('No meter model found');
1750
+ console.debug(`No meter model found on unit ${unitId}`);
1539
1751
  return null;
1540
1752
  }
1541
- console.debug(`Reading Meter Data from Model ${model.id} at base address: ${model.address}`);
1753
+ console.debug(`Reading Meter Data from Model ${model.id} at base address: ${model.address} (unit ${unitId})`);
1542
1754
  try {
1543
1755
  // Different meter models have different register offsets
1544
1756
  console.debug(`Meter is Model ${model.id}`);
@@ -1582,7 +1794,7 @@ class SunspecModbusClient {
1582
1794
  energySFOffset = 54; // TotWh_SF - Total Energy scale factor
1583
1795
  }
1584
1796
  // Read entire model block in a single Modbus call
1585
- const buffer = await this.readModelBlock(model);
1797
+ const buffer = await this.readModelBlock(unitId, model);
1586
1798
  // Extract scale factors from buffer
1587
1799
  const powerSF = this.extractValue(buffer, powerSFOffset, 'int16');
1588
1800
  const freqSF = this.extractValue(buffer, freqSFOffset, 'int16');
@@ -1620,16 +1832,16 @@ class SunspecModbusClient {
1620
1832
  /**
1621
1833
  * Read common block data (Model 1)
1622
1834
  */
1623
- async readCommonBlock() {
1624
- const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Common);
1835
+ async readCommonBlock(unitId) {
1836
+ const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Common);
1625
1837
  if (!model) {
1626
- console.error('Common block model not found');
1838
+ console.error(`Common block model not found on unit ${unitId}`);
1627
1839
  return null;
1628
1840
  }
1629
- console.log(`Reading Common Block - Model address: ${model.address}`);
1841
+ console.log(`Reading Common Block - Model address: ${model.address} (unit ${unitId})`);
1630
1842
  try {
1631
1843
  // Read entire model block in a single Modbus call
1632
- const buffer = await this.readModelBlock(model);
1844
+ const buffer = await this.readModelBlock(unitId, model);
1633
1845
  // Common block offsets are relative to the model start (after ID and Length header,
1634
1846
  // but readModelBlock reads from model.address which includes the data area).
1635
1847
  // The offsets below are relative to the data start within the model block.
@@ -1666,24 +1878,24 @@ class SunspecModbusClient {
1666
1878
  /**
1667
1879
  * Get serial number from device
1668
1880
  */
1669
- async getSerialNumber() {
1670
- const commonData = await this.readCommonBlock();
1881
+ async getSerialNumber(unitId) {
1882
+ const commonData = await this.readCommonBlock(unitId);
1671
1883
  return commonData?.serialNumber;
1672
1884
  }
1673
1885
  /**
1674
- * Check if connected
1886
+ * Check if a specific unit is connected on this network device
1675
1887
  */
1676
- isConnected() {
1677
- return this.connected;
1888
+ isConnected(unitId) {
1889
+ return this.modbusInstances.has(unitId);
1678
1890
  }
1679
1891
  /**
1680
- * Check if connection is healthy
1892
+ * Check if a specific unit's connection is healthy
1681
1893
  */
1682
- isHealthy() {
1683
- return this.connected && this.connectionHealth.isHealthy();
1894
+ isHealthy(unitId) {
1895
+ return this.modbusInstances.has(unitId) && this.connectionHealth.isHealthy();
1684
1896
  }
1685
1897
  /**
1686
- * Get connection health details
1898
+ * Get connection health details (shared across all units on this network device)
1687
1899
  */
1688
1900
  getConnectionHealth() {
1689
1901
  return this.connectionHealth;
@@ -1691,15 +1903,15 @@ class SunspecModbusClient {
1691
1903
  /**
1692
1904
  * Read inverter settings from Model 121 (Inverter Settings)
1693
1905
  */
1694
- async readInverterSettings() {
1695
- const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Settings);
1906
+ async readInverterSettings(unitId) {
1907
+ const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Settings);
1696
1908
  if (!model) {
1697
- console.debug('Settings model 121 not found');
1909
+ console.debug(`Settings model 121 not found on unit ${unitId}`);
1698
1910
  return null;
1699
1911
  }
1700
1912
  try {
1701
1913
  // Read entire model block in a single Modbus call
1702
- const buffer = await this.readModelBlock(model);
1914
+ const buffer = await this.readModelBlock(unitId, model);
1703
1915
  // Extract scale factors from buffer (offsets 22-31)
1704
1916
  const scaleFactors = {
1705
1917
  WMax_SF: this.extractValue(buffer, 22, 'int16'),
@@ -1783,15 +1995,15 @@ class SunspecModbusClient {
1783
1995
  /**
1784
1996
  * Read inverter controls from Model 123 (Immediate Inverter Controls)
1785
1997
  */
1786
- async readInverterControls() {
1787
- const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Controls);
1998
+ async readInverterControls(unitId) {
1999
+ const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Controls);
1788
2000
  if (!model) {
1789
- console.log('Controls model 123 not found');
2001
+ console.log(`Controls model 123 not found on unit ${unitId}`);
1790
2002
  return null;
1791
2003
  }
1792
2004
  try {
1793
2005
  // Read entire model block in a single Modbus call
1794
- const buffer = await this.readModelBlock(model);
2006
+ const buffer = await this.readModelBlock(unitId, model);
1795
2007
  // Extract scale factors from buffer (offsets 21-23)
1796
2008
  const scaleFactors = {
1797
2009
  WMaxLimPct_SF: this.extractValue(buffer, 21, 'int16'),
@@ -1878,27 +2090,28 @@ class SunspecModbusClient {
1878
2090
  /**
1879
2091
  * Write Block 121 - Inverter Basic Settings
1880
2092
  */
1881
- async writeInverterSettings(settings) {
1882
- const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Settings);
2093
+ async writeInverterSettings(unitId, settings) {
2094
+ const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Settings);
1883
2095
  if (!model) {
1884
- console.error('Settings model 121 not found');
2096
+ console.error(`Settings model 121 not found on unit ${unitId}`);
1885
2097
  return false;
1886
2098
  }
1887
2099
  const baseAddr = model.address;
1888
2100
  try {
2101
+ const instance = this.getInstance(unitId);
1889
2102
  // For each setting, write the value if provided
1890
2103
  // Note: This is a simplified implementation. In production, you'd batch writes
1891
- if (settings.WMax !== undefined && this.modbusClient) {
2104
+ if (settings.WMax !== undefined) {
1892
2105
  // Need to read scale factor first if not provided
1893
- const sfBuffer = await this.modbusClient.readHoldingRegisters(baseAddr + 22, 1);
2106
+ const sfBuffer = await instance.readHoldingRegisters(baseAddr + 22, 1);
1894
2107
  const scaleFactor = sfBuffer.readInt16BE(0);
1895
2108
  const scaledValue = Math.round(settings.WMax / Math.pow(10, scaleFactor));
1896
2109
  // Writing registers needs to be implemented in EnergyAppModbusInstance
1897
2110
  // For now, log the write operation
1898
2111
  console.log(`Would write value ${scaledValue} to register ${baseAddr}`);
1899
2112
  }
1900
- if (settings.VRef !== undefined && this.modbusClient) {
1901
- const sfBuffer = await this.modbusClient.readHoldingRegisters(baseAddr + 23, 1);
2113
+ if (settings.VRef !== undefined) {
2114
+ const sfBuffer = await instance.readHoldingRegisters(baseAddr + 23, 1);
1902
2115
  const scaleFactor = sfBuffer.readInt16BE(0);
1903
2116
  const scaledValue = Math.round(settings.VRef / Math.pow(10, scaleFactor));
1904
2117
  console.log(`Would write value ${scaledValue} to register ${baseAddr + 1}`);
@@ -1915,41 +2128,41 @@ class SunspecModbusClient {
1915
2128
  /**
1916
2129
  * Write inverter controls to Model 123 (Immediate Inverter Controls)
1917
2130
  */
1918
- async writeInverterControls(controls) {
1919
- const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Controls);
2131
+ async writeInverterControls(unitId, controls) {
2132
+ const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Controls);
1920
2133
  if (!model) {
1921
- console.error('Controls model 123 not found');
2134
+ console.error(`Controls model 123 not found on unit ${unitId}`);
1922
2135
  return false;
1923
2136
  }
1924
2137
  const baseAddr = model.address;
1925
2138
  try {
1926
2139
  // Connection control (Register 2)
1927
2140
  if (controls.Conn !== undefined) {
1928
- await this.writeRegisterValue(baseAddr + 2, controls.Conn, 'uint16');
2141
+ await this.writeRegisterValue(unitId, baseAddr + 2, controls.Conn, 'uint16');
1929
2142
  console.log(`Set connection control to ${controls.Conn}`);
1930
2143
  }
1931
2144
  // Power limit control (Register 3) - needs scale factor
1932
2145
  if (controls.WMaxLimPct !== undefined) {
1933
- const scaleFactor = await this.readRegisterValue(baseAddr + 21, 1, 'int16');
2146
+ const scaleFactor = await this.readRegisterValue(unitId, baseAddr + 21, 1, 'int16');
1934
2147
  const scaledValue = Math.round(controls.WMaxLimPct / Math.pow(10, scaleFactor));
1935
- await this.writeRegisterValue(baseAddr + 3, scaledValue, 'uint16');
2148
+ await this.writeRegisterValue(unitId, baseAddr + 3, scaledValue, 'uint16');
1936
2149
  console.log(`Set power limit to ${controls.WMaxLimPct}% (scaled: ${scaledValue})`);
1937
2150
  }
1938
2151
  // Throttle enable/disable (Register 7)
1939
2152
  if (controls.WMaxLim_Ena !== undefined) {
1940
- await this.writeRegisterValue(baseAddr + 7, controls.WMaxLim_Ena, 'uint16');
2153
+ await this.writeRegisterValue(unitId, baseAddr + 7, controls.WMaxLim_Ena, 'uint16');
1941
2154
  console.log(`Set throttle enable to ${controls.WMaxLim_Ena}`);
1942
2155
  }
1943
2156
  // Power factor control (Register 8) - needs scale factor
1944
2157
  if (controls.OutPFSet !== undefined) {
1945
- const scaleFactor = await this.readRegisterValue(baseAddr + 22, 1, 'int16');
2158
+ const scaleFactor = await this.readRegisterValue(unitId, baseAddr + 22, 1, 'int16');
1946
2159
  const scaledValue = Math.round(controls.OutPFSet / Math.pow(10, scaleFactor));
1947
- await this.writeRegisterValue(baseAddr + 8, scaledValue, 'int16');
2160
+ await this.writeRegisterValue(unitId, baseAddr + 8, scaledValue, 'int16');
1948
2161
  console.log(`Set power factor to ${controls.OutPFSet} (scaled: ${scaledValue})`);
1949
2162
  }
1950
2163
  // Power factor enable/disable (Register 12)
1951
2164
  if (controls.OutPFSet_Ena !== undefined) {
1952
- await this.writeRegisterValue(baseAddr + 12, controls.OutPFSet_Ena, 'uint16');
2165
+ await this.writeRegisterValue(unitId, baseAddr + 12, controls.OutPFSet_Ena, 'uint16');
1953
2166
  console.log(`Set PF enable to ${controls.OutPFSet_Ena}`);
1954
2167
  }
1955
2168
  console.log('Inverter controls written successfully');
@@ -1970,21 +2183,21 @@ class SunspecModbusClient {
1970
2183
  * @param limitW - Power limit in Watts, or null to remove the limit
1971
2184
  * @returns true if successful, false otherwise
1972
2185
  */
1973
- async setFeedInLimit(limitW) {
2186
+ async setFeedInLimit(unitId, limitW) {
1974
2187
  if (limitW === null) {
1975
2188
  // Remove limit: disable WMaxLim_Ena
1976
- console.log('Removing feed-in limit (disabling WMaxLim_Ena)');
1977
- return this.writeInverterControls({ WMaxLim_Ena: sunspec_interfaces_js_1.SunspecEnableControl.DISABLED });
2189
+ console.log(`Removing feed-in limit (disabling WMaxLim_Ena) on unit ${unitId}`);
2190
+ return this.writeInverterControls(unitId, { WMaxLim_Ena: sunspec_interfaces_js_1.SunspecEnableControl.DISABLED });
1978
2191
  }
1979
2192
  // Read WMax from Model 121 to compute percentage
1980
- const settings = await this.readInverterSettings();
2193
+ const settings = await this.readInverterSettings(unitId);
1981
2194
  if (!settings || !settings.WMax) {
1982
- console.error('Cannot set feed-in limit: unable to read WMax from Model 121');
2195
+ console.error(`Cannot set feed-in limit on unit ${unitId}: unable to read WMax from Model 121`);
1983
2196
  return false;
1984
2197
  }
1985
2198
  const pct = (limitW / settings.WMax) * 100;
1986
- console.log(`Setting feed-in limit to ${limitW}W (${pct.toFixed(2)}% of WMax ${settings.WMax}W)`);
1987
- return this.writeInverterControls({
2199
+ console.log(`Setting feed-in limit to ${limitW}W (${pct.toFixed(2)}% of WMax ${settings.WMax}W) on unit ${unitId}`);
2200
+ return this.writeInverterControls(unitId, {
1988
2201
  WMaxLimPct: pct,
1989
2202
  WMaxLim_Ena: sunspec_interfaces_js_1.SunspecEnableControl.ENABLED
1990
2203
  });