@enyo-energy/sunspec-sdk 0.0.21 → 0.0.22

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.
@@ -7,6 +7,7 @@ const enyo_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo
7
7
  const enyo_data_bus_value_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js");
8
8
  const enyo_source_enum_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-source.enum.js");
9
9
  const enyo_meter_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-meter-appliance.js");
10
+ const enyo_battery_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js");
10
11
  /**
11
12
  * Base abstract class for all Sunspec devices
12
13
  */
@@ -60,6 +61,7 @@ class SunspecInverter extends BaseSunspecDevice {
60
61
  // Get device info from common block
61
62
  const commonData = await this.sunspecClient.readCommonBlock();
62
63
  const inverterSettings = await this.sunspecClient.readInverterSettings();
64
+ const mpptDataList = await this.sunspecClient.readAllMPPTData();
63
65
  // Create or update appliance
64
66
  try {
65
67
  this.applianceId = await this.applianceManager.createOrUpdateAppliance({
@@ -74,6 +76,7 @@ class SunspecInverter extends BaseSunspecDevice {
74
76
  vendorName: commonData?.manufacturer,
75
77
  },
76
78
  inverter: {
79
+ dcStrings: this.mapDcStringToApplianceMetadata(this.mapMPPTToStrings(mpptDataList)),
77
80
  maxPvProductionW: inverterSettings?.WMax
78
81
  }
79
82
  });
@@ -108,6 +111,7 @@ class SunspecInverter extends BaseSunspecDevice {
108
111
  const inverterData = await this.sunspecClient.readInverterData();
109
112
  const mpptDataList = await this.sunspecClient.readAllMPPTData();
110
113
  const inverterSettings = await this.sunspecClient.readInverterSettings();
114
+ const dcStrings = this.mapMPPTToStrings(mpptDataList);
111
115
  if (inverterData) {
112
116
  const inverterMessage = {
113
117
  id: (0, node_crypto_1.randomUUID)(),
@@ -125,11 +129,20 @@ class SunspecInverter extends BaseSunspecDevice {
125
129
  voltageL1: inverterData.voltageAN || 0,
126
130
  voltageL2: inverterData.voltageBN ?? undefined,
127
131
  voltageL3: inverterData.voltageCN ?? undefined,
128
- strings: this.mapMPPTToStrings(mpptDataList)
132
+ strings: dcStrings
129
133
  }
130
134
  };
131
135
  messages.push(inverterMessage);
132
136
  }
137
+ if (this.applianceId) {
138
+ const appliance = await this.applianceManager.findApplianceById(this.applianceId);
139
+ await this.applianceManager.updateAppliance(this.applianceId, {
140
+ inverter: {
141
+ ...appliance?.inverter,
142
+ dcStrings: this.mapDcStringToApplianceMetadata(dcStrings)
143
+ }
144
+ });
145
+ }
133
146
  this.lastUpdateTime = timestamp.getTime();
134
147
  }
135
148
  catch (error) {
@@ -201,6 +214,9 @@ class SunspecInverter extends BaseSunspecDevice {
201
214
  return undefined;
202
215
  }
203
216
  }
217
+ mapDcStringToApplianceMetadata(mpptDataList) {
218
+ return mpptDataList.map(s => ({ index: s.index, name: s.name }));
219
+ }
204
220
  }
205
221
  exports.SunspecInverter = SunspecInverter;
206
222
  /**
@@ -210,7 +226,7 @@ class SunspecBattery extends BaseSunspecDevice {
210
226
  /**
211
227
  * Connect to the battery and create/update the appliance
212
228
  */
213
- async connect() {
229
+ async connect(inverterApplianceId) {
214
230
  // Ensure Sunspec client is connected
215
231
  await this.ensureConnected();
216
232
  // Check if battery models exist
@@ -222,7 +238,12 @@ class SunspecBattery extends BaseSunspecDevice {
222
238
  // Get device info
223
239
  const commonData = await this.sunspecClient.readCommonBlock();
224
240
  const batteryData = await this.sunspecClient.readBatteryData();
241
+ const storageMode = this.determineStorageMode(batteryData);
242
+ const features = [];
225
243
  // Create or update appliance
244
+ if (batteryData?.chaGriSet !== undefined) {
245
+ features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.GridCharging);
246
+ }
226
247
  try {
227
248
  this.applianceId = await this.applianceManager.createOrUpdateAppliance({
228
249
  name: this.name,
@@ -234,6 +255,14 @@ class SunspecBattery extends BaseSunspecDevice {
234
255
  serialNumber: commonData?.serialNumber,
235
256
  modelName: commonData?.model,
236
257
  vendorName: commonData?.manufacturer,
258
+ },
259
+ battery: {
260
+ connectedToApplianceId: inverterApplianceId,
261
+ storageMode: this.mapToEnyoStorageMode(storageMode),
262
+ maxChargingPowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.inWRte !== undefined ? batteryData.wChaMax * batteryData.inWRte : undefined,
263
+ maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
264
+ features,
265
+ gridChargingEnabled: batteryData?.chaGriSet === 1
237
266
  }
238
267
  });
239
268
  console.log(`Sunspec Battery connected: ${this.networkDevice.hostname} (${this.applianceId})`);
@@ -309,6 +338,19 @@ class SunspecBattery extends BaseSunspecDevice {
309
338
  else {
310
339
  console.warn('No battery data available from device');
311
340
  }
341
+ if (this.applianceId) {
342
+ const appliance = await this.applianceManager.findApplianceById(this.applianceId);
343
+ const storageMode = this.determineStorageMode(batteryData);
344
+ await this.applianceManager.updateAppliance(this.applianceId, {
345
+ battery: {
346
+ ...appliance?.battery,
347
+ storageMode: this.mapToEnyoStorageMode(storageMode),
348
+ maxChargingPowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.inWRte !== undefined ? batteryData.wChaMax * batteryData.inWRte : undefined,
349
+ maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
350
+ gridChargingEnabled: batteryData?.chaGriSet === 1
351
+ }
352
+ });
353
+ }
312
354
  this.lastUpdateTime = timestamp.getTime();
313
355
  }
314
356
  catch (error) {
@@ -359,6 +401,145 @@ class SunspecBattery extends BaseSunspecDevice {
359
401
  return undefined;
360
402
  }
361
403
  }
404
+ /**
405
+ * Set the battery storage mode
406
+ *
407
+ * @param mode - The storage mode to set:
408
+ * - 'charge': Allow charging only
409
+ * - 'discharge': Allow discharging only
410
+ * - 'holding': Prevent both charging and discharging (to avoid battery drain)
411
+ * - 'auto': Allow both charging and discharging (normal operation)
412
+ * @returns Promise<boolean> - true if successful, false otherwise
413
+ */
414
+ async setStorageMode(mode) {
415
+ if (!this.isConnected()) {
416
+ console.error('Battery not connected');
417
+ return false;
418
+ }
419
+ console.log(`Setting battery storage mode to: ${mode}`);
420
+ return this.sunspecClient.setStorageMode(mode);
421
+ }
422
+ /**
423
+ * Enable or disable grid charging
424
+ *
425
+ * When enabled, the battery can charge from the grid.
426
+ * When disabled, the battery only charges from PV.
427
+ *
428
+ * Note: When grid charging is enabled, you should also set WChaMax
429
+ * to control the charging power from the grid.
430
+ *
431
+ * @param enable - true to enable grid charging, false to disable
432
+ * @returns Promise<boolean> - true if successful, false otherwise
433
+ */
434
+ async enableGridCharging(enable) {
435
+ if (!this.isConnected()) {
436
+ console.error('Battery not connected');
437
+ return false;
438
+ }
439
+ console.log(`${enable ? 'Enabling' : 'Disabling'} grid charging for battery`);
440
+ return this.sunspecClient.enableGridCharging(enable);
441
+ }
442
+ /**
443
+ * Set battery charging power from grid
444
+ *
445
+ * This sets the maximum charging power (WChaMax) that the battery
446
+ * will draw when charging from the grid. Should be used together
447
+ * with enableGridCharging(true).
448
+ *
449
+ * @param powerW - Maximum charging power in Watts
450
+ * @returns Promise<boolean> - true if successful, false otherwise
451
+ */
452
+ async setChargingPower(powerW) {
453
+ if (!this.isConnected()) {
454
+ console.error('Battery not connected');
455
+ return false;
456
+ }
457
+ console.log(`Setting battery charging power to: ${powerW}W`);
458
+ return this.sunspecClient.writeBatteryControls({ wChaMax: powerW });
459
+ }
460
+ /**
461
+ * Get current battery control settings
462
+ *
463
+ * @returns Promise<SunspecBatteryControls | null> - Current control settings or null if error
464
+ */
465
+ async getBatteryControls() {
466
+ if (!this.isConnected()) {
467
+ console.error('Battery not connected');
468
+ return null;
469
+ }
470
+ return this.sunspecClient.readBatteryControls();
471
+ }
472
+ /**
473
+ * Write custom battery control settings
474
+ *
475
+ * Advanced method for direct control of battery parameters.
476
+ * Use the simplified methods (setStorageMode, enableGridCharging) for common operations.
477
+ *
478
+ * @param controls - Partial battery control settings to write
479
+ * @returns Promise<boolean> - true if successful, false otherwise
480
+ */
481
+ async writeBatteryControls(controls) {
482
+ if (!this.isConnected()) {
483
+ console.error('Battery not connected');
484
+ return false;
485
+ }
486
+ console.log('Writing battery controls:', controls);
487
+ return this.sunspecClient.writeBatteryControls(controls);
488
+ }
489
+ mapToEnyoStorageMode(storageMode) {
490
+ switch (storageMode) {
491
+ case sunspec_interfaces_js_1.SunspecStorageMode.AUTO:
492
+ return enyo_battery_appliance_js_1.EnyoBatteryStorageMode.AUTO;
493
+ case sunspec_interfaces_js_1.SunspecStorageMode.CHARGE:
494
+ return enyo_battery_appliance_js_1.EnyoBatteryStorageMode.CHARGE;
495
+ case sunspec_interfaces_js_1.SunspecStorageMode.DISCHARGE:
496
+ return enyo_battery_appliance_js_1.EnyoBatteryStorageMode.DISCHARGE;
497
+ case sunspec_interfaces_js_1.SunspecStorageMode.HOLDING:
498
+ return enyo_battery_appliance_js_1.EnyoBatteryStorageMode.HOLDING;
499
+ default:
500
+ return undefined;
501
+ }
502
+ }
503
+ /**
504
+ * Get the current storage mode based on StorCtl_Mod register
505
+ *
506
+ * @returns Promise<SunspecStorageMode | null> - Current mode or null if error
507
+ */
508
+ async getStorageMode() {
509
+ const controls = await this.getBatteryControls();
510
+ return this.determineStorageMode(controls);
511
+ }
512
+ determineStorageMode(controls) {
513
+ if (!controls || controls.storCtlMod === undefined) {
514
+ return null;
515
+ }
516
+ const chargeEnabled = (controls.storCtlMod & 0x0001) !== 0;
517
+ const dischargeEnabled = (controls.storCtlMod & 0x0002) !== 0;
518
+ if (chargeEnabled && dischargeEnabled) {
519
+ return sunspec_interfaces_js_1.SunspecStorageMode.AUTO;
520
+ }
521
+ else if (chargeEnabled && !dischargeEnabled) {
522
+ return sunspec_interfaces_js_1.SunspecStorageMode.CHARGE;
523
+ }
524
+ else if (!chargeEnabled && dischargeEnabled) {
525
+ return sunspec_interfaces_js_1.SunspecStorageMode.DISCHARGE;
526
+ }
527
+ else {
528
+ return sunspec_interfaces_js_1.SunspecStorageMode.HOLDING;
529
+ }
530
+ }
531
+ /**
532
+ * Check if grid charging is enabled
533
+ *
534
+ * @returns Promise<boolean | null> - true if enabled, false if disabled, null if error
535
+ */
536
+ async isGridChargingEnabled() {
537
+ const controls = await this.getBatteryControls();
538
+ if (!controls || controls.chaGriSet === undefined) {
539
+ return null;
540
+ }
541
+ return controls.chaGriSet === 1; // 1 = GRID, 0 = PV
542
+ }
362
543
  }
363
544
  exports.SunspecBattery = SunspecBattery;
364
545
  /**
@@ -1,3 +1,4 @@
1
+ import { type SunspecBatteryControls, SunspecStorageMode } from "./sunspec-interfaces.cjs";
1
2
  import { ApplianceManager, EnergyApp } from "@enyo-energy/energy-app-sdk";
2
3
  import { EnyoApplianceName } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
3
4
  import { EnyoNetworkDevice } from "@enyo-energy/energy-app-sdk/dist/types/enyo-network-device.js";
@@ -55,6 +56,7 @@ export declare class SunspecInverter extends BaseSunspecDevice {
55
56
  */
56
57
  private mapMPPTToStrings;
57
58
  private mapMPPTOperatingState;
59
+ private mapDcStringToApplianceMetadata;
58
60
  }
59
61
  /**
60
62
  * Sunspec Battery implementation
@@ -63,7 +65,7 @@ export declare class SunspecBattery extends BaseSunspecDevice {
63
65
  /**
64
66
  * Connect to the battery and create/update the appliance
65
67
  */
66
- connect(): Promise<void>;
68
+ connect(inverterApplianceId?: string): Promise<void>;
67
69
  disconnect(): Promise<void>;
68
70
  /**
69
71
  * Map SunSpec battery charge state to Enyo battery state
@@ -83,6 +85,71 @@ export declare class SunspecBattery extends BaseSunspecDevice {
83
85
  * Determine battery state from MPPT strings
84
86
  */
85
87
  private determineBatteryStateFromMPPT;
88
+ /**
89
+ * Set the battery storage mode
90
+ *
91
+ * @param mode - The storage mode to set:
92
+ * - 'charge': Allow charging only
93
+ * - 'discharge': Allow discharging only
94
+ * - 'holding': Prevent both charging and discharging (to avoid battery drain)
95
+ * - 'auto': Allow both charging and discharging (normal operation)
96
+ * @returns Promise<boolean> - true if successful, false otherwise
97
+ */
98
+ setStorageMode(mode: SunspecStorageMode): Promise<boolean>;
99
+ /**
100
+ * Enable or disable grid charging
101
+ *
102
+ * When enabled, the battery can charge from the grid.
103
+ * When disabled, the battery only charges from PV.
104
+ *
105
+ * Note: When grid charging is enabled, you should also set WChaMax
106
+ * to control the charging power from the grid.
107
+ *
108
+ * @param enable - true to enable grid charging, false to disable
109
+ * @returns Promise<boolean> - true if successful, false otherwise
110
+ */
111
+ enableGridCharging(enable: boolean): Promise<boolean>;
112
+ /**
113
+ * Set battery charging power from grid
114
+ *
115
+ * This sets the maximum charging power (WChaMax) that the battery
116
+ * will draw when charging from the grid. Should be used together
117
+ * with enableGridCharging(true).
118
+ *
119
+ * @param powerW - Maximum charging power in Watts
120
+ * @returns Promise<boolean> - true if successful, false otherwise
121
+ */
122
+ setChargingPower(powerW: number): Promise<boolean>;
123
+ /**
124
+ * Get current battery control settings
125
+ *
126
+ * @returns Promise<SunspecBatteryControls | null> - Current control settings or null if error
127
+ */
128
+ getBatteryControls(): Promise<SunspecBatteryControls | null>;
129
+ /**
130
+ * Write custom battery control settings
131
+ *
132
+ * Advanced method for direct control of battery parameters.
133
+ * Use the simplified methods (setStorageMode, enableGridCharging) for common operations.
134
+ *
135
+ * @param controls - Partial battery control settings to write
136
+ * @returns Promise<boolean> - true if successful, false otherwise
137
+ */
138
+ writeBatteryControls(controls: Partial<SunspecBatteryControls>): Promise<boolean>;
139
+ private mapToEnyoStorageMode;
140
+ /**
141
+ * Get the current storage mode based on StorCtl_Mod register
142
+ *
143
+ * @returns Promise<SunspecStorageMode | null> - Current mode or null if error
144
+ */
145
+ getStorageMode(): Promise<SunspecStorageMode | null>;
146
+ private determineStorageMode;
147
+ /**
148
+ * Check if grid charging is enabled
149
+ *
150
+ * @returns Promise<boolean | null> - true if enabled, false if disabled, null if error
151
+ */
152
+ isGridChargingEnabled(): Promise<boolean | null>;
86
153
  }
87
154
  /**
88
155
  * Sunspec Meter implementation
@@ -3,7 +3,7 @@
3
3
  * SunSpec block interfaces with block numbers
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SunspecVArPctMode = exports.SunspecEnableControl = exports.SunspecConnectionControl = exports.SunspecStorageControlMode = exports.SunspecBatteryChargeState = exports.SunspecMPPTOperatingState = exports.SunspecModelId = void 0;
6
+ exports.SunspecStorageMode = exports.SunspecChargeSource = exports.SunspecVArPctMode = exports.SunspecEnableControl = exports.SunspecConnectionControl = exports.SunspecStorageControlMode = exports.SunspecBatteryChargeState = exports.SunspecMPPTOperatingState = exports.SunspecModelId = void 0;
7
7
  /**
8
8
  * Common Sunspec Model IDs
9
9
  */
@@ -57,11 +57,18 @@ var SunspecBatteryChargeState;
57
57
  })(SunspecBatteryChargeState || (exports.SunspecBatteryChargeState = SunspecBatteryChargeState = {}));
