@enyo-energy/sunspec-sdk 0.0.29 → 0.0.31
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 +162 -5
- package/dist/cjs/sunspec-devices.d.cts +19 -1
- package/dist/cjs/sunspec-modbus-client.cjs +69 -33
- package/dist/cjs/sunspec-modbus-client.d.cts +2 -3
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/sunspec-devices.d.ts +19 -1
- package/dist/sunspec-devices.js +163 -6
- package/dist/sunspec-modbus-client.d.ts +2 -3
- package/dist/sunspec-modbus-client.js +69 -33
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -31,15 +31,19 @@ class BaseSunspecDevice {
|
|
|
31
31
|
sunspecClient;
|
|
32
32
|
applianceManager;
|
|
33
33
|
unitId;
|
|
34
|
+
port;
|
|
35
|
+
baseAddress;
|
|
34
36
|
applianceId;
|
|
35
37
|
lastUpdateTime = 0;
|
|
36
|
-
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1) {
|
|
38
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000) {
|
|
37
39
|
this.energyApp = energyApp;
|
|
38
40
|
this.name = name;
|
|
39
41
|
this.networkDevice = networkDevice;
|
|
40
42
|
this.sunspecClient = sunspecClient;
|
|
41
43
|
this.applianceManager = applianceManager;
|
|
42
44
|
this.unitId = unitId;
|
|
45
|
+
this.port = port;
|
|
46
|
+
this.baseAddress = baseAddress;
|
|
43
47
|
}
|
|
44
48
|
/**
|
|
45
49
|
* Check if the device is connected
|
|
@@ -56,9 +60,9 @@ class BaseSunspecDevice {
|
|
|
56
60
|
async ensureConnected() {
|
|
57
61
|
if (!this.sunspecClient.isConnected()) {
|
|
58
62
|
await this.sunspecClient.connect(this.networkDevice.hostname, // primary
|
|
59
|
-
|
|
63
|
+
this.port, this.unitId, this.networkDevice.ipAddress // secondary fallback
|
|
60
64
|
);
|
|
61
|
-
await this.sunspecClient.discoverModels();
|
|
65
|
+
await this.sunspecClient.discoverModels(this.baseAddress);
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
}
|
|
@@ -71,9 +75,9 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
71
75
|
// Ensure Sunspec client is connected
|
|
72
76
|
if (!this.sunspecClient.isConnected()) {
|
|
73
77
|
await this.sunspecClient.connect(this.networkDevice.hostname, // primary
|
|
74
|
-
|
|
78
|
+
this.port, this.unitId, this.networkDevice.ipAddress // secondary fallback
|
|
75
79
|
);
|
|
76
|
-
await this.sunspecClient.discoverModels();
|
|
80
|
+
await this.sunspecClient.discoverModels(this.baseAddress);
|
|
77
81
|
}
|
|
78
82
|
// Get device info from common block
|
|
79
83
|
const commonData = await this.sunspecClient.readCommonBlock();
|
|
@@ -245,6 +249,8 @@ exports.SunspecInverter = SunspecInverter;
|
|
|
245
249
|
* Sunspec Battery implementation
|
|
246
250
|
*/
|
|
247
251
|
class SunspecBattery extends BaseSunspecDevice {
|
|
252
|
+
dataBusListenerId;
|
|
253
|
+
dataBus;
|
|
248
254
|
/**
|
|
249
255
|
* Connect to the battery and create/update the appliance
|
|
250
256
|
*/
|
|
@@ -288,12 +294,14 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
288
294
|
}
|
|
289
295
|
});
|
|
290
296
|
console.log(`Sunspec Battery connected: ${this.networkDevice.hostname} (${this.applianceId})`);
|
|
297
|
+
this.startDataBusListening();
|
|
291
298
|
}
|
|
292
299
|
catch (error) {
|
|
293
300
|
console.error(`Failed to create battery appliance: ${error}`);
|
|
294
301
|
}
|
|
295
302
|
}
|
|
296
303
|
async disconnect() {
|
|
304
|
+
this.stopDataBusListening();
|
|
297
305
|
if (this.applianceId) {
|
|
298
306
|
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
|
|
299
307
|
}
|
|
@@ -562,6 +570,155 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
562
570
|
}
|
|
563
571
|
return controls.chaGriSet === 1; // 1 = GRID, 0 = PV
|
|
564
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* Start listening for storage commands on the data bus.
|
|
575
|
+
* Idempotent — does nothing if already listening.
|
|
576
|
+
*/
|
|
577
|
+
startDataBusListening() {
|
|
578
|
+
if (this.dataBusListenerId) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
this.dataBus = this.energyApp.useDataBus();
|
|
582
|
+
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
583
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartStorageGridChargeV1,
|
|
584
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopStorageGridChargeV1,
|
|
585
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageDischargeLimitV1
|
|
586
|
+
], (entry) => this.handleStorageCommand(entry));
|
|
587
|
+
console.log(`Battery ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Stop listening for storage commands on the data bus.
|
|
591
|
+
*/
|
|
592
|
+
stopDataBusListening() {
|
|
593
|
+
if (this.dataBusListenerId && this.dataBus) {
|
|
594
|
+
this.dataBus.unsubscribe(this.dataBusListenerId);
|
|
595
|
+
console.log(`Battery ${this.applianceId}: stopped data bus listening (listener ${this.dataBusListenerId})`);
|
|
596
|
+
}
|
|
597
|
+
this.dataBusListenerId = undefined;
|
|
598
|
+
this.dataBus = undefined;
|
|
599
|
+
}
|
|
600
|
+
handleStorageCommand(entry) {
|
|
601
|
+
if (entry.applianceId !== this.applianceId) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
void (async () => {
|
|
605
|
+
try {
|
|
606
|
+
switch (entry.message) {
|
|
607
|
+
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartStorageGridChargeV1:
|
|
608
|
+
await this.handleStartGridCharge(entry);
|
|
609
|
+
break;
|
|
610
|
+
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopStorageGridChargeV1:
|
|
611
|
+
await this.handleStopGridCharge(entry);
|
|
612
|
+
break;
|
|
613
|
+
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageDischargeLimitV1:
|
|
614
|
+
await this.handleSetDischargeLimit(entry);
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
console.error(`Battery ${this.applianceId}: error handling ${entry.message}:`, error);
|
|
620
|
+
}
|
|
621
|
+
})();
|
|
622
|
+
}
|
|
623
|
+
async handleStartGridCharge(msg) {
|
|
624
|
+
if (!this.isConnected() || !this.applianceId) {
|
|
625
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, "Not connected");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
console.log(`Battery ${this.applianceId}: handling StartStorageGridChargeV1 (powerLimitW=${msg.data.powerLimitW})`);
|
|
629
|
+
// Read current state for logging and rollback
|
|
630
|
+
const controls = await this.getBatteryControls();
|
|
631
|
+
console.log(`Battery ${this.applianceId}: current state - chaGriSet=${controls?.chaGriSet}, wChaMax=${controls?.wChaMax}, storCtlMod=${controls?.storCtlMod}`);
|
|
632
|
+
const originalChaGriSet = controls?.chaGriSet;
|
|
633
|
+
const originalWChaMax = controls?.wChaMax;
|
|
634
|
+
// Step 1: Enable grid charging (Register 17: chaGriSet=GRID)
|
|
635
|
+
const step1 = await this.enableGridCharging(true);
|
|
636
|
+
if (!step1) {
|
|
637
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to enable grid charging');
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
// Step 2: Set charging power (Register 2: wChaMax)
|
|
641
|
+
const step2 = await this.setChargingPower(msg.data.powerLimitW);
|
|
642
|
+
if (!step2) {
|
|
643
|
+
// Rollback step 1
|
|
644
|
+
await this.writeBatteryControls({ chaGriSet: originalChaGriSet });
|
|
645
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set charging power');
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
// Step 3: Set storage mode to CHARGE (Register 5: storCtlMod=0x0001)
|
|
649
|
+
const step3 = await this.setStorageMode(sunspec_interfaces_js_1.SunspecStorageMode.CHARGE);
|
|
650
|
+
if (!step3) {
|
|
651
|
+
// Rollback steps 1+2
|
|
652
|
+
await this.writeBatteryControls({ chaGriSet: originalChaGriSet, wChaMax: originalWChaMax });
|
|
653
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set storage mode to charge');
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
657
|
+
}
|
|
658
|
+
async handleStopGridCharge(msg) {
|
|
659
|
+
if (!this.isConnected() || !this.applianceId) {
|
|
660
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
console.log(`Battery ${this.applianceId}: handling StopStorageGridChargeV1`);
|
|
664
|
+
// Read current state for logging and rollback
|
|
665
|
+
const controls = await this.getBatteryControls();
|
|
666
|
+
console.log(`Battery ${this.applianceId}: current state - chaGriSet=${controls?.chaGriSet}, storCtlMod=${controls?.storCtlMod}`);
|
|
667
|
+
const originalChaGriSet = controls?.chaGriSet;
|
|
668
|
+
// Step 1: Disable grid charging (Register 17: chaGriSet=PV)
|
|
669
|
+
const step1 = await this.enableGridCharging(false);
|
|
670
|
+
if (!step1) {
|
|
671
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to disable grid charging');
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
// Step 2: Set storage mode to AUTO (Register 5: storCtlMod=0x0003)
|
|
675
|
+
const step2 = await this.setStorageMode(sunspec_interfaces_js_1.SunspecStorageMode.AUTO);
|
|
676
|
+
if (!step2) {
|
|
677
|
+
// Rollback step 1
|
|
678
|
+
await this.writeBatteryControls({ chaGriSet: originalChaGriSet });
|
|
679
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set storage mode to auto');
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
683
|
+
}
|
|
684
|
+
async handleSetDischargeLimit(msg) {
|
|
685
|
+
if (!this.isConnected() || !this.applianceId) {
|
|
686
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
console.log(`Battery ${this.applianceId}: handling SetStorageDischargeLimitV1 (dischargeLimitPercent=${msg.data.dischargeLimitPercent})`);
|
|
690
|
+
// Read current state for logging
|
|
691
|
+
const controls = await this.getBatteryControls();
|
|
692
|
+
console.log(`Battery ${this.applianceId}: current state - outWRte=${controls?.outWRte}`);
|
|
693
|
+
// Set discharge limit (Register 12: outWRte)
|
|
694
|
+
const success = await this.writeBatteryControls({ outWRte: msg.data.dischargeLimitPercent });
|
|
695
|
+
if (!success) {
|
|
696
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set discharge limit');
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
700
|
+
}
|
|
701
|
+
sendCommandAcknowledge(messageId, acknowledgeMessage, answer, rejectionReason) {
|
|
702
|
+
if (!this.dataBus || !this.applianceId) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const ackMessage = {
|
|
706
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
707
|
+
message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.CommandAcknowledgeV1,
|
|
708
|
+
type: 'answer',
|
|
709
|
+
source: enyo_source_enum_js_1.EnyoSourceEnum.Device,
|
|
710
|
+
applianceId: this.applianceId,
|
|
711
|
+
timestampIso: new Date().toISOString(),
|
|
712
|
+
data: {
|
|
713
|
+
messageId,
|
|
714
|
+
acknowledgeMessage,
|
|
715
|
+
answer,
|
|
716
|
+
rejectionReason
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
console.log(`Battery ${this.applianceId}: sending ${answer} for ${acknowledgeMessage} (messageId=${messageId}${rejectionReason ? `, reason=${rejectionReason}` : ''})`);
|
|
720
|
+
this.dataBus.sendMessage([ackMessage]);
|
|
721
|
+
}
|
|
565
722
|
}
|
|
566
723
|
exports.SunspecBattery = SunspecBattery;
|
|
567
724
|
/**
|
|
@@ -14,9 +14,11 @@ export declare abstract class BaseSunspecDevice {
|
|
|
14
14
|
protected readonly sunspecClient: SunspecModbusClient;
|
|
15
15
|
protected readonly applianceManager: ApplianceManager;
|
|
16
16
|
protected readonly unitId: number;
|
|
17
|
+
protected readonly port: number;
|
|
18
|
+
protected readonly baseAddress: number;
|
|
17
19
|
protected applianceId?: string;
|
|
18
20
|
protected lastUpdateTime: number;
|
|
19
|
-
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number);
|
|
21
|
+
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number);
|
|
20
22
|
/**
|
|
21
23
|
* Connect to the device and create/update the appliance
|
|
22
24
|
*/
|
|
@@ -62,6 +64,8 @@ export declare class SunspecInverter extends BaseSunspecDevice {
|
|
|
62
64
|
* Sunspec Battery implementation
|
|
63
65
|
*/
|
|
64
66
|
export declare class SunspecBattery extends BaseSunspecDevice {
|
|
67
|
+
private dataBusListenerId?;
|
|
68
|
+
private dataBus?;
|
|
65
69
|
/**
|
|
66
70
|
* Connect to the battery and create/update the appliance
|
|
67
71
|
*/
|
|
@@ -150,6 +154,20 @@ export declare class SunspecBattery extends BaseSunspecDevice {
|
|
|
150
154
|
* @returns Promise<boolean | null> - true if enabled, false if disabled, null if error
|
|
151
155
|
*/
|
|
152
156
|
isGridChargingEnabled(): Promise<boolean | null>;
|
|
157
|
+
/**
|
|
158
|
+
* Start listening for storage commands on the data bus.
|
|
159
|
+
* Idempotent — does nothing if already listening.
|
|
160
|
+
*/
|
|
161
|
+
startDataBusListening(): void;
|
|
162
|
+
/**
|
|
163
|
+
* Stop listening for storage commands on the data bus.
|
|
164
|
+
*/
|
|
165
|
+
stopDataBusListening(): void;
|
|
166
|
+
private handleStorageCommand;
|
|
167
|
+
private handleStartGridCharge;
|
|
168
|
+
private handleStopGridCharge;
|
|
169
|
+
private handleSetDischargeLimit;
|
|
170
|
+
private sendCommandAcknowledge;
|
|
153
171
|
}
|
|
154
172
|
/**
|
|
155
173
|
* Sunspec Meter implementation
|
|
@@ -29,7 +29,6 @@ class SunspecModbusClient {
|
|
|
29
29
|
modbusClient = null;
|
|
30
30
|
discoveredModels = new Map();
|
|
31
31
|
connected = false;
|
|
32
|
-
baseAddress = 40001;
|
|
33
32
|
connectionHealth;
|
|
34
33
|
faultTolerantReader = null;
|
|
35
34
|
modbusDataTypeConverter;
|
|
@@ -225,49 +224,86 @@ class SunspecModbusClient {
|
|
|
225
224
|
/**
|
|
226
225
|
* Detect the base address and addressing mode (0-based or 1-based) for SunSpec
|
|
227
226
|
*/
|
|
228
|
-
async detectSunspecBaseAddress() {
|
|
227
|
+
async detectSunspecBaseAddress(customBaseAddress) {
|
|
229
228
|
if (!this.modbusClient) {
|
|
230
229
|
throw new Error('Modbus client not initialized');
|
|
231
230
|
}
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
this.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
231
|
+
// If custom base address provided, try it first (1-based, then 0-based variant)
|
|
232
|
+
if (customBaseAddress !== undefined) {
|
|
233
|
+
console.log(`Detect models for custom base address '${customBaseAddress}' ...`);
|
|
234
|
+
// Try 0-based at custom address
|
|
235
|
+
try {
|
|
236
|
+
const sunspecId = await this.modbusClient.readRegisterStringValue(customBaseAddress, 2);
|
|
237
|
+
if (sunspecId.includes('SunS')) {
|
|
238
|
+
console.log(`Detected 0-based addressing mode (base address: ${customBaseAddress})`);
|
|
239
|
+
return {
|
|
240
|
+
baseAddress: customBaseAddress,
|
|
241
|
+
isZeroBased: true,
|
|
242
|
+
nextAddress: customBaseAddress + 2
|
|
243
|
+
};
|
|
244
|
+
}
|
|
243
245
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
246
|
+
catch (error) {
|
|
247
|
+
console.debug(`Could not read SunS at ${customBaseAddress}:`, error);
|
|
248
|
+
}
|
|
249
|
+
// Try 1-based at custom address (customBaseAddress + 1)
|
|
250
|
+
try {
|
|
251
|
+
const sunspecId = await this.modbusClient.readRegisterStringValue(customBaseAddress + 1, 2);
|
|
252
|
+
if (sunspecId.includes('SunS')) {
|
|
253
|
+
console.log(`Detected 1-based addressing mode (base address: ${customBaseAddress + 1})`);
|
|
254
|
+
return {
|
|
255
|
+
baseAddress: customBaseAddress + 1,
|
|
256
|
+
isZeroBased: false,
|
|
257
|
+
nextAddress: customBaseAddress + 3
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
console.debug(`Could not read SunS at ${customBaseAddress + 1}:`, error);
|
|
259
263
|
}
|
|
260
264
|
}
|
|
261
|
-
|
|
262
|
-
|
|
265
|
+
else {
|
|
266
|
+
// Try 1-based addressing first (most common)
|
|
267
|
+
try {
|
|
268
|
+
const sunspecId = await this.modbusClient.readRegisterStringValue(40001, 2);
|
|
269
|
+
if (sunspecId.includes('SunS')) {
|
|
270
|
+
console.log('Detected 1-based addressing mode (base address: 40001)');
|
|
271
|
+
return {
|
|
272
|
+
baseAddress: 40001,
|
|
273
|
+
isZeroBased: false,
|
|
274
|
+
nextAddress: 40003
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
console.debug('Could not read SunS at 40001:', error);
|
|
280
|
+
}
|
|
281
|
+
// Try 0-based addressing
|
|
282
|
+
try {
|
|
283
|
+
const sunspecId = await this.modbusClient.readRegisterStringValue(40000, 2);
|
|
284
|
+
if (sunspecId.includes('SunS')) {
|
|
285
|
+
console.log('Detected 0-based addressing mode (base address: 40000)');
|
|
286
|
+
return {
|
|
287
|
+
baseAddress: 40000,
|
|
288
|
+
isZeroBased: true,
|
|
289
|
+
nextAddress: 40002
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
console.debug('Could not read SunS at 40000:', error);
|
|
295
|
+
}
|
|
263
296
|
}
|
|
264
|
-
|
|
297
|
+
const addressesChecked = customBaseAddress !== undefined
|
|
298
|
+
? `${customBaseAddress}, ${customBaseAddress + 1}, 40000, or 40001`
|
|
299
|
+
: '40000 or 40001';
|
|
300
|
+
throw new Error(`Device is not SunSpec compliant - "SunS" identifier not found at addresses ${addressesChecked}`);
|
|
265
301
|
}
|
|
266
302
|
/**
|
|
267
303
|
* Discover all available Sunspec models
|
|
268
304
|
* Automatically detects base address (40000 or 40001) and scans from there
|
|
269
305
|
*/
|
|
270
|
-
async discoverModels() {
|
|
306
|
+
async discoverModels(customBaseAddress) {
|
|
271
307
|
if (!this.connected) {
|
|
272
308
|
throw new Error('Not connected to Modbus device');
|
|
273
309
|
}
|
|
@@ -277,7 +313,7 @@ class SunspecModbusClient {
|
|
|
277
313
|
console.log('Starting Sunspec model discovery...');
|
|
278
314
|
try {
|
|
279
315
|
// Detect the base address and addressing mode
|
|
280
|
-
const addressInfo = await this.detectSunspecBaseAddress();
|
|
316
|
+
const addressInfo = await this.detectSunspecBaseAddress(customBaseAddress);
|
|
281
317
|
currentAddress = addressInfo.nextAddress;
|
|
282
318
|
while (currentAddress < maxAddress) {
|
|
283
319
|
// Read model ID and length
|
|
@@ -25,7 +25,6 @@ export declare class SunspecModbusClient {
|
|
|
25
25
|
private modbusClient;
|
|
26
26
|
private discoveredModels;
|
|
27
27
|
private connected;
|
|
28
|
-
private baseAddress;
|
|
29
28
|
private connectionHealth;
|
|
30
29
|
private faultTolerantReader;
|
|
31
30
|
private modbusDataTypeConverter;
|
|
@@ -76,7 +75,7 @@ export declare class SunspecModbusClient {
|
|
|
76
75
|
/**
|
|
77
76
|
* Detect the base address and addressing mode (0-based or 1-based) for SunSpec
|
|
78
77
|
*/
|
|
79
|
-
detectSunspecBaseAddress(): Promise<{
|
|
78
|
+
detectSunspecBaseAddress(customBaseAddress?: number): Promise<{
|
|
80
79
|
baseAddress: number;
|
|
81
80
|
isZeroBased: boolean;
|
|
82
81
|
nextAddress: number;
|
|
@@ -85,7 +84,7 @@ export declare class SunspecModbusClient {
|
|
|
85
84
|
* Discover all available Sunspec models
|
|
86
85
|
* Automatically detects base address (40000 or 40001) and scans from there
|
|
87
86
|
*/
|
|
88
|
-
discoverModels(): Promise<Map<number, SunspecModel>>;
|
|
87
|
+
discoverModels(customBaseAddress?: number): Promise<Map<number, SunspecModel>>;
|
|
89
88
|
/**
|
|
90
89
|
* Find a specific model by ID
|
|
91
90
|
*/
|
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.31';
|
|
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
|
@@ -14,9 +14,11 @@ export declare abstract class BaseSunspecDevice {
|
|
|
14
14
|
protected readonly sunspecClient: SunspecModbusClient;
|
|
15
15
|
protected readonly applianceManager: ApplianceManager;
|
|
16
16
|
protected readonly unitId: number;
|
|
17
|
+
protected readonly port: number;
|
|
18
|
+
protected readonly baseAddress: number;
|
|
17
19
|
protected applianceId?: string;
|
|
18
20
|
protected lastUpdateTime: number;
|
|
19
|
-
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number);
|
|
21
|
+
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number);
|
|
20
22
|
/**
|
|
21
23
|
* Connect to the device and create/update the appliance
|
|
22
24
|
*/
|
|
@@ -62,6 +64,8 @@ export declare class SunspecInverter extends BaseSunspecDevice {
|
|
|
62
64
|
* Sunspec Battery implementation
|
|
63
65
|
*/
|
|
64
66
|
export declare class SunspecBattery extends BaseSunspecDevice {
|
|
67
|
+
private dataBusListenerId?;
|
|
68
|
+
private dataBus?;
|
|
65
69
|
/**
|
|
66
70
|
* Connect to the battery and create/update the appliance
|
|
67
71
|
*/
|
|
@@ -150,6 +154,20 @@ export declare class SunspecBattery extends BaseSunspecDevice {
|
|
|
150
154
|
* @returns Promise<boolean | null> - true if enabled, false if disabled, null if error
|
|
151
155
|
*/
|
|
152
156
|
isGridChargingEnabled(): Promise<boolean | null>;
|
|
157
|
+
/**
|
|
158
|
+
* Start listening for storage commands on the data bus.
|
|
159
|
+
* Idempotent — does nothing if already listening.
|
|
160
|
+
*/
|
|
161
|
+
startDataBusListening(): void;
|
|
162
|
+
/**
|
|
163
|
+
* Stop listening for storage commands on the data bus.
|
|
164
|
+
*/
|
|
165
|
+
stopDataBusListening(): void;
|
|
166
|
+
private handleStorageCommand;
|
|
167
|
+
private handleStartGridCharge;
|
|
168
|
+
private handleStopGridCharge;
|
|
169
|
+
private handleSetDischargeLimit;
|
|
170
|
+
private sendCommandAcknowledge;
|
|
153
171
|
}
|
|
154
172
|
/**
|
|
155
173
|
* Sunspec Meter implementation
|
package/dist/sunspec-devices.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SunspecBatteryChargeState, SunspecModelId, SunspecMPPTOperatingState, SunspecStorageMode } from "./sunspec-interfaces.js";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { EnyoApplianceConnectionType, EnyoApplianceStateEnum, EnyoApplianceTopologyFeatureEnum, EnyoApplianceTypeEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
|
|
4
|
-
import { EnyoBatteryStateEnum, EnyoDataBusMessageEnum, EnyoInverterStateEnum, EnyoStringStateEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
|
|
4
|
+
import { EnyoBatteryStateEnum, EnyoCommandAcknowledgeAnswerEnum, EnyoDataBusMessageEnum, EnyoInverterStateEnum, EnyoStringStateEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
|
|
5
5
|
import { EnyoSourceEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-source.enum.js";
|
|
6
6
|
import { EnyoMeterApplianceAvailableFeaturesEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-meter-appliance.js";
|
|
7
7
|
import { EnyoBatteryFeature, EnyoBatteryStorageMode } from "@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js";
|
|
@@ -28,15 +28,19 @@ export class BaseSunspecDevice {
|
|
|
28
28
|
sunspecClient;
|
|
29
29
|
applianceManager;
|
|
30
30
|
unitId;
|
|
31
|
+
port;
|
|
32
|
+
baseAddress;
|
|
31
33
|
applianceId;
|
|
32
34
|
lastUpdateTime = 0;
|
|
33
|
-
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1) {
|
|
35
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000) {
|
|
34
36
|
this.energyApp = energyApp;
|
|
35
37
|
this.name = name;
|
|
36
38
|
this.networkDevice = networkDevice;
|
|
37
39
|
this.sunspecClient = sunspecClient;
|
|
38
40
|
this.applianceManager = applianceManager;
|
|
39
41
|
this.unitId = unitId;
|
|
42
|
+
this.port = port;
|
|
43
|
+
this.baseAddress = baseAddress;
|
|
40
44
|
}
|
|
41
45
|
/**
|
|
42
46
|
* Check if the device is connected
|
|
@@ -53,9 +57,9 @@ export class BaseSunspecDevice {
|
|
|
53
57
|
async ensureConnected() {
|
|
54
58
|
if (!this.sunspecClient.isConnected()) {
|
|
55
59
|
await this.sunspecClient.connect(this.networkDevice.hostname, // primary
|
|
56
|
-
|
|
60
|
+
this.port, this.unitId, this.networkDevice.ipAddress // secondary fallback
|
|
57
61
|
);
|
|
58
|
-
await this.sunspecClient.discoverModels();
|
|
62
|
+
await this.sunspecClient.discoverModels(this.baseAddress);
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
65
|
}
|
|
@@ -67,9 +71,9 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
67
71
|
// Ensure Sunspec client is connected
|
|
68
72
|
if (!this.sunspecClient.isConnected()) {
|
|
69
73
|
await this.sunspecClient.connect(this.networkDevice.hostname, // primary
|
|
70
|
-
|
|
74
|
+
this.port, this.unitId, this.networkDevice.ipAddress // secondary fallback
|
|
71
75
|
);
|
|
72
|
-
await this.sunspecClient.discoverModels();
|
|
76
|
+
await this.sunspecClient.discoverModels(this.baseAddress);
|
|
73
77
|
}
|
|
74
78
|
// Get device info from common block
|
|
75
79
|
const commonData = await this.sunspecClient.readCommonBlock();
|
|
@@ -240,6 +244,8 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
240
244
|
* Sunspec Battery implementation
|
|
241
245
|
*/
|
|
242
246
|
export class SunspecBattery extends BaseSunspecDevice {
|
|
247
|
+
dataBusListenerId;
|
|
248
|
+
dataBus;
|
|
243
249
|
/**
|
|
244
250
|
* Connect to the battery and create/update the appliance
|
|
245
251
|
*/
|
|
@@ -283,12 +289,14 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
283
289
|
}
|
|
284
290
|
});
|
|
285
291
|
console.log(`Sunspec Battery connected: ${this.networkDevice.hostname} (${this.applianceId})`);
|
|
292
|
+
this.startDataBusListening();
|
|
286
293
|
}
|
|
287
294
|
catch (error) {
|
|
288
295
|
console.error(`Failed to create battery appliance: ${error}`);
|
|
289
296
|
}
|
|
290
297
|
}
|
|
291
298
|
async disconnect() {
|
|
299
|
+
this.stopDataBusListening();
|
|
292
300
|
if (this.applianceId) {
|
|
293
301
|
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
|
|
294
302
|
}
|
|
@@ -557,6 +565,155 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
557
565
|
}
|
|
558
566
|
return controls.chaGriSet === 1; // 1 = GRID, 0 = PV
|
|
559
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* Start listening for storage commands on the data bus.
|
|
570
|
+
* Idempotent — does nothing if already listening.
|
|
571
|
+
*/
|
|
572
|
+
startDataBusListening() {
|
|
573
|
+
if (this.dataBusListenerId) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
this.dataBus = this.energyApp.useDataBus();
|
|
577
|
+
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
578
|
+
EnyoDataBusMessageEnum.StartStorageGridChargeV1,
|
|
579
|
+
EnyoDataBusMessageEnum.StopStorageGridChargeV1,
|
|
580
|
+
EnyoDataBusMessageEnum.SetStorageDischargeLimitV1
|
|
581
|
+
], (entry) => this.handleStorageCommand(entry));
|
|
582
|
+
console.log(`Battery ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Stop listening for storage commands on the data bus.
|
|
586
|
+
*/
|
|
587
|
+
stopDataBusListening() {
|
|
588
|
+
if (this.dataBusListenerId && this.dataBus) {
|
|
589
|
+
this.dataBus.unsubscribe(this.dataBusListenerId);
|
|
590
|
+
console.log(`Battery ${this.applianceId}: stopped data bus listening (listener ${this.dataBusListenerId})`);
|
|
591
|
+
}
|
|
592
|
+
this.dataBusListenerId = undefined;
|
|
593
|
+
this.dataBus = undefined;
|
|
594
|
+
}
|
|
595
|
+
handleStorageCommand(entry) {
|
|
596
|
+
if (entry.applianceId !== this.applianceId) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
void (async () => {
|
|
600
|
+
try {
|
|
601
|
+
switch (entry.message) {
|
|
602
|
+
case EnyoDataBusMessageEnum.StartStorageGridChargeV1:
|
|
603
|
+
await this.handleStartGridCharge(entry);
|
|
604
|
+
break;
|
|
605
|
+
case EnyoDataBusMessageEnum.StopStorageGridChargeV1:
|
|
606
|
+
await this.handleStopGridCharge(entry);
|
|
607
|
+
break;
|
|
608
|
+
case EnyoDataBusMessageEnum.SetStorageDischargeLimitV1:
|
|
609
|
+
await this.handleSetDischargeLimit(entry);
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
console.error(`Battery ${this.applianceId}: error handling ${entry.message}:`, error);
|
|
615
|
+
}
|
|
616
|
+
})();
|
|
617
|
+
}
|
|
618
|
+
async handleStartGridCharge(msg) {
|
|
619
|
+
if (!this.isConnected() || !this.applianceId) {
|
|
620
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, "Not connected");
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
console.log(`Battery ${this.applianceId}: handling StartStorageGridChargeV1 (powerLimitW=${msg.data.powerLimitW})`);
|
|
624
|
+
// Read current state for logging and rollback
|
|
625
|
+
const controls = await this.getBatteryControls();
|
|
626
|
+
console.log(`Battery ${this.applianceId}: current state - chaGriSet=${controls?.chaGriSet}, wChaMax=${controls?.wChaMax}, storCtlMod=${controls?.storCtlMod}`);
|
|
627
|
+
const originalChaGriSet = controls?.chaGriSet;
|
|
628
|
+
const originalWChaMax = controls?.wChaMax;
|
|
629
|
+
// Step 1: Enable grid charging (Register 17: chaGriSet=GRID)
|
|
630
|
+
const step1 = await this.enableGridCharging(true);
|
|
631
|
+
if (!step1) {
|
|
632
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to enable grid charging');
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
// Step 2: Set charging power (Register 2: wChaMax)
|
|
636
|
+
const step2 = await this.setChargingPower(msg.data.powerLimitW);
|
|
637
|
+
if (!step2) {
|
|
638
|
+
// Rollback step 1
|
|
639
|
+
await this.writeBatteryControls({ chaGriSet: originalChaGriSet });
|
|
640
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set charging power');
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
// Step 3: Set storage mode to CHARGE (Register 5: storCtlMod=0x0001)
|
|
644
|
+
const step3 = await this.setStorageMode(SunspecStorageMode.CHARGE);
|
|
645
|
+
if (!step3) {
|
|
646
|
+
// Rollback steps 1+2
|
|
647
|
+
await this.writeBatteryControls({ chaGriSet: originalChaGriSet, wChaMax: originalWChaMax });
|
|
648
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set storage mode to charge');
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
652
|
+
}
|
|
653
|
+
async handleStopGridCharge(msg) {
|
|
654
|
+
if (!this.isConnected() || !this.applianceId) {
|
|
655
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.NotSupported);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
console.log(`Battery ${this.applianceId}: handling StopStorageGridChargeV1`);
|
|
659
|
+
// Read current state for logging and rollback
|
|
660
|
+
const controls = await this.getBatteryControls();
|
|
661
|
+
console.log(`Battery ${this.applianceId}: current state - chaGriSet=${controls?.chaGriSet}, storCtlMod=${controls?.storCtlMod}`);
|
|
662
|
+
const originalChaGriSet = controls?.chaGriSet;
|
|
663
|
+
// Step 1: Disable grid charging (Register 17: chaGriSet=PV)
|
|
664
|
+
const step1 = await this.enableGridCharging(false);
|
|
665
|
+
if (!step1) {
|
|
666
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to disable grid charging');
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
// Step 2: Set storage mode to AUTO (Register 5: storCtlMod=0x0003)
|
|
670
|
+
const step2 = await this.setStorageMode(SunspecStorageMode.AUTO);
|
|
671
|
+
if (!step2) {
|
|
672
|
+
// Rollback step 1
|
|
673
|
+
await this.writeBatteryControls({ chaGriSet: originalChaGriSet });
|
|
674
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set storage mode to auto');
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
678
|
+
}
|
|
679
|
+
async handleSetDischargeLimit(msg) {
|
|
680
|
+
if (!this.isConnected() || !this.applianceId) {
|
|
681
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.NotSupported);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
console.log(`Battery ${this.applianceId}: handling SetStorageDischargeLimitV1 (dischargeLimitPercent=${msg.data.dischargeLimitPercent})`);
|
|
685
|
+
// Read current state for logging
|
|
686
|
+
const controls = await this.getBatteryControls();
|
|
687
|
+
console.log(`Battery ${this.applianceId}: current state - outWRte=${controls?.outWRte}`);
|
|
688
|
+
// Set discharge limit (Register 12: outWRte)
|
|
689
|
+
const success = await this.writeBatteryControls({ outWRte: msg.data.dischargeLimitPercent });
|
|
690
|
+
if (!success) {
|
|
691
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set discharge limit');
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
695
|
+
}
|
|
696
|
+
sendCommandAcknowledge(messageId, acknowledgeMessage, answer, rejectionReason) {
|
|
697
|
+
if (!this.dataBus || !this.applianceId) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const ackMessage = {
|
|
701
|
+
id: randomUUID(),
|
|
702
|
+
message: EnyoDataBusMessageEnum.CommandAcknowledgeV1,
|
|
703
|
+
type: 'answer',
|
|
704
|
+
source: EnyoSourceEnum.Device,
|
|
705
|
+
applianceId: this.applianceId,
|
|
706
|
+
timestampIso: new Date().toISOString(),
|
|
707
|
+
data: {
|
|
708
|
+
messageId,
|
|
709
|
+
acknowledgeMessage,
|
|
710
|
+
answer,
|
|
711
|
+
rejectionReason
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
console.log(`Battery ${this.applianceId}: sending ${answer} for ${acknowledgeMessage} (messageId=${messageId}${rejectionReason ? `, reason=${rejectionReason}` : ''})`);
|
|
715
|
+
this.dataBus.sendMessage([ackMessage]);
|
|
716
|
+
}
|
|
560
717
|
}
|
|
561
718
|
/**
|
|
562
719
|
* Sunspec Meter implementation
|
|
@@ -25,7 +25,6 @@ export declare class SunspecModbusClient {
|
|
|
25
25
|
private modbusClient;
|
|
26
26
|
private discoveredModels;
|
|
27
27
|
private connected;
|
|
28
|
-
private baseAddress;
|
|
29
28
|
private connectionHealth;
|
|
30
29
|
private faultTolerantReader;
|
|
31
30
|
private modbusDataTypeConverter;
|
|
@@ -76,7 +75,7 @@ export declare class SunspecModbusClient {
|
|
|
76
75
|
/**
|
|
77
76
|
* Detect the base address and addressing mode (0-based or 1-based) for SunSpec
|
|
78
77
|
*/
|
|
79
|
-
detectSunspecBaseAddress(): Promise<{
|
|
78
|
+
detectSunspecBaseAddress(customBaseAddress?: number): Promise<{
|
|
80
79
|
baseAddress: number;
|
|
81
80
|
isZeroBased: boolean;
|
|
82
81
|
nextAddress: number;
|
|
@@ -85,7 +84,7 @@ export declare class SunspecModbusClient {
|
|
|
85
84
|
* Discover all available Sunspec models
|
|
86
85
|
* Automatically detects base address (40000 or 40001) and scans from there
|
|
87
86
|
*/
|
|
88
|
-
discoverModels(): Promise<Map<number, SunspecModel>>;
|
|
87
|
+
discoverModels(customBaseAddress?: number): Promise<Map<number, SunspecModel>>;
|
|
89
88
|
/**
|
|
90
89
|
* Find a specific model by ID
|
|
91
90
|
*/
|
|
@@ -26,7 +26,6 @@ export class SunspecModbusClient {
|
|
|
26
26
|
modbusClient = null;
|
|
27
27
|
discoveredModels = new Map();
|
|
28
28
|
connected = false;
|
|
29
|
-
baseAddress = 40001;
|
|
30
29
|
connectionHealth;
|
|
31
30
|
faultTolerantReader = null;
|
|
32
31
|
modbusDataTypeConverter;
|
|
@@ -222,49 +221,86 @@ export class SunspecModbusClient {
|
|
|
222
221
|
/**
|
|
223
222
|
* Detect the base address and addressing mode (0-based or 1-based) for SunSpec
|
|
224
223
|
*/
|
|
225
|
-
async detectSunspecBaseAddress() {
|
|
224
|
+
async detectSunspecBaseAddress(customBaseAddress) {
|
|
226
225
|
if (!this.modbusClient) {
|
|
227
226
|
throw new Error('Modbus client not initialized');
|
|
228
227
|
}
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
this.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
228
|
+
// If custom base address provided, try it first (1-based, then 0-based variant)
|
|
229
|
+
if (customBaseAddress !== undefined) {
|
|
230
|
+
console.log(`Detect models for custom base address '${customBaseAddress}' ...`);
|
|
231
|
+
// Try 0-based at custom address
|
|
232
|
+
try {
|
|
233
|
+
const sunspecId = await this.modbusClient.readRegisterStringValue(customBaseAddress, 2);
|
|
234
|
+
if (sunspecId.includes('SunS')) {
|
|
235
|
+
console.log(`Detected 0-based addressing mode (base address: ${customBaseAddress})`);
|
|
236
|
+
return {
|
|
237
|
+
baseAddress: customBaseAddress,
|
|
238
|
+
isZeroBased: true,
|
|
239
|
+
nextAddress: customBaseAddress + 2
|
|
240
|
+
};
|
|
241
|
+
}
|
|
240
242
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
243
|
+
catch (error) {
|
|
244
|
+
console.debug(`Could not read SunS at ${customBaseAddress}:`, error);
|
|
245
|
+
}
|
|
246
|
+
// Try 1-based at custom address (customBaseAddress + 1)
|
|
247
|
+
try {
|
|
248
|
+
const sunspecId = await this.modbusClient.readRegisterStringValue(customBaseAddress + 1, 2);
|
|
249
|
+
if (sunspecId.includes('SunS')) {
|
|
250
|
+
console.log(`Detected 1-based addressing mode (base address: ${customBaseAddress + 1})`);
|
|
251
|
+
return {
|
|
252
|
+
baseAddress: customBaseAddress + 1,
|
|
253
|
+
isZeroBased: false,
|
|
254
|
+
nextAddress: customBaseAddress + 3
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
console.debug(`Could not read SunS at ${customBaseAddress + 1}:`, error);
|
|
256
260
|
}
|
|
257
261
|
}
|
|
258
|
-
|
|
259
|
-
|
|
262
|
+
else {
|
|
263
|
+
// Try 1-based addressing first (most common)
|
|
264
|
+
try {
|
|
265
|
+
const sunspecId = await this.modbusClient.readRegisterStringValue(40001, 2);
|
|
266
|
+
if (sunspecId.includes('SunS')) {
|
|
267
|
+
console.log('Detected 1-based addressing mode (base address: 40001)');
|
|
268
|
+
return {
|
|
269
|
+
baseAddress: 40001,
|
|
270
|
+
isZeroBased: false,
|
|
271
|
+
nextAddress: 40003
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
console.debug('Could not read SunS at 40001:', error);
|
|
277
|
+
}
|
|
278
|
+
// Try 0-based addressing
|
|
279
|
+
try {
|
|
280
|
+
const sunspecId = await this.modbusClient.readRegisterStringValue(40000, 2);
|
|
281
|
+
if (sunspecId.includes('SunS')) {
|
|
282
|
+
console.log('Detected 0-based addressing mode (base address: 40000)');
|
|
283
|
+
return {
|
|
284
|
+
baseAddress: 40000,
|
|
285
|
+
isZeroBased: true,
|
|
286
|
+
nextAddress: 40002
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
console.debug('Could not read SunS at 40000:', error);
|
|
292
|
+
}
|
|
260
293
|
}
|
|
261
|
-
|
|
294
|
+
const addressesChecked = customBaseAddress !== undefined
|
|
295
|
+
? `${customBaseAddress}, ${customBaseAddress + 1}, 40000, or 40001`
|
|
296
|
+
: '40000 or 40001';
|
|
297
|
+
throw new Error(`Device is not SunSpec compliant - "SunS" identifier not found at addresses ${addressesChecked}`);
|
|
262
298
|
}
|
|
263
299
|
/**
|
|
264
300
|
* Discover all available Sunspec models
|
|
265
301
|
* Automatically detects base address (40000 or 40001) and scans from there
|
|
266
302
|
*/
|
|
267
|
-
async discoverModels() {
|
|
303
|
+
async discoverModels(customBaseAddress) {
|
|
268
304
|
if (!this.connected) {
|
|
269
305
|
throw new Error('Not connected to Modbus device');
|
|
270
306
|
}
|
|
@@ -274,7 +310,7 @@ export class SunspecModbusClient {
|
|
|
274
310
|
console.log('Starting Sunspec model discovery...');
|
|
275
311
|
try {
|
|
276
312
|
// Detect the base address and addressing mode
|
|
277
|
-
const addressInfo = await this.detectSunspecBaseAddress();
|
|
313
|
+
const addressInfo = await this.detectSunspecBaseAddress(customBaseAddress);
|
|
278
314
|
currentAddress = addressInfo.nextAddress;
|
|
279
315
|
while (currentAddress < maxAddress) {
|
|
280
316
|
// Read model ID and length
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enyo-energy/sunspec-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.31",
|
|
4
4
|
"description": "enyo Energy Sunspec SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"typescript": "^5.8.3"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@enyo-energy/energy-app-sdk": "^0.0.
|
|
40
|
+
"@enyo-energy/energy-app-sdk": "^0.0.60"
|
|
41
41
|
},
|
|
42
42
|
"volta": {
|
|
43
43
|
"node": "22.17.0"
|