@enyo-energy/sunspec-sdk 0.0.81 → 0.0.83
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 +91 -8
- package/dist/cjs/sunspec-devices.d.cts +24 -0
- package/dist/cjs/sunspec-interfaces.cjs +17 -1
- package/dist/cjs/sunspec-interfaces.d.cts +15 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/sunspec-devices.d.ts +24 -0
- package/dist/sunspec-devices.js +92 -9
- package/dist/sunspec-interfaces.d.ts +15 -0
- package/dist/sunspec-interfaces.js +16 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -107,6 +107,15 @@ class BaseSunspecDevice {
|
|
|
107
107
|
dataBus;
|
|
108
108
|
retryManager;
|
|
109
109
|
consecutiveReconnectFailures = 0;
|
|
110
|
+
/**
|
|
111
|
+
* True once {@link markOffline} has set the appliance state to Offline and
|
|
112
|
+
* it has not yet been re-asserted Connected. Used by
|
|
113
|
+
* {@link reassertConnectedIfRecovered} to heal the shared-socket case: when
|
|
114
|
+
* a sibling device on the same unit wins the reconnect race, this device's
|
|
115
|
+
* readData() sees isConnected() === true and skips tryReconnect(), so it
|
|
116
|
+
* would otherwise stay Offline forever despite reading data fine.
|
|
117
|
+
*/
|
|
118
|
+
appliedOffline = false;
|
|
110
119
|
/**
|
|
111
120
|
* Prefix used when persisting calibration snapshots via the library's
|
|
112
121
|
* {@link SnapshotService}. Kept identical to the key the SDK used before
|
|
@@ -181,6 +190,7 @@ class BaseSunspecDevice {
|
|
|
181
190
|
if (this.applianceId) {
|
|
182
191
|
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Connected);
|
|
183
192
|
}
|
|
193
|
+
this.appliedOffline = false;
|
|
184
194
|
await this.onConnectionRestored();
|
|
185
195
|
const postStats = this.sunspecClient.getConnectionStats();
|
|
186
196
|
console.log(`${this.constructor.name} ${this.applianceId}: Reconnection successful after ${attempt} attempt(s) (opens=${postStats.opens}, closes=${postStats.closes}, openUnits=${postStats.openUnits})`);
|
|
@@ -249,12 +259,46 @@ class BaseSunspecDevice {
|
|
|
249
259
|
if (this.applianceId) {
|
|
250
260
|
try {
|
|
251
261
|
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
|
|
262
|
+
this.appliedOffline = true;
|
|
252
263
|
}
|
|
253
264
|
catch (error) {
|
|
254
265
|
console.error(`${this.constructor.name} ${this.applianceId}: Failed to mark appliance offline: ${error}`);
|
|
255
266
|
}
|
|
256
267
|
}
|
|
257
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Heal the shared-socket recovery gap. When several devices share one
|
|
271
|
+
* SunspecModbusClient on the same unit (e.g. a Fronius GEN24 inverter and
|
|
272
|
+
* its battery), only the device that detects !isConnected() first runs
|
|
273
|
+
* tryReconnect() — and that is the only path that flips the appliance back
|
|
274
|
+
* to Connected. Siblings then see isConnected() === true on their next
|
|
275
|
+
* readData() and skip tryReconnect() entirely, so without this they would
|
|
276
|
+
* stay Offline forever despite reading data fine.
|
|
277
|
+
*
|
|
278
|
+
* Call this on the success path of readData() (after the
|
|
279
|
+
* markOfflineIfUnhealthy() guard has confirmed the unit is healthy). It is
|
|
280
|
+
* a cheap no-op unless this device was previously marked offline.
|
|
281
|
+
*/
|
|
282
|
+
async reassertConnectedIfRecovered() {
|
|
283
|
+
if (!this.appliedOffline || !this.applianceId) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (!this.sunspecClient.isHealthy(this.unitId)) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
console.log(`${this.constructor.name} ${this.applianceId}: connection recovered via shared socket — re-asserting Connected`);
|
|
290
|
+
try {
|
|
291
|
+
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Connected);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
console.error(`${this.constructor.name} ${this.applianceId}: Failed to re-assert Connected: ${error}`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
this.appliedOffline = false;
|
|
298
|
+
this.retryManager.reset();
|
|
299
|
+
this.consecutiveReconnectFailures = 0;
|
|
300
|
+
await this.onConnectionRestored();
|
|
301
|
+
}
|
|
258
302
|
sendCommandAcknowledge(messageId, acknowledgeMessage, answer, rejectionReason) {
|
|
259
303
|
if (!this.dataBus || !this.applianceId) {
|
|
260
304
|
return;
|
|
@@ -302,6 +346,12 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
302
346
|
storage;
|
|
303
347
|
errorState = { activeCodes: [], lastStatus: 'healthy' };
|
|
304
348
|
snapshotService;
|
|
349
|
+
// Whether we've emitted a status update at least once since (re)connecting.
|
|
350
|
+
// The persisted errorState can diverge from what the core actually shows
|
|
351
|
+
// (e.g. a faulted that was never cleared while our local state reads
|
|
352
|
+
// healthy), so we force one re-assert of the current status on the first
|
|
353
|
+
// successful read of each session to converge the core.
|
|
354
|
+
statusReassertedThisSession = false;
|
|
305
355
|
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig, appliance, useTls) {
|
|
306
356
|
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig, appliance, useTls);
|
|
307
357
|
this.capabilities = capabilities;
|
|
@@ -350,9 +400,13 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
350
400
|
else {
|
|
351
401
|
try {
|
|
352
402
|
const existingAppliance = await this.applianceManager.findApplianceById(this.applianceId);
|
|
403
|
+
// Drop any previously core-stored `status` so we don't echo a
|
|
404
|
+
// stale `faulted` back through ApplianceSave — status is driven
|
|
405
|
+
// solely by ApplianceStateUpdateV1 (detectAndEmitStatusTransition).
|
|
406
|
+
const { status: _staleStatus, ...prevMetadata } = existingAppliance?.metadata ?? {};
|
|
353
407
|
await this.applianceManager.updateAppliance(this.applianceId, {
|
|
354
408
|
metadata: {
|
|
355
|
-
...
|
|
409
|
+
...prevMetadata,
|
|
356
410
|
connectionType: enyo_appliance_js_1.EnyoApplianceConnectionType.Connector,
|
|
357
411
|
state: enyo_appliance_js_1.EnyoApplianceStateEnum.Connected,
|
|
358
412
|
modbus: { unitId: this.unitId, baseAddress: this.baseAddress },
|
|
@@ -376,6 +430,9 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
376
430
|
console.log(`MPPT model found for inverter ${this.networkDevice.hostname}`);
|
|
377
431
|
}
|
|
378
432
|
await this.loadErrorState();
|
|
433
|
+
// Fresh session: force the next successful read to re-assert the true
|
|
434
|
+
// status so a stale faulted in the core is cleared on (re)start.
|
|
435
|
+
this.statusReassertedThisSession = false;
|
|
379
436
|
this.startDataBusListening();
|
|
380
437
|
// Cold-start recovery: if the persisted state says we were stuck in
|
|
381
438
|
// connection_lost but connect() just succeeded (we read commonBlock,
|
|
@@ -420,6 +477,10 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
420
477
|
if (await this.markOfflineIfUnhealthy()) {
|
|
421
478
|
return messages;
|
|
422
479
|
}
|
|
480
|
+
// Healthy read: if a sibling on the same shared socket reconnected us
|
|
481
|
+
// (so our own tryReconnect() never ran), flip our appliance state back
|
|
482
|
+
// to Connected. No-op unless this device is still marked offline.
|
|
483
|
+
await this.reassertConnectedIfRecovered();
|
|
423
484
|
if (inverterData) {
|
|
424
485
|
const pvPowerW = dcStrings.reduce((sum, s) => sum + (s.powerW || 0), 0);
|
|
425
486
|
console.debug(`Got PV Power from DC strings: ${pvPowerW}W`);
|
|
@@ -590,16 +651,30 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
590
651
|
if (!this.applianceId || !this.dataBus)
|
|
591
652
|
return;
|
|
592
653
|
const { codes, codeIds } = this.decodeActiveErrors(data);
|
|
593
|
-
|
|
594
|
-
|
|
654
|
+
// Status is driven by BOTH the decoded event-register error bits AND the
|
|
655
|
+
// SunSpec operating state. Only operatingState 7 (FAULT) is a genuine
|
|
656
|
+
// fault; MPPT (4), SLEEPING (2) and the other states are normal. This
|
|
657
|
+
// is re-evaluated every poll so a producing inverter (operatingState=4,
|
|
658
|
+
// no error bits) reports Healthy even if it was previously faulted.
|
|
659
|
+
const operatingFault = data.operatingState === sunspec_interfaces_js_1.SunspecInverterOperatingState.FAULT;
|
|
660
|
+
const newStatus = (codeIds.length > 0 || operatingFault)
|
|
661
|
+
? enyo_appliance_js_1.EnyoApplianceStatusEnum.Faulted
|
|
662
|
+
: enyo_appliance_js_1.EnyoApplianceStatusEnum.Healthy;
|
|
663
|
+
const newLastStatus = newStatus === enyo_appliance_js_1.EnyoApplianceStatusEnum.Healthy ? 'healthy' : 'faulted';
|
|
664
|
+
// Emit when the effective status changed (covers faulted/connection_lost
|
|
665
|
+
// -> healthy convergence), when the active error-code set changed, or
|
|
666
|
+
// once per session to re-assert the true status to the core (clears a
|
|
667
|
+
// stale faulted that diverged from our local state). Otherwise stay
|
|
668
|
+
// quiet so we don't republish the same status every 10s poll.
|
|
669
|
+
const statusChanged = newLastStatus !== this.errorState.lastStatus;
|
|
670
|
+
const mustReassert = !this.statusReassertedThisSession;
|
|
671
|
+
if (!statusChanged && !this.hasErrorSetChanged(codeIds) && !mustReassert) {
|
|
595
672
|
return;
|
|
596
673
|
}
|
|
597
|
-
const newStatus = codeIds.length === 0
|
|
598
|
-
? enyo_appliance_js_1.EnyoApplianceStatusEnum.Healthy
|
|
599
|
-
: enyo_appliance_js_1.EnyoApplianceStatusEnum.Faulted;
|
|
600
674
|
const message = this.buildStatusMessage(newStatus, codes, timestamp);
|
|
601
|
-
console.log(`Inverter ${this.applianceId}: status transition -> ${newStatus} (codes=[${codeIds.join(', ')}])`);
|
|
675
|
+
console.log(`Inverter ${this.applianceId}: status transition -> ${newStatus} (codes=[${codeIds.join(', ')}], operatingState=${data.operatingState})`);
|
|
602
676
|
this.dataBus.sendMessage([message]);
|
|
677
|
+
this.statusReassertedThisSession = true;
|
|
603
678
|
this.errorState = {
|
|
604
679
|
evt1: data.events,
|
|
605
680
|
evt2: data.events2,
|
|
@@ -608,7 +683,7 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
608
683
|
evtVnd3: data.vendorEvents3,
|
|
609
684
|
evtVnd4: data.vendorEvents4,
|
|
610
685
|
activeCodes: codeIds,
|
|
611
|
-
lastStatus:
|
|
686
|
+
lastStatus: newLastStatus,
|
|
612
687
|
};
|
|
613
688
|
await this.persistErrorState();
|
|
614
689
|
}
|
|
@@ -1183,6 +1258,10 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1183
1258
|
if (await this.markOfflineIfUnhealthy()) {
|
|
1184
1259
|
return messages;
|
|
1185
1260
|
}
|
|
1261
|
+
// Healthy read: if a sibling on the same shared socket reconnected us
|
|
1262
|
+
// (so our own tryReconnect() never ran), flip our appliance state back
|
|
1263
|
+
// to Connected. No-op unless this device is still marked offline.
|
|
1264
|
+
await this.reassertConnectedIfRecovered();
|
|
1186
1265
|
if (batteryData) {
|
|
1187
1266
|
// See `selectLiveBatteryPowerW` for the source-preference rationale.
|
|
1188
1267
|
const batteryPowerW = selectLiveBatteryPowerW(mpptBatteryPowerW, batteryData);
|
|
@@ -1696,6 +1775,10 @@ class SunspecMeter extends BaseSunspecDevice {
|
|
|
1696
1775
|
if (await this.markOfflineIfUnhealthy()) {
|
|
1697
1776
|
return messages;
|
|
1698
1777
|
}
|
|
1778
|
+
// Healthy read: if a sibling on the same shared socket reconnected us
|
|
1779
|
+
// (so our own tryReconnect() never ran), flip our appliance state back
|
|
1780
|
+
// to Connected. No-op unless this device is still marked offline.
|
|
1781
|
+
await this.reassertConnectedIfRecovered();
|
|
1699
1782
|
if (meterData) {
|
|
1700
1783
|
const meterMessage = {
|
|
1701
1784
|
id: (0, node_crypto_1.randomUUID)(),
|
|
@@ -27,6 +27,15 @@ export declare abstract class BaseSunspecDevice {
|
|
|
27
27
|
protected dataBus?: EnergyAppDataBus;
|
|
28
28
|
protected retryManager: ConnectionRetryManager;
|
|
29
29
|
protected consecutiveReconnectFailures: number;
|
|
30
|
+
/**
|
|
31
|
+
* True once {@link markOffline} has set the appliance state to Offline and
|
|
32
|
+
* it has not yet been re-asserted Connected. Used by
|
|
33
|
+
* {@link reassertConnectedIfRecovered} to heal the shared-socket case: when
|
|
34
|
+
* a sibling device on the same unit wins the reconnect race, this device's
|
|
35
|
+
* readData() sees isConnected() === true and skips tryReconnect(), so it
|
|
36
|
+
* would otherwise stay Offline forever despite reading data fine.
|
|
37
|
+
*/
|
|
38
|
+
private appliedOffline;
|
|
30
39
|
/**
|
|
31
40
|
* Prefix used when persisting calibration snapshots via the library's
|
|
32
41
|
* {@link SnapshotService}. Kept identical to the key the SDK used before
|
|
@@ -90,6 +99,20 @@ export declare abstract class BaseSunspecDevice {
|
|
|
90
99
|
* connection after the backoff interval. Then update appliance state.
|
|
91
100
|
*/
|
|
92
101
|
protected markOffline(): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Heal the shared-socket recovery gap. When several devices share one
|
|
104
|
+
* SunspecModbusClient on the same unit (e.g. a Fronius GEN24 inverter and
|
|
105
|
+
* its battery), only the device that detects !isConnected() first runs
|
|
106
|
+
* tryReconnect() — and that is the only path that flips the appliance back
|
|
107
|
+
* to Connected. Siblings then see isConnected() === true on their next
|
|
108
|
+
* readData() and skip tryReconnect() entirely, so without this they would
|
|
109
|
+
* stay Offline forever despite reading data fine.
|
|
110
|
+
*
|
|
111
|
+
* Call this on the success path of readData() (after the
|
|
112
|
+
* markOfflineIfUnhealthy() guard has confirmed the unit is healthy). It is
|
|
113
|
+
* a cheap no-op unless this device was previously marked offline.
|
|
114
|
+
*/
|
|
115
|
+
protected reassertConnectedIfRecovered(): Promise<void>;
|
|
93
116
|
protected sendCommandAcknowledge(messageId: string, acknowledgeMessage: EnyoDataBusMessageEnum | string, answer: EnyoCommandAcknowledgeAnswerEnum, rejectionReason?: string): void;
|
|
94
117
|
/**
|
|
95
118
|
* Build a typed {@link SnapshotService} bound to this appliance's calibration storage,
|
|
@@ -108,6 +131,7 @@ export declare class SunspecInverter extends BaseSunspecDevice {
|
|
|
108
131
|
private storage?;
|
|
109
132
|
private errorState;
|
|
110
133
|
private snapshotService?;
|
|
134
|
+
private statusReassertedThisSession;
|
|
111
135
|
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, capabilities?: SunspecInverterCapability[], retryConfig?: IRetryConfig, appliance?: EnyoAppliance, useTls?: boolean);
|
|
112
136
|
connect(): Promise<void>;
|
|
113
137
|
disconnect(): Promise<void>;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* SunSpec block interfaces with block numbers
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.SUNSPEC_CONTROLLABLE_FEATURES = exports.SunspecMeterCapability = exports.SunspecBatteryFeatureModeKind = exports.SunspecBatteryCapability = exports.SunspecInverterCapability = exports.SunspecStorageMode = exports.SunspecChargeSource = exports.SunspecVArPctMode = exports.SunspecEnableControl = exports.SunspecConnectionControl = exports.SunspecStorageControlMode = exports.SunspecBatteryEvent1 = exports.SunspecBatteryBankState = exports.SunspecBatteryType = exports.SunspecBatteryControlMode = exports.SunspecBatteryChargeState = exports.SunspecMPPTOperatingState = exports.SUNSPEC_CONNECTION_LOST_CODE = exports.SunspecInverterEvent1 = exports.SunspecModelId = exports.DEFAULT_RETRY_CONFIG = void 0;
|
|
6
|
+
exports.SUNSPEC_CONTROLLABLE_FEATURES = exports.SunspecMeterCapability = exports.SunspecBatteryFeatureModeKind = exports.SunspecBatteryCapability = exports.SunspecInverterCapability = exports.SunspecStorageMode = exports.SunspecChargeSource = exports.SunspecVArPctMode = exports.SunspecEnableControl = exports.SunspecConnectionControl = exports.SunspecStorageControlMode = exports.SunspecBatteryEvent1 = exports.SunspecBatteryBankState = exports.SunspecBatteryType = exports.SunspecBatteryControlMode = exports.SunspecBatteryChargeState = exports.SunspecInverterOperatingState = exports.SunspecMPPTOperatingState = exports.SUNSPEC_CONNECTION_LOST_CODE = exports.SunspecInverterEvent1 = exports.SunspecModelId = exports.DEFAULT_RETRY_CONFIG = void 0;
|
|
7
7
|
const enyo_battery_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js");
|
|
8
8
|
exports.DEFAULT_RETRY_CONFIG = {
|
|
9
9
|
phases: [
|
|
@@ -87,6 +87,22 @@ var SunspecMPPTOperatingState;
|
|
|
87
87
|
SunspecMPPTOperatingState[SunspecMPPTOperatingState["TEST"] = 9] = "TEST";
|
|
88
88
|
SunspecMPPTOperatingState[SunspecMPPTOperatingState["RESERVED_10"] = 10] = "RESERVED_10";
|
|
89
89
|
})(SunspecMPPTOperatingState || (exports.SunspecMPPTOperatingState = SunspecMPPTOperatingState = {}));
|
|
90
|
+
/**
|
|
91
|
+
* Inverter Operating State values (SunSpec `St`, register offset 38) for
|
|
92
|
+
* models 101/103/111/112/113. Mirrors the SunSpec spec. Only `FAULT` is a
|
|
93
|
+
* genuine fault; `SLEEPING` and `MPPT` are normal production/standby states.
|
|
94
|
+
*/
|
|
95
|
+
var SunspecInverterOperatingState;
|
|
96
|
+
(function (SunspecInverterOperatingState) {
|
|
97
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["OFF"] = 1] = "OFF";
|
|
98
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["SLEEPING"] = 2] = "SLEEPING";
|
|
99
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["STARTING"] = 3] = "STARTING";
|
|
100
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["MPPT"] = 4] = "MPPT";
|
|
101
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["THROTTLED"] = 5] = "THROTTLED";
|
|
102
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["SHUTTING_DOWN"] = 6] = "SHUTTING_DOWN";
|
|
103
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["FAULT"] = 7] = "FAULT";
|
|
104
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["STANDBY"] = 8] = "STANDBY";
|
|
105
|
+
})(SunspecInverterOperatingState || (exports.SunspecInverterOperatingState = SunspecInverterOperatingState = {}));
|
|
90
106
|
/**
|
|
91
107
|
* Battery Charge State values for Model 124
|
|
92
108
|
*/
|
|
@@ -222,6 +222,21 @@ export declare enum SunspecMPPTOperatingState {
|
|
|
222
222
|
TEST = 9,
|
|
223
223
|
RESERVED_10 = 10
|
|
224
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Inverter Operating State values (SunSpec `St`, register offset 38) for
|
|
227
|
+
* models 101/103/111/112/113. Mirrors the SunSpec spec. Only `FAULT` is a
|
|
228
|
+
* genuine fault; `SLEEPING` and `MPPT` are normal production/standby states.
|
|
229
|
+
*/
|
|
230
|
+
export declare enum SunspecInverterOperatingState {
|
|
231
|
+
OFF = 1,
|
|
232
|
+
SLEEPING = 2,
|
|
233
|
+
STARTING = 3,
|
|
234
|
+
MPPT = 4,// Maximum Power Point Tracking active (producing)
|
|
235
|
+
THROTTLED = 5,// Power output is being limited
|
|
236
|
+
SHUTTING_DOWN = 6,
|
|
237
|
+
FAULT = 7,
|
|
238
|
+
STANDBY = 8
|
|
239
|
+
}
|
|
225
240
|
/**
|
|
226
241
|
* Battery Charge State values for Model 124
|
|
227
242
|
*/
|
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.83';
|
|
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
|
@@ -27,6 +27,15 @@ export declare abstract class BaseSunspecDevice {
|
|
|
27
27
|
protected dataBus?: EnergyAppDataBus;
|
|
28
28
|
protected retryManager: ConnectionRetryManager;
|
|
29
29
|
protected consecutiveReconnectFailures: number;
|
|
30
|
+
/**
|
|
31
|
+
* True once {@link markOffline} has set the appliance state to Offline and
|
|
32
|
+
* it has not yet been re-asserted Connected. Used by
|
|
33
|
+
* {@link reassertConnectedIfRecovered} to heal the shared-socket case: when
|
|
34
|
+
* a sibling device on the same unit wins the reconnect race, this device's
|
|
35
|
+
* readData() sees isConnected() === true and skips tryReconnect(), so it
|
|
36
|
+
* would otherwise stay Offline forever despite reading data fine.
|
|
37
|
+
*/
|
|
38
|
+
private appliedOffline;
|
|
30
39
|
/**
|
|
31
40
|
* Prefix used when persisting calibration snapshots via the library's
|
|
32
41
|
* {@link SnapshotService}. Kept identical to the key the SDK used before
|
|
@@ -90,6 +99,20 @@ export declare abstract class BaseSunspecDevice {
|
|
|
90
99
|
* connection after the backoff interval. Then update appliance state.
|
|
91
100
|
*/
|
|
92
101
|
protected markOffline(): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Heal the shared-socket recovery gap. When several devices share one
|
|
104
|
+
* SunspecModbusClient on the same unit (e.g. a Fronius GEN24 inverter and
|
|
105
|
+
* its battery), only the device that detects !isConnected() first runs
|
|
106
|
+
* tryReconnect() — and that is the only path that flips the appliance back
|
|
107
|
+
* to Connected. Siblings then see isConnected() === true on their next
|
|
108
|
+
* readData() and skip tryReconnect() entirely, so without this they would
|
|
109
|
+
* stay Offline forever despite reading data fine.
|
|
110
|
+
*
|
|
111
|
+
* Call this on the success path of readData() (after the
|
|
112
|
+
* markOfflineIfUnhealthy() guard has confirmed the unit is healthy). It is
|
|
113
|
+
* a cheap no-op unless this device was previously marked offline.
|
|
114
|
+
*/
|
|
115
|
+
protected reassertConnectedIfRecovered(): Promise<void>;
|
|
93
116
|
protected sendCommandAcknowledge(messageId: string, acknowledgeMessage: EnyoDataBusMessageEnum | string, answer: EnyoCommandAcknowledgeAnswerEnum, rejectionReason?: string): void;
|
|
94
117
|
/**
|
|
95
118
|
* Build a typed {@link SnapshotService} bound to this appliance's calibration storage,
|
|
@@ -108,6 +131,7 @@ export declare class SunspecInverter extends BaseSunspecDevice {
|
|
|
108
131
|
private storage?;
|
|
109
132
|
private errorState;
|
|
110
133
|
private snapshotService?;
|
|
134
|
+
private statusReassertedThisSession;
|
|
111
135
|
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, capabilities?: SunspecInverterCapability[], retryConfig?: IRetryConfig, appliance?: EnyoAppliance, useTls?: boolean);
|
|
112
136
|
connect(): Promise<void>;
|
|
113
137
|
disconnect(): Promise<void>;
|
package/dist/sunspec-devices.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SunspecBatteryChargeState, SunspecBatteryFeatureModeKind, SunspecEnableControl, SunspecInverterCapability, SunspecInverterEvent1, SunspecModelId, SunspecMPPTOperatingState, SunspecStorageMode, SUNSPEC_CONNECTION_LOST_CODE } from "./sunspec-interfaces.js";
|
|
1
|
+
import { SunspecBatteryChargeState, SunspecBatteryFeatureModeKind, SunspecEnableControl, SunspecInverterCapability, SunspecInverterEvent1, SunspecInverterOperatingState, 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";
|
|
@@ -101,6 +101,15 @@ export class BaseSunspecDevice {
|
|
|
101
101
|
dataBus;
|
|
102
102
|
retryManager;
|
|
103
103
|
consecutiveReconnectFailures = 0;
|
|
104
|
+
/**
|
|
105
|
+
* True once {@link markOffline} has set the appliance state to Offline and
|
|
106
|
+
* it has not yet been re-asserted Connected. Used by
|
|
107
|
+
* {@link reassertConnectedIfRecovered} to heal the shared-socket case: when
|
|
108
|
+
* a sibling device on the same unit wins the reconnect race, this device's
|
|
109
|
+
* readData() sees isConnected() === true and skips tryReconnect(), so it
|
|
110
|
+
* would otherwise stay Offline forever despite reading data fine.
|
|
111
|
+
*/
|
|
112
|
+
appliedOffline = false;
|
|
104
113
|
/**
|
|
105
114
|
* Prefix used when persisting calibration snapshots via the library's
|
|
106
115
|
* {@link SnapshotService}. Kept identical to the key the SDK used before
|
|
@@ -175,6 +184,7 @@ export class BaseSunspecDevice {
|
|
|
175
184
|
if (this.applianceId) {
|
|
176
185
|
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Connected);
|
|
177
186
|
}
|
|
187
|
+
this.appliedOffline = false;
|
|
178
188
|
await this.onConnectionRestored();
|
|
179
189
|
const postStats = this.sunspecClient.getConnectionStats();
|
|
180
190
|
console.log(`${this.constructor.name} ${this.applianceId}: Reconnection successful after ${attempt} attempt(s) (opens=${postStats.opens}, closes=${postStats.closes}, openUnits=${postStats.openUnits})`);
|
|
@@ -243,12 +253,46 @@ export class BaseSunspecDevice {
|
|
|
243
253
|
if (this.applianceId) {
|
|
244
254
|
try {
|
|
245
255
|
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
|
|
256
|
+
this.appliedOffline = true;
|
|
246
257
|
}
|
|
247
258
|
catch (error) {
|
|
248
259
|
console.error(`${this.constructor.name} ${this.applianceId}: Failed to mark appliance offline: ${error}`);
|
|
249
260
|
}
|
|
250
261
|
}
|
|
251
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Heal the shared-socket recovery gap. When several devices share one
|
|
265
|
+
* SunspecModbusClient on the same unit (e.g. a Fronius GEN24 inverter and
|
|
266
|
+
* its battery), only the device that detects !isConnected() first runs
|
|
267
|
+
* tryReconnect() — and that is the only path that flips the appliance back
|
|
268
|
+
* to Connected. Siblings then see isConnected() === true on their next
|
|
269
|
+
* readData() and skip tryReconnect() entirely, so without this they would
|
|
270
|
+
* stay Offline forever despite reading data fine.
|
|
271
|
+
*
|
|
272
|
+
* Call this on the success path of readData() (after the
|
|
273
|
+
* markOfflineIfUnhealthy() guard has confirmed the unit is healthy). It is
|
|
274
|
+
* a cheap no-op unless this device was previously marked offline.
|
|
275
|
+
*/
|
|
276
|
+
async reassertConnectedIfRecovered() {
|
|
277
|
+
if (!this.appliedOffline || !this.applianceId) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (!this.sunspecClient.isHealthy(this.unitId)) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
console.log(`${this.constructor.name} ${this.applianceId}: connection recovered via shared socket — re-asserting Connected`);
|
|
284
|
+
try {
|
|
285
|
+
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Connected);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
console.error(`${this.constructor.name} ${this.applianceId}: Failed to re-assert Connected: ${error}`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this.appliedOffline = false;
|
|
292
|
+
this.retryManager.reset();
|
|
293
|
+
this.consecutiveReconnectFailures = 0;
|
|
294
|
+
await this.onConnectionRestored();
|
|
295
|
+
}
|
|
252
296
|
sendCommandAcknowledge(messageId, acknowledgeMessage, answer, rejectionReason) {
|
|
253
297
|
if (!this.dataBus || !this.applianceId) {
|
|
254
298
|
return;
|
|
@@ -295,6 +339,12 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
295
339
|
storage;
|
|
296
340
|
errorState = { activeCodes: [], lastStatus: 'healthy' };
|
|
297
341
|
snapshotService;
|
|
342
|
+
// Whether we've emitted a status update at least once since (re)connecting.
|
|
343
|
+
// The persisted errorState can diverge from what the core actually shows
|
|
344
|
+
// (e.g. a faulted that was never cleared while our local state reads
|
|
345
|
+
// healthy), so we force one re-assert of the current status on the first
|
|
346
|
+
// successful read of each session to converge the core.
|
|
347
|
+
statusReassertedThisSession = false;
|
|
298
348
|
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig, appliance, useTls) {
|
|
299
349
|
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig, appliance, useTls);
|
|
300
350
|
this.capabilities = capabilities;
|
|
@@ -343,9 +393,13 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
343
393
|
else {
|
|
344
394
|
try {
|
|
345
395
|
const existingAppliance = await this.applianceManager.findApplianceById(this.applianceId);
|
|
396
|
+
// Drop any previously core-stored `status` so we don't echo a
|
|
397
|
+
// stale `faulted` back through ApplianceSave — status is driven
|
|
398
|
+
// solely by ApplianceStateUpdateV1 (detectAndEmitStatusTransition).
|
|
399
|
+
const { status: _staleStatus, ...prevMetadata } = existingAppliance?.metadata ?? {};
|
|
346
400
|
await this.applianceManager.updateAppliance(this.applianceId, {
|
|
347
401
|
metadata: {
|
|
348
|
-
...
|
|
402
|
+
...prevMetadata,
|
|
349
403
|
connectionType: EnyoApplianceConnectionType.Connector,
|
|
350
404
|
state: EnyoApplianceStateEnum.Connected,
|
|
351
405
|
modbus: { unitId: this.unitId, baseAddress: this.baseAddress },
|
|
@@ -369,6 +423,9 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
369
423
|
console.log(`MPPT model found for inverter ${this.networkDevice.hostname}`);
|
|
370
424
|
}
|
|
371
425
|
await this.loadErrorState();
|
|
426
|
+
// Fresh session: force the next successful read to re-assert the true
|
|
427
|
+
// status so a stale faulted in the core is cleared on (re)start.
|
|
428
|
+
this.statusReassertedThisSession = false;
|
|
372
429
|
this.startDataBusListening();
|
|
373
430
|
// Cold-start recovery: if the persisted state says we were stuck in
|
|
374
431
|
// connection_lost but connect() just succeeded (we read commonBlock,
|
|
@@ -413,6 +470,10 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
413
470
|
if (await this.markOfflineIfUnhealthy()) {
|
|
414
471
|
return messages;
|
|
415
472
|
}
|
|
473
|
+
// Healthy read: if a sibling on the same shared socket reconnected us
|
|
474
|
+
// (so our own tryReconnect() never ran), flip our appliance state back
|
|
475
|
+
// to Connected. No-op unless this device is still marked offline.
|
|
476
|
+
await this.reassertConnectedIfRecovered();
|
|
416
477
|
if (inverterData) {
|
|
417
478
|
const pvPowerW = dcStrings.reduce((sum, s) => sum + (s.powerW || 0), 0);
|
|
418
479
|
console.debug(`Got PV Power from DC strings: ${pvPowerW}W`);
|
|
@@ -583,16 +644,30 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
583
644
|
if (!this.applianceId || !this.dataBus)
|
|
584
645
|
return;
|
|
585
646
|
const { codes, codeIds } = this.decodeActiveErrors(data);
|
|
586
|
-
|
|
587
|
-
|
|
647
|
+
// Status is driven by BOTH the decoded event-register error bits AND the
|
|
648
|
+
// SunSpec operating state. Only operatingState 7 (FAULT) is a genuine
|
|
649
|
+
// fault; MPPT (4), SLEEPING (2) and the other states are normal. This
|
|
650
|
+
// is re-evaluated every poll so a producing inverter (operatingState=4,
|
|
651
|
+
// no error bits) reports Healthy even if it was previously faulted.
|
|
652
|
+
const operatingFault = data.operatingState === SunspecInverterOperatingState.FAULT;
|
|
653
|
+
const newStatus = (codeIds.length > 0 || operatingFault)
|
|
654
|
+
? EnyoApplianceStatusEnum.Faulted
|
|
655
|
+
: EnyoApplianceStatusEnum.Healthy;
|
|
656
|
+
const newLastStatus = newStatus === EnyoApplianceStatusEnum.Healthy ? 'healthy' : 'faulted';
|
|
657
|
+
// Emit when the effective status changed (covers faulted/connection_lost
|
|
658
|
+
// -> healthy convergence), when the active error-code set changed, or
|
|
659
|
+
// once per session to re-assert the true status to the core (clears a
|
|
660
|
+
// stale faulted that diverged from our local state). Otherwise stay
|
|
661
|
+
// quiet so we don't republish the same status every 10s poll.
|
|
662
|
+
const statusChanged = newLastStatus !== this.errorState.lastStatus;
|
|
663
|
+
const mustReassert = !this.statusReassertedThisSession;
|
|
664
|
+
if (!statusChanged && !this.hasErrorSetChanged(codeIds) && !mustReassert) {
|
|
588
665
|
return;
|
|
589
666
|
}
|
|
590
|
-
const newStatus = codeIds.length === 0
|
|
591
|
-
? EnyoApplianceStatusEnum.Healthy
|
|
592
|
-
: EnyoApplianceStatusEnum.Faulted;
|
|
593
667
|
const message = this.buildStatusMessage(newStatus, codes, timestamp);
|
|
594
|
-
console.log(`Inverter ${this.applianceId}: status transition -> ${newStatus} (codes=[${codeIds.join(', ')}])`);
|
|
668
|
+
console.log(`Inverter ${this.applianceId}: status transition -> ${newStatus} (codes=[${codeIds.join(', ')}], operatingState=${data.operatingState})`);
|
|
595
669
|
this.dataBus.sendMessage([message]);
|
|
670
|
+
this.statusReassertedThisSession = true;
|
|
596
671
|
this.errorState = {
|
|
597
672
|
evt1: data.events,
|
|
598
673
|
evt2: data.events2,
|
|
@@ -601,7 +676,7 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
601
676
|
evtVnd3: data.vendorEvents3,
|
|
602
677
|
evtVnd4: data.vendorEvents4,
|
|
603
678
|
activeCodes: codeIds,
|
|
604
|
-
lastStatus:
|
|
679
|
+
lastStatus: newLastStatus,
|
|
605
680
|
};
|
|
606
681
|
await this.persistErrorState();
|
|
607
682
|
}
|
|
@@ -1175,6 +1250,10 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
1175
1250
|
if (await this.markOfflineIfUnhealthy()) {
|
|
1176
1251
|
return messages;
|
|
1177
1252
|
}
|
|
1253
|
+
// Healthy read: if a sibling on the same shared socket reconnected us
|
|
1254
|
+
// (so our own tryReconnect() never ran), flip our appliance state back
|
|
1255
|
+
// to Connected. No-op unless this device is still marked offline.
|
|
1256
|
+
await this.reassertConnectedIfRecovered();
|
|
1178
1257
|
if (batteryData) {
|
|
1179
1258
|
// See `selectLiveBatteryPowerW` for the source-preference rationale.
|
|
1180
1259
|
const batteryPowerW = selectLiveBatteryPowerW(mpptBatteryPowerW, batteryData);
|
|
@@ -1687,6 +1766,10 @@ export class SunspecMeter extends BaseSunspecDevice {
|
|
|
1687
1766
|
if (await this.markOfflineIfUnhealthy()) {
|
|
1688
1767
|
return messages;
|
|
1689
1768
|
}
|
|
1769
|
+
// Healthy read: if a sibling on the same shared socket reconnected us
|
|
1770
|
+
// (so our own tryReconnect() never ran), flip our appliance state back
|
|
1771
|
+
// to Connected. No-op unless this device is still marked offline.
|
|
1772
|
+
await this.reassertConnectedIfRecovered();
|
|
1690
1773
|
if (meterData) {
|
|
1691
1774
|
const meterMessage = {
|
|
1692
1775
|
id: randomUUID(),
|
|
@@ -222,6 +222,21 @@ export declare enum SunspecMPPTOperatingState {
|
|
|
222
222
|
TEST = 9,
|
|
223
223
|
RESERVED_10 = 10
|
|
224
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Inverter Operating State values (SunSpec `St`, register offset 38) for
|
|
227
|
+
* models 101/103/111/112/113. Mirrors the SunSpec spec. Only `FAULT` is a
|
|
228
|
+
* genuine fault; `SLEEPING` and `MPPT` are normal production/standby states.
|
|
229
|
+
*/
|
|
230
|
+
export declare enum SunspecInverterOperatingState {
|
|
231
|
+
OFF = 1,
|
|
232
|
+
SLEEPING = 2,
|
|
233
|
+
STARTING = 3,
|
|
234
|
+
MPPT = 4,// Maximum Power Point Tracking active (producing)
|
|
235
|
+
THROTTLED = 5,// Power output is being limited
|
|
236
|
+
SHUTTING_DOWN = 6,
|
|
237
|
+
FAULT = 7,
|
|
238
|
+
STANDBY = 8
|
|
239
|
+
}
|
|
225
240
|
/**
|
|
226
241
|
* Battery Charge State values for Model 124
|
|
227
242
|
*/
|
|
@@ -84,6 +84,22 @@ export var SunspecMPPTOperatingState;
|
|
|
84
84
|
SunspecMPPTOperatingState[SunspecMPPTOperatingState["TEST"] = 9] = "TEST";
|
|
85
85
|
SunspecMPPTOperatingState[SunspecMPPTOperatingState["RESERVED_10"] = 10] = "RESERVED_10";
|
|
86
86
|
})(SunspecMPPTOperatingState || (SunspecMPPTOperatingState = {}));
|
|
87
|
+
/**
|
|
88
|
+
* Inverter Operating State values (SunSpec `St`, register offset 38) for
|
|
89
|
+
* models 101/103/111/112/113. Mirrors the SunSpec spec. Only `FAULT` is a
|
|
90
|
+
* genuine fault; `SLEEPING` and `MPPT` are normal production/standby states.
|
|
91
|
+
*/
|
|
92
|
+
export var SunspecInverterOperatingState;
|
|
93
|
+
(function (SunspecInverterOperatingState) {
|
|
94
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["OFF"] = 1] = "OFF";
|
|
95
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["SLEEPING"] = 2] = "SLEEPING";
|
|
96
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["STARTING"] = 3] = "STARTING";
|
|
97
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["MPPT"] = 4] = "MPPT";
|
|
98
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["THROTTLED"] = 5] = "THROTTLED";
|
|
99
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["SHUTTING_DOWN"] = 6] = "SHUTTING_DOWN";
|
|
100
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["FAULT"] = 7] = "FAULT";
|
|
101
|
+
SunspecInverterOperatingState[SunspecInverterOperatingState["STANDBY"] = 8] = "STANDBY";
|
|
102
|
+
})(SunspecInverterOperatingState || (SunspecInverterOperatingState = {}));
|
|
87
103
|
/**
|
|
88
104
|
* Battery Charge State values for Model 124
|
|
89
105
|
*/
|
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.83",
|
|
4
4
|
"description": "enyo Energy Sunspec SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@enyo-energy/appliance-calibration": "0.0.2",
|
|
44
|
-
"@enyo-energy/energy-app-sdk": "^0.0.
|
|
44
|
+
"@enyo-energy/energy-app-sdk": "^0.0.160"
|
|
45
45
|
},
|
|
46
46
|
"volta": {
|
|
47
47
|
"node": "22.17.0"
|