node-red-contrib-huawei-solar
Version:
Node-RED nodes for reading data from Huawei SmartLogger 3000 and SUN2000 inverters via Modbus TCP
683 lines (580 loc) • 30 kB
JavaScript
/**
* Huawei SUN2000 Inverter Functions
*
* Simple implementation for reading data from SUN2000 inverters via Modbus TCP
* Based on modbus-slave.md documentation - using remapped register access
*/
const { HuaweiModbusClient, combineU32RegistersLE, combineI32RegistersLE, toSignedInt16 } = require('./modbus-utils');
/**
* SUN2000 Inverter Functions Class
* Supports both remapped register access and direct register access for comprehensive data
*/
class SUN2000Functions {
constructor(client) {
this.client = client;
}
// ============================================================================
// DEVICE IDENTIFICATION REGISTERS (Direct Access)
// ============================================================================
/**
* Read device model name
* Register: 30000 (String, 15 registers = 30 bytes)
*/
async readDeviceModel(deviceAddress) {
const result = await this.client.readString(30000, 15, 30, deviceAddress);
return result.success ? result.data : null;
}
/**
* Read device serial number
* Register: 30015 (String, 10 registers = 20 bytes)
*/
async readSerialNumber(deviceAddress) {
const result = await this.client.readString(30015, 10, 20, deviceAddress);
return result.success ? result.data : null;
}
/**
* Read firmware version
* Register: 31025 (String, 15 registers = 30 bytes)
*/
async readFirmwareVersion(deviceAddress) {
const result = await this.client.readString(31025, 15, 30, deviceAddress);
return result.success ? result.data : null;
}
/**
* Read number of PV strings
* Register: 30071 (U16)
*/
async readNumberOfStrings(deviceAddress) {
const result = await this.client.readU16(30071, deviceAddress);
return result.success ? result.data : null;
}
/**
* Read rated power
* Register: 30073-30074 (U32, gain=1000)
*/
async readRatedPower(deviceAddress) {
const result = await this.client.readU32(30073, deviceAddress);
return result.success ? result.data / 1000.0 : null;
}
// ============================================================================
// ENHANCED POWER & ENERGY DATA (Direct Access)
// ============================================================================
/**
* Read input power (direct register - more accurate than remapped)
* Register: 32064-32065 (I32, gain=1000)
*/
async readInputPowerDirect(deviceAddress) {
const result = await this.client.readI32(32064, deviceAddress);
return result.success ? result.data / 1000.0 : null;
}
/**
* Read active power (direct register)
* Register: 32080-32081 (I32, gain=1000)
*/
async readActivePowerDirect(deviceAddress) {
const result = await this.client.readI32(32080, deviceAddress);
return result.success ? result.data / 1000.0 : null;
}
/**
* Read reactive power (direct register)
* Register: 32082-32083 (I32, gain=1000)
*/
async readReactivePowerDirect(deviceAddress) {
const result = await this.client.readI32(32082, deviceAddress);
return result.success ? result.data / 1000.0 : null;
}
/**
* Read power factor (direct register)
* Register: 32084 (I16, gain=1000)
*/
async readPowerFactorDirect(deviceAddress) {
const result = await this.client.readI16(32084, deviceAddress);
return result.success ? result.data / 1000.0 : null;
}
/**
* Read peak power today
* Register: 32078-32079 (I32, gain=1000)
*/
async readPeakPowerToday(deviceAddress) {
const result = await this.client.readI32(32078, deviceAddress);
return result.success ? result.data / 1000.0 : null;
}
/**
* Read daily energy yield
* Register: 32114-32115 (U32, gain=100)
*/
async readDailyEnergyYield(deviceAddress) {
const result = await this.client.readU32(32114, deviceAddress);
return result.success ? result.data / 100.0 : null;
}
/**
* Read total energy yield
* Register: 32106-32107 (U32, gain=100)
*/
async readTotalEnergyYield(deviceAddress) {
const result = await this.client.readU32(32106, deviceAddress);
return result.success ? result.data / 100.0 : null;
}
/**
* Read inverter efficiency
* Register: 32086 (U16, gain=100)
*/
async readEfficiency(deviceAddress) {
const result = await this.client.readU16(32086, deviceAddress);
return result.success ? result.data / 100.0 : null;
}
// ============================================================================
// GRID VOLTAGE & CURRENT MEASUREMENTS (Direct Access)
// ============================================================================
/**
* Read grid line voltages (UAB, UBC, UCA)
* Registers: 32066-32068 (U16, gain=10)
*/
async readGridLineVoltages(deviceAddress) {
const result = await this.client.readHoldingRegisters(32066, 3, deviceAddress);
if (!result.success || !result.data || result.data.length < 3) {
return {};
}
return {
UAB: result.data[0] / 10.0,
UBC: result.data[1] / 10.0,
UCA: result.data[2] / 10.0
};
}
/**
* Read grid phase voltages (A, B, C)
* Registers: 32069-32071 (U16, gain=10)
*/
async readGridPhaseVoltages(deviceAddress) {
const result = await this.client.readHoldingRegisters(32069, 3, deviceAddress);
if (!result.success || !result.data || result.data.length < 3) {
return {};
}
return {
A: result.data[0] / 10.0,
B: result.data[1] / 10.0,
C: result.data[2] / 10.0
};
}
/**
* Read grid phase currents (A, B, C)
* Registers: 32072-32077 (I32, gain=1000)
*/
async readGridPhaseCurrents(deviceAddress) {
const result = await this.client.readHoldingRegisters(32072, 6, deviceAddress);
if (!result.success || !result.data || result.data.length < 6) {
return {};
}
return {
A: combineI32RegistersLE(result.data[0], result.data[1]) / 1000.0,
B: combineI32RegistersLE(result.data[2], result.data[3]) / 1000.0,
C: combineI32RegistersLE(result.data[4], result.data[5]) / 1000.0
};
}
/**
* Read grid frequency
* Register: 32085 (U16, gain=100)
*/
async readGridFrequency(deviceAddress) {
const result = await this.client.readU16(32085, deviceAddress);
return result.success ? result.data / 100.0 : null;
}
// ============================================================================
// PV STRING DATA (Direct Access)
// ============================================================================
/**
* Read PV string data for specified number of strings
* String n: Voltage = 32014 + (2×n), Current = 32015 + (2×n)
* Voltage gain=10, Current gain=100
*/
async readPVStrings(deviceAddress, stringCount, useIEC) {
// If string count not provided, try to read it first
if (!stringCount) {
const detectedStringCount = await this.readNumberOfStrings(deviceAddress);
if (detectedStringCount !== null && detectedStringCount > 0) {
stringCount = detectedStringCount;
} else {
stringCount = 4; // Default to 4 strings if can't determine
}
}
// Limit to 24 strings maximum as per documentation
stringCount = Math.min(stringCount, 24);
const strings = [];
// Read strings in batches to minimize requests
const batchSize = 10; // Read 10 strings at a time (20 registers)
for (let i = 0; i < stringCount; i += batchSize) {
const remainingStrings = Math.min(batchSize, stringCount - i);
const startRegister = 32016 + (i * 2); // PV1 starts at 32016
const registerCount = remainingStrings * 2;
const result = await this.client.readHoldingRegisters(startRegister, registerCount, deviceAddress);
if (result.success && result.data) {
for (let j = 0; j < remainingStrings; j++) {
const voltageIndex = j * 2;
const currentIndex = j * 2 + 1;
if (voltageIndex < result.data.length && currentIndex < result.data.length) {
const voltage = toSignedInt16(result.data[voltageIndex]) / 10.0;
const current = toSignedInt16(result.data[currentIndex]) / 100.0;
const power = voltage * current; // Calculate power in watts
const stringData = {
[useIEC ? 'n' : 'stringNumber']: i + j + 1,
[useIEC ? 'U' : 'voltage']: voltage,
[useIEC ? 'I' : 'current']: current,
[useIEC ? 'P' : 'power']: power
};
strings.push(stringData);
}
}
}
}
return strings;
}
// ============================================================================
// STATUS & TEMPERATURE (Direct Access)
// ============================================================================
/**
* Read device status
* Register: 32089 (ENUM16)
*/
async readDeviceStatus(deviceAddress) {
const result = await this.client.readU16(32089, deviceAddress);
if (!result.success || result.data === undefined) {
return {};
}
const code = result.data;
let text = 'Unknown';
switch (code) {
case 0x0000: text = 'Standby: initializing'; break;
case 0x0200: text = 'On-grid'; break;
case 0x0201: text = 'Grid connection: power limited'; break;
case 0x0300: text = 'Shutdown: fault'; break;
case 0x0500: text = 'Spot-check ready'; break;
default: text = `Unknown status (0x${code.toString(16).toUpperCase()})`;
}
return { code, text };
}
/**
* Read running status bitfield
* Register: 32002 (Bitfield16)
*/
async readRunningStatus(deviceAddress) {
const result = await this.client.readU16(32002, deviceAddress);
return result.success ? result.data : null;
}
/**
* Read internal temperature
* Register: 32087 (I16, gain=10)
*/
async readInternalTemperature(deviceAddress) {
const result = await this.client.readI16(32087, deviceAddress);
return result.success ? result.data / 10.0 : null;
}
/**
* Read insulation resistance (direct register)
* Register: 32088 (U16, gain=1000)
*/
async readInsulationResistanceDirect(deviceAddress) {
const result = await this.client.readU16(32088, deviceAddress);
return result.success ? result.data / 1000.0 : null;
}
// ============================================================================
// ALARM SYSTEM (Direct Access)
// ============================================================================
/**
* Read all alarm registers and decode them
* Registers: 32008 (Alarm1), 32009 (Alarm2), 32010 (Alarm3)
*/
async readAlarms(deviceAddress) {
const result = await this.client.readHoldingRegisters(32008, 3, deviceAddress);
if (!result.success || !result.data || result.data.length < 3) {
return {};
}
const alarm1 = result.data[0];
const alarm2 = result.data[1];
const alarm3 = result.data[2];
const alarmTexts = [];
// Decode Alarm 1 bits
if (alarm1 & (1 << 0)) alarmTexts.push('High String Input Voltage (Major)');
if (alarm1 & (1 << 1)) alarmTexts.push('DC Arc Fault (Major)');
if (alarm1 & (1 << 2)) alarmTexts.push('String Reverse Connection (Major)');
if (alarm1 & (1 << 3)) alarmTexts.push('String Current Backfeed (Warning)');
if (alarm1 & (1 << 4)) alarmTexts.push('Abnormal String Power (Warning)');
if (alarm1 & (1 << 5)) alarmTexts.push('AFCI Self-Check Fail (Major)');
if (alarm1 & (1 << 6)) alarmTexts.push('Phase Wire Short-Circuited to PE (Major)');
if (alarm1 & (1 << 7)) alarmTexts.push('Grid Loss (Major)');
if (alarm1 & (1 << 8)) alarmTexts.push('Grid Undervoltage (Major)');
if (alarm1 & (1 << 9)) alarmTexts.push('Grid Overvoltage (Major)');
if (alarm1 & (1 << 10)) alarmTexts.push('Grid Voltage Imbalance (Major)');
if (alarm1 & (1 << 11)) alarmTexts.push('Grid Overfrequency (Major)');
if (alarm1 & (1 << 12)) alarmTexts.push('Grid Underfrequency (Major)');
if (alarm1 & (1 << 13)) alarmTexts.push('Unstable Grid Frequency (Major)');
if (alarm1 & (1 << 14)) alarmTexts.push('Output Overcurrent (Major)');
if (alarm1 & (1 << 15)) alarmTexts.push('Output DC Component Overhigh (Major)');
// Decode Alarm 2 bits
if (alarm2 & (1 << 0)) alarmTexts.push('Abnormal Residual Current (Major)');
if (alarm2 & (1 << 1)) alarmTexts.push('Abnormal Grounding (Major)');
if (alarm2 & (1 << 2)) alarmTexts.push('Low Insulation Resistance (Major)');
if (alarm2 & (1 << 3)) alarmTexts.push('Overtemperature (Minor)');
if (alarm2 & (1 << 4)) alarmTexts.push('Device Fault (Major)');
if (alarm2 & (1 << 5)) alarmTexts.push('Upgrade Failed or Version Mismatch (Minor)');
if (alarm2 & (1 << 6)) alarmTexts.push('License Expired (Warning)');
if (alarm2 & (1 << 7)) alarmTexts.push('Faulty Monitoring Unit (Minor)');
if (alarm2 & (1 << 8)) alarmTexts.push('Faulty Power Collector (Major)');
if (alarm2 & (1 << 9)) alarmTexts.push('Battery Abnormal (Minor)');
if (alarm2 & (1 << 10)) alarmTexts.push('Active Islanding (Major)');
if (alarm2 & (1 << 11)) alarmTexts.push('Passive Islanding (Major)');
if (alarm2 & (1 << 12)) alarmTexts.push('Transient AC Overvoltage (Major)');
if (alarm2 & (1 << 13)) alarmTexts.push('Peripheral Port Short Circuit (Warning)');
if (alarm2 & (1 << 14)) alarmTexts.push('Churn Output Overload (Major)');
if (alarm2 & (1 << 15)) alarmTexts.push('Abnormal PV Module Configuration (Major)');
// Decode Alarm 3 bits
if (alarm3 & (1 << 0)) alarmTexts.push('Optimizer Fault (Warning)');
if (alarm3 & (1 << 1)) alarmTexts.push('Built-in PID Operation Abnormal (Minor)');
if (alarm3 & (1 << 2)) alarmTexts.push('High Input String Voltage to Ground (Major)');
if (alarm3 & (1 << 3)) alarmTexts.push('External Fan Abnormal (Major)');
if (alarm3 & (1 << 4)) alarmTexts.push('Battery Reverse Connection (Major)');
if (alarm3 & (1 << 5)) alarmTexts.push('On-grid/Off-grid Controller Abnormal (Major)');
if (alarm3 & (1 << 6)) alarmTexts.push('PV String Loss (Warning)');
if (alarm3 & (1 << 7)) alarmTexts.push('Internal Fan Abnormal (Major)');
if (alarm3 & (1 << 8)) alarmTexts.push('DC Protection Unit Abnormal (Major)');
if (alarm3 & (1 << 9)) alarmTexts.push('EL Unit Abnormal (Minor)');
if (alarm3 & (1 << 10)) alarmTexts.push('Active Adjustment Instruction Abnormal (Major)');
if (alarm3 & (1 << 11)) alarmTexts.push('Reactive Adjustment Instruction Abnormal (Major)');
if (alarm3 & (1 << 12)) alarmTexts.push('CT Wiring Abnormal (Major)');
if (alarm3 & (1 << 13)) alarmTexts.push('DC Arc Fault (ADMC - Manual Clear Required) (Major)');
if (alarm3 & (1 << 14)) alarmTexts.push('DC Switch Abnormal (Minor)');
if (alarm3 & (1 << 15)) alarmTexts.push('Low Battery Discharge Capacity (Warning)');
return { alarm1, alarm2, alarm3, alarmTexts };
}
/**
* Read fault code
* Register: 32090 (U16)
*/
async readFaultCode(deviceAddress) {
const result = await this.client.readU16(32090, deviceAddress);
return result.success ? result.data : null;
}
// ============================================================================
// LEGACY REMAPPED REGISTER ACCESS (for backward compatibility)
// ============================================================================
/**
* Calculate remapped register address
* Formula: 51000 + (25 × (Device Address - 1)) + Offset
*/
getRemappedRegister(deviceAddress, offset) {
return 51000 + (25 * (deviceAddress - 1)) + offset;
}
/**
* Read comprehensive inverter data using direct register access
* Combines basic remapped data with enhanced direct register data
*/
async readInverterData(deviceAddress, deviceName, dataCategories, alwaysIncludeAlarmTexts, useIEC) {
const result = {
unitId: deviceAddress
};
if (deviceName) result.deviceName = deviceName;
try {
// Always read basic power data via remapped registers (for backward compatibility)
await this.readRemappedData(result, deviceAddress, useIEC);
// Read enhanced data based on categories (default to all if not specified)
const categories = dataCategories || ['device', 'power', 'voltages', 'currents', 'strings', 'status', 'alarms'];
// Device identification
if (categories.includes('device')) {
const [model, serialNumber, firmwareVersion, numberOfStrings, ratedPower] = await Promise.all([
this.readDeviceModel(deviceAddress),
this.readSerialNumber(deviceAddress),
this.readFirmwareVersion(deviceAddress),
this.readNumberOfStrings(deviceAddress),
this.readRatedPower(deviceAddress)
]);
if (model) result.model = model;
if (serialNumber) result.serialNumber = serialNumber;
if (firmwareVersion) result.firmwareVersion = firmwareVersion;
if (numberOfStrings !== null) result.numberOfStrings = numberOfStrings;
if (ratedPower !== null) result.ratedPower = ratedPower;
}
// Enhanced power and energy data
if (categories.includes('power')) {
const [peakPowerToday, dailyEnergyYield, totalEnergyYield, efficiency] = await Promise.all([
this.readPeakPowerToday(deviceAddress),
this.readDailyEnergyYield(deviceAddress),
this.readTotalEnergyYield(deviceAddress),
this.readEfficiency(deviceAddress)
]);
if (peakPowerToday !== null) result[useIEC ? 'Pmax' : 'peakPowerToday'] = peakPowerToday;
if (dailyEnergyYield !== null) result[useIEC ? 'EPId' : 'dailyEnergyYield'] = dailyEnergyYield;
if (totalEnergyYield !== null) result[useIEC ? 'EPI' : 'totalEnergyYield'] = totalEnergyYield;
if (efficiency !== null) result[useIEC ? 'eff' : 'efficiency'] = efficiency;
}
// Grid voltages
if (categories.includes('voltages')) {
const [lineVoltages, phaseVoltages] = await Promise.all([
this.readGridLineVoltages(deviceAddress),
this.readGridPhaseVoltages(deviceAddress)
]);
if (lineVoltages.UAB !== undefined) result[useIEC ? 'Uab' : 'gridVoltageUAB'] = lineVoltages.UAB;
if (lineVoltages.UBC !== undefined) result[useIEC ? 'Ubc' : 'gridVoltageUBC'] = lineVoltages.UBC;
if (lineVoltages.UCA !== undefined) result[useIEC ? 'Uca' : 'gridVoltageUCA'] = lineVoltages.UCA;
if (phaseVoltages.A !== undefined) result[useIEC ? 'Ua' : 'phaseAVoltage'] = phaseVoltages.A;
if (phaseVoltages.B !== undefined) result[useIEC ? 'Ub' : 'phaseBVoltage'] = phaseVoltages.B;
if (phaseVoltages.C !== undefined) result[useIEC ? 'Uc' : 'phaseCVoltage'] = phaseVoltages.C;
}
// Grid currents and frequency
if (categories.includes('currents')) {
const [phaseCurrents, gridFrequency] = await Promise.all([
this.readGridPhaseCurrents(deviceAddress),
this.readGridFrequency(deviceAddress)
]);
if (phaseCurrents.A !== undefined) result[useIEC ? 'Ia' : 'phaseACurrent'] = phaseCurrents.A;
if (phaseCurrents.B !== undefined) result[useIEC ? 'Ib' : 'phaseBCurrent'] = phaseCurrents.B;
if (phaseCurrents.C !== undefined) result[useIEC ? 'Ic' : 'phaseCCurrent'] = phaseCurrents.C;
if (gridFrequency !== null) result[useIEC ? 'Fr' : 'gridFrequency'] = gridFrequency;
}
// PV string data
if (categories.includes('strings')) {
const pvStrings = await this.readPVStrings(deviceAddress, result.numberOfStrings, useIEC);
if (pvStrings.length > 0) {
result[useIEC ? 'pv' : 'pvStrings'] = pvStrings;
}
}
// Status and temperature
if (categories.includes('status')) {
const [deviceStatus, runningStatus, internalTemp, insulationResistance] = await Promise.all([
this.readDeviceStatus(deviceAddress),
this.readRunningStatus(deviceAddress),
this.readInternalTemperature(deviceAddress),
this.readInsulationResistanceDirect(deviceAddress)
]);
if (deviceStatus.code !== undefined) result.deviceStatus = deviceStatus.code;
if (deviceStatus.text) result.deviceStatusText = deviceStatus.text;
if (runningStatus !== null) result.runningStatus = runningStatus;
if (internalTemp !== null) result[useIEC ? 'TempInt' : 'internalTemperature'] = internalTemp;
if (insulationResistance !== null) result.insulationResistance = insulationResistance;
}
// Alarms and faults
if (categories.includes('alarms')) {
const [alarms, faultCode] = await Promise.all([
this.readAlarms(deviceAddress),
this.readFaultCode(deviceAddress)
]);
if (alarms.alarm1 !== undefined) result.alarm1 = alarms.alarm1;
if (alarms.alarm2 !== undefined) result.alarm2 = alarms.alarm2;
if (alarms.alarm3 !== undefined) result.alarm3 = alarms.alarm3;
if (alwaysIncludeAlarmTexts || (alarms.alarmTexts && alarms.alarmTexts.length > 0)) {
result.alarmTexts = alarms.alarmTexts || [];
}
if (faultCode !== null) result.faultCode = faultCode;
}
} catch (error) {
result.error = error instanceof Error ? error.message : 'Unknown error reading inverter data';
}
return result;
}
/**
* Read basic remapped register data (for backward compatibility)
*/
async readRemappedData(result, deviceAddress, useIEC) {
try {
// Read power data (offsets 0-8)
const baseRegister = this.getRemappedRegister(deviceAddress, 0);
// Try to read multiple registers at once for efficiency
// Registers 0-8: Active Power (I32), Reactive Power (I32), DC Current (I16), Input Power (U32), Insulation (U16), Power Factor (I16)
const powerResult = await this.client.readHoldingRegisters(baseRegister, 9, 0);
if (powerResult.success && powerResult.data && powerResult.data.length >= 9) {
const registers = powerResult.data;
// Active Power: Offset 0-1 (I32, gain=1000)
const activePowerRaw = combineI32RegistersLE(registers[0], registers[1]);
result[useIEC ? 'P' : 'activePower'] = activePowerRaw / 1000.0;
// Reactive Power: Offset 2-3 (I32, gain=1000)
const reactivePowerRaw = combineI32RegistersLE(registers[2], registers[3]);
result[useIEC ? 'Q' : 'reactivePower'] = reactivePowerRaw / 1000.0;
// DC Current: Offset 4 (I16, gain=100)
result[useIEC ? 'dcI' : 'dcCurrent'] = toSignedInt16(registers[4]) / 100.0;
// Input Power: Offset 5-6 (U32, gain=1000)
const inputPowerRaw = combineU32RegistersLE(registers[5], registers[6]);
result[useIEC ? 'dcP' : 'inputPower'] = inputPowerRaw / 1000.0;
// Power Factor: Offset 8 (I16, gain=1000)
result[useIEC ? 'PF' : 'powerFactor'] = toSignedInt16(registers[8]) / 1000.0;
}
// Read status and temperature (offsets 9, 11)
const statusRegister = this.getRemappedRegister(deviceAddress, 9);
const statusResult = await this.client.readU16(statusRegister, 0);
if (statusResult.success) {
result.status = statusResult.data;
}
const tempRegister = this.getRemappedRegister(deviceAddress, 11);
const tempResult = await this.client.readI16(tempRegister, 0);
if (tempResult.success) {
result[useIEC ? 'TempCab' : 'cabinetTemperature'] = tempResult.data / 10.0;
}
// Read fault data (offsets 12-17)
const faultRegister = this.getRemappedRegister(deviceAddress, 12);
const faultResult = await this.client.readHoldingRegisters(faultRegister, 6, 0);
if (faultResult.success && faultResult.data && faultResult.data.length >= 6) {
const faultRegisters = faultResult.data;
// Major Fault: Offset 12-13 (U32)
result.majorFault = combineU32RegistersLE(faultRegisters[0], faultRegisters[1]);
// Minor Fault: Offset 14-15 (U32)
result.minorFault = combineU32RegistersLE(faultRegisters[2], faultRegisters[3]);
// Warning: Offset 16-17 (U32)
result.warning = combineU32RegistersLE(faultRegisters[4], faultRegisters[5]);
}
} catch (error) {
// Don't throw here, just let the main method handle errors
}
}
/**
* Read data from multiple inverters
* Processes them sequentially to avoid overwhelming the device
*/
async readMultipleInverters(devices, dataCategories, alwaysIncludeAlarmTexts, useIEC) {
const results = [];
// Process inverters one by one to be safe
for (const device of devices) {
try {
const inverterData = await this.readInverterData(device.deviceAddress, device.deviceName, dataCategories, alwaysIncludeAlarmTexts, useIEC);
results.push(inverterData);
// Small delay between inverters to be gentle on the network
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
// Add error entry for failed inverter
results.push({
unitId: device.unitId,
deviceName: device.deviceName,
error: error instanceof Error ? error.message : 'Failed to read inverter'
});
}
}
return results;
}
/**
* Parse address list string into array of numbers
* Examples: "12,13,14,15" -> [12,13,14,15]
* "12-15" -> [12,13,14,15]
* "1-2,6,8" -> [1,2,6,8]
*/
static parseAddressList(addressString) {
const addresses = [];
const parts = addressString.split(',').map(s => s.trim());
for (const part of parts) {
if (part.includes('-')) {
// Handle range like "12-15"
const [start, end] = part.split('-').map(Number);
if (!isNaN(start) && !isNaN(end) && start <= end) {
for (let i = start; i <= end; i++) {
if (i >= 1 && i <= 247) {
addresses.push(i);
}
}
}
} else {
// Handle individual address like "12"
const addr = Number(part);
if (!isNaN(addr) && addr >= 1 && addr <= 247) {
addresses.push(addr);
}
}
}
return [...new Set(addresses)].sort((a, b) => a - b);
}
}
module.exports = { SUN2000Functions };