[
    {
        "id": "c1f51103fd352c53",
        "type": "tab",
        "label": "Titan SOC MPPT Control",
        "disabled": false,
        "info": ""
    },
    {
        "id": "acd4f1a905aecfb9",
        "type": "victron-input-battery",
        "z": "c1f51103fd352c53",
        "service": "com.victronenergy.battery/512",
        "path": "/Soc",
        "serviceObj": {
            "service": "com.victronenergy.battery/512",
            "name": "TITAN-Lithium",
            "communityTag": "battery"
        },
        "pathObj": {
            "path": "/Soc",
            "type": "float",
            "name": "State of charge (%)"
        },
        "initial": "",
        "name": "Titan SOC",
        "onlyChanges": true,
        "outputs": 1,
        "conditionalMode": false,
        "outputTrue": "",
        "outputFalse": "",
        "debounce": "",
        "x": 480,
        "y": 200,
        "wires": [
            [
                "1b83d2e56259c345",
                "5b41034820a6f894",
                "f5b2933a4c03b85e"
            ]
        ]
    },
    {
        "id": "1b83d2e56259c345",
        "type": "function",
        "z": "c1f51103fd352c53",
        "name": "MPPT mode + adjustable hysteresis",
        "func": "let mode = context.get(\"mode\") || \"auto\";\nlet soc = context.get(\"latest_soc\");\n\n// Dashboard/manual mode change.\nif (msg.topic === \"mode\") {\n    if (![\"auto\", \"on\", \"off\"].includes(msg.payload)) {\n        node.warn(`Invalid mode: ${msg.payload}`);\n        return null;\n    }\n\n    mode = msg.payload;\n    context.set(\"mode\", mode);\n}\n// Dashboard minimum-SOC change.\nelse if (msg.topic === \"min_soc\") {\n    const newMinSoc = Number(msg.payload);\n\n    if (!Number.isFinite(newMinSoc) || newMinSoc < 20 || newMinSoc >= 95) {\n        node.warn(`Invalid minimum SOC: ${msg.payload}`);\n        return null;\n    }\n\n    flow.set(\"min_soc\", newMinSoc);\n}\n// Anything else arriving here is a Titan SOC update.\nelse {\n    soc = Number(msg.payload);\n\n    if (!Number.isFinite(soc)) {\n        node.warn(`Invalid SOC: ${msg.payload}`);\n        return null;\n    }\n\n    context.set(\"latest_soc\", soc);\n}\n\nconst storedMinSoc = flow.get(\"min_soc\");\nconst minSoc = Number(storedMinSoc !== undefined ? storedMinSoc : 80);\nconst maxSoc = 95;\n\n// Manual modes do not need an SOC value.\nif (mode === \"on\") {\n    msg.payload = 1;\n    msg.mppt_enabled = true;\n    msg.control_mode = mode;\n    msg.soc = soc;\n    msg.min_soc = minSoc;\n\n    node.status({\n        fill: \"blue\",\n        shape: \"dot\",\n        text: `FORCE ON${Number.isFinite(soc) ? ` - ${soc.toFixed(1)}%` : \"\"}`\n    });\n\n    return msg;\n}\n\nif (mode === \"off\") {\n    msg.payload = 4;\n    msg.mppt_enabled = false;\n    msg.control_mode = mode;\n    msg.soc = soc;\n    msg.min_soc = minSoc;\n\n    node.status({\n        fill: \"red\",\n        shape: \"dot\",\n        text: `FORCE OFF${Number.isFinite(soc) ? ` - ${soc.toFixed(1)}%` : \"\"}`\n    });\n\n    return msg;\n}\n\n// AUTO mode requires a current SOC value. A threshold change will use the\n// last SOC received from the Titan, so changing the number acts immediately.\nif (!Number.isFinite(soc)) {\n    node.status({\n        fill: \"yellow\",\n        shape: \"ring\",\n        text: `AUTO - waiting for SOC - ${minSoc.toFixed(0)}/${maxSoc}%`\n    });\n    return null;\n}\n\nlet enabled = context.get(\"mppt_enabled\");\n\n// Deterministic behaviour after Node-RED/Cerbo restart:\n// <= minSoc       -> ON\n// >= maxSoc (95%) -> OFF\n// between them    -> retain previous state; if no previous state, OFF\nif (enabled === undefined) {\n    enabled = soc <= minSoc;\n}\n\nif (soc <= minSoc) {\n    enabled = true;\n} else if (soc >= maxSoc) {\n    enabled = false;\n}\n\ncontext.set(\"mppt_enabled\", enabled);\n\nmsg.soc = soc;\nmsg.min_soc = minSoc;\nmsg.max_soc = maxSoc;\nmsg.payload = enabled ? 1 : 4;\nmsg.mppt_enabled = enabled;\nmsg.control_mode = mode;\n\nnode.status({\n    fill: enabled ? \"green\" : \"grey\",\n    shape: \"dot\",\n    text: `AUTO ${minSoc.toFixed(0)}/${maxSoc}% - SOC ${soc.toFixed(1)}% - MPPT ${enabled ? \"ON\" : \"OFF\"}`\n});\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 760,
        "y": 280,
        "wires": [
            [
                "e7a2db7cdd6e0f83"
            ]
        ]
    },
    {
        "id": "e7a2db7cdd6e0f83",
        "type": "rbe",
        "z": "c1f51103fd352c53",
        "name": "Only on change",
        "func": "rbe",
        "gap": "",
        "start": "",
        "inout": "out",
        "septopics": true,
        "property": "payload",
        "topi": "topic",
        "x": 1030,
        "y": 280,
        "wires": [
            [
                "3fb84f7351cf5ab5",
                "7d358cf604955c50"
            ]
        ]
    },
    {
        "id": "3fb84f7351cf5ab5",
        "type": "victron-output-solarcharger",
        "z": "c1f51103fd352c53",
        "service": "com.victronenergy.solarcharger/279",
        "path": "/Mode",
        "serviceObj": {
            "service": "com.victronenergy.solarcharger/279",
            "name": "SmartSolar Charger MPPT 75/15 rev2",
            "communityTag": "solarcharger"
        },
        "pathObj": {
            "path": "/Mode",
            "type": "enum",
            "name": "Charger on/off",
            "enum": {
                "1": "On",
                "4": "Off"
            },
            "mode": "both"
        },
        "name": "SmartSolar on/off",
        "onlyChanges": true,
        "outputs": 0,
        "conditionalMode": false,
        "outputTrue": "",
        "outputFalse": "",
        "debounce": "",
        "x": 1270,
        "y": 280,
        "wires": []
    },
    {
        "id": "5b41034820a6f894",
        "type": "debug",
        "z": "c1f51103fd352c53",
        "name": "SOC",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 710,
        "y": 200,
        "wires": []
    },
    {
        "id": "7d358cf604955c50",
        "type": "debug",
        "z": "c1f51103fd352c53",
        "name": "MPPT command",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1270,
        "y": 360,
        "wires": []
    },
    {
        "id": "e733d87266e3a250",
        "type": "inject",
        "z": "c1f51103fd352c53",
        "name": "AUTO",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": "1",
        "topic": "mode",
        "payload": "auto",
        "payloadType": "str",
        "x": 290,
        "y": 320,
        "wires": [
            [
                "99bc1c5aba61d0b1"
            ]
        ]
    },
    {
        "id": "445c395d90a9c0b9",
        "type": "inject",
        "z": "c1f51103fd352c53",
        "name": "FORCE ON",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": "0.1",
        "topic": "mode",
        "payload": "on",
        "payloadType": "str",
        "x": 270,
        "y": 360,
        "wires": [
            [
                "99bc1c5aba61d0b1"
            ]
        ]
    },
    {
        "id": "083d371eda137ab1",
        "type": "inject",
        "z": "c1f51103fd352c53",
        "name": "FORCE OFF",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": "0.1",
        "topic": "mode",
        "payload": "off",
        "payloadType": "str",
        "x": 270,
        "y": 400,
        "wires": [
            [
                "99bc1c5aba61d0b1"
            ]
        ]
    },
    {
        "id": "99bc1c5aba61d0b1",
        "type": "ui-radio-group",
        "z": "c1f51103fd352c53",
        "group": "92af5d9650a426e6",
        "name": "Charge Control",
        "label": "Select Option:",
        "order": 2,
        "width": 0,
        "height": 0,
        "columns": 1,
        "passthru": true,
        "options": [
            {
                "label": "Auto",
                "value": "auto",
                "type": "str"
            },
            {
                "label": "Force On",
                "value": "on",
                "type": "str"
            },
            {
                "label": "Force Off",
                "value": "off",
                "type": "str"
            }
        ],
        "payload": "",
        "topic": "mode",
        "topicType": "str",
        "className": "",
        "x": 500,
        "y": 360,
        "wires": [
            [
                "1b83d2e56259c345"
            ]
        ]
    },
    {
        "id": "f5b2933a4c03b85e",
        "type": "ui-gauge",
        "z": "c1f51103fd352c53",
        "name": "SoC",
        "group": "92af5d9650a426e6",
        "order": 0,
        "value": "payload",
        "valueType": "msg",
        "width": 3,
        "height": 3,
        "gtype": "gauge-half",
        "gstyle": "needle",
        "title": "SoC",
        "alwaysShowTitle": false,
        "floatingTitlePosition": "top-left",
        "units": "%",
        "icon": "",
        "prefix": "",
        "suffix": "",
        "segments": [
            {
                "from": "10",
                "color": "#ea5353",
                "text": "",
                "textType": "label"
            },
            {
                "from": "40",
                "color": "#ffc800",
                "text": "",
                "textType": "label"
            },
            {
                "from": "85",
                "color": "#5cd65c",
                "text": "",
                "textType": "label"
            }
        ],
        "min": "10",
        "max": "100",
        "sizeThickness": 16,
        "sizeGap": 4,
        "sizeKeyThickness": 8,
        "styleRounded": true,
        "styleGlow": false,
        "className": "",
        "x": 710,
        "y": 140,
        "wires": [
            []
        ]
    },
    {
        "id": "85448f05ece38a95",
        "type": "ui-number-input",
        "z": "c1f51103fd352c53",
        "group": "92af5d9650a426e6",
        "name": "min_soc",
        "label": "Set minimum SoC",
        "order": 0,
        "width": 0,
        "height": 0,
        "topic": "min_soc",
        "topicType": "str",
        "min": "20",
        "max": "95",
        "step": 1,
        "tooltip": "Start charging when the battery falls below this value",
        "delay": 300,
        "passthru": true,
        "sendOnDelay": true,
        "sendOnBlur": true,
        "sendOnEnter": true,
        "className": "",
        "clearable": false,
        "icon": "",
        "iconPosition": "left",
        "iconInnerPosition": "inside",
        "spinner": "default",
        "x": 480,
        "y": 280,
        "wires": [
            [
                "1b83d2e56259c345"
            ]
        ]
    },
    {
        "id": "92af5d9650a426e6",
        "type": "ui-group",
        "name": "Lithium Charging",
        "page": "f37316d350e8e153",
        "width": 6,
        "height": 1,
        "order": 1,
        "showTitle": true,
        "className": "",
        "visible": "true",
        "disabled": "false",
        "groupType": "default"
    },
    {
        "id": "f37316d350e8e153",
        "type": "ui-page",
        "name": "Power",
        "ui": "4ad838c5adcc0af8",
        "path": "/power",
        "icon": "home",
        "layout": "grid",
        "theme": "ea5f7be13d6cc647",
        "breakpoints": [
            {
                "name": "Default",
                "px": "0",
                "cols": "3"
            },
            {
                "name": "Tablet",
                "px": "576",
                "cols": "6"
            },
            {
                "name": "Small Desktop",
                "px": "768",
                "cols": "9"
            },
            {
                "name": "Desktop",
                "px": "1024",
                "cols": "12"
            }
        ],
        "order": 1,
        "className": "",
        "visible": "true",
        "disabled": "false"
    },
    {
        "id": "4ad838c5adcc0af8",
        "type": "ui-base",
        "name": "Blue Opal",
        "path": "/dashboard",
        "appIcon": "",
        "includeClientData": true,
        "acceptsClientConfig": [
            "ui-notification",
            "ui-control"
        ],
        "showPathInSidebar": false,
        "headerContent": "page",
        "navigationStyle": "default",
        "titleBarStyle": "default",
        "showReconnectNotification": true,
        "notificationDisplayTime": 1,
        "showDisconnectNotification": true,
        "allowInstall": false
    },
    {
        "id": "ea5f7be13d6cc647",
        "type": "ui-theme",
        "name": "Default Theme",
        "colors": {
            "surface": "#fcfcfd",
            "primary": "#0d74ce",
            "bgPage": "#f9f9fb",
            "groupBg": "#ffffff",
            "groupOutline": "#d9d9e0"
        },
        "sizes": {
            "density": "default",
            "pagePadding": "12px",
            "groupGap": "12px",
            "groupBorderRadius": "4px",
            "widgetGap": "12px"
        }
    },
    {
        "id": "9e13a0ffce6be0d9",
        "type": "global-config",
        "env": [],
        "modules": {
            "@victronenergy/node-red-contrib-victron": "1.6.64",
            "@flowfuse/node-red-dashboard": "1.31.0"
        }
    }
]