@enyo-energy/sunspec-sdk 0.0.67 → 0.0.69
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/README.md +24 -0
- package/dist/cjs/sunspec-devices.cjs +83 -6
- package/dist/cjs/sunspec-devices.d.cts +22 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/sunspec-devices.d.ts +22 -0
- package/dist/sunspec-devices.js +84 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@ SunSpec Modbus client for reading data from solar inverters, batteries, meters,
|
|
|
4
4
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
7
|
+
- [Appliance Manager Integration](#appliance-manager-integration)
|
|
7
8
|
- [How Addressing Works](#how-addressing-works)
|
|
8
9
|
- [Bulk Register Reading](#bulk-register-reading)
|
|
9
10
|
- [Data Types](#data-types)
|
|
@@ -22,6 +23,29 @@ SunSpec Modbus client for reading data from solar inverters, batteries, meters,
|
|
|
22
23
|
|
|
23
24
|
---
|
|
24
25
|
|
|
26
|
+
## Appliance Manager Integration
|
|
27
|
+
|
|
28
|
+
`SunspecInverter`, `SunspecBattery`, and `SunspecMeter` take an `ApplianceManager` from `@enyo-energy/energy-app-sdk` and call `createOrUpdateAppliance` with the device's `networkDevice` plus the common block's `serialNumber` (which may be empty for some firmware revisions).
|
|
29
|
+
|
|
30
|
+
Since `@enyo-energy/energy-app-sdk` 0.0.135 the default identifier strategy is `SerialNumberStrategy`. If a device's common block has no serial number, `createOrUpdateAppliance` throws `MissingIdentifierError` and the SDK logs the error and skips appliance creation.
|
|
31
|
+
|
|
32
|
+
Consumers whose devices may not expose a serial number should configure a fallback when initializing the `ApplianceManager`, e.g.:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { ApplianceManager, FallbackIdentifierStrategy, SerialNumberStrategy, HostnameStrategy } from '@enyo-energy/energy-app-sdk';
|
|
36
|
+
|
|
37
|
+
const applianceManager = await ApplianceManager.initialize(energyApp, {
|
|
38
|
+
identifierStrategy: new FallbackIdentifierStrategy([
|
|
39
|
+
new SerialNumberStrategy(),
|
|
40
|
+
new HostnameStrategy(),
|
|
41
|
+
]),
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This SDK does not configure the strategy itself — it is the consumer app's responsibility.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
25
49
|
## How Addressing Works
|
|
26
50
|
|
|
27
51
|
### Base Address Detection
|
|
@@ -278,7 +278,9 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
278
278
|
// Get device info from common block
|
|
279
279
|
const commonData = await this.sunspecClient.readCommonBlock(this.unitId);
|
|
280
280
|
const inverterSettings = await this.sunspecClient.readInverterSettings(this.unitId);
|
|
281
|
+
const inverterControls = await this.sunspecClient.readInverterControls(this.unitId);
|
|
281
282
|
const mpptDataList = await this.sunspecClient.readAllMPPTData(this.unitId);
|
|
283
|
+
const activeProductionLimitationW = this.computeActiveProductionLimitW(inverterSettings, inverterControls);
|
|
282
284
|
// Create or update appliance (skip if an existing appliance was provided)
|
|
283
285
|
if (!this.applianceId) {
|
|
284
286
|
try {
|
|
@@ -296,7 +298,8 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
296
298
|
},
|
|
297
299
|
inverter: {
|
|
298
300
|
dcStrings: this.mapDcStringToApplianceMetadata(this.mapMPPTToStrings(mpptDataList)),
|
|
299
|
-
maxPvProductionW: inverterSettings?.WMax
|
|
301
|
+
maxPvProductionW: inverterSettings?.WMax,
|
|
302
|
+
activeProductionLimitationW: activeProductionLimitationW
|
|
300
303
|
}
|
|
301
304
|
});
|
|
302
305
|
console.log(`Sunspec Inverter connected: ${this.networkDevice.hostname} (${this.applianceId})`);
|
|
@@ -318,7 +321,8 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
318
321
|
inverter: {
|
|
319
322
|
...existingAppliance?.inverter,
|
|
320
323
|
dcStrings: this.mapDcStringToApplianceMetadata(this.mapMPPTToStrings(mpptDataList)),
|
|
321
|
-
maxPvProductionW: inverterSettings?.WMax
|
|
324
|
+
maxPvProductionW: inverterSettings?.WMax,
|
|
325
|
+
activeProductionLimitationW: activeProductionLimitationW
|
|
322
326
|
}
|
|
323
327
|
});
|
|
324
328
|
console.log(`Sunspec Inverter connected: ${this.networkDevice.hostname} (${this.applianceId})`);
|
|
@@ -338,7 +342,12 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
338
342
|
async disconnect() {
|
|
339
343
|
this.stopDataBusListening();
|
|
340
344
|
if (this.applianceId) {
|
|
341
|
-
|
|
345
|
+
try {
|
|
346
|
+
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
console.error(`Failed to mark inverter offline on disconnect: ${error}`);
|
|
350
|
+
}
|
|
342
351
|
}
|
|
343
352
|
// Note: We don't disconnect the sunspecClient as it may be shared
|
|
344
353
|
}
|
|
@@ -355,7 +364,9 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
355
364
|
const inverterData = await this.sunspecClient.readInverterData(this.unitId);
|
|
356
365
|
const mpptDataList = await this.sunspecClient.readAllMPPTData(this.unitId);
|
|
357
366
|
const inverterSettings = await this.sunspecClient.readInverterSettings(this.unitId);
|
|
367
|
+
const inverterControls = await this.sunspecClient.readInverterControls(this.unitId);
|
|
358
368
|
const dcStrings = this.mapMPPTToStrings(mpptDataList);
|
|
369
|
+
const activeProductionLimitationW = this.computeActiveProductionLimitW(inverterSettings, inverterControls);
|
|
359
370
|
// SDK readers swallow modbus errors and return null/[]; detect that here so the
|
|
360
371
|
// appliance is marked offline and the retry manager starts its backoff.
|
|
361
372
|
if (await this.markOfflineIfUnhealthy()) {
|
|
@@ -396,7 +407,8 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
396
407
|
await this.applianceManager.updateAppliance(this.applianceId, {
|
|
397
408
|
inverter: {
|
|
398
409
|
...appliance?.inverter,
|
|
399
|
-
dcStrings: this.mapDcStringToApplianceMetadata(dcStrings)
|
|
410
|
+
dcStrings: this.mapDcStringToApplianceMetadata(dcStrings),
|
|
411
|
+
activeProductionLimitationW: activeProductionLimitationW
|
|
400
412
|
}
|
|
401
413
|
});
|
|
402
414
|
}
|
|
@@ -568,6 +580,25 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
568
580
|
console.log(`Inverter ${this.applianceId}: emitting faulted (${sunspec_interfaces_js_1.SUNSPEC_CONNECTION_LOST_CODE}) after ${consecutiveFailures} consecutive reconnect failures`);
|
|
569
581
|
this.dataBus.sendMessage([message]);
|
|
570
582
|
}
|
|
583
|
+
/**
|
|
584
|
+
* Compute the currently active feed-in / production limit in Watts from the
|
|
585
|
+
* Model 121 settings (WMax) and Model 123 controls (WMaxLim_Ena, WMaxLimPct).
|
|
586
|
+
* Returns null when no limit is applied, when the registers cannot be read,
|
|
587
|
+
* or when the limit equals WMax (i.e. not actually throttling) — null is
|
|
588
|
+
* returned (not undefined) so the field is cleared on the appliance.
|
|
589
|
+
*/
|
|
590
|
+
computeActiveProductionLimitW(settings, controls) {
|
|
591
|
+
if (!settings?.WMax || !controls)
|
|
592
|
+
return null;
|
|
593
|
+
if (controls.WMaxLim_Ena !== sunspec_interfaces_js_1.SunspecEnableControl.ENABLED)
|
|
594
|
+
return null;
|
|
595
|
+
if (controls.WMaxLimPct === undefined)
|
|
596
|
+
return null;
|
|
597
|
+
const limitW = (settings.WMax * controls.WMaxLimPct) / 100;
|
|
598
|
+
if (limitW >= settings.WMax)
|
|
599
|
+
return null;
|
|
600
|
+
return limitW;
|
|
601
|
+
}
|
|
571
602
|
mapOperatingState(state) {
|
|
572
603
|
if (!state)
|
|
573
604
|
return enyo_data_bus_value_js_1.EnyoInverterStateEnum.Off;
|
|
@@ -726,6 +757,8 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
726
757
|
if (batteryData?.wChaMax !== undefined) {
|
|
727
758
|
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.ChargeLimitation);
|
|
728
759
|
}
|
|
760
|
+
const activeChargeLimitW = this.computeActiveChargeLimitW(batteryData);
|
|
761
|
+
const activeDischargeLimitW = this.computeActiveDischargeLimitW(batteryData);
|
|
729
762
|
// Create or update appliance (skip if an existing appliance was provided)
|
|
730
763
|
if (!this.applianceId) {
|
|
731
764
|
try {
|
|
@@ -746,6 +779,8 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
746
779
|
storageMode: this.mapToEnyoStorageMode(storageMode),
|
|
747
780
|
maxChargingPowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.inWRte !== undefined ? batteryData.wChaMax * batteryData.inWRte : undefined,
|
|
748
781
|
maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
|
|
782
|
+
activeChargeLimitW: activeChargeLimitW,
|
|
783
|
+
activeDischargeLimitW: activeDischargeLimitW,
|
|
749
784
|
features,
|
|
750
785
|
gridChargingEnabled: batteryData?.chaGriSet === 1
|
|
751
786
|
}
|
|
@@ -772,6 +807,8 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
772
807
|
storageMode: this.mapToEnyoStorageMode(storageMode),
|
|
773
808
|
maxChargingPowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.inWRte !== undefined ? batteryData.wChaMax * batteryData.inWRte : undefined,
|
|
774
809
|
maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
|
|
810
|
+
activeChargeLimitW: activeChargeLimitW,
|
|
811
|
+
activeDischargeLimitW: activeDischargeLimitW,
|
|
775
812
|
features,
|
|
776
813
|
gridChargingEnabled: batteryData?.chaGriSet === 1
|
|
777
814
|
}
|
|
@@ -787,7 +824,12 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
787
824
|
async disconnect() {
|
|
788
825
|
this.stopDataBusListening();
|
|
789
826
|
if (this.applianceId) {
|
|
790
|
-
|
|
827
|
+
try {
|
|
828
|
+
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
console.error(`Failed to mark battery offline on disconnect: ${error}`);
|
|
832
|
+
}
|
|
791
833
|
}
|
|
792
834
|
}
|
|
793
835
|
/**
|
|
@@ -878,6 +920,8 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
878
920
|
storageMode: this.mapToEnyoStorageMode(storageMode),
|
|
879
921
|
maxChargingPowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.inWRte !== undefined ? batteryData.wChaMax * batteryData.inWRte : undefined,
|
|
880
922
|
maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
|
|
923
|
+
activeChargeLimitW: this.computeActiveChargeLimitW(batteryData),
|
|
924
|
+
activeDischargeLimitW: this.computeActiveDischargeLimitW(batteryData),
|
|
881
925
|
gridChargingEnabled: batteryData?.chaGriSet === 1
|
|
882
926
|
}
|
|
883
927
|
});
|
|
@@ -1056,6 +1100,34 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1056
1100
|
const controls = await this.getBatteryControls();
|
|
1057
1101
|
return this.determineStorageMode(controls);
|
|
1058
1102
|
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Compute the currently active charge limit in Watts from Model 124's
|
|
1105
|
+
* wChaMax and the charge rate setpoint inWRte (% of WChaMax). Returns
|
|
1106
|
+
* null when no limit is applied, when the registers cannot be read, or
|
|
1107
|
+
* when the rate is at 100% (no throttling) — null is returned (not
|
|
1108
|
+
* undefined) so the field is cleared on the appliance.
|
|
1109
|
+
*/
|
|
1110
|
+
computeActiveChargeLimitW(batteryData) {
|
|
1111
|
+
if (!batteryData || batteryData.wChaMax === undefined || batteryData.inWRte === undefined) {
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
if (batteryData.inWRte >= 100)
|
|
1115
|
+
return null;
|
|
1116
|
+
return (batteryData.wChaMax * batteryData.inWRte) / 100;
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Compute the currently active discharge limit in Watts from Model 124's
|
|
1120
|
+
* wChaMax and the discharge rate setpoint outWRte (% of WDisChaMax).
|
|
1121
|
+
* Returns null when no limit is applied or the registers cannot be read.
|
|
1122
|
+
*/
|
|
1123
|
+
computeActiveDischargeLimitW(batteryData) {
|
|
1124
|
+
if (!batteryData || batteryData.wChaMax === undefined || batteryData.outWRte === undefined) {
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
if (batteryData.outWRte >= 100)
|
|
1128
|
+
return null;
|
|
1129
|
+
return (batteryData.wChaMax * batteryData.outWRte) / 100;
|
|
1130
|
+
}
|
|
1059
1131
|
determineStorageMode(controls) {
|
|
1060
1132
|
if (!controls || controls.storCtlMod === undefined) {
|
|
1061
1133
|
return null;
|
|
@@ -1331,7 +1403,12 @@ class SunspecMeter extends BaseSunspecDevice {
|
|
|
1331
1403
|
*/
|
|
1332
1404
|
async disconnect() {
|
|
1333
1405
|
if (this.applianceId) {
|
|
1334
|
-
|
|
1406
|
+
try {
|
|
1407
|
+
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
|
|
1408
|
+
}
|
|
1409
|
+
catch (error) {
|
|
1410
|
+
console.error(`Failed to mark meter offline on disconnect: ${error}`);
|
|
1411
|
+
}
|
|
1335
1412
|
}
|
|
1336
1413
|
// Close just this meter's unit; other devices on the same network device stay open.
|
|
1337
1414
|
await this.sunspecClient.disconnectUnit(this.unitId);
|
|
@@ -129,6 +129,14 @@ export declare class SunspecInverter extends BaseSunspecDevice {
|
|
|
129
129
|
private persistErrorState;
|
|
130
130
|
private detectAndEmitStatusTransition;
|
|
131
131
|
protected onConnectionFailure(consecutiveFailures: number): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* Compute the currently active feed-in / production limit in Watts from the
|
|
134
|
+
* Model 121 settings (WMax) and Model 123 controls (WMaxLim_Ena, WMaxLimPct).
|
|
135
|
+
* Returns null when no limit is applied, when the registers cannot be read,
|
|
136
|
+
* or when the limit equals WMax (i.e. not actually throttling) — null is
|
|
137
|
+
* returned (not undefined) so the field is cleared on the appliance.
|
|
138
|
+
*/
|
|
139
|
+
private computeActiveProductionLimitW;
|
|
132
140
|
private mapOperatingState;
|
|
133
141
|
/**
|
|
134
142
|
* Map MPPT data to DC string structure for data bus
|
|
@@ -245,6 +253,20 @@ export declare class SunspecBattery extends BaseSunspecDevice {
|
|
|
245
253
|
* @returns Promise<SunspecStorageMode | null> - Current mode or null if error
|
|
246
254
|
*/
|
|
247
255
|
getStorageMode(): Promise<SunspecStorageMode | null>;
|
|
256
|
+
/**
|
|
257
|
+
* Compute the currently active charge limit in Watts from Model 124's
|
|
258
|
+
* wChaMax and the charge rate setpoint inWRte (% of WChaMax). Returns
|
|
259
|
+
* null when no limit is applied, when the registers cannot be read, or
|
|
260
|
+
* when the rate is at 100% (no throttling) — null is returned (not
|
|
261
|
+
* undefined) so the field is cleared on the appliance.
|
|
262
|
+
*/
|
|
263
|
+
private computeActiveChargeLimitW;
|
|
264
|
+
/**
|
|
265
|
+
* Compute the currently active discharge limit in Watts from Model 124's
|
|
266
|
+
* wChaMax and the discharge rate setpoint outWRte (% of WDisChaMax).
|
|
267
|
+
* Returns null when no limit is applied or the registers cannot be read.
|
|
268
|
+
*/
|
|
269
|
+
private computeActiveDischargeLimitW;
|
|
248
270
|
private determineStorageMode;
|
|
249
271
|
/**
|
|
250
272
|
* Check if grid charging is enabled
|
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.69';
|
|
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
|
@@ -129,6 +129,14 @@ export declare class SunspecInverter extends BaseSunspecDevice {
|
|
|
129
129
|
private persistErrorState;
|
|
130
130
|
private detectAndEmitStatusTransition;
|
|
131
131
|
protected onConnectionFailure(consecutiveFailures: number): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* Compute the currently active feed-in / production limit in Watts from the
|
|
134
|
+
* Model 121 settings (WMax) and Model 123 controls (WMaxLim_Ena, WMaxLimPct).
|
|
135
|
+
* Returns null when no limit is applied, when the registers cannot be read,
|
|
136
|
+
* or when the limit equals WMax (i.e. not actually throttling) — null is
|
|
137
|
+
* returned (not undefined) so the field is cleared on the appliance.
|
|
138
|
+
*/
|
|
139
|
+
private computeActiveProductionLimitW;
|
|
132
140
|
private mapOperatingState;
|
|
133
141
|
/**
|
|
134
142
|
* Map MPPT data to DC string structure for data bus
|
|
@@ -245,6 +253,20 @@ export declare class SunspecBattery extends BaseSunspecDevice {
|
|
|
245
253
|
* @returns Promise<SunspecStorageMode | null> - Current mode or null if error
|
|
246
254
|
*/
|
|
247
255
|
getStorageMode(): Promise<SunspecStorageMode | null>;
|
|
256
|
+
/**
|
|
257
|
+
* Compute the currently active charge limit in Watts from Model 124's
|
|
258
|
+
* wChaMax and the charge rate setpoint inWRte (% of WChaMax). Returns
|
|
259
|
+
* null when no limit is applied, when the registers cannot be read, or
|
|
260
|
+
* when the rate is at 100% (no throttling) — null is returned (not
|
|
261
|
+
* undefined) so the field is cleared on the appliance.
|
|
262
|
+
*/
|
|
263
|
+
private computeActiveChargeLimitW;
|
|
264
|
+
/**
|
|
265
|
+
* Compute the currently active discharge limit in Watts from Model 124's
|
|
266
|
+
* wChaMax and the discharge rate setpoint outWRte (% of WDisChaMax).
|
|
267
|
+
* Returns null when no limit is applied or the registers cannot be read.
|
|
268
|
+
*/
|
|
269
|
+
private computeActiveDischargeLimitW;
|
|
248
270
|
private determineStorageMode;
|
|
249
271
|
/**
|
|
250
272
|
* Check if grid charging is enabled
|
package/dist/sunspec-devices.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SunspecBatteryChargeState, SunspecInverterCapability, SunspecInverterEvent1, SunspecModelId, SunspecMPPTOperatingState, SunspecStorageMode, SUNSPEC_CONNECTION_LOST_CODE } from "./sunspec-interfaces.js";
|
|
1
|
+
import { SunspecBatteryChargeState, SunspecEnableControl, SunspecInverterCapability, SunspecInverterEvent1, SunspecModelId, SunspecMPPTOperatingState, SunspecStorageMode, SUNSPEC_CONNECTION_LOST_CODE } from "./sunspec-interfaces.js";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { EnyoApplianceConnectionType, EnyoApplianceStateEnum, EnyoApplianceStatusEnum, EnyoApplianceTopologyFeatureEnum, EnyoApplianceTypeEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
|
|
4
4
|
import { ConnectionRetryManager } from "./connection-retry-manager.js";
|
|
@@ -274,7 +274,9 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
274
274
|
// Get device info from common block
|
|
275
275
|
const commonData = await this.sunspecClient.readCommonBlock(this.unitId);
|
|
276
276
|
const inverterSettings = await this.sunspecClient.readInverterSettings(this.unitId);
|
|
277
|
+
const inverterControls = await this.sunspecClient.readInverterControls(this.unitId);
|
|
277
278
|
const mpptDataList = await this.sunspecClient.readAllMPPTData(this.unitId);
|
|
279
|
+
const activeProductionLimitationW = this.computeActiveProductionLimitW(inverterSettings, inverterControls);
|
|
278
280
|
// Create or update appliance (skip if an existing appliance was provided)
|
|
279
281
|
if (!this.applianceId) {
|
|
280
282
|
try {
|
|
@@ -292,7 +294,8 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
292
294
|
},
|
|
293
295
|
inverter: {
|
|
294
296
|
dcStrings: this.mapDcStringToApplianceMetadata(this.mapMPPTToStrings(mpptDataList)),
|
|
295
|
-
maxPvProductionW: inverterSettings?.WMax
|
|
297
|
+
maxPvProductionW: inverterSettings?.WMax,
|
|
298
|
+
activeProductionLimitationW: activeProductionLimitationW
|
|
296
299
|
}
|
|
297
300
|
});
|
|
298
301
|
console.log(`Sunspec Inverter connected: ${this.networkDevice.hostname} (${this.applianceId})`);
|
|
@@ -314,7 +317,8 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
314
317
|
inverter: {
|
|
315
318
|
...existingAppliance?.inverter,
|
|
316
319
|
dcStrings: this.mapDcStringToApplianceMetadata(this.mapMPPTToStrings(mpptDataList)),
|
|
317
|
-
maxPvProductionW: inverterSettings?.WMax
|
|
320
|
+
maxPvProductionW: inverterSettings?.WMax,
|
|
321
|
+
activeProductionLimitationW: activeProductionLimitationW
|
|
318
322
|
}
|
|
319
323
|
});
|
|
320
324
|
console.log(`Sunspec Inverter connected: ${this.networkDevice.hostname} (${this.applianceId})`);
|
|
@@ -334,7 +338,12 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
334
338
|
async disconnect() {
|
|
335
339
|
this.stopDataBusListening();
|
|
336
340
|
if (this.applianceId) {
|
|
337
|
-
|
|
341
|
+
try {
|
|
342
|
+
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
console.error(`Failed to mark inverter offline on disconnect: ${error}`);
|
|
346
|
+
}
|
|
338
347
|
}
|
|
339
348
|
// Note: We don't disconnect the sunspecClient as it may be shared
|
|
340
349
|
}
|
|
@@ -351,7 +360,9 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
351
360
|
const inverterData = await this.sunspecClient.readInverterData(this.unitId);
|
|
352
361
|
const mpptDataList = await this.sunspecClient.readAllMPPTData(this.unitId);
|
|
353
362
|
const inverterSettings = await this.sunspecClient.readInverterSettings(this.unitId);
|
|
363
|
+
const inverterControls = await this.sunspecClient.readInverterControls(this.unitId);
|
|
354
364
|
const dcStrings = this.mapMPPTToStrings(mpptDataList);
|
|
365
|
+
const activeProductionLimitationW = this.computeActiveProductionLimitW(inverterSettings, inverterControls);
|
|
355
366
|
// SDK readers swallow modbus errors and return null/[]; detect that here so the
|
|
356
367
|
// appliance is marked offline and the retry manager starts its backoff.
|
|
357
368
|
if (await this.markOfflineIfUnhealthy()) {
|
|
@@ -392,7 +403,8 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
392
403
|
await this.applianceManager.updateAppliance(this.applianceId, {
|
|
393
404
|
inverter: {
|
|
394
405
|
...appliance?.inverter,
|
|
395
|
-
dcStrings: this.mapDcStringToApplianceMetadata(dcStrings)
|
|
406
|
+
dcStrings: this.mapDcStringToApplianceMetadata(dcStrings),
|
|
407
|
+
activeProductionLimitationW: activeProductionLimitationW
|
|
396
408
|
}
|
|
397
409
|
});
|
|
398
410
|
}
|
|
@@ -564,6 +576,25 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
564
576
|
console.log(`Inverter ${this.applianceId}: emitting faulted (${SUNSPEC_CONNECTION_LOST_CODE}) after ${consecutiveFailures} consecutive reconnect failures`);
|
|
565
577
|
this.dataBus.sendMessage([message]);
|
|
566
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* Compute the currently active feed-in / production limit in Watts from the
|
|
581
|
+
* Model 121 settings (WMax) and Model 123 controls (WMaxLim_Ena, WMaxLimPct).
|
|
582
|
+
* Returns null when no limit is applied, when the registers cannot be read,
|
|
583
|
+
* or when the limit equals WMax (i.e. not actually throttling) — null is
|
|
584
|
+
* returned (not undefined) so the field is cleared on the appliance.
|
|
585
|
+
*/
|
|
586
|
+
computeActiveProductionLimitW(settings, controls) {
|
|
587
|
+
if (!settings?.WMax || !controls)
|
|
588
|
+
return null;
|
|
589
|
+
if (controls.WMaxLim_Ena !== SunspecEnableControl.ENABLED)
|
|
590
|
+
return null;
|
|
591
|
+
if (controls.WMaxLimPct === undefined)
|
|
592
|
+
return null;
|
|
593
|
+
const limitW = (settings.WMax * controls.WMaxLimPct) / 100;
|
|
594
|
+
if (limitW >= settings.WMax)
|
|
595
|
+
return null;
|
|
596
|
+
return limitW;
|
|
597
|
+
}
|
|
567
598
|
mapOperatingState(state) {
|
|
568
599
|
if (!state)
|
|
569
600
|
return EnyoInverterStateEnum.Off;
|
|
@@ -721,6 +752,8 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
721
752
|
if (batteryData?.wChaMax !== undefined) {
|
|
722
753
|
features.push(EnyoBatteryFeature.ChargeLimitation);
|
|
723
754
|
}
|
|
755
|
+
const activeChargeLimitW = this.computeActiveChargeLimitW(batteryData);
|
|
756
|
+
const activeDischargeLimitW = this.computeActiveDischargeLimitW(batteryData);
|
|
724
757
|
// Create or update appliance (skip if an existing appliance was provided)
|
|
725
758
|
if (!this.applianceId) {
|
|
726
759
|
try {
|
|
@@ -741,6 +774,8 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
741
774
|
storageMode: this.mapToEnyoStorageMode(storageMode),
|
|
742
775
|
maxChargingPowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.inWRte !== undefined ? batteryData.wChaMax * batteryData.inWRte : undefined,
|
|
743
776
|
maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
|
|
777
|
+
activeChargeLimitW: activeChargeLimitW,
|
|
778
|
+
activeDischargeLimitW: activeDischargeLimitW,
|
|
744
779
|
features,
|
|
745
780
|
gridChargingEnabled: batteryData?.chaGriSet === 1
|
|
746
781
|
}
|
|
@@ -767,6 +802,8 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
767
802
|
storageMode: this.mapToEnyoStorageMode(storageMode),
|
|
768
803
|
maxChargingPowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.inWRte !== undefined ? batteryData.wChaMax * batteryData.inWRte : undefined,
|
|
769
804
|
maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
|
|
805
|
+
activeChargeLimitW: activeChargeLimitW,
|
|
806
|
+
activeDischargeLimitW: activeDischargeLimitW,
|
|
770
807
|
features,
|
|
771
808
|
gridChargingEnabled: batteryData?.chaGriSet === 1
|
|
772
809
|
}
|
|
@@ -782,7 +819,12 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
782
819
|
async disconnect() {
|
|
783
820
|
this.stopDataBusListening();
|
|
784
821
|
if (this.applianceId) {
|
|
785
|
-
|
|
822
|
+
try {
|
|
823
|
+
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
|
|
824
|
+
}
|
|
825
|
+
catch (error) {
|
|
826
|
+
console.error(`Failed to mark battery offline on disconnect: ${error}`);
|
|
827
|
+
}
|
|
786
828
|
}
|
|
787
829
|
}
|
|
788
830
|
/**
|
|
@@ -873,6 +915,8 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
873
915
|
storageMode: this.mapToEnyoStorageMode(storageMode),
|
|
874
916
|
maxChargingPowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.inWRte !== undefined ? batteryData.wChaMax * batteryData.inWRte : undefined,
|
|
875
917
|
maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
|
|
918
|
+
activeChargeLimitW: this.computeActiveChargeLimitW(batteryData),
|
|
919
|
+
activeDischargeLimitW: this.computeActiveDischargeLimitW(batteryData),
|
|
876
920
|
gridChargingEnabled: batteryData?.chaGriSet === 1
|
|
877
921
|
}
|
|
878
922
|
});
|
|
@@ -1051,6 +1095,34 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
1051
1095
|
const controls = await this.getBatteryControls();
|
|
1052
1096
|
return this.determineStorageMode(controls);
|
|
1053
1097
|
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Compute the currently active charge limit in Watts from Model 124's
|
|
1100
|
+
* wChaMax and the charge rate setpoint inWRte (% of WChaMax). Returns
|
|
1101
|
+
* null when no limit is applied, when the registers cannot be read, or
|
|
1102
|
+
* when the rate is at 100% (no throttling) — null is returned (not
|
|
1103
|
+
* undefined) so the field is cleared on the appliance.
|
|
1104
|
+
*/
|
|
1105
|
+
computeActiveChargeLimitW(batteryData) {
|
|
1106
|
+
if (!batteryData || batteryData.wChaMax === undefined || batteryData.inWRte === undefined) {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
if (batteryData.inWRte >= 100)
|
|
1110
|
+
return null;
|
|
1111
|
+
return (batteryData.wChaMax * batteryData.inWRte) / 100;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Compute the currently active discharge limit in Watts from Model 124's
|
|
1115
|
+
* wChaMax and the discharge rate setpoint outWRte (% of WDisChaMax).
|
|
1116
|
+
* Returns null when no limit is applied or the registers cannot be read.
|
|
1117
|
+
*/
|
|
1118
|
+
computeActiveDischargeLimitW(batteryData) {
|
|
1119
|
+
if (!batteryData || batteryData.wChaMax === undefined || batteryData.outWRte === undefined) {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
if (batteryData.outWRte >= 100)
|
|
1123
|
+
return null;
|
|
1124
|
+
return (batteryData.wChaMax * batteryData.outWRte) / 100;
|
|
1125
|
+
}
|
|
1054
1126
|
determineStorageMode(controls) {
|
|
1055
1127
|
if (!controls || controls.storCtlMod === undefined) {
|
|
1056
1128
|
return null;
|
|
@@ -1325,7 +1397,12 @@ export class SunspecMeter extends BaseSunspecDevice {
|
|
|
1325
1397
|
*/
|
|
1326
1398
|
async disconnect() {
|
|
1327
1399
|
if (this.applianceId) {
|
|
1328
|
-
|
|
1400
|
+
try {
|
|
1401
|
+
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
|
|
1402
|
+
}
|
|
1403
|
+
catch (error) {
|
|
1404
|
+
console.error(`Failed to mark meter offline on disconnect: ${error}`);
|
|
1405
|
+
}
|
|
1329
1406
|
}
|
|
1330
1407
|
// Close just this meter's unit; other devices on the same network device stay open.
|
|
1331
1408
|
await this.sunspecClient.disconnectUnit(this.unitId);
|
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.69",
|
|
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.135"
|
|
41
41
|
},
|
|
42
42
|
"volta": {
|
|
43
43
|
"node": "22.17.0"
|