@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.
- package/dist/cjs/sunspec-devices.cjs +45 -42
- package/dist/cjs/sunspec-interfaces.cjs +3 -0
- package/dist/cjs/sunspec-interfaces.d.cts +7 -2
- package/dist/cjs/sunspec-modbus-client.cjs +490 -277
- package/dist/cjs/sunspec-modbus-client.d.cts +115 -53
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/sunspec-devices.js +45 -42
- package/dist/sunspec-interfaces.d.ts +7 -2
- package/dist/sunspec-interfaces.js +3 -0
- package/dist/sunspec-modbus-client.d.ts +115 -53
- package/dist/sunspec-modbus-client.js +488 -277
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
25
|
-
private
|
|
26
|
-
private
|
|
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
|
|
61
|
+
* Disconnect from all units of this network device.
|
|
47
62
|
*
|
|
48
|
-
* Note: connection parameters are preserved so reconnect() can
|
|
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
|
-
*
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
*
|
|
60
|
-
*
|
|
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
|
|
65
|
-
* just-opened
|
|
66
|
-
* lock and ensure
|
|
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
|
|
96
|
+
private _openUnit;
|
|
69
97
|
/**
|
|
70
|
-
* Close the
|
|
98
|
+
* Close the Modbus instance for a single unit. Idempotent. Caller must hold the
|
|
99
|
+
* connection lock.
|
|
71
100
|
*/
|
|
72
|
-
private
|
|
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
|
|
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
|
-
|
|
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,19 +195,34 @@ 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>;
|
|
203
|
+
/**
|
|
204
|
+
* Read inverter data. Detects which SunSpec inverter model the device exposes —
|
|
205
|
+
* int+SF (101/103) or float (111/112/113) — by checking the discovered model directory,
|
|
206
|
+
* and dispatches to the appropriate decoder.
|
|
207
|
+
*/
|
|
208
|
+
readInverterData(unitId: number): Promise<SunspecInverterData | null>;
|
|
161
209
|
/**
|
|
162
|
-
* Read inverter data from Model
|
|
210
|
+
* Read three-phase inverter data from Model 103 (int + scale factor encoding).
|
|
163
211
|
*/
|
|
164
|
-
|
|
212
|
+
private readThreePhaseInverterData_IntSF;
|
|
165
213
|
/**
|
|
166
214
|
* Read single phase inverter data (Model 101)
|
|
167
215
|
*/
|
|
168
216
|
private readSinglePhaseInverterData;
|
|
217
|
+
/**
|
|
218
|
+
* Read inverter data from a float-variant model: 111 (single-phase), 112 (split-phase), or 113 (three-phase).
|
|
219
|
+
*
|
|
220
|
+
* All three models share the same SunSpec register layout — measurement fields are 32-bit IEEE 754 floats
|
|
221
|
+
* (no scale factors), status registers stay enum16, event registers stay bitfield32. Phase scope differs
|
|
222
|
+
* by model: 111 populates phase A only; 112 phases A+B; 113 all three. Unpopulated phase fields read NaN
|
|
223
|
+
* and are returned as undefined.
|
|
224
|
+
*/
|
|
225
|
+
private readFloatInverterData;
|
|
169
226
|
/**
|
|
170
227
|
* Extract inverter scale factors from a pre-read model buffer
|
|
171
228
|
*/
|
|
@@ -176,6 +233,11 @@ export declare class SunspecModbusClient {
|
|
|
176
233
|
*/
|
|
177
234
|
applyScaleFactor(value: number, scaleFactor: number, dataType?: 'uint16' | 'int16' | 'acc32', fieldName?: string, offset?: number, modelId?: number): number | undefined;
|
|
178
235
|
logRegisterRead(modelId: number, offset: number, fieldName: string, rawValue: number | string | undefined, dataType?: string): void;
|
|
236
|
+
/**
|
|
237
|
+
* Read a float32 (IEEE 754, big-endian, 2 registers) from a model buffer.
|
|
238
|
+
* SunSpec uses NaN as the "unimplemented" sentinel for floats; returns undefined in that case.
|
|
239
|
+
*/
|
|
240
|
+
private extractFloat32OrUndefined;
|
|
179
241
|
/**
|
|
180
242
|
* Read scale factors from Model 160 (MPPT)
|
|
181
243
|
*
|
|
@@ -191,7 +253,7 @@ export declare class SunspecModbusClient {
|
|
|
191
253
|
* Extract MPPT scale factors from a pre-read model buffer
|
|
192
254
|
*/
|
|
193
255
|
private extractMPPTScaleFactors;
|
|
194
|
-
readMPPTScaleFactors(): Promise<{
|
|
256
|
+
readMPPTScaleFactors(unitId: number): Promise<{
|
|
195
257
|
DCA_SF: number;
|
|
196
258
|
DCV_SF: number;
|
|
197
259
|
DCW_SF: number;
|
|
@@ -204,11 +266,11 @@ export declare class SunspecModbusClient {
|
|
|
204
266
|
/**
|
|
205
267
|
* Read MPPT data from Model 160
|
|
206
268
|
*/
|
|
207
|
-
readMPPTData(moduleId?: number): Promise<SunspecMPPTData | null>;
|
|
269
|
+
readMPPTData(unitId: number, moduleId?: number): Promise<SunspecMPPTData | null>;
|
|
208
270
|
/**
|
|
209
271
|
* Read all MPPT strings from Model 160 (Multiple MPPT)
|
|
210
272
|
*/
|
|
211
|
-
readAllMPPTData(): Promise<SunspecMPPTData[]>;
|
|
273
|
+
readAllMPPTData(unitId: number): Promise<SunspecMPPTData[]>;
|
|
212
274
|
/**
|
|
213
275
|
* Map battery charge state to human-readable name
|
|
214
276
|
* @param state The numeric charge state value
|
|
@@ -230,67 +292,67 @@ export declare class SunspecModbusClient {
|
|
|
230
292
|
/**
|
|
231
293
|
* Read battery base data from Model 802 (Battery Base)
|
|
232
294
|
*/
|
|
233
|
-
readBatteryBaseData(): Promise<SunspecBatteryBaseData | null>;
|
|
295
|
+
readBatteryBaseData(unitId: number): Promise<SunspecBatteryBaseData | null>;
|
|
234
296
|
/**
|
|
235
297
|
* Read battery data from Model 124 (Basic Storage) with fallback to Model 802 / Model 803
|
|
236
298
|
*/
|
|
237
|
-
readBatteryData(): Promise<SunspecBatteryData | null>;
|
|
299
|
+
readBatteryData(unitId: number): Promise<SunspecBatteryData | null>;
|
|
238
300
|
/**
|
|
239
301
|
* Write battery control settings to Model 124
|
|
240
302
|
*/
|
|
241
|
-
writeBatteryControls(controls: Partial<SunspecBatteryControls>): Promise<boolean>;
|
|
303
|
+
writeBatteryControls(unitId: number, controls: Partial<SunspecBatteryControls>): Promise<boolean>;
|
|
242
304
|
/**
|
|
243
305
|
* Set battery storage mode (simplified interface)
|
|
244
306
|
*/
|
|
245
|
-
setStorageMode(mode: SunspecStorageMode): Promise<boolean>;
|
|
307
|
+
setStorageMode(unitId: number, mode: SunspecStorageMode): Promise<boolean>;
|
|
246
308
|
/**
|
|
247
309
|
* Enable or disable grid charging
|
|
248
310
|
*/
|
|
249
|
-
enableGridCharging(enable: boolean): Promise<boolean>;
|
|
311
|
+
enableGridCharging(unitId: number, enable: boolean): Promise<boolean>;
|
|
250
312
|
/**
|
|
251
313
|
* Read battery control settings from Model 124 (Basic Storage Controls)
|
|
252
314
|
*/
|
|
253
|
-
readBatteryControls(): Promise<SunspecBatteryControls | null>;
|
|
315
|
+
readBatteryControls(unitId: number): Promise<SunspecBatteryControls | null>;
|
|
254
316
|
/**
|
|
255
317
|
* Read meter data from Model 201 (Single Phase) / Model 203 (Three Phase) / Model 204 (Split Phase)
|
|
256
318
|
*/
|
|
257
|
-
readMeterData(): Promise<SunspecMeterData | null>;
|
|
319
|
+
readMeterData(unitId: number): Promise<SunspecMeterData | null>;
|
|
258
320
|
/**
|
|
259
321
|
* Read common block data (Model 1)
|
|
260
322
|
*/
|
|
261
|
-
readCommonBlock(): Promise<any>;
|
|
323
|
+
readCommonBlock(unitId: number): Promise<any>;
|
|
262
324
|
/**
|
|
263
325
|
* Get serial number from device
|
|
264
326
|
*/
|
|
265
|
-
getSerialNumber(): Promise<string | undefined>;
|
|
327
|
+
getSerialNumber(unitId: number): Promise<string | undefined>;
|
|
266
328
|
/**
|
|
267
|
-
* Check if connected
|
|
329
|
+
* Check if a specific unit is connected on this network device
|
|
268
330
|
*/
|
|
269
|
-
isConnected(): boolean;
|
|
331
|
+
isConnected(unitId: number): boolean;
|
|
270
332
|
/**
|
|
271
|
-
* Check if connection is healthy
|
|
333
|
+
* Check if a specific unit's connection is healthy
|
|
272
334
|
*/
|
|
273
|
-
isHealthy(): boolean;
|
|
335
|
+
isHealthy(unitId: number): boolean;
|
|
274
336
|
/**
|
|
275
|
-
* Get connection health details
|
|
337
|
+
* Get connection health details (shared across all units on this network device)
|
|
276
338
|
*/
|
|
277
339
|
getConnectionHealth(): IConnectionHealth;
|
|
278
340
|
/**
|
|
279
341
|
* Read inverter settings from Model 121 (Inverter Settings)
|
|
280
342
|
*/
|
|
281
|
-
readInverterSettings(): Promise<SunspecInverterSettings | null>;
|
|
343
|
+
readInverterSettings(unitId: number): Promise<SunspecInverterSettings | null>;
|
|
282
344
|
/**
|
|
283
345
|
* Read inverter controls from Model 123 (Immediate Inverter Controls)
|
|
284
346
|
*/
|
|
285
|
-
readInverterControls(): Promise<SunspecInverterControls | null>;
|
|
347
|
+
readInverterControls(unitId: number): Promise<SunspecInverterControls | null>;
|
|
286
348
|
/**
|
|
287
349
|
* Write Block 121 - Inverter Basic Settings
|
|
288
350
|
*/
|
|
289
|
-
writeInverterSettings(settings: Partial<SunspecInverterSettings>): Promise<boolean>;
|
|
351
|
+
writeInverterSettings(unitId: number, settings: Partial<SunspecInverterSettings>): Promise<boolean>;
|
|
290
352
|
/**
|
|
291
353
|
* Write inverter controls to Model 123 (Immediate Inverter Controls)
|
|
292
354
|
*/
|
|
293
|
-
writeInverterControls(controls: Partial<SunspecInverterControls>): Promise<boolean>;
|
|
355
|
+
writeInverterControls(unitId: number, controls: Partial<SunspecInverterControls>): Promise<boolean>;
|
|
294
356
|
/**
|
|
295
357
|
* Set the inverter feed-in power limit using Model 123 (Immediate Inverter Controls)
|
|
296
358
|
*
|
|
@@ -301,5 +363,5 @@ export declare class SunspecModbusClient {
|
|
|
301
363
|
* @param limitW - Power limit in Watts, or null to remove the limit
|
|
302
364
|
* @returns true if successful, false otherwise
|
|
303
365
|
*/
|
|
304
|
-
setFeedInLimit(limitW: number | null): Promise<boolean>;
|
|
366
|
+
setFeedInLimit(unitId: number, limitW: number | null): Promise<boolean>;
|
|
305
367
|
}
|
package/dist/cjs/version.cjs
CHANGED
|
@@ -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.
|
|
12
|
+
exports.SDK_VERSION = '0.0.54';
|
|
13
13
|
/**
|
|
14
14
|
* Gets the current SDK version.
|
|
15
15
|
* @returns The semantic version string of the SDK
|
package/dist/cjs/version.d.cts
CHANGED
package/dist/sunspec-devices.js
CHANGED
|
@@ -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},
|
|
149
|
+
`opens=${stats.opens}, closes=${stats.closes}, openUnits=${stats.openUnits})`);
|
|
150
150
|
try {
|
|
151
|
-
|
|
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},
|
|
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.
|
|
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},
|
|
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
|
-
//
|
|
1263
|
-
await this.sunspecClient.
|
|
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(),
|
|
@@ -30,6 +30,9 @@ export declare enum SunspecModelId {
|
|
|
30
30
|
Common = 1,
|
|
31
31
|
Inverter3Phase = 103,
|
|
32
32
|
InverterSinglePhase = 101,
|
|
33
|
+
InverterSinglePhaseFloat = 111,
|
|
34
|
+
InverterSplitPhaseFloat = 112,
|
|
35
|
+
Inverter3PhaseFloat = 113,
|
|
33
36
|
MPPT = 160,
|
|
34
37
|
Battery = 124,
|
|
35
38
|
BatteryBase = 802,
|
|
@@ -74,10 +77,12 @@ export interface SunspecCommonBlock extends SunspecBlock {
|
|
|
74
77
|
deviceAddress?: number;
|
|
75
78
|
}
|
|
76
79
|
/**
|
|
77
|
-
* Inverter data structure
|
|
80
|
+
* Inverter data structure. Covers SunSpec int+SF inverter models 101/103 and float variants 111/112/113.
|
|
81
|
+
* The per-field offset comments below describe the int+SF (101/103) layout; float variants 111/112/113
|
|
82
|
+
* use 32-bit IEEE 754 float values at different offsets — see the float reader implementations.
|
|
78
83
|
*/
|
|
79
84
|
export interface SunspecInverterData extends SunspecBlock {
|
|
80
|
-
blockNumber: 103 | 101;
|
|
85
|
+
blockNumber: 103 | 101 | 113 | 112 | 111;
|
|
81
86
|
acCurrent?: number;
|
|
82
87
|
phaseACurrent?: number;
|
|
83
88
|
phaseBCurrent?: number;
|
|
@@ -17,6 +17,9 @@ export var SunspecModelId;
|
|
|
17
17
|
SunspecModelId[SunspecModelId["Common"] = 1] = "Common";
|
|
18
18
|
SunspecModelId[SunspecModelId["Inverter3Phase"] = 103] = "Inverter3Phase";
|
|
19
19
|
SunspecModelId[SunspecModelId["InverterSinglePhase"] = 101] = "InverterSinglePhase";
|
|
20
|
+
SunspecModelId[SunspecModelId["InverterSinglePhaseFloat"] = 111] = "InverterSinglePhaseFloat";
|
|
21
|
+
SunspecModelId[SunspecModelId["InverterSplitPhaseFloat"] = 112] = "InverterSplitPhaseFloat";
|
|
22
|
+
SunspecModelId[SunspecModelId["Inverter3PhaseFloat"] = 113] = "Inverter3PhaseFloat";
|
|
20
23
|
SunspecModelId[SunspecModelId["MPPT"] = 160] = "MPPT";
|
|
21
24
|
SunspecModelId[SunspecModelId["Battery"] = 124] = "Battery";
|
|
22
25
|
SunspecModelId[SunspecModelId["BatteryBase"] = 802] = "BatteryBase";
|