@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.
@@ -0,0 +1,67 @@
1
+ import { EnergyAppStorage } from "@enyo-energy/energy-app-sdk/dist/packages/energy-app-storage.js";
2
+ import type { SunspecBatteryControls, SunspecInverterControls } from "./sunspec-interfaces.js";
3
+ export type CalibrationDeviceType = "inverter" | "battery";
4
+ /**
5
+ * Persisted snapshot of a device's writable control registers, taken at the start of a
6
+ * calibration. Used to roll the device back when calibration ends.
7
+ *
8
+ * `modifiedFields` records which control-block field names were touched by *other* write
9
+ * commands (SetInverterFeedInLimitV1, StartStorageGridChargeV1, etc.) while the calibration
10
+ * was active — on stop, only those fields are written back, leaving fields the calibration
11
+ * itself didn't disturb alone.
12
+ */
13
+ export interface CalibrationSnapshot {
14
+ applianceId: string;
15
+ deviceType: CalibrationDeviceType;
16
+ unitId: number;
17
+ startedAtIso: string;
18
+ inverterControls?: SunspecInverterControls;
19
+ batteryControls?: SunspecBatteryControls;
20
+ modifiedFields: string[];
21
+ }
22
+ /** Calibration is capped at 5 minutes of wall time, including across restarts. */
23
+ export declare const CALIBRATION_AUTO_STOP_MS: number;
24
+ export type CalibrationRestoreReason = "stop" | "auto";
25
+ export type CalibrationRestoreCallback = (snapshot: CalibrationSnapshot, reason: CalibrationRestoreReason) => Promise<void>;
26
+ /**
27
+ * Per-appliance service that snapshots a SunSpec device's writable control registers when a
28
+ * calibration starts, tracks which registers other commands mutate while it is active, and
29
+ * restores those registers when calibration stops (either explicitly or via the 5-minute
30
+ * auto-stop timer).
31
+ *
32
+ * Snapshots are persisted via `EnergyAppStorage` so a crash/restart mid-calibration does not
33
+ * leave the device in a calibration state without a rollback. On `initialize()`, any stored
34
+ * snapshot is reloaded; the auto-stop deadline is computed from `startedAtIso` so total
35
+ * calibration time remains capped at `autoStopMs` across restarts.
36
+ */
37
+ export declare class CalibrationSnapshotService {
38
+ private readonly storage;
39
+ private readonly applianceId;
40
+ private readonly onRestore;
41
+ private readonly autoStopMs;
42
+ private snapshot?;
43
+ private autoStopTimer?;
44
+ constructor(storage: EnergyAppStorage, applianceId: string, onRestore: CalibrationRestoreCallback, autoStopMs?: number);
45
+ private storageKey;
46
+ initialize(): Promise<void>;
47
+ isCalibrating(): boolean;
48
+ getSnapshot(): CalibrationSnapshot | undefined;
49
+ startCalibration(input: {
50
+ deviceType: CalibrationDeviceType;
51
+ unitId: number;
52
+ inverterControls?: SunspecInverterControls;
53
+ batteryControls?: SunspecBatteryControls;
54
+ }): Promise<void>;
55
+ recordModification(fields: string[]): Promise<void>;
56
+ /**
57
+ * Clear the timer, remove the snapshot from memory + storage, and return the snapshot so
58
+ * the caller can restore the modified registers. Returns undefined if no calibration was
59
+ * active.
60
+ */
61
+ stopCalibration(): Promise<CalibrationSnapshot | undefined>;
62
+ private remainingMs;
63
+ private scheduleAutoStop;
64
+ private clearAutoStopTimer;
65
+ private fireAutoStop;
66
+ private persist;
67
+ }
@@ -0,0 +1,160 @@
1
+ /** Calibration is capped at 5 minutes of wall time, including across restarts. */
2
+ export const CALIBRATION_AUTO_STOP_MS = 5 * 60 * 1000;
3
+ /**
4
+ * Per-appliance service that snapshots a SunSpec device's writable control registers when a
5
+ * calibration starts, tracks which registers other commands mutate while it is active, and
6
+ * restores those registers when calibration stops (either explicitly or via the 5-minute
7
+ * auto-stop timer).
8
+ *
9
+ * Snapshots are persisted via `EnergyAppStorage` so a crash/restart mid-calibration does not
10
+ * leave the device in a calibration state without a rollback. On `initialize()`, any stored
11
+ * snapshot is reloaded; the auto-stop deadline is computed from `startedAtIso` so total
12
+ * calibration time remains capped at `autoStopMs` across restarts.
13
+ */
14
+ export class CalibrationSnapshotService {
15
+ storage;
16
+ applianceId;
17
+ onRestore;
18
+ autoStopMs;
19
+ snapshot;
20
+ autoStopTimer;
21
+ constructor(storage, applianceId, onRestore, autoStopMs = CALIBRATION_AUTO_STOP_MS) {
22
+ this.storage = storage;
23
+ this.applianceId = applianceId;
24
+ this.onRestore = onRestore;
25
+ this.autoStopMs = autoStopMs;
26
+ }
27
+ storageKey() {
28
+ return `sunspec-calibration-snapshot-${this.applianceId}`;
29
+ }
30
+ async initialize() {
31
+ try {
32
+ const loaded = await this.storage.load(this.storageKey());
33
+ if (!loaded) {
34
+ return;
35
+ }
36
+ this.snapshot = loaded;
37
+ console.log(`CalibrationSnapshotService ${this.applianceId}: loaded persisted snapshot (started ${loaded.startedAtIso}, modifiedFields=[${loaded.modifiedFields.join(', ')}])`);
38
+ const remainingMs = this.remainingMs(loaded);
39
+ if (remainingMs <= 0) {
40
+ console.warn(`CalibrationSnapshotService ${this.applianceId}: snapshot exceeded ${this.autoStopMs}ms deadline — auto-restoring immediately`);
41
+ await this.fireAutoStop();
42
+ }
43
+ else {
44
+ this.scheduleAutoStop(remainingMs);
45
+ }
46
+ }
47
+ catch (error) {
48
+ console.error(`CalibrationSnapshotService ${this.applianceId}: failed to load persisted snapshot: ${error}`);
49
+ }
50
+ }
51
+ isCalibrating() {
52
+ return this.snapshot !== undefined;
53
+ }
54
+ getSnapshot() {
55
+ return this.snapshot;
56
+ }
57
+ async startCalibration(input) {
58
+ this.clearAutoStopTimer();
59
+ this.snapshot = {
60
+ applianceId: this.applianceId,
61
+ deviceType: input.deviceType,
62
+ unitId: input.unitId,
63
+ startedAtIso: new Date().toISOString(),
64
+ inverterControls: input.inverterControls,
65
+ batteryControls: input.batteryControls,
66
+ modifiedFields: [],
67
+ };
68
+ await this.persist();
69
+ this.scheduleAutoStop(this.autoStopMs);
70
+ console.log(`CalibrationSnapshotService ${this.applianceId}: calibration started for ${input.deviceType} (auto-stop in ${this.autoStopMs}ms)`);
71
+ }
72
+ async recordModification(fields) {
73
+ if (!this.snapshot) {
74
+ return;
75
+ }
76
+ let added = false;
77
+ for (const field of fields) {
78
+ if (!this.snapshot.modifiedFields.includes(field)) {
79
+ this.snapshot.modifiedFields.push(field);
80
+ added = true;
81
+ }
82
+ }
83
+ if (added) {
84
+ await this.persist();
85
+ console.log(`CalibrationSnapshotService ${this.applianceId}: tracked modified fields [${this.snapshot.modifiedFields.join(', ')}]`);
86
+ }
87
+ }
88
+ /**
89
+ * Clear the timer, remove the snapshot from memory + storage, and return the snapshot so
90
+ * the caller can restore the modified registers. Returns undefined if no calibration was
91
+ * active.
92
+ */
93
+ async stopCalibration() {
94
+ this.clearAutoStopTimer();
95
+ const snap = this.snapshot;
96
+ this.snapshot = undefined;
97
+ if (snap) {
98
+ try {
99
+ await this.storage.remove(this.storageKey());
100
+ }
101
+ catch (error) {
102
+ console.error(`CalibrationSnapshotService ${this.applianceId}: failed to remove persisted snapshot: ${error}`);
103
+ }
104
+ }
105
+ return snap;
106
+ }
107
+ remainingMs(snap) {
108
+ const startedAtMs = Date.parse(snap.startedAtIso);
109
+ if (Number.isNaN(startedAtMs)) {
110
+ return this.autoStopMs;
111
+ }
112
+ return startedAtMs + this.autoStopMs - Date.now();
113
+ }
114
+ scheduleAutoStop(delayMs) {
115
+ this.clearAutoStopTimer();
116
+ this.autoStopTimer = setTimeout(() => {
117
+ void this.fireAutoStop();
118
+ }, delayMs);
119
+ if (typeof this.autoStopTimer.unref === "function") {
120
+ this.autoStopTimer.unref();
121
+ }
122
+ }
123
+ clearAutoStopTimer() {
124
+ if (this.autoStopTimer) {
125
+ clearTimeout(this.autoStopTimer);
126
+ this.autoStopTimer = undefined;
127
+ }
128
+ }
129
+ async fireAutoStop() {
130
+ const snap = this.snapshot;
131
+ this.snapshot = undefined;
132
+ this.clearAutoStopTimer();
133
+ if (!snap) {
134
+ return;
135
+ }
136
+ try {
137
+ await this.storage.remove(this.storageKey());
138
+ }
139
+ catch (error) {
140
+ console.error(`CalibrationSnapshotService ${this.applianceId}: failed to remove persisted snapshot during auto-stop: ${error}`);
141
+ }
142
+ try {
143
+ await this.onRestore(snap, "auto");
144
+ }
145
+ catch (error) {
146
+ console.error(`CalibrationSnapshotService ${this.applianceId}: auto-stop restore failed: ${error}`);
147
+ }
148
+ }
149
+ async persist() {
150
+ if (!this.snapshot) {
151
+ return;
152
+ }
153
+ try {
154
+ await this.storage.save(this.storageKey(), this.snapshot);
155
+ }
156
+ catch (error) {
157
+ console.error(`CalibrationSnapshotService ${this.applianceId}: failed to persist snapshot: ${error}`);
158
+ }
159
+ }
160
+ }
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CalibrationSnapshotService = exports.CALIBRATION_AUTO_STOP_MS = void 0;
4
+ /** Calibration is capped at 5 minutes of wall time, including across restarts. */
5
+ exports.CALIBRATION_AUTO_STOP_MS = 5 * 60 * 1000;
6
+ /**
7
+ * Per-appliance service that snapshots a SunSpec device's writable control registers when a
8
+ * calibration starts, tracks which registers other commands mutate while it is active, and
9
+ * restores those registers when calibration stops (either explicitly or via the 5-minute
10
+ * auto-stop timer).
11
+ *
12
+ * Snapshots are persisted via `EnergyAppStorage` so a crash/restart mid-calibration does not
13
+ * leave the device in a calibration state without a rollback. On `initialize()`, any stored
14
+ * snapshot is reloaded; the auto-stop deadline is computed from `startedAtIso` so total
15
+ * calibration time remains capped at `autoStopMs` across restarts.
16
+ */
17
+ class CalibrationSnapshotService {
18
+ storage;
19
+ applianceId;
20
+ onRestore;
21
+ autoStopMs;
22
+ snapshot;
23
+ autoStopTimer;
24
+ constructor(storage, applianceId, onRestore, autoStopMs = exports.CALIBRATION_AUTO_STOP_MS) {
25
+ this.storage = storage;
26
+ this.applianceId = applianceId;
27
+ this.onRestore = onRestore;
28
+ this.autoStopMs = autoStopMs;
29
+ }
30
+ storageKey() {
31
+ return `sunspec-calibration-snapshot-${this.applianceId}`;
32
+ }
33
+ async initialize() {
34
+ try {
35
+ const loaded = await this.storage.load(this.storageKey());
36
+ if (!loaded) {
37
+ return;
38
+ }
39
+ this.snapshot = loaded;
40
+ console.log(`CalibrationSnapshotService ${this.applianceId}: loaded persisted snapshot (started ${loaded.startedAtIso}, modifiedFields=[${loaded.modifiedFields.join(', ')}])`);
41
+ const remainingMs = this.remainingMs(loaded);
42
+ if (remainingMs <= 0) {
43
+ console.warn(`CalibrationSnapshotService ${this.applianceId}: snapshot exceeded ${this.autoStopMs}ms deadline — auto-restoring immediately`);
44
+ await this.fireAutoStop();
45
+ }
46
+ else {
47
+ this.scheduleAutoStop(remainingMs);
48
+ }
49
+ }
50
+ catch (error) {
51
+ console.error(`CalibrationSnapshotService ${this.applianceId}: failed to load persisted snapshot: ${error}`);
52
+ }
53
+ }
54
+ isCalibrating() {
55
+ return this.snapshot !== undefined;
56
+ }
57
+ getSnapshot() {
58
+ return this.snapshot;
59
+ }
60
+ async startCalibration(input) {
61
+ this.clearAutoStopTimer();
62
+ this.snapshot = {
63
+ applianceId: this.applianceId,
64
+ deviceType: input.deviceType,
65
+ unitId: input.unitId,
66
+ startedAtIso: new Date().toISOString(),
67
+ inverterControls: input.inverterControls,
68
+ batteryControls: input.batteryControls,
69
+ modifiedFields: [],
70
+ };
71
+ await this.persist();
72
+ this.scheduleAutoStop(this.autoStopMs);
73
+ console.log(`CalibrationSnapshotService ${this.applianceId}: calibration started for ${input.deviceType} (auto-stop in ${this.autoStopMs}ms)`);
74
+ }
75
+ async recordModification(fields) {
76
+ if (!this.snapshot) {
77
+ return;
78
+ }
79
+ let added = false;
80
+ for (const field of fields) {
81
+ if (!this.snapshot.modifiedFields.includes(field)) {
82
+ this.snapshot.modifiedFields.push(field);
83
+ added = true;
84
+ }
85
+ }
86
+ if (added) {
87
+ await this.persist();
88
+ console.log(`CalibrationSnapshotService ${this.applianceId}: tracked modified fields [${this.snapshot.modifiedFields.join(', ')}]`);
89
+ }
90
+ }
91
+ /**
92
+ * Clear the timer, remove the snapshot from memory + storage, and return the snapshot so
93
+ * the caller can restore the modified registers. Returns undefined if no calibration was
94
+ * active.
95
+ */
96
+ async stopCalibration() {
97
+ this.clearAutoStopTimer();
98
+ const snap = this.snapshot;
99
+ this.snapshot = undefined;
100
+ if (snap) {
101
+ try {
102
+ await this.storage.remove(this.storageKey());
103
+ }
104
+ catch (error) {
105
+ console.error(`CalibrationSnapshotService ${this.applianceId}: failed to remove persisted snapshot: ${error}`);
106
+ }
107
+ }
108
+ return snap;
109
+ }
110
+ remainingMs(snap) {
111
+ const startedAtMs = Date.parse(snap.startedAtIso);
112
+ if (Number.isNaN(startedAtMs)) {
113
+ return this.autoStopMs;
114
+ }
115
+ return startedAtMs + this.autoStopMs - Date.now();
116
+ }
117
+ scheduleAutoStop(delayMs) {
118
+ this.clearAutoStopTimer();
119
+ this.autoStopTimer = setTimeout(() => {
120
+ void this.fireAutoStop();
121
+ }, delayMs);
122
+ if (typeof this.autoStopTimer.unref === "function") {
123
+ this.autoStopTimer.unref();
124
+ }
125
+ }
126
+ clearAutoStopTimer() {
127
+ if (this.autoStopTimer) {
128
+ clearTimeout(this.autoStopTimer);
129
+ this.autoStopTimer = undefined;
130
+ }
131
+ }
132
+ async fireAutoStop() {
133
+ const snap = this.snapshot;
134
+ this.snapshot = undefined;
135
+ this.clearAutoStopTimer();
136
+ if (!snap) {
137
+ return;
138
+ }
139
+ try {
140
+ await this.storage.remove(this.storageKey());
141
+ }
142
+ catch (error) {
143
+ console.error(`CalibrationSnapshotService ${this.applianceId}: failed to remove persisted snapshot during auto-stop: ${error}`);
144
+ }
145
+ try {
146
+ await this.onRestore(snap, "auto");
147
+ }
148
+ catch (error) {
149
+ console.error(`CalibrationSnapshotService ${this.applianceId}: auto-stop restore failed: ${error}`);
150
+ }
151
+ }
152
+ async persist() {
153
+ if (!this.snapshot) {
154
+ return;
155
+ }
156
+ try {
157
+ await this.storage.save(this.storageKey(), this.snapshot);
158
+ }
159
+ catch (error) {
160
+ console.error(`CalibrationSnapshotService ${this.applianceId}: failed to persist snapshot: ${error}`);
161
+ }
162
+ }
163
+ }
164
+ exports.CalibrationSnapshotService = CalibrationSnapshotService;
@@ -0,0 +1,67 @@
1
+ import { EnergyAppStorage } from "@enyo-energy/energy-app-sdk/dist/packages/energy-app-storage.js";
2
+ import type { SunspecBatteryControls, SunspecInverterControls } from "./sunspec-interfaces.cjs";
3
+ export type CalibrationDeviceType = "inverter" | "battery";
4
+ /**
5
+ * Persisted snapshot of a device's writable control registers, taken at the start of a
6
+ * calibration. Used to roll the device back when calibration ends.
7
+ *
8
+ * `modifiedFields` records which control-block field names were touched by *other* write
9
+ * commands (SetInverterFeedInLimitV1, StartStorageGridChargeV1, etc.) while the calibration
10
+ * was active — on stop, only those fields are written back, leaving fields the calibration
11
+ * itself didn't disturb alone.
12
+ */
13
+ export interface CalibrationSnapshot {
14
+ applianceId: string;
15
+ deviceType: CalibrationDeviceType;
16
+ unitId: number;
17
+ startedAtIso: string;
18
+ inverterControls?: SunspecInverterControls;
19
+ batteryControls?: SunspecBatteryControls;
20
+ modifiedFields: string[];
21
+ }
22
+ /** Calibration is capped at 5 minutes of wall time, including across restarts. */
23
+ export declare const CALIBRATION_AUTO_STOP_MS: number;
24
+ export type CalibrationRestoreReason = "stop" | "auto";
25
+ export type CalibrationRestoreCallback = (snapshot: CalibrationSnapshot, reason: CalibrationRestoreReason) => Promise<void>;
26
+ /**
27
+ * Per-appliance service that snapshots a SunSpec device's writable control registers when a
28
+ * calibration starts, tracks which registers other commands mutate while it is active, and
29
+ * restores those registers when calibration stops (either explicitly or via the 5-minute
30
+ * auto-stop timer).
31
+ *
32
+ * Snapshots are persisted via `EnergyAppStorage` so a crash/restart mid-calibration does not
33
+ * leave the device in a calibration state without a rollback. On `initialize()`, any stored
34
+ * snapshot is reloaded; the auto-stop deadline is computed from `startedAtIso` so total
35
+ * calibration time remains capped at `autoStopMs` across restarts.
36
+ */
37
+ export declare class CalibrationSnapshotService {
38
+ private readonly storage;
39
+ private readonly applianceId;
40
+ private readonly onRestore;
41
+ private readonly autoStopMs;
42
+ private snapshot?;
43
+ private autoStopTimer?;
44
+ constructor(storage: EnergyAppStorage, applianceId: string, onRestore: CalibrationRestoreCallback, autoStopMs?: number);
45
+ private storageKey;
46
+ initialize(): Promise<void>;
47
+ isCalibrating(): boolean;
48
+ getSnapshot(): CalibrationSnapshot | undefined;
49
+ startCalibration(input: {
50
+ deviceType: CalibrationDeviceType;
51
+ unitId: number;
52
+ inverterControls?: SunspecInverterControls;
53
+ batteryControls?: SunspecBatteryControls;
54
+ }): Promise<void>;
55
+ recordModification(fields: string[]): Promise<void>;
56
+ /**
57
+ * Clear the timer, remove the snapshot from memory + storage, and return the snapshot so
58
+ * the caller can restore the modified registers. Returns undefined if no calibration was
59
+ * active.
60
+ */
61
+ stopCalibration(): Promise<CalibrationSnapshot | undefined>;
62
+ private remainingMs;
63
+ private scheduleAutoStop;
64
+ private clearAutoStopTimer;
65
+ private fireAutoStop;
66
+ private persist;
67
+ }
@@ -18,6 +18,7 @@ exports.getSdkVersion = exports.SDK_VERSION = exports.ConnectionRetryManager = v
18
18
  __exportStar(require("./sunspec-interfaces.cjs"), exports);
19
19
  __exportStar(require("./sunspec-devices.cjs"), exports);
20
20
  __exportStar(require("./sunspec-modbus-client.cjs"), exports);
21
+ __exportStar(require("./calibration-snapshot-service.cjs"), exports);
21
22
  var connection_retry_manager_js_1 = require("./connection-retry-manager.cjs");
22
23
  Object.defineProperty(exports, "ConnectionRetryManager", { enumerable: true, get: function () { return connection_retry_manager_js_1.ConnectionRetryManager; } });
23
24
  var version_js_1 = require("./version.cjs");
@@ -1,5 +1,6 @@
1
1
  export * from './sunspec-interfaces.cjs';
2
2
  export * from './sunspec-devices.cjs';
3
3
  export * from './sunspec-modbus-client.cjs';
4
+ export * from './calibration-snapshot-service.cjs';
4
5
  export { ConnectionRetryManager } from './connection-retry-manager.cjs';
5
6
  export { SDK_VERSION, getSdkVersion } from './version.cjs';