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