@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
package/dist/sunspec-devices.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { SunspecBatteryChargeState, SunspecEnableControl, SunspecInverterCapability, SunspecInverterEvent1, SunspecModelId, SunspecMPPTOperatingState, SunspecStorageMode, SUNSPEC_CONNECTION_LOST_CODE } from "./sunspec-interfaces.js";
|
|
1
|
+
import { SunspecBatteryChargeState, SunspecBatteryFeatureModeKind, SunspecEnableControl, SunspecInverterCapability, SunspecInverterEvent1, SunspecModelId, SunspecMPPTOperatingState, SunspecStorageMode, SUNSPEC_CONNECTION_LOST_CODE } from "./sunspec-interfaces.js";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { EnyoApplianceConnectionType, EnyoApplianceStateEnum, EnyoApplianceStatusEnum, EnyoApplianceTopologyFeatureEnum, EnyoApplianceTypeEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
|
|
4
4
|
import { ConnectionRetryManager } from "./connection-retry-manager.js";
|
|
5
5
|
import { EnyoBatteryStateEnum, EnyoCommandAcknowledgeAnswerEnum, EnyoDataBusMessageEnum, EnyoInverterStateEnum, EnyoStringStateEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
|
|
6
|
+
import { SnapshotService, } from "@enyo-energy/appliance-calibration";
|
|
7
|
+
import { SunspecCalibrationStorage } from "./sunspec-calibration-storage.js";
|
|
8
|
+
import { SunspecBatteryCalibrationDriver } from "./sunspec-battery-calibration-driver.js";
|
|
9
|
+
import { SunspecBatteryScheduleHandler } from "./sunspec-battery-schedule-handler.js";
|
|
10
|
+
import { SunspecBatteryFeatureCalibrator, decodeFeatureResults } from "./sunspec-battery-feature-calibrator.js";
|
|
6
11
|
import { EnyoSourceEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-source.enum.js";
|
|
7
12
|
import { EnyoMeterApplianceAvailableFeaturesEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-meter-appliance.js";
|
|
8
13
|
import { EnyoBatteryFeature, EnyoBatteryStorageMode } from "@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js";
|
|
@@ -96,6 +101,13 @@ export class BaseSunspecDevice {
|
|
|
96
101
|
dataBus;
|
|
97
102
|
retryManager;
|
|
98
103
|
consecutiveReconnectFailures = 0;
|
|
104
|
+
/**
|
|
105
|
+
* Prefix used when persisting calibration snapshots via the library's
|
|
106
|
+
* {@link SnapshotService}. Kept identical to the key the SDK used before
|
|
107
|
+
* the migration to `@enyo-energy/appliance-calibration` so existing
|
|
108
|
+
* snapshots stored by older builds are still picked up on startup.
|
|
109
|
+
*/
|
|
110
|
+
static CALIBRATION_SNAPSHOT_KEY_PREFIX = "sunspec-calibration-snapshot-";
|
|
99
111
|
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, retryConfig, appliance, useTls) {
|
|
100
112
|
this.energyApp = energyApp;
|
|
101
113
|
this.name = name;
|
|
@@ -249,6 +261,20 @@ export class BaseSunspecDevice {
|
|
|
249
261
|
console.log(`${this.constructor.name} ${this.applianceId}: sending ${answer} for ${acknowledgeMessage} (messageId=${messageId}${rejectionReason ? `, reason=${rejectionReason}` : ''})`);
|
|
250
262
|
this.dataBus.sendMessage([ackMessage]);
|
|
251
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Build a typed {@link SnapshotService} bound to this appliance's calibration storage,
|
|
266
|
+
* using the legacy SDK storage key prefix so prior installs upgrade seamlessly. Returns
|
|
267
|
+
* undefined if the appliance has not been registered yet.
|
|
268
|
+
*/
|
|
269
|
+
buildSnapshotService(onRestore) {
|
|
270
|
+
if (!this.applianceId) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
const storage = new SunspecCalibrationStorage(this.energyApp.useStorage());
|
|
274
|
+
return new SnapshotService(storage, this.applianceId, onRestore, {
|
|
275
|
+
storageKeyPrefix: BaseSunspecDevice.CALIBRATION_SNAPSHOT_KEY_PREFIX,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
252
278
|
}
|
|
253
279
|
/**
|
|
254
280
|
* Sunspec Inverter implementation using dynamic model discovery
|
|
@@ -259,6 +285,7 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
259
285
|
static CONNECTION_FAULT_THRESHOLD = 3;
|
|
260
286
|
storage;
|
|
261
287
|
errorState = { activeCodes: [], lastStatus: 'healthy' };
|
|
288
|
+
snapshotService;
|
|
262
289
|
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig, appliance, useTls) {
|
|
263
290
|
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig, appliance, useTls);
|
|
264
291
|
this.capabilities = capabilities;
|
|
@@ -671,8 +698,16 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
671
698
|
if (this.dataBusListenerId) {
|
|
672
699
|
return;
|
|
673
700
|
}
|
|
701
|
+
// Fire and forget — snapshot service hydration must not block listener setup.
|
|
702
|
+
// Listener registration before await ensures we don't miss messages that arrive
|
|
703
|
+
// during the initial load.
|
|
704
|
+
void this.initSnapshotService();
|
|
674
705
|
this.dataBus = this.energyApp.useDataBus();
|
|
675
|
-
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
706
|
+
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
707
|
+
EnyoDataBusMessageEnum.SetInverterFeedInLimitV1,
|
|
708
|
+
EnyoDataBusMessageEnum.StartCalibrationV1,
|
|
709
|
+
EnyoDataBusMessageEnum.StopCalibrationV1,
|
|
710
|
+
], (entry) => this.handleInverterCommand(entry));
|
|
676
711
|
console.log(`Inverter ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
677
712
|
}
|
|
678
713
|
/**
|
|
@@ -692,8 +727,16 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
692
727
|
}
|
|
693
728
|
void (async () => {
|
|
694
729
|
try {
|
|
695
|
-
|
|
696
|
-
|
|
730
|
+
switch (entry.message) {
|
|
731
|
+
case EnyoDataBusMessageEnum.SetInverterFeedInLimitV1:
|
|
732
|
+
await this.handleSetFeedInLimit(entry);
|
|
733
|
+
break;
|
|
734
|
+
case EnyoDataBusMessageEnum.StartCalibrationV1:
|
|
735
|
+
await this.handleStartCalibration(entry);
|
|
736
|
+
break;
|
|
737
|
+
case EnyoDataBusMessageEnum.StopCalibrationV1:
|
|
738
|
+
await this.handleStopCalibration(entry);
|
|
739
|
+
break;
|
|
697
740
|
}
|
|
698
741
|
}
|
|
699
742
|
catch (error) {
|
|
@@ -717,18 +760,229 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
717
760
|
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set feed-in limit');
|
|
718
761
|
return;
|
|
719
762
|
}
|
|
763
|
+
// setFeedInLimit writes WMaxLimPct + WMaxLim_Ena (see SunspecModbusClient.setFeedInLimit).
|
|
764
|
+
// Record both so an active calibration can roll them back on stop.
|
|
765
|
+
await this.snapshotService?.recordModification(['WMaxLimPct', 'WMaxLim_Ena']);
|
|
766
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Lazily construct the per-appliance {@link SnapshotService}. Reloads any
|
|
770
|
+
* persisted snapshot from storage so an in-flight calibration survives
|
|
771
|
+
* process restarts. Idempotent.
|
|
772
|
+
*/
|
|
773
|
+
async initSnapshotService() {
|
|
774
|
+
if (this.snapshotService) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
const service = this.buildSnapshotService((snapshot, reason) => this.restoreInverterSnapshot(snapshot, reason));
|
|
778
|
+
if (!service) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
this.snapshotService = service;
|
|
782
|
+
await this.snapshotService.initialize();
|
|
783
|
+
console.log(`Inverter ${this.applianceId}: snapshot service initialized`);
|
|
784
|
+
}
|
|
785
|
+
async handleStartCalibration(msg) {
|
|
786
|
+
if (!this.isConnected() || !this.applianceId) {
|
|
787
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (!this.snapshotService) {
|
|
791
|
+
await this.initSnapshotService();
|
|
792
|
+
}
|
|
793
|
+
if (!this.snapshotService) {
|
|
794
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Snapshot service unavailable');
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (this.snapshotService.isCalibrating()) {
|
|
798
|
+
console.log(`Inverter ${this.applianceId}: calibration already active — ack idempotently`);
|
|
799
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
console.log(`Inverter ${this.applianceId}: handling StartCalibrationV1`);
|
|
803
|
+
const controls = await this.sunspecClient.readInverterControls(this.unitId);
|
|
804
|
+
if (!controls) {
|
|
805
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read inverter controls for snapshot');
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
try {
|
|
809
|
+
await this.snapshotService.startCalibration(controls);
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to persist snapshot: ${error}`);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
816
|
+
}
|
|
817
|
+
async handleStopCalibration(msg) {
|
|
818
|
+
if (!this.applianceId) {
|
|
819
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not initialized');
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
console.log(`Inverter ${this.applianceId}: handling StopCalibrationV1`);
|
|
823
|
+
// SnapshotService.stopCalibration fires `restoreInverterSnapshot` internally with
|
|
824
|
+
// reason="stop" before returning. Any restore failure is logged by the callback
|
|
825
|
+
// (the persisted snapshot is already gone by then — see library notes) so we
|
|
826
|
+
// always ack Accepted from here.
|
|
827
|
+
await this.snapshotService?.stopCalibration();
|
|
720
828
|
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
721
829
|
}
|
|
830
|
+
/**
|
|
831
|
+
* `onRestore` callback for the inverter's {@link SnapshotService}. Writes only the
|
|
832
|
+
* subset of writable inverter fields that other commands actually touched during the
|
|
833
|
+
* calibration. Catches and logs failures rather than throwing — the snapshot has
|
|
834
|
+
* already been removed from storage by the time this runs, so a throw is unrecoverable
|
|
835
|
+
* and is best logged for operator action.
|
|
836
|
+
*/
|
|
837
|
+
async restoreInverterSnapshot(snapshot, reason) {
|
|
838
|
+
const source = snapshot.payload;
|
|
839
|
+
const partial = {};
|
|
840
|
+
const WRITABLE_INVERTER_FIELDS = ['Conn', 'WMaxLimPct', 'WMaxLim_Ena', 'OutPFSet', 'OutPFSet_Ena'];
|
|
841
|
+
for (const field of WRITABLE_INVERTER_FIELDS) {
|
|
842
|
+
if (snapshot.modifiedFields.includes(field) && source[field] !== undefined) {
|
|
843
|
+
partial[field] = source[field];
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (Object.keys(partial).length === 0) {
|
|
847
|
+
console.log(`Inverter ${this.applianceId}: calibration ${reason} — no modified fields to restore`);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
console.log(`Inverter ${this.applianceId}: calibration ${reason} — restoring [${Object.keys(partial).join(', ')}]`);
|
|
851
|
+
try {
|
|
852
|
+
const ok = await this.sunspecClient.writeInverterControls(this.unitId, partial);
|
|
853
|
+
if (!ok) {
|
|
854
|
+
console.error(`Inverter ${this.applianceId}: calibration ${reason} — writeInverterControls returned false`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
console.error(`Inverter ${this.applianceId}: calibration ${reason} — writeInverterControls threw: ${error}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
722
861
|
}
|
|
723
862
|
/**
|
|
724
863
|
* Sunspec Battery implementation
|
|
725
864
|
*/
|
|
726
865
|
export class SunspecBattery extends BaseSunspecDevice {
|
|
866
|
+
featureMode;
|
|
727
867
|
capabilities;
|
|
728
|
-
|
|
868
|
+
snapshotService;
|
|
869
|
+
calibrationDriver;
|
|
870
|
+
batteryCalibrator;
|
|
871
|
+
calibrationResultStore;
|
|
872
|
+
scheduleHandler;
|
|
873
|
+
/**
|
|
874
|
+
* Battery features that imply outbound control writes from the host action-taker.
|
|
875
|
+
* These are stripped from `appliance.battery.features` until the calibration result
|
|
876
|
+
* store says this battery has been successfully calibrated — see
|
|
877
|
+
* {@link computeAdvertisedFeatures}.
|
|
878
|
+
*/
|
|
879
|
+
static CONTROLLABLE_FEATURES = [
|
|
880
|
+
EnyoBatteryFeature.GridCharging,
|
|
881
|
+
EnyoBatteryFeature.GridDischarging,
|
|
882
|
+
EnyoBatteryFeature.ChargeLimitation,
|
|
883
|
+
EnyoBatteryFeature.DischargeLimitation,
|
|
884
|
+
];
|
|
885
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, featureMode, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig, appliance, useTls) {
|
|
729
886
|
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig, appliance, useTls);
|
|
887
|
+
this.featureMode = featureMode;
|
|
730
888
|
this.capabilities = capabilities;
|
|
731
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* Wire the battery into `@enyo-energy/appliance-calibration`'s
|
|
892
|
+
* `BatteryCalibrator` test-charge flow. Call once after {@link connect}.
|
|
893
|
+
*
|
|
894
|
+
* Consumers own the `CalibrationResultStore` (shared across appliances so
|
|
895
|
+
* the persisted result map doesn't get clobbered) and the
|
|
896
|
+
* `CalibrationTrigger` that drives `runCalibration()` on whatever schedule
|
|
897
|
+
* makes sense for the host. Pass overrides for the calibrator's defaults
|
|
898
|
+
* (`testPowerW`, response thresholds, etc.) via `opts.config`.
|
|
899
|
+
*/
|
|
900
|
+
configureCalibration(opts) {
|
|
901
|
+
if (this.featureMode.kind !== SunspecBatteryFeatureModeKind.CalibrationBased) {
|
|
902
|
+
throw new Error(`SunspecBattery.configureCalibration: featureMode is '${this.featureMode.kind}', ` +
|
|
903
|
+
`but calibration is only valid in '${SunspecBatteryFeatureModeKind.CalibrationBased}' mode. ` +
|
|
904
|
+
`Pass { kind: SunspecBatteryFeatureModeKind.CalibrationBased } to the constructor.`);
|
|
905
|
+
}
|
|
906
|
+
if (this.batteryCalibrator) {
|
|
907
|
+
return this.batteryCalibrator;
|
|
908
|
+
}
|
|
909
|
+
if (!this.applianceId) {
|
|
910
|
+
throw new Error("SunspecBattery.configureCalibration: connect() must complete first (applianceId required)");
|
|
911
|
+
}
|
|
912
|
+
if (!this.snapshotService) {
|
|
913
|
+
throw new Error("SunspecBattery.configureCalibration: snapshot service not initialized — call connect() first");
|
|
914
|
+
}
|
|
915
|
+
const driver = new SunspecBatteryCalibrationDriver(this.sunspecClient, this.unitId, this.energyApp.useDataBus());
|
|
916
|
+
this.calibrationDriver = driver;
|
|
917
|
+
this.calibrationResultStore = opts.resultStore;
|
|
918
|
+
this.batteryCalibrator = new SunspecBatteryFeatureCalibrator({
|
|
919
|
+
applianceId: this.applianceId,
|
|
920
|
+
driver,
|
|
921
|
+
sunspecClient: this.sunspecClient,
|
|
922
|
+
unitId: this.unitId,
|
|
923
|
+
readBatteryData: () => this.sunspecClient.readBatteryData(this.unitId),
|
|
924
|
+
snapshotService: this.snapshotService,
|
|
925
|
+
resultStore: opts.resultStore,
|
|
926
|
+
config: opts.config,
|
|
927
|
+
});
|
|
928
|
+
return this.batteryCalibrator;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Detect which battery features the device's registers expose. This is the
|
|
932
|
+
* raw, register-only view; the {@link featureMode} configured at
|
|
933
|
+
* construction decides what is actually published — see
|
|
934
|
+
* {@link resolveAdvertisedFeatures}.
|
|
935
|
+
*/
|
|
936
|
+
detectFromRegisters(batteryData) {
|
|
937
|
+
const features = [];
|
|
938
|
+
if (batteryData?.chaGriSet !== undefined) {
|
|
939
|
+
features.push(EnyoBatteryFeature.GridCharging);
|
|
940
|
+
}
|
|
941
|
+
if (batteryData?.wChaMax !== undefined) {
|
|
942
|
+
features.push(EnyoBatteryFeature.ChargeLimitation);
|
|
943
|
+
}
|
|
944
|
+
return features;
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Resolve `appliance.battery.features` for the configured mode. Called on
|
|
948
|
+
* every connect() and readData() cycle so that, in calibration-based mode,
|
|
949
|
+
* the controllable features appear as soon as the calibrator flips
|
|
950
|
+
* `isCalibrated` (no synchronous push).
|
|
951
|
+
*/
|
|
952
|
+
resolveAdvertisedFeatures(batteryData) {
|
|
953
|
+
const allow = this.featureMode.allowedFeatures;
|
|
954
|
+
const intersect = (xs) => allow ? xs.filter(f => allow.includes(f)) : xs;
|
|
955
|
+
switch (this.featureMode.kind) {
|
|
956
|
+
case SunspecBatteryFeatureModeKind.Disabled:
|
|
957
|
+
return allow ? [...allow] : [];
|
|
958
|
+
case SunspecBatteryFeatureModeKind.RegisterBased:
|
|
959
|
+
return intersect(this.detectFromRegisters(batteryData));
|
|
960
|
+
case SunspecBatteryFeatureModeKind.CalibrationBased: {
|
|
961
|
+
const detected = intersect(this.detectFromRegisters(batteryData));
|
|
962
|
+
if (!this.applianceId || !this.calibrationResultStore) {
|
|
963
|
+
return detected.filter(f => !SunspecBattery.CONTROLLABLE_FEATURES.includes(f));
|
|
964
|
+
}
|
|
965
|
+
const result = this.calibrationResultStore.getResult(this.applianceId);
|
|
966
|
+
if (!result || result.state !== 'calibrated') {
|
|
967
|
+
return detected.filter(f => !SunspecBattery.CONTROLLABLE_FEATURES.includes(f));
|
|
968
|
+
}
|
|
969
|
+
// Per-feature gating: only publish controllable features whose
|
|
970
|
+
// probe passed during the last calibration. See
|
|
971
|
+
// `decodeFeatureResults` and `SunspecBatteryFeatureCalibrator`.
|
|
972
|
+
const passed = decodeFeatureResults(result.notes);
|
|
973
|
+
return detected.filter(f => !SunspecBattery.CONTROLLABLE_FEATURES.includes(f) || passed.has(f));
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Returns the per-battery `BatteryCalibrator` created by
|
|
979
|
+
* {@link configureCalibration}, or `undefined` if calibration was never
|
|
980
|
+
* configured. Consumers register the returned instance with their own
|
|
981
|
+
* `CalibrationTrigger`.
|
|
982
|
+
*/
|
|
983
|
+
getBatteryCalibrator() {
|
|
984
|
+
return this.batteryCalibrator;
|
|
985
|
+
}
|
|
732
986
|
/**
|
|
733
987
|
* Connect to the battery and create/update the appliance
|
|
734
988
|
*/
|
|
@@ -745,13 +999,7 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
745
999
|
const commonData = await this.sunspecClient.readCommonBlock(this.unitId);
|
|
746
1000
|
const batteryData = await this.sunspecClient.readBatteryData(this.unitId);
|
|
747
1001
|
const storageMode = this.determineStorageMode(batteryData);
|
|
748
|
-
const features =
|
|
749
|
-
if (batteryData?.chaGriSet !== undefined) {
|
|
750
|
-
features.push(EnyoBatteryFeature.GridCharging);
|
|
751
|
-
}
|
|
752
|
-
if (batteryData?.wChaMax !== undefined) {
|
|
753
|
-
features.push(EnyoBatteryFeature.ChargeLimitation);
|
|
754
|
-
}
|
|
1002
|
+
const features = this.resolveAdvertisedFeatures(batteryData);
|
|
755
1003
|
const activeChargeLimitW = this.computeActiveChargeLimitW(batteryData);
|
|
756
1004
|
const activeDischargeLimitW = this.computeActiveDischargeLimitW(batteryData);
|
|
757
1005
|
// Create or update appliance (skip if an existing appliance was provided)
|
|
@@ -818,6 +1066,11 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
818
1066
|
}
|
|
819
1067
|
async disconnect() {
|
|
820
1068
|
this.stopDataBusListening();
|
|
1069
|
+
if (this.calibrationDriver) {
|
|
1070
|
+
this.calibrationDriver.stop();
|
|
1071
|
+
this.calibrationDriver = undefined;
|
|
1072
|
+
this.batteryCalibrator = undefined;
|
|
1073
|
+
}
|
|
821
1074
|
if (this.applianceId) {
|
|
822
1075
|
try {
|
|
823
1076
|
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
|
|
@@ -886,6 +1139,11 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
886
1139
|
else if (!advancedBatteryModel) {
|
|
887
1140
|
batteryPowerW = mpptBatteryPowerW;
|
|
888
1141
|
}
|
|
1142
|
+
// Feed the calibration driver's synchronous power cache. No-op when calibration
|
|
1143
|
+
// was never configured.
|
|
1144
|
+
if (batteryPowerW !== undefined && this.calibrationDriver) {
|
|
1145
|
+
this.calibrationDriver.updateBatteryPowerCache(batteryPowerW);
|
|
1146
|
+
}
|
|
889
1147
|
const batteryMessage = {
|
|
890
1148
|
id: randomUUID(),
|
|
891
1149
|
message: EnyoDataBusMessageEnum.BatteryValuesUpdateV1,
|
|
@@ -909,6 +1167,9 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
909
1167
|
if (this.applianceId) {
|
|
910
1168
|
const appliance = await this.applianceManager.findApplianceById(this.applianceId);
|
|
911
1169
|
const storageMode = this.determineStorageMode(batteryData);
|
|
1170
|
+
// Republish features each cycle so the controllable set appears as soon
|
|
1171
|
+
// as the calibrator flips isCalibrated — see resolveAdvertisedFeatures.
|
|
1172
|
+
const features = this.resolveAdvertisedFeatures(batteryData);
|
|
912
1173
|
await this.applianceManager.updateAppliance(this.applianceId, {
|
|
913
1174
|
battery: {
|
|
914
1175
|
...appliance?.battery,
|
|
@@ -917,6 +1178,7 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
917
1178
|
maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
|
|
918
1179
|
activeChargeLimitW: this.computeActiveChargeLimitW(batteryData),
|
|
919
1180
|
activeDischargeLimitW: this.computeActiveDischargeLimitW(batteryData),
|
|
1181
|
+
features,
|
|
920
1182
|
gridChargingEnabled: batteryData?.chaGriSet === 1
|
|
921
1183
|
}
|
|
922
1184
|
});
|
|
@@ -1162,13 +1424,27 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
1162
1424
|
if (this.dataBusListenerId) {
|
|
1163
1425
|
return;
|
|
1164
1426
|
}
|
|
1427
|
+
// Fire and forget — snapshot service hydration must not block listener setup.
|
|
1428
|
+
void this.initSnapshotService();
|
|
1165
1429
|
this.dataBus = this.energyApp.useDataBus();
|
|
1166
1430
|
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
1167
|
-
EnyoDataBusMessageEnum.
|
|
1168
|
-
EnyoDataBusMessageEnum.
|
|
1169
|
-
EnyoDataBusMessageEnum.
|
|
1170
|
-
EnyoDataBusMessageEnum.SetStorageChargeLimitV1
|
|
1431
|
+
EnyoDataBusMessageEnum.SetStorageScheduleV1,
|
|
1432
|
+
EnyoDataBusMessageEnum.StartCalibrationV1,
|
|
1433
|
+
EnyoDataBusMessageEnum.StopCalibrationV1,
|
|
1171
1434
|
], (entry) => this.handleStorageCommand(entry));
|
|
1435
|
+
if (!this.applianceId) {
|
|
1436
|
+
throw new Error("SunspecBattery.startDataBusListening: applianceId required — call connect() first.");
|
|
1437
|
+
}
|
|
1438
|
+
this.scheduleHandler = new SunspecBatteryScheduleHandler({
|
|
1439
|
+
dataBus: this.dataBus,
|
|
1440
|
+
interval: this.energyApp.useInterval(),
|
|
1441
|
+
storage: this.energyApp.useStorage(),
|
|
1442
|
+
applianceId: this.applianceId,
|
|
1443
|
+
sunspecClient: this.sunspecClient,
|
|
1444
|
+
unitId: this.unitId,
|
|
1445
|
+
getSnapshotService: () => this.snapshotService,
|
|
1446
|
+
onErrorCallback: (err) => console.warn(`Battery ${this.applianceId}: schedule handler rejected input: ${err.message}`),
|
|
1447
|
+
});
|
|
1172
1448
|
console.log(`Battery ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
1173
1449
|
}
|
|
1174
1450
|
/**
|
|
@@ -1181,6 +1457,7 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
1181
1457
|
}
|
|
1182
1458
|
this.dataBusListenerId = undefined;
|
|
1183
1459
|
this.dataBus = undefined;
|
|
1460
|
+
this.scheduleHandler?.dispose();
|
|
1184
1461
|
}
|
|
1185
1462
|
handleStorageCommand(entry) {
|
|
1186
1463
|
if (entry.applianceId !== this.applianceId) {
|
|
@@ -1189,17 +1466,14 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
1189
1466
|
void (async () => {
|
|
1190
1467
|
try {
|
|
1191
1468
|
switch (entry.message) {
|
|
1192
|
-
case EnyoDataBusMessageEnum.
|
|
1193
|
-
|
|
1194
|
-
break;
|
|
1195
|
-
case EnyoDataBusMessageEnum.StopStorageGridChargeV1:
|
|
1196
|
-
await this.handleStopGridCharge(entry);
|
|
1469
|
+
case EnyoDataBusMessageEnum.SetStorageScheduleV1:
|
|
1470
|
+
this.handleSetStorageScheduleAck(entry);
|
|
1197
1471
|
break;
|
|
1198
|
-
case EnyoDataBusMessageEnum.
|
|
1199
|
-
await this.
|
|
1472
|
+
case EnyoDataBusMessageEnum.StartCalibrationV1:
|
|
1473
|
+
await this.handleStartCalibration(entry);
|
|
1200
1474
|
break;
|
|
1201
|
-
case EnyoDataBusMessageEnum.
|
|
1202
|
-
await this.
|
|
1475
|
+
case EnyoDataBusMessageEnum.StopCalibrationV1:
|
|
1476
|
+
await this.handleStopCalibration(entry);
|
|
1203
1477
|
break;
|
|
1204
1478
|
}
|
|
1205
1479
|
}
|
|
@@ -1208,114 +1482,112 @@ export class SunspecBattery extends BaseSunspecDevice {
|
|
|
1208
1482
|
}
|
|
1209
1483
|
})();
|
|
1210
1484
|
}
|
|
1211
|
-
|
|
1485
|
+
/**
|
|
1486
|
+
* Acknowledge a SetStorageScheduleV1 message. The actual schedule application
|
|
1487
|
+
* is owned by `this.scheduleHandler` (subscribed independently via its own
|
|
1488
|
+
* data-bus listener registered in its constructor). The ack reports "received
|
|
1489
|
+
* & queued" rather than "applied" — schedule entries play out over time, and
|
|
1490
|
+
* per-entry write failures land in `console.error` via the handler's
|
|
1491
|
+
* onChange wrapper.
|
|
1492
|
+
*/
|
|
1493
|
+
handleSetStorageScheduleAck(msg) {
|
|
1212
1494
|
if (!this.isConnected() || !this.applianceId) {
|
|
1213
1495
|
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, "Not connected");
|
|
1214
1496
|
return;
|
|
1215
1497
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
if (
|
|
1225
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to enable grid charging');
|
|
1226
|
-
return;
|
|
1227
|
-
}
|
|
1228
|
-
// Step 2: Set charging power (Register 2: wChaMax)
|
|
1229
|
-
const step2 = await this.setChargingPower(msg.data.powerLimitW);
|
|
1230
|
-
if (!step2) {
|
|
1231
|
-
// Rollback step 1
|
|
1232
|
-
await this.writeBatteryControls({ chaGriSet: originalChaGriSet });
|
|
1233
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set charging power');
|
|
1498
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Lazily construct the per-appliance {@link SnapshotService}. Reloads any
|
|
1502
|
+
* persisted snapshot from storage so an in-flight calibration survives
|
|
1503
|
+
* process restarts. Idempotent.
|
|
1504
|
+
*/
|
|
1505
|
+
async initSnapshotService() {
|
|
1506
|
+
if (this.snapshotService) {
|
|
1234
1507
|
return;
|
|
1235
1508
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
if (!step3) {
|
|
1239
|
-
// Rollback steps 1+2
|
|
1240
|
-
await this.writeBatteryControls({ chaGriSet: originalChaGriSet, wChaMax: originalWChaMax });
|
|
1241
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set storage mode to charge');
|
|
1509
|
+
const service = this.buildSnapshotService((snapshot, reason) => this.restoreBatterySnapshot(snapshot, reason));
|
|
1510
|
+
if (!service) {
|
|
1242
1511
|
return;
|
|
1243
1512
|
}
|
|
1244
|
-
this.
|
|
1513
|
+
this.snapshotService = service;
|
|
1514
|
+
await this.snapshotService.initialize();
|
|
1515
|
+
console.log(`Battery ${this.applianceId}: snapshot service initialized`);
|
|
1245
1516
|
}
|
|
1246
|
-
async
|
|
1517
|
+
async handleStartCalibration(msg) {
|
|
1247
1518
|
if (!this.isConnected() || !this.applianceId) {
|
|
1248
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.
|
|
1519
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
|
|
1249
1520
|
return;
|
|
1250
1521
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
const controls = await this.getBatteryControls();
|
|
1254
|
-
console.log(`Battery ${this.applianceId}: current state - chaGriSet=${controls?.chaGriSet}, storCtlMod=${controls?.storCtlMod}`);
|
|
1255
|
-
const originalChaGriSet = controls?.chaGriSet;
|
|
1256
|
-
// Step 1: Disable grid charging (Register 17: chaGriSet=PV)
|
|
1257
|
-
const step1 = await this.enableGridCharging(false);
|
|
1258
|
-
if (!step1) {
|
|
1259
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to disable grid charging');
|
|
1260
|
-
return;
|
|
1522
|
+
if (!this.snapshotService) {
|
|
1523
|
+
await this.initSnapshotService();
|
|
1261
1524
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
if (!step2) {
|
|
1265
|
-
// Rollback step 1
|
|
1266
|
-
await this.writeBatteryControls({ chaGriSet: originalChaGriSet });
|
|
1267
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set storage mode to auto');
|
|
1525
|
+
if (!this.snapshotService) {
|
|
1526
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Snapshot service unavailable');
|
|
1268
1527
|
return;
|
|
1269
1528
|
}
|
|
1270
|
-
this.
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
if (!this.isConnected() || !this.applianceId) {
|
|
1274
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.NotSupported);
|
|
1529
|
+
if (this.snapshotService.isCalibrating()) {
|
|
1530
|
+
console.log(`Battery ${this.applianceId}: calibration already active — ack idempotently`);
|
|
1531
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1275
1532
|
return;
|
|
1276
1533
|
}
|
|
1277
|
-
console.log(`Battery ${this.applianceId}: handling
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
if (!controls?.wChaMax) {
|
|
1282
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read wChaMax for discharge limit conversion');
|
|
1534
|
+
console.log(`Battery ${this.applianceId}: handling StartCalibrationV1`);
|
|
1535
|
+
const controls = await this.sunspecClient.readBatteryControls(this.unitId);
|
|
1536
|
+
if (!controls) {
|
|
1537
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read battery controls for snapshot');
|
|
1283
1538
|
return;
|
|
1284
1539
|
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
if (!success) {
|
|
1291
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set discharge limit');
|
|
1540
|
+
try {
|
|
1541
|
+
await this.snapshotService.startCalibration(controls);
|
|
1542
|
+
}
|
|
1543
|
+
catch (error) {
|
|
1544
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to persist snapshot: ${error}`);
|
|
1292
1545
|
return;
|
|
1293
1546
|
}
|
|
1294
1547
|
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1295
1548
|
}
|
|
1296
|
-
async
|
|
1297
|
-
if (!this.
|
|
1298
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.
|
|
1549
|
+
async handleStopCalibration(msg) {
|
|
1550
|
+
if (!this.applianceId) {
|
|
1551
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not initialized');
|
|
1299
1552
|
return;
|
|
1300
1553
|
}
|
|
1301
|
-
console.log(`Battery ${this.applianceId}: handling
|
|
1302
|
-
//
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1554
|
+
console.log(`Battery ${this.applianceId}: handling StopCalibrationV1`);
|
|
1555
|
+
// SnapshotService.stopCalibration fires `restoreBatterySnapshot` internally
|
|
1556
|
+
// with reason="stop" before returning. Failures from the restore callback are
|
|
1557
|
+
// logged inside the callback (the persisted snapshot is already gone — see
|
|
1558
|
+
// library notes) so we always ack Accepted here.
|
|
1559
|
+
await this.snapshotService?.stopCalibration();
|
|
1560
|
+
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Accepted);
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* `onRestore` callback for the battery's {@link SnapshotService}. Writes only the
|
|
1564
|
+
* subset of writable battery fields touched during the calibration. Errors are
|
|
1565
|
+
* caught and logged — by the time this fires the snapshot is gone from storage,
|
|
1566
|
+
* so a throw would just propagate into the void.
|
|
1567
|
+
*/
|
|
1568
|
+
async restoreBatterySnapshot(snapshot, reason) {
|
|
1569
|
+
const source = snapshot.payload;
|
|
1570
|
+
const partial = {};
|
|
1571
|
+
const WRITABLE_BATTERY_FIELDS = ['storCtlMod', 'chaGriSet', 'wChaMax', 'inWRte', 'outWRte', 'minRsvPct'];
|
|
1572
|
+
for (const field of WRITABLE_BATTERY_FIELDS) {
|
|
1573
|
+
if (snapshot.modifiedFields.includes(field) && source[field] !== undefined) {
|
|
1574
|
+
partial[field] = source[field];
|
|
1575
|
+
}
|
|
1308
1576
|
}
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
console.log(`Battery ${this.applianceId}: calculated charge limit: ${chargeLimitPercent.toFixed(1)}% (${msg.data.chargeLimitW}W / ${controls.wChaMax}W)`);
|
|
1312
|
-
// Set charge limit (Register 13: inWRte)
|
|
1313
|
-
const success = await this.writeBatteryControls({ inWRte: chargeLimitPercent });
|
|
1314
|
-
if (!success) {
|
|
1315
|
-
this.sendCommandAcknowledge(msg.id, msg.message, EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set charge limit');
|
|
1577
|
+
if (Object.keys(partial).length === 0) {
|
|
1578
|
+
console.log(`Battery ${this.applianceId}: calibration ${reason} — no modified fields to restore`);
|
|
1316
1579
|
return;
|
|
1317
1580
|
}
|
|
1318
|
-
|
|
1581
|
+
console.log(`Battery ${this.applianceId}: calibration ${reason} — restoring [${Object.keys(partial).join(', ')}]`);
|
|
1582
|
+
try {
|
|
1583
|
+
const ok = await this.sunspecClient.writeBatteryControls(this.unitId, partial);
|
|
1584
|
+
if (!ok) {
|
|
1585
|
+
console.error(`Battery ${this.applianceId}: calibration ${reason} — writeBatteryControls returned false`);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
catch (error) {
|
|
1589
|
+
console.error(`Battery ${this.applianceId}: calibration ${reason} — writeBatteryControls threw: ${error}`);
|
|
1590
|
+
}
|
|
1319
1591
|
}
|
|
1320
1592
|
}
|
|
1321
1593
|
/**
|
|
@@ -1391,11 +1663,13 @@ export class SunspecMeter extends BaseSunspecDevice {
|
|
|
1391
1663
|
console.error(`Failed to update meter appliance: ${error}`);
|
|
1392
1664
|
}
|
|
1393
1665
|
}
|
|
1666
|
+
this.startDataBusListening();
|
|
1394
1667
|
}
|
|
1395
1668
|
/**
|
|
1396
1669
|
* Disconnect from the meter and update appliance state
|
|
1397
1670
|
*/
|
|
1398
1671
|
async disconnect() {
|
|
1672
|
+
this.stopDataBusListening();
|
|
1399
1673
|
if (this.applianceId) {
|
|
1400
1674
|
try {
|
|
1401
1675
|
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
|
|
@@ -1407,6 +1681,35 @@ export class SunspecMeter extends BaseSunspecDevice {
|
|
|
1407
1681
|
// Close just this meter's unit; other devices on the same network device stay open.
|
|
1408
1682
|
await this.sunspecClient.disconnectUnit(this.unitId);
|
|
1409
1683
|
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Meter does not implement calibration; it only subscribes to Start/StopCalibrationV1 to
|
|
1686
|
+
* answer NotSupported (per the data-bus contract that every command must be acknowledged).
|
|
1687
|
+
*/
|
|
1688
|
+
startDataBusListening() {
|
|
1689
|
+
if (this.dataBusListenerId) {
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
this.dataBus = this.energyApp.useDataBus();
|
|
1693
|
+
this.dataBusListenerId = this.dataBus.listenForMessages([
|
|
1694
|
+
EnyoDataBusMessageEnum.StartCalibrationV1,
|
|
1695
|
+
EnyoDataBusMessageEnum.StopCalibrationV1,
|
|
1696
|
+
], (entry) => this.handleMeterCommand(entry));
|
|
1697
|
+
console.log(`Meter ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
|
|
1698
|
+
}
|
|
1699
|
+
stopDataBusListening() {
|
|
1700
|
+
if (this.dataBusListenerId && this.dataBus) {
|
|
1701
|
+
this.dataBus.unsubscribe(this.dataBusListenerId);
|
|
1702
|
+
console.log(`Meter ${this.applianceId}: stopped data bus listening (listener ${this.dataBusListenerId})`);
|
|
1703
|
+
}
|
|
1704
|
+
this.dataBusListenerId = undefined;
|
|
1705
|
+
this.dataBus = undefined;
|
|
1706
|
+
}
|
|
1707
|
+
handleMeterCommand(entry) {
|
|
1708
|
+
if (entry.applianceId !== this.applianceId) {
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
this.sendCommandAcknowledge(entry.id, entry.message, EnyoCommandAcknowledgeAnswerEnum.NotSupported, 'Meter does not support calibration');
|
|
1712
|
+
}
|
|
1410
1713
|
/**
|
|
1411
1714
|
* Update meter data and return data bus messages
|
|
1412
1715
|
*/
|