@enyo-energy/sunspec-sdk 0.0.51 → 0.0.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,23 +19,38 @@
19
19
  import { type SunspecInverterControls, type SunspecInverterData, type SunspecInverterSettings, type SunspecMeterData, type SunspecModel, type SunspecMPPTData, type SunspecBatteryData, type SunspecBatteryBaseData, type SunspecBatteryControls, SunspecStorageMode } from "./sunspec-interfaces.cjs";
20
20
  import { EnergyAppModbusDataType, IConnectionHealth } from "@enyo-energy/energy-app-sdk/dist/implementations/modbus/interfaces.js";
21
21
  import { EnergyApp } from "@enyo-energy/energy-app-sdk";
22
+ /**
23
+ * Get (or create) the singleton SunspecModbusClient for this network device.
24
+ * Increments the refcount; pair with `releaseSunspecClient` on teardown.
25
+ */
26
+ export declare function getOrCreateSunspecClient(energyApp: EnergyApp, host: string, port?: number): SunspecModbusClient;
27
+ /**
28
+ * Release a refcount on the network-device client. When the last consumer releases,
29
+ * the client is fully disconnected (all units closed) and removed from the registry.
30
+ *
31
+ * Releasing more times than acquired is a programming error and is logged; the count
32
+ * is clamped at zero so repeat releases are no-ops.
33
+ */
34
+ export declare function releaseSunspecClient(host: string, port?: number): Promise<void>;
22
35
  export declare class SunspecModbusClient {
23
36
  private energyApp;
24
- private modbusClient;
25
- private discoveredModels;
26
- private connected;
37
+ private modbusInstances;
38
+ private faultTolerantReaders;
39
+ private discoveredModelsByUnit;
27
40
  private connectionHealth;
28
- private faultTolerantReader;
29
41
  private modbusDataTypeConverter;
30
42
  private connectionParams;
43
+ private knownUnits;
31
44
  private autoReconnectEnabled;
32
45
  private openCount;
33
46
  private closeCount;
34
- private currentlyOpen;
35
47
  private operationChain;
36
48
  constructor(energyApp: EnergyApp);
37
49
  /**
38
- * Connect to Modbus device
50
+ * Connect to a Modbus device unit. Multiple unit IDs on the same network device share this
51
+ * client; calling connect() with different unit IDs adds each as a separately-managed unit.
52
+ * The first call locks in the host/port for this client; later calls for a different host
53
+ * are rejected.
39
54
  * @param host Primary host (hostname) to connect to
40
55
  * @param port Modbus port (default 502)
41
56
  * @param unitId Modbus unit ID (default 1)
@@ -43,33 +58,47 @@ export declare class SunspecModbusClient {
43
58
  */
44
59
  connect(host: string, port?: number, unitId?: number, secondaryHost?: string): Promise<void>;
45
60
  /**
46
- * Disconnect from Modbus device.
61
+ * Disconnect from all units of this network device.
47
62
  *
48
- * Note: connection parameters are preserved so reconnect() can be called afterwards.
49
- * They will be overwritten by the next connect() call anyway.
63
+ * Note: connection parameters and the set of known units are preserved so reconnect() can
64
+ * be called afterwards. They will be overwritten by the next connect() call anyway.
50
65
  */
51
66
  disconnect(): Promise<void>;
52
67
  /**
53
- * Reconnect using stored connection parameters
54
- * First tries primaryHost (hostname), then falls back to secondaryHost (ipAddress) if available
55
- * Returns true if reconnection was successful, false otherwise
68
+ * Disconnect a single unit on this network device. Other units on the same device stay open.
69
+ */
70
+ disconnectUnit(unitId: number): Promise<void>;
71
+ /**
72
+ * Reconnect every previously-known unit on this network device using stored connection
73
+ * parameters. First tries primaryHost (hostname), then falls back to secondaryHost
74
+ * (ipAddress) if available. Returns true only if every known unit reconnected successfully.
56
75
  */
57
76
  reconnect(): Promise<boolean>;
58
77
  /**
59
- * Attempt to establish a connection to a specific host. Caller must hold the connection lock.
60
- * Returns true if successful, false otherwise.
78
+ * Reconnect a single previously-known unit on this network device. Other units on the
79
+ * same client stay open. Useful when one device's polling loop detects a dropped
80
+ * connection and only needs to recover its own unit, not thrash siblings.
81
+ *
82
+ * Returns true if the unit reconnected successfully (on primary or secondary host).
83
+ */
84
+ reconnectUnit(unitId: number): Promise<boolean>;
85
+ /**
86
+ * Attempt to (re)open every requested unit on a specific host. Caller must hold the
87
+ * connection lock. Closes any pre-existing per-unit instances first. Returns true only
88
+ * if every unit succeeded.
61
89
  */
62
90
  private attemptConnection;
63
91
  /**
64
- * Open a new Modbus socket and wire up the fault-tolerant reader. On any failure, the
65
- * just-opened socket (if any) is closed so we never leak. Caller must hold the connection
66
- * lock and ensure no prior socket is open (use _closeSocket first).
92
+ * Open a new Modbus instance for one unit ID and wire up its fault-tolerant reader.
93
+ * On any failure, the just-opened instance (if any) is closed so we never leak. Caller must
94
+ * hold the connection lock and ensure the unit is not already open.
67
95
  */
68
- private _openSocket;
96
+ private _openUnit;
69
97
  /**
70
- * Close the current Modbus socket if any. Idempotent. Caller must hold the connection lock.
98
+ * Close the Modbus instance for a single unit. Idempotent. Caller must hold the
99
+ * connection lock.
71
100
  */
72
- private _closeSocket;
101
+ private _closeUnit;
73
102
  /**
74
103
  * Run `fn` with exclusive access to connection-state transitions. Subsequent calls queue
75
104
  * behind any in-flight one. A rejected `fn` does not poison the chain for later callers.
@@ -78,13 +107,26 @@ export declare class SunspecModbusClient {
78
107
  private recordOpen;
79
108
  private recordClose;
80
109
  /**
81
- * Get cumulative open/close counts for this client. Useful for spotting connection leaks.
110
+ * Get cumulative open/close counts for this client (across all unit IDs). Useful for
111
+ * spotting connection leaks.
82
112
  */
83
113
  getConnectionStats(): {
84
114
  opens: number;
85
115
  closes: number;
86
- currentlyOpen: number;
116
+ openUnits: number;
87
117
  };
118
+ /**
119
+ * Get the EnergyAppModbusInstance for a unit, throwing if it isn't open.
120
+ */
121
+ private getInstance;
122
+ /**
123
+ * Get the fault-tolerant reader for a unit, throwing if it isn't open.
124
+ */
125
+ private getReader;
126
+ /**
127
+ * Get (or create) the discovered-models map for a unit.
128
+ */
129
+ private getModelsMap;
88
130
  /**
89
131
  * Enable or disable automatic reconnection
90
132
  */
@@ -96,20 +138,20 @@ export declare class SunspecModbusClient {
96
138
  /**
97
139
  * Detect the base address and addressing mode (0-based or 1-based) for SunSpec
98
140
  */
99
- detectSunspecBaseAddress(customBaseAddress?: number): Promise<{
141
+ detectSunspecBaseAddress(unitId: number, customBaseAddress?: number): Promise<{
100
142
  baseAddress: number;
101
143
  isZeroBased: boolean;
102
144
  nextAddress: number;
103
145
  }>;
104
146
  /**
105
- * Discover all available Sunspec models
147
+ * Discover all available Sunspec models for a unit
106
148
  * Automatically detects base address (40000 or 40001) and scans from there
107
149
  */
108
- discoverModels(customBaseAddress?: number): Promise<Map<number, SunspecModel>>;
150
+ discoverModels(unitId: number, customBaseAddress?: number): Promise<Map<number, SunspecModel>>;
109
151
  /**
110
- * Find a specific model by ID
152
+ * Find a specific model by ID for a given unit
111
153
  */
112
- findModel(modelId: number): SunspecModel | undefined;
154
+ findModel(unitId: number, modelId: number): SunspecModel | undefined;
113
155
  /**
114
156
  * Check if a value is "unimplemented" according to Sunspec specification
115
157
  * Returns true if the value represents an unimplemented/not applicable register
@@ -153,15 +195,15 @@ export declare class SunspecModbusClient {
153
195
  /**
154
196
  * Helper to read register value(s) using the fault-tolerant reader with data type conversion
155
197
  */
156
- readRegisterValue(address: number, quantity: number | undefined, dataType: EnergyAppModbusDataType): Promise<number | string | number[]>;
198
+ readRegisterValue(unitId: number, address: number, quantity: number | undefined, dataType: EnergyAppModbusDataType): Promise<number | string | number[]>;
157
199
  /**
158
200
  * Helper to write register value(s)
159
201
  */
160
- writeRegisterValue(address: number, value: number | number[], dataType?: EnergyAppModbusDataType): Promise<boolean>;
202
+ writeRegisterValue(unitId: number, address: number, value: number | number[], dataType?: EnergyAppModbusDataType): Promise<boolean>;
161
203
  /**
162
204
  * Read inverter data from Model 101 (Single Phase) / Model 103 (Three Phase)
163
205
  */
164
- readInverterData(): Promise<SunspecInverterData | null>;
206
+ readInverterData(unitId: number): Promise<SunspecInverterData | null>;
165
207
  /**
166
208
  * Read single phase inverter data (Model 101)
167
209
  */
@@ -191,7 +233,7 @@ export declare class SunspecModbusClient {
191
233
  * Extract MPPT scale factors from a pre-read model buffer
192
234
  */
193
235
  private extractMPPTScaleFactors;
194
- readMPPTScaleFactors(): Promise<{
236
+ readMPPTScaleFactors(unitId: number): Promise<{
195
237
  DCA_SF: number;
196
238
  DCV_SF: number;
197
239
  DCW_SF: number;
@@ -204,11 +246,11 @@ export declare class SunspecModbusClient {
204
246
  /**
205
247
  * Read MPPT data from Model 160
206
248
  */
207
- readMPPTData(moduleId?: number): Promise<SunspecMPPTData | null>;
249
+ readMPPTData(unitId: number, moduleId?: number): Promise<SunspecMPPTData | null>;
208
250
  /**
209
251
  * Read all MPPT strings from Model 160 (Multiple MPPT)
210
252
  */
211
- readAllMPPTData(): Promise<SunspecMPPTData[]>;
253
+ readAllMPPTData(unitId: number): Promise<SunspecMPPTData[]>;
212
254
  /**
213
255
  * Map battery charge state to human-readable name
214
256
  * @param state The numeric charge state value
@@ -230,67 +272,67 @@ export declare class SunspecModbusClient {
230
272
  /**
231
273
  * Read battery base data from Model 802 (Battery Base)
232
274
  */
233
- readBatteryBaseData(): Promise<SunspecBatteryBaseData | null>;
275
+ readBatteryBaseData(unitId: number): Promise<SunspecBatteryBaseData | null>;
234
276
  /**
235
277
  * Read battery data from Model 124 (Basic Storage) with fallback to Model 802 / Model 803
236
278
  */
237
- readBatteryData(): Promise<SunspecBatteryData | null>;
279
+ readBatteryData(unitId: number): Promise<SunspecBatteryData | null>;
238
280
  /**
239
281
  * Write battery control settings to Model 124
240
282
  */
241
- writeBatteryControls(controls: Partial<SunspecBatteryControls>): Promise<boolean>;
283
+ writeBatteryControls(unitId: number, controls: Partial<SunspecBatteryControls>): Promise<boolean>;
242
284
  /**
243
285
  * Set battery storage mode (simplified interface)
244
286
  */
245
- setStorageMode(mode: SunspecStorageMode): Promise<boolean>;
287
+ setStorageMode(unitId: number, mode: SunspecStorageMode): Promise<boolean>;
246
288
  /**
247
289
  * Enable or disable grid charging
248
290
  */
249
- enableGridCharging(enable: boolean): Promise<boolean>;
291
+ enableGridCharging(unitId: number, enable: boolean): Promise<boolean>;
250
292
  /**
251
293
  * Read battery control settings from Model 124 (Basic Storage Controls)
252
294
  */
253
- readBatteryControls(): Promise<SunspecBatteryControls | null>;
295
+ readBatteryControls(unitId: number): Promise<SunspecBatteryControls | null>;
254
296
  /**
255
297
  * Read meter data from Model 201 (Single Phase) / Model 203 (Three Phase) / Model 204 (Split Phase)
256
298
  */
257
- readMeterData(): Promise<SunspecMeterData | null>;
299
+ readMeterData(unitId: number): Promise<SunspecMeterData | null>;
258
300
  /**
259
301
  * Read common block data (Model 1)
260
302
  */
261
- readCommonBlock(): Promise<any>;
303
+ readCommonBlock(unitId: number): Promise<any>;
262
304
  /**
263
305
  * Get serial number from device
264
306
  */
265
- getSerialNumber(): Promise<string | undefined>;
307
+ getSerialNumber(unitId: number): Promise<string | undefined>;
266
308
  /**
267
- * Check if connected
309
+ * Check if a specific unit is connected on this network device
268
310
  */
269
- isConnected(): boolean;
311
+ isConnected(unitId: number): boolean;
270
312
  /**
271
- * Check if connection is healthy
313
+ * Check if a specific unit's connection is healthy
272
314
  */
273
- isHealthy(): boolean;
315
+ isHealthy(unitId: number): boolean;
274
316
  /**
275
- * Get connection health details
317
+ * Get connection health details (shared across all units on this network device)
276
318
  */
277
319
  getConnectionHealth(): IConnectionHealth;
278
320
  /**
279
321
  * Read inverter settings from Model 121 (Inverter Settings)
280
322
  */
281
- readInverterSettings(): Promise<SunspecInverterSettings | null>;
323
+ readInverterSettings(unitId: number): Promise<SunspecInverterSettings | null>;
282
324
  /**
283
325
  * Read inverter controls from Model 123 (Immediate Inverter Controls)
284
326
  */
285
- readInverterControls(): Promise<SunspecInverterControls | null>;
327
+ readInverterControls(unitId: number): Promise<SunspecInverterControls | null>;
286
328
  /**
287
329
  * Write Block 121 - Inverter Basic Settings
288
330
  */
289
- writeInverterSettings(settings: Partial<SunspecInverterSettings>): Promise<boolean>;
331
+ writeInverterSettings(unitId: number, settings: Partial<SunspecInverterSettings>): Promise<boolean>;
290
332
  /**
291
333
  * Write inverter controls to Model 123 (Immediate Inverter Controls)
292
334
  */
293
- writeInverterControls(controls: Partial<SunspecInverterControls>): Promise<boolean>;
335
+ writeInverterControls(unitId: number, controls: Partial<SunspecInverterControls>): Promise<boolean>;
294
336
  /**
295
337
  * Set the inverter feed-in power limit using Model 123 (Immediate Inverter Controls)
296
338
  *
@@ -301,5 +343,5 @@ export declare class SunspecModbusClient {
301
343
  * @param limitW - Power limit in Watts, or null to remove the limit
302
344
  * @returns true if successful, false otherwise
303
345
  */
304
- setFeedInLimit(limitW: number | null): Promise<boolean>;
346
+ setFeedInLimit(unitId: number, limitW: number | null): Promise<boolean>;
305
347
  }
@@ -9,7 +9,7 @@ exports.getSdkVersion = getSdkVersion;
9
9
  /**
10
10
  * Current version of the enyo Energy App SDK.
11
11
  */
12
- exports.SDK_VERSION = '0.0.51';
12
+ exports.SDK_VERSION = '0.0.52';
13
13
  /**
14
14
  * Gets the current SDK version.
15
15
  * @returns The semantic version string of the SDK
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "0.0.51";
8
+ export declare const SDK_VERSION = "0.0.52";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
@@ -113,7 +113,7 @@ export class BaseSunspecDevice {
113
113
  * Check if the device is connected
114
114
  */
115
115
  isConnected() {
116
- return this.sunspecClient.isHealthy();
116
+ return this.sunspecClient.isHealthy(this.unitId);
117
117
  }
118
118
  /**
119
119
  * Get the appliance IDs managed by this device
@@ -122,11 +122,11 @@ export class BaseSunspecDevice {
122
122
  * Ensure the Sunspec client is connected and models are discovered
123
123
  */
124
124
  async ensureConnected() {
125
- if (!this.sunspecClient.isConnected()) {
125
+ if (!this.sunspecClient.isConnected(this.unitId)) {
126
126
  await this.sunspecClient.connect(this.networkDevice.hostname, // primary
127
127
  this.port, this.unitId, this.networkDevice.ipAddress // secondary fallback
128
128
  );
129
- await this.sunspecClient.discoverModels(this.baseAddress);
129
+ await this.sunspecClient.discoverModels(this.unitId, this.baseAddress);
130
130
  }
131
131
  }
132
132
  /**
@@ -146,12 +146,15 @@ export class BaseSunspecDevice {
146
146
  const stats = this.sunspecClient.getConnectionStats();
147
147
  console.log(`${this.constructor.name} ${this.applianceId}: Reconnect attempt #${attempt} ` +
148
148
  `(phase: ${phase.intervalMs / 1000}s interval, elapsed: ${elapsed}s, ` +
149
- `opens=${stats.opens}, closes=${stats.closes}, current=${stats.currentlyOpen})`);
149
+ `opens=${stats.opens}, closes=${stats.closes}, openUnits=${stats.openUnits})`);
150
150
  try {
151
- const success = await this.sunspecClient.reconnect();
151
+ // Reconnect just this device's unit, not every unit on the shared client.
152
+ // This avoids thrashing sibling devices on the same network device when one
153
+ // device's poll loop detects a dropped connection.
154
+ const success = await this.sunspecClient.reconnectUnit(this.unitId);
152
155
  if (success) {
153
- // Re-discover models after reconnect
154
- await this.sunspecClient.discoverModels(this.baseAddress);
156
+ // Re-discover models for this unit after reconnect
157
+ await this.sunspecClient.discoverModels(this.unitId, this.baseAddress);
155
158
  this.retryManager.reset();
156
159
  this.consecutiveReconnectFailures = 0;
157
160
  // Update appliance state to Connected
@@ -159,7 +162,7 @@ export class BaseSunspecDevice {
159
162
  await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Connected);
160
163
  }
161
164
  const postStats = this.sunspecClient.getConnectionStats();
162
- console.log(`${this.constructor.name} ${this.applianceId}: Reconnection successful after ${attempt} attempt(s) (opens=${postStats.opens}, closes=${postStats.closes}, current=${postStats.currentlyOpen})`);
165
+ console.log(`${this.constructor.name} ${this.applianceId}: Reconnection successful after ${attempt} attempt(s) (opens=${postStats.opens}, closes=${postStats.closes}, openUnits=${postStats.openUnits})`);
163
166
  return true;
164
167
  }
165
168
  this.consecutiveReconnectFailures += 1;
@@ -187,15 +190,15 @@ export class BaseSunspecDevice {
187
190
  async markOffline() {
188
191
  this.retryManager.markDisconnected();
189
192
  try {
190
- if (this.sunspecClient.isConnected()) {
191
- await this.sunspecClient.disconnect();
193
+ if (this.sunspecClient.isConnected(this.unitId)) {
194
+ await this.sunspecClient.disconnectUnit(this.unitId);
192
195
  }
193
196
  }
194
197
  catch (error) {
195
198
  console.error(`${this.constructor.name} ${this.applianceId}: error closing socket on markOffline: ${error}`);
196
199
  }
197
200
  const stats = this.sunspecClient.getConnectionStats();
198
- console.log(`${this.constructor.name} ${this.applianceId}: marked offline (opens=${stats.opens}, closes=${stats.closes}, current=${stats.currentlyOpen})`);
201
+ console.log(`${this.constructor.name} ${this.applianceId}: marked offline (opens=${stats.opens}, closes=${stats.closes}, openUnits=${stats.openUnits})`);
199
202
  if (this.applianceId) {
200
203
  try {
201
204
  await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
@@ -242,16 +245,16 @@ export class SunspecInverter extends BaseSunspecDevice {
242
245
  }
243
246
  async connect() {
244
247
  // Ensure Sunspec client is connected
245
- if (!this.sunspecClient.isConnected()) {
248
+ if (!this.sunspecClient.isConnected(this.unitId)) {
246
249
  await this.sunspecClient.connect(this.networkDevice.hostname, // primary
247
250
  this.port, this.unitId, this.networkDevice.ipAddress // secondary fallback
248
251
  );
249
- await this.sunspecClient.discoverModels(this.baseAddress);
252
+ await this.sunspecClient.discoverModels(this.unitId, this.baseAddress);
250
253
  }
251
254
  // Get device info from common block
252
- const commonData = await this.sunspecClient.readCommonBlock();
253
- const inverterSettings = await this.sunspecClient.readInverterSettings();
254
- const mpptDataList = await this.sunspecClient.readAllMPPTData();
255
+ const commonData = await this.sunspecClient.readCommonBlock(this.unitId);
256
+ const inverterSettings = await this.sunspecClient.readInverterSettings(this.unitId);
257
+ const mpptDataList = await this.sunspecClient.readAllMPPTData(this.unitId);
255
258
  // Create or update appliance (skip if an existing appliance was provided)
256
259
  if (!this.applianceId) {
257
260
  try {
@@ -301,7 +304,7 @@ export class SunspecInverter extends BaseSunspecDevice {
301
304
  }
302
305
  }
303
306
  // Check for MPPT models
304
- const mpptModel = this.sunspecClient.findModel(SunspecModelId.MPPT);
307
+ const mpptModel = this.sunspecClient.findModel(this.unitId, SunspecModelId.MPPT);
305
308
  if (mpptModel) {
306
309
  console.log(`MPPT model found for inverter ${this.networkDevice.hostname}`);
307
310
  }
@@ -325,9 +328,9 @@ export class SunspecInverter extends BaseSunspecDevice {
325
328
  const timestamp = new Date();
326
329
  try {
327
330
  // Read inverter data
328
- const inverterData = await this.sunspecClient.readInverterData();
329
- const mpptDataList = await this.sunspecClient.readAllMPPTData();
330
- const inverterSettings = await this.sunspecClient.readInverterSettings();
331
+ const inverterData = await this.sunspecClient.readInverterData(this.unitId);
332
+ const mpptDataList = await this.sunspecClient.readAllMPPTData(this.unitId);
333
+ const inverterSettings = await this.sunspecClient.readInverterSettings(this.unitId);
331
334
  const dcStrings = this.mapMPPTToStrings(mpptDataList);
332
335
  if (inverterData) {
333
336
  const pvPowerW = dcStrings.reduce((sum, s) => sum + (s.powerW || 0), 0);
@@ -653,7 +656,7 @@ export class SunspecInverter extends BaseSunspecDevice {
653
656
  return;
654
657
  }
655
658
  console.log(`Inverter ${this.applianceId}: handling SetInverterFeedInLimitV1 (feedInLimitW=${msg.data.feedInLimitW})`);
656
- const success = await this.sunspecClient.setFeedInLimit(msg.data.feedInLimitW);
659
+ const success = await this.sunspecClient.setFeedInLimit(this.unitId, msg.data.feedInLimitW);
657
660
  if (!success) {
658
661
  this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set feed-in limit');
659
662
  return;
@@ -677,14 +680,14 @@ export class SunspecBattery extends BaseSunspecDevice {
677
680
  // Ensure Sunspec client is connected
678
681
  await this.ensureConnected();
679
682
  // Check if battery models exist
680
- const hasBattery = this.sunspecClient.findModel(SunspecModelId.Battery) !== undefined ||
681
- this.sunspecClient.findModel(SunspecModelId.BatteryBase) !== undefined;
683
+ const hasBattery = this.sunspecClient.findModel(this.unitId, SunspecModelId.Battery) !== undefined ||
684
+ this.sunspecClient.findModel(this.unitId, SunspecModelId.BatteryBase) !== undefined;
682
685
  if (!hasBattery) {
683
686
  throw new Error('No battery model found in device');
684
687
  }
685
688
  // Get device info
686
- const commonData = await this.sunspecClient.readCommonBlock();
687
- const batteryData = await this.sunspecClient.readBatteryData();
689
+ const commonData = await this.sunspecClient.readCommonBlock(this.unitId);
690
+ const batteryData = await this.sunspecClient.readBatteryData(this.unitId);
688
691
  const storageMode = this.determineStorageMode(batteryData);
689
692
  const features = [];
690
693
  if (batteryData?.chaGriSet !== undefined) {
@@ -793,12 +796,12 @@ export class SunspecBattery extends BaseSunspecDevice {
793
796
  const timestamp = new Date();
794
797
  try {
795
798
  // Read actual battery data from SunSpec device
796
- const batteryData = await this.sunspecClient.readBatteryData();
797
- const mpptDataList = await this.sunspecClient.readAllMPPTData();
799
+ const batteryData = await this.sunspecClient.readBatteryData(this.unitId);
800
+ const mpptDataList = await this.sunspecClient.readAllMPPTData(this.unitId);
798
801
  const mpptBatteryPowerW = this.extractBatteryPowerFromMPPT(mpptDataList);
799
802
  if (batteryData) {
800
- const advancedBatteryModel = this.sunspecClient.findModel(801);
801
- const batteryBaseModel = this.sunspecClient.findModel(SunspecModelId.BatteryBase);
803
+ const advancedBatteryModel = this.sunspecClient.findModel(this.unitId, 801);
804
+ const batteryBaseModel = this.sunspecClient.findModel(this.unitId, SunspecModelId.BatteryBase);
802
805
  // Determine battery power: prefer model 802 w field, then MPPT extraction, then undefined
803
806
  let batteryPowerW;
804
807
  if (batteryBaseModel && (batteryData.chargePower !== undefined || batteryData.dischargePower !== undefined)) {
@@ -908,7 +911,7 @@ export class SunspecBattery extends BaseSunspecDevice {
908
911
  return false;
909
912
  }
910
913
  console.log(`Setting battery storage mode to: ${mode}`);
911
- return this.sunspecClient.setStorageMode(mode);
914
+ return this.sunspecClient.setStorageMode(this.unitId, mode);
912
915
  }
913
916
  /**
914
917
  * Enable or disable grid charging
@@ -928,7 +931,7 @@ export class SunspecBattery extends BaseSunspecDevice {
928
931
  return false;
929
932
  }
930
933
  console.log(`${enable ? 'Enabling' : 'Disabling'} grid charging for battery`);
931
- return this.sunspecClient.enableGridCharging(enable);
934
+ return this.sunspecClient.enableGridCharging(this.unitId, enable);
932
935
  }
933
936
  /**
934
937
  * Set battery charging power from grid
@@ -946,7 +949,7 @@ export class SunspecBattery extends BaseSunspecDevice {
946
949
  return false;
947
950
  }
948
951
  console.log(`Setting battery charging power to: ${powerW}W`);
949
- return this.sunspecClient.writeBatteryControls({ wChaMax: powerW });
952
+ return this.sunspecClient.writeBatteryControls(this.unitId, { wChaMax: powerW });
950
953
  }
951
954
  /**
952
955
  * Get current battery control settings
@@ -958,7 +961,7 @@ export class SunspecBattery extends BaseSunspecDevice {
958
961
  console.error('Battery not connected');
959
962
  return null;
960
963
  }
961
- return this.sunspecClient.readBatteryControls();
964
+ return this.sunspecClient.readBatteryControls(this.unitId);
962
965
  }
963
966
  /**
964
967
  * Read full battery base data from Model 802
@@ -973,7 +976,7 @@ export class SunspecBattery extends BaseSunspecDevice {
973
976
  console.error('Battery not connected');
974
977
  return null;
975
978
  }
976
- return this.sunspecClient.readBatteryBaseData();
979
+ return this.sunspecClient.readBatteryBaseData(this.unitId);
977
980
  }
978
981
  /**
979
982
  * Write custom battery control settings
@@ -990,7 +993,7 @@ export class SunspecBattery extends BaseSunspecDevice {
990
993
  return false;
991
994
  }
992
995
  console.log('Writing battery controls:', controls);
993
- return this.sunspecClient.writeBatteryControls(controls);
996
+ return this.sunspecClient.writeBatteryControls(this.unitId, controls);
994
997
  }
995
998
  mapToEnyoStorageMode(storageMode) {
996
999
  switch (storageMode) {
@@ -1198,14 +1201,14 @@ export class SunspecMeter extends BaseSunspecDevice {
1198
1201
  // Connect with specific unit ID for meter
1199
1202
  await this.ensureConnected();
1200
1203
  // Check if meter models exist
1201
- const hasMeter = this.sunspecClient.findModel(SunspecModelId.Meter3Phase) !== undefined ||
1202
- this.sunspecClient.findModel(SunspecModelId.MeterWye) !== undefined ||
1203
- this.sunspecClient.findModel(SunspecModelId.MeterSinglePhase) !== undefined;
1204
+ const hasMeter = this.sunspecClient.findModel(this.unitId, SunspecModelId.Meter3Phase) !== undefined ||
1205
+ this.sunspecClient.findModel(this.unitId, SunspecModelId.MeterWye) !== undefined ||
1206
+ this.sunspecClient.findModel(this.unitId, SunspecModelId.MeterSinglePhase) !== undefined;
1204
1207
  if (!hasMeter) {
1205
1208
  throw new Error('No meter model found in device');
1206
1209
  }
1207
1210
  // Get device info
1208
- const commonData = await this.sunspecClient.readCommonBlock();
1211
+ const commonData = await this.sunspecClient.readCommonBlock(this.unitId);
1209
1212
  // Create or update appliance (skip if an existing appliance was provided)
1210
1213
  if (!this.applianceId) {
1211
1214
  try {
@@ -1259,8 +1262,8 @@ export class SunspecMeter extends BaseSunspecDevice {
1259
1262
  if (this.applianceId) {
1260
1263
  await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
1261
1264
  }
1262
- // Disconnect the client since meter uses its own connection
1263
- await this.sunspecClient.disconnect();
1265
+ // Close just this meter's unit; other devices on the same network device stay open.
1266
+ await this.sunspecClient.disconnectUnit(this.unitId);
1264
1267
  }
1265
1268
  /**
1266
1269
  * Update meter data and return data bus messages
@@ -1275,7 +1278,7 @@ export class SunspecMeter extends BaseSunspecDevice {
1275
1278
  const timestamp = new Date();
1276
1279
  try {
1277
1280
  // Read meter data
1278
- const meterData = await this.sunspecClient.readMeterData();
1281
+ const meterData = await this.sunspecClient.readMeterData(this.unitId);
1279
1282
  if (meterData) {
1280
1283
  const meterMessage = {
1281
1284
  id: randomUUID(),