@enyo-energy/sunspec-sdk 0.0.71 → 0.0.73
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 +305 -215
- package/dist/cjs/sunspec-devices.d.cts +129 -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 +129 -19
- package/dist/sunspec-devices.js +304 -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
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SunspecMeter = exports.SunspecBattery = exports.SunspecInverter = exports.BaseSunspecDevice = void 0;
|
|
4
|
+
exports.detectFeaturesFromRegisters = detectFeaturesFromRegisters;
|
|
5
|
+
exports.filterFeaturesByCalibrationResult = filterFeaturesByCalibrationResult;
|
|
4
6
|
const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
|
|
5
7
|
const node_crypto_1 = require("node:crypto");
|
|
6
8
|
const enyo_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js");
|
|
7
9
|
const connection_retry_manager_js_1 = require("./connection-retry-manager.cjs");
|
|
8
10
|
const enyo_data_bus_value_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js");
|
|
9
|
-
const
|
|
11
|
+
const appliance_calibration_1 = require("@enyo-energy/appliance-calibration");
|
|
12
|
+
const sunspec_calibration_storage_js_1 = require("./sunspec-calibration-storage.cjs");
|
|
13
|
+
const sunspec_battery_calibration_driver_js_1 = require("./sunspec-battery-calibration-driver.cjs");
|
|
14
|
+
const sunspec_battery_schedule_handler_js_1 = require("./sunspec-battery-schedule-handler.cjs");
|
|
15
|
+
const sunspec_battery_feature_calibrator_js_1 = require("./sunspec-battery-feature-calibrator.cjs");
|
|
10
16
|
const enyo_source_enum_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-source.enum.js");
|
|
11
17
|
const enyo_meter_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-meter-appliance.js");
|
|
12
18
|
const enyo_battery_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js");
|
|
@@ -100,7 +106,13 @@ class BaseSunspecDevice {
|
|
|
100
106
|
dataBus;
|
|
101
107
|
retryManager;
|
|
102
108
|
consecutiveReconnectFailures = 0;
|
|
103
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Prefix used when persisting calibration snapshots via the library's
|
|
111
|
+
* {@link SnapshotService}. Kept identical to the key the SDK used before
|
|
112
|
+
* the migration to `@enyo-energy/appliance-calibration` so existing
|
|
113
|
+
* snapshots stored by older builds are still picked up on startup.
|
|
114
|
+
*/
|
|
115
|
+
static CALIBRATION_SNAPSHOT_KEY_PREFIX = "sunspec-calibration-snapshot-";
|
|
104
116
|
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, retryConfig, appliance, useTls) {
|
|
105
117
|
this.energyApp = energyApp;
|
|
106
118
|
this.name = name;
|
|
@@ -255,25 +267,18 @@ class BaseSunspecDevice {
|
|
|
255
267
|
this.dataBus.sendMessage([ackMessage]);
|
|
256
268
|
}
|
|
257
269
|
/**
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
270
|
+
* Build a typed {@link SnapshotService} bound to this appliance's calibration storage,
|
|
271
|
+
* using the legacy SDK storage key prefix so prior installs upgrade seamlessly. Returns
|
|
272
|
+
* undefined if the appliance has not been registered yet.
|
|
261
273
|
*/
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
return;
|
|
274
|
+
buildSnapshotService(onRestore) {
|
|
275
|
+
if (!this.applianceId) {
|
|
276
|
+
return undefined;
|
|
265
277
|
}
|
|
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.
|
|
278
|
+
const storage = new sunspec_calibration_storage_js_1.SunspecCalibrationStorage(this.energyApp.useStorage());
|
|
279
|
+
return new appliance_calibration_1.SnapshotService(storage, this.applianceId, onRestore, {
|
|
280
|
+
storageKeyPrefix: BaseSunspecDevice.CALIBRATION_SNAPSHOT_KEY_PREFIX,
|
|
281
|
+
});
|
|
277
282
|
}
|
|
278
283
|
}
|
|
279
284
|
exports.BaseSunspecDevice = BaseSunspecDevice;
|
|
@@ -286,6 +291,7 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
286
291
|
static CONNECTION_FAULT_THRESHOLD = 3;
|
|
287
292
|
storage;
|
|
288
293
|
errorState = { activeCodes: [], lastStatus: 'healthy' };
|
|
294
|
+
snapshotService;
|
|
289
295
|
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig, appliance, useTls) {
|
|
290
296
|
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig, appliance, useTls);
|
|
291
297
|
this.capabilities = capabilities;
|
|
@@ -698,10 +704,10 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
698
704
|
if (this.dataBusListenerId) {
|
|
699
705
|
return;
|
|
700
706
|
}
|
|
701
|
-
// Fire and forget —
|
|
707
|
+
// Fire and forget — snapshot service hydration must not block listener setup.
|
|
702
708
|
// Listener registration before await ensures we don't miss messages that arrive
|
|
703
|
-
// during initial load.
|
|
704
|
-
void this.
|
|
709
|
+
// during the initial load.
|
|
710
|
+
void this.initSnapshotService();
|
|
705
711
|
this.dataBus = this.energyApp.useDataBus();
|
|
706
712
|
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
707
713
|
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetInverterFeedInLimitV1,
|
|
@@ -762,22 +768,39 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
762
768
|
}
|
|
763
769
|
// setFeedInLimit writes WMaxLimPct + WMaxLim_Ena (see SunspecModbusClient.setFeedInLimit).
|
|
764
770
|
// Record both so an active calibration can roll them back on stop.
|
|
765
|
-
await this.
|
|
771
|
+
await this.snapshotService?.recordModification(['WMaxLimPct', 'WMaxLim_Ena']);
|
|
766
772
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
767
773
|
}
|
|
774
|
+
/**
|
|
775
|
+
* Lazily construct the per-appliance {@link SnapshotService}. Reloads any
|
|
776
|
+
* persisted snapshot from storage so an in-flight calibration survives
|
|
777
|
+
* process restarts. Idempotent.
|
|
778
|
+
*/
|
|
779
|
+
async initSnapshotService() {
|
|
780
|
+
if (this.snapshotService) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const service = this.buildSnapshotService((snapshot, reason) => this.restoreInverterSnapshot(snapshot, reason));
|
|
784
|
+
if (!service) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
this.snapshotService = service;
|
|
788
|
+
await this.snapshotService.initialize();
|
|
789
|
+
console.log(`Inverter ${this.applianceId}: snapshot service initialized`);
|
|
790
|
+
}
|
|
768
791
|
async handleStartCalibration(msg) {
|
|
769
792
|
if (!this.isConnected() || !this.applianceId) {
|
|
770
793
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
|
|
771
794
|
return;
|
|
772
795
|
}
|
|
773
|
-
if (!this.
|
|
774
|
-
await this.
|
|
796
|
+
if (!this.snapshotService) {
|
|
797
|
+
await this.initSnapshotService();
|
|
775
798
|
}
|
|
776
|
-
if (!this.
|
|
777
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, '
|
|
799
|
+
if (!this.snapshotService) {
|
|
800
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Snapshot service unavailable');
|
|
778
801
|
return;
|
|
779
802
|
}
|
|
780
|
-
if (this.
|
|
803
|
+
if (this.snapshotService.isCalibrating()) {
|
|
781
804
|
console.log(`Inverter ${this.applianceId}: calibration already active — ack idempotently`);
|
|
782
805
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
783
806
|
return;
|
|
@@ -789,11 +812,7 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
789
812
|
return;
|
|
790
813
|
}
|
|
791
814
|
try {
|
|
792
|
-
await this.
|
|
793
|
-
deviceType: 'inverter',
|
|
794
|
-
unitId: this.unitId,
|
|
795
|
-
inverterControls: controls,
|
|
796
|
-
});
|
|
815
|
+
await this.snapshotService.startCalibration(controls);
|
|
797
816
|
}
|
|
798
817
|
catch (error) {
|
|
799
818
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to persist snapshot: ${error}`);
|
|
@@ -807,30 +826,23 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
807
826
|
return;
|
|
808
827
|
}
|
|
809
828
|
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
|
-
}
|
|
829
|
+
// SnapshotService.stopCalibration fires `restoreInverterSnapshot` internally with
|
|
830
|
+
// reason="stop" before returning. Any restore failure is logged by the callback
|
|
831
|
+
// (the persisted snapshot is already gone by then — see library notes) so we
|
|
832
|
+
// always ack Accepted from here.
|
|
833
|
+
await this.snapshotService?.stopCalibration();
|
|
823
834
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
824
835
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
836
|
+
/**
|
|
837
|
+
* `onRestore` callback for the inverter's {@link SnapshotService}. Writes only the
|
|
838
|
+
* subset of writable inverter fields that other commands actually touched during the
|
|
839
|
+
* calibration. Catches and logs failures rather than throwing — the snapshot has
|
|
840
|
+
* already been removed from storage by the time this runs, so a throw is unrecoverable
|
|
841
|
+
* and is best logged for operator action.
|
|
842
|
+
*/
|
|
843
|
+
async restoreInverterSnapshot(snapshot, reason) {
|
|
844
|
+
const source = snapshot.payload;
|
|
830
845
|
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
846
|
const WRITABLE_INVERTER_FIELDS = ['Conn', 'WMaxLimPct', 'WMaxLim_Ena', 'OutPFSet', 'OutPFSet_Ena'];
|
|
835
847
|
for (const field of WRITABLE_INVERTER_FIELDS) {
|
|
836
848
|
if (snapshot.modifiedFields.includes(field) && source[field] !== undefined) {
|
|
@@ -842,22 +854,175 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
842
854
|
return;
|
|
843
855
|
}
|
|
844
856
|
console.log(`Inverter ${this.applianceId}: calibration ${reason} — restoring [${Object.keys(partial).join(', ')}]`);
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
857
|
+
try {
|
|
858
|
+
const ok = await this.sunspecClient.writeInverterControls(this.unitId, partial);
|
|
859
|
+
if (!ok) {
|
|
860
|
+
console.error(`Inverter ${this.applianceId}: calibration ${reason} — writeInverterControls returned false`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
catch (error) {
|
|
864
|
+
console.error(`Inverter ${this.applianceId}: calibration ${reason} — writeInverterControls threw: ${error}`);
|
|
848
865
|
}
|
|
849
866
|
}
|
|
850
867
|
}
|
|
851
868
|
exports.SunspecInverter = SunspecInverter;
|
|
869
|
+
/**
|
|
870
|
+
* Pure register-presence detection — exported so it can be unit-tested
|
|
871
|
+
* without the full `SunspecBattery` scaffold. Maps each Model 124 writable
|
|
872
|
+
* register to the `EnyoBatteryFeature` it represents:
|
|
873
|
+
*
|
|
874
|
+
* - `chaGriSet` → `GridCharging` (charge source PV/GRID)
|
|
875
|
+
* - `wChaMax` → `ChargeLimitation` (max charge power)
|
|
876
|
+
* - `outWRte` → `DischargeLimitation` (discharge rate %)
|
|
877
|
+
* - `storCtlMod` → `GridDischarging` (closest signal that external discharge
|
|
878
|
+
* control is available; the inverter decides where it goes)
|
|
879
|
+
*/
|
|
880
|
+
function detectFeaturesFromRegisters(batteryData) {
|
|
881
|
+
const features = [];
|
|
882
|
+
if (batteryData?.chaGriSet !== undefined)
|
|
883
|
+
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.GridCharging);
|
|
884
|
+
if (batteryData?.wChaMax !== undefined)
|
|
885
|
+
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.ChargeLimitation);
|
|
886
|
+
if (batteryData?.outWRte !== undefined)
|
|
887
|
+
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.DischargeLimitation);
|
|
888
|
+
if (batteryData?.storCtlMod !== undefined)
|
|
889
|
+
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.GridDischarging);
|
|
890
|
+
return features;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Apply the per-feature calibration filter to a register-detected set.
|
|
894
|
+
* Exported for unit-test access to the legacy-result fallback branch.
|
|
895
|
+
*
|
|
896
|
+
* The contract:
|
|
897
|
+
* - No `result` or `result.state !== 'calibrated'` → strip every controllable
|
|
898
|
+
* feature (safe fallback while waiting for calibration).
|
|
899
|
+
* - `result.state === 'calibrated'` with a decodable per-feature payload →
|
|
900
|
+
* publish only the controllable features whose probes passed.
|
|
901
|
+
* - `result.state === 'calibrated'` with **no** decodable payload → legacy
|
|
902
|
+
* data (pre-feature-calibrator SDK). The new calibrator only marks
|
|
903
|
+
* `state=calibrated` when at least one probe passes, so an empty decoded
|
|
904
|
+
* set with `state=calibrated` unambiguously signals legacy results. Publish
|
|
905
|
+
* the full detected set to preserve the old all-or-nothing semantics on
|
|
906
|
+
* upgrade.
|
|
907
|
+
*/
|
|
908
|
+
function filterFeaturesByCalibrationResult(detected, result, controllable) {
|
|
909
|
+
if (!result || result.state !== 'calibrated') {
|
|
910
|
+
return detected.filter(f => !controllable.includes(f));
|
|
911
|
+
}
|
|
912
|
+
const passed = (0, sunspec_battery_feature_calibrator_js_1.decodeFeatureResults)(result.notes);
|
|
913
|
+
if (passed.size === 0)
|
|
914
|
+
return detected;
|
|
915
|
+
return detected.filter(f => !controllable.includes(f) || passed.has(f));
|
|
916
|
+
}
|
|
852
917
|
/**
|
|
853
918
|
* Sunspec Battery implementation
|
|
854
919
|
*/
|
|
855
920
|
class SunspecBattery extends BaseSunspecDevice {
|
|
921
|
+
featureMode;
|
|
856
922
|
capabilities;
|
|
857
|
-
|
|
923
|
+
snapshotService;
|
|
924
|
+
calibrationDriver;
|
|
925
|
+
batteryCalibrator;
|
|
926
|
+
calibrationResultStore;
|
|
927
|
+
scheduleHandler;
|
|
928
|
+
/**
|
|
929
|
+
* Battery features that imply outbound control writes from the host action-taker.
|
|
930
|
+
* These are stripped from `appliance.battery.features` until the calibration result
|
|
931
|
+
* store says this battery has been successfully calibrated — see
|
|
932
|
+
* {@link computeAdvertisedFeatures}.
|
|
933
|
+
*/
|
|
934
|
+
static CONTROLLABLE_FEATURES = [
|
|
935
|
+
enyo_battery_appliance_js_1.EnyoBatteryFeature.GridCharging,
|
|
936
|
+
enyo_battery_appliance_js_1.EnyoBatteryFeature.GridDischarging,
|
|
937
|
+
enyo_battery_appliance_js_1.EnyoBatteryFeature.ChargeLimitation,
|
|
938
|
+
enyo_battery_appliance_js_1.EnyoBatteryFeature.DischargeLimitation,
|
|
939
|
+
];
|
|
940
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, featureMode, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig, appliance, useTls) {
|
|
858
941
|
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig, appliance, useTls);
|
|
942
|
+
this.featureMode = featureMode;
|
|
859
943
|
this.capabilities = capabilities;
|
|
860
944
|
}
|
|
945
|
+
/**
|
|
946
|
+
* Wire the battery into `@enyo-energy/appliance-calibration`'s
|
|
947
|
+
* `BatteryCalibrator` test-charge flow. Call once after {@link connect}.
|
|
948
|
+
*
|
|
949
|
+
* Consumers own the `CalibrationResultStore` (shared across appliances so
|
|
950
|
+
* the persisted result map doesn't get clobbered) and the
|
|
951
|
+
* `CalibrationTrigger` that drives `runCalibration()` on whatever schedule
|
|
952
|
+
* makes sense for the host. Pass overrides for the calibrator's defaults
|
|
953
|
+
* (`testPowerW`, response thresholds, etc.) via `opts.config`.
|
|
954
|
+
*/
|
|
955
|
+
configureCalibration(opts) {
|
|
956
|
+
if (this.featureMode.kind !== sunspec_interfaces_js_1.SunspecBatteryFeatureModeKind.CalibrationBased) {
|
|
957
|
+
throw new Error(`SunspecBattery.configureCalibration: featureMode is '${this.featureMode.kind}', ` +
|
|
958
|
+
`but calibration is only valid in '${sunspec_interfaces_js_1.SunspecBatteryFeatureModeKind.CalibrationBased}' mode. ` +
|
|
959
|
+
`Pass { kind: SunspecBatteryFeatureModeKind.CalibrationBased } to the constructor.`);
|
|
960
|
+
}
|
|
961
|
+
if (this.batteryCalibrator) {
|
|
962
|
+
return this.batteryCalibrator;
|
|
963
|
+
}
|
|
964
|
+
if (!this.applianceId) {
|
|
965
|
+
throw new Error("SunspecBattery.configureCalibration: connect() must complete first (applianceId required)");
|
|
966
|
+
}
|
|
967
|
+
if (!this.snapshotService) {
|
|
968
|
+
throw new Error("SunspecBattery.configureCalibration: snapshot service not initialized — call connect() first");
|
|
969
|
+
}
|
|
970
|
+
const driver = new sunspec_battery_calibration_driver_js_1.SunspecBatteryCalibrationDriver(this.sunspecClient, this.unitId, this.energyApp.useDataBus());
|
|
971
|
+
this.calibrationDriver = driver;
|
|
972
|
+
this.calibrationResultStore = opts.resultStore;
|
|
973
|
+
this.batteryCalibrator = new sunspec_battery_feature_calibrator_js_1.SunspecBatteryFeatureCalibrator({
|
|
974
|
+
applianceId: this.applianceId,
|
|
975
|
+
driver,
|
|
976
|
+
sunspecClient: this.sunspecClient,
|
|
977
|
+
unitId: this.unitId,
|
|
978
|
+
readBatteryData: () => this.sunspecClient.readBatteryData(this.unitId),
|
|
979
|
+
snapshotService: this.snapshotService,
|
|
980
|
+
resultStore: opts.resultStore,
|
|
981
|
+
config: opts.config,
|
|
982
|
+
});
|
|
983
|
+
return this.batteryCalibrator;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Detect which battery features the device's registers expose. This is the
|
|
987
|
+
* raw, register-only view; the {@link featureMode} configured at
|
|
988
|
+
* construction decides what is actually published — see
|
|
989
|
+
* {@link resolveAdvertisedFeatures}.
|
|
990
|
+
*/
|
|
991
|
+
detectFromRegisters(batteryData) {
|
|
992
|
+
return detectFeaturesFromRegisters(batteryData);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Resolve `appliance.battery.features` for the configured mode. Called on
|
|
996
|
+
* every connect() and readData() cycle so that, in calibration-based mode,
|
|
997
|
+
* the controllable features appear as soon as the calibrator flips
|
|
998
|
+
* `isCalibrated` (no synchronous push).
|
|
999
|
+
*/
|
|
1000
|
+
resolveAdvertisedFeatures(batteryData) {
|
|
1001
|
+
const allow = this.featureMode.allowedFeatures;
|
|
1002
|
+
const intersect = (xs) => allow ? xs.filter(f => allow.includes(f)) : xs;
|
|
1003
|
+
switch (this.featureMode.kind) {
|
|
1004
|
+
case sunspec_interfaces_js_1.SunspecBatteryFeatureModeKind.Disabled:
|
|
1005
|
+
return allow ? [...allow] : [];
|
|
1006
|
+
case sunspec_interfaces_js_1.SunspecBatteryFeatureModeKind.RegisterBased:
|
|
1007
|
+
return intersect(this.detectFromRegisters(batteryData));
|
|
1008
|
+
case sunspec_interfaces_js_1.SunspecBatteryFeatureModeKind.CalibrationBased: {
|
|
1009
|
+
const detected = intersect(this.detectFromRegisters(batteryData));
|
|
1010
|
+
const result = this.applianceId
|
|
1011
|
+
? this.calibrationResultStore?.getResult(this.applianceId)
|
|
1012
|
+
: undefined;
|
|
1013
|
+
return filterFeaturesByCalibrationResult(detected, result, SunspecBattery.CONTROLLABLE_FEATURES);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Returns the per-battery `BatteryCalibrator` created by
|
|
1019
|
+
* {@link configureCalibration}, or `undefined` if calibration was never
|
|
1020
|
+
* configured. Consumers register the returned instance with their own
|
|
1021
|
+
* `CalibrationTrigger`.
|
|
1022
|
+
*/
|
|
1023
|
+
getBatteryCalibrator() {
|
|
1024
|
+
return this.batteryCalibrator;
|
|
1025
|
+
}
|
|
861
1026
|
/**
|
|
862
1027
|
* Connect to the battery and create/update the appliance
|
|
863
1028
|
*/
|
|
@@ -874,13 +1039,7 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
874
1039
|
const commonData = await this.sunspecClient.readCommonBlock(this.unitId);
|
|
875
1040
|
const batteryData = await this.sunspecClient.readBatteryData(this.unitId);
|
|
876
1041
|
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
|
-
}
|
|
1042
|
+
const features = this.resolveAdvertisedFeatures(batteryData);
|
|
884
1043
|
const activeChargeLimitW = this.computeActiveChargeLimitW(batteryData);
|
|
885
1044
|
const activeDischargeLimitW = this.computeActiveDischargeLimitW(batteryData);
|
|
886
1045
|
// Create or update appliance (skip if an existing appliance was provided)
|
|
@@ -947,6 +1106,11 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
947
1106
|
}
|
|
948
1107
|
async disconnect() {
|
|
949
1108
|
this.stopDataBusListening();
|
|
1109
|
+
if (this.calibrationDriver) {
|
|
1110
|
+
this.calibrationDriver.stop();
|
|
1111
|
+
this.calibrationDriver = undefined;
|
|
1112
|
+
this.batteryCalibrator = undefined;
|
|
1113
|
+
}
|
|
950
1114
|
if (this.applianceId) {
|
|
951
1115
|
try {
|
|
952
1116
|
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
|
|
@@ -1015,6 +1179,11 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1015
1179
|
else if (!advancedBatteryModel) {
|
|
1016
1180
|
batteryPowerW = mpptBatteryPowerW;
|
|
1017
1181
|
}
|
|
1182
|
+
// Feed the calibration driver's synchronous power cache. No-op when calibration
|
|
1183
|
+
// was never configured.
|
|
1184
|
+
if (batteryPowerW !== undefined && this.calibrationDriver) {
|
|
1185
|
+
this.calibrationDriver.updateBatteryPowerCache(batteryPowerW);
|
|
1186
|
+
}
|
|
1018
1187
|
const batteryMessage = {
|
|
1019
1188
|
id: (0, node_crypto_1.randomUUID)(),
|
|
1020
1189
|
message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.BatteryValuesUpdateV1,
|
|
@@ -1038,6 +1207,9 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1038
1207
|
if (this.applianceId) {
|
|
1039
1208
|
const appliance = await this.applianceManager.findApplianceById(this.applianceId);
|
|
1040
1209
|
const storageMode = this.determineStorageMode(batteryData);
|
|
1210
|
+
// Republish features each cycle so the controllable set appears as soon
|
|
1211
|
+
// as the calibrator flips isCalibrated — see resolveAdvertisedFeatures.
|
|
1212
|
+
const features = this.resolveAdvertisedFeatures(batteryData);
|
|
1041
1213
|
await this.applianceManager.updateAppliance(this.applianceId, {
|
|
1042
1214
|
battery: {
|
|
1043
1215
|
...appliance?.battery,
|
|
@@ -1046,6 +1218,7 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1046
1218
|
maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
|
|
1047
1219
|
activeChargeLimitW: this.computeActiveChargeLimitW(batteryData),
|
|
1048
1220
|
activeDischargeLimitW: this.computeActiveDischargeLimitW(batteryData),
|
|
1221
|
+
features,
|
|
1049
1222
|
gridChargingEnabled: batteryData?.chaGriSet === 1
|
|
1050
1223
|
}
|
|
1051
1224
|
});
|
|
@@ -1291,17 +1464,27 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1291
1464
|
if (this.dataBusListenerId) {
|
|
1292
1465
|
return;
|
|
1293
1466
|
}
|
|
1294
|
-
// Fire and forget —
|
|
1295
|
-
void this.
|
|
1467
|
+
// Fire and forget — snapshot service hydration must not block listener setup.
|
|
1468
|
+
void this.initSnapshotService();
|
|
1296
1469
|
this.dataBus = this.energyApp.useDataBus();
|
|
1297
1470
|
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,
|
|
1471
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageScheduleV1,
|
|
1302
1472
|
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1,
|
|
1303
1473
|
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1,
|
|
1304
1474
|
], (entry) => this.handleStorageCommand(entry));
|
|
1475
|
+
if (!this.applianceId) {
|
|
1476
|
+
throw new Error("SunspecBattery.startDataBusListening: applianceId required — call connect() first.");
|
|
1477
|
+
}
|
|
1478
|
+
this.scheduleHandler = new sunspec_battery_schedule_handler_js_1.SunspecBatteryScheduleHandler({
|
|
1479
|
+
dataBus: this.dataBus,
|
|
1480
|
+
interval: this.energyApp.useInterval(),
|
|
1481
|
+
storage: this.energyApp.useStorage(),
|
|
1482
|
+
applianceId: this.applianceId,
|
|
1483
|
+
sunspecClient: this.sunspecClient,
|
|
1484
|
+
unitId: this.unitId,
|
|
1485
|
+
getSnapshotService: () => this.snapshotService,
|
|
1486
|
+
onErrorCallback: (err) => console.warn(`Battery ${this.applianceId}: schedule handler rejected input: ${err.message}`),
|
|
1487
|
+
});
|
|
1305
1488
|
console.log(`Battery ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
1306
1489
|
}
|
|
1307
1490
|
/**
|
|
@@ -1314,6 +1497,7 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1314
1497
|
}
|
|
1315
1498
|
this.dataBusListenerId = undefined;
|
|
1316
1499
|
this.dataBus = undefined;
|
|
1500
|
+
this.scheduleHandler?.dispose();
|
|
1317
1501
|
}
|
|
1318
1502
|
handleStorageCommand(entry) {
|
|
1319
1503
|
if (entry.applianceId !== this.applianceId) {
|
|
@@ -1322,17 +1506,8 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1322
1506
|
void (async () => {
|
|
1323
1507
|
try {
|
|
1324
1508
|
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);
|
|
1509
|
+
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageScheduleV1:
|
|
1510
|
+
this.handleSetStorageScheduleAck(entry);
|
|
1336
1511
|
break;
|
|
1337
1512
|
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1:
|
|
1338
1513
|
await this.handleStartCalibration(entry);
|
|
@@ -1347,132 +1522,51 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1347
1522
|
}
|
|
1348
1523
|
})();
|
|
1349
1524
|
}
|
|
1350
|
-
|
|
1525
|
+
/**
|
|
1526
|
+
* Acknowledge a SetStorageScheduleV1 message. The actual schedule application
|
|
1527
|
+
* is owned by `this.scheduleHandler` (subscribed independently via its own
|
|
1528
|
+
* data-bus listener registered in its constructor). The ack reports "received
|
|
1529
|
+
* & queued" rather than "applied" — schedule entries play out over time, and
|
|
1530
|
+
* per-entry write failures land in `console.error` via the handler's
|
|
1531
|
+
* onChange wrapper.
|
|
1532
|
+
*/
|
|
1533
|
+
handleSetStorageScheduleAck(msg) {
|
|
1351
1534
|
if (!this.isConnected() || !this.applianceId) {
|
|
1352
1535
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, "Not connected");
|
|
1353
1536
|
return;
|
|
1354
1537
|
}
|
|
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
1538
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1437
1539
|
}
|
|
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');
|
|
1540
|
+
/**
|
|
1541
|
+
* Lazily construct the per-appliance {@link SnapshotService}. Reloads any
|
|
1542
|
+
* persisted snapshot from storage so an in-flight calibration survives
|
|
1543
|
+
* process restarts. Idempotent.
|
|
1544
|
+
*/
|
|
1545
|
+
async initSnapshotService() {
|
|
1546
|
+
if (this.snapshotService) {
|
|
1449
1547
|
return;
|
|
1450
1548
|
}
|
|
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');
|
|
1549
|
+
const service = this.buildSnapshotService((snapshot, reason) => this.restoreBatterySnapshot(snapshot, reason));
|
|
1550
|
+
if (!service) {
|
|
1458
1551
|
return;
|
|
1459
1552
|
}
|
|
1460
|
-
|
|
1461
|
-
this.
|
|
1553
|
+
this.snapshotService = service;
|
|
1554
|
+
await this.snapshotService.initialize();
|
|
1555
|
+
console.log(`Battery ${this.applianceId}: snapshot service initialized`);
|
|
1462
1556
|
}
|
|
1463
1557
|
async handleStartCalibration(msg) {
|
|
1464
1558
|
if (!this.isConnected() || !this.applianceId) {
|
|
1465
1559
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
|
|
1466
1560
|
return;
|
|
1467
1561
|
}
|
|
1468
|
-
if (!this.
|
|
1469
|
-
await this.
|
|
1562
|
+
if (!this.snapshotService) {
|
|
1563
|
+
await this.initSnapshotService();
|
|
1470
1564
|
}
|
|
1471
|
-
if (!this.
|
|
1472
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, '
|
|
1565
|
+
if (!this.snapshotService) {
|
|
1566
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Snapshot service unavailable');
|
|
1473
1567
|
return;
|
|
1474
1568
|
}
|
|
1475
|
-
if (this.
|
|
1569
|
+
if (this.snapshotService.isCalibrating()) {
|
|
1476
1570
|
console.log(`Battery ${this.applianceId}: calibration already active — ack idempotently`);
|
|
1477
1571
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1478
1572
|
return;
|
|
@@ -1484,11 +1578,7 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1484
1578
|
return;
|
|
1485
1579
|
}
|
|
1486
1580
|
try {
|
|
1487
|
-
await this.
|
|
1488
|
-
deviceType: 'battery',
|
|
1489
|
-
unitId: this.unitId,
|
|
1490
|
-
batteryControls: controls,
|
|
1491
|
-
});
|
|
1581
|
+
await this.snapshotService.startCalibration(controls);
|
|
1492
1582
|
}
|
|
1493
1583
|
catch (error) {
|
|
1494
1584
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to persist snapshot: ${error}`);
|
|
@@ -1502,27 +1592,22 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1502
1592
|
return;
|
|
1503
1593
|
}
|
|
1504
1594
|
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
|
-
}
|
|
1595
|
+
// SnapshotService.stopCalibration fires `restoreBatterySnapshot` internally
|
|
1596
|
+
// with reason="stop" before returning. Failures from the restore callback are
|
|
1597
|
+
// logged inside the callback (the persisted snapshot is already gone — see
|
|
1598
|
+
// library notes) so we always ack Accepted here.
|
|
1599
|
+
await this.snapshotService?.stopCalibration();
|
|
1517
1600
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1518
1601
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1602
|
+
/**
|
|
1603
|
+
* `onRestore` callback for the battery's {@link SnapshotService}. Writes only the
|
|
1604
|
+
* subset of writable battery fields touched during the calibration. Errors are
|
|
1605
|
+
* caught and logged — by the time this fires the snapshot is gone from storage,
|
|
1606
|
+
* so a throw would just propagate into the void.
|
|
1607
|
+
*/
|
|
1608
|
+
async restoreBatterySnapshot(snapshot, reason) {
|
|
1609
|
+
const source = snapshot.payload;
|
|
1524
1610
|
const partial = {};
|
|
1525
|
-
// Whitelist of battery fields that writeBatteryControls actually writes (Model 124).
|
|
1526
1611
|
const WRITABLE_BATTERY_FIELDS = ['storCtlMod', 'chaGriSet', 'wChaMax', 'inWRte', 'outWRte', 'minRsvPct'];
|
|
1527
1612
|
for (const field of WRITABLE_BATTERY_FIELDS) {
|
|
1528
1613
|
if (snapshot.modifiedFields.includes(field) && source[field] !== undefined) {
|
|
@@ -1534,9 +1619,14 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1534
1619
|
return;
|
|
1535
1620
|
}
|
|
1536
1621
|
console.log(`Battery ${this.applianceId}: calibration ${reason} — restoring [${Object.keys(partial).join(', ')}]`);
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1622
|
+
try {
|
|
1623
|
+
const ok = await this.sunspecClient.writeBatteryControls(this.unitId, partial);
|
|
1624
|
+
if (!ok) {
|
|
1625
|
+
console.error(`Battery ${this.applianceId}: calibration ${reason} — writeBatteryControls returned false`);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
catch (error) {
|
|
1629
|
+
console.error(`Battery ${this.applianceId}: calibration ${reason} — writeBatteryControls threw: ${error}`);
|
|
1540
1630
|
}
|
|
1541
1631
|
}
|
|
1542
1632
|
}
|