Danke euch! Das komplette Dashboard ist leider zu sehr mit meinen eigenen Entities, Helpern und Backend-Paketen verzahnt, um es 1:1 zu teilen — würde bei euch so nicht laufen. Es ist was, was auf meinem Plan steht das skalierbarer zu machen, für mich und dann auch für die Community.
Aber hier eine Karte aus dem gleichen Baukasten als eigenständiges Beispiel, komplett unabhängig testbar: eine Klima-Zeilenkarte mit farbcodiertem Status-Icon, aktueller Temperatur und +/- Stepper mit sofortigem UI-Feedback (kein Warten auf den Service-Call).
Voraussetzung: nur die HACS-Karte button-card — keine weiteren Templates nötig.
Setup: einfach eure eigenen climate.*-Entity-IDs in die climate_entities:-Liste eintragen — Name, Icon und Zustandsfarbe kommen automatisch aus dem jeweiligen Entity.
Das Grundmuster (optimistisches UI + debounced Service-Call, Icon-Tap öffnet More-Info) ist das gleiche, das ich für alle Stepper-Controls im Dashboard verwende — lässt sich auf andere Domains (z. B. light mit Helligkeit) übertragen.
Grundsätzlich ist jede Karte im Dashboard einzeln von Hand aufgebaut — vorher steht aber ein festes Design-Konzept (Farben, Typografie, Abstände, Radien), das dann Karte für Karte umgesetzt und live getestet wird, damit am Ende alles wie aus einem Guss wirkt.
# Standalone custom:button-card, keine externen Templates noetig.
# Benoetigt nur HACS "button-card" (https://github.com/custom-cards/button-card).
#
# Setup: einfach eure eigenen climate.*-Entity-IDs in die Liste unten
# (climate_entities) eintragen — Name, Icon und Zustandsfarbe werden
# automatisch aus dem jeweiligen Entity gezogen. Kein Label/Icon pro
# Raum noetig.
#
# Was sie zeigt:
# - pro Entity: Status-Icon (Farbe + Symbol je nach HVAC-Modus),
# Name, aktuelle Temperatur, +/- Stepper mit optimistischem
# UI-Update + debounced set_temperature-Call, Tap aufs Icon
# oeffnet die native More-Info.
#
type: custom:button-card
show_name: false
show_icon: false
show_state: false
triggers_update: all
tap_action:
action: none
variables:
title: Klima
climate_entities:
- climate.living_room
- climate.bedroom
- climate.office
styles:
card:
- background: rgba(20, 20, 22, 0.72)
- border: 1px solid rgba(255, 255, 255, 0.07)
- border-radius: 18px
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28)
- padding: 16px
grid:
- grid-template-areas: '"content"'
custom_fields:
content:
- pointer-events: auto
- position: relative
- z-index: 5
custom_fields:
content: |
[[[
if (!window.__qhomeDummyPending) window.__qhomeDummyPending = {};
if (!window.__qhomeDummyTimers) window.__qhomeDummyTimers = {};
const ACCENT = '#4AA8FF';
const MUTED = 'rgba(190,190,190,0.50)';
const MODE_PRESENTATION = {
heat: { icon: 'mdi:fire', color: '#FF9E45' },
cool: { icon: 'mdi:snowflake', color: ACCENT },
dry: { icon: 'mdi:water-percent', color: '#56D63F' },
fan_only: { icon: 'mdi:fan', color: ACCENT },
auto: { icon: 'mdi:thermostat-auto', color: ACCENT },
off: { icon: 'mdi:thermostat', color: MUTED }
};
function fmt(value) {
return Number.isFinite(value) ? value.toFixed(1).replace('.', ',') : '–';
}
function labelFor(entityId, state) {
return state?.attributes?.friendly_name || entityId;
}
window.__qhomeDummyAdjust = function (entityId, direction, targetId) {
const st = hass.states[entityId];
if (!st) return;
const step = st.attributes.target_temp_step || 0.5;
const min = st.attributes.min_temp ?? 16;
const max = st.attributes.max_temp ?? 30;
const pending = window.__qhomeDummyPending[entityId];
const base = Number.isFinite(pending) ? pending : Number(st.attributes.temperature);
const next = Math.min(max, Math.max(min, Math.round((base + direction * step) * 10) / 10));
window.__qhomeDummyPending[entityId] = next;
const targetEl = document.getElementById(targetId);
if (targetEl) targetEl.textContent = fmt(next) + ' °C';
clearTimeout(window.__qhomeDummyTimers[entityId]);
window.__qhomeDummyTimers[entityId] = setTimeout(() => {
hass.callService('climate', 'set_temperature', {
entity_id: entityId,
temperature: window.__qhomeDummyPending[entityId]
}).catch((err) => console.error('QHOME dummy: set_temperature failed', err))
.finally(() => { delete window.__qhomeDummyPending[entityId]; });
}, 400);
};
window.__qhomeDummyMoreInfo = function (entityId) {
document.querySelector('home-assistant')?.dispatchEvent(
new CustomEvent('hass-more-info', { bubbles: true, composed: true, detail: { entityId } })
);
};
const entities = variables.climate_entities || [];
const rows = entities.map((entityId, index) => {
const st = hass.states[entityId];
const unavailable = !st || ['unavailable', 'unknown'].includes(st.state);
const mode = st ? st.state : 'off';
const presentation = MODE_PRESENTATION[mode] || MODE_PRESENTATION.off;
const label = labelFor(entityId, st);
const current = st ? Number(st.attributes.current_temperature) : NaN;
const pending = window.__qhomeDummyPending[entityId];
const target = Number.isFinite(pending) ? pending : (st ? Number(st.attributes.temperature) : NaN);
const targetId = 'qhome_dummy_target_' + entityId.replace(/\./g, '_');
const isLast = index === entities.length - 1;
return `
<div style="
display:flex;align-items:center;gap:12px;padding:12px 2px;
${isLast ? '' : 'border-bottom:1px solid rgba(255,255,255,0.055);'}
">
<ha-icon icon="${presentation.icon}"
style="--mdc-icon-size:22px;color:${unavailable ? MUTED : presentation.color};flex:0 0 auto;cursor:pointer;"
onclick="window.__qhomeDummyMoreInfo('${entityId}')"
></ha-icon>
<div style="flex:1;min-width:0;">
<div style="font-size:14px;color:#F4F4F4;">${label}</div>
<div style="font-size:12px;color:rgba(220,220,220,0.72);margin-top:2px;">
${unavailable ? 'Nicht verfügbar' : fmt(current) + ' °C aktuell'}
</div>
</div>
<button
style="width:30px;height:30px;border-radius:9px;border:none;background:rgba(24,24,27,0.82);color:#F4F4F4;font-size:17px;line-height:1;"
${unavailable ? 'disabled' : ''}
onclick="event.stopPropagation(); window.__qhomeDummyAdjust('${entityId}', -1, '${targetId}');"
>–</button>
<div id="${targetId}" style="min-width:62px;text-align:center;font-size:14px;font-weight:600;color:${unavailable ? MUTED : ACCENT};">
${fmt(target)} °C
</div>
<button
style="width:30px;height:30px;border-radius:9px;border:none;background:rgba(24,24,27,0.82);color:#F4F4F4;font-size:17px;line-height:1;"
${unavailable ? 'disabled' : ''}
onclick="event.stopPropagation(); window.__qhomeDummyAdjust('${entityId}', 1, '${targetId}');"
>+</button>
</div>
`;
}).join('');
return `
<div>
<div style="text-align:center;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:rgba(220,220,220,0.72);margin-bottom:8px;">
${variables.title}
</div>
${rows}
</div>
`;
]]]