@enyo-energy/sunspec-sdk 0.0.32 → 0.0.34

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.
@@ -1,122 +1,105 @@
1
1
  "use strict";
2
2
  /**
3
- * Connection Retry Manager with Exponential Backoff
3
+ * Connection Retry Manager with Tiered Schedule
4
4
  *
5
- * Manages automatic connection retries with configurable exponential backoff.
6
- * Backoff sequence (with defaults): 1s 1.5s 2.25s 3.375s → 5.06s → 7.59s → 11.39s → 17.09s → 25.63s → 30s (capped)
5
+ * Poll-based retry manager no timers or setTimeout loops.
6
+ * Called on each readData() cycle; determines whether enough time has elapsed
7
+ * to attempt a reconnection based on the current retry phase.
8
+ *
9
+ * Default schedule:
10
+ * - Phase 1: every 10s for 1 minute
11
+ * - Phase 2: every 30s for 2 minutes
12
+ * - Phase 3: every 1m for 5 minutes
13
+ * - Phase 4: every 5m forever
7
14
  */
8
15
  Object.defineProperty(exports, "__esModule", { value: true });
9
16
  exports.ConnectionRetryManager = void 0;
10
17
  const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
11
18
  class ConnectionRetryManager {
12
19
  config;
13
- currentAttempt = 0;
14
- pendingRetryTimeout = null;
15
- isRetrying = false;
20
+ disconnectedSince = null;
21
+ lastAttemptTime = 0;
22
+ attemptCount = 0;
16
23
  constructor(config) {
17
- this.config = { ...sunspec_interfaces_js_1.DEFAULT_RETRY_CONFIG, ...config };
24
+ this.config = config ?? sunspec_interfaces_js_1.DEFAULT_RETRY_CONFIG;
18
25
  }
19
26
  /**
20
- * Calculate the next delay using exponential backoff
21
- * Formula: min(initialDelayMs * backoffFactor^attempt, maxDelayMs)
27
+ * Mark the connection as disconnected. Sets the timestamp if not already set.
22
28
  */
23
- getNextDelay() {
24
- const delay = this.config.initialDelayMs * Math.pow(this.config.backoffFactor, this.currentAttempt);
25
- return Math.min(delay, this.config.maxDelayMs);
29
+ markDisconnected() {
30
+ if (this.disconnectedSince === null) {
31
+ this.disconnectedSince = Date.now();
32
+ }
26
33
  }
27
34
  /**
28
- * Reset the retry state on successful connection
35
+ * Reset all retry state (call on successful reconnect).
29
36
  */
30
37
  reset() {
31
- this.currentAttempt = 0;
32
- this.isRetrying = false;
33
- this.cancelPendingRetry();
38
+ this.disconnectedSince = null;
39
+ this.lastAttemptTime = 0;
40
+ this.attemptCount = 0;
34
41
  }
35
42
  /**
36
- * Check if we should retry based on max attempts
37
- * Returns true if we haven't exceeded maxAttempts or if maxAttempts is -1 (infinite)
43
+ * Determine which retry phase we're currently in based on elapsed time since disconnect.
38
44
  */
39
- shouldRetry() {
40
- if (this.config.maxAttempts === -1) {
41
- return true;
45
+ getCurrentPhase() {
46
+ const elapsed = this.getElapsedMs();
47
+ let cumulativeDuration = 0;
48
+ for (const phase of this.config.phases) {
49
+ if (phase.durationMs === 0) {
50
+ // This is the final "forever" phase
51
+ return phase;
52
+ }
53
+ cumulativeDuration += phase.durationMs;
54
+ if (elapsed < cumulativeDuration) {
55
+ return phase;
56
+ }
42
57
  }
43
- return this.currentAttempt < this.config.maxAttempts;
58
+ // Fallback: return last phase
59
+ return this.config.phases[this.config.phases.length - 1];
44
60
  }
45
61
  /**
46
- * Get the current attempt number (0-indexed)
62
+ * Check if we should attempt a reconnection now.
63
+ * Called on every readData() poll cycle.
47
64
  */
48
- getCurrentAttempt() {
49
- return this.currentAttempt;
65
+ shouldAttemptNow() {
66
+ if (this.disconnectedSince === null) {
67
+ return false;
68
+ }
69
+ const now = Date.now();
70
+ const phase = this.getCurrentPhase();
71
+ return (now - this.lastAttemptTime) >= phase.intervalMs;
50
72
  }
51
73
  /**
52
- * Get the max attempts configuration
74
+ * Record that an attempt was just made.
53
75
  */
54
- getMaxAttempts() {
55
- return this.config.maxAttempts;
76
+ recordAttempt() {
77
+ this.lastAttemptTime = Date.now();
78
+ this.attemptCount++;
56
79
  }
57
80
  /**
58
- * Check if a retry is currently in progress
81
+ * Get the number of reconnection attempts made.
59
82
  */
60
- isRetryInProgress() {
61
- return this.isRetrying;
83
+ getAttemptCount() {
84
+ return this.attemptCount;
62
85
  }
63
86
  /**
64
- * Schedule a reconnection attempt with exponential backoff delay
65
- * Returns a promise that resolves when the callback completes
87
+ * Get milliseconds elapsed since disconnect was first detected.
66
88
  */
67
- async scheduleRetry(callback) {
68
- if (!this.shouldRetry()) {
69
- console.log(`Max retry attempts (${this.config.maxAttempts}) reached. Giving up.`);
70
- return false;
71
- }
72
- if (this.isRetrying) {
73
- console.log('Retry already in progress, skipping duplicate request.');
74
- return false;
89
+ getElapsedMs() {
90
+ if (this.disconnectedSince === null) {
91
+ return 0;
75
92
  }
76
- this.isRetrying = true;
77
- const delay = this.getNextDelay();
78
- const attemptNumber = this.currentAttempt + 1;
79
- const maxAttemptsStr = this.config.maxAttempts === -1 ? '∞' : this.config.maxAttempts.toString();
80
- console.log(`Scheduling reconnection attempt ${attemptNumber}/${maxAttemptsStr} in ${delay}ms`);
81
- return new Promise((resolve) => {
82
- this.pendingRetryTimeout = setTimeout(async () => {
83
- this.pendingRetryTimeout = null;
84
- this.currentAttempt++;
85
- try {
86
- console.log(`Attempting reconnection (attempt ${attemptNumber}/${maxAttemptsStr})...`);
87
- const success = await callback();
88
- if (success) {
89
- console.log('Reconnection successful!');
90
- this.reset();
91
- resolve(true);
92
- }
93
- else {
94
- console.log('Reconnection attempt failed.');
95
- this.isRetrying = false;
96
- resolve(false);
97
- }
98
- }
99
- catch (error) {
100
- console.error(`Reconnection attempt failed with error: ${error}`);
101
- this.isRetrying = false;
102
- resolve(false);
103
- }
104
- }, delay);
105
- });
93
+ return Date.now() - this.disconnectedSince;
106
94
  }
107
95
  /**
108
- * Cancel any pending retry attempt
96
+ * Check if the manager is currently tracking a disconnection.
109
97
  */
110
- cancelPendingRetry() {
111
- if (this.pendingRetryTimeout) {
112
- clearTimeout(this.pendingRetryTimeout);
113
- this.pendingRetryTimeout = null;
114
- this.isRetrying = false;
115
- console.log('Pending retry cancelled.');
116
- }
98
+ isDisconnected() {
99
+ return this.disconnectedSince !== null;
117
100
  }
118
101
  /**
119
- * Get the current configuration
102
+ * Get the current configuration.
120
103
  */
121
104
  getConfig() {
122
105
  return { ...this.config };
@@ -1,53 +1,58 @@
1
1
  /**
2
- * Connection Retry Manager with Exponential Backoff
2
+ * Connection Retry Manager with Tiered Schedule
3
3
  *
4
- * Manages automatic connection retries with configurable exponential backoff.
5
- * Backoff sequence (with defaults): 1s 1.5s 2.25s 3.375s → 5.06s → 7.59s → 11.39s → 17.09s → 25.63s → 30s (capped)
4
+ * Poll-based retry manager no timers or setTimeout loops.
5
+ * Called on each readData() cycle; determines whether enough time has elapsed
6
+ * to attempt a reconnection based on the current retry phase.
7
+ *
8
+ * Default schedule:
9
+ * - Phase 1: every 10s for 1 minute
10
+ * - Phase 2: every 30s for 2 minutes
11
+ * - Phase 3: every 1m for 5 minutes
12
+ * - Phase 4: every 5m forever
6
13
  */
7
- import { IRetryConfig } from './sunspec-interfaces.cjs';
14
+ import { IRetryConfig, IRetryPhase } from './sunspec-interfaces.cjs';
8
15
  export declare class ConnectionRetryManager {
9
16
  private config;
10
- private currentAttempt;
11
- private pendingRetryTimeout;
12
- private isRetrying;
13
- constructor(config?: Partial<IRetryConfig>);
17
+ private disconnectedSince;
18
+ private lastAttemptTime;
19
+ private attemptCount;
20
+ constructor(config?: IRetryConfig);
14
21
  /**
15
- * Calculate the next delay using exponential backoff
16
- * Formula: min(initialDelayMs * backoffFactor^attempt, maxDelayMs)
22
+ * Mark the connection as disconnected. Sets the timestamp if not already set.
17
23
  */
18
- getNextDelay(): number;
24
+ markDisconnected(): void;
19
25
  /**
20
- * Reset the retry state on successful connection
26
+ * Reset all retry state (call on successful reconnect).
21
27
  */
22
28
  reset(): void;
23
29
  /**
24
- * Check if we should retry based on max attempts
25
- * Returns true if we haven't exceeded maxAttempts or if maxAttempts is -1 (infinite)
30
+ * Determine which retry phase we're currently in based on elapsed time since disconnect.
26
31
  */
27
- shouldRetry(): boolean;
32
+ getCurrentPhase(): IRetryPhase;
28
33
  /**
29
- * Get the current attempt number (0-indexed)
34
+ * Check if we should attempt a reconnection now.
35
+ * Called on every readData() poll cycle.
30
36
  */
31
- getCurrentAttempt(): number;
37
+ shouldAttemptNow(): boolean;
32
38
  /**
33
- * Get the max attempts configuration
39
+ * Record that an attempt was just made.
34
40
  */
35
- getMaxAttempts(): number;
41
+ recordAttempt(): void;
36
42
  /**
37
- * Check if a retry is currently in progress
43
+ * Get the number of reconnection attempts made.
38
44
  */
39
- isRetryInProgress(): boolean;
45
+ getAttemptCount(): number;
40
46
  /**
41
- * Schedule a reconnection attempt with exponential backoff delay
42
- * Returns a promise that resolves when the callback completes
47
+ * Get milliseconds elapsed since disconnect was first detected.
43
48
  */
44
- scheduleRetry(callback: () => Promise<boolean>): Promise<boolean>;
49
+ getElapsedMs(): number;
45
50
  /**
46
- * Cancel any pending retry attempt
51
+ * Check if the manager is currently tracking a disconnection.
47
52
  */
48
- cancelPendingRetry(): void;
53
+ isDisconnected(): boolean;
49
54
  /**
50
- * Get the current configuration
55
+ * Get the current configuration.
51
56
  */
52
57
  getConfig(): IRetryConfig;
53
58
  }
@@ -1,26 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SunspecMeter = exports.SunspecBattery = exports.SunspecInverter = exports.BaseSunspecDevice = void 0;
3
+ exports.SunspecMeter = exports.SunspecBattery = exports.SunspecInverter = exports.BaseSunspecDevice = exports.ENYO_DATA_BUS_SET_INVERTER_FEED_IN_LIMIT_V1 = void 0;
4
4
  const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
5
5
  const node_crypto_1 = require("node:crypto");
6
6
  const enyo_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js");
7
+ const connection_retry_manager_js_1 = require("./connection-retry-manager.cjs");
7
8
  const enyo_data_bus_value_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js");
8
9
  const enyo_source_enum_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-source.enum.js");
9
10
  const enyo_meter_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-meter-appliance.js");
10
11
  const enyo_battery_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js");
11
- /**
12
- * Extract battery discharge power from MPPT data.
13
- * Returns the discharge power in Watts (positive value), or 0 if no discharge.
14
- */
15
- function extractBatteryDischargePowerFromMPPT(mpptDataList) {
16
- let dischargePowerW = 0;
17
- for (const mppt of mpptDataList) {
18
- if (mppt.stringId === 'StDisCha 4' && mppt.dcPower !== undefined && mppt.dcPower > 0) {
19
- dischargePowerW += mppt.dcPower;
20
- }
21
- }
22
- return dischargePowerW;
23
- }
12
+ // TODO: Remove once added to @enyo-energy/energy-app-sdk EnyoDataBusMessageEnum
13
+ exports.ENYO_DATA_BUS_SET_INVERTER_FEED_IN_LIMIT_V1 = 'SetInverterFeedInLimitV1';
24
14
  /**
25
15
  * Base abstract class for all Sunspec devices
26
16
  */
@@ -35,7 +25,10 @@ class BaseSunspecDevice {
35
25
  baseAddress;
36
26
  applianceId;
37
27
  lastUpdateTime = 0;
38
- constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000) {
28
+ dataBusListenerId;
29
+ dataBus;
30
+ retryManager;
31
+ constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, retryConfig) {
39
32
  this.energyApp = energyApp;
40
33
  this.name = name;
41
34
  this.networkDevice = networkDevice;
@@ -44,6 +37,7 @@ class BaseSunspecDevice {
44
37
  this.unitId = unitId;
45
38
  this.port = port;
46
39
  this.baseAddress = baseAddress;
40
+ this.retryManager = new connection_retry_manager_js_1.ConnectionRetryManager(retryConfig);
47
41
  }
48
42
  /**
49
43
  * Check if the device is connected
@@ -65,12 +59,87 @@ class BaseSunspecDevice {
65
59
  await this.sunspecClient.discoverModels(this.baseAddress);
66
60
  }
67
61
  }
62
+ /**
63
+ * Attempt a reconnection if the tiered retry schedule allows it.
64
+ * Called from readData() when the device is disconnected.
65
+ * Returns true if reconnection succeeded.
66
+ */
67
+ async tryReconnect() {
68
+ this.retryManager.markDisconnected();
69
+ if (!this.retryManager.shouldAttemptNow()) {
70
+ return false;
71
+ }
72
+ this.retryManager.recordAttempt();
73
+ const phase = this.retryManager.getCurrentPhase();
74
+ const attempt = this.retryManager.getAttemptCount();
75
+ const elapsed = Math.round(this.retryManager.getElapsedMs() / 1000);
76
+ console.log(`${this.constructor.name} ${this.applianceId}: Reconnect attempt #${attempt} ` +
77
+ `(phase: ${phase.intervalMs / 1000}s interval, elapsed: ${elapsed}s)`);
78
+ try {
79
+ const success = await this.sunspecClient.reconnect();
80
+ if (success) {
81
+ // Re-discover models after reconnect
82
+ await this.sunspecClient.discoverModels(this.baseAddress);
83
+ this.retryManager.reset();
84
+ // Update appliance state to Connected
85
+ if (this.applianceId) {
86
+ await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Connected);
87
+ }
88
+ console.log(`${this.constructor.name} ${this.applianceId}: Reconnection successful after ${attempt} attempt(s)`);
89
+ return true;
90
+ }
91
+ }
92
+ catch (error) {
93
+ console.error(`${this.constructor.name} ${this.applianceId}: Reconnect attempt #${attempt} failed: ${error}`);
94
+ }
95
+ return false;
96
+ }
97
+ /**
98
+ * Mark the device as offline: update appliance state and start tracking disconnection.
99
+ */
100
+ async markOffline() {
101
+ this.retryManager.markDisconnected();
102
+ if (this.applianceId) {
103
+ try {
104
+ await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
105
+ }
106
+ catch (error) {
107
+ console.error(`${this.constructor.name} ${this.applianceId}: Failed to mark appliance offline: ${error}`);
108
+ }
109
+ }
110
+ }
111
+ sendCommandAcknowledge(messageId, acknowledgeMessage, answer, rejectionReason) {
112
+ if (!this.dataBus || !this.applianceId) {
113
+ return;
114
+ }
115
+ const ackMessage = {
116
+ id: (0, node_crypto_1.randomUUID)(),
117
+ message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.CommandAcknowledgeV1,
118
+ type: 'answer',
119
+ source: enyo_source_enum_js_1.EnyoSourceEnum.Device,
120
+ applianceId: this.applianceId,
121
+ timestampIso: new Date().toISOString(),
122
+ data: {
123
+ messageId,
124
+ acknowledgeMessage: acknowledgeMessage,
125
+ answer,
126
+ rejectionReason
127
+ }
128
+ };
129
+ console.log(`${this.constructor.name} ${this.applianceId}: sending ${answer} for ${acknowledgeMessage} (messageId=${messageId}${rejectionReason ? `, reason=${rejectionReason}` : ''})`);
130
+ this.dataBus.sendMessage([ackMessage]);
131
+ }
68
132
  }
69
133
  exports.BaseSunspecDevice = BaseSunspecDevice;
70
134
  /**
71
135
  * Sunspec Inverter implementation using dynamic model discovery
72
136
  */
73
137
  class SunspecInverter extends BaseSunspecDevice {
138
+ capabilities;
139
+ constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig) {
140
+ super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig);
141
+ this.capabilities = capabilities;
142
+ }
74
143
  async connect() {
75
144
  // Ensure Sunspec client is connected
76
145
  if (!this.sunspecClient.isConnected()) {
@@ -111,8 +180,10 @@ class SunspecInverter extends BaseSunspecDevice {
111
180
  if (mpptModel) {
112
181
  console.log(`MPPT model found for inverter ${this.networkDevice.hostname}`);
113
182
  }
183
+ this.startDataBusListening();
114
184
  }
115
185
  async disconnect() {
186
+ this.stopDataBusListening();
116
187
  if (this.applianceId) {
117
188
  await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
118
189
  }
@@ -123,7 +194,9 @@ class SunspecInverter extends BaseSunspecDevice {
123
194
  }
124
195
  async readData(clockId, resolution) {
125
196
  if (!this.isConnected()) {
126
- return [];
197
+ await this.tryReconnect();
198
+ if (!this.isConnected())
199
+ return [];
127
200
  }
128
201
  const messages = [];
129
202
  const timestamp = new Date();
@@ -133,12 +206,9 @@ class SunspecInverter extends BaseSunspecDevice {
133
206
  const mpptDataList = await this.sunspecClient.readAllMPPTData();
134
207
  const inverterSettings = await this.sunspecClient.readInverterSettings();
135
208
  const dcStrings = this.mapMPPTToStrings(mpptDataList);
136
- // Calculate battery discharge power to subtract from AC power
137
- const batteryDischargePowerW = extractBatteryDischargePowerFromMPPT(mpptDataList);
138
209
  if (inverterData) {
139
- const totalAcPowerW = inverterData.acPower || 0;
140
- const purePvPowerW = Math.max(0, totalAcPowerW - batteryDischargePowerW);
141
- console.log(`Got Battery Discharge power ${batteryDischargePowerW} and Inverter Power W ${totalAcPowerW} with pure PV Power ${purePvPowerW}`);
210
+ const pvPowerW = dcStrings.reduce((sum, s) => sum + (s.powerW || 0), 0);
211
+ console.log(`Got PV Power from DC strings: ${pvPowerW}W`);
142
212
  const inverterMessage = {
143
213
  id: (0, node_crypto_1.randomUUID)(),
144
214
  message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.InverterValuesUpdateV1,
@@ -149,7 +219,7 @@ class SunspecInverter extends BaseSunspecDevice {
149
219
  timestampIso: timestamp.toISOString(),
150
220
  resolution,
151
221
  data: {
152
- pvPowerW: purePvPowerW,
222
+ pvPowerW,
153
223
  activePowerLimitationW: inverterSettings?.WMax,
154
224
  state: this.mapOperatingState(inverterData.operatingState),
155
225
  voltageL1: inverterData.voltageAN || 0,
@@ -173,6 +243,7 @@ class SunspecInverter extends BaseSunspecDevice {
173
243
  }
174
244
  catch (error) {
175
245
  console.error(`Error updating inverter data: ${error}`);
246
+ await this.markOffline();
176
247
  }
177
248
  return messages;
178
249
  }
@@ -243,14 +314,73 @@ class SunspecInverter extends BaseSunspecDevice {
243
314
  mapDcStringToApplianceMetadata(mpptDataList) {
244
315
  return mpptDataList.map(s => ({ index: s.index, name: s.name }));
245
316
  }
317
+ /**
318
+ * Start listening for inverter commands on the data bus.
319
+ * Idempotent — does nothing if already listening.
320
+ */
321
+ startDataBusListening() {
322
+ if (this.dataBusListenerId) {
323
+ return;
324
+ }
325
+ this.dataBus = this.energyApp.useDataBus();
326
+ this.dataBusListenerId = this.dataBus.listenForMessages([exports.ENYO_DATA_BUS_SET_INVERTER_FEED_IN_LIMIT_V1], (entry) => this.handleInverterCommand(entry));
327
+ console.log(`Inverter ${this.applianceId}: started data bus listening (listener ${this.dataBusListenerId})`);
328
+ }
329
+ /**
330
+ * Stop listening for inverter commands on the data bus.
331
+ */
332
+ stopDataBusListening() {
333
+ if (this.dataBusListenerId && this.dataBus) {
334
+ this.dataBus.unsubscribe(this.dataBusListenerId);
335
+ console.log(`Inverter ${this.applianceId}: stopped data bus listening (listener ${this.dataBusListenerId})`);
336
+ }
337
+ this.dataBusListenerId = undefined;
338
+ this.dataBus = undefined;
339
+ }
340
+ handleInverterCommand(entry) {
341
+ if (entry.applianceId !== this.applianceId) {
342
+ return;
343
+ }
344
+ void (async () => {
345
+ try {
346
+ if (entry.message === exports.ENYO_DATA_BUS_SET_INVERTER_FEED_IN_LIMIT_V1) {
347
+ await this.handleSetFeedInLimit(entry);
348
+ }
349
+ }
350
+ catch (error) {
351
+ console.error(`Inverter ${this.applianceId}: error handling ${entry.message}:`, error);
352
+ }
353
+ })();
354
+ }
355
+ async handleSetFeedInLimit(msg) {
356
+ // Check capability
357
+ if (!this.capabilities.includes(sunspec_interfaces_js_1.SunspecInverterCapability.FeedInLimit)) {
358
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported, 'FeedInLimit capability not enabled');
359
+ return;
360
+ }
361
+ if (!this.isConnected() || !this.applianceId) {
362
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Not connected');
363
+ return;
364
+ }
365
+ console.log(`Inverter ${this.applianceId}: handling SetInverterFeedInLimitV1 (feedInLimitW=${msg.data.feedInLimitW})`);
366
+ const success = await this.sunspecClient.setFeedInLimit(msg.data.feedInLimitW);
367
+ if (!success) {
368
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set feed-in limit');
369
+ return;
370
+ }
371
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
372
+ }
246
373
  }
247
374
  exports.SunspecInverter = SunspecInverter;
248
375
  /**
249
376
  * Sunspec Battery implementation
250
377
  */
251
378
  class SunspecBattery extends BaseSunspecDevice {
252
- dataBusListenerId;
253
- dataBus;
379
+ capabilities;
380
+ constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig) {
381
+ super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig);
382
+ this.capabilities = capabilities;
383
+ }
254
384
  /**
255
385
  * Connect to the battery and create/update the appliance
256
386
  */
@@ -337,7 +467,9 @@ class SunspecBattery extends BaseSunspecDevice {
337
467
  */
338
468
  async readData(clockId, resolution) {
339
469
  if (!this.isConnected()) {
340
- return [];
470
+ await this.tryReconnect();
471
+ if (!this.isConnected())
472
+ return [];
341
473
  }
342
474
  const messages = [];
343
475
  const timestamp = new Date();
@@ -395,6 +527,7 @@ class SunspecBattery extends BaseSunspecDevice {
395
527
  }
396
528
  catch (error) {
397
529
  console.error(`Error updating battery data: ${error}`);
530
+ await this.markOffline();
398
531
  }
399
532
  return messages;
400
533
  }
@@ -711,45 +844,36 @@ class SunspecBattery extends BaseSunspecDevice {
711
844
  this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported);
712
845
  return;
713
846
  }
714
- console.log(`Battery ${this.applianceId}: handling SetStorageDischargeLimitV1 (dischargeLimitPercent=${msg.data.dischargeLimitPercent})`);
715
- // Read current state for logging
847
+ console.log(`Battery ${this.applianceId}: handling SetStorageDischargeLimitV1 (dischargeLimitW=${msg.data.dischargeLimitW})`);
848
+ // Read current state to get wChaMax for percentage conversion
716
849
  const controls = await this.getBatteryControls();
717
- console.log(`Battery ${this.applianceId}: current state - outWRte=${controls?.outWRte}`);
850
+ console.log(`Battery ${this.applianceId}: current state - outWRte=${controls?.outWRte}, wChaMax=${controls?.wChaMax}`);
851
+ if (!controls?.wChaMax) {
852
+ this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to read wChaMax for discharge limit conversion');
853
+ return;
854
+ }
855
+ // Convert watts to percentage of WDisChaMax (using wChaMax), clamped to 0-100%
856
+ const dischargeLimitPercent = Math.min(100, Math.max(0, (msg.data.dischargeLimitW / controls.wChaMax) * 100));
857
+ console.log(`Battery ${this.applianceId}: calculated discharge limit: ${dischargeLimitPercent.toFixed(1)}% (${msg.data.dischargeLimitW}W / ${controls.wChaMax}W)`);
718
858
  // Set discharge limit (Register 12: outWRte)
719
- const success = await this.writeBatteryControls({ outWRte: msg.data.dischargeLimitPercent });
859
+ const success = await this.writeBatteryControls({ outWRte: dischargeLimitPercent });
720
860
  if (!success) {
721
861
  this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, 'Failed to set discharge limit');
722
862
  return;
723
863
  }
724
864
  this.sendCommandAcknowledge(msg.id, msg.message, enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted);
725
865
  }
726
- sendCommandAcknowledge(messageId, acknowledgeMessage, answer, rejectionReason) {
727
- if (!this.dataBus || !this.applianceId) {
728
- return;
729
- }
730
- const ackMessage = {
731
- id: (0, node_crypto_1.randomUUID)(),
732
- message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.CommandAcknowledgeV1,
733
- type: 'answer',
734
- source: enyo_source_enum_js_1.EnyoSourceEnum.Device,
735
- applianceId: this.applianceId,
736
- timestampIso: new Date().toISOString(),
737
- data: {
738
- messageId,
739
- acknowledgeMessage,
740
- answer,
741
- rejectionReason
742
- }
743
- };
744
- console.log(`Battery ${this.applianceId}: sending ${answer} for ${acknowledgeMessage} (messageId=${messageId}${rejectionReason ? `, reason=${rejectionReason}` : ''})`);
745
- this.dataBus.sendMessage([ackMessage]);
746
- }
747
866
  }
748
867
  exports.SunspecBattery = SunspecBattery;
749
868
  /**
750
869
  * Sunspec Meter implementation
751
870
  */
752
871
  class SunspecMeter extends BaseSunspecDevice {
872
+ capabilities;
873
+ constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig) {
874
+ super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig);
875
+ this.capabilities = capabilities;
876
+ }
753
877
  /**
754
878
  * Connect to the meter and create/update the appliance
755
879
  */
@@ -806,7 +930,9 @@ class SunspecMeter extends BaseSunspecDevice {
806
930
  */
807
931
  async readData(clockId, resolution) {
808
932
  if (!this.isConnected()) {
809
- return [];
933
+ await this.tryReconnect();
934
+ if (!this.isConnected())
935
+ return [];
810
936
  }
811
937
  const messages = [];
812
938
  const timestamp = new Date();
@@ -837,6 +963,7 @@ class SunspecMeter extends BaseSunspecDevice {
837
963
  }
838
964
  catch (error) {
839
965
  console.error(`Error updating meter data: ${error}`);
966
+ await this.markOffline();
840
967
  }
841
968
  return messages;
842
969
  }