@enyo-energy/sunspec-sdk 0.0.69 → 0.0.71

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.
@@ -6,6 +6,7 @@ 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 calibration_snapshot_service_js_1 = require("./calibration-snapshot-service.cjs");
9
10
  const enyo_source_enum_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-source.enum.js");
10
11
  const enyo_meter_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-meter-appliance.js");
11
12
  const enyo_battery_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js");
@@ -99,6 +100,7 @@ class BaseSunspecDevice {
99
100
  dataBus;
100
101
  retryManager;
101
102
  consecutiveReconnectFailures = 0;
103
+ calibrationService;
102
104
  constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, retryConfig, appliance, useTls) {
103
105
  this.energyApp = energyApp;
104
106
  this.name = name;
@@ -252,6 +254,27 @@ class BaseSunspecDevice {
252
254
  console.log(`${this.constructor.name} ${this.applianceId}: sending ${answer} for ${acknowledgeMessage} (messageId=${messageId}${rejectionReason ? `, reason=${rejectionReason}` : ''})`);
253
255
  this.dataBus.sendMessage([ackMessage]);
254
256
  }
257
+ /**
258
+ * Lazily construct and initialize the per-appliance CalibrationSnapshotService. Reloads
259
+ * any persisted snapshot from storage so an in-flight calibration survives restarts.
260
+ * Idempotent — subsequent calls are no-ops once the service exists.
261
+ */
262
+ async initCalibrationService(deviceType) {
263
+ if (this.calibrationService || !this.applianceId) {
264
+ return;
265
+ }
266
+ this.calibrationService = new calibration_snapshot_service_js_1.CalibrationSnapshotService(this.energyApp.useStorage(), this.applianceId, (snapshot, reason) => this.restoreFromCalibrationSnapshot(snapshot, reason));
267
+ await this.calibrationService.initialize();
268
+ console.log(`${this.constructor.name} ${this.applianceId}: calibration service initialized (${deviceType})`);
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.
277
+ }
255
278
  }
256
279
  exports.BaseSunspecDevice = BaseSunspecDevice;
