I think I have finally found the problem, but no idea how to fix it. It’s taken a long time to track it down as it takes a long time to test each thing because it takes so long to see if the heap is increasing, staying flat, or decreasing. And I don’t have time to do it every day.
The problem appears to be a rule I run to update energy meters (kWh) for my SolarEdge solar PV systems. When this rule is enabled, heap increases until crash.
I am connecting to two SolarEdge 6KW inverters (Two houses side by side. Two independent systems) via Modbus and the SunSpec extension. I only get Lifetime Import, Export, and Production, so I need to calculate Lifetime Consumption, Self Consumption, and Self Consumption Coverage. As well as Day, Week, Month, and Year statistics for all 6 readings (Import, Export, Production, Consumption, Self Consumption, and Self Consumption Coverage).
It is triggered by any one of the 3 lifetime readings from either inverter (6 triggers total) so can run quite frequently. I admit I haven’t tested reducing the poll time significantly to see if it improves, as I suspect it will just take longer to see the ‘Cleaned Heap Percent’ increase.
I have a similar rule that updates live Power readings (kW) that triggers at a similar if not identical frequency but does not cause the ‘Cleaned Heap Percent’ to increase. I will include all the rules in my solaredge.js rule file just for maximum info or if someone else using SolarEdge wants to use my error code rule etc.
Everything seems like fairly basic coding. I’m not an professional. It’s really just a couple of functions, a few additions and subtraction of numbers, and a few calls to database (the database bit is my best guess at the moment).
Anyone have any ideas?
console.loggerName = 'org.openhab.SolarEdge';
let storedH1Errors = items.H1_SolarEdge_LastErrors.state.split(',')
let storedH2Errors = items.H2_SolarEdge_LastErrors.state.split(',')
let H1Errors = cache.private.put('h1errors', storedH1Errors)
let H2Errors = cache.private.put('h2errors', storedH2Errors)
function roundTo3(input) {
return Math.round(Number.parseFloat(input) * 1000) / 1000;
}
function calculateAggregatePeriod(type, period, houseNo) {
let midnight = time.toZDT('00:00');
let dayOfWeek = time.toZDT().dayOfWeek().value();
let startOfWeek = midnight.minusDays(dayOfWeek - 1);
let startOfMonth = midnight.withDayOfMonth(1);
let startOfYear = midnight.withDayOfYear(1);
let aggregatePrefix = houseNo + "_SolarEdge_";
// Lifetime Consumption
let totalImport = roundTo3(items.getItem(aggregatePrefix + "AggregateLifetime_Import").numericState);
let totalExport = roundTo3(items.getItem(aggregatePrefix + "AggregateLifetime_Export").numericState);
let totalProduction = roundTo3(items.getItem(aggregatePrefix + "AggregateLifetime_Production").numericState);
let deltaSince;
let periodStart;
switch (period) {
case "Day":
deltaSince = items.getItem(aggregatePrefix + "AggregateLifetime_" + type).history.deltaSince(midnight);
periodStart = midnight;
break;
case "Week":
deltaSince = items.getItem(aggregatePrefix + "AggregateLifetime_" + type).history.deltaSince(startOfWeek);
periodStart = startOfWeek;
break;
case "Month":
deltaSince = items.getItem(aggregatePrefix + "AggregateLifetime_" + type).history.deltaSince(startOfMonth);
periodStart = startOfMonth;
break;
case "Year":
deltaSince = items.getItem(aggregatePrefix + "AggregateLifetime_" + type).history.deltaSince(startOfYear);
periodStart = startOfYear;
break;
default:
return null;
}
if (deltaSince === null) {
let currentValue = roundTo3(items.getItem(houseNo + "_SolarEdge_AggregateLifetime_" + type).numericState)
let minimum = items.getItem(aggregatePrefix + "AggregateLifetime_" + type).history.minimumSince(periodStart).numericState;
return roundTo3(currentValue - minimum);
} else {
return roundTo3(deltaSince);
}
}
function updateAggregatePeriod(totalImport, totalExport, totalProduction, period, houseNo) {
items.getItem(houseNo + "_SolarEdge_Aggregate" + period + "_Import").postUpdate(totalImport);
items.getItem(houseNo + "_SolarEdge_Aggregate" + period + "_Export").postUpdate(totalExport);
items.getItem(houseNo + "_SolarEdge_Aggregate" + period + "_Production").postUpdate(totalProduction);
let totalConsumption = roundTo3(totalImport + totalProduction - totalExport);
items.getItem(houseNo + "_SolarEdge_Aggregate" + period + "_Consumption").postUpdate(totalConsumption);
let totalSelfConsumption = roundTo3(totalProduction - totalExport);
items.getItem(houseNo + "_SolarEdge_Aggregate" + period + "_SelfConsumption").postUpdate(totalSelfConsumption);
let totalCoverage = Math.round(totalSelfConsumption / totalConsumption * 100);
items.getItem(houseNo + "_SolarEdge_Aggregate" + period + "_SelfConsumptionCoverage").postUpdate(totalCoverage);
}
// ************************* THIS IS THE RULE THAT SEEMS TO BE THE PROBLEM ***********************
rules.JSRule({
name: "SolarEdge - Energy Meters",
description: "Update Aggregate Energy items",
triggers: [
triggers.ItemStateChangeTrigger('H1_SolarEdge_AggregateLifetime_Import', undefined, undefined),
triggers.ItemStateChangeTrigger('H1_SolarEdge_AggregateLifetime_Export', undefined, undefined),
triggers.ItemStateChangeTrigger('H1_SolarEdge_AggregateLifetime_Production', undefined, undefined),
triggers.ItemStateChangeTrigger('H2_SolarEdge_AggregateLifetime_Import', undefined, undefined),
triggers.ItemStateChangeTrigger('H2_SolarEdge_AggregateLifetime_Export', undefined, undefined),
triggers.ItemStateChangeTrigger('H2_SolarEdge_AggregateLifetime_Production', undefined, undefined)
],
execute: (event) => {
let itemName = event.itemName;
let houseNo = itemName.split("_")[0];
// Lifetime
let totalImport = roundTo3(items.getItem(houseNo + "_SolarEdge_AggregateLifetime_Import").numericState);
let totalExport = roundTo3(items.getItem(houseNo + "_SolarEdge_AggregateLifetime_Export").numericState);
let totalProduction = roundTo3(items.getItem(houseNo + "_SolarEdge_AggregateLifetime_Production").numericState);
if (totalImport !== null && totalExport !== null && totalProduction !== null) {
updateAggregatePeriod(totalImport, totalExport, totalProduction, "Lifetime", houseNo);
}
// Day
let totalImportDay = calculateAggregatePeriod("Import", "Day", houseNo);
let totalExportDay = calculateAggregatePeriod("Export", "Day", houseNo);
let totalProductionDay = calculateAggregatePeriod("Production", "Day", houseNo);
updateAggregatePeriod(totalImportDay, totalExportDay, totalProductionDay, "Day", houseNo);
// Week
let totalImportWeek = calculateAggregatePeriod("Import", "Week", houseNo);
let totalExportWeek = calculateAggregatePeriod("Export", "Week", houseNo);
let totalProductionWeek = calculateAggregatePeriod("Production", "Week", houseNo);
updateAggregatePeriod(totalImportWeek, totalExportWeek, totalProductionWeek, "Week", houseNo);
// Month
let totalImportMonth = calculateAggregatePeriod("Import", "Month", houseNo);
let totalExportMonth = calculateAggregatePeriod("Export", "Month", houseNo);
let totalProductionMonth = calculateAggregatePeriod("Production", "Month", houseNo);
updateAggregatePeriod(totalImportMonth, totalExportMonth, totalProductionMonth, "Month", houseNo);
// Year
let totalImportYear = calculateAggregatePeriod("Import", "Year", houseNo);
let totalExportYear = calculateAggregatePeriod("Export", "Year", houseNo);
let totalProductionYear = calculateAggregatePeriod("Production", "Year", houseNo);
updateAggregatePeriod(totalImportYear, totalExportYear, totalProductionYear, "Year", houseNo);
}
});
rules.JSRule({
name: "SolarEdge - Power Meters",
description: "Update Live Power items",
triggers: [
triggers.ItemStateChangeTrigger('H1_SolarEdge_TotalRealPower', undefined, undefined),
triggers.ItemStateChangeTrigger('H2_SolarEdge_TotalRealPower', undefined, undefined)
],
execute: (event) => {
let houseNo = event.itemName.split('_')[0].replace('H', '')
let totalPower = Number.parseInt(items.getItem("H" + houseNo + "_SolarEdge_TotalRealPower").state)
var production = Number.parseInt(items.getItem("H" + houseNo + "_SolarEdge_Live_Production").state)
var liveExport = Math.max(0, totalPower)
var liveImport = Math.min(0, totalPower) * -1;
var consumption = Math.max(0, liveImport + production - liveExport)
var selfConsumption = consumption - liveImport
items.getItem("H" + houseNo + "_SolarEdge_Live_Import").postUpdate(liveImport + " W")
items.getItem("H" + houseNo + "_SolarEdge_Live_Export").postUpdate(liveExport + " W")
items.getItem("H" + houseNo + "_SolarEdge_Live_Consumption").postUpdate(consumption + " W")
items.getItem("H" + houseNo + "_SolarEdge_Live_SelfConsumption").postUpdate(selfConsumption + " W")
}
});
rules.JSRule({
name: "SolarEdge - Add Errors to SolarEdge Log",
description: "Store inverter errors in the 'Last Errors' item",
triggers: [
triggers.ItemStateChangeTrigger('H1_SolarEdge_ErrorCode', 0, undefined),
triggers.ItemStateChangeTrigger('H2_SolarEdge_ErrorCode', 0, undefined)
],
execute: (event) => {
let houseNumber = event.itemName.split('_')[0].replace('H', '')
console.info('Inverter ' + houseNumber + ' detected an error')
var errorList = [
{ code: '0', description: 'No error' },
{ code: '25,134', description: 'Isolation Fault' },
{ code: '10,37,38', description: 'Ground Current - RCD' },
{ code: '14', description: 'AC Voltage Too High' },
{ code: '15', description: 'DC Voltage Too High (surge)' },
{ code: '16,149,153,181,166,167,168,170', description: 'Hardware Error' },
{ code: '17,117', description: 'Temperature Too High' },
{ code: '24', description: 'Faulty Temp. Sensor' },
{ code: '26', description: 'Faulty AC Relay' },
{ code: '28', description: 'RCD Sensor Error' },
{ code: '29,30', description: 'Phase Balance Error' },
{ code: '31,33', description: 'AC Voltage Too High' },
{ code: '32,41', description: 'AC Voltage Too Low' },
{ code: '34,64,65,66', description: 'AC Freq Too High' },
{ code: '35,67,68,69', description: 'AC Freq Too Low' },
{ code: '36', description: 'DC Injection' },
{ code: '40', description: 'Islanding' },
{ code: '44', description: 'No Country Selected' },
{ code: '46', description: 'Phase Unbalance' },
{ code: '144', description: 'Islanding Passive' },
{ code: '145', description: 'UDC Max' },
{ code: '146', description: 'UDC Min' },
{ code: '147,150,151,12', description: 'Arc Fault Detected' },
{ code: '152', description: 'Arc detector self-test failed' },
{ code: '178', description: 'Internal RGM Error' },
{ code: '185', description: 'Energy Meter Comm. Error' },
{ code: '13', description: 'ARC_PWR_ DETECT' },
{ code: '55', description: 'V-Line Max' },
{ code: '56', description: 'V-Line Min' },
{ code: '57,59,60', description: 'I-ACDC L1/L2/L3. AC overcurrent' },
{ code: '61', description: 'I-RCD STEP. Ground Current RCD' },
{ code: '100,101,102', description: 'TZ L1/L2/L3 AC overcurrent' },
{ code: '158', description: 'Controller 3/12/34 Err' },
{ code: '199', description: 'RSD Error' },
{ code: '127', description: 'IRCDMax' },
{ code: '169', description: 'RCD Error' },
{ code: '133', description: 'Temp Sensor fault' },
{ code: '123', description: 'MainError' },
{ code: '96,98', description: 'Islanding Trip 1/2' },
{ code: '62', description: 'I-RCD MAX' },
{ code: '171', description: 'Over voltage Vin' },
{ code: '163', description: 'Tz Over current 1/2/3' },
{ code: '166,167,168', description: 'Tz Over voltage cap 1/2/3' },
{ code: '169', description: 'Tz Over current Rcd' },
{ code: '178,179,180', description: 'Vf1/2/3 surge' },
{ code: '137', description: 'RCD Test' },
{ code: '96,98', description: 'Islanding Trip1/2' }
];
function getErrorDescription(errorCode) {
for (var i = 0; i < errorList.length; i++) {
var codes = expandCodes(errorList[i].code);
var description = errorList[i].description;
if (codes.includes(errorCode)) {
return description;
}
}
return 'Error code not found';
}
function expandCodes(codes) {
return codes.split(',').map(code => code.trim());
}
var errorCodeToSearch = event.newState;
var errorDescription = getErrorDescription(errorCodeToSearch);
let h = event.itemName.split('_')[0]
let houseNo = h.replace('H', '')
let timestamp = time.toZDT().toString().replace('Z[SYSTEM]', '').replace('T', ' ').split('.')[0]
let logMessage = [timestamp + '>' + errorCodeToSearch + '>' + errorDescription]
let logFileMessage = timestamp + " > Inverter " + houseNo + " > Error Code: " + errorCodeToSearch + " > " + errorDescription
let logFile = items.getItem("H" + houseNo + "_SolarEdge_ErrorLog_Write")
if (h == 'H1') {
H1Errors = cache.private.get('h1errors')
H1Errors = logMessage.concat(H1Errors)
if (H1Errors.length > 10) {
H1Errors.length = 10
}
cache.private.put('h1errors', H1Errors)
items.H1_SolarEdge_LastErrors.postUpdate(H1Errors)
logFile.sendCommand(logFileMessage)
} else if (h == 'H2') {
H2Errors = cache.private.get('h2errors')
H2Errors = logMessage.concat(H2Errors)
if (H2Errors.length > 10) {
H2Errors.length = 10
}
cache.private.put('h2errors', H2Errors)
items.H2_SolarEdge_LastErrors.postUpdate(H2Errors)
logFile.sendCommand(logFileMessage)
}
}
})
rules.JSRule({
name: "SolarEdge - Add Status to SolarEdge Log",
description: "",
triggers: [
triggers.ItemStateChangeTrigger('H1_SolarEdge_InverterStatus', undefined, undefined),
triggers.ItemStateChangeTrigger('H2_SolarEdge_InverterStatus', undefined, undefined)
],
execute: (event) => {
let newData = event.newState
let oldData = event.oldState
let inputName = event.itemName
let inputItem = items.getItem(inputName)
let houseNo = inputName.split("_")[0].replace("H", "")
let logFile = items.getItem("H" + houseNo + "_SolarEdge_ErrorLog_Write")
let aC = items.getItem(inputName.replace("InverterStatus", "ACVoltage")).state;
let dC = items.getItem(inputName.replace("InverterStatus", "DCVoltage")).state;
let liveImport = items.getItem(inputName.replace("InverterStatus", "Live_Import")).state;
let liveExport = items.getItem(inputName.replace("InverterStatus", "Live_Export")).state;
let liveProduction = items.getItem(inputName.replace("InverterStatus", "Live_Production")).state;
// console.info("Adding Inverter Status to Error Log")
let timestamp = time.toZDT().toString().replace('Z[SYSTEM]', '').replace('T', ' ').split('.')[0]
let logMessage = [timestamp + '>Status change>' + oldData + " to " + newData]
let logFileMessage = (timestamp + " - Status change: " + oldData + " to " + newData + " - AC: " + aC + " - DC: " + dC + " - Import: " + liveImport + " - Export: " + liveExport + " - Production: " + liveProduction).toString()
if (houseNo == '1') {
H1Errors = cache.private.get('h1errors')
H1Errors = logMessage.concat(H1Errors);
if (H1Errors.length > 10) {
H1Errors.length = 10
}
cache.private.put('h1errors', H1Errors)
items.H1_SolarEdge_LastErrors.postUpdate(H1Errors);
logFile.sendCommand(logFileMessage);
} else if (houseNo == '2') {
H2Errors = cache.private.get('h2errors')
H2Errors = logMessage.concat(H2Errors) ;
if (H2Errors.length > 10) {
H2Errors.length = 10
}
cache.private.put('h2errors', H2Errors)
items.H2_SolarEdge_LastErrors.postUpdate(H2Errors);
logFile.sendCommand(logFileMessage);
}
}
});
rules.JSRule({
name: "SolarEdge - Clear SolarEdge Log",
description: "Clear Error Log",
triggers: [
triggers.ItemStateUpdateTrigger('Config', 'Clear_SolarEdge1_ErrorLog'),
triggers.ItemStateUpdateTrigger('Config', 'Clear_SolarEdge2_ErrorLog')
],
execute: (event) => {
let houseNo = event.receivedState.toString().split("_")[1].replace("SolarEdge", "")
console.info("Clearing Error Log of SolarEdge " + houseNo)
if (houseNo == "1") {
H1Errors = []
cache.private.put('h1errors', H1Errors)
} else if (houseNo = "2") {
H2Errors = []
cache.private.put('h2errors', H2Errors)
}
items.getItem("H" + houseNo + "_SolarEdge_LastErrors").sendCommand('')
items.Config.sendCommand('waiting...')
}
});
I also have a few transformation scripts applied to these items and things. They all seem to run fine without increasing the heap.
autoStateDescription.js
Applied to all energy and power items
(function(data) {
function round(input, points) {
return Math.round(input * Math.pow(10, points)) / Math.pow(10, points);
}
if (data == 'NULL') {
return;
}
let newData = Quantity(data)
let dimension = newData.dimension
let dimensionType;
if (dimension == "[L]²·[M]/[T]²") {
dimensionType = "energy"
newData = newData.toUnit("Wh")
} else if (dimension == "[L]²·[M]/[T]³") {
dimensionType = "power"
newData = newData.toUnit("W")
}
let dataNumber = newData.float
let unit = newData.symbol;
let xUnit = Math.round(dataNumber) + " " + unit
let kxUnit = round(dataNumber / Math.pow(10, 3), 1) + " k" + unit
let MxUnit = round(dataNumber / Math.pow(10, 6), 2) + " M" + unit
let GxUnit = round(dataNumber / Math.pow(10, 9), 2) + " G" + unit
let TxUnit = round(dataNumber / Math.pow(10, 12), 2) + " T" + unit
let output;
if (dataNumber >= Math.pow(10, 3) && dataNumber < Math.pow(10, 6)) {
output = kxUnit
} else if (dataNumber >= Math.pow(10, 6) && dataNumber < Math.pow(10, 9)) {
output = MxUnit
} else if (dataNumber >= Math.pow(10, 9) && dataNumber < Math.pow(10, 12)) {
output = GxUnit
} else if (dataNumber >= Math.pow(10, 12)) {
output = TxUnit
} else {
output = xUnit
}
return output.toString()
})(input)
solaredge_LiveImport.js and solaredge_LiveExport.js
For turning my single reading of a negative and positive total power, to independent Import and Export Items
(function(i) {
var clamp = Math.min(0, Number.parseFloat(i)) * -1;
return clamp.toString() + " W"
})(input)
(function(i) {
var clamp = Math.max(0, Number.parseFloat(i));
return clamp.toString() + " W"
})(input)
Error Catch
6 transformation scripts (3 things; import, export, production x 2 PV systems) to ignore some spikes in the data I sometimes get.
(function(data) {
// console.info('Starting H1_SolarEdge_AggregateLifetime_Export Transformation')
if (data == "NULL" || data == "UNDEF") {
data = Quantity("0 W")
}
let returnValue;
let itemState = items.getItem('H1_SolarEdge_AggregateLifetime_Export').quantityState
if (Quantity(data).lessThan(itemState)) {
console.error('Error in House 1 Lifetime Export Modbus data, use old reading of ' + itemState)
returnValue = itemState
} else {
// console.info('Use new reading ' + data )
returnValue = data
}
return returnValue
})(input)
screenshot of my widget for reading all this data -