Inverter ON/Off button on Dashboard (feature request)

I have a product feature request, forgive me if this is not the right place to post it.

TLDR; It would be AMAZING if the ‘inverter’ image on the dashboard (overview) simply had an ‘on/off’ button right there on it.

I absolutely love the power and capability of the Cerbo GX and touch display. The only gripe is the common function to turn on or off the inverter takes 4 button clicks across 3 menu screens. It’s an extremely common item to turn on or off as a user might wish to conserve energy by leaving the inverter off when not needed.

I’m new to node-red and looking into the functionality if I can do this myself, but it would be so great if it existed out of the box

Cheers!

John

Mine is one click away from the overview, V3.80~27 but it’s been around a while.

I’m thinking it took you 1 click to get to that screen. A second click on the ‘on’ button you circled. A third click to select the ‘off’ mode, and a 4th click on the ‘set’ button (to save it).

I’m looking for an always present ‘on/off’ toggle right on the overview screen… true one click toggle.

This was raised when v2 of the GUI was released. I’m not sure they see it as an issue. What would be nice is if there was a dedicated RV screen (like there is in V1 gui), but instead they added a boat screen which didn’t improve anything for RV use.

It’s because of this (and a few other issues) that I eventually switched back to the V1 gui, and also why I now have home assistant running on a rpi in my van so I can have a UI that’s much more suitable to RV use

See this thread

Top left toggle icon on the brief and overview screen has inverter on/off and grid current limit. Its two clicks. Sorry.

Still four clicks like OP described it, the only difference is that you’re opening a different menu first,

I dont think its a hassle to go into a menu and change the operation mode, however thats highly subjective. But i do agree that theres not really anything for energy saving. By now ive gotten familiar with NodeRed to automatically switch some BatteryProtect through the GX relays how i want it, but a general load shedding function would be nice for people that dont want to dive into hours of reading and tinkering to get a relay to switch on some values.

Thanks @drs68 , your post is exactly what I’m referring to. I’m really interested in your ‘home assistant’ capabilities… would you mind DM me (is that a thing on this platform)? You can reach me at {an email address}

thanks!

No it is not. Ican pass it on though

If it is something done often. A button connected to a digital i out and a short node red flow can sort ou the issue and make it 1 touch.

A preferred alternative would be if I could wire up a physical switch to control a relay that toggles the inverter on/off. Then the user wouldn’t need screens at button clicks at all… just flip on the inverter, cook, and flip it off again.

Anyone know if you can toggle the multiplus inverter on/off via physical relay switch?

If you’re interested in using home assistant in your van I’d recommend the YouTube channel of Smarty Van, which is where I got started.

Smarty Van

Of course, through the remote on/off/charger switch.

But

That doesnt work anymore if the MP is connected to a GX or DMC over VE.Bus. Nice isnt it.

I didnt like that as well, so i removed the internal on/off/charger (since that take precedence over any other state input) rocker switch, and wired in a remote mounted switch instead (actually two relay contacts, but thats a detail).

You can see my system here

Okay,

Probably, yea.

Honestly I haven’t turned my inverter off since I commissioned the system. It’s either on the road doing it’s RV thing, or in the barn keeping the battery at 60%.

I can’t help but laugh out loud that we are discussing the difference between 2 and 4 clicks. No offense to anyone. I know this is a forum of very smart and creative people trying to accomplish new and better ways of doing things but what a classic example of overthinking. I do really appreciate you guys.:grin:

Fair, but also it’s between 1 and 4 clicks. My initial request was a simple one-click on/off button. And if you do it 4 times per evening you start to notice the difference between ‘hitting a switch’ 4 times and navigating 16 clicks across various menu screens.

Honestly though the digital input (physical switch) → on/off logic is likely my best bet. I’ll head down the node-red rabbit hole :wink:

Isn’t zero load power around 15W?