58
58
  /**
59
59
  * Storage Control Mode bitfield values for Model 124
60
+ *
61
+ * The StorCtl_Mod register controls who has control over the battery:
62
+ * - When bits are set, external control is enabled for that function
63
+ * - When bits are clear, the battery may be in autonomous/self-consumption mode
64
+ * - To ensure external control works properly, the appropriate bits must be set
60
65
  */
61
66
  var SunspecStorageControlMode;
62
67
  (function (SunspecStorageControlMode) {
63
68
  SunspecStorageControlMode[SunspecStorageControlMode["CHARGE"] = 1] = "CHARGE";
64
- SunspecStorageControlMode[SunspecStorageControlMode["DISCHARGE"] = 2] = "DISCHARGE"; // Bit 1 - Enable discharge mode
69
+ SunspecStorageControlMode[SunspecStorageControlMode["DISCHARGE"] = 2] = "DISCHARGE"; // Bit 1 - Enable discharge mode (allow external discharge control)
70
+ // Note: Higher bits may control other modes like autonomous/self-consumption
71
+ // but these are device-specific and not standardized in SunSpec
65
72
  })(SunspecStorageControlMode || (exports.SunspecStorageControlMode = SunspecStorageControlMode = {}));
66
73
  /**
67
74
  * Enum values for connection control
@@ -88,3 +95,21 @@ var SunspecVArPctMode;
88
95
  SunspecVArPctMode[SunspecVArPctMode["WMAX"] = 1] = "WMAX";
89
96
  SunspecVArPctMode[SunspecVArPctMode["VARMAX"] = 2] = "VARMAX";
90
97
  })(SunspecVArPctMode || (exports.SunspecVArPctMode = SunspecVArPctMode = {}));
98
+ /**
99
+ * Enum values for battery charge source (Model 124, Register 17)
100
+ */
101
+ var SunspecChargeSource;
102
+ (function (SunspecChargeSource) {
103
+ SunspecChargeSource[SunspecChargeSource["PV"] = 0] = "PV";
104
+ SunspecChargeSource[SunspecChargeSource["GRID"] = 1] = "GRID"; // Charge from Grid
105
+ })(SunspecChargeSource || (exports.SunspecChargeSource = SunspecChargeSource = {}));
106
+ /**
107
+ * Storage modes for simplified battery control
108
+ */
109
+ var SunspecStorageMode;
110
+ (function (SunspecStorageMode) {
111
+ SunspecStorageMode["CHARGE"] = "charge";
112
+ SunspecStorageMode["DISCHARGE"] = "discharge";
113
+ SunspecStorageMode["HOLDING"] = "holding";
114
+ SunspecStorageMode["AUTO"] = "auto"; // Both charge and discharge allowed
115
+ })(SunspecStorageMode || (exports.SunspecStorageMode = SunspecStorageMode = {}));
@@ -170,9 +170,14 @@ export declare enum SunspecBatteryChargeState {
170
170
  }
