@enyo-energy/sunspec-sdk 0.0.73 → 0.0.75

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.
@@ -486,7 +486,10 @@ class SunspecModbusClient {
486
486
  }
487
487
  catch (error) {
488
488
  console.debug(`No SunSpec device at unit ${unitId}: ${error}`);
489
- console.log(`Discovery complete for unit ${unitId}. Found 0 models: []`);
489
+ // Discovery summary at line 594 below is the info-level outcome.
490
+ // This 0-models branch is the same outcome as a normal "no device found",
491
+ // covered already by the debug above. Keep at debug.
492
+ console.debug(`Discovery complete for unit ${unitId}. Found 0 models: []`);
490
493
  return models;
491
494
  }
492
495
  currentAddress = addressInfo.nextAddress;
@@ -495,14 +498,14 @@ class SunspecModbusClient {
495
498
  const buffer = await instance.readHoldingRegisters(currentAddress, 2);
496
499
  const modelData = [buffer.readUInt16BE(0), buffer.readUInt16BE(2)];
497
500
  if (!modelData || modelData.length < 2) {
498
- console.log(`No data at address ${currentAddress}, ending discovery`);
501
+ console.debug(`No data at address ${currentAddress}, ending discovery`);
499
502
  break;
500
503
  }
501
504
  const modelId = modelData[0];
502
505
  const modelLength = modelData[1];
503
506
  // Check for end marker
504
507
  if (modelId === 0xFFFF || modelId === 65535) {
505
- console.log(`Found end marker at address ${currentAddress}`);
508
+ console.debug(`Found end marker at address ${currentAddress}`);
506
509
  break;
507
510
  }
508
511
  // Store discovered model
@@ -512,7 +515,9 @@ class SunspecModbusClient {
512
515
  length: modelLength
513
516
  };
514
517
  models.set(modelId, model);
515
- console.log(`Discovered Model ${modelId} at address ${currentAddress} with length ${modelLength} (unit ${unitId})`);
518
+ // Per-model discovery step. The end-of-discovery summary below is the
519
+ // info-level outcome; the per-model walk is debug-only.
520
+ console.debug(`Discovered Model ${modelId} at address ${currentAddress} with length ${modelLength} (unit ${unitId})`);
516
521
  // Jump to next model: current address + 2 (header) + model length
517
522
  currentAddress = currentAddress + 2 + modelLength;
518
523
  }
@@ -701,7 +706,9 @@ class SunspecModbusClient {
701
706
  // Write to holding registers
702
707
  await instance.writeMultipleRegisters(address, registerValues);
703
708
  this.connectionHealth.recordSuccess();
704
- console.log(`Successfully wrote value ${value} to register ${address} (unit ${unitId})`);
709
+ // Per-register write success fires for every parameter inside higher-level
710
+ // writers (writeBatteryControls / writeInverterControls). Demoted to debug.
711
+ console.debug(`Successfully wrote value ${value} to register ${address} (unit ${unitId})`);
705
712
  return true;
706
713
  }
707
714
  catch (error) {
@@ -1599,7 +1606,10 @@ class SunspecModbusClient {
1599
1606
  data.dischargePower = Math.abs((data.outWRte / 100) * data.wChaMax);
1600
1607
  console.debug(`Calculated Discharge Power (inWRte: ${data.outWRte}, wChaMax: ${data.wChaMax}): ${data.dischargePower?.toFixed(2)} W`);
1601
1608
  }
1602
- console.debug('[Model 124] Battery Data:', data);
1609
+ // Single-line JSON debug dump Node's default formatter would split
1610
+ // the object across many lines; stringify keeps the whole snapshot
1611
+ // on one log entry so it stays grep-able and round-trippable.
1612
+ console.debug(`[Battery] unit=${unitId} model=124 data=${JSON.stringify(data)}`);
1603
1613
  return data;
1604
1614
  }