257
280
  /**
@@ -675,8 +698,16 @@ class SunspecInverter extends BaseSunspecDevice {
675
698
  if (this.dataBusListenerId) {
676
699
  return;
677
700
  }
701
+ // Fire and forget — calibration service hydration must not block listening setup.
702
+ // Listener registration before await ensures we don't miss messages that arrive
703
+ // during initial load.
704
+ void this.initCalibrationService('inverter');
678
705
  this.dataBus = this.energyApp.useDataBus();
679
- this.dataBusListenerId = this.dataBus.listenForMessages([enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetInverterFeedInLimitV1], (entry) => this.handleInverterCommand(entry));
706
+ this.dataBusListenerId = this.dataBus.listenForMessages([
707
+ enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetInverterFeedInLimitV1,
708
+ enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1,
709
+ enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1,
710
+ ], (entry) => this.handleInverterCommand(entry));
680
711
  console.log(`Inverter ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
681
712
  }
682
713
  /**
@@ -696,8 +727,16 @@ class SunspecInverter extends BaseSunspecDevice {
696
727
  }
697
728
  void (async () => {
698
729
  try {
699
- if (entry.message === enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetInverterFeedInLimitV1) {
700
- await this.handleSetFeedInLimit(entry);
730
+ switch (entry.message) {
731
+ case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetInverterFeedInLimitV1:
732
+ await this.handleSetFeedInLimit(entry);
733
+ break;
734
+ case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1:
735
+ await this.handleStartCalibration(entry);
736
+ break;
737
+ case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1:
738
+ await this.handleStopCalibration(entry);
739
+ break;
701
740
  }
702
741
  }
703
742
  catch (error) {
@@ -721,8 +760,93 @@ class SunspecInverter extends BaseSunspecDevice {
721
760
  this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set feed-in limit');
722
761
  return;
723
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.calibrationService?.recordModification(['WMaxLimPct', 'WMaxLim_Ena']);
724
766
  this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
725
767
  }
768
+ async handleStartCalibration(msg) {
769
+ if (!this.isConnected() || !this.applianceId) {
770
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
771
+ return;
772
+ }
773
+ if (!this.calibrationService) {
774
+ await this.initCalibrationService('inverter');
775
+ }
776
+ if (!this.calibrationService) {
777
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Calibration service unavailable');
778
+ return;
779
+ }
780
+ if (this.calibrationService.isCalibrating()) {
781
+ console.log(`Inverter ${this.applianceId}: calibration already active — ack idempotently`);
782
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
783
+ return;
784
+ }
785
+ console.log(`Inverter ${this.applianceId}: handling StartCalibrationV1`);
786
+ const controls = await this.sunspecClient.readInverterControls(this.unitId);
787
+ if (!controls) {
788
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read inverter controls for snapshot');
789
+ return;
790
+ }
791
+ try {
792
+ await this.calibrationService.startCalibration({
793
+ deviceType: 'inverter',
794
+ unitId: this.unitId,
795
+ inverterControls: controls,
796
+ });
797
+ }
798
+ catch (error) {
799
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to persist snapshot: ${error}`);
800
+ return;
801
+ }
802
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
803
+ }
804
+ async handleStopCalibration(msg) {
805
+ if (!this.applianceId) {
806
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not initialized');
807
+ return;
808
+ }
809
+ console.log(`Inverter ${this.applianceId}: handling StopCalibrationV1`);
810
+ const snapshot = await this.calibrationService?.stopCalibration();
811
+ if (!snapshot) {
812
+ // No active calibration — treat as a no-op success.
813
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
814
+ return;
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
+ }
823
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
824
+ }
825
+ async restoreFromCalibrationSnapshot(snapshot, reason) {
826
+ if (snapshot.deviceType !== 'inverter' || !snapshot.inverterControls) {
827
+ return;
828
+ }
829
+ const source = snapshot.inverterControls;
830
+ 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
+ const WRITABLE_INVERTER_FIELDS = ['Conn', 'WMaxLimPct', 'WMaxLim_Ena', 'OutPFSet', 'OutPFSet_Ena'];
835
+ for (const field of WRITABLE_INVERTER_FIELDS) {
836
+ if (snapshot.modifiedFields.includes(field) && source[field] !== undefined) {
837
+ partial[field] = source[field];
838
+ }
839
+ }
840
+ if (Object.keys(partial).length === 0) {
841
+ console.log(`Inverter ${this.applianceId}: calibration ${reason} — no modified fields to restore`);
842
+ return;
843
+ }
844
+ console.log(`Inverter ${this.applianceId}: calibration ${reason} — restoring [${Object.keys(partial).join(', ')}]`);
845
+ const ok = await this.sunspecClient.writeInverterControls(snapshot.unitId, partial);
846
+ if (!ok) {
847
+ throw new Error('writeInverterControls returned false');
848
+ }
849
+ }
726
850
  }
727
851
  exports.SunspecInverter = SunspecInverter;
728
852
  /**
@@ -1167,12 +1291,16 @@ class SunspecBattery extends BaseSunspecDevice {
1167
1291
  if (this.dataBusListenerId) {
1168
1292
  return;
1169
1293
  }
1294
+ // Fire and forget — calibration service hydration must not block listening setup.
1295
+ void this.initCalibrationService('battery');
1170
1296
  this.dataBus = this.energyApp.useDataBus();
1171
1297
  this.dataBusListenerId = this.dataBus.listenForMessages([
1172
1298
  enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartStorageGridChargeV1,
1173
1299
  enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopStorageGridChargeV1,
1174
1300
  enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageDischargeLimitV1,
1175
- enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageChargeLimitV1
1301
+ enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageChargeLimitV1,
1302
+ enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1,
1303
+ enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1,
1176
1304
  ], (entry) => this.handleStorageCommand(entry));
1177
1305
  console.log(`Battery ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
1178
1306
  }
@@ -1206,6 +1334,12 @@ class SunspecBattery extends BaseSunspecDevice {
1206
1334
  case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageChargeLimitV1:
1207
1335
  await this.handleSetChargeLimit(entry);
1208
1336
  break;
1337
+ case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1:
1338
+ await this.handleStartCalibration(entry);
1339
+ break;
1340
+ case enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1:
1341
+ await this.handleStopCalibration(entry);
1342
+ break;
1209
1343
  }
1210
1344
  }
1211
1345
  catch (error) {
@@ -1246,6 +1380,7 @@ class SunspecBattery extends BaseSunspecDevice {
1246
1380
  this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set storage mode to charge');
1247
1381
  return;
1248
1382
  }
1383
+ await this.calibrationService?.recordModification(['chaGriSet', 'wChaMax', 'storCtlMod']);
1249
1384
  this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
1250
1385
  }
1251
1386
  async handleStopGridCharge(msg) {
@@ -1272,6 +1407,7 @@ class SunspecBattery extends BaseSunspecDevice {
1272
1407
  this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set storage mode to auto');
1273
1408
  return;
1274
1409
  }
1410
+ await this.calibrationService?.recordModification(['chaGriSet', 'storCtlMod']);
1275
1411
  this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
1276
1412
  }
1277
1413
  async handleSetDischargeLimit(msg) {
@@ -1296,6 +1432,7 @@ class SunspecBattery extends BaseSunspecDevice {
1296
1432
  this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set discharge limit');
1297
1433
  return;
1298
1434
  }
1435
+ await this.calibrationService?.recordModification(['outWRte']);
1299
1436
  this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
1300
1437
  }
1301
1438
  async handleSetChargeLimit(msg) {
@@ -1320,8 +1457,88 @@ class SunspecBattery extends BaseSunspecDevice {
1320
1457
  this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set charge limit');
1321
1458
  return;
1322
1459
  }
1460
+ await this.calibrationService?.recordModification(['inWRte']);
1323
1461
  this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
1324
1462
  }
1463
+ async handleStartCalibration(msg) {
1464
+ if (!this.isConnected() || !this.applianceId) {
1465
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
1466
+ return;
1467
+ }
1468
+ if (!this.calibrationService) {
1469
+ await this.initCalibrationService('battery');
1470
+ }
1471
+ if (!this.calibrationService) {
1472
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Calibration service unavailable');
1473
+ return;
1474
+ }
1475
+ if (this.calibrationService.isCalibrating()) {
1476
+ console.log(`Battery ${this.applianceId}: calibration already active — ack idempotently`);
1477
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
1478
+ return;
1479
+ }
1480
+ console.log(`Battery ${this.applianceId}: handling StartCalibrationV1`);
1481
+ const controls = await this.sunspecClient.readBatteryControls(this.unitId);
1482
+ if (!controls) {
1483
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read battery controls for snapshot');
1484
+ return;
1485
+ }
1486
+ try {
1487
+ await this.calibrationService.startCalibration({
1488
+ deviceType: 'battery',
1489
+ unitId: this.unitId,
1490
+ batteryControls: controls,
1491
+ });
1492
+ }
1493
+ catch (error) {
1494
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, `Failed to persist snapshot: ${error}`);
1495
+ return;
1496
+ }
1497
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
1498
+ }
1499
+ async handleStopCalibration(msg) {
1500
+ if (!this.applianceId) {
1501
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not initialized');
1502
+ return;
1503
+ }
1504
+ console.log(`Battery ${this.applianceId}: handling StopCalibrationV1`);
1505
+ const snapshot = await this.calibrationService?.stopCalibration();
1506
+ if (!snapshot) {
1507
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
1508
+ return;
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
+ }
1517
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
1518
+ }
1519
+ async restoreFromCalibrationSnapshot(snapshot, reason) {
1520
+ if (snapshot.deviceType !== 'battery' || !snapshot.batteryControls) {
1521
+ return;
1522
+ }
1523
+ const source = snapshot.batteryControls;
1524
+ const partial = {};
1525
+ // Whitelist of battery fields that writeBatteryControls actually writes (Model 124).
1526
+ const WRITABLE_BATTERY_FIELDS = ['storCtlMod', 'chaGriSet', 'wChaMax', 'inWRte', 'outWRte', 'minRsvPct'];
1527
+ for (const field of WRITABLE_BATTERY_FIELDS) {
1528
+ if (snapshot.modifiedFields.includes(field) && source[field] !== undefined) {
1529
+ partial[field] = source[field];
1530
+ }
1531
+ }
1532
+ if (Object.keys(partial).length === 0) {
1533
+ console.log(`Battery ${this.applianceId}: calibration ${reason} — no modified fields to restore`);
1534
+ return;
1535
+ }
1536
+ console.log(`Battery ${this.applianceId}: calibration ${reason} — restoring [${Object.keys(partial).join(', ')}]`);
1537
+ const ok = await this.sunspecClient.writeBatteryControls(snapshot.unitId, partial);
1538
+ if (!ok) {
1539
+ throw new Error('writeBatteryControls returned false');
1540
+ }
1541
+ }
1325
1542
  }
1326
1543
  exports.SunspecBattery = SunspecBattery;
1327
1544
  /**
@@ -1397,11 +1614,13 @@ class SunspecMeter extends BaseSunspecDevice {
1397
1614
  console.error(`Failed to update meter appliance: ${error}`);
1398
1615
  }
1399
1616
  }
1617
+ this.startDataBusListening();
1400
1618
  }
1401
1619
  /**
1402
1620
  * Disconnect from the meter and update appliance state
1403
1621
  */