171
171
  /**
172
172
  * Storage Control Mode bitfield values for Model 124
173
+ *
174
+ * The StorCtl_Mod register controls who has control over the battery:
175
+ * - When bits are set, external control is enabled for that function
176
+ * - When bits are clear, the battery may be in autonomous/self-consumption mode
177
+ * - To ensure external control works properly, the appropriate bits must be set
173
178
  */
174
179
  export declare enum SunspecStorageControlMode {
175
- CHARGE = 1,// Bit 0 - Enable charge mode
180
+ CHARGE = 1,// Bit 0 - Enable charge mode (allow external charge control)
176
181
  DISCHARGE = 2
177
182
  }
178
183
  /**
@@ -338,3 +343,36 @@ export declare enum SunspecVArPctMode {
338
343
  WMAX = 1,
339
344
  VARMAX = 2
340
345
  }
346
+ /**
347
+ * Enum values for battery charge source (Model 124, Register 17)
348
+ */
349
+ export declare enum SunspecChargeSource {
350
+ PV = 0,// Charge from PV only
351
+ GRID = 1
352
+ }
353
+ /**
354
+ * Storage modes for simplified battery control
355
+ */
356
+ export declare enum SunspecStorageMode {
357
+ CHARGE = "charge",// Allow charging only
358
+ DISCHARGE = "discharge",// Allow discharging only
359
+ HOLDING = "holding",// No charge or discharge (holding mode)
360
+ AUTO = "auto"
361
+ }
362
+ /**
363
+ * Battery control structure for writing to Model 124
364
+ * Used for controlling battery charge/discharge behavior
365
+ *
366
+ * IMPORTANT: To enable grid charging with specific power:
367
+ * 1. Set storCtlMod with appropriate bits to enable external control
368
+ * 2. Set chaGriSet = 1 to allow grid charging
369
+ * 3. Set wChaMax to the desired charging power in Watts
370
+ */
371
+ export interface SunspecBatteryControls {
372
+ storCtlMod?: number;
373
+ chaGriSet?: number;
374
+ wChaMax?: number;
375
+ inWRte?: number;
376
+ outWRte?: number;
377
+ minRsvPct?: number;
378
+ }
@@ -265,6 +265,52 @@ class SunspecModbusClient {
265
265
  throw error;
266
266
  }
