@enyo-energy/sunspec-sdk 0.0.1

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,111 @@
1
+ import { type SunspecInverterControls, type SunspecInverterData, type SunspecInverterSettings, type SunspecMeterData, type SunspecModel, type SunspecMPPTData } from "./sunspec-interfaces.cjs";
2
+ import { IConnectionHealth } from "@enyo-energy/energy-app-sdk/dist/implementations/modbus/interfaces.js";
3
+ import { EnergyApp } from "@enyo-energy/energy-app-sdk";
4
+ export declare class SunspecModbusClient {
5
+ private energyApp;
6
+ private modbusClient;
7
+ private discoveredModels;
8
+ private scaleFactors;
9
+ private connected;
10
+ private baseAddress;
11
+ private connectionHealth;
12
+ private faultTolerantReader;
13
+ constructor(energyApp: EnergyApp);
14
+ /**
15
+ * Connect to Modbus device
16
+ */
17
+ connect(host: string, port?: number, unitId?: number): Promise<void>;
18
+ /**
19
+ * Disconnect from Modbus device
20
+ */
21
+ disconnect(): Promise<void>;
22
+ /**
23
+ * Discover all available Sunspec models
24
+ * Scans through the address space starting at 40001
25
+ */
26
+ discoverModels(): Promise<Map<number, SunspecModel>>;
27
+ /**
28
+ * Find a specific model by ID
29
+ */
30
+ findModel(modelId: number): SunspecModel | undefined;
31
+ /**
32
+ * Read a register value and apply scale factor
33
+ */
34
+ readRegisterWithScaleFactor(valueAddress: number, scaleFactorAddress?: number, quantity?: number): Promise<number>;
35
+ /**
36
+ * Convert unsigned 16-bit value to signed
37
+ */
38
+ private convertToSigned16;
39
+ /**
40
+ * Helper to clean string values by removing null characters
41
+ */
42
+ private cleanString;
43
+ /**
44
+ * Helper to read register value(s) using the fault-tolerant reader with data type conversion
45
+ */
46
+ private readRegisterValue;
47
+ /**
48
+ * Read inverter data from Model 103 (Three Phase)
49
+ */
50
+ readInverterData(): Promise<SunspecInverterData | null>;
51
+ /**
52
+ * Read single phase inverter data (Model 101)
53
+ */
54
+ private readSinglePhaseInverterData;
55
+ /**
56
+ * Read inverter scale factors
57
+ */
58
+ private readInverterScaleFactors;
59
+ /**
60
+ * Apply scale factor to a value
61
+ */
62
+ private applyScaleFactor;
63
+ /**
64
+ * Read MPPT data from Model 160
65
+ */
66
+ readMPPTData(moduleId?: number): Promise<SunspecMPPTData | null>;
67
+ /**
68
+ * Read all available MPPT strings
69
+ */
70
+ readAllMPPTData(): Promise<SunspecMPPTData[]>;
71
+ /**
72
+ * Read meter data (Model 203 for 3-phase)
73
+ */
74
+ readMeterData(): Promise<SunspecMeterData | null>;
75
+ /**
76
+ * Read common block data (Model 1)
77
+ */
78
+ readCommonBlock(): Promise<any>;
79
+ /**
80
+ * Get serial number from device
81
+ */
82
+ getSerialNumber(): Promise<string | undefined>;
83
+ /**
84
+ * Check if connected
85
+ */
86
+ isConnected(): boolean;
87
+ /**
88
+ * Check if connection is healthy
89
+ */
90
+ isHealthy(): boolean;
91
+ /**
92
+ * Get connection health details
93
+ */
94
+ getConnectionHealth(): IConnectionHealth;
95
+ /**
96
+ * Read Block 121 - Inverter Basic Settings
97
+ */
98
+ readInverterSettings(): Promise<SunspecInverterSettings | null>;
99
+ /**
100
+ * Read Block 123 - Immediate Inverter Controls
101
+ */
102
+ readInverterControls(): Promise<SunspecInverterControls | null>;
103
+ /**
104
+ * Write Block 121 - Inverter Basic Settings
105
+ */
106
+ writeInverterSettings(settings: Partial<SunspecInverterSettings>): Promise<boolean>;
107
+ /**
108
+ * Write Block 123 - Immediate Inverter Controls
109
+ */
110
+ writeInverterControls(controls: Partial<SunspecInverterControls>): Promise<boolean>;
111
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ /**
3
+ * Auto-generated file containing the current SDK version.
4
+ * This file is generated by scripts/extract-version.js and should not be edited manually.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.SDK_VERSION = void 0;
8
+ exports.getSdkVersion = getSdkVersion;
9
+ /**
10
+ * Current version of the enyo Energy App SDK.
11
+ */
12
+ exports.SDK_VERSION = '0.0.1';
13
+ /**
14
+ * Gets the current SDK version.
15
+ * @returns The semantic version string of the SDK
16
+ */
17
+ function getSdkVersion() {
18
+ return exports.SDK_VERSION;
19
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Auto-generated file containing the current SDK version.
3
+ * This file is generated by scripts/extract-version.js and should not be edited manually.
4
+ */
5
+ /**
6
+ * Current version of the enyo Energy App SDK.
7
+ */
8
+ export declare const SDK_VERSION = "0.0.1";
9
+ /**
10
+ * Gets the current SDK version.
11
+ * @returns The semantic version string of the SDK
12
+ */
13
+ export declare function getSdkVersion(): string;
@@ -0,0 +1,87 @@
1
+ import { ApplianceManager, EnergyApp } from "@enyo-energy/energy-app-sdk";
2
+ import { EnyoApplianceName } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
3
+ import { EnyoNetworkDevice } from "@enyo-energy/energy-app-sdk/dist/types/enyo-network-device.js";
4
+ import { SunspecModbusClient } from "./sunspec-modbus-client.js";
5
+ import { EnyoDataBusMessage } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
6
+ /**
7
+ * Base abstract class for all Sunspec devices
8
+ */
9
+ export declare abstract class BaseSunspecDevice {
10
+ protected readonly energyApp: EnergyApp;
11
+ readonly name: EnyoApplianceName[];
12
+ readonly networkDevice: EnyoNetworkDevice;
13
+ protected readonly sunspecClient: SunspecModbusClient;
14
+ protected readonly applianceManager: ApplianceManager;
15
+ protected readonly unitId: number;
16
+ protected applianceId?: string;
17
+ protected lastUpdateTime: number;
18
+ protected constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number);
19
+ /**
20
+ * Connect to the device and create/update the appliance
21
+ */
22
+ abstract connect(): Promise<void>;
23
+ /**
24
+ * Disconnect from the device and update appliance state
25
+ */
26
+ abstract disconnect(): Promise<void>;
27
+ /**
28
+ * Update device data and return data bus messages
29
+ */
30
+ abstract readData(clockId: string, resolution: '10s' | '30s' | '1m' | '15m'): Promise<EnyoDataBusMessage[]>;
31
+ /**
32
+ * Check if the device is connected
33
+ */
34
+ isConnected(): boolean;
35
+ /**
36
+ * Get the appliance IDs managed by this device
37
+ */
38
+ /**
39
+ * Ensure the Sunspec client is connected and models are discovered
40
+ */
41
+ protected ensureConnected(): Promise<void>;
42
+ }
43
+ /**
44
+ * Sunspec Inverter implementation using dynamic model discovery
45
+ */
46
+ export declare class SunspecInverter extends BaseSunspecDevice {
47
+ connect(): Promise<void>;
48
+ disconnect(): Promise<void>;
49
+ isConnected(): boolean;
50
+ readData(clockId: string, resolution: '10s' | '30s' | '1m' | '15m'): Promise<EnyoDataBusMessage[]>;
51
+ private mapOperatingState;
52
+ /**
53
+ * Map MPPT data to DC string structure for data bus
54
+ */
55
+ private mapMPPTToStrings;
56
+ }
57
+ /**
58
+ * Sunspec Battery implementation
59
+ */
60
+ export declare class SunspecBattery extends BaseSunspecDevice {
61
+ /**
62
+ * Connect to the battery and create/update the appliance
63
+ */
64
+ connect(): Promise<void>;
65
+ disconnect(): Promise<void>;
66
+ /**
67
+ * Update battery data and return data bus messages
68
+ */
69
+ readData(clockId: string, resolution: '10s' | '30s' | '1m' | '15m'): Promise<EnyoDataBusMessage[]>;
70
+ }
71
+ /**
72
+ * Sunspec Meter implementation
73
+ */
74
+ export declare class SunspecMeter extends BaseSunspecDevice {
75
+ /**
76
+ * Connect to the meter and create/update the appliance
77
+ */
78
+ connect(): Promise<void>;
79
+ /**
80
+ * Disconnect from the meter and update appliance state
81
+ */
82
+ disconnect(): Promise<void>;
83
+ /**
84
+ * Update meter data and return data bus messages
85
+ */
86
+ readData(clockId: string, resolution: '10s' | '30s' | '1m' | '15m'): Promise<EnyoDataBusMessage[]>;
87
+ }
@@ -0,0 +1,327 @@
1
+ import { SunspecModelId } from "./sunspec-interfaces.js";
2
+ import { randomUUID } from "node:crypto";
3
+ import { EnyoApplianceConnectionType, EnyoApplianceStateEnum, EnyoApplianceTypeEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
4
+ import { EnyoBatteryStateEnum, EnyoDataBusMessageEnum, EnyoInverterStateEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
5
+ import { EnyoSourceEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-source.enum.js";
6
+ /**
7
+ * Base abstract class for all Sunspec devices
8
+ */
9
+ export class BaseSunspecDevice {
10
+ energyApp;
11
+ name;
12
+ networkDevice;
13
+ sunspecClient;
14
+ applianceManager;
15
+ unitId;
16
+ applianceId;
17
+ lastUpdateTime = 0;
18
+ constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1) {
19
+ this.energyApp = energyApp;
20
+ this.name = name;
21
+ this.networkDevice = networkDevice;
22
+ this.sunspecClient = sunspecClient;
23
+ this.applianceManager = applianceManager;
24
+ this.unitId = unitId;
25
+ }
26
+ /**
27
+ * Check if the device is connected
28
+ */
29
+ isConnected() {
30
+ return this.sunspecClient.isConnected();
31
+ }
32
+ /**
33
+ * Get the appliance IDs managed by this device
34
+ */
35
+ /**
36
+ * Ensure the Sunspec client is connected and models are discovered
37
+ */
38
+ async ensureConnected() {
39
+ if (!this.sunspecClient.isConnected()) {
40
+ await this.sunspecClient.connect(this.networkDevice.ipAddress, 502, this.unitId);
41
+ await this.sunspecClient.discoverModels();
42
+ }
43
+ }
44
+ }
45
+ /**
46
+ * Sunspec Inverter implementation using dynamic model discovery
47
+ */
48
+ export class SunspecInverter extends BaseSunspecDevice {
49
+ async connect() {
50
+ // Ensure Sunspec client is connected
51
+ if (!this.sunspecClient.isConnected()) {
52
+ await this.sunspecClient.connect(this.networkDevice.ipAddress, 502, this.unitId);
53
+ await this.sunspecClient.discoverModels();
54
+ }
55
+ // Get device info from common block
56
+ const commonData = await this.sunspecClient.readCommonBlock();
57
+ // Create or update appliance
58
+ try {
59
+ this.applianceId = await this.applianceManager.createOrUpdateAppliance({
60
+ name: this.name,
61
+ type: EnyoApplianceTypeEnum.Inverter,
62
+ networkDevices: [this.networkDevice],
63
+ metadata: {
64
+ connectionType: EnyoApplianceConnectionType.Connector,
65
+ state: EnyoApplianceStateEnum.Connected,
66
+ serialNumber: commonData?.serialNumber,
67
+ modelName: commonData?.model,
68
+ vendorName: commonData?.manufacturer,
69
+ }
70
+ });
71
+ console.log(`Sunspec Inverter connected: ${this.networkDevice.hostname} (${this.applianceId})`);
72
+ }
73
+ catch (error) {
74
+ console.error(`Failed to create inverter appliance: ${error}`);
75
+ }
76
+ // Check for MPPT models
77
+ const mpptModel = this.sunspecClient.findModel(SunspecModelId.MPPT);
78
+ if (mpptModel) {
79
+ console.log(`MPPT model found for inverter ${this.networkDevice.hostname}`);
80
+ }
81
+ }
82
+ async disconnect() {
83
+ if (this.applianceId) {
84
+ await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
85
+ }
86
+ // Note: We don't disconnect the sunspecClient as it may be shared
87
+ }
88
+ isConnected() {
89
+ return this.sunspecClient.isConnected();
90
+ }
91
+ async readData(clockId, resolution) {
92
+ if (!this.isConnected()) {
93
+ return [];
94
+ }
95
+ const messages = [];
96
+ const timestamp = new Date();
97
+ try {
98
+ // Read inverter data
99
+ const inverterData = await this.sunspecClient.readInverterData();
100
+ const mpptDataList = await this.sunspecClient.readAllMPPTData();
101
+ if (inverterData) {
102
+ const inverterMessage = {
103
+ id: randomUUID(),
104
+ message: EnyoDataBusMessageEnum.InverterValuesUpdateV1,
105
+ type: 'message',
106
+ source: EnyoSourceEnum.Device,
107
+ applianceId: this.applianceId,
108
+ clockId,
109
+ timestampIso: timestamp.toISOString(),
110
+ resolution,
111
+ data: {
112
+ pvPowerW: inverterData.acPower || 0,
113
+ activePowerLimitationW: inverterData.acPower || 0, // Using AC power as default
114
+ state: this.mapOperatingState(inverterData.operatingState),
115
+ voltageL1: inverterData.voltageAN || 0,
116
+ voltageL2: inverterData.voltageBN,
117
+ voltageL3: inverterData.voltageCN,
118
+ strings: this.mapMPPTToStrings(mpptDataList)
119
+ }
120
+ };
121
+ messages.push(inverterMessage);
122
+ }
123
+ this.lastUpdateTime = timestamp.getTime();
124
+ }
125
+ catch (error) {
126
+ console.error(`Error updating inverter data: ${error}`);
127
+ }
128
+ return messages;
129
+ }
130
+ mapOperatingState(state) {
131
+ if (!state)
132
+ return EnyoInverterStateEnum.Off;
133
+ const stateMap = {
134
+ 1: EnyoInverterStateEnum.Off,
135
+ 2: EnyoInverterStateEnum.Sleeping,
136
+ 3: EnyoInverterStateEnum.Starting,
137
+ 4: EnyoInverterStateEnum.Mppt,
138
+ 5: EnyoInverterStateEnum.Throttled,
139
+ 6: EnyoInverterStateEnum.ShuttingDown,
140
+ 7: EnyoInverterStateEnum.Fault,
141
+ 8: EnyoInverterStateEnum.Standby
142
+ };
143
+ return stateMap[state] || EnyoInverterStateEnum.Off;
144
+ }
145
+ /**
146
+ * Map MPPT data to DC string structure for data bus
147
+ */
148
+ mapMPPTToStrings(mpptDataList) {
149
+ const result = [];
150
+ mpptDataList.forEach((mppt, index) => {
151
+ result.push({
152
+ index: index + 1,
153
+ voltage: mppt.dcVoltage,
154
+ powerW: mppt.dcPower
155
+ });
156
+ });
157
+ return result;
158
+ }
159
+ }
160
+ /**
161
+ * Sunspec Battery implementation
162
+ */
163
+ export class SunspecBattery extends BaseSunspecDevice {
164
+ /**
165
+ * Connect to the battery and create/update the appliance
166
+ */
167
+ async connect() {
168
+ // Ensure Sunspec client is connected
169
+ await this.ensureConnected();
170
+ // Check if battery models exist
171
+ const hasBattery = this.sunspecClient.findModel(SunspecModelId.Battery) !== undefined ||
172
+ this.sunspecClient.findModel(SunspecModelId.BatteryBase) !== undefined;
173
+ if (!hasBattery) {
174
+ throw new Error('No battery model found in device');
175
+ }
176
+ // Get device info
177
+ const commonData = await this.sunspecClient.readCommonBlock();
178
+ // Create or update appliance
179
+ try {
180
+ this.applianceId = await this.applianceManager.createOrUpdateAppliance({
181
+ name: this.name,
182
+ type: EnyoApplianceTypeEnum.Storage,
183
+ networkDevices: [this.networkDevice],
184
+ metadata: {
185
+ connectionType: EnyoApplianceConnectionType.Connector,
186
+ state: EnyoApplianceStateEnum.Connected,
187
+ serialNumber: commonData?.serialNumber ? `${commonData.serialNumber}-BAT` : undefined,
188
+ modelName: commonData?.model ? `${commonData.model} Battery` : 'Battery',
189
+ vendorName: commonData?.manufacturer,
190
+ }
191
+ });
192
+ console.log(`Sunspec Battery connected: ${this.networkDevice.hostname} (${this.applianceId})`);
193
+ }
194
+ catch (error) {
195
+ console.error(`Failed to create battery appliance: ${error}`);
196
+ }
197
+ }
198
+ async disconnect() {
199
+ if (this.applianceId) {
200
+ await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
201
+ }
202
+ }
203
+ /**
204
+ * Update battery data and return data bus messages
205
+ */
206
+ async readData(clockId, resolution) {
207
+ if (!this.isConnected()) {
208
+ return [];
209
+ }
210
+ const messages = [];
211
+ const timestamp = new Date();
212
+ try {
213
+ // For now, return basic battery data
214
+ // In a real implementation, we would read from battery models
215
+ const batteryMessage = {
216
+ id: randomUUID(),
217
+ message: EnyoDataBusMessageEnum.BatteryValuesUpdateV1,
218
+ type: 'message',
219
+ source: EnyoSourceEnum.Device,
220
+ applianceId: this.applianceId,
221
+ clockId,
222
+ timestampIso: timestamp.toISOString(),
223
+ resolution,
224
+ data: {
225
+ batterySoC: 50, // Placeholder - would read from battery model
226
+ batteryPowerW: 0,
227
+ state: EnyoBatteryStateEnum.Holding
228
+ }
229
+ };
230
+ messages.push(batteryMessage);
231
+ this.lastUpdateTime = timestamp.getTime();
232
+ }
233
+ catch (error) {
234
+ console.error(`Error updating battery data: ${error}`);
235
+ }
236
+ return messages;
237
+ }
238
+ }
239
+ /**
240
+ * Sunspec Meter implementation
241
+ */
242
+ export class SunspecMeter extends BaseSunspecDevice {
243
+ /**
244
+ * Connect to the meter and create/update the appliance
245
+ */
246
+ async connect() {
247
+ // Connect with specific unit ID for meter
248
+ await this.ensureConnected();
249
+ // Check if meter models exist
250
+ const hasMeter = this.sunspecClient.findModel(SunspecModelId.Meter3Phase) !== undefined ||
251
+ this.sunspecClient.findModel(SunspecModelId.MeterWye) !== undefined ||
252
+ this.sunspecClient.findModel(SunspecModelId.MeterSinglePhase) !== undefined;
253
+ if (!hasMeter) {
254
+ throw new Error('No meter model found in device');
255
+ }
256
+ // Get device info
257
+ const commonData = await this.sunspecClient.readCommonBlock();
258
+ // Create or update appliance
259
+ try {
260
+ this.applianceId = await this.applianceManager.createOrUpdateAppliance({
261
+ name: this.name,
262
+ type: EnyoApplianceTypeEnum.Meter,
263
+ networkDevices: [this.networkDevice],
264
+ metadata: {
265
+ connectionType: EnyoApplianceConnectionType.Connector,
266
+ state: EnyoApplianceStateEnum.Connected,
267
+ serialNumber: commonData?.serialNumber ? `${commonData.serialNumber}-MTR` : undefined,
268
+ modelName: commonData?.model ? `${commonData.model} Meter` : 'Meter',
269
+ vendorName: commonData?.manufacturer,
270
+ }
271
+ });
272
+ console.log(`Sunspec Meter connected: ${this.networkDevice.hostname} unit ${this.unitId} (${this.applianceId})`);
273
+ }
274
+ catch (error) {
275
+ console.error(`Failed to create meter appliance: ${error}`);
276
+ }
277
+ }
278
+ /**
279
+ * Disconnect from the meter and update appliance state
280
+ */
281
+ async disconnect() {
282
+ if (this.applianceId) {
283
+ await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
284
+ }
285
+ // Disconnect the client since meter uses its own connection
286
+ await this.sunspecClient.disconnect();
287
+ }
288
+ /**
289
+ * Update meter data and return data bus messages
290
+ */
291
+ async readData(clockId, resolution) {
292
+ if (!this.isConnected()) {
293
+ return [];
294
+ }
295
+ const messages = [];
296
+ const timestamp = new Date();
297
+ try {
298
+ // Read meter data
299
+ const meterData = await this.sunspecClient.readMeterData();
300
+ if (meterData) {
301
+ const meterMessage = {
302
+ id: randomUUID(),
303
+ message: EnyoDataBusMessageEnum.MeterValuesUpdateV1,
304
+ type: 'message',
305
+ source: EnyoSourceEnum.Device,
306
+ applianceId: this.applianceId,
307
+ clockId,
308
+ timestampIso: timestamp.toISOString(),
309
+ resolution,
310
+ data: {
311
+ gridPowerW: meterData.totalPower || 0,
312
+ gridFeedInWh: Number(meterData.exportedEnergy || 0),
313
+ gridConsumptionWh: Number(meterData.importedEnergy || 0),
314
+ selfConsumptionW: undefined,
315
+ selfConsumptionWh: undefined
316
+ }
317
+ };
318
+ messages.push(meterMessage);
319
+ }
320
+ this.lastUpdateTime = timestamp.getTime();
321
+ }
322
+ catch (error) {
323
+ console.error(`Error updating meter data: ${error}`);
324
+ }
325
+ return messages;
326
+ }
327
+ }