@enyo-energy/sunspec-sdk 0.0.48 → 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 +251 -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/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 +253 -2
- package/dist/sunspec-interfaces.d.ts +42 -0
- package/dist/sunspec-interfaces.js +28 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -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() {
|
|
@@ -268,7 +357,12 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
268
357
|
}
|
|
269
358
|
};
|
|
270
359
|
messages.push(inverterMessage);
|
|
360
|
+
const statusMessage = await this.detectAndEmitStatusTransition(inverterData, timestamp);
|
|
361
|
+
if (statusMessage) {
|
|
362
|
+
messages.push(statusMessage);
|
|
363
|
+
}
|
|
271
364
|
}
|
|
365
|
+
this.consecutiveReconnectFailures = 0;
|
|
272
366
|
if (this.applianceId) {
|
|
273
367
|
const appliance = await this.applianceManager.findApplianceById(this.applianceId);
|
|
274
368
|
await this.applianceManager.updateAppliance(this.applianceId, {
|
|
@@ -286,6 +380,163 @@ class SunspecInverter extends BaseSunspecDevice {
|
|
|
286
380
|
}
|
|
287
381
|
return messages;
|
|
288
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
|
+
}
|
|
289
540
|
mapOperatingState(state) {
|
|
290
541
|
if (!state)
|
|
291
542
|
return enyo_data_bus_value_js_1.EnyoInverterStateEnum.Off;
|
|
@@ -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
|
*
|
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() {
|
|
@@ -264,7 +353,12 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
264
353
|
}
|
|
265
354
|
};
|
|
266
355
|
messages.push(inverterMessage);
|
|
356
|
+
const statusMessage = await this.detectAndEmitStatusTransition(inverterData, timestamp);
|
|
357
|
+
if (statusMessage) {
|
|
358
|
+
messages.push(statusMessage);
|
|
359
|
+
}
|
|
267
360
|
}
|
|
361
|
+
this.consecutiveReconnectFailures = 0;
|
|
268
362
|
if (this.applianceId) {
|
|
269
363
|
const appliance = await this.applianceManager.findApplianceById(this.applianceId);
|
|
270
364
|
await this.applianceManager.updateAppliance(this.applianceId, {
|
|
@@ -282,6 +376,163 @@ export class SunspecInverter extends BaseSunspecDevice {
|
|
|
282
376
|
}
|
|
283
377
|
return messages;
|
|
284
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
|
+
}
|
|
285
536
|
mapOperatingState(state) {
|
|
286
537
|
if (!state)
|
|
287
538
|
return EnyoInverterStateEnum.Off;
|
|
@@ -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
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED