Shelly RGBW Loop controller
- Home
- Scripting & Virtual components
- Shelly RGBW Loop controller
Shelly RGBW Loop controller
It is possible to create, edit, and run RGBW color loop presets directly from a Shelly device using scripting, a built-in web UI, input actions, and virtual components.
This script turns the Shelly device into a local RGBW loop controller. With the built-in web editor, it is easy to create and edit presets, combine manual color control, and run preset-based automations.
Requirements & Notes
- A Shellly Pro RGBWW PM (Device profileRGB and lights x 2)
- Or Shelly Plus RGBW PM (Device profile RGBW)
- Or Shelly RGBCCT Bulp Gen3
- Tested with Firmware 1.7.5
Notes.
This script uses a significant amount of device resources. Avoid running other scripts at the same time.Web interface access:
http://<ip-address>/script/<script-id>/ui
Example:http://192.168.2.197/script/1/ui
Features
Main page (UI)
Manual Color
Set the exact color you want.
Color Loop
- Choose from predefined presets or manually created presets
- Adjust brightness and speed of the active preset
Warning:
If a preset is already running at its maximum speed, increasing the speed further may cause the script or device to crash.
Input Settings
Configure what each input press should do.
Available actions:
- Toggle – Switches between the selected preset and off
- Preset – Always activates the selected preset
- Cycle – Cycles through selected presets on each activation
- Dim (long press only) – Adjusts RGBW brightness up or down while held
Note:
To avoid conflicts with default input you should set input to detached mode.
Saving settings while a fast-running preset is active may cause the script or device to crash.
Editor
- Edit built-in or custom presets
- Create new presets
- Code view for easy copy/paste of presets
Virtual Button integration (Only Pro RGBWW and RGBCCT Bulp)
- Each preset can be assigned a Virtual Button ID (
btn:201–208) - Pressing a Virtual Button starts the assigned preset
btn:200is reserved for OFF
Additional notes
- The script automatically enables auto start
- The script renames the output to show the web interface URL
- All settings and custom presets are stored in KVS (Key-Value Storage)
- The script is resource-intensive and should not be run alongside other scripts
Script for Pro RGBWW PM
Shelly Pro RGBWW Loop Controller (Click to expand)
// Pro RGBW Loop Controller
// Ver. 1.0
// UI: http://<ip>/script/<id>/ui
// Editor: http://<ip>/script/<id>/edit
// Creator Ronni.N - Shelly Nordics
// ─── CONFIG ───────────────────────────────────────────────────────────────────
var CONFIG={preset:"Rainbow",speed:1.0,id:0,autostart:false,brightness:100};
// ─── DIM ──────────────────────────────────────────────────────────────────────
var DIM={dir:1,timer:null,active:false,wasRunning:false};
function startDim(){
if(DIM.active)return;
DIM.active=true;DIM.dir=-DIM.dir;DIM.wasRunning=running;
if(running)pauseLoop();
dimStep();
}
function stopDim(){
DIM.active=false;
if(DIM.timer){Timer.clear(DIM.timer);DIM.timer=null;}
if(DIM.wasRunning){running=true;stepIndex=0;nextStep();}
}
function dimStep(){
if(!DIM.active)return;
CONFIG.brightness=Math.max(1,Math.min(100,CONFIG.brightness+DIM.dir*2));
Shelly.call("RGB.Set",{id:CONFIG.id,brightness:CONFIG.brightness},null);
if(CONFIG.brightness<=1||CONFIG.brightness>=100){stopDim();return;}
DIM.timer=Timer.set(150,false,dimStep);
}
// ─── SETTINGS ─────────────────────────────────────────────────────────────────
var SETTINGS={
input0_short: {action:"toggle",preset:"Rainbow",cycle:[]},
input0_double:{action:"none", preset:"last", cycle:[]},
input0_triple:{action:"none", preset:"last", cycle:[]},
input0_long: {action:"none", preset:"last", cycle:[]},
input1_short: {action:"none", preset:"last", cycle:[]},
input1_double:{action:"none", preset:"last", cycle:[]},
input1_triple:{action:"none", preset:"last", cycle:[]},
input1_long: {action:"none", preset:"last", cycle:[]},
input2_short: {action:"none", preset:"last", cycle:[]},
input2_double:{action:"none", preset:"last", cycle:[]},
input2_triple:{action:"none", preset:"last", cycle:[]},
input2_long: {action:"none", preset:"last", cycle:[]},
input3_short: {action:"none", preset:"last", cycle:[]},
input3_double:{action:"none", preset:"last", cycle:[]},
input3_triple:{action:"none", preset:"last", cycle:[]},
input3_long: {action:"none", preset:"last", cycle:[]},
};
var _inputHandler=null;
function applyInputHandlers(){
if(_inputHandler!==null){Shelly.removeEventHandler(_inputHandler);_inputHandler=null;}
_inputHandler=Shelly.addEventHandler(function(e){
var info=e.info||e,comp=info.component||"",ev=info.event||"",inp=-1;
if(comp==="input:0")inp=0;
else if(comp==="input:1")inp=1;
else if(comp==="input:2")inp=2;
else if(comp==="input:3")inp=3;
if(inp>=0){
var key="input"+inp+"_",longCfg=SETTINGS[key+"long"];
if(ev==="single_push") {handleAction(SETTINGS[key+"short"]);}
else if(ev==="double_push") {handleAction(SETTINGS[key+"double"]);}
else if(ev==="triple_push") {handleAction(SETTINGS[key+"triple"]);}
else if(ev==="long_push"&&longCfg&&longCfg.action==="dim") {startDim();}
else if(ev==="btn_up" &&longCfg&&longCfg.action==="dim") {stopDim();}
return;
}
if(ev==="single_push"&&comp.indexOf("button:")===0){
var vcId=parseInt(comp.slice(7));
if(vcId===200){stopLoop();return;}
for(var nm in VCMAP){
if(VCMAP[nm]===vcId){setPreset(nm,true);return;}
}
}
});
}
function handleAction(cfg){
if(!cfg)return;
var act=cfg.action||"";
if(act==="toggle"){
if(running)stopLoop();
else{if(cfg.preset&&(cfg.preset in PRESETS))CONFIG.preset=cfg.preset;startLoop();}
}
else if(act==="preset"){
if(cfg.preset&&(cfg.preset in PRESETS)){setPreset(cfg.preset,!running);}
}
else if(act==="cycle"){
var pool=(cfg.cycle&&cfg.cycle.length>0)?cfg.cycle:Object.keys(PRESETS).filter(function(k){return k!=="manuel"&&k!=="_preview";});
var next=pool[(pool.indexOf(CONFIG.preset)+1)%pool.length];
setPreset(next,!running);
}
}
// ─── PRESETS ──────────────────────────────────────────────────────────────────
var PRESETS={
Rainbow: "255,0,0,0,100,2|255,127,0,0,100,2|0,255,0,0,100,2|0,0,255,0,100,2|148,0,211,0,100,2",
Candle: "255,100,10,20,70,1.4|255,80,5,10,55,1.3|255,120,20,25,80,1.5",
Ocean: "0,50,255,4,20,1.5|0,180,191,4,30,1.5|0,100,255,2,15,1.5",
Sunset: "92,43,50,0,41,3|145,75,20,2,88,7.5|255,208,36,0,100,10",
Party: "255,0,0,0,100,0.5|0,255,0,0,100,0.5|0,0,255,0,100,0.5|255,255,0,0,100,0.5",
Nordic: "0,255,120,0,60,5|100,0,200,0,70,5|50,255,150,0,65,6",
Breath_White: "200,220,255,255,100,3|200,220,255,255,5,3",
Breath_Red: "255,0,0,0,100,2.5|255,0,0,0,5,2.5",
Breath_Green: "0,255,0,0,100,2.5|0,255,0,0,5,2.5",
Breath_Blue: "0,0,255,0,100,2.5|0,0,255,0,5,2.5",
Breath_Yellow:"255,200,0,0,100,2.5|255,200,0,0,5,3",
Breath_Purple:"148,0,211,0,100,2.5|148,0,211,0,5,2.5",
Breath_RGB: "255,0,0,0,100,1.5|255,0,0,0,5,1.5|0,255,0,0,100,1.5|0,255,0,0,5,1.5|0,0,255,0,100,1.5|0,0,255,0,5,1.5",
manuel: "255,0,0,0,100,1",
};
// ─── LOOP ─────────────────────────────────────────────────────────────────────
var stepIndex=0,loopTimer=null,running=false;
function nextStep(){
if(!running)return;
var raw=PRESETS[CONFIG.preset];
if(!raw)return;
if(typeof raw==="string"){raw=decPreset(raw);PRESETS[CONFIG.preset]=raw;}
var steps=raw;
if(!steps||!steps.length)return;
var s=steps[stepIndex%steps.length];
var br=Math.round(s.br*CONFIG.brightness/100);if(br<1)br=1;
Shelly.call("RGB.Set",{id:CONFIG.id,on:true,rgb:[s.r,s.g,s.b],brightness:br,transition_duration:s.dur/CONFIG.speed},null);
Shelly.call("Light.Set",{id:CONFIG.id,on:s.w>0,brightness:s.w},null);
stepIndex++;
loopTimer=Timer.set(Math.round(s.dur/CONFIG.speed*1000),false,nextStep);
}
function startLoop(){if(running)return;running=true;stepIndex=0;nextStep();}
function stopLoop(){running=false;if(loopTimer){Timer.clear(loopTimer);loopTimer=null;}Shelly.call("RGB.Set",{id:CONFIG.id,on:false},null);Shelly.call("Light.Set",{id:CONFIG.id,on:false},null);}
function pauseLoop(){running=false;if(loopTimer){Timer.clear(loopTimer);loopTimer=null;}}
function setPreset(name,autostart){
if(!(name in PRESETS))return false;
if(PRESETS[name]===null){
Shelly.call("KVS.Get",{key:"rgbw_preset_"+name},function(r,e){
if(!e&&r&&r.value)PRESETS[name]=decPreset(r.value);
if(PRESETS[name]){
CONFIG.preset=name;stepIndex=0;
if(running){if(loopTimer)Timer.clear(loopTimer);nextStep();}
else if(autostart){running=true;nextStep();}
}
},null);
return true;
}
if(typeof PRESETS[name]==="string")PRESETS[name]=decPreset(PRESETS[name]);
CONFIG.preset=name;stepIndex=0;
if(running){if(loopTimer)Timer.clear(loopTimer);nextStep();}
else if(autostart){running=true;nextStep();}
return true;
}
var VCMAP={};
function loadVCMap(){
Shelly.call("KVS.Get",{key:"rgbw_vcmap"},function(r,e){
if(!e&&r&&r.value){try{VCMAP=JSON.parse(r.value);}catch(ex){VCMAP={};}}
},null);
}
function updateVC(name){
}
// ─── KVS ──────────────────────────────────────────────────────────────────────
// Settings: "action,preset,cycle1:cycle2|action,preset|action|action" (4 parts, pipe-sep)
// Preset: "r,g,b,w,br,dur|r,g,b,w,br,dur|..."
function encPreset(steps){
var out=[];
for(var i=0;i<steps.length;i++){var s=steps[i];out.push(s.r+","+s.g+","+s.b+","+s.w+","+s.br+","+s.dur);}
return out.join("|");
}
function decPreset(v){
var parts=v.split("|"),out=[];
for(var i=0;i<parts.length;i++){var f=parts[i].split(",");if(f.length<6)continue;out.push({r:+f[0],g:+f[1],b:+f[2],w:+f[3],br:+f[4],dur:+f[5]});}
return out;
}
function loadKVS(){
var done=0,types=["short","double","triple","long"];
function onDone(){if(++done===4)applyInputHandlers();}
function parseSetting(n,v){
var pts=v.split("|");
for(var k=0;k<4;k++){
var p=(pts[k]||"none").split(",");
SETTINGS["input"+n+"_"+types[k]]={action:p[0]||"none",preset:p[1]||"last",cycle:p[2]?p[2].split(":"):[]};
}
}
for(var i=0;i<4;i++){
(function(n){
Shelly.call("KVS.Get",{key:"setting_i"+n},function(r,e){
if(!e&&r&&r.value)parseSetting(n,r.value);
onDone();
},null);
})(i);
}
Shelly.call("KVS.GetMany",{match:"rgbw_preset_*"},function(r,e){
if(e||!r||!r.items)return;
for(var i=0;i<r.items.length;i++){
var item=r.items[i];
if(!item||!item.key||item.key.indexOf("rgbw_preset_")!==0)continue;
var nm=item.key.slice(12);
if(nm)PRESETS[nm]=null;
}
loadVCMap();
},null);
}
// ─── HELPERS ──────────────────────────────────────────────────────────────────
function parseQuery(q){
var out={};if(!q)return out;
var parts=q.split("&");
for(var i=0;i<parts.length;i++){var kv=parts[i].split("=");if(kv.length===2)out[kv[0]]=kv[1];}
return out;
}
function send(res,body,ct){
res.code=200;
res.headers=[["Content-Type",(ct||"text/plain")+"; charset=utf-8"]];
res.body=body;res.send();
}
// ─── CSS ──────────────────────────────────────────────────────────────────────
function buildCSS(){
return 'body{font:14px/1.5 system-ui,sans-serif;background:#161616;color:#e0e0e0;margin:0;padding:0}'
+'.wrap{max-width:500px;margin:0 auto;padding:12px;display:flex;flex-direction:column;gap:8px}'
+'.topbar{background:#222222;border-radius:5px;padding:12px 16px;display:flex;justify-content:space-between;align-items:center}'
+'.topbar b{font-size:17px;font-weight:900}'
+'.card{background:#222222;border-radius:5px;padding:14px 16px}'
+'.cardtitle{font-size:12px;font-weight:700;letter-spacing:2px;text-transform:uppercase;opacity:1;margin-bottom:12px}'
+'a.btn,button.btn{font-size:13px;font-weight:600;padding:5px 12px;background:#1095c1;color:#edf0f3;border:1px solid #00aeef44;border-radius:3px;text-decoration:none;cursor:pointer}'
+'.r{display:grid;grid-template-columns:90px 1fr 46px;align-items:center;gap:8px;margin:8px 0}'
+'.l{font-size:12px;color:#c7c7c7}'
+'.v{font-size:12px;font-weight:400;color:#fff;text-align:right}'
+'input[type=range]{width:100%;height:4px;cursor:pointer;background:#333;border-radius:2px;appearance:none;-webkit-appearance:none}'
+'button{padding:6px 12px;cursor:pointer;background:#252627;color:#ccc;border:1px solid #828282;font:14px system-ui;border-radius:7px}'
+'button:hover{background:#2e3032}'
+'button.on{background:#141414;border-color:#00aeef;color:#00aeef}'
+'.acts{display:flex;gap:6px;margin-top:8px}'
+'.acts button{flex:1;padding:9px;font-size:13px;font-weight:700;border-radius:8px}'
+'#bst{background:#0f1f10;color:#4d4;border-color:#1e3e1e}'
+'#bsp{background:#1f0f0f;color:#d44;border-color:#3e1e1e}'
+'.savebtn{background:#0f1f10;color:#4d4;border-color:#1e3e1e}'
+'.delbtn{background:#1f0f0f;color:#d44;border-color:#3e1e1e}'
+'#lg{font-size:10px;font-family:monospace;color:#555;max-height:40px;overflow-y:auto;background:#111214;padding:6px 10px;border-radius:7px}'
+'.step{background:#2d2d2d;border:1px solid #222;border-radius:8px;padding:10px 12px;margin:6px 0}'
+'.step-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}'
+'.step-num{font-size:12px;font-weight:700;color:#c7c7c7}'
+'.swatch{width:26px;height:26px;border-radius:5px;border:1px solid #333;flex-shrink:0}'
+'.sr2{display:grid;grid-template-columns:28px 1fr 38px;align-items:center;gap:6px;margin:8px 0}'
+'.fl{font-size:12px;color:#c7c7c7}'
+'.sv{font-size:11px;color:#ccc;text-align:right}'
+'#psel{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px}'
+'#psel button{font-size:13px;padding:6px 12px}'
+'#psel button.custom{border-color:#fa4}'
+'#pbtn.on{background:#002a38;color:#00aeef;border-color:#00aeef}'
+'#cbtn.on{background:#252627;color:#ccc;border-color:#828282}'
+'input[type=text]{width:100%;box-sizing:border-box;background:#161616;border:1px solid #333;color:#eee;padding:6px 10px;border-radius:6px;font:13px system-ui}'
+'select{width:100%;background:#161616;border:1px solid #333;color:#eee;padding:5px 8px;border-radius:6px;font:13px system-ui}'
+'#vcsel{width:auto;border:1px solid #828282 !important}'
+'.row3{display:grid;grid-template-columns:90px 1fr 1fr;align-items:center;gap:8px;margin:5px 0}'
+'.lbl2{font-size:12px;color:#888}'
+'.cycle-wrap{display:flex;flex-wrap:wrap;gap:4px;margin:4px 0 8px}'
+'.cycle-wrap label{display:flex;align-items:center;gap:4px;font-size:12px;background:#111214;border:1px solid #333;border-radius:6px;padding:3px 8px;cursor:pointer}'
+'.cycle-wrap input{accent-color:#00aeef}'
+'#detachbtn{background:#1095c1;color:#edf0f3;border:1px solid #00aeef44;border-radius:6px;padding:8px 16px;font-size:13px;font-weight:600;cursor:pointer;transition:background 0.2s}'
+'#detachbtn:hover{background:#0d7ea3}'
+'#detachbtn.done{background:#1a4a1a;color:#4d4;border-color:#4d4}';
}
// ─── MAIN UI ──────────────────────────────────────────────────────────────────
function buildHTML(sid){
return '<!DOCTYPE html><html><head><meta charset=UTF-8><meta name=viewport content="width=device-width,initial-scale=1"><title>RGBW</title>'
+'<link rel=stylesheet href="/script/'+sid+'/asset?f=css"></head><body><div class=wrap>'
+'<div class=topbar>'
+'<div><b>Pro RGBW Loop Controller</b><div id=mt style=font-size:12px;opacity:.6;margin-top:2px>Status: ...</div></div>'
+'<a href="/script/'+sid+'/asset?f=settings" class=btn>Input Settings</a></div>'
+'<div class=card><div class=cardtitle>Manual color</div>'
+'<div class=r><span class=l>Red</span><input type=range id=R min=0 max=255 value=255 style=accent-color:#fa4141 oninput=upd()><span class=v id=vR>255</span></div>'
+'<div class=r><span class=l>Green</span><input type=range id=G min=0 max=255 value=0 style=accent-color:#41fa6c oninput=upd()><span class=v id=vG>0</span></div>'
+'<div class=r><span class=l>Blue</span><input type=range id=B min=0 max=255 value=0 style=accent-color:#06f oninput=upd()><span class=v id=vB>0</span></div>'
+'<div class=r><span class=l>White</span><input type=range id=W min=0 max=100 value=0 style=accent-color:#ebebeb oninput=upd()><span class=v id=vW>0</span></div>'
+'<div class=r><span class=l>Brightness</span><input type=range id=BR min=1 max=100 value=100 style=accent-color:#ffe68c oninput=upd()><span class=v id=vBR>100</span></div>'
+'<div class=acts><button onclick=sendColor()>Send color</button><button onclick=turnOff()>Off</button></div></div>'
+'<div class=card>'
+'<div style=display:flex;justify-content:space-between;align-items:center;margin-bottom:12px>'
+'<span class=cardtitle style=margin-bottom:0>Color loop</span>'
+'<a href="/script/'+sid+'/edit" class=btn>Edit</a></div>'
+'<div id=pb style=display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px></div>'
+'<div class=r><span class=l>Brightness</span><input type=range id=LBR min=1 max=100 value=100 style=accent-color:#ffe68c oninput=sendBr()><span class=v id=vLBR>100</span></div>'
+'<div style="background:#1a1500;border:1px solid #3a2e00;border-radius:8px;padding:8px 12px;margin-top:8px">'
+'<div class=r style=margin:0><span class=l>Speed</span><input type=range id=SP min=-3 max=3 step=0.1 value=0 style=accent-color:#00aeef oninput=updSP()><span class=v id=vSP>0x</span></div>'
+'<div style="font-size:12px;color:#00aeef;margin-top:6px">Warning: Experimental — Speeds above the fastest preset may crash the script and reboot the device. Transition duration set in settings may also affect this.</div>'
+'</div>'
+'<div class=acts><button id=bst>Start</button><button id=bsp>Stop</button></div>'
+'</div><div id=lg></div></div>'
+'<script src="/script/'+sid+'/asset?f=app"></scr'+'ipt></body></html>';
}
function buildAPP(sid){
var js='var B="/script/'+sid+'",P="Rainbow";';
js+='function Q(i){return document.getElementById(i)}';
js+='function lg(m){var d=Q("lg");d.innerHTML="<div>"+m+"</div>"+d.innerHTML}';
js+='function tr(id,val,max,col){var el=Q(id);if(!el)return;var p=Math.round(val/max*100);el.style.background="linear-gradient(to right,"+col+" "+p+"%,#333 "+p+"%)"}';
js+='function upd(){Q("vR").textContent=Q("R").value;Q("vG").textContent=Q("G").value;Q("vB").textContent=Q("B").value;Q("vW").textContent=Q("W").value;Q("vBR").textContent=Q("BR").value;tr("R",+Q("R").value,255,"#fa4141");tr("G",+Q("G").value,255,"#41fa6c");tr("B",+Q("B").value,255,"#06f");tr("W",+Q("W").value,100,"#d9d9d9");tr("BR",+Q("BR").value,100,"#ffe68c");};';
js+='function updSP(){var v=+Q("SP").value,m=v>=0?(1+v):(1/(1-v));Q("vSP").textContent=(v>=0?"+":"")+v.toFixed(1)+"x";tr("SP",v+3,6,"#00aeef");fetch(B+"/ctrl?cmd=speed&val="+m.toFixed(3))}';
js+='function api(cmd,q){fetch(B+"/ctrl?cmd="+cmd+(q||"")).then(function(r){return r.text()}).then(function(t){lg(cmd+":"+t)})}';
js+='function sendBr(){Q("vLBR").textContent=Q("LBR").value;tr("LBR",+Q("LBR").value,100,"#ffe68c");api("brightness","&val="+Q("LBR").value)}';
js+='function turnOff(){api("stop");Q("mt").textContent="Status: Stopped"}function sendColor(){api("stop");api("set","&r="+Q("R").value+"&g="+Q("G").value+"&b="+Q("B").value+"&w="+Q("W").value+"&br="+Q("BR").value+"&dur=0");P="manuel";Q("mt").textContent="Status: Manual color"}';
js+='function load(){fetch(B+"/ctrl?cmd=status").then(function(r){return r.text()}).then(function(t){';
js+='var d=JSON.parse(t),h="";P=d.preset;';
js+='for(var i=0;i<d.presets.length;i++){var k=d.presets[i];if(k==="manuel"||k==="_preview")continue;h+="<button data-p="+k+(k===P?" class=on":"")+">"+k+"</button>"}';
js+='Q("pb").innerHTML=h;';
js+='Q("pb").onclick=function(e){if(e.target.dataset.p){var prev=document.querySelector("#pb button.on");if(prev)prev.classList.remove("on");e.target.classList.add("on");P=e.target.dataset.p;api("preset","&name="+P);if(Q("mt").textContent.indexOf("Loop:")>=0)Q("mt").textContent="Status: Loop: "+P}};';
js+='Q("mt").textContent="Status: "+(d.running?"Loop: "+d.preset:"Stopped")})}';
js+='Q("bst").onclick=function(){fetch(B+"/ctrl?cmd=speed&val="+(+Q("SP").value>=0?(1+(+Q("SP").value)):(1/(1-(+Q("SP").value)))).toFixed(3)).then(function(){api("start");Q("mt").textContent="Status: Loop: "+P})};';
js+='Q("bsp").onclick=function(){api("stop");Q("mt").textContent="Status: Stopped"};';
js+='load();upd();tr("LBR",100,100,"#ffe68c");tr("SP",3,6,"#00aeef");';
return js;
}
// ─── EDITOR ───────────────────────────────────────────────────────────────────
function buildEditHTML(sid){
return '<!DOCTYPE html><html><head><meta charset=UTF-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Editor</title>'
+'<link rel=stylesheet href="/script/'+sid+'/asset?f=css"></head><body><div class=wrap>'
+'<div class=topbar><b>Preset Editor</b><a href="/script/'+sid+'/ui" class=btn>Back</a></div>'
+'<div class=card>'
+'<div class=cardtitle>Select preset</div>'
+'<div id=psel></div>'
+'<div style=display:flex;gap:6px>'
+'<input type=text id=newname placeholder="New preset name...">'
+'<button onclick=newPreset() style=white-space:nowrap>+ New</button>'
+'</div></div>'
+'<div class=card id=editor style=display:none>'
+'<div style=margin-bottom:10px>'
+'<div style=display:flex;justify-content:space-between;align-items:center;margin-bottom:8px>'
+'<span class=cardtitle style=margin-bottom:0>Editing: <b id=ename></b></span></div>'
+'<div style=display:flex;gap:4px;flex-wrap:wrap;margin-bottom:10px>'
+'<button id=pbtn onclick=previewPreset()>Preview</button>'
+'<button class=savebtn id=applybtn onclick=applyCode() style=display:none>Apply</button>'
+'<button class=savebtn id=savebtn onclick=savePreset()>Save</button>'
+'<button class=delbtn id=dbtn onclick=deletePreset()>Delete</button>'
+'<button id=cbtn onclick=toggleCode() style=margin-left:auto>Code</button>'
+'</div>'
+'<div style=display:flex;align-items:center;gap:8px>'
+'<span style=font-size:13px;color:#aaa>Virtual Button:</span>'
+'<select id=vcsel style=background:#161616;color:#eee;border:1px solid #333333;border-radius:6px;padding:4px 8px;font-size:13px;appearance:none;-webkit-appearance:none>'
+'<option value="">None</option>'
+'<option value="200" disabled style=color:#555>btn:200 — Off</option>'
+'<option value="201">btn:201</option><option value="202">btn:202</option>'
+'<option value="203">btn:203</option><option value="204">btn:204</option><option value="205">btn:205</option>'
+'<option value="206">btn:206</option><option value="207">btn:207</option><option value="208">btn:208</option>'
+'</select>'
+'<span style=font-size:12px;color:#666>Shelly app trigger</span>'
+'</div></div>'
+'<div id=codepanel style=display:none>'
+'<textarea id=codeta style=width:100%;height:200px;resize:none;background:#161616;color:#0f0;border:1px solid #333;border-radius:6px;font:12px monospace;padding:8px;box-sizing:border-box></textarea>'
+'</div>'
+'<div id=steps></div>'
+'<button onclick=addStep() id=addbtn style=margin-top:8px;width:100%;padding:10px>+ Add step</button>'
+'</div>'
+'<div id=lg></div></div>'
+'<script src="/script/'+sid+'/asset?f=eapp1"></scr'+'ipt>'
+'<script src="/script/'+sid+'/asset?f=eapp2"></scr'+'ipt>'
+'</body></html>';
}
function buildEAPP1(sid){
var js='var B="/script/'+sid+'",curName=null,steps=[],isBuiltin=false;';
js+='var BUILTIN={Rainbow:1,Candle:1,Ocean:1,Sunset:1,Party:1,Breath_White:1,Breath_Red:1,Breath_Green:1,Breath_Blue:1,Breath_Yellow:1,Breath_Purple:1,Nordic:1,Breath_RGB:1};';
js+='function Q(i){return document.getElementById(i)}';
js+='function lg(m){var d=Q("lg");d.innerHTML="<div>"+m+"</div>"+d.innerHTML}';
js+='function h2(n){return("0"+n.toString(16)).slice(-2)}';
js+='function toHex(r,g,b){return"#"+h2(r)+h2(g)+h2(b)}';
js+='function tr(id,val,max,col){var el=Q(id);if(!el)return;var p=Math.round(val/max*100);el.style.background="linear-gradient(to right,"+col+" "+p+"%,#333 "+p+"%)"}';
js+='function loadList(){fetch(B+"/ctrl?cmd=status").then(function(r){return r.text()}).then(function(t){';
js+='var d=JSON.parse(t),h="";';
js+='for(var i=0;i<d.presets.length;i++){var k=d.presets[i];if(k==="manuel"||k==="_preview")continue;';
js+='h+="<button onclick=\\"loadPreset(\'"+k+"\')\\" "+(curName===k?"class=on":BUILTIN[k]?"":"class=custom")+">"+k+"</button>"}';
js+='Q("psel").innerHTML=h}).catch(function(e){lg("ERR:"+e)})}';
js+='function loadPreset(name){fetch(B+"/ctrl?cmd=get_preset&name="+name).then(function(r){return r.text()}).then(function(t){'; js+='steps=JSON.parse(t);curName=name;isBuiltin=!!BUILTIN[name];';
js+='Q("ename").textContent=name;Q("editor").style.display="";Q("dbtn").style.display=isBuiltin?"none":"";';
js+='if(Q("codepanel").style.display!=="none")Q("codeta").value=JSON.stringify(steps,null,2);';
js+='fetch(B+"/ctrl?cmd=get_vc&name="+name).then(function(r){return r.text()}).then(function(v){Q("vcsel").value=v||""});';
js+='renderSteps();loadList()}).catch(function(e){lg("ERR:"+e)})}';
js+='function mkSl(i,id,mn,mx,val,col,sfx,stp){return"<div class=sr2><span class=fl>"+id.toUpperCase()+"</span><input type=range id="+id+i+" min="+mn+" max="+mx+" step="+(stp||1)+" value="+val+" oninput=upd("+i+") style=\\"accent-color:"+col+"\\"><span class=sv id=v"+id+i+">"+val+(sfx||"")+"</span></div>"}';
js+='function renderSteps(){var h="";for(var i=0;i<steps.length;i++){var s=steps[i];';
js+='h+="<div class=step><div class=step-hd><span class=step-num>Step "+(i+1)+"</span>";';
js+='h+="<div style=\\"display:flex;gap:6px;align-items:center\\"><div class=swatch id=sw"+i+" style=\\"background:"+toHex(s.r,s.g,s.b)+"\\"></div>";';
js+='h+="<button onclick=\\"moveStep("+i+",-1)\\">^</button><button onclick=\\"moveStep("+i+",1)\\">v</button>";';
js+='h+="<button onclick=\\"delStep("+i+")\\" class=delbtn>X</button></div></div>";';
js+='h+="<div>"+mkSl(i,"r",0,255,s.r,"#fa4141")+mkSl(i,"g",0,255,s.g,"#41fa6c")+mkSl(i,"b",0,255,s.b,"#06f")+mkSl(i,"w",0,100,s.w,"#d9d9d9")+mkSl(i,"br",1,100,s.br,"#ffe68c","%")+mkSl(i,"du",0.5,30,s.dur,"#6af","s",0.5)+"</div></div>";}';
js+='Q("steps").innerHTML=h;';
js+='for(var si=0;si<steps.length;si++){tr("r"+si,steps[si].r,255,"#fa4141");tr("g"+si,steps[si].g,255,"#41fa6c");tr("b"+si,steps[si].b,255,"#06f");tr("w"+si,steps[si].w,100,"#d9d9d9");tr("br"+si,steps[si].br,100,"#ffe68c");tr("du"+si,steps[si].dur,30,"#6af");}}';
js+='loadList();';
return js;
}
function buildEAPP2(sid){
var js='';
js+='function upd(i){var r=+Q("r"+i).value,g=+Q("g"+i).value,b=+Q("b"+i).value,w=+Q("w"+i).value,br=+Q("br"+i).value,du=parseFloat(Q("du"+i).value);';
js+='steps[i]={r:r,g:g,b:b,w:w,br:br,dur:du};';
js+='Q("sw"+i).style.background=toHex(r,g,b);Q("vr"+i).textContent=r;Q("vg"+i).textContent=g;Q("vb"+i).textContent=b;Q("vw"+i).textContent=w;Q("vbr"+i).textContent=br+"%";Q("vdu"+i).textContent=du+"s";';
js+='tr("r"+i,r,255,"#fa4141");tr("g"+i,g,255,"#41fa6c");tr("b"+i,b,255,"#06f");tr("w"+i,w,100,"#d9d9d9");tr("br"+i,br,100,"#ffe68c");tr("du"+i,du,30,"#6af");';
js+='if(previewing){if(_pt)clearTimeout(_pt);_pt=setTimeout(sendPreview,200)}}';
js+='function addStep(){steps.push({r:255,g:0,b:0,w:0,br:100,dur:1});renderSteps()}';
js+='function delStep(i){steps.splice(i,1);renderSteps()}';
js+='function moveStep(i,d){if(i+d<0||i+d>=steps.length)return;var t=steps[i];steps[i]=steps[i+d];steps[i+d]=t;renderSteps()}';
js+='var previewing=false,_pt=null;';
js+='function sendPreview(){fetch(B+"/ctrl?cmd=preview",{method:"POST",body:JSON.stringify(steps)}).then(function(r){return r.text()}).then(function(t){lg("preview:"+t)})}';
js+='function previewPreset(){if(previewing){fetch(B+"/ctrl?cmd=stop");previewing=false;Q("pbtn").textContent="Preview";Q("pbtn").classList.remove("on");return}previewing=true;Q("pbtn").textContent="Stop";Q("pbtn").classList.add("on");sendPreview()}';
js+='function savePreset(){if(!curName)return;fetch(B+"/ctrl?cmd=save_preset&name="+curName,{method:"POST",body:JSON.stringify(steps)}).then(function(r){return r.text()}).then(function(t){var vc=Q("vcsel").value;fetch(B+"/ctrl?cmd=save_vc&name="+curName+"&vc="+vc);lg("saved:"+t);loadList()})}';
js+='function toggleCode(){var cp=Q("codepanel"),sp=Q("steps"),ab=Q("addbtn"),cb=Q("cbtn"),sb=Q("savebtn"),ap=Q("applybtn");';
js+='if(cp.style.display==="none"){cp.style.display="";sp.style.display="none";ab.style.display="none";sb.style.display="none";ap.style.display="";var ta=Q("codeta");ta.value=JSON.stringify(steps,null,2);ta.style.height=Math.max(200,steps.length*9*18+40)+"px";cb.textContent="UI";cb.classList.add("on")}';
js+='else{cp.style.display="none";sp.style.display="";ab.style.display="";sb.style.display="";ap.style.display="none";cb.textContent="Code";cb.classList.remove("on")}}';
js+='function applyCode(){try{var p=JSON.parse(Q("codeta").value);if(!Array.isArray(p)||!p.length){lg("Invalid JSON");return}steps=p;toggleCode();renderSteps();if(previewing)sendPreview()}catch(e){lg("JSON error:"+e.message)}}';
js+='function deletePreset(){if(!curName||isBuiltin)return;if(!confirm("Delete "+curName+"?"))return;fetch(B+"/ctrl?cmd=del_preset&name="+curName).then(function(r){return r.text()}).then(function(){curName=null;Q("editor").style.display="none";loadList()})}';
js+='function newPreset(){var n=Q("newname").value.trim().replace(/[^a-z0-9_]/gi,"_");if(!n)return;curName=n;isBuiltin=false;steps=[{r:255,g:0,b:0,w:0,br:100,dur:1}];Q("ename").textContent=n;Q("editor").style.display="";Q("dbtn").style.display="";Q("newname").value="";renderSteps();loadList()}';
return js;
}
// ─── SETTINGS UI ───────────────────────────────────────── ───────────────────
function buildSettingsHTML(sid){
return '<!DOCTYPE html><html><head><meta charset=UTF-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Input Settings</title>'
+'<link rel=stylesheet href="/script/'+sid+'/asset?f=css"></head><body><div class=wrap>'
+'<div class=topbar><b>Input Settings</b>'
+'<div style=display:flex;gap:8px><button onclick=save() class=savebtn>Save</button><a href="/script/'+sid+'/ui" class=btn>Back</a></div></div>'
+'<div style="background:#1f1800;border:1px solid #3a2e00;border-radius:8px;padding:10px 14px;font-size:12px;color:#cc9;margin-bottom:8px">'
+'Save Settings while "fast" preset is running may cause the script to crash'
+'</div>'
+'<div style="background:#1f1800;border:1px solid #3a2e00;border-radius:8px;padding:12px 14px;margin-bottom:8px">'
+'<div style="font-size:12px;color:#cc9;margin-bottom:10px">For the script to handle inputs independently, the RGBW component must be set to detached mode. This disconnects the physical inputs from directly controlling the light, and lets the script manage them instead. Click the button below to apply.</div>'
+'<button id=detachbtn onclick=detach()>Set detached mode</button>'
+'</div>'
+'<div id=scards></div><div id=lg></div></div>'
+'<script src="/script/'+sid+'/asset?f=sapp"></scr'+'ipt></body></html>';
}
function buildSAPP(sid){
var js='var B="/script/'+sid+'";';
js+='function Q(i){return document.getElementById(i)}';
js+='function lg(m){var d=Q("lg");d.innerHTML="<div>"+m+"</div>"+d.innerHTML}';
js+='function updA(i){var a=Q("a"+i).value,ps=Q("p"+i),cy=Q("cy"+i);ps.style.visibility=(a==="toggle"||a==="preset")?"visible":"hidden";cy.style.display=(a==="cycle")?"block":"none"}';
js+='var KEYS=["input0_short","input0_double","input0_triple","input0_long","input1_short","input1_double","input1_triple","input1_long","input2_short","input2_double","input2_triple","input2_long","input3_short","input3_double","input3_triple","input3_long"];';
js+='var TYPES=["Short","Double","Triple","Long"];';
js+='function buildCards(presets){var h="",ch="";';
js+='for(var pi=0;pi<presets.length;pi++){var pk=presets[pi];if(pk==="manuel"||pk==="_preview")continue;ch+="<label><input type=checkbox value=\\""+pk+"\\"> "+pk+"</label>"}';
js+='for(var inp=0;inp<4;inp++){h+="<div class=card style=\\"margin-bottom:8px\\"><div class=cardtitle>Input "+inp+"</div>";';
js+='for(var t=0;t<4;t++){var idx=inp*4+t;';
js+='h+="<div class=row3><span class=lbl2>"+TYPES[t]+"</span>";';
js+='h+="<select id=\\"a"+idx+"\\" onchange=\\"updA("+idx+")\\">"';
js+='+"<option value=none>None</option><option value=toggle>Toggle</option><option value=preset>Preset</option><option value=cycle>Cycle</option>"';
js+='+(t===3?"<option value=dim>Dim</option>":"")+"</select>";';
js+='h+="<select id=\\"p"+idx+"\\" style=\\"visibility:hidden\\"><option value=last>Last used</option></select></div>";';
js+='h+="<div id=\\"cy"+idx+"\\" style=\\"display:none\\"><span class=lbl2>Cycle between:</span><div class=cycle-wrap id=\\"cw"+idx+"\\">"+ch+"</div></div>";}';
js+='h+="</div>";}Q("scards").innerHTML=h}';
js+='function load(){fetch(B+"/ctrl?cmd=status").then(function(r){return r.text()}).then(function(t){';
js+='var d=JSON.parse(t),ph="<option value=last>Last used</option>";';
js+='for(var i=0;i<d.presets.length;i++){var k=d.presets[i];if(k==="manuel"||k==="_preview")continue;ph+="<option value=\\""+k+"\\">"+k+"</option>"}';
js+='buildCards(d.presets);';
js+='for(var i=0;i<16;i++){Q("p"+i).innerHTML=ph}';
js+='fetch(B+"/ctrl?cmd=get_settings").then(function(r){return r.text()}).then(function(t){';
js+='var s=JSON.parse(t);for(var i=0;i<KEYS.length;i++){var c=s[KEYS[i]]||{};Q("a"+i).value=c.action||"none";if(c.preset)Q("p"+i).value=c.preset;';
js+='if(c.cycle&&c.cycle.length){var cbs=Q("cw"+i).querySelectorAll("input");for(var j=0;j<cbs.length;j++){cbs[j].checked=c.cycle.indexOf(cbs[j].value)>=0}}';
js+='updA(i)}})});}';
js+='function save(){var cfg={};';
js+='for(var i=0;i<KEYS.length;i++){var act=Q("a"+i).value,cyc=[];';
js+='if(act==="cycle"){var cbs=Q("cw"+i).querySelectorAll("input:checked");for(var j=0;j<cbs.length;j++)cyc.push(cbs[j].value)}';
js+='cfg[KEYS[i]]={action:act,preset:Q("p"+i).value,cycle:cyc}}';
js+='fetch(B+"/ctrl?cmd=save_settings",{method:"POST",body:JSON.stringify(cfg)}).then(function(r){return r.text()}).then(function(t){lg("saved:"+t)})}';
js+='function detach(){var btn=Q("detachbtn");fetch(B+"/ctrl?cmd=detach").then(function(r){return r.text()}).then(function(t){btn.textContent="Done";btn.classList.add("done");lg("detach:"+t)})}';
js+='load();';
return js;
}
// ─── HTTP ENDPOINTS ─────── ── ─ ──────────────────────────────────────────────
var _sid=Shelly.getCurrentScriptId()+"";
HTTPServer.registerEndpoint("ctrl",function(req,res){
var q=parseQuery(req.query),cmd=q.cmd,ok="ok";
if(cmd==="start"){startLoop();}
else if(cmd==="stop"){stopLoop();}
else if(cmd==="pause"){pauseLoop();}
else if(cmd==="preset"){ok=setPreset(q.name||"")?"ok":"err";}
else if(cmd==="speed"){CONFIG.speed=parseFloat(q.val)||1;}
else if(cmd==="brightness"){CONFIG.brightness=parseInt(q.val)||100;}
else if(cmd==="set"){
var sr=+q.r||0,sg=+q.g||0,sb=+q.b||0,sw=+q.w||0,sbr=+q.br||100,sdu=parseFloat(q.dur)||1;
PRESETS.manuel=[{r:sr,g:sg,b:sb,w:sw,br:sbr,dur:sdu}];
Shelly.call("RGB.Set",{id:CONFIG.id,on:true,rgb:[sr,sg,sb],brightness:sbr,transition_duration:sdu},null);
Shelly.call("Light.Set",{id:CONFIG.id,on:sw>0,brightness:sw},null);
}
else if(cmd==="status"){
ok=JSON.stringify({running:running,preset:CONFIG.preset,speed:CONFIG.speed,presets:Object.keys(PRESETS)});
}
else if(cmd==="get_preset"){
var n=q.name||"";
if(typeof PRESETS[n]==="string")PRESETS[n]=decPreset(PRESETS[n]);
if(PRESETS[n]){
ok=JSON.stringify(PRESETS[n]);
} else if(n in PRESETS){
Shelly.call("KVS.Get",{key:"rgbw_preset_"+n},function(r,e){
res.code=200;
res.headers=[["Content-Type","application/json; charset=utf-8"]];
res.body=(!e&&r&&r.value)?JSON.stringify(decPreset(r.value)):"[]";
res.send();
},null);
return;
} else{ok="err";}
}
else if(cmd==="preview"){
try{var ps=JSON.parse(req.body||"[]");if(ps.length){pauseLoop();PRESETS._preview=ps;CONFIG.preset="_preview";running=true;stepIndex=0;nextStep();}}catch(ex){ok="err";}
}
else if(cmd==="save_preset"){
var n=q.name||"";
try{var ps=JSON.parse(req.body||"[]");PRESETS[n]=ps;Shelly.call("KVS.Set",{key:"rgbw_preset_"+n,value:encPreset(ps)},null);}
catch(ex){ok="err";}
}
else if(cmd==="del_preset"){
var n=q.name||"";
delete PRESETS[n];
Shelly.call("KVS.Delete",{key:"rgbw_preset_"+n},null);
}
else if(cmd==="save_vc"){
var n=q.name||"",vc=parseInt(q.vc);
if(n){
if(vc>=200&&vc<=208){VCMAP[n]=vc;}
else{delete VCMAP[n];}
Shelly.call("KVS.Set",{key:"rgbw_vcmap",value:JSON.stringify(VCMAP)},null);
}
}
else if(cmd==="get_vc"){
var n=q.name||"";
ok=String(VCMAP[n]||"");
}
else if(cmd==="detach"){
Shelly.call("RGB.SetConfig",{id:0,config:{in_mode:"detached"}},null);
for(var di=0;di<4;di++)Shelly.call("Input.SetConfig",{id:di,config:{factory_reset:false}},null);
}
else if(cmd==="get_settings"){ok=JSON.stringify(SETTINGS);}
else if(cmd==="save_settings"){
try{
var s=JSON.parse(req.body||"{}");
SETTINGS=s;
var types=["short","double","triple","long"];
var vals=[];
for(var n=0;n<4;n++){
var parts=[];
for(var k=0;k<4;k++){
var c=s["input"+n+"_"+types[k]]||{};
var a=c.action||"none",p=c.preset||"last",cy=(c.cycle||[]).join(":");
parts.push(cy?a+","+p+","+cy:(p!=="last"?a+","+p:a));
}
vals.push(parts.join("|"));
}
function saveNext(i){
if(i>=4){applyInputHandlers();return;}
Shelly.call("KVS.Set",{key:"setting_i"+i,value:vals[i]},function(){saveNext(i+1);},null);
}
saveNext(0);
}catch(ex){ok="err";}
}
else{ok="unknown";}
send(res,ok);
});
HTTPServer.registerEndpoint("ui", function(req,res){send(res,buildHTML(_sid),"text/html");});
HTTPServer.registerEndpoint("edit",function(req,res){send(res,buildEditHTML(_sid),"text/html");});
HTTPServer.registerEndpoint("asset",function(req,res){
var f=parseQuery(req.query).f||"";
if (f==="css") send(res,buildCSS(),"text/css");
else if(f==="app") send(res,buildAPP(_sid),"text/javascript");
else if(f==="eapp1") send(res,buildEAPP1(_sid),"text/javascript");
else if(f==="eapp2") send(res,buildEAPP2(_sid),"text/javascript");
else if(f==="settings") send(res,buildSettingsHTML(_sid),"text/html");
else if(f==="sapp") send(res,buildSAPP(_sid),"text/javascript");
else send(res,"not found");
});
// ─── BOOT ─────────────────────────────────────────────────────────────────────
loadKVS();
if(CONFIG.autostart)startLoop();
print("RGBW ready. Script ID: "+_sid);
Timer.set(2000,false,function(){
Shelly.call("Script.SetConfig",{id:Shelly.getCurrentScriptId(),config:{enable:true}},function(){
Shelly.call("rgb.setConfig",{id:0,config:{"name":"http://"+(Shelly.getComponentStatus("wifi").sta_ip??"192.168.33.1")+"/script/"+Shelly.getCurrentScriptId()+"/ui"}},null);
});
});
print("http://" + (Shelly.getComponentStatus("wifi").sta_ip ?? "192.168.33.1") + "/script/" + Shelly.getCurrentScriptId() + "/ui"); Script for Plus RGBW
Shelly Plus RGBW Loop Controller (Click to expand)
// Plus RGBW Loop Controller
// Ver. 1.1 (ported from Pro RGBWW to Shelly Plus RGBW)
// UI: http://<ip>/script/<id>/ui
// Editor: http://<ip>/script/<id>/edit
// Creator Ronni.N - Shelly Nordics
// ─── CONFIG ───────────────────────────────────────────────────────────────────
var CONFIG={preset:"Rainbow",speed:1.0,id:0,autostart:false,brightness:100};
// ─── DIM ──────────────────────────────────────────────────────────────────────
var DIM={dir:1,timer:null,active:false,wasRunning:false};
function startDim(){
if(DIM.active)return;
DIM.active=true;DIM.dir=-DIM.dir;DIM.wasRunning=running;
if(running)pauseLoop();
dimStep();
}
function stopDim(){
DIM.active=false;
if(DIM.timer){Timer.clear(DIM.timer);DIM.timer=null;}
if(DIM.wasRunning){running=true;stepIndex=0;nextStep();}
}
function dimStep(){
if(!DIM.active)return;
CONFIG.brightness=Math.max(1,Math.min(100,CONFIG.brightness+DIM.dir*2));
Shelly.call("RGBW.Set",{id:CONFIG.id,brightness:CONFIG.brightness},null);
if(CONFIG.brightness<=1||CONFIG.brightness>=100){stopDim();return;}
DIM.timer=Timer.set(150,false,dimStep);
}
// ─── SETTINGS ─────────────────────────────────────────────────────────────────
var SETTINGS={
input0_short: {action:"toggle",preset:"Rainbow",cycle:[]},
input0_double:{action:"none", preset:"last", cycle:[]},
input0_triple:{action:"none", preset:"last", cycle:[]},
input0_long: {action:"none", preset:"last", cycle:[]},
input1_short: {action:"none", preset:"last", cycle:[]},
input1_double:{action:"none", preset:"last", cycle:[]},
input1_triple:{action:"none", preset:"last", cycle:[]},
input1_long: {action:"none", preset:"last", cycle:[]},
input2_short: {action:"none", preset:"last", cycle:[]},
input2_double:{action:"none", preset:"last", cycle:[]},
input2_triple:{action:"none", preset:"last", cycle:[]},
input2_long: {action:"none", preset:"last", cycle:[]},
input3_short: {action:"none", preset:"last", cycle:[]},
input3_double:{action:"none", preset:"last", cycle:[]},
input3_triple:{action:"none", preset:"last", cycle:[]},
input3_long: {action:"none", preset:"last", cycle:[]},
};
var _inputHandler=null;
function applyInputHandlers(){
if(_inputHandler!==null){Shelly.removeEventHandler(_inputHandler);_inputHandler=null;}
_inputHandler=Shelly.addEventHandler(function(e){
var info=e.info||e,comp=info.component||"",ev=info.event||"",inp=-1;
if(comp==="input:0")inp=0;
else if(comp==="input:1")inp=1;
else if(comp==="input:2")inp=2;
else if(comp==="input:3")inp=3;
if(inp>=0){
var key="input"+inp+"_",longCfg=SETTINGS[key+"long"];
if(ev==="single_push") {handleAction(SETTINGS[key+"short"]);}
else if(ev==="double_push") {handleAction(SETTINGS[key+"double"]);}
else if(ev==="triple_push") {handleAction(SETTINGS[key+"triple"]);}
else if(ev==="long_push"&&longCfg&&longCfg.action==="dim") {startDim();}
else if(ev==="btn_up" &&longCfg&&longCfg.action==="dim") {stopDim();}
return;
}
});
}
function handleAction(cfg){
if(!cfg)return;
var act=cfg.action||"";
if(act==="toggle"){
if(running)stopLoop();
else{if(cfg.preset&&(cfg.preset in PRESETS))CONFIG.preset=cfg.preset;startLoop();}
}
else if(act==="preset"){
if(cfg.preset&&(cfg.preset in PRESETS)){setPreset(cfg.preset,!running);}
}
else if(act==="cycle"){
var pool=(cfg.cycle&&cfg.cycle.length>0)?cfg.cycle:Object.keys(PRESETS).filter(function(k){return k!=="manuel"&&k!=="_preview";});
var next=pool[(pool.indexOf(CONFIG.preset)+1)%pool.length];
setPreset(next,!running);
}
}
// ─�����─ PRESETS ──────────────────────────────────────────────────────────────────
var PRESETS={
Rainbow: "255,0,0,0,100,2|255,127,0,0,100,2|0,255,0,0,100,2|0,0,255,0,100,2|148,0,211,0,100,2",
Candle: "255,100,10,20,70,1.4|255,80,5,10,55,1.3|255,120,20,25,80,1.5",
Ocean: "0,50,255,4,20,1.5|0,180,191,4,30,1.5|0,100,255,2,15,1.5",
Sunset: "92,43,50,0,41,3|145,75,20,2,88,7.5|255,208,36,0,100,10",
Party: "255,0,0,0,100,0.5|0,255,0,0,100,0.5|0,0,255,0,100,0.5|255,255,0,0,100,0.5",
Nordic: "0,255,120,0,60,5|100,0,200,0,70,5|50,255,150,0,65,6",
Breath_White: "200,220,255,255,100,3|200,220,255,255,5,3",
Breath_Red: "255,0,0,0,100,2.5|255,0,0,0,5,2.5",
Breath_Green: "0,255,0,0,100,2.5|0,255,0,0,5,2.5",
Breath_Blue: "0,0,255,0,100,2.5|0,0,255,0,5,2.5",
Breath_Yellow:"255,200,0,0,100,2.5|255,200,0,0,5,3",
Breath_Purple:"148,0,211,0,100,2.5|148,0,211,0,5,2.5",
Breath_RGB: "255,0,0,0,100,1.5|255,0,0,0,5,1.5|0,255,0,0,100,1.5|0,255,0,0,5,1.5|0,0,255,0,100,1.5|0,0,255,0,5,1.5",
manuel: "255,0,0,0,100,1",
};
// ─── LOOP ─────────────────────────────────────────────────────────────────────
var stepIndex=0,loopTimer=null,running=false;
function nextStep(){
if(!running)return;
var raw=PRESETS[CONFIG.preset];
if(!raw)return;
if(typeof raw==="string"){raw=decPreset(raw);PRESETS[CONFIG.preset]=raw;}
var steps=raw;
if(!steps||!steps.length)return;
var s=steps[stepIndex%steps.length];
var br=Math.round(s.br*CONFIG.brightness/100);if(br<1)br=1;
Shelly.call("RGBW.Set",{id:CONFIG.id,on:true,rgb:[s.r,s.g,s.b],white:s.w,brightness:br,transition_duration:s.dur/CONFIG.speed},null);
stepIndex++;
loopTimer=Timer.set(Math.round(s.dur/CONFIG.speed*1000),false,nextStep);
}
function startLoop(){if(running)return;running=true;stepIndex=0;nextStep();}
function stopLoop(){running=false;if(loopTimer){Timer.clear(loopTimer);loopTimer=null;}Shelly.call("RGBW.Set",{id:CONFIG.id,on:false},null);}
function pauseLoop(){running=false;if(loopTimer){Timer.clear(loopTimer);loopTimer=null;}}
function setPreset(name,autostart){
if(!(name in PRESETS))return false;
if(PRESETS[name]===null){
Shelly.call("KVS.Get",{key:"rgbw_preset_"+name},function(r,e){
if(!e&&r&&r.value)PRESETS[name]=decPreset(r.value);
if(PRESETS[name]){
CONFIG.preset=name;stepIndex=0;
if(running){if(loopTimer)Timer.clear(loopTimer);nextStep();}
else if(autostart){running=true;nextStep();}
}
},null);
return true;
}
if(typeof PRESETS[name]==="string")PRESETS[name]=decPreset(PRESETS[name]);
CONFIG.preset=name;stepIndex=0;
if(running){if(loopTimer)Timer.clear(loopTimer);nextStep();}
else if(autostart){running=true;nextStep();}
return true;
}
// ─── KVS ���─���───────────────────────────────────────────────────────────────────
// Settings: "action,preset,cycle1:cycle2|action,preset|action|action" (4 parts, pipe-sep)
// Preset: "r,g,b,w,br,dur|r,g,b,w,br,dur|..."
function encPreset(steps){
var out=[];
for(var i=0;i<steps.length;i++){var s=steps[i];out.push(s.r+","+s.g+","+s.b+","+s.w+","+s.br+","+s.dur);}
return out.join("|");
}
function decPreset(v){
var parts=v.split("|"),out=[];
for(var i=0;i<parts.length;i++){var f=parts[i].split(",");if(f.length<6)continue;out.push({r:+f[0],g:+f[1],b:+f[2],w:+f[3],br:+f[4],dur:+f[5]});}
return out;
}
function loadKVS(){
var done=0,types=["short","double","triple","long"];
function onDone(){if(++done===4)applyInputHandlers();}
function parseSetting(n,v){
var pts=v.split("|");
for(var k=0;k<4;k++){
var p=(pts[k]||"none").split(",");
SETTINGS["input"+n+"_"+types[k]]={action:p[0]||"none",preset:p[1]||"last",cycle:p[2]?p[2].split(":"):[]};
}
}
for(var i=0;i<4;i++){
(function(n){
Shelly.call("KVS.Get",{key:"setting_i"+n},function(r,e){
if(!e&&r&&r.value)parseSetting(n,r.value);
onDone();
},null);
})(i);
}
Shelly.call("KVS.GetMany",{match:"rgbw_preset_*"},function(r,e){
if(e||!r||!r.items)return;
for(var i=0;i<r.items.length;i++){
var item=r.items[i];
if(!item||!item.key||item.key.indexOf("rgbw_preset_")!==0)continue;
var nm=item.key.slice(12);
if(nm)PRESETS[nm]=null;
}
},null);
}
// ─── HELPERS ──────────────────────────────────────────────────────────────────
function parseQuery(q){
var out={};if(!q)return out;
var parts=q.split("&");
for(var i=0;i<parts.length;i++){var kv=parts[i].split("=");if(kv.length===2)out[kv[0]]=kv[1];}
return out;
}
function send(res,body,ct){
res.code=200;
res.headers=[["Content-Type",(ct||"text/plain")+"; charset=utf-8"]];
res.body=body;res.send();
}
// ─── CSS ──────────────────────────────────────────────────────────────────────
function buildCSS(){
return 'body{font:14px/1.5 system-ui,sans-serif;background:#161616;color:#e0e0e0;margin:0;padding:0}'
+'.wrap{max-width:500px;margin:0 auto;padding:12px;display:flex;flex-direction:column;gap:8px}'
+'.topbar{background:#222222;border-radius:5px;padding:12px 16px;display:flex;justify-content:space-between;align-items:center}'
+'.topbar b{font-size:17px;font-weight:900}'
+'.card{background:#222222;border-radius:5px;padding:14px 16px}'
+'.cardtitle{font-size:12px;font-weight:700;letter-spacing:2px;text-transform:uppercase;opacity:1;margin-bottom:12px}'
+'a.btn,button.btn{font-size:13px;font-weight:600;padding:5px 12px;background:#1095c1;color:#edf0f3;border:1px solid #00aeef44;border-radius:3px;text-decoration:none;cursor:pointer}'
+'.r{display:grid;grid-template-columns:90px 1fr 46px;align-items:center;gap:8px;margin:8px 0}'
+'.l{font-size:12px;color:#c7c7c7}'
+'.v{font-size:12px;font-weight:400;color:#fff;text-align:right}'
+'input[type=range]{width:100%;height:4px;cursor:pointer;background:#333;border-radius:2px;appearance:none;-webkit-appearance:none}'
+'button{padding:6px 12px;cursor:pointer;background:#252627;color:#ccc;border:1px solid #828282;font:14px system-ui;border-radius:7px}'
+'button:hover{background:#2e3032}'
+'button.on{background:#141414;border-color:#00aeef;color:#00aeef}'
+'.acts{display:flex;gap:6px;margin-top:8px}'
+'.acts button{flex:1;padding:9px;font-size:13px;font-weight:700;border-radius:8px}'
+'#bst{background:#0f1f10;color:#4d4;border-color:#1e3e1e}'
+'#bsp{background:#1f0f0f;color:#d44;border-color:#3e1e1e}'
+'.savebtn{background:#0f1f10;color:#4d4;border-color:#1e3e1e}'
+'.delbtn{background:#1f0f0f;color:#d44;border-color:#3e1e1e}'
+'#lg{font-size:10px;font-family:monospace;color:#555;max-height:40px;overflow-y:auto;background:#111214;padding:6px 10px;border-radius:7px}'
+'.step{background:#2d2d2d;border:1px solid #222;border-radius:8px;padding:10px 12px;margin:6px 0}'
+'.step-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}'
+'.step-num{font-size:12px;font-weight:700;color:#c7c7c7}'
+'.swatch{width:26px;height:26px;border-radius:5px;border:1px solid #333;flex-shrink:0}'
+'.sr2{display:grid;grid-template-columns:28px 1fr 38px;align-items:center;gap:6px;margin:8px 0}'
+'.fl{font-size:12px;color:#c7c7c7}'
+'.sv{font-size:11px;color:#ccc;text-align:right}'
+'#psel{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px}'
+'#psel button{font-size:13px;padding:6px 12px}'
+'#psel button.custom{border-color:#fa4}'
+'#pbtn.on{background:#002a38;color:#00aeef;border-color:#00aeef}'
+'#cbtn.on{background:#252627;color:#ccc;border-color:#828282}'
+'input[type=text]{width:100%;box-sizing:border-box;background:#161616;border:1px solid #333;color:#eee;padding:6px 10px;border-radius:6px;font:13px system-ui}'
+'select{width:100%;background:#161616;border:1px solid #333;color:#eee;padding:5px 8px;border-radius:6px;font:13px system-ui}'
+'.row3{display:grid;grid-template-columns:90px 1fr 1fr;align-items:center;gap:8px;margin:5px 0}'
+'.lbl2{font-size:12px;color:#888}'
+'.cycle-wrap{display:flex;flex-wrap:wrap;gap:4px;margin:4px 0 8px}'
+'.cycle-wrap label{display:flex;align-items:center;gap:4px;font-size:12px;background:#111214;border:1px solid #333;border-radius:6px;padding:3px 8px;cursor:pointer}'
+'.cycle-wrap input{accent-color:#00aeef}';
}
// ─── MAIN UI ──────────────────────────────────────────────────────────────────
function buildHTML(sid){
return '<!DOCTYPE html><html><head><meta charset=UTF-8><meta name=viewport content="width=device-width,initial-scale=1"><title>RGBW</title>'
+'<link rel=stylesheet href="/script/'+sid+'/asset?f=css"></head><body><div class=wrap>'
+'<div class=topbar>'
+'<div><b>Plus RGBW Loop Controller</b><div id=mt style=font-size:12px;opacity:.6;margin-top:2px>Status: ...</div></div>'
+'<a href="/script/'+sid+'/asset?f=settings" class=btn>Input Settings</a></div>'
+'<div class=card><div class=cardtitle>Manual color</div>'
+'<div class=r><span class=l>Red</span><input type=range id=R min=0 max=255 value=255 style=accent-color:#fa4141 oninput=upd()><span class=v id=vR>255</span></div>'
+'<div class=r><span class=l>Green</span><input type=range id=G min=0 max=255 value=0 style=accent-color:#41fa6c oninput=upd()><span class=v id=vG>0</span></div>'
+'<div class=r><span class=l>Blue</span><input type=range id=B min=0 max=255 value=0 style=accent-color:#06f oninput=upd()><span class=v id=vB>0</span></div>'
+'<div class=r><span class=l>White</span><input type=range id=W min=0 max=255 value=0 style=accent-color:#ebebeb oninput=upd()><span class=v id=vW>0</span></div>'
+'<div class=r><span class=l>Brightness</span><input type=range id=BR min=1 max=100 value=100 style=accent-color:#ffe68c oninput=upd()><span class=v id=vBR>100</span></div>'
+'<div class=acts><button onclick=sendColor()>Send color</button><button onclick=turnOff()>Off</button></div></div>'
+'<div class=card>'
+'<div style=display:flex;justify-content:space-between;align-items:center;margin-bottom:12px>'
+'<span class=cardtitle style=margin-bottom:0>Color loop</span>'
+'<a href="/script/'+sid+'/edit" class=btn>Edit</a></div>'
+'<div id=pb style=display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px></div>'
+'<div class=r><span class=l>Brightness</span><input type=range id=LBR min=1 max=100 value=100 style=accent-color:#ffe68c oninput=sendBr()><span class=v id=vLBR>100</span></div>'
+'<div style="background:#1a1500;border:1px solid #3a2e00;border-radius:8px;padding:8px 12px;margin-top:8px">'
+'<div class=r style=margin:0><span class=l>Speed</span><input type=range id=SP min=-3 max=3 step=0.1 value=0 style=accent-color:#00aeef oninput=updSP()><span class=v id=vSP>0x</span></div>'
+'<div style="font-size:12px;color:#00aeef;margin-top:6px">Warning: Experimental — Speeds above the fastest preset may crash the script and reboot the device. Transition duration set in settings may also affect this.</div>'
+'</div>'
+'<div class=acts><button id=bst>Start</button><button id=bsp>Stop</button></div>'
+'</div><div id=lg></div></div>'
+'<script src="/script/'+sid+'/asset?f=app"></scr'+'ipt></body></html>';
}
function buildAPP(sid){
var js='var B="/script/'+sid+'",P="Rainbow";';
js+='function Q(i){return document.getElementById(i)}';
js+='function lg(m){var d=Q("lg");d.innerHTML="<div>"+m+"</div>"+d.innerHTML}';
js+='function tr(id,val,max,col){var el=Q(id);if(!el)return;var p=Math.round(val/max*100);el.style.background="linear-gradient(to right,"+col+" "+p+"%,#333 "+p+"%)"}';
js+='function upd(){Q("vR").textContent=Q("R").value;Q("vG").textContent=Q("G").value;Q("vB").textContent=Q("B").value;Q("vW").textContent=Q("W").value;Q("vBR").textContent=Q("BR").value;tr("R",+Q("R").value,255,"#fa4141");tr("G",+Q("G").value,255,"#41fa6c");tr("B",+Q("B").value,255,"#06f");tr("W",+Q("W").value,255,"#d9d9d9");tr("BR",+Q("BR").value,100,"#ffe68c");};';
js+='function updSP(){var v=+Q("SP").value,m=v>=0?(1+v):(1/(1-v));Q("vSP").textContent=(v>=0?"+":"")+v.toFixed(1)+"x";tr("SP",v+3,6,"#00aeef");fetch(B+"/ctrl?cmd=speed&val="+m.toFixed(3))}';
js+='function api(cmd,q){fetch(B+"/ctrl?cmd="+cmd+(q||"")).then(function(r){return r.text()}).then(function(t){lg(cmd+":"+t)})}';
js+='function sendBr(){Q("vLBR").textContent=Q("LBR").value;tr("LBR",+Q("LBR").value,100,"#ffe68c");api("brightness","&val="+Q("LBR").value)}';
js+='function turnOff(){api("stop");Q("mt").textContent="Status: Stopped"}function sendColor(){api("set","&r="+Q("R").value+"&g="+Q("G").value+"&b="+Q("B").value+"&w="+Q("W").value+"&br="+Q("BR").value+"&dur=0");P="manuel";Q("mt").textContent="Status: Manual color"}';
js+='function load(){fetch(B+"/ctrl?cmd=status").then(function(r){return r.text()}).then(function(t){';
js+='var d=JSON.parse(t),h="";P=d.preset;';
js+='for(var i=0;i<d.presets.length;i++){var k=d.presets[i];if(k==="manuel"||k==="_preview")continue;h+="<button data-p="+k+(k===P?" class=on":"")+">"+k+"</button>"}';
js+='Q("pb").innerHTML=h;';
js+='Q("pb").onclick=function(e){if(e.target.dataset.p){var prev=document.querySelector("#pb button.on");if(prev)prev.classList.remove("on");e.target.classList.add("on");P=e.target.dataset.p;api("preset","&name="+P);if(Q("mt").textContent.indexOf("Loop:")>=0)Q("mt").textContent="Status: Loop: "+P}};';
js+='Q("mt").textContent="Status: "+(d.running?"Loop: "+d.preset:"Stopped")})}';
js+='Q("bst").onclick=function(){fetch(B+"/ctrl?cmd=speed&val="+(+Q("SP").value>=0?(1+(+Q("SP").value)):(1/(1-(+Q("SP").value)))).toFixed(3)).then(function(){api("start");Q("mt").textContent="Status: Loop: "+P})};';
js+='Q("bsp").onclick=function(){api("stop");Q("mt").textContent="Status: Stopped"};';
js+='load();upd();tr("LBR",100,100,"#ffe68c");tr("SP",3,6,"#00aeef");';
return js;
}
// ─── EDITOR ───────────────────────────────────────────────────────────────────
function buildEditHTML(sid){
return '<!DOCTYPE html><html><head><meta charset=UTF-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Editor</title>'
+'<link rel=stylesheet href="/script/'+sid+'/asset?f=css"></head><body><div class=wrap>'
+'<div class=topbar><b>Preset Editor</b><a href="/script/'+sid+'/ui" class=btn>Back</a></div>'
+'<div class=card>'
+'<div class=cardtitle>Select preset</div>'
+'<div id=psel></div>'
+'<div style=display:flex;gap:6px>'
+'<input type=text id=newname placeholder="New preset name...">'
+'<button onclick=newPreset() style=white-space:nowrap>+ New</button>'
+'</div></div>'
+'<div class=card id=editor style=display:none>'
+'<div style=margin-bottom:10px>'
+'<div style=display:flex;justify-content:space-between;align-items:center;margin-bottom:8px>'
+'<span class=cardtitle style=margin-bottom:0>Editing: <b id=ename></b></span></div>'
+'<div style=display:flex;gap:4px;flex-wrap:wrap;margin-bottom:10px>'
+'<button id=pbtn onclick=previewPreset()>Preview</button>'
+'<button class=savebtn id=applybtn onclick=applyCode() style=display:none>Apply</button>'
+'<button class=savebtn id=savebtn onclick=savePreset()>Save</button>'
+'<button class=delbtn id=dbtn onclick=deletePreset()>Delete</button>'
+'<button id=cbtn onclick=toggleCode() style=margin-left:auto>Code</button>'
+'</div>'
+'<div id=codepanel style=display:none>'
+'<textarea id=codeta style=width:100%;height:200px;resize:none;background:#161616;color:#0f0;border:1px solid #333;border-radius:6px;font:12px monospace;padding:8px;box-sizing:border-box></textarea>'
+'</div>'
+'<div id=steps></div>'
+'<button onclick=addStep() id=addbtn style=margin-top:8px;width:100%;padding:10px>+ Add step</button>'
+'</div>'
+'<div id=lg></div></div>'
+'<script src="/script/'+sid+'/asset?f=eapp1"></scr'+'ipt>'
+'<script src="/script/'+sid+'/asset?f=eapp2"></scr'+'ipt>'
+'</body></html>';
}
function buildEAPP1(sid){
var js='var B="/script/'+sid+'",curName=null,steps=[],isBuiltin=false;';
js+='var BUILTIN={Rainbow:1,Candle:1,Ocean:1,Sunset:1,Party:1,Breath_White:1,Breath_Red:1,Breath_Green:1,Breath_Blue:1,Breath_Yellow:1,Breath_Purple:1,Nordic:1,Breath_RGB:1};';
js+='function Q(i){return document.getElementById(i)}';
js+='function lg(m){var d=Q("lg");d.innerHTML="<div>"+m+"</div>"+d.innerHTML}';
js+='function h2(n){return("0"+n.toString(16)).slice(-2)}';
js+='function toHex(r,g,b){return"#"+h2(r)+h2(g)+h2(b)}';
js+='function tr(id,val,max,col){var el=Q(id);if(!el)return;var p=Math.round(val/max*100);el.style.background="linear-gradient(to right,"+col+" "+p+"%,#333 "+p+"%)"}';
js+='function loadList(){fetch(B+"/ctrl?cmd=status").then(function(r){return r.text()}).then(function(t){';
js+='var d=JSON.parse(t),h="";';
js+='for(var i=0;i<d.presets.length;i++){var k=d.presets[i];if(k==="manuel"||k==="_preview")continue;';
js+='h+="<button onclick=\\"loadPreset(\'"+k+"\')\\" "+(curName===k?"class=on":BUILTIN[k]?"":"class=custom")+">"+k+"</button>"}';
js+='Q("psel").innerHTML=h}).catch(function(e){lg("ERR:"+e)})}';
js+='function loadPreset(name){fetch(B+"/ctrl?cmd=get_preset&name="+name).then(function(r){return r.text()}).then(function(t){'; js+='steps=JSON.parse(t);curName=name;isBuiltin=!!BUILTIN[name];';
js+='Q("ename").textContent=name;Q("editor").style.display="";Q("dbtn").style.display=isBuiltin?"none":"";';
js+='if(Q("codepanel").style.display!=="none")Q("codeta").value=JSON.stringify(steps,null,2);';
js+='renderSteps();loadList()}).catch(function(e){lg("ERR:"+e)})}';
js+='function mkSl(i,id,mn,mx,val,col,sfx,stp){return"<div class=sr2><span class=fl>"+id.toUpperCase()+"</span><input type=range id="+id+i+" min="+mn+" max="+mx+" step="+(stp||1)+" value="+val+" oninput=upd("+i+") style=\\"accent-color:"+col+"\\"><span class=sv id=v"+id+i+">"+val+(sfx||"")+"</span></div>"}';
js+='function renderSteps(){var h="";for(var i=0;i<steps.length;i++){var s=steps[i];';
js+='h+="<div class=step><div class=step-hd><span class=step-num>Step "+(i+1)+"</span>";';
js+='h+="<div style=\\"display:flex;gap:6px;align-items:center\\"><div class=swatch id=sw"+i+" style=\\"background:"+toHex(s.r,s.g,s.b)+"\\"></div>";';
js+='h+="<button onclick=\\"moveStep("+i+",-1)\\">^</button><button onclick=\\"moveStep("+i+",1)\\">v</button>";';
js+='h+="<button onclick=\\"delStep("+i+")\\" class=delbtn>X</button></div></div>";';
js+='h+="<div>"+mkSl(i,"r",0,255,s.r,"#fa4141")+mkSl(i,"g",0,255,s.g,"#41fa6c")+mkSl(i,"b",0,255,s.b,"#06f")+mkSl(i,"w",0,255,s.w,"#d9d9d9")+mkSl(i,"br",1,100,s.br,"#ffe68c","%")+mkSl(i,"du",0.5,30,s.dur,"#6af","s",0.5)+"</div></div>";}';
js+='Q("steps").innerHTML=h;';
js+='for(var si=0;si<steps.length;si++){tr("r"+si,steps[si].r,255,"#fa4141");tr("g"+si,steps[si].g,255,"#41fa6c");tr("b"+si,steps[si].b,255,"#06f");tr("w"+si,steps[si].w,255,"#d9d9d9");tr("br"+si,steps[si].br,100,"#ffe68c");tr("du"+si,steps[si].dur,30,"#6af");}}';
js+='loadList();';
return js;
}
function buildEAPP2(sid){
var js='';
js+='function upd(i){var r=+Q("r"+i).value,g=+Q("g"+i).value,b=+Q("b"+i).value,w=+Q("w"+i).value,br=+Q("br"+i).value,du=parseFloat(Q("du"+i).value);';
js+='steps[i]={r:r,g:g,b:b,w:w,br:br,dur:du};';
js+='Q("sw"+i).style.background=toHex(r,g,b);Q("vr"+i).textContent=r;Q("vg"+i).textContent=g;Q("vb"+i).textContent=b;Q("vw"+i).textContent=w;Q("vbr"+i).textContent=br+"%";Q("vdu"+i).textContent=du+"s";';
js+='tr("r"+i,r,255,"#fa4141");tr("g"+i,g,255,"#41fa6c");tr("b"+i,b,255,"#06f");tr("w"+i,w,255,"#d9d9d9");tr("br"+i,br,100,"#ffe68c");tr("du"+i,du,30,"#6af");';
js+='if(previewing){if(_pt)clearTimeout(_pt);_pt=setTimeout(sendPreview,200)}}';
js+='function addStep(){steps.push({r:255,g:0,b:0,w:0,br:100,dur:1});renderSteps()}';
js+='function delStep(i){steps.splice(i,1);renderSteps()}';
js+='function moveStep(i,d){if(i+d<0||i+d>=steps.length)return;var t=steps[i];steps[i]=steps[i+d];steps[i+d]=t;renderSteps()}';
js+='var previewing=false,_pt=null;';
js+='function sendPreview(){fetch(B+"/ctrl?cmd=preview",{method:"POST",body:JSON.stringify(steps)}).then(function(r){return r.text()}).then(function(t){lg("preview:"+t)})}';
js+='function previewPreset(){if(previewing){fetch(B+"/ctrl?cmd=stop");previewing=false;Q("pbtn").textContent="Preview";Q("pbtn").classList.remove("on");return}previewing=true;Q("pbtn").textContent="Stop";Q("pbtn").classList.add("on");sendPreview()}';
js+='function savePreset(){if(!curName)return;fetch(B+"/ctrl?cmd=save_preset&name="+curName,{method:"POST",body:JSON.stringify(steps)}).then(function(r){return r.text()}).then(function(t){lg("saved:"+t);loadList()})}';
js+='function toggleCode(){var cp=Q("codepanel"),sp=Q("steps"),ab=Q("addbtn"),cb=Q("cbtn"),sb=Q("savebtn"),ap=Q("applybtn");';
js+='if(cp.style.display==="none"){cp.style.display="";sp.style.display="none";ab.style.display="none";sb.style.display="none";ap.style.display="";var ta=Q("codeta");ta.value=JSON.stringify(steps,null,2);ta.style.height=Math.max(200,steps.length*9*18+40)+"px";cb.textContent="UI";cb.classList.add("on")}';
js+='else{cp.style.display="none";sp.style.display="";ab.style.display="";sb.style.display="";ap.style.display="none";cb.textContent="Code";cb.classList.remove("on")}}';
js+='function applyCode(){try{var p=JSON.parse(Q("codeta").value);if(!Array.isArray(p)||!p.length){lg("Invalid JSON");return}steps=p;toggleCode();renderSteps();if(previewing)sendPreview()}catch(e){lg("JSON error:"+e.message)}}';
js+='function deletePreset(){if(!curName||isBuiltin)return;if(!confirm("Delete "+curName+"?"))return;fetch(B+"/ctrl?cmd=del_preset&name="+curName).then(function(r){return r.text()}).then(function(){curName=null;Q("editor").style.display="none";loadList()})}';
js+='function newPreset(){var n=Q("newname").value.trim().replace(/[^a-z0-9_]/gi,"_");if(!n)return;curName=n;isBuiltin=false;steps=[{r:255,g:0,b:0,w:0,br:100,dur:1}];Q("ename").textContent=n;Q("editor").style.display="";Q("dbtn").style.display="";Q("newname").value="";renderSteps();loadList()}';
return js;
}
// ─── SETTINGS UI ───────────────────────────────────────── ───────────────────
function buildSettingsHTML(sid){
return '<!DOCTYPE html><html><head><meta charset=UTF-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Input Settings</title>'
+'<link rel=stylesheet href="/script/'+sid+'/asset?f=css"></head><body><div class=wrap>'
+'<div class=topbar><b>Input Settings</b>'
+'<div style=display:flex;gap:8px><button onclick=save() class=savebtn>Save</button><a href="/script/'+sid+'/ui" class=btn>Back</a></div></div>'
+'<div style="background:#1f1800;border:1px solid #3a2e00;border-radius:8px;padding:10px 14px;font-size:12px;color:#cc9;margin-bottom:8px">'
+'Save Settings while "fast" preset is running may cause the script to crash'
+'</div>'
+'<div style="background:#1f1800;border:1px solid #3a2e00;border-radius:8px;padding:10px 14px;font-size:12px;color:#cc9;margin-bottom:8px">'
+'For the script to handle inputs independently, the RGBW component must be set to detached mode. This disconnects the physical inputs from directly controlling the light, and lets the script manage them instead. Click the button below to apply.'
+'<div style="margin-top:8px"><button onclick="setDetached(this)" class=btn>Set detached mode</button></div>'
+'</div>'
+'<div id=scards></div><div id=lg></div></div>'
+'<script src="/script/'+sid+'/asset?f=sapp"></scr'+'ipt></body></html>';
}
function buildSAPP(sid){
var js='var B="/script/'+sid+'";';
js+='function setDetached(btn){fetch(B+"/ctrl?cmd=detach").then(function(){btn.textContent="Done!";btn.disabled=true})}'
js+='function Q(i){return document.getElementById(i)}';
js+='function lg(m){var d=Q("lg");d.innerHTML="<div>"+m+"</div>"+d.innerHTML}';
js+='function updA(i){var a=Q("a"+i).value,ps=Q("p"+i),cy=Q("cy"+i);ps.style.visibility=(a==="toggle"||a==="preset")?"visible":"hidden";cy.style.display=(a==="cycle")?"block":"none"}';
js+='var KEYS=["input0_short","input0_double","input0_triple","input0_long","input1_short","input1_double","input1_triple","input1_long","input2_short","input2_double","input2_triple","input2_long","input3_short","input3_double","input3_triple","input3_long"];';
js+='var TYPES=["Short","Double","Triple","Long"];';
js+='function buildCards(presets){var h="",ch="";';
js+='for(var pi=0;pi<presets.length;pi++){var pk=presets[pi];if(pk==="manuel"||pk==="_preview")continue;ch+="<label><input type=checkbox value=\\""+pk+"\\"> "+pk+"</label>"}';
js+='for(var inp=0;inp<4;inp++){h+="<div class=card style=\\"margin-bottom:8px\\"><div class=cardtitle>Input "+inp+"</div>";';
js+='for(var t=0;t<4;t++){var idx=inp*4+t;';
js+='h+="<div class=row3><span class=lbl2>"+TYPES[t]+"</span>";';
js+='h+="<select id=\\"a"+idx+"\\" onchange=\\"updA("+idx+")\\">"';
js+='+"<option value=none>None</option><option value=toggle>Toggle</option><option value=preset>Preset</option><option value=cycle>Cycle</option>"';
js+='+(t===3?"<option value=dim>Dim</option>":"")+"</select>";';
js+='h+="<select id=\\"p"+idx+"\\" style=\\"visibility:hidden\\"><option value=last>Last used</option></select></div>";';
js+='h+="<div id=\\"cy"+idx+"\\" style=\\"display:none\\"><span class=lbl2>Cycle between:</span><div class=cycle-wrap id=\\"cw"+idx+"\\">"+ch+"</div></div>";}';
js+='h+="</div>";}Q("scards").innerHTML=h}';
js+='function load(){fetch(B+"/ctrl?cmd=status").then(function(r){return r.text()}).then(function(t){';
js+='var d=JSON.parse(t),ph="<option value=last>Last used</option>";';
js+='for(var i=0;i<d.presets.length;i++){var k=d.presets[i];if(k==="manuel"||k==="_preview")continue;ph+="<option value=\\""+k+"\\">"+k+"</option>"}';
js+='buildCards(d.presets);';
js+='for(var i=0;i<16;i++){Q("p"+i).innerHTML=ph}';
js+='fetch(B+"/ctrl?cmd=get_settings").then(function(r){return r.text()}).then(function(t){';
js+='var s=JSON.parse(t);for(var i=0;i<KEYS.length;i++){var c=s[KEYS[i]]||{};Q("a"+i).value=c.action||"none";if(c.preset)Q("p"+i).value=c.preset;';
js+='if(c.cycle&&c.cycle.length){var cbs=Q("cw"+i).querySelectorAll("input");for(var j=0;j<cbs.length;j++){cbs[j].checked=c.cycle.indexOf(cbs[j].value)>=0}}';
js+='updA(i)}})});}';
js+='function save(){var cfg={};';
js+='for(var i=0;i<KEYS.length;i++){var act=Q("a"+i).value,cyc=[];';
js+='if(act==="cycle"){var cbs=Q("cw"+i).querySelectorAll("input:checked");for(var j=0;j<cbs.length;j++)cyc.push(cbs[j].value)}';
js+='cfg[KEYS[i]]={action:act,preset:Q("p"+i).value,cycle:cyc}}';
js+='fetch(B+"/ctrl?cmd=save_settings",{method:"POST",body:JSON.stringify(cfg)}).then(function(r){return r.text()}).then(function(t){lg("saved:"+t)})}';
js+='load();';
return js;
}
// ─── HTTP ENDPOINTS ─────── ── ─ ──────────────────────────────────────────────
var _sid=Shelly.getCurrentScriptId()+"";
HTTPServer.registerEndpoint("ctrl",function(req,res){
var q=parseQuery(req.query),cmd=q.cmd,ok="ok";
if(cmd==="start"){startLoop();}
else if(cmd==="stop"){stopLoop();}
else if(cmd==="pause"){pauseLoop();}
else if(cmd==="preset"){ok=setPreset(q.name||"")?"ok":"err";}
else if(cmd==="speed"){CONFIG.speed=parseFloat(q.val)||1;}
else if(cmd==="brightness"){CONFIG.brightness=parseInt(q.val)||100;}
else if(cmd==="set"){
var sr=+q.r||0,sg=+q.g||0,sb=+q.b||0,sw=+q.w||0,sbr=+q.br||100,sdu=parseFloat(q.dur)||1;
pauseLoop();
PRESETS.manuel=[{r:sr,g:sg,b:sb,w:sw,br:sbr,dur:sdu}];
Shelly.call("RGBW.Set",{id:CONFIG.id,on:true,rgb:[sr,sg,sb],white:sw,brightness:sbr,transition_duration:sdu},null);
}
else if(cmd==="status"){
ok=JSON.stringify({running:running,preset:CONFIG.preset,speed:CONFIG.speed,presets:Object.keys(PRESETS)});
}
else if(cmd==="get_preset"){
var n=q.name||"";
if(typeof PRESETS[n]==="string")PRESETS[n]=decPreset(PRESETS[n]);
if(PRESETS[n]){
ok=JSON.stringify(PRESETS[n]);
} else if(n in PRESETS){
Shelly.call("KVS.Get",{key:"rgbw_preset_"+n},function(r,e){
res.code=200;
res.headers=[["Content-Type","application/json; charset=utf-8"]];
res.body=(!e&&r&&r.value)?JSON.stringify(decPreset(r.value)):"[]";
res.send();
},null);
return;
} else{ok="err";}
}
else if(cmd==="preview"){
try{var ps=JSON.parse(req.body||"[]");if(ps.length){pauseLoop();PRESETS._preview=ps;CONFIG.preset="_preview";running=true;stepIndex=0;nextStep();}}catch(ex){ok="err";}
}
else if(cmd==="save_preset"){
var n=q.name||"";
try{var ps=JSON.parse(req.body||"[]");PRESETS[n]=ps;Shelly.call("KVS.Set",{key:"rgbw_preset_"+n,value:encPreset(ps)},null);}
catch(ex){ok="err";}
}
else if(cmd==="del_preset"){
var n=q.name||"";
delete PRESETS[n];
Shelly.call("KVS.Delete",{key:"rgbw_preset_"+n},null);
}
else if(cmd==="detach"){
Shelly.call("RGBW.SetConfig",{id:0,config:{in_mode:"detached"}},null);
for(var di=0;di<4;di++)Shelly.call("Input.SetConfig",{id:di,config:{factory_reset:false}},null);
}
else if(cmd==="get_settings"){ok=JSON.stringify(SETTINGS);}
else if(cmd==="save_settings"){
try{
var s=JSON.parse(req.body||"{}");
SETTINGS=s;
var types=["short","double","triple","long"];
var vals=[];
for(var n=0;n<4;n++){
var parts=[];
for(var k=0;k<4;k++){
var c=s["input"+n+"_"+types[k]]||{};
var a=c.action||"none",p=c.preset||"last",cy=(c.cycle||[]).join(":");
parts.push(cy?a+","+p+","+cy:(p!=="last"?a+","+p:a));
}
vals.push(parts.join("|"));
}
function saveNext(i){
if(i>=4){applyInputHandlers();return;}
Shelly.call("KVS.Set",{key:"setting_i"+i,value:vals[i]},function(){saveNext(i+1);},null);
}
saveNext(0);
}catch(ex){ok="err";}
}
else{ok="unknown";}
send(res,ok);
});
HTTPServer.registerEndpoint("ui", function(req,res){send(res,buildHTML(_sid),"text/html");});
HTTPServer.registerEndpoint("edit",function(req,res){send(res,buildEditHTML(_sid),"text/html");});
HTTPServer.registerEndpoint("asset",function(req,res){
var f=parseQuery(req.query).f||"";
if (f==="css") send(res,buildCSS(),"text/css");
else if(f==="app") send(res,buildAPP(_sid),"text/javascript");
else if(f==="eapp1") send(res,buildEAPP1(_sid),"text/javascript");
else if(f==="eapp2") send(res,buildEAPP2(_sid),"text/javascript");
else if(f==="settings") send(res,buildSettingsHTML(_sid),"text/html");
else if(f==="sapp") send(res,buildSAPP(_sid),"text/javascript");
else send(res,"not found");
});
// ���── BOOT ─────────────────────────────────────────────────────────────────────
loadKVS();
if(CONFIG.autostart)startLoop();
print("RGBW ready. Script ID: "+_sid);
Timer.set(2000,false,function(){
Shelly.call("Script.SetConfig",{id:Shelly.getCurrentScriptId(),config:{enable:true}},function(){
Shelly.call("RGBW.SetConfig",{id:0,config:{"name":"http://"+(Shelly.getComponentStatus("wifi").sta_ip??"192.168.33.1")+"/script/"+Shelly.getCurrentScriptId()+"/ui"}},null);
});
});
print("http://" + (Shelly.getComponentStatus("wifi").sta_ip ?? "192.168.33.1") + "/script/" + Shelly.getCurrentScriptId() + "/ui"); Script for RGBCCT Bulb Gen3
Shelly RGBCCT Bulb Loop Controller (Click to expand)
// RGBCCT Bulp Loop Controller
// Ver. 1.2 (ported from RGBW to RGBCCT, white 0-255 replaced by ct 2700-6500)
// UI: http://<ip>/script/<id>/ui
// Editor: http://<ip>/script/<id>/edit
// Creator Ronni.N - Shelly Nordics
// ─── CONFIG ───────────────────────────────────────────────────────────────────
var CONFIG={preset:"Rainbow",speed:1.0,id:0,autostart:false,brightness:100};
// ─ ─ PRESETS ──────────────────────────────────────────────────────────────────
var PRESETS={
Rainbow: "255,0,0,4000,100,2|255,127,0,4000,100,2|0,255,0,4000,100,2|0,0,255,4000,100,2|148,0,211,4000,100,2",
Candle: "255,100,10,2700,70,1.4|255,80,5,2700,55,1.3|255,120,20,2700,80,1.5",
Ocean: "0,50,255,4000,20,1.5|0,180,191,4000,30,1.5|0,100,255,4000,15,1.5",
Sunset: "92,43,50,2700,41,3|145,75,20,2700,88,7.5|255,208,36,2700,100,10",
Party: "255,0,0,4000,100,0.5|0,255,0,4000,100,0.5|0,0,255,4000,100,0.5|255,255,0,4000,100,0.5",
Nordic: "0,255,120,4000,60,5|100,0,200,4000,70,5|50,255,150,4000,65,6",
Breath_White: "200,220,255,6500,100,3|200,220,255,6500,5,3",
Breath_Red: "255,0,0,4000,100,2.5|255,0,0,4000,5,2.5",
Breath_Green: "0,255,0,4000,100,2.5|0,255,0,4000,5,2.5",
Breath_Blue: "0,0,255,4000,100,2.5|0,0,255,4000,5,2.5",
Breath_Yellow:"255,200,0,4000,100,2.5|255,200,0,4000,5,3",
Breath_Purple:"148,0,211,4000,100,2.5|148,0,211,4000,5,2.5",
Breath_RGB: "255,0,0,4000,100,1.5|255,0,0,4000,5,1.5|0,255,0,4000,100,1.5|0,255,0,4000,5,1.5|0,0,255,4000,100,1.5|0,0,255,4000,5,1.5",
manuel: "255,0,0,4000,100,1",
};
// ─── LOOP ─────────────────────────────────────────────────────────────────────
var stepIndex=0,loopTimer=null,running=false;
function nextStep(){
if(!running)return;
var raw=PRESETS[CONFIG.preset];
if(!raw)return;
if(typeof raw==="string"){raw=decPreset(raw);PRESETS[CONFIG.preset]=raw;}
var steps=raw;
if(!steps||!steps.length)return;
var s=steps[stepIndex%steps.length];
var br=Math.round(s.br*CONFIG.brightness/100);if(br<1)br=1;
Shelly.call("RGBCCT.Set",{id:CONFIG.id,on:true,rgb:[s.r,s.g,s.b],ct:s.ct,brightness:br,transition_duration:s.dur/CONFIG.speed},null);
stepIndex++;
loopTimer=Timer.set(Math.round(s.dur/CONFIG.speed*1000),false,nextStep);
}
function startLoop(){if(running)return;running=true;stepIndex=0;nextStep();}
function stopLoop(){running=false;if(loopTimer){Timer.clear(loopTimer);loopTimer=null;}Shelly.call("RGBCCT.Set",{id:CONFIG.id,on:false},null);}
function pauseLoop(){running=false;if(loopTimer){Timer.clear(loopTimer);loopTimer=null;}}
function setPreset(name,autostart){
if(!(name in PRESETS))return false;
if(PRESETS[name]===null){
Shelly.call("KVS.Get",{key:"rgbcct_preset_"+name},function(r,e){
if(!e&&r&&r.value)PRESETS[name]=decPreset(r.value);
if(PRESETS[name]){
CONFIG.preset=name;stepIndex=0;
if(running){if(loopTimer)Timer.clear(loopTimer);nextStep();}
else if(autostart){running=true;nextStep();}
}
},null);
return true;
}
if(typeof PRESETS[name]==="string")PRESETS[name]=decPreset(PRESETS[name]);
CONFIG.preset=name;stepIndex=0;
if(running){if(loopTimer)Timer.clear(loopTimer);nextStep();}
else if(autostart){running=true;nextStep();}
return true;
}
// ─── VIRTUAL COMPONENTS ───────────────────────────────────────────────────────
var VCMAP={};
function loadVCMap(){
Shelly.call("KVS.Get",{key:"rgbcct_vcmap"},function(r,e){
if(!e&&r&&r.value){try{VCMAP=JSON.parse(r.value);}catch(ex){VCMAP={};}}
},null);
}
Shelly.addEventHandler(function(e){
var info=e.info||e,ev=info.event||"",comp=info.component||"";
if(ev==="single_push"&&comp.indexOf("button:")===0){
var vcId=parseInt(comp.slice(7));
if(vcId===200){stopLoop();return;}
for(var nm in VCMAP){
if(VCMAP[nm]===vcId){setPreset(nm,true);return;}
}
}
});
// Preset: "r,g,b,ct,br,dur|r,g,b,ct,br,dur|..."
function encPreset(steps){
var out=[];
for(var i=0;i<steps.length;i++){var s=steps[i];out.push(s.r+","+s.g+","+s.b+","+s.ct+","+s.br+","+s.dur);}
return out.join("|");
}
function decPreset(v){
var parts=v.split("|"),out=[];
for(var i=0;i<parts.length;i++){var f=parts[i].split(",");if(f.length<6)continue;out.push({r:+f[0],g:+f[1],b:+f[2],ct:+f[3],br:+f[4],dur:+f[5]});}
return out;
}
function loadKVS(){
Shelly.call("KVS.GetMany",{match:"rgbcct_preset_*"},function(r,e){
if(e||!r||!r.items)return;
for(var i=0;i<r.items.length;i++){
var item=r.items[i];
if(!item||!item.key||item.key.indexOf("rgbcct_preset_")!==0)continue;
var nm=item.key.slice(14);
if(nm)PRESETS[nm]=null;
}
loadVCMap();
},null);
}
// ─── HELPERS ──────────────────────────────────────────────────────────────────
function parseQuery(q){
var out={};if(!q)return out;
var parts=q.split("&");
for(var i=0;i<parts.length;i++){var kv=parts[i].split("=");if(kv.length===2)out[kv[0]]=kv[1];}
return out;
}
function send(res,body,ct){
res.code=200;
res.headers=[["Content-Type",(ct||"text/plain")+"; charset=utf-8"]];
res.body=body;res.send();
}
// ─── CSS ──────────────────────────────────────────────────────────────────────
function buildCSS(){
return 'body{font:14px/1.5 system-ui,sans-serif;background:#161616;color:#e0e0e0;margin:0;padding:0}'
+'.wrap{max-width:500px;margin:0 auto;padding:12px;display:flex;flex-direction:column;gap:8px}'
+'.topbar{background:#222222;border-radius:5px;padding:12px 16px;display:flex;justify-content:space-between;align-items:center}'
+'.topbar b{font-size:17px;font-weight:900}'
+'.card{background:#222222;border-radius:5px;padding:14px 16px}'
+'.cardtitle{font-size:12px;font-weight:700;letter-spacing:2px;text-transform:uppercase;opacity:1;margin-bottom:12px}'
+'a.btn,button.btn{font-size:13px;font-weight:600;padding:5px 12px;background:#1095c1;color:#edf0f3;border:1px solid #00aeef44;border-radius:3px;text-decoration:none;cursor:pointer}'
+'.r{display:grid;grid-template-columns:90px 1fr 46px;align-items:center;gap:8px;margin:8px 0}'
+'.l{font-size:12px;color:#c7c7c7}'
+'.v{font-size:12px;font-weight:400;color:#fff;text-align:right}'
+'input[type=range]{width:100%;height:4px;cursor:pointer;background:#333;border-radius:2px;appearance:none;-webkit-appearance:none}'
+'button{padding:6px 12px;cursor:pointer;background:#252627;color:#ccc;border:1px solid #828282;font:14px system-ui;border-radius:7px}'
+'button:hover{background:#2e3032}'
+'button.on{background:#141414;border-color:#00aeef;color:#00aeef}'
+'.acts{display:flex;gap:6px;margin-top:8px}'
+'.acts button{flex:1;padding:9px;font-size:13px;font-weight:700;border-radius:8px}'
+'#bst{background:#0f1f10;color:#4d4;border-color:#1e3e1e}'
+'#bsp{background:#1f0f0f;color:#d44;border-color:#3e1e1e}'
+'.savebtn{background:#0f1f10;color:#4d4;border-color:#1e3e1e}'
+'.delbtn{background:#1f0f0f;color:#d44;border-color:#3e1e1e}'
+'#lg{font-size:10px;font-family:monospace;color:#555;max-height:40px;overflow-y:auto;background:#111214;padding:6px 10px;border-radius:7px}'
+'.step{background:#2d2d2d;border:1px solid #222;border-radius:8px;padding:10px 12px;margin:6px 0}'
+'.step-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}'
+'.step-num{font-size:12px;font-weight:700;color:#c7c7c7}'
+'.swatch{width:26px;height:26px;border-radius:5px;border:1px solid #333;flex-shrink:0}'
+'.sr2{display:grid;grid-template-columns:28px 1fr 38px;align-items:center;gap:6px;margin:8px 0}'
+'.fl{font-size:12px;color:#c7c7c7}'
+'.sv{font-size:11px;color:#ccc;text-align:right}'
+'#psel{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px}'
+'#psel button{font-size:13px;padding:6px 12px}'
+'#psel button.custom{border-color:#fa4}'
+'#pbtn.on{background:#002a38;color:#00aeef;border-color:#00aeef}'
+'#cbtn.on{background:#252627;color:#ccc;border-color:#828282}'
+'input[type=text]{width:100%;box-sizing:border-box;background:#161616;border:1px solid #333;color:#eee;padding:6px 10px;border-radius:6px;font:13px system-ui}'
+'select{width:100%;background:#161616;border:1px solid #333;color:#eee;padding:5px 8px;border-radius:6px;font:13px system-ui}'
+'#vcsel{width:auto;border:1px solid #828282 !important}';
}
// ─── MAIN UI ──────────────────────────────────────────────────────────────────
function buildHTML(sid){
return '<!DOCTYPE html><html><head><meta charset=UTF-8><meta name=viewport content="width=device-width,initial-scale=1"><title>RGBCCT</title>'
+'<link rel=stylesheet href="/script/'+sid+'/asset?f=css"></head><body><div class=wrap>'
+'<div class=topbar>'
+'<div><b>RGBCCT Bulp Loop Controller</b><div id=mt style=font-size:12px;opacity:.6;margin-top:2px>Status: ...</div></div>'
+'</div>'
+'<div class=card><div class=cardtitle>Manual color</div>'
+'<div class=r><span class=l>Red</span><input type=range id=R min=0 max=255 value=255 style=accent-color:#fa4141 oninput=upd()><span class=v id=vR>255</span></div>'
+'<div class=r><span class=l>Green</span><input type=range id=G min=0 max=255 value=0 style=accent-color:#41fa6c oninput=upd()><span class=v id=vG>0</span></div>'
+'<div class=r><span class=l>Blue</span><input type=range id=B min=0 max=255 value=0 style=accent-color:#06f oninput=upd()><span class=v id=vB>0</span></div>'
+'<div class=r><span class=l>CT (K)</span><input type=range id=W min=2700 max=6500 value=4000 style=accent-color:#ebebeb oninput=upd()><span class=v id=vW>4000</span></div>'
+'<div class=r><span class=l>Brightness</span><input type=range id=BR min=1 max=100 value=100 style=accent-color:#ffe68c oninput=upd()><span class=v id=vBR>100</span></div>'
+'<div class=acts><button onclick=sendColor()>Send color</button><button onclick=turnOff()>Off</button></div></div>'
+'<div class=card>'
+'<div style=display:flex;justify-content:space-between;align-items:center;margin-bottom:12px>'
+'<span class=cardtitle style=margin-bottom:0>Color loop</span>'
+'<a href="/script/'+sid+'/edit" class=btn>Edit</a></div>'
+'<div id=pb style=display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px></div>'
+'<div class=r><span class=l>Brightness</span><input type=range id=LBR min=1 max=100 value=100 style=accent-color:#ffe68c oninput=sendBr()><span class=v id=vLBR>100</span></div>'
+'<div style="background:#1a1500;border:1px solid #3a2e00;border-radius:8px;padding:8px 12px;margin-top:8px">'
+'<div class=r style=margin:0><span class=l>Speed</span><input type=range id=SP min=-3 max=3 step=0.1 value=0 style=accent-color:#00aeef oninput=updSP()><span class=v id=vSP>0x</span></div>'
+'<div style="font-size:12px;color:#00aeef;margin-top:6px">Warning: Experimental — Speeds above the fastest preset may crash the script and reboot the device. Transition duration set in settings may also affect this.</div>'
+'</div>'
+'<div class=acts><button id=bst>Start</button><button id=bsp>Stop</button></div>'
+'</div><div id=lg></div></div>'
+'<script src="/script/'+sid+'/asset?f=app"></scr'+'ipt></body></html>';
}
function buildAPP(sid){
var js='var B="/script/'+sid+'",P="Rainbow";';
js+='function Q(i){return document.getElementById(i)}';
js+='function lg(m){var d=Q("lg");d.innerHTML="<div>"+m+"</div>"+d.innerHTML}';
js+='function tr(id,val,max,col){var el=Q(id);if(!el)return;var p=Math.round(val/max*100);el.style.background="linear-gradient(to right,"+col+" "+p+"%,#333 "+p+"%)"}';
js+='function upd(){Q("vR").textContent=Q("R").value;Q("vG").textContent=Q("G").value;Q("vB").textContent=Q("B").value;Q("vW").textContent=Q("W").value+"K";Q("vBR").textContent=Q("BR").value;tr("R",+Q("R").value,255,"#fa4141");tr("G",+Q("G").value,255,"#41fa6c");tr("B",+Q("B").value,255,"#06f");tr("W",+Q("W").value-2700,3800,"#d9d9d9");tr("BR",+Q("BR").value,100,"#ffe68c");};';
js+='function updSP(){var v=+Q("SP").value,m=v>=0?(1+v):(1/(1-v));Q("vSP").textContent=(v>=0?"+":"")+v.toFixed(1)+"x";tr("SP",v+3,6,"#00aeef");fetch(B+"/ctrl?cmd=speed&val="+m.toFixed(3))}';
js+='function api(cmd,q){fetch(B+"/ctrl?cmd="+cmd+(q||"")).then(function(r){return r.text()}).then(function(t){lg(cmd+":"+t)})}';
js+='function sendBr(){Q("vLBR").textContent=Q("LBR").value;tr("LBR",+Q("LBR").value,100,"#ffe68c");api("brightness","&val="+Q("LBR").value)}';
js+='function turnOff(){api("stop");Q("mt").textContent="Status: Stopped"}function sendColor(){api("set","&r="+Q("R").value+"&g="+Q("G").value+"&b="+Q("B").value+"&ct="+Q("W").value+"&br="+Q("BR").value+"&dur=0");P="manuel";Q("mt").textContent="Status: Manual color"}';
js+='function load(){fetch(B+"/ctrl?cmd=status").then(function(r){return r.text()}).then(function(t){';
js+='var d=JSON.parse(t),h="";P=d.preset;';
js+='for(var i=0;i<d.presets.length;i++){var k=d.presets[i];if(k==="manuel"||k==="_preview")continue;h+="<button data-p="+k+(k===P?" class=on":"")+">"+k+"</button>"}';
js+='Q("pb").innerHTML=h;';
js+='Q("pb").onclick=function(e){if(e.target.dataset.p){var prev=document.querySelector("#pb button.on");if(prev)prev.classList.remove("on");e.target.classList.add("on");P=e.target.dataset.p;api("preset","&name="+P);if(Q("mt").textContent.indexOf("Loop:")>=0)Q("mt").textContent="Status: Loop: "+P}};';
js+='Q("mt").textContent="Status: "+(d.running?"Loop: "+d.preset:"Stopped")})}';
js+='Q("bst").onclick=function(){fetch(B+"/ctrl?cmd=speed&val="+(+Q("SP").value>=0?(1+(+Q("SP").value)):(1/(1-(+Q("SP").value)))).toFixed(3)).then(function(){api("start");Q("mt").textContent="Status: Loop: "+P})};';
js+='Q("bsp").onclick=function(){api("stop");Q("mt").textContent="Status: Stopped"};';
js+='load();upd();tr("LBR",100,100,"#ffe68c");tr("SP",3,6,"#00aeef");';
return js;
}
// ─── EDITOR ───────────────────────────────────────────────────────────────────
function buildEditHTML(sid){
return '<!DOCTYPE html><html><head><meta charset=UTF-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Editor</title>'
+'<link rel=stylesheet href="/script/'+sid+'/asset?f=css"></head><body><div class=wrap>'
+'<div class=topbar><b>Preset Editor</b><a href="/script/'+sid+'/ui" class=btn>Back</a></div>'
+'<div class=card>'
+'<div class=cardtitle>Select preset</div>'
+'<div id=psel></div>'
+'<div style=display:flex;gap:6px>'
+'<input type=text id=newname placeholder="New preset name...">'
+'<button onclick=newPreset() style=white-space:nowrap>+ New</button>'
+'</div></div>'
+'<div class=card id=editor style=display:none>'
+'<div style=margin-bottom:10px>'
+'<div style=display:flex;justify-content:space-between;align-items:center;margin-bottom:8px>'
+'<span class=cardtitle style=margin-bottom:0>Editing: <b id=ename></b></span></div>'
+'<div style=display:flex;gap:4px;flex-wrap:wrap;margin-bottom:10px>'
+'<button id=pbtn onclick=previewPreset()>Preview</button>'
+'<button class=savebtn id=applybtn onclick=applyCode() style=display:none>Apply</button>'
+'<button class=savebtn id=savebtn onclick=savePreset()>Save</button>'
+'<button class=delbtn id=dbtn onclick=deletePreset()>Delete</button>'
+'<button id=cbtn onclick=toggleCode() style=margin-left:auto>Code</button>'
+'</div>'
+'<div style=display:flex;align-items:center;gap:8px;margin-bottom:10px>'
+'<span style=font-size:13px;color:#aaa>Virtual Button:</span>'
+'<select id=vcsel>'
+'<option value="">None</option>'
+'<option value="200" disabled style=color:#555>btn:200 — Off</option>'
+'<option value="201">btn:201</option><option value="202">btn:202</option>'
+'<option value="203">btn:203</option><option value="204">btn:204</option><option value="205">btn:205</option>'
+'<option value="206">btn:206</option><option value="207">btn:207</option><option value="208">btn:208</option>'
+'</select>'
+'<span style=font-size:12px;color:#666>Shelly app trigger</span>'
+'</div>'
+'<div id=codepanel style=display:none>'
+'<textarea id=codeta style=width:100%;height:200px;resize:none;background:#161616;color:#0f0;border:1px solid #333;border-radius:6px;font:12px monospace;padding:8px;box-sizing:border-box></textarea>'
+'</div>'
+'<div id=steps></div>'
+'<button onclick=addStep() id=addbtn style=margin-top:8px;width:100%;padding:10px>+ Add step</button>'
+'</div>'
+'<div id=lg></div></div>'
+'<script src="/script/'+sid+'/asset?f=eapp1"></scr'+'ipt>'
+'<script src="/script/'+sid+'/asset?f=eapp2"></scr'+'ipt>'
+'</body></html>';
}
function buildEAPP1(sid){
var js='var B="/script/'+sid+'",curName=null,steps=[],isBuiltin=false;';
js+='var BUILTIN={Rainbow:1,Candle:1,Ocean:1,Sunset:1,Party:1,Breath_White:1,Breath_Red:1,Breath_Green:1,Breath_Blue:1,Breath_Yellow:1,Breath_Purple:1,Nordic:1,Breath_RGB:1};';
js+='function Q(i){return document.getElementById(i)}';
js+='function lg(m){var d=Q("lg");d.innerHTML="<div>"+m+"</div>"+d.innerHTML}';
js+='function h2(n){return("0"+n.toString(16)).slice(-2)}';
js+='function toHex(r,g,b){return"#"+h2(r)+h2(g)+h2(b)}';
js+='function tr(id,val,max,col){var el=Q(id);if(!el)return;var p=Math.round(val/max*100);el.style.background="linear-gradient(to right,"+col+" "+p+"%,#333 "+p+"%)"}';
js+='function loadList(){fetch(B+"/ctrl?cmd=status").then(function(r){return r.text()}).then(function(t){';
js+='var d=JSON.parse(t),h="";';
js+='for(var i=0;i<d.presets.length;i++){var k=d.presets[i];if(k==="manuel"||k==="_preview")continue;';
js+='h+="<button onclick=\\"loadPreset(\'"+k+"\')\\" "+(curName===k?"class=on":BUILTIN[k]?"":"class=custom")+">"+k+"</button>"}';
js+='Q("psel").innerHTML=h}).catch(function(e){lg("ERR:"+e)})}';
js+='function loadPreset(name){fetch(B+"/ctrl?cmd=get_preset&name="+name).then(function(r){return r.text()}).then(function(t){'; js+='steps=JSON.parse(t);curName=name;isBuiltin=!!BUILTIN[name];';
js+='Q("ename").textContent=name;Q("editor").style.display="";Q("dbtn").style.display=isBuiltin?"none":"";';
js+='if(Q("codepanel").style.display!=="none")Q("codeta").value=JSON.stringify(steps,null,2);';
js+='fetch(B+"/ctrl?cmd=get_vc&name="+name).then(function(r){return r.text()}).then(function(v){Q("vcsel").value=v||""});';
js+='renderSteps();loadList()}).catch(function(e){lg("ERR:"+e)})}';
js+='function mkSl(i,id,mn,mx,val,col,sfx,stp){return"<div class=sr2><span class=fl>"+id.toUpperCase()+"</span><input type=range id="+id+i+" min="+mn+" max="+mx+" step="+(stp||1)+" value="+val+" oninput=upd("+i+") style=\\"accent-color:"+col+"\\"><span class=sv id=v"+id+i+">"+val+(sfx||"")+"</span></div>"}';
js+='function renderSteps(){var h="";for(var i=0;i<steps.length;i++){var s=steps[i];';
js+='h+="<div class=step><div class=step-hd><span class=step-num>Step "+(i+1)+"</span>";';
js+='h+="<div style=\\"display:flex;gap:6px;align-items:center\\"><div class=swatch id=sw"+i+" style=\\"background:"+toHex(s.r,s.g,s.b)+"\\"></div>";';
js+='h+="<button onclick=\\"moveStep("+i+",-1)\\">^</button><button onclick=\\"moveStep("+i+",1)\\">v</button>";';
js+='h+="<button onclick=\\"delStep("+i+")\\" class=delbtn>X</button></div></div>";';
js+='h+="<div>"+mkSl(i,"r",0,255,s.r,"#fa4141")+mkSl(i,"g",0,255,s.g,"#41fa6c")+mkSl(i,"b",0,255,s.b,"#06f")+mkSl(i,"ct",2700,6500,s.ct,"#d9d9d9","K")+mkSl(i,"br",1,100,s.br,"#ffe68c","%")+mkSl(i,"du",0.5,30,s.dur,"#6af","s",0.5)+"</div></div>";}';
js+='Q("steps").innerHTML=h;';
js+='for(var si=0;si<steps.length;si++){tr("r"+si,steps[si].r,255,"#fa4141");tr("g"+si,steps[si].g,255,"#41fa6c");tr("b"+si,steps[si].b,255,"#06f");tr("ct"+si,steps[si].ct-2700,3800,"#d9d9d9");tr("br"+si,steps[si].br,100,"#ffe68c");tr("du"+si,steps[si].dur,30,"#6af");}}';
js+='loadList();';
return js;
}
function buildEAPP2(sid){
var js='';
js+='function upd(i){var r=+Q("r"+i).value,g=+Q("g"+i).value,b=+Q("b"+i).value,ct=+Q("ct"+i).value,br=+Q("br"+i).value,du=parseFloat(Q("du"+i).value);';
js+='steps[i]={r:r,g:g,b:b,ct:ct,br:br,dur:du};';
js+='Q("sw"+i).style.background=toHex(r,g,b);Q("vr"+i).textContent=r;Q("vg"+i).textContent=g;Q("vb"+i).textContent=b;Q("vct"+i).textContent=ct+"K";Q("vbr"+i).textContent=br+"%";Q("vdu"+i).textContent=du+"s";';
js+='tr("r"+i,r,255,"#fa4141");tr("g"+i,g,255,"#41fa6c");tr("b"+i,b,255,"#06f");tr("ct"+i,ct-2700,3800,"#d9d9d9");tr("br"+i,br,100,"#ffe68c");tr("du"+i,du,30,"#6af");';
js+='if(previewing){if(_pt)clearTimeout(_pt);_pt=setTimeout(sendPreview,200)}}';
js+='function addStep(){steps.push({r:255,g:0,b:0,ct:4000,br:100,dur:1});renderSteps()}';
js+='function delStep(i){steps.splice(i,1);renderSteps()}';
js+='function moveStep(i,d){if(i+d<0||i+d>=steps.length)return;var t=steps[i];steps[i]=steps[i+d];steps[i+d]=t;renderSteps()}';
js+='var previewing=false,_pt=null;';
js+='function sendPreview(){fetch(B+"/ctrl?cmd=preview",{method:"POST",body:JSON.stringify(steps)}).then(function(r){return r.text()}).then(function(t){lg("preview:"+t)})}';
js+='function previewPreset(){if(previewing){fetch(B+"/ctrl?cmd=stop");previewing=false;Q("pbtn").textContent="Preview";Q("pbtn").classList.remove("on");return}previewing=true;Q("pbtn").textContent="Stop";Q("pbtn").classList.add("on");sendPreview()}';
js+='function savePreset(){if(!curName)return;fetch(B+"/ctrl?cmd=save_preset&name="+curName,{method:"POST",body:JSON.stringify(steps)}).then(function(r){return r.text()}).then(function(t){var vc=Q("vcsel").value;fetch(B+"/ctrl?cmd=save_vc&name="+curName+"&vc="+vc);lg("saved:"+t);loadList()})}';
js+='function toggleCode(){var cp=Q("codepanel"),sp=Q("steps"),ab=Q("addbtn"),cb=Q("cbtn"),sb=Q("savebtn"),ap=Q("applybtn");';
js+='if(cp.style.display==="none"){cp.style.display="";sp.style.display="none";ab.style.display="none";sb.style.display="none";ap.style.display="";var ta=Q("codeta");ta.value=JSON.stringify(steps,null,2);ta.style.height=Math.max(200,steps.length*9*18+40)+"px";cb.textContent="UI";cb.classList.add("on")}';
js+='else{cp.style.display="none";sp.style.display="";ab.style.display="";sb.style.display="";ap.style.display="none";cb.textContent="Code";cb.classList.remove("on")}}';
js+='function applyCode(){try{var p=JSON.parse(Q("codeta").value);if(!Array.isArray(p)||!p.length){lg("Invalid JSON");return}steps=p;toggleCode();renderSteps();if(previewing)sendPreview()}catch(e){lg("JSON error:"+e.message)}}';
js+='function deletePreset(){if(!curName||isBuiltin)return;if(!confirm("Delete "+curName+"?"))return;fetch(B+"/ctrl?cmd=del_preset&name="+curName).then(function(r){return r.text()}).then(function(){curName=null;Q("editor").style.display="none";loadList()})}';
js+='function newPreset(){var n=Q("newname").value.trim().replace(/[^a-z0-9_]/gi,"_");if(!n)return;curName=n;isBuiltin=false;steps=[{r:255,g:0,b:0,ct:4000,br:100,dur:1}];Q("ename").textContent=n;Q("editor").style.display="";Q("dbtn").style.display="";Q("newname").value="";renderSteps();loadList()}';
return js;
}
// ─── HTTP ENDPOINTS ───────────────────────────────────────────────────────────
var _sid=Shelly.getCurrentScriptId()+"";
HTTPServer.registerEndpoint("ctrl",function(req,res){
var q=parseQuery(req.query),cmd=q.cmd,ok="ok";
if(cmd==="start"){startLoop();}
else if(cmd==="stop"){stopLoop();}
else if(cmd==="pause"){pauseLoop();}
else if(cmd==="preset"){ok=setPreset(q.name||"")?"ok":"err";}
else if(cmd==="speed"){CONFIG.speed=parseFloat(q.val)||1;}
else if(cmd==="brightness"){CONFIG.brightness=parseInt(q.val)||100;}
else if(cmd==="set"){
var sr=+q.r||0,sg=+q.g||0,sb=+q.b||0,sct=+q.ct||4000,sbr=+q.br||100,sdu=parseFloat(q.dur)||1;
pauseLoop();
PRESETS.manuel=[{r:sr,g:sg,b:sb,ct:sct,br:sbr,dur:sdu}];
Shelly.call("RGBCCT.Set",{id:CONFIG.id,on:true,rgb:[sr,sg,sb],ct:sct,brightness:sbr,transition_duration:sdu},null);
}
else if(cmd==="status"){
ok=JSON.stringify({running:running,preset:CONFIG.preset,speed:CONFIG.speed,presets:Object.keys(PRESETS)});
}
else if(cmd==="get_preset"){
var n=q.name||"";
if(typeof PRESETS[n]==="string")PRESETS[n]=decPreset(PRESETS[n]);
if(PRESETS[n]){
ok=JSON.stringify(PRESETS[n]);
} else if(n in PRESETS){
Shelly.call("KVS.Get",{key:"rgbcct_preset_"+n},function(r,e){
res.code=200;
res.headers=[["Content-Type","application/json; charset=utf-8"]];
res.body=(!e&&r&&r.value)?JSON.stringify(decPreset(r.value)):"[]";
res.send();
},null);
return;
} else{ok="err";}
}
else if(cmd==="preview"){
try{var ps=JSON.parse(req.body||"[]");if(ps.length){pauseLoop();PRESETS._preview=ps;CONFIG.preset="_preview";running=true;stepIndex=0;nextStep();}}catch(ex){ok="err";}
}
else if(cmd==="save_preset"){
var n=q.name||"";
try{var ps=JSON.parse(req.body||"[]");PRESETS[n]=ps;Shelly.call("KVS.Set",{key:"rgbcct_preset_"+n,value:encPreset(ps)},null);}
catch(ex){ok="err";}
}
else if(cmd==="del_preset"){
var n=q.name||"";
delete PRESETS[n];
Shelly.call("KVS.Delete",{key:"rgbcct_preset_"+n},null);
}
else if(cmd==="save_vc"){
var n=q.name||"",vc=parseInt(q.vc);
if(n){
if(vc>=200&&vc<=208){VCMAP[n]=vc;}
else{delete VCMAP[n];}
Shelly.call("KVS.Set",{key:"rgbcct_vcmap",value:JSON.stringify(VCMAP)},null);
}
}
else if(cmd==="get_vc"){
var n=q.name||"";
ok=String(VCMAP[n]||"");
}
else{ok="unknown";}
send(res,ok);
});
HTTPServer.registerEndpoint("ui", function(req,res){send(res,buildHTML(_sid),"text/html");});
HTTPServer.registerEndpoint("edit",function(req,res){send(res,buildEditHTML(_sid),"text/html");});
HTTPServer.registerEndpoint("asset",function(req,res){
var f=parseQuery(req.query).f||"";
if (f==="css") send(res,buildCSS(),"text/css");
else if(f==="app") send(res,buildAPP(_sid),"text/javascript");
else if(f==="eapp1") send(res,buildEAPP1(_sid),"text/javascript");
else if(f==="eapp2") send(res,buildEAPP2(_sid),"text/javascript");
else send(res,"not found");
});
// ── BOOT ─────────────────────────────────────────────────────────────────────
loadKVS();
if(CONFIG.autostart)startLoop();
print("RGBCCT ready. Script ID: "+_sid);
Timer.set(2000,false,function(){
Shelly.call("Script.SetConfig",{id:Shelly.getCurrentScriptId(),config:{enable:true}},function(){
Shelly.call("RGBCCT.SetConfig",{id:0,config:{"name":"http://"+(Shelly.getComponentStatus("wifi").sta_ip??"192.168.33.1")+"/script/"+Shelly.getCurrentScriptId()+"/ui"}},null);
});
});
print("http://" + (Shelly.getComponentStatus("wifi").sta_ip ?? "192.168.33.1") + "/script/" + Shelly.getCurrentScriptId() + "/ui");


