@enyo-energy/sunspec-sdk 0.0.73 → 0.0.74
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 +5 -1
- package/dist/cjs/sunspec-battery-schedule-handler.cjs +72 -5
- package/dist/cjs/sunspec-battery-schedule-handler.d.cts +27 -0
- package/dist/cjs/sunspec-modbus-client.cjs +12 -5
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/sunspec-battery-schedule-handler.d.ts +27 -0
- package/dist/sunspec-battery-schedule-handler.js +73 -6
- package/dist/sunspec-modbus-client.js +12 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -332,7 +332,11 @@ See [`@enyo-energy/appliance-calibration`'s README](https://www.npmjs.com/packag
|
|
|
332
332
|
3. Activates the first entry immediately, then advances to subsequent entries as their `seconds` offsets elapse on a 1-second tick (driven by `EnergyApp.useInterval()`).
|
|
333
333
|
4. Each `Charge` entry writes `chaGriSet=GRID`, `inWRte = powerW / installedWChaMax × 100`, `storCtlMod=CHARGE`.
|
|
334
334
|
5. Each `Discharge` entry writes `chaGriSet=PV`, `outWRte = powerW / installedWChaMax × 100`, `storCtlMod=DISCHARGE`.
|
|
335
|
-
6.
|
|
335
|
+
6. Restores the snapshotted pre-schedule registers (`storCtlMod`, `chaGriSet`, `wChaMax`, `inWRte`, `outWRte`) **only on `mode: auto`, `disconnect()`, or process-restart recovery** — not when one `mode: schedule` is replaced by another. Schedule-to-schedule replacement keeps the device on the active register set and lets the new schedule's first entry take over directly, so consecutive schedules don't leak a `storCtlMod=AUTO` (`0x3`) write between them. The pre-schedule snapshot stays sticky across every replacement until one of the three terminal events fires.
|
|
336
|
+
|
|
337
|
+
### Register write order
|
|
338
|
+
|
|
339
|
+
`writeBatteryControls` issues each register write sequentially in this order so the device never sees a stale-parameter window when the control mode changes: `chaGriSet → wChaMax → inWRte → outWRte → minRsvPct → storCtlMod`. Source pin and the limit/rate parameters land first; the control mode is written last so the device only "starts acting" once every governing value is fresh.
|
|
336
340
|
|
|
337
341
|
### Power cap
|
|
338
342
|
|
|
@@ -18,6 +18,23 @@ class SunspecBatteryScheduleHandler extends storage_schedule_handler_js_1.Storag
|
|
|
18
18
|
getSnapshotService;
|
|
19
19
|
onErrorCallback;
|
|
20
20
|
installedWChaMax;
|
|
21
|
+
/**
|
|
22
|
+
* Sticky pre-schedule snapshot, captured once on the first `onInit` and
|
|
23
|
+
* held across every subsequent schedule-to-schedule replacement. Cleared
|
|
24
|
+
* only when a real rollback fires (`mode: auto` from the data bus or
|
|
25
|
+
* `dispose`). Lets the eventual restore write the *true* baseline rather
|
|
26
|
+
* than the last-active-entry register set that the device happens to be
|
|
27
|
+
* in at the moment of replacement.
|
|
28
|
+
*/
|
|
29
|
+
originalBaseline;
|
|
30
|
+
/**
|
|
31
|
+
* Set inside the overridden `applyMessage` when an incoming
|
|
32
|
+
* `mode: schedule` message would replace an already-running schedule.
|
|
33
|
+
* Consumed (and reset) inside `onRollback` so the base library's
|
|
34
|
+
* automatic `doReleaseSchedule → onRollback` step does not actually
|
|
35
|
+
* write the snapshot back during a replacement.
|
|
36
|
+
*/
|
|
37
|
+
suppressNextRollbackWrite = false;
|
|
21
38
|
constructor(opts) {
|
|
22
39
|
super(opts);
|
|
23
40
|
this.sunspecClient = opts.sunspecClient;
|
|
@@ -26,19 +43,53 @@ class SunspecBatteryScheduleHandler extends storage_schedule_handler_js_1.Storag
|
|
|
26
43
|
this.getSnapshotService = opts.getSnapshotService;
|
|
27
44
|
this.onErrorCallback = opts.onErrorCallback;
|
|
28
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Override the base-class data-bus router so a schedule-to-schedule
|
|
48
|
+
* replacement marks the next `onRollback` as "skip the write". The base
|
|
49
|
+
* library still owns the actual schedule lifecycle — we just steer one
|
|
50
|
+
* decision inside `onRollback`. `mode: auto` and any path that does not
|
|
51
|
+
* have an active schedule fall through unchanged, so the rollback fires
|
|
52
|
+
* normally there.
|
|
53
|
+
*/
|
|
54
|
+
async applyMessage(msg) {
|
|
55
|
+
if (msg.applianceId === this.applianceIdForLog
|
|
56
|
+
&& msg.data.mode === enyo_data_bus_value_js_1.EnyoStorageScheduleModeEnum.Schedule
|
|
57
|
+
&& this.getActiveEntry() !== undefined) {
|
|
58
|
+
this.suppressNextRollbackWrite = true;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
await super.applyMessage(msg);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
// Defence in depth: if the base class skipped the rollback for
|
|
65
|
+
// any reason (validation error, disposed, etc.), the flag would
|
|
66
|
+
// otherwise stay set and silently swallow the next real rollback.
|
|
67
|
+
this.suppressNextRollbackWrite = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
29
70
|
async onInit(_active) {
|
|
71
|
+
if (this.originalBaseline !== undefined) {
|
|
72
|
+
// Replacement install: keep handing the library the sticky
|
|
73
|
+
// baseline so the persisted snapshot continues to point at the
|
|
74
|
+
// original pre-schedule state. The library will overwrite its
|
|
75
|
+
// storage row with the same value — harmless.
|
|
76
|
+
this.installedWChaMax = this.originalBaseline.wChaMax;
|
|
77
|
+
return { ...this.originalBaseline };
|
|
78
|
+
}
|
|
30
79
|
const controls = await this.sunspecClient.readBatteryControls(this.unitId);
|
|
31
80
|
if (!controls) {
|
|
32
81
|
throw new Error(`SunspecBatteryScheduleHandler ${this.applianceIdForLog}: failed to read pre-schedule controls`);
|
|
33
82
|
}
|
|
34
|
-
|
|
35
|
-
return {
|
|
83
|
+
const baseline = {
|
|
36
84
|
storCtlMod: controls.storCtlMod,
|
|
37
85
|
chaGriSet: controls.chaGriSet,
|
|
38
86
|
wChaMax: controls.wChaMax,
|
|
39
87
|
inWRte: controls.inWRte,
|
|
40
88
|
outWRte: controls.outWRte,
|
|
41
89
|
};
|
|
90
|
+
this.originalBaseline = baseline;
|
|
91
|
+
this.installedWChaMax = baseline.wChaMax;
|
|
92
|
+
return { ...baseline };
|
|
42
93
|
}
|
|
43
94
|
onChange(active, _previous) {
|
|
44
95
|
void this.applyEntry(active).catch(err => {
|
|
@@ -46,6 +97,25 @@ class SunspecBatteryScheduleHandler extends storage_schedule_handler_js_1.Storag
|
|
|
46
97
|
});
|
|
47
98
|
}
|
|
48
99
|
onRollback(registers) {
|
|
100
|
+
if (this.suppressNextRollbackWrite) {
|
|
101
|
+
// Schedule-to-schedule replacement. Do NOT write the snapshot
|
|
102
|
+
// back; the new schedule's first `onChange` will own the
|
|
103
|
+
// register state. `installedWChaMax` stays valid (set in
|
|
104
|
+
// onInit) so the very next applyEntry has a baseline to divide
|
|
105
|
+
// against — this is the fix for the
|
|
106
|
+
// "no usable wChaMax baseline (installedWChaMax=undefined)"
|
|
107
|
+
// race seen in production logs.
|
|
108
|
+
this.suppressNextRollbackWrite = false;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Real rollback (mode: auto, dispose, restart recovery). Clear the
|
|
112
|
+
// sticky baseline so the next `onInit` re-reads the device state.
|
|
113
|
+
// Important: clear synchronously here — NOT in the async finally
|
|
114
|
+
// that follows — so a concurrent `onInit` queued behind this one
|
|
115
|
+
// (impossible today, but defensive) cannot observe a half-cleared
|
|
116
|
+
// state.
|
|
117
|
+
this.originalBaseline = undefined;
|
|
118
|
+
this.installedWChaMax = undefined;
|
|
49
119
|
void (async () => {
|
|
50
120
|
try {
|
|
51
121
|
await this.sunspecClient.writeBatteryControls(this.unitId, registers);
|
|
@@ -56,9 +126,6 @@ class SunspecBatteryScheduleHandler extends storage_schedule_handler_js_1.Storag
|
|
|
56
126
|
catch (err) {
|
|
57
127
|
console.error(`SunspecBatteryScheduleHandler ${this.applianceIdForLog}: onRollback failed: ${err}`);
|
|
58
128
|
}
|
|
59
|
-
finally {
|
|
60
|
-
this.installedWChaMax = undefined;
|
|
61
|
-
}
|
|
62
129
|
})();
|
|
63
130
|
}
|
|
64
131
|
onError(err) {
|
|
@@ -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.cjs";
|
|
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;
|
|
@@ -1666,11 +1666,11 @@ class SunspecModbusClient {
|
|
|
1666
1666
|
const baseAddr = model.address;
|
|
1667
1667
|
console.log(`Writing Battery Controls to Model 124 at base address: ${baseAddr} (unit ${unitId})`);
|
|
1668
1668
|
try {
|
|
1669
|
-
// Write
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1669
|
+
// Write order: source pin and parameter writes land BEFORE the
|
|
1670
|
+
// control mode so the device only "starts acting" once every
|
|
1671
|
+
// value that governs the action is fresh. The reverse order
|
|
1672
|
+
// leaks a Modbus RTT-sized window where storCtlMod sees stale
|
|
1673
|
+
// chaGriSet / wChaMax / inWRte / outWRte.
|
|
1674
1674
|
// Write charge source setting (Register 17)
|
|
1675
1675
|
if (controls.chaGriSet !== undefined) {
|
|
1676
1676
|
await this.writeRegisterValue(unitId, baseAddr + 17, controls.chaGriSet, 'uint16');
|
|
@@ -1708,6 +1708,13 @@ class SunspecModbusClient {
|
|
|
1708
1708
|
await this.writeRegisterValue(unitId, baseAddr + 7, scaledValue, 'uint16');
|
|
1709
1709
|
console.log(`Set minimum reserve to ${controls.minRsvPct}% (scaled: ${scaledValue})`);
|
|
1710
1710
|
}
|
|
1711
|
+
// Storage control mode (Register 5) — written LAST so all
|
|
1712
|
+
// governing parameters are already in place when the device
|
|
1713
|
+
// transitions into the new mode.
|
|
1714
|
+
if (controls.storCtlMod !== undefined) {
|
|
1715
|
+
await this.writeRegisterValue(unitId, baseAddr + 5, controls.storCtlMod, 'uint16');
|
|
1716
|
+
console.log(`Set storage control mode to 0x${controls.storCtlMod.toString(16)}`);
|
|
1717
|
+
}
|
|
1711
1718
|
console.log('Battery controls written successfully');
|
|
1712
1719
|
return true;
|
|
1713
1720
|
}
|
package/dist/cjs/version.cjs
CHANGED
|
@@ -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.
|
|
12
|
+
exports.SDK_VERSION = '0.0.74';
|
|
13
13
|
/**
|
|
14
14
|
* Gets the current SDK version.
|
|
15
15
|
* @returns The semantic version string of the SDK
|
package/dist/cjs/version.d.cts
CHANGED
|
@@ -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
|
-
|
|
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) {
|
|
@@ -1661,11 +1661,11 @@ export class SunspecModbusClient {
|
|
|
1661
1661
|
const baseAddr = model.address;
|
|
1662
1662
|
console.log(`Writing Battery Controls to Model 124 at base address: ${baseAddr} (unit ${unitId})`);
|
|
1663
1663
|
try {
|
|
1664
|
-
// Write
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1664
|
+
// Write order: source pin and parameter writes land BEFORE the
|
|
1665
|
+
// control mode so the device only "starts acting" once every
|
|
1666
|
+
// value that governs the action is fresh. The reverse order
|
|
1667
|
+
// leaks a Modbus RTT-sized window where storCtlMod sees stale
|
|
1668
|
+
// chaGriSet / wChaMax / inWRte / outWRte.
|
|
1669
1669
|
// Write charge source setting (Register 17)
|
|
1670
1670
|
if (controls.chaGriSet !== undefined) {
|
|
1671
1671
|
await this.writeRegisterValue(unitId, baseAddr + 17, controls.chaGriSet, 'uint16');
|
|
@@ -1703,6 +1703,13 @@ export class SunspecModbusClient {
|
|
|
1703
1703
|
await this.writeRegisterValue(unitId, baseAddr + 7, scaledValue, 'uint16');
|
|
1704
1704
|
console.log(`Set minimum reserve to ${controls.minRsvPct}% (scaled: ${scaledValue})`);
|
|
1705
1705
|
}
|
|
1706
|
+
// Storage control mode (Register 5) — written LAST so all
|
|
1707
|
+
// governing parameters are already in place when the device
|
|
1708
|
+
// transitions into the new mode.
|
|
1709
|
+
if (controls.storCtlMod !== undefined) {
|
|
1710
|
+
await this.writeRegisterValue(unitId, baseAddr + 5, controls.storCtlMod, 'uint16');
|
|
1711
|
+
console.log(`Set storage control mode to 0x${controls.storCtlMod.toString(16)}`);
|
|
1712
|
+
}
|
|
1706
1713
|
console.log('Battery controls written successfully');
|
|
1707
1714
|
return true;
|
|
1708
1715
|
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED