in der configuration.yaml habe ich folgendes stehen, damit über weblink auf das Dashboard zugegriffen werden kann. Die Index.html liegt im meinem Fall unter File Editor /www/index.html ab.
Zudem braucht es noch einen Langlebigen Zugriffstoken der unter Sicherheit erstellt wird und zum Login verwendet wird. Den gebe ich einmalig ein, es sei denn, man verändert ständig was, dann wird’s etwas nervig jedes mal den frontcache zu refreshen und die Eingabe zu wiederholen. 
panel_iframe:
smarthome:
title: “Smart Home”
icon: “mdi:home-heart”
url: “http://Meine-Ip:8123/local/index.html”
index.html (eigenen Longlife Token implementieren)
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Andy & Sandra – Thermostat</title>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&display=swap" rel="stylesheet">
<style>
:root {
--bg: #f2f2f7;
--card: #ffffff;
--blue: #3b82f6;
--t1: #1c1c1e;
--t2: #6b6b6e;
--r24: 24px;
--shadow: 0 4px 20px rgba(0,0,0,0.08);
}
* { box-sizing: border-box; margin:0; padding:0; }
body {
font-family: 'Nunito', sans-serif;
background: var(--bg);
min-height: 100vh;
max-width: 430px;
margin: 0 auto;
color: var(--t1);
}
#token-screen {
position: fixed; inset: 0; background: var(--bg);
display: flex; flex-direction: column; align-items: center;
justify-content: center; padding: 30px; z-index: 100;
}
#token-screen h2 { font-size: 28px; font-weight: 900; margin-bottom: 8px; }
#token-screen p { font-size: 14px; color: var(--t2); text-align: center; margin-bottom: 24px; }
input {
width: 100%; padding: 16px; border-radius: 16px;
border: 2px solid rgba(59,130,246,0.3); background: white;
font-family: 'Nunito',sans-serif; font-size: 15px; font-weight: 700;
margin-bottom: 12px; outline: none;
}
button {
width: 100%; padding: 16px; border-radius: 16px;
background: var(--blue); color: white; border: none;
font-family: 'Nunito',sans-serif; font-size: 16px; font-weight: 800;
cursor: pointer;
}
#conn-banner {
position: fixed; top: 0; left: 0; right: 0; max-width: 430px; margin: 0 auto;
background: #ef4444; color: white; text-align: center; padding: 10px;
font-weight: 700; display: none; z-index: 999;
}
#conn-banner.show { display: block; }
.climate-card {
background: linear-gradient(135deg,#eff6ff,#f0f9ff);
border-radius: var(--r24); padding: 24px;
box-shadow: var(--shadow); margin: 20px 14px;
border: 1.5px solid rgba(59,130,246,0.15);
}
.cc-header {
display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;
}
.cc-label { font-size: 15px; font-weight: 900; color: var(--t1); }
.cc-temp {
font-size: 62px; font-weight: 900; letter-spacing: -3px; line-height: 1;
margin: 8px 0;
}
.cc-temp sup { font-size: 26px; }
.cc-stats {
display: flex; gap: 24px; margin-top: 16px;
}
.cs-label { font-size: 10px; color: var(--t2); font-weight: 700; text-transform: uppercase; }
.cs-val { font-size: 17px; font-weight: 800; color: var(--t1); }
.cc-btns {
display: flex; gap: 12px; margin-top: 24px;
}
.cc-btn {
flex: 1; padding: 14px; border-radius: 14px;
border: none; font-weight: 800; font-size: 15px; cursor: pointer;
}
.cc-btn.cool { background: #e0f0ff; color: var(--blue); }
.cc-btn.warm { background: var(--blue); color: white; }
.power-btn {
width: 52px; height: 52px; border-radius: 16px;
display: flex; align-items: center; justify-content: center;
font-size: 24px; background: rgba(249,115,22,0.15); color: #f97316;
cursor: pointer;
}
.power-btn.off { background: #e5e7eb; color: #64748b; }
</style>
</head>
<body>
<!-- Token Screen -->
<div id="token-screen">
<div style="font-size:64px;margin-bottom:20px">🌡️</div>
<h2>Thermostat</h2>
<p>Verbinde mit Home Assistant</p>
<input id="host-input" type="text" placeholder="https://dein-ha.ui.nabu.casa" value="">
<input id="token-input" type="password" placeholder="Long-Lived Access Token">
<button onclick="connectManual()">Verbinden</button>
<div id="token-err" style="color:#ef4444;font-weight:700;margin-top:12px;font-size:13px"></div>
</div>
<!-- Connection Banner -->
<div id="conn-banner">⚠️ Verbindung unterbrochen – versuche erneut...</div>
<!-- Main App -->
<div id="app" style="display:none; padding:20px 14px;">
<div style="text-align:center;margin-bottom:20px">
<h1 style="font-size:28px;font-weight:900;margin-bottom:4px">Klima Steuerung</h1>
<p id="greeting" style="color:var(--t2);font-weight:600">Guten Tag</p>
</div>
<!-- Wohnzimmer -->
<div class="climate-card" id="card-wohnzimmer">
<div class="cc-header">
<div class="cc-label">🛋️ Wohnzimmer</div>
<div class="power-btn" id="power-wohnzimmer" onclick="toggleClimate('climate.heizung_wohnzimmer')">🔥</div>
</div>
<div class="cc-temp" id="temp-wohnzimmer">–<sup>°</sup></div>
<div class="cc-stats">
<div><div class="cs-label">Aktuell</div><div class="cs-val" id="act-wohnzimmer">–°</div></div>
<div><div class="cs-label">Feuchte</div><div class="cs-val" id="hum-wohnzimmer">–%</div></div>
<div><div class="cs-label">Außen</div><div class="cs-val" id="out-wohnzimmer">–°</div></div>
</div>
<div class="cc-btns">
<button class="cc-btn cool" onclick="setTemp('climate.heizung_wohnzimmer', -0.5)">- Kühler</button>
<button class="cc-btn warm" onclick="setTemp('climate.heizung_wohnzimmer', 0.5)">+ Wärmer</button>
</div>
</div>
<!-- Schlafzimmer -->
<div class="climate-card" id="card-schlafzimmer">
<div class="cc-header">
<div class="cc-label">🛏️ Schlafzimmer</div>
<div class="power-btn" id="power-schlafzimmer" onclick="toggleClimate('climate.smart_radiator_thermostat_x')">🔥</div>
</div>
<div class="cc-temp" id="temp-schlafzimmer">–<sup>°</sup></div>
<div class="cc-stats">
<div><div class="cs-label">Aktuell</div><div class="cs-val" id="act-schlafzimmer">–°</div></div>
<div><div class="cs-label">Feuchte</div><div class="cs-val" id="hum-schlafzimmer">–%</div></div>
<div><div class="cs-label">Außen</div><div class="cs-val" id="out-schlafzimmer">–°</div></div>
</div>
<div class="cc-btns">
<button class="cc-btn cool" onclick="setTemp('climate.smart_radiator_thermostat_x', -0.5)">- Kühler</button>
<button class="cc-btn warm" onclick="setTemp('climate.smart_radiator_thermostat_x', 0.5)">+ Wärmer</button>
</div>
</div>
<!-- Büro -->
<div class="climate-card" id="card-buero">
<div class="cc-header">
<div class="cc-label">💼 Büro</div>
<div class="power-btn" id="power-buero" onclick="toggleClimate('climate.smart_radiator_thermostat_x_2')">🔥</div>
</div>
<div class="cc-temp" id="temp-buero">–<sup>°</sup></div>
<div class="cc-stats">
<div><div class="cs-label">Aktuell</div><div class="cs-val" id="act-buero">–°</div></div>
<div><div class="cs-label">Feuchte</div><div class="cs-val" id="hum-buero">–%</div></div>
<div><div class="cs-label">Außen</div><div class="cs-val" id="out-buero">–°</div></div>
</div>
<div class="cc-btns">
<button class="cc-btn cool" onclick="setTemp('climate.smart_radiator_thermostat_x_2', -0.5)">- Kühler</button>
<button class="cc-btn warm" onclick="setTemp('climate.smart_radiator_thermostat_x_2', 0.5)">+ Wärmer</button>
</div>
</div>
</div>
<script>
// ── CONFIG ─────────────────────────────────────
let HA_URL = '';
let HA_TOKEN = '';
let ws = null;
let msgId = 1;
let states = {};
// Deine Thermostat-Entitäten (anpassen falls nötig)
const CLIMATE_ROOMS = {
wohnzimmer: { entity: 'climate.heizung_wohnzimmer', sensor: 'sensor.heizung_wohnzimmer_temperatur', hum: 'sensor.heizung_wohnzimmer_luftfeuchtigkeit' },
schlafzimmer: { entity: 'climate.smart_radiator_thermostat_x', sensor: 'sensor.smart_radiator_thermostat_x_temperatur', hum: 'sensor.smart_radiator_thermostat_x_luftfeuchtigkeit' },
buero: { entity: 'climate.smart_radiator_thermostat_x_2', sensor: 'sensor.smart_radiator_thermostat_x_temperatur_2', hum: 'sensor.smart_radiator_thermostat_x_luftfeuchtigkeit_2' }
};
// ── CONNECT ─────────────────────────────────────
function connectManual() {
let url = document.getElementById('host-input').value.trim();
const token = document.getElementById('token-input').value.trim();
if (!url || !token) {
showErr('Bitte URL und Token eingeben');
return;
}
HA_URL = url.startsWith('http') ? url : 'https://' + url;
HA_TOKEN = token;
localStorage.setItem('ha_url', HA_URL);
localStorage.setItem('ha_token', HA_TOKEN);
startWebSocket();
}
function startWebSocket() {
const wsUrl = HA_URL.replace('https://', 'wss://').replace('http://', 'ws://') + '/api/websocket';
ws = new WebSocket(wsUrl);
ws.onopen = () => console.log('WebSocket verbunden');
ws.onmessage = (evt) => {
const msg = JSON.parse(evt.data);
if (msg.type === 'auth_required') {
ws.send(JSON.stringify({ type: 'auth', access_token: HA_TOKEN }));
}
else if (msg.type === 'auth_ok') {
document.getElementById('token-screen').style.display = 'none';
document.getElementById('app').style.display = 'block';
ws.send(JSON.stringify({ id: msgId++, type: 'get_states' }));
subscribeEvents();
}
else if (msg.type === 'auth_invalid') {
showErr('Token ungültig');
logout();
}
else if (msg.type === 'result' && msg.success) {
msg.result.forEach(s => states[s.entity_id] = s);
renderAll();
}
else if (msg.type === 'event' && msg.event?.event_type === 'state_changed') {
const d = msg.event.data;
if (d.new_state) {
states[d.entity_id] = d.new_state;
renderEntity(d.entity_id);
}
}
};
ws.onerror = ws.onclose = () => {
document.getElementById('conn-banner').classList.add('show');
setTimeout(startWebSocket, 5000);
};
}
function subscribeEvents() {
ws.send(JSON.stringify({ id: msgId++, type: 'subscribe_events', event_type: 'state_changed' }));
}
function logout() {
localStorage.removeItem('ha_token');
document.getElementById('app').style.display = 'none';
document.getElementById('token-screen').style.display = 'flex';
}
// ── RENDER ─────────────────────────────────────
function renderAll() {
updateGreeting();
Object.keys(CLIMATE_ROOMS).forEach(renderClimateRoom);
}
function renderEntity(entityId) {
if (entityId.startsWith('climate.') || entityId.includes('temperatur') || entityId.includes('luftfeuchtigkeit')) {
renderAll();
}
}
function renderClimateRoom(key) {
const room = CLIMATE_ROOMS[key];
const target = states[room.entity]?.attributes?.temperature || '–';
const current = states[room.sensor]?.state || '–';
const humidity = states[room.hum]?.state || '–';
const outside = states['sensor.openweathermap_temperature_2']?.state || '–';
document.getElementById('temp-' + key).innerHTML = target + '<sup>°</sup>';
document.getElementById('act-' + key).textContent = current + '°';
document.getElementById('hum-' + key).textContent = humidity + '%';
document.getElementById('out-' + key).textContent = outside + '°';
// Power Button Status
const isOn = states[room.entity]?.state !== 'off';
const powerBtn = document.getElementById('power-' + key);
if (powerBtn) powerBtn.classList.toggle('off', !isOn);
}
function updateGreeting() {
const h = new Date().getHours();
const greeting = h < 12 ? 'Guten Morgen' : h < 18 ? 'Guten Tag' : 'Guten Abend';
document.getElementById('greeting').textContent = greeting + ', Andy & Sandra';
}
// ── CONTROLS ─────────────────────────────────────
function toggleClimate(entityId) {
const isOff = states[entityId]?.state === 'off';
callService('climate', isOff ? 'turn_on' : 'turn_off', { entity_id: entityId });
}
function setTemp(entityId, delta) {
let current = parseFloat(states[entityId]?.attributes?.temperature) || 21;
let newTemp = Math.round((current + delta) * 2) / 2;
callService('climate', 'set_temperature', { entity_id: entityId, temperature: newTemp });
}
function callService(domain, service, data) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
id: msgId++,
type: 'call_service',
domain: domain,
service: service,
service_data: data
}));
}
}
function showErr(msg) {
document.getElementById('token-err').textContent = msg;
}
// ── AUTO CONNECT ─────────────────────────────────
window.addEventListener('load', () => {
const savedUrl = localStorage.getItem('ha_url');
const savedToken = localStorage.getItem('ha_token');
if (savedUrl) document.getElementById('host-input').value = savedUrl;
if (savedUrl && savedToken) {
HA_URL = savedUrl;
HA_TOKEN = savedToken;
startWebSocket();
}
});
</script>
</body>
</html>
by HarryP: Post formatiert*