[{"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;\">✓ 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"}}]