UNPKG

node-red-contrib-huawei-solar

Version:

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

254 lines (216 loc) 10.4 kB
const { HuaweiModbusClient } = require('./utils/modbus-utils'); const { SUN2000Functions } = require('./utils/sun2000-functions'); module.exports = function(RED) { function Sun2000Node(config) { RED.nodes.createNode(this, config); const node = this; node.on('input', async function(msg) { try { const host = config.host; const port = config.port || 502; const inverterAddress = config.inverterAddress || 12; const timeout = config.timeout || 5000; const retries = config.retries || 3; const alwaysIncludeAlarmTexts = config.alwaysIncludeAlarmTexts || false; const namingConvention = config.namingConvention || 'descriptive'; const useIEC = namingConvention === 'iec61850'; const splitOutputs = config.splitOutputs || false; // Build data categories array from checkboxes const dataCategories = []; if (config.dataDevice) dataCategories.push('device'); if (config.dataPower) dataCategories.push('power'); if (config.dataVoltages) dataCategories.push('voltages'); if (config.dataCurrents) dataCategories.push('currents'); if (config.dataStrings) dataCategories.push('strings'); if (config.dataStatus) dataCategories.push('status'); if (config.dataAlarms) dataCategories.push('alarms'); // Ensure at least one category is selected if (dataCategories.length === 0) { dataCategories.push('power'); // Default fallback } // Determine device name let deviceName; if (config.useCustomName && config.deviceName) { deviceName = config.deviceName; } else { deviceName = `SUN2000-${inverterAddress}`; } // Set node status node.status({ fill: "yellow", shape: "ring", text: "connecting" }); // Create Modbus client configuration const modbusConfig = { host, port, unitId: 0, // Use unit ID 0 for remapped register access timeout, retries, }; const modbusClient = new HuaweiModbusClient(modbusConfig); const sun2000 = new SUN2000Functions(modbusClient); try { const connected = await modbusClient.connect(); if (!connected) { throw new Error(`Failed to connect to SmartLogger at ${host}:${port}`); } node.status({ fill: "green", shape: "dot", text: "reading data" }); // Create device object for the single inverter const device = { unitId: inverterAddress, deviceAddress: inverterAddress, deviceName: deviceName }; // Read data from the inverter const inverterData = await sun2000.readInverterData( device.deviceAddress, device.deviceName, dataCategories, alwaysIncludeAlarmTexts, useIEC ); // Create output with nested structure const timestamp = new Date().toISOString(); let outputPayload; if (inverterData.error) { // Error case outputPayload = { ts: timestamp, unitId: inverterAddress, deviceName: deviceName, error: inverterData.error }; } else { // Success case - choose output format based on splitOutputs setting if (splitOutputs) { const { telemetryPayload, statusPayload } = createSplitInverterData(inverterData, timestamp); // Send telemetry to output 1, status to output 2 const telemetryMsg = { ...msg, payload: telemetryPayload }; const statusMsg = { ...msg, payload: statusPayload }; node.send([telemetryMsg, statusMsg]); } else { // Single nested output (original behavior) outputPayload = createNestedInverterData(inverterData, timestamp); msg.payload = outputPayload; node.send(msg); } } node.status({ fill: "green", shape: "dot", text: "success" }); } finally { await modbusClient.disconnect(); } } catch (error) { node.status({ fill: "red", shape: "ring", text: "error" }); if (config.continueOnFail) { msg.payload = { ts: new Date().toISOString(), error: error.message, success: false, unitId: config.inverterAddress || 12 }; node.send(msg); } else { node.error(error.message, msg); } } }); node.on('close', function() { node.status({}); }); } // Helper functions for data classification and splitting function getTelemetryFields() { return [ // Descriptive naming 'activePower', 'reactivePower', 'inputPower', 'powerFactor', 'efficiency', 'peakPowerToday', 'dailyEnergyYield', 'totalEnergyYield', 'gridVoltageUAB', 'gridVoltageUBC', 'gridVoltageUCA', 'phaseAVoltage', 'phaseBVoltage', 'phaseCVoltage', 'phaseACurrent', 'phaseBCurrent', 'phaseCCurrent', 'gridFrequency', 'internalTemperature', 'cabinetTemperature', 'dcCurrent', 'numberOfStrings', 'ratedPower', 'insulationResistance', 'pvStrings', // IEC 61850 naming 'P', 'Q', 'dcP', 'PF', 'eff', 'Pmax', 'EPId', 'EPI', 'Uab', 'Ubc', 'Uca', 'Ua', 'Ub', 'Uc', 'Ia', 'Ib', 'Ic', 'Fr', 'TempInt', 'TempCab', 'dcI', 'pv' ]; } function getStatusFields() { return [ 'status', 'deviceStatus', 'deviceStatusText', 'runningStatus', 'majorFault', 'minorFault', 'warning', 'alarm1', 'alarm2', 'alarm3', 'faultCode', 'alarmTexts' ]; } function getIdentificationFields() { return ['unitId', 'deviceName', 'serialNumber', 'model']; } function createNestedInverterData(inverterData, timestamp) { const telemetryFields = getTelemetryFields(); const statusFields = getStatusFields(); const identificationFields = getIdentificationFields(); // Root level data: timestamp + device identification const rootData = { ts: timestamp, ...Object.fromEntries( identificationFields .filter(field => inverterData[field] !== undefined) .map(field => [field, inverterData[field]]) ) }; // Nested telemetry object const telemetryData = Object.fromEntries( telemetryFields .filter(field => inverterData[field] !== undefined) .map(field => [field, inverterData[field]]) ); // Nested status object const statusData = Object.fromEntries( statusFields .filter(field => inverterData[field] !== undefined) .map(field => [field, inverterData[field]]) ); // Build final nested structure const result = { ...rootData }; // Include telemetry and status objects if they have data if (Object.keys(telemetryData).length > 0) { result.telemetry = telemetryData; } if (Object.keys(statusData).length > 0) { result.status = statusData; } return result; } function createSplitInverterData(inverterData, timestamp) { const telemetryFields = getTelemetryFields(); const statusFields = getStatusFields(); const identificationFields = getIdentificationFields(); // Base identification data for both outputs const baseData = { ts: timestamp, ...Object.fromEntries( identificationFields .filter(field => inverterData[field] !== undefined) .map(field => [field, inverterData[field]]) ) }; // Telemetry output: flat structure with identification + telemetry data const telemetryPayload = { ...baseData, ...Object.fromEntries( telemetryFields .filter(field => inverterData[field] !== undefined) .map(field => [field, inverterData[field]]) ) }; // Status output: flat structure with identification + status data const statusPayload = { ...baseData, ...Object.fromEntries( statusFields .filter(field => inverterData[field] !== undefined) .map(field => [field, inverterData[field]]) ) }; return { telemetryPayload, statusPayload }; } RED.nodes.registerType("sun2000", Sun2000Node); };