@enyo-energy/sunspec-sdk 0.0.47 → 0.0.49
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/dist/cjs/sunspec-devices.cjs +253 -0
- package/dist/cjs/sunspec-devices.d.cts +47 -2
- package/dist/cjs/sunspec-interfaces.cjs +29 -1
- package/dist/cjs/sunspec-interfaces.d.cts +42 -0
- package/dist/cjs/sunspec-modbus-client.cjs +8 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/sunspec-devices.d.ts +47 -2
- package/dist/sunspec-devices.js +255 -2
- package/dist/sunspec-interfaces.d.ts +42 -0
- package/dist/sunspec-interfaces.js +28 -0
- package/dist/sunspec-modbus-client.js +8 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -9,6 +9,77 @@ const enyo_data_bus_value_js_1 = require("@enyo-energy/energy-app-sdk/dist/types
|
|
|
9
9
|
const enyo_source_enum_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-source.enum.js");
|
|
10
10
|
const enyo_meter_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-meter-appliance.js");
|
|
11
11
|
const enyo_battery_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js");
|
|
12
|
+
/**
|
|
13
|
+
* Translated messages (en, de) for the standard SunSpec inverter Evt1 bits.
|
|
14
|
+
* Consumers can render these directly to end users; if no entry matches a
|
|
15
|
+
* locale, the SDK contract is to fall back to the machine-readable `code`.
|
|
16
|
+
*/
|
|
17
|
+
const SUNSPEC_INVERTER_EVENT1_MESSAGES = {
|
|
18
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.GROUND_FAULT]: [
|
|
19
|
+
{ language: 'en', message: 'Ground fault detected' },
|
|
20
|
+
{ language: 'de', message: 'Erdschluss erkannt' },
|
|
21
|
+
],
|
|
22
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.DC_OVER_VOLT]: [
|
|
23
|
+
{ language: 'en', message: 'DC over-voltage' },
|
|
24
|
+
{ language: 'de', message: 'DC-Überspannung' },
|
|
25
|
+
],
|
|
26
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.AC_DISCONNECT]: [
|
|
27
|
+
{ language: 'en', message: 'AC disconnect open' },
|
|
28
|
+
{ language: 'de', message: 'AC-Trennschalter offen' },
|
|
29
|
+
],
|
|
30
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.DC_DISCONNECT]: [
|
|
31
|
+
{ language: 'en', message: 'DC disconnect open' },
|
|
32
|
+
{ language: 'de', message: 'DC-Trennschalter offen' },
|
|
33
|
+
],
|
|
34
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.GRID_DISCONNECT]: [
|
|
35
|
+
{ language: 'en', message: 'Grid disconnected' },
|
|
36
|
+
{ language: 'de', message: 'Netz getrennt' },
|
|
37
|
+
],
|
|
38
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.CABINET_OPEN]: [
|
|
39
|
+
{ language: 'en', message: 'Cabinet open' },
|
|
40
|
+
{ language: 'de', message: 'Gehäuse offen' },
|
|
41
|
+
],
|
|
42
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.MANUAL_SHUTDOWN]: [
|
|
43
|
+
{ language: 'en', message: 'Manual shutdown' },
|
|
44
|
+
{ language: 'de', message: 'Manuelle Abschaltung' },
|
|
45
|
+
],
|
|
46
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.OVER_TEMP]: [
|
|
47
|
+
{ language: 'en', message: 'Over temperature' },
|
|
48
|
+
{ language: 'de', message: 'Übertemperatur' },
|
|
49
|
+
],
|
|
50
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.OVER_FREQUENCY]: [
|
|
51
|
+
{ language: 'en', message: 'AC frequency above limit' },
|
|
52
|
+
{ language: 'de', message: 'AC-Frequenz zu hoch' },
|
|
53
|
+
],
|
|
54
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.UNDER_FREQUENCY]: [
|
|
55
|
+
{ language: 'en', message: 'AC frequency below limit' },
|
|
56
|
+
{ language: 'de', message: 'AC-Frequenz zu niedrig' },
|
|
57
|
+
],
|
|
58
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.AC_OVER_VOLT]: [
|
|
59
|
+
{ language: 'en', message: 'AC over-voltage' },
|
|
60
|
+
{ language: 'de', message: 'AC-Überspannung' },
|
|
61
|
+
],
|
|
62
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.AC_UNDER_VOLT]: [
|
|
63
|
+
{ language: 'en', message: 'AC under-voltage' },
|
|
64
|
+
{ language: 'de', message: 'AC-Unterspannung' },
|
|
65
|
+
],
|
|
66
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.BLOWN_STRING_FUSE]: [
|
|
67
|
+
{ language: 'en', message: 'Blown string fuse' },
|
|
68
|
+
{ language: 'de', message: 'Strangsicherung defekt' },
|
|
69
|
+
],
|
|
70
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.UNDER_TEMP]: [
|
|
71
|
+
{ language: 'en', message: 'Under temperature' },
|
|
72
|
+
{ language: 'de', message: 'Untertemperatur' },
|
|
73
|
+
],
|
|
74
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.MEMORY_LOSS]: [
|
|
75
|
+
{ language: 'en', message: 'Memory loss' },
|
|
76
|
+
{ language: 'de', message: 'Speicherverlust' },
|
|
77
|
+
],
|
|
78
|
+
[sunspec_interfaces_js_1.SunspecInverterEvent1.HW_TEST_FAILURE]: [
|
|
79
|
+
{ language: 'en', message: 'Hardware self-test failed' },
|
|
80
|
+
{ language: 'de', message: 'Hardware-Selbsttest fehlgeschlagen' },
|
|
81
|
+
],
|
|
82
|
+
};
|
|
12
83
|
/**
|
|
13
84
|
* Base abstract class for all Sunspec devices
|
|
14
85
|
*/
|
|
@@ -26,6 +97,7 @@ class BaseSunspecDevice {
|
|
|
26
97
|
dataBusListenerId;
|
|
27
98
|
dataBus;
|
|
28
99
|
retryManager;
|
|
100
|
+
consecutiveReconnectFailures = 0;
|
|
29
101
|
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, retryConfig, appliance) {
|
|
30
102
|
this.energyApp = energyApp;
|
|
31
103
|
this.name = name;
|
|
@@ -84,6 +156,7 @@ class BaseSunspecDevice {
|
|
|
84
156
|
// Re-discover models after reconnect
|
|
85
157
|
await this.sunspecClient.discoverModels(this.baseAddress);
|
|
86
158
|
this.retryManager.reset();
|
|
159
|
+
this.consecutiveReconnectFailures = 0;
|
|
87
160
|
// Update appliance state to Connected
|
|
88
161
|
if (this.applianceId) {
|
|
89
162
|
await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Connected);
|
|
@@ -92,12 +165,23 @@ class BaseSunspecDevice {
|
|
|
92
165
|
console.log(`${this.constructor.name} ${this.applianceId}: Reconnection successful after ${attempt} attempt(s) (opens=${postStats.opens}, closes=${postStats.closes}, current=${postStats.currentlyOpen})`);
|
|
93
166
|
return true;
|
|
94
167
|
}
|
|
168
|
+
this.consecutiveReconnectFailures += 1;
|
|
169
|
+
await this.onConnectionFailure(this.consecutiveReconnectFailures);
|
|
95
170
|
}
|
|
96
171
|
catch (error) {
|
|
97
172
|
console.error(`${this.constructor.name} ${this.applianceId}: Reconnect attempt #${attempt} failed: ${error}`);
|
|
173
|
+
this.consecutiveReconnectFailures += 1;
|
|
174
|
+
await this.onConnectionFailure(this.consecutiveReconnectFailures);
|
|
98
175
|
}
|
|
99
176
|
return false;
|
|
100
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Hook for subclasses to react to a failed reconnect attempt. Called once
|
|
180
|
+
* per failed attempt with the running consecutive-failure count.
|
|
181
|
+
*/
|
|
182
|
+
async onConnectionFailure(_consecutiveFailures) {
|
|
183
|
+
// Default: no-op. SunspecInverter overrides to publish a faulted status.
|
|
184
|
+
}
|
|
101
185
|
/**
|
|
102
186
|
* Mark the device as offline: close the underlying socket so the next readData()
|
|
103
187
|
* cycle sees isConnected() === false and tryReconnect() can establish a fresh
|
|
@@ -152,6 +236,10 @@ exports.BaseSunspecDevice = BaseSunspecDevice;
|
|
|
152
236
|
*/
|
|
153
237
|
class SunspecInverter extends BaseSunspecDevice {
|
|
154
238
|
capabilities;
|
|
239
|
+
/** Emit a connection-lost faulted status after this many consecutive failed reconnect attempts. */
|
|
240
|
+
static CONNECTION_FAULT_THRESHOLD = 3;
|
|
241
|
+
storage;
|
|
242
|
+
errorState = { activeCodes: [], lastStatus: 'healthy' };
|
|
155
243
|
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig, appliance) {
|
|
156
244
|
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig, appliance);
|
|
157
245
|
this.capabilities = capabilities;
|
|
@@ -221,6 +309,7 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
221
309
|
if (mpptModel) {
|
|
222
310
|
console.log(`MPPT model found for inverter ${this.networkDevice.hostname}`);
|
|
223
311
|
}
|
|
312
|
+
await this.loadErrorState();
|
|
224
313
|
this.startDataBusListening();
|
|
225
314
|
}
|
|
226
315
|
async disconnect() {
|
|
@@ -263,11 +352,17 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
263
352
|
voltageL1: inverterData.voltageAN || 0,
|
|
264
353
|
voltageL2: inverterData.voltageBN ?? undefined,
|
|
265
354
|
voltageL3: inverterData.voltageCN ?? undefined,
|
|
355
|
+
meterValueWh: inverterData.acEnergy,
|
|
266
356
|
strings: dcStrings
|
|
267
357
|
}
|
|
268
358
|
};
|
|
269
359
|
messages.push(inverterMessage);
|
|
360
|
+
const statusMessage = await this.detectAndEmitStatusTransition(inverterData, timestamp);
|
|
361
|
+
if (statusMessage) {
|
|
362
|
+
messages.push(statusMessage);
|
|
363
|
+
}
|
|
270
364
|
}
|
|
365
|
+
this.consecutiveReconnectFailures = 0;
|
|
271
366
|
if (this.applianceId) {
|
|
272
367
|
const appliance = await this.applianceManager.findApplianceById(this.applianceId);
|
|
273
368
|
await this.applianceManager.updateAppliance(this.applianceId, {
|
|
@@ -285,6 +380,163 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
285
380
|
}
|
|
286
381
|
return messages;
|
|
287
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* Decode all active error bits from the inverter event registers into
|
|
385
|
+
* `EnyoApplianceErrorCode`s. Override in a subclass to customize the
|
|
386
|
+
* mapping wholesale, or override the per-register helpers
|
|
387
|
+
* (`mapEvt1Bit`, `mapEvt2Bit`, `mapVendorEventBit`) to customize a
|
|
388
|
+
* specific register — typical use-case is vendor-specific bit names.
|
|
389
|
+
*/
|
|
390
|
+
decodeActiveErrors(data) {
|
|
391
|
+
const codes = [];
|
|
392
|
+
const codeIds = [];
|
|
393
|
+
const collect = (raw, mapper) => {
|
|
394
|
+
const value = raw ?? 0;
|
|
395
|
+
for (let bit = 0; bit < 32; bit++) {
|
|
396
|
+
if ((value & (1 << bit)) === 0)
|
|
397
|
+
continue;
|
|
398
|
+
const code = mapper(bit);
|
|
399
|
+
if (!code)
|
|
400
|
+
continue;
|
|
401
|
+
codes.push(code);
|
|
402
|
+
codeIds.push(code.code);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
collect(data.events, (bit) => this.mapEvt1Bit(bit));
|
|
406
|
+
collect(data.events2, (bit) => this.mapEvt2Bit(bit));
|
|
407
|
+
collect(data.vendorEvents1, (bit) => this.mapVendorEventBit(1, bit));
|
|
408
|
+
collect(data.vendorEvents2, (bit) => this.mapVendorEventBit(2, bit));
|
|
409
|
+
collect(data.vendorEvents3, (bit) => this.mapVendorEventBit(3, bit));
|
|
410
|
+
collect(data.vendorEvents4, (bit) => this.mapVendorEventBit(4, bit));
|
|
411
|
+
codeIds.sort();
|
|
412
|
+
return { codes, codeIds };
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Map a set bit in the standard Evt1 register to an error code with
|
|
416
|
+
* de/en translations. Override to customize standard-bit translations.
|
|
417
|
+
*/
|
|
418
|
+
mapEvt1Bit(bit) {
|
|
419
|
+
const named = sunspec_interfaces_js_1.SunspecInverterEvent1[bit];
|
|
420
|
+
if (!named) {
|
|
421
|
+
return { code: `inverter_event1_bit_${bit}` };
|
|
422
|
+
}
|
|
423
|
+
const messages = SUNSPEC_INVERTER_EVENT1_MESSAGES[bit];
|
|
424
|
+
return messages ? { code: named, messages } : { code: named };
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Map a set bit in the Evt2 register. Reserved in the SunSpec spec, so
|
|
428
|
+
* the default emits a generic code with no translation. Override for
|
|
429
|
+
* vendor-specific Evt2 semantics.
|
|
430
|
+
*/
|
|
431
|
+
mapEvt2Bit(bit) {
|
|
432
|
+
return { code: `inverter_event2_bit_${bit}` };
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Map a set bit in one of the four vendor-specific event registers.
|
|
436
|
+
* Override in a vendor-specific subclass to translate vendor bits.
|
|
437
|
+
* Return `undefined` to drop a bit entirely.
|
|
438
|
+
*/
|
|
439
|
+
mapVendorEventBit(registerIndex, bit) {
|
|
440
|
+
return { code: `inverter_vendor_event${registerIndex}_bit_${bit}` };
|
|
441
|
+
}
|
|
442
|
+
hasErrorSetChanged(newCodeIds) {
|
|
443
|
+
const prev = this.errorState.activeCodes;
|
|
444
|
+
if (prev.length !== newCodeIds.length)
|
|
445
|
+
return true;
|
|
446
|
+
for (let i = 0; i < prev.length; i++) {
|
|
447
|
+
if (prev[i] !== newCodeIds[i])
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
buildStatusMessage(status, errorCodes, timestamp) {
|
|
453
|
+
return {
|
|
454
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
455
|
+
message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.ApplianceStateUpdateV1,
|
|
456
|
+
type: 'message',
|
|
457
|
+
source: enyo_source_enum_js_1.EnyoSourceEnum.Device,
|
|
458
|
+
applianceId: this.applianceId,
|
|
459
|
+
timestampIso: timestamp.toISOString(),
|
|
460
|
+
data: {
|
|
461
|
+
status,
|
|
462
|
+
errorCodes,
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
storageKey() {
|
|
467
|
+
return `sunspec-inverter-error-state-${this.applianceId}`;
|
|
468
|
+
}
|
|
469
|
+
async loadErrorState() {
|
|
470
|
+
try {
|
|
471
|
+
this.storage = this.energyApp.useStorage();
|
|
472
|
+
const loaded = await this.storage.load(this.storageKey());
|
|
473
|
+
if (loaded) {
|
|
474
|
+
this.errorState = loaded;
|
|
475
|
+
console.log(`Inverter ${this.applianceId}: loaded persisted error state (lastStatus=${loaded.lastStatus}, codes=[${loaded.activeCodes.join(', ')}])`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
console.error(`Inverter ${this.applianceId}: failed to load persisted error state: ${error}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async persistErrorState() {
|
|
483
|
+
if (!this.storage)
|
|
484
|
+
return;
|
|
485
|
+
try {
|
|
486
|
+
await this.storage.save(this.storageKey(), this.errorState);
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
console.error(`Inverter ${this.applianceId}: failed to persist error state: ${error}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async detectAndEmitStatusTransition(data, timestamp) {
|
|
493
|
+
if (!this.applianceId)
|
|
494
|
+
return undefined;
|
|
495
|
+
const { codes, codeIds } = this.decodeActiveErrors(data);
|
|
496
|
+
const recoveringFromConnectionLoss = this.errorState.lastStatus === 'connection_lost';
|
|
497
|
+
if (!recoveringFromConnectionLoss && !this.hasErrorSetChanged(codeIds)) {
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
const newStatus = codeIds.length === 0
|
|
501
|
+
? enyo_appliance_js_1.EnyoApplianceStatusEnum.Healthy
|
|
502
|
+
: enyo_appliance_js_1.EnyoApplianceStatusEnum.Faulted;
|
|
503
|
+
this.errorState = {
|
|
504
|
+
evt1: data.events,
|
|
505
|
+
evt2: data.events2,
|
|
506
|
+
evtVnd1: data.vendorEvents1,
|
|
507
|
+
evtVnd2: data.vendorEvents2,
|
|
508
|
+
evtVnd3: data.vendorEvents3,
|
|
509
|
+
evtVnd4: data.vendorEvents4,
|
|
510
|
+
activeCodes: codeIds,
|
|
511
|
+
lastStatus: newStatus === enyo_appliance_js_1.EnyoApplianceStatusEnum.Healthy ? 'healthy' : 'faulted',
|
|
512
|
+
};
|
|
513
|
+
await this.persistErrorState();
|
|
514
|
+
console.log(`Inverter ${this.applianceId}: status transition -> ${newStatus} (codes=[${codeIds.join(', ')}])`);
|
|
515
|
+
return this.buildStatusMessage(newStatus, codes, timestamp);
|
|
516
|
+
}
|
|
517
|
+
async onConnectionFailure(consecutiveFailures) {
|
|
518
|
+
if (!this.applianceId || !this.dataBus)
|
|
519
|
+
return;
|
|
520
|
+
if (consecutiveFailures < SunspecInverter.CONNECTION_FAULT_THRESHOLD)
|
|
521
|
+
return;
|
|
522
|
+
if (this.errorState.lastStatus === 'connection_lost')
|
|
523
|
+
return;
|
|
524
|
+
const errorCodes = [{
|
|
525
|
+
code: sunspec_interfaces_js_1.SUNSPEC_CONNECTION_LOST_CODE,
|
|
526
|
+
messages: [
|
|
527
|
+
{ language: 'en', message: 'Modbus connection to inverter lost' },
|
|
528
|
+
{ language: 'de', message: 'Modbus-Verbindung zum Wechselrichter verloren' },
|
|
529
|
+
]
|
|
530
|
+
}];
|
|
531
|
+
const message = this.buildStatusMessage(enyo_appliance_js_1.EnyoApplianceStatusEnum.Faulted, errorCodes, new Date());
|
|
532
|
+
this.errorState = {
|
|
533
|
+
activeCodes: [sunspec_interfaces_js_1.SUNSPEC_CONNECTION_LOST_CODE],
|
|
534
|
+
lastStatus: 'connection_lost',
|
|
535
|
+
};
|
|
536
|
+
await this.persistErrorState();
|
|
537
|
+
console.log(`Inverter ${this.applianceId}: emitting faulted (${sunspec_interfaces_js_1.SUNSPEC_CONNECTION_LOST_CODE}) after ${consecutiveFailures} consecutive reconnect failures`);
|
|
538
|
+
this.dataBus.sendMessage([message]);
|
|
539
|
+
}
|
|
288
540
|
mapOperatingState(state) {
|
|
289
541
|
if (!state)
|
|
290
542
|
return enyo_data_bus_value_js_1.EnyoInverterStateEnum.Off;
|
|
@@ -320,6 +572,7 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
320
572
|
current: mppt.dcCurrent,
|
|
321
573
|
voltage: mppt.dcVoltage,
|
|
322
574
|
powerW: mppt.dcPower,
|
|
575
|
+
meterValueWh: mppt.dcEnergy,
|
|
323
576
|
});
|
|
324
577
|
}
|
|
325
578
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type IRetryConfig, type SunspecBatteryBaseData, SunspecBatteryCapability, type SunspecBatteryControls, SunspecInverterCapability, SunspecMeterCapability, SunspecStorageMode } from "./sunspec-interfaces.cjs";
|
|
1
|
+
import { type IRetryConfig, type SunspecBatteryBaseData, SunspecBatteryCapability, type SunspecBatteryControls, type SunspecInverterData, SunspecInverterCapability, SunspecMeterCapability, SunspecStorageMode } from "./sunspec-interfaces.cjs";
|
|
2
2
|
import { ApplianceManager, EnergyApp } from "@enyo-energy/energy-app-sdk";
|
|
3
|
-
import { type EnyoAppliance, EnyoApplianceName } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
|
|
3
|
+
import { type EnyoAppliance, type EnyoApplianceErrorCode, EnyoApplianceName } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
|
|
4
4
|
import { EnyoNetworkDevice } from "@enyo-energy/energy-app-sdk/dist/types/enyo-network-device.js";
|
|
5
5
|
import { SunspecModbusClient } from "./sunspec-modbus-client.cjs";
|
|
6
6
|
import { ConnectionRetryManager } from "./connection-retry-manager.cjs";
|
|
@@ -23,6 +23,7 @@ export declare abstract class BaseSunspecDevice {
|
|
|
23
23
|
protected dataBusListenerId?: string;
|
|
24
24
|
protected dataBus?: EnergyAppDataBus;
|
|
25
25
|
protected retryManager: ConnectionRetryManager;
|
|
26
|
+
protected consecutiveReconnectFailures: number;
|
|
26
27
|
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, retryConfig?: IRetryConfig, appliance?: EnyoAppliance);
|
|
27
28
|
/**
|
|
28
29
|
* Connect to the device and create/update the appliance
|
|
@@ -53,6 +54,11 @@ export declare abstract class BaseSunspecDevice {
|
|
|
53
54
|
* Returns true if reconnection succeeded.
|
|
54
55
|
*/
|
|
55
56
|
protected tryReconnect(): Promise<boolean>;
|
|
57
|
+
/**
|
|
58
|
+
* Hook for subclasses to react to a failed reconnect attempt. Called once
|
|
59
|
+
* per failed attempt with the running consecutive-failure count.
|
|
60
|
+
*/
|
|
61
|
+
protected onConnectionFailure(_consecutiveFailures: number): Promise<void>;
|
|
56
62
|
/**
|
|
57
63
|
* Mark the device as offline: close the underlying socket so the next readData()
|
|
58
64
|
* cycle sees isConnected() === false and tryReconnect() can establish a fresh
|
|
@@ -66,10 +72,49 @@ export declare abstract class BaseSunspecDevice {
|
|
|
66
72
|
*/
|
|
67
73
|
export declare class SunspecInverter extends BaseSunspecDevice {
|
|
68
74
|
private readonly capabilities;
|
|
75
|
+
/** Emit a connection-lost faulted status after this many consecutive failed reconnect attempts. */
|
|
76
|
+
private static readonly CONNECTION_FAULT_THRESHOLD;
|
|
77
|
+
private storage?;
|
|
78
|
+
private errorState;
|
|
69
79
|
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, capabilities?: SunspecInverterCapability[], retryConfig?: IRetryConfig, appliance?: EnyoAppliance);
|
|
70
80
|
connect(): Promise<void>;
|
|
71
81
|
disconnect(): Promise<void>;
|
|
72
82
|
readData(clockId: string, resolution: '10s' | '30s' | '1m' | '15m'): Promise<EnyoDataBusMessage[]>;
|
|
83
|
+
/**
|
|
84
|
+
* Decode all active error bits from the inverter event registers into
|
|
85
|
+
* `EnyoApplianceErrorCode`s. Override in a subclass to customize the
|
|
86
|
+
* mapping wholesale, or override the per-register helpers
|
|
87
|
+
* (`mapEvt1Bit`, `mapEvt2Bit`, `mapVendorEventBit`) to customize a
|
|
88
|
+
* specific register — typical use-case is vendor-specific bit names.
|
|
89
|
+
*/
|
|
90
|
+
protected decodeActiveErrors(data: SunspecInverterData): {
|
|
91
|
+
codes: EnyoApplianceErrorCode[];
|
|
92
|
+
codeIds: string[];
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Map a set bit in the standard Evt1 register to an error code with
|
|
96
|
+
* de/en translations. Override to customize standard-bit translations.
|
|
97
|
+
*/
|
|
98
|
+
protected mapEvt1Bit(bit: number): EnyoApplianceErrorCode | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* Map a set bit in the Evt2 register. Reserved in the SunSpec spec, so
|
|
101
|
+
* the default emits a generic code with no translation. Override for
|
|
102
|
+
* vendor-specific Evt2 semantics.
|
|
103
|
+
*/
|
|
104
|
+
protected mapEvt2Bit(bit: number): EnyoApplianceErrorCode | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Map a set bit in one of the four vendor-specific event registers.
|
|
107
|
+
* Override in a vendor-specific subclass to translate vendor bits.
|
|
108
|
+
* Return `undefined` to drop a bit entirely.
|
|
109
|
+
*/
|
|
110
|
+
protected mapVendorEventBit(registerIndex: 1 | 2 | 3 | 4, bit: number): EnyoApplianceErrorCode | undefined;
|
|
111
|
+
private hasErrorSetChanged;
|
|
112
|
+
private buildStatusMessage;
|
|
113
|
+
private storageKey;
|
|
114
|
+
private loadErrorState;
|
|
115
|
+
private persistErrorState;
|
|
116
|
+
private detectAndEmitStatusTransition;
|
|
117
|
+
protected onConnectionFailure(consecutiveFailures: number): Promise<void>;
|
|
73
118
|
private mapOperatingState;
|
|
74
119
|
/**
|
|
75
120
|
* Map MPPT data to DC string structure for data bus
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* SunSpec block interfaces with block numbers
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.SunspecMeterCapability = exports.SunspecBatteryCapability = exports.SunspecInverterCapability = exports.SunspecStorageMode = exports.SunspecChargeSource = exports.SunspecVArPctMode = exports.SunspecEnableControl = exports.SunspecConnectionControl = exports.SunspecStorageControlMode = exports.SunspecBatteryEvent1 = exports.SunspecBatteryBankState = exports.SunspecBatteryType = exports.SunspecBatteryControlMode = exports.SunspecBatteryChargeState = exports.SunspecMPPTOperatingState = exports.SunspecModelId = exports.DEFAULT_RETRY_CONFIG = void 0;
|
|
6
|
+
exports.SunspecMeterCapability = exports.SunspecBatteryCapability = exports.SunspecInverterCapability = exports.SunspecStorageMode = exports.SunspecChargeSource = exports.SunspecVArPctMode = exports.SunspecEnableControl = exports.SunspecConnectionControl = exports.SunspecStorageControlMode = exports.SunspecBatteryEvent1 = exports.SunspecBatteryBankState = exports.SunspecBatteryType = exports.SunspecBatteryControlMode = exports.SunspecBatteryChargeState = exports.SunspecMPPTOperatingState = exports.SUNSPEC_CONNECTION_LOST_CODE = exports.SunspecInverterEvent1 = exports.SunspecModelId = exports.DEFAULT_RETRY_CONFIG = void 0;
|
|
7
7
|
exports.DEFAULT_RETRY_CONFIG = {
|
|
8
8
|
phases: [
|
|
9
9
|
{ intervalMs: 10_000, durationMs: 60_000 }, // Phase 1: every 10s for 1 minute
|
|
@@ -33,6 +33,34 @@ var SunspecModelId;
|
|
|
33
33
|
SunspecModelId[SunspecModelId["Controls"] = 123] = "Controls";
|
|
34
34
|
SunspecModelId[SunspecModelId["EndMarker"] = 65535] = "EndMarker";
|
|
35
35
|
})(SunspecModelId || (exports.SunspecModelId = SunspecModelId = {}));
|
|
36
|
+
/**
|
|
37
|
+
* Inverter Event 1 bit positions for Model 101/103
|
|
38
|
+
* Offset 40-41: Evt1 - Inverter event bitfield
|
|
39
|
+
*/
|
|
40
|
+
var SunspecInverterEvent1;
|
|
41
|
+
(function (SunspecInverterEvent1) {
|
|
42
|
+
SunspecInverterEvent1[SunspecInverterEvent1["GROUND_FAULT"] = 0] = "GROUND_FAULT";
|
|
43
|
+
SunspecInverterEvent1[SunspecInverterEvent1["DC_OVER_VOLT"] = 1] = "DC_OVER_VOLT";
|
|
44
|
+
SunspecInverterEvent1[SunspecInverterEvent1["AC_DISCONNECT"] = 2] = "AC_DISCONNECT";
|
|
45
|
+
SunspecInverterEvent1[SunspecInverterEvent1["DC_DISCONNECT"] = 3] = "DC_DISCONNECT";
|
|
46
|
+
SunspecInverterEvent1[SunspecInverterEvent1["GRID_DISCONNECT"] = 4] = "GRID_DISCONNECT";
|
|
47
|
+
SunspecInverterEvent1[SunspecInverterEvent1["CABINET_OPEN"] = 5] = "CABINET_OPEN";
|
|
48
|
+
SunspecInverterEvent1[SunspecInverterEvent1["MANUAL_SHUTDOWN"] = 6] = "MANUAL_SHUTDOWN";
|
|
49
|
+
SunspecInverterEvent1[SunspecInverterEvent1["OVER_TEMP"] = 7] = "OVER_TEMP";
|
|
50
|
+
SunspecInverterEvent1[SunspecInverterEvent1["OVER_FREQUENCY"] = 8] = "OVER_FREQUENCY";
|
|
51
|
+
SunspecInverterEvent1[SunspecInverterEvent1["UNDER_FREQUENCY"] = 9] = "UNDER_FREQUENCY";
|
|
52
|
+
SunspecInverterEvent1[SunspecInverterEvent1["AC_OVER_VOLT"] = 10] = "AC_OVER_VOLT";
|
|
53
|
+
SunspecInverterEvent1[SunspecInverterEvent1["AC_UNDER_VOLT"] = 11] = "AC_UNDER_VOLT";
|
|
54
|
+
SunspecInverterEvent1[SunspecInverterEvent1["BLOWN_STRING_FUSE"] = 12] = "BLOWN_STRING_FUSE";
|
|
55
|
+
SunspecInverterEvent1[SunspecInverterEvent1["UNDER_TEMP"] = 13] = "UNDER_TEMP";
|
|
56
|
+
SunspecInverterEvent1[SunspecInverterEvent1["MEMORY_LOSS"] = 14] = "MEMORY_LOSS";
|
|
57
|
+
SunspecInverterEvent1[SunspecInverterEvent1["HW_TEST_FAILURE"] = 15] = "HW_TEST_FAILURE";
|
|
58
|
+
})(SunspecInverterEvent1 || (exports.SunspecInverterEvent1 = SunspecInverterEvent1 = {}));
|
|
59
|
+
/**
|
|
60
|
+
* Error code emitted when the modbus connection has been lost for several
|
|
61
|
+
* consecutive reconnect attempts.
|
|
62
|
+
*/
|
|
63
|
+
exports.SUNSPEC_CONNECTION_LOST_CODE = 'modbus_connection_lost';
|
|
36
64
|
/**
|
|
37
65
|
* MPPT Operating State values for Model 160
|
|
38
66
|
* These represent the DC module/string operating states
|
|
@@ -110,6 +110,48 @@ export interface SunspecInverterData extends SunspecBlock {
|
|
|
110
110
|
vendorEvents3?: number;
|
|
111
111
|
vendorEvents4?: number;
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Inverter Event 1 bit positions for Model 101/103
|
|
115
|
+
* Offset 40-41: Evt1 - Inverter event bitfield
|
|
116
|
+
*/
|
|
117
|
+
export declare enum SunspecInverterEvent1 {
|
|
118
|
+
GROUND_FAULT = 0,
|
|
119
|
+
DC_OVER_VOLT = 1,
|
|
120
|
+
AC_DISCONNECT = 2,
|
|
121
|
+
DC_DISCONNECT = 3,
|
|
122
|
+
GRID_DISCONNECT = 4,
|
|
123
|
+
CABINET_OPEN = 5,
|
|
124
|
+
MANUAL_SHUTDOWN = 6,
|
|
125
|
+
OVER_TEMP = 7,
|
|
126
|
+
OVER_FREQUENCY = 8,
|
|
127
|
+
UNDER_FREQUENCY = 9,
|
|
128
|
+
AC_OVER_VOLT = 10,
|
|
129
|
+
AC_UNDER_VOLT = 11,
|
|
130
|
+
BLOWN_STRING_FUSE = 12,
|
|
131
|
+
UNDER_TEMP = 13,
|
|
132
|
+
MEMORY_LOSS = 14,
|
|
133
|
+
HW_TEST_FAILURE = 15
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Error code emitted when the modbus connection has been lost for several
|
|
137
|
+
* consecutive reconnect attempts.
|
|
138
|
+
*/
|
|
139
|
+
export declare const SUNSPEC_CONNECTION_LOST_CODE = "modbus_connection_lost";
|
|
140
|
+
/**
|
|
141
|
+
* Persisted error state for a SunSpec inverter, written to storage so the
|
|
142
|
+
* status reporter can detect transitions across process restarts and avoid
|
|
143
|
+
* re-emitting `faulted` for the same already-known errors.
|
|
144
|
+
*/
|
|
145
|
+
export interface SunspecInverterPersistedErrorState {
|
|
146
|
+
evt1?: number;
|
|
147
|
+
evt2?: number;
|
|
148
|
+
evtVnd1?: number;
|
|
149
|
+
evtVnd2?: number;
|
|
150
|
+
evtVnd3?: number;
|
|
151
|
+
evtVnd4?: number;
|
|
152
|
+
activeCodes: string[];
|
|
153
|
+
lastStatus: 'healthy' | 'faulted' | 'connection_lost';
|
|
154
|
+
}
|
|
113
155
|
/**
|
|
114
156
|
* MPPT data structure based on Model 160
|
|
115
157
|
*
|
|
@@ -621,6 +621,7 @@ class SunspecModbusClient {
|
|
|
621
621
|
V_SF: this.extractValue(buffer, 13, 'int16'),
|
|
622
622
|
W_SF: this.extractValue(buffer, 10, 'int16'),
|
|
623
623
|
Hz_SF: this.extractValue(buffer, 12, 'int16'),
|
|
624
|
+
WH_SF: this.extractValue(buffer, 26, 'int16'),
|
|
624
625
|
DCA_SF: this.extractValue(buffer, 18, 'int16'),
|
|
625
626
|
DCV_SF: this.extractValue(buffer, 19, 'int16'),
|
|
626
627
|
DCW_SF: this.extractValue(buffer, 21, 'int16')
|
|
@@ -629,6 +630,7 @@ class SunspecModbusClient {
|
|
|
629
630
|
this.logRegisterRead(101, 13, 'V_SF', scaleFactors.V_SF, 'int16');
|
|
630
631
|
this.logRegisterRead(101, 10, 'W_SF', scaleFactors.W_SF, 'int16');
|
|
631
632
|
this.logRegisterRead(101, 12, 'Hz_SF', scaleFactors.Hz_SF, 'int16');
|
|
633
|
+
this.logRegisterRead(101, 26, 'WH_SF', scaleFactors.WH_SF, 'int16');
|
|
632
634
|
this.logRegisterRead(101, 18, 'DCA_SF', scaleFactors.DCA_SF, 'int16');
|
|
633
635
|
this.logRegisterRead(101, 19, 'DCV_SF', scaleFactors.DCV_SF, 'int16');
|
|
634
636
|
this.logRegisterRead(101, 21, 'DCW_SF', scaleFactors.DCW_SF, 'int16');
|
|
@@ -653,6 +655,12 @@ class SunspecModbusClient {
|
|
|
653
655
|
dcPower: this.applyScaleFactor(dcPowerRaw, scaleFactors.DCW_SF, 'int16', 'DC Power', 20, 101),
|
|
654
656
|
operatingState: stateRaw
|
|
655
657
|
};
|
|
658
|
+
// Read AC Energy (32-bit accumulator) - Offset 24-25
|
|
659
|
+
const acEnergy = this.extractValue(buffer, 24, 'uint32', 2);
|
|
660
|
+
this.logRegisterRead(101, 24, 'AC Energy', acEnergy, 'acc32');
|
|
661
|
+
data.acEnergy = !this.isUnimplementedValue(acEnergy, 'acc32')
|
|
662
|
+
? acEnergy * Math.pow(10, scaleFactors.WH_SF)
|
|
663
|
+
: undefined;
|
|
656
664
|
console.debug('[Model 101] Single Phase Inverter Data:', data);
|
|
657
665
|
return data;
|
|
658
666
|
}
|
package/dist/cjs/version.cjs
CHANGED
|
@@ -9,7 +9,7 @@ exports.getSdkVersion = getSdkVersion;
|
|
|
9
9
|
/**
|
|
10
10
|
* Current version of the enyo Energy App SDK.
|
|
11
11
|
*/
|
|
12
|
-
exports.SDK_VERSION = '0.0.
|
|
12
|
+
exports.SDK_VERSION = '0.0.49';
|
|
13
13
|
/**
|
|
14
14
|
* Gets the current SDK version.
|
|
15
15
|
* @returns The semantic version string of the SDK
|
package/dist/cjs/version.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type IRetryConfig, type SunspecBatteryBaseData, SunspecBatteryCapability, type SunspecBatteryControls, SunspecInverterCapability, SunspecMeterCapability, SunspecStorageMode } from "./sunspec-interfaces.js";
|
|
1
|
+
import { type IRetryConfig, type SunspecBatteryBaseData, SunspecBatteryCapability, type SunspecBatteryControls, type SunspecInverterData, SunspecInverterCapability, SunspecMeterCapability, SunspecStorageMode } from "./sunspec-interfaces.js";
|
|
2
2
|
import { ApplianceManager, EnergyApp } from "@enyo-energy/energy-app-sdk";
|
|
3
|
-
import { type EnyoAppliance, EnyoApplianceName } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
|
|
3
|
+
import { type EnyoAppliance, type EnyoApplianceErrorCode, EnyoApplianceName } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
|
|
4
4
|
import { EnyoNetworkDevice } from "@enyo-energy/energy-app-sdk/dist/types/enyo-network-device.js";
|
|
5
5
|
import { SunspecModbusClient } from "./sunspec-modbus-client.js";
|
|
6
6
|
import { ConnectionRetryManager } from "./connection-retry-manager.js";
|
|
@@ -23,6 +23,7 @@ export declare abstract class BaseSunspecDevice {
|
|
|
23
23
|
protected dataBusListenerId?: string;
|
|
24
24
|
protected dataBus?: EnergyAppDataBus;
|
|
25
25
|
protected retryManager: ConnectionRetryManager;
|
|
26
|
+
protected consecutiveReconnectFailures: number;
|
|
26
27
|
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, retryConfig?: IRetryConfig, appliance?: EnyoAppliance);
|
|
27
28
|
/**
|
|
28
29
|
* Connect to the device and create/update the appliance
|
|
@@ -53,6 +54,11 @@ export declare abstract class BaseSunspecDevice {
|
|
|
53
54
|
* Returns true if reconnection succeeded.
|
|
54
55
|
*/
|
|
55
56
|
protected tryReconnect(): Promise<boolean>;
|
|
57
|
+
/**
|
|
58
|
+
* Hook for subclasses to react to a failed reconnect attempt. Called once
|
|
59
|
+
* per failed attempt with the running consecutive-failure count.
|
|
60
|
+
*/
|
|
61
|
+
protected onConnectionFailure(_consecutiveFailures: number): Promise<void>;
|
|
56
62
|
/**
|
|
57
63
|
* Mark the device as offline: close the underlying socket so the next readData()
|
|
58
64
|
* cycle sees isConnected() === false and tryReconnect() can establish a fresh
|
|
@@ -66,10 +72,49 @@ export declare abstract class BaseSunspecDevice {
|
|
|
66
72
|
*/
|
|
67
73
|
export declare class SunspecInverter extends BaseSunspecDevice {
|
|
68
74
|
private readonly capabilities;
|
|
75
|
+
/** Emit a connection-lost faulted status after this many consecutive failed reconnect attempts. */
|
|
76
|
+
private static readonly CONNECTION_FAULT_THRESHOLD;
|
|
77
|
+
private storage?;
|
|
78
|
+
private errorState;
|
|
69
79
|
constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, capabilities?: SunspecInverterCapability[], retryConfig?: IRetryConfig, appliance?: EnyoAppliance);
|
|
70
80
|
connect(): Promise<void>;
|
|
71
81
|
disconnect(): Promise<void>;
|
|
72
82
|
readData(clockId: string, resolution: '10s' | '30s' | '1m' | '15m'): Promise<EnyoDataBusMessage[]>;
|
|
83
|
+
/**
|
|
84
|
+
* Decode all active error bits from the inverter event registers into
|
|
85
|
+
* `EnyoApplianceErrorCode`s. Override in a subclass to customize the
|
|
86
|
+
* mapping wholesale, or override the per-register helpers
|
|
87
|
+
* (`mapEvt1Bit`, `mapEvt2Bit`, `mapVendorEventBit`) to customize a
|
|
88
|
+
* specific register — typical use-case is vendor-specific bit names.
|
|
89
|
+
*/
|
|
90
|
+
protected decodeActiveErrors(data: SunspecInverterData): {
|
|
91
|
+
codes: EnyoApplianceErrorCode[];
|
|
92
|
+
codeIds: string[];
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Map a set bit in the standard Evt1 register to an error code with
|
|
96
|
+
* de/en translations. Override to customize standard-bit translations.
|
|
97
|
+
*/
|
|
98
|
+
protected mapEvt1Bit(bit: number): EnyoApplianceErrorCode | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* Map a set bit in the Evt2 register. Reserved in the SunSpec spec, so
|
|
101
|
+
* the default emits a generic code with no translation. Override for
|
|
102
|
+
* vendor-specific Evt2 semantics.
|
|
103
|
+
*/
|
|
104
|
+
protected mapEvt2Bit(bit: number): EnyoApplianceErrorCode | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Map a set bit in one of the four vendor-specific event registers.
|
|
107
|
+
* Override in a vendor-specific subclass to translate vendor bits.
|
|
108
|
+
* Return `undefined` to drop a bit entirely.
|
|
109
|
+
*/
|
|
110
|
+
protected mapVendorEventBit(registerIndex: 1 | 2 | 3 | 4, bit: number): EnyoApplianceErrorCode | undefined;
|
|
111
|
+
private hasErrorSetChanged;
|
|
112
|
+
private buildStatusMessage;
|
|
113
|
+
private storageKey;
|
|
114
|
+
private loadErrorState;
|
|
115
|
+
private persistErrorState;
|
|
116
|
+
private detectAndEmitStatusTransition;
|
|
117
|
+
protected onConnectionFailure(consecutiveFailures: number): Promise<void>;
|
|
73
118
|
private mapOperatingState;
|
|
74
119
|
/**
|
|
75
120
|
* Map MPPT data to DC string structure for data bus
|
package/dist/sunspec-devices.js
CHANGED
|
@@ -1,11 +1,82 @@
|
|
|
1
|
-
import { SunspecBatteryChargeState, SunspecInverterCapability, SunspecModelId, SunspecMPPTOperatingState, SunspecStorageMode } from "./sunspec-interfaces.js";
|
|
1
|
+
import { SunspecBatteryChargeState, SunspecInverterCapability, SunspecInverterEvent1, SunspecModelId, SunspecMPPTOperatingState, SunspecStorageMode, SUNSPEC_CONNECTION_LOST_CODE } from "./sunspec-interfaces.js";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { EnyoApplianceConnectionType, EnyoApplianceStateEnum, EnyoApplianceTopologyFeatureEnum, EnyoApplianceTypeEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
|
|
3
|
+
import { EnyoApplianceConnectionType, EnyoApplianceStateEnum, EnyoApplianceStatusEnum, EnyoApplianceTopologyFeatureEnum, EnyoApplianceTypeEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
|
|
4
4
|
import { ConnectionRetryManager } from "./connection-retry-manager.js";
|
|
5
5
|
import { EnyoBatteryStateEnum, EnyoCommandAcknowledgeAnswerEnum, EnyoDataBusMessageEnum, EnyoInverterStateEnum, EnyoStringStateEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
|
|
6
6
|
import { EnyoSourceEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-source.enum.js";
|
|
7
7
|
import { EnyoMeterApplianceAvailableFeaturesEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-meter-appliance.js";
|
|
8
8
|
import { EnyoBatteryFeature, EnyoBatteryStorageMode } from "@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js";
|
|
9
|
+
/**
|
|
10
|
+
* Translated messages (en, de) for the standard SunSpec inverter Evt1 bits.
|
|
11
|
+
* Consumers can render these directly to end users; if no entry matches a
|
|
12
|
+
* locale, the SDK contract is to fall back to the machine-readable `code`.
|
|
13
|
+
*/
|
|
14
|
+
const SUNSPEC_INVERTER_EVENT1_MESSAGES = {
|
|
15
|
+
[SunspecInverterEvent1.GROUND_FAULT]: [
|
|
16
|
+
{ language: 'en', message: 'Ground fault detected' },
|
|
17
|
+
{ language: 'de', message: 'Erdschluss erkannt' },
|
|
18
|
+
],
|
|
19
|
+
[SunspecInverterEvent1.DC_OVER_VOLT]: [
|
|
20
|
+
{ language: 'en', message: 'DC over-voltage' },
|
|
21
|
+
{ language: 'de', message: 'DC-Überspannung' },
|
|
22
|
+
],
|
|
23
|
+
[SunspecInverterEvent1.AC_DISCONNECT]: [
|
|
24
|
+
{ language: 'en', message: 'AC disconnect open' },
|
|
25
|
+
{ language: 'de', message: 'AC-Trennschalter offen' },
|
|
26
|
+
],
|
|
27
|
+
[SunspecInverterEvent1.DC_DISCONNECT]: [
|
|
28
|
+
{ language: 'en', message: 'DC disconnect open' },
|
|
29
|
+
{ language: 'de', message: 'DC-Trennschalter offen' },
|
|
30
|
+
],
|
|
31
|
+
[SunspecInverterEvent1.GRID_DISCONNECT]: [
|
|
32
|
+
{ language: 'en', message: 'Grid disconnected' },
|
|
33
|
+
{ language: 'de', message: 'Netz getrennt' },
|
|
34
|
+
],
|
|
35
|
+
[SunspecInverterEvent1.CABINET_OPEN]: [
|
|
36
|
+
{ language: 'en', message: 'Cabinet open' },
|
|
37
|
+
{ language: 'de', message: 'Gehäuse offen' },
|
|
38
|
+
],
|
|
39
|
+
[SunspecInverterEvent1.MANUAL_SHUTDOWN]: [
|
|
40
|
+
{ language: 'en', message: 'Manual shutdown' },
|
|
41
|
+
{ language: 'de', message: 'Manuelle Abschaltung' },
|
|
42
|
+
],
|
|
43
|
+
[SunspecInverterEvent1.OVER_TEMP]: [
|
|
44
|
+
{ language: 'en', message: 'Over temperature' },
|
|
45
|
+
{ language: 'de', message: 'Übertemperatur' },
|
|
46
|
+
],
|
|
47
|
+
[SunspecInverterEvent1.OVER_FREQUENCY]: [
|
|
48
|
+
{ language: 'en', message: 'AC frequency above limit' },
|
|
49
|
+
{ language: 'de', message: 'AC-Frequenz zu hoch' },
|
|
50
|
+
],
|
|
51
|
+
[SunspecInverterEvent1.UNDER_FREQUENCY]: [
|
|
52
|
+
{ language: 'en', message: 'AC frequency below limit' },
|
|
53
|
+
{ language: 'de', message: 'AC-Frequenz zu niedrig' },
|
|
54
|
+
],
|
|
55
|
+
[SunspecInverterEvent1.AC_OVER_VOLT]: [
|
|
56
|
+
{ language: 'en', message: 'AC over-voltage' },
|
|
57
|
+
{ language: 'de', message: 'AC-Überspannung' },
|
|
58
|
+
],
|
|
59
|
+
[SunspecInverterEvent1.AC_UNDER_VOLT]: [
|
|
60
|
+
{ language: 'en', message: 'AC under-voltage' },
|
|
61
|
+
{ language: 'de', message: 'AC-Unterspannung' },
|
|
62
|
+
],
|
|
63
|
+
[SunspecInverterEvent1.BLOWN_STRING_FUSE]: [
|
|
64
|
+
{ language: 'en', message: 'Blown string fuse' },
|
|
65
|
+
{ language: 'de', message: 'Strangsicherung defekt' },
|
|
66
|
+
],
|
|
67
|
+
[SunspecInverterEvent1.UNDER_TEMP]: [
|
|
68
|
+
{ language: 'en', message: 'Under temperature' },
|
|
69
|
+
{ language: 'de', message: 'Untertemperatur' },
|
|
70
|
+
],
|
|
71
|
+
[SunspecInverterEvent1.MEMORY_LOSS]: [
|
|
72
|
+
{ language: 'en', message: 'Memory loss' },
|
|
73
|
+
{ language: 'de', message: 'Speicherverlust' },
|
|
74
|
+
],
|
|
75
|
+
[SunspecInverterEvent1.HW_TEST_FAILURE]: [
|
|
76
|
+
{ language: 'en', message: 'Hardware self-test failed' },
|
|
77
|
+
{ language: 'de', message: 'Hardware-Selbsttest fehlgeschlagen' },
|
|
78
|
+
],
|
|
79
|
+
};
|
|
9
80
|
/**
|
|
10
81
|
* Base abstract class for all Sunspec devices
|
|
11
82
|
*/
|
|
@@ -23,6 +94,7 @@ export class BaseSunspecDevice {
|
|
|
23
94
|
dataBusListenerId;
|
|
24
95
|
dataBus;
|
|
25
96
|
retryManager;
|
|
97
|
+
consecutiveReconnectFailures = 0;
|
|
26
98
|
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, retryConfig, appliance) {
|
|
27
99
|
this.energyApp = energyApp;
|
|
28
100
|
this.name = name;
|
|
@@ -81,6 +153,7 @@ export class BaseSunspecDevice {
|
|
|
81
153
|
// Re-discover models after reconnect
|
|
82
154
|
await this.sunspecClient.discoverModels(this.baseAddress);
|
|
83
155
|
this.retryManager.reset();
|
|
156
|
+
this.consecutiveReconnectFailures = 0;
|
|
84
157
|
// Update appliance state to Connected
|
|
85
158
|
if (this.applianceId) {
|
|
86
159
|
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Connected);
|
|
@@ -89,12 +162,23 @@ export class BaseSunspecDevice {
|
|
|
89
162
|
console.log(`${this.constructor.name} ${this.applianceId}: Reconnection successful after ${attempt} attempt(s) (opens=${postStats.opens}, closes=${postStats.closes}, current=${postStats.currentlyOpen})`);
|
|
90
163
|
return true;
|
|
91
164
|
}
|
|
165
|
+
this.consecutiveReconnectFailures += 1;
|
|
166
|
+
await this.onConnectionFailure(this.consecutiveReconnectFailures);
|
|
92
167
|
}
|
|
93
168
|
catch (error) {
|
|
94
169
|
console.error(`${this.constructor.name} ${this.applianceId}: Reconnect attempt #${attempt} failed: ${error}`);
|
|
170
|
+
this.consecutiveReconnectFailures += 1;
|
|
171
|
+
await this.onConnectionFailure(this.consecutiveReconnectFailures);
|
|
95
172
|
}
|
|
96
173
|
return false;
|
|
97
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Hook for subclasses to react to a failed reconnect attempt. Called once
|
|
177
|
+
* per failed attempt with the running consecutive-failure count.
|
|
178
|
+
*/
|
|
179
|
+
async onConnectionFailure(_consecutiveFailures) {
|
|
180
|
+
// Default: no-op. SunspecInverter overrides to publish a faulted status.
|
|
181
|
+
}
|
|
98
182
|
/**
|
|
99
183
|
* Mark the device as offline: close the underlying socket so the next readData()
|
|
100
184
|
* cycle sees isConnected() === false and tryReconnect() can establish a fresh
|
|
@@ -148,6 +232,10 @@ export class BaseSunspecDevice {
|
|
|
148
232
|
*/
|
|
149
233
|
export class SunspecInverter extends BaseSunspecDevice {
|
|
150
234
|
capabilities;
|
|
235
|
+
/** Emit a connection-lost faulted status after this many consecutive failed reconnect attempts. */
|
|
236
|
+
static CONNECTION_FAULT_THRESHOLD = 3;
|
|
237
|
+
storage;
|
|
238
|
+
errorState = { activeCodes: [], lastStatus: 'healthy' };
|
|
151
239
|
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1, port = 502, baseAddress = 40000, capabilities = [], retryConfig, appliance) {
|
|
152
240
|
super(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId, port, baseAddress, retryConfig, appliance);
|
|
153
241
|
this.capabilities = capabilities;
|
|
@@ -217,6 +305,7 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
217
305
|
if (mpptModel) {
|
|
218
306
|
console.log(`MPPT model found for inverter ${this.networkDevice.hostname}`);
|
|
219
307
|
}
|
|
308
|
+
await this.loadErrorState();
|
|
220
309
|
this.startDataBusListening();
|
|
221
310
|
}
|
|
222
311
|
async disconnect() {
|
|
@@ -259,11 +348,17 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
259
348
|
voltageL1: inverterData.voltageAN || 0,
|
|
260
349
|
voltageL2: inverterData.voltageBN ?? undefined,
|
|
261
350
|
voltageL3: inverterData.voltageCN ?? undefined,
|
|
351
|
+
meterValueWh: inverterData.acEnergy,
|
|
262
352
|
strings: dcStrings
|
|
263
353
|
}
|
|
264
354
|
};
|
|
265
355
|
messages.push(inverterMessage);
|
|
356
|
+
const statusMessage = await this.detectAndEmitStatusTransition(inverterData, timestamp);
|
|
357
|
+
if (statusMessage) {
|
|
358
|
+
messages.push(statusMessage);
|
|
359
|
+
}
|
|
266
360
|
}
|
|
361
|
+
this.consecutiveReconnectFailures = 0;
|
|
267
362
|
if (this.applianceId) {
|
|
268
363
|
const appliance = await this.applianceManager.findApplianceById(this.applianceId);
|
|
269
364
|
await this.applianceManager.updateAppliance(this.applianceId, {
|
|
@@ -281,6 +376,163 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
281
376
|
}
|
|
282
377
|
return messages;
|
|
283
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Decode all active error bits from the inverter event registers into
|
|
381
|
+
* `EnyoApplianceErrorCode`s. Override in a subclass to customize the
|
|
382
|
+
* mapping wholesale, or override the per-register helpers
|
|
383
|
+
* (`mapEvt1Bit`, `mapEvt2Bit`, `mapVendorEventBit`) to customize a
|
|
384
|
+
* specific register — typical use-case is vendor-specific bit names.
|
|
385
|
+
*/
|
|
386
|
+
decodeActiveErrors(data) {
|
|
387
|
+
const codes = [];
|
|
388
|
+
const codeIds = [];
|
|
389
|
+
const collect = (raw, mapper) => {
|
|
390
|
+
const value = raw ?? 0;
|
|
391
|
+
for (let bit = 0; bit < 32; bit++) {
|
|
392
|
+
if ((value & (1 << bit)) === 0)
|
|
393
|
+
continue;
|
|
394
|
+
const code = mapper(bit);
|
|
395
|
+
if (!code)
|
|
396
|
+
continue;
|
|
397
|
+
codes.push(code);
|
|
398
|
+
codeIds.push(code.code);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
collect(data.events, (bit) => this.mapEvt1Bit(bit));
|
|
402
|
+
collect(data.events2, (bit) => this.mapEvt2Bit(bit));
|
|
403
|
+
collect(data.vendorEvents1, (bit) => this.mapVendorEventBit(1, bit));
|
|
404
|
+
collect(data.vendorEvents2, (bit) => this.mapVendorEventBit(2, bit));
|
|
405
|
+
collect(data.vendorEvents3, (bit) => this.mapVendorEventBit(3, bit));
|
|
406
|
+
collect(data.vendorEvents4, (bit) => this.mapVendorEventBit(4, bit));
|
|
407
|
+
codeIds.sort();
|
|
408
|
+
return { codes, codeIds };
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Map a set bit in the standard Evt1 register to an error code with
|
|
412
|
+
* de/en translations. Override to customize standard-bit translations.
|
|
413
|
+
*/
|
|
414
|
+
mapEvt1Bit(bit) {
|
|
415
|
+
const named = SunspecInverterEvent1[bit];
|
|
416
|
+
if (!named) {
|
|
417
|
+
return { code: `inverter_event1_bit_${bit}` };
|
|
418
|
+
}
|
|
419
|
+
const messages = SUNSPEC_INVERTER_EVENT1_MESSAGES[bit];
|
|
420
|
+
return messages ? { code: named, messages } : { code: named };
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Map a set bit in the Evt2 register. Reserved in the SunSpec spec, so
|
|
424
|
+
* the default emits a generic code with no translation. Override for
|
|
425
|
+
* vendor-specific Evt2 semantics.
|
|
426
|
+
*/
|
|
427
|
+
mapEvt2Bit(bit) {
|
|
428
|
+
return { code: `inverter_event2_bit_${bit}` };
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Map a set bit in one of the four vendor-specific event registers.
|
|
432
|
+
* Override in a vendor-specific subclass to translate vendor bits.
|
|
433
|
+
* Return `undefined` to drop a bit entirely.
|
|
434
|
+
*/
|
|
435
|
+
mapVendorEventBit(registerIndex, bit) {
|
|
436
|
+
return { code: `inverter_vendor_event${registerIndex}_bit_${bit}` };
|
|
437
|
+
}
|
|
438
|
+
hasErrorSetChanged(newCodeIds) {
|
|
439
|
+
const prev = this.errorState.activeCodes;
|
|
440
|
+
if (prev.length !== newCodeIds.length)
|
|
441
|
+
return true;
|
|
442
|
+
for (let i = 0; i < prev.length; i++) {
|
|
443
|
+
if (prev[i] !== newCodeIds[i])
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
buildStatusMessage(status, errorCodes, timestamp) {
|
|
449
|
+
return {
|
|
450
|
+
id: randomUUID(),
|
|
451
|
+
message: EnyoDataBusMessageEnum.ApplianceStateUpdateV1,
|
|
452
|
+
type: 'message',
|
|
453
|
+
source: EnyoSourceEnum.Device,
|
|
454
|
+
applianceId: this.applianceId,
|
|
455
|
+
timestampIso: timestamp.toISOString(),
|
|
456
|
+
data: {
|
|
457
|
+
status,
|
|
458
|
+
errorCodes,
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
storageKey() {
|
|
463
|
+
return `sunspec-inverter-error-state-${this.applianceId}`;
|
|
464
|
+
}
|
|
465
|
+
async loadErrorState() {
|
|
466
|
+
try {
|
|
467
|
+
this.storage = this.energyApp.useStorage();
|
|
468
|
+
const loaded = await this.storage.load(this.storageKey());
|
|
469
|
+
if (loaded) {
|
|
470
|
+
this.errorState = loaded;
|
|
471
|
+
console.log(`Inverter ${this.applianceId}: loaded persisted error state (lastStatus=${loaded.lastStatus}, codes=[${loaded.activeCodes.join(', ')}])`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
console.error(`Inverter ${this.applianceId}: failed to load persisted error state: ${error}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async persistErrorState() {
|
|
479
|
+
if (!this.storage)
|
|
480
|
+
return;
|
|
481
|
+
try {
|
|
482
|
+
await this.storage.save(this.storageKey(), this.errorState);
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
console.error(`Inverter ${this.applianceId}: failed to persist error state: ${error}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async detectAndEmitStatusTransition(data, timestamp) {
|
|
489
|
+
if (!this.applianceId)
|
|
490
|
+
return undefined;
|
|
491
|
+
const { codes, codeIds } = this.decodeActiveErrors(data);
|
|
492
|
+
const recoveringFromConnectionLoss = this.errorState.lastStatus === 'connection_lost';
|
|
493
|
+
if (!recoveringFromConnectionLoss && !this.hasErrorSetChanged(codeIds)) {
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
const newStatus = codeIds.length === 0
|
|
497
|
+
? EnyoApplianceStatusEnum.Healthy
|
|
498
|
+
: EnyoApplianceStatusEnum.Faulted;
|
|
499
|
+
this.errorState = {
|
|
500
|
+
evt1: data.events,
|
|
501
|
+
evt2: data.events2,
|
|
502
|
+
evtVnd1: data.vendorEvents1,
|
|
503
|
+
evtVnd2: data.vendorEvents2,
|
|
504
|
+
evtVnd3: data.vendorEvents3,
|
|
505
|
+
evtVnd4: data.vendorEvents4,
|
|
506
|
+
activeCodes: codeIds,
|
|
507
|
+
lastStatus: newStatus === EnyoApplianceStatusEnum.Healthy ? 'healthy' : 'faulted',
|
|
508
|
+
};
|
|
509
|
+
await this.persistErrorState();
|
|
510
|
+
console.log(`Inverter ${this.applianceId}: status transition -> ${newStatus} (codes=[${codeIds.join(', ')}])`);
|
|
511
|
+
return this.buildStatusMessage(newStatus, codes, timestamp);
|
|
512
|
+
}
|
|
513
|
+
async onConnectionFailure(consecutiveFailures) {
|
|
514
|
+
if (!this.applianceId || !this.dataBus)
|
|
515
|
+
return;
|
|
516
|
+
if (consecutiveFailures < SunspecInverter.CONNECTION_FAULT_THRESHOLD)
|
|
517
|
+
return;
|
|
518
|
+
if (this.errorState.lastStatus === 'connection_lost')
|
|
519
|
+
return;
|
|
520
|
+
const errorCodes = [{
|
|
521
|
+
code: SUNSPEC_CONNECTION_LOST_CODE,
|
|
522
|
+
messages: [
|
|
523
|
+
{ language: 'en', message: 'Modbus connection to inverter lost' },
|
|
524
|
+
{ language: 'de', message: 'Modbus-Verbindung zum Wechselrichter verloren' },
|
|
525
|
+
]
|
|
526
|
+
}];
|
|
527
|
+
const message = this.buildStatusMessage(EnyoApplianceStatusEnum.Faulted, errorCodes, new Date());
|
|
528
|
+
this.errorState = {
|
|
529
|
+
activeCodes: [SUNSPEC_CONNECTION_LOST_CODE],
|
|
530
|
+
lastStatus: 'connection_lost',
|
|
531
|
+
};
|
|
532
|
+
await this.persistErrorState();
|
|
533
|
+
console.log(`Inverter ${this.applianceId}: emitting faulted (${SUNSPEC_CONNECTION_LOST_CODE}) after ${consecutiveFailures} consecutive reconnect failures`);
|
|
534
|
+
this.dataBus.sendMessage([message]);
|
|
535
|
+
}
|
|
284
536
|
mapOperatingState(state) {
|
|
285
537
|
if (!state)
|
|
286
538
|
return EnyoInverterStateEnum.Off;
|
|
@@ -316,6 +568,7 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
316
568
|
current: mppt.dcCurrent,
|
|
317
569
|
voltage: mppt.dcVoltage,
|
|
318
570
|
powerW: mppt.dcPower,
|
|
571
|
+
meterValueWh: mppt.dcEnergy,
|
|
319
572
|
});
|
|
320
573
|
}
|
|
321
574
|
});
|
|
@@ -110,6 +110,48 @@ export interface SunspecInverterData extends SunspecBlock {
|
|
|
110
110
|
vendorEvents3?: number;
|
|
111
111
|
vendorEvents4?: number;
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Inverter Event 1 bit positions for Model 101/103
|
|
115
|
+
* Offset 40-41: Evt1 - Inverter event bitfield
|
|
116
|
+
*/
|
|
117
|
+
export declare enum SunspecInverterEvent1 {
|
|
118
|
+
GROUND_FAULT = 0,
|
|
119
|
+
DC_OVER_VOLT = 1,
|
|
120
|
+
AC_DISCONNECT = 2,
|
|
121
|
+
DC_DISCONNECT = 3,
|
|
122
|
+
GRID_DISCONNECT = 4,
|
|
123
|
+
CABINET_OPEN = 5,
|
|
124
|
+
MANUAL_SHUTDOWN = 6,
|
|
125
|
+
OVER_TEMP = 7,
|
|
126
|
+
OVER_FREQUENCY = 8,
|
|
127
|
+
UNDER_FREQUENCY = 9,
|
|
128
|
+
AC_OVER_VOLT = 10,
|
|
129
|
+
AC_UNDER_VOLT = 11,
|
|
130
|
+
BLOWN_STRING_FUSE = 12,
|
|
131
|
+
UNDER_TEMP = 13,
|
|
132
|
+
MEMORY_LOSS = 14,
|
|
133
|
+
HW_TEST_FAILURE = 15
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Error code emitted when the modbus connection has been lost for several
|
|
137
|
+
* consecutive reconnect attempts.
|
|
138
|
+
*/
|
|
139
|
+
export declare const SUNSPEC_CONNECTION_LOST_CODE = "modbus_connection_lost";
|
|
140
|
+
/**
|
|
141
|
+
* Persisted error state for a SunSpec inverter, written to storage so the
|
|
142
|
+
* status reporter can detect transitions across process restarts and avoid
|
|
143
|
+
* re-emitting `faulted` for the same already-known errors.
|
|
144
|
+
*/
|
|
145
|
+
export interface SunspecInverterPersistedErrorState {
|
|
146
|
+
evt1?: number;
|
|
147
|
+
evt2?: number;
|
|
148
|
+
evtVnd1?: number;
|
|
149
|
+
evtVnd2?: number;
|
|
150
|
+
evtVnd3?: number;
|
|
151
|
+
evtVnd4?: number;
|
|
152
|
+
activeCodes: string[];
|
|
153
|
+
lastStatus: 'healthy' | 'faulted' | 'connection_lost';
|
|
154
|
+
}
|
|
113
155
|
/**
|
|
114
156
|
* MPPT data structure based on Model 160
|
|
115
157
|
*
|
|
@@ -30,6 +30,34 @@ export var SunspecModelId;
|
|
|
30
30
|
SunspecModelId[SunspecModelId["Controls"] = 123] = "Controls";
|
|
31
31
|
SunspecModelId[SunspecModelId["EndMarker"] = 65535] = "EndMarker";
|
|
32
32
|
})(SunspecModelId || (SunspecModelId = {}));
|
|
33
|
+
/**
|
|
34
|
+
* Inverter Event 1 bit positions for Model 101/103
|
|
35
|
+
* Offset 40-41: Evt1 - Inverter event bitfield
|
|
36
|
+
*/
|
|
37
|
+
export var SunspecInverterEvent1;
|
|
38
|
+
(function (SunspecInverterEvent1) {
|
|
39
|
+
SunspecInverterEvent1[SunspecInverterEvent1["GROUND_FAULT"] = 0] = "GROUND_FAULT";
|
|
40
|
+
SunspecInverterEvent1[SunspecInverterEvent1["DC_OVER_VOLT"] = 1] = "DC_OVER_VOLT";
|
|
41
|
+
SunspecInverterEvent1[SunspecInverterEvent1["AC_DISCONNECT"] = 2] = "AC_DISCONNECT";
|
|
42
|
+
SunspecInverterEvent1[SunspecInverterEvent1["DC_DISCONNECT"] = 3] = "DC_DISCONNECT";
|
|
43
|
+
SunspecInverterEvent1[SunspecInverterEvent1["GRID_DISCONNECT"] = 4] = "GRID_DISCONNECT";
|
|
44
|
+
SunspecInverterEvent1[SunspecInverterEvent1["CABINET_OPEN"] = 5] = "CABINET_OPEN";
|
|
45
|
+
SunspecInverterEvent1[SunspecInverterEvent1["MANUAL_SHUTDOWN"] = 6] = "MANUAL_SHUTDOWN";
|
|
46
|
+
SunspecInverterEvent1[SunspecInverterEvent1["OVER_TEMP"] = 7] = "OVER_TEMP";
|
|
47
|
+
SunspecInverterEvent1[SunspecInverterEvent1["OVER_FREQUENCY"] = 8] = "OVER_FREQUENCY";
|
|
48
|
+
SunspecInverterEvent1[SunspecInverterEvent1["UNDER_FREQUENCY"] = 9] = "UNDER_FREQUENCY";
|
|
49
|
+
SunspecInverterEvent1[SunspecInverterEvent1["AC_OVER_VOLT"] = 10] = "AC_OVER_VOLT";
|
|
50
|
+
SunspecInverterEvent1[SunspecInverterEvent1["AC_UNDER_VOLT"] = 11] = "AC_UNDER_VOLT";
|
|
51
|
+
SunspecInverterEvent1[SunspecInverterEvent1["BLOWN_STRING_FUSE"] = 12] = "BLOWN_STRING_FUSE";
|
|
52
|
+
SunspecInverterEvent1[SunspecInverterEvent1["UNDER_TEMP"] = 13] = "UNDER_TEMP";
|
|
53
|
+
SunspecInverterEvent1[SunspecInverterEvent1["MEMORY_LOSS"] = 14] = "MEMORY_LOSS";
|
|
54
|
+
SunspecInverterEvent1[SunspecInverterEvent1["HW_TEST_FAILURE"] = 15] = "HW_TEST_FAILURE";
|
|
55
|
+
})(SunspecInverterEvent1 || (SunspecInverterEvent1 = {}));
|
|
56
|
+
/**
|
|
57
|
+
* Error code emitted when the modbus connection has been lost for several
|
|
58
|
+
* consecutive reconnect attempts.
|
|
59
|
+
*/
|
|
60
|
+
export const SUNSPEC_CONNECTION_LOST_CODE = 'modbus_connection_lost';
|
|
33
61
|
/**
|
|
34
62
|
* MPPT Operating State values for Model 160
|
|
35
63
|
* These represent the DC module/string operating states
|
|
@@ -618,6 +618,7 @@ export class SunspecModbusClient {
|
|
|
618
618
|
V_SF: this.extractValue(buffer, 13, 'int16'),
|
|
619
619
|
W_SF: this.extractValue(buffer, 10, 'int16'),
|
|
620
620
|
Hz_SF: this.extractValue(buffer, 12, 'int16'),
|
|
621
|
+
WH_SF: this.extractValue(buffer, 26, 'int16'),
|
|
621
622
|
DCA_SF: this.extractValue(buffer, 18, 'int16'),
|
|
622
623
|
DCV_SF: this.extractValue(buffer, 19, 'int16'),
|
|
623
624
|
DCW_SF: this.extractValue(buffer, 21, 'int16')
|
|
@@ -626,6 +627,7 @@ export class SunspecModbusClient {
|
|
|
626
627
|
this.logRegisterRead(101, 13, 'V_SF', scaleFactors.V_SF, 'int16');
|
|
627
628
|
this.logRegisterRead(101, 10, 'W_SF', scaleFactors.W_SF, 'int16');
|
|
628
629
|
this.logRegisterRead(101, 12, 'Hz_SF', scaleFactors.Hz_SF, 'int16');
|
|
630
|
+
this.logRegisterRead(101, 26, 'WH_SF', scaleFactors.WH_SF, 'int16');
|
|
629
631
|
this.logRegisterRead(101, 18, 'DCA_SF', scaleFactors.DCA_SF, 'int16');
|
|
630
632
|
this.logRegisterRead(101, 19, 'DCV_SF', scaleFactors.DCV_SF, 'int16');
|
|
631
633
|
this.logRegisterRead(101, 21, 'DCW_SF', scaleFactors.DCW_SF, 'int16');
|
|
@@ -650,6 +652,12 @@ export class SunspecModbusClient {
|
|
|
650
652
|
dcPower: this.applyScaleFactor(dcPowerRaw, scaleFactors.DCW_SF, 'int16', 'DC Power', 20, 101),
|
|
651
653
|
operatingState: stateRaw
|
|
652
654
|
};
|
|
655
|
+
// Read AC Energy (32-bit accumulator) - Offset 24-25
|
|
656
|
+
const acEnergy = this.extractValue(buffer, 24, 'uint32', 2);
|
|
657
|
+
this.logRegisterRead(101, 24, 'AC Energy', acEnergy, 'acc32');
|
|
658
|
+
data.acEnergy = !this.isUnimplementedValue(acEnergy, 'acc32')
|
|
659
|
+
? acEnergy * Math.pow(10, scaleFactors.WH_SF)
|
|
660
|
+
: undefined;
|
|
653
661
|
console.debug('[Model 101] Single Phase Inverter Data:', data);
|
|
654
662
|
return data;
|
|
655
663
|
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enyo-energy/sunspec-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.49",
|
|
4
4
|
"description": "enyo Energy Sunspec SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"typescript": "^5.8.3"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@enyo-energy/energy-app-sdk": "^0.0.
|
|
40
|
+
"@enyo-energy/energy-app-sdk": "^0.0.113"
|
|
41
41
|
},
|
|
42
42
|
"volta": {
|
|
43
43
|
"node": "22.17.0"
|