<!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