[
  {
    "id": "mhb-universal-tab",
    "type": "tab",
    "label": "MHB Cerbo automatisch",
    "disabled": false,
    "info": "Universele MHB-flow. Meldt zichzelf aan met het lokale VRM Portal-ID. Opdrachten worden pas na koppeling in MHB Beheer vrijgegeven."
  },
  {
    "id": "mhb-universal-note",
    "type": "comment",
    "z": "mhb-universal-tab",
    "name": "Universeel v8 · lagere Cerbo-belasting · automatische VRM-herkenning",
    "info": "Geen klantgegevens of sleutel invullen. De unieke apparaatidentiteit wordt lokaal onder /data/home/nodered/.node-red bewaard.",
    "x": 470,
    "y": 40,
    "wires": []
  },
  {
    "id": "mhb-universal-poll",
    "type": "inject",
    "z": "mhb-universal-tab",
    "name": "Opdrachten iedere 3 seconden",
    "props": [
      {
        "p": "payload"
      }
    ],
    "repeat": "3",
    "once": true,
    "onceDelay": 1,
    "payload": "",
    "payloadType": "date",
    "x": 150,
    "y": 100,
    "wires": [
      [
        "mhb-universal-config"
      ]
    ]
  },
  {
    "id": "mhb-universal-config",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Beveiligde opdrachtverbinding",
    "func": "const PORTAL_ID=flow.get('mhb_portal_id'),TOKEN=flow.get('mhb_token');\nif(!PORTAL_ID||!TOKEN){node.status({fill:'yellow',shape:'ring',text:'wacht op koppeling'});return null;}\nmsg.method='GET';\nmsg.url='https://mhbbeheer.site/api/cerbo-node-red/'+PORTAL_ID+'/commands/next';\nmsg.headers={'X-MHB-Cerbo-Token':TOKEN};\nreturn msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 425,
    "y": 100,
    "wires": [
      [
        "mhb-universal-http-get"
      ]
    ]
  },
  {
    "id": "mhb-universal-http-get",
    "type": "http request",
    "z": "mhb-universal-tab",
    "name": "Opdracht ophalen",
    "method": "use",
    "ret": "txt",
    "paytoqs": "ignore",
    "url": "",
    "persist": false,
    "insecureHTTPParser": false,
    "authType": "",
    "senderr": false,
    "headers": [],
    "x": 680,
    "y": 100,
    "wires": [
      [
        "mhb-command-json-safe"
      ]
    ]
  },
  {
    "id": "mhb-command-json-safe",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Antwoord veilig lezen",
    "func": "if(Number(msg.statusCode)<200||Number(msg.statusCode)>=300){node.status({fill:'yellow',shape:'ring',text:'server tijdelijk niet bereikbaar'});return null;}try{msg.payload=typeof msg.payload==='string'?JSON.parse(msg.payload):msg.payload;node.status({fill:'green',shape:'dot',text:'verbonden'});return msg;}catch(e){node.status({fill:'yellow',shape:'ring',text:'ongeldig antwoord overgeslagen'});return null;}",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 905,
    "y": 100,
    "wires": [
      [
        "mhb-command-router"
      ]
    ]
  },
  {
    "id": "mhb-command-router",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "ESS-opdracht apart verwerken",
    "func": "const job=(msg.payload||{}).job;if(job&&job.type==='ess_settings')return [msg,null];return [null,msg];",
    "outputs": 2,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 1135,
    "y": 100,
    "wires": [
      [
        "mhb-ess-command-build"
      ],
      [
        "mhb-universal-dispatch"
      ]
    ]
  },
  {
    "id": "mhb-universal-dispatch",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Veilige Cerbo-opdracht",
    "func": "const job=(msg.payload||{}).job;if(!job)return [null,null,null,null];\nconst p=job.payload||{};\nif(job.type==='dvcc'){const c=p.current_enabled?Number(p.charge_current):-1,v=p.voltage_enabled?Number(p.charge_voltage):0;if((c!==-1&&(c<1||c>200))||(v!==0&&(v<40||v>64))){node.error('DVCC buiten veilige grens',msg);return [null,null,null,null];}flow.set('mhb_pending',{id:job.id,type:'dvcc',current:c,voltage:v});return [{payload:c},{payload:v},null,{payload:job.id}];}\nif(job.type==='power'||job.type==='reset'){const portal=flow.get('mhb_detected_portal'),instance=flow.get('mhb_vebus_instance');if(!portal||instance===undefined||instance===null){node.error('VE.Bus-apparaat nog niet automatisch herkend',msg);return [null,null,null,null];}const command=p.command||p.requested_state;let path,value,expected;if(command==='power_on'){path='Mode';value=3;expected=3;}else if(command==='power_off'){path='Mode';value=4;expected=4;}else if(command==='alarm_reset'){path='SystemReset';value=1;}else{node.error('Opdracht niet toegestaan',msg);return [null,null,null,null];}flow.set('mhb_pending',{id:job.id,type:job.type,command:command,expected_mode:expected});return [null,null,{topic:'W/'+portal+'/vebus/'+instance+'/'+path,payload:JSON.stringify({value:value}),qos:0,retain:false},{payload:job.id}];}\nnode.error('Opdrachttype niet toegestaan',msg);return [null,null,null,null];",
    "outputs": 4,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 935,
    "y": 100,
    "wires": [
      [
        "mhb-universal-out-current"
      ],
      [
        "mhb-universal-out-voltage"
      ],
      [
        "mhb-mqtt-command"
      ],
      [
        "mhb-universal-delay"
      ]
    ]
  },
  {
    "id": "mhb-universal-out-current",
    "type": "victron-output-ess",
    "z": "mhb-universal-tab",
    "service": "com.victronenergy.settings",
    "path": "/Settings/SystemSetup/MaxChargeCurrent",
    "serviceObj": {
      "service": "com.victronenergy.settings",
      "name": "Settings"
    },
    "pathObj": {
      "path": "/Settings/SystemSetup/MaxChargeCurrent",
      "type": "float",
      "name": "DVCC Charge current limit (A)",
      "mode": "both"
    },
    "initial": "",
    "name": "Schrijf DVCC-laadstroom",
    "x": 1240,
    "y": 75,
    "wires": []
  },
  {
    "id": "mhb-universal-out-voltage",
    "type": "victron-output-ess",
    "z": "mhb-universal-tab",
    "service": "com.victronenergy.settings",
    "path": "/Settings/SystemSetup/MaxChargeVoltage",
    "serviceObj": {
      "service": "com.victronenergy.settings",
      "name": "Settings"
    },
    "pathObj": {
      "path": "/Settings/SystemSetup/MaxChargeVoltage",
      "type": "float",
      "name": "DVCC Maximum charge voltage (V)",
      "mode": "both"
    },
    "initial": "",
    "name": "Schrijf DVCC-laadspanning",
    "x": 1245,
    "y": 125,
    "wires": []
  },
  {
    "id": "mhb-universal-read-current",
    "type": "victron-input-ess",
    "z": "mhb-universal-tab",
    "service": "com.victronenergy.settings",
    "path": "/Settings/SystemSetup/MaxChargeCurrent",
    "serviceObj": {
      "service": "com.victronenergy.settings",
      "name": "Settings"
    },
    "pathObj": {
      "path": "/Settings/SystemSetup/MaxChargeCurrent",
      "type": "float",
      "name": "DVCC Charge current limit (A)",
      "mode": "both"
    },
    "name": "Werkelijke laadstroom",
    "onlyChanges": true,
    "roundValues": "2",
    "rateLimit": 0,
    "conditionalMode": false,
    "outputOnChange": false,
    "debounce": 0,
    "x": 180,
    "y": 230,
    "wires": [
      [
        "mhb-universal-store-current"
      ]
    ]
  },
  {
    "id": "mhb-universal-store-current",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Onthoud stroom",
    "func": "flow.set('mhb_actual_current',Number(msg.payload));return msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 440,
    "y": 230,
    "wires": [
      [
        "mhb-universal-debug"
      ]
    ]
  },
  {
    "id": "mhb-universal-read-voltage",
    "type": "victron-input-ess",
    "z": "mhb-universal-tab",
    "service": "com.victronenergy.settings",
    "path": "/Settings/SystemSetup/MaxChargeVoltage",
    "serviceObj": {
      "service": "com.victronenergy.settings",
      "name": "Settings"
    },
    "pathObj": {
      "path": "/Settings/SystemSetup/MaxChargeVoltage",
      "type": "float",
      "name": "DVCC Maximum charge voltage (V)",
      "mode": "both"
    },
    "name": "Werkelijke laadspanning",
    "onlyChanges": true,
    "roundValues": "2",
    "rateLimit": 0,
    "conditionalMode": false,
    "outputOnChange": false,
    "debounce": 0,
    "x": 185,
    "y": 280,
    "wires": [
      [
        "mhb-universal-store-voltage"
      ]
    ]
  },
  {
    "id": "mhb-universal-store-voltage",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Onthoud spanning",
    "func": "flow.set('mhb_actual_voltage',Number(msg.payload));return msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 445,
    "y": 280,
    "wires": [
      [
        "mhb-universal-debug"
      ]
    ]
  },
  {
    "id": "mhb-universal-debug",
    "type": "debug",
    "z": "mhb-universal-tab",
    "name": "Werkelijke lokale waarde",
    "active": false,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "payload",
    "targetType": "msg",
    "x": 700,
    "y": 255,
    "wires": []
  },
  {
    "id": "mhb-universal-delay",
    "type": "delay",
    "z": "mhb-universal-tab",
    "name": "Wacht op lokale bevestiging",
    "pauseType": "delay",
    "timeout": "4",
    "timeoutUnits": "seconds",
    "rate": "1",
    "nbRateUnits": "1",
    "rateUnits": "second",
    "randomFirst": "1",
    "randomLast": "5",
    "randomUnits": "seconds",
    "drop": false,
    "allowrate": false,
    "outputs": 1,
    "x": 980,
    "y": 180,
    "wires": [
      [
        "mhb-universal-complete"
      ]
    ]
  },
  {
    "id": "mhb-universal-complete",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Bevestig werkelijke waarden",
    "func": "const p=flow.get('mhb_pending'),portal=flow.get('mhb_portal_id'),token=flow.get('mhb_token');if(!p||!portal||!token)return null;let ok=false,result={},error=null;if(p.type==='dvcc'){const c=flow.get('mhb_actual_current'),v=flow.get('mhb_actual_voltage');ok=Number.isFinite(c)&&Number.isFinite(v)&&Math.abs(c-p.current)<0.05&&Math.abs(v-p.voltage)<0.05;result={charge_current:c,charge_voltage:v,confirmed_locally:ok};error=ok?null:'lokale_waarde_niet_bevestigd';}else if(p.type==='power'){const mode=Number(flow.get('mhb_actual_mode'));ok=mode===p.expected_mode;result={mode:mode,expected_mode:p.expected_mode,confirmed_locally:ok};error=ok?null:'vebus_mode_niet_bevestigd';}else if(p.type==='reset'){ok=true;result={reset_sent:true,vebus_instance:flow.get('mhb_vebus_instance')};}msg.method='POST';msg.url='https://mhbbeheer.site/api/cerbo-node-red/'+portal+'/commands/'+p.id+'/complete';msg.headers={'X-MHB-Cerbo-Token':token,'Content-Type':'application/json'};msg.payload={ok:ok,result:result,error:error};return msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 1245,
    "y": 180,
    "wires": [
      [
        "mhb-universal-http-complete"
      ]
    ]
  },
  {
    "id": "mhb-universal-http-complete",
    "type": "http request",
    "z": "mhb-universal-tab",
    "name": "Resultaat naar MHB",
    "method": "use",
    "ret": "txt",
    "paytoqs": "ignore",
    "url": "",
    "persist": false,
    "insecureHTTPParser": false,
    "authType": "",
    "senderr": false,
    "headers": [],
    "x": 1480,
    "y": 180,
    "wires": [
      [
        "mhb-universal-result"
      ]
    ]
  },
  {
    "id": "mhb-universal-result",
    "type": "debug",
    "z": "mhb-universal-tab",
    "name": "MHB-resultaat",
    "active": false,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "payload",
    "targetType": "msg",
    "x": 1705,
    "y": 180,
    "wires": []
  },
  {
    "id": "mhb-live-soc",
    "type": "victron-input-system",
    "z": "mhb-universal-tab",
    "service": "com.victronenergy.system/0",
    "path": "/Dc/Battery/Soc",
    "serviceObj": {
      "service": "com.victronenergy.system/0",
      "name": "Venus system",
      "communityTag": "system"
    },
    "pathObj": {
      "path": "/Dc/Battery/Soc",
      "type": "float",
      "name": "Battery State of Charge (%)"
    },
    "name": "battery_soc",
    "onlyChanges": true,
    "roundValues": "1",
    "rateLimit": 0,
    "conditionalMode": false,
    "outputOnChange": false,
    "debounce": 0,
    "x": 135,
    "y": 360,
    "wires": [
      [
        "mhb-live-store"
      ]
    ]
  },
  {
    "id": "mhb-live-bv",
    "type": "victron-input-system",
    "z": "mhb-universal-tab",
    "service": "com.victronenergy.system/0",
    "path": "/Dc/Battery/Voltage",
    "serviceObj": {
      "service": "com.victronenergy.system/0",
      "name": "Venus system",
      "communityTag": "system"
    },
    "pathObj": {
      "path": "/Dc/Battery/Voltage",
      "type": "float",
      "name": "Battery voltage (V)"
    },
    "name": "battery_voltage",
    "onlyChanges": true,
    "roundValues": "2",
    "rateLimit": 0,
    "conditionalMode": false,
    "outputOnChange": false,
    "debounce": 0,
    "x": 145,
    "y": 400,
    "wires": [
      [
        "mhb-live-store"
      ]
    ]
  },
  {
    "id": "mhb-live-bi",
    "type": "victron-input-system",
    "z": "mhb-universal-tab",
    "service": "com.victronenergy.system/0",
    "path": "/Dc/Battery/Current",
    "serviceObj": {
      "service": "com.victronenergy.system/0",
      "name": "Venus system",
      "communityTag": "system"
    },
    "pathObj": {
      "path": "/Dc/Battery/Current",
      "type": "float",
      "name": "Battery current (A)"
    },
    "name": "battery_current",
    "onlyChanges": true,
    "roundValues": "1",
    "rateLimit": 0,
    "conditionalMode": false,
    "outputOnChange": false,
    "debounce": 0,
    "x": 145,
    "y": 440,
    "wires": [
      [
        "mhb-live-store"
      ]
    ]
  },
  {
    "id": "mhb-live-bp",
    "type": "victron-input-system",
    "z": "mhb-universal-tab",
    "service": "com.victronenergy.system/0",
    "path": "/Dc/Battery/Power",
    "serviceObj": {
      "service": "com.victronenergy.system/0",
      "name": "Venus system",
      "communityTag": "system"
    },
    "pathObj": {
      "path": "/Dc/Battery/Power",
      "type": "float",
      "name": "Battery Power (W)"
    },
    "name": "battery_power",
    "onlyChanges": true,
    "roundValues": "0",
    "rateLimit": 0,
    "conditionalMode": false,
    "outputOnChange": false,
    "debounce": 0,
    "x": 140,
    "y": 480,
    "wires": [
      [
        "mhb-live-store"
      ]
    ]
  },
  {
    "id": "mhb-live-bt",
    "type": "victron-input-system",
    "z": "mhb-universal-tab",
    "service": "com.victronenergy.system/0",
    "path": "/Dc/Battery/Temperature",
    "serviceObj": {
      "service": "com.victronenergy.system/0",
      "name": "Venus system",
      "communityTag": "system"
    },
    "pathObj": {
      "path": "/Dc/Battery/Temperature",
      "type": "float",
      "name": "Battery temperature (°C)"
    },
    "name": "battery_temperature",
    "onlyChanges": true,
    "roundValues": "1",
    "rateLimit": 0,
    "conditionalMode": false,
    "outputOnChange": false,
    "debounce": 0,
    "x": 155,
    "y": 520,
    "wires": [
      [
        "mhb-live-store"
      ]
    ]
  },
  {
    "id": "mhb-live-pv",
    "type": "victron-input-system",
    "z": "mhb-universal-tab",
    "service": "com.victronenergy.system/0",
    "path": "/Dc/Pv/Power",
    "serviceObj": {
      "service": "com.victronenergy.system/0",
      "name": "Venus system",
      "communityTag": "system"
    },
    "pathObj": {
      "path": "/Dc/Pv/Power",
      "type": "float",
      "name": "MPPTs - power (W)"
    },
    "name": "pv_power",
    "onlyChanges": true,
    "roundValues": "0",
    "rateLimit": 0,
    "conditionalMode": false,
    "outputOnChange": false,
    "debounce": 0,
    "x": 125,
    "y": 560,
    "wires": [
      [
        "mhb-live-store"
      ]
    ]
  },
  {
    "id": "mhb-live-grid",
    "type": "victron-input-system",
    "z": "mhb-universal-tab",
    "service": "com.victronenergy.system/0",
    "path": "/Ac/Grid/L1/Power",
    "serviceObj": {
      "service": "com.victronenergy.system/0",
      "name": "Venus system",
      "communityTag": "system"
    },
    "pathObj": {
      "path": "/Ac/Grid/L1/Power",
      "type": "float",
      "name": "Grid L1 (W)"
    },
    "name": "grid_power",
    "onlyChanges": true,
    "roundValues": "0",
    "rateLimit": 0,
    "conditionalMode": false,
    "outputOnChange": false,
    "debounce": 0,
    "x": 130,
    "y": 600,
    "wires": [
      [
        "mhb-live-store"
      ]
    ]
  },
  {
    "id": "mhb-live-store",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Live Cerbo-data onthouden",
    "func": "const names=['battery_soc','battery_voltage','battery_current','battery_power','battery_temperature','pv_power','grid_power'];\nconst source=String(msg.topic||'');\nif(names.includes(source)) flow.set('mhb_live_'+source,msg.payload);\nreturn msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 450,
    "y": 540,
    "wires": []
  },
  {
    "id": "mhb-universal-heartbeat",
    "type": "inject",
    "z": "mhb-universal-tab",
    "name": "Live status iedere 3 seconden",
    "props": [
      {
        "p": "payload"
      }
    ],
    "repeat": "3",
    "once": true,
    "onceDelay": 3,
    "payload": "",
    "payloadType": "date",
    "x": 185,
    "y": 800,
    "wires": [
      [
        "mhb-universal-heartbeat-build"
      ]
    ]
  },
  {
    "id": "mhb-universal-heartbeat-build",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Status maken",
    "func": "const portal=flow.get(\"mhb_portal_id\"),token=flow.get(\"mhb_token\");if(!portal||!token)return null;const now=Date.now();msg.method=\"POST\";msg.url=\"https://mhbbeheer.site/api/cerbo-node-red/\"+portal+\"/heartbeat\";msg.headers={\"X-MHB-Cerbo-Token\":token,\"Content-Type\":\"application/json\"};msg.payload={charge_current:flow.get(\"mhb_actual_current\"),charge_voltage:flow.get(\"mhb_actual_voltage\"),battery_soc:flow.get(\"mhb_live_battery_soc\"),battery_voltage:flow.get(\"mhb_live_battery_voltage\"),battery_current:flow.get(\"mhb_live_battery_current\"),battery_power:flow.get(\"mhb_live_battery_power\"),battery_temperature:flow.get(\"mhb_live_battery_temperature\"),pv_power:flow.get(\"mhb_live_pv_power\"),grid_power:flow.get(\"mhb_live_grid_power\"),system_state:flow.get(\"mhb_live_system_state\"),charge_disabled:flow.get(\"mhb_live_charge_disabled\"),discharge_disabled:flow.get(\"mhb_live_discharge_disabled\")};const last=Number(flow.get(\"mhb_extended_sent_at\")||0);if(now-last>=15000){msg.payload.extended=flow.get(\"mhb_extended\")||{};flow.set(\"mhb_extended_sent_at\",now);}return msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 435,
    "y": 800,
    "wires": [
      [
        "mhb-universal-heartbeat-http"
      ]
    ]
  },
  {
    "id": "mhb-universal-heartbeat-http",
    "type": "http request",
    "z": "mhb-universal-tab",
    "name": "Cerbo-status naar MHB",
    "method": "use",
    "ret": "txt",
    "paytoqs": "ignore",
    "url": "",
    "persist": false,
    "insecureHTTPParser": false,
    "authType": "",
    "senderr": false,
    "headers": [],
    "x": 690,
    "y": 370,
    "wires": []
  },
  {
    "id": "mhb-portal-input",
    "type": "victron-input-system",
    "z": "mhb-universal-tab",
    "service": "com.victronenergy.system/0",
    "path": "/Serial",
    "serviceObj": {
      "service": "com.victronenergy.system/0",
      "name": "Venus system",
      "communityTag": "system"
    },
    "pathObj": {
      "path": "/Serial",
      "type": "string",
      "name": "Serial (System)"
    },
    "name": "VRM Portal-ID automatisch",
    "onlyChanges": true,
    "roundValues": "no",
    "rateLimit": 0,
    "x": 180,
    "y": 900,
    "wires": [
      [
        "mhb-portal-store"
      ]
    ]
  },
  {
    "id": "mhb-portal-store",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "VRM-ID onthouden",
    "func": "const portal=String(msg.payload||'').trim();if(portal){flow.set('mhb_detected_portal',portal);msg.payload=Date.now();return msg;}return null;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 445,
    "y": 900,
    "wires": [
      [
        "mhb-enroll-build"
      ]
    ]
  },
  {
    "id": "mhb-identity-start",
    "type": "inject",
    "z": "mhb-universal-tab",
    "name": "Identiteit laden bij start",
    "props": [
      {
        "p": "payload"
      }
    ],
    "once": true,
    "onceDelay": 1,
    "payload": "",
    "payloadType": "date",
    "x": 190,
    "y": 950,
    "wires": [
      [
        "mhb-identity-file"
      ]
    ]
  },
  {
    "id": "mhb-identity-file",
    "type": "file in",
    "z": "mhb-universal-tab",
    "name": "Lokale MHB-identiteit",
    "filename": "/data/home/nodered/.node-red/mhb-device.json",
    "filenameType": "str",
    "format": "utf8",
    "chunk": false,
    "sendError": false,
    "encoding": "none",
    "allProps": false,
    "x": 450,
    "y": 950,
    "wires": [
      [
        "mhb-identity-parse"
      ]
    ]
  },
  {
    "id": "mhb-identity-parse",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Identiteit controleren",
    "func": "try{const v=typeof msg.payload==='string'?JSON.parse(msg.payload):msg.payload;if(v&&String(v.device_id||'').length>=16&&String(v.device_secret||'').length>=32){flow.set('mhb_device_id',v.device_id);flow.set('mhb_device_secret',v.device_secret);msg.payload=Date.now();return msg;}}catch(e){}node.error('MHB-identiteit ontbreekt of is ongeldig',msg);return null;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 710,
    "y": 950,
    "wires": [
      [
        "mhb-enroll-build"
      ]
    ]
  },
  {
    "id": "mhb-identity-catch",
    "type": "catch",
    "z": "mhb-universal-tab",
    "name": "Nieuwe identiteit nodig",
    "scope": [
      "mhb-identity-file",
      "mhb-identity-parse"
    ],
    "uncaught": false,
    "x": 190,
    "y": 1000,
    "wires": [
      [
        "mhb-identity-generate"
      ]
    ]
  },
  {
    "id": "mhb-identity-generate",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Unieke identiteit maken",
    "func": "const chars='ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';const random=n=>Array.from({length:n},()=>chars[Math.floor(Math.random()*chars.length)]).join('');const value={device_id:'cerbo_'+random(24),device_secret:random(64)};flow.set('mhb_device_id',value.device_id);flow.set('mhb_device_secret',value.device_secret);return [{payload:Date.now()},{payload:JSON.stringify(value),filename:'/data/home/nodered/.node-red/mhb-device.json'}];",
    "outputs": 2,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 460,
    "y": 1000,
    "wires": [
      [
        "mhb-enroll-build"
      ],
      [
        "mhb-identity-save"
      ]
    ]
  },
  {
    "id": "mhb-identity-save",
    "type": "file",
    "z": "mhb-universal-tab",
    "name": "Identiteit veilig bewaren",
    "filename": "/data/home/nodered/.node-red/mhb-device.json",
    "filenameType": "str",
    "appendNewline": false,
    "createDir": true,
    "overwriteFile": "true",
    "encoding": "none",
    "x": 750,
    "y": 1020,
    "wires": [
      []
    ]
  },
  {
    "id": "mhb-enroll-timer",
    "type": "inject",
    "z": "mhb-universal-tab",
    "name": "Koppeling iedere 5 minuten controleren",
    "props": [
      {
        "p": "payload"
      }
    ],
    "repeat": "300",
    "once": true,
    "onceDelay": 4,
    "payload": "",
    "payloadType": "date",
    "x": 205,
    "y": 1070,
    "wires": [
      [
        "mhb-enroll-build"
      ]
    ]
  },
  {
    "id": "mhb-enroll-build",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Aanmelding maken",
    "func": "const portal=flow.get(\"mhb_detected_portal\"),deviceId=flow.get(\"mhb_device_id\"),secret=flow.get(\"mhb_device_secret\");if(!portal||!deviceId||!secret){node.status({fill:\"yellow\",shape:\"ring\",text:\"identiteit/VRM-ID laden\"});return null;}const now=Date.now(),last=Number(flow.get(\"mhb_enroll_sent_at\")||0),linked=Boolean(flow.get(\"mhb_portal_id\")&&flow.get(\"mhb_token\"));if(now-last<(linked?3600000:300000))return null;flow.set(\"mhb_enroll_sent_at\",now);msg.method=\"POST\";msg.url=\"https://mhbbeheer.site/api/cerbo-node-red/enroll/register\";msg.headers={\"Content-Type\":\"application/json\"};msg.payload={portal_id:portal,device_id:deviceId,device_secret:secret,device_name:\"Cerbo GX\",node_red_version:\"1.6.56\",metadata:{flow_version:\"8\"}};return msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 465,
    "y": 1070,
    "wires": [
      [
        "mhb-enroll-http"
      ]
    ]
  },
  {
    "id": "mhb-enroll-http",
    "type": "http request",
    "z": "mhb-universal-tab",
    "name": "Melden bij MHB Beheer",
    "method": "use",
    "ret": "txt",
    "paytoqs": "ignore",
    "url": "",
    "persist": false,
    "insecureHTTPParser": false,
    "authType": "",
    "senderr": false,
    "headers": [],
    "x": 720,
    "y": 1070,
    "wires": [
      [
        "mhb-enroll-json-safe"
      ]
    ]
  },
  {
    "id": "mhb-enroll-json-safe",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Aanmelding veilig lezen",
    "func": "if(Number(msg.statusCode)<200||Number(msg.statusCode)>=300){node.status({fill:'yellow',shape:'ring',text:'later opnieuw proberen'});return null;}try{msg.payload=typeof msg.payload==='string'?JSON.parse(msg.payload):msg.payload;node.status({fill:'green',shape:'dot',text:'antwoord ontvangen'});return msg;}catch(e){node.status({fill:'yellow',shape:'ring',text:'ongeldig antwoord overgeslagen'});return null;}",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 950,
    "y": 1040,
    "wires": [
      [
        "mhb-enroll-result"
      ]
    ]
  },
  {
    "id": "mhb-enroll-result",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Koppeling activeren",
    "func": "const r=msg.payload||{};if(!r.ok){node.status({fill:'red',shape:'ring',text:'aanmelding mislukt'});return null;}if(r.linked){flow.set('mhb_portal_id',r.portal_id);flow.set('mhb_token',flow.get('mhb_device_secret'));node.status({fill:'green',shape:'dot',text:'gekoppeld · '+r.portal_id});}else{flow.set('mhb_portal_id',null);flow.set('mhb_token',null);node.status({fill:'yellow',shape:'ring',text:r.suggestion?'gevonden · koppelen in MHB':'wacht op VRM-import'});}return msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 980,
    "y": 1070,
    "wires": []
  },
  {
    "id": "mhb-mqtt-vebus-discovery",
    "type": "mqtt in",
    "z": "mhb-universal-tab",
    "name": "VE.Bus automatisch herkennen",
    "topic": "N/+/vebus/+/DeviceInstance",
    "qos": "0",
    "datatype": "auto-detect",
    "broker": "mhb-local-mqtt",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 195,
    "y": 1140,
    "wires": [
      [
        "mhb-mqtt-vebus-store"
      ]
    ]
  },
  {
    "id": "mhb-mqtt-vebus-store",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "VE.Bus nummer onthouden",
    "func": "const parts=String(msg.topic||'').split('/');if(parts.length>=5){const portal=parts[1],instance=parts[3];if(portal===String(flow.get('mhb_detected_portal')||'')){flow.set('mhb_vebus_instance',instance);node.status({fill:'green',shape:'dot',text:'VE.Bus '+instance});}}return msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 485,
    "y": 1140,
    "wires": []
  },
  {
    "id": "mhb-mqtt-mode-input",
    "type": "mqtt in",
    "z": "mhb-universal-tab",
    "name": "Werkelijke VE.Bus-modus",
    "topic": "N/+/vebus/+/Mode",
    "qos": "0",
    "datatype": "auto-detect",
    "broker": "mhb-local-mqtt",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 190,
    "y": 1190,
    "wires": [
      [
        "mhb-mqtt-mode-store"
      ]
    ]
  },
  {
    "id": "mhb-mqtt-mode-store",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Modus bevestigen",
    "func": "try{const value=typeof msg.payload==='string'?JSON.parse(msg.payload).value:msg.payload.value;flow.set('mhb_actual_mode',Number(value));node.status({fill:'green',shape:'dot',text:'modus '+value});}catch(e){}return msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 460,
    "y": 1190,
    "wires": []
  },
  {
    "id": "mhb-mqtt-keepalive-timer",
    "type": "inject",
    "z": "mhb-universal-tab",
    "name": "VE.Bus verversen",
    "props": [
      {
        "p": "payload"
      }
    ],
    "repeat": "30",
    "once": true,
    "onceDelay": 6,
    "payload": "",
    "payloadType": "str",
    "x": 180,
    "y": 1240,
    "wires": [
      [
        "mhb-mqtt-keepalive-build"
      ]
    ]
  },
  {
    "id": "mhb-mqtt-keepalive-build",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Lokale gegevens opvragen",
    "func": "const portal=flow.get('mhb_detected_portal');if(!portal)return null;msg.topic='R/'+portal+'/keepalive';msg.payload='';return msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 460,
    "y": 1240,
    "wires": [
      [
        "mhb-mqtt-command"
      ]
    ]
  },
  {
    "id": "mhb-mqtt-command",
    "type": "mqtt out",
    "z": "mhb-universal-tab",
    "name": "Veilige lokale VE.Bus-opdracht",
    "topic": "",
    "qos": "",
    "retain": "",
    "respTopic": "",
    "contentType": "",
    "userProps": "",
    "correl": "",
    "expiry": "",
    "broker": "mhb-local-mqtt",
    "x": 780,
    "y": 1215,
    "wires": []
  },
  {
    "id": "mhb-mqtt-system-all",
    "type": "mqtt in",
    "z": "mhb-universal-tab",
    "name": "Net en AC PV per fase",
    "topic": "N/+/system/0/#",
    "qos": "0",
    "datatype": "auto-detect",
    "broker": "mhb-local-mqtt",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 185,
    "y": 1320,
    "wires": [
      [
        "mhb-mqtt-extended-store"
      ]
    ]
  },
  {
    "id": "mhb-mqtt-mppt-all",
    "type": "mqtt in",
    "z": "mhb-universal-tab",
    "name": "Alle MPPT-laders",
    "topic": "N/+/solarcharger/+/#",
    "qos": "0",
    "datatype": "auto-detect",
    "broker": "mhb-local-mqtt",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 180,
    "y": 1360,
    "wires": [
      [
        "mhb-mqtt-extended-store"
      ]
    ]
  },
  {
    "id": "mhb-mqtt-pvinverter-all",
    "type": "mqtt in",
    "z": "mhb-universal-tab",
    "name": "Alle AC PV-omvormers",
    "topic": "N/+/pvinverter/+/#",
    "qos": "0",
    "datatype": "auto-detect",
    "broker": "mhb-local-mqtt",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 195,
    "y": 1400,
    "wires": [
      [
        "mhb-mqtt-extended-store"
      ]
    ]
  },
  {
    "id": "mhb-mqtt-battery-all",
    "type": "mqtt in",
    "z": "mhb-universal-tab",
    "name": "BMS cellen en blokkeringen",
    "topic": "N/+/battery/+/#",
    "qos": "0",
    "datatype": "auto-detect",
    "broker": "mhb-local-mqtt",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 205,
    "y": 1440,
    "wires": [
      [
        "mhb-mqtt-extended-store"
      ]
    ]
  },
  {
    "id": "mhb-mqtt-vebus-all",
    "type": "mqtt in",
    "z": "mhb-universal-tab",
    "name": "VE.Bus alarmen",
    "topic": "N/+/vebus/+/#",
    "qos": "0",
    "datatype": "auto-detect",
    "broker": "mhb-local-mqtt",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 175,
    "y": 1480,
    "wires": [
      [
        "mhb-mqtt-extended-store"
      ]
    ]
  },
  {
    "id": "mhb-mqtt-platform-all",
    "type": "mqtt in",
    "z": "mhb-universal-tab",
    "name": "Cerbo uptime",
    "topic": "N/+/platform/+/#",
    "qos": "0",
    "datatype": "auto-detect",
    "broker": "mhb-local-mqtt",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 165,
    "y": 1520,
    "wires": [
      [
        "mhb-mqtt-extended-store"
      ]
    ]
  },
  {
    "id": "mhb-mqtt-grid-all",
    "type": "mqtt in",
    "z": "mhb-universal-tab",
    "name": "Netmeter spanning en Hz",
    "topic": "N/+/grid/+/#",
    "qos": "0",
    "datatype": "auto-detect",
    "broker": "mhb-local-mqtt",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 190,
    "y": 1560,
    "wires": [
      [
        "mhb-mqtt-extended-store"
      ]
    ]
  },
  {
    "id": "mhb-mqtt-temperature-all",
    "type": "mqtt in",
    "z": "mhb-universal-tab",
    "name": "Alle Cerbo temperatuursensoren",
    "topic": "N/+/temperature/+/#",
    "qos": "0",
    "datatype": "auto-detect",
    "broker": "mhb-local-mqtt",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 215,
    "y": 1645,
    "wires": [
      [
        "mhb-temperature-store"
      ]
    ]
  },
  {
    "id": "mhb-temperature-store",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Temperatuursensoren onthouden",
    "func": "const p=String(msg.topic||'').split('/');if(p.length<5||p[0]!=='N')return null;const portal=String(flow.get('mhb_detected_portal')||'').toLowerCase();if(portal&&String(p[1]).toLowerCase()!==portal)return null;const instance=String(p[3]),path='/'+p.slice(4).join('/');if(!['/Temperature','/CustomName','/ProductName','/Status','/TemperatureType','/DeviceInstance'].includes(path))return null;let value;try{const v=typeof msg.payload==='string'?JSON.parse(msg.payload):msg.payload;value=v&&typeof v==='object'&&Object.prototype.hasOwnProperty.call(v,'value')?v.value:v;}catch(e){return null;}if(value===undefined||value===null)return null;let ext=flow.get('mhb_extended');if(!ext||typeof ext!=='object')ext={};ext.temperatures=ext.temperatures||{};ext.temperatures[instance]=ext.temperatures[instance]||{};ext.temperatures[instance][path]=value;ext.updated_at=new Date().toISOString();flow.set('mhb_extended',ext);node.status({fill:'green',shape:'dot',text:(ext.temperatures[instance]['/CustomName']||'sensor '+instance)+' · '+(ext.temperatures[instance]['/Temperature']??'-')+' °C'});return null;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 520,
    "y": 1645,
    "wires": []
  },
  {
    "id": "mhb-mqtt-ess-settings",
    "type": "mqtt in",
    "z": "mhb-universal-tab",
    "name": "ESS-instellingen live",
    "topic": "N/+/settings/0/Settings/CGwacs/#",
    "qos": "0",
    "datatype": "auto-detect",
    "broker": "mhb-local-mqtt",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 190,
    "y": 1690,
    "wires": [
      [
        "mhb-ess-settings-store"
      ]
    ]
  },
  {
    "id": "mhb-mqtt-dess-settings",
    "type": "mqtt in",
    "z": "mhb-universal-tab",
    "name": "Dynamic ESS-status live",
    "topic": "N/+/settings/0/Settings/DynamicEss/#",
    "qos": "0",
    "datatype": "auto-detect",
    "broker": "mhb-local-mqtt",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 200,
    "y": 1730,
    "wires": [
      [
        "mhb-ess-settings-store"
      ]
    ]
  },
  {
    "id": "mhb-ess-settings-store",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "ESS-waarden onthouden",
    "func": "const p=String(msg.topic||'').split('/');if(p.length<6||p[0]!=='N')return null;const portal=String(flow.get('mhb_detected_portal')||'').toLowerCase();if(portal&&String(p[1]).toLowerCase()!==portal)return null;const path='/'+p.slice(4).join('/'),allowed=['/Settings/CGwacs/Hub4Mode','/Settings/CGwacs/BatteryLife/State','/Settings/CGwacs/BatteryLife/MinimumSocLimit','/Settings/CGwacs/BatteryLife/SocLimit','/Settings/CGwacs/AcPowerSetPoint','/Settings/CGwacs/MaxChargePower','/Settings/CGwacs/MaxDischargePower','/Settings/CGwacs/PreventFeedback','/Settings/CGwacs/OvervoltageFeedIn','/Settings/DynamicEss/Mode'];if(!allowed.includes(path))return null;let value;try{const v=typeof msg.payload==='string'?JSON.parse(msg.payload):msg.payload;value=v&&typeof v==='object'&&Object.prototype.hasOwnProperty.call(v,'value')?v.value:v;}catch(e){return null;}if(value===undefined||value===null)return null;let ext=flow.get('mhb_extended');if(!ext||typeof ext!=='object')ext={};ext.ess=ext.ess||{};ext.ess[path]=value;ext.updated_at=new Date().toISOString();flow.set('mhb_extended',ext);node.status({fill:'green',shape:'dot',text:path.split('/').pop()+' · '+value});return null;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 480,
    "y": 1710,
    "wires": []
  },
  {
    "id": "mhb-ess-command-build",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Veilige ESS-instellingen schrijven",
    "func": "const job=(msg.payload||{}).job,p=job&&job.payload||{},portal=flow.get('mhb_detected_portal');if(!job||!portal)return [null,null];const specs={minimum_soc:['Settings/CGwacs/BatteryLife/MinimumSocLimit',0,100],grid_setpoint:['Settings/CGwacs/AcPowerSetPoint',-10000,10000],max_charge_power:['Settings/CGwacs/MaxChargePower',-1,50000],max_discharge_power:['Settings/CGwacs/MaxDischargePower',-1,50000],prevent_feedback:['Settings/CGwacs/PreventFeedback',0,1],overvoltage_feed_in:['Settings/CGwacs/OvervoltageFeedIn',0,1]};const expected={},messages=[];for(const [key,spec] of Object.entries(specs)){if(!Object.prototype.hasOwnProperty.call(p,key))continue;let value=(key==='prevent_feedback'||key==='overvoltage_feed_in')?(p[key]?1:0):Number(p[key]);if(!Number.isFinite(value)||value<spec[1]||value>spec[2]){node.error('ESS-waarde buiten veilige grens: '+key,msg);return [null,null];}expected['/Settings/'+spec[0].slice('Settings/'.length)]=value;messages.push({topic:'W/'+portal+'/settings/0/'+spec[0],payload:JSON.stringify({value:value}),qos:0,retain:false});}if(!messages.length){node.error('Geen ESS-instellingen ontvangen',msg);return [null,null];}flow.set('mhb_pending_ess',{id:job.id,expected:expected});return [messages,{payload:job.id}];",
    "outputs": 2,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 1390,
    "y": 80,
    "wires": [
      [
        "mhb-mqtt-command"
      ],
      [
        "mhb-ess-confirm-delay"
      ]
    ]
  },
  {
    "id": "mhb-ess-confirm-delay",
    "type": "delay",
    "z": "mhb-universal-tab",
    "name": "ESS lokaal controleren",
    "pauseType": "delay",
    "timeout": "4",
    "timeoutUnits": "seconds",
    "rate": "1",
    "nbRateUnits": "1",
    "rateUnits": "second",
    "randomFirst": "1",
    "randomLast": "5",
    "randomUnits": "seconds",
    "drop": false,
    "allowrate": false,
    "outputs": 1,
    "x": 1640,
    "y": 120,
    "wires": [
      [
        "mhb-ess-complete"
      ]
    ]
  },
  {
    "id": "mhb-ess-complete",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "ESS-resultaat bevestigen",
    "func": "const pending=flow.get('mhb_pending_ess'),portal=flow.get('mhb_portal_id'),token=flow.get('mhb_token');if(!pending||!portal||!token)return null;const ext=flow.get('mhb_extended')||{},actual=ext.ess||{},result={},failed=[];for(const [path,wanted] of Object.entries(pending.expected)){const got=Number(actual[path]);result[path]=Number.isFinite(got)?got:null;if(!Number.isFinite(got)||Math.abs(got-Number(wanted))>0.05)failed.push(path);}msg.method='POST';msg.url='https://mhbbeheer.site/api/cerbo-node-red/'+portal+'/commands/'+pending.id+'/complete';msg.headers={'X-MHB-Cerbo-Token':token,'Content-Type':'application/json'};msg.payload={ok:failed.length===0,result:{settings:result,confirmed_locally:failed.length===0},error:failed.length?'ess_local_value_not_confirmed:'+failed.join(','):null};flow.set('mhb_pending_ess',null);return msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 1880,
    "y": 120,
    "wires": [
      [
        "mhb-universal-http-complete"
      ]
    ]
  },
  {
    "id": "mhb-uptime-timer",
    "type": "inject",
    "z": "mhb-universal-tab",
    "name": "Uptime iedere minuut",
    "props": [
      {
        "p": "payload"
      }
    ],
    "repeat": "60",
    "once": true,
    "onceDelay": 8,
    "payload": "",
    "payloadType": "date",
    "x": 190,
    "y": 1600,
    "wires": [
      [
        "mhb-uptime-exec"
      ]
    ]
  },
  {
    "id": "mhb-uptime-exec",
    "type": "exec",
    "z": "mhb-universal-tab",
    "command": "cat /proc/uptime",
    "addpay": "",
    "append": "",
    "useSpawn": "false",
    "timer": "5",
    "winHide": false,
    "oldrc": false,
    "name": "Lees Cerbo-uptime",
    "x": 440,
    "y": 1600,
    "wires": [
      [
        "mhb-uptime-store"
      ],
      [],
      []
    ]
  },
  {
    "id": "mhb-uptime-store",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Uptime onthouden",
    "func": "const seconds=Number(String(msg.payload||'').trim().split(' ')[0]);if(!Number.isFinite(seconds))return null;let ext=flow.get('mhb_extended');if(!ext||typeof ext!=='object')ext={};ext.cerbo=ext.cerbo||{};ext.cerbo['/Uptime']=Math.floor(seconds);ext.updated_at=new Date().toISOString();flow.set('mhb_extended',ext);node.status({fill:'green',shape:'dot',text:Math.floor(seconds/86400)+' dagen'});return null;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 680,
    "y": 1600,
    "wires": []
  },
  {
    "id": "mhb-mqtt-extended-store",
    "type": "function",
    "z": "mhb-universal-tab",
    "name": "Universele Cerbo-data filteren",
    "func": "const p=String(msg.topic||'').split('/');if(p.length<5||p[0]!=='N')return null;const portal=String(flow.get('mhb_detected_portal')||'').toLowerCase();if(portal&&String(p[1]).toLowerCase()!==portal)return null;let value;try{const v=typeof msg.payload==='string'?JSON.parse(msg.payload):msg.payload;value=v&&typeof v==='object'&&Object.prototype.hasOwnProperty.call(v,'value')?v.value:v;}catch(e){return null;}if(value===undefined||value===null)return null;const service=String(p[2]).split('.').pop(),instance=String(p[3]),parts=p.slice(4),path='/'+parts.join('/');let ext=flow.get('mhb_extended');if(!ext||typeof ext!=='object')ext={};ext.grid=ext.grid||{};ext.ac_pv=ext.ac_pv||{};ext.mppt=ext.mppt||{};ext.bms=ext.bms||{};ext.vebus_alarms=ext.vebus_alarms||{};ext.cerbo=ext.cerbo||{};if(service==='grid'&&path==='/Ac/Frequency'){ext.grid.frequency=value;}else if(service==='grid'&&parts[0]==='Ac'&&['L1','L2','L3'].includes(parts[1])&&['Power','Voltage','Current','Frequency'].includes(parts[2])){ext.grid[parts[1].toLowerCase()+'_'+parts[2].toLowerCase()]=value;}else if(service==='system'&&parts[0]==='Ac'&&parts[1]==='Grid'&&['L1','L2','L3'].includes(parts[2])&&['Power','Voltage','Current','Frequency'].includes(parts[3])){ext.grid[parts[2].toLowerCase()+'_'+parts[3].toLowerCase()]=value;}else if(service==='system'&&parts[0]==='Ac'&&['PvOnGrid','PvOnOutput','PvOnInput'].includes(parts[1])&&['L1','L2','L3'].includes(parts[2])&&parts[3]==='Power'){ext.ac_pv[parts[1]+'_'+parts[2].toLowerCase()]=value;}else if(service==='solarcharger'&&['/Yield/Power','/Pv/V','/Dc/0/Current','/State','/ErrorCode','/Yield/User','/ProductName','/CustomName'].includes(path)){ext.mppt[instance]=ext.mppt[instance]||{};ext.mppt[instance][path]=value;}else if(service==='pvinverter'&&((parts[0]==='Ac'&&['L1','L2','L3'].includes(parts[1])&&['Power','Voltage','Current','Frequency'].includes(parts[2]))||['/StatusCode','/ErrorCode','/ProductName','/CustomName'].includes(path))){const key='device_'+instance;ext.ac_pv[key]=ext.ac_pv[key]||{};ext.ac_pv[key][path]=value;}else if(service==='battery'&&(path.startsWith('/System/MinCell')||path.startsWith('/System/MaxCell')||path.startsWith('/Voltages/Cell')||path==='/Io/AllowToCharge'||path==='/Io/AllowToDischarge'||path==='/Info/MaxChargeCurrent'||path==='/Info/MaxDischargeCurrent'||path.startsWith('/Alarms/'))){ext.bms[instance]=ext.bms[instance]||{};ext.bms[instance][path]=value;}else if(service==='vebus'&&path.startsWith('/Alarms/')){ext.vebus_alarms[instance]=ext.vebus_alarms[instance]||{};ext.vebus_alarms[instance][path]=value;}else if(service==='platform'&&(path.toLowerCase().includes('uptime')||path.toLowerCase().includes('firmware')||path.toLowerCase().includes('productname'))){ext.cerbo[path]=value;}else{return null;}ext.updated_at=new Date().toISOString();flow.set('mhb_extended',ext);node.status({fill:'green',shape:'dot',text:'live '+service+' · '+path});return null;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 490,
    "y": 1420,
    "wires": []
  },
  {
    "id": "mhb-local-mqtt",
    "type": "mqtt-broker",
    "name": "Lokale Cerbo MQTT",
    "broker": "127.0.0.1",
    "port": "1883",
    "clientid": "",
    "autoConnect": true,
    "usetls": false,
    "protocolVersion": "4",
    "keepalive": "60",
    "cleansession": true,
    "autoUnsubscribe": true,
    "birthTopic": "",
    "birthQos": "0",
    "birthPayload": "",
    "birthMsg": {},
    "closeTopic": "",
    "closeQos": "0",
    "closePayload": "",
    "closeMsg": {},
    "willTopic": "",
    "willQos": "0",
    "willPayload": "",
    "willMsg": {}
  },
  {
    "id": "victron-client-id",
    "type": "victron-client",
    "name": "Victron Energy Client",
    "showValues": true,
    "contextStore": true,
    "enablePolling": false
  }
]
