[
  {"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 · automatische VRM-herkenning · eerst koppelen in MHB Beheer","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":"Iedere 2 seconden","props":[{"p":"payload"}],"repeat":"2","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-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":"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;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'),extended:flow.get('mhb_extended')||{}};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":"Aanmelden iedere 15 seconden","props":[{"p":"payload"}],"repeat":"15","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;}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:'5'}};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","mhb-bms-live-store"]]},
  {"id":"mhb-bms-live-store","type":"function","z":"mhb-universal-tab","name":"BMS spanning en temperatuur","func":"const p=String(msg.topic||'').split('/');if(p.length<5)return null;const instance=String(p[3]),path='/'+p.slice(4).join('/');if(!['/Dc/0/Voltage','/Dc/0/Current','/Dc/0/Temperature','/System/MinCellTemperature','/System/MaxCellTemperature','/System/NrOfModulesOnline','/System/NrOfModulesOffline'].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.bms=ext.bms||{};ext.bms[instance]=ext.bms[instance]||{};ext.bms[instance][path]=value;ext.updated_at=new Date().toISOString();flow.set('mhb_extended',ext);node.status({fill:'green',shape:'dot',text:path+' · '+value});return null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":1460,"wires":[]},
  {"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-grid-frequency","type":"mqtt in","z":"mhb-universal-tab","name":"Netfrequentie Hz","topic":"N/+/grid/+/Ac/Frequency","qos":"0","datatype":"auto-detect","broker":"mhb-local-mqtt","nl":false,"rap":true,"rh":0,"inputs":0,"x":180,"y":1585,"wires":[["mhb-grid-frequency-store"]]},
  {"id":"mhb-grid-frequency-store","type":"function","z":"mhb-universal-tab","name":"Hz onthouden","func":"let value;try{const v=typeof msg.payload==='string'?JSON.parse(msg.payload):msg.payload;value=Number(v&&typeof v==='object'&&Object.prototype.hasOwnProperty.call(v,'value')?v.value:v);}catch(e){return null;}if(!Number.isFinite(value))return null;let ext=flow.get('mhb_extended');if(!ext||typeof ext!=='object')ext={};ext.grid=ext.grid||{};ext.grid.frequency=value;ext.updated_at=new Date().toISOString();flow.set('mhb_extended',ext);node.status({fill:'green',shape:'dot',text:value.toFixed(2)+' Hz'});return null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":1585,"wires":[]},
  {"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'&&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}
]
