[ { "id": "725a74874dcafaeb", "type": "tab", "label": "Boiler readout", "disabled": false, "info": "", "env": [] }, { "id": "inject_temp", "type": "inject", "z": "725a74874dcafaeb", "name": "Read Temp", "props": [], "repeat": "300", "crontab": "", "once": true, "onceDelay": "160", "topic": "", "x": 130, "y": 40, "wires": [ [ "get_temp", "truma_reconnect_exec" ] ] }, { "id": "get_temp", "type": "truma-inetx-get", "z": "725a74874dcafaeb", "name": "WaterHeating get", "device": "1a318074fbabf5dd", "topics": "WaterHeating,EnergySrc", "x": 350, "y": 40, "wires": [ [ "extract_temp", "21cb95da632a0a68", "f8b921c1c53fd7df" ] ] }, { "id": "extract_temp", "type": "function", "z": "725a74874dcafaeb", "name": "Extract Temperature", "func": "let waterTopic = msg.payload?.topics?.WaterHeating?.parameters;\n\nlet waterRaw = waterTopic?.Temp?.value;\n\nif (waterRaw === undefined) {\n node.warn(\"No water temperature found\");\n return null;\n}\n\nlet water = waterRaw / 10;\n\nnode.log(\"🚿 Water: \" + water + \" °C\");\n\n// IMPORTANT: Victron-compatible format\nmsg.payload = {\n \"/Temperature\": water,\n};\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 590, "y": 40, "wires": [ [ "debug_only", "48620512af8555ea" ] ] }, { "id": "debug_only", "type": "debug", "z": "725a74874dcafaeb", "name": "Temp Only", "active": true, "tosidebar": false, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "payload", "statusType": "auto", "x": 810, "y": 40, "wires": [] }, { "id": "48620512af8555ea", "type": "victron-virtual", "z": "725a74874dcafaeb", "name": "Boiler", "outputs": 1, "device": "temperature", "default_values": false, "acload_nrofphases": 1, "enable_s2support": false, "battery_capacity": 25, "include_battery_temperature": false, "battery_voltage_preset": "24", "battery_voltage_custom": 24, "generator_type": "ac", "generator_nrofphases": 1, "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": 0, "pvinverter_nrofphases": 1, "switch_1_type": 1, "switch_1_min": 0, "switch_1_max": "", "switch_1_initial": 0, "switch_1_label": "", "switch_1_unit": "", "switch_1_step": 1, "switch_1_customname": "", "switch_1_group": "", "switch_1_include_measurement": false, "switch_1_rgb_color_wheel": false, "switch_1_cct_wheel": false, "switch_1_rgb_white_dimmer": false, "fluid_type": 0, "include_tank_battery": false, "include_tank_temperature": false, "tank_battery_voltage": 3.3, "tank_capacity": 0.2, "temperature_type": "5", "include_humidity": false, "include_pressure": false, "include_temp_battery": false, "temp_battery_voltage": 3.3, "x": 790, "y": 140, "wires": [ [] ] }, { "id": "6f4b77031ea85d35", "type": "victron-input-battery", "z": "725a74874dcafaeb", "service": "com.victronenergy.battery/278", "path": "/Soc", "serviceObj": { "service": "com.victronenergy.battery/278", "name": "SmartShunt Dumbo", "communityTag": "battery" }, "pathObj": { "path": "/Soc", "type": "float", "name": "State of charge (%)" }, "name": "SOC Input", "onlyChanges": false, "roundValues": "1", "rateLimit": "0.2", "conditionalMode": false, "condition1HysteresisEnabled": false, "condition1HysteresisThreshold": "", "outputTrue": "", "outputFalse": "", "debounce": "", "x": 100, "y": 440, "wires": [ [ "aa853766bbe69145", "82d3bc0f679e7b16" ] ] }, { "id": "d8915ec0694bd178", "type": "debug", "z": "725a74874dcafaeb", "name": "Mode/Active", "active": true, "tosidebar": false, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "payload", "statusType": "auto", "x": 810, "y": 240, "wires": [] }, { "id": "21cb95da632a0a68", "type": "function", "z": "725a74874dcafaeb", "name": "Extract Mode and Active", "func": "let waterTopic = msg.payload?.topics?.WaterHeating?.parameters;\n\nif (!waterTopic) {\n node.warn(\"No WaterHeating parameters found\");\n return null;\n}\n\n// Active (0/1)\nlet active = waterTopic?.Active?.value ?? null;\n\n// Mode (enum index)\nlet mode = waterTopic?.Mode?.value ?? null;\n\nmsg.payload = {\n mode: mode,\n active: active,\n};\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 590, "y": 240, "wires": [ [ "d8915ec0694bd178" ] ] }, { "id": "aa853766bbe69145", "type": "delay", "z": "725a74874dcafaeb", "name": "", "pauseType": "rate", "timeout": "5", "timeoutUnits": "seconds", "rate": "1", "nbRateUnits": "5", "rateUnits": "minute", "randomFirst": "1", "randomLast": "5", "randomUnits": "seconds", "drop": true, "allowrate": false, "outputs": 1, "x": 280, "y": 440, "wires": [ [ "c3b08145f5706211" ] ] }, { "id": "94b240e6583d269f", "type": "truma-inetx-set", "z": "725a74874dcafaeb", "name": "Truma set", "device": "1a318074fbabf5dd", "topic": "WaterHeating", "parameter": "Active", "value": "", "x": 1140, "y": 500, "wires": [ [] ] }, { "id": "d615a66145a3b4d0", "type": "debug", "z": "725a74874dcafaeb", "name": "Mode/Active", "active": true, "tosidebar": false, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "payload", "statusType": "auto", "x": 810, "y": 320, "wires": [] }, { "id": "f8b921c1c53fd7df", "type": "function", "z": "725a74874dcafaeb", "name": "Extract Energy", "func": "// === Electricity + Diesel Extractor ===\nlet energyTopic = msg.payload?.topics?.EnergySrc?.parameters;\nlet dieselTopic = msg.payload?.topics?.EnergySrc?.parameters;\n\nif (!energyTopic && !dieselTopic) {\n node.warn(\"No EnergySrc or Diesel heating parameters found\");\n return null;\n}\n\n// === Electricity ===\nlet electricLevel = energyTopic?.ElectricLevel?.value ?? null;\n\n// === Diesel / Gas ===\nlet dieselLevel = energyTopic?.DieselLevel?.value ?? null;\n\n// Output\nmsg.payload = {\n electricity: electricLevel,\n diesel: dieselLevel,\n};\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 560, "y": 320, "wires": [ [ "d615a66145a3b4d0" ] ] }, { "id": "7d642502bb81da94", "type": "function", "z": "725a74874dcafaeb", "name": "Mode 2", "func": "msg.payload = {\n topic: \"WaterHeating\",\n parameter: \"Mode\",\n value: 2\n};\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 920, "y": 440, "wires": [ [ "94b240e6583d269f" ] ] }, { "id": "c3b08145f5706211", "type": "function", "z": "725a74874dcafaeb", "name": "Boiler logic", "func": "const boilerAuto = flow.get(\"boilerauto\");\n\n// STOP if auto mode is OFF\nif (!boilerAuto) {\n node.status({\n fill: \"yellow\",\n shape: \"ring\",\n text: \"Boiler Auto OFF\"\n });\n return null;\n}\n\n// Parse SOC safely\nconst soc = flow.get(\"soc\");\n\nif (isNaN(soc)) {\n node.error(\"Invalid SOC value: \" + JSON.stringify(msg.payload));\n return null;\n}\n\n// 🔋 NEW: average surplus constraint\nconst avgSurplus = flow.get(\"avgSurplusCurrent\") || 0;\n\n// Persist heating state\nlet heatingEnabled = context.get(\"heatingEnabled\") || false;\n\n// Hysteresis + surplus rule\nif (soc >= 100 && avgSurplus > 10) {\n heatingEnabled = true;\n msg.payload = \"on\";\n} \nelse if (soc <= 95) {\n heatingEnabled = false;\n msg.payload = \"off\";\n}\n// else: keep previous state\nelse {\n msg.payload = heatingEnabled ? \"on\" : \"off\";\n}\n\n// Save state\ncontext.set(\"heatingEnabled\", heatingEnabled);\n\n// Status display\nnode.status({\n fill: heatingEnabled ? \"green\" : \"grey\",\n shape: \"dot\",\n text: `SOC ${soc}% | Avg ${avgSurplus.toFixed(1)}A | ${heatingEnabled ? \"Heating ON\" : \"OFF\"}`\n});\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 530, "y": 440, "wires": [ [ "d708652220f45fa8", "929cd04eed419f17" ] ] }, { "id": "a396956bd04b989d", "type": "function", "z": "725a74874dcafaeb", "name": "ElectricLevel 2", "func": "msg.payload = {\n topic: \"EnergySrc\",\n parameter: \"ElectricLevel\",\n value: 1\n};\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 940, "y": 480, "wires": [ [ "94b240e6583d269f" ] ] }, { "id": "4a9b671ae4ed5234", "type": "function", "z": "725a74874dcafaeb", "name": "DieselLevel 0", "func": "msg.payload = {\n topic: \"EnergySrc\",\n parameter: \"DieselLevel\",\n value: 0\n};\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 940, "y": 520, "wires": [ [ "94b240e6583d269f" ] ] }, { "id": "d708652220f45fa8", "type": "switch", "z": "725a74874dcafaeb", "name": "on", "property": "payload", "propertyType": "msg", "rules": [ { "t": "eq", "v": "on", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 710, "y": 440, "wires": [ [ "7d642502bb81da94", "a396956bd04b989d", "4a9b671ae4ed5234", "bae0d3ebf7d1fa4c", "0f8a91064cb7d0d6" ] ] }, { "id": "929cd04eed419f17", "type": "switch", "z": "725a74874dcafaeb", "name": "off", "property": "payload", "propertyType": "msg", "rules": [ { "t": "eq", "v": "off", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 710, "y": 680, "wires": [ [ "9a5af30e2c8e768a" ] ] }, { "id": "bae0d3ebf7d1fa4c", "type": "function", "z": "725a74874dcafaeb", "name": "Active 1", "func": "msg.payload = {\n topic: \"WaterHeating\",\n parameter: \"Active\",\n value: 1\n};\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 920, "y": 560, "wires": [ [ "94b240e6583d269f" ] ] }, { "id": "9a5af30e2c8e768a", "type": "function", "z": "725a74874dcafaeb", "name": "Active 0", "func": "msg.payload = {\n topic: \"WaterHeating\",\n parameter: \"Active\",\n value: 0\n};\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 920, "y": 680, "wires": [ [ "94b240e6583d269f" ] ] }, { "id": "0f8a91064cb7d0d6", "type": "function", "z": "725a74874dcafaeb", "name": "Inverter on", "func": "return { payload: 3 };", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 930, "y": 600, "wires": [ [ "b4ec10ca108df01f" ] ] }, { "id": "b4ec10ca108df01f", "type": "victron-output-vebus", "z": "725a74874dcafaeb", "service": "com.victronenergy.vebus/276", "path": "/Mode", "serviceObj": { "service": "com.victronenergy.vebus/276", "name": "MultiPlus-II 12/3000/120-32", "communityTag": "vebus" }, "pathObj": { "path": "/Mode", "type": "enum", "name": "Switch Position", "enum": { "1": "Charger Only", "2": "Inverter Only", "3": "On", "4": "Off" }, "mode": "both" }, "name": "Set Inverter ON", "onlyChanges": true, "conditionalMode": false, "condition1HysteresisEnabled": false, "condition1HysteresisThreshold": "", "outputTrue": "", "outputFalse": "", "debounce": "", "x": 1120, "y": 600, "wires": [] }, { "id": "truma_reconnect_exec", "type": "exec", "z": "725a74874dcafaeb", "command": "bluetoothctl connect 2C:A7:74:1C:AB:64", "addpay": false, "append": "", "timer": "", "winHide": false, "name": "", "x": 300, "y": 780, "wires": [ [ "d0dbcebe59d821a1" ], [ "16c8b6a53d8fa1c6" ], [] ] }, { "id": "d0dbcebe59d821a1", "type": "debug", "z": "725a74874dcafaeb", "name": "debug 1", "active": true, "tosidebar": true, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "payload", "statusType": "auto", "x": 640, "y": 740, "wires": [] }, { "id": "16c8b6a53d8fa1c6", "type": "debug", "z": "725a74874dcafaeb", "name": "debug 2", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 640, "y": 800, "wires": [] }, { "id": "e8ec231d030d4bf9", "type": "victron-virtual-switch", "z": "725a74874dcafaeb", "name": "Boiler auto", "outputs": 2, "switch_1_type": 1, "switch_1_initial": 0, "switch_1_label": "", "switch_1_customname": "Auto", "switch_1_group": "Boiler", "switch_1_include_measurement": false, "switch_1_rgb_color_wheel": false, "switch_1_cct_wheel": false, "switch_1_rgb_white_dimmer": false, "switch_1_show_ui_input": 1, "switch_1_passthrough_mode": "auto_only", "x": 110, "y": 540, "wires": [ [], [ "1aeac71a211912c2" ] ] }, { "id": "1aeac71a211912c2", "type": "function", "z": "725a74874dcafaeb", "name": "BoilerAuto", "func": "flow.set('boilerauto', msg.payload);\nreturn null;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 290, "y": 540, "wires": [ [ "c3b08145f5706211" ] ] }, { "id": "surplus_store", "type": "function", "z": "725a74874dcafaeb", "name": "Average surplus A (95-100%)", "func": "const soc = flow.get(\"soc\");\nlet current = msg.payload;\n\nlet samples = flow.get('surplusSamples') || [];\nlet avgBefore = flow.get('avgSurplusCurrent') || 0;\n\n// Only learn in top SOC window\nif (soc >= 95 && soc <= 100 && current > 0) {\n\n samples.push(current);\n\n // keep last 20 samples\n if (samples.length > 20) samples.shift();\n\n flow.set('surplusSamples', samples);\n\n let avg = samples.reduce((a, b) => a + b, 0) / samples.length;\n flow.set('avgSurplusCurrent', avg);\n\n // 🔍 LOG: learning mode active\n node.status({\n fill: \"green\",\n shape: \"dot\",\n text: `LEARN SOC ${soc}% I=${current.toFixed(1)}A AVG=${avg.toFixed(1)}A`\n });\n\n node.log(\n `🔋 Learning surplus | SOC=${soc}% | Current=${current}A | Samples=${samples.length} | Avg=${avg.toFixed(2)}A`\n );\n\n} else {\n\n // 🔍 LOG: not learning\n node.status({\n fill: \"grey\",\n shape: \"ring\",\n text: `IDLE SOC ${soc}% I=${current}A`\n });\n\n node.log(\n `⏸ Not learning | SOC=${soc}% | Current=${current}A | AvgBefore=${avgBefore.toFixed(2)}A`\n );\n}\n\nmsg.payload = {\n avgSurplusCurrent: flow.get('avgSurplusCurrent') || 0\n};\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 600, "y": 900, "wires": [ [ "c3b08145f5706211" ] ] }, { "id": "d6b1553d2a10dd13", "type": "victron-input-battery", "z": "725a74874dcafaeb", "service": "com.victronenergy.battery/278", "path": "/Dc/0/Current", "serviceObj": { "service": "com.victronenergy.battery/278", "name": "SmartShunt Dumbo", "communityTag": "battery" }, "pathObj": { "path": "/Dc/0/Current", "type": "float", "name": "Battery current (A)" }, "name": "Battery current A", "onlyChanges": false, "roundValues": "1", "rateLimit": "0.2", "conditionalMode": false, "condition1HysteresisEnabled": false, "condition1HysteresisThreshold": "", "outputTrue": "", "outputFalse": "", "debounce": "", "x": 120, "y": 900, "wires": [ [ "ed9a250ee2404a91" ] ] }, { "id": "82d3bc0f679e7b16", "type": "function", "z": "725a74874dcafaeb", "name": "SOC conversion", "func": "flow.set('soc', msg.payload);\nreturn null;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 120, "y": 840, "wires": [ [ "ed9a250ee2404a91" ] ] }, { "id": "ed9a250ee2404a91", "type": "delay", "z": "725a74874dcafaeb", "name": "", "pauseType": "rate", "timeout": "5", "timeoutUnits": "seconds", "rate": "1", "nbRateUnits": "1", "rateUnits": "minute", "randomFirst": "1", "randomLast": "5", "randomUnits": "seconds", "drop": true, "allowrate": false, "outputs": 1, "x": 350, "y": 900, "wires": [ [ "surplus_store" ] ] }, { "id": "1a318074fbabf5dd", "type": "truma-inetx-device", "z": "725a74874dcafaeb", "name": "Dumbo", "bluetooth": "bluez", "targetMode": "device", "deviceName": "Truma iNetX-1CAB64", "deviceAddress": "2C:A7:74:1C:AB:64" }, { "id": "d85b2ed07dbc8869", "type": "global-config", "env": [], "modules": { "node-red-contrib-truma-inetx": "1.0.2", "@victronenergy/node-red-contrib-victron": "1.7.13" } } ]