@enyo-energy/sunspec-sdk 0.0.71 → 0.0.72
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 +302 -0
- package/dist/cjs/index.cjs +30 -2
- package/dist/cjs/index.d.cts +6 -1
- package/dist/cjs/sunspec-battery-calibration-driver.cjs +158 -0
- package/dist/cjs/sunspec-battery-calibration-driver.d.cts +63 -0
- package/dist/cjs/sunspec-battery-feature-calibrator.cjs +350 -0
- package/dist/cjs/sunspec-battery-feature-calibrator.d.cts +89 -0
- package/dist/cjs/sunspec-battery-schedule-handler.cjs +92 -0
- package/dist/cjs/sunspec-battery-schedule-handler.d.cts +67 -0
- package/dist/cjs/sunspec-calibration-storage.cjs +47 -0
- package/dist/cjs/sunspec-calibration-storage.d.cts +24 -0
- package/dist/cjs/sunspec-devices.cjs +270 -215
- package/dist/cjs/sunspec-devices.d.cts +99 -19
- package/dist/cjs/sunspec-interfaces.cjs +42 -1
- package/dist/cjs/sunspec-interfaces.d.cts +66 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +12 -1
- package/dist/sunspec-battery-calibration-driver.d.ts +63 -0
- package/dist/sunspec-battery-calibration-driver.js +154 -0
- package/dist/sunspec-battery-feature-calibrator.d.ts +89 -0
- package/dist/sunspec-battery-feature-calibrator.js +345 -0
- package/dist/sunspec-battery-schedule-handler.d.ts +67 -0
- package/dist/sunspec-battery-schedule-handler.js +88 -0
- package/dist/sunspec-calibration-storage.d.ts +24 -0
- package/dist/sunspec-calibration-storage.js +42 -0
- package/dist/sunspec-devices.d.ts +99 -19
- package/dist/sunspec-devices.js +271 -216
- package/dist/sunspec-interfaces.d.ts +66 -0
- package/dist/sunspec-interfaces.js +41 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
|
@@ -6,7 +6,11 @@ const node_crypto_1 = require("node:crypto");
|
|
|
6
6
|
const enyo_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js");
|
|
7
7
|
const connection_retry_manager_js_1 = require("./connection-retry-manager.cjs");
|
|
8
8
|
const enyo_data_bus_value_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js");
|
|
9
|
-
const
|
|
9
|
+
const appliance_calibration_1 = require("@enyo-energy/appliance-calibration");
|
|
10
|
+
const sunspec_calibration_storage_js_1 = require("./sunspec-calibration-storage.cjs");
|
|
11
|
+
const sunspec_battery_calibration_driver_js_1 = require("./sunspec-battery-calibration-driver.cjs");
|
|
12
|
+
const sunspec_battery_schedule_handler_js_1 = require("./sunspec-battery-schedule-handler.cjs");
|
|
13
|
+
const sunspec_battery_feature_calibrator_js_1 = require("./sunspec-battery-feature-calibrator.cjs");
|
|
10
14
|
const enyo_source_enum_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-source.enum.js");
|
|
11
15
|
const enyo_meter_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-meter-appliance.js");
|
|
12
16
|
const enyo_battery_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js");
|
|
@@ -100,7 +104,13 @@ class BaseSunspecDevice {
|
|
|
100
104
|
dataBus;
|
|
101
105
|
retryManager;
|
|
102
106
|
consecutiveReconnectFailures = 0;
|
|
103
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Prefix used when persisting calibration snapshots via the library's
|
|
109
|
+
* {@link SnapshotService}. Kept identical to the key the SDK used before
|
|
110
|
+
* the migration to `@enyo-energy/appliance-calibration` so existing
|
|
111
|
+
* snapshots stored by older builds are still picked up on startup.
|
|
112
|
+
*/
|
|
113
|
+
static CALIBRATION_SNAPSHOT_KEY_PREFIX = "sunspec-calibration-snapshot-";
|
|
104
114
|
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, retryConfig, appliance, useTls) {
|
|
105
115
|
this.energyApp = energyApp;
|
|
106
116
|
this.name = name;
|
|
@@ -255,25 +265,18 @@ class BaseSunspecDevice {
|
|
|
255
265
|
this.dataBus.sendMessage([ackMessage]);
|
|
256
266
|
}
|
|
257
267
|
/**
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
268
|
+
* Build a typed {@link SnapshotService} bound to this appliance's calibration storage,
|
|
269
|
+
* using the legacy SDK storage key prefix so prior installs upgrade seamlessly. Returns
|
|
270
|
+
* undefined if the appliance has not been registered yet.
|
|
261
271
|
*/
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
return;
|
|
272
|
+
buildSnapshotService(onRestore) {
|
|
273
|
+
if (!this.applianceId) {
|
|
274
|
+
return undefined;
|
|
265
275
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Subclasses implement how to write the modified fields of a snapshot back to the device.
|
|
272
|
-
* Called both on explicit StopCalibrationV1 and when the 5-minute auto-stop fires.
|
|
273
|
-
*/
|
|
274
|
-
async restoreFromCalibrationSnapshot(_snapshot, _reason) {
|
|
275
|
-
// Default: subclasses override. Base implementation is a no-op so meter and other
|
|
276
|
-
// non-controllable devices don't need to implement it.
|
|
276
|
+
const storage = new sunspec_calibration_storage_js_1.SunspecCalibrationStorage(this.energyApp.useStorage());
|
|
277
|
+
return new appliance_calibration_1.SnapshotService(storage, this.applianceId, onRestore, {
|
|
278
|
+
storageKeyPrefix: BaseSunspecDevice.CALIBRATION_SNAPSHOT_KEY_PREFIX,
|
|
279
|
+
});
|
|
277
280
|
}
|
|
278
281
|
}
|
|
279
282
|
exports.BaseSunspecDevice = BaseSunspecDevice;
|
|
@@ -286,6 +289,7 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
286
289
|
static CONNECTION_FAULT_THRESHOLD = 3;
|
|
287
290
|
storage;
|
|
288
291
|
errorState = { activeCodes: [], lastStatus: 'healthy' };
|
|
292
|
+
snapshotService;
|
|
289
293
|
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig, appliance, useTls) {
|
|
290
294
|
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig, appliance, useTls);
|
|
291
295
|
this.capabilities = capabilities;
|
|
@@ -698,10 +702,10 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
698
702
|
if (this.dataBusListenerId) {
|
|
699
703
|
return;
|
|
700
704
|
}
|
|
701
|
-
// Fire and forget —
|
|
705
|
+
// Fire and forget — snapshot service hydration must not block listener setup.
|
|
702
706
|
// Listener registration before await ensures we don't miss messages that arrive
|
|
703
|
-
// during initial load.
|
|
704
|
-
void this.
|
|
707
|
+
// during the initial load.
|
|
708
|
+
void this.initSnapshotService();
|
|
705
709
|
this.dataBus = this.energyApp.useDataBus();
|
|
706
710
|
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
707
711
|
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetInverterFeedInLimitV1,
|
|
@@ -762,22 +766,39 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
762
766
|
}
|
|
763
767
|
// setFeedInLimit writes WMaxLimPct + WMaxLim_Ena (see SunspecModbusClient.setFeedInLimit).
|
|
764
768
|
// Record both so an active calibration can roll them back on stop.
|
|
765
|
-
await this.
|
|
769
|
+
await this.snapshotService?.recordModification(['WMaxLimPct', 'WMaxLim_Ena']);
|
|
766
770
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
767
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* Lazily construct the per-appliance {@link SnapshotService}. Reloads any
|
|
774
|
+
* persisted snapshot from storage so an in-flight calibration survives
|
|
775
|
+
* process restarts. Idempotent.
|
|
776
|
+
*/
|
|
777
|
+
async initSnapshotService() {
|
|
778
|
+
if (this.snapshotService) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const service = this.buildSnapshotService((snapshot, reason) => this.restoreInverterSnapshot(snapshot, reason));
|
|
782
|
+
if (!service) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
this.snapshotService = service;
|
|
786
|
+
await this.snapshotService.initialize();
|
|
787
|
+
console.log(`Inverter ${this.applianceId}: snapshot service initialized`);
|
|
788
|
+
}
|
|
768
789
|
async handleStartCalibration(msg) {
|
|
769
790
|
if (!this.isConnected() || !this.applianceId) {
|
|
770
791
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
|
|
771
792
|
return;
|
|
772
793
|
}
|
|
773
|
-
if (!this.
|
|
774
|
-
await this.
|
|
794
|
+
if (!this.snapshotService) {
|
|
795
|
+
await this.initSnapshotService();
|
|
775
796
|
}
|
|
776
|
-
if (!this.
|
|
777
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, '
|
|
797
|
+
if (!this.snapshotService) {
|
|
798
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Snapshot service unavailable');
|
|
778
799
|
return;
|
|
779
800
|
}
|
|
780
|
-
if (this.
|
|
801
|
+
if (this.snapshotService.isCalibrating()) {
|
|
781
802
|
console.log(`Inverter ${this.applianceId}: calibration already active — ack idempotently`);
|
|
782
803
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
783
804
|
return;
|
|
@@ -789,11 +810,7 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
789
810
|
return;
|
|
790
811
|
}
|
|
791
812
|
try {
|
|
792
|
-
await this.
|
|
793
|
-
deviceType: 'inverter',
|
|
794
|
-
unitId: this.unitId,
|
|
795
|
-
inverterControls: controls,
|
|
796
|
-
});
|
|
813
|
+
await this.snapshotService.startCalibration(controls);
|
|
797
814
|
}
|
|
798
815
|
catch (error) {
|
|
799
816
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to persist snapshot: ${error}`);
|
|
@@ -807,30 +824,23 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
807
824
|
return;
|
|
808
825
|
}
|
|
809
826
|
console.log(`Inverter ${this.applianceId}: handling StopCalibrationV1`);
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
}
|
|
816
|
-
try {
|
|
817
|
-
await this.restoreFromCalibrationSnapshot(snapshot, 'stop');
|
|
818
|
-
}
|
|
819
|
-
catch (error) {
|
|
820
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to restore registers: ${error}`);
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
827
|
+
// SnapshotService.stopCalibration fires `restoreInverterSnapshot` internally with
|
|
828
|
+
// reason="stop" before returning. Any restore failure is logged by the callback
|
|
829
|
+
// (the persisted snapshot is already gone by then — see library notes) so we
|
|
830
|
+
// always ack Accepted from here.
|
|
831
|
+
await this.snapshotService?.stopCalibration();
|
|
823
832
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
824
833
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
834
|
+
/**
|
|
835
|
+
* `onRestore` callback for the inverter's {@link SnapshotService}. Writes only the
|
|
836
|
+
* subset of writable inverter fields that other commands actually touched during the
|
|
837
|
+
* calibration. Catches and logs failures rather than throwing — the snapshot has
|
|
838
|
+
* already been removed from storage by the time this runs, so a throw is unrecoverable
|
|
839
|
+
* and is best logged for operator action.
|
|
840
|
+
*/
|
|
841
|
+
async restoreInverterSnapshot(snapshot, reason) {
|
|
842
|
+
const source = snapshot.payload;
|
|
830
843
|
const partial = {};
|
|
831
|
-
// Whitelist of inverter fields that writeInverterControls actually writes (Model 123).
|
|
832
|
-
// Iterating a typed const list keeps the projection type-safe and bounds the rollback
|
|
833
|
-
// to fields the SDK knows how to restore.
|
|
834
844
|
const WRITABLE_INVERTER_FIELDS = ['Conn', 'WMaxLimPct', 'WMaxLim_Ena', 'OutPFSet', 'OutPFSet_Ena'];
|
|
835
845
|
for (const field of WRITABLE_INVERTER_FIELDS) {
|
|
836
846
|
if (snapshot.modifiedFields.includes(field) && source[field] !== undefined) {
|
|
@@ -842,9 +852,14 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
842
852
|
return;
|
|
843
853
|
}
|
|
844
854
|
console.log(`Inverter ${this.applianceId}: calibration ${reason} — restoring [${Object.keys(partial).join(', ')}]`);
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
855
|
+
try {
|
|
856
|
+
const ok = await this.sunspecClient.writeInverterControls(this.unitId, partial);
|
|
857
|
+
if (!ok) {
|
|
858
|
+
console.error(`Inverter ${this.applianceId}: calibration ${reason} — writeInverterControls returned false`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
console.error(`Inverter ${this.applianceId}: calibration ${reason} — writeInverterControls threw: ${error}`);
|
|
848
863
|
}
|
|
849
864
|
}
|
|
850
865
|
}
|
|
@@ -853,11 +868,126 @@ exports.SunspecInverter = SunspecInverter;
|
|
|
853
868
|
* Sunspec Battery implementation
|
|
854
869
|
*/
|
|
855
870
|
class SunspecBattery extends BaseSunspecDevice {
|
|
871
|
+
featureMode;
|
|
856
872
|
capabilities;
|
|
857
|
-
|
|
873
|
+
snapshotService;
|
|
874
|
+
calibrationDriver;
|
|
875
|
+
batteryCalibrator;
|
|
876
|
+
calibrationResultStore;
|
|
877
|
+
scheduleHandler;
|
|
878
|
+
/**
|
|
879
|
+
* Battery features that imply outbound control writes from the host action-taker.
|
|
880
|
+
* These are stripped from `appliance.battery.features` until the calibration result
|
|
881
|
+
* store says this battery has been successfully calibrated — see
|
|
882
|
+
* {@link computeAdvertisedFeatures}.
|
|
883
|
+
*/
|
|
884
|
+
static CONTROLLABLE_FEATURES = [
|
|
885
|
+
enyo_battery_appliance_js_1.EnyoBatteryFeature.GridCharging,
|
|
886
|
+
enyo_battery_appliance_js_1.EnyoBatteryFeature.GridDischarging,
|
|
887
|
+
enyo_battery_appliance_js_1.EnyoBatteryFeature.ChargeLimitation,
|
|
888
|
+
enyo_battery_appliance_js_1.EnyoBatteryFeature.DischargeLimitation,
|
|
889
|
+
];
|
|
890
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, featureMode, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig, appliance, useTls) {
|
|
858
891
|
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig, appliance, useTls);
|
|
892
|
+
this.featureMode = featureMode;
|
|
859
893
|
this.capabilities = capabilities;
|
|
860
894
|
}
|
|
895
|
+
/**
|
|
896
|
+
* Wire the battery into `@enyo-energy/appliance-calibration`'s
|
|
897
|
+
* `BatteryCalibrator` test-charge flow. Call once after {@link connect}.
|
|
898
|
+
*
|
|
899
|
+
* Consumers own the `CalibrationResultStore` (shared across appliances so
|
|
900
|
+
* the persisted result map doesn't get clobbered) and the
|
|
901
|
+
* `CalibrationTrigger` that drives `runCalibration()` on whatever schedule
|
|
902
|
+
* makes sense for the host. Pass overrides for the calibrator's defaults
|
|
903
|
+
* (`testPowerW`, response thresholds, etc.) via `opts.config`.
|
|
904
|
+
*/
|
|
905
|
+
configureCalibration(opts) {
|
|
906
|
+
if (this.featureMode.kind !== sunspec_interfaces_js_1.SunspecBatteryFeatureModeKind.CalibrationBased) {
|
|
907
|
+
throw new Error(`SunspecBattery.configureCalibration: featureMode is '${this.featureMode.kind}', ` +
|
|
908
|
+
`but calibration is only valid in '${sunspec_interfaces_js_1.SunspecBatteryFeatureModeKind.CalibrationBased}' mode. ` +
|
|
909
|
+
`Pass { kind: SunspecBatteryFeatureModeKind.CalibrationBased } to the constructor.`);
|
|
910
|
+
}
|
|
911
|
+
if (this.batteryCalibrator) {
|
|
912
|
+
return this.batteryCalibrator;
|
|
913
|
+
}
|
|
914
|
+
if (!this.applianceId) {
|
|
915
|
+
throw new Error("SunspecBattery.configureCalibration: connect() must complete first (applianceId required)");
|
|
916
|
+
}
|
|
917
|
+
if (!this.snapshotService) {
|
|
918
|
+
throw new Error("SunspecBattery.configureCalibration: snapshot service not initialized — call connect() first");
|
|
919
|
+
}
|
|
920
|
+
const driver = new sunspec_battery_calibration_driver_js_1.SunspecBatteryCalibrationDriver(this.sunspecClient, this.unitId, this.energyApp.useDataBus());
|
|
921
|
+
this.calibrationDriver = driver;
|
|
922
|
+
this.calibrationResultStore = opts.resultStore;
|
|
923
|
+
this.batteryCalibrator = new sunspec_battery_feature_calibrator_js_1.SunspecBatteryFeatureCalibrator({
|
|
924
|
+
applianceId: this.applianceId,
|
|
925
|
+
driver,
|
|
926
|
+
sunspecClient: this.sunspecClient,
|
|
927
|
+
unitId: this.unitId,
|
|
928
|
+
readBatteryData: () => this.sunspecClient.readBatteryData(this.unitId),
|
|
929
|
+
snapshotService: this.snapshotService,
|
|
930
|
+
resultStore: opts.resultStore,
|
|
931
|
+
config: opts.config,
|
|
932
|
+
});
|
|
933
|
+
return this.batteryCalibrator;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Detect which battery features the device's registers expose. This is the
|
|
937
|
+
* raw, register-only view; the {@link featureMode} configured at
|
|
938
|
+
* construction decides what is actually published — see
|
|
939
|
+
* {@link resolveAdvertisedFeatures}.
|
|
940
|
+
*/
|
|
941
|
+
detectFromRegisters(batteryData) {
|
|
942
|
+
const features = [];
|
|
943
|
+
if (batteryData?.chaGriSet !== undefined) {
|
|
944
|
+
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.GridCharging);
|
|
945
|
+
}
|
|
946
|
+
if (batteryData?.wChaMax !== undefined) {
|
|
947
|
+
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.ChargeLimitation);
|
|
948
|
+
}
|
|
949
|
+
return features;
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Resolve `appliance.battery.features` for the configured mode. Called on
|
|
953
|
+
* every connect() and readData() cycle so that, in calibration-based mode,
|
|
954
|
+
* the controllable features appear as soon as the calibrator flips
|
|
955
|
+
* `isCalibrated` (no synchronous push).
|
|
956
|
+
*/
|
|
957
|
+
resolveAdvertisedFeatures(batteryData) {
|
|
958
|
+
const allow = this.featureMode.allowedFeatures;
|
|
959
|
+
const intersect = (xs) => allow ? xs.filter(f => allow.includes(f)) : xs;
|
|
960
|
+
switch (this.featureMode.kind) {
|
|
961
|
+
case sunspec_interfaces_js_1.SunspecBatteryFeatureModeKind.Disabled:
|
|
962
|
+
return allow ? [...allow] : [];
|
|
963
|
+
case sunspec_interfaces_js_1.SunspecBatteryFeatureModeKind.RegisterBased:
|
|
964
|
+
return intersect(this.detectFromRegisters(batteryData));
|
|
965
|
+
case sunspec_interfaces_js_1.SunspecBatteryFeatureModeKind.CalibrationBased: {
|
|
966
|
+
const detected = intersect(this.detectFromRegisters(batteryData));
|
|
967
|
+
if (!this.applianceId || !this.calibrationResultStore) {
|
|
968
|
+
return detected.filter(f => !SunspecBattery.CONTROLLABLE_FEATURES.includes(f));
|
|
969
|
+
}
|
|
970
|
+
const result = this.calibrationResultStore.getResult(this.applianceId);
|
|
971
|
+
if (!result || result.state !== 'calibrated') {
|
|
972
|
+
return detected.filter(f => !SunspecBattery.CONTROLLABLE_FEATURES.includes(f));
|
|
973
|
+
}
|
|
974
|
+
// Per-feature gating: only publish controllable features whose
|
|
975
|
+
// probe passed during the last calibration. See
|
|
976
|
+
// `decodeFeatureResults` and `SunspecBatteryFeatureCalibrator`.
|
|
977
|
+
const passed = (0, sunspec_battery_feature_calibrator_js_1.decodeFeatureResults)(result.notes);
|
|
978
|
+
return detected.filter(f => !SunspecBattery.CONTROLLABLE_FEATURES.includes(f) || passed.has(f));
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Returns the per-battery `BatteryCalibrator` created by
|
|
984
|
+
* {@link configureCalibration}, or `undefined` if calibration was never
|
|
985
|
+
* configured. Consumers register the returned instance with their own
|
|
986
|
+
* `CalibrationTrigger`.
|
|
987
|
+
*/
|
|
988
|
+
getBatteryCalibrator() {
|
|
989
|
+
return this.batteryCalibrator;
|
|
990
|
+
}
|
|
861
991
|
/**
|
|
862
992
|
* Connect to the battery and create/update the appliance
|
|
863
993
|
*/
|
|
@@ -874,13 +1004,7 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
874
1004
|
const commonData = await this.sunspecClient.readCommonBlock(this.unitId);
|
|
875
1005
|
const batteryData = await this.sunspecClient.readBatteryData(this.unitId);
|
|
876
1006
|
const storageMode = this.determineStorageMode(batteryData);
|
|
877
|
-
const features =
|
|
878
|
-
if (batteryData?.chaGriSet !== undefined) {
|
|
879
|
-
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.GridCharging);
|
|
880
|
-
}
|
|
881
|
-
if (batteryData?.wChaMax !== undefined) {
|
|
882
|
-
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.ChargeLimitation);
|
|
883
|
-
}
|
|
1007
|
+
const features = this.resolveAdvertisedFeatures(batteryData);
|
|
884
1008
|
const activeChargeLimitW = this.computeActiveChargeLimitW(batteryData);
|
|
885
1009
|
const activeDischargeLimitW = this.computeActiveDischargeLimitW(batteryData);
|
|
886
1010
|
// Create or update appliance (skip if an existing appliance was provided)
|
|
@@ -947,6 +1071,11 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
947
1071
|
}
|
|
948
1072
|
async disconnect() {
|
|
949
1073
|
this.stopDataBusListening();
|
|
1074
|
+
if (this.calibrationDriver) {
|
|
1075
|
+
this.calibrationDriver.stop();
|
|
1076
|
+
this.calibrationDriver = undefined;
|
|
1077
|
+
this.batteryCalibrator = undefined;
|
|
1078
|
+
}
|
|
950
1079
|
if (this.applianceId) {
|
|
951
1080
|
try {
|
|
952
1081
|
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
|
|
@@ -1015,6 +1144,11 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1015
1144
|
else if (!advancedBatteryModel) {
|
|
1016
1145
|
batteryPowerW = mpptBatteryPowerW;
|
|
1017
1146
|
}
|
|
1147
|
+
// Feed the calibration driver's synchronous power cache. No-op when calibration
|
|
1148
|
+
// was never configured.
|
|
1149
|
+
if (batteryPowerW !== undefined && this.calibrationDriver) {
|
|
1150
|
+
this.calibrationDriver.updateBatteryPowerCache(batteryPowerW);
|
|
1151
|
+
}
|
|
1018
1152
|
const batteryMessage = {
|
|
1019
1153
|
id: (0, node_crypto_1.randomUUID)(),
|
|
1020
1154
|
message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.BatteryValuesUpdateV1,
|
|
@@ -1038,6 +1172,9 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1038
1172
|
if (this.applianceId) {
|
|
1039
1173
|
const appliance = await this.applianceManager.findApplianceById(this.applianceId);
|
|
1040
1174
|
const storageMode = this.determineStorageMode(batteryData);
|
|
1175
|
+
// Republish features each cycle so the controllable set appears as soon
|
|
1176
|
+
// as the calibrator flips isCalibrated — see resolveAdvertisedFeatures.
|
|
1177
|
+
const features = this.resolveAdvertisedFeatures(batteryData);
|
|
1041
1178
|
await this.applianceManager.updateAppliance(this.applianceId, {
|
|
1042
1179
|
battery: {
|
|
1043
1180
|
...appliance?.battery,
|
|
@@ -1046,6 +1183,7 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1046
1183
|
maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
|
|
1047
1184
|
activeChargeLimitW: this.computeActiveChargeLimitW(batteryData),
|
|
1048
1185
|
activeDischargeLimitW: this.computeActiveDischargeLimitW(batteryData),
|
|
1186
|
+
features,
|
|
1049
1187
|
gridChargingEnabled: batteryData?.chaGriSet === 1
|
|
1050
1188
|
}
|
|
1051
1189
|
});
|
|
@@ -1291,17 +1429,27 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1291
1429
|
if (this.dataBusListenerId) {
|
|
1292
1430
|
return;
|
|
1293
1431
|
}
|
|
1294
|
-
// Fire and forget —
|
|
1295
|
-
void this.
|
|
1432
|
+
// Fire and forget — snapshot service hydration must not block listener setup.
|
|
1433
|
+
void this.initSnapshotService();
|
|
1296
1434
|
this.dataBus = this.energyApp.useDataBus();
|
|
1297
1435
|
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
1298
|
-
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.
|
|
1299
|
-
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopStorageGridChargeV1,
|
|
1300
|
-
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageDischargeLimitV1,
|
|
1301
|
-
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageChargeLimitV1,
|
|
1436
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageScheduleV1,
|
|
1302
1437
|
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1,
|
|
1303
1438
|
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1,
|
|
1304
1439
|
], (entry) => this.handleStorageCommand(entry));
|
|
1440
|
+
if (!this.applianceId) {
|
|
1441
|
+
throw new Error("SunspecBattery.startDataBusListening: applianceId required — call connect() first.");
|
|
1442
|
+
}
|
|
1443
|
+
this.scheduleHandler = new sunspec_battery_schedule_handler_js_1.SunspecBatteryScheduleHandler({
|
|
1444
|
+
dataBus: this.dataBus,
|
|
1445
|
+
interval: this.energyApp.useInterval(),
|
|
1446
|
+
storage: this.energyApp.useStorage(),
|
|
1447
|
+
applianceId: this.applianceId,
|
|
1448
|
+
sunspecClient: this.sunspecClient,
|
|
1449
|
+
unitId: this.unitId,
|
|
1450
|
+
getSnapshotService: () => this.snapshotService,
|
|
1451
|
+
onErrorCallback: (err) => console.warn(`Battery ${this.applianceId}: schedule handler rejected input: ${err.message}`),
|
|
1452
|
+
});
|
|
1305
1453
|
console.log(`Battery ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
1306
1454
|
}
|
|
1307
1455
|
/**
|
|
@@ -1314,6 +1462,7 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1314
1462
|
}
|
|
1315
1463
|
this.dataBusListenerId = undefined;
|
|
1316
1464
|
this.dataBus = undefined;
|
|
1465
|
+
this.scheduleHandler?.dispose();
|
|
1317
1466
|
}
|
|
1318
1467
|
handleStorageCommand(entry) {
|
|
1319
1468
|
if (entry.applianceId !== this.applianceId) {
|
|
@@ -1322,17 +1471,8 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1322
1471
|
void (async () => {
|
|
1323
1472
|
try {
|
|
1324
1473
|
switch (entry.message) {
|
|
1325
|
-
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.
|
|
1326
|
-
|
|
1327
|
-
break;
|
|
1328
|
-
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopStorageGridChargeV1:
|
|
1329
|
-
await this.handleStopGridCharge(entry);
|
|
1330
|
-
break;
|
|
1331
|
-
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageDischargeLimitV1:
|
|
1332
|
-
await this.handleSetDischargeLimit(entry);
|
|
1333
|
-
break;
|
|
1334
|
-
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageChargeLimitV1:
|
|
1335
|
-
await this.handleSetChargeLimit(entry);
|
|
1474
|
+
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageScheduleV1:
|
|
1475
|
+
this.handleSetStorageScheduleAck(entry);
|
|
1336
1476
|
break;
|
|
1337
1477
|
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1:
|
|
1338
1478
|
await this.handleStartCalibration(entry);
|
|
@@ -1347,132 +1487,51 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1347
1487
|
}
|
|
1348
1488
|
})();
|
|
1349
1489
|
}
|
|
1350
|
-
|
|
1490
|
+
/**
|
|
1491
|
+
* Acknowledge a SetStorageScheduleV1 message. The actual schedule application
|
|
1492
|
+
* is owned by `this.scheduleHandler` (subscribed independently via its own
|
|
1493
|
+
* data-bus listener registered in its constructor). The ack reports "received
|
|
1494
|
+
* & queued" rather than "applied" — schedule entries play out over time, and
|
|
1495
|
+
* per-entry write failures land in `console.error` via the handler's
|
|
1496
|
+
* onChange wrapper.
|
|
1497
|
+
*/
|
|
1498
|
+
handleSetStorageScheduleAck(msg) {
|
|
1351
1499
|
if (!this.isConnected() || !this.applianceId) {
|
|
1352
1500
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, "Not connected");
|
|
1353
1501
|
return;
|
|
1354
1502
|
}
|
|
1355
|
-
console.log(`Battery ${this.applianceId}: handling StartStorageGridChargeV1 (powerLimitW=${msg.data.powerLimitW})`);
|
|
1356
|
-
// Read current state for logging and rollback
|
|
1357
|
-
const controls = await this.getBatteryControls();
|
|
1358
|
-
console.log(`Battery ${this.applianceId}: current state - chaGriSet=${controls?.chaGriSet}, wChaMax=${controls?.wChaMax}, storCtlMod=${controls?.storCtlMod}`);
|
|
1359
|
-
const originalChaGriSet = controls?.chaGriSet;
|
|
1360
|
-
const originalWChaMax = controls?.wChaMax;
|
|
1361
|
-
// Step 1: Enable grid charging (Register 17: chaGriSet=GRID)
|
|
1362
|
-
const step1 = await this.enableGridCharging(true);
|
|
1363
|
-
if (!step1) {
|
|
1364
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to enable grid charging');
|
|
1365
|
-
return;
|
|
1366
|
-
}
|
|
1367
|
-
// Step 2: Set charging power (Register 2: wChaMax)
|
|
1368
|
-
const step2 = await this.setChargingPower(msg.data.powerLimitW);
|
|
1369
|
-
if (!step2) {
|
|
1370
|
-
// Rollback step 1
|
|
1371
|
-
await this.writeBatteryControls({ chaGriSet: originalChaGriSet });
|
|
1372
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set charging power');
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
|
-
// Step 3: Set storage mode to CHARGE (Register 5: storCtlMod=0x0001)
|
|
1376
|
-
const step3 = await this.setStorageMode(sunspec_interfaces_js_1.SunspecStorageMode.CHARGE);
|
|
1377
|
-
if (!step3) {
|
|
1378
|
-
// Rollback steps 1+2
|
|
1379
|
-
await this.writeBatteryControls({ chaGriSet: originalChaGriSet, wChaMax: originalWChaMax });
|
|
1380
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set storage mode to charge');
|
|
1381
|
-
return;
|
|
1382
|
-
}
|
|
1383
|
-
await this.calibrationService?.recordModification(['chaGriSet', 'wChaMax', 'storCtlMod']);
|
|
1384
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1385
|
-
}
|
|
1386
|
-
async handleStopGridCharge(msg) {
|
|
1387
|
-
if (!this.isConnected() || !this.applianceId) {
|
|
1388
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported);
|
|
1389
|
-
return;
|
|
1390
|
-
}
|
|
1391
|
-
console.log(`Battery ${this.applianceId}: handling StopStorageGridChargeV1`);
|
|
1392
|
-
// Read current state for logging and rollback
|
|
1393
|
-
const controls = await this.getBatteryControls();
|
|
1394
|
-
console.log(`Battery ${this.applianceId}: current state - chaGriSet=${controls?.chaGriSet}, storCtlMod=${controls?.storCtlMod}`);
|
|
1395
|
-
const originalChaGriSet = controls?.chaGriSet;
|
|
1396
|
-
// Step 1: Disable grid charging (Register 17: chaGriSet=PV)
|
|
1397
|
-
const step1 = await this.enableGridCharging(false);
|
|
1398
|
-
if (!step1) {
|
|
1399
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to disable grid charging');
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1402
|
-
// Step 2: Set storage mode to AUTO (Register 5: storCtlMod=0x0003)
|
|
1403
|
-
const step2 = await this.setStorageMode(sunspec_interfaces_js_1.SunspecStorageMode.AUTO);
|
|
1404
|
-
if (!step2) {
|
|
1405
|
-
// Rollback step 1
|
|
1406
|
-
await this.writeBatteryControls({ chaGriSet: originalChaGriSet });
|
|
1407
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set storage mode to auto');
|
|
1408
|
-
return;
|
|
1409
|
-
}
|
|
1410
|
-
await this.calibrationService?.recordModification(['chaGriSet', 'storCtlMod']);
|
|
1411
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1412
|
-
}
|
|
1413
|
-
async handleSetDischargeLimit(msg) {
|
|
1414
|
-
if (!this.isConnected() || !this.applianceId) {
|
|
1415
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported);
|
|
1416
|
-
return;
|
|
1417
|
-
}
|
|
1418
|
-
console.log(`Battery ${this.applianceId}: handling SetStorageDischargeLimitV1 (dischargeLimitW=${msg.data.dischargeLimitW})`);
|
|
1419
|
-
// Read current state to get wChaMax for percentage conversion
|
|
1420
|
-
const controls = await this.getBatteryControls();
|
|
1421
|
-
console.log(`Battery ${this.applianceId}: current state - outWRte=${controls?.outWRte}, wChaMax=${controls?.wChaMax}`);
|
|
1422
|
-
if (!controls?.wChaMax) {
|
|
1423
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read wChaMax for discharge limit conversion');
|
|
1424
|
-
return;
|
|
1425
|
-
}
|
|
1426
|
-
// Convert watts to percentage of WDisChaMax (using wChaMax), clamped to 0-100%
|
|
1427
|
-
const dischargeLimitPercent = Math.min(100, Math.max(0, (msg.data.dischargeLimitW / controls.wChaMax) * 100));
|
|
1428
|
-
console.log(`Battery ${this.applianceId}: calculated discharge limit: ${dischargeLimitPercent.toFixed(1)}% (${msg.data.dischargeLimitW}W / ${controls.wChaMax}W)`);
|
|
1429
|
-
// Set discharge limit (Register 12: outWRte)
|
|
1430
|
-
const success = await this.writeBatteryControls({ outWRte: dischargeLimitPercent });
|
|
1431
|
-
if (!success) {
|
|
1432
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set discharge limit');
|
|
1433
|
-
return;
|
|
1434
|
-
}
|
|
1435
|
-
await this.calibrationService?.recordModification(['outWRte']);
|
|
1436
1503
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1437
1504
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
const controls = await this.getBatteryControls();
|
|
1446
|
-
console.log(`Battery ${this.applianceId}: current state - inWRte=${controls?.inWRte}, wChaMax=${controls?.wChaMax}`);
|
|
1447
|
-
if (!controls?.wChaMax) {
|
|
1448
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read wChaMax for charge limit conversion');
|
|
1505
|
+
/**
|
|
1506
|
+
* Lazily construct the per-appliance {@link SnapshotService}. Reloads any
|
|
1507
|
+
* persisted snapshot from storage so an in-flight calibration survives
|
|
1508
|
+
* process restarts. Idempotent.
|
|
1509
|
+
*/
|
|
1510
|
+
async initSnapshotService() {
|
|
1511
|
+
if (this.snapshotService) {
|
|
1449
1512
|
return;
|
|
1450
1513
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
console.log(`Battery ${this.applianceId}: calculated charge limit: ${chargeLimitPercent.toFixed(1)}% (${msg.data.chargeLimitW}W / ${controls.wChaMax}W)`);
|
|
1454
|
-
// Set charge limit (Register 13: inWRte)
|
|
1455
|
-
const success = await this.writeBatteryControls({ inWRte: chargeLimitPercent });
|
|
1456
|
-
if (!success) {
|
|
1457
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set charge limit');
|
|
1514
|
+
const service = this.buildSnapshotService((snapshot, reason) => this.restoreBatterySnapshot(snapshot, reason));
|
|
1515
|
+
if (!service) {
|
|
1458
1516
|
return;
|
|
1459
1517
|
}
|
|
1460
|
-
|
|
1461
|
-
this.
|
|
1518
|
+
this.snapshotService = service;
|
|
1519
|
+
await this.snapshotService.initialize();
|
|
1520
|
+
console.log(`Battery ${this.applianceId}: snapshot service initialized`);
|
|
1462
1521
|
}
|
|
1463
1522
|
async handleStartCalibration(msg) {
|
|
1464
1523
|
if (!this.isConnected() || !this.applianceId) {
|
|
1465
1524
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
|
|
1466
1525
|
return;
|
|
1467
1526
|
}
|
|
1468
|
-
if (!this.
|
|
1469
|
-
await this.
|
|
1527
|
+
if (!this.snapshotService) {
|
|
1528
|
+
await this.initSnapshotService();
|
|
1470
1529
|
}
|
|
1471
|
-
if (!this.
|
|
1472
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, '
|
|
1530
|
+
if (!this.snapshotService) {
|
|
1531
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Snapshot service unavailable');
|
|
1473
1532
|
return;
|
|
1474
1533
|
}
|
|
1475
|
-
if (this.
|
|
1534
|
+
if (this.snapshotService.isCalibrating()) {
|
|
1476
1535
|
console.log(`Battery ${this.applianceId}: calibration already active — ack idempotently`);
|
|
1477
1536
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1478
1537
|
return;
|
|
@@ -1484,11 +1543,7 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1484
1543
|
return;
|
|
1485
1544
|
}
|
|
1486
1545
|
try {
|
|
1487
|
-
await this.
|
|
1488
|
-
deviceType: 'battery',
|
|
1489
|
-
unitId: this.unitId,
|
|
1490
|
-
batteryControls: controls,
|
|
1491
|
-
});
|
|
1546
|
+
await this.snapshotService.startCalibration(controls);
|
|
1492
1547
|
}
|
|
1493
1548
|
catch (error) {
|
|
1494
1549
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to persist snapshot: ${error}`);
|
|
@@ -1502,27 +1557,22 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1502
1557
|
return;
|
|
1503
1558
|
}
|
|
1504
1559
|
console.log(`Battery ${this.applianceId}: handling StopCalibrationV1`);
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
try {
|
|
1511
|
-
await this.restoreFromCalibrationSnapshot(snapshot, 'stop');
|
|
1512
|
-
}
|
|
1513
|
-
catch (error) {
|
|
1514
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to restore registers: ${error}`);
|
|
1515
|
-
return;
|
|
1516
|
-
}
|
|
1560
|
+
// SnapshotService.stopCalibration fires `restoreBatterySnapshot` internally
|
|
1561
|
+
// with reason="stop" before returning. Failures from the restore callback are
|
|
1562
|
+
// logged inside the callback (the persisted snapshot is already gone — see
|
|
1563
|
+
// library notes) so we always ack Accepted here.
|
|
1564
|
+
await this.snapshotService?.stopCalibration();
|
|
1517
1565
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1518
1566
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1567
|
+
/**
|
|
1568
|
+
* `onRestore` callback for the battery's {@link SnapshotService}. Writes only the
|
|
1569
|
+
* subset of writable battery fields touched during the calibration. Errors are
|
|
1570
|
+
* caught and logged — by the time this fires the snapshot is gone from storage,
|
|
1571
|
+
* so a throw would just propagate into the void.
|
|
1572
|
+
*/
|
|
1573
|
+
async restoreBatterySnapshot(snapshot, reason) {
|
|
1574
|
+
const source = snapshot.payload;
|
|
1524
1575
|
const partial = {};
|
|
1525
|
-
// Whitelist of battery fields that writeBatteryControls actually writes (Model 124).
|
|
1526
1576
|
const WRITABLE_BATTERY_FIELDS = ['storCtlMod', 'chaGriSet', 'wChaMax', 'inWRte', 'outWRte', 'minRsvPct'];
|
|
1527
1577
|
for (const field of WRITABLE_BATTERY_FIELDS) {
|
|
1528
1578
|
if (snapshot.modifiedFields.includes(field) && source[field] !== undefined) {
|
|
@@ -1534,9 +1584,14 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1534
1584
|
return;
|
|
1535
1585
|
}
|
|
1536
1586
|
console.log(`Battery ${this.applianceId}: calibration ${reason} — restoring [${Object.keys(partial).join(', ')}]`);
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1587
|
+
try {
|
|
1588
|
+
const ok = await this.sunspecClient.writeBatteryControls(this.unitId, partial);
|
|
1589
|
+
if (!ok) {
|
|
1590
|
+
console.error(`Battery ${this.applianceId}: calibration ${reason} — writeBatteryControls returned false`);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
catch (error) {
|
|
1594
|
+
console.error(`Battery ${this.applianceId}: calibration ${reason} — writeBatteryControls threw: ${error}`);
|
|
1540
1595
|
}
|
|
1541
1596
|
}
|
|
1542
1597
|
}
|