UNPKG

node-red-contrib-huawei-solar

Version:

Node-RED nodes for reading data from Huawei SmartLogger 3000 and SUN2000 inverters via Modbus TCP

564 lines (496 loc) 20.8 kB
/** * Huawei SmartLogger 3000 Functions * * Complete implementation of SmartLogger register reading functions * Converted from TypeScript with critical fixes applied: * - SmartLogger Unit ID: 3 (not 0 as documented) * - Little-endian register combination * - Proper gain factor applications */ const { HuaweiModbusClient, parsePlantStatus, parseConnectionStatus } = require('./modbus-utils'); /** * SmartLogger Functions Class * Handles all SmartLogger 3000 register operations */ class SmartLoggerFunctions { constructor(client, unitId = 3) { this.client = client; this.unitId = unitId; } // ============================================================================ // SYSTEM CONTROL REGISTERS (40000-40299) // ============================================================================ /** * Read system date/time as UTC timestamp * Registers: 40000-40001 (U32, epoch seconds) */ async readSystemDateTime() { const result = await this.client.readU32(40000, this.unitId); return result.success ? result.data : null; } /** * Read location city identifier * Registers: 40002-40003 (U32) */ async readLocationCity() { const result = await this.client.readU32(40002, this.unitId); return result.success ? result.data : null; } /** * Read daylight saving time enable status * Register: 40004 (U16) */ async readDstEnable() { const result = await this.client.readU16(40004, this.unitId); return result.success ? Boolean(result.data) : null; } /** * Read all system control data */ async readSystemData(useIEC) { const [datetime, locationCity, dstEnable] = await Promise.all([ this.readSystemDateTime(), this.readLocationCity(), this.readDstEnable() ]); const result = {}; if (datetime !== null) result.datetime = datetime; if (locationCity !== null) result.locationCity = locationCity; if (dstEnable !== null) result.dstEnable = dstEnable; return result; } // ============================================================================ // POWER MONITORING REGISTERS (40500-40599) // ============================================================================ /** * Read total DC current from all inverters * Register: 40500 (I16, gain=10) */ async readDcCurrentTotal() { const result = await this.client.readI16(40500, this.unitId); return result.success ? result.data / 10.0 : null; } /** * Read total input power from all inverters * Registers: 40521-40522 (U32, gain=1000) */ async readInputPowerTotal() { const result = await this.client.readU32(40521, this.unitId); return result.success ? result.data / 1000.0 : null; } /** * Read total CO2 reduction * Registers: 40523-40524 (U32, gain=10) */ async readCO2Reduction() { const result = await this.client.readU32(40523, this.unitId); return result.success ? result.data / 10.0 : null; } /** * Read total active power output (AC) * Registers: 40525-40526 (I32, gain=1000) */ async readActivePowerTotal() { const result = await this.client.readI32(40525, this.unitId); return result.success ? result.data / 1000.0 : null; } /** * Read total reactive power * Registers: 40544-40545 (I32, gain=1000) */ async readReactivePowerTotal() { const result = await this.client.readI32(40544, this.unitId); return result.success ? result.data / 1000.0 : null; } /** * Read system power factor * Register: 40532 (I16, gain=1000) */ async readPowerFactor() { const result = await this.client.readI16(40532, this.unitId); return result.success ? result.data / 1000.0 : null; } /** * Read plant operational status (Qinghai region) * Register: 40543 (U16) */ async readPlantStatus() { const result = await this.client.readU16(40543, this.unitId); return result.success ? parsePlantStatus(result.data) : null; } /** * Read plant operational status (Xinjiang region) * Register: 40566 (U16) */ async readPlantStatusXinjiang() { const result = await this.client.readU16(40566, this.unitId); if (!result.success || result.data === undefined) return null; // Parse Xinjiang-specific status codes switch (result.data) { case 0: return 'Idle'; case 1: return 'On-grid'; case 2: return 'On-grid with self-derating'; case 3: return 'On-grid with power limit'; case 4: return 'Planned outage'; case 5: return 'Power limit outage'; case 6: return 'Fault outage'; case 7: return 'Communication interrupt'; default: return `Unknown (${result.data})`; } } /** * Read total energy yield (lifetime) * Registers: 40560-40561 (U32, gain=10) */ async readTotalEnergy() { const result = await this.client.readU32(40560, this.unitId); return result.success ? result.data / 10.0 : null; } /** * Read daily energy yield * Registers: 40562-40563 (U32, gain=10) */ async readDailyEnergy() { const result = await this.client.readU32(40562, this.unitId); return result.success ? result.data / 10.0 : null; } /** * Read all power monitoring data */ async readPowerData(useIEC) { const [ dcCurrentTotal, inputPowerTotal, co2Reduction, activePowerTotal, reactivePowerTotal, powerFactor, plantStatus, plantStatusXinjiang, totalEnergy, dailyEnergy ] = await Promise.all([ this.readDcCurrentTotal(), this.readInputPowerTotal(), this.readCO2Reduction(), this.readActivePowerTotal(), this.readReactivePowerTotal(), this.readPowerFactor(), this.readPlantStatus(), this.readPlantStatusXinjiang(), this.readTotalEnergy(), this.readDailyEnergy() ]); const result = {}; if (dcCurrentTotal !== null) result[useIEC ? 'dcI' : 'dcCurrentTotal'] = dcCurrentTotal; if (inputPowerTotal !== null) result[useIEC ? 'dcP' : 'inputPowerTotal'] = inputPowerTotal; if (co2Reduction !== null) result[useIEC ? 'CO2' : 'co2Reduction'] = co2Reduction; if (activePowerTotal !== null) result[useIEC ? 'P' : 'activePowerTotal'] = activePowerTotal; if (reactivePowerTotal !== null) result[useIEC ? 'Q' : 'reactivePowerTotal'] = reactivePowerTotal; if (powerFactor !== null) result[useIEC ? 'PF' : 'powerFactor'] = powerFactor; if (plantStatus !== null) result[useIEC ? 'status' : 'plantStatus'] = plantStatus; if (plantStatusXinjiang !== null) result[useIEC ? 'statusXJ' : 'plantStatusXinjiang'] = plantStatusXinjiang; if (totalEnergy !== null) result[useIEC ? 'EPI' : 'totalEnergy'] = totalEnergy; if (dailyEnergy !== null) result[useIEC ? 'EPId' : 'dailyEnergy'] = dailyEnergy; return result; } // ============================================================================ // ENVIRONMENTAL MONITORING REGISTERS (40031-40037) // ============================================================================ /** * Read current wind speed * Register: 40031 (I16, gain=10) */ async readWindSpeed() { const result = await this.client.readI16(40031, this.unitId); return result.success ? result.data / 10.0 : null; } /** * Read wind direction * Register: 40032 (I16, 0-359 degrees) */ async readWindDirection() { const result = await this.client.readI16(40032, this.unitId); return result.success ? result.data : null; } /** * Read PV module temperature * Register: 40033 (I16, gain=10) */ async readPvTemperature() { const result = await this.client.readI16(40033, this.unitId); return result.success ? result.data / 10.0 : null; } /** * Read ambient air temperature * Register: 40034 (I16, gain=10) */ async readAmbientTemperature() { const result = await this.client.readI16(40034, this.unitId); return result.success ? result.data / 10.0 : null; } /** * Read solar irradiance * Register: 40035 (I16, gain=10) */ async readIrradiance() { const result = await this.client.readI16(40035, this.unitId); return result.success ? result.data / 10.0 : null; } /** * Read daily irradiation accumulation * Registers: 40036-40037 (U32, gain=1000) */ async readDailyIrradiation() { const result = await this.client.readU32(40036, this.unitId); return result.success ? result.data / 1000.0 : null; } /** * Read all environmental monitoring data */ async readEnvironmentalData(useIEC) { const [ windSpeed, windDirection, pvTemperature, ambientTemperature, irradiance, dailyIrradiation ] = await Promise.all([ this.readWindSpeed(), this.readWindDirection(), this.readPvTemperature(), this.readAmbientTemperature(), this.readIrradiance(), this.readDailyIrradiation() ]); const result = {}; if (windSpeed !== null) result[useIEC ? 'WindSpd' : 'windSpeed'] = windSpeed; if (windDirection !== null) result[useIEC ? 'WindDir' : 'windDirection'] = windDirection; if (pvTemperature !== null) result[useIEC ? 'TempPV' : 'pvTemperature'] = pvTemperature; if (ambientTemperature !== null) result[useIEC ? 'TempAmb' : 'ambientTemperature'] = ambientTemperature; if (irradiance !== null) result[useIEC ? 'Irr' : 'irradiance'] = irradiance; if (dailyIrradiation !== null) result[useIEC ? 'IrrDly' : 'dailyIrradiation'] = dailyIrradiation; return result; } // ============================================================================ // ALARM REGISTERS (50000+) // ============================================================================ /** * Read alarm information register 1 * Register: 50000 (U16, bit field) */ async readAlarmInfo1() { const result = await this.client.readU16(50000, this.unitId); return result.success ? result.data : null; } /** * Read alarm information register 2 * Register: 50001 (U16, bit field) */ async readAlarmInfo2() { const result = await this.client.readU16(50001, this.unitId); return result.success ? result.data : null; } /** * Read certificate-related alarms * Register: 50002 (U16) */ async readCertificateAlarms() { const result = await this.client.readU16(50002, this.unitId); return result.success ? result.data : null; } /** * Read all alarm data */ async readAlarmData(useIEC) { const [alarmInfo1, alarmInfo2, certificateAlarms] = await Promise.all([ this.readAlarmInfo1(), this.readAlarmInfo2(), this.readCertificateAlarms() ]); const result = {}; if (alarmInfo1 !== null) result.alarmInfo1 = alarmInfo1; if (alarmInfo2 !== null) result.alarmInfo2 = alarmInfo2; if (certificateAlarms !== null) result.certificateAlarms = certificateAlarms; return result; } // ============================================================================ // DEVICE DISCOVERY AND INFORMATION // ============================================================================ /** * Read device name string * Registers: 65524-65533 (10x U16, 20-byte string) */ async readDeviceName(unitId) { const targetUnitId = unitId || this.unitId; const result = await this.client.readString(65524, 10, 20, targetUnitId); return result.success ? result.data : null; } /** * Read device connection status * Register: 65534 (U16) */ async readConnectionStatus(unitId) { const targetUnitId = unitId || this.unitId; const result = await this.client.readU16(65534, targetUnitId); return result.success ? parseConnectionStatus(result.data) : null; } /** * Read device ModBus address * Register: 65523 (U16) */ async readDeviceAddress(unitId) { const targetUnitId = unitId || this.unitId; const result = await this.client.readU16(65523, targetUnitId); return result.success ? result.data : null; } /** * Read physical port number * Register: 65522 (U16) */ async readPortNumber(unitId) { const targetUnitId = unitId || this.unitId; const result = await this.client.readU16(65522, targetUnitId); return result.success ? result.data : null; } /** * Discover all connected devices by scanning unit IDs (sequential - slower but safer) */ async discoverAllDevices(unitRange = Array.from({length: 247}, (_, i) => i + 1)) { const discovered = []; for (const unitId of unitRange) { try { // Try to read device name first (most reliable public register) const deviceName = await this.readDeviceName(unitId); if (deviceName) { // Get additional device info const [connectionStatus, portNumber, deviceAddress] = await Promise.all([ this.readConnectionStatus(unitId), this.readPortNumber(unitId), this.readDeviceAddress(unitId) ]); const device = { unitId }; if (deviceName) device.deviceName = deviceName; if (connectionStatus) device.connectionStatus = connectionStatus; if (portNumber !== null) device.portNumber = portNumber; if (deviceAddress !== null) device.deviceAddress = deviceAddress; discovered.push(device); } } catch (error) { // Skip unresponsive units continue; } } return discovered; } /** * Discover all connected devices using parallel scanning (faster but more network intensive) * Creates separate client instances to avoid unit ID conflicts */ async discoverAllDevicesParallel(unitRange = Array.from({length: 247}, (_, i) => i + 1), concurrency = 10) { const discovered = []; // Process units in batches to limit concurrent connections for (let i = 0; i < unitRange.length; i += concurrency) { const batch = unitRange.slice(i, i + concurrency); const batchPromises = batch.map(async (unitId) => { // Create a dedicated client for this unit ID to avoid conflicts const originalConfig = this.client.getConfig(); const { HuaweiModbusClient } = require('./modbus-utils'); const dedicatedClient = new HuaweiModbusClient({ ...originalConfig, unitId: unitId // Set the unit ID for this specific client }); try { // Connect the dedicated client const connected = await dedicatedClient.connect(); if (!connected) { return null; } // Try to read device name first (most reliable public register) const deviceNameResult = await dedicatedClient.readString(65524, 10, 20, unitId); if (deviceNameResult.success && deviceNameResult.data) { const deviceName = deviceNameResult.data; // Get additional device info in parallel using the dedicated client const [connectionStatusResult, portNumberResult, deviceAddressResult] = await Promise.all([ dedicatedClient.readU16(65534, unitId), dedicatedClient.readU16(65522, unitId), dedicatedClient.readU16(65523, unitId) ]); // Parse connection status let connectionStatus; if (connectionStatusResult.success && connectionStatusResult.data !== undefined) { const statusValue = connectionStatusResult.data; connectionStatus = statusValue === 0xB001 ? 'Online' : statusValue === 0xB000 ? 'Offline' : `Unknown (0x${statusValue.toString(16).toUpperCase()})`; } const result = { unitId, deviceName }; if (connectionStatus) result.connectionStatus = connectionStatus; if (portNumberResult.success && portNumberResult.data !== undefined) result.portNumber = portNumberResult.data; if (deviceAddressResult.success && deviceAddressResult.data !== undefined) result.deviceAddress = deviceAddressResult.data; return result; } } catch (error) { // Skip unresponsive units } finally { // Always disconnect the dedicated client await dedicatedClient.disconnect(); } return null; }); const batchResults = await Promise.all(batchPromises); // Add successful discoveries to results for (const result of batchResults) { if (result) { discovered.push(result); } } } return discovered; } /** * Discover SUN2000 inverters specifically (typically units 12-15) - sequential */ async discoverInverters(unitRange = [12, 13, 14, 15]) { const inverters = []; for (const unitId of unitRange) { try { const deviceName = await this.readDeviceName(unitId); if (deviceName && deviceName.includes('SUN2000')) { const [connectionStatus, portNumber, deviceAddress] = await Promise.all([ this.readConnectionStatus(unitId), this.readPortNumber(unitId), this.readDeviceAddress(unitId) ]); const inverter = { unitId, deviceName }; if (connectionStatus) inverter.connectionStatus = connectionStatus; if (portNumber !== null) inverter.portNumber = portNumber; if (deviceAddress !== null) inverter.deviceAddress = deviceAddress; inverters.push(inverter); } } catch (error) { continue; } } return inverters; } /** * Read all SmartLogger data in one call */ async readAllData() { const [system, power, environmental, alarms] = await Promise.all([ this.readSystemData(), this.readPowerData(), this.readEnvironmentalData(), this.readAlarmData() ]); return { system, power, environmental, alarms }; } } module.exports = { SmartLoggerFunctions };