Ja, kann ich machen. Bitte beachte, die Werte stammen von einem Quandify Watergrip und werden nicht über eine Wasseruhr abgelesen. Die Karte wird rot, falls der Watergrip eine Leckage in der Leitung erkennt. Um den Jahresverbrauch zu ermitteln benutze ich die Variable “v_year_start”. Der Tagesverbrauch wird über einen Helfer “sensor.wasserverbrauch_tag” zurückgesetzt.
type: custom:button-card
show_name: false
show_icon: false
show_state: false
triggers_update:
- sensor.wasserverbrauch_total_volume
- sensor.wasserverbrauch_water_temperature
- sensor.wasserverbrauch_water_type
- sensor.wasserverbrauch_tag
- binary_sensor.wasserverbrauch_leak
variables:
v_volume: sensor.wasserverbrauch_total_volume
v_temp: sensor.wasserverbrauch_water_temperature
v_type: sensor.wasserverbrauch_water_type
v_daily: sensor.wasserverbrauch_tag
v_leak: binary_sensor.wasserverbrauch_leak
v_year_start: 18.225
styles:
card:
- width: 214px
- height: 377px
- border-radius: 16px
- padding: 20px
- overflow: hidden
- margin-top: "-3px"
- margin-left: 1px
- border: |
[[[
return states[variables.v_leak]?.state === 'on'
? '1px solid rgba(195,67,85,0.5)'
: '1px solid rgba(67,152,195,0.25)';
]]]
- background: |
[[[
return states[variables.v_leak]?.state === 'on'
? 'linear-gradient(145deg, #4a1525 0%, #300a14 40%, #1a050c 100%)'
: 'linear-gradient(145deg, #1c3a50 0%, #0e2333 40%, #0a1c2b 100%)';
]]]
- box-shadow: |
[[[
return states[variables.v_leak]?.state === 'on'
? '0 8px 32px rgba(0,0,0,0.5), 0 0 35px rgba(195,67,85,0.45), 0 0 0 1px rgba(195,67,85,0.5)'
: '0 8px 32px rgba(0,0,0,0.4), 0 0 0 1px rgba(67,152,195,0.12)';
]]]
- animation: |
[[[
return states[variables.v_leak]?.state === 'on'
? 'leak-pulse 1.5s ease-in-out infinite alternate'
: 'none';
]]]
grid:
- grid-template-areas: "\"topbar\" \"main\""
- grid-template-columns: 1fr
- grid-template-rows: 0px 1fr
- z-index: "2"
custom_fields:
topbar:
- position: absolute
- top: "0"
- left: "0"
- right: "0"
- bottom: "0"
- pointer-events: none
- z-index: "1"
main:
- z-index: "2"
- width: 100%
- height: 100%
custom_fields:
topbar: |
[[[
const leak = states[variables.v_leak]?.state === 'on';
const c1 = leak ? '#C34355' : '#4398C3';
const c2 = leak ? '#e37e8e' : '#7EC8E3';
const glow = leak ? 'rgba(195,67,85,0.3)' : 'rgba(67,152,195,0.18)';
return `
<div style="position:absolute;top:0;left:0;right:0;height:3px;
background:linear-gradient(90deg,transparent,${c1},${c2},${c1},transparent);
z-index:3;pointer-events:none;"></div>
<div style="position:absolute;top:-20px;right:-20px;
width:150px;height:150px;border-radius:50%;
background:radial-gradient(circle,${glow} 0%,transparent 70%);
pointer-events:none;z-index:1;"></div>`;
]]]
main: |
[[[
const inv = ['unavailable','unknown','none',''];
const leak = states[variables.v_leak]?.state === 'on';
// ── Farbschema ────────────────────────────────────────────────
const accent = leak ? '#e37e8e' : '#7EC8E3';
const accent2 = leak ? 'rgba(227,126,142,1)' : 'rgba(126,200,227,1)';
const muted = leak ? 'rgba(195,67,85,1)' : 'rgba(67,152,195,1)';
const waveC1 = leak ? '#C34355' : '#4398C3';
const waveC2 = leak ? 'rgba(195,67,85,0.35)' : 'rgba(126,200,227,0.35)';
const gearC = leak ? 'rgba(195,67,85,1)' : 'rgba(67,152,195,1)';
const glowTxt = leak
? 'text-shadow:0 0 20px rgba(195,67,85,0.5);'
: 'text-shadow:0 0 20px rgba(67,152,195,0.4);';
// ── Daten laden ───────────────────────────────────────────────
const volRaw = states[variables.v_volume]?.state ?? '';
const tempRaw = states[variables.v_temp]?.state ?? '';
const typeRaw = states[variables.v_type]?.state ?? '';
const dailyRaw = states[variables.v_daily]?.state ?? '';
const vol = parseFloat(volRaw);
const temp = parseFloat(tempRaw);
const daily = parseFloat(dailyRaw);
// ── Berechnungen ──────────────────────────────────────────────
const volM3 = isNaN(vol) ? null : vol / 1000;
const dailyM3 = isNaN(daily) ? null : daily / 1000;
const yearTotal = volM3 !== null ? variables.v_year_start + volM3 : null;
const yearStr = yearTotal !== null ? yearTotal.toFixed(3) : '—';
const dailyStr = dailyM3 === null ? '—'
: dailyM3 < 1
? Math.round(daily) + ' L'
: dailyM3.toFixed(3) + ' mÂł';
const typeOut = inv.includes((typeRaw||'').toLowerCase()) ? '—' : typeRaw;
// ── SVG Wasseruhr ─────────────────────────────────────────────
const waterSvg = `
<style>
@keyframes wv1 {from{transform:translateX(0)}to{transform:translateX(-50%)}}
@keyframes wv2 {from{transform:translateX(0)}to{transform:translateX(-50%)}}
@keyframes wdrop{
0% {opacity:0;transform:translateY(-6px)}
50% {opacity:0.8}
100%{opacity:0;transform:translateY(6px)}
}
@keyframes cog {from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
</style>
<div style="position:relative;width:140px;height:140px;">
<svg width="140" height="140" viewBox="0 0 120 120">
<defs>
<clipPath id="wclip2"><circle cx="60" cy="60" r="50"/></clipPath>
</defs>
<circle cx="60" cy="60" r="52"
fill="${leak ? 'rgba(195,67,85,0.1)' : 'rgba(67,152,195,0.07)'}"
stroke="${leak ? 'rgba(195,67,85,0.3)' : 'rgba(67,152,195,0.22)'}"
stroke-width="1"/>
<g clip-path="url(#wclip2)">
<rect x="10" y="48" width="100" height="60" fill="${waveC1}" opacity="0.45"/>
<g style="animation:wv1 2.5s linear infinite">
<path d="M0,48 Q15,42 30,48 Q45,54 60,48 Q75,42 90,48 Q105,54 120,48
Q135,42 150,48 Q165,54 180,48 Q195,42 210,48 Q225,54 240,48
V115 H0 Z" fill="${waveC1}" opacity="0.6"/>
</g>
<g style="animation:wv2 3.8s linear infinite reverse">
<path d="M0,50 Q15,44 30,50 Q45,56 60,50 Q75,44 90,50 Q105,56 120,50
Q135,44 150,50 Q165,56 180,50 Q195,44 210,50 Q225,56 240,50
V115 H0 Z" fill="${waveC2}"/>
</g>
</g>
<circle cx="60" cy="60" r="52" fill="none"
stroke="${leak ? 'rgba(195,67,85,0.4)' : 'rgba(67,152,195,0.35)'}"
stroke-width="1.5"/>
<circle cx="60" cy="60" r="40" fill="none"
stroke="${leak ? 'rgba(195,67,85,0.15)' : 'rgba(67,152,195,0.1)'}"
stroke-width="0.5"/>
<path d="M60,28 Q71,42 71,51 A11,11 0 0,1 49,51 Q49,42 60,28Z"
fill="rgba(255,255,255,0.18)" stroke="rgba(255,255,255,0.3)" stroke-width="0.5"/>
<circle cx="60" cy="52" r="2.5" fill="white" opacity="0.6"
style="animation:wdrop 1.8s ease-in-out infinite"/>
<text x="60" y="75" text-anchor="middle"
font-family="'EXO 2'" font-size="10" font-weight="500"
fill="${accent}" letter-spacing="0.5">${yearStr}</text>
<text x="60" y="86" text-anchor="middle"
font-family="'EXO 2'" font-size="10" font-weight="500"
fill="${accent2}">mÂł / Jahr</text>
</svg>
<!-- Zahnrad als ha-icon ĂĽber dem SVG -->
<div style="
position:absolute;bottom:140px;right:-15px;
width:24px;height:24px;border-radius:50%;
background:${leak ? 'rgba(195,67,85,0.12)' : 'rgba(67,152,195,0.12)'};
border:1px solid ${leak ? 'rgba(195,67,85,0.3)' : 'rgba(67,152,195,0.3)'};
display:flex;align-items:center;justify-content:center;">
<ha-icon icon="mdi:cog" style="
--mdc-icon-size:18px;
color:${gearC};
animation:cog 3s linear infinite;
display:block;">
</ha-icon>
</div>
</div>`;
return `
<div style="width:100%;height:100%;display:flex;flex-direction:column;">
<!-- BLOCK 1: HEADER -->
<div style="display:flex;align-items:center;
justify-content:space-between;margin-bottom:8px;">
<div style="display:flex;align-items:center;">
<span style="font-family:'EXO 2';font-size:16px;font-weight:500;
letter-spacing:0.5px;text-transform:uppercase;color:${accent};">
Wasserzähler
</span>
</div>
</div>
<!-- BLOCK 2: SVG WASSERUHR -->
<div style="display:flex;justify-content:center;margin-bottom:10px;">
${waterSvg}
</div>
<!-- BLOCK 3: JAHRESVERBRAUCH -->
<div style="text-align:center;margin-bottom:7px;">
<div style="font-family:'EXO 2';font-size:12px;font-weight:500;
letter-spacing:0.5px;text-transform:uppercase;
color:${muted};margin-bottom:2px;">
Jahresverbrauch ${new Date().getFullYear()}
</div>
<div style="display:flex;align-items:baseline;justify-content:center;gap:5px;">
<span style="font-family:'EXO 2';font-size:36px;font-weight:500;
color:#E8F4FC;letter-spacing:-0.5px;line-height:1;${glowTxt}">
${yearStr}
</span>
<span style="font-family:'EXO 2';font-size:14px;font-weight:500;
color:${accent2};">mÂł</span>
</div>
</div>
<!-- TRENNLINIE -->
<div style="height:1px;margin-bottom:7px;
background:linear-gradient(90deg,transparent,${leak
? 'rgba(195,67,85,0.4)' : 'rgba(67,152,195,0.4)'},transparent);">
</div>
<!-- BLOCK 4: TAGESVERBRAUCH -->
<div style="text-align:center;margin-bottom:7px;">
<div style="font-family:'EXO 2';font-size:12px;font-weight:500;
letter-spacing:0.5px;text-transform:uppercase;
color:${muted};margin-bottom:2px;">
Tagesverbrauch
</div>
<div style="display:flex;align-items:baseline;justify-content:center;gap:5px;">
<span style="font-family:'EXO 2';font-size:26px;font-weight:500;
color:${accent};letter-spacing:-0.5px;line-height:1;
text-shadow:0 0 14px ${waveC1}77;">
${dailyStr}
</span>
</div>
</div>
<!-- TRENNLINIE -->
<div style="height:1px;margin-bottom:7px;
background:linear-gradient(90deg,transparent,${leak
? 'rgba(195,67,85,0.4)' : 'rgba(67,152,195,0.4)'},transparent);">
</div>
<!-- BLOCK 5: FOOTER -->
<div style="display:flex;align-items:center;justify-content:center;gap:16px;">
<div style="display:flex;align-items:center;gap:4px;">
<ha-icon icon="mdi:thermometer-water" style="
--mdc-icon-size:20px;color:${muted};"></ha-icon>
<span style="font-family:'EXO 2';font-size:14px;font-weight:500;
letter-spacing:0.5px;color:${accent2};">
${isNaN(temp) ? '—' : temp.toFixed(1)} °C
</span>
</div>
<div style="display:flex;align-items:center;gap:4px;">
<ha-icon icon="mdi:water-outline" style="
--mdc-icon-size:14px;color:${muted};"></ha-icon>
<span style="font-family:'EXO 2';font-size:14px;font-weight:500;
letter-spacing:0.5px;color:${accent2};">
${typeOut}
</span>
</div>
</div>
</div>`;
]]]
extra_styles: |
@keyframes leak-pulse {
0% { box-shadow: 0 8px 32px rgba(0,0,0,0.5), 0 0 20px rgba(195,67,85,0.2); }
100% { box-shadow: 0 8px 32px rgba(0,0,0,0.5), 0 0 50px rgba(195,67,85,0.7), 0 0 0 1px rgba(195,67,85,0.6); }
}
Bitte nicht wundern, wenn die Farben mal in HEX und in RGB angegeben sind. Das stammt alles noch aus den Bastelversionen (Optik).
Viel SpaĂź beim nachbauen!