@enyo-energy/sunspec-sdk 0.0.82 → 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.
@@ -346,6 +346,12 @@ class SunspecInverter extends BaseSunspecDevice {
346
346
  storage;
347
347
  errorState = { activeCodes: [], lastStatus: 'healthy' };
348
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;
349
355
  constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig, appliance, useTls) {
350
356
  super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig, appliance, useTls);
351
357
  this.capabilities = capabilities;
@@ -394,9 +400,13 @@ class SunspecInverter extends BaseSunspecDevice {
394
400
  else {
395
401
  try {
396
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 ?? {};
397
407
  await this.applianceManager.updateAppliance(this.applianceId, {
398
408
  metadata: {
399
- ...existingAppliance?.metadata,
409
+ ...prevMetadata,
400
410
  connectionType: enyo_appliance_js_1.EnyoApplianceConnectionType.Connector,
401
411
  state: enyo_appliance_js_1.EnyoApplianceStateEnum.Connected,
402
412
  modbus: { unitId: this.unitId, baseAddress: this.baseAddress },
@@ -420,6 +430,9 @@ class SunspecInverter extends BaseSunspecDevice {
420
430
  console.log(`MPPT model found for inverter ${this.networkDevice.hostname}`);
421
431
  }
422
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;
423
436
  this.startDataBusListening();
424
437
  // Cold-start recovery: if the persisted state says we were stuck in
425
438
  // connection_lost but connect() just succeeded (we read commonBlock,
@@ -638,16 +651,30 @@ class SunspecInverter extends BaseSunspecDevice {
638
651
  if (!this.applianceId || !this.dataBus)
639
652
  return;
640
653
  const { codes, codeIds } = this.decodeActiveErrors(data);
641
- const recoveringFromConnectionLoss = this.errorState.lastStatus === 'connection_lost';
642
- if (!recoveringFromConnectionLoss && !this.hasErrorSetChanged(codeIds)) {
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) {
643
672
  return;
644
673
  }
645
- const newStatus = codeIds.length === 0
646
- ? enyo_appliance_js_1.EnyoApplianceStatusEnum.Healthy
647
- : enyo_appliance_js_1.EnyoApplianceStatusEnum.Faulted;
648
674
  const message = this.buildStatusMessage(newStatus, codes, timestamp);
649
- 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})`);
650
676
  this.dataBus.sendMessage([message]);
677
+ this.statusReassertedThisSession = true;
651
678
  this.errorState = {
652
679
  evt1: data.events,
653
680
  evt2: data.events2,
@@ -656,7 +683,7 @@ class SunspecInverter extends BaseSunspecDevice {
656
683
  evtVnd3: data.vendorEvents3,
657
684
  evtVnd4: data.vendorEvents4,
658
685
  activeCodes: codeIds,
659
- lastStatus: newStatus === enyo_appliance_js_1.EnyoApplianceStatusEnum.Healthy ? 'healthy' : 'faulted',
686
+ lastStatus: newLastStatus,
660
687
  };
661
688
  await this.persistErrorState();
662
689
  }
@@ -131,6 +131,7 @@ export declare class SunspecInverter extends BaseSunspecDevice {
131
131
  private storage?;
132
132
  private errorState;
133
133
  private snapshotService?;
134
+ private statusReassertedThisSession;
134
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);
135
136
  connect(): Promise<void>;
136
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
  */
@@ -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.82';
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
@@ -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.82";
8
+ export declare const SDK_VERSION = "0.0.83";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
@@ -131,6 +131,7 @@ export declare class SunspecInverter extends BaseSunspecDevice {
131
131
  private storage?;
132
132
  private errorState;
133
133
  private snapshotService?;
134
+ private statusReassertedThisSession;
134
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);
135
136
  connect(): Promise<void>;
136
137
  disconnect(): Promise<void>;
@@ -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";
@@ -339,6 +339,12 @@ export class SunspecInverter extends BaseSunspecDevice {
339
339
  storage;
340
340
  errorState = { activeCodes: [], lastStatus: 'healthy' };
341
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;
342
348
  constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig, appliance, useTls) {
343
349
  super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig, appliance, useTls);
344
350
  this.capabilities = capabilities;
@@ -387,9 +393,13 @@ export class SunspecInverter extends BaseSunspecDevice {
387
393
  else {
388
394
  try {
389
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 ?? {};
390
400
  await this.applianceManager.updateAppliance(this.applianceId, {
391
401
  metadata: {
392
- ...existingAppliance?.metadata,
402
+ ...prevMetadata,
393
403
  connectionType: EnyoApplianceConnectionType.Connector,
394
404
  state: EnyoApplianceStateEnum.Connected,
395
405
  modbus: { unitId: this.unitId, baseAddress: this.baseAddress },
@@ -413,6 +423,9 @@ export class SunspecInverter extends BaseSunspecDevice {
413
423
  console.log(`MPPT model found for inverter ${this.networkDevice.hostname}`);
414
424
  }
415
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;
416
429
  this.startDataBusListening();
417
430
  // Cold-start recovery: if the persisted state says we were stuck in
418
431
  // connection_lost but connect() just succeeded (we read commonBlock,
@@ -631,16 +644,30 @@ export class SunspecInverter extends BaseSunspecDevice {
631
644
  if (!this.applianceId || !this.dataBus)
632
645
  return;
633
646
  const { codes, codeIds } = this.decodeActiveErrors(data);
634
- const recoveringFromConnectionLoss = this.errorState.lastStatus === 'connection_lost';
635
- if (!recoveringFromConnectionLoss && !this.hasErrorSetChanged(codeIds)) {
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) {
636
665
  return;
637
666
  }
638
- const newStatus = codeIds.length === 0
639
- ? EnyoApplianceStatusEnum.Healthy
640
- : EnyoApplianceStatusEnum.Faulted;
641
667
  const message = this.buildStatusMessage(newStatus, codes, timestamp);
642
- 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})`);
643
669
  this.dataBus.sendMessage([message]);
670
+ this.statusReassertedThisSession = true;
644
671
  this.errorState = {
645
672
  evt1: data.events,
646
673
  evt2: data.events2,
@@ -649,7 +676,7 @@ export class SunspecInverter extends BaseSunspecDevice {
649
676
  evtVnd3: data.vendorEvents3,
650
677
  evtVnd4: data.vendorEvents4,
651
678
  activeCodes: codeIds,
652
- lastStatus: newStatus === EnyoApplianceStatusEnum.Healthy ? 'healthy' : 'faulted',
679
+ lastStatus: newLastStatus,
653
680
  };
654
681
  await this.persistErrorState();
655
682
  }
@@ -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
@@ -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.82";
8
+ export declare const SDK_VERSION = "0.0.83";
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.82';
8
+ export const SDK_VERSION = '0.0.83';
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.82",
3
+ "version": "0.0.83",
4
4
  "description": "enyo Energy Sunspec SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",