HTML
<!DOCTYPE html>
<html>
<head>
<style>
#devices_roller { display: flex; flex-wrap: wrap; gap: 15px; }
.device {
flex: 1 1 130px; max-width: 200px; padding: 10px;
border: 1px solid #003366; border-radius: 12px;
box-shadow: 2px 2px 5px rgba(0,0,0,0.2); color: white;
}
.device h3 {
margin: 5px 0 10px 0;
font-size: 14px; text-align: center;
}
.btn {
color: white; width: 100%; height: 38px; margin: 4px 0;
border: none; border-radius: 8px; font-size: 11px; cursor: pointer;
}
.btn-up { background: green; }
.btn-stop { background: blue; }
.btn-down { background: orange; color: black; }
.status {
margin-top: 5px;
font-size: 12px;
text-align: center;
font-weight: bold;
color: #fff;
}
</style>
</head>
<body>
<div id="devices_roller"></div>
<script>
function initRollerDashboard(){
const prefix = "roller"; // eindeutiger Prefix nur für dieses Panel
const roomColors = {
"Wohnzimmer": "#1c5a1b",
"Esszimmer": "#19bcd2",
"Terasse": "#7f19d2",
"Küche": "#3fe23c",
"Bad": "#3fe23c",
"Flur": "#3ce2d7"
};
const devices = [
{ room: "Wohnzimmer", devices: [
{ ip:"192.168.178.71", type:"roller", name:"Rollo 1"},
{ ip:"192.168.178.72", type:"roller", name:"Rollo 2"}
]},
{ room: "Terasse", devices: [
{ ip:"192.168.178.131", type:"roller", name:"Markise"}
]},
{ room: "Esszimmer", devices: [
{ ip:"192.168.178.83", type:"roller", name:"Rollo 1"},
{ ip:"192.168.178.84", type:"roller", name:"Rollo 2"}
]},
{ room: "Küche", devices: [
{ ip:"192.168.178.89", type:"roller", name:"Rollo"}
]},
{ room: "Bad", devices: [
{ ip:"192.168.178.195", type:"roller", name:"Rollo"}
]},
{ room: "Flur", devices: [
{ ip:"192.168.178.99", type:"roller", name:"Rollo"}
]}
];
const devicesDiv = document.getElementById("devices_"+prefix);
devicesDiv.innerHTML = "";
devices.forEach(roomBlock => {
const block = document.createElement("div");
block.className = "device";
block.style.backgroundColor = roomColors[roomBlock.room] "#4169E1";
const title = document.createElement("h3");
title.innerText = roomBlock.room;
block.appendChild(title);
roomBlock.devices.forEach(device => {
if(device.type==="roller"){
addRollerButtons(block,device,prefix);
initWebSocket(device,prefix);
}
});
devicesDiv.appendChild(block);
});
function sendRollerCmd(ip,action){
let method=action==="open"?"Cover.Open":action==="close"?"Cover.Close":"Cover.Stop";
fetch(`http://${ip}/rpc`,{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({id:1,method,params:{id:0}})
});
}
function initWebSocket(device,prefix,attempt=0){
try{
const ws=new WebSocket(`ws://${device.ip}/rpc`);
ws.onopen=()=>{
// 1) Initialstatus holen
ws.send('{"id":1,"src":"grafana","method":"Shelly.GetStatus"}');
// 2) Live-Updates abonnieren
ws.send('{"id":2,"src":"grafana","method":"Shelly.Subscribe","params":{"events":["*"]}}');
};
ws.onmessage=(e)=>{
const obj=JSON.parse(e.data);
const status=obj.result||obj.params;
if(status && status["cover:0"]){
const cover=status["cover:0"];
const pos=cover.current_pos;
const state=cover.state;
const st=document.getElementById(`${prefix}_cover_status_${device.ip}`);
if(st){
if(state==="open") st.innerText="Status: offen";
else if(state==="closed") st.innerText="Status: geschlossen";
else if(typeof pos==="number") st.innerText="Position: "+pos+"%";
else st.innerText="Status: unbekannt";
}
}
};
ws.onclose=()=>setTimeout(()=>initWebSocket(device,prefix,attempt+1),2000);
}catch(e){console.error("WS-Fehler",e);}
}
function addRollerButtons(block,device,prefix){
[["AUF","btn-up","open"],["STOP","btn-stop","stop"],["AB","btn-down","close"]]
.forEach(([txt,cls,act])=>{
const b=document.createElement("button");
b.className="btn "+cls;
b.innerText=device.name+" "+txt;
b.onclick=()=>sendRollerCmd(device.ip,act);
block.appendChild(b);
});
const st=document.createElement("div");
st.id=`${prefix}_cover_status_${device.ip}`;
st.className="status";
st.innerText="Status: unbekannt";
block.appendChild(st);
}
}
/* Start */
function safeInit(){
try{initRollerDashboard();}catch(e){console.error("Init-Fehler:",e);}
}
safeInit();
if(document.readyState==="loading"){
document.addEventListener("DOMContentLoaded",safeInit);
}
setTimeout(safeInit,500);
</script>
</body>
</html>
So sieht zb meine Erdgeschoss Steuerung Rolläden aus mit Schalt Überwachung, achtung das ist mit Shelly 2 pm plus (gen4) getestet und funktioniert auch.
<!DOCTYPE html>
<html>
<head>
<style>
#devices_somfy { display: flex; flex-wrap: wrap; gap: 15px; }
.device {
flex: 1 1 150px; max-width: 220px; padding: 10px;
border: 1px solid #003366; border-radius: 12px;
box-shadow: 2px 2px 5px rgba(0,0,0,0.2); color: white;
}
.device h3 { margin: 5px 0 10px 0; font-size: 14px; text-align: center; }
.btn { color: white; width: 100%; height: 38px; margin: 4px 0;
border: none; border-radius: 8px; font-size: 12px; cursor: pointer; }
.btn-up { background: green; }
.btn-stop { background: blue; }
.btn-down { background: yellow; color: black; }
.status { margin-top: 5px; font-size: 12px; text-align: center; font-weight: bold; color: #fff; }
</style>
</head>
<body>
<div id="devices_somfy"></div>
<script>
function initSomfyDashboard(){
const prefix="somfy";
const roomColors = {
"Arbeitszimmer": "#444a97",
"Schlafzimmer": "#d2cc19"
};
const devices = [
{ room:"Arbeitszimmer", devices:[{ip:"192.168.178.61", name:"Rollo Somfy"}]},
{ room:"Schlafzimmer", devices:[{ip:"192.168.178.65", name:"Rollo Somfy"}]}
];
const devicesDiv=document.getElementById("devices_"+prefix);
devicesDiv.innerHTML="";
devices.forEach(roomBlock=>{
const block=document.createElement("div");
block.className="device";
block.style.backgroundColor=roomColors[roomBlock.room]||"#4169E1";
const title=document.createElement("h3");
title.innerText=roomBlock.room;
block.appendChild(title);
roomBlock.devices.forEach(device=>{
addSomfyButtons(block,device,prefix);
initWebSocket(device,prefix);
});
devicesDiv.appendChild(block);
});
function sendSomfyCmd(ip,action){
if(action==="up"){
fetch(`http://${ip}/rpc%60,%7Bmethod:%22POST%22,headers:%7B%22Content-Type%22:%22application/json"},
body:JSON.stringify({id:1,method:"Switch.Set",params:{id:0,on:true}})});
fetch(`http://${ip}/rpc%60,%7Bmethod:%22POST%22,headers:%7B%22Content-Type%22:%22application/json"},
body:JSON.stringify({id:2,method:"Switch.Set",params:{id:1,on:false}})});
}
if(action==="down"){
fetch(`http://${ip}/rpc%60,%7Bmethod:%22POST%22,headers:%7B%22Content-Type%22:%22application/json"},
body:JSON.stringify({id:3,method:"Switch.Set",params:{id:1,on:true}})});
fetch(`http://${ip}/rpc%60,%7Bmethod:%22POST%22,headers:%7B%22Content-Type%22:%22application/json"},
body:JSON.stringify({id:4,method:"Switch.Set",params:{id:0,on:false}})});
}
if(action==="stop"){
fetch(`http://${ip}/rpc%60,%7Bmethod:%22POST%22,headers:%7B%22Content-Type%22:%22application/json"},
body:JSON.stringify({id:5,method:"Switch.Set",params:{id:0,on:false}})});
fetch(`http://${ip}/rpc%60,%7Bmethod:%22POST%22,headers:%7B%22Content-Type%22:%22application/json"},
body:JSON.stringify({id:6,method:"Switch.Set",params:{id:1,on:false}})});
}
}
function initWebSocket(device,prefix,attempt=0){
try{
const ws=new WebSocket(`ws://${device.ip}/rpc`);
ws.onopen=()=>{
ws.send('{"id":1,"src":"grafana","method":"Shelly.GetStatus"}');
ws.send('{"id":2,"src":"grafana","method":"Shelly.Subscribe","params":{"events":["*"]}}');
};
ws.onmessage=(e)=>{
const obj=JSON.parse(e.data);
const status=obj.result||obj.params;
if(status && status["switch:0"] && status["switch:1"]){
const st=document.getElementById(`${prefix}_status_${device.ip}`);
if(st){
if(status["switch:0"].output) st.innerText="Status: AUF";
else if(status["switch:1"].output) st.innerText="Status: AB";
else st.innerText="Status: STOP";
}
}
};
ws.onclose=()=>setTimeout(()=>initWebSocket(device,prefix,attempt+1),2000);
}catch(e){console.error("WS-Fehler",e);}
}
function addSomfyButtons(block,device,prefix){
[["AUF","btn-up","up"],["STOP","btn-stop","stop"],["AB","btn-down","down"]]
.forEach(([txt,cls,act])=>{
const b=document.createElement("button");
b.className="btn "+cls;
b.innerText=device.name+" "+txt;
b.onclick=()=>sendSomfyCmd(device.ip,act);
block.appendChild(b);
});
const st=document.createElement("div");
st.id=`${prefix}_status_${device.ip}`;
st.className="status";
st.innerText="Status: unbekannt";
block.appendChild(st);
}
}
function safeInit(){ try{initSomfyDashboard();}catch(e){console.error(e);} }
safeInit();
if(document.readyState==="loading"){ document.addEventListener("DOMContentLoaded",safeInit); }
setTimeout(safeInit,500);
</script>
</body>
</html>
Das wäre die Rolladen Schaltung als Schalter version für 2 Kontakte.
<!DOCTYPE html>
<html>
<head>
<style>
/* Container nur für dieses Panel (EG) */
#devices_eg { display: flex; flex-wrap: wrap; gap: 15px; }
/* Raumkarte */
.device {
flex: 1 1 130px; max-width: 200px; padding: 10px;
border: 1px solid #003366; border-radius: 12px;
box-shadow: 2px 2px 5px rgba(0,0,0,0.2); color: white;
}
.device h3 { margin: 5px 0 10px 0; font-size: 14px; text-align: center; }
/* Schalt-Buttons */
.btn {
color: white; width: 100%; height: 38px; margin: 4px 0;
border: none; border-radius: 8px; font-size: 12px; cursor: pointer;
}
.btn-on { background: green; }
.btn-off { background: red; }
</style>
</head>
<body>
<div id="devices_eg"></div>
<script>
(function(){
const PREFIX = "eg"; // eindeutig für dieses Panel
const CONTAINER_ID = "devices_eg";
const roomColors = {
"Arbeitszimmer": "#444a97",
"Schlafzimmer": "#d2cc19",
"Wohnzimmer": "#1c5a1b",
"Terrasse": "#7f19d2",
"Esszimmer": "#19bcd2",
"Küche": "#7e1161",
"Bad": "#3fe23c",
"Flur": "#3ce2d7"
};
// === EG Geräte-Liste ===
const devices = [
{ room: "Arbeitszimmer", devices: [
{ ip:"192.168.178.60", relays:[{id:0,name:"Licht"}] },
{ ip:"192.168.178.62", relays:[{id:0,name:"Steckdose 1"}] },
{ ip:"192.168.178.63", relays:[{id:0,name:"Steckdose 2"}] }
]},
{ room: "Schlafzimmer", devices: [
{ ip:"192.168.178.64", relays:[{id:0,name:"Licht"}] },
{ ip:"192.168.178.66", relays:[{id:0,name:"Steckdose 1"}] },
{ ip:"192.168.178.67", relays:[{id:0,name:"Steckdose 2"}] },
{ ip:"192.168.178.68", relays:[{id:0,name:"Steckdose 3"}] },
{ ip:"192.168.178.69", relays:[{id:0,name:"Steckdose 4"}] }
]},
{ room: "Wohnzimmer", devices: [
{ ip:"192.168.178.70", relays:[{id:0,name:"Licht"}] },
{ ip:"192.168.178.73", relays:[{id:0,name:"Steckdose 1"}] },
{ ip:"192.168.178.74", relays:[{id:0,name:"Steckdose 2"}] },
{ ip:"192.168.178.75", relays:[{id:0,name:"Steckdose 3"}] },
{ ip:"192.168.178.76", relays:[{id:0,name:"Steckdose 4"}] },
{ ip:"192.168.178.77", relays:[{id:0,name:"Steckdose 5"}] }
]},
{ room: "Terrasse", devices: [
{ ip:"192.168.178.78", relays:[{id:0,name:"Licht"}] },
{ ip:"192.168.178.80", relays:[{id:0,name:"Steckdose 1"}] },
{ ip:"192.168.178.81", relays:[{id:0,name:"Steckdose 2"}] }
]},
{ room: "Esszimmer", devices: [
{ ip:"192.168.178.82", relays:[{id:0,name:"Licht"}] },
{ ip:"192.168.178.85", relays:[{id:0,name:"Steckdose 1"}] },
{ ip:"192.168.178.86", relays:[{id:0,name:"Steckdose 2"}] },
{ ip:"192.168.178.87", relays:[{id:0,name:"Steckdose 3"}] }
]},
{ room: "Küche", devices: [
{ ip:"192.168.178.88", relays:[{id:0,name:"Licht"}] },
// HIER Änderung: Shelly 2PM → 2 Relais für 2 Hauben
{ ip:"192.168.178.90", relays:[
{id:0,name:"Dunstabzug 1"},
{id:1,name:"Dunstabzug 2"}
]},
{ ip:"192.168.178.91", relays:[
{id:0,name:"Herd Phase 1"},
{id:1,name:"Herd Phase 2"},
{id:2,name:"Herd Phase 3"}
]},
{ ip:"192.168.178.92", relays:[{id:0,name:"Steckdose 1"}] },
{ ip:"192.168.178.93", relays:[{id:0,name:"Steckdose 2"}] }
]},
{ room: "Bad", devices: [
{ ip:"192.168.178.94", relays:[{id:0,name:"Licht"}] },
{ ip:"192.168.178.96", relays:[{id:0,name:"Spiegel Licht"}] }
]},
{ room: "Flur", devices: [
{ ip:"192.168.178.97", relays:[{id:0,name:"Licht Innen"}] },
{ ip:"192.168.178.98", relays:[{id:0,name:"Treppe Licht 1"}] },
{ ip:"192.168.178.100", relays:[{id:0,name:"Treppe Licht 2"}] },
{ ip:"192.168.178.102", relays:[{id:0,name:"Steckdose Haustür"}] },
{ ip:"192.168.178.103", relays:[{id:0,name:"Haustür Licht"}] }
]}
];
/* ===========================
GLOBALER WEBSOCKET-POOL
=========================== */
const WS_POOL_KEY = "__ShellyWsPool";
const pool = window[WS_POOL_KEY] (window[WS_POOL_KEY] = new Map());
function ensureSocket(ip){
if (pool.has(ip)) return pool.get(ip);
const entry = { ws:null, listeners:new Set(), lastMsgTs:0, pollTimer:null };
pool.set(ip, entry);
function connect(attempt=0){
try {
const ws = new WebSocket(`ws://${ip}/rpc`);
entry.ws = ws;
ws.onopen = () => {
try{ ws.send('{"id":1,"src":"grafana","method":"Shelly.GetStatus"}'); }catch(_){}
};
ws.onmessage = (e) => {
entry.lastMsgTs = Date.now();
const msg = JSON.parse(e.data);
const status = msg.result msg.params;
if(!status) return;
Object.keys(status).forEach(k=>{
const m=k.match(/^switch:(\d+)$/);
if(m && status[k] && typeof status[k].output==="boolean"){
const relId=parseInt(m[1],10);
const isOn=status[k].output;
entry.listeners.forEach(fn=>{ try{fn({ip,relId,isOn});}catch(_){}} );
}
});
};
ws.onclose=()=>{ setTimeout(()=>connect(attempt+1), Math.min(10000,1000*(attempt+1))); };
ws.onerror=()=>{ try{ws.close();}catch(_){}};
} catch(e){ setTimeout(()=>connect(attempt+1),2000); }
}
connect();
return entry;
}
function subscribe(ip, handler){
const entry=ensureSocket(ip);
entry.listeners.add(handler);
(async()=>{
try{
const res=await fetch(`http://${ip}/rpc/Shelly.GetStatus`,{
method:"POST",headers:{"Content-Type":"application/json"},body:"{}"
});
const data=await res.json();
const status=data && data.result ? data.result : null;
if(status){
Object.keys(status).forEach(k=>{
const m=k.match(/^switch:(\d+)$/);
if(m && status[k] && typeof status[k].output==="boolean"){
handler({ip, relId:parseInt(m[1],10), isOn:status[k].output});
}
});
}
}catch(_){}
})();
}
/* ===========================
UI
=========================== */
const root=document.getElementById(CONTAINER_ID);
root.innerHTML="";
devices.forEach(roomBlock=>{
const card=document.createElement("div");
card.className="device";
card.style.backgroundColor=roomColors[roomBlock.room]||"#4169E1";
const h=document.createElement("h3");
h.textContent=roomBlock.room;
card.appendChild(h);
roomBlock.devices.forEach(dev=>{
dev.relays.forEach(rel=>{
const btn=document.createElement("button");
btn.id=`${PREFIX}_relay_${dev.ip}_${rel.id}`;
btn.className="btn btn-off";
btn.textContent=`${rel.name} AUS`;
btn.onclick=()=>toggleRelay(dev.ip, rel.id, rel.name);
card.appendChild(btn);
subscribe(dev.ip, ({ip,relId,isOn})=>{
if(relId!==rel.id) return;
const b=document.getElementById(`${PREFIX}_relay_${ip}_${relId}`);
if(!b) return;
if(isOn){
b.className="btn btn-on";
b.textContent=`${rel.name} EIN`;
} else {
b.className="btn btn-off";
b.textContent=`${rel.name} AUS`;
}
});
});
});
root.appendChild(card);
});
/* ===========================
Toggle
=========================== */
function toggleRelay(ip, relayId, label){
fetch(`http://${ip}/rpc/Switch.Toggle`,{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({id:relayId})
}).catch(err=>console.error("Toggle-Fehler",ip,relayId,err));
}
})(); // Ende
</script>
</body>
</html>
Alles anzeigen
Und das Wäre für die Zimmer die Schalter und Steckdosen Steuerung für das Grafana Text Plugin, hir wurden Shelly 1 pm plus (gen4) getestet und verwendet.
HTML
<!DOCTYPE html>
<html>
<head>
<style>
#devices_garage { display: flex; flex-wrap: wrap; gap: 15px; }
.device {
flex: 1 1 130px; max-width: 200px; padding: 10px;
border: 1px solid #003366; border-radius: 12px;
box-shadow: 2px 2px 5px rgba(0,0,0,0.2); color: white;
}
.device h3 {
margin: 5px 0 10px 0;
font-size: 14px; text-align: center;
}
.btn {
color: white; width: 100%; height: 38px; margin: 4px 0;
border: none; border-radius: 8px; font-size: 12px; cursor: pointer;
}
.btn-up { background: green; }
.btn-stop { background: blue; }
.btn-down { background: yellow; color: black; }
.status {
margin-top: 5px;
font-size: 12px;
text-align: center;
font-weight: bold;
color: #fff;
}
</style>
</head>
<body>
<div id="devices_garage"></div>
<script>
(function(){
const PREFIX = "garage";
const CONTAINER_ID = "devices_garage";
const roomColors = {
"Hebebühne": "#5b8def",
"Garage": "#e67e22",
"Grube": "#1abc9c",
"Stall": "#9b59b6"
};
const devices = [
{ room: "Hebebühne", devices: [ { ip:"192.168.178.104", name:"Tor" } ]},
{ room: "Garage", devices: [ { ip:"192.168.178.109", name:"Tor" } ]},
{ room: "Grube", devices: [ { ip:"192.168.178.110", name:"Tor" } ]},
{ room: "Stall", devices: [ { ip:"192.168.178.111", name:"Tor" } ]}
];
const root = document.getElementById(CONTAINER_ID);
root.innerHTML = "";
devices.forEach(roomBlock=>{
const card=document.createElement("div");
card.className="device";
card.style.backgroundColor=roomColors[roomBlock.room] "#4169E1";
const h=document.createElement("h3");
h.textContent=roomBlock.room;
card.appendChild(h);
roomBlock.devices.forEach(dev=>{
addRollerButtons(card,dev);
initWebSocket(dev);
});
root.appendChild(card);
});
/* ---------- Funktionen ---------- */
function sendRollerCmd(ip,action){
const method = action==="open" ? "Cover.Open"
: action==="close" ? "Cover.Close"
: "Cover.Stop";
fetch(`http://${ip}/rpc`,{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({id:1,method,params:{id:0}})
}).catch(err=>console.error("Roller-Fehler",ip,err));
}
function translateState(state,pos){
if(typeof pos==="number"){
if(pos===0) return `ZU (${pos}%)`;
if(pos===100) return `AUF (${pos}%)`;
}
if(!state) return `unbekannt${pos!==null?` (${pos}%)`:""}`;
const s = String(state).toLowerCase().trim();
if (s==="open" s==="opened") return `AUF (${pos??"?"}%)`;
if (s==="close"|| s==="closed") return `ZU (${pos??"?"}%)`;
if (s==="opening") return `öffnet (${pos??"?"}%)`;
if (s==="closing") return `schließt (${pos??"?"}%)`;
if (s==="stopped" s==="stop") return `STOPP (${pos??"?"}%)`;
return `${state}${pos!==null?` (${pos}%)`:""}`;
}
function updateCoverStatus(ip, cover){
if(!cover) return;
const stEl = document.getElementById(`${PREFIX}_cover_status_${ip}`);
if(!stEl) return;
const pos = (typeof cover.pos === "number") ? cover.pos
: (typeof cover.current_pos === "number") ? cover.current_pos
: (typeof cover.applied_pos === "number") ? cover.applied_pos
: null;
const rawState = cover.state cover.current_state cover.applied_state;
stEl.innerText = "Status: " + translateState(rawState,pos);
}
function initWebSocket(device, attempt=0){
try{
const ws = new WebSocket(`ws://${device.ip}/rpc`);
ws.onopen = ()=>{
try { ws.send('{"id":1,"src":"grafana","method":"Cover.GetStatus","params":{"id":0}}'); } catch(_){}
};
ws.onmessage = (e)=>{
let data;
try { data = JSON.parse(e.data); } catch(_){ return; }
if (data.result) {
updateCoverStatus(device.ip, data.result);
}
if (data.params && data.params["cover:0"]) {
updateCoverStatus(device.ip, data.params["cover:0"]);
}
};
ws.onclose = ()=>{
setTimeout(()=>initWebSocket(device, attempt+1), Math.min(10000, 1000*(attempt+1)));
};
ws.onerror = ()=>{ try{ ws.close(); }catch(_){ } };
}catch(e){
console.error("WS-Fehler", device.ip, e);
}
}
function addRollerButtons(block,device){
[
["AUF","btn-up","open"],
["STOP","btn-stop","stop"],
["AB","btn-down","close"]
].forEach(([txt,cls,act])=>{
const b=document.createElement("button");
b.className="btn "+cls;
b.innerText=device.name+" "+txt;
b.onclick=()=>sendRollerCmd(device.ip,act);
block.appendChild(b);
});
const st=document.createElement("div");
st.id=`${PREFIX}_cover_status_${device.ip}`;
st.className="status";
st.innerText="Status: unbekannt";
block.appendChild(st);
}
})(); // Ende IIFE
</script>
</body>
</html>
Alles anzeigen
So sieht meine Garagentor Steuerung aus ebenfalls mit Zustandsüberwachung.
Und zuguter letzt meine Torlicht Steuerung als Skript für die Shelly 1pm plus gn4 damit die Lichter an gehen wenn das Tor Hoch fährt.
Code
// ###########################################################// Shelly 1PM Plus (Gen4) – folgt Tor (Shelly 2PM im Cover-Modus)// mit Sonnenstands-Sperre (nur nachts aktiv)// ###########################################################//// Endgültige Regeln (nur aktiv wenn es dunkel ist!):// - Bei 20% öffnen -> Licht EIN// - Bei 100% -> Licht 20s EIN, danach AUS (nur einmal)// - Wenn Tor von 100% auf <100% fährt (Start Schließen) -> Licht EIN// - Bei 20% schließen -> Licht AUS//// ###########################################################// IP-Adresse des Shelly 2PM (Torsteuerung)let sourceShelly = "192.168.178.104";// Abfrageintervall (ms)let interval = 5000; // alle 5 Sekunden// Haltezeit wenn Tor komplett offen (in Sekunden)let holdTime = 120;// interne Zuständelet lastOn = false;let holdTimer = null;let holdActive = false;let lastPos = null;// Sonnenauf-/unterganglet sunrise = null;let sunset = null;// Hilfsfunktionenfunction switchLight(on) { Shelly.call("Switch.Set", { id: 0, on: on }); lastOn = on; print("Licht", on ? "EIN" : "AUS");}function cancelHoldTimer() { if (holdTimer !== null) { Timer.clear(holdTimer); holdTimer = null; print("Hold-Timer abgebrochen"); }}function isNight() { if (!sunrise || !sunset) return true; // wenn keine Werte -> sicherheitshalber aktiv let now = (new Date()).getTime() / 1000; // aktuelle Zeit in UNIX Sekunden return (now < sunrise || now > sunset);}function updateSunTimes() { Shelly.call("Sys.GetStatus", {}, function(res) { if (res && res.sunrise && res.sunset) { sunrise = res.sunrise; sunset = res.sunset; print("Sonnenaufgang:", sunrise, "Sonnenuntergang:", sunset); } else { print("Keine Sonnenzeiten gefunden – Nacht-Check immer TRUE"); } });}// Hauptprüfungfunction checkCover() { if (!isNight()) { // Tagsüber -> Licht sicher ausschalten und überspringen if (lastOn) { switchLight(false); cancelHoldTimer(); holdActive = false; } print("Tag erkannt – Lichtsteuerung gesperrt"); return; } let url = "http://" + sourceShelly + "/rpc/Cover.GetStatus?id=0"; Shelly.call("HTTP.GET", { url: url }, function(res) { if (!res || res.code !== 200) { print("HTTP Fehler:", res ? res.code : "kein Ergebnis"); return; } try { let data = JSON.parse(res.body); let pos = data.current_pos; if (typeof pos === "string") pos = parseFloat(pos); print("Tor-Position:", pos, "%", "(last:", lastPos, ")"); // --- Regel 2: Bei 100% -> Licht 20s EIN, danach AUS (nur einmal) if (pos >= 100) { if (!holdActive) { holdActive = true; if (!lastOn) { switchLight(true); print("Licht EIN (Tor 100%, Timer gestartet)"); } cancelHoldTimer(); holdTimer = Timer.set(holdTime * 1000, false, function() { switchLight(false); print("Licht AUS (Timer abgelaufen bei 100%)"); }); } lastPos = pos; return; } // --- Regel 3: Start Schließen von 100% -> Licht EIN if (lastPos !== null && lastPos >= 100 && pos < 100) { if (!lastOn) { switchLight(true); print("Licht EIN (Start Schließen von 100% -> <100%)"); } holdActive = false; cancelHoldTimer(); } if (lastPos !== null) { // --- Regel 1: Bei 20% öffnen -> Crossing von <20% auf >=20% if (lastPos < 20 && pos >= 20) { if (!lastOn) { switchLight(true); print("Licht EIN (Öffnen: Crossing über 20%)"); } } // --- Regel 4: Bei 20% schließen -> Crossing von >20% auf <=20% if (lastPos > 20 && pos <= 20) { if (lastOn) { switchLight(false); print("Licht AUS (Schließen: Crossing unter 20%)"); } holdActive = false; cancelHoldTimer(); } } lastPos = pos; } catch (e) { print("Parsing Fehler:", e); } });}// Timer startenTimer.set(interval, true, checkCover);// Sonnenauf-/untergang alle 10min aktualisierenupdateSunTimes();Timer.set(600000, true, updateSunTimes); // 10 min