[ { "id": "e3f4e1a786badc85", "type": "tab", "label": "Flow 2", "disabled": false, "info": "", "env": [] }, { "id": "ccd0e2d29102ae4e", "type": "http request", "z": "e3f4e1a786badc85", "name": "HTTP: Get EOS plan (optional)", "method": "GET", "ret": "obj", "paytoqs": "ignore", "url": "http://192.168.10.110:8503/v1/energy-management/plan", "tls": "", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, "headers": [], "x": 430, "y": 420, "wires": [ [ "95c8766e487a7a69", "5da6b615085cd648" ] ] }, { "id": "95c8766e487a7a69", "type": "debug", "z": "e3f4e1a786badc85", "name": "Plan in (short)", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload.id", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 680, "y": 380, "wires": [] }, { "id": "5da6b615085cd648", "type": "function", "z": "e3f4e1a786badc85", "name": "SCHEDULER: plan -> battery1 timers (reset old on new plan)", "func": "// Schedules battery1 instructions. On each new plan, cancels all old timers.\n// Input: msg.payload = EOS plan JSON.\n// Output: emits messages at execution_time with msg.payload = instruction.\n\nconst plan = msg.payload;\nnode.status({fill:\"blue\",shape:\"dot\",text:\"plan received\"});\n\n// store & clear old timers\nconst timers = context.get('timers') || [];\nfor (const t of timers) {\n try { clearTimeout(t); } catch(e) {}\n}\ncontext.set('timers', []);\n\n// basic validation\nif (!plan || !Array.isArray(plan.instructions)) {\n node.status({fill:\"red\",shape:\"ring\",text:\"no instructions\"});\n return null;\n}\n\n// filter battery1, sort by execution_time\nconst instr = plan.instructions\n .filter(i => i && i.actuator_id === 'battery1')\n .sort((a,b) => new Date(a.execution_time) - new Date(b.execution_time));\n\nif (instr.length === 0) {\n node.status({fill:\"yellow\",shape:\"ring\",text:\"no battery1 instructions\"});\n return null;\n}\n\nconst now = Date.now();\nconst newTimers = [];\n\n// optional: tag plan id\nconst planId = plan.id || 'plan';\n\nfor (const i of instr) {\n const tExec = new Date(i.execution_time).getTime();\n const delay = Math.max(0, tExec - now);\n\n const handle = setTimeout(() => {\n node.send({\n topic: 'battery1',\n plan_id: planId,\n payload: i\n });\n }, delay);\n\n newTimers.push(handle);\n}\n\ncontext.set('timers', newTimers);\nnode.status({fill:\"green\",shape:\"dot\",text:`scheduled ${instr.length} instr`});\n\n// nächster aktiver Modus = erste Instruktion\nconst nextMode = instr[0]?.operation_mode_id || null;\n\n// also emit a small info message immediately (optional)\nnode.send({\n topic: \"scheduler_info\",\n payload: {\n plan_id: planId,\n scheduled: instr.length,\n active_operation_mode: nextMode\n }\n});\n\nreturn null;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "// on redeploy, clear timers\nconst timers = context.get('timers') || [];\nfor (const t of timers) {\n try { clearTimeout(t); } catch(e) {}\n}\ncontext.set('timers', []);\n", "libs": [], "x": 340, "y": 500, "wires": [ [ "88f0750b70cd1add", "7c5cda32f2e2663d" ] ] }, { "id": "88f0750b70cd1add", "type": "switch", "z": "e3f4e1a786badc85", "name": "route info vs instruction", "property": "topic", "propertyType": "msg", "rules": [ { "t": "eq", "v": "scheduler_info", "vt": "str" }, { "t": "else" } ], "checkall": "true", "repair": false, "outputs": 2, "x": 730, "y": 500, "wires": [ [ "c7ea7e062ac13483" ], [ "ac3450c4107e9047" ] ] }, { "id": "c7ea7e062ac13483", "type": "debug", "z": "e3f4e1a786badc85", "name": "Scheduler info", "active": false, "tosidebar": true, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "payload", "statusType": "auto", "x": 1000, "y": 420, "wires": [] }, { "id": "ac3450c4107e9047", "type": "function", "z": "e3f4e1a786badc85", "name": "MAP: FRBCInstruction -> CGwacs writes", "func": "const i = msg.payload;\nif (!i || !i.operation_mode_id) return null;\n\nconst mode = i.operation_mode_id;\n\n// factor kann als String kommen\nconst fRaw = i.operation_mode_factor;\nlet factor = (typeof fRaw === \"number\") ? fRaw : parseFloat(String(fRaw));\nif (!isFinite(factor)) factor = 1;\nfactor = Math.max(0, Math.min(1, factor)); // clamp 0..1\n\n// Victron: MaxCharge/MaxDischarge als ON/OFF (0/100)\nconst onOff = (factor > 0) ? 100 : 0;\n\n// === KONFIG ===\nconst maxPowerTotalW = 21000; // 3 x 7000W\nconst gridSetpoint = 50; // ✅ leichter Import-Bias\n\nconst setW = Math.round(maxPowerTotalW * factor);\n\n// Mode 2: +W Import, -W Export\nconst importSetpoint = +setW + gridSetpoint;\nconst exportSetpoint = -setW + gridSetpoint;\n\nlet maxChargePct = null;\nlet maxDischargePct = null;\nlet maxFeedInPower = null;\n\nconst prev = flow.get(\"gridSupport2\") || { active: false, mode: null };\nlet wasNonExport = context.get(\"wasNonExport\") || false;\n\nfunction setGridSupport2(active, setpointW, modeStr) {\n flow.set(\"gridSupport2\", {\n active,\n mode: modeStr || null,\n factor,\n setpointW: Number(setpointW) || gridSetpoint,\n ts: Date.now(),\n biasW: gridSetpoint\n });\n}\n\nfunction setNeutralState() {\n // kein GridSupport, aber wir wollen Bias aktiv halten\n setGridSupport2(false, gridSetpoint, mode);\n}\n\nswitch (mode) {\n case \"SELF_CONSUMPTION\":\n maxChargePct = onOff;\n maxDischargePct = onOff;\n if (wasNonExport) maxFeedInPower = -1;\n wasNonExport = false;\n\n setNeutralState();\n break;\n\n case \"NON_EXPORT\":\n maxChargePct = onOff;\n maxDischargePct = 0; // Batterie darf NICHT entladen\n maxFeedInPower = 0; // keine Einspeisung\n wasNonExport = true;\n\n setNeutralState();\n break;\n \n\n case \"FORCED_CHARGE\":\n maxChargePct = 100;\n maxDischargePct = 0;\n if (wasNonExport) { maxFeedInPower = -1; wasNonExport = false; }\n // Wenn factor == 0: kein Grid-Laden -> neutral/bias\n if (factor > 0) {\n setGridSupport2(true, importSetpoint, mode); // <-- nutzt setW = maxPowerTotalW * factor\n } else {\n setNeutralState();\n }\n break;\n\n\n case \"IDLE\":\n maxChargePct = 0;\n maxDischargePct = 0;\n if (wasNonExport) maxFeedInPower = -1;\n wasNonExport = false;\n \n setNeutralState();\n break;\n\n case \"GRID_SUPPORT_IMPORT\":\n maxChargePct = 100;\n maxDischargePct = 0;\n if (wasNonExport) { maxFeedInPower = -1; wasNonExport = false; }\n\n setGridSupport2(true, importSetpoint, mode);\n break;\n\n case \"GRID_SUPPORT_EXPORT\":\n maxChargePct = 0;\n maxDischargePct = 100;\n maxFeedInPower = -1;\n wasNonExport = false;\n\n setGridSupport2(true, exportSetpoint, mode);\n break;\n\n default:\n return null;\n}\n\ncontext.set(\"wasNonExport\", wasNonExport);\n\n// Wenn wir GridSupport verlassen: sofort auf Bias setzen (nicht auf 0)\nconst leavingGridSupport =\n (prev.active === true) &&\n (mode !== \"GRID_SUPPORT_IMPORT\" && mode !== \"GRID_SUPPORT_EXPORT\");\n\nconst msgs = [];\nfunction add(topic, value) {\n if (value === null || value === undefined) return;\n msgs.push({\n topic,\n payload: value,\n instruction_time: i.execution_time,\n operation_mode_id: mode,\n operation_mode_factor: factor,\n plan_id: msg.plan_id\n });\n}\n\nadd(\"MaxChargePercentage\", maxChargePct);\nadd(\"MaxDischargePercentage\", maxDischargePct);\nadd(\"MaxFeedInPower\", maxFeedInPower);\n\nif (leavingGridSupport) {\n add(\"AcPowerSetPoint\", gridSetpoint); // ✅ statt 0\n}\n\nreturn [msgs];\n", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 720, "y": 580, "wires": [ [ "87a19f89e12e51b2", "0e053587e68dad00", "3866c4314c45bf6c" ] ] }, { "id": "87a19f89e12e51b2", "type": "switch", "z": "e3f4e1a786badc85", "name": "Which setting?", "property": "topic", "propertyType": "msg", "rules": [ { "t": "eq", "v": "MaxChargePercentage", "vt": "str" }, { "t": "eq", "v": "MaxDischargePercentage", "vt": "str" }, { "t": "eq", "v": "MaxFeedInPower", "vt": "str" }, { "t": "eq", "v": "AcPowerSetPoint", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 4, "x": 1040, "y": 600, "wires": [ [ "f45950ac50ab0a77" ], [ "c12fcc2298741353" ], [ "f75dbaaadcf711b6" ], [ "680dfbb1de412366" ] ] }, { "id": "3356e8e0ff7a9c7e", "type": "inject", "z": "e3f4e1a786badc85", "name": "", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "*/15 0-23 * * *", "once": true, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 190, "y": 420, "wires": [ [ "ccd0e2d29102ae4e" ] ] }, { "id": "f45950ac50ab0a77", "type": "victron-output-custom", "z": "e3f4e1a786badc85", "service": "com.victronenergy.settings", "path": "/Settings/CGwacs/MaxChargePercentage", "serviceObj": { "service": "com.victronenergy.settings", "name": "com.victronenergy.settings" }, "pathObj": { "path": "/Settings/CGwacs/MaxChargePercentage", "name": "/Settings/CGwacs/MaxChargePercentage", "type": "number", "value": 100 }, "name": "", "onlyChanges": false, "x": 1450, "y": 520, "wires": [] }, { "id": "c12fcc2298741353", "type": "victron-output-custom", "z": "e3f4e1a786badc85", "service": "com.victronenergy.settings", "path": "/Settings/CGwacs/MaxDischargePercentage", "serviceObj": { "service": "com.victronenergy.settings", "name": "com.victronenergy.settings" }, "pathObj": { "path": "/Settings/CGwacs/MaxDischargePercentage", "name": "/Settings/CGwacs/MaxDischargePercentage", "type": "number", "value": 100 }, "name": "", "onlyChanges": false, "x": 1460, "y": 580, "wires": [] }, { "id": "f75dbaaadcf711b6", "type": "victron-output-custom", "z": "e3f4e1a786badc85", "service": "com.victronenergy.settings", "path": "/Settings/CGwacs/MaxFeedInPower", "serviceObj": { "service": "com.victronenergy.settings", "name": "com.victronenergy.settings" }, "pathObj": { "path": "/Settings/CGwacs/MaxFeedInPower", "name": "/Settings/CGwacs/MaxFeedInPower", "type": "number", "value": -1 }, "name": "", "onlyChanges": false, "x": 1430, "y": 640, "wires": [] }, { "id": "0e053587e68dad00", "type": "debug", "z": "e3f4e1a786badc85", "name": "debug 130", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 1030, "y": 500, "wires": [] }, { "id": "68d4b6b2f0e9954d", "type": "victron-output-custom", "z": "e3f4e1a786badc85", "service": "com.victronenergy.settings", "path": "/Settings/CGwacs/PreventFeedback", "serviceObj": { "service": "com.victronenergy.settings", "name": "com.victronenergy.settings" }, "pathObj": { "path": "/Settings/CGwacs/PreventFeedback", "name": "/Settings/CGwacs/PreventFeedback", "type": "number", "value": 1 }, "name": "", "onlyChanges": false, "x": 1070, "y": 140, "wires": [] }, { "id": "f2672d08f7c5aa72", "type": "inject", "z": "e3f4e1a786badc85", "name": "", "props": [ { "p": "payload" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "1", "payloadType": "num", "x": 730, "y": 120, "wires": [ [ "68d4b6b2f0e9954d" ] ] }, { "id": "2bb7ce23fe959ee7", "type": "inject", "z": "e3f4e1a786badc85", "name": "", "props": [ { "p": "payload" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "0", "payloadType": "num", "x": 730, "y": 160, "wires": [ [ "68d4b6b2f0e9954d" ] ] }, { "id": "bfa3a00887a81b8c", "type": "comment", "z": "e3f4e1a786badc85", "name": "Prevent AC-Feed-in into Grid", "info": "", "x": 780, "y": 60, "wires": [] }, { "id": "919b19aec1118896", "type": "comment", "z": "e3f4e1a786badc85", "name": "Schaltung für EOS control", "info": "", "x": 230, "y": 60, "wires": [] }, { "id": "1f628572c90eda2f", "type": "inject", "z": "e3f4e1a786badc85", "name": "", "props": [ { "p": "payload" } ], "repeat": "60", "crontab": "", "once": true, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 570, "y": 640, "wires": [ [ "3866c4314c45bf6c" ] ] }, { "id": "3866c4314c45bf6c", "type": "function", "z": "e3f4e1a786badc85", "name": "Watchdog renew", "func": "const state = flow.get(\"gridSupport2\");\nconst MAX_AGE_MS = 10 * 60 * 1000;\n\nlet w = 30; // default bias fallback\n\nif (state) {\n const age = Date.now() - (state.ts || 0);\n if (age <= MAX_AGE_MS) {\n // wenn active -> setpointW, sonst biasW\n w = state.active ? (Number(state.setpointW) || 0) : (Number(state.biasW) || 30);\n }\n}\n\nreturn { topic: \"AcPowerSetPoint\", payload: w };\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 790, "y": 640, "wires": [ [ "4876faa569763ff8", "87a19f89e12e51b2" ] ] }, { "id": "b0814610d28adb91", "type": "inject", "z": "e3f4e1a786badc85", "name": "Testplan IDLE", "props": [ { "p": "payload" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "{\"id\":\"plan-genetic@2025-12-27T15:50:39.873619+00:00\",\"generated_at\": \"2025-12-27T15:50:39.873642Z\",\"valid_from\":\"2025-12-27T15:00:00Z\",\"valid_until\": null,\"instructions\": [{\"id\": \"battery1@a4f850f4-08db-453c-8bb3-dcd0572a41c5\",\"execution_time\": \"2025-12-27T15:00:00Z\",\"abnormal_condition\": false,\"type\": \"FRBCInstruction\",\"actuator_id\": \"battery1\",\"operation_mode_id\": \"IDLE\",\"operation_mode_factor\": 0.3,\"resource_id\": \"battery1\"}],\"comment\": \"Energy management plan derived from GeneticSolution.\" }", "payloadType": "json", "x": 190, "y": 260, "wires": [ [ "5da6b615085cd648" ] ] }, { "id": "4876faa569763ff8", "type": "debug", "z": "e3f4e1a786badc85", "name": "debug 131", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 950, "y": 720, "wires": [] }, { "id": "bfad066bdbee7504", "type": "inject", "z": "e3f4e1a786badc85", "name": "Testplan SELF_CONSUMPTION", "props": [ { "p": "payload" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "{\"id\":\"plan-genetic@2025-12-27T15:50:39.873619+00:00\",\"generated_at\": \"2025-12-27T15:50:39.873642Z\",\"valid_from\":\"2025-12-27T15:00:00Z\",\"valid_until\": null,\"instructions\": [{\"id\": \"battery1@a4f850f4-08db-453c-8bb3-dcd0572a41c5\",\"execution_time\": \"2025-12-27T15:00:00Z\",\"abnormal_condition\": false,\"type\": \"FRBCInstruction\",\"actuator_id\": \"battery1\",\"operation_mode_id\": \"SELF_CONSUMPTION\",\"operation_mode_factor\": 0.3,\"resource_id\": \"battery1\"}],\"comment\": \"Energy management plan derived from GeneticSolution.\" }", "payloadType": "json", "x": 250, "y": 220, "wires": [ [ "5da6b615085cd648" ] ] }, { "id": "9a7220af11a9fbc0", "type": "inject", "z": "e3f4e1a786badc85", "name": "Testplan NON_EXPORT", "props": [ { "p": "payload" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "{\"id\":\"plan-genetic@2025-12-27T15:50:39.873619+00:00\",\"generated_at\": \"2025-12-27T15:50:39.873642Z\",\"valid_from\":\"2025-12-27T15:00:00Z\",\"valid_until\": null,\"instructions\": [{\"id\": \"battery1@a4f850f4-08db-453c-8bb3-dcd0572a41c5\",\"execution_time\": \"2025-12-27T15:00:00Z\",\"abnormal_condition\": false,\"type\": \"FRBCInstruction\",\"actuator_id\": \"battery1\",\"operation_mode_id\": \"NON_EXPORT\",\"operation_mode_factor\": 0.75,\"resource_id\": \"battery1\"}],\"comment\": \"Energy management plan derived from GeneticSolution.\" }", "payloadType": "json", "x": 230, "y": 180, "wires": [ [ "5da6b615085cd648" ] ] }, { "id": "36a6d65542560598", "type": "inject", "z": "e3f4e1a786badc85", "name": "Testplan GRID_SUPPORT_IMPORT", "props": [ { "p": "payload" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "{\"id\":\"plan-genetic@2025-12-27T15:50:39.873619+00:00\",\"generated_at\": \"2025-12-27T15:50:39.873642Z\",\"valid_from\":\"2025-12-27T15:00:00Z\",\"valid_until\": null,\"instructions\": [{\"id\": \"battery1@a4f850f4-08db-453c-8bb3-dcd0572a41c5\",\"execution_time\": \"2025-12-27T15:00:00Z\",\"abnormal_condition\": false,\"type\": \"FRBCInstruction\",\"actuator_id\": \"battery1\",\"operation_mode_id\": \"GRID_SUPPORT_IMPORT\",\"operation_mode_factor\": 0.5,\"resource_id\": \"battery1\"}],\"comment\": \"Energy management plan derived from GeneticSolution.\" }", "payloadType": "json", "x": 260, "y": 100, "wires": [ [ "5da6b615085cd648" ] ] }, { "id": "680dfbb1de412366", "type": "victron-output-custom", "z": "e3f4e1a786badc85", "service": "com.victronenergy.settings", "path": "/Settings/CGwacs/AcPowerSetPoint", "serviceObj": { "service": "com.victronenergy.settings", "name": "com.victronenergy.settings" }, "pathObj": { "path": "/Settings/CGwacs/AcPowerSetPoint", "name": "/Settings/CGwacs/AcPowerSetPoint", "type": "number", "value": 50 }, "name": "", "onlyChanges": false, "x": 1430, "y": 720, "wires": [] }, { "id": "9313491f89c8bb77", "type": "function", "z": "e3f4e1a786badc85", "name": "EOS: build PUT /v1/resource/status request", "func": "// Erwartet:\n// msg.resource_id (required)\n// msg.actuator_id (optional)\n// msg.payload = status object\n// Baut msg.method, msg.url, msg.headers.\n\nconst rid = msg.resource_id;\nif (!rid) {\n node.error('resource_id fehlt', msg);\n return null;\n}\n\nconst aid = msg.actuator_id;\n\n// Query bauen\nlet url = `http://192.168.10.110:8503/v1/resource/status?resource_id=${encodeURIComponent(rid)}`;\nif (aid) url += `&actuator_id=${encodeURIComponent(aid)}`;\n\nmsg.method = 'PUT';\nmsg.url = url;\nmsg.headers = msg.headers || {};\nmsg.headers['content-type'] = 'application/json';\n\nreturn msg;", "outputs": 1, "noerr": 0, "x": 1250, "y": 880, "wires": [ [ "0e9aa89f452c5a03", "a37cec55bdcbdbbc" ] ] }, { "id": "0e9aa89f452c5a03", "type": "http request", "z": "e3f4e1a786badc85", "name": "HTTP: PUT EOS resource/status", "method": "use", "ret": "obj", "paytoqs": "ignore", "url": "", "tls": "", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, "headers": [], "x": 1280, "y": 980, "wires": [ [ "5cb6541c9a3ed262" ] ] }, { "id": "a37cec55bdcbdbbc", "type": "debug", "z": "e3f4e1a786badc85", "name": "EOS status PUT (request)", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "true", "targetType": "msg", "x": 1590, "y": 860, "wires": [] }, { "id": "5cb6541c9a3ed262", "type": "debug", "z": "e3f4e1a786badc85", "name": "EOS status PUT (response)", "active": true, "tosidebar": true, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "payload", "statusType": "auto", "x": 1620, "y": 980, "wires": [] }, { "id": "d0e6900a33be58c7", "type": "victron-input-battery", "z": "e3f4e1a786badc85", "service": "com.victronenergy.battery/512", "path": "/Soc", "serviceObj": { "service": "com.victronenergy.battery/512", "name": "Batterien" }, "pathObj": { "path": "/Soc", "type": "float", "name": "State of charge (%)" }, "name": "", "onlyChanges": true, "x": 220, "y": 900, "wires": [ [ "cc2e103f7dcc109f" ] ] }, { "id": "cc2e103f7dcc109f", "type": "function", "z": "e3f4e1a786badc85", "name": "function 43", "func": "msg.payload = msg.payload /100;\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 590, "y": 900, "wires": [ [ "4b813794257cd685" ] ] }, { "id": "c4a94dc1525ccabf", "type": "debug", "z": "e3f4e1a786badc85", "name": "debug 132", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 1350, "y": 840, "wires": [] }, { "id": "7c5cda32f2e2663d", "type": "function", "z": "e3f4e1a786badc85", "name": "EOS: instruction -> FRBCActuatorStatus", "func": "// Input: msg.payload = FRBCInstruction\n// Output: msg.resource_id/msg.actuator_id/msg.payload (FRBCActuatorStatus)\n\nfunction tsEOS(d) {\n const pad = (n) => String(n).padStart(2, \"0\");\n return (\n d.getFullYear() +\n pad(d.getMonth() + 1) +\n pad(d.getDate()) +\n \"T\" +\n pad(d.getHours()) + \":\" +\n pad(d.getMinutes()) + \":\" +\n pad(d.getSeconds())\n );\n}\n\nconst i = msg.payload;\nif (!i || !i.operation_mode_id) return null;\n\nlet factor = (typeof i.operation_mode_factor === \"number\")\n ? i.operation_mode_factor\n : parseFloat(String(i.operation_mode_factor));\n\nif (!isFinite(factor)) factor = 1;\nfactor = Math.max(0, Math.min(1, factor));\n\nconst prevMode = context.get(\"last_mode\") || i.operation_mode_id;\ncontext.set(\"last_mode\", i.operation_mode_id);\n\n// So wie du es jetzt willst:\nmsg.resource_id = \"FRBCActuatorStatus\";\n// actuator_id sinnvollerweise dein Aktor, bei dir battery1\nmsg.actuator_id = i.actuator_id || \"battery1\";\n\nmsg.payload = {\n type: \"FRBCActuatorStatus\",\n active_operation_mode_id: String(i.operation_mode_id),\n operation_mode_factor: String(factor),\n previous_operation_mode_id: String(prevMode),\n transistion_timestamp: tsEOS(new Date())\n};\n\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 860, "y": 860, "wires": [ [ "c4a94dc1525ccabf", "9313491f89c8bb77" ] ] }, { "id": "4b813794257cd685", "type": "function", "z": "e3f4e1a786badc85", "name": "EOS: SoC -> FRBCStorageStatus", "func": "// Input: msg.payload = SoC 0..1 (z.B. 0.88)\n// Output: msg.resource_id/msg.actuator_id/msg.payload (FRBCStorageStatus)\n\nlet soc = Number(msg.payload);\nif (!isFinite(soc)) return null;\n\nsoc = Math.max(0, Math.min(1, soc));\n\nmsg.resource_id = \"FRBCStorageStatus\";\nmsg.actuator_id = \"battery1\"; // oder aus msg.actuator_id übernehmen, wenn du willst\n\nmsg.payload = {\n type: \"FRBCStorageStatus\",\n present_fill_level: String(soc)\n};\n\nreturn msg;\n", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 880, "y": 900, "wires": [ [ "9313491f89c8bb77" ] ] }, { "id": "8ef8ab873c8340c9", "type": "victron-input-custom", "z": "e3f4e1a786badc85", "service": "com.victronenergy.battery/288", "path": "/Dc/0/Power", "serviceObj": { "service": "com.victronenergy.battery/288", "name": "SmartShunt 1000A/50mV (288)" }, "pathObj": { "path": "/Dc/0/Power", "name": "/Dc/0/Power", "type": "number", "value": 266.9849853515625 }, "name": "", "onlyChanges": false, "x": 270, "y": 800, "wires": [ [ "da46d454f11f4240" ] ] }, { "id": "b8ed9ea07d3349b4", "type": "function", "z": "e3f4e1a786badc85", "name": "EOS: BatteryPower -> PowerMeasurement", "func": "// Input: msg.payload = gemessener Leistungswert (Number oder String, z.B. 1000)\n// Output: msg.resource_id / msg.actuator_id / msg.payload (PowerMeasurement)\n\nfunction tsEOS(d) {\n const pad = (n) => String(n).padStart(2, \"0\");\n return (\n d.getFullYear() +\n pad(d.getMonth() + 1) +\n pad(d.getDate()) +\n \"T\" +\n pad(d.getHours()) + \":\" +\n pad(d.getMinutes()) + \":\" +\n pad(d.getSeconds())\n );\n}\n\n// Messwert einlesen\n// Input: msg.payload = gemessener Leistungswert (Number oder String, z.B. 1000)\n// Output: msg.resource_id / msg.actuator_id / msg.payload (PowerMeasurement)\n\nconst raw = Number(msg.payload);\nif (!isFinite(raw)) return null;\n\nmsg.resource_id = \"PowerMeasurement\";\nmsg.actuator_id = \"ELECTRIC.POWER.3_PHASE_SYM\";\n// optional: runden\nconst txt = `${Math.round(raw)} W`;\nnode.status({\n fill: \"green\",\n shape: \"dot\",\n text: txt\n});\nmsg.payload = {\n type: \"PowerMeasurement\",\n measurement_timestamp: new Date().toISOString(), // ✅ ISO-8601\n values: [\n {\n commodity_quantity: \"ELECTRIC.POWER.3_PHASE_SYM\",\n value: String(Math.round(raw))\n }\n ]\n};\n\nreturn msg;\n\n\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 890, "y": 800, "wires": [ [ "dd21c8186d69e319", "9313491f89c8bb77" ] ] }, { "id": "dd21c8186d69e319", "type": "debug", "z": "e3f4e1a786badc85", "name": "debug 133", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 1170, "y": 800, "wires": [] }, { "id": "da46d454f11f4240", "type": "delay", "z": "e3f4e1a786badc85", "name": "", "pauseType": "queue", "timeout": "1", "timeoutUnits": "minutes", "rate": "1", "nbRateUnits": "1", "rateUnits": "minute", "randomFirst": "1", "randomLast": "5", "randomUnits": "seconds", "drop": true, "allowrate": false, "outputs": 1, "x": 580, "y": 740, "wires": [ [ "b8ed9ea07d3349b4" ] ] }, { "id": "57250f47d6f7aa6f", "type": "inject", "z": "e3f4e1a786badc85", "name": "Testplan FOCED_CHARGE", "props": [ { "p": "payload" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "{\"id\":\"plan-genetic@2025-12-27T15:50:39.873619+00:00\",\"generated_at\": \"2025-12-27T15:50:39.873642Z\",\"valid_from\":\"2025-12-27T15:00:00Z\",\"valid_until\": null,\"instructions\": [{\"id\": \"battery1@a4f850f4-08db-453c-8bb3-dcd0572a41c5\",\"execution_time\": \"2025-12-27T15:00:00Z\",\"abnormal_condition\": false,\"type\": \"FRBCInstruction\",\"actuator_id\": \"battery1\",\"operation_mode_id\": \"FORCED_CHARGE\",\"operation_mode_factor\": 0.5,\"resource_id\": \"battery1\"}],\"comment\": \"Energy management plan derived from GeneticSolution.\" }", "payloadType": "json", "x": 240, "y": 140, "wires": [ [ "5da6b615085cd648" ] ] } ]