1404
1622
  async disconnect() {
1623
+ this.stopDataBusListening();
1405
1624
  if (this.applianceId) {
1406
1625
  try {
1407
1626
  await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
@@ -1413,6 +1632,35 @@ class SunspecMeter extends BaseSunspecDevice {
1413
1632
  // Close just this meter's unit; other devices on the same network device stay open.
1414
1633
  await this.sunspecClient.disconnectUnit(this.unitId);
1415
1634
  }
1635
+ /**
1636
+ * Meter does not implement calibration; it only subscribes to Start/StopCalibrationV1 to
1637
+ * answer NotSupported (per the data-bus contract that every command must be acknowledged).
1638
+ */
1639
+ startDataBusListening() {
1640
+ if (this.dataBusListenerId) {
1641
+ return;
1642
+ }
1643
+ this.dataBus = this.energyApp.useDataBus();
1644
+ this.dataBusListenerId = this.dataBus.listenForMessages([
1645
+ enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StartCalibrationV1,
1646
+ enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.StopCalibrationV1,
1647
+ ], (entry) => this.handleMeterCommand(entry));
1648
+ console.log(`Meter ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
1649
+ }
1650
+ stopDataBusListening() {
1651
+ if (this.dataBusListenerId && this.dataBus) {
1652
+ this.dataBus.unsubscribe(this.dataBusListenerId);
1653
+ console.log(`Meter ${this.applianceId}: stopped data bus listening (listener ${this.dataBusListenerId})`);
1654
+ }
1655
+ this.dataBusListenerId = undefined;
1656
+ this.dataBus = undefined;
1657
+ }
1658
+ handleMeterCommand(entry) {
1659
+ if (entry.applianceId !== this.applianceId) {
1660
+ return;
1661
+ }
1662
+ this.sendCommandAcknowledge(entry.id, entry.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported, 'Meter does not support calibration');
1663
+ }
1416
1664
  /**
1417
1665
  * Update meter data and return data bus messages
1418
1666
  */
@@ -5,6 +5,7 @@ import { EnyoNetworkDevice } from "@enyo-energy/energy-app-sdk/dist/types/enyo-n
5
5
  import { SunspecModbusClient } from "./sunspec-modbus-client.cjs";
6
6
  import { ConnectionRetryManager } from "./connection-retry-manager.cjs";
7
7
  import { EnyoCommandAcknowledgeAnswerEnum, EnyoDataBusMessage, EnyoDataBusMessageEnum, EnyoDataBusMessageResolution } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
8
+ import { type CalibrationDeviceType, type CalibrationRestoreReason, type CalibrationSnapshot, CalibrationSnapshotService } from "./calibration-snapshot-service.cjs";
8
9
  import { EnergyAppDataBus } from "@enyo-energy/energy-app-sdk/dist/packages/energy-app-data-bus.js";
9
10
  /**
10
11
  * Base abstract class for all Sunspec devices
@@ -25,6 +26,7 @@ export declare abstract class BaseSunspecDevice {
25
26
  protected dataBus?: EnergyAppDataBus;
26
27
  protected retryManager: ConnectionRetryManager;
27
28
  protected consecutiveReconnectFailures: number;
29
+ protected calibrationService?: CalibrationSnapshotService;
28
30
  constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, retryConfig?: IRetryConfig, appliance?: EnyoAppliance, useTls?: boolean | undefined);
29
31
  /**
30
32
  * Connect to the device and create/update the appliance
@@ -77,6 +79,17 @@ export declare abstract class BaseSunspecDevice {
77
79
  */
78
80
  protected markOffline(): Promise<void>;
79
81
  protected sendCommandAcknowledge(messageId: string, acknowledgeMessage: EnyoDataBusMessageEnum | string, answer: EnyoCommandAcknowledgeAnswerEnum, rejectionReason?: string): void;
82
+ /**
83
+ * Lazily construct and initialize the per-appliance CalibrationSnapshotService. Reloads
84
+ * any persisted snapshot from storage so an in-flight calibration survives restarts.
85
+ * Idempotent — subsequent calls are no-ops once the service exists.
86
+ */
87
+ protected initCalibrationService(deviceType: CalibrationDeviceType): Promise<void>;
88
+ /**
89
+ * Subclasses implement how to write the modified fields of a snapshot back to the device.
90
+ * Called both on explicit StopCalibrationV1 and when the 5-minute auto-stop fires.
91
+ */
92
+ protected restoreFromCalibrationSnapshot(_snapshot: CalibrationSnapshot, _reason: CalibrationRestoreReason): Promise<void>;
80
93
  }
81
94
  /**
82
95
  * Sunspec Inverter implementation using dynamic model discovery
@@ -156,6 +169,9 @@ export declare class SunspecInverter extends BaseSunspecDevice {
156
169
  stopDataBusListening(): void;
157
170
  private handleInverterCommand;
158
171
  private handleSetFeedInLimit;
172
+ private handleStartCalibration;
173
+ private handleStopCalibration;
174
+ protected restoreFromCalibrationSnapshot(snapshot: CalibrationSnapshot, reason: CalibrationRestoreReason): Promise<void>;
159
175
  }
160
176
  /**
161
177
  * Sunspec Battery implementation
@@ -288,6 +304,9 @@ export declare class SunspecBattery extends BaseSunspecDevice {
288
304
  private handleStopGridCharge;
289
305
  private handleSetDischargeLimit;
290
306
  private handleSetChargeLimit;
307
+ private handleStartCalibration;
308
+ private handleStopCalibration;
309
+ protected restoreFromCalibrationSnapshot(snapshot: CalibrationSnapshot, reason: CalibrationRestoreReason): Promise<void>;
291
310
  }
292
311
  /**
293
312
  * Sunspec Meter implementation
@@ -303,6 +322,13 @@ export declare class SunspecMeter extends BaseSunspecDevice {
303
322
  * Disconnect from the meter and update appliance state
304
323
  */
305
324
  disconnect(): Promise<void>;
325
+ /**
326
+ * Meter does not implement calibration; it only subscribes to Start/StopCalibrationV1 to
327
+ * answer NotSupported (per the data-bus contract that every command must be acknowledged).
328
+ */
329
+ startDataBusListening(): void;
330
+ stopDataBusListening(): void;
331
+ private handleMeterCommand;
306
332
  /**
307
333
  * Update meter data and return data bus messages
308
334
  */
@@ -9,7 +9,7 @@ exports.getSdkVersion = getSdkVersion;
9
9
  /**
10
10
  * Current version of the enyo Energy App SDK.
11
11
  */
12
- exports.SDK_VERSION = '0.0.69';
12
+ exports.SDK_VERSION = '0.0.71';
13
13
  /**
14
14
  * Gets the current SDK version.
15
15
  * @returns The semantic version string of the SDK
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "0.0.69";
8
+ export declare const SDK_VERSION = "0.0.71";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './sunspec-interfaces.js';
2
2
  export * from './sunspec-devices.js';
3
3
  export * from './sunspec-modbus-client.js';
4
+ export * from './calibration-snapshot-service.js';
4
5
  export { ConnectionRetryManager } from './connection-retry-manager.js';
5
6
  export { SDK_VERSION, getSdkVersion } from './version.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './sunspec-interfaces.js';
2
2
  export * from './sunspec-devices.js';
3
3
  export * from './sunspec-modbus-client.js';
4
+ export * from './calibration-snapshot-service.js';
4
5
  export { ConnectionRetryManager } from './connection-retry-manager.js';
5
6
  export { SDK_VERSION, getSdkVersion } from './version.js';
@@ -5,6 +5,7 @@ import { EnyoNetworkDevice } from "@enyo-energy/energy-app-sdk/dist/types/enyo-n
5
5
  import { SunspecModbusClient } from "./sunspec-modbus-client.js";
6
6
  import { ConnectionRetryManager } from "./connection-retry-manager.js";
7
7
  import { EnyoCommandAcknowledgeAnswerEnum, EnyoDataBusMessage, EnyoDataBusMessageEnum, EnyoDataBusMessageResolution } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
8
+ import { type CalibrationDeviceType, type CalibrationRestoreReason, type CalibrationSnapshot, CalibrationSnapshotService } from "./calibration-snapshot-service.js";
8
9
  import { EnergyAppDataBus } from "@enyo-energy/energy-app-sdk/dist/packages/energy-app-data-bus.js";
9
10
  /**
10
11
  * Base abstract class for all Sunspec devices
@@ -25,6 +26,7 @@ export declare abstract class BaseSunspecDevice {
25
26
  protected dataBus?: EnergyAppDataBus;
26
27
  protected retryManager: ConnectionRetryManager;
27
28
  protected consecutiveReconnectFailures: number;
29
+ protected calibrationService?: CalibrationSnapshotService;
28
30
  constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, retryConfig?: IRetryConfig, appliance?: EnyoAppliance, useTls?: boolean | undefined);
29
31
  /**
30
32
  * Connect to the device and create/update the appliance
@@ -77,6 +79,17 @@ export declare abstract class BaseSunspecDevice {
77
79
  */
78
80
  protected markOffline(): Promise<void>;
79
81
  protected sendCommandAcknowledge(messageId: string, acknowledgeMessage: EnyoDataBusMessageEnum | string, answer: EnyoCommandAcknowledgeAnswerEnum, rejectionReason?: string): void;
82
+ /**
83
+ * Lazily construct and initialize the per-appliance CalibrationSnapshotService. Reloads
84
+ * any persisted snapshot from storage so an in-flight calibration survives restarts.
85
+ * Idempotent — subsequent calls are no-ops once the service exists.
86
+ */
87
+ protected initCalibrationService(deviceType: CalibrationDeviceType): Promise<void>;
88
+ /**
89
+ * Subclasses implement how to write the modified fields of a snapshot back to the device.
90
+ * Called both on explicit StopCalibrationV1 and when the 5-minute auto-stop fires.
91
+ */
92
+ protected restoreFromCalibrationSnapshot(_snapshot: CalibrationSnapshot, _reason: CalibrationRestoreReason): Promise<void>;
80
93
  }
81
94
  /**
82
95
  * Sunspec Inverter implementation using dynamic model discovery
@@ -156,6 +169,9 @@ export declare class SunspecInverter extends BaseSunspecDevice {
156
169
  stopDataBusListening(): void;
157
170
  private handleInverterCommand;
158
171
  private handleSetFeedInLimit;
172
+ private handleStartCalibration;
173
+ private handleStopCalibration;
174
+ protected restoreFromCalibrationSnapshot(snapshot: CalibrationSnapshot, reason: CalibrationRestoreReason): Promise<void>;
159
175
  }
160
176
  /**
161
177
  * Sunspec Battery implementation
@@ -288,6 +304,9 @@ export declare class SunspecBattery extends BaseSunspecDevice {
288
304
  private handleStopGridCharge;
289
305
  private handleSetDischargeLimit;
290
306
  private handleSetChargeLimit;
307
+ private handleStartCalibration;
308
+ private handleStopCalibration;
309
+ protected restoreFromCalibrationSnapshot(snapshot: CalibrationSnapshot, reason: CalibrationRestoreReason): Promise<void>;
291
310
  }
292
311
  /**
293
312
  * Sunspec Meter implementation
@@ -303,6 +322,13 @@ export declare class SunspecMeter extends BaseSunspecDevice {
303
322
  * Disconnect from the meter and update appliance state
304
323
  */
305
324
  disconnect(): Promise<void>;
325
+ /**
326
+ * Meter does not implement calibration; it only subscribes to Start/StopCalibrationV1 to
327
+ * answer NotSupported (per the data-bus contract that every command must be acknowledged).
328
+ */
329
+ startDataBusListening(): void;
330
+ stopDataBusListening(): void;
331
+ private handleMeterCommand;
306
332
  /**
307
333
  * Update meter data and return data bus messages
308
334
  */