System: Two - 10kVa Quattros programmed for split phase, Four 300ah 24v Victron Lithium NG’s, Lynx BMS NG, Cerbo. Greetings all! I am a novice when it comes to Node-RED and I’ve been trying to create a flow that ignores AC-in 1 from 80-20% SOC allowing my batteries to discharge from 80% to 20% off-grid and once my batteries hit 20%, I’d like the Quattros to grid charge them back to 80%. I’ve created a flow for this (as well as some other bells and whistles) and it will complete 3-4 cycles and then, for some reason always in the evening or morning, it will lose it’s mind and take the batteries to the discharge floor and shut my whole home off. I’ve tried doing several things and I’ve now just hard-coded the thresholds - hoping that that will solve it. But eventually, I’d like to be able to change the thresholds on the Node-RED dashboard. Has anyone run into this before? How did you solve it?
[
{
“id”: “tab_battery”,
“type”: “tab”,
“label”: “Battery AC Control”,
“disabled”: false,
“info”: “”
},
{
“id”: “soc_input”,
“type”: “mqtt in”,
“z”: “tab_battery”,
“name”: “Battery SOC”,
“topic”: “N/c0619ababd0c/system/0/Dc/Battery/Soc”,
“qos”: “0”,
“datatype”: “json”,
“broker”: “broker_cerbo”,
“nl”: true,
“rap”: true,
“rh”: 0,
“inputs”: 0,
“x”: 110,
“y”: 480,
“wires”: [
[
“soc_gauge”,
“96b6e454819e86c2”,
“soc_logic”
]
]
},
{
“id”: “soc_logic”,
“type”: “function”,
“z”: “tab_battery”,
“name”: “SOC Logic”,
“func”: “var soc = Math.round(msg.payload.value);\n\n// HARDCODED THRESHOLDS - DO NOT CHANGE\nvar lowThreshold = 20;\nvar highThreshold = 80;\n\n// Get state variables\nvar manualOverride = flow.get(‘manualOverride’) || false;\nvar lastState = flow.get(‘lastIgnoreAC’);\nvar lastCommandTime = flow.get(‘lastCommandTime’) || 0;\nvar now = Date.now();\nvar minInterval = 10000;\n\nnode.warn(“SOC:” + soc + " low:" + lowThreshold + " high:" + highThreshold + " state:" + lastState);\n\n// ========== FAILSAFE: EMERGENCY at 15% - ALWAYS SEND ==========\nif (soc <= 15) {\n flow.set(‘lastIgnoreAC’, false);\n flow.set(‘lastCommandTime’, now);\n var cmdMsg = {payload: JSON.stringify({“value”: 0})};\n node.status({fill:“red”,shape:“dot”,text:"
EMERGENCY " + soc + “%”});\n node.send([cmdMsg, {payload: “
EMERGENCY - GRID CHARGING”}]);\n return null;\n}\n\n// ========== CRITICAL: At or below low threshold - ALWAYS SEND ==========\nif (soc <= lowThreshold) {\n flow.set(‘lastIgnoreAC’, false);\n flow.set(‘lastCommandTime’, now);\n var cmdMsg = {payload: JSON.stringify({“value”: 0})};\n node.status({fill:“green”,shape:“dot”,text:“Grid ON (” + soc + “%)”});\n node.send([cmdMsg, {payload: “GRID CHARGING”}]);\n return null;\n}\n\n// ========== MANUAL OVERRIDE ==========\nif (manualOverride) {\n if (soc <= 30) {\n node.status({fill:“yellow”,shape:“dot”,text:“MANUAL blocked <30%”});\n node.send([null, {payload: “MANUAL (blocked - low SOC)”}]);\n return null;\n }\n node.status({fill:“yellow”,shape:“dot”,text:“MANUAL (” + soc + “%)”});\n node.send([null, {payload: “MANUAL OVERRIDE”}]);\n return null;\n}\n\n// ========== NORMAL OPERATION ==========\nif (soc >= highThreshold) {\n if (lastState !== true && (now - lastCommandTime) > minInterval) {\n flow.set(‘lastIgnoreAC’, true);\n flow.set(‘lastCommandTime’, now);\n var cmdMsg = {payload: JSON.stringify({“value”: 1})};\n node.status({fill:“blue”,shape:“dot”,text:“Off-Grid (” + soc + “%)”});\n node.send([cmdMsg, {payload: “OFF-GRID”}]);\n return null;\n } else {\n node.status({fill:“blue”,shape:“ring”,text:“Off-Grid (” + soc + “%)”});\n node.send([null, {payload: “OFF-GRID”}]);\n return null;\n }\n}\n\n// Between thresholds: Maintain current state\nif (lastState === undefined || lastState === null) {\n lastState = false;\n flow.set(‘lastIgnoreAC’, false);\n}\n\nvar statusText = lastState ? “OFF-GRID” : “GRID CHARGING”;\nnode.status({fill:“grey”,shape:“ring”,text:soc + "% - " + statusText});\nnode.send([null, {payload: statusText}]);\nreturn null;”,
“outputs”: 2,
“timeout”: “”,
“noerr”: 0,
“initialize”: “”,
“finalize”: “”,
“libs”: ,
“x”: 170,
“y”: 580,
“wires”: [
[
“debug_soc”,
“ignore_ac_out”
],
[
“status_text”,
“b3f4f7818f4d2ca4”
]
],
“outputLabels”: [
“MQTT Command”,
“Status Text”
]
},
{
“id”: “ignore_ac_out”,
“type”: “mqtt out”,
“z”: “tab_battery”,
“name”: “Set Ignore AC”,
“topic”: “W/c0619ababd0c/vebus/276/Ac/Control/IgnoreAcIn1”,
“qos”: “0”,
“retain”: “”,
“respTopic”: “”,
“contentType”: “”,
“userProps”: “”,
“correl”: “”,
“expiry”: “”,
“broker”: “broker_cerbo”,
“x”: 300,
“y”: 280,
“wires”:
},
{
“id”: “debug_soc”,
“type”: “debug”,
“z”: “tab_battery”,
“name”: “Debug Commands”,
“active”: true,
“tosidebar”: true,
“console”: false,
“tostatus”: false,
“complete”: “payload”,
“targetType”: “msg”,
“statusVal”: “”,
“statusType”: “auto”,
“x”: 810,
“y”: 320,
“wires”:
},
{
“id”: “manual_switch”,
“type”: “ui_switch”,
“z”: “tab_battery”,
“name”: “Manual Override”,
“label”: “Manual Override”,
“tooltip”: “”,
“group”: “grp_status”,
“order”: 4,
“width”: 6,
“height”: 1,
“passthru”: true,
“decouple”: “false”,
“topic”: “manual”,
“topicType”: “str”,
“style”: “”,
“onvalue”: “true”,
“onvalueType”: “bool”,
“onicon”: “”,
“oncolor”: “”,
“offvalue”: “false”,
“offvalueType”: “bool”,
“officon”: “”,
“offcolor”: “”,
“animate”: true,
“className”: “”,
“x”: 380,
“y”: 220,
“wires”: [
[
“set_manual”
]
]
},
{
“id”: “set_manual”,
“type”: “function”,
“z”: “tab_battery”,
“name”: “Set Manual Mode”,
“func”: “flow.set(‘manualOverride’, msg.payload);\nif (msg.payload) {\n node.status({fill:“yellow”,shape:“dot”,text:“Manual ON”});\n} else {\n node.status({fill:“green”,shape:“dot”,text:“Auto Mode”});\n}\nreturn msg;”,
“outputs”: 1,
“timeout”: “”,
“noerr”: 0,
“initialize”: “”,
“finalize”: “”,
“libs”: ,
“x”: 870,
“y”: 220,
“wires”: [
]
},
{
“id”: “manual_grid_on”,
“type”: “ui_button”,
“z”: “tab_battery”,
“name”: “Force Grid ON”,
“group”: “grp_status”,
“order”: 5,
“width”: 3,
“height”: 1,
“passthru”: false,
“label”: “GRID ON”,
“tooltip”: “Force grid charging”,
“color”: “”,
“bgcolor”: “#4CAF50”,
“className”: “”,
“icon”: “fa-plug”,
“payload”: “{“value”: 0}”,
“payloadType”: “json”,
“topic”: “”,
“topicType”: “str”,
“x”: 120,
“y”: 120,
“wires”: [
[
“manual_command”
]
]
},
{
“id”: “manual_grid_off”,
“type”: “ui_button”,
“z”: “tab_battery”,
“name”: “Force Off-Grid”,
“group”: “grp_status”,
“order”: 6,
“width”: 3,
“height”: 1,
“passthru”: false,
“label”: “OFF-GRID”,
“tooltip”: “Force off-grid mode”,
“color”: “”,
“bgcolor”: “#2196F3”,
“className”: “”,
“icon”: “fa-battery-full”,
“payload”: “{“value”: 1}”,
“payloadType”: “json”,
“topic”: “”,
“topicType”: “str”,
“x”: 120,
“y”: 60,
“wires”: [
[
“manual_command”
]
]
},
{
“id”: “manual_command”,
“type”: “function”,
“z”: “tab_battery”,
“name”: “Manual Command”,
“func”: “// Only allow manual commands if override is enabled\nvar manualOverride = flow.get(‘manualOverride’) || false;\n\nif (!manualOverride) {\n node.status({fill:“red”,shape:“ring”,text:“Enable override first!”});\n return null;\n}\n\nmsg.payload = JSON.stringify(msg.payload);\n\nvar mode = msg.payload.includes(‘1’) ? ‘OFF-GRID’ : ‘GRID CHARGING’;\nnode.status({fill:“yellow”,shape:“dot”,text:"Manual: " + mode});\n\n// Send to both outputs: MQTT command and status text\nvar statusMsg = {payload: mode};\nreturn [msg, statusMsg];”,
“outputs”: 2,
“timeout”: “”,
“noerr”: 0,
“initialize”: “”,
“finalize”: “”,
“libs”: ,
“x”: 270,
“y”: 380,
“wires”: [
[
“debug_soc”,
“ignore_ac_out”
],
[
“status_text”
]
]
},
{
“id”: “soc_gauge”,
“type”: “ui_gauge”,
“z”: “tab_battery”,
“name”: “SOC Gauge”,
“group”: “grp_status”,
“order”: 1,
“width”: 6,
“height”: 4,
“gtype”: “gage”,
“title”: “Battery SOC”,
“label”: “%”,
“format”: “{{msg.payload.value | number:0}}”,
“min”: 0,
“max”: “100”,
“colors”: [
“#ca3838”,
“#e6e600”,
“#00b500”
],
“seg1”: “20”,
“seg2”: “50”,
“diff”: false,
“className”: “”,
“x”: 690,
“y”: 560,
“wires”:
},
{
“id”: “soc_chart”,
“type”: “ui_chart”,
“z”: “tab_battery”,
“name”: “SOC History”,
“group”: “grp_status”,
“order”: 3,
“width”: 6,
“height”: 4,
“label”: “SOC Trend”,
“chartType”: “line”,
“legend”: “false”,
“xformat”: “HH:mm”,
“interpolate”: “linear”,
“nodata”: “Waiting for data…”,
“dot”: false,
“ymin”: “0”,
“ymax”: “100”,
“removeOlder”: “6”,
“removeOlderPoints”: “”,
“removeOlderUnit”: “3600”,
“cutout”: 0,
“useOneColor”: false,
“useUTC”: false,
“colors”: [
“#1f77b4”,
“#000000”,
“#000000”,
“#000000”,
“#000000”,
“#000000”,
“#000000”,
“#000000”,
“#000000”
],
“outputs”: 1,
“useDifferentColor”: false,
“className”: “”,
“x”: 750,
“y”: 440,
“wires”: [
]
},
{
“id”: “status_text”,
“type”: “ui_text”,
“z”: “tab_battery”,
“group”: “grp_status”,
“order”: 2,
“width”: 6,
“height”: 1,
“name”: “Current Mode”,
“label”: “Mode”,
“format”: “{{msg.payload}}”,
“layout”: “row-spread”,
“className”: “”,
“style”: false,
“font”: “”,
“fontSize”: “”,
“color”: “#000000”,
“x”: 900,
“y”: 680,
“wires”:
},
{
“id”: “low_threshold”,
“type”: “ui_numeric”,
“z”: “tab_battery”,
“name”: “Low Threshold”,
“label”: “Low Threshold %”,
“tooltip”: “SOC below this enables grid charging”,
“group”: “grp_status”,
“order”: 8,
“width”: 6,
“height”: 1,
“wrap”: false,
“passthru”: true,
“topic”: “low”,
“topicType”: “str”,
“format”: “{{value}}”,
“min”: “10”,
“max”: “99”,
“step”: 1,
“className”: “”,
“x”: 580,
“y”: 200,
“wires”: [
[
“set_thresholds”
]
]
},
{
“id”: “high_threshold”,
“type”: “ui_numeric”,
“z”: “tab_battery”,
“name”: “High Threshold”,
“label”: “High Threshold %”,
“tooltip”: “SOC above this enables off-grid”,
“group”: “grp_status”,
“order”: 7,
“width”: 6,
“height”: 1,
“wrap”: false,
“passthru”: true,
“topic”: “high”,
“topicType”: “str”,
“format”: “{{value}}”,
“min”: “15”,
“max”: “100”,
“step”: 1,
“className”: “”,
“x”: 580,
“y”: 160,
“wires”: [
[
“set_thresholds”
]
]
},
{
“id”: “set_thresholds”,
“type”: “function”,
“z”: “tab_battery”,
“name”: “Set Thresholds”,
“func”: “if (msg.topic === ‘low’) {\n flow.set(‘lowThreshold’, msg.payload, ‘file’);\n node.status({fill:“green”,shape:“dot”,text:"Low: " + msg.payload + “%”});\n} else if (msg.topic === ‘high’) {\n flow.set(‘highThreshold’, msg.payload, ‘file’);\n node.status({fill:“blue”,shape:“dot”,text:"High: " + msg.payload + “%”});\n}\nreturn msg;\n”,
“outputs”: 1,
“timeout”: “”,
“noerr”: 0,
“initialize”: “”,
“finalize”: “”,
“libs”: ,
“x”: 860,
“y”: 160,
“wires”: [
]
},
{
“id”: “init_thresholds”,
“type”: “inject”,
“z”: “tab_battery”,
“name”: “Init Defaults”,
“props”: [
{
“p”: “payload”
}
],
“repeat”: “”,
“crontab”: “”,
“once”: true,
“onceDelay”: 0.1,
“topic”: “”,
“payload”: “”,
“payloadType”: “date”,
“x”: 590,
“y”: 100,
“wires”: [
[
“set_defaults”
]
]
},
{
“id”: “set_defaults”,
“type”: “function”,
“z”: “tab_battery”,
“name”: “Set Defaults”,
“func”: “flow.set(‘lowThreshold’, 20);\nflow.set(‘highThreshold’, 80);\nflow.set(‘manualOverride’, false);\nnode.status({fill:“green”,shape:“dot”,text:“Defaults set”});\nreturn msg;”,
“outputs”: 1,
“x”: 850,
“y”: 100,
“wires”: [
]
},
{
“id”: “vebus_discovery”,
“type”: “mqtt in”,
“z”: “tab_battery”,
“name”: “VEBus Discovery”,
“topic”: “N/HQ2451HEMX9/vebus/#”,
“qos”: “0”,
“datatype”: “auto”,
“broker”: “broker_cerbo”,
“nl”: true,
“rap”: true,
“rh”: 0,
“inputs”: 0,
“x”: 580,
“y”: 60,
“wires”: [
[
“debug_vebus”
]
]
},
{
“id”: “debug_vebus”,
“type”: “debug”,
“z”: “tab_battery”,
“name”: “VEBus Messages”,
“active”: true,
“tosidebar”: true,
“console”: false,
“tostatus”: false,
“complete”: “N/HQ2451HEMX9/+/+/Dc/Battery/Soc”,
“targetType”: “msg”,
“statusVal”: “”,
“statusType”: “auto”,
“x”: 830,
“y”: 60,
“wires”:
},
{
“id”: “96b6e454819e86c2”,
“type”: “function”,
“z”: “tab_battery”,
“name”: “Format for Chart”,
“func”: “msg.payload = msg.payload.value;\n return msg;”,
“outputs”: 1,
“timeout”: 0,
“noerr”: 0,
“initialize”: “”,
“finalize”: “”,
“libs”: ,
“x”: 350,
“y”: 460,
“wires”: [
[
“soc_chart”
]
]
},
{
“id”: “1065a8bee8357cd1”,
“type”: “pushover”,
“z”: “tab_battery”,
“name”: “Emergency Alert”,
“device”: “”,
“title”: “
BATTERY EMERGENCY”,
“priority”: “1”,
“sound”: “siren”,
“url”: “”,
“url_title”: “”,
“html”: false,
“x”: 900,
“y”: 760,
“wires”:
},
{
“id”: “b3f4f7818f4d2ca4”,
“type”: “function”,
“z”: “tab_battery”,
“name”: “Format Emergency”,
“func”: “if (msg.payload && msg.payload.includes(“EMERGENCY”)) {\n msg.payload = “Battery SOC is critically low! System has switched to emergency grid charging.”;\n msg.topic = “
BATTERY EMERGENCY”;\n msg.priority = 2;\n msg.retry = 60;\n msg.expire = 3600;\n return msg;\n}\nreturn null;”,
“outputs”: 1,
“timeout”: 0,
“noerr”: 0,
“initialize”: “”,
“finalize”: “”,
“libs”: ,
“x”: 490,
“y”: 820,
“wires”: [
[
“1065a8bee8357cd1”
]
]
},
{
“id”: “broker_cerbo”,
“type”: “mqtt-broker”,
“name”: “Cerbo GX”,
“broker”: “localhost”,
“port”: “1883”,
“clientid”: “”,
“autoConnect”: true,
“usetls”: false,
“protocolVersion”: “4”,
“keepalive”: “60”,
“cleansession”: true,
“autoUnsubscribe”: true,
“birthTopic”: “”,
“birthQos”: “0”,
“birthRetain”: “false”,
“birthPayload”: “”,
“birthMsg”: {},
“closeTopic”: “”,
“closeQos”: “0”,
“closeRetain”: “false”,
“closePayload”: “”,
“closeMsg”: {},
“willTopic”: “”,
“willQos”: “0”,
“willRetain”: “false”,
“willPayload”: “”,
“willMsg”: {},
“userProps”: “”,
“sessionExpiry”: “”
},
{
“id”: “grp_status”,
“type”: “ui_group”,
“name”: “System Control Panel”,
“tab”: “tab_dashboard”,
“order”: 1,
“disp”: true,
“width”: 6,
“collapse”: false,
“className”: “”
},
{
“id”: “tab_dashboard”,
“type”: “ui_tab”,
“name”: “1010 Information Center”,
“icon”: “mi-home”,
“order”: 1,
“disabled”: false,
“hidden”: false
},
{
“id”: “e09621a3df4fb2bd”,
“type”: “global-config”,
“env”: ,
“modules”: {
“node-red-dashboard”: “3.6.6”,
“node-red-node-pushover”: “0.3.1”
}
}
]