267
267
  }
268
+ /**
269
+ * Helper to write register value(s)
270
+ */
271
+ async writeRegisterValue(address, value, dataType = 'uint16') {
272
+ if (!this.modbusClient) {
273
+ throw new Error('Modbus client not initialized');
274
+ }
275
+ try {
276
+ // Convert value to array of register values
277
+ let registerValues;
278
+ if (Array.isArray(value)) {
279
+ // For arrays, use directly
280
+ registerValues = value;
281
+ }
282
+ else {
283
+ // For single values, convert based on data type
284
+ switch (dataType) {
285
+ case 'uint32':
286
+ case 'int32':
287
+ // Split 32-bit value into two 16-bit registers
288
+ const high = (value >>> 16) & 0xFFFF;
289
+ const low = value & 0xFFFF;
290
+ registerValues = [high, low];
291
+ break;
292
+ case 'int16':
293
+ // For signed 16-bit, ensure proper representation
294
+ registerValues = [value & 0xFFFF];
295
+ break;
296
+ case 'uint16':
297
+ default:
298
+ registerValues = [value & 0xFFFF];
299
+ break;
300
+ }
301
+ }
302
+ // Write to holding registers
303
+ await this.modbusClient.writeMultipleRegisters(address, registerValues);
304
+ this.connectionHealth.recordSuccess();
305
+ console.log(`Successfully wrote value ${value} to register ${address}`);
306
+ return true;
307
+ }
308
+ catch (error) {
309
+ this.connectionHealth.recordFailure(error);
310
+ console.error(`Error writing register at address ${address}: ${error}`);
311
+ throw error;
312
+ }
313
+ }
268
314
  /**
269
315
  * Read inverter data from Model 103 (Three Phase)
270
316
  */
