@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.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # enyo Sunspec SDK
@@ -0,0 +1,334 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SunspecMeter = exports.SunspecBattery = exports.SunspecInverter = exports.BaseSunspecDevice = void 0;
4
+ const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
5
+ const node_crypto_1 = require("node:crypto");
6
+ const enyo_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js");
7
+ const enyo_data_bus_value_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js");
8
+ const enyo_source_enum_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-source.enum.js");
9
+ /**
10
+ * Base abstract class for all Sunspec devices
11
+ */
12
+ class BaseSunspecDevice {
13
+ energyApp;
14
+ name;
15
+ networkDevice;
16
+ sunspecClient;
17
+ applianceManager;
18
+ unitId;
19
+ applianceId;
20
+ lastUpdateTime = 0;
21
+ constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1) {
22
+ this.energyApp = energyApp;
23
+ this.name = name;
24
+ this.networkDevice = networkDevice;
25
+ this.sunspecClient = sunspecClient;
26
+ this.applianceManager = applianceManager;
27
+ this.unitId = unitId;
28
+ }
29
+ /**
30
+ * Check if the device is connected
31
+ */
32
+ isConnected() {
33
+ return this.sunspecClient.isConnected();
34
+ }
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
+ async ensureConnected() {
42
+ if (!this.sunspecClient.isConnected()) {
43
+ await this.sunspecClient.connect(this.networkDevice.ipAddress, 502, this.unitId);
44
+ await this.sunspecClient.discoverModels();
45
+ }
46
+ }
47
+ }
48
+ exports.BaseSunspecDevice = BaseSunspecDevice;
49
+ /**
50
+ * Sunspec Inverter implementation using dynamic model discovery
51
+ */
52
+ class SunspecInverter extends BaseSunspecDevice {
53
+ async connect() {
54
+ // Ensure Sunspec client is connected
55
+ if (!this.sunspecClient.isConnected()) {
56
+ await this.sunspecClient.connect(this.networkDevice.ipAddress, 502, this.unitId);
57
+ await this.sunspecClient.discoverModels();
58
+ }
59
+ // Get device info from common block
60
+ const commonData = await this.sunspecClient.readCommonBlock();
61
+ // Create or update appliance
62
+ try {
63
+ this.applianceId = await this.applianceManager.createOrUpdateAppliance({
64
+ name: this.name,
65
+ type: enyo_appliance_js_1.EnyoApplianceTypeEnum.Inverter,
66
+ networkDevices: [this.networkDevice],
67
+ metadata: {
68
+ connectionType: enyo_appliance_js_1.EnyoApplianceConnectionType.Connector,
69
+ state: enyo_appliance_js_1.EnyoApplianceStateEnum.Connected,
70
+ serialNumber: commonData?.serialNumber,
71
+ modelName: commonData?.model,
72
+ vendorName: commonData?.manufacturer,
73
+ }
74
+ });
75
+ console.log(`Sunspec Inverter connected: ${this.networkDevice.hostname} (${this.applianceId})`);
76
+ }
77
+ catch (error) {
78
+ console.error(`Failed to create inverter appliance: ${error}`);
79
+ }
80
+ // Check for MPPT models
81
+ const mpptModel = this.sunspecClient.findModel(sunspec_interfaces_js_1.SunspecModelId.MPPT);
82
+ if (mpptModel) {
83
+ console.log(`MPPT model found for inverter ${this.networkDevice.hostname}`);
84
+ }
85
+ }
86
+ async disconnect() {
87
+ if (this.applianceId) {
88
+ await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
89
+ }
90
+ // Note: We don't disconnect the sunspecClient as it may be shared
91
+ }
92
+ isConnected() {
93
+ return this.sunspecClient.isConnected();
94
+ }
95
+ async readData(clockId, resolution) {
96
+ if (!this.isConnected()) {
97
+ return [];
98
+ }
99
+ const messages = [];
100
+ const timestamp = new Date();
101
+ try {
102
+ // Read inverter data
103
+ const inverterData = await this.sunspecClient.readInverterData();
104
+ const mpptDataList = await this.sunspecClient.readAllMPPTData();
105
+ if (inverterData) {
106
+ const inverterMessage = {
107
+ id: (0, node_crypto_1.randomUUID)(),
108
+ message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.InverterValuesUpdateV1,
109
+ type: 'message',
110
+ source: enyo_source_enum_js_1.EnyoSourceEnum.Device,
111
+ applianceId: this.applianceId,
112
+ clockId,
113
+ timestampIso: timestamp.toISOString(),
114
+ resolution,
115
+ data: {
116
+ pvPowerW: inverterData.acPower || 0,
117
+ activePowerLimitationW: inverterData.acPower || 0, // Using AC power as default
118
+ state: this.mapOperatingState(inverterData.operatingState),
119
+ voltageL1: inverterData.voltageAN || 0,
120
+ voltageL2: inverterData.voltageBN,
121
+ voltageL3: inverterData.voltageCN,
122
+ strings: this.mapMPPTToStrings(mpptDataList)
123
+ }
124
+ };
125
+ messages.push(inverterMessage);
126
+ }
127
+ this.lastUpdateTime = timestamp.getTime();
128
+ }
129
+ catch (error) {
130
+ console.error(`Error updating inverter data: ${error}`);
131
+ }
132
+ return messages;
133
+ }
134
+ mapOperatingState(state) {
135
+ if (!state)
136
+ return enyo_data_bus_value_js_1.EnyoInverterStateEnum.Off;
137
+ const stateMap = {
138
+ 1: enyo_data_bus_value_js_1.EnyoInverterStateEnum.Off,
139
+ 2: enyo_data_bus_value_js_1.EnyoInverterStateEnum.Sleeping,
140
+ 3: enyo_data_bus_value_js_1.EnyoInverterStateEnum.Starting,
141
+ 4: enyo_data_bus_value_js_1.EnyoInverterStateEnum.Mppt,
142
+ 5: enyo_data_bus_value_js_1.EnyoInverterStateEnum.Throttled,
143
+ 6: enyo_data_bus_value_js_1.EnyoInverterStateEnum.ShuttingDown,
144
+ 7: enyo_data_bus_value_js_1.EnyoInverterStateEnum.Fault,
145
+ 8: enyo_data_bus_value_js_1.EnyoInverterStateEnum.Standby
146
+ };
147
+ return stateMap[state] || enyo_data_bus_value_js_1.EnyoInverterStateEnum.Off;
148
+ }
149
+ /**
150
+ * Map MPPT data to DC string structure for data bus
151
+ */
152
+ mapMPPTToStrings(mpptDataList) {
153
+ const result = [];
154
+ mpptDataList.forEach((mppt, index) => {
155
+ result.push({
156
+ index: index + 1,
157
+ voltage: mppt.dcVoltage,
158
+ powerW: mppt.dcPower
159
+ });
160
+ });
161
+ return result;
162
+ }
163
+ }
164
+ exports.SunspecInverter = SunspecInverter;
165
+ /**
166
+ * Sunspec Battery implementation
167
+ */
168
+ class SunspecBattery extends BaseSunspecDevice {
169
+ /**
170
+ * Connect to the battery and create/update the appliance
171
+ */
172
+ async connect() {
173
+ // Ensure Sunspec client is connected
174
+ await this.ensureConnected();
175
+ // Check if battery models exist
176
+ const hasBattery = this.sunspecClient.findModel(sunspec_interfaces_js_1.SunspecModelId.Battery) !== undefined ||
177
+ this.sunspecClient.findModel(sunspec_interfaces_js_1.SunspecModelId.BatteryBase) !== undefined;
178
+ if (!hasBattery) {
179
+ throw new Error('No battery model found in device');
180
+ }
181
+ // Get device info
182
+ const commonData = await this.sunspecClient.readCommonBlock();
183
+ // Create or update appliance
184
+ try {
185
+ this.applianceId = await this.applianceManager.createOrUpdateAppliance({
186
+ name: this.name,
187
+ type: enyo_appliance_js_1.EnyoApplianceTypeEnum.Storage,
188
+ networkDevices: [this.networkDevice],
189
+ metadata: {
190
+ connectionType: enyo_appliance_js_1.EnyoApplianceConnectionType.Connector,
191
+ state: enyo_appliance_js_1.EnyoApplianceStateEnum.Connected,
192
+ serialNumber: commonData?.serialNumber ? `${commonData.serialNumber}-BAT` : undefined,
193
+ modelName: commonData?.model ? `${commonData.model} Battery` : 'Battery',
194
+ vendorName: commonData?.manufacturer,
195
+ }
196
+ });
197
+ console.log(`Sunspec Battery connected: ${this.networkDevice.hostname} (${this.applianceId})`);
198
+ }
199
+ catch (error) {
200
+ console.error(`Failed to create battery appliance: ${error}`);
201
+ }
202
+ }
203
+ async disconnect() {
204
+ if (this.applianceId) {
205
+ await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
206
+ }
207
+ }
208
+ /**
209
+ * Update battery data and return data bus messages
210
+ */
211
+ async readData(clockId, resolution) {
212
+ if (!this.isConnected()) {
213
+ return [];
214
+ }
215
+ const messages = [];
216
+ const timestamp = new Date();
217
+ try {
218
+ // For now, return basic battery data
219
+ // In a real implementation, we would read from battery models
220
+ const batteryMessage = {
221
+ id: (0, node_crypto_1.randomUUID)(),
222
+ message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.BatteryValuesUpdateV1,
223
+ type: 'message',
224
+ source: enyo_source_enum_js_1.EnyoSourceEnum.Device,
225
+ applianceId: this.applianceId,
226
+ clockId,
227
+ timestampIso: timestamp.toISOString(),
228
+ resolution,
229
+ data: {
230
+ batterySoC: 50, // Placeholder - would read from battery model
231
+ batteryPowerW: 0,
232
+ state: enyo_data_bus_value_js_1.EnyoBatteryStateEnum.Holding
233
+ }
234
+ };
235
+ messages.push(batteryMessage);
236
+ this.lastUpdateTime = timestamp.getTime();
237
+ }
238
+ catch (error) {
239
+ console.error(`Error updating battery data: ${error}`);
240
+ }
241
+ return messages;
242
+ }
243
+ }
244
+ exports.SunspecBattery = SunspecBattery;
245
+ /**
246
+ * Sunspec Meter implementation
247
+ */
248
+ class SunspecMeter extends BaseSunspecDevice {
249
+ /**
250
+ * Connect to the meter and create/update the appliance
251
+ */
252
+ async connect() {
253
+ // Connect with specific unit ID for meter
254
+ await this.ensureConnected();
255
+ // Check if meter models exist
256
+ const hasMeter = this.sunspecClient.findModel(sunspec_interfaces_js_1.SunspecModelId.Meter3Phase) !== undefined ||
257
+ this.sunspecClient.findModel(sunspec_interfaces_js_1.SunspecModelId.MeterWye) !== undefined ||
258
+ this.sunspecClient.findModel(sunspec_interfaces_js_1.SunspecModelId.MeterSinglePhase) !== undefined;
259
+ if (!hasMeter) {
260
+ throw new Error('No meter model found in device');
261
+ }
262
+ // Get device info
263
+ const commonData = await this.sunspecClient.readCommonBlock();
264
+ // Create or update appliance
265
+ try {
266
+ this.applianceId = await this.applianceManager.createOrUpdateAppliance({
267
+ name: this.name,
268
+ type: enyo_appliance_js_1.EnyoApplianceTypeEnum.Meter,
269
+ networkDevices: [this.networkDevice],
270
+ metadata: {
271
+ connectionType: enyo_appliance_js_1.EnyoApplianceConnectionType.Connector,
272
+ state: enyo_appliance_js_1.EnyoApplianceStateEnum.Connected,
273
+ serialNumber: commonData?.serialNumber ? `${commonData.serialNumber}-MTR` : undefined,
274
+ modelName: commonData?.model ? `${commonData.model} Meter` : 'Meter',
275
+ vendorName: commonData?.manufacturer,
276
+ }
277
+ });
278
+ console.log(`Sunspec Meter connected: ${this.networkDevice.hostname} unit ${this.unitId} (${this.applianceId})`);
279
+ }
280
+ catch (error) {
281
+ console.error(`Failed to create meter appliance: ${error}`);
282
+ }
283
+ }
284
+ /**
285
+ * Disconnect from the meter and update appliance state
286
+ */
287
+ async disconnect() {
288
+ if (this.applianceId) {
289
+ await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
290
+ }
291
+ // Disconnect the client since meter uses its own connection
292
+ await this.sunspecClient.disconnect();
293
+ }
294
+ /**
295
+ * Update meter data and return data bus messages
296
+ */
297
+ async readData(clockId, resolution) {
298
+ if (!this.isConnected()) {
299
+ return [];
300
+ }
301
+ const messages = [];
302
+ const timestamp = new Date();
303
+ try {
304
+ // Read meter data
305
+ const meterData = await this.sunspecClient.readMeterData();
306
+ if (meterData) {
307
+ const meterMessage = {
308
+ id: (0, node_crypto_1.randomUUID)(),
309
+ message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.MeterValuesUpdateV1,
310
+ type: 'message',
311
+ source: enyo_source_enum_js_1.EnyoSourceEnum.Device,
312
+ applianceId: this.applianceId,
313
+ clockId,
314
+ timestampIso: timestamp.toISOString(),
315
+ resolution,
316
+ data: {
317
+ gridPowerW: meterData.totalPower || 0,
318
+ gridFeedInWh: Number(meterData.exportedEnergy || 0),
319
+ gridConsumptionWh: Number(meterData.importedEnergy || 0),
320
+ selfConsumptionW: undefined,
321
+ selfConsumptionWh: undefined
322
+ }
323
+ };
324
+ messages.push(meterMessage);
325
+ }
326
+ this.lastUpdateTime = timestamp.getTime();
327
+ }
328
+ catch (error) {
329
+ console.error(`Error updating meter data: ${error}`);
330
+ }
331
+ return messages;
332
+ }
333
+ }
334
+ exports.SunspecMeter = SunspecMeter;
@@ -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.cjs";
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,52 @@
1
+ "use strict";
2
+ /**
3
+ * SunSpec block interfaces with block numbers
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SunspecVArPctMode = exports.SunspecEnableControl = exports.SunspecConnectionControl = exports.SunspecModelId = void 0;
7
+ /**
8
+ * Common Sunspec Model IDs
9
+ */
10
+ var SunspecModelId;
11
+ (function (SunspecModelId) {
12
+ SunspecModelId[SunspecModelId["Common"] = 1] = "Common";
13
+ SunspecModelId[SunspecModelId["Inverter3Phase"] = 103] = "Inverter3Phase";
14
+ SunspecModelId[SunspecModelId["InverterSinglePhase"] = 101] = "InverterSinglePhase";
15
+ SunspecModelId[SunspecModelId["MPPT"] = 160] = "MPPT";
16
+ SunspecModelId[SunspecModelId["Battery"] = 124] = "Battery";
17
+ SunspecModelId[SunspecModelId["BatteryBase"] = 802] = "BatteryBase";
18
+ SunspecModelId[SunspecModelId["BatteryControl"] = 803] = "BatteryControl";
19
+ SunspecModelId[SunspecModelId["MeterSinglePhase"] = 201] = "MeterSinglePhase";
20
+ SunspecModelId[SunspecModelId["Meter3Phase"] = 203] = "Meter3Phase";
21
+ SunspecModelId[SunspecModelId["MeterWye"] = 204] = "MeterWye";
22
+ SunspecModelId[SunspecModelId["Nameplate"] = 120] = "Nameplate";
23
+ SunspecModelId[SunspecModelId["Settings"] = 121] = "Settings";
24
+ SunspecModelId[SunspecModelId["Status"] = 122] = "Status";
25
+ SunspecModelId[SunspecModelId["Controls"] = 123] = "Controls";
26
+ SunspecModelId[SunspecModelId["EndMarker"] = 65535] = "EndMarker";
27
+ })(SunspecModelId || (exports.SunspecModelId = SunspecModelId = {}));
28
+ /**
29
+ * Enum values for connection control
30
+ */
31
+ var SunspecConnectionControl;
32
+ (function (SunspecConnectionControl) {
33
+ SunspecConnectionControl[SunspecConnectionControl["DISCONNECT"] = 0] = "DISCONNECT";
34
+ SunspecConnectionControl[SunspecConnectionControl["CONNECT"] = 1] = "CONNECT";
35
+ })(SunspecConnectionControl || (exports.SunspecConnectionControl = SunspecConnectionControl = {}));
36
+ /**
37
+ * Enum values for enable/disable controls
38
+ */
39
+ var SunspecEnableControl;
40
+ (function (SunspecEnableControl) {
41
+ SunspecEnableControl[SunspecEnableControl["DISABLED"] = 0] = "DISABLED";
42
+ SunspecEnableControl[SunspecEnableControl["ENABLED"] = 1] = "ENABLED";
43
+ })(SunspecEnableControl || (exports.SunspecEnableControl = SunspecEnableControl = {}));
44
+ /**
45
+ * Enum values for VAR percent mode
46
+ */
47
+ var SunspecVArPctMode;
48
+ (function (SunspecVArPctMode) {
49
+ SunspecVArPctMode[SunspecVArPctMode["NONE"] = 0] = "NONE";
50
+ SunspecVArPctMode[SunspecVArPctMode["WMAX"] = 1] = "WMAX";
51
+ SunspecVArPctMode[SunspecVArPctMode["VARMAX"] = 2] = "VARMAX";
52
+ })(SunspecVArPctMode || (exports.SunspecVArPctMode = SunspecVArPctMode = {}));