[{"id":"aeb84866df2f5a22","type":"tab","label":"inverter V5.0 fallback","disabled":false,"info":"","env":[]},{"id":"4956470aae6b5550","type":"group","z":"aeb84866df2f5a22","name":"CTX TARGET receiver - move to Inverter tab","style":{"stroke":"#0088cc","label":true},"nodes":["475d7b4f0c2afd15","fc54b65d135b6024","18dd171d28d20efb","0e86650f5daaad7a"],"x":34,"y":679,"w":862,"h":122},{"id":"f3043cc0313d690f","type":"inject","z":"aeb84866df2f5a22","name":"Dashboard Init","props":[],"repeat":"","crontab":"","once":true,"onceDelay":"0.1","x":305,"y":50,"wires":[["d1978b9a4234f9dc"]]},{"id":"d1978b9a4234f9dc","type":"function","z":"aeb84866df2f5a22","name":"Set Initial Defaults","func":"// Use last-saved values, fallback only if nothing saved.\nconst DEFAULT_MODE = \"manual\";\nconst DEFAULT_TIME_SECS = 720 * 60;  // fallback only if never saved before\nconst DEFAULT_RUNMODE = \"normal\";\n\n// Try to get last-saved mode and timer from context, else fallback.\nlet lastMode = flow.get('iv_mode') || DEFAULT_MODE;\nlet lastTimeSecs = flow.get('iv_timer_value');\nif (typeof lastTimeSecs !== \"number\" || isNaN(lastTimeSecs)) lastTimeSecs = DEFAULT_TIME_SECS;\nlet lastRunMode = flow.get('inverter_runmode') || DEFAULT_RUNMODE;\n\n// Save to flow (namespaced)\nflow.set('iv_mode', lastMode);\nflow.set('iv_timer_value', lastTimeSecs);\nflow.set('iv_timer_running', false);\nflow.set('iv_timer_remaining', 0);\nflow.set('iv_manual_state', 0);\nflow.set('inverter_runmode', lastRunMode);\n\n// Derive H/M/S just for UI widgets\nlet H = Math.floor(lastTimeSecs / 3600);\nlet M = Math.floor((lastTimeSecs % 3600) / 60);\nlet S = lastTimeSecs % 60;\nflow.set('iv_H', H);\nflow.set('iv_M', M);\nflow.set('iv_S', S);\n\n// 5 outputs (as wired below)\nreturn [\n    { payload: lastMode },                             // -> Mode selector\n    { H, M, selectedMode: lastMode },                  // -> Timer Hours/Mins UI\n    { S, selectedMode: lastMode },                     // -> (Seconds UI - disabled)\n    { payload: lastRunMode, _external: true },         // -> Runmode buttons (normal/eco)\n    { topic: \"init\" }                                  // -> Status formatter\n];","outputs":5,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":610,"y":50,"wires":[["577d769ff53434a8"],["b71a12b7de60479b"],["a829b564d1a09b27"],["28d0a13641a5f1d5"],["76797bcad8d3026d"]]},{"id":"577d769ff53434a8","type":"ui_template","z":"aeb84866df2f5a22","group":"grp_main","name":"Mode Selector Buttons","order":4,"width":2,"height":1,"format":"<div style=\"text-align: center;\">\n  <div style=\"font-weight: bold; font-size: 13px; margin-bottom: 2px;\">Mode</div>\n  <div style=\"display: flex; justify-content: center; gap: 8px;\">\n    <button ng-click=\"select('manual')\" ng-class=\"{selected: selectedMode === 'manual'}\" class=\"nr-dashboard-button mode-button-vert\">\n      <i class=\"fa fa-power-off\" style=\"font-size:17px; margin-bottom:2px;\"></i>\n      <span style=\"font-size:12px; display:block;\">ON/OFF</span>\n    </button>\n    <button ng-click=\"select('timer')\" ng-class=\"{selected: selectedMode === 'timer'}\" class=\"nr-dashboard-button mode-button-vert\">\n      <i class=\"fa fa-clock-o\" style=\"font-size:17px; margin-bottom:2px;\"></i>\n      <span style=\"font-size:12px; display:block;\">TIMER</span>\n    </button>\n  </div>\n</div>\n<style>\n.mode-button-vert {\n  display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 3px 0 0 0; min-width: 58px; height: 46px; border-radius: 7px; border: 1.2px solid #aaa; background: #fff; font-size: 12px; font-weight: 600; transition: border-color 0.13s, background 0.13s; overflow: hidden; white-space: nowrap; }\n.mode-button-vert.selected {\n  background-color: #e8f3ff !important; border-color: #1b9fff !important; z-index: 2; }\n.mode-button-vert:focus {\n  outline: none; border-color: #005b99; }\n.mode-button-vert:hover {\n  border-color: #1b9fff; }\n</style>\n<script>\n(function(scope) {\n  scope.selectedMode = 'manual';\n  scope.select = function(mode) { scope.selectedMode = mode; scope.send({payload: mode}); };\n  scope.$watch('msg.payload', function(val) {\n    if (val === 'manual' || val === 'timer') { scope.selectedMode = val; }\n  });\n})(scope);\n</script>\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":335,"y":135,"wires":[["9ca3b6e359eca2cd"]]},{"id":"28d0a13641a5f1d5","type":"ui_template","z":"aeb84866df2f5a22","group":"grp_main","name":"Inverter Run Mode (Normal/Eco)","order":8,"width":2,"height":1,"format":"<div style=\"text-align: center;\">\n  <div style=\"font-weight: bold; font-size: 13px; margin-bottom: 2px;\">Inverter Mode</div>\n  <div style=\"display: flex; justify-content: center; gap: 8px;\">\n    <button ng-click=\"select('normal')\" ng-class=\"{'selected': selectedRunMode === 'normal', 'normal-btn': true}\" class=\"nr-dashboard-button runmode-btn normal-btn\">\n      <i class=\"fa fa-bolt\" style=\"font-size:17px; margin-bottom:2px;\"></i>\n      <span style=\"font-size:12px; display:block;\">Normal</span>\n    </button>\n    <button ng-click=\"select('eco')\" ng-class=\"{'selected': selectedRunMode === 'eco', 'eco-btn': true}\" class=\"nr-dashboard-button runmode-btn eco-btn\">\n      <i class=\"fa fa-leaf\" style=\"font-size:17px; margin-bottom:2px;\"></i>\n      <span style=\"font-size:12px; display:block;\">Eco</span>\n    </button>\n  </div>\n</div>\n<style>\n.runmode-btn { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 3px 0 0 0; min-width: 58px; height: 46px; border-radius: 7px; border: 2px solid #aaa; background: #fff; font-size: 12px; font-weight: 600; transition: border-color 0.13s, background 0.13s, color 0.13s; overflow: hidden; white-space: nowrap; }\n.normal-btn { border-color: #ff9800 !important; color: #ff9800 !important; }\n.eco-btn { border-color: #23b457 !important; color: #23b457 !important; }\n.normal-btn.selected { background: #fff7e5 !important; color: #ff9800 !important; border-color: #ff9800 !important; }\n.eco-btn.selected { background: #eafaf1 !important; color: #23b457 !important; border-color: #23b457 !important; }\n.runmode-btn:focus { outline: none; box-shadow: 0 0 0 2px #eee; }\n.normal-btn:hover, .normal-btn.selected:hover { background: #ffe4b2 !important; border-color: #e68a00 !important; color: #e68a00 !important; }\n.eco-btn:hover, .eco-btn.selected:hover { background: #d8f6e4 !important; border-color: #16a047 !important; color: #16a047 !important; }\n</style>\n<script>\n(function(scope) {\n  scope.$watch('msg.payload', function(val) { if (val === 'normal' || val === 'eco') { scope.selectedRunMode = val; } });\n  scope.select = function(runmode) { scope.send({payload: runmode}); };\n})(scope);\n</script>\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":335,"y":100,"wires":[["a998fa3e704062a2"]]},{"id":"9ca3b6e359eca2cd","type":"function","z":"aeb84866df2f5a22","name":"Store & Cancel on Mode Change","func":"// Store & Cancel on Mode Change (namespaced iv_*)\nconst prevMode = flow.get('iv_mode');\nconst newMode = msg.payload;\nflow.set('iv_mode', newMode);\n\nlet didChange = prevMode && prevMode !== newMode;\nlet inverterMsg = null;\n\n// Always notify Status Formatter\nconst statusMsg = { topic: \"mode_change\" };\n\nif (didChange) {\n    flow.set('iv_manual_state', 0);\n    flow.set('iv_timer_running', false);\n    flow.set('iv_timer_remaining', 0);\n    inverterMsg = { payload: 4 };  // OFF = 4\n}\nreturn [inverterMsg, statusMsg, Object.assign({}, msg, { selectedMode: newMode })];","outputs":3,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":610,"y":175,"wires":[["e4d198cdfe2f980c"],["76797bcad8d3026d"],["140bbdd49ce890fd","b71a12b7de60479b","a829b564d1a09b27"]]},{"id":"b71a12b7de60479b","type":"ui_template","z":"aeb84866df2f5a22","group":"grp_main","name":"Timer Hours Mins Row","order":5,"width":2,"height":1,"format":"<div style=\"display:flex;gap:8px;align-items:center;justify-content:center;height:100%;border:2px solid #888;border-radius:10px;padding:2px;\">\n  <div style=\"display:flex;flex-direction:column;align-items:center;\">\n    <label style=\"font-size:11px;font-weight:600;\">Hours</label>\n    <input type=\"number\" min=\"0\" max=\"23\" ng-model=\"H\" ng-change=\"changed('H')\"\n      ng-disabled=\"selectedMode !== 'timer'\"\n      style=\"width:42px;text-align:center;border-radius:5px;border:1.5px solid #aaa;font-size:17px;font-weight:bold;opacity:{{selectedMode !== 'timer' ? 0.5 : 1}};\">\n  </div>\n  <div style=\"display:flex;flex-direction:column;align-items:center;\">\n    <label style=\"font-size:11px;font-weight:600;\">Mins</label>\n    <input type=\"number\" min=\"0\" max=\"59\" step=\"5\" ng-model=\"M\" ng-change=\"changed('M')\"\n      ng-disabled=\"selectedMode !== 'timer'\"\n      style=\"width:42px;text-align:center;border-radius:5px;border:1.5px solid #aaa;font-size:17px;font-weight:bold;opacity:{{selectedMode !== 'timer' ? 0.5 : 1}};\">\n  </div>\n</div>\n<script>\n(function(scope){\n  function clamp(x,min,max){return Math.max(min,Math.min(max,Number(x)||0));}\n  function setFromMsg(msg) {\n    if(msg && typeof msg.H !== 'undefined') scope.H = Number(msg.H);\n    if(msg && typeof msg.M !== 'undefined') scope.M = Number(msg.M);\n    if(msg && typeof msg.selectedMode !== 'undefined') scope.selectedMode = msg.selectedMode;\n  }\n  setFromMsg(scope.msg); // set initial\n\n  scope.$watch('msg', function(msg) {\n    setFromMsg(msg);\n  });\n\n  scope.changed = function(which){\n    if(which===\"H\") scope.H = clamp(scope.H,0,23);\n    if(which===\"M\") scope.M = clamp(scope.M,0,59);\n    scope.send({topic:which,payload:scope[which]});\n  };\n})(scope);\n</script>\n","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":335,"y":175,"wires":[["2558c098b6dfd29c"]]},{"id":"a829b564d1a09b27","type":"ui_template","z":"aeb84866df2f5a22","d":true,"group":"grp_main","name":"Timer Seconds Row","order":6,"width":1,"height":1,"format":"<div style=\"display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;border:2px solid #888;border-radius:10px;padding:2px;\">\n  <label style=\"font-size:11px;font-weight:600;\">Sec</label>\n  <input type=\"number\" min=\"0\" max=\"59\" ng-model=\"S\" ng-change=\"changed('S')\" ng-disabled=\"selectedMode !== 'timer'\" style=\"width:42px;text-align:center;border-radius:5px;border:1.5px solid #aaa;font-size:17px;font-weight:bold;opacity:{{selectedMode !== 'timer' ? 0.5 : 1}};\">\n</div>\n<script>\n(function(scope){\n  function clamp(x,min,max){return Math.max(min,Math.min(max,Number(x)||0));}\n  function setFromMsg(msg) {\n    if(msg && typeof msg.S !== 'undefined') scope.S = Number(msg.S);\n    if(msg && typeof msg.selectedMode !== 'undefined') scope.selectedMode = msg.selectedMode;\n  }\n  setFromMsg(scope.msg); // set initial\n\n  scope.$watch('msg', function(msg) {\n    setFromMsg(msg);\n  });\n\n  scope.changed = function(which){\n    scope.S = clamp(scope.S,0,59);\n    scope.send({topic:which,payload:scope[which]});\n  };\n})(scope);\n</script>\n","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":335,"y":215,"wires":[["2558c098b6dfd29c"]]},{"id":"2558c098b6dfd29c","type":"function","z":"aeb84866df2f5a22","name":"Merge Timer Fields","func":"// H/M/S numeric input merge & set flow values (iv_ namespace)\nlet H = flow.get('iv_H') || 0;\nlet M = flow.get('iv_M') || 0;\nlet S = flow.get('iv_S') || 0;\nif(msg.topic === \"H\") H = Number(msg.payload) || 0;\nif(msg.topic === \"M\") M = Number(msg.payload) || 0;\nif(msg.topic === \"S\") S = Number(msg.payload) || 0;\nflow.set('iv_H', H);\nflow.set('iv_M', M);\nflow.set('iv_S', S);\nlet totalSeconds = H*3600 + M*60 + S;\nflow.set('iv_timer_value', totalSeconds);\n\nconst mode = flow.get('iv_mode') || 'manual';\n\nif (mode === \"timer\") {\n    flow.set('iv_timer_running', false);\n    flow.set('iv_timer_remaining', 0);\n    msg.payload = totalSeconds;\n    msg.topic = 'auto_reset';\n    msg.selectedMode = mode;\n    return msg; // only trigger updates in timer mode\n}\nreturn null;","outputs":1,"x":590,"y":225,"wires":[["140bbdd49ce890fd","b71a12b7de60479b","a829b564d1a09b27","76797bcad8d3026d"]]},{"id":"140bbdd49ce890fd","type":"function","z":"aeb84866df2f5a22","name":"Button Label Updater","func":"// Button Label Updater (namespaced, emits only on change)\nconst mode = flow.get('iv_mode') || 'manual';\nconst running = flow.get('iv_timer_running') || false;\n\n// Optionally update iv_manual_state from live inverter codes in msg.payload\nlet invOn = flow.get('iv_manual_state') || 0; // 1=ON, 0=OFF\nif (typeof msg.payload === \"number\" && [2,4,5].includes(msg.payload)) {\n    invOn = (msg.payload === 2 || msg.payload === 5) ? 1 : 0;\n    flow.set('iv_manual_state', invOn);\n}\n\nlet label;\nif (mode === 'manual') {\n    label = invOn ? \"Turn OFF\" : \"Turn ON\";\n} else {\n    label = running ? \"Cancel Timer\" : \"Start Timer\";\n}\n\nlet lastLabel = context.get('lastLabel');\nif (label !== lastLabel) {\n    context.set('lastLabel', label);\n    return { label };\n}\nreturn null;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":615,"y":280,"wires":[["fa337c10b2208fbb"]]},{"id":"fa337c10b2208fbb","type":"ui_template","z":"aeb84866df2f5a22","group":"grp_main","name":"Start/Cancel/Toggle Button","order":1,"width":"2","height":"1","format":"<div style=\"display:flex; justify-content:center; align-items:center; height:100%;\"><button ng-click=\"press()\" ng-class=\"[btnClass, pressed ? 'pressed' : '']\"class=\"custom-btn\"><span ng-bind=\"label\"></span></button></div><style>.custom-btn {width:100%; height:48px;border: 2px solid #888;border-radius: 10px;font-size: 18px;font-weight: bold;box-shadow: 0 2px 8px rgba(0,0,0,0.10);transition: all 0.13s cubic-bezier(.42,0,.58,1);outline: none;cursor: pointer;user-select: none;}.btn-green { background: #e6f9ed; color: #20722e; border-color: #23b457; }.btn-red { background: #fbeaea; color: #a12323; border-color: #e03e3e; }.btn-blue { background: #e6f3fb; color: #186b97; border-color: #2692d2; }.btn-orange { background: #fff2e0; color: #a55c00; border-color: #ff9800; }.custom-btn.pressed {filter: brightness(0.93);transform: scale(0.96);box-shadow: 0 1px 2px #bbb;border-width: 3px;}</style><script>(function(scope) {function setButton(label) {scope.label = label || \"Press\";scope.btnClass = \"btn-blue\";if(label === \"Start Timer\") scope.btnClass = \"btn-green\";else if(label === \"Cancel Timer\") scope.btnClass = \"btn-orange\";else if(label === \"Turn ON\") scope.btnClass = \"btn-green\";else if(label === \"Turn OFF\") scope.btnClass = \"btn-red\";}setButton(scope.msg && scope.msg.label);scope.$watch('msg', function(msg) {if(msg && msg.label) setButton(msg.label);});scope.pressed = false;scope.press = function() {scope.pressed = true;scope.send({payload: \"button\"});setTimeout(function(){scope.pressed = false;scope.$apply();}, 120);};})(scope);</script>","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":895,"y":280,"wires":[["579ded0c4e7d3056"]]},{"id":"579ded0c4e7d3056","type":"function","z":"aeb84866df2f5a22","name":"Prepare Trigger","func":"// Only trigger on digital input == 7 (e.g. closed, pressed, or \"ON\")\nif (msg.payload === 7) {\n    msg.trigger = true;\n    msg.source = \"digital\";\n    return msg;\n}\nif (msg.payload === \"button\") {\n    msg.trigger = true;\n    msg.source = \"button\";\n    return msg;\n}\nreturn null;","outputs":1,"x":545,"y":330,"wires":[["40ea291b1c282c1b"]]},{"id":"40ea291b1c282c1b","type":"function","z":"aeb84866df2f5a22","name":"Trigger Handler","func":"// Decide action based on mode and context (iv_* namespaced)\nif (!msg.trigger) return null;\nconst mode = flow.get('iv_mode') || 'timer';\nlet invOn = flow.get('iv_manual_state') || 0; // 1=ON, 0=OFF\nlet running = flow.get('iv_timer_running') || false;\nconst runmode = flow.get('inverter_runmode') || 'normal';\nlet ON_VALUE = runmode === 'eco' ? 5 : 2; // 5 = Eco, 2 = Normal\n\nif (mode === 'manual') {\n    // Toggle: 2/5 for ON, 4 for OFF\n    const nextVal = invOn ? 4 : ON_VALUE; // 4=OFF, 2/5=ON\n    invOn = nextVal === ON_VALUE ? 1 : 0;\n    flow.set('iv_manual_state', invOn);\n    flow.set('last_inverter_mode', nextVal, 'file');\n    return [\n        { payload: nextVal },\n        { payload: invOn ? `Inverter ON (${runmode === 'eco' ? \"Eco\" : \"Normal\"})` : \"Inverter OFF (Manual)\", topic: 'manual' },\n        null,                                   // no ui_control spam\n        { payload: \"update\" }                   // ping label updater\n    ];\n}\nif (running) {\n    flow.set('iv_timer_running', false);\n    flow.set('iv_timer_remaining', 0);\n    flow.set('last_inverter_mode', 4, 'file');\n    return [\n        { payload: 4 }, // OFF\n        { payload: \"Timer cancelled.\", topic: \"cancelled\" },\n        null,\n        { payload: \"update\" }\n    ];\n}\nconst seconds = flow.get('iv_timer_value') || 0;\nif (seconds <= 0) {\n    return [null, { payload: \"Set time first!\", topic: \"notset\" }, null, null];\n}\nflow.set('iv_timer_remaining', seconds);\nflow.set('iv_timer_running', true);\nflow.set('last_inverter_mode', ON_VALUE, 'file');\nreturn [\n    { payload: ON_VALUE, topic: \"start\", _first: true }, // 2 or 5\n    null,\n    null,\n    { payload: \"update\" }\n];","outputs":4,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1130,"y":100,"wires":[["8483edbeb6cb5689","e4d198cdfe2f980c","76797bcad8d3026d","270c187e36e0749b"],["76797bcad8d3026d"],[],["140bbdd49ce890fd"]]},{"id":"e4d198cdfe2f980c","type":"victron-output-inverter","z":"aeb84866df2f5a22","service":"com.victronenergy.inverter/277","path":"/Mode","serviceObj":{"service":"com.victronenergy.inverter/277","name":"Phoenix Inverter 12V 1200VA 230V"},"pathObj":{"path":"/Mode","type":"enum","name":"Mode","enum":{"1":"Charger only","2":"Inverter only","3":"On","4":"Off","5":"Low Power/Eco","251":"Passthrough","252":"Standby","253":"Hibernate"},"mode":"both"},"name":"Inverter Output","onlyChanges":false,"outputs":0,"x":1480,"y":100,"wires":[]},{"id":"8483edbeb6cb5689","type":"delay","z":"aeb84866df2f5a22","name":"1s Timer Tick","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"1","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":1020,"y":380,"wires":[["aa8a8a2287facb82"]]},{"id":"aa8a8a2287facb82","type":"function","z":"aeb84866df2f5a22","name":"Timer Countdown","func":"// Timer tick for countdown in timer mode (iv_* namespace)\nif (!flow.get('iv_timer_running')) {\n    return [null, null, null, { payload: \"update\" }];\n}\nlet remaining = flow.get('iv_timer_remaining') || 0;\nif (remaining > 1) {\n    remaining--;\n    flow.set('iv_timer_remaining', remaining);\n    return [\n        { _first: false }, // inverter stays on\n        { payload: remaining, topic: 'counting' },\n        null,\n        { payload: \"update\" }\n    ];\n} else {\n    flow.set('iv_timer_running', false);\n    flow.set('iv_timer_remaining', 0);\n    flow.set('last_inverter_mode', 4, 'file');\n    return [\n        { payload: 4 }, // OFF\n        { payload: 0, topic: 'done' },\n        null,\n        { payload: \"update\" }\n    ];\n}","outputs":4,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1240,"y":400,"wires":[["e4d198cdfe2f980c","8483edbeb6cb5689"],["76797bcad8d3026d"],["140bbdd49ce890fd"],[]]},{"id":"d024d30721db0f3e","type":"ui_template","z":"aeb84866df2f5a22","group":"grp_main","name":"Countdown Status (Only if Payload)","order":7,"width":"4","height":1,"format":"<div style=\"text-align:center; border: 2px solid #888; border-radius: 10px; padding: 6px; transition: all 0.3s ease;\">\n  <div style=\"font-size: 14px; font-weight: bold;\">Status</div>\n  <div style=\"font-size: 18px;\">\n    <i class=\"fa\"\n       ng-class=\"{\n         'fa-hourglass-start': msg.payload && msg.payload.indexOf('Ready') === 0,\n         'fa-hourglass-half': msg.payload && msg.payload.indexOf('Counting') === 0,\n         'fa-check-circle': msg.payload && msg.payload.indexOf('completed') !== -1,\n         'fa-ban': msg.payload && msg.payload.indexOf('cancelled') !== -1,\n         'fa-clock-o': msg.payload && msg.payload.indexOf('Set time first!') === 0\n       }\"\n       ng-style=\"{\n         color: \n           msg.payload && msg.payload.indexOf('Ready') === 0 ? '#23b457' : \n           msg.payload && msg.payload.indexOf('Counting') === 0 ? '#186b97' :\n           msg.payload && msg.payload.indexOf('completed') !== -1 ? '#23b457' :\n           msg.payload && msg.payload.indexOf('cancelled') !== -1 ? '#ca8600' :\n           msg.payload && msg.payload.indexOf('Set time first!') === 0 ? '#ff9800' :\n           '#888'\n       }\"\n       style=\"margin-right: 6px;\"></i>\n    <span ng-if=\"msg.payload && msg.payload.indexOf('Counting:') === 0\">\n      <span style=\"color:#186b97;font-weight:600;\">Counting:</span>\n      <span class=\"blinking-num blue-blink\" ng-if=\"msg.timerRunning\">\n        {{msg.payload.slice(10)}}\n      </span>\n      <span ng-if=\"!msg.timerRunning\" style=\"color:#186b97;font-weight:bold;\">\n        {{msg.payload.slice(10)}}\n      </span>\n    </span>\n    <span ng-if=\"msg.payload && msg.payload.indexOf('Ready:') === 0\">\n      <span style=\"color:#20722e;font-weight:600;\">Ready:</span>\n      <span style=\"color:#23b457;font-weight:bold;\">\n        {{msg.payload.slice(7)}}\n      </span>\n    </span>\n    <span ng-if=\"msg.payload && msg.payload.indexOf('Timer cancelled.') === 0\">\n      <span style=\"font-weight:600;color:#ca8600\">Timer cancelled.</span>\n      <span style=\"color:gray;\">\n        {{msg.payload.slice(msg.payload.indexOf('Ready:'))}}\n      </span>\n    </span>\n    <span ng-if=\"msg.payload && msg.payload.indexOf('Timer completed') === 0\">\n      <span style=\"color:#23b457;font-weight:700;\">&#10003; Timer completed</span>\n    </span>\n    <span ng-if=\"msg.payload && msg.payload.indexOf('Set time first!') === 0\">\n      <span style=\"color:#ff9800;font-weight:600;\">Set time first!</span>\n      <span style=\"color:#2692d2;\">\n        {{msg.payload.slice(msg.payload.indexOf('Ready:'))}}\n      </span>\n    </span>\n    <span ng-if=\"msg.payload && msg.payload.indexOf('Manual mode:') === 0\">\n  <span style=\"color:#888;font-weight:600;\">Manual Mode:</span>\n  <span style=\"color:#aaa;\">No timer running</span>\n</span>\n<span ng-if=\"!msg.payload || msg.payload === ''\">\n  <span style=\"color:#888;\">No status</span>\n</span>\n\n  </div>\n</div>\n<style>\n.blinking-num { animation: blinkNum 0.8s steps(2, start) infinite; }\n@keyframes blinkNum { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }\n.blue-blink { color: #186b97; font-weight: bold; }\n</style>\n","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":1810,"y":225,"wires":[[]]},{"id":"8b8c9a704b7af7be","type":"victron-input-inverter","z":"aeb84866df2f5a22","service":"com.victronenergy.inverter/277","path":"/Mode","serviceObj":{"service":"com.victronenergy.inverter/277","name":"Phoenix Inverter 12V 1200VA 230V"},"pathObj":{"path":"/Mode","type":"enum","name":"Mode","enum":{"1":"Charger only","2":"Inverter only","3":"On","4":"Off","5":"Low Power/Eco","251":"Passthrough","252":"Standby","253":"Hibernate"},"mode":"both"},"name":"Inverter State In","onlyChanges":false,"outputs":1,"x":255,"y":350,"wires":[["140bbdd49ce890fd","13aa493388b2e137"]]},{"id":"72c622c3b6ec6cec","type":"ui_template","z":"aeb84866df2f5a22","group":"grp_main","name":"Inverter Big ON/OFF","order":2,"width":"1","height":1,"format":"<div style=\"display: flex;flex-direction: column;align-items: center;justify-content: center;height: 100%;border: 2px solid #888;border-radius: 10px;padding: 10px 4px;\">\n  <div ng-if=\"msg.payload === 2\" style=\"color:#23b457;font-weight:bold;font-size:32px;line-height:1;\">ON</div>\n  <div ng-if=\"msg.payload === 5\" style=\"color:#2692d2;font-weight:bold;font-size:32px;line-height:1;\">ECO</div>\n  <div ng-if=\"msg.payload === 4\" style=\"color:#e03e3e;font-weight:bold;font-size:32px;line-height:1;\">OFF</div>\n</div>\n","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":360,"y":250,"wires":[[]]},{"id":"86e2957b9e5ef4a1","type":"victron-input-digitalinput","z":"aeb84866df2f5a22","service":"com.victronenergy.digitalinput/3","path":"/State","serviceObj":{"service":"com.victronenergy.digitalinput/3","name":"Input 3"},"pathObj":{"path":"/State","type":"enum","name":"Digital input state","enum":{"0":"low","1":"high","2":"off","3":"on","4":"no","5":"yes","6":"open","7":"closed","8":"ok","9":"alarm","10":"running","11":"stopped"}},"name":"Input 3","onlyChanges":true,"outputs":1,"x":360,"y":375,"wires":[["579ded0c4e7d3056"]]},{"id":"66e7c8465111aba0","type":"function","z":"aeb84866df2f5a22","name":"Auto Off Logic","func":"const threshold = flow.get('auto_off_threshold', 'file');\nif (typeof threshold !== \"number\" || isNaN(threshold)) return null; // Only run if set\n\nconst delay_mins = 1; // mins till auto shutdown after below threshold\nconst delay_ms = delay_mins * 60 * 1000;\nconst autoActive = flow.get('auto_off_active', 'file') || false;\nconst inverterOn = flow.get('iv_manual_state') || 0; // 1=ON, 0=OFF\n\nlet belowSince = context.get('belowSince') || 0;\nconst now = Date.now();\n\nif (!autoActive) {\n    if (belowSince) context.set('belowSince', 0);\n    flow.set('autoOffBelowSince', 0);\n    return null;\n}\nif (!inverterOn) {\n    if (belowSince) context.set('belowSince', 0);\n    flow.set('autoOffBelowSince', 0);\n    return null;\n}\nif (typeof msg.payload !== 'number') return null;\n\nif (msg.payload < threshold) {\n    if (!belowSince) {\n      belowSince = now;\n      context.set('belowSince', belowSince);\n      flow.set('autoOffBelowSince', belowSince);\n    }\n    if ((now - belowSince) >= delay_ms) {\n      context.set('belowSince', 0);\n      flow.set('autoOffBelowSince', 0);\n      node.warn('Auto-Off: Triggered due to low power.');\n      return {payload: 4}; // 4 = OFF for Victron\n    }\n    return null;\n} else {\n    if (belowSince) context.set('belowSince', 0);\n    flow.set('autoOffBelowSince', 0);\n    return null;\n}\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":755,"y":400,"wires":[["e4d198cdfe2f980c"]]},{"id":"a998fa3e704062a2","type":"function","z":"aeb84866df2f5a22","name":"Store RunMode to Flow","func":"// Update run mode (eco/normal). If inverter is ON in manual mode, push change to Victron.\nconst incoming = msg.payload === \"eco\" ? \"eco\" : \"normal\";\nconst prevRunmode = flow.get(\"inverter_runmode\") || 'normal';\nconst inverterOn = flow.get('iv_manual_state') || 0; // 1=ON, 0=OFF\nconst inverterMode = flow.get('iv_mode') || 'manual';\n\nlet lastUi = context.get('lastUi');\nconst shouldUiUpdate = (incoming !== lastUi);\n\nif (msg._external) {\n    if (incoming !== prevRunmode) flow.set(\"inverter_runmode\", incoming);\n    if (shouldUiUpdate) context.set('lastUi', incoming);\n    return [shouldUiUpdate ? { payload: incoming } : null, null];\n}\n\nif (incoming === prevRunmode) return null;\nflow.set(\"inverter_runmode\", incoming);\n\nif (inverterMode === 'manual' && inverterOn) {\n    let newVal = incoming === 'eco' ? 5 : 2;\n    flow.set('last_inverter_mode', newVal, 'file');\n    if (shouldUiUpdate) context.set('lastUi', incoming);\n    return [ shouldUiUpdate ? { payload: incoming } : null, { payload: newVal } ];\n}\n\nif (shouldUiUpdate) context.set('lastUi', incoming);\nreturn [shouldUiUpdate ? { payload: incoming } : null, null];","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":620,"y":125,"wires":[["28d0a13641a5f1d5"],["e4d198cdfe2f980c"]]},{"id":"13aa493388b2e137","type":"function","z":"aeb84866df2f5a22","name":"UI Only On Change","func":"// Only output to UI when inverter state changes\nlet state = msg.payload;\nlet lastState = context.get('lastState');\nif (state !== lastState) {\n    context.set('lastState', state);\n    return msg;\n}\nreturn null;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":295,"y":300,"wires":[["72c622c3b6ec6cec"]]},{"id":"5b941272f223e902","type":"ui_ui_control","z":"aeb84866df2f5a22","name":"","x":90,"y":50,"wires":[["d1978b9a4234f9dc"]]},{"id":"4292809185fc8fe6","type":"inject","z":"aeb84866df2f5a22","name":"Check N/A Timer","props":[],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"x":340,"y":625,"wires":[["87a22fb7ed9a51d8"]]},{"id":"cf5d4c6a11751b44","type":"function","z":"aeb84866df2f5a22","name":"Track Power Value","func":"// Pass through if numeric, 0 is valid\nif (typeof msg.payload === \"number\" && !isNaN(msg.payload)) {\n    flow.set(\"last_power_value\", msg.payload);\n    flow.set(\"last_power_time\", Date.now());\n    return [msg, null];\n}\nreturn [null, null];","outputs":2,"noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":585,"wires":[["1dda22bac960b3c8","43ce6ab3f6901043"],[]]},{"id":"87a22fb7ed9a51d8","type":"function","z":"aeb84866df2f5a22","name":"Power N/A Checker","func":"const now = Date.now();\nconst lastSeen = flow.get(\"last_power_time\") || 0;\nconst val = flow.get(\"last_power_value\");\nif (now - lastSeen > 5000 || typeof val === 'undefined') {\n    // Over 5s since last value\n    return { payload: \"N/A\" };\n} else {\n    return null; // Don't spam if still fresh\n}","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":560,"y":625,"wires":[["1dda22bac960b3c8"]]},{"id":"1dda22bac960b3c8","type":"ui_template","z":"aeb84866df2f5a22","group":"grp_main","name":"Inverter Power Display","order":3,"width":1,"height":1,"format":"<div style=\"display: flex;flex-direction: column;align-items: center;justify-content: center;height: 100%;border: 2px solid #888;border-radius: 10px;padding: 4px;\"><div style=\"font-size:11px;font-weight:600;text-align:center;width:100%;\">Power</div><div style=\"font-size:18px;font-weight:bold;text-align:center;line-height:1;margin-top:2px;\"><span ng-if=\"msg.payload === 'N/A'\" style=\"color:gray;\">N/A</span><span ng-if=\"msg.payload !== 'N/A'\" style=\"color:#186b97;\">{{msg.payload}}</span><div style=\"font-size:11px;font-weight:600;text-align:center;width:100%;\">Watts</div></div></div>","storeOutMessages":true,"fwdInMessages":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":800,"y":575,"wires":[[]]},{"id":"460c8b04352fa022","type":"ui_template","z":"aeb84866df2f5a22","group":"grp_main","name":"Auto Off Controls","order":9,"width":"2","height":1,"format":"<div style=\"text-align: center;\">\n  <div style=\"font-weight: bold; font-size: 13px; margin-bottom: 2px;\">Auto Off</div>\n  <div style=\"display: flex; justify-content: center; gap: 8px;\">\n    <button ng-click=\"toggleActive()\"\n            ng-class=\"{'mode-button-vert': true, 'selected': autoActive, 'inactive': !autoActive}\"\n            style=\"min-width: 58px; height: 46px; padding: 3px 0 0 0;\">\n      <i class=\"fa\" ng-class=\"autoActive ? 'fa-toggle-on' : 'fa-toggle-off'\" style=\"font-size:17px; margin-bottom:2px;\"></i>\n      <span style=\"font-size:12px; display:block;\">{{autoActive ? 'Active' : 'Inactive'}}</span>\n    </button>\n    <div ng-if=\"autoActive\" style=\"display:flex; flex-direction:column; align-items:center; justify-content:center;\">\n      <input type=\"number\" ng-model=\"threshold\" min=\"1\" max=\"1000\" step=\"1\" ng-blur=\"saveIfChanged()\"\n        placeholder=\"W\"\n        style=\"width:52px; text-align:center; font-size:17px; font-weight:bold; border-radius:5px; border:1.2px solid #aaa; margin-bottom:1px; height:32px; background:#fff;\">\n      <span style=\"font-size:12px; font-weight:600; margin-top:2px;\">Watts</span>\n    </div>\n  </div>\n</div>\n<style>\n.mode-button-vert {\n  display: flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 7px; border: 1.2px solid #aaa; background: #fff; font-size: 12px; font-weight: 600; transition: border-color 0.13s, background 0.13s; overflow: hidden; white-space: nowrap;\n}\n.mode-button-vert.selected {\n  background-color: #e6f9ed !important; border-color: #23b457 !important; color: #20722e !important; z-index: 2;\n}\n.mode-button-vert.inactive {\n  background-color: #fbeaea !important; border-color: #e03e3e !important; color: #a12323 !important;\n}\n.mode-button-vert:focus { outline: none; border-color: #005b99; }\n.mode-button-vert:hover { border-color: #1b9fff; }\n</style>\n<script>\n(function(scope){\n  // Loads from incoming msg\n  function loadFromMsg(msg) {\n    if(msg && typeof msg.threshold !== 'undefined') scope.threshold = msg.threshold;\n    if(msg && typeof msg.autoActive !== 'undefined') scope.autoActive = msg.autoActive;\n    if(typeof scope.prevThreshold === 'undefined' || isNaN(scope.prevThreshold)) scope.prevThreshold = scope.threshold;\n  }\n  scope.$watch('msg', function(msg){\n    loadFromMsg(msg);\n  });\n  scope.toggleActive = function(){\n    scope.autoActive = !scope.autoActive;\n    scope.send({topic: \"auto_off_active\", payload: scope.autoActive});\n  };\n  scope.saveIfChanged = function() {\n    let val = Number(scope.threshold);\n    if (!isNaN(val) && val > 0 && val !== scope.prevThreshold) {\n      scope.send({topic: \"auto_off_threshold\", payload: val});\n      scope.prevThreshold = val;\n    } else if (isNaN(val) || val <= 0) {\n      scope.threshold = scope.prevThreshold;\n    }\n  };\n})(scope);\n</script>\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":210,"y":450,"wires":[["32b01f42466e0341"]]},{"id":"32b01f42466e0341","type":"function","z":"aeb84866df2f5a22","name":"Store & Recall AutoOff State (persistent, default=30)","func":"// --- CHANGE DEFAULT HERE! ---\nconst DEFAULT_THRESHOLD = 30; // <<-- Change default threshold here\n// --- END ---\n\n// 1. Store any changes sent by the UI\nif (msg && msg.hasOwnProperty('topic')) {\n    if (msg.topic === \"auto_off_active\") {\n        flow.set('auto_off_active', !!msg.payload, 'file');\n    }\n    if (msg.topic === \"auto_off_threshold\") {\n        let n = Number(msg.payload);\n        if (typeof n === 'number' && !isNaN(n) && n > 0) {\n            flow.set('auto_off_threshold', n, 'file');\n        }\n    }\n}\n\n// 2. Always output latest state to the UI\nlet active = flow.get('auto_off_active', 'file');\nif (typeof active !== 'boolean') active = false;\nlet thresh = flow.get('auto_off_threshold', 'file');\n\n// *** SET DEFAULT IF NOT SET ***\nif (typeof thresh !== 'number' || isNaN(thresh) || thresh <= 0) {\n    thresh = DEFAULT_THRESHOLD;\n    flow.set('auto_off_threshold', thresh, 'file'); // <- Save it!\n}\n\nreturn [{ threshold: thresh, autoActive: active }];","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":450,"wires":[["460c8b04352fa022"]]},{"id":"76797bcad8d3026d","type":"function","z":"aeb84866df2f5a22","name":"Status Formatter","func":"// Status Formatter (iv_* namespace)\nconst mode = flow.get('iv_mode') || 'timer';\nconst running = flow.get('iv_timer_running');\nconst remaining = flow.get('iv_timer_remaining') || 0;\nconst last_set = flow.get('iv_timer_value') || 0;\n\nfunction hms(val) {\n    let h = String(Math.floor(val / 3600)).padStart(2, '0');\n    let m = String(Math.floor((val % 3600) / 60)).padStart(2, '0');\n    let s = String(val % 60).padStart(2, '0');\n    return `${h}:${m}:${s}`;\n}\n\nlet payload = \"\";\nif (mode === 'manual') {\n    payload = \"Manual mode: No timer running\";\n} else if (msg.topic === 'auto_reset') {\n    flow.set('iv_timer_running', false);\n    flow.set('iv_timer_remaining', 0);\n    payload = `Ready: ${hms(last_set)}`;\n} else if (msg.topic === 'start') {\n    payload = `Counting: ${hms(remaining)}`;\n} else if (msg.topic === 'counting') {\n    payload = `Counting: ${hms(Number(msg.payload))}`;\n} else if (msg.topic === 'done') {\n    payload = \"Timer completed\";\n} else if (msg.topic === 'cancelled') {\n    payload = `Timer cancelled. Ready: ${hms(last_set)}`;\n} else if (msg.topic === 'notset') {\n    payload = `Set time first! Ready: ${hms(last_set)}`;\n} else {\n    payload = mode === 'manual' ? \"Manual mode: No timer running\" : `Ready: ${hms(last_set)}`;\n}\n\ncontext.set('last', { payload, running: !!running });\nreturn { payload, timerRunning: !!running };","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1490,"y":280,"wires":[["d024d30721db0f3e"]]},{"id":"ce0969939a85b127","type":"victron-input-acload","z":"aeb84866df2f5a22","service":"com.victronenergy.acload/31","path":"/Ac/L1/Power","serviceObj":{"service":"com.victronenergy.acload/31","name":"AC Load meter ET112","communityTag":"acload"},"pathObj":{"path":"/Ac/L1/Power","type":"float","name":"L1 Power (W)"},"name":"AC Load","onlyChanges":false,"roundValues":"1","outputs":1,"conditionalMode":false,"outputTrue":"","outputFalse":"","debounce":"","x":115,"y":525,"wires":[["cf5d4c6a11751b44","66e7c8465111aba0"]]},{"id":"43ce6ab3f6901043","type":"link out","z":"aeb84866df2f5a22","name":"link out 1","mode":"link","links":["b9fe9fae74f6a960"],"x":850,"y":620,"wires":[],"l":true},{"id":"270c187e36e0749b","type":"debug","z":"aeb84866df2f5a22","name":"debug 4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1400,"y":40,"wires":[]},{"id":"a5fa4020bf5fc763","type":"link in","z":"aeb84866df2f5a22","name":"Inverter Manual link in","links":["ff3c4806f32cc1ed"],"x":1740,"y":100,"wires":[[]],"l":true},{"id":"475d7b4f0c2afd15","type":"link in","z":"aeb84866df2f5a22","g":"4956470aae6b5550","name":"context command in - Inverter","links":["c181e5ef8e85ba40"],"x":180,"y":720,"wires":[["fc54b65d135b6024"]],"l":true},{"id":"fc54b65d135b6024","type":"function","z":"aeb84866df2f5a22","g":"4956470aae6b5550","name":"Inverter context target","func":"const TARGET = \"inverter\";\nif (msg.contextTarget !== TARGET) return null;\n\nfunction safeSet(scope, key, value, store) {\n    try {\n        if (store) scope.set(key, value, store);\n        else scope.set(key, value);\n        return {ok:true};\n    } catch (e) {\n        return {ok:false, error:String(e && e.message ? e.message : e)};\n    }\n}\nfunction safeKeys(scope, store) {\n    try {\n        return store ? scope.keys(store) : scope.keys();\n    } catch (e) {\n        return {error:String(e && e.message ? e.message : e)};\n    }\n}\nfunction clearScope(store) {\n    const keys = safeKeys(flow, store);\n    if (!Array.isArray(keys)) return {ok:false, error:keys.error || \"Could not list keys\", count:0, result:[]};\n    const result = [];\n    for (const key of keys) result.push({key:key, result:safeSet(flow, key, undefined, store)});\n    return {ok:true, count:keys.length, result:result};\n}\n\nconst action = String(msg.contextAction || \"\");\nlet payload;\nlet toast;\n\nif (action === \"show\") {\n    payload = {target:TARGET, action:\"show\", time:new Date().toISOString(), note:\"Receiver must be on Inverter tab.\", flow_ram_keys:safeKeys(flow, null), flow_file_keys:safeKeys(flow, \"file\")};\n    toast = \"Inverter flow keys shown.\";\n} else if (action === \"clear_ram\") {\n    if (msg.confirmClear !== \"CTX_REMOTE_CLEAR_OK\") return null;\n    payload = {target:TARGET, action:\"clear_ram\", time:new Date().toISOString(), note:\"Cleared flow RAM where this receiver lives.\", result:clearScope(null)};\n    toast = \"Cleared Inverter FLOW RAM.\";\n} else if (action === \"clear_file\") {\n    if (msg.confirmClear !== \"CTX_REMOTE_CLEAR_OK\") return null;\n    payload = {target:TARGET, action:\"clear_file\", time:new Date().toISOString(), note:\"Cleared flow FILE where this receiver lives.\", result:clearScope(\"file\")};\n    toast = \"Cleared Inverter FLOW FILE.\";\n} else {\n    payload = {target:TARGET, ok:false, error:\"Unknown target action\", action:action};\n    toast = \"Unknown Inverter context action.\";\n}\n\nmsg.payload = payload;\nmsg.toast = toast;\nmsg.topic = \"context/result/\" + TARGET;\nnode.status({fill:action === \"show\" ? \"blue\" : \"orange\", shape:\"dot\", text:action});\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":410,"y":720,"wires":[["18dd171d28d20efb","0e86650f5daaad7a"]]},{"id":"18dd171d28d20efb","type":"link out","z":"aeb84866df2f5a22","g":"4956470aae6b5550","name":"context result out - Inverter","mode":"link","links":["78f7a23582d43eab"],"x":700,"y":720,"wires":[],"l":true},{"id":"0e86650f5daaad7a","type":"debug","z":"aeb84866df2f5a22","g":"4956470aae6b5550","name":"Inverter context target result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":730,"y":760,"wires":[]},{"id":"grp_main","type":"ui_group","name":"Inverter V5.0","tab":"dashboard_tab","order":3,"disp":true,"width":"4","collapse":true,"className":""},{"id":"dashboard_tab","type":"ui_tab","name":"Main","icon":"developer_dashboard","order":1,"disabled":false,"hidden":false},{"id":"e62419efacc1e6fa","type":"global-config","env":[],"modules":{"node-red-dashboard":"3.6.6","@victronenergy/node-red-contrib-victron":"1.6.64"}}]

2 versus 4 doesn’t sound much but it’s pretty annoying having to do it every time we want to use the hob/kettle or air fryer in the van.

It’s the fact you’re having to tap all over the screen too. I’m going to wire up a switch to one of the Digital Inputs and hook that up to the inverter on/off instead.

So I purchased an off brand Neural Link on Temu and found some bio hackers on the dark web to help me wire it into my brain. Once I get the Node Red flow right it should be able to predict when I want to turn my inverter off and on by slight fluctuations in my dopamine and serotonin levels. By my calculations this should be the equivalent of -3 clicks.

(The above statement is meant to be light hearted satire as indicated by this emoji :grinning_face: . It is in no way meant to insult anyone or tread on anyone’s ego. It was done purely for fun)