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
JavaScript
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);
};