@enyo-energy/sunspec-sdk 0.0.50 → 0.0.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/sunspec-devices.cjs +45 -42
- package/dist/cjs/sunspec-modbus-client.cjs +448 -272
- package/dist/cjs/sunspec-modbus-client.d.cts +104 -46
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/sunspec-devices.js +45 -42
- package/dist/sunspec-modbus-client.d.ts +104 -46
- package/dist/sunspec-modbus-client.js +446 -272
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SunspecModbusClient = void 0;
|
|
4
|
+
exports.getOrCreateSunspecClient = getOrCreateSunspecClient;
|
|
5
|
+
exports.releaseSunspecClient = releaseSunspecClient;
|
|
4
6
|
/**
|
|
5
7
|
* Sunspec Modbus Client Implementation
|
|
6
8
|
*
|
|
@@ -23,165 +25,351 @@ const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
|
|
|
23
25
|
const EnergyAppModbusConnectionHealth_js_1 = require("@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusConnectionHealth.js");
|
|
24
26
|
const EnergyAppModbusFaultTolerantReader_js_1 = require("@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusFaultTolerantReader.js");
|
|
25
27
|
const EnergyAppModbusDataTypeConverter_js_1 = require("@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusDataTypeConverter.js");
|
|
28
|
+
/**
|
|
29
|
+
* Module-level registry: one SunspecModbusClient per network device (host:port).
|
|
30
|
+
* Refcounted so the underlying connection only tears down when the last consumer releases.
|
|
31
|
+
*/
|
|
32
|
+
const CLIENT_REGISTRY = new Map();
|
|
33
|
+
const registryKey = (host, port) => `${host}:${port}`;
|
|
34
|
+
/**
|
|
35
|
+
* Get (or create) the singleton SunspecModbusClient for this network device.
|
|
36
|
+
* Increments the refcount; pair with `releaseSunspecClient` on teardown.
|
|
37
|
+
*/
|
|
38
|
+
function getOrCreateSunspecClient(energyApp, host, port = 502) {
|
|
39
|
+
const key = registryKey(host, port);
|
|
40
|
+
let entry = CLIENT_REGISTRY.get(key);
|
|
41
|
+
if (!entry) {
|
|
42
|
+
entry = { client: new SunspecModbusClient(energyApp), refcount: 0 };
|
|
43
|
+
CLIENT_REGISTRY.set(key, entry);
|
|
44
|
+
}
|
|
45
|
+
entry.refcount++;
|
|
46
|
+
return entry.client;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Release a refcount on the network-device client. When the last consumer releases,
|
|
50
|
+
* the client is fully disconnected (all units closed) and removed from the registry.
|
|
51
|
+
*
|
|
52
|
+
* Releasing more times than acquired is a programming error and is logged; the count
|
|
53
|
+
* is clamped at zero so repeat releases are no-ops.
|
|
54
|
+
*/
|
|
55
|
+
async function releaseSunspecClient(host, port = 502) {
|
|
56
|
+
const key = registryKey(host, port);
|
|
57
|
+
const entry = CLIENT_REGISTRY.get(key);
|
|
58
|
+
if (!entry)
|
|
59
|
+
return;
|
|
60
|
+
if (entry.refcount <= 0) {
|
|
61
|
+
console.warn(`releaseSunspecClient(${host}:${port}): refcount already 0 — extra release ignored`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
entry.refcount--;
|
|
65
|
+
if (entry.refcount === 0) {
|
|
66
|
+
// Disconnect first, THEN remove from registry, so a concurrent getOrCreate
|
|
67
|
+
// doesn't bind a fresh client to the same host:port while sockets are still
|
|
68
|
+
// closing on the old one.
|
|
69
|
+
try {
|
|
70
|
+
await entry.client.disconnect();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
// Guard against the very-unlikely case where someone re-acquired during await.
|
|
74
|
+
if (CLIENT_REGISTRY.get(key) === entry && entry.refcount === 0) {
|
|
75
|
+
CLIENT_REGISTRY.delete(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
26
80
|
class SunspecModbusClient {
|
|
27
81
|
energyApp;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
82
|
+
// Per-unit Modbus instance/reader/discovered-models maps. One client = one network device,
|
|
83
|
+
// many unit IDs share that client (each unit ID is one EnergyAppModbusInstance underneath).
|
|
84
|
+
modbusInstances = new Map();
|
|
85
|
+
faultTolerantReaders = new Map();
|
|
86
|
+
discoveredModelsByUnit = new Map();
|
|
31
87
|
connectionHealth;
|
|
32
|
-
faultTolerantReader = null;
|
|
33
88
|
modbusDataTypeConverter;
|
|
34
89
|
connectionParams = null;
|
|
90
|
+
// Per-unit known parameters so reconnect() can replay every unit ID we've seen.
|
|
91
|
+
knownUnits = new Set();
|
|
35
92
|
autoReconnectEnabled = true;
|
|
36
93
|
openCount = 0;
|
|
37
94
|
closeCount = 0;
|
|
38
|
-
|
|
95
|
+
// Serializes connection-state transitions so concurrent callers cannot open duplicate
|
|
96
|
+
// sockets for the same unit ID while one is still alive.
|
|
97
|
+
operationChain = Promise.resolve();
|
|
39
98
|
constructor(energyApp) {
|
|
40
99
|
this.energyApp = energyApp;
|
|
41
100
|
this.connectionHealth = new EnergyAppModbusConnectionHealth_js_1.EnergyAppModbusConnectionHealth();
|
|
42
101
|
this.modbusDataTypeConverter = new EnergyAppModbusDataTypeConverter_js_1.EnergyAppModbusDataTypeConverter();
|
|
43
102
|
}
|
|
44
103
|
/**
|
|
45
|
-
* Connect to Modbus device
|
|
104
|
+
* Connect to a Modbus device unit. Multiple unit IDs on the same network device share this
|
|
105
|
+
* client; calling connect() with different unit IDs adds each as a separately-managed unit.
|
|
106
|
+
* The first call locks in the host/port for this client; later calls for a different host
|
|
107
|
+
* are rejected.
|
|
46
108
|
* @param host Primary host (hostname) to connect to
|
|
47
109
|
* @param port Modbus port (default 502)
|
|
48
110
|
* @param unitId Modbus unit ID (default 1)
|
|
49
111
|
* @param secondaryHost Optional secondary host (ipAddress) for fallback during reconnection
|
|
50
112
|
*/
|
|
51
113
|
async connect(host, port = 502, unitId = 1, secondaryHost) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
114
|
+
return this.withConnectionLock(async () => {
|
|
115
|
+
if (this.connectionParams) {
|
|
116
|
+
if (this.connectionParams.primaryHost !== host || this.connectionParams.port !== port) {
|
|
117
|
+
throw new Error(`SunspecModbusClient is bound to ${this.connectionParams.primaryHost}:${this.connectionParams.port}; ` +
|
|
118
|
+
`cannot reuse it for ${host}:${port}. Use getOrCreateSunspecClient() per (host, port).`);
|
|
119
|
+
}
|
|
120
|
+
if (secondaryHost && !this.connectionParams.secondaryHost) {
|
|
121
|
+
this.connectionParams.secondaryHost = secondaryHost;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
this.connectionParams = { primaryHost: host, secondaryHost, port };
|
|
126
|
+
}
|
|
127
|
+
this.knownUnits.add(unitId);
|
|
128
|
+
if (this.modbusInstances.has(unitId)) {
|
|
129
|
+
console.debug(`connect(): unit ${unitId} on ${host}:${port} already open, skipping`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await this._openUnit(host, port, unitId);
|
|
133
|
+
console.log(`Connected to Sunspec device at ${host}:${port} unit ${unitId} (opens=${this.openCount}, closes=${this.closeCount}, openUnits=${this.modbusInstances.size})`);
|
|
62
134
|
});
|
|
63
|
-
// Create fault-tolerant reader with connection health monitoring
|
|
64
|
-
if (this.modbusClient) {
|
|
65
|
-
this.faultTolerantReader = new EnergyAppModbusFaultTolerantReader_js_1.EnergyAppModbusFaultTolerantReader(this.modbusClient, this.connectionHealth);
|
|
66
|
-
}
|
|
67
|
-
this.connected = true;
|
|
68
|
-
this.connectionHealth.recordSuccess();
|
|
69
|
-
this.recordOpen();
|
|
70
|
-
console.log(`Connected to Sunspec device at ${host}:${port} unit ${unitId} (opens=${this.openCount}, closes=${this.closeCount}, current=${this.currentlyOpen})`);
|
|
71
135
|
}
|
|
72
136
|
/**
|
|
73
|
-
* Disconnect from
|
|
137
|
+
* Disconnect from all units of this network device.
|
|
74
138
|
*
|
|
75
|
-
* Note: connection parameters are preserved so reconnect() can
|
|
76
|
-
* They will be overwritten by the next connect() call anyway.
|
|
139
|
+
* Note: connection parameters and the set of known units are preserved so reconnect() can
|
|
140
|
+
* be called afterwards. They will be overwritten by the next connect() call anyway.
|
|
77
141
|
*/
|
|
78
142
|
async disconnect() {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.connected = false;
|
|
84
|
-
this.discoveredModels.clear();
|
|
85
|
-
this.recordClose();
|
|
143
|
+
return this.withConnectionLock(async () => {
|
|
144
|
+
if (this.modbusInstances.size === 0) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
86
147
|
const host = this.connectionParams?.primaryHost ?? 'unknown';
|
|
87
148
|
const port = this.connectionParams?.port ?? 0;
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
149
|
+
const units = [...this.modbusInstances.keys()];
|
|
150
|
+
for (const unitId of units) {
|
|
151
|
+
await this._closeUnit(unitId);
|
|
152
|
+
}
|
|
153
|
+
console.log(`Disconnected from Sunspec device at ${host}:${port} units [${units.join(',')}] (opens=${this.openCount}, closes=${this.closeCount})`);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Disconnect a single unit on this network device. Other units on the same device stay open.
|
|
158
|
+
*/
|
|
159
|
+
async disconnectUnit(unitId) {
|
|
160
|
+
return this.withConnectionLock(async () => {
|
|
161
|
+
if (!this.modbusInstances.has(unitId))
|
|
162
|
+
return;
|
|
163
|
+
const host = this.connectionParams?.primaryHost ?? 'unknown';
|
|
164
|
+
const port = this.connectionParams?.port ?? 0;
|
|
165
|
+
await this._closeUnit(unitId);
|
|
166
|
+
console.log(`Disconnected unit ${unitId} from Sunspec device at ${host}:${port} (opens=${this.openCount}, closes=${this.closeCount})`);
|
|
167
|
+
});
|
|
91
168
|
}
|
|
92
169
|
/**
|
|
93
|
-
* Reconnect using stored connection
|
|
94
|
-
* First tries primaryHost (hostname), then falls back to secondaryHost
|
|
95
|
-
* Returns true if
|
|
170
|
+
* Reconnect every previously-known unit on this network device using stored connection
|
|
171
|
+
* parameters. First tries primaryHost (hostname), then falls back to secondaryHost
|
|
172
|
+
* (ipAddress) if available. Returns true only if every known unit reconnected successfully.
|
|
96
173
|
*/
|
|
97
174
|
async reconnect() {
|
|
98
|
-
|
|
99
|
-
|
|
175
|
+
return this.withConnectionLock(async () => {
|
|
176
|
+
if (!this.connectionParams) {
|
|
177
|
+
console.error('Cannot reconnect: no connection parameters stored. Call connect() first.');
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
if (this.knownUnits.size === 0) {
|
|
181
|
+
console.error('Cannot reconnect: no known unit IDs. Call connect(unitId) at least once first.');
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
const { primaryHost, secondaryHost, port } = this.connectionParams;
|
|
185
|
+
const units = [...this.knownUnits];
|
|
186
|
+
console.log(`Attempting to reconnect to primary host ${primaryHost}:${port} units [${units.join(',')}]...`);
|
|
187
|
+
const primarySuccess = await this.attemptConnection(primaryHost, port, units);
|
|
188
|
+
if (primarySuccess) {
|
|
189
|
+
console.log(`Successfully reconnected to primary host ${primaryHost}:${port} units [${units.join(',')}]`);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
if (secondaryHost && secondaryHost !== primaryHost) {
|
|
193
|
+
console.log(`Primary host failed, attempting secondary host ${secondaryHost}:${port} units [${units.join(',')}]...`);
|
|
194
|
+
const secondarySuccess = await this.attemptConnection(secondaryHost, port, units);
|
|
195
|
+
if (secondarySuccess) {
|
|
196
|
+
console.log(`Successfully reconnected to secondary host ${secondaryHost}:${port} units [${units.join(',')}]`);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
console.error(`Reconnection failed to all available hosts`);
|
|
201
|
+
return false;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Reconnect a single previously-known unit on this network device. Other units on the
|
|
206
|
+
* same client stay open. Useful when one device's polling loop detects a dropped
|
|
207
|
+
* connection and only needs to recover its own unit, not thrash siblings.
|
|
208
|
+
*
|
|
209
|
+
* Returns true if the unit reconnected successfully (on primary or secondary host).
|
|
210
|
+
*/
|
|
211
|
+
async reconnectUnit(unitId) {
|
|
212
|
+
return this.withConnectionLock(async () => {
|
|
213
|
+
if (!this.connectionParams) {
|
|
214
|
+
console.error(`Cannot reconnectUnit(${unitId}): no connection parameters stored. Call connect() first.`);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
const { primaryHost, secondaryHost, port } = this.connectionParams;
|
|
218
|
+
console.log(`Attempting to reconnect unit ${unitId} on primary host ${primaryHost}:${port}...`);
|
|
219
|
+
const primarySuccess = await this.attemptConnection(primaryHost, port, [unitId]);
|
|
220
|
+
if (primarySuccess) {
|
|
221
|
+
console.log(`Successfully reconnected unit ${unitId} on primary host ${primaryHost}:${port}`);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (secondaryHost && secondaryHost !== primaryHost) {
|
|
225
|
+
console.log(`Primary host failed for unit ${unitId}, attempting secondary host ${secondaryHost}:${port}...`);
|
|
226
|
+
const secondarySuccess = await this.attemptConnection(secondaryHost, port, [unitId]);
|
|
227
|
+
if (secondarySuccess) {
|
|
228
|
+
console.log(`Successfully reconnected unit ${unitId} on secondary host ${secondaryHost}:${port}`);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
console.error(`Reconnection failed for unit ${unitId} on all available hosts`);
|
|
100
233
|
return false;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Attempt to (re)open every requested unit on a specific host. Caller must hold the
|
|
238
|
+
* connection lock. Closes any pre-existing per-unit instances first. Returns true only
|
|
239
|
+
* if every unit succeeded.
|
|
240
|
+
*/
|
|
241
|
+
async attemptConnection(host, port, units) {
|
|
242
|
+
for (const unitId of units) {
|
|
243
|
+
if (this.modbusInstances.has(unitId)) {
|
|
244
|
+
await this._closeUnit(unitId);
|
|
245
|
+
}
|
|
101
246
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
console.log(`Successfully reconnected to primary host ${primaryHost}:${port} unit ${unitId}`);
|
|
247
|
+
try {
|
|
248
|
+
for (const unitId of units) {
|
|
249
|
+
await this._openUnit(host, port, unitId);
|
|
250
|
+
}
|
|
251
|
+
console.log(`Connection attempt to ${host}:${port} units [${units.join(',')}] succeeded (opens=${this.openCount}, closes=${this.closeCount}, openUnits=${this.modbusInstances.size})`);
|
|
108
252
|
return true;
|
|
109
253
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const secondarySuccess = await this.attemptConnection(secondaryHost, port, unitId);
|
|
114
|
-
if (secondarySuccess) {
|
|
115
|
-
console.log(`Successfully reconnected to secondary host ${secondaryHost}:${port} unit ${unitId}`);
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
console.error(`Connection attempt to ${host}:${port} failed: ${error}`);
|
|
256
|
+
return false;
|
|
118
257
|
}
|
|
119
|
-
console.error(`Reconnection failed to all available hosts`);
|
|
120
|
-
this.connected = false;
|
|
121
|
-
return false;
|
|
122
258
|
}
|
|
123
259
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
260
|
+
* Open a new Modbus instance for one unit ID and wire up its fault-tolerant reader.
|
|
261
|
+
* On any failure, the just-opened instance (if any) is closed so we never leak. Caller must
|
|
262
|
+
* hold the connection lock and ensure the unit is not already open.
|
|
126
263
|
*/
|
|
127
|
-
async
|
|
264
|
+
async _openUnit(host, port, unitId) {
|
|
265
|
+
let candidate = null;
|
|
128
266
|
try {
|
|
129
|
-
|
|
130
|
-
if (this.modbusClient) {
|
|
131
|
-
const wasConnected = this.connected;
|
|
132
|
-
try {
|
|
133
|
-
await this.modbusClient.disconnect();
|
|
134
|
-
}
|
|
135
|
-
catch (e) {
|
|
136
|
-
// Ignore disconnect errors during reconnection
|
|
137
|
-
}
|
|
138
|
-
this.modbusClient = null;
|
|
139
|
-
this.faultTolerantReader = null;
|
|
140
|
-
if (wasConnected) {
|
|
141
|
-
this.connected = false;
|
|
142
|
-
this.recordClose();
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
// Attempt connection
|
|
146
|
-
this.modbusClient = await this.energyApp.useModbus().connect({
|
|
267
|
+
candidate = await this.energyApp.useModbus().connect({
|
|
147
268
|
host,
|
|
148
269
|
port,
|
|
149
270
|
unitId,
|
|
150
271
|
timeout: 5000
|
|
151
272
|
});
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this.faultTolerantReader = new EnergyAppModbusFaultTolerantReader_js_1.EnergyAppModbusFaultTolerantReader(this.modbusClient, this.connectionHealth);
|
|
273
|
+
if (!candidate) {
|
|
274
|
+
throw new Error(`useModbus().connect returned null for ${host}:${port} unit ${unitId}`);
|
|
155
275
|
}
|
|
156
|
-
|
|
276
|
+
const reader = new EnergyAppModbusFaultTolerantReader_js_1.EnergyAppModbusFaultTolerantReader(candidate, this.connectionHealth);
|
|
277
|
+
this.modbusInstances.set(unitId, candidate);
|
|
278
|
+
this.faultTolerantReaders.set(unitId, reader);
|
|
157
279
|
this.connectionHealth.recordSuccess();
|
|
158
280
|
this.recordOpen();
|
|
159
|
-
console.log(`Connection attempt to ${host}:${port} unit ${unitId} succeeded (opens=${this.openCount}, closes=${this.closeCount}, current=${this.currentlyOpen})`);
|
|
160
|
-
return true;
|
|
161
281
|
}
|
|
162
|
-
catch (
|
|
163
|
-
|
|
164
|
-
|
|
282
|
+
catch (err) {
|
|
283
|
+
if (candidate) {
|
|
284
|
+
try {
|
|
285
|
+
await candidate.disconnect();
|
|
286
|
+
}
|
|
287
|
+
catch { /* ignore */ }
|
|
288
|
+
}
|
|
289
|
+
this.modbusInstances.delete(unitId);
|
|
290
|
+
this.faultTolerantReaders.delete(unitId);
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Close the Modbus instance for a single unit. Idempotent. Caller must hold the
|
|
296
|
+
* connection lock.
|
|
297
|
+
*/
|
|
298
|
+
async _closeUnit(unitId) {
|
|
299
|
+
const instance = this.modbusInstances.get(unitId);
|
|
300
|
+
const wasOpen = instance !== undefined;
|
|
301
|
+
this.modbusInstances.delete(unitId);
|
|
302
|
+
this.faultTolerantReaders.delete(unitId);
|
|
303
|
+
this.discoveredModelsByUnit.delete(unitId);
|
|
304
|
+
if (instance) {
|
|
305
|
+
try {
|
|
306
|
+
await instance.disconnect();
|
|
307
|
+
}
|
|
308
|
+
catch { /* ignore */ }
|
|
165
309
|
}
|
|
310
|
+
if (wasOpen)
|
|
311
|
+
this.recordClose();
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Run `fn` with exclusive access to connection-state transitions. Subsequent calls queue
|
|
315
|
+
* behind any in-flight one. A rejected `fn` does not poison the chain for later callers.
|
|
316
|
+
*/
|
|
317
|
+
withConnectionLock(fn) {
|
|
318
|
+
const run = this.operationChain.then(fn, fn);
|
|
319
|
+
this.operationChain = run.catch(() => undefined);
|
|
320
|
+
return run;
|
|
166
321
|
}
|
|
167
322
|
recordOpen() {
|
|
168
323
|
this.openCount++;
|
|
169
|
-
this.
|
|
324
|
+
const expected = this.modbusInstances.size;
|
|
325
|
+
const delta = this.openCount - this.closeCount;
|
|
326
|
+
if (delta > expected) {
|
|
327
|
+
console.error(`SunspecModbusClient: open/close imbalance — ` +
|
|
328
|
+
`opens=${this.openCount}, closes=${this.closeCount}, instances=${expected}. ` +
|
|
329
|
+
`This indicates a code path bypassing the connection lock — please investigate.`);
|
|
330
|
+
}
|
|
170
331
|
}
|
|
171
332
|
recordClose() {
|
|
172
333
|
this.closeCount++;
|
|
173
|
-
if (this.
|
|
174
|
-
this.
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
console.warn(`SunspecModbusClient: closeCount incremented while currentlyOpen was 0 — possible double-close`);
|
|
334
|
+
if (this.closeCount > this.openCount) {
|
|
335
|
+
console.warn(`SunspecModbusClient: closeCount=${this.closeCount} exceeded openCount=${this.openCount} — possible double-close`);
|
|
178
336
|
}
|
|
179
337
|
}
|
|
180
338
|
/**
|
|
181
|
-
* Get cumulative open/close counts for this client. Useful for
|
|
339
|
+
* Get cumulative open/close counts for this client (across all unit IDs). Useful for
|
|
340
|
+
* spotting connection leaks.
|
|
182
341
|
*/
|
|
183
342
|
getConnectionStats() {
|
|
184
|
-
return { opens: this.openCount, closes: this.closeCount,
|
|
343
|
+
return { opens: this.openCount, closes: this.closeCount, openUnits: this.modbusInstances.size };
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Get the EnergyAppModbusInstance for a unit, throwing if it isn't open.
|
|
347
|
+
*/
|
|
348
|
+
getInstance(unitId) {
|
|
349
|
+
const inst = this.modbusInstances.get(unitId);
|
|
350
|
+
if (!inst)
|
|
351
|
+
throw new Error(`Modbus instance for unit ${unitId} not initialized — call connect(host, port, ${unitId}) first`);
|
|
352
|
+
return inst;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Get the fault-tolerant reader for a unit, throwing if it isn't open.
|
|
356
|
+
*/
|
|
357
|
+
getReader(unitId) {
|
|
358
|
+
const reader = this.faultTolerantReaders.get(unitId);
|
|
359
|
+
if (!reader)
|
|
360
|
+
throw new Error(`Fault-tolerant reader for unit ${unitId} not initialized — call connect(host, port, ${unitId}) first`);
|
|
361
|
+
return reader;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Get (or create) the discovered-models map for a unit.
|
|
365
|
+
*/
|
|
366
|
+
getModelsMap(unitId) {
|
|
367
|
+
let m = this.discoveredModelsByUnit.get(unitId);
|
|
368
|
+
if (!m) {
|
|
369
|
+
m = new Map();
|
|
370
|
+
this.discoveredModelsByUnit.set(unitId, m);
|
|
371
|
+
}
|
|
372
|
+
return m;
|
|
185
373
|
}
|
|
186
374
|
/**
|
|
187
375
|
* Enable or disable automatic reconnection
|
|
@@ -199,16 +387,14 @@ class SunspecModbusClient {
|
|
|
199
387
|
/**
|
|
200
388
|
* Detect the base address and addressing mode (0-based or 1-based) for SunSpec
|
|
201
389
|
*/
|
|
202
|
-
async detectSunspecBaseAddress(customBaseAddress) {
|
|
203
|
-
|
|
204
|
-
throw new Error('Modbus client not initialized');
|
|
205
|
-
}
|
|
390
|
+
async detectSunspecBaseAddress(unitId, customBaseAddress) {
|
|
391
|
+
const instance = this.getInstance(unitId);
|
|
206
392
|
// If custom base address provided, try it first (1-based, then 0-based variant)
|
|
207
393
|
if (customBaseAddress !== undefined) {
|
|
208
394
|
console.log(`Detect models for custom base address '${customBaseAddress}' ...`);
|
|
209
395
|
// Try 0-based at custom address
|
|
210
396
|
try {
|
|
211
|
-
const sunspecId = await
|
|
397
|
+
const sunspecId = await instance.readRegisterStringValue(customBaseAddress, 2);
|
|
212
398
|
if (sunspecId.includes('SunS')) {
|
|
213
399
|
console.log(`Detected 0-based addressing mode (base address: ${customBaseAddress})`);
|
|
214
400
|
return {
|
|
@@ -223,7 +409,7 @@ class SunspecModbusClient {
|
|
|
223
409
|
}
|
|
224
410
|
// Try 1-based at custom address (customBaseAddress + 1)
|
|
225
411
|
try {
|
|
226
|
-
const sunspecId = await
|
|
412
|
+
const sunspecId = await instance.readRegisterStringValue(customBaseAddress + 1, 2);
|
|
227
413
|
if (sunspecId.includes('SunS')) {
|
|
228
414
|
console.log(`Detected 1-based addressing mode (base address: ${customBaseAddress + 1})`);
|
|
229
415
|
return {
|
|
@@ -240,7 +426,7 @@ class SunspecModbusClient {
|
|
|
240
426
|
else {
|
|
241
427
|
// Try 1-based addressing first (most common)
|
|
242
428
|
try {
|
|
243
|
-
const sunspecId = await
|
|
429
|
+
const sunspecId = await instance.readRegisterStringValue(40001, 2);
|
|
244
430
|
if (sunspecId.includes('SunS')) {
|
|
245
431
|
console.log('Detected 1-based addressing mode (base address: 40001)');
|
|
246
432
|
return {
|
|
@@ -255,7 +441,7 @@ class SunspecModbusClient {
|
|
|
255
441
|
}
|
|
256
442
|
// Try 0-based addressing
|
|
257
443
|
try {
|
|
258
|
-
const sunspecId = await
|
|
444
|
+
const sunspecId = await instance.readRegisterStringValue(40000, 2);
|
|
259
445
|
if (sunspecId.includes('SunS')) {
|
|
260
446
|
console.log('Detected 0-based addressing mode (base address: 40000)');
|
|
261
447
|
return {
|
|
@@ -275,27 +461,22 @@ class SunspecModbusClient {
|
|
|
275
461
|
throw new Error(`Device is not SunSpec compliant - "SunS" identifier not found at addresses ${addressesChecked}`);
|
|
276
462
|
}
|
|
277
463
|
/**
|
|
278
|
-
* Discover all available Sunspec models
|
|
464
|
+
* Discover all available Sunspec models for a unit
|
|
279
465
|
* Automatically detects base address (40000 or 40001) and scans from there
|
|
280
466
|
*/
|
|
281
|
-
async discoverModels(customBaseAddress) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
this.discoveredModels.clear();
|
|
467
|
+
async discoverModels(unitId, customBaseAddress) {
|
|
468
|
+
const instance = this.getInstance(unitId);
|
|
469
|
+
const models = this.getModelsMap(unitId);
|
|
470
|
+
models.clear();
|
|
286
471
|
const maxAddress = 50000; // Safety limit
|
|
287
472
|
let currentAddress = 0;
|
|
288
|
-
console.log(
|
|
473
|
+
console.log(`Starting Sunspec model discovery for unit ${unitId}...`);
|
|
289
474
|
try {
|
|
290
475
|
// Detect the base address and addressing mode
|
|
291
|
-
const addressInfo = await this.detectSunspecBaseAddress(customBaseAddress);
|
|
476
|
+
const addressInfo = await this.detectSunspecBaseAddress(unitId, customBaseAddress);
|
|
292
477
|
currentAddress = addressInfo.nextAddress;
|
|
293
478
|
while (currentAddress < maxAddress) {
|
|
294
|
-
|
|
295
|
-
if (!this.modbusClient) {
|
|
296
|
-
throw new Error('Modbus client not initialized');
|
|
297
|
-
}
|
|
298
|
-
const buffer = await this.modbusClient.readHoldingRegisters(currentAddress, 2);
|
|
479
|
+
const buffer = await instance.readHoldingRegisters(currentAddress, 2);
|
|
299
480
|
const modelData = [buffer.readUInt16BE(0), buffer.readUInt16BE(2)];
|
|
300
481
|
if (!modelData || modelData.length < 2) {
|
|
301
482
|
console.log(`No data at address ${currentAddress}, ending discovery`);
|
|
@@ -314,23 +495,23 @@ class SunspecModbusClient {
|
|
|
314
495
|
address: currentAddress,
|
|
315
496
|
length: modelLength
|
|
316
497
|
};
|
|
317
|
-
|
|
318
|
-
console.log(`Discovered Model ${modelId} at address ${currentAddress} with length ${modelLength}`);
|
|
498
|
+
models.set(modelId, model);
|
|
499
|
+
console.log(`Discovered Model ${modelId} at address ${currentAddress} with length ${modelLength} (unit ${unitId})`);
|
|
319
500
|
// Jump to next model: current address + 2 (header) + model length
|
|
320
501
|
currentAddress = currentAddress + 2 + modelLength;
|
|
321
502
|
}
|
|
322
503
|
}
|
|
323
504
|
catch (error) {
|
|
324
|
-
console.error(`Error during model discovery at address ${currentAddress}: ${error}`);
|
|
505
|
+
console.error(`Error during model discovery at address ${currentAddress} (unit ${unitId}): ${error}`);
|
|
325
506
|
}
|
|
326
|
-
console.log(`Discovery complete. Found ${
|
|
327
|
-
return
|
|
507
|
+
console.log(`Discovery complete for unit ${unitId}. Found ${models.size} models`);
|
|
508
|
+
return models;
|
|
328
509
|
}
|
|
329
510
|
/**
|
|
330
|
-
* Find a specific model by ID
|
|
511
|
+
* Find a specific model by ID for a given unit
|
|
331
512
|
*/
|
|
332
|
-
findModel(modelId) {
|
|
333
|
-
return this.
|
|
513
|
+
findModel(unitId, modelId) {
|
|
514
|
+
return this.discoveredModelsByUnit.get(unitId)?.get(modelId);
|
|
334
515
|
}
|
|
335
516
|
/**
|
|
336
517
|
* Check if a value is "unimplemented" according to Sunspec specification
|
|
@@ -406,17 +587,15 @@ class SunspecModbusClient {
|
|
|
406
587
|
* Read an entire model's register block in a single Modbus call.
|
|
407
588
|
* Returns a Buffer containing all registers for the model.
|
|
408
589
|
*/
|
|
409
|
-
async readModelBlock(model) {
|
|
410
|
-
|
|
411
|
-
throw new Error('Fault-tolerant reader not initialized');
|
|
412
|
-
}
|
|
590
|
+
async readModelBlock(unitId, model) {
|
|
591
|
+
const reader = this.getReader(unitId);
|
|
413
592
|
// Read model.length + 2 registers: the 2-register header (ID + length) plus all data registers.
|
|
414
593
|
// This way buffer offsets match the convention used throughout: offset 0 = model ID,
|
|
415
594
|
// offset 1 = model length, offset 2 = first data register, etc.
|
|
416
595
|
const totalRegisters = model.length + 2;
|
|
417
|
-
const result = await
|
|
596
|
+
const result = await reader.readHoldingRegisters(model.address, totalRegisters);
|
|
418
597
|
if (!result.success || !result.value) {
|
|
419
|
-
throw new Error(`Failed to read model block ${model.id} at address ${model.address}: ${result.error?.message || 'Unknown error'}`);
|
|
598
|
+
throw new Error(`Failed to read model block ${model.id} at address ${model.address} (unit ${unitId}): ${result.error?.message || 'Unknown error'}`);
|
|
420
599
|
}
|
|
421
600
|
this.connectionHealth.recordSuccess();
|
|
422
601
|
return result.value;
|
|
@@ -441,12 +620,10 @@ class SunspecModbusClient {
|
|
|
441
620
|
/**
|
|
442
621
|
* Helper to read register value(s) using the fault-tolerant reader with data type conversion
|
|
443
622
|
*/
|
|
444
|
-
async readRegisterValue(address, quantity = 1, dataType) {
|
|
445
|
-
|
|
446
|
-
throw new Error('Fault-tolerant reader not initialized');
|
|
447
|
-
}
|
|
623
|
+
async readRegisterValue(unitId, address, quantity = 1, dataType) {
|
|
624
|
+
const reader = this.getReader(unitId);
|
|
448
625
|
try {
|
|
449
|
-
const result = await
|
|
626
|
+
const result = await reader.readHoldingRegisters(address, quantity);
|
|
450
627
|
// Check if the read was successful
|
|
451
628
|
if (!result.success || !result.value) {
|
|
452
629
|
throw new Error(`Failed to read register at address ${address}: ${result.error?.message || 'Unknown error'}`);
|
|
@@ -467,10 +644,8 @@ class SunspecModbusClient {
|
|
|
467
644
|
/**
|
|
468
645
|
* Helper to write register value(s)
|
|
469
646
|
*/
|
|
470
|
-
async writeRegisterValue(address, value, dataType = 'uint16') {
|
|
471
|
-
|
|
472
|
-
throw new Error('Modbus client not initialized');
|
|
473
|
-
}
|
|
647
|
+
async writeRegisterValue(unitId, address, value, dataType = 'uint16') {
|
|
648
|
+
const instance = this.getInstance(unitId);
|
|
474
649
|
try {
|
|
475
650
|
// Convert value to array of register values
|
|
476
651
|
let registerValues;
|
|
@@ -499,9 +674,9 @@ class SunspecModbusClient {
|
|
|
499
674
|
}
|
|
500
675
|
}
|
|
501
676
|
// Write to holding registers
|
|
502
|
-
await
|
|
677
|
+
await instance.writeMultipleRegisters(address, registerValues);
|
|
503
678
|
this.connectionHealth.recordSuccess();
|
|
504
|
-
console.log(`Successfully wrote value ${value} to register ${address}`);
|
|
679
|
+
console.log(`Successfully wrote value ${value} to register ${address} (unit ${unitId})`);
|
|
505
680
|
return true;
|
|
506
681
|
}
|
|
507
682
|
catch (error) {
|
|
@@ -513,23 +688,23 @@ class SunspecModbusClient {
|
|
|
513
688
|
/**
|
|
514
689
|
* Read inverter data from Model 101 (Single Phase) / Model 103 (Three Phase)
|
|
515
690
|
*/
|
|
516
|
-
async readInverterData() {
|
|
517
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Inverter3Phase);
|
|
691
|
+
async readInverterData(unitId) {
|
|
692
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Inverter3Phase);
|
|
518
693
|
if (!model) {
|
|
519
|
-
console.
|
|
520
|
-
const singlePhaseModel = this.findModel(sunspec_interfaces_js_1.SunspecModelId.InverterSinglePhase);
|
|
694
|
+
console.debug(`Inverter model 103 not found on unit ${unitId}, trying single phase model 101`);
|
|
695
|
+
const singlePhaseModel = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.InverterSinglePhase);
|
|
521
696
|
if (!singlePhaseModel) {
|
|
522
|
-
console.error(
|
|
697
|
+
console.error(`No inverter model found on unit ${unitId}`);
|
|
523
698
|
return null;
|
|
524
699
|
}
|
|
525
700
|
console.warn('IMPORTANT: Working with single-phase inverter, but 3-phase is expected!');
|
|
526
|
-
return this.readSinglePhaseInverterData(singlePhaseModel);
|
|
701
|
+
return this.readSinglePhaseInverterData(unitId, singlePhaseModel);
|
|
527
702
|
}
|
|
528
|
-
console.
|
|
703
|
+
console.debug(`Found 3-phase inverter model 103 at address ${model.address} with length ${model.length} (unit ${unitId})`);
|
|
529
704
|
try {
|
|
530
705
|
// Read entire model block in a single Modbus call
|
|
531
|
-
console.
|
|
532
|
-
const buffer = await this.readModelBlock(model);
|
|
706
|
+
console.debug(`Reading Inverter Data from Model ${model.id} at base address: ${model.address} (unit ${unitId})`);
|
|
707
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
533
708
|
// Extract all scale factors from buffer
|
|
534
709
|
const scaleFactors = this.extractInverterScaleFactors(buffer);
|
|
535
710
|
// Extract raw values from buffer
|
|
@@ -610,11 +785,11 @@ class SunspecModbusClient {
|
|
|
610
785
|
/**
|
|
611
786
|
* Read single phase inverter data (Model 101)
|
|
612
787
|
*/
|
|
613
|
-
async readSinglePhaseInverterData(model) {
|
|
788
|
+
async readSinglePhaseInverterData(unitId, model) {
|
|
614
789
|
try {
|
|
615
|
-
console.
|
|
790
|
+
console.debug(`Reading Single-Phase Inverter Data from Model 101 at base address: ${model.address} (unit ${unitId})`);
|
|
616
791
|
// Read entire model block in a single Modbus call
|
|
617
|
-
const buffer = await this.readModelBlock(model);
|
|
792
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
618
793
|
// Extract scale factors from buffer
|
|
619
794
|
const scaleFactors = {
|
|
620
795
|
A_SF: this.extractValue(buffer, 6, 'int16'),
|
|
@@ -763,18 +938,18 @@ class SunspecModbusClient {
|
|
|
763
938
|
this.logRegisterRead(160, 5, 'DCWH_SF', scaleFactors.DCWH_SF, 'int16');
|
|
764
939
|
return scaleFactors;
|
|
765
940
|
}
|
|
766
|
-
async readMPPTScaleFactors() {
|
|
767
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MPPT);
|
|
941
|
+
async readMPPTScaleFactors(unitId) {
|
|
942
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MPPT);
|
|
768
943
|
if (!model) {
|
|
769
|
-
console.
|
|
944
|
+
console.debug(`MPPT model 160 not found on unit ${unitId}`);
|
|
770
945
|
return null;
|
|
771
946
|
}
|
|
772
947
|
try {
|
|
773
|
-
const buffer = await this.readModelBlock(model);
|
|
948
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
774
949
|
return this.extractMPPTScaleFactors(buffer);
|
|
775
950
|
}
|
|
776
951
|
catch (error) {
|
|
777
|
-
console.error(`Error reading MPPT scale factors: ${error}`);
|
|
952
|
+
console.error(`Error reading MPPT scale factors (unit ${unitId}): ${error}`);
|
|
778
953
|
return null;
|
|
779
954
|
}
|
|
780
955
|
}
|
|
@@ -800,7 +975,7 @@ class SunspecModbusClient {
|
|
|
800
975
|
this.isUnimplementedValue(dcCurrentRaw, 'uint16') &&
|
|
801
976
|
this.isUnimplementedValue(dcVoltageRaw, 'uint16') &&
|
|
802
977
|
this.isUnimplementedValue(dcPowerRaw, 'uint16')) {
|
|
803
|
-
console.
|
|
978
|
+
console.debug(`MPPT module ${moduleId} appears to be unconnected (all values are 0xFFFF)`);
|
|
804
979
|
return null;
|
|
805
980
|
}
|
|
806
981
|
const temperatureScaleFactor = -1;
|
|
@@ -835,52 +1010,52 @@ class SunspecModbusClient {
|
|
|
835
1010
|
/**
|
|
836
1011
|
* Read MPPT data from Model 160
|
|
837
1012
|
*/
|
|
838
|
-
async readMPPTData(moduleId = 1) {
|
|
839
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MPPT);
|
|
1013
|
+
async readMPPTData(unitId, moduleId = 1) {
|
|
1014
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MPPT);
|
|
840
1015
|
if (!model) {
|
|
841
|
-
console.
|
|
1016
|
+
console.debug(`MPPT model 160 not found on unit ${unitId}`);
|
|
842
1017
|
return null;
|
|
843
1018
|
}
|
|
844
1019
|
try {
|
|
845
1020
|
// Read entire model block in a single Modbus call
|
|
846
|
-
const buffer = await this.readModelBlock(model);
|
|
1021
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
847
1022
|
const scaleFactors = this.extractMPPTScaleFactors(buffer);
|
|
848
1023
|
return this.extractMPPTModuleData(buffer, model, moduleId, scaleFactors);
|
|
849
1024
|
}
|
|
850
1025
|
catch (error) {
|
|
851
|
-
console.error(`Error reading MPPT data for module ${moduleId}: ${error}`);
|
|
1026
|
+
console.error(`Error reading MPPT data for module ${moduleId} (unit ${unitId}): ${error}`);
|
|
852
1027
|
return null;
|
|
853
1028
|
}
|
|
854
1029
|
}
|
|
855
1030
|
/**
|
|
856
1031
|
* Read all MPPT strings from Model 160 (Multiple MPPT)
|
|
857
1032
|
*/
|
|
858
|
-
async readAllMPPTData() {
|
|
1033
|
+
async readAllMPPTData(unitId) {
|
|
859
1034
|
const mpptData = [];
|
|
860
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MPPT);
|
|
1035
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MPPT);
|
|
861
1036
|
if (!model) {
|
|
862
|
-
console.
|
|
1037
|
+
console.debug(`MPPT model 160 not found on unit ${unitId}`);
|
|
863
1038
|
return [];
|
|
864
1039
|
}
|
|
865
1040
|
try {
|
|
866
1041
|
// Read entire model block in a single Modbus call
|
|
867
|
-
const buffer = await this.readModelBlock(model);
|
|
1042
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
868
1043
|
const scaleFactors = this.extractMPPTScaleFactors(buffer);
|
|
869
1044
|
// Read the module count from register 8
|
|
870
1045
|
let moduleCount = 4; // Default fallback value
|
|
871
1046
|
const count = this.extractValue(buffer, 8, 'uint16');
|
|
872
1047
|
if (!this.isUnimplementedValue(count, 'uint16') && count > 0 && count <= 20) {
|
|
873
1048
|
moduleCount = count;
|
|
874
|
-
console.
|
|
1049
|
+
console.debug(`MPPT module count from register 8: ${moduleCount}`);
|
|
875
1050
|
}
|
|
876
1051
|
else {
|
|
877
|
-
console.
|
|
1052
|
+
console.debug(`Invalid or unimplemented module count (${count}), using default: ${moduleCount}`);
|
|
878
1053
|
}
|
|
879
1054
|
// Extract each MPPT module from the same buffer
|
|
880
1055
|
for (let i = 1; i <= moduleCount; i++) {
|
|
881
1056
|
try {
|
|
882
1057
|
const data = this.extractMPPTModuleData(buffer, model, i, scaleFactors);
|
|
883
|
-
console.
|
|
1058
|
+
console.debug(`MPPT ${i} has id ${data?.id} (${data?.stringId}) with ${data?.dcPower}W`);
|
|
884
1059
|
if (data &&
|
|
885
1060
|
(data.dcCurrent !== undefined ||
|
|
886
1061
|
data.dcVoltage !== undefined ||
|
|
@@ -1025,16 +1200,16 @@ class SunspecModbusClient {
|
|
|
1025
1200
|
/**
|
|
1026
1201
|
* Read battery base data from Model 802 (Battery Base)
|
|
1027
1202
|
*/
|
|
1028
|
-
async readBatteryBaseData() {
|
|
1029
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.BatteryBase);
|
|
1203
|
+
async readBatteryBaseData(unitId) {
|
|
1204
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.BatteryBase);
|
|
1030
1205
|
if (!model) {
|
|
1031
|
-
console.
|
|
1206
|
+
console.debug(`Battery Base model 802 not found on unit ${unitId}`);
|
|
1032
1207
|
return null;
|
|
1033
1208
|
}
|
|
1034
|
-
console.
|
|
1209
|
+
console.debug(`Reading Battery Base Data from Model 802 at base address: ${model.address} (unit ${unitId})`);
|
|
1035
1210
|
try {
|
|
1036
1211
|
// Read entire model block in a single Modbus call
|
|
1037
|
-
const buffer = await this.readModelBlock(model);
|
|
1212
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1038
1213
|
// Extract scale factors from buffer (offsets 52-63)
|
|
1039
1214
|
const sf = this.extractBatteryBaseScaleFactors(buffer);
|
|
1040
1215
|
// Extract raw values from buffer
|
|
@@ -1163,27 +1338,27 @@ class SunspecModbusClient {
|
|
|
1163
1338
|
/**
|
|
1164
1339
|
* Read battery data from Model 124 (Basic Storage) with fallback to Model 802 / Model 803
|
|
1165
1340
|
*/
|
|
1166
|
-
async readBatteryData() {
|
|
1341
|
+
async readBatteryData(unitId) {
|
|
1167
1342
|
// Try Model 124 first (Basic Storage Controls)
|
|
1168
|
-
let model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Battery);
|
|
1343
|
+
let model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Battery);
|
|
1169
1344
|
// Fall back to other battery models if needed
|
|
1170
1345
|
if (!model) {
|
|
1171
|
-
model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.BatteryBase);
|
|
1346
|
+
model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.BatteryBase);
|
|
1172
1347
|
}
|
|
1173
1348
|
if (!model) {
|
|
1174
|
-
model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.BatteryControl);
|
|
1349
|
+
model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.BatteryControl);
|
|
1175
1350
|
}
|
|
1176
1351
|
if (!model) {
|
|
1177
|
-
console.
|
|
1352
|
+
console.debug(`No battery model found on unit ${unitId}`);
|
|
1178
1353
|
return null;
|
|
1179
1354
|
}
|
|
1180
|
-
console.
|
|
1355
|
+
console.debug(`Reading Battery Data from Model ${model.id} at base address: ${model.address} (unit ${unitId})`);
|
|
1181
1356
|
try {
|
|
1182
1357
|
if (model.id === 124) {
|
|
1183
1358
|
// Model 124: Basic Storage Controls
|
|
1184
|
-
console.
|
|
1359
|
+
console.debug('Using Model 124 (Basic Storage Controls)');
|
|
1185
1360
|
// Read entire model block in a single Modbus call
|
|
1186
|
-
const buffer = await this.readModelBlock(model);
|
|
1361
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1187
1362
|
// Extract scale factors from buffer (offsets 18-25)
|
|
1188
1363
|
const scaleFactors = {
|
|
1189
1364
|
WChaMax_SF: this.extractValue(buffer, 18, 'int16'),
|
|
@@ -1271,20 +1446,20 @@ class SunspecModbusClient {
|
|
|
1271
1446
|
// Calculate charge/discharge power if rates are available
|
|
1272
1447
|
if (data.inWRte !== undefined && data.wChaMax !== undefined) {
|
|
1273
1448
|
data.chargePower = (data.inWRte / 100) * data.wChaMax;
|
|
1274
|
-
console.
|
|
1449
|
+
console.debug(`Calculated Charge Power: (inWRte: ${data.inWRte}, wChaMax: ${data.wChaMax}) ${data.chargePower?.toFixed(2)} W`);
|
|
1275
1450
|
}
|
|
1276
1451
|
if (data.outWRte !== undefined && data.wChaMax !== undefined) {
|
|
1277
1452
|
// Assuming WDisChaMax is similar to WChaMax for simplicity
|
|
1278
1453
|
data.dischargePower = Math.abs((data.outWRte / 100) * data.wChaMax);
|
|
1279
|
-
console.
|
|
1454
|
+
console.debug(`Calculated Discharge Power (inWRte: ${data.outWRte}, wChaMax: ${data.wChaMax}): ${data.dischargePower?.toFixed(2)} W`);
|
|
1280
1455
|
}
|
|
1281
1456
|
console.debug('[Model 124] Battery Data:', data);
|
|
1282
1457
|
return data;
|
|
1283
1458
|
}
|
|
1284
1459
|
else if (model.id === 802) {
|
|
1285
1460
|
// Model 802: Battery Base
|
|
1286
|
-
console.
|
|
1287
|
-
const baseData = await this.readBatteryBaseData();
|
|
1461
|
+
console.debug('Using Model 802 (Battery Base)');
|
|
1462
|
+
const baseData = await this.readBatteryBaseData(unitId);
|
|
1288
1463
|
if (!baseData) {
|
|
1289
1464
|
return null;
|
|
1290
1465
|
}
|
|
@@ -1320,7 +1495,7 @@ class SunspecModbusClient {
|
|
|
1320
1495
|
}
|
|
1321
1496
|
else {
|
|
1322
1497
|
// Handle other battery models (803) if needed
|
|
1323
|
-
console.
|
|
1498
|
+
console.debug(`Battery Model ${model.id} reading not yet implemented`);
|
|
1324
1499
|
return {
|
|
1325
1500
|
blockNumber: model.id,
|
|
1326
1501
|
blockAddress: model.address,
|
|
@@ -1336,55 +1511,55 @@ class SunspecModbusClient {
|
|
|
1336
1511
|
/**
|
|
1337
1512
|
* Write battery control settings to Model 124
|
|
1338
1513
|
*/
|
|
1339
|
-
async writeBatteryControls(controls) {
|
|
1340
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Battery);
|
|
1514
|
+
async writeBatteryControls(unitId, controls) {
|
|
1515
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Battery);
|
|
1341
1516
|
if (!model) {
|
|
1342
|
-
console.error(
|
|
1517
|
+
console.error(`Battery model 124 not found on unit ${unitId}`);
|
|
1343
1518
|
return false;
|
|
1344
1519
|
}
|
|
1345
1520
|
const baseAddr = model.address;
|
|
1346
|
-
console.log(`Writing Battery Controls to Model 124 at base address: ${baseAddr}`);
|
|
1521
|
+
console.log(`Writing Battery Controls to Model 124 at base address: ${baseAddr} (unit ${unitId})`);
|
|
1347
1522
|
try {
|
|
1348
1523
|
// Write storage control mode (Register 5)
|
|
1349
1524
|
if (controls.storCtlMod !== undefined) {
|
|
1350
|
-
await this.writeRegisterValue(baseAddr + 5, controls.storCtlMod, 'uint16');
|
|
1525
|
+
await this.writeRegisterValue(unitId, baseAddr + 5, controls.storCtlMod, 'uint16');
|
|
1351
1526
|
console.log(`Set storage control mode to 0x${controls.storCtlMod.toString(16)}`);
|
|
1352
1527
|
}
|
|
1353
1528
|
// Write charge source setting (Register 17)
|
|
1354
1529
|
if (controls.chaGriSet !== undefined) {
|
|
1355
|
-
await this.writeRegisterValue(baseAddr + 17, controls.chaGriSet, 'uint16');
|
|
1530
|
+
await this.writeRegisterValue(unitId, baseAddr + 17, controls.chaGriSet, 'uint16');
|
|
1356
1531
|
console.log(`Set charge source to ${controls.chaGriSet === sunspec_interfaces_js_1.SunspecChargeSource.GRID ? 'GRID' : 'PV'}`);
|
|
1357
1532
|
}
|
|
1358
1533
|
// Write maximum charge power (Register 2) - needs scale factor
|
|
1359
1534
|
if (controls.wChaMax !== undefined) {
|
|
1360
1535
|
const scaleFactorAddr = baseAddr + 18;
|
|
1361
|
-
const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
|
|
1536
|
+
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1362
1537
|
const scaledValue = Math.round(controls.wChaMax / Math.pow(10, scaleFactor));
|
|
1363
|
-
await this.writeRegisterValue(baseAddr + 2, scaledValue, 'uint16');
|
|
1538
|
+
await this.writeRegisterValue(unitId, baseAddr + 2, scaledValue, 'uint16');
|
|
1364
1539
|
console.log(`Set max charge power to ${controls.wChaMax}W (scaled: ${scaledValue})`);
|
|
1365
1540
|
}
|
|
1366
1541
|
// Write charge rate (Register 13) - needs scale factor
|
|
1367
1542
|
if (controls.inWRte !== undefined) {
|
|
1368
1543
|
const scaleFactorAddr = baseAddr + 25;
|
|
1369
|
-
const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
|
|
1544
|
+
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1370
1545
|
const scaledValue = Math.round(controls.inWRte / Math.pow(10, scaleFactor));
|
|
1371
|
-
await this.writeRegisterValue(baseAddr + 13, scaledValue, 'int16');
|
|
1546
|
+
await this.writeRegisterValue(unitId, baseAddr + 13, scaledValue, 'int16');
|
|
1372
1547
|
console.log(`Set charge rate to ${controls.inWRte}% (scaled: ${scaledValue})`);
|
|
1373
1548
|
}
|
|
1374
1549
|
// Write discharge rate (Register 12) - needs scale factor
|
|
1375
1550
|
if (controls.outWRte !== undefined) {
|
|
1376
1551
|
const scaleFactorAddr = baseAddr + 25;
|
|
1377
|
-
const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
|
|
1552
|
+
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1378
1553
|
const scaledValue = Math.round(controls.outWRte / Math.pow(10, scaleFactor));
|
|
1379
|
-
await this.writeRegisterValue(baseAddr + 12, scaledValue, 'int16');
|
|
1554
|
+
await this.writeRegisterValue(unitId, baseAddr + 12, scaledValue, 'int16');
|
|
1380
1555
|
console.log(`Set discharge rate to ${controls.outWRte}% (scaled: ${scaledValue})`);
|
|
1381
1556
|
}
|
|
1382
1557
|
// Write minimum reserve percentage (Register 7) - needs scale factor
|
|
1383
1558
|
if (controls.minRsvPct !== undefined) {
|
|
1384
1559
|
const scaleFactorAddr = baseAddr + 21;
|
|
1385
|
-
const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
|
|
1560
|
+
const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
|
|
1386
1561
|
const scaledValue = Math.round(controls.minRsvPct / Math.pow(10, scaleFactor));
|
|
1387
|
-
await this.writeRegisterValue(baseAddr + 7, scaledValue, 'uint16');
|
|
1562
|
+
await this.writeRegisterValue(unitId, baseAddr + 7, scaledValue, 'uint16');
|
|
1388
1563
|
console.log(`Set minimum reserve to ${controls.minRsvPct}% (scaled: ${scaledValue})`);
|
|
1389
1564
|
}
|
|
1390
1565
|
console.log('Battery controls written successfully');
|
|
@@ -1398,7 +1573,7 @@ class SunspecModbusClient {
|
|
|
1398
1573
|
/**
|
|
1399
1574
|
* Set battery storage mode (simplified interface)
|
|
1400
1575
|
*/
|
|
1401
|
-
async setStorageMode(mode) {
|
|
1576
|
+
async setStorageMode(unitId, mode) {
|
|
1402
1577
|
let storCtlMod;
|
|
1403
1578
|
switch (mode) {
|
|
1404
1579
|
case sunspec_interfaces_js_1.SunspecStorageMode.CHARGE:
|
|
@@ -1422,29 +1597,29 @@ class SunspecModbusClient {
|
|
|
1422
1597
|
return false;
|
|
1423
1598
|
}
|
|
1424
1599
|
console.log(`Setting storage mode to ${mode} (control bits: 0x${storCtlMod.toString(16)})`);
|
|
1425
|
-
return this.writeBatteryControls({ storCtlMod });
|
|
1600
|
+
return this.writeBatteryControls(unitId, { storCtlMod });
|
|
1426
1601
|
}
|
|
1427
1602
|
/**
|
|
1428
1603
|
* Enable or disable grid charging
|
|
1429
1604
|
*/
|
|
1430
|
-
async enableGridCharging(enable) {
|
|
1605
|
+
async enableGridCharging(unitId, enable) {
|
|
1431
1606
|
const chaGriSet = enable ? sunspec_interfaces_js_1.SunspecChargeSource.GRID : sunspec_interfaces_js_1.SunspecChargeSource.PV;
|
|
1432
|
-
console.log(`${enable ? 'Enabling' : 'Disabling'} grid charging`);
|
|
1433
|
-
return this.writeBatteryControls({ chaGriSet });
|
|
1607
|
+
console.log(`${enable ? 'Enabling' : 'Disabling'} grid charging on unit ${unitId}`);
|
|
1608
|
+
return this.writeBatteryControls(unitId, { chaGriSet });
|
|
1434
1609
|
}
|
|
1435
1610
|
/**
|
|
1436
1611
|
* Read battery control settings from Model 124 (Basic Storage Controls)
|
|
1437
1612
|
*/
|
|
1438
|
-
async readBatteryControls() {
|
|
1439
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Battery);
|
|
1613
|
+
async readBatteryControls(unitId) {
|
|
1614
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Battery);
|
|
1440
1615
|
if (!model) {
|
|
1441
|
-
console.log(
|
|
1616
|
+
console.log(`Battery model 124 not found on unit ${unitId}`);
|
|
1442
1617
|
return null;
|
|
1443
1618
|
}
|
|
1444
|
-
console.log(`Reading Battery Controls from Model 124 at base address: ${model.address}`);
|
|
1619
|
+
console.log(`Reading Battery Controls from Model 124 at base address: ${model.address} (unit ${unitId})`);
|
|
1445
1620
|
try {
|
|
1446
1621
|
// Read entire model block in a single Modbus call
|
|
1447
|
-
const buffer = await this.readModelBlock(model);
|
|
1622
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1448
1623
|
// Extract scale factors from buffer
|
|
1449
1624
|
const scaleFactors = {
|
|
1450
1625
|
WChaMax_SF: this.extractValue(buffer, 18, 'int16'),
|
|
@@ -1483,22 +1658,22 @@ class SunspecModbusClient {
|
|
|
1483
1658
|
/**
|
|
1484
1659
|
* Read meter data from Model 201 (Single Phase) / Model 203 (Three Phase) / Model 204 (Split Phase)
|
|
1485
1660
|
*/
|
|
1486
|
-
async readMeterData() {
|
|
1487
|
-
let model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Meter3Phase);
|
|
1661
|
+
async readMeterData(unitId) {
|
|
1662
|
+
let model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Meter3Phase);
|
|
1488
1663
|
if (!model) {
|
|
1489
|
-
model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MeterWye);
|
|
1664
|
+
model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MeterWye);
|
|
1490
1665
|
}
|
|
1491
1666
|
if (!model) {
|
|
1492
|
-
model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MeterSinglePhase);
|
|
1667
|
+
model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MeterSinglePhase);
|
|
1493
1668
|
}
|
|
1494
1669
|
if (!model) {
|
|
1495
|
-
console.
|
|
1670
|
+
console.debug(`No meter model found on unit ${unitId}`);
|
|
1496
1671
|
return null;
|
|
1497
1672
|
}
|
|
1498
|
-
console.
|
|
1673
|
+
console.debug(`Reading Meter Data from Model ${model.id} at base address: ${model.address} (unit ${unitId})`);
|
|
1499
1674
|
try {
|
|
1500
1675
|
// Different meter models have different register offsets
|
|
1501
|
-
console.
|
|
1676
|
+
console.debug(`Meter is Model ${model.id}`);
|
|
1502
1677
|
let powerOffset;
|
|
1503
1678
|
let powerSFOffset;
|
|
1504
1679
|
let freqOffset;
|
|
@@ -1508,7 +1683,7 @@ class SunspecModbusClient {
|
|
|
1508
1683
|
let energySFOffset;
|
|
1509
1684
|
switch (model.id) {
|
|
1510
1685
|
case 203: // 3-phase meter (Delta)
|
|
1511
|
-
console.
|
|
1686
|
+
console.debug('Using Model 203 (3-Phase Meter Delta) offsets');
|
|
1512
1687
|
powerOffset = 18; // W - Total Real Power (int16)
|
|
1513
1688
|
powerSFOffset = 22; // W_SF - Power scale factor
|
|
1514
1689
|
freqOffset = 16; // Hz - Frequency (uint16)
|
|
@@ -1518,7 +1693,7 @@ class SunspecModbusClient {
|
|
|
1518
1693
|
energySFOffset = 54; // TotWh_SF - Total Energy scale factor
|
|
1519
1694
|
break;
|
|
1520
1695
|
case 204: // 3-phase meter (Wye)
|
|
1521
|
-
console.
|
|
1696
|
+
console.debug('Using Model 204 (3-Phase Meter Wye) offsets');
|
|
1522
1697
|
powerOffset = 18; // W - Total Real Power (int16)
|
|
1523
1698
|
powerSFOffset = 22; // W_SF - Power scale factor
|
|
1524
1699
|
freqOffset = 16; // Hz - Frequency (uint16)
|
|
@@ -1529,7 +1704,7 @@ class SunspecModbusClient {
|
|
|
1529
1704
|
break;
|
|
1530
1705
|
case 201: // Single-phase meter
|
|
1531
1706
|
default:
|
|
1532
|
-
console.
|
|
1707
|
+
console.debug('Using Model 201 (Single-Phase Meter) offsets');
|
|
1533
1708
|
powerOffset = 18; // W - Total Real Power (int16)
|
|
1534
1709
|
powerSFOffset = 22; // W_SF - Power scale factor
|
|
1535
1710
|
freqOffset = 16; // Hz - Frequency (uint16)
|
|
@@ -1539,7 +1714,7 @@ class SunspecModbusClient {
|
|
|
1539
1714
|
energySFOffset = 54; // TotWh_SF - Total Energy scale factor
|
|
1540
1715
|
}
|
|
1541
1716
|
// Read entire model block in a single Modbus call
|
|
1542
|
-
const buffer = await this.readModelBlock(model);
|
|
1717
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1543
1718
|
// Extract scale factors from buffer
|
|
1544
1719
|
const powerSF = this.extractValue(buffer, powerSFOffset, 'int16');
|
|
1545
1720
|
const freqSF = this.extractValue(buffer, freqSFOffset, 'int16');
|
|
@@ -1577,16 +1752,16 @@ class SunspecModbusClient {
|
|
|
1577
1752
|
/**
|
|
1578
1753
|
* Read common block data (Model 1)
|
|
1579
1754
|
*/
|
|
1580
|
-
async readCommonBlock() {
|
|
1581
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Common);
|
|
1755
|
+
async readCommonBlock(unitId) {
|
|
1756
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Common);
|
|
1582
1757
|
if (!model) {
|
|
1583
|
-
console.error(
|
|
1758
|
+
console.error(`Common block model not found on unit ${unitId}`);
|
|
1584
1759
|
return null;
|
|
1585
1760
|
}
|
|
1586
|
-
console.log(`Reading Common Block - Model address: ${model.address}`);
|
|
1761
|
+
console.log(`Reading Common Block - Model address: ${model.address} (unit ${unitId})`);
|
|
1587
1762
|
try {
|
|
1588
1763
|
// Read entire model block in a single Modbus call
|
|
1589
|
-
const buffer = await this.readModelBlock(model);
|
|
1764
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1590
1765
|
// Common block offsets are relative to the model start (after ID and Length header,
|
|
1591
1766
|
// but readModelBlock reads from model.address which includes the data area).
|
|
1592
1767
|
// The offsets below are relative to the data start within the model block.
|
|
@@ -1623,24 +1798,24 @@ class SunspecModbusClient {
|
|
|
1623
1798
|
/**
|
|
1624
1799
|
* Get serial number from device
|
|
1625
1800
|
*/
|
|
1626
|
-
async getSerialNumber() {
|
|
1627
|
-
const commonData = await this.readCommonBlock();
|
|
1801
|
+
async getSerialNumber(unitId) {
|
|
1802
|
+
const commonData = await this.readCommonBlock(unitId);
|
|
1628
1803
|
return commonData?.serialNumber;
|
|
1629
1804
|
}
|
|
1630
1805
|
/**
|
|
1631
|
-
* Check if connected
|
|
1806
|
+
* Check if a specific unit is connected on this network device
|
|
1632
1807
|
*/
|
|
1633
|
-
isConnected() {
|
|
1634
|
-
return this.
|
|
1808
|
+
isConnected(unitId) {
|
|
1809
|
+
return this.modbusInstances.has(unitId);
|
|
1635
1810
|
}
|
|
1636
1811
|
/**
|
|
1637
|
-
* Check if connection is healthy
|
|
1812
|
+
* Check if a specific unit's connection is healthy
|
|
1638
1813
|
*/
|
|
1639
|
-
isHealthy() {
|
|
1640
|
-
return this.
|
|
1814
|
+
isHealthy(unitId) {
|
|
1815
|
+
return this.modbusInstances.has(unitId) && this.connectionHealth.isHealthy();
|
|
1641
1816
|
}
|
|
1642
1817
|
/**
|
|
1643
|
-
* Get connection health details
|
|
1818
|
+
* Get connection health details (shared across all units on this network device)
|
|
1644
1819
|
*/
|
|
1645
1820
|
getConnectionHealth() {
|
|
1646
1821
|
return this.connectionHealth;
|
|
@@ -1648,15 +1823,15 @@ class SunspecModbusClient {
|
|
|
1648
1823
|
/**
|
|
1649
1824
|
* Read inverter settings from Model 121 (Inverter Settings)
|
|
1650
1825
|
*/
|
|
1651
|
-
async readInverterSettings() {
|
|
1652
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Settings);
|
|
1826
|
+
async readInverterSettings(unitId) {
|
|
1827
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Settings);
|
|
1653
1828
|
if (!model) {
|
|
1654
|
-
console.
|
|
1829
|
+
console.debug(`Settings model 121 not found on unit ${unitId}`);
|
|
1655
1830
|
return null;
|
|
1656
1831
|
}
|
|
1657
1832
|
try {
|
|
1658
1833
|
// Read entire model block in a single Modbus call
|
|
1659
|
-
const buffer = await this.readModelBlock(model);
|
|
1834
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1660
1835
|
// Extract scale factors from buffer (offsets 22-31)
|
|
1661
1836
|
const scaleFactors = {
|
|
1662
1837
|
WMax_SF: this.extractValue(buffer, 22, 'int16'),
|
|
@@ -1740,15 +1915,15 @@ class SunspecModbusClient {
|
|
|
1740
1915
|
/**
|
|
1741
1916
|
* Read inverter controls from Model 123 (Immediate Inverter Controls)
|
|
1742
1917
|
*/
|
|
1743
|
-
async readInverterControls() {
|
|
1744
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Controls);
|
|
1918
|
+
async readInverterControls(unitId) {
|
|
1919
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Controls);
|
|
1745
1920
|
if (!model) {
|
|
1746
|
-
console.log(
|
|
1921
|
+
console.log(`Controls model 123 not found on unit ${unitId}`);
|
|
1747
1922
|
return null;
|
|
1748
1923
|
}
|
|
1749
1924
|
try {
|
|
1750
1925
|
// Read entire model block in a single Modbus call
|
|
1751
|
-
const buffer = await this.readModelBlock(model);
|
|
1926
|
+
const buffer = await this.readModelBlock(unitId, model);
|
|
1752
1927
|
// Extract scale factors from buffer (offsets 21-23)
|
|
1753
1928
|
const scaleFactors = {
|
|
1754
1929
|
WMaxLimPct_SF: this.extractValue(buffer, 21, 'int16'),
|
|
@@ -1835,27 +2010,28 @@ class SunspecModbusClient {
|
|
|
1835
2010
|
/**
|
|
1836
2011
|
* Write Block 121 - Inverter Basic Settings
|
|
1837
2012
|
*/
|
|
1838
|
-
async writeInverterSettings(settings) {
|
|
1839
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Settings);
|
|
2013
|
+
async writeInverterSettings(unitId, settings) {
|
|
2014
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Settings);
|
|
1840
2015
|
if (!model) {
|
|
1841
|
-
console.error(
|
|
2016
|
+
console.error(`Settings model 121 not found on unit ${unitId}`);
|
|
1842
2017
|
return false;
|
|
1843
2018
|
}
|
|
1844
2019
|
const baseAddr = model.address;
|
|
1845
2020
|
try {
|
|
2021
|
+
const instance = this.getInstance(unitId);
|
|
1846
2022
|
// For each setting, write the value if provided
|
|
1847
2023
|
// Note: This is a simplified implementation. In production, you'd batch writes
|
|
1848
|
-
if (settings.WMax !== undefined
|
|
2024
|
+
if (settings.WMax !== undefined) {
|
|
1849
2025
|
// Need to read scale factor first if not provided
|
|
1850
|
-
const sfBuffer = await
|
|
2026
|
+
const sfBuffer = await instance.readHoldingRegisters(baseAddr + 22, 1);
|
|
1851
2027
|
const scaleFactor = sfBuffer.readInt16BE(0);
|
|
1852
2028
|
const scaledValue = Math.round(settings.WMax / Math.pow(10, scaleFactor));
|
|
1853
2029
|
// Writing registers needs to be implemented in EnergyAppModbusInstance
|
|
1854
2030
|
// For now, log the write operation
|
|
1855
2031
|
console.log(`Would write value ${scaledValue} to register ${baseAddr}`);
|
|
1856
2032
|
}
|
|
1857
|
-
if (settings.VRef !== undefined
|
|
1858
|
-
const sfBuffer = await
|
|
2033
|
+
if (settings.VRef !== undefined) {
|
|
2034
|
+
const sfBuffer = await instance.readHoldingRegisters(baseAddr + 23, 1);
|
|
1859
2035
|
const scaleFactor = sfBuffer.readInt16BE(0);
|
|
1860
2036
|
const scaledValue = Math.round(settings.VRef / Math.pow(10, scaleFactor));
|
|
1861
2037
|
console.log(`Would write value ${scaledValue} to register ${baseAddr + 1}`);
|
|
@@ -1872,41 +2048,41 @@ class SunspecModbusClient {
|
|
|
1872
2048
|
/**
|
|
1873
2049
|
* Write inverter controls to Model 123 (Immediate Inverter Controls)
|
|
1874
2050
|
*/
|
|
1875
|
-
async writeInverterControls(controls) {
|
|
1876
|
-
const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Controls);
|
|
2051
|
+
async writeInverterControls(unitId, controls) {
|
|
2052
|
+
const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Controls);
|
|
1877
2053
|
if (!model) {
|
|
1878
|
-
console.error(
|
|
2054
|
+
console.error(`Controls model 123 not found on unit ${unitId}`);
|
|
1879
2055
|
return false;
|
|
1880
2056
|
}
|
|
1881
2057
|
const baseAddr = model.address;
|
|
1882
2058
|
try {
|
|
1883
2059
|
// Connection control (Register 2)
|
|
1884
2060
|
if (controls.Conn !== undefined) {
|
|
1885
|
-
await this.writeRegisterValue(baseAddr + 2, controls.Conn, 'uint16');
|
|
2061
|
+
await this.writeRegisterValue(unitId, baseAddr + 2, controls.Conn, 'uint16');
|
|
1886
2062
|
console.log(`Set connection control to ${controls.Conn}`);
|
|
1887
2063
|
}
|
|
1888
2064
|
// Power limit control (Register 3) - needs scale factor
|
|
1889
2065
|
if (controls.WMaxLimPct !== undefined) {
|
|
1890
|
-
const scaleFactor = await this.readRegisterValue(baseAddr + 21, 1, 'int16');
|
|
2066
|
+
const scaleFactor = await this.readRegisterValue(unitId, baseAddr + 21, 1, 'int16');
|
|
1891
2067
|
const scaledValue = Math.round(controls.WMaxLimPct / Math.pow(10, scaleFactor));
|
|
1892
|
-
await this.writeRegisterValue(baseAddr + 3, scaledValue, 'uint16');
|
|
2068
|
+
await this.writeRegisterValue(unitId, baseAddr + 3, scaledValue, 'uint16');
|
|
1893
2069
|
console.log(`Set power limit to ${controls.WMaxLimPct}% (scaled: ${scaledValue})`);
|
|
1894
2070
|
}
|
|
1895
2071
|
// Throttle enable/disable (Register 7)
|
|
1896
2072
|
if (controls.WMaxLim_Ena !== undefined) {
|
|
1897
|
-
await this.writeRegisterValue(baseAddr + 7, controls.WMaxLim_Ena, 'uint16');
|
|
2073
|
+
await this.writeRegisterValue(unitId, baseAddr + 7, controls.WMaxLim_Ena, 'uint16');
|
|
1898
2074
|
console.log(`Set throttle enable to ${controls.WMaxLim_Ena}`);
|
|
1899
2075
|
}
|
|
1900
2076
|
// Power factor control (Register 8) - needs scale factor
|
|
1901
2077
|
if (controls.OutPFSet !== undefined) {
|
|
1902
|
-
const scaleFactor = await this.readRegisterValue(baseAddr + 22, 1, 'int16');
|
|
2078
|
+
const scaleFactor = await this.readRegisterValue(unitId, baseAddr + 22, 1, 'int16');
|
|
1903
2079
|
const scaledValue = Math.round(controls.OutPFSet / Math.pow(10, scaleFactor));
|
|
1904
|
-
await this.writeRegisterValue(baseAddr + 8, scaledValue, 'int16');
|
|
2080
|
+
await this.writeRegisterValue(unitId, baseAddr + 8, scaledValue, 'int16');
|
|
1905
2081
|
console.log(`Set power factor to ${controls.OutPFSet} (scaled: ${scaledValue})`);
|
|
1906
2082
|
}
|
|
1907
2083
|
// Power factor enable/disable (Register 12)
|
|
1908
2084
|
if (controls.OutPFSet_Ena !== undefined) {
|
|
1909
|
-
await this.writeRegisterValue(baseAddr + 12, controls.OutPFSet_Ena, 'uint16');
|
|
2085
|
+
await this.writeRegisterValue(unitId, baseAddr + 12, controls.OutPFSet_Ena, 'uint16');
|
|
1910
2086
|
console.log(`Set PF enable to ${controls.OutPFSet_Ena}`);
|
|
1911
2087
|
}
|
|
1912
2088
|
console.log('Inverter controls written successfully');
|
|
@@ -1927,21 +2103,21 @@ class SunspecModbusClient {
|
|
|
1927
2103
|
* @param limitW - Power limit in Watts, or null to remove the limit
|
|
1928
2104
|
* @returns true if successful, false otherwise
|
|
1929
2105
|
*/
|
|
1930
|
-
async setFeedInLimit(limitW) {
|
|
2106
|
+
async setFeedInLimit(unitId, limitW) {
|
|
1931
2107
|
if (limitW === null) {
|
|
1932
2108
|
// Remove limit: disable WMaxLim_Ena
|
|
1933
|
-
console.log(
|
|
1934
|
-
return this.writeInverterControls({ WMaxLim_Ena: sunspec_interfaces_js_1.SunspecEnableControl.DISABLED });
|
|
2109
|
+
console.log(`Removing feed-in limit (disabling WMaxLim_Ena) on unit ${unitId}`);
|
|
2110
|
+
return this.writeInverterControls(unitId, { WMaxLim_Ena: sunspec_interfaces_js_1.SunspecEnableControl.DISABLED });
|
|
1935
2111
|
}
|
|
1936
2112
|
// Read WMax from Model 121 to compute percentage
|
|
1937
|
-
const settings = await this.readInverterSettings();
|
|
2113
|
+
const settings = await this.readInverterSettings(unitId);
|
|
1938
2114
|
if (!settings || !settings.WMax) {
|
|
1939
|
-
console.error(
|
|
2115
|
+
console.error(`Cannot set feed-in limit on unit ${unitId}: unable to read WMax from Model 121`);
|
|
1940
2116
|
return false;
|
|
1941
2117
|
}
|
|
1942
2118
|
const pct = (limitW / settings.WMax) * 100;
|
|
1943
|
-
console.log(`Setting feed-in limit to ${limitW}W (${pct.toFixed(2)}% of WMax ${settings.WMax}W)`);
|
|
1944
|
-
return this.writeInverterControls({
|
|
2119
|
+
console.log(`Setting feed-in limit to ${limitW}W (${pct.toFixed(2)}% of WMax ${settings.WMax}W) on unit ${unitId}`);
|
|
2120
|
+
return this.writeInverterControls(unitId, {
|
|
1945
2121
|
WMaxLimPct: pct,
|
|
1946
2122
|
WMaxLim_Ena: sunspec_interfaces_js_1.SunspecEnableControl.ENABLED
|
|
1947
2123
|
});
|