Zeigt her eure Dashboards!

Hallo Community,

ich bin Andy und bin seit etwa einem Jahr im HA Universum unterwegs und habe dank der zahlreichen Videos von Simon den Umstieg in ein professionelleres Smart Home getraut. Ich bin eher ein stiller Mitleser dieses tollen Forums und fast schon ehrfürchtig wie kreativ hier viele sind. Es gab dadurch jede menge Anregung, eigenständige Dashboards zu bauen und bin mit Hilfe meines besten Programmierfreundes KI auf die Idee gekommen eins für unterwegs zu entwickeln. Eins für’s Smartphone, dass übersichtlich ist, jede Menge Funktionen parat hält und auch noch gut aussehen soll. Ich bin ein kleiner Design-Freak und das was ich haben wollte hat in yaml nicht so ganz geklappt. Bestimmt ist es auch in yaml umsetzbar aber meine Kenntnisse sind noch etwas begrenzt und habe mich entschieden das Dashboard in html zu schreiben, eben weil HA die Möglichkeit anbietet.

Zudem habe ich auch nicht sooo viele smarte devices, dass es für meine Verhältnisse noch übersichtlich gestaltet werden konnte und es sollte automatisch an jede Displaygröße angepasst sein.
Zudem habe ich versucht den IOS Flow beizubehalten und die Bereiche so intuitiv wie möglich zu gestalten. Kurzdruck oder Langdruck auf etwaige Kacheln lösen ein popup mit div. Veranschaulichungen aus, die Bereiche mit den Punkten lassen sich von links nach rechts wischen. Alle Lampen und Thermostate etc. können gesteuert werden.

Hoffe es sind nicht zu viele Bilder auf einmal : ) Ein kurzes Video konnte ich nicht hochladen….

Besten Grüße

1 „Gefällt mir“

Sieht wirklich gelungen aus. Top !

Könntest ja mal so eine Kachel sharen, insbesondere der Temp Verlauf 24 Stunden ist cool geworden (hier Wohnzimmer)

