@enyo-energy/sunspec-sdk 0.0.80 → 0.0.82

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.
@@ -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;
@@ -420,6 +464,10 @@ class SunspecInverter extends BaseSunspecDevice {
420
464
  if (await this.markOfflineIfUnhealthy()) {
421
465
  return messages;
422
466
  }
467
+ // Healthy read: if a sibling on the same shared socket reconnected us
468
+ // (so our own tryReconnect() never ran), flip our appliance state back
469
+ // to Connected. No-op unless this device is still marked offline.
470
+ await this.reassertConnectedIfRecovered();
423
471
  if (inverterData) {
424
472
  const pvPowerW = dcStrings.reduce((sum, s) => sum + (s.powerW || 0), 0);
425
473
  console.debug(`Got PV Power from DC strings: ${pvPowerW}W`);
@@ -1183,6 +1231,10 @@ class SunspecBattery extends BaseSunspecDevice {
1183
1231
  if (await this.markOfflineIfUnhealthy()) {
1184
1232
  return messages;
1185
1233
  }
1234
+ // Healthy read: if a sibling on the same shared socket reconnected us
1235
+ // (so our own tryReconnect() never ran), flip our appliance state back
1236
+ // to Connected. No-op unless this device is still marked offline.
1237
+ await this.reassertConnectedIfRecovered();
1186
1238
  if (batteryData) {
1187
1239
  // See `selectLiveBatteryPowerW` for the source-preference rationale.
1188
1240
  const batteryPowerW = selectLiveBatteryPowerW(mpptBatteryPowerW, batteryData);
@@ -1696,6 +1748,10 @@ class SunspecMeter extends BaseSunspecDevice {
1696
1748
  if (await this.markOfflineIfUnhealthy()) {
1697
1749
  return messages;
1698
1750
  }
1751
+ // Healthy read: if a sibling on the same shared socket reconnected us
1752
+ // (so our own tryReconnect() never ran), flip our appliance state back
1753
+ // to Connected. No-op unless this device is still marked offline.
1754
+ await this.reassertConnectedIfRecovered();
1699
1755
  if (meterData) {
1700
1756
  const meterMessage = {
1701
1757
  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,
@@ -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.80';
12
+ exports.SDK_VERSION = '0.0.82';
13
13
  /**
14
14
  * Gets the current SDK version.
15
15
  * @returns The semantic version string of the SDK
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "0.0.80";
8
+ export declare const SDK_VERSION = "0.0.82";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
@@ -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,
@@ -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;
@@ -413,6 +457,10 @@ export class SunspecInverter extends BaseSunspecDevice {
413
457
  if (await this.markOfflineIfUnhealthy()) {
414
458
  return messages;
415
459
  }
460
+ // Healthy read: if a sibling on the same shared socket reconnected us
461
+ // (so our own tryReconnect() never ran), flip our appliance state back
462
+ // to Connected. No-op unless this device is still marked offline.
463
+ await this.reassertConnectedIfRecovered();
416
464
  if (inverterData) {
417
465
  const pvPowerW = dcStrings.reduce((sum, s) => sum + (s.powerW || 0), 0);
418
466
  console.debug(`Got PV Power from DC strings: ${pvPowerW}W`);
@@ -1175,6 +1223,10 @@ export class SunspecBattery extends BaseSunspecDevice {
1175
1223
  if (await this.markOfflineIfUnhealthy()) {
1176
1224
  return messages;
1177
1225
  }
1226
+ // Healthy read: if a sibling on the same shared socket reconnected us
1227
+ // (so our own tryReconnect() never ran), flip our appliance state back
1228
+ // to Connected. No-op unless this device is still marked offline.
1229
+ await this.reassertConnectedIfRecovered();
1178
1230
  if (batteryData) {
1179
1231
  // See `selectLiveBatteryPowerW` for the source-preference rationale.
1180
1232
  const batteryPowerW = selectLiveBatteryPowerW(mpptBatteryPowerW, batteryData);
@@ -1687,6 +1739,10 @@ export class SunspecMeter extends BaseSunspecDevice {
1687
1739
  if (await this.markOfflineIfUnhealthy()) {
1688
1740
  return messages;
1689
1741
  }
1742
+ // Healthy read: if a sibling on the same shared socket reconnected us
1743
+ // (so our own tryReconnect() never ran), flip our appliance state back
1744
+ // to Connected. No-op unless this device is still marked offline.
1745
+ await this.reassertConnectedIfRecovered();
1690
1746
  if (meterData) {
1691
1747
  const meterMessage = {
1692
1748
  id: randomUUID(),
package/dist/version.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "0.0.80";
8
+ export declare const SDK_VERSION = "0.0.82";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/dist/version.js CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export const SDK_VERSION = '0.0.80';
8
+ export const SDK_VERSION = '0.0.82';
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enyo-energy/sunspec-sdk",
3
- "version": "0.0.80",
3
+ "version": "0.0.82",
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.149"
44
+ "@enyo-energy/energy-app-sdk": "^0.0.160"
45
45
  },
46
46
  "volta": {
47
47
  "node": "22.17.0"