I have created a proof of concept node-red flow controlling my Growatt inverters. It’s using OpenInverter project which is a replacement firmware for Growatts wifi stick allowing full control locally. In my configuration this is done by configuring it to use the Cerbo’s intertal MQTT server. Since Node-Red is also running on the Cerbo I keep all the control in one place. GitHub - OpenInverterGateway/OpenInverterGateway: Firmware replacement for Growatt ShineWiFi-S . Apart from Growatt it also on/off controls a 2 panel inverter from APSystems using a HomeWizard smart socket.
This flow fetches the current EnergyZero electricity price every 5 minutes and uses it to control PV output and Victron ESS behavior. Once the Victron Minimum Discharge SOC setting is known, it runs in two modes:
• If price > 0, PV is enabled at 100%, throttling is disabled, and ESS target/min SOC is set to the configured minimum.
• If price ≤ 0, PV is set to 0%, throttling is enabled, and ESS target SOC is set to 100% (prefers charging/importing).
It also listens for grid-loss alarms and battery SOC. On grid loss it normally turns PV off, but if the battery SOC falls below 50% during an outage it forces PV to 100% (emergency override) until SOC recovers to 90%, then clears the override. Commands go out via MQTT to multiple PV controllers plus an HTTP call to toggle a PV shed switch.
I hope this is useful for someone.
[
{
"id": "bdbb4b2f64a2ea6f",
"type": "group",
"z": "b51fc0718e9c7f9d",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"e5c6a0996694e615",
"29fb60c427c39f8e",
"f187f03a749a140c",
"a1fa04f4896cc614",
"169dab67c8eecb5c",
"795f70d361eb852d",
"e48a83570023d43f",
"eb98fb36c0858bfc",
"f9ecf63ba68d3e89",
"03e042b6c49e7145",
"7a1d5ae47e397e2e",
"2d3733302885b499",
"d36b13b7e53900e3",
"e322d0801857ae34",
"41017fefc353bd5a",
"b2986e48ca7cc4d9",
"927efb2a9b581949",
"4986f56544299c23",
"948625324d0ad0aa",
"3aecf133104345cb"
],
"x": 34,
"y": 1659,
"w": 1392,
"h": 562
},
{
"id": "e5c6a0996694e615",
"type": "mqtt out",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"name": "",
"topic": "Solar/garage/command/power/set/activerate",
"qos": "1",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "6251341b8ce002d3",
"x": 1150,
"y": 2000,
"wires": []
},
{
"id": "29fb60c427c39f8e",
"type": "mqtt out",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"name": "",
"topic": "Solar/veranda/command/power/set/activerate",
"qos": "1",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "6251341b8ce002d3",
"x": 1160,
"y": 2060,
"wires": []
},
{
"id": "f187f03a749a140c",
"type": "mqtt out",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"name": "",
"topic": "Solar/roof/command/power/set/activerate",
"qos": "1",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "6251341b8ce002d3",
"x": 1140,
"y": 2120,
"wires": []
},
{
"id": "a1fa04f4896cc614",
"type": "function",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"name": "PV Control",
"func": "// =====================\n// Context\n// =====================\nlet currentEnergyPrice = context.get('currentEnergyPrice');\nlet lastGridStatus = context.get('lastGridStatus');\nlet batterySOC = context.get('batterySOC');\nlet socOverrideActive = context.get('socOverrideActive') || false;\n\nlet minDischargeSOC = context.get('minDischargeSOC'); // NO fallback\nlet settingsReady = context.get('settingsReady') || false; // gate\n\n// =====================\n// Nice status helper\n// =====================\nfunction setNiceStatus(color, title, lines = []) {\n const text = [title, ...lines].join(\" | \");\n node.status({ fill: color, shape: \"dot\", text });\n}\n\nfunction isFiniteNumber(v) {\n const n = Number(v);\n return Number.isFinite(n) ? n : null;\n}\n\n// =====================\n// VENUS SETTINGS (CONFIG INPUT)\n// - Stores minDischargeSOC\n// - If price is already known AND no grid-loss override is active,\n// immediately applies PV control + throttling + targetSOC + pvShed.\n// =====================\nif (msg.topic === \"Venus settings - Minimum Discharge SOC (%)\") {\n const value = isFiniteNumber(msg.payload);\n\n if (value === null || value < 0 || value > 100) {\n setNiceStatus(\"red\", \"❌ Invalid Venus Setting\", [\n `Payload: ${msg.payload}`\n ]);\n return null;\n }\n\n minDischargeSOC = value;\n context.set('minDischargeSOC', minDischargeSOC);\n\n settingsReady = true;\n context.set('settingsReady', true);\n\n // If grid is lost and override is active, we only store the setting (don't change outputs)\n if (lastGridStatus === 2 && socOverrideActive) {\n setNiceStatus(\"blue\", \"⚙️ Settings Loaded\", [\n `Min Discharge SOC: ${minDischargeSOC}%`,\n \"Grid lost override active (no apply)\"\n ]);\n return null;\n }\n\n // Apply immediately only if price is known\n const priceVal = isFiniteNumber(currentEnergyPrice);\n if (priceVal === null) {\n setNiceStatus(\"blue\", \"⚙️ Settings Loaded\", [\n `Min Discharge SOC: ${minDischargeSOC}%`,\n \"Waiting for price to apply\"\n ]);\n return null;\n }\n\n const isPositivePrice = priceVal > 0;\n const pvOutputPercent = isPositivePrice ? 100 : 0;\n const targetSOC = isPositivePrice ? minDischargeSOC : 100;\n const throttling = !isPositivePrice;\n\n setNiceStatus(isPositivePrice ? \"green\" : \"red\", \"⚙️ Settings Applied\", [\n `Min SOC: ${minDischargeSOC}%`,\n `Price: ${priceVal}`,\n `PV: ${pvOutputPercent}%`,\n `Target SOC: ${targetSOC}%`\n ]);\n\n return [[\n { topic: \"pvControl\", payload: JSON.stringify({ value: pvOutputPercent }) },\n { topic: \"throttling\", payload: throttling },\n { topic: \"targetSOC\", payload: targetSOC },\n { topic: \"pvShed\", payload: { power_on: isPositivePrice } },\n {\n topic: \"debug\",\n payload: {\n mode: \"venus_settings_update_apply\",\n minDischargeSOC,\n currentEnergyPrice: priceVal,\n pvOutputPercent,\n throttling,\n targetSOC\n }\n }\n ]];\n}\n\n// =====================\n// GATE: Do nothing until Venus settings are received\n// =====================\nif (!settingsReady || minDischargeSOC === undefined) {\n setNiceStatus(\"yellow\", \"⏳ Waiting for Venus Settings\", [\n \"Minimum Discharge SOC not received yet\"\n ]);\n return null;\n}\n\n// =====================\n// ENERGY PRICE HANDLING\n// =====================\nif (msg.topic === \"energyzero\") {\n currentEnergyPrice = msg.payload;\n context.set('currentEnergyPrice', currentEnergyPrice);\n\n // If grid is lost and override is active, do not change PV based on price\n if (lastGridStatus === 2 && socOverrideActive) return null;\n\n const priceVal = isFiniteNumber(currentEnergyPrice);\n if (priceVal === null) {\n setNiceStatus(\"red\", \"❌ Price Invalid\", [`Payload: ${msg.payload}`]);\n return null;\n }\n\n const isPositivePrice = priceVal > 0;\n const pvOutputPercent = isPositivePrice ? 100 : 0;\n const targetSOC = isPositivePrice ? minDischargeSOC : 100;\n const throttling = !isPositivePrice;\n\n setNiceStatus(isPositivePrice ? \"green\" : \"red\", \"⚡ Price Control\", [\n `Price: ${priceVal}`,\n `PV: ${pvOutputPercent}%`,\n `Target SOC: ${targetSOC}%`,\n `Min SOC: ${minDischargeSOC}%`\n ]);\n\n return [[\n { topic: \"pvControl\", payload: JSON.stringify({ value: pvOutputPercent }) },\n { topic: \"throttling\", payload: throttling },\n { topic: \"targetSOC\", payload: targetSOC },\n { topic: \"pvShed\", payload: { power_on: isPositivePrice } }, // PV Shed control\n {\n topic: \"debug\",\n payload: {\n mode: \"price_only_control\",\n currentEnergyPrice: priceVal,\n pvOutputPercent,\n throttling,\n targetSOC,\n minDischargeSOC\n }\n }\n ]];\n}\n\n// =====================\n// GRID LOSS ALARM HANDLING\n// =====================\nif (msg.topic === \"Multiplus-II - Grid - Grid lost alarm\") {\n const alarmValue = msg.payload;\n const wasGridLost = lastGridStatus === 2;\n\n context.set('lastGridStatus', alarmValue);\n lastGridStatus = alarmValue;\n\n // ---- Grid lost ----\n if (alarmValue === 2) {\n if (batterySOC < 50) {\n context.set('socOverrideActive', true);\n socOverrideActive = true;\n\n setNiceStatus(\"orange\", \"🚨 Emergency Override\", [\n \"Grid Lost\",\n `Battery: ${batterySOC}%`,\n \"PV forced to 100%\"\n ]);\n\n return [[\n { topic: \"pvControl\", payload: JSON.stringify({ value: 100 }) },\n { topic: \"throttling\", payload: true },\n { topic: \"pvShed\", payload: { power_on: true } }, // PV Shed ON during override\n {\n topic: \"debug\",\n payload: {\n mode: \"grid_loss_soc_override\",\n batterySOC,\n pvOutputPercent: 100\n }\n }\n ]];\n }\n\n setNiceStatus(\"orange\", \"⚠️ Grid Lost\", [\n `Battery: ${batterySOC}%`,\n \"PV OFF\"\n ]);\n\n return [[\n { topic: \"pvControl\", payload: JSON.stringify({ value: 0 }) },\n { topic: \"throttling\", payload: true },\n { topic: \"pvShed\", payload: { power_on: false } }, // PV Shed OFF on grid loss\n {\n topic: \"debug\",\n payload: {\n mode: \"grid_loss\",\n batterySOC,\n pvOutputPercent: 0\n }\n }\n ]];\n }\n\n // ---- Grid returned ----\n if (alarmValue === 0 && wasGridLost) {\n context.set('socOverrideActive', false);\n socOverrideActive = false;\n\n const priceVal = isFiniteNumber(currentEnergyPrice);\n const priceKnown = priceVal !== null;\n\n // If price unknown, we can't apply price-based control yet\n if (!priceKnown) {\n setNiceStatus(\"green\", \"✅ Grid Restored\", [\n \"Price unknown (waiting)\",\n `Min SOC: ${minDischargeSOC}%`\n ]);\n return null;\n }\n\n const isPositivePrice = priceVal > 0;\n const pvOutputPercent = isPositivePrice ? 100 : 0;\n const targetSOC = isPositivePrice ? minDischargeSOC : 100;\n const throttling = !isPositivePrice;\n\n setNiceStatus(isPositivePrice ? \"green\" : \"red\", \"✅ Grid Restored\", [\n `Price: ${priceVal}`,\n `PV: ${pvOutputPercent}%`,\n `Target SOC: ${targetSOC}%`,\n `Min SOC: ${minDischargeSOC}%`\n ]);\n\n return [[\n { topic: \"pvControl\", payload: JSON.stringify({ value: pvOutputPercent }) },\n { topic: \"throttling\", payload: throttling },\n { topic: \"targetSOC\", payload: targetSOC },\n { topic: \"pvShed\", payload: { power_on: isPositivePrice } }, // Resume PV Shed based on price\n {\n topic: \"debug\",\n payload: {\n mode: \"grid_return_resume\",\n currentEnergyPrice: priceVal,\n pvOutputPercent,\n throttling,\n targetSOC,\n minDischargeSOC\n }\n }\n ]];\n }\n\n return null;\n}\n\n// =====================\n// BATTERY SOC HANDLING\n// =====================\nif (msg.topic === \"Pylontech battery - State of charge (%)\") {\n batterySOC = msg.payload;\n context.set('batterySOC', batterySOC);\n\n // Override ends when SOC recovers\n if (socOverrideActive && batterySOC >= 90) {\n context.set('socOverrideActive', false);\n socOverrideActive = false;\n\n setNiceStatus(\"orange\", \"🔋 SOC Recovered\", [\n `Battery: ${batterySOC}%`,\n \"Override cleared\",\n \"PV OFF\"\n ]);\n\n return [[\n { topic: \"pvControl\", payload: JSON.stringify({ value: 0 }) },\n { topic: \"pvShed\", payload: { power_on: false } }, // Shed off when override ends\n {\n topic: \"debug\",\n payload: {\n mode: \"soc_recovered\",\n batterySOC,\n pvOutputPercent: 0\n }\n }\n ]];\n }\n\n // If grid lost and SOC low, activate override\n if (!socOverrideActive && lastGridStatus === 2 && batterySOC < 50) {\n context.set('socOverrideActive', true);\n socOverrideActive = true;\n\n setNiceStatus(\"orange\", \"🚨 Emergency Override\", [\n \"Grid Lost\",\n `Battery: ${batterySOC}%`,\n \"PV forced to 100%\"\n ]);\n\n return [[\n { topic: \"pvControl\", payload: JSON.stringify({ value: 100 }) },\n { topic: \"pvShed\", payload: { power_on: true } }, // Shed on when override activates\n {\n topic: \"debug\",\n payload: {\n mode: \"soc_low_grid_lost\",\n batterySOC,\n pvOutputPercent: 100\n }\n }\n ]];\n }\n\n // Monitoring status (no outputs)\n const priceVal = isFiniteNumber(currentEnergyPrice);\n setNiceStatus(\"grey\", \"📊 Monitoring\", [\n `Battery: ${batterySOC}%`,\n `Grid: ${lastGridStatus === 2 ? \"Lost\" : \"OK\"}`,\n `Min SOC: ${minDischargeSOC}%`,\n `Price: ${priceVal === null ? \"n/a\" : priceVal}`\n ]);\n\n return null;\n}\n\n// =====================\n// DEFAULT\n// =====================\nreturn null;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 770,
"y": 1800,
"wires": [
[
"03e042b6c49e7145"
]
]
},
{
"id": "169dab67c8eecb5c",
"type": "inject",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"name": "Every 5 minutes",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "300",
"crontab": "",
"once": true,
"onceDelay": "5",
"topic": "energyzero",
"payload": "",
"payloadType": "date",
"x": 170,
"y": 1700,
"wires": [
[
"795f70d361eb852d"
]
]
},
{
"id": "795f70d361eb852d",
"type": "function",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"name": "EnergyZero",
"func": "let now = new Date();\nlet hour = now.getUTCHours().toString();\nlet url_dt_str = now.toISOString().slice(0, 13); // Format: YYYY-MM-DDTHH\nlet log_dt_str = now.toLocaleString('en-GB', { timeZone: 'UTC' }); // Format: DD/MM/YYYY HH:MM:SS\n\n// Log the fetch action\nnode.log(`${log_dt_str} Fetching tariff information`);\n\n// Construct the URL\nlet url = `https://api.energyzero.nl/v1/energyprices?fromDate=${url_dt_str}%3A00%3A00.000Z&tillDate=${now.toISOString().slice(0, 10)}T${hour}%3A59%3A59.999Z&interval=4&usageType=1&inclBtw=False`;\n\n// Pass the URL to the next node\nmsg.url = url;\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 390,
"y": 1700,
"wires": [
[
"e48a83570023d43f"
]
]
},
{
"id": "e48a83570023d43f",
"type": "http request",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"name": "",
"method": "GET",
"ret": "txt",
"paytoqs": "ignore",
"url": "",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 150,
"y": 1760,
"wires": [
[
"eb98fb36c0858bfc"
]
]
},
{
"id": "eb98fb36c0858bfc",
"type": "json",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"name": "",
"property": "payload",
"action": "",
"pretty": false,
"x": 310,
"y": 1760,
"wires": [
[
"f9ecf63ba68d3e89"
]
]
},
{
"id": "f9ecf63ba68d3e89",
"type": "function",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"name": "Format JSON",
"func": "// Input: payload.Prices[0].price\nif (!msg.payload || !msg.payload.Prices || !msg.payload.Prices[0] || typeof msg.payload.Prices[0].price !== 'number') {\n node.error(\"Invalid input. msg.payload.Prices[0].price must be a number.\");\n node.status({ fill: \"red\", shape: \"ring\", text: \"Invalid input\" });\n return null;\n}\n\nlet basePrice = msg.payload.Prices[0].price; // Base price\nlet purchaseCost = 0.028; // EnergyZero purchase cost\nlet tax = 0.09161; // Tax 2025: 0.10154 2026: 0.09161\n\n// Calculate final price\nlet finalPrice = (basePrice + purchaseCost + tax) * 1.21;\n\n// Set only the final price to the payload\nmsg.payload = finalPrice.toFixed(4);\n\n// Set the topic to \"energyzero\"\nmsg.topic = \"energyzero\";\n\n// Update node status with the calculated price\nnode.status({ fill: \"green\", shape: \"dot\", text: `€${msg.payload}` });\n\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 1760,
"wires": [
[
"a1fa04f4896cc614",
"41017fefc353bd5a"
]
]
},
{
"id": "03e042b6c49e7145",
"type": "switch",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"name": "",
"property": "topic",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "throttling",
"vt": "str"
},
{
"t": "eq",
"v": "pvShed",
"vt": "str"
},
{
"t": "eq",
"v": "pvControl",
"vt": "str"
},
{
"t": "eq",
"v": "targetSOC",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 4,
"x": 750,
"y": 2060,
"wires": [
[
"7a1d5ae47e397e2e"
],
[
"e322d0801857ae34"
],
[
"e5c6a0996694e615",
"29fb60c427c39f8e",
"f187f03a749a140c"
],
[
"948625324d0ad0aa"
]
]
},
{
"id": "7a1d5ae47e397e2e",
"type": "link out",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"name": "Throttle",
"mode": "link",
"links": [
"2ef59f8dc6aeadc0",
"5313d8a762e80b1a",
"96e6aa7745635e58"
],
"x": 1005,
"y": 1880,
"wires": []
},
{
"id": "2d3733302885b499",
"type": "victron-output-settings",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"service": "com.victronenergy.settings",
"path": "/Settings/CGwacs/BatteryLife/MinimumSocLimit",
"serviceObj": {
"service": "com.victronenergy.settings",
"name": "Venus settings"
},
"pathObj": {
"path": "/Settings/CGwacs/BatteryLife/MinimumSocLimit",
"type": "float",
"name": "ESS Minimum SoC (unless grid fails) (%)",
"mode": "both"
},
"name": "",
"onlyChanges": false,
"x": 1190,
"y": 2180,
"wires": []
},
{
"id": "d36b13b7e53900e3",
"type": "victron-input-vebus",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"service": "com.victronenergy.vebus/276",
"path": "/Alarms/GridLost",
"serviceObj": {
"service": "com.victronenergy.vebus/276",
"name": "Multiplus-II - Grid"
},
"pathObj": {
"path": "/Alarms/GridLost",
"type": "enum",
"name": "Grid lost alarm",
"enum": {
"0": "Ok",
"2": "Alarm"
}
},
"initial": "",
"name": "",
"onlyChanges": false,
"x": 260,
"y": 1840,
"wires": [
[
"a1fa04f4896cc614"
]
]
},
{
"id": "e322d0801857ae34",
"type": "http request",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"name": "Change PV Shed Status",
"method": "PUT",
"ret": "txt",
"paytoqs": "ignore",
"url": "http://192.168.88.23/api/v1/state",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [
{
"keyType": "Content-Type",
"keyValue": "",
"valueType": "application/json",
"valueValue": ""
}
],
"x": 1090,
"y": 1940,
"wires": [
[]
]
},
{
"id": "41017fefc353bd5a",
"type": "function",
"z": "b51fc0718e9c7f9d",
"d": true,
"g": "bdbb4b2f64a2ea6f",
"name": "send markdown",
"func": "var message = \"Prijs kWh: \" + msg.payload + \"\";\nmsg.payload = {chatId : -xxxxx, type : 'message', content : message};\n\n// activate markdown\nmsg.payload.options = {disable_web_page_preview : true, parse_mode : \"Markdown\"};\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 840,
"y": 1700,
"wires": [
[
"927efb2a9b581949"
]
]
},
{
"id": "b2986e48ca7cc4d9",
"type": "link out",
"z": "b51fc0718e9c7f9d",
"d": true,
"g": "bdbb4b2f64a2ea6f",
"name": "Telegram",
"mode": "link",
"links": [
"1cc41c3726114c61"
],
"x": 1165,
"y": 1700,
"wires": []
},
{
"id": "927efb2a9b581949",
"type": "delay",
"z": "b51fc0718e9c7f9d",
"d": true,
"g": "bdbb4b2f64a2ea6f",
"name": "",
"pauseType": "rate",
"timeout": "5",
"timeoutUnits": "minutes",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "hour",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": true,
"allowrate": false,
"outputs": 1,
"x": 1050,
"y": 1700,
"wires": [
[
"b2986e48ca7cc4d9"
]
]
},
{
"id": "4986f56544299c23",
"type": "victron-input-ess",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"service": "com.victronenergy.settings",
"path": "/Settings/CGwacs/BatteryLife/MinimumSocLimit",
"serviceObj": {
"service": "com.victronenergy.settings",
"name": "Venus settings"
},
"pathObj": {
"path": "/Settings/CGwacs/BatteryLife/MinimumSocLimit",
"type": "integer",
"name": "Minimum Discharge SOC (%)"
},
"name": "",
"onlyChanges": false,
"x": 230,
"y": 1960,
"wires": [
[
"a1fa04f4896cc614"
]
]
},
{
"id": "948625324d0ad0aa",
"type": "rbe",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"name": "At change only",
"func": "rbe",
"gap": "",
"start": "",
"inout": "out",
"septopics": true,
"property": "payload",
"topi": "topic",
"x": 860,
"y": 2180,
"wires": [
[
"2d3733302885b499"
]
]
},
{
"id": "3aecf133104345cb",
"type": "victron-input-battery",
"z": "b51fc0718e9c7f9d",
"g": "bdbb4b2f64a2ea6f",
"service": "com.victronenergy.battery/512",
"path": "/Soc",
"serviceObj": {
"service": "com.victronenergy.battery/512",
"name": "Pylontech battery"
},
"pathObj": {
"path": "/Soc",
"type": "float",
"name": "State of charge (%)"
},
"initial": "",
"name": "",
"onlyChanges": false,
"x": 250,
"y": 1900,
"wires": [
[
"a1fa04f4896cc614"
]
]
},
{
"id": "6251341b8ce002d3",
"type": "mqtt-broker",
"name": "venus.local",
"broker": "192.168.88.20",
"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": "7b1362bd84451d1c",
"type": "global-config",
"env": [],
"modules": {
"@victronenergy/node-red-contrib-victron": "1.6.56"
}
}
]
[
{
"id": "8658e94b65d3a6be",
"type": "group",
"z": "b51fc0718e9c7f9d",
"style": {
"stroke": "#999999",
"stroke-opacity": "1",
"fill": "none",
"fill-opacity": "1",
"label": true,
"label-position": "nw",
"color": "#a4a4a4"
},
"nodes": [
"5001faaa4e3d77bc",
"c73a421597f55a1f",
"396f9c9de0c55b20",
"2b6cc1c190f73a88",
"0e8d46730e00ffa7",
"1c6f1aa4b23094cd",
"f1fb6101f06e5965",
"b8db983e52530d6a",
"efc1cd5755471669",
"a6a5dd7d547fca63",
"35781d9a72c87286",
"4ee6f205806e4e53",
"284233453c55723a",
"c4bbe0efe71ff812"
],
"x": 34,
"y": 899,
"w": 1272,
"h": 242
},
{
"id": "5001faaa4e3d77bc",
"type": "victron-virtual",
"z": "b51fc0718e9c7f9d",
"g": "8658e94b65d3a6be",
"name": "PV Shed",
"outputs": 1,
"device": "pvinverter",
"default_values": false,
"enable_s2support": false,
"battery_capacity": 25,
"include_battery_temperature": false,
"battery_voltage_custom": "",
"include_engine_hours": false,
"include_starter_voltage": false,
"include_history_energy": false,
"grid_nrofphases": 1,
"include_motor_temp": false,
"include_controller_temp": false,
"include_coolant_temp": false,
"include_motor_rpm": true,
"include_motor_direction": true,
"position": "1",
"pvinverter_nrofphases": 1,
"fluid_type": 0,
"include_tank_battery": false,
"include_tank_temperature": false,
"tank_battery_voltage": 3.3,
"tank_capacity": 0.2,
"temperature_type": 2,
"include_humidity": false,
"include_pressure": false,
"include_temp_battery": false,
"temp_battery_voltage": 3.3,
"x": 1000,
"y": 980,
"wires": [
[
"284233453c55723a"
]
]
},
{
"id": "c73a421597f55a1f",
"type": "inject",
"z": "b51fc0718e9c7f9d",
"g": "8658e94b65d3a6be",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "5",
"topic": "/Ac/MaxPower",
"payload": "0.8",
"payloadType": "num",
"x": 450,
"y": 940,
"wires": [
[
"4ee6f205806e4e53"
]
]
},
{
"id": "396f9c9de0c55b20",
"type": "delay",
"z": "b51fc0718e9c7f9d",
"g": "8658e94b65d3a6be",
"name": "",
"pauseType": "rate",
"timeout": "5",
"timeoutUnits": "minutes",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "minute",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": true,
"allowrate": false,
"outputs": 1,
"x": 700,
"y": 1040,
"wires": [
[
"2b6cc1c190f73a88"
]
]
},
{
"id": "2b6cc1c190f73a88",
"type": "function",
"z": "b51fc0718e9c7f9d",
"g": "8658e94b65d3a6be",
"name": "PV Output",
"func": "var d = moment().format(\"YYYYMMDD\");\nvar t = moment().format(\"HH:mm\");\nvar v2 = msg.payload;\n\n// Set v2 to 0 if msg.payload is negative\nif (v2 < 0) {\n v2 = 0;\n}\n\nmsg.payload = {\n d: d,\n t: t,\n v2: v2\n}\nmsg.action = msg.payload\nmsg.headers = { \n 'X-Pvoutput-Apikey': 'xxxx',\n 'X-Pvoutput-SystemId': 'xxxx',\n 'Content-Type': 'application/x-www-form-urlencoded'\n};\nmsg.url = \"http://pvoutput.org/service/r2/addstatus.jsp\";\nreturn msg;\n\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
{
"var": "moment",
"module": "moment"
}
],
"x": 870,
"y": 1040,
"wires": [
[
"0e8d46730e00ffa7"
]
]
},
{
"id": "0e8d46730e00ffa7",
"type": "http request",
"z": "b51fc0718e9c7f9d",
"g": "8658e94b65d3a6be",
"name": "Post",
"method": "POST",
"ret": "txt",
"paytoqs": "ignore",
"url": "",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 1010,
"y": 1040,
"wires": [
[
"1c6f1aa4b23094cd"
]
]
},
{
"id": "1c6f1aa4b23094cd",
"type": "debug",
"z": "b51fc0718e9c7f9d",
"g": "8658e94b65d3a6be",
"name": "debug 8",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "msg",
"x": 1200,
"y": 1040,
"wires": []
},
{
"id": "f1fb6101f06e5965",
"type": "inject",
"z": "b51fc0718e9c7f9d",
"g": "8658e94b65d3a6be",
"name": "",
"props": [
{
"p": "payload"
}
],
"repeat": "5",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 150,
"y": 980,
"wires": [
[
"a6a5dd7d547fca63"
]
]
},
{
"id": "b8db983e52530d6a",
"type": "inject",
"z": "b51fc0718e9c7f9d",
"g": "8658e94b65d3a6be",
"name": "",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "1",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 150,
"y": 1020,
"wires": [
[
"efc1cd5755471669"
]
]
},
{
"id": "efc1cd5755471669",
"type": "http request",
"z": "b51fc0718e9c7f9d",
"g": "8658e94b65d3a6be",
"name": "PV - HomeWizard Information",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "http://192.168.88.23/api",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 410,
"y": 1020,
"wires": [
[
"4ee6f205806e4e53"
]
]
},
{
"id": "a6a5dd7d547fca63",
"type": "http request",
"z": "b51fc0718e9c7f9d",
"g": "8658e94b65d3a6be",
"name": "PV - HomeWizard Measurements",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "http://192.168.88.23/api/v1/data",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 400,
"y": 980,
"wires": [
[
"4ee6f205806e4e53"
]
]
},
{
"id": "35781d9a72c87286",
"type": "inject",
"z": "b51fc0718e9c7f9d",
"g": "8658e94b65d3a6be",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "1",
"topic": "throttling",
"payload": "false",
"payloadType": "str",
"x": 460,
"y": 1060,
"wires": [
[
"4ee6f205806e4e53"
]
]
},
{
"id": "4ee6f205806e4e53",
"type": "function",
"z": "b51fc0718e9c7f9d",
"g": "8658e94b65d3a6be",
"name": "Parse & Route PV Data",
"func": "const data = msg.payload;\n\nlet payload = {};\nlet statusCode = null;\n\n// --- Throttling (optional): if you send msg.topic=\"throttling\" ---\nif (msg.topic === \"throttling\" && (data === true || data === false)) {\n context.set(\"isThrottling\", data);\n return null;\n}\nconst isThrottling = context.get(\"isThrottling\") === true;\nif (isThrottling) statusCode = 12;\n\n// --- One-time inject: /Ac/MaxPower ---\nif (msg.topic === \"/Ac/MaxPower\") {\n payload[\"/Ac/MaxPower\"] = Number(data);\n msg.payload = payload;\n return msg;\n}\n\n// --- Determine phase mode from product_type (when available) ---\n// HWE-KWH3 => 3-phase\n// HWE-KWH1, HWE-SKT, unknown => 1-phase\nlet phaseMode = null; // 1 or 3\n\nif (typeof data === \"object\" && data && data.product_type) {\n payload[\"/ProductName\"] = String(data.product_type);\n if (data.serial) payload[\"/Serial\"] = String(data.serial);\n\n if (data.product_type === \"HWE-KWH3\") phaseMode = 3;\n else phaseMode = 1; // includes HWE-KWH1, HWE-SKT, and any unknown types\n\n // Save mode so measurement messages can use it\n context.set(\"phaseMode\", phaseMode);\n\n msg.payload = payload;\n return msg;\n}\n\n// If no product_type in this msg, fall back to last known mode (default 1)\nphaseMode = context.get(\"phaseMode\") || 1;\n\n// --- Measurement handling ---\nif (typeof data === \"object\" && data && (\"wifi_ssid\" in data)) {\n\n if (phaseMode === 3) {\n // Voltage\n payload[\"/Ac/L1/Voltage\"] = Number(data.active_voltage_l1_v);\n payload[\"/Ac/L2/Voltage\"] = Number(data.active_voltage_l2_v);\n payload[\"/Ac/L3/Voltage\"] = Number(data.active_voltage_l3_v);\n\n // Current\n payload[\"/Ac/L1/Current\"] = Number(data.active_current_l1_a);\n payload[\"/Ac/L2/Current\"] = Number(data.active_current_l2_a);\n payload[\"/Ac/L3/Current\"] = Number(data.active_current_l3_a);\n\n // Power (negated for PV export display)\n payload[\"/Ac/L1/Power\"] = -1 * Number(data.active_power_l1_w);\n payload[\"/Ac/L2/Power\"] = -1 * Number(data.active_power_l2_w);\n payload[\"/Ac/L3/Power\"] = -1 * Number(data.active_power_l3_w);\n payload[\"/Ac/Power\"] = -1 * Number(data.active_power_w);\n\n // Energy\n if (\"total_power_export_kwh\" in data) {\n const total = Number(data.total_power_export_kwh);\n payload[\"/Ac/Energy/Forward\"] = total;\n\n const perPhase = total / 3;\n payload[\"/Ac/L1/Energy/Forward\"] = perPhase;\n payload[\"/Ac/L2/Energy/Forward\"] = perPhase;\n payload[\"/Ac/L3/Energy/Forward\"] = perPhase;\n }\n\n // StatusCode fallback (if not throttling)\n if (statusCode === null) {\n statusCode = Math.abs(Number(data.active_power_w)) > 0 ? 7 : 8;\n }\n\n } else {\n // 1-phase (L1 only)\n payload[\"/Ac/L1/Voltage\"] = Number(data.active_voltage_v);\n payload[\"/Ac/L1/Current\"] = Number(data.active_current_a);\n\n // Power (negated for PV export display)\n payload[\"/Ac/L1/Power\"] = -1 * Number(data.active_power_l1_w);\n payload[\"/Ac/Power\"] = -1 * Number(data.active_power_w);\n\n // Energy\n if (\"total_power_export_kwh\" in data) {\n const total = Number(data.total_power_export_kwh);\n payload[\"/Ac/Energy/Forward\"] = total;\n payload[\"/Ac/L1/Energy/Forward\"] = total;\n }\n\n // StatusCode fallback (if not throttling)\n if (statusCode === null) {\n statusCode = Math.abs(Number(data.active_power_w)) > 0 ? 7 : 8;\n }\n }\n\n if (statusCode !== null) payload[\"/StatusCode\"] = statusCode;\n\n msg.payload = payload;\n return msg;\n}\n\nreturn null;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 730,
"y": 980,
"wires": [
[
"5001faaa4e3d77bc",
"396f9c9de0c55b20"
]
]
},
{
"id": "284233453c55723a",
"type": "debug",
"z": "b51fc0718e9c7f9d",
"g": "8658e94b65d3a6be",
"name": "debug 10",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 1200,
"y": 980,
"wires": []
},
{
"id": "c4bbe0efe71ff812",
"type": "link in",
"z": "b51fc0718e9c7f9d",
"g": "8658e94b65d3a6be",
"name": "Throttle",
"links": [
"7a1d5ae47e397e2e"
],
"x": 525,
"y": 1100,
"wires": [
[
"4ee6f205806e4e53"
]
]
},
{
"id": "79453a14912c5c0f",
"type": "global-config",
"env": [],
"modules": {
"@victronenergy/node-red-contrib-victron": "1.6.56"
}
}
]