UNPKG

node-red-contrib-huawei-solar

Version:

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

289 lines (256 loc) 8.74 kB
/** * Huawei Modbus Utilities * * Core utility functions for Modbus communication with Huawei devices. * Key fixes applied: * - LITTLE-ENDIAN byte order (not big-endian as documented) * - SmartLogger Unit ID: 3 (not 0 as documented) * - Proper error handling and timeouts */ const ModbusRTU = require('modbus-serial'); /** * Combine two 16-bit registers into 32-bit unsigned (LITTLE-ENDIAN) * Critical: Huawei uses little-endian despite documentation claiming big-endian */ function combineU32RegistersLE(lowWord, highWord) { return (lowWord << 16) | highWord; } /** * Combine two 16-bit registers into 32-bit signed (LITTLE-ENDIAN) */ function combineI32RegistersLE(lowWord, highWord) { const combined = (lowWord << 16) | highWord; // Handle sign extension for 32-bit signed values return combined >= 0x80000000 ? combined - 0x100000000 : combined; } /** * Combine four 16-bit registers into 64-bit unsigned (LITTLE-ENDIAN) */ function combineU64RegistersLE(reg1, reg2, reg3, reg4) { // JavaScript's Number.MAX_SAFE_INTEGER is 2^53-1, sufficient for most use cases return (reg1 * Math.pow(2, 48)) + (reg2 * Math.pow(2, 32)) + (reg3 * Math.pow(2, 16)) + reg4; } /** * Decode string from register values (each register = 2 ASCII chars) */ function decodeStringRegisters(registers, maxLength = 20) { const chars = []; for (const reg of registers) { // Extract two bytes from each 16-bit register chars.push(String.fromCharCode((reg >> 8) & 0xFF)); // High byte chars.push(String.fromCharCode(reg & 0xFF)); // Low byte } // Join and strip null terminators return chars.join('').substring(0, maxLength).replace(/\0+$/, ''); } /** * Convert signed 16-bit register value to proper signed integer */ function toSignedInt16(value) { return value >= 0x8000 ? value - 0x10000 : value; } /** * Modbus Client Wrapper with connection management and error handling */ class HuaweiModbusClient { constructor(config) { this.client = new ModbusRTU(); this.config = config; this.isConnected = false; // Set timeout and retry options this.client.setTimeout(config.timeout); this.client.setID(config.unitId); } /** * Get the client configuration */ getConfig() { return { ...this.config }; } /** * Connect to Modbus TCP device */ async connect() { try { await this.client.connectTCP(this.config.host, { port: this.config.port }); this.isConnected = true; return true; } catch (error) { this.isConnected = false; return false; } } /** * Disconnect from device */ async disconnect() { try { this.client.close(); this.isConnected = false; } catch (error) { // Ignore disconnect errors } } /** * Check if client is connected */ isClientConnected() { return this.isConnected && this.client.isOpen; } /** * Read holding registers with retry logic and proper error handling */ async readHoldingRegisters(address, count, unitId) { if (!this.isClientConnected()) { const connected = await this.connect(); if (!connected) { return { success: false, error: 'Failed to connect to Modbus device' }; } } // Set unit ID if provided if (unitId !== undefined) { this.client.setID(unitId); } let lastError = ''; for (let attempt = 0; attempt <= this.config.retries; attempt++) { try { const result = await this.client.readHoldingRegisters(address, count); // Reset unit ID back to default if it was changed if (unitId !== undefined) { this.client.setID(this.config.unitId); } return { success: true, data: result.data }; } catch (error) { lastError = error instanceof Error ? error.message : 'Unknown Modbus error'; // Wait before retry (exponential backoff) if (attempt < this.config.retries) { await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 100)); } } } // Reset unit ID back to default if it was changed if (unitId !== undefined) { this.client.setID(this.config.unitId); } return { success: false, error: `Failed after ${this.config.retries + 1} attempts: ${lastError}` }; } /** * Read single U16 register */ async readU16(address, unitId) { const result = await this.readHoldingRegisters(address, 1, unitId); if (!result.success || !result.data || result.data.length === 0) { return { success: false, error: result.error }; } return { success: true, data: result.data[0] }; } /** * Read single I16 register (signed) */ async readI16(address, unitId) { const result = await this.readU16(address, unitId); if (!result.success || result.data === undefined) { return result; } return { success: true, data: toSignedInt16(result.data) }; } /** * Read U32 register (2 consecutive registers, little-endian) */ async readU32(address, unitId) { const result = await this.readHoldingRegisters(address, 2, unitId); if (!result.success || !result.data || result.data.length < 2) { return { success: false, error: result.error }; } return { success: true, data: combineU32RegistersLE(result.data[0], result.data[1]) }; } /** * Read I32 register (2 consecutive registers, little-endian, signed) */ async readI32(address, unitId) { const result = await this.readHoldingRegisters(address, 2, unitId); if (!result.success || !result.data || result.data.length < 2) { return { success: false, error: result.error }; } return { success: true, data: combineI32RegistersLE(result.data[0], result.data[1]) }; } /** * Read string from multiple registers */ async readString(address, count, maxLength, unitId) { const result = await this.readHoldingRegisters(address, count, unitId); if (!result.success || !result.data) { return { success: false, error: result.error }; } return { success: true, data: decodeStringRegisters(result.data, maxLength) }; } /** * Calculate inverter base address for remapped registers * Formula: 51000 + (25 × (Device Address - 1)) */ static calculateInverterBaseAddress(inverterUnitId) { return 51000 + (25 * (inverterUnitId - 1)); } } /** * Connection status enumeration */ const ConnectionStatus = { Online: 'Online', Offline: 'Offline', Unknown: 'Unknown' }; /** * Parse connection status from register value */ function parseConnectionStatus(statusValue) { switch (statusValue) { case 0xB001: return ConnectionStatus.Online; case 0xB000: return ConnectionStatus.Offline; default: return ConnectionStatus.Unknown; } } /** * Plant status enumeration based on Huawei documentation */ const PlantStatus = { Unlimited: 1, Limited: 2, Idle: 3, Outage: 4, CommInterrupt: 5 }; /** * Parse plant status from register value */ function parsePlantStatus(statusValue) { switch (statusValue) { case PlantStatus.Unlimited: return 'Unlimited'; case PlantStatus.Limited: return 'Limited'; case PlantStatus.Idle: return 'Idle'; case PlantStatus.Outage: return 'Outage'; case PlantStatus.CommInterrupt: return 'Communication Interrupt'; default: return `Unknown (${statusValue})`; } } module.exports = { combineU32RegistersLE, combineI32RegistersLE, combineU64RegistersLE, decodeStringRegisters, toSignedInt16, HuaweiModbusClient, ConnectionStatus, parseConnectionStatus, PlantStatus, parsePlantStatus };