1605
1615
  else if (model.id === 802) {
@@ -1636,17 +1646,18 @@ class SunspecModbusClient {
1636
1646
  chargePower,
1637
1647
  dischargePower,
1638
1648
  };
1639
- console.debug('[Model 802] Battery Data:', result);
1649
+ console.debug(`[Battery] unit=${unitId} model=802 data=${JSON.stringify(result)}`);
1640
1650
  return result;
1641
1651
  }
1642
1652
  else {
1643
- // Handle other battery models (803) if needed
1644
- console.debug(`Battery Model ${model.id} reading not yet implemented`);
1645
- return {
1653
+ // Handle other battery models (803) if needed.
1654
+ const stub = {
1646
1655
  blockNumber: model.id,
1647
1656
  blockAddress: model.address,
1648
- blockLength: model.length
1657
+ blockLength: model.length,
1649
1658
  };
1659
+ console.debug(`[Battery] unit=${unitId} model=${model.id} (not yet implemented) data=${JSON.stringify(stub)}`);
1660
+ return stub;
1650
1661
  }
1651
1662
  }
1652
1663
  catch (error) {
@@ -1664,17 +1675,21 @@ class SunspecModbusClient {
1664
1675
  return false;
1665
1676
  }
1666
1677
  const baseAddr = model.address;
1667
- console.log(`Writing Battery Controls to Model 124 at base address: ${baseAddr} (unit ${unitId})`);
1678
+ // Per-register write trace is high-volume (every schedule entry transition fires
1679
+ // a writeBatteryControls). Demoted to debug; consumers that need the trail can
1680
+ // raise their log level. High-level callers (setStorageMode, enableGridCharging,
1681
+ // setFeedInLimit, etc.) still emit one info-level line per action.
1682
+ console.debug(`Writing Battery Controls to Model 124 at base address: ${baseAddr} (unit ${unitId})`);
1668
1683
  try {
1669
- // Write storage control mode (Register 5)
1670
- if (controls.storCtlMod !== undefined) {
1671
- await this.writeRegisterValue(unitId, baseAddr + 5, controls.storCtlMod, 'uint16');
1672
- console.log(`Set storage control mode to 0x${controls.storCtlMod.toString(16)}`);
1673
- }
1684
+ // Write order: source pin and parameter writes land BEFORE the
1685
+ // control mode so the device only "starts acting" once every
1686
+ // value that governs the action is fresh. The reverse order
1687
+ // leaks a Modbus RTT-sized window where storCtlMod sees stale
1688
+ // chaGriSet / wChaMax / inWRte / outWRte.
1674
1689
  // Write charge source setting (Register 17)
1675
1690
  if (controls.chaGriSet !== undefined) {
1676
1691
  await this.writeRegisterValue(unitId, baseAddr + 17, controls.chaGriSet, 'uint16');
1677
- console.log(`Set charge source to ${controls.chaGriSet === sunspec_interfaces_js_1.SunspecChargeSource.GRID ? 'GRID' : 'PV'}`);
1692
+ console.debug(`Set charge source to ${controls.chaGriSet === sunspec_interfaces_js_1.SunspecChargeSource.GRID ? 'GRID' : 'PV'}`);
1678
1693
  }
1679
1694
  // Write maximum charge power (Register 2) - needs scale factor
1680
1695
  if (controls.wChaMax !== undefined) {
@@ -1682,7 +1697,7 @@ class SunspecModbusClient {
1682
1697
  const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
1683
1698
  const scaledValue = Math.round(controls.wChaMax / Math.pow(10, scaleFactor));
1684
1699
  await this.writeRegisterValue(unitId, baseAddr + 2, scaledValue, 'uint16');
1685
- console.log(`Set max charge power to ${controls.wChaMax}W (scaled: ${scaledValue})`);
1700
+ console.debug(`Set max charge power to ${controls.wChaMax}W (scaled: ${scaledValue})`);
1686
1701
  }
1687
1702
  // Write charge rate (Register 13) - needs scale factor
1688
1703
  if (controls.inWRte !== undefined) {
@@ -1690,7 +1705,7 @@ class SunspecModbusClient {
1690
1705
  const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
1691
1706
  const scaledValue = Math.round(controls.inWRte / Math.pow(10, scaleFactor));
1692
1707
  await this.writeRegisterValue(unitId, baseAddr + 13, scaledValue, 'int16');
1693
- console.log(`Set charge rate to ${controls.inWRte}% (scaled: ${scaledValue})`);
1708
+ console.debug(`Set charge rate to ${controls.inWRte}% (scaled: ${scaledValue})`);
1694
1709
  }
1695
1710
  // Write discharge rate (Register 12) - needs scale factor
1696
1711
  if (controls.outWRte !== undefined) {
@@ -1698,7 +1713,7 @@ class SunspecModbusClient {
1698
1713
  const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
1699
1714
  const scaledValue = Math.round(controls.outWRte / Math.pow(10, scaleFactor));
1700
1715
  await this.writeRegisterValue(unitId, baseAddr + 12, scaledValue, 'int16');
1701
- console.log(`Set discharge rate to ${controls.outWRte}% (scaled: ${scaledValue})`);
1716
+ console.debug(`Set discharge rate to ${controls.outWRte}% (scaled: ${scaledValue})`);
1702
1717
  }
1703
1718
  // Write minimum reserve percentage (Register 7) - needs scale factor
1704
1719
  if (controls.minRsvPct !== undefined) {
@@ -1706,9 +1721,16 @@ class SunspecModbusClient {
1706
1721
  const scaleFactor = await this.readRegisterValue(unitId, scaleFactorAddr, 1, 'int16');
1707
1722
  const scaledValue = Math.round(controls.minRsvPct / Math.pow(10, scaleFactor));
1708
1723
  await this.writeRegisterValue(unitId, baseAddr + 7, scaledValue, 'uint16');
1709
- console.log(`Set minimum reserve to ${controls.minRsvPct}% (scaled: ${scaledValue})`);
1724
+ console.debug(`Set minimum reserve to ${controls.minRsvPct}% (scaled: ${scaledValue})`);
1725
+ }
1726
+ // Storage control mode (Register 5) — written LAST so all
1727
+ // governing parameters are already in place when the device
1728
+ // transitions into the new mode.
1729
+ if (controls.storCtlMod !== undefined) {
1730
+ await this.writeRegisterValue(unitId, baseAddr + 5, controls.storCtlMod, 'uint16');
1731
+ console.debug(`Set storage control mode to 0x${controls.storCtlMod.toString(16)}`);
1710
1732
  }
1711
- console.log('Battery controls written successfully');
1733
+ console.debug('Battery controls written successfully');
1712
1734
  return true;
1713
1735
  }
1714
1736
  catch (error) {
@@ -1759,10 +1781,12 @@ class SunspecModbusClient {
1759
1781
  async readBatteryControls(unitId) {
1760
1782
  const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Battery);
1761
1783
  if (!model) {
1762
- console.log(`Battery model 124 not found on unit ${unitId}`);
1784
+ // Fires per readBatteryControls attempt — noisy on non-battery units
1785
+ // that go through the same code path. Demoted to debug.
1786
+ console.debug(`Battery model 124 not found on unit ${unitId}`);
1763
1787
  return null;
1764
1788
  }
1765
- console.log(`Reading Battery Controls from Model 124 at base address: ${model.address} (unit ${unitId})`);
1789
+ console.debug(`Reading Battery Controls from Model 124 at base address: ${model.address} (unit ${unitId})`);
1766
1790
  try {
1767
1791
  // Read entire model block in a single Modbus call
1768
1792
  const buffer = await this.readModelBlock(unitId, model);
@@ -1977,7 +2001,7 @@ class SunspecModbusClient {
1977
2001
  console.debug(`Common block model not found on unit ${unitId}`);
1978
2002
  return null;
1979
2003
  }
1980
- console.log(`Reading Common Block - Model address: ${model.address} (unit ${unitId})`);
2004
+ console.debug(`Reading Common Block - Model address: ${model.address} (unit ${unitId})`);
1981
2005
  try {
1982
2006
  // Read entire model block in a single Modbus call
1983
2007
  const buffer = await this.readModelBlock(unitId, model);
@@ -2137,7 +2161,8 @@ class SunspecModbusClient {
2137
2161
  async readInverterControls(unitId) {
2138
2162
  const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Controls);
2139
2163
  if (!model) {
2140
- console.log(`Controls model 123 not found on unit ${unitId}`);
2164
+ // Same trace-only rationale as the Model 124 not-found message above.
2165
+ console.debug(`Controls model 123 not found on unit ${unitId}`);
2141
2166
  return null;
2142
2167
  }
2143
2168
  try {
@@ -2246,17 +2271,17 @@ class SunspecModbusClient {
2246
2271
  const scaleFactor = sfBuffer.readInt16BE(0);
2247
2272
  const scaledValue = Math.round(settings.WMax / Math.pow(10, scaleFactor));
2248
2273
  // Writing registers needs to be implemented in EnergyAppModbusInstance
2249
- // For now, log the write operation
2250
- console.log(`Would write value ${scaledValue} to register ${baseAddr}`);
2274
+ // For now, log the (would-be) write operation. Dry-run trace only — debug.
2275
+ console.debug(`Would write value ${scaledValue} to register ${baseAddr}`);
2251
2276
  }
2252
2277
  if (settings.VRef !== undefined) {
2253
2278
  const sfBuffer = await instance.readHoldingRegisters(baseAddr + 23, 1);
2254
2279
  const scaleFactor = sfBuffer.readInt16BE(0);
2255
2280
  const scaledValue = Math.round(settings.VRef / Math.pow(10, scaleFactor));
2256
- console.log(`Would write value ${scaledValue} to register ${baseAddr + 1}`);
2281
+ console.debug(`Would write value ${scaledValue} to register ${baseAddr + 1}`);
2257
2282
  }
2258
2283
  // Add more write operations for other settings as needed
2259
- console.log('Inverter settings written successfully');
2284
+ console.debug('Inverter settings written successfully');
2260
2285
  return true;
2261
2286
  }
2262
2287
  catch (error) {
@@ -2275,36 +2300,37 @@ class SunspecModbusClient {
2275
2300
  }
2276
2301
  const baseAddr = model.address;
2277
2302
  try {
2278
- // Connection control (Register 2)
2303
+ // Per-field inverter-control writes are debug-only; the high-level callers
2304
+ // (`setFeedInLimit`, etc.) still log the action and outcome at info level.
2279
2305
  if (controls.Conn !== undefined) {
2280
2306
  await this.writeRegisterValue(unitId, baseAddr + 2, controls.Conn, 'uint16');
2281
- console.log(`Set connection control to ${controls.Conn}`);
2307
+ console.debug(`Set connection control to ${controls.Conn}`);
2282
2308
  }
2283
2309
  // Power limit control (Register 3) - needs scale factor
2284
2310
  if (controls.WMaxLimPct !== undefined) {
2285
2311
  const scaleFactor = await this.readRegisterValue(unitId, baseAddr + 21, 1, 'int16');
2286
2312
  const scaledValue = Math.round(controls.WMaxLimPct / Math.pow(10, scaleFactor));
2287
2313
  await this.writeRegisterValue(unitId, baseAddr + 3, scaledValue, 'uint16');
2288
- console.log(`Set power limit to ${controls.WMaxLimPct}% (scaled: ${scaledValue})`);
2314
+ console.debug(`Set power limit to ${controls.WMaxLimPct}% (scaled: ${scaledValue})`);
2289
2315
  }
2290
2316
  // Throttle enable/disable (Register 7)
2291
2317
  if (controls.WMaxLim_Ena !== undefined) {
2292
2318
  await this.writeRegisterValue(unitId, baseAddr + 7, controls.WMaxLim_Ena, 'uint16');
2293
- console.log(`Set throttle enable to ${controls.WMaxLim_Ena}`);
2319
+ console.debug(`Set throttle enable to ${controls.WMaxLim_Ena}`);
2294
2320
  }
2295
2321
  // Power factor control (Register 8) - needs scale factor
2296
2322
  if (controls.OutPFSet !== undefined) {
2297
2323
  const scaleFactor = await this.readRegisterValue(unitId, baseAddr + 22, 1, 'int16');
2298
2324
  const scaledValue = Math.round(controls.OutPFSet / Math.pow(10, scaleFactor));
2299
2325
  await this.writeRegisterValue(unitId, baseAddr + 8, scaledValue, 'int16');
2300
- console.log(`Set power factor to ${controls.OutPFSet} (scaled: ${scaledValue})`);
2326
+ console.debug(`Set power factor to ${controls.OutPFSet} (scaled: ${scaledValue})`);
2301
2327
  }
2302
2328
  // Power factor enable/disable (Register 12)
2303
2329
  if (controls.OutPFSet_Ena !== undefined) {
2304
2330
  await this.writeRegisterValue(unitId, baseAddr + 12, controls.OutPFSet_Ena, 'uint16');
2305
- console.log(`Set PF enable to ${controls.OutPFSet_Ena}`);
2331
+ console.debug(`Set PF enable to ${controls.OutPFSet_Ena}`);
2306
2332
  }
2307
- console.log('Inverter controls written successfully');
2333
+ console.debug('Inverter controls written successfully');
2308
2334
  return true;
2309
2335
  }
2310
2336
  catch (error) {
@@ -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.73';
12
+ exports.SDK_VERSION = '0.0.75';
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.73";
8
+ export declare const SDK_VERSION = "0.0.75";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
@@ -1,4 +1,5 @@
1
1
  import { StorageScheduleHandler, type ActiveStorageScheduleEntry, type StorageScheduleHandlerOptions } from "@enyo-energy/energy-app-sdk/dist/implementations/storage/storage-schedule-handler.js";
2
+ import { type EnyoDataBusSetStorageScheduleV1 } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
2
3
  import { type SnapshotService } from "@enyo-energy/appliance-calibration";
3
4
  import { type SunspecBatteryControls } from "./sunspec-interfaces.js";
4
5
  /**
@@ -58,7 +59,33 @@ export declare class SunspecBatteryScheduleHandler extends StorageScheduleHandle
58
59
  private readonly getSnapshotService;
59
60
  private readonly onErrorCallback?;
60
61
  private installedWChaMax;
62
+ /**
63
+ * Sticky pre-schedule snapshot, captured once on the first `onInit` and
64
+ * held across every subsequent schedule-to-schedule replacement. Cleared
65
+ * only when a real rollback fires (`mode: auto` from the data bus or
66
+ * `dispose`). Lets the eventual restore write the *true* baseline rather
67
+ * than the last-active-entry register set that the device happens to be
68
+ * in at the moment of replacement.
69
+ */
70
+ private originalBaseline?;
71
+ /**
72
+ * Set inside the overridden `applyMessage` when an incoming
73
+ * `mode: schedule` message would replace an already-running schedule.
74
+ * Consumed (and reset) inside `onRollback` so the base library's
75
+ * automatic `doReleaseSchedule → onRollback` step does not actually
76
+ * write the snapshot back during a replacement.
77
+ */
78
+ private suppressNextRollbackWrite;
61
79
  constructor(opts: SunspecBatteryScheduleHandlerOptions);
80
+ /**
81
+ * Override the base-class data-bus router so a schedule-to-schedule
82
+ * replacement marks the next `onRollback` as "skip the write". The base
83
+ * library still owns the actual schedule lifecycle — we just steer one
84
+ * decision inside `onRollback`. `mode: auto` and any path that does not
85
+ * have an active schedule fall through unchanged, so the rollback fires
86
+ * normally there.
87
+ */
88
+ applyMessage(msg: EnyoDataBusSetStorageScheduleV1): Promise<void>;
62
89
  protected onInit(_active: ActiveStorageScheduleEntry): Promise<SunspecScheduleRegisters>;
63
90
  protected onChange(active: ActiveStorageScheduleEntry, _previous: ActiveStorageScheduleEntry | undefined): void;
64
91
  protected onRollback(registers: SunspecScheduleRegisters): void;
@@ -1,5 +1,5 @@
1
1
  import { StorageScheduleHandler, } from "@enyo-energy/energy-app-sdk/dist/implementations/storage/storage-schedule-handler.js";
2
- import { EnyoStorageScheduleDirectionEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
2
+ import { EnyoStorageScheduleDirectionEnum, EnyoStorageScheduleModeEnum, } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
3
3
  import { SunspecChargeSource, SunspecStorageControlMode, } from "./sunspec-interfaces.js";
4
4
  /**
5
5
  * Concrete `StorageScheduleHandler` for SunSpec Model 124 batteries. The base
@@ -15,6 +15,23 @@ export class SunspecBatteryScheduleHandler extends StorageScheduleHandler {
15
15
  getSnapshotService;
16
16
  onErrorCallback;
17
17
  installedWChaMax;
18
+ /**
19
+ * Sticky pre-schedule snapshot, captured once on the first `onInit` and
20
+ * held across every subsequent schedule-to-schedule replacement. Cleared
21
+ * only when a real rollback fires (`mode: auto` from the data bus or
22
+ * `dispose`). Lets the eventual restore write the *true* baseline rather
23
+ * than the last-active-entry register set that the device happens to be
24
+ * in at the moment of replacement.
25
+ */
26
+ originalBaseline;
27
+ /**
28
+ * Set inside the overridden `applyMessage` when an incoming
29
+ * `mode: schedule` message would replace an already-running schedule.
30
+ * Consumed (and reset) inside `onRollback` so the base library's
31
+ * automatic `doReleaseSchedule → onRollback` step does not actually
32
+ * write the snapshot back during a replacement.
33
+ */
34
+ suppressNextRollbackWrite = false;
18
35
  constructor(opts) {
19
36
  super(opts);
20
37
  this.sunspecClient = opts.sunspecClient;
@@ -23,19 +40,53 @@ export class SunspecBatteryScheduleHandler extends StorageScheduleHandler {
23
40
  this.getSnapshotService = opts.getSnapshotService;
24
41
  this.onErrorCallback = opts.onErrorCallback;
25
42
  }
43
+ /**
44
+ * Override the base-class data-bus router so a schedule-to-schedule
45
+ * replacement marks the next `onRollback` as "skip the write". The base
46
+ * library still owns the actual schedule lifecycle — we just steer one
47
+ * decision inside `onRollback`. `mode: auto` and any path that does not
48
+ * have an active schedule fall through unchanged, so the rollback fires
49
+ * normally there.
50
+ */
51
+ async applyMessage(msg) {
52
+ if (msg.applianceId === this.applianceIdForLog
53
+ && msg.data.mode === EnyoStorageScheduleModeEnum.Schedule
54
+ && this.getActiveEntry() !== undefined) {
55
+ this.suppressNextRollbackWrite = true;
56
+ }
57
+ try {
58
+ await super.applyMessage(msg);
59
+ }
60
+ finally {
61
+ // Defence in depth: if the base class skipped the rollback for
62
+ // any reason (validation error, disposed, etc.), the flag would
63
+ // otherwise stay set and silently swallow the next real rollback.
64
+ this.suppressNextRollbackWrite = false;
65
+ }
66
+ }
26
67
  async onInit(_active) {
68
+ if (this.originalBaseline !== undefined) {
69
+ // Replacement install: keep handing the library the sticky
70
+ // baseline so the persisted snapshot continues to point at the
71
+ // original pre-schedule state. The library will overwrite its
72
+ // storage row with the same value — harmless.
73
+ this.installedWChaMax = this.originalBaseline.wChaMax;
74
+ return { ...this.originalBaseline };
75
+ }
27
76
  const controls = await this.sunspecClient.readBatteryControls(this.unitId);
28
77
  if (!controls) {
29
78
  throw new Error(`SunspecBatteryScheduleHandler ${this.applianceIdForLog}: failed to read pre-schedule controls`);
30
79
  }
31
- this.installedWChaMax = controls.wChaMax;
32
- return {
80
+ const baseline = {
33
81
  storCtlMod: controls.storCtlMod,
34
82
  chaGriSet: controls.chaGriSet,
35
83
  wChaMax: controls.wChaMax,
36
84
  inWRte: controls.inWRte,
37
85
  outWRte: controls.outWRte,
38
86
  };
87
+ this.originalBaseline = baseline;
88
+ this.installedWChaMax = baseline.wChaMax;
89
+ return { ...baseline };
39
90
  }
40
91
  onChange(active, _previous) {
41
92
  void this.applyEntry(active).catch(err => {
@@ -43,6 +94,25 @@ export class SunspecBatteryScheduleHandler extends StorageScheduleHandler {
43
94
  });
44
95
  }
45
96
  onRollback(registers) {
97
+ if (this.suppressNextRollbackWrite) {
98
+ // Schedule-to-schedule replacement. Do NOT write the snapshot
99
+ // back; the new schedule's first `onChange` will own the
100
+ // register state. `installedWChaMax` stays valid (set in
101
+ // onInit) so the very next applyEntry has a baseline to divide
102
+ // against — this is the fix for the
103
+ // "no usable wChaMax baseline (installedWChaMax=undefined)"
104
+ // race seen in production logs.
105
+ this.suppressNextRollbackWrite = false;
106
+ return;
107
+ }
108
+ // Real rollback (mode: auto, dispose, restart recovery). Clear the
109
+ // sticky baseline so the next `onInit` re-reads the device state.
110
+ // Important: clear synchronously here — NOT in the async finally
111
+ // that follows — so a concurrent `onInit` queued behind this one
112
+ // (impossible today, but defensive) cannot observe a half-cleared
113
+ // state.
114
+ this.originalBaseline = undefined;
115
+ this.installedWChaMax = undefined;
46
116
  void (async () => {
47
117
  try {
48
118
  await this.sunspecClient.writeBatteryControls(this.unitId, registers);
@@ -53,9 +123,6 @@ export class SunspecBatteryScheduleHandler extends StorageScheduleHandler {
53
123
  catch (err) {
54
124
  console.error(`SunspecBatteryScheduleHandler ${this.applianceIdForLog}: onRollback failed: ${err}`);
55
125
  }
56
- finally {
57
- this.installedWChaMax = undefined;
58
- }
59
126
  })();
60
127
  }
61
128
  onError(err) {
@@ -178,8 +178,6 @@ export declare class SunspecInverter extends BaseSunspecDevice {
178
178
  * process restarts. Idempotent.
179
179
  */
180
180
  private initSnapshotService;
181
- private handleStartCalibration;
182
- private handleStopCalibration;
183
181
  /**
184
182
  * `onRestore` callback for the inverter's {@link SnapshotService}. Writes only the
185
183
  * subset of writable inverter fields that other commands actually touched during the
@@ -408,8 +406,6 @@ export declare class SunspecBattery extends BaseSunspecDevice {
408
406
  * process restarts. Idempotent.
409
407
  */
410
408
  private initSnapshotService;
411
- private handleStartCalibration;
412
- private handleStopCalibration;
413
409
  /**
414
410
  * `onRestore` callback for the battery's {@link SnapshotService}. Writes only the
415
411
  * subset of writable battery fields touched during the calibration. Errors are
@@ -432,13 +428,6 @@ export declare class SunspecMeter extends BaseSunspecDevice {
432
428
  * Disconnect from the meter and update appliance state
433
429
  */
434
430
  disconnect(): Promise<void>;
435
- /**
436
- * Meter does not implement calibration; it only subscribes to Start/StopCalibrationV1 to
437
- * answer NotSupported (per the data-bus contract that every command must be acknowledged).
438
- */
439
- startDataBusListening(): void;
440
- stopDataBusListening(): void;
441
- private handleMeterCommand;
442
431
  /**
443
432
  * Update meter data and return data bus messages
444
433
  */