@@ -797,6 +843,145 @@ class SunspecModbusClient {
797
843
  return null;
798
844
  }
799
845
  }
846
+ /**
847
+ * Write battery control settings to Model 124
848
+ */
849
+ async writeBatteryControls(controls) {
850
+ const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Battery);
851
+ if (!model) {
852
+ console.error('Battery model 124 not found');
853
+ return false;
854
+ }
855
+ const baseAddr = model.address;
856
+ console.log(`Writing Battery Controls to Model 124 at base address: ${baseAddr}`);
857
+ try {
858
+ // Write storage control mode (Register 5)
859
+ if (controls.storCtlMod !== undefined) {
860
+ await this.writeRegisterValue(baseAddr + 5, controls.storCtlMod, 'uint16');
861
+ console.log(`Set storage control mode to 0x${controls.storCtlMod.toString(16)}`);
862
+ }
863
+ // Write charge source setting (Register 17)
864
+ if (controls.chaGriSet !== undefined) {
865
+ await this.writeRegisterValue(baseAddr + 17, controls.chaGriSet, 'uint16');
866
+ console.log(`Set charge source to ${controls.chaGriSet === sunspec_interfaces_js_1.SunspecChargeSource.GRID ? 'GRID' : 'PV'}`);
867
+ }
868
+ // Write maximum charge power (Register 2) - needs scale factor
869
+ if (controls.wChaMax !== undefined) {
870
+ const scaleFactorAddr = baseAddr + 18;
871
+ const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
872
+ const scaledValue = Math.round(controls.wChaMax / Math.pow(10, scaleFactor));
873
+ await this.writeRegisterValue(baseAddr + 2, scaledValue, 'uint16');
874
+ console.log(`Set max charge power to ${controls.wChaMax}W (scaled: ${scaledValue})`);
875
+ }
876
+ // Write charge rate (Register 13) - needs scale factor
877
+ if (controls.inWRte !== undefined) {
878
+ const scaleFactorAddr = baseAddr + 25;
879
+ const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
880
+ const scaledValue = Math.round(controls.inWRte / Math.pow(10, scaleFactor));
881
+ await this.writeRegisterValue(baseAddr + 13, scaledValue, 'int16');
882
+ console.log(`Set charge rate to ${controls.inWRte}% (scaled: ${scaledValue})`);
883
+ }
884
+ // Write discharge rate (Register 12) - needs scale factor
885
+ if (controls.outWRte !== undefined) {
886
+ const scaleFactorAddr = baseAddr + 25;
887
+ const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
888
+ const scaledValue = Math.round(controls.outWRte / Math.pow(10, scaleFactor));
889
+ await this.writeRegisterValue(baseAddr + 12, scaledValue, 'int16');
890
+ console.log(`Set discharge rate to ${controls.outWRte}% (scaled: ${scaledValue})`);
891
+ }
892
+ // Write minimum reserve percentage (Register 7) - needs scale factor
893
+ if (controls.minRsvPct !== undefined) {
894
+ const scaleFactorAddr = baseAddr + 21;
895
+ const scaleFactor = await this.readRegisterValue(scaleFactorAddr, 1, 'int16');
896
+ const scaledValue = Math.round(controls.minRsvPct / Math.pow(10, scaleFactor));
897
+ await this.writeRegisterValue(baseAddr + 7, scaledValue, 'uint16');
898
+ console.log(`Set minimum reserve to ${controls.minRsvPct}% (scaled: ${scaledValue})`);
899
+ }
900
+ console.log('Battery controls written successfully');
901
+ return true;
902
+ }
903
+ catch (error) {
904
+ console.error(`Error writing battery controls: ${error}`);
905
+ return false;
906
+ }
907
+ }
908
+ /**
909
+ * Set battery storage mode (simplified interface)
910
+ */
911
+ async setStorageMode(mode) {
912
+ let storCtlMod;
913
+ switch (mode) {
914
+ case sunspec_interfaces_js_1.SunspecStorageMode.CHARGE:
915
+ // Enable charge only (bit 0 = 1, bit 1 = 0)
916
+ storCtlMod = sunspec_interfaces_js_1.SunspecStorageControlMode.CHARGE;
917
+ break;
918
+ case sunspec_interfaces_js_1.SunspecStorageMode.DISCHARGE:
919
+ // Enable discharge only (bit 0 = 0, bit 1 = 1)
920
+ storCtlMod = sunspec_interfaces_js_1.SunspecStorageControlMode.DISCHARGE;
921
+ break;
922
+ case sunspec_interfaces_js_1.SunspecStorageMode.HOLDING:
923
+ // Disable both charge and discharge (bits 0 and 1 = 0)
924
+ storCtlMod = 0;
925
+ break;
926
+ case sunspec_interfaces_js_1.SunspecStorageMode.AUTO:
927
+ // Enable both charge and discharge (bits 0 and 1 = 1)
928
+ storCtlMod = sunspec_interfaces_js_1.SunspecStorageControlMode.CHARGE | sunspec_interfaces_js_1.SunspecStorageControlMode.DISCHARGE;
929
+ break;
930
+ default:
931
+ console.error(`Invalid storage mode: ${mode}`);
932
+ return false;
933
+ }
934
+ console.log(`Setting storage mode to ${mode} (control bits: 0x${storCtlMod.toString(16)})`);
935
+ return this.writeBatteryControls({ storCtlMod });
936
+ }
937
+ /**
938
+ * Enable or disable grid charging
939
+ */
940
+ async enableGridCharging(enable) {
941
+ const chaGriSet = enable ? sunspec_interfaces_js_1.SunspecChargeSource.GRID : sunspec_interfaces_js_1.SunspecChargeSource.PV;
942
+ console.log(`${enable ? 'Enabling' : 'Disabling'} grid charging`);
943
+ return this.writeBatteryControls({ chaGriSet });
944
+ }
945
+ /**
946
+ * Read current battery control settings
947
+ */
948
+ async readBatteryControls() {
949
+ const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Battery);
950
+ if (!model) {
951
+ console.log('Battery model 124 not found');
952
+ return null;
953
+ }
954
+ const baseAddr = model.address;
955
+ console.log(`Reading Battery Controls from Model 124 at base address: ${baseAddr}`);
956
+ try {
957
+ // Read scale factors
958
+ const scaleFactors = {
959
+ WChaMax_SF: await this.readRegisterValue(baseAddr + 18, 1, 'int16'),
960
+ MinRsvPct_SF: await this.readRegisterValue(baseAddr + 21, 1, 'int16'),
961
+ InOutWRte_SF: await this.readRegisterValue(baseAddr + 25, 1, 'int16')
962
+ };
963
+ // Read raw values
964
+ const wChaMaxRaw = await this.readRegisterValue(baseAddr + 2, 1, 'uint16');
965
+ const storCtlModRaw = await this.readRegisterValue(baseAddr + 5, 1, 'uint16');
966
+ const minRsvPctRaw = await this.readRegisterValue(baseAddr + 7, 1, 'uint16');
967
+ const outWRteRaw = await this.readRegisterValue(baseAddr + 12, 1, 'int16');
968
+ const inWRteRaw = await this.readRegisterValue(baseAddr + 13, 1, 'int16');
969
+ const chaGriSetRaw = await this.readRegisterValue(baseAddr + 17, 1, 'uint16');
970
+ // Apply scale factors and return control settings
971
+ return {
972
+ storCtlMod: !this.isUnimplementedValue(storCtlModRaw, 'bitfield16') ? storCtlModRaw : undefined,
973
+ chaGriSet: !this.isUnimplementedValue(chaGriSetRaw, 'enum16') ? chaGriSetRaw : undefined,
974
+ wChaMax: this.applyScaleFactor(wChaMaxRaw, scaleFactors.WChaMax_SF, 'uint16'),
975
+ inWRte: this.applyScaleFactor(inWRteRaw, scaleFactors.InOutWRte_SF, 'int16'),
976
+ outWRte: this.applyScaleFactor(outWRteRaw, scaleFactors.InOutWRte_SF, 'int16'),
977
+ minRsvPct: this.applyScaleFactor(minRsvPctRaw, scaleFactors.MinRsvPct_SF, 'uint16')
978
+ };
979
+ }
980
+ catch (error) {
981
+ console.error(`Error reading battery controls: ${error}`);
982
+ return null;
983
+ }
984
+ }
800
985
  /**
801
986
  * Read meter data (Model 203 for 3-phase)
802
987
  */