@impact0815/node-red-contrib-alphaess-modbus 0.0.0-stage → 0.4.0
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/CHANGELOG.md +98 -0
- package/LICENSE +21 -0
- package/README.de.md +422 -0
- package/README.md +427 -2
- package/alphaess-modbus.html +256 -0
- package/alphaess-modbus.js +399 -0
- package/examples/alphaess-modbus-example.json +420 -0
- package/lib/commands.js +145 -0
- package/lib/context-store.js +30 -0
- package/lib/derived.js +163 -0
- package/lib/i18n.js +57 -0
- package/lib/modbus-tcp.js +204 -0
- package/lib/mqtt.js +80 -0
- package/lib/registers.js +434 -0
- package/locales/de/alphaess-modbus.html +317 -0
- package/locales/de/alphaess-modbus.json +106 -0
- package/locales/en-US/alphaess-modbus.html +315 -0
- package/locales/en-US/alphaess-modbus.json +106 -0
- package/package.json +54 -5
package/lib/registers.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* AlphaESS register map, based on the "AlphaESS Household Modbus Register Parameter List"
|
|
4
|
+
* (successor of "Register Parameter List V1.1"). Only household-relevant, readable blocks are included.
|
|
5
|
+
*
|
|
6
|
+
* count = registers read with a current firmware
|
|
7
|
+
* minCount = registers of the older documentation (V1.1). If a system rejects the longer
|
|
8
|
+
* request (Modbus exception 2/3), the node falls back to minCount automatically.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------- blocks ----
|
|
12
|
+
const BLOCKS = {
|
|
13
|
+
grid: { start: 0x0010, count: 51, minCount: 39, label: 'Grid meter' },
|
|
14
|
+
pvMeter: { start: 0x0090, count: 39, label: 'PV meter' },
|
|
15
|
+
battery: { start: 0x0100, count: 73, label: 'Battery' },
|
|
16
|
+
batteryInfo: { start: 0x0150, count: 30, label: 'Battery serial numbers' },
|
|
17
|
+
inverter: { start: 0x0400, count: 85, minCount: 83, label: 'Inverter' },
|
|
18
|
+
inverterInfo: { start: 0x0640, count: 25, minCount: 20, label: 'Inverter info' },
|
|
19
|
+
systemInfo: { start: 0x0740, count: 25, minCount: 15, label: 'System info' },
|
|
20
|
+
systemConfig: { start: 0x0800, count: 18, label: 'System config' },
|
|
21
|
+
timePeriod: { start: 0x084F, count: 19, label: 'Time period control' },
|
|
22
|
+
dispatch: { start: 0x0880, count: 11, minCount: 9, label: 'Dispatch' },
|
|
23
|
+
systemRun: { start: 0x08D0, count: 6, label: 'System running data' }
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ----------------------------------------------------------------- enums ----
|
|
27
|
+
const WORK_MODE = ['Wait', 'Online', 'UPS', 'Bypass', 'Fault', 'DC', 'SelfTest',
|
|
28
|
+
'Check', 'Update Master', 'Update Slave', 'Update ARM'];
|
|
29
|
+
|
|
30
|
+
const BATTERY_TYPE = {
|
|
31
|
+
2: 'M4860', 3: 'M48100', 13: '48112-P', 16: 'Smile5-BAT', 24: 'M4856-P',
|
|
32
|
+
27: 'Smile-BAT-10.3P', 30: 'Smile-BAT-10.1P', 33: 'Smile-BAT-5.8P',
|
|
33
|
+
34: 'Smile-BAT5-JP', 35: 'Smile-BAT-13.7P'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const RELAY_STATUS = ['Charge and discharge relays open', 'Only discharge relay closed',
|
|
37
|
+
'Only charge relay closed', 'Charge and discharge relays closed'];
|
|
38
|
+
|
|
39
|
+
// Note 7 / Note 29
|
|
40
|
+
const DISPATCH_MODE = {
|
|
41
|
+
1: 'Battery only charges from PV', 2: 'State of Charge control', 3: 'Load Following',
|
|
42
|
+
4: 'Maximise Output', 5: 'Normal Mode', 6: 'Optimise Consumption',
|
|
43
|
+
7: 'Maximise Consumption', 8: 'ECO Mode', 9: 'FCAS Mode', 10: 'PV Power Setting',
|
|
44
|
+
19: 'No Battery Charge', 20: 'BurnIn Mode', 21: 'OSW Battery only charges from PV',
|
|
45
|
+
22: 'OSW State of Charge control', 23: 'OSW Load Following', 25: 'OSW No Battery Charge'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const DISPATCH_PV_SWITCH = { 1: 'Open', 2: 'Close' };
|
|
49
|
+
|
|
50
|
+
const SYSTEM_MODE = { 1: 'AC', 2: 'DC', 3: 'Hybrid' };
|
|
51
|
+
const METER_CT = ['Grid & PV use CT', 'Grid CT, PV meter', 'Grid meter, PV CT', 'Grid & PV use meter'];
|
|
52
|
+
const TIME_PERIOD_FLAG = ['Disabled', 'Charge enabled', 'Discharge enabled', 'Charge & discharge enabled'];
|
|
53
|
+
|
|
54
|
+
// Bit tables (index = bit number)
|
|
55
|
+
const BATTERY_FAULT = { // Note 4
|
|
56
|
+
'EMS2.5': {
|
|
57
|
+
2: 'Cell Temp Differ', 3: 'Balancer Fault', 4: 'Charge Over Current', 5: 'Balancer Mos Fault',
|
|
58
|
+
6: 'Discharge Over Current', 7: 'Pole Over Temp', 8: 'Cell Over Volt', 9: 'Cell Volt Differ',
|
|
59
|
+
10: 'Discharge Low Temp', 12: 'Cell Low Volt', 13: 'ISO Comm Fault', 14: 'LMU SN Repeat',
|
|
60
|
+
16: 'IR Fault', 17: 'LMU Comm Fault', 18: 'Cell Over Temp', 19: 'BMU Comm Fault',
|
|
61
|
+
21: 'Charge Low Temp', 23: 'Volt Detect Fault', 24: 'Wire Harness Fault', 26: 'Relay Fault',
|
|
62
|
+
27: 'LMU ID Repeat', 28: 'LMU ID Discontinuous', 29: 'Current Sensor Fault', 31: 'Temp Sensor Fault'
|
|
63
|
+
},
|
|
64
|
+
'EMS3.x': {
|
|
65
|
+
0: 'Temperature sensor error', 1: 'Mos error', 2: 'Circuit breaker open',
|
|
66
|
+
3: 'Dial switching mode inconsistence', 4: 'Slave battery communication lost', 5: 'Sn missing',
|
|
67
|
+
6: 'Master battery communication lost', 7: 'Firmware versions inconsistence', 8: 'Multi master error',
|
|
68
|
+
9: 'Mos high temperature', 10: 'Insulation fault', 11: 'Total pressure abnormal',
|
|
69
|
+
12: 'Mos feedback failure', 13: 'Prefilled failure', 14: '17823 communication failure',
|
|
70
|
+
15: '17841 communication failure', 16: 'Mos temperature sensor error'
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const BATTERY_WARNING = { // Note 28
|
|
75
|
+
'EMS2.5': {},
|
|
76
|
+
'EMS3.x': {
|
|
77
|
+
0: 'Temperature imbalance', 1: 'Over temperature', 2: 'Discharge low temperature',
|
|
78
|
+
3: 'Charge low temperature', 4: 'Discharge over current', 5: 'Charge over current',
|
|
79
|
+
6: 'Cell over voltage', 7: 'Cell low voltage', 8: 'Software versions inconsistent',
|
|
80
|
+
9: 'Mos temperature sensor error', 10: 'SOC inconsistent', 11: 'BMS communication lost',
|
|
81
|
+
12: 'BMS fan error'
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const SYSTEM_FAULT = { // Note 6, platform EMS2.5/EMS3.5/EMS3.6
|
|
86
|
+
0: 'Network_Card_Fault', 1: 'Rtc_Fault', 2: 'EEprom_Fault', 3: 'INV_Comms_Error',
|
|
87
|
+
4: 'Grid_Meter_Lost', 5: 'PV_Meter_Lost', 6: 'BMS_Lost', 7: 'UPS_Battery_Volt_Low', 8: 'Backup_Overload',
|
|
88
|
+
9: 'INV_Slave_Lost', 10: 'INV_Master_Lost', 11: 'Parallel_Comm_Error', 12: 'Parallel_Mode_Differ',
|
|
89
|
+
13: 'Flash_Fault', 14: 'SDRAM error', 15: 'Extension CAN error', 16: 'inv type not specified'
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const INVERTER_FAULT1 = ['Grid_OVP', 'Grid_UVP', 'Grid_OFP', 'Grid_UFP', 'phase_locked_fault', 'bus_ovp1',
|
|
93
|
+
'bus_ovp2', 'insulation_fault', 'gfci_fault', 'gfci_test_fault', 'grid_relay_fault', 'over_temperature',
|
|
94
|
+
'pv_reverse', 'bat_reverse', 'm_s_com_fault', 'display_com_fault', 'chip1_upgrade_fault', 'mppt1_ovp',
|
|
95
|
+
'mppt1_sw_ocp', 'mppt1_hw_ocp', 'mppt1_otp', 'mppt2_ovp', 'mppt2_sw_ocp', 'mppt2_hw_ocp', 'mppt2_otp',
|
|
96
|
+
'bat_ovp', 'bat_uvp', 'battery_lose', 'bat_otp', 'bat1_charge_ocp', 'bat1_discharge_ocp', 'bat2_charge_ocp'];
|
|
97
|
+
|
|
98
|
+
const INVERTER_FAULT2 = ['bat2_discharge_ocp', 'bat1_hw_ocp', 'bat2_hw_ocp', 'inv_otp', 'inv_ovp', 'inv_uvp',
|
|
99
|
+
'output_dc_over_current', 'inv_ocp', 'inv_hw_ocp', 'output_dc_over_voltage', 'output_short',
|
|
100
|
+
'output_overload', 'apu_uvp', 'bat_relay_fault', 'dc_input_disturbance', 'grid_disturbance',
|
|
101
|
+
'gird_unbalance', 'freq_jitter', 'grid_overcurrent', 'grid_current_track_fault', 'backup_ovp',
|
|
102
|
+
'dc_bus_unbalancevolt', 'dc_bus_undervolt', 'dc_bus_unbalancevolt2', 'igbt_over_current',
|
|
103
|
+
'grid_disturbance2', 'afci_check_protect', 'grid_current_sampling_abnormal', 'dsp_selfcheck',
|
|
104
|
+
'grid_short_time_over_current', 'bat_overvolt_hardware_fault', 'zero_ground_fault'];
|
|
105
|
+
|
|
106
|
+
// Note 27
|
|
107
|
+
const INVERTER_FAULT_EXT1 = ['ac_hct_check_failure', 'dci_consistency_failure', 'gfci_consistency_failure',
|
|
108
|
+
'relay_device_failure', 'ac_hct_failure', 'gournd_i_failure', 'utility_phase_failure', 'utility_loss',
|
|
109
|
+
'internal_fan_failure', 'fac_consistency_failure', 'vac_consitency_failure', 'phase_angle_failure',
|
|
110
|
+
'dsp_communication_failure', 'eeprom_rw_failure', 'vac_failure', 'fac_failure', 'external_fan_failure',
|
|
111
|
+
'afci_device_failure', 'bus_soft_timeout', 'dc_bus_short', 'inv_soft_timeout', 'grid_load_reverse',
|
|
112
|
+
'lpe_reverse', 'ems_sci', 'ems_can', 'sps_12v_ref', '1p5v_ref', '0p5v_ref', 'ntc_loss', 'inv_hct',
|
|
113
|
+
'load_ct', 'pv1_ct'];
|
|
114
|
+
|
|
115
|
+
const INVERTER_FAULT_EXT2 = ['pv2_ct', 'bat1_ct', 'bat2_ct', 'bypass_rly', 'load_rly', 'npe_rly', 'dci',
|
|
116
|
+
'watchdog', 'inv_open_loop', 'sw_consistency', 'n_n_reverse_lost', 'ini_fault', 'dsp_b_fault',
|
|
117
|
+
'inverter_circuit_abnormal', 'boost_circuit_abnormal', 'data_storage_error', 'para_can',
|
|
118
|
+
'para_synsignal_wrong', 'para_sw_diff', 'para_module_wrong', 'para_negative_power', 'para_multi_master',
|
|
119
|
+
'para_turnon_wrong', 'hw_ver_diff', 'bus_unbalance', 'inv_line_short'];
|
|
120
|
+
|
|
121
|
+
// Note 32
|
|
122
|
+
const INVERTER_WARNING1 = ['bat_over_voltage_alarm', 'bat_under_voltage_alarm', 'output_overload_alarm',
|
|
123
|
+
'abnormal_temperature_sensor', 'dc_power_alarm', 'battery_stops_running_alarm', 'over_temperature_alarm',
|
|
124
|
+
'pv_volt_high_alarm', 'bat_open_alarm', 'bat_reverse_alarm', 'bus_over_alarm', 'grid_loss_alarm',
|
|
125
|
+
'grid_volt_alarm', 'grid_freq_alarm', '10min_grid_volt_alarm', 'grid_volt_inst_over', 'pe_loss_alarm',
|
|
126
|
+
'ln_reverse', 'low_temper_alarm', 'gfci_alarm', 'iso_alarm', 'dci_alarm', 'dcv_alarm', 'island_alarm',
|
|
127
|
+
'fan_abnormal_alarm', 'n_loss_alarm', 'ems_sci_alarm', 'ems_can_alarm', 'flashid_alarm',
|
|
128
|
+
'read_flash_alarm', 'write_flash_alarm', 'machine_type_alarm'];
|
|
129
|
+
|
|
130
|
+
const INVERTER_WARNING2 = ['inv_volt_low_alarm', 'inv_over_curr_sw_alarm', 'inv_over_curr_hw_alarm',
|
|
131
|
+
'bst_over_curr_sw_alarm', 'bst_over_curr_hw_alarm', 'buck_bst_over_curr_sw_alarm',
|
|
132
|
+
'buck_bst_over_curr_hw_alarm', 'bus_under', 'no_pv_input_alarm', 'input_power_limit_alarm',
|
|
133
|
+
'output_power_limit_alarm', 'reduce_pby_over_freq_alarm', 'reduce_pby_over_volt_alarm',
|
|
134
|
+
'reduce_pby_over_temp_alarm', 'hvrt_alarm', 'lvrt_alarm', 'ntc_fail_alarm', 'grid_waveform_abnormal_alarm',
|
|
135
|
+
'eps_capacitance_decrease', 'para_alarm', 'para_error_location', 'para_avg_overload',
|
|
136
|
+
'para_module_addr_same', 'para_online_enter_fail', 'para_unbalance_power', 'para_turnon_inconsistent',
|
|
137
|
+
'grid_backup_n_lost', 'bat_num_abnormal', 'grid_phase_order_fault', 'dcv_sample_abnormal',
|
|
138
|
+
'blackbox_flash_fault', 'rtc_fault'];
|
|
139
|
+
|
|
140
|
+
// --------------------------------------------------------------- helpers ----
|
|
141
|
+
function reader(values, start) {
|
|
142
|
+
const has = (a, n) => a - start >= 0 && a - start + (n || 1) <= values.length;
|
|
143
|
+
const reg = (a) => {
|
|
144
|
+
const v = values[a - start];
|
|
145
|
+
if (v === undefined) throw new RangeError(`Register 0x${a.toString(16)} not in block`);
|
|
146
|
+
return v & 0xFFFF;
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
has,
|
|
150
|
+
u16: reg,
|
|
151
|
+
s16: (a) => { const v = reg(a); return v & 0x8000 ? v - 0x10000 : v; },
|
|
152
|
+
u32: (a) => ((reg(a) << 16) | reg(a + 1)) >>> 0,
|
|
153
|
+
s32: (a) => (reg(a) << 16) | reg(a + 1),
|
|
154
|
+
ascii: (a, n) => {
|
|
155
|
+
let s = '';
|
|
156
|
+
for (let i = 0; i < n; i++) {
|
|
157
|
+
const v = reg(a + i);
|
|
158
|
+
s += String.fromCharCode((v >> 8) & 0xFF, v & 0xFF);
|
|
159
|
+
}
|
|
160
|
+
// strip NUL and non-printable characters (unused registers often contain 0x00 or 0xFF)
|
|
161
|
+
return s.replace(/[^\x20-\x7E]/g, '').trim();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const round = (v, d) => { const f = Math.pow(10, d); return Math.round(v * f) / f; };
|
|
167
|
+
|
|
168
|
+
/** returns { raw, hex, active: [{ bit, text }] } */
|
|
169
|
+
function bits(value, table) {
|
|
170
|
+
const active = [];
|
|
171
|
+
for (let b = 0; b < 32; b++) {
|
|
172
|
+
if ((value >>> b) & 1) active.push({ bit: b, text: (table && table[b]) || `bit ${b}` });
|
|
173
|
+
}
|
|
174
|
+
return { raw: value, hex: '0x' + value.toString(16).toUpperCase().padStart(8, '0'), active };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const hm = (h, m) => String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0');
|
|
178
|
+
|
|
179
|
+
// -------------------------------------------------------------- decoders ----
|
|
180
|
+
function decodeMeter(values, S) {
|
|
181
|
+
const r = reader(values, S); // 0x0010 (grid) or 0x0090 (PV meter), same layout
|
|
182
|
+
const d = {
|
|
183
|
+
totalEnergyFeedToGrid: round(r.u32(S + 0) * 0.01, 2), // kWh
|
|
184
|
+
totalEnergyConsumeFromGrid: round(r.u32(S + 2) * 0.01, 2), // kWh
|
|
185
|
+
voltage: [r.u16(S + 4), r.u16(S + 5), r.u16(S + 6)], // V
|
|
186
|
+
current: [round(r.s16(S + 7) * 0.1, 1), round(r.s16(S + 8) * 0.1, 1), round(r.s16(S + 9) * 0.1, 1)],
|
|
187
|
+
frequency: round(r.u16(S + 10) * 0.01, 2),
|
|
188
|
+
activePower: [r.s32(S + 11), r.s32(S + 13), r.s32(S + 15)],
|
|
189
|
+
totalActivePower: r.s32(S + 17),
|
|
190
|
+
reactivePower: [r.s32(S + 19), r.s32(S + 21), r.s32(S + 23)],
|
|
191
|
+
totalReactivePower: r.s32(S + 25),
|
|
192
|
+
apparentPower: [r.s32(S + 27), r.s32(S + 29), r.s32(S + 31)],
|
|
193
|
+
totalApparentPower: r.s32(S + 33),
|
|
194
|
+
powerFactor: [round(r.s16(S + 35) * 0.01, 2), round(r.s16(S + 36) * 0.01, 2), round(r.s16(S + 37) * 0.01, 2)],
|
|
195
|
+
totalPowerFactor: round(r.s16(S + 38) * 0.01, 2)
|
|
196
|
+
};
|
|
197
|
+
// per-phase energy counters (0x0037..0x0042, grid meter only, newer firmware)
|
|
198
|
+
if (S === 0x0010 && r.has(0x0037, 12)) {
|
|
199
|
+
d.energyConsumeFromGridPhase = [0x0037, 0x003B, 0x003F].map((a) => round(r.s32(a) * 0.01, 2)); // kWh
|
|
200
|
+
d.energyFeedToGridPhase = [0x0039, 0x003D, 0x0041].map((a) => round(r.s32(a) * 0.01, 2)); // kWh
|
|
201
|
+
}
|
|
202
|
+
return d;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function decodeBattery(values, platform) {
|
|
206
|
+
const r = reader(values, 0x0100);
|
|
207
|
+
const status = r.u16(0x0103);
|
|
208
|
+
const relay = r.u16(0x0104);
|
|
209
|
+
const type = r.u16(0x011A);
|
|
210
|
+
const faultTable = BATTERY_FAULT[platform] || BATTERY_FAULT['EMS3.x'];
|
|
211
|
+
const warnTable = BATTERY_WARNING[platform] || BATTERY_WARNING['EMS3.x'];
|
|
212
|
+
const faults = [];
|
|
213
|
+
const warnings = [];
|
|
214
|
+
for (let i = 0; i < 6; i++) faults.push(r.u32(0x0131 + i * 2));
|
|
215
|
+
for (let i = 0; i < 6; i++) warnings.push(r.u32(0x013D + i * 2));
|
|
216
|
+
return {
|
|
217
|
+
voltage: round(r.u16(0x0100) * 0.1, 1),
|
|
218
|
+
current: round(r.s16(0x0101) * 0.1, 1),
|
|
219
|
+
soc: round(r.u16(0x0102) * 0.1, 1),
|
|
220
|
+
status: { raw: status, charge: (status >> 8) & 0xFF, discharge: status & 0xFF },
|
|
221
|
+
relayStatus: { raw: relay, text: RELAY_STATUS[relay] || 'unknown' },
|
|
222
|
+
minCellVoltage: { value: round(r.u16(0x0107) * 0.001, 3), pack: r.u16(0x0105), cell: r.u16(0x0106) },
|
|
223
|
+
maxCellVoltage: { value: round(r.u16(0x010A) * 0.001, 3), pack: r.u16(0x0108), cell: r.u16(0x0109) },
|
|
224
|
+
cellVoltageSpread: round((r.u16(0x010A) - r.u16(0x0107)) * 0.001, 3),
|
|
225
|
+
minCellTemperature: { value: round(r.s16(0x010D) * 0.1, 1), pack: r.u16(0x010B), cell: r.u16(0x010C) },
|
|
226
|
+
maxCellTemperature: { value: round(r.s16(0x0110) * 0.1, 1), pack: r.u16(0x010E), cell: r.u16(0x010F) },
|
|
227
|
+
maxChargeCurrent: round(r.u16(0x0111) * 0.1, 1),
|
|
228
|
+
maxDischargeCurrent: round(r.u16(0x0112) * 0.1, 1),
|
|
229
|
+
chargeCutoffVoltage: round(r.u16(0x0113) * 0.1, 1),
|
|
230
|
+
dischargeCutoffVoltage: round(r.u16(0x0114) * 0.1, 1),
|
|
231
|
+
bmuSoftwareVersion: r.u16(0x0115),
|
|
232
|
+
lmuSoftwareVersion: r.u16(0x0116),
|
|
233
|
+
isoSoftwareVersion: r.u16(0x0117),
|
|
234
|
+
moduleCount: r.u16(0x0118),
|
|
235
|
+
capacity: round(r.u16(0x0119) * 0.1, 1), // kWh
|
|
236
|
+
type: { raw: type, text: BATTERY_TYPE[type] || 'unknown' },
|
|
237
|
+
soh: round(r.u16(0x011B) * 0.1, 1),
|
|
238
|
+
warning: bits(r.u32(0x011C), warnTable),
|
|
239
|
+
fault: bits(r.u32(0x011E), faultTable),
|
|
240
|
+
chargeEnergy: round(r.u32(0x0120) * 0.1, 1), // kWh
|
|
241
|
+
dischargeEnergy: round(r.u32(0x0122) * 0.1, 1), // kWh
|
|
242
|
+
energyChargeFromGrid: round(r.u32(0x0124) * 0.1, 1), // kWh
|
|
243
|
+
power: r.s16(0x0126), // W, - charge / + discharge
|
|
244
|
+
remainingTime: r.u16(0x0127), // min
|
|
245
|
+
implementationChargeSoc: round(r.u16(0x0128) * 0.1, 1),
|
|
246
|
+
implementationDischargeSoc: round(r.u16(0x0129) * 0.1, 1),
|
|
247
|
+
remainingChargeSoc: round(r.u16(0x012A) * 0.1, 1),
|
|
248
|
+
remainingDischargeSoc: round(r.u16(0x012B) * 0.1, 1),
|
|
249
|
+
maxChargePower: r.u16(0x012C), // W
|
|
250
|
+
maxDischargePower: r.u16(0x012D), // W
|
|
251
|
+
mosControl: r.u16(0x012E),
|
|
252
|
+
socCalibration: r.u16(0x012F),
|
|
253
|
+
singleCutErrorCode: r.u16(0x0130),
|
|
254
|
+
faults, // fault1..6 (raw uint32, no bit table in the documentation)
|
|
255
|
+
warnings // warning1..6
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** battery serial numbers 0x0150..0x016D, 6 × 10 characters; empty slots are left out */
|
|
260
|
+
function decodeBatteryInfo(values) {
|
|
261
|
+
const r = reader(values, 0x0150);
|
|
262
|
+
const serialNumbers = [];
|
|
263
|
+
for (let i = 0; i < 6; i++) {
|
|
264
|
+
const sn = r.ascii(0x0150 + i * 5, 5);
|
|
265
|
+
if (sn) serialNumbers.push(sn);
|
|
266
|
+
}
|
|
267
|
+
return { serialNumbers };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function decodeInverter(values) {
|
|
271
|
+
const r = reader(values, 0x0400);
|
|
272
|
+
const pv = [];
|
|
273
|
+
for (let i = 0; i < 6; i++) {
|
|
274
|
+
const b = 0x041D + i * 4;
|
|
275
|
+
pv.push({ voltage: round(r.u16(b) * 0.1, 1), current: round(r.u16(b + 1) * 0.1, 1), power: r.u32(b + 2) });
|
|
276
|
+
}
|
|
277
|
+
const workMode = r.u16(0x0440);
|
|
278
|
+
const w1 = r.u32(0x0436), w2 = r.u32(0x0438);
|
|
279
|
+
const d = {
|
|
280
|
+
voltage: [0, 1, 2].map((i) => round(r.u16(0x0400 + i) * 0.1, 1)),
|
|
281
|
+
current: [0, 1, 2].map((i) => round(r.s16(0x0403 + i) * 0.1, 1)),
|
|
282
|
+
power: [r.s32(0x0406), r.s32(0x0408), r.s32(0x040A)],
|
|
283
|
+
powerTotal: r.s32(0x040C),
|
|
284
|
+
backup: {
|
|
285
|
+
voltage: [0, 1, 2].map((i) => round(r.u16(0x040E + i) * 0.1, 1)),
|
|
286
|
+
current: [0, 1, 2].map((i) => round(r.u16(0x0411 + i) * 0.1, 1)),
|
|
287
|
+
power: [r.u32(0x0414), r.u32(0x0416), r.u32(0x0418)],
|
|
288
|
+
powerTotal: r.u32(0x041A),
|
|
289
|
+
frequency: round(r.u16(0x0449) * 0.01, 2)
|
|
290
|
+
},
|
|
291
|
+
gridFrequency: round(r.u16(0x041C) * 0.01, 2),
|
|
292
|
+
pv,
|
|
293
|
+
pvPowerTotal: pv.reduce((s, p) => s + p.power, 0),
|
|
294
|
+
temperature: round(r.u16(0x0435) * 0.1, 1),
|
|
295
|
+
warning1: w1,
|
|
296
|
+
warning2: w2,
|
|
297
|
+
warningBits: { warning1: bits(w1, INVERTER_WARNING1), warning2: bits(w2, INVERTER_WARNING2) },
|
|
298
|
+
fault1: bits(r.u32(0x043A), INVERTER_FAULT1),
|
|
299
|
+
fault2: bits(r.u32(0x043C), INVERTER_FAULT2),
|
|
300
|
+
totalPvEnergy: round(r.u32(0x043E) * 0.1, 1), // kWh
|
|
301
|
+
workMode: { raw: workMode, text: WORK_MODE[workMode] || 'unknown' },
|
|
302
|
+
batteryVoltage: r.u16(0x0441), // 1 V/bit
|
|
303
|
+
batteryCurrent: round(r.u16(0x0442) * 0.1, 1),
|
|
304
|
+
batteryPower: r.s16(0x0443),
|
|
305
|
+
totalReactivePower: r.s32(0x0444),
|
|
306
|
+
totalApparentPower: r.s32(0x0446),
|
|
307
|
+
frequency: round(r.u16(0x0448) * 0.01, 2),
|
|
308
|
+
powerFactor: round(r.s16(0x044A) * 0.01, 2),
|
|
309
|
+
faultExtend1: bits(r.u32(0x044B), INVERTER_FAULT_EXT1),
|
|
310
|
+
faultExtend2: bits(r.u32(0x044D), INVERTER_FAULT_EXT2),
|
|
311
|
+
faultExtend3: r.u32(0x044F),
|
|
312
|
+
faultExtend4: r.u32(0x0451)
|
|
313
|
+
};
|
|
314
|
+
// PV total power register (0x0453, newer firmware); pvPowerTotal stays the sum of PV1..PV6
|
|
315
|
+
if (r.has(0x0453, 2)) d.pvPowerTotalRegister = r.u32(0x0453);
|
|
316
|
+
return d;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function decodeInverterInfo(values) {
|
|
320
|
+
const r = reader(values, 0x0640);
|
|
321
|
+
const d = {
|
|
322
|
+
masterSoftwareVersion: r.ascii(0x0640, 5),
|
|
323
|
+
slaveSoftwareVersion: r.ascii(0x0645, 5),
|
|
324
|
+
serialNumber: r.ascii(0x064A, 10)
|
|
325
|
+
};
|
|
326
|
+
if (r.has(0x0654, 5)) d.armSoftwareVersion = r.ascii(0x0654, 5);
|
|
327
|
+
return d;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function decodeSystemInfo(values) {
|
|
331
|
+
const r = reader(values, 0x0740);
|
|
332
|
+
const ym = r.u16(0x0740), dh = r.u16(0x0741), ms = r.u16(0x0742);
|
|
333
|
+
const vh = r.u16(0x074B), vm = r.u16(0x074C), vl = r.u16(0x074D);
|
|
334
|
+
const d = {
|
|
335
|
+
systemTime: {
|
|
336
|
+
year: 2000 + (ym >> 8), month: ym & 0xFF, day: dh >> 8, hour: dh & 0xFF, minute: ms >> 8, second: ms & 0xFF
|
|
337
|
+
},
|
|
338
|
+
emsSerialNumber: r.ascii(0x0743, 8),
|
|
339
|
+
emsVersion: { high: vh, middle: vm, low: vl, text: `${vh}.${vm}.${vl}` },
|
|
340
|
+
protocolVersion: r.u16(0x074E)
|
|
341
|
+
};
|
|
342
|
+
if (r.has(0x074F, 4)) {
|
|
343
|
+
const suffix = r.ascii(0x074F, 4);
|
|
344
|
+
d.emsVersion.suffix = suffix;
|
|
345
|
+
if (suffix) d.emsVersion.text += suffix.startsWith('-') || suffix.startsWith('.') ? suffix : `-${suffix}`;
|
|
346
|
+
}
|
|
347
|
+
if (r.has(0x0753, 6)) d.wifiSerialNumber = r.ascii(0x0753, 6);
|
|
348
|
+
return d;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function decodeSystemConfig(values) {
|
|
352
|
+
const r = reader(values, 0x0800);
|
|
353
|
+
const ip = (a) => { const h = r.u16(a), l = r.u16(a + 1); return [h >> 8, h & 0xFF, l >> 8, l & 0xFF].join('.'); };
|
|
354
|
+
const mode = r.u16(0x0805), ct = r.u16(0x0806);
|
|
355
|
+
return {
|
|
356
|
+
maxFeedIntoGridPercent: r.u16(0x0800),
|
|
357
|
+
pvCapacityStorage: r.u32(0x0801), // W
|
|
358
|
+
pvCapacityGridInverter: r.u32(0x0803), // W
|
|
359
|
+
systemMode: { raw: mode, text: SYSTEM_MODE[mode] || 'unknown' },
|
|
360
|
+
meterCtSelect: { raw: ct, text: METER_CT[ct] || 'unknown' },
|
|
361
|
+
batteryReady: r.u16(0x0807),
|
|
362
|
+
ipMethod: r.u16(0x0808) ? 'STATIC' : 'DHCP',
|
|
363
|
+
localIp: ip(0x0809),
|
|
364
|
+
subnetMask: ip(0x080B),
|
|
365
|
+
gateway: ip(0x080D),
|
|
366
|
+
modbusAddress: r.u16(0x080F),
|
|
367
|
+
modbusBaudRate: r.u16(0x0810),
|
|
368
|
+
threePhaseUnbalance: r.u16(0x0811)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function decodeTimePeriod(values, socScale) {
|
|
373
|
+
const r = reader(values, 0x084F);
|
|
374
|
+
const s = socScale || 0.1;
|
|
375
|
+
const flag = r.u16(0x084F);
|
|
376
|
+
return {
|
|
377
|
+
flag: { raw: flag, text: TIME_PERIOD_FLAG[flag] || 'unknown' },
|
|
378
|
+
upsReserveSoc: round(r.u16(0x0850) * s, 1),
|
|
379
|
+
chargeCutSoc: round(r.u16(0x0855) * s, 1),
|
|
380
|
+
discharge1: { start: hm(r.u16(0x0851), r.u16(0x085A)), stop: hm(r.u16(0x0852), r.u16(0x085B)) },
|
|
381
|
+
discharge2: { start: hm(r.u16(0x0853), r.u16(0x085C)), stop: hm(r.u16(0x0854), r.u16(0x085D)) },
|
|
382
|
+
charge1: { start: hm(r.u16(0x0856), r.u16(0x085E)), stop: hm(r.u16(0x0857), r.u16(0x085F)) },
|
|
383
|
+
charge2: { start: hm(r.u16(0x0858), r.u16(0x0860)), stop: hm(r.u16(0x0859), r.u16(0x0861)) },
|
|
384
|
+
raw: values.slice()
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function decodeDispatch(values) {
|
|
389
|
+
const r = reader(values, 0x0880);
|
|
390
|
+
const mode = r.u16(0x0885);
|
|
391
|
+
const d = {
|
|
392
|
+
active: r.u16(0x0880) === 1,
|
|
393
|
+
activePower: r.s32(0x0881) - 32000, // W, + discharge / - charge
|
|
394
|
+
reactivePower: r.s32(0x0883) - 32000, // var
|
|
395
|
+
mode: { raw: mode, text: DISPATCH_MODE[mode] || 'unknown' },
|
|
396
|
+
soc: round(r.u16(0x0886) * 0.4, 1), // %
|
|
397
|
+
time: r.u32(0x0887) // s
|
|
398
|
+
};
|
|
399
|
+
if (r.has(0x0889, 2)) {
|
|
400
|
+
const pvSwitch = r.u16(0x088A);
|
|
401
|
+
d.para7 = r.u16(0x0889); // aging/test energy flow (Note 29, Tab 2)
|
|
402
|
+
d.pvSwitch = { raw: pvSwitch, text: DISPATCH_PV_SWITCH[pvSwitch] || 'not set' };
|
|
403
|
+
}
|
|
404
|
+
return d;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function decodeSystemRun(values) {
|
|
408
|
+
const r = reader(values, 0x08D0);
|
|
409
|
+
return {
|
|
410
|
+
pvInverterEnergy: round(r.u32(0x08D0) * 0.1, 1), // kWh
|
|
411
|
+
systemTotalPvEnergy: round(r.u32(0x08D2) * 0.1, 1), // kWh
|
|
412
|
+
systemFault: bits(r.u32(0x08D4), SYSTEM_FAULT)
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function decodeBlock(name, values, opts) {
|
|
417
|
+
opts = opts || {};
|
|
418
|
+
switch (name) {
|
|
419
|
+
case 'grid': return decodeMeter(values, 0x0010);
|
|
420
|
+
case 'pvMeter': return decodeMeter(values, 0x0090);
|
|
421
|
+
case 'battery': return decodeBattery(values, opts.platform);
|
|
422
|
+
case 'batteryInfo': return decodeBatteryInfo(values);
|
|
423
|
+
case 'inverter': return decodeInverter(values);
|
|
424
|
+
case 'inverterInfo': return decodeInverterInfo(values);
|
|
425
|
+
case 'systemInfo': return decodeSystemInfo(values);
|
|
426
|
+
case 'systemConfig': return decodeSystemConfig(values);
|
|
427
|
+
case 'timePeriod': return decodeTimePeriod(values, opts.socScale);
|
|
428
|
+
case 'dispatch': return decodeDispatch(values);
|
|
429
|
+
case 'systemRun': return decodeSystemRun(values);
|
|
430
|
+
default: throw new Error(`Unknown block ${name}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
module.exports = { BLOCKS, WORK_MODE, BATTERY_TYPE, DISPATCH_MODE, SYSTEM_FAULT, reader, bits, decodeBlock, round };
|