Vielen Dank!
Ich kann den code für die Kachel extrahieren - bin mir aber nicht sicher ob das weiterhilft, da viele Funktionen dann nicht mehr gegeben sind :slight_smile:

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thermostat Demo</title>
<style>
body{
font-family:Arial;
background:#f2f2f7;
display:flex;
align-items:center;
justify-content:center;
height:100vh;
margin:0;
}
.climate-card{
background:white;
border-radius:20px;
padding:20px;
width:260px;
box-shadow:0 4px 18px rgba(0,0,0,0.12);
cursor:pointer;
}
.cc-header{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:8px;
}
.cc-label{
font-weight:900;
font-size:14px;
}
.cc-power{
width:32px;
height:32px;
border-radius:10px;
background:#eee;
display:flex;
align-items:center;
justify-content:center;
cursor:pointer;
}
.cc-temp{
font-size:48px;
font-weight:900;
}
.cc-btns{
display:flex;
gap:10px;
margin-top:10px;
}
.cc-btn{
flex:1;
padding:10px;
border:none;
border-radius:10px;
font-weight:800;
cursor:pointer;
}
.cool{background:#e8f0fe}
.warm{background:#3b82f6;color:white}
.popup{
position:fixed;
inset:0;
background:rgba(0,0,0,0.4);
display:none;
align-items:center;
justify-content:center;
}
.popup-inner{
background:white;
border-radius:20px;
padding:20px;
width:300px;
text-align:center;
}
canvas{
margin-top:10px;
}
</style>
</head>
<body>
<div class="climate-card" onclick="openPopup()">
<div class="cc-header">
<div class="cc-label">🛋️ Wohnzimmer</div>
<div class="cc-power" onclick="toggleClimate(event)">⏻</div>
</div>
<div class="cc-temp" id="temp">21°</div>
<div class="cc-btns">
<button class="cc-btn cool" onclick="changeTemp(event,-0.5)">-</button>
<button class="cc-btn warm" onclick="changeTemp(event,0.5)">+</button>
</div>
</div>
<div class="popup" id="popup" onclick="closePopup()">
<div class="popup-inner" onclick="event.stopPropagation()"><h3>Temperaturverlauf</h3>
<h2 id="popupTemp">21°</h2>
<div class="cc-btns">
<button class="cc-btn cool" onclick="changeTemp(event,-0.5)">-</button>
<button class="cc-btn warm" onclick="changeTemp(event,0.5)">+</button>
</div>
</div>
</div><script>
let<script>
let temp=21
let history=[20.5,20.8,21,20.7,21.2,21]
function updateUI(){
document.getElementById("temp").innerText=temp.toFixed(1)+"°"
document.getElementById("popupTemp").innerText=temp.toFixed(1)+"°"
drawChart()
}
function changeTemp(e,val){
e.stopPropagation()
temp+=val
history.push(temp)
if(history.length>20) history.shift()
updateUI()
}
function toggleClimate(e){
e.stopPropagation()
alert("Heizung an/aus (Demo)")
}
function openPopup(){
document.getElementById("popup").style.display="flex"
drawChart()
}
function closePopup(){
document.getElementById("popup").style.display="none"
}
function drawChart(){
let canvas=document.getElementById("chart")
let ctx=canvas.getContext("2d")
ctx.clearRect(0,0,canvas.width,canvas.height)
let max=Math.max(...history)+0.5
let min=Math.min(...history)-0.5
let step=canvas.width/(history.length-1)
ctx.beginPath()
history.forEach((t,i)=>{
let x=i*step
let y=canvas.height-( (t-min)/(max-min)*canvas.height )
if(i===0) ctx.moveTo(x,y)
else ctx.lineTo(x,y)
})
ctx.strokeStyle="#3b82f6"
ctx.lineWidth=3
ctx.stroke()
}
setInterval(()=>{
let drift=(Math.random()-0.5)*0.2
temp+=drift
history.push(temp)
if(history.length>20) history.shift()
updateUI()
},4000)
updateUI()
</script>tml>

Welchen Wertebereich bildet ihr denn so in euren Temperatur- und Feuchtigkeits-Charts ab, dass so deutliche Schwankungen zu sehen sind? Bei mir ändert sich die Raumtemperatur mal vielleicht so 2-3 Grad im Tagesverlauf und ich habe Quasi nur gerade Linien:

Und das html lässt du dann wie laufen? Html card?

Schaut echt super aus!

Könntes du eine “Vorlage” hochladen vom Grundgerüst damit ich so etwas nachbauen bzw. umbauen kann? :slightly_smiling_face:

Hab mir auch ein Dashboard gebaut, geht mit dem Layout wohl zum scrollen am Handy als auch auf dem Tablet im Eingangsbereich mit Fully Kiosk in 3 Spalten nebeneinander:

  1. Reihe mit Knöpfen mit Radio und Licht an/aus, außerdem Sprung zur Detailsteuerung der Heizung und Fenster
  2. Wettervorhersage vom DWD
  3. Temperatur draußen sowie Luftquali innen, jeweils farbcodiert nach Zustand
  4. Familienkalender
  5. Auto Ladestand und Leasingkilometer
  6. Status WaMa, Trockner, 3D-Drucker

Hier ist es ziemlich selbsterklärend:

  1. Batterie, bei den blauen Balken wird die Batterie aus dem Netz geladen (macht das HEMS basierend auf dem dynamischen Strompreis)
  2. Strompreis heute (farbig) und morgen (grau), außerdem in blau unterlegt wie viel Strom in den einzelnen Viertelstunden verbraucht wurde
  3. Und die zwei Standardiagramme aus dem Energy Dashboard

Bin aktuell ziemlich zufrieden :slight_smile:

Wenn’s wärmer wird, wird noch so was wie Regentonnenfüllstand und Bodenfeuchte aufgenommen.

Habe auf dem Tablet die Überwachung von Pflanzen jetzt mit animierter Anzeige:

Klick

1 „Gefällt mir“

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. :slight_smile:

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>

:crayon:by HarryP: Post formatiert*

@cyriax Könntest du mir den Code für Wohnung und Garten einmal zur Verfügung stellen? Das wäre mega.
Klasse Dashboard

habe für meine Heizung ein extra Dashboard eingerichtet … schaut aktuell so aus

1 „Gefällt mir“

So schaut meins aus. über solarassistant eingebunden Mqq

1 „Gefällt mir“

(Beitrag vom Verfasser gelöscht)

Wäre vielleicht sinnvoll, das unter einem eigenen Thread zu eröffnen. Hier geht es eher um die Vorstellung funktionierender Dashboards als Ideen Lieferant

Hi zusammen, ich wollte auch mal meine Dashboards hochladen. Einmal habe ich den Start Screen von meinem mobilen Dashboard und einmal mein Freizeitprojekt, ein komplettes Custom Dashboard im Wiser Style :slight_smile:

Und nochmal ein kleiner Einblick in mein Wallpanel nachdem ich nur zwei Bilder pro Post hochladen kann :slight_smile:

2 „Gefällt mir“

Sind die Balken beim Wetter eine Prognose der Sonnenscheindauer? Wenn ja, wie ist das gemacht (woher die Daten und wie visualisiert?)

Das sind nur die Temperaturen die Tagsüber herrschen, aber wenn ich drauf drücke habe ich ein Pop-up für die Horizon Card und Wetterkarte, die Horizon Card könnte das sein was du suchst :grinning_face:

Hi Nussfuellung,
könntest du bitte den code Teilen?
Ich finde gerade den Eingangsscreen schön aufgreräumt und würde den gerne nachbauen :slight_smile: