@enyo-energy/sunspec-sdk 0.0.70 → 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/calibration-snapshot-service.d.ts +67 -0
- package/dist/calibration-snapshot-service.js +160 -0
- package/dist/cjs/calibration-snapshot-service.cjs +164 -0
- package/dist/cjs/calibration-snapshot-service.d.cts +67 -0
- package/dist/cjs/index.cjs +30 -1
- package/dist/cjs/index.d.cts +6 -0
- 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 +407 -104
- package/dist/cjs/sunspec-devices.d.cts +112 -6
- 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 -0
- package/dist/index.js +12 -0
- 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 +112 -6
- package/dist/sunspec-devices.js +408 -105
- 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,6 +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 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");
|
|
9
14
|
const enyo_source_enum_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-source.enum.js");
|
|
10
15
|
const enyo_meter_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-meter-appliance.js");
|
|
11
16
|
const enyo_battery_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js");
|
|
@@ -99,6 +104,13 @@ class BaseSunspecDevice {
|
|
|
99
104
|
dataBus;
|
|
100
105
|
retryManager;
|
|
101
106
|
consecutiveReconnectFailures = 0;
|
|
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-";
|
|
102
114
|
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, retryConfig, appliance, useTls) {
|
|
103
115
|
this.energyApp = energyApp;
|
|
104
116
|
this.name = name;
|
|
@@ -252,6 +264,20 @@ class BaseSunspecDevice {
|
|
|
252
264
|
console.log(`${this.constructor.name} ${this.applianceId}: sending ${answer} for ${acknowledgeMessage} (messageId=${messageId}${rejectionReason ? `, reason=${rejectionReason}` : ''})`);
|
|
253
265
|
this.dataBus.sendMessage([ackMessage]);
|
|
254
266
|
}
|
|
267
|
+
/**
|
|
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.
|
|
271
|
+
*/
|
|
272
|
+
buildSnapshotService(onRestore) {
|
|
273
|
+
if (!this.applianceId) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
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
|
+
});
|
|
280
|
+
}
|
|
255
281
|
}
|
|
256
282
|
exports.BaseSunspecDevice = BaseSunspecDevice;
|
|
257
283
|
/**
|
|
@@ -263,6 +289,7 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
263
289
|
static CONNECTION_FAULT_THRESHOLD = 3;
|
|
264
290
|
storage;
|
|
265
291
|
errorState = { activeCodes: [], lastStatus: 'healthy' };
|
|
292
|
+
snapshotService;
|
|
266
293
|
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig, appliance, useTls) {
|
|
267
294
|
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig, appliance, useTls);
|
|
268
295
|
this.capabilities = capabilities;
|
|
@@ -675,8 +702,16 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
675
702
|
if (this.dataBusListenerId) {
|
|
676
703
|
return;
|
|
677
704
|
}
|
|
705
|
+
// Fire and forget — snapshot service hydration must not block listener setup.
|
|
706
|
+
// Listener registration before await ensures we don't miss messages that arrive
|
|
707
|
+
// during the initial load.
|
|
708
|
+
void this.initSnapshotService();
|
|
678
709
|
this.dataBus = this.energyApp.useDataBus();
|
|
679
|
-
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
710
|
+
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
711
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetInverterFeedInLimitV1,
|
|
712
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1,
|
|
713
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1,
|
|
714
|
+
], (entry) => this.handleInverterCommand(entry));
|
|
680
715
|
console.log(`Inverter ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
681
716
|
}
|
|
682
717
|
/**
|
|
@@ -696,8 +731,16 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
696
731
|
}
|
|
697
732
|
void (async () => {
|
|
698
733
|
try {
|
|
699
|
-
|
|
700
|
-
|
|
734
|
+
switch (entry.message) {
|
|
735
|
+
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetInverterFeedInLimitV1:
|
|
736
|
+
await this.handleSetFeedInLimit(entry);
|
|
737
|
+
break;
|
|
738
|
+
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1:
|
|
739
|
+
await this.handleStartCalibration(entry);
|
|
740
|
+
break;
|
|
741
|
+
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1:
|
|
742
|
+
await this.handleStopCalibration(entry);
|
|
743
|
+
break;
|
|
701
744
|
}
|
|
702
745
|
}
|
|
703
746
|
catch (error) {
|
|
@@ -721,19 +764,230 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
721
764
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set feed-in limit');
|
|
722
765
|
return;
|
|
723
766
|
}
|
|
767
|
+
// setFeedInLimit writes WMaxLimPct + WMaxLim_Ena (see SunspecModbusClient.setFeedInLimit).
|
|
768
|
+
// Record both so an active calibration can roll them back on stop.
|
|
769
|
+
await this.snapshotService?.recordModification(['WMaxLimPct', 'WMaxLim_Ena']);
|
|
770
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
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
|
+
}
|
|
789
|
+
async handleStartCalibration(msg) {
|
|
790
|
+
if (!this.isConnected() || !this.applianceId) {
|
|
791
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (!this.snapshotService) {
|
|
795
|
+
await this.initSnapshotService();
|
|
796
|
+
}
|
|
797
|
+
if (!this.snapshotService) {
|
|
798
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Snapshot service unavailable');
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (this.snapshotService.isCalibrating()) {
|
|
802
|
+
console.log(`Inverter ${this.applianceId}: calibration already active — ack idempotently`);
|
|
803
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
console.log(`Inverter ${this.applianceId}: handling StartCalibrationV1`);
|
|
807
|
+
const controls = await this.sunspecClient.readInverterControls(this.unitId);
|
|
808
|
+
if (!controls) {
|
|
809
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read inverter controls for snapshot');
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
try {
|
|
813
|
+
await this.snapshotService.startCalibration(controls);
|
|
814
|
+
}
|
|
815
|
+
catch (error) {
|
|
816
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to persist snapshot: ${error}`);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
820
|
+
}
|
|
821
|
+
async handleStopCalibration(msg) {
|
|
822
|
+
if (!this.applianceId) {
|
|
823
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not initialized');
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
console.log(`Inverter ${this.applianceId}: handling StopCalibrationV1`);
|
|
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();
|
|
724
832
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
725
833
|
}
|
|
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;
|
|
843
|
+
const partial = {};
|
|
844
|
+
const WRITABLE_INVERTER_FIELDS = ['Conn', 'WMaxLimPct', 'WMaxLim_Ena', 'OutPFSet', 'OutPFSet_Ena'];
|
|
845
|
+
for (const field of WRITABLE_INVERTER_FIELDS) {
|
|
846
|
+
if (snapshot.modifiedFields.includes(field) && source[field] !== undefined) {
|
|
847
|
+
partial[field] = source[field];
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (Object.keys(partial).length === 0) {
|
|
851
|
+
console.log(`Inverter ${this.applianceId}: calibration ${reason} — no modified fields to restore`);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
console.log(`Inverter ${this.applianceId}: calibration ${reason} — restoring [${Object.keys(partial).join(', ')}]`);
|
|
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}`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
726
865
|
}
|
|
727
866
|
exports.SunspecInverter = SunspecInverter;
|
|
728
867
|
/**
|
|
729
868
|
* Sunspec Battery implementation
|
|
730
869
|
*/
|
|
731
870
|
class SunspecBattery extends BaseSunspecDevice {
|
|
871
|
+
featureMode;
|
|
732
872
|
capabilities;
|
|
733
|
-
|
|
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) {
|
|
734
891
|
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig, appliance, useTls);
|
|
892
|
+
this.featureMode = featureMode;
|
|
735
893
|
this.capabilities = capabilities;
|
|
736
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
|
+
}
|
|
737
991
|
/**
|
|
738
992
|
* Connect to the battery and create/update the appliance
|
|
739
993
|
*/
|
|
@@ -750,13 +1004,7 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
750
1004
|
const commonData = await this.sunspecClient.readCommonBlock(this.unitId);
|
|
751
1005
|
const batteryData = await this.sunspecClient.readBatteryData(this.unitId);
|
|
752
1006
|
const storageMode = this.determineStorageMode(batteryData);
|
|
753
|
-
const features =
|
|
754
|
-
if (batteryData?.chaGriSet !== undefined) {
|
|
755
|
-
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.GridCharging);
|
|
756
|
-
}
|
|
757
|
-
if (batteryData?.wChaMax !== undefined) {
|
|
758
|
-
features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.ChargeLimitation);
|
|
759
|
-
}
|
|
1007
|
+
const features = this.resolveAdvertisedFeatures(batteryData);
|
|
760
1008
|
const activeChargeLimitW = this.computeActiveChargeLimitW(batteryData);
|
|
761
1009
|
const activeDischargeLimitW = this.computeActiveDischargeLimitW(batteryData);
|
|
762
1010
|
// Create or update appliance (skip if an existing appliance was provided)
|
|
@@ -823,6 +1071,11 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
823
1071
|
}
|
|
824
1072
|
async disconnect() {
|
|
825
1073
|
this.stopDataBusListening();
|
|
1074
|
+
if (this.calibrationDriver) {
|
|
1075
|
+
this.calibrationDriver.stop();
|
|
1076
|
+
this.calibrationDriver = undefined;
|
|
1077
|
+
this.batteryCalibrator = undefined;
|
|
1078
|
+
}
|
|
826
1079
|
if (this.applianceId) {
|
|
827
1080
|
try {
|
|
828
1081
|
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
|
|
@@ -891,6 +1144,11 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
891
1144
|
else if (!advancedBatteryModel) {
|
|
892
1145
|
batteryPowerW = mpptBatteryPowerW;
|
|
893
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
|
+
}
|
|
894
1152
|
const batteryMessage = {
|
|
895
1153
|
id: (0, node_crypto_1.randomUUID)(),
|
|
896
1154
|
message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.BatteryValuesUpdateV1,
|
|
@@ -914,6 +1172,9 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
914
1172
|
if (this.applianceId) {
|
|
915
1173
|
const appliance = await this.applianceManager.findApplianceById(this.applianceId);
|
|
916
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);
|
|
917
1178
|
await this.applianceManager.updateAppliance(this.applianceId, {
|
|
918
1179
|
battery: {
|
|
919
1180
|
...appliance?.battery,
|
|
@@ -922,6 +1183,7 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
922
1183
|
maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
|
|
923
1184
|
activeChargeLimitW: this.computeActiveChargeLimitW(batteryData),
|
|
924
1185
|
activeDischargeLimitW: this.computeActiveDischargeLimitW(batteryData),
|
|
1186
|
+
features,
|
|
925
1187
|
gridChargingEnabled: batteryData?.chaGriSet === 1
|
|
926
1188
|
}
|
|
927
1189
|
});
|
|
@@ -1167,13 +1429,27 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1167
1429
|
if (this.dataBusListenerId) {
|
|
1168
1430
|
return;
|
|
1169
1431
|
}
|
|
1432
|
+
// Fire and forget — snapshot service hydration must not block listener setup.
|
|
1433
|
+
void this.initSnapshotService();
|
|
1170
1434
|
this.dataBus = this.energyApp.useDataBus();
|
|
1171
1435
|
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
1172
|
-
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.
|
|
1173
|
-
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.
|
|
1174
|
-
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.
|
|
1175
|
-
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageChargeLimitV1
|
|
1436
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageScheduleV1,
|
|
1437
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1,
|
|
1438
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1,
|
|
1176
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
|
+
});
|
|
1177
1453
|
console.log(`Battery ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
1178
1454
|
}
|
|
1179
1455
|
/**
|
|
@@ -1186,6 +1462,7 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1186
1462
|
}
|
|
1187
1463
|
this.dataBusListenerId = undefined;
|
|
1188
1464
|
this.dataBus = undefined;
|
|
1465
|
+
this.scheduleHandler?.dispose();
|
|
1189
1466
|
}
|
|
1190
1467
|
handleStorageCommand(entry) {
|
|
1191
1468
|
if (entry.applianceId !== this.applianceId) {
|
|
@@ -1194,17 +1471,14 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1194
1471
|
void (async () => {
|
|
1195
1472
|
try {
|
|
1196
1473
|
switch (entry.message) {
|
|
1197
|
-
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.
|
|
1198
|
-
|
|
1199
|
-
break;
|
|
1200
|
-
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopStorageGridChargeV1:
|
|
1201
|
-
await this.handleStopGridCharge(entry);
|
|
1474
|
+
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageScheduleV1:
|
|
1475
|
+
this.handleSetStorageScheduleAck(entry);
|
|
1202
1476
|
break;
|
|
1203
|
-
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.
|
|
1204
|
-
await this.
|
|
1477
|
+
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1:
|
|
1478
|
+
await this.handleStartCalibration(entry);
|
|
1205
1479
|
break;
|
|
1206
|
-
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.
|
|
1207
|
-
await this.
|
|
1480
|
+
case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1:
|
|
1481
|
+
await this.handleStopCalibration(entry);
|
|
1208
1482
|
break;
|
|
1209
1483
|
}
|
|
1210
1484
|
}
|
|
@@ -1213,114 +1487,112 @@ class SunspecBattery extends BaseSunspecDevice {
|
|
|
1213
1487
|
}
|
|
1214
1488
|
})();
|
|
1215
1489
|
}
|
|
1216
|
-
|
|
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) {
|
|
1217
1499
|
if (!this.isConnected() || !this.applianceId) {
|
|
1218
1500
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, "Not connected");
|
|
1219
1501
|
return;
|
|
1220
1502
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
if (
|
|
1230
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to enable grid charging');
|
|
1231
|
-
return;
|
|
1232
|
-
}
|
|
1233
|
-
// Step 2: Set charging power (Register 2: wChaMax)
|
|
1234
|
-
const step2 = await this.setChargingPower(msg.data.powerLimitW);
|
|
1235
|
-
if (!step2) {
|
|
1236
|
-
// Rollback step 1
|
|
1237
|
-
await this.writeBatteryControls({ chaGriSet: originalChaGriSet });
|
|
1238
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set charging power');
|
|
1503
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1504
|
+
}
|
|
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) {
|
|
1239
1512
|
return;
|
|
1240
1513
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
if (!step3) {
|
|
1244
|
-
// Rollback steps 1+2
|
|
1245
|
-
await this.writeBatteryControls({ chaGriSet: originalChaGriSet, wChaMax: originalWChaMax });
|
|
1246
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set storage mode to charge');
|
|
1514
|
+
const service = this.buildSnapshotService((snapshot, reason) => this.restoreBatterySnapshot(snapshot, reason));
|
|
1515
|
+
if (!service) {
|
|
1247
1516
|
return;
|
|
1248
1517
|
}
|
|
1249
|
-
this.
|
|
1518
|
+
this.snapshotService = service;
|
|
1519
|
+
await this.snapshotService.initialize();
|
|
1520
|
+
console.log(`Battery ${this.applianceId}: snapshot service initialized`);
|
|
1250
1521
|
}
|
|
1251
|
-
async
|
|
1522
|
+
async handleStartCalibration(msg) {
|
|
1252
1523
|
if (!this.isConnected() || !this.applianceId) {
|
|
1253
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.
|
|
1524
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
|
|
1254
1525
|
return;
|
|
1255
1526
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
const controls = await this.getBatteryControls();
|
|
1259
|
-
console.log(`Battery ${this.applianceId}: current state - chaGriSet=${controls?.chaGriSet}, storCtlMod=${controls?.storCtlMod}`);
|
|
1260
|
-
const originalChaGriSet = controls?.chaGriSet;
|
|
1261
|
-
// Step 1: Disable grid charging (Register 17: chaGriSet=PV)
|
|
1262
|
-
const step1 = await this.enableGridCharging(false);
|
|
1263
|
-
if (!step1) {
|
|
1264
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to disable grid charging');
|
|
1265
|
-
return;
|
|
1527
|
+
if (!this.snapshotService) {
|
|
1528
|
+
await this.initSnapshotService();
|
|
1266
1529
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
if (!step2) {
|
|
1270
|
-
// Rollback step 1
|
|
1271
|
-
await this.writeBatteryControls({ chaGriSet: originalChaGriSet });
|
|
1272
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set storage mode to auto');
|
|
1530
|
+
if (!this.snapshotService) {
|
|
1531
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Snapshot service unavailable');
|
|
1273
1532
|
return;
|
|
1274
1533
|
}
|
|
1275
|
-
this.
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
if (!this.isConnected() || !this.applianceId) {
|
|
1279
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported);
|
|
1534
|
+
if (this.snapshotService.isCalibrating()) {
|
|
1535
|
+
console.log(`Battery ${this.applianceId}: calibration already active — ack idempotently`);
|
|
1536
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1280
1537
|
return;
|
|
1281
1538
|
}
|
|
1282
|
-
console.log(`Battery ${this.applianceId}: handling
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
if (!controls?.wChaMax) {
|
|
1287
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read wChaMax for discharge limit conversion');
|
|
1539
|
+
console.log(`Battery ${this.applianceId}: handling StartCalibrationV1`);
|
|
1540
|
+
const controls = await this.sunspecClient.readBatteryControls(this.unitId);
|
|
1541
|
+
if (!controls) {
|
|
1542
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read battery controls for snapshot');
|
|
1288
1543
|
return;
|
|
1289
1544
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
if (!success) {
|
|
1296
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set discharge limit');
|
|
1545
|
+
try {
|
|
1546
|
+
await this.snapshotService.startCalibration(controls);
|
|
1547
|
+
}
|
|
1548
|
+
catch (error) {
|
|
1549
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to persist snapshot: ${error}`);
|
|
1297
1550
|
return;
|
|
1298
1551
|
}
|
|
1299
1552
|
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1300
1553
|
}
|
|
1301
|
-
async
|
|
1302
|
-
if (!this.
|
|
1303
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.
|
|
1554
|
+
async handleStopCalibration(msg) {
|
|
1555
|
+
if (!this.applianceId) {
|
|
1556
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not initialized');
|
|
1304
1557
|
return;
|
|
1305
1558
|
}
|
|
1306
|
-
console.log(`Battery ${this.applianceId}: handling
|
|
1307
|
-
//
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1559
|
+
console.log(`Battery ${this.applianceId}: handling StopCalibrationV1`);
|
|
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();
|
|
1565
|
+
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1566
|
+
}
|
|
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;
|
|
1575
|
+
const partial = {};
|
|
1576
|
+
const WRITABLE_BATTERY_FIELDS = ['storCtlMod', 'chaGriSet', 'wChaMax', 'inWRte', 'outWRte', 'minRsvPct'];
|
|
1577
|
+
for (const field of WRITABLE_BATTERY_FIELDS) {
|
|
1578
|
+
if (snapshot.modifiedFields.includes(field) && source[field] !== undefined) {
|
|
1579
|
+
partial[field] = source[field];
|
|
1580
|
+
}
|
|
1313
1581
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
console.log(`Battery ${this.applianceId}: calculated charge limit: ${chargeLimitPercent.toFixed(1)}% (${msg.data.chargeLimitW}W / ${controls.wChaMax}W)`);
|
|
1317
|
-
// Set charge limit (Register 13: inWRte)
|
|
1318
|
-
const success = await this.writeBatteryControls({ inWRte: chargeLimitPercent });
|
|
1319
|
-
if (!success) {
|
|
1320
|
-
this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set charge limit');
|
|
1582
|
+
if (Object.keys(partial).length === 0) {
|
|
1583
|
+
console.log(`Battery ${this.applianceId}: calibration ${reason} — no modified fields to restore`);
|
|
1321
1584
|
return;
|
|
1322
1585
|
}
|
|
1323
|
-
|
|
1586
|
+
console.log(`Battery ${this.applianceId}: calibration ${reason} — restoring [${Object.keys(partial).join(', ')}]`);
|
|
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}`);
|
|
1595
|
+
}
|
|
1324
1596
|
}
|
|
1325
1597
|
}
|
|
1326
1598
|
exports.SunspecBattery = SunspecBattery;
|
|
@@ -1397,11 +1669,13 @@ class SunspecMeter extends BaseSunspecDevice {
|
|
|
1397
1669
|
console.error(`Failed to update meter appliance: ${error}`);
|
|
1398
1670
|
}
|
|
1399
1671
|
}
|
|
1672
|
+
this.startDataBusListening();
|
|
1400
1673
|
}
|
|
1401
1674
|
/**
|
|
1402
1675
|
* Disconnect from the meter and update appliance state
|
|
1403
1676
|
*/
|
|
1404
1677
|
async disconnect() {
|
|
1678
|
+
this.stopDataBusListening();
|
|
1405
1679
|
if (this.applianceId) {
|
|
1406
1680
|
try {
|
|
1407
1681
|
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
|
|
@@ -1413,6 +1687,35 @@ class SunspecMeter extends BaseSunspecDevice {
|
|
|
1413
1687
|
// Close just this meter's unit; other devices on the same network device stay open.
|
|
1414
1688
|
await this.sunspecClient.disconnectUnit(this.unitId);
|
|
1415
1689
|
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Meter does not implement calibration; it only subscribes to Start/StopCalibrationV1 to
|
|
1692
|
+
* answer NotSupported (per the data-bus contract that every command must be acknowledged).
|
|
1693
|
+
*/
|
|
1694
|
+
startDataBusListening() {
|
|
1695
|
+
if (this.dataBusListenerId) {
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
this.dataBus = this.energyApp.useDataBus();
|
|
1699
|
+
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
1700
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1,
|
|
1701
|
+
enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1,
|
|
1702
|
+
], (entry) => this.handleMeterCommand(entry));
|
|
1703
|
+
console.log(`Meter ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
1704
|
+
}
|
|
1705
|
+
stopDataBusListening() {
|
|
1706
|
+
if (this.dataBusListenerId && this.dataBus) {
|
|
1707
|
+
this.dataBus.unsubscribe(this.dataBusListenerId);
|
|
1708
|
+
console.log(`Meter ${this.applianceId}: stopped data bus listening (listener ${this.dataBusListenerId})`);
|
|
1709
|
+
}
|
|
1710
|
+
this.dataBusListenerId = undefined;
|
|
1711
|
+
this.dataBus = undefined;
|
|
1712
|
+
}
|
|
1713
|
+
handleMeterCommand(entry) {
|
|
1714
|
+
if (entry.applianceId !== this.applianceId) {
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
this.sendCommandAcknowledge(entry.id, entry.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported, 'Meter does not support calibration');
|
|
1718
|
+
}
|
|
1416
1719
|
/**
|
|
1417
1720
|
* Update meter data and return data bus messages
|
|
1418
1721
|
*/
|