mirror of
https://github.com/Anashost/HA-Animated-cards.git
synced 2026-05-07 11:09:13 +00:00
127 KiB
127 KiB
Home Assistant Animated Appliances Cards V2
Dishwasher
Washing Machine
Dryer
Combo Washer & Dryer
Fridge
This YouTube Video explains how to do it.
Loading image... please wait
Note
If you are using the Sections view type, you may need to set
rowsto around1.5for the card, otherwise the card may appear compressed. (USE THIS ONLY IF YOU HAVE ISSUES).grid_options: rows: 1.6
Cards (Smart)
1 - Smart Dishwasher
type: custom:button-card
entity: sensor.smart_dishwasher_status
name: Smart Dishwasher
show_state: false
show_label: true
variables:
sensor_status: sensor.smart_dishwasher_status
sensor_progress: sensor.smart_dishwasher_progress
sensor_time_remaining: sensor.smart_dishwasher_time_remaining
sensor_power: sensor.smart_dishwasher_power
sensor_percentage: sensor.smart_dishwasher_percentage
sensor_door: binary_sensor.smart_dishwasher_door
max_time: 150
state_idle: idle, off, standby, unknown, unavailable
state_running: wash, washing, rinse, rinsing, pre-wash
state_drying: dry, drying
state_done: finished, complete, end
size_icon: 45px
size_shape: 65px
size_card_height: 95px
font_primary: 15px
font_secondary: 12px
font_badge: 11px
styles:
card:
- "--config-icon-size": "[[[ return variables.size_icon ]]]"
- "--config-shape-size": "[[[ return variables.size_shape ]]]"
- "--config-card-height": "[[[ return variables.size_card_height ]]]"
- "--config-font-primary": "[[[ return variables.font_primary ]]]"
- "--config-font-secondary": "[[[ return variables.font_secondary ]]]"
- "--config-font-badge": "[[[ return variables.font_badge ]]]"
- height: var(--config-card-height) !important
- padding: 0px !important
- overflow: hidden
- position: relative
- transition: all 0.5s ease
grid:
- padding: 12px 16px
- height: 100%
- box-sizing: border-box
- grid-template-areas: "\"i n\" \"i l\""
- grid-template-columns: var(--config-shape-size) 1fr
- grid-template-rows: auto auto
- align-content: center
- gap: 0px 12px
- position: relative
icon:
- width: var(--config-icon-size)
- height: var(--config-icon-size)
- color: var(--primary-text-color)
- z-index: 1
- animation: var(--appliance-anim-shake) !important
- transform-origin: 50% 60%
img_cell:
- width: var(--config-shape-size)
- height: var(--config-shape-size)
- border-radius: 50%
- border: 1px solid var(--divider-color) !important
- background: rgba(128, 128, 128, 0.1) !important
- position: relative
- overflow: hidden !important
- justify-self: start
name:
- justify-self: start
- font-size: var(--config-font-primary)
- font-weight: 500
- align-self: end
- margin-bottom: 2px
- position: relative
label:
- justify-self: start
- font-size: var(--config-font-secondary)
- opacity: 0.7
- align-self: start
- margin-top: 2px
- position: relative
custom_fields:
badge1:
- position: absolute
- top: 10px
- right: 10px
- background: rgba(var(--appliance-color), 0.15)
- color: rgb(var(--appliance-color))
- border: 1px solid rgba(var(--appliance-color), 0.3)
- padding: 2px 10px
- border-radius: 12px
- font-size: var(--config-font-badge)
- font-weight: 600
- text-transform: uppercase
- letter-spacing: 0.5px
- white-space: nowrap
- z-index: 1
- transition: all 0.5s ease
badge2:
- position: absolute
- top: 38px
- right: 10px
- padding: 4px 8px
- font-size: 10px
- letter-spacing: 0.5px
- white-space: nowrap
- opacity: 0.9
- text-transform: uppercase
- font-weight: 500
- z-index: 5
- transition: all 0.5s ease
bar:
- position: absolute
- bottom: 0
- left: 0
- height: 3.5px
- width: var(--appliance-level)
- background: rgb(var(--appliance-color))
- box-shadow: 0 0 10px rgb(var(--appliance-color))
- transition: width 0.5s ease
tap_action:
action: more-info
label: |
[[[ return entity ? entity.state : 'Entity Setup Required'; ]]]
icon: mdi:dishwasher
custom_fields:
badge1: " "
badge2: " "
bar: " "
extra_styles: |
[[[
let ent_progress = variables.sensor_progress;
let ent_timerem = variables.sensor_time_remaining;
let ent_power = variables.sensor_power;
let ent_percent = variables.sensor_percentage;
let max_time = variables.max_time;
// Safely grab variables. If missing in YAML, falls back to empty strings to prevent crashes.
let state_idle = (variables.state_idle || '').split(',').map(s => s.trim().toLowerCase());
let state_running = (variables.state_running || '').split(',').map(s => s.trim().toLowerCase());
let state_drying = (variables.state_drying || '').split(',').map(s => s.trim().toLowerCase());
let state_done = (variables.state_done || '').split(',').map(s => s.trim().toLowerCase());
let status = states[ent_progress] ? String(states[ent_progress].state) : 'unknown';
let _s = status.trim().toLowerCase();
if (/^\d+$/.test(_s)) {
if (state_idle.includes(_s)) status = state_idle.find(s => !/^\d+$/.test(s)) || status;
else if (state_running.includes(_s)) status = state_running.find(s => !/^\d+$/.test(s)) || status;
else if (state_drying.includes(_s)) status = state_drying.find(s => !/^\d+$/.test(s)) || status;
else if (state_done.includes(_s)) status = state_done.find(s => !/^\d+$/.test(s)) || status;
}
let status_clean = status.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
let s_lower = status.toLowerCase();
let is_idle = state_idle.includes(s_lower);
let is_running = state_running.includes(s_lower);
let is_drying = state_drying.includes(s_lower);
let is_done = state_done.includes(s_lower);
let raw_val = states[ent_timerem] ? states[ent_timerem].state.trim() : '0';
let uom = states[ent_timerem] && states[ent_timerem].attributes ? states[ent_timerem].attributes.unit_of_measurement : '';
let time_rem = 0;
if (raw_val.includes('-') && raw_val.includes(':')) {
let end_ts = new Date(raw_val);
let now = new Date();
time_rem = end_ts > now ? Math.floor((end_ts - now) / 60000) : 0;
} else if (raw_val.includes(':')) {
let parts = raw_val.split(':');
if (parts.length === 3) time_rem = (parseInt(parts[0]) * 60) + parseInt(parts[1]);
else if (parts.length === 2) time_rem = (parseInt(parts[0]) * 60) + parseInt(parts[1]);
} else {
let parsed_val = parseFloat(raw_val) || 0;
let uom_lower = uom ? uom.toLowerCase() : '';
if (uom_lower === 'h' || uom_lower === 'hours') {
time_rem = Math.floor(parsed_val * 60);
} else if (uom_lower === 'm' || uom_lower === 'min' || uom_lower === 'minutes') {
time_rem = Math.floor(parsed_val);
} else {
if (parsed_val > 0 && parsed_val <= 10 && raw_val.includes('.')) {
time_rem = Math.floor(parsed_val * 60);
} else {
time_rem = Math.floor(parsed_val);
}
}
}
time_rem = Math.max(0, time_rem);
let raw_percent_str = states[ent_percent] ? states[ent_percent].state.trim() : '';
let raw_percent = (raw_percent_str !== '' && raw_percent_str !== 'unknown') ? parseFloat(raw_percent_str) : -1;
let raw_power = states[ent_power] ? parseFloat(states[ent_power].state) : NaN;
let power_text = !isNaN(raw_power) ? Math.round(raw_power) + 'W' : '';
let hours = Math.floor(time_rem / 60);
let mins = time_rem % 60;
let time_formatted = (time_rem > 0 && !is_idle) ? `${hours}h ${mins.toString().padStart(2, '0')}m` : '';
let progress = 0;
if (is_idle) {
progress = 0;
} else {
if (raw_percent >= 0) {
progress = parseInt(raw_percent);
} else {
let safe_max = Math.max(parseFloat(max_time), time_rem);
progress = Math.max(5, Math.floor(((safe_max - time_rem) / safe_max) * 100));
}
}
progress = Math.max(0, Math.min(100, progress));
let color = '158, 158, 158';
let anim_type = 'none';
let icon_shake = 'none';
let wave_anim = 'none';
let overlay_img = 'none';
if (is_drying) {
color = '255, 152, 0';
anim_type = 'steam-rise 2s ease-in-out infinite';
wave_anim = 'wave 4s linear infinite';
overlay_img = 'linear-gradient(0deg, transparent, rgba(255,255,255,0.5), transparent)';
} else if (is_done) {
color = '76, 175, 80';
anim_type = 'sparkle 2s infinite';
wave_anim = 'wave 4s linear infinite';
overlay_img = 'radial-gradient(circle, rgba(255,255,255,0.8) 10%, transparent 60%)';
} else if (is_running) {
color = '33, 150, 243';
anim_type = 'bubbles 1s linear infinite';
icon_shake = 'shake 0.8s ease-in-out infinite';
wave_anim = 'wave 4s linear infinite';
overlay_img = 'radial-gradient(2px 2px at 20% 80%, white, transparent), radial-gradient(2px 2px at 50% 70%, white, transparent)';
}
let ent_door = variables.sensor_door;
let door_state = (ent_door && states[ent_door]) ? states[ent_door].state.toLowerCase() : null;
let corner_color = color;
let corner_display = 'none'; // Default to hidden
if (ent_door && door_state && door_state !== 'unknown' && door_state !== 'unavailable') {
corner_display = 'block';
let is_open = (door_state === 'on' || door_state === 'open');
if (is_open) {
corner_color = '244, 67, 54'; // Red when Open
}
}
let badge1 = [status_clean, power_text].filter(Boolean).join(' • ');
let badge2 = time_formatted;
let b2_bg = `rgba(${color}, 0.15)`;
let b2_border = `1px solid rgba(128,128,128, 0.2)`;
let b2_br = `2px solid rgb(${color})`;
return `
#card {
--appliance-color: ${color};
--appliance-level: ${progress}%;
--appliance-anim-overlay: ${anim_type};
--appliance-anim-shake: ${icon_shake};
--appliance-anim-wave: ${wave_anim};
--appliance-overlay-bg: ${overlay_img};
--door-corner-color: rgb(${corner_color});
--door-corner-display: ${corner_display};
}
#card::after {
content: '';
display: var(--door-corner-display);
position: absolute;
top: -0.5px;
left: -0.5px;
opacity: 0.7;
width: 15px;
height: 15px;
border-top: 5px solid var(--door-corner-color);
border-left: 5px solid var(--door-corner-color);
border-top-left-radius: var(--ha-card-border-radius, 12px);
pointer-events: none;
transition: border-color 0.5s ease;
}
#badge1::before { content: "${badge1}"; }
#badge2 {
display: ${badge2 ? 'block' : 'none'};
background: ${b2_bg};
color: var(--primary-text-color, #fff);
border-top: ${b2_border};
border-bottom: ${b2_border};
border-left: ${b2_border};
border-left: ${b2_br};
border-radius: 6px !important;
}
#badge2::before { content: "${badge2}"; }
#img-cell::before {
content: ''; position: absolute; left: -50%; width: 200%; height: 200%;
top: calc(100% - var(--appliance-level)); background: rgba(var(--appliance-color), 0.6) !important;
border-radius: 40%; animation: var(--appliance-anim-wave) !important;
transition: top 0.5s ease; z-index: 1;
display: ${wave_anim === 'none' ? 'none' : 'block'};
}
#img-cell::after {
content: ''; position: absolute; inset: 0; background-image: var(--appliance-overlay-bg);
background-size: 100% 100%; animation: var(--appliance-anim-overlay) !important; z-index: 1;
display: ${anim_type === 'none' ? 'none' : 'block'};
}
@keyframes wave { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes shake { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(5deg) translateY(-1px); } 75% { transform: rotate(-5deg) translateY(1px); } }
@keyframes bubbles { 0% { transform: translateY(10px); opacity: 0; } 50% { opacity: 1; } 100% { transform: translateY(-20px); opacity: 0; } }
@keyframes steam-rise { 0% { opacity: 0; transform: translateY(5px); } 50% { opacity: 0.8; } 100% { opacity: 0; transform: translateY(-10px); } }
@keyframes sparkle { 0%, 100% { opacity: 0.3; transform: scale(0.9); } 50% { opacity: 1; transform: scale(1.1); } }
`;
]]]
2 - Smart Washing Machine
type: custom:button-card
entity: sensor.smart_washer_status
name: Smart Washer
show_state: false
show_label: true
variables:
sensor_status: sensor.smart_washer_status
sensor_progress: sensor.smart_washer_progress
sensor_time_remaining: sensor.smart_washer_time_remaining
sensor_power: sensor.smart_washer_power
sensor_percentage: sensor.smart_washer_percentage
sensor_door: binary_sensor.smart_washer_door
max_time: 150
state_idle: idle, off, standby, unknown, unavailable
state_running: wash, washing, rinse, rinsing, pre-wash, soak
state_spinning: spin, spinning
state_drying: dry, drying
state_done: finished, complete, end
size_icon: 45px
size_shape: 65px
size_card_height: 95px
font_primary: 15px
font_secondary: 12px
font_badge: 11px
styles:
icon:
- width: var(--config-icon-size)
- height: var(--config-icon-size)
- color: var(--primary-text-color)
- z-index: 1
- animation: var(--appliance-anim-shake) !important
- transform-origin: 50% 50%
img_cell:
- width: var(--config-shape-size)
- height: var(--config-shape-size)
- border-radius: 50%
- border: 1px solid var(--divider-color) !important
- background: rgba(128, 128, 128, 0.1) !important
- position: relative
- overflow: hidden !important
- justify-self: start
card:
- "--config-icon-size": "[[[ return variables.size_icon ]]]"
- "--config-shape-size": "[[[ return variables.size_shape ]]]"
- "--config-card-height": "[[[ return variables.size_card_height ]]]"
- "--config-font-primary": "[[[ return variables.font_primary ]]]"
- "--config-font-secondary": "[[[ return variables.font_secondary ]]]"
- "--config-font-badge": "[[[ return variables.font_badge ]]]"
- height: var(--config-card-height) !important
- padding: 0px !important
- overflow: hidden
- position: relative
- transition: all 0.5s ease
grid:
- padding: 12px 16px
- height: 100%
- box-sizing: border-box
- grid-template-areas: "\"i n\" \"i l\""
- grid-template-columns: var(--config-shape-size) 1fr
- grid-template-rows: auto auto
- align-content: center
- gap: 0px 12px
- position: relative
name:
- justify-self: start
- font-size: var(--config-font-primary)
- font-weight: 500
- align-self: end
- margin-bottom: 2px
- position: relative
label:
- justify-self: start
- font-size: var(--config-font-secondary)
- opacity: 0.7
- align-self: start
- margin-top: 2px
- position: relative
custom_fields:
badge1:
- position: absolute
- top: 10px
- right: 10px
- background: rgba(var(--appliance-color), 0.15)
- color: rgb(var(--appliance-color))
- border: 1px solid rgba(var(--appliance-color), 0.3)
- padding: 2px 10px
- border-radius: 12px
- font-size: var(--config-font-badge)
- font-weight: 600
- text-transform: uppercase
- letter-spacing: 0.5px
- white-space: nowrap
- z-index: 1
- transition: all 0.5s ease
badge2:
- position: absolute
- top: 38px
- right: 10px
- padding: 4px 8px
- font-size: 10px
- letter-spacing: 0.5px
- white-space: nowrap
- opacity: 0.9
- text-transform: uppercase
- font-weight: 500
- z-index: 1
- transition: all 0.5s ease
bar:
- position: absolute
- bottom: 0
- left: 0
- height: 3.5px
- width: var(--appliance-level)
- background: rgb(var(--appliance-color))
- box-shadow: 0 0 10px rgb(var(--appliance-color))
- transition: width 0.5s ease
tap_action:
action: more-info
label: |
[[[ return entity ? entity.state : 'Entity Setup Required'; ]]]
icon: mdi:washing-machine
custom_fields:
badge1: " "
badge2: " "
bar: " "
extra_styles: |
[[[
let ent_progress = variables.sensor_progress;
let ent_timerem = variables.sensor_time_remaining;
let ent_power = variables.sensor_power;
let ent_percent = variables.sensor_percentage;
let max_time = variables.max_time;
let state_idle = (variables.state_idle || '').split(',').map(s => s.trim().toLowerCase());
let state_running = (variables.state_running || '').split(',').map(s => s.trim().toLowerCase());
let state_spinning = (variables.state_spinning || '').split(',').map(s => s.trim().toLowerCase());
let state_drying = (variables.state_drying || '').split(',').map(s => s.trim().toLowerCase());
let state_done = (variables.state_done || '').split(',').map(s => s.trim().toLowerCase());
let status = states[ent_progress] ? String(states[ent_progress].state) : 'unknown';
let _s = status.trim().toLowerCase();
if (/^\d+$/.test(_s)) {
if (state_idle.includes(_s)) status = state_idle.find(s => !/^\d+$/.test(s)) || status;
else if (state_running.includes(_s)) status = state_running.find(s => !/^\d+$/.test(s)) || status;
else if (state_spinning.includes(_s)) status = state_spinning.find(s => !/^\d+$/.test(s)) || status;
else if (state_drying.includes(_s)) status = state_drying.find(s => !/^\d+$/.test(s)) || status;
else if (state_done.includes(_s)) status = state_done.find(s => !/^\d+$/.test(s)) || status;
}
let status_clean = status.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
let s_lower = status.toLowerCase();
let raw_val = states[ent_timerem] ? states[ent_timerem].state.trim() : '0';
let uom = states[ent_timerem] && states[ent_timerem].attributes ? states[ent_timerem].attributes.unit_of_measurement : '';
let time_rem = 0;
if (raw_val.includes('-') && raw_val.includes(':')) {
let end_ts = new Date(raw_val);
let now = new Date();
time_rem = end_ts > now ? Math.floor((end_ts - now) / 60000) : 0;
} else if (raw_val.includes(':')) {
let parts = raw_val.split(':');
if (parts.length === 3) time_rem = (parseInt(parts[0]) * 60) + parseInt(parts[1]);
else if (parts.length === 2) time_rem = (parseInt(parts[0]) * 60) + parseInt(parts[1]);
} else {
let parsed_val = parseFloat(raw_val) || 0;
let uom_lower = uom ? uom.toLowerCase() : '';
if (uom_lower === 'h' || uom_lower === 'hours') {
time_rem = Math.floor(parsed_val * 60);
} else if (uom_lower === 'm' || uom_lower === 'min' || uom_lower === 'minutes') {
time_rem = Math.floor(parsed_val);
} else {
if (parsed_val > 0 && parsed_val <= 10 && raw_val.includes('.')) {
time_rem = Math.floor(parsed_val * 60);
} else {
time_rem = Math.floor(parsed_val);
}
}
}
time_rem = Math.max(0, time_rem);
let raw_percent_str = states[ent_percent] ? states[ent_percent].state.trim() : '';
let raw_percent = (raw_percent_str !== '' && raw_percent_str !== 'unknown') ? parseFloat(raw_percent_str) : -1;
let raw_power = states[ent_power] ? parseFloat(states[ent_power].state) : NaN;
let power_text = !isNaN(raw_power) ? Math.round(raw_power) + 'W' : '';
let is_idle = state_idle.includes(s_lower);
let hours = Math.floor(time_rem / 60);
let mins = time_rem % 60;
let time_formatted = (time_rem > 0 && !is_idle) ? `${hours}h ${mins.toString().padStart(2, '0')}m` : '';
let progress = 0;
if (is_idle) {
progress = 0;
} else {
if (raw_percent >= 0) {
progress = parseInt(raw_percent);
} else {
let safe_max = Math.max(parseFloat(max_time), time_rem);
progress = Math.max(5, Math.floor(((safe_max - time_rem) / safe_max) * 100));
}
}
progress = Math.max(0, Math.min(100, progress));
let is_running = state_running.includes(s_lower);
let is_spinning = state_spinning.includes(s_lower);
let is_drying = state_drying.includes(s_lower);
let is_done = state_done.includes(s_lower);
let color = '158, 158, 158';
let anim_type = 'none';
let icon_shake = 'none';
let wave_anim = 'none';
let overlay_img = 'none';
if (is_drying) {
color = '255, 152, 0';
anim_type = 'steam-rise 2s ease-in-out infinite';
wave_anim = 'wave 4s linear infinite';
overlay_img = 'linear-gradient(0deg, transparent, rgba(255,255,255,0.5), transparent)';
} else if (is_spinning) {
color = '0, 170, 170';
anim_type = 'none';
icon_shake = 'washer-spin-smooth 0.8s linear infinite';
wave_anim = 'wave 2s linear infinite';
overlay_img = 'radial-gradient(circle, rgba(255,255,255,0.3) 10%, transparent 60%)';
} else if (is_done) {
color = '76, 175, 80';
anim_type = 'sparkle 2s infinite';
wave_anim = 'wave 4s linear infinite';
overlay_img = 'radial-gradient(circle, rgba(255,255,255,0.8) 10%, transparent 60%)';
} else if (is_running) {
color = '33, 150, 243';
anim_type = 'bubbles 1s linear infinite';
icon_shake = 'shake 1.5s ease-in-out infinite';
wave_anim = 'wave 4s linear infinite';
overlay_img = 'radial-gradient(2px 2px at 20% 80%, white, transparent), radial-gradient(2px 2px at 50% 70%, white, transparent)';
}
// --- SMART ADAPTIVE CORNER LOGIC ---
let ent_door = variables.sensor_door;
let door_state = (ent_door && states[ent_door]) ? states[ent_door].state.toLowerCase() : null;
let corner_color = color;
let corner_display = 'none'; // Default to hidden
if (ent_door && door_state && door_state !== 'unknown' && door_state !== 'unavailable') {
corner_display = 'block';
let is_open = (door_state === 'on' || door_state === 'open');
if (is_open) {
corner_color = '244, 67, 54'; // Red when Open
}
}
// -----------------------------------
let badge1 = [status_clean, power_text].filter(Boolean).join(' • ');
let badge2 = time_formatted;
let b2_bg = `rgba(${color}, 0.15)`;
let b2_border = `1px solid rgba(128,128,128, 0.2)`;
let b2_br = `2px solid rgb(${color})`;
return `
#card {
--appliance-color: ${color};
--appliance-level: ${progress}%;
--appliance-anim-overlay: ${anim_type};
--appliance-anim-shake: ${icon_shake};
--appliance-anim-wave: ${wave_anim};
--appliance-overlay-bg: ${overlay_img};
--door-corner-color: rgb(${corner_color});
--door-corner-display: ${corner_display};
}
/* Rounded Adaptive Corner Accent */
#card::after {
content: '';
display: var(--door-corner-display);
position: absolute;
top: -0.5px;
left: -0.5px;
opacity: 0.7;
width: 15px;
height: 15px;
border-top: 5px solid var(--door-corner-color);
border-left: 5px solid var(--door-corner-color);
border-top-left-radius: var(--ha-card-border-radius, 12px);
pointer-events: none;
transition: border-color 0.5s ease;
}
#badge1::before { content: "${badge1}"; }
#badge2 {
display: ${badge2 ? 'block' : 'none'};
background: ${b2_bg};
color: var(--primary-text-color, #fff);
border-top: ${b2_border};
border-bottom: ${b2_border};
border-left: ${b2_border};
border-left: ${b2_br};
border-radius: 6px !important;
}
#badge2::before { content: "${badge2}"; }
#img-cell::before {
content: ''; position: absolute; left: -50%; width: 200%; height: 200%;
top: calc(100% - var(--appliance-level)); background: rgba(var(--appliance-color), 0.6) !important;
border-radius: 40%; animation: var(--appliance-anim-wave) !important;
transition: top 0.5s ease; z-index: 1;
display: ${wave_anim === 'none' ? 'none' : 'block'};
}
#img-cell::after {
content: ''; position: absolute; inset: 0; background-image: var(--appliance-overlay-bg);
background-size: 100% 100%; animation: var(--appliance-anim-overlay) !important; z-index: 1;
display: ${anim_type === 'none' ? 'none' : 'block'};
}
@keyframes wave { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes shake { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(5deg) translateY(-1px); } 75% { transform: rotate(-5deg) translateY(1px); } }
@keyframes bubbles { 0% { transform: translateY(10px); opacity: 0; } 50% { opacity: 1; } 100% { transform: translateY(-20px); opacity: 0; } }
@keyframes steam-rise { 0% { opacity: 0; transform: translateY(5px); } 50% { opacity: 0.8; } 100% { opacity: 0; transform: translateY(-10px); } }
@keyframes sparkle { 0%, 100% { opacity: 0.3; transform: scale(0.9); } 50% { opacity: 1; transform: scale(1.1); } }
@keyframes washer-spin-smooth {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
]]]
3 - Smart Dryer
type: custom:button-card
entity: sensor.smart_dryer_status
name: Smart Dryer
show_state: false
show_label: true
variables:
sensor_status: sensor.smart_dryer_status
sensor_progress: sensor.smart_dryer_progress
sensor_time_remaining: sensor.smart_dryer_time_remaining
sensor_power: sensor.smart_dryer_power
sensor_percentage: sensor.smart_dryer_percentage
sensor_door: binary_sensor.smart_dryer_door
max_time: 150
state_idle: idle, off, standby, unknown, unavailable
state_drying: drying, tumble, dry, heat, heating, tumbling
state_cooling: cooling, cool down, anti-crease, air fluff
state_done: finished, complete, end
size_icon: 45px
size_shape: 65px
size_card_height: 95px
font_primary: 15px
font_secondary: 12px
font_badge: 11px
styles:
icon:
- width: var(--config-icon-size)
- height: var(--config-icon-size)
- color: var(--primary-text-color)
- z-index: 1
- animation: var(--appliance-anim-shake) !important
- transform-origin: 50% 60%
img_cell:
- width: var(--config-shape-size)
- height: var(--config-shape-size)
- border-radius: 50%
- border: 1px solid var(--divider-color) !important
- background: rgba(128, 128, 128, 0.1) !important
- position: relative
- overflow: hidden !important
- justify-self: start
card:
- "--config-icon-size": "[[[ return variables.size_icon ]]]"
- "--config-shape-size": "[[[ return variables.size_shape ]]]"
- "--config-card-height": "[[[ return variables.size_card_height ]]]"
- "--config-font-primary": "[[[ return variables.font_primary ]]]"
- "--config-font-secondary": "[[[ return variables.font_secondary ]]]"
- "--config-font-badge": "[[[ return variables.font_badge ]]]"
- height: var(--config-card-height) !important
- padding: 0px !important
- overflow: hidden
- position: relative
- transition: all 0.5s ease
grid:
- padding: 12px 16px
- height: 100%
- box-sizing: border-box
- grid-template-areas: "\"i n\" \"i l\""
- grid-template-columns: var(--config-shape-size) 1fr
- grid-template-rows: auto auto
- align-content: center
- gap: 0px 12px
- position: relative
name:
- justify-self: start
- font-size: var(--config-font-primary)
- font-weight: 500
- align-self: end
- margin-bottom: 2px
- position: relative
label:
- justify-self: start
- font-size: var(--config-font-secondary)
- opacity: 0.7
- align-self: start
- margin-top: 2px
- position: relative
custom_fields:
badge1:
- position: absolute
- top: 10px
- right: 10px
- background: rgba(var(--appliance-color), 0.15)
- color: rgb(var(--appliance-color))
- border: 1px solid rgba(var(--appliance-color), 0.3)
- padding: 2px 10px
- border-radius: 12px
- font-size: var(--config-font-badge)
- font-weight: 600
- text-transform: uppercase
- letter-spacing: 0.5px
- white-space: nowrap
- z-index: 1
- transition: all 0.5s ease
badge2:
- position: absolute
- top: 38px
- right: 10px
- padding: 4px 8px
- font-size: 10px
- letter-spacing: 0.5px
- white-space: nowrap
- opacity: 0.9
- text-transform: uppercase
- font-weight: 500
- z-index: 1
- transition: all 0.5s ease
bar:
- position: absolute
- bottom: 0
- left: 0
- height: 3.5px
- width: var(--appliance-level)
- background: rgb(var(--appliance-color))
- box-shadow: 0 0 10px rgb(var(--appliance-color))
- transition: width 0.5s ease
tap_action:
action: more-info
label: |
[[[ return entity ? entity.state : 'Entity Setup Required'; ]]]
icon: mdi:tumble-dryer
custom_fields:
badge1: " "
badge2: " "
bar: " "
extra_styles: |
[[[
let ent_progress = variables.sensor_progress;
let ent_timerem = variables.sensor_time_remaining;
let ent_power = variables.sensor_power;
let ent_percent = variables.sensor_percentage;
let max_time = variables.max_time;
let state_idle = (variables.state_idle || '').split(',').map(s => s.trim().toLowerCase());
let state_drying = (variables.state_drying || '').split(',').map(s => s.trim().toLowerCase());
let state_cooling = (variables.state_cooling || '').split(',').map(s => s.trim().toLowerCase());
let state_done = (variables.state_done || '').split(',').map(s => s.trim().toLowerCase());
let status = states[ent_progress] ? String(states[ent_progress].state) : 'unknown';
let _s = status.trim().toLowerCase();
if (/^\d+$/.test(_s)) {
if (state_idle.includes(_s)) status = state_idle.find(s => !/^\d+$/.test(s)) || status;
else if (state_drying.includes(_s)) status = state_drying.find(s => !/^\d+$/.test(s)) || status;
else if (state_cooling.includes(_s)) status = state_cooling.find(s => !/^\d+$/.test(s)) || status;
else if (state_done.includes(_s)) status = state_done.find(s => !/^\d+$/.test(s)) || status;
}
let status_clean = status.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
let s_lower = status.toLowerCase();
let raw_val = states[ent_timerem] ? states[ent_timerem].state.trim() : '0';
let uom = states[ent_timerem] && states[ent_timerem].attributes ? states[ent_timerem].attributes.unit_of_measurement : '';
let time_rem = 0;
if (raw_val.includes('-') && raw_val.includes(':')) {
let end_ts = new Date(raw_val);
let now = new Date();
time_rem = end_ts > now ? Math.floor((end_ts - now) / 60000) : 0;
} else if (raw_val.includes(':')) {
let parts = raw_val.split(':');
if (parts.length === 3) time_rem = (parseInt(parts[0]) * 60) + parseInt(parts[1]);
else if (parts.length === 2) time_rem = (parseInt(parts[0]) * 60) + parseInt(parts[1]);
} else {
let parsed_val = parseFloat(raw_val) || 0;
let uom_lower = uom ? uom.toLowerCase() : '';
if (uom_lower === 'h' || uom_lower === 'hours') time_rem = Math.floor(parsed_val * 60);
else if (uom_lower === 'm' || uom_lower === 'min' || uom_lower === 'minutes') time_rem = Math.floor(parsed_val);
else time_rem = (parsed_val > 0 && parsed_val <= 10 && raw_val.includes('.')) ? Math.floor(parsed_val * 60) : Math.floor(parsed_val);
}
time_rem = Math.max(0, time_rem);
let raw_percent_str = states[ent_percent] ? states[ent_percent].state.trim() : '';
let raw_percent = (raw_percent_str !== '' && raw_percent_str !== 'unknown') ? parseFloat(raw_percent_str) : -1;
let raw_power = states[ent_power] ? parseFloat(states[ent_power].state) : NaN;
let power_text = !isNaN(raw_power) ? Math.round(raw_power) + 'W' : '';
let is_idle = state_idle.includes(s_lower);
let hours = Math.floor(time_rem / 60);
let mins = time_rem % 60;
let time_formatted = (time_rem > 0 && !is_idle) ? `${hours}h ${mins.toString().padStart(2, '0')}m` : '';
let progress = 0;
if (is_idle) {
progress = 0;
} else {
if (raw_percent >= 0) {
progress = parseInt(raw_percent);
} else {
let safe_max = Math.max(parseFloat(max_time), time_rem);
progress = Math.max(5, Math.floor(((safe_max - time_rem) / safe_max) * 100));
}
}
progress = Math.max(0, Math.min(100, progress));
let is_drying = state_drying.includes(s_lower);
let is_cooling = state_cooling.includes(s_lower);
let is_done = state_done.includes(s_lower);
let color = '158, 158, 158';
let anim_type = 'none';
let icon_shake = 'none';
let wave_anim = 'none';
let overlay_img = 'none';
if (is_drying) {
color = '255, 152, 0';
anim_type = 'steam-rise 2s ease-in-out infinite';
wave_anim = 'wave 4s linear infinite';
overlay_img = 'linear-gradient(0deg, transparent, rgba(255,255,255,0.4), transparent)';
} else if (is_cooling) {
color = '33, 150, 243';
anim_type = 'breeze 3s ease-in-out infinite';
icon_shake = 'wobble 2s ease-in-out infinite';
wave_anim = 'wave 6s linear infinite';
overlay_img = 'radial-gradient(circle, rgba(255,255,255,0.5) 0%, transparent 70%)';
} else if (is_done) {
color = '76, 175, 80';
anim_type = 'sparkle 2s infinite';
wave_anim = 'wave 4s linear infinite';
overlay_img = 'radial-gradient(circle, rgba(255,255,255,0.8) 10%, transparent 60%)';
}
let ent_door = variables.sensor_door;
let door_state = (ent_door && states[ent_door]) ? states[ent_door].state.toLowerCase() : null;
let corner_color = color;
let corner_display = 'none';
if (ent_door && door_state && door_state !== 'unknown' && door_state !== 'unavailable') {
corner_display = 'block';
let is_open = (door_state === 'on' || door_state === 'open');
if (is_open) {
corner_color = '244, 67, 54';
}
}
let badge1 = [status_clean, power_text].filter(Boolean).join(' • ');
let badge2 = time_formatted;
return `
#card {
--appliance-color: ${color};
--appliance-level: ${progress}%;
--appliance-anim-overlay: ${anim_type};
--appliance-anim-shake: ${icon_shake};
--appliance-anim-wave: ${wave_anim};
--appliance-overlay-bg: ${overlay_img};
--door-corner-color: rgb(${corner_color});
--door-corner-display: ${corner_display};
}
#card::after {
content: '';
display: var(--door-corner-display);
position: absolute;
top: -0.5px;
left: -0.5px;
opacity: 0.7;
width: 15px;
height: 15px;
border-top: 5px solid var(--door-corner-color);
border-left: 5px solid var(--door-corner-color);
border-top-left-radius: var(--ha-card-border-radius, 12px);
pointer-events: none;
transition: border-color 0.5s ease;
}
#badge1::before { content: "${badge1}"; }
#badge2 {
display: ${badge2 ? 'block' : 'none'};
background: rgba(${color}, 0.15);
color: var(--primary-text-color, #fff);
border-top: 1px solid rgba(128,128,128, 0.2);
border-bottom: 1px solid rgba(128,128,128, 0.2);
border-left: 2px solid rgb(${color});
border-radius: 6px !important;
}
#badge2::before { content: "${badge2}"; }
#img-cell::before {
content: ''; position: absolute; left: -50%; width: 200%; height: 200%;
top: calc(100% - var(--appliance-level)); background: rgba(var(--appliance-color), 0.6) !important;
border-radius: 40%; animation: var(--appliance-anim-wave) !important;
transition: top 0.5s ease; z-index: 1;
display: ${wave_anim === 'none' ? 'none' : 'block'};
}
#img-cell::after {
content: ''; position: absolute; inset: 0; background-image: var(--appliance-overlay-bg);
background-size: 100% 100%; animation: var(--appliance-anim-overlay) !important; z-index: 1;
display: ${anim_type === 'none' ? 'none' : 'block'};
}
@keyframes wave { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes shake { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(10deg) translateY(-2px); } 50% { transform: rotate(0deg); } 75% { transform: rotate(-10deg) translateY(2px); } }
@keyframes wobble { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(5deg); } }
@keyframes steam-rise { 0% { opacity: 0; transform: translateY(10px) scale(0.9); } 50% { opacity: 0.6; } 100% { opacity: 0; transform: translateY(-20px) scale(1.1); } }
@keyframes breeze { 0% { opacity: 0.2; transform: scale(0.95); } 50% { opacity: 0.5; transform: scale(1.05); } 100% { opacity: 0.2; transform: scale(0.95); } }
@keyframes sparkle { 0%, 100% { opacity: 0.3; transform: scale(0.9); } 50% { opacity: 1; transform: scale(1.1); } }
`;
]]]
4 - Smart Combo Washing machine & Dryer
type: custom:button-card
entity: sensor.smart_combo_status
name: Smart Combo
show_state: false
show_label: true
variables:
sensor_status: sensor.smart_combo_status
sensor_progress: sensor.smart_combo_progress
sensor_time_remaining: sensor.smart_combo_time_remaining
sensor_power: sensor.smart_combo_power
sensor_percentage: sensor.smart_combo_percentage
sensor_door: binary_sensor.smart_combo_door
max_time: 150
state_idle: idle, off, standby, unknown, unavailable
state_washing: wash, washing, rinse, rinsing, pre-wash, soak
state_spinning: spin, spinning, drain
state_drying: dry, drying, tumble, tumbling
state_cooling: cooling, cool down, anti-crease
state_done: finished, complete, end
size_icon: 45px
size_shape: 65px
size_card_height: 95px
font_primary: 15px
font_secondary: 12px
font_badge: 11px
styles:
icon:
- width: var(--config-icon-size)
- height: var(--config-icon-size)
- color: var(--primary-text-color)
- z-index: 1
- animation: var(--appliance-anim-shake) !important
- transform-origin: center center
img_cell:
- width: var(--config-shape-size)
- height: var(--config-shape-size)
- border-radius: 50%
- border: 1px solid var(--divider-color) !important
- background: rgba(128, 128, 128, 0.1) !important
- position: relative
- overflow: hidden !important
- justify-self: start
card:
- "--config-icon-size": "[[[ return variables.size_icon ]]]"
- "--config-shape-size": "[[[ return variables.size_shape ]]]"
- "--config-card-height": "[[[ return variables.size_card_height ]]]"
- "--config-font-primary": "[[[ return variables.font_primary ]]]"
- "--config-font-secondary": "[[[ return variables.font_secondary ]]]"
- "--config-font-badge": "[[[ return variables.font_badge ]]]"
- height: var(--config-card-height) !important
- padding: 0px !important
- overflow: hidden
- position: relative
- transition: all 0.5s ease
grid:
- padding: 12px 16px
- height: 100%
- box-sizing: border-box
- grid-template-areas: "\"i n\" \"i l\""
- grid-template-columns: var(--config-shape-size) 1fr
- grid-template-rows: auto auto
- align-content: center
- gap: 0px 12px
- position: relative
name:
- justify-self: start
- font-size: var(--config-font-primary)
- font-weight: 500
- align-self: end
- margin-bottom: 2px
- position: relative
label:
- justify-self: start
- font-size: var(--config-font-secondary)
- opacity: 0.7
- align-self: start
- margin-top: 2px
- position: relative
custom_fields:
badge1:
- position: absolute
- top: 10px
- right: 10px
- background: rgba(var(--appliance-color), 0.15)
- color: rgb(var(--appliance-color))
- border: 1px solid rgba(var(--appliance-color), 0.3)
- padding: 2px 10px
- border-radius: 12px
- font-size: var(--config-font-badge)
- font-weight: 600
- text-transform: uppercase
- letter-spacing: 0.5px
- white-space: nowrap
- z-index: 1
- transition: all 0.5s ease
badge2:
- position: absolute
- top: 38px
- right: 10px
- padding: 4px 8px
- font-size: 10px
- letter-spacing: 0.5px
- white-space: nowrap
- opacity: 0.9
- text-transform: uppercase
- font-weight: 500
- z-index: 1
- transition: all 0.5s ease
bar:
- position: absolute
- bottom: 0
- left: 0
- height: 3.5px
- width: var(--appliance-level)
- background: rgb(var(--appliance-color))
- box-shadow: 0 0 10px rgb(var(--appliance-color))
- transition: width 0.5s ease
tap_action:
action: more-info
label: |
[[[ return entity ? entity.state : 'Entity Setup Required'; ]]]
icon: mdi:washing-machine
custom_fields:
badge1: " "
badge2: " "
bar: " "
extra_styles: |
[[[
let ent_progress = variables.sensor_progress;
let ent_timerem = variables.sensor_time_remaining;
let ent_power = variables.sensor_power;
let ent_percent = variables.sensor_percentage;
let max_time = variables.max_time;
let state_idle = (variables.state_idle || '').split(',').map(s => s.trim().toLowerCase());
let state_washing = (variables.state_washing || '').split(',').map(s => s.trim().toLowerCase());
let state_spinning = (variables.state_spinning || '').split(',').map(s => s.trim().toLowerCase());
let state_drying = (variables.state_drying || '').split(',').map(s => s.trim().toLowerCase());
let state_cooling = (variables.state_cooling || '').split(',').map(s => s.trim().toLowerCase());
let state_done = (variables.state_done || '').split(',').map(s => s.trim().toLowerCase());
let status = states[ent_progress] ? String(states[ent_progress].state) : 'unknown';
let _s = status.trim().toLowerCase();
if (/^\d+$/.test(_s)) {
if (state_idle.includes(_s)) status = state_idle.find(s => !/^\d+$/.test(s)) || status;
else if (state_washing.includes(_s)) status = state_washing.find(s => !/^\d+$/.test(s)) || status;
else if (state_spinning.includes(_s)) status = state_spinning.find(s => !/^\d+$/.test(s)) || status;
else if (state_drying.includes(_s)) status = state_drying.find(s => !/^\d+$/.test(s)) || status;
else if (state_cooling.includes(_s)) status = state_cooling.find(s => !/^\d+$/.test(s)) || status;
else if (state_done.includes(_s)) status = state_done.find(s => !/^\d+$/.test(s)) || status;
}
let status_clean = status.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
let s_lower = status.toLowerCase();
let raw_val = states[ent_timerem] ? states[ent_timerem].state.trim() : '0';
let uom = states[ent_timerem] && states[ent_timerem].attributes ? states[ent_timerem].attributes.unit_of_measurement : '';
let time_rem = 0;
if (raw_val.includes('-') && raw_val.includes(':')) {
let end_ts = new Date(raw_val);
let now = new Date();
time_rem = end_ts > now ? Math.floor((end_ts - now) / 60000) : 0;
} else if (raw_val.includes(':')) {
let parts = raw_val.split(':');
if (parts.length === 3) time_rem = (parseInt(parts[0]) * 60) + parseInt(parts[1]);
else if (parts.length === 2) time_rem = (parseInt(parts[0]) * 60) + parseInt(parts[1]);
} else {
let parsed_val = parseFloat(raw_val) || 0;
let uom_lower = uom ? uom.toLowerCase() : '';
if (uom_lower === 'h' || uom_lower === 'hours') time_rem = Math.floor(parsed_val * 60);
else if (uom_lower === 'm' || uom_lower === 'min' || uom_lower === 'minutes') time_rem = Math.floor(parsed_val);
else time_rem = (parsed_val > 0 && parsed_val <= 10 && raw_val.includes('.')) ? Math.floor(parsed_val * 60) : Math.floor(parsed_val);
}
time_rem = Math.max(0, time_rem);
let raw_percent_str = states[ent_percent] ? states[ent_percent].state.trim() : '';
let raw_percent = (raw_percent_str !== '' && raw_percent_str !== 'unknown') ? parseFloat(raw_percent_str) : -1;
let raw_power = states[ent_power] ? parseFloat(states[ent_power].state) : NaN;
let power_text = !isNaN(raw_power) ? Math.round(raw_power) + 'W' : '';
let is_idle = state_idle.includes(s_lower);
let hours = Math.floor(time_rem / 60);
let mins = time_rem % 60;
let time_formatted = (time_rem > 0 && !is_idle) ? `${hours}h ${mins.toString().padStart(2, '0')}m` : '';
let progress = 0;
if (is_idle) {
progress = 0;
} else {
if (raw_percent >= 0) {
progress = parseInt(raw_percent);
} else {
let safe_max = Math.max(parseFloat(max_time), time_rem);
progress = Math.max(5, Math.floor(((safe_max - time_rem) / safe_max) * 100));
}
}
progress = Math.max(0, Math.min(100, progress));
let is_washing = state_washing.includes(s_lower);
let is_spinning = state_spinning.includes(s_lower);
let is_drying = state_drying.includes(s_lower);
let is_cooling = state_cooling.includes(s_lower);
let is_done = state_done.includes(s_lower);
let color = '158, 158, 158';
let anim_type = 'none';
let icon_shake = 'none';
let wave_anim = 'none';
let overlay_img = 'none';
if (is_drying) {
color = '255, 152, 0';
anim_type = 'steam-rise 2s ease-in-out infinite';
wave_anim = 'wave 4s linear infinite';
overlay_img = 'linear-gradient(0deg, transparent, rgba(255,255,255,0.4), transparent)';
} else if (is_cooling) {
color = '33, 150, 243';
anim_type = 'breeze 3s ease-in-out infinite';
icon_shake = 'wobble 2s ease-in-out infinite';
wave_anim = 'wave 6s linear infinite';
overlay_img = 'radial-gradient(circle, rgba(255,255,255,0.5) 0%, transparent 70%)';
} else if (is_spinning) {
color = '0, 170, 170';
icon_shake = 'washer-spin-smooth 0.8s linear infinite';
wave_anim = 'wave 2s linear infinite';
overlay_img = 'radial-gradient(circle, rgba(255,255,255,0.3) 10%, transparent 60%)';
} else if (is_washing) {
color = '33, 150, 243';
anim_type = 'bubbles 1s linear infinite';
icon_shake = 'shake 1.5s ease-in-out infinite';
wave_anim = 'wave 4s linear infinite';
overlay_img = 'radial-gradient(2px 2px at 20% 80%, white, transparent), radial-gradient(2px 2px at 50% 70%, white, transparent)';
} else if (is_done) {
color = '76, 175, 80';
anim_type = 'sparkle 2s infinite';
wave_anim = 'wave 4s linear infinite';
overlay_img = 'radial-gradient(circle, rgba(255,255,255,0.8) 10%, transparent 60%)';
}
let ent_door = variables.sensor_door;
let door_state = (ent_door && states[ent_door]) ? states[ent_door].state.toLowerCase() : null;
let corner_color = color;
let corner_display = 'none';
if (ent_door && door_state && door_state !== 'unknown' && door_state !== 'unavailable') {
corner_display = 'block';
let is_open = (door_state === 'on' || door_state === 'open');
if (is_open) {
corner_color = '244, 67, 54';
}
}
let badge1 = [status_clean, power_text].filter(Boolean).join(' • ');
let badge2 = time_formatted;
return `
#card {
--appliance-color: ${color};
--appliance-level: ${progress}%;
--appliance-anim-overlay: ${anim_type};
--appliance-anim-shake: ${icon_shake};
--appliance-anim-wave: ${wave_anim};
--appliance-overlay-bg: ${overlay_img};
--door-corner-color: rgb(${corner_color});
--door-corner-display: ${corner_display};
}
#card::after {
content: '';
display: var(--door-corner-display);
position: absolute;
top: -0.5px;
left: -0.5px;
opacity: 0.7;
width: 15px;
height: 15px;
border-top: 5px solid var(--door-corner-color);
border-left: 5px solid var(--door-corner-color);
border-top-left-radius: var(--ha-card-border-radius, 12px);
pointer-events: none;
transition: border-color 0.5s ease;
}
#badge1::before { content: "${badge1}"; }
#badge2 {
display: ${badge2 ? 'block' : 'none'};
background: rgba(${color}, 0.15);
color: var(--primary-text-color, #fff);
border-top: 1px solid rgba(128,128,128, 0.2);
border-bottom: 1px solid rgba(128,128,128, 0.2);
border-left: 2px solid rgb(${color});
border-radius: 6px !important;
}
#badge2::before { content: "${badge2}"; }
#img-cell::before {
content: ''; position: absolute; left: -50%; width: 200%; height: 200%;
top: calc(100% - var(--appliance-level)); background: rgba(var(--appliance-color), 0.6) !important;
border-radius: 40%; animation: var(--appliance-anim-wave) !important;
transition: top 0.5s ease; z-index: 1;
display: ${wave_anim === 'none' ? 'none' : 'block'};
}
#img-cell::after {
content: ''; position: absolute; inset: 0; background-image: var(--appliance-overlay-bg);
background-size: 100% 100%; animation: var(--appliance-anim-overlay) !important; z-index: 1;
display: ${anim_type === 'none' ? 'none' : 'block'};
}
@keyframes wave { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes shake { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(5deg) translateY(-1px); } 75% { transform: rotate(-5deg) translateY(1px); } }
@keyframes wobble { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(5deg); } }
@keyframes washer-spin-smooth { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@keyframes bubbles { 0% { transform: translateY(10px); opacity: 0; } 50% { opacity: 1; } 100% { transform: translateY(-20px); opacity: 0; } }
@keyframes steam-rise { 0% { opacity: 0; transform: translateY(10px) scale(0.9); } 50% { opacity: 0.6; } 100% { opacity: 0; transform: translateY(-20px) scale(1.1); } }
@keyframes breeze { 0% { opacity: 0.2; transform: scale(0.95); } 50% { opacity: 0.5; transform: scale(1.05); } 100% { opacity: 0.2; transform: scale(0.95); } }
@keyframes sparkle { 0%, 100% { opacity: 0.3; transform: scale(0.9); } 50% { opacity: 1; transform: scale(1.1); } }
`;
]]]
5 - Smart Fridge
type: custom:button-card
entity: sensor.smart_fridge_status
name: Smart Refrigerator
show_state: false
show_label: true
variables:
sensor_status: sensor.smart_fridge_status
sensor_door: binary_sensor.smart_fridge_door
sensor_temp_fridge: sensor.smart_fridge_temp_fridge
sensor_temp_freezer: sensor.smart_fridge_temp_freezer
sensor_power: sensor.smart_fridge_power
max_power_w: 200
max_temp_fridge: 6
max_temp_freezer: -10
state_cooling: cool, cooling, running, active
state_super: super_cool, rapid, boost
state_defrost: defrost, defrosting
tap_action:
action: more-info
label: >
[[[ return entity.state.replace(/[-_]/g, ' ').replace(/\b\w/g, c =>
c.toUpperCase()); ]]]
icon: |
[[[
let door = states[variables.sensor_door];
return (door && (door.state === 'on' || door.state === 'open')) ? 'mdi:fridge-alert' : 'mdi:fridge';
]]]
custom_fields:
bg1: " "
bg2: " "
badge1: " "
badge2: " "
bar: " "
styles:
card:
- height: 95px !important
- padding: 0px !important
- overflow: hidden
- position: relative
- transition: box-shadow 1s ease, background 1s ease
grid:
- padding: 12px 16px
- height: 100%
- box-sizing: border-box
- grid-template-areas: "\"i n\" \"i l\""
- grid-template-columns: 65px 1fr
- grid-template-rows: auto auto
- align-content: center
- gap: 0px 12px
- position: relative
- z-index: 2
icon:
- width: 45px
- height: 45px
- color: rgb(var(--appliance-color))
- transform-origin: 50% 60%
- animation: var(--appliance-icon-anim) !important
- z-index: 3
img_cell:
- width: 65px
- height: 65px
- border-radius: 50% !important
- background: rgba(var(--appliance-color), 0.1) !important
- border: 1px solid rgba(var(--appliance-color), 0.3)
- position: relative
- overflow: visible !important
- justify-self: start
- z-index: 1
name:
- justify-self: start
- font-size: 15px
- font-weight: 500
- align-self: end
- margin-bottom: 2px
- position: relative
- z-index: 2
label:
- justify-self: start
- font-size: 12px
- opacity: 0.7
- align-self: start
- margin-top: 2px
- position: relative
- z-index: 2
custom_fields:
bg1:
- position: absolute
- inset: 0
- pointer-events: none
- z-index: 0
- display: var(--appliance-bg-d1)
- background-image: var(--appliance-bg-img1)
- background-size: var(--appliance-bg-sz1)
- background-repeat: var(--appliance-bg-rep1)
- animation: var(--appliance-bg-anim1)
- opacity: var(--appliance-bg-op1)
bg2:
- position: absolute
- inset: 0
- pointer-events: none
- z-index: 0
- display: var(--appliance-bg-d2)
- background-image: var(--appliance-bg-img2)
- background-size: var(--appliance-bg-sz2)
- background-repeat: repeat
- animation: var(--appliance-bg-anim2)
- opacity: var(--appliance-bg-op2)
badge1:
- position: absolute
- top: 10px
- right: 10px
- white-space: nowrap
- padding: 3px 8px
- font-size: 10px
- letter-spacing: 0.5px
- text-transform: uppercase
- font-weight: 700
- z-index: 1
- transition: all 0.5s ease
badge2:
- position: absolute
- top: 38px
- right: 10px
- white-space: nowrap
- padding: 4px 8px
- font-size: 10px
- letter-spacing: 0.5px
- text-transform: uppercase
- font-weight: 500
- z-index: 1
- transition: all 0.5s ease
bar:
- position: absolute
- bottom: 0
- left: 0
- height: 3px
- width: var(--appliance-level)
- background: rgb(var(--appliance-color))
- box-shadow: 0 0 10px rgba(var(--appliance-color), 0.5)
- transition: width 0.5s ease, background 0.5s ease
- z-index: 1
extra_styles: |
[[[
let ent_door = variables.sensor_door;
let ent_temp_f = variables.sensor_temp_fridge;
let ent_temp_z = variables.sensor_temp_freezer;
let ent_power = variables.sensor_power;
let max_power_w = variables.max_power_w;
let max_temp_f = variables.max_temp_fridge;
let max_temp_z = variables.max_temp_freezer;
let state_cooling = variables.state_cooling.split(',').map(s => s.trim().toLowerCase());
let state_super = variables.state_super.split(',').map(s => s.trim().toLowerCase());
let state_defrost = variables.state_defrost.split(',').map(s => s.trim().toLowerCase());
let status = states[variables.sensor_status] ? states[variables.sensor_status].state.toLowerCase() : 'unknown';
let status_clean = status.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
let door_state_val = states[ent_door] ? states[ent_door].state.toLowerCase() : 'unknown';
let door_open = door_state_val === 'on' || door_state_val === 'open';
let raw_temp_f = states[ent_temp_f] ? states[ent_temp_f].state : '';
let raw_temp_z = states[ent_temp_z] ? states[ent_temp_z].state : '';
let bad_states = ['unknown', 'unavailable', ''];
let t_f = !bad_states.includes(raw_temp_f) ? raw_temp_f + '°' : '';
let t_z = !bad_states.includes(raw_temp_z) ? raw_temp_z + '°' : '';
let val_f = parseFloat(raw_temp_f); if (isNaN(val_f)) val_f = -999;
let val_z = parseFloat(raw_temp_z); if (isNaN(val_z)) val_z = -999;
let alert_f = (val_f > max_temp_f) && (val_f !== -999);
let alert_z = (val_z > max_temp_z) && (val_z !== -999);
let raw_power = states[ent_power] ? parseFloat(states[ent_power].state) : NaN;
let power_w = !isNaN(raw_power) ? Math.round(raw_power) : 0;
let power_text = !isNaN(raw_power) ? power_w + 'W' : '';
let calc_prog = Math.floor((power_w / max_power_w) * 100);
let progress = Math.max(0, Math.min(calc_prog, 100));
let has_alert = door_open || alert_f || alert_z;
let is_cooling = state_cooling.includes(status);
let is_super = state_super.includes(status);
let is_defrost = state_defrost.includes(status);
let color = '158, 158, 158'; let card_shadow = 'var(--ha-card-box-shadow, none)';
let bg_d1 = 'none'; let bg_img1 = 'none'; let bg_sz1 = 'none'; let bg_anim1 = 'none'; let bg_op1 = '1'; let bg_rep1 = 'repeat';
let bg_d2 = 'none'; let bg_img2 = 'none'; let bg_sz2 = 'none'; let bg_anim2 = 'none'; let bg_op2 = '1';
let icon_anim = 'none'; let anim_frost = 'none'; let anim_drip = 'none';
let frost_shadow = 'none'; let overlay_img = 'none';
let snow_img_1 = `radial-gradient(circle at 20px 30px, white 2px, transparent 3px), radial-gradient(circle at 50px 160px, white 2px, transparent 3px), radial-gradient(circle at 90px 40px, white 2px, transparent 3px), radial-gradient(circle at 130px 80px, white 2px, transparent 3px), radial-gradient(circle at 160px 120px, white 2px, transparent 3px), radial-gradient(circle at 240px 300px, white 2px, transparent 3px), radial-gradient(circle at 280px 100px, white 2px, transparent 3px)`;
let snow_img_2 = `radial-gradient(circle at 10px 10px, rgba(255,255,255,0.7) 1px, transparent 2px), radial-gradient(circle at 30px 90px, rgba(255,255,255,0.7) 1px, transparent 2px), radial-gradient(circle at 80px 50px, rgba(255,255,255,0.7) 1px, transparent 2px), radial-gradient(circle at 110px 190px, rgba(255,255,255,0.7) 1px, transparent 2px), radial-gradient(circle at 150px 100px, rgba(255,255,255,0.7) 1px, transparent 2px), radial-gradient(circle at 220px 250px, rgba(255,255,255,0.7) 1px, transparent 2px)`;
let drop_img = `radial-gradient(ellipse at center, rgba(255,152,0,0.5) 0%, transparent 60%), radial-gradient(ellipse at center, rgba(255,152,0,0.4) 0%, transparent 60%), radial-gradient(ellipse at center, rgba(255,152,0,0.5) 0%, transparent 60%), radial-gradient(ellipse at center, rgba(255,152,0,0.4) 0%, transparent 60%)`;
if (door_open) {
color = '244, 67, 54'; card_shadow = 'inset 0 0 50px rgba(244, 67, 54, 0.15)';
icon_anim = 'shake-alert 0.5s ease-in-out infinite';
} else if (is_super) {
color = '0, 188, 212'; card_shadow = 'inset 0 0 60px rgba(0, 188, 212, 0.15)';
bg_d1 = 'block'; bg_img1 = snow_img_1; bg_sz1 = '300px 400px'; bg_anim1 = 'snow-fall-1 8s linear infinite'; bg_op1 = '0.15'; bg_rep1 = 'repeat';
bg_d2 = 'block'; bg_img2 = snow_img_2; bg_sz2 = '300px 300px'; bg_anim2 = 'snow-fall-2 4s linear infinite'; bg_op2 = '0.1';
icon_anim = 'compressor-rumble 0.1s linear infinite';
anim_frost = 'pulse-frost 1.5s ease-in-out infinite';
frost_shadow = 'inset 0 0 25px 5px rgba(0, 188, 212, 0.6)';
} else if (is_cooling) {
color = '129, 212, 250'; card_shadow = 'inset 0 0 40px rgba(129, 212, 250, 0.1)';
bg_d1 = 'block'; bg_img1 = snow_img_1; bg_sz1 = '300px 400px'; bg_anim1 = 'snow-fall-1 16s linear infinite'; bg_op1 = '0.08'; bg_rep1 = 'repeat';
bg_d2 = 'block'; bg_img2 = snow_img_2; bg_sz2 = '300px 300px'; bg_anim2 = 'snow-fall-2 8s linear infinite'; bg_op2 = '0.05';
icon_anim = 'compressor-rumble 0.3s linear infinite';
anim_frost = 'pulse-frost 3s ease-in-out infinite';
frost_shadow = 'inset 0 0 15px 2px rgba(129, 212, 250, 0.5)';
} else if (is_defrost) {
color = '255, 152, 0'; card_shadow = 'inset 0 0 40px rgba(255, 152, 0, 0.08)';
bg_d1 = 'block'; bg_img1 = drop_img; bg_sz1 = '100% 100%'; bg_anim1 = 'accumulate-drip-card 4s ease-in infinite'; bg_op1 = '1'; bg_rep1 = 'no-repeat';
anim_drip = 'accumulate-drip-icon 3s ease-in infinite';
overlay_img = `radial-gradient(ellipse at center, rgba(255,193,7,0.8) 0%, transparent 60%), radial-gradient(ellipse at center, rgba(255,193,7,0.6) 0%, transparent 60%)`;
}
let c_cool = '33, 150, 243';
let c_alert = '244, 67, 54';
let badge1 = door_open ? '⚠️ DOOR OPEN' : [status_clean, power_text].filter(Boolean).join(' • ');
let b1_c = door_open ? c_alert : color;
let b1_bg = `rgba(${b1_c}, 0.15)`;
let b1_border = `1px solid rgba(${b1_c}, 0.3)`;
let b1_bl = `2px solid rgb(${b1_c})`;
let b1_br = b1_border;
let b1_color = `rgb(${b1_c})`;
let badge2 = '';
let b2_bg = 'transparent'; let b2_bl = 'none'; let b2_br = 'none'; let b2_border = 'none';
if (t_f || t_z) {
let f_str = alert_f ? `⚠️ ${t_f}` : t_f;
let z_str = alert_z ? `⚠️ ${t_z}` : t_z;
badge2 = [f_str, z_str].filter(Boolean).join(' | ');
let c_f = alert_f ? c_alert : c_cool;
let c_z = alert_z ? c_alert : c_cool;
b2_border = `1px solid rgba(128,128,128, 0.2)`;
if (t_f && t_z) {
b2_bg = `linear-gradient(90deg, rgba(${c_f}, 0.15) 0%, rgba(${c_z}, 0.15) 100%)`;
b2_bl = `2px solid rgb(${c_f})`;
b2_br = `2px solid rgb(${c_z})`;
} else if (t_f) {
b2_bg = `rgba(${c_f}, 0.15)`;
b2_bl = `2px solid rgb(${c_f})`;
b2_br = b2_border;
} else if (t_z) {
b2_bg = `rgba(${c_z}, 0.15)`;
b2_bl = b2_border;
b2_br = `2px solid rgb(${c_z})`;
}
}
let corner_color = color;
let corner_display = 'none';
if (states[ent_door] && !['unknown', 'unavailable', ''].includes(door_state_val)) {
corner_display = 'block';
if (door_open) {
corner_color = '244, 67, 54';
}
}
return `
#card {
--appliance-color: ${color};
--appliance-level: ${progress}%;
--appliance-bg-d1: ${bg_d1}; --appliance-bg-img1: ${bg_img1}; --appliance-bg-sz1: ${bg_sz1}; --appliance-bg-anim1: ${bg_anim1}; --appliance-bg-op1: ${bg_op1}; --appliance-bg-rep1: ${bg_rep1};
--appliance-bg-d2: ${bg_d2}; --appliance-bg-img2: ${bg_img2}; --appliance-bg-sz2: ${bg_sz2}; --appliance-bg-anim2: ${bg_anim2}; --appliance-bg-op2: ${bg_op2};
--appliance-icon-anim: ${icon_anim};
--appliance-anim-frost: ${anim_frost};
--appliance-anim-drip: ${anim_drip};
--appliance-frost-shadow: ${frost_shadow};
--appliance-overlay-bg: ${overlay_img};
--door-corner-color: rgb(${corner_color});
--door-corner-display: ${corner_display};
box-shadow: ${card_shadow} !important;
}
#card::after {
content: '';
display: var(--door-corner-display);
position: absolute;
top: -0.5px;
left: -0.5px;
opacity: 0.7;
width: 15px;
height: 15px;
border-top: 5px solid var(--door-corner-color);
border-left: 5px solid var(--door-corner-color);
border-top-left-radius: var(--ha-card-border-radius, 12px);
pointer-events: none;
transition: border-color 0.5s ease;
z-index: 1;
}
#badge1 {
background: ${b1_bg};
color: ${b1_color};
border-top: ${b1_border};
border-bottom: ${b1_border};
border-left: ${b1_bl};
border-right: ${b1_br};
border-radius: 6px !important;
}
#badge1::before { content: "${badge1}"; }
#badge2 {
display: ${badge2 ? 'block' : 'none'};
background: ${b2_bg};
color: var(--primary-text-color, #fff);
border-top: ${b2_border};
border-bottom: ${b2_border};
border-left: ${b2_bl};
border-right: ${b2_br};
border-radius: 6px !important;
}
#badge2::before { content: "${badge2}"; }
#img-cell::before {
content: ''; position: absolute; inset: 0; box-shadow: var(--appliance-frost-shadow);
animation: var(--appliance-anim-frost) !important; z-index: 1; border-radius: 50%; pointer-events: none;
}
#img-cell::after {
content: ''; position: absolute; inset: 0; background-image: var(--appliance-overlay-bg);
background-repeat: no-repeat; animation: var(--appliance-anim-drip) !important; z-index: 1;
border-radius: 50%; pointer-events: none;
}
@keyframes snow-fall-1 { from { background-position: 0 0; } to { background-position: 0 400px; } }
@keyframes snow-fall-2 { from { background-position: 0 0; } to { background-position: 0 300px; } }
@keyframes accumulate-drip-card {
0% { background-position: 15% 10%, 45% 20%, 75% 15%, 85% 30%; background-size: 0px 0px, 0px 0px, 0px 0px, 0px 0px; opacity: 0; }
40% { background-position: 15% 30%, 45% 40%, 75% 35%, 85% 50%; background-size: 15px 25px, 20px 30px, 12px 20px, 25px 35px; opacity: 0.4; }
80% { background-position: 15% 80%, 45% 85%, 75% 75%, 85% 90%; background-size: 15px 35px, 20px 40px, 12px 30px, 25px 45px; opacity: 0.2; }
100% { background-position: 15% 100%, 45% 105%, 75% 95%, 85% 110%; background-size: 10px 40px, 15px 45px, 8px 35px, 20px 50px; opacity: 0; }
}
@keyframes pulse-frost { 0%, 100% { opacity: 0.4; transform: scale(0.95); } 50% { opacity: 0.8; transform: scale(1); } }
@keyframes accumulate-drip-icon {
0% { background-position: 30% 10%, 70% 20%; background-size: 0px 0px, 0px 0px; opacity: 0; }
40% { background-position: 30% 30%, 70% 40%; background-size: 8px 12px, 12px 16px; opacity: 1; }
80% { background-position: 30% 80%, 70% 90%; background-size: 8px 18px, 12px 22px; opacity: 0.8; }
100% { background-position: 30% 100%, 70% 110%; background-size: 6px 20px, 10px 25px; opacity: 0; }
}
@keyframes compressor-rumble {
0% { transform: translate(0, 0); } 25% { transform: translate(-1px, 0.5px); } 50% { transform: translate(1px, -0.5px); } 75% { transform: translate(-0.5px, 0.5px); } 100% { transform: translate(0, 0); }
}
@keyframes shake-alert {
0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(5deg) translateY(-1px); } 75% { transform: rotate(-5deg) translateY(1px); }
}
`;
]]]
Cards (Dumb)
1 - Dumb Dishwasher (smart plug)
type: custom:button-card
entity: binary_sensor.dishwasher_active_delay
name: Dumb Dishwasher
show_state: false
show_label: true
variables:
ent_helper: binary_sensor.dishwasher_active_delay
ent_switch: switch.smart_plug
ent_power: sensor.smart_plug_power
sensor_door: binary_sensor.dishwasher_door_contact
thresh_heat: 1000
thresh_active: 5
size_icon: 45px
size_shape: 65px
size_card_height: 95px
font_primary: 15px
font_secondary: 13px
font_badge: 11px
styles:
card:
- "--config-icon-size": "[[[ return variables.size_icon ]]]"
- "--config-shape-size": "[[[ return variables.size_shape ]]]"
- "--config-card-height": "[[[ return variables.size_card_height ]]]"
- "--config-font-primary": "[[[ return variables.font_primary ]]]"
- "--config-font-secondary": "[[[ return variables.font_secondary ]]]"
- "--config-font-badge": "[[[ return variables.font_badge ]]]"
- height: var(--config-card-height) !important
- padding: 0px !important
- overflow: hidden
- position: relative
- transition: all 0.5s ease
grid:
- padding: 12px 16px
- height: 100%
- box-sizing: border-box
- grid-template-areas: "\"i n\" \"i l\""
- grid-template-columns: var(--config-shape-size) 1fr
- grid-template-rows: auto auto
- align-content: center
- gap: 0px 12px
- position: relative
icon:
- width: var(--config-icon-size)
- height: var(--config-icon-size)
- color: white
- z-index: 1
- animation: var(--appliance-anim-shake) !important
- transform-origin: 50% 50%
img_cell:
- width: var(--config-shape-size)
- height: var(--config-shape-size)
- border-radius: 50%
- border: 1px solid rgba(255, 255, 255, 0.1) !important
- background: rgba(255, 255, 255, 0.05) !important
- position: relative
- overflow: hidden !important
- justify-self: start
name:
- justify-self: start
- font-size: var(--config-font-primary)
- font-weight: 500
- align-self: end
- margin-bottom: 2px
- position: relative
label:
- justify-self: start
- font-size: var(--config-font-secondary)
- opacity: 0.7
- align-self: start
- margin-top: 2px
- position: relative
custom_fields:
badge1:
- position: absolute
- top: 10px
- right: 10px
- background: rgba(var(--appliance-color), 0.15)
- color: rgb(var(--appliance-color))
- border: 1px solid rgba(var(--appliance-color), 0.3)
- padding: 2px 10px
- border-radius: 12px
- font-size: var(--config-font-badge)
- font-weight: 600
- text-transform: uppercase
- letter-spacing: 0.5px
- white-space: nowrap
- z-index: 1
badge2:
- position: absolute
- top: 38px
- right: 10px
- padding: 4px 8px
- font-size: 10px
- letter-spacing: 0.5px
- white-space: nowrap
- opacity: 0.9
- text-transform: uppercase
- font-weight: 500
- z-index: 1
bar:
- position: absolute
- bottom: 0
- left: 0
- height: 3.5px
- width: var(--appliance-level)
- background: rgb(var(--appliance-color))
- box-shadow: 0 0 10px rgb(var(--appliance-color))
- transition: width 0.5s ease
tap_action:
action: more-info
label: |
[[[
let ent = variables.ent_helper;
let state = states[ent] ? states[ent].state : 'unknown';
if (state === 'on') return 'Running';
if (state === 'off') return 'Not Running';
return state;
]]]
icon: mdi:dishwasher
custom_fields:
badge1: " "
badge2: " "
bar: " "
extra_styles: |
[[[
let ent_switch = variables.ent_switch;
let ent_power = variables.ent_power;
let ent_helper = variables.ent_helper;
let thresh_heat = variables.thresh_heat;
let thresh_active = variables.thresh_active;
let switch_state = states[ent_switch] ? states[ent_switch].state : 'unknown';
let power_state = states[ent_power];
let is_power_valid = power_state && !['unknown', 'unavailable', 'none'].includes(power_state.state.toLowerCase());
let power = is_power_valid ? Math.round(parseFloat(power_state.state)) : 0;
let power_text = is_power_valid ? ` • ${power}W` : '';
let helper_obj = states[ent_helper];
let has_helper = helper_obj && !['unknown', 'unavailable', 'none'].includes(helper_obj.state);
let status_bin = has_helper ? helper_obj.state : (power > thresh_active ? 'on' : 'off');
let time_str = '';
let seconds = 0;
if (status_bin === 'on' && switch_state === 'on') {
if (has_helper && helper_obj.last_changed) {
let start_time = new Date(helper_obj.last_changed);
let now = new Date();
seconds = Math.max(0, Math.floor((now - start_time) / 1000));
let hours = Math.floor(seconds / 3600);
let mins = Math.floor((seconds % 3600) / 60);
time_str = seconds > 60 ? `${hours}h ${mins.toString().padStart(2, '0')}m` : 'Started';
} else {
time_str = 'Started';
}
}
let status_text = 'Idle';
let color = '158, 158, 158';
let anim_type = 'none';
let icon_shake = 'none';
let wave_anim = 'none';
let overlay_img = 'none';
let level = 0;
let badge1 = '';
if (switch_state === 'off') {
status_text = 'Plug Off';
color = '244, 67, 54';
badge1 = status_text;
} else if (['unavailable', 'unknown'].includes(switch_state)) {
status_text = 'Offline';
color = '158, 158, 158';
badge1 = status_text;
} else if (status_bin === 'on') {
if (power > thresh_heat) {
status_text = 'Heating';
color = '255, 152, 0';
anim_type = 'steam-rise 2s ease-in-out infinite';
overlay_img = 'linear-gradient(0deg, transparent, rgba(255,255,255,0.5), transparent)';
} else {
status_text = 'Washing';
color = '33, 150, 243';
anim_type = 'bubbles 1s linear infinite';
overlay_img = 'radial-gradient(2px 2px at 20% 80%, white, transparent), radial-gradient(2px 2px at 50% 70%, white, transparent)';
icon_shake = 'shake 0.8s ease-in-out infinite';
}
wave_anim = 'wave 4s linear infinite';
let mins_elapsed = Math.floor(seconds / 60);
level = Math.min(80, 10 + (Math.floor(mins_elapsed / 10) * 10));
badge1 = `${status_text}${power_text}`;
} else {
status_text = 'Idle';
color = '158, 158, 158';
badge1 = `${status_text}${power_text}`;
}
let ent_door = variables.sensor_door;
let door_state = (ent_door && states[ent_door]) ? states[ent_door].state.toLowerCase() : null;
let corner_color = (switch_state === 'off') ? '158, 158, 158' : color;
let corner_display = 'none';
if (ent_door && door_state && door_state !== 'unknown' && door_state !== 'unavailable') {
corner_display = 'block';
let is_open = (door_state === 'on' || door_state === 'open');
if (is_open) {
corner_color = '244, 67, 54';
}
}
let b2_bg = `rgba(${color}, 0.15)`;
let b2_border = `1px solid rgba(128,128,128, 0.2)`;
let b2_br = `2px solid rgb(${color})`;
return `
#card {
--appliance-color: ${color};
--appliance-level: ${level}%;
--appliance-anim-overlay: ${anim_type};
--appliance-anim-shake: ${icon_shake};
--appliance-anim-wave: ${wave_anim};
--appliance-overlay-bg: ${overlay_img};
--door-corner-color: rgb(${corner_color});
--door-corner-display: ${corner_display};
}
#card::after {
content: '';
display: var(--door-corner-display);
position: absolute;
top: -0.5px;
left: -0.5px;
opacity: 0.7;
width: 15px;
height: 15px;
border-top: 5px solid var(--door-corner-color);
border-left: 5px solid var(--door-corner-color);
border-top-left-radius: var(--ha-card-border-radius, 12px);
pointer-events: none;
transition: border-color 0.5s ease;
}
#badge1::before { content: "${badge1}"; }
#badge2 {
display: ${time_str ? 'block' : 'none'};
background: ${b2_bg};
color: var(--primary-text-color, #fff);
border-top: ${b2_border}; border-bottom: ${b2_border}; border-right: ${b2_border};
border-left: ${b2_br}; border-radius: 6px !important;
}
#badge2::before { content: "${time_str}"; }
#img-cell::before {
content: ''; position: absolute; left: -50%; width: 200%; height: 200%;
top: calc(100% - var(--appliance-level)); background: rgba(var(--appliance-color), 0.6) !important;
border-radius: 40%; animation: var(--appliance-anim-wave) !important;
transition: top 0.5s ease; z-index: 1;
display: ${wave_anim === 'none' ? 'none' : 'block'};
}
#img-cell::after {
content: ''; position: absolute; inset: 0; background-image: var(--appliance-overlay-bg);
background-size: 100% 100%; animation: var(--appliance-anim-overlay) !important; z-index: 1;
display: ${anim_type === 'none' ? 'none' : 'block'};
}
@keyframes wave { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes shake { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(5deg) translateY(-1px); } 75% { transform: rotate(-5deg) translateY(1px); } }
@keyframes bubbles { 0% { transform: translateY(10px); opacity: 0; } 50% { opacity: 1; } 100% { transform: translateY(-20px); opacity: 0; } }
@keyframes steam-rise { 0% { opacity: 0; transform: translateY(5px); } 50% { opacity: 0.8; } 100% { opacity: 0; transform: translateY(-10px); } }
`;
]]]
Dumb Dishwasher (Helper/Template)
- binary_sensor:
- name: "Dishwasher Active delay"
unique_id: dishwasher_active_delay
# Change the entity_id below to match your actual smart plug
state: >
{{ states('sensor.smart_plug_power')|float(0) > 5 }}
delay_off: "00:05:00"
device_class: running
icon: mdi:dishwasher
2 - Dumb Washing Machine (smart plug)
type: custom:button-card
entity: binary_sensor.washing_machine_active_delay
name: Dumb Washer
show_state: false
show_label: true
variables:
ent_helper: binary_sensor.washing_machine_active_delay
ent_switch: switch.smart_plug
ent_power: sensor.smart_plug_power
sensor_door: binary_sensor.washer_door_contact
thresh_heat: 1500
thresh_spin: 150
thresh_active: 5
size_icon: 45px
size_shape: 65px
size_card_height: 95px
font_primary: 15px
font_secondary: 13px
font_badge: 11px
styles:
card:
- "--config-icon-size": "[[[ return variables.size_icon ]]]"
- "--config-shape-size": "[[[ return variables.size_shape ]]]"
- "--config-card-height": "[[[ return variables.size_card_height ]]]"
- "--config-font-primary": "[[[ return variables.font_primary ]]]"
- "--config-font-secondary": "[[[ return variables.font_secondary ]]]"
- "--config-font-badge": "[[[ return variables.font_badge ]]]"
- height: var(--config-card-height) !important
- padding: 0px !important
- overflow: hidden
- position: relative
- transition: all 0.5s ease
grid:
- padding: 12px 16px
- height: 100%
- box-sizing: border-box
- grid-template-areas: "\"i n\" \"i l\""
- grid-template-columns: var(--config-shape-size) 1fr
- grid-template-rows: auto auto
- align-content: center
- gap: 0px 12px
- position: relative
icon:
- width: var(--config-icon-size)
- height: var(--config-icon-size)
- color: white
- z-index: 1
- animation: var(--appliance-anim-shake) !important
- transform-origin: 50% 50%
img_cell:
- width: var(--config-shape-size)
- height: var(--config-shape-size)
- border-radius: 50%
- border: 1px solid rgba(255, 255, 255, 0.1) !important
- background: rgba(255, 255, 255, 0.05) !important
- position: relative
- overflow: hidden !important
- justify-self: start
name:
- justify-self: start
- font-size: var(--config-font-primary)
- font-weight: 500
- align-self: end
- margin-bottom: 2px
- position: relative
label:
- justify-self: start
- font-size: var(--config-font-secondary)
- opacity: 0.7
- align-self: start
- margin-top: 2px
- position: relative
custom_fields:
badge1:
- position: absolute
- top: 10px
- right: 10px
- background: rgba(var(--appliance-color), 0.15)
- color: rgb(var(--appliance-color))
- border: 1px solid rgba(var(--appliance-color), 0.3)
- padding: 2px 10px
- border-radius: 12px
- font-size: var(--config-font-badge)
- font-weight: 600
- text-transform: uppercase
- letter-spacing: 0.5px
- white-space: nowrap
- z-index: 1
- transition: all 0.5s ease
badge2:
- position: absolute
- top: 38px
- right: 10px
- padding: 4px 8px
- font-size: 10px
- letter-spacing: 0.5px
- white-space: nowrap
- opacity: 0.9
- text-transform: uppercase
- font-weight: 500
- z-index: 1
- transition: all 0.5s ease
bar:
- position: absolute
- bottom: 0
- left: 0
- height: 3.5px
- width: var(--appliance-level)
- background: rgb(var(--appliance-color))
- box-shadow: 0 0 10px rgb(var(--appliance-color))
- transition: width 0.5s ease
tap_action:
action: more-info
label: |
[[[
let ent = variables.ent_helper;
let state = states[ent] ? states[ent].state : 'unknown';
if (state === 'on') return 'Running';
if (state === 'off') return 'Not Running';
return state;
]]]
icon: mdi:washing-machine
custom_fields:
badge1: " "
badge2: " "
bar: " "
extra_styles: |
[[[
let ent_switch = variables.ent_switch;
let ent_power = variables.ent_power;
let ent_helper = variables.ent_helper;
let thresh_heat = variables.thresh_heat;
let thresh_spin = variables.thresh_spin;
let thresh_active = variables.thresh_active;
let switch_state = states[ent_switch] ? states[ent_switch].state : 'unknown';
let power_state = states[ent_power];
let is_power_valid = power_state && !['unknown', 'unavailable', 'none'].includes(power_state.state.toLowerCase());
let power = is_power_valid ? Math.round(parseFloat(power_state.state)) : 0;
let power_text = is_power_valid ? ` • ${power}W` : '';
let helper_obj = states[ent_helper];
let has_helper = helper_obj && !['unknown', 'unavailable', 'none'].includes(helper_obj.state);
let status_bin = has_helper ? helper_obj.state : (power > thresh_active ? 'on' : 'off');
let time_str = '';
let seconds = 0;
if (status_bin === 'on' && switch_state === 'on') {
if (has_helper && helper_obj.last_changed) {
let start_time = new Date(helper_obj.last_changed);
let now = new Date();
seconds = Math.max(0, Math.floor((now - start_time) / 1000));
let hours = Math.floor(seconds / 3600);
let mins = Math.floor((seconds % 3600) / 60);
if (seconds > 60) {
time_str = `${hours}h ${mins.toString().padStart(2, '0')}m`;
} else {
time_str = 'Started';
}
} else {
time_str = 'Started';
}
}
let status_text = 'Idle';
let color = '158, 158, 158';
let anim_type = 'none';
let icon_shake = 'none';
let wave_anim = 'none';
let overlay_img = 'none';
let level = 0;
let badge1 = '';
if (switch_state === 'off') {
status_text = 'Plug Off';
color = '244, 67, 54';
badge1 = status_text;
} else if (['unavailable', 'unknown'].includes(switch_state)) {
status_text = 'Offline';
color = '158, 158, 158';
badge1 = status_text;
} else if (status_bin === 'on') {
if (power > thresh_heat) {
status_text = 'Heating';
color = '255, 87, 34'; // Orange for heat
anim_type = 'bubbles 2s linear infinite';
icon_shake = 'none'; // No intense shaking while heating
overlay_img = 'radial-gradient(2px 2px at 20% 80%, white, transparent), radial-gradient(2px 2px at 50% 70%, white, transparent)';
} else if (power > thresh_spin) {
status_text = 'Spinning';
color = '0, 170, 170';
anim_type = 'bubbles 2s linear infinite';
icon_shake = 'washer-spin-smooth 0.8s linear infinite';
overlay_img = 'radial-gradient(2px 2px at 20% 80%, white, transparent), radial-gradient(2px 2px at 50% 70%, white, transparent)';
} else {
status_text = 'Washing';
color = '33, 150, 243'; // Blue for washing
anim_type = 'bubbles 2s linear infinite';
overlay_img = 'radial-gradient(2px 2px at 20% 80%, white, transparent), radial-gradient(2px 2px at 50% 70%, white, transparent)';
icon_shake = 'shake 2s ease-in-out infinite';
}
wave_anim = 'wave 4s linear infinite';
let mins_elapsed = Math.floor(seconds / 60);
let extra_level = Math.floor(mins_elapsed / 10) * 10;
level = Math.min(80, 10 + extra_level);
badge1 = `${status_text}${power_text}`;
} else {
status_text = 'Idle';
color = '158, 158, 158';
badge1 = `${status_text}${power_text}`;
}
let ent_door = variables.sensor_door;
let door_state = (ent_door && states[ent_door]) ? states[ent_door].state.toLowerCase() : null;
let corner_color = (switch_state === 'off') ? '158, 158, 158' : color;
let corner_display = 'none';
if (ent_door && door_state && door_state !== 'unknown' && door_state !== 'unavailable') {
corner_display = 'block';
let is_open = (door_state === 'on' || door_state === 'open');
if (is_open) {
corner_color = '244, 67, 54';
}
}
let b2_bg = `rgba(${color}, 0.15)`;
let b2_border = `1px solid rgba(128,128,128, 0.2)`;
let b2_br = `2px solid rgb(${color})`;
return `
#card {
--appliance-color: ${color};
--appliance-level: ${level}%;
--appliance-anim-overlay: ${anim_type};
--appliance-anim-shake: ${icon_shake};
--appliance-anim-wave: ${wave_anim};
--appliance-overlay-bg: ${overlay_img};
--door-corner-color: rgb(${corner_color});
--door-corner-display: ${corner_display};
}
#card::after {
content: '';
display: var(--door-corner-display);
position: absolute;
top: -0.5px;
left: -0.5px;
opacity: 0.7;
width: 15px;
height: 15px;
border-top: 5px solid var(--door-corner-color);
border-left: 5px solid var(--door-corner-color);
border-top-left-radius: var(--ha-card-border-radius, 12px);
pointer-events: none;
transition: border-color 0.5s ease;
}
#badge1::before { content: "${badge1}"; }
#badge2 {
display: ${time_str ? 'block' : 'none'};
background: ${b2_bg};
color: var(--primary-text-color, #fff);
border-top: ${b2_border};
border-bottom: ${b2_border};
border-right: ${b2_border};
border-left: ${b2_br};
border-radius: 6px !important;
}
#badge2::before { content: "${time_str}"; }
#img-cell::before {
content: ''; position: absolute; left: -50%; width: 200%; height: 200%;
top: calc(100% - var(--appliance-level)); background: rgba(var(--appliance-color), 0.6) !important;
border-radius: 40%; animation: var(--appliance-anim-wave) !important;
transition: top 0.5s ease; z-index: 1;
display: ${wave_anim === 'none' ? 'none' : 'block'};
}
#img-cell::after {
content: ''; position: absolute; inset: 0; background-image: var(--appliance-overlay-bg);
background-size: 100% 100%; animation: var(--appliance-anim-overlay) !important; z-index: 1;
display: ${anim_type === 'none' ? 'none' : 'block'};
}
@keyframes wave { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes shake { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(5deg) translateY(-1px); } 75% { transform: rotate(-5deg) translateY(1px); } }
@keyframes bubbles { 0% { transform: translateY(10px); opacity: 0; } 50% { opacity: 1; } 100% { transform: translateY(-20px); opacity: 0; } }
@keyframes washer-spin-smooth {
0% { transform: rotate(0deg) translate(0,0); }
25% { transform: rotate(90deg) translate(0.5px, 0.5px); }
50% { transform: rotate(180deg) translate(0,0); }
75% { transform: rotate(270deg) translate(-0.5px, -0.5px); }
100% { transform: rotate(360deg) translate(0,0); }
}
`;
]]]
Dumb Washing Machine (Helper/Template)
- binary_sensor:
- name: "Washing Machine Active delay"
unique_id: washing_machine_active_delay
# Change the entity_id below to match your actual smart plug
state: >
{{ states('sensor.smart_plug_power')|float(0) > 5 }}
delay_off: "00:05:00"
device_class: running
icon: mdi:washing-machine
3 - Dumb Dryer (smart plug)
type: custom:button-card
entity: binary_sensor.dryer_active_delay
name: Dumb Dryer
show_state: false
show_label: true
variables:
ent_helper: binary_sensor.dryer_active_delay
ent_switch: switch.smart_plug
ent_power: sensor.smart_plug_power
sensor_door: binary_sensor.dryer_door_contact
thresh_heat: 500
thresh_active: 5
size_icon: 45px
size_shape: 65px
size_card_height: 95px
font_primary: 15px
font_secondary: 13px
font_badge: 11px
styles:
card:
- "--config-icon-size": "[[[ return variables.size_icon ]]]"
- "--config-shape-size": "[[[ return variables.size_shape ]]]"
- "--config-card-height": "[[[ return variables.size_card_height ]]]"
- "--config-font-primary": "[[[ return variables.font_primary ]]]"
- "--config-font-secondary": "[[[ return variables.font_secondary ]]]"
- "--config-font-badge": "[[[ return variables.font_badge ]]]"
- height: var(--config-card-height) !important
- padding: 0px !important
- overflow: hidden
- position: relative
- transition: all 0.5s ease
grid:
- padding: 12px 16px
- height: 100%
- box-sizing: border-box
- grid-template-areas: "\"i n\" \"i l\""
- grid-template-columns: var(--config-shape-size) 1fr
- grid-template-rows: auto auto
- align-content: center
- gap: 0px 12px
- position: relative
icon:
- width: var(--config-icon-size)
- height: var(--config-icon-size)
- color: white
- z-index: 1
- animation: var(--appliance-anim-shake) !important
- transform-origin: 50% 50%
img_cell:
- width: var(--config-shape-size)
- height: var(--config-shape-size)
- border-radius: 50%
- border: 1px solid rgba(255, 255, 255, 0.1) !important
- background: rgba(255, 255, 255, 0.05) !important
- position: relative
- overflow: hidden !important
- justify-self: start
name:
- justify-self: start
- font-size: var(--config-font-primary)
- font-weight: 500
- align-self: end
- margin-bottom: 2px
- position: relative
label:
- justify-self: start
- font-size: var(--config-font-secondary)
- opacity: 0.7
- align-self: start
- margin-top: 2px
- position: relative
custom_fields:
badge1:
- position: absolute
- top: 10px
- right: 10px
- background: rgba(var(--appliance-color), 0.15)
- color: rgb(var(--appliance-color))
- border: 1px solid rgba(var(--appliance-color), 0.3)
- padding: 2px 10px
- border-radius: 12px
- font-size: var(--config-font-badge)
- font-weight: 600
- text-transform: uppercase
- letter-spacing: 0.5px
- white-space: nowrap
- z-index: 1
badge2:
- position: absolute
- top: 38px
- right: 10px
- padding: 4px 8px
- font-size: 10px
- letter-spacing: 0.5px
- white-space: nowrap
- opacity: 0.9
- text-transform: uppercase
- font-weight: 500
- z-index: 1
bar:
- position: absolute
- bottom: 0
- left: 0
- height: 3.5px
- width: var(--appliance-level)
- background: rgb(var(--appliance-color))
- box-shadow: 0 0 10px rgb(var(--appliance-color))
- transition: width 0.5s ease
tap_action:
action: more-info
label: |
[[[
let ent = variables.ent_helper;
let state = states[ent] ? states[ent].state : 'unknown';
if (state === 'on') return 'Running';
if (state === 'off') return 'Not Running';
return state;
]]]
icon: mdi:tumble-dryer
custom_fields:
badge1: " "
badge2: " "
bar: " "
extra_styles: |
[[[
let ent_switch = variables.ent_switch;
let ent_power = variables.ent_power;
let ent_helper = variables.ent_helper;
let thresh_heat = variables.thresh_heat;
let thresh_active = variables.thresh_active;
let switch_state = states[ent_switch] ? states[ent_switch].state : 'unknown';
let power_state = states[ent_power];
let is_power_valid = power_state && !['unknown', 'unavailable', 'none'].includes(power_state.state.toLowerCase());
let power = is_power_valid ? Math.round(parseFloat(power_state.state)) : 0;
let power_text = is_power_valid ? ` • ${power}W` : '';
let helper_obj = states[ent_helper];
let has_helper = helper_obj && !['unknown', 'unavailable', 'none'].includes(helper_obj.state);
let status_bin = has_helper ? helper_obj.state : (power > thresh_active ? 'on' : 'off');
let time_str = '';
let seconds = 0;
if (status_bin === 'on' && switch_state === 'on') {
if (has_helper && helper_obj.last_changed) {
let start_time = new Date(helper_obj.last_changed);
let now = new Date();
seconds = Math.max(0, Math.floor((now - start_time) / 1000));
let hours = Math.floor(seconds / 3600);
let mins = Math.floor((seconds % 3600) / 60);
time_str = seconds > 60 ? `${hours}h ${mins.toString().padStart(2, '0')}m` : 'Started';
} else {
time_str = 'Started';
}
}
let status_text = 'Idle';
let color = '158, 158, 158';
let anim_type = 'none';
let icon_shake = 'none';
let wave_anim = 'none';
let overlay_img = 'none';
let level = 0;
let badge1 = '';
if (switch_state === 'off') {
status_text = 'Plug Off';
color = '244, 67, 54';
badge1 = status_text;
} else if (['unavailable', 'unknown'].includes(switch_state)) {
status_text = 'Offline';
color = '158, 158, 158';
badge1 = status_text;
} else if (status_bin === 'on') {
if (power > thresh_heat) {
status_text = 'Drying';
color = '255, 152, 0';
anim_type = 'steam-rise 2s ease-in-out infinite';
overlay_img = 'linear-gradient(0deg, transparent, rgba(255,255,255,0.4), transparent)';
icon_shake = 'none';
} else {
status_text = 'Tumbling';
color = '33, 150, 243';
anim_type = 'breeze 3s ease-in-out infinite';
overlay_img = 'radial-gradient(circle, rgba(255,255,255,0.5) 0%, transparent 70%)';
icon_shake = 'wobble 2s ease-in-out infinite';
}
wave_anim = 'wave 4s linear infinite';
let mins_elapsed = Math.floor(seconds / 60);
level = Math.min(80, 10 + (Math.floor(mins_elapsed / 10) * 10));
badge1 = `${status_text}${power_text}`;
} else {
status_text = 'Idle';
color = '158, 158, 158';
badge1 = `${status_text}${power_text}`;
}
let ent_door = variables.sensor_door;
let door_state = (ent_door && states[ent_door]) ? states[ent_door].state.toLowerCase() : null;
let corner_color = (switch_state === 'off') ? '158, 158, 158' : color;
let corner_display = 'none';
if (ent_door && door_state && door_state !== 'unknown' && door_state !== 'unavailable') {
corner_display = 'block';
let is_open = (door_state === 'on' || door_state === 'open');
if (is_open) {
corner_color = '244, 67, 54';
}
}
let b2_bg = `rgba(${color}, 0.15)`;
let b2_border = `1px solid rgba(128,128,128, 0.2)`;
let b2_br = `2px solid rgb(${color})`;
return `
#card {
--appliance-color: ${color};
--appliance-level: ${level}%;
--appliance-anim-overlay: ${anim_type};
--appliance-anim-shake: ${icon_shake};
--appliance-anim-wave: ${wave_anim};
--appliance-overlay-bg: ${overlay_img};
--door-corner-color: rgb(${corner_color});
--door-corner-display: ${corner_display};
}
#card::after {
content: '';
display: var(--door-corner-display);
position: absolute;
top: -0.5px;
left: -0.5px;
opacity: 0.7;
width: 15px;
height: 15px;
border-top: 5px solid var(--door-corner-color);
border-left: 5px solid var(--door-corner-color);
border-top-left-radius: var(--ha-card-border-radius, 12px);
pointer-events: none;
transition: border-color 0.5s ease;
}
#badge1::before { content: "${badge1}"; }
#badge2 {
display: ${time_str ? 'block' : 'none'};
background: ${b2_bg};
color: var(--primary-text-color, #fff);
border-top: ${b2_border}; border-bottom: ${b2_border}; border-right: ${b2_border};
border-left: ${b2_br}; border-radius: 6px !important;
}
#badge2::before { content: "${time_str}"; }
#img-cell::before {
content: ''; position: absolute; left: -50%; width: 200%; height: 200%;
top: calc(100% - var(--appliance-level)); background: rgba(var(--appliance-color), 0.6) !important;
border-radius: 40%; animation: var(--appliance-anim-wave) !important;
transition: top 0.5s ease; z-index: 1;
display: ${wave_anim === 'none' ? 'none' : 'block'};
}
#img-cell::after {
content: ''; position: absolute; inset: 0; background-image: var(--appliance-overlay-bg);
background-size: 100% 100%; animation: var(--appliance-anim-overlay) !important; z-index: 1;
display: ${anim_type === 'none' ? 'none' : 'block'};
}
@keyframes wave { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes wobble { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(5deg); } }
@keyframes steam-rise { 0% { opacity: 0; transform: translateY(10px) scale(0.9); } 50% { opacity: 0.6; } 100% { opacity: 0; transform: translateY(-20px) scale(1.1); } }
@keyframes breeze { 0% { opacity: 0.2; transform: scale(0.95); } 50% { opacity: 0.5; transform: scale(1.05); } 100% { opacity: 0.2; transform: scale(0.95); } }
`;
]]]
Dumb Dryer (Helper/Template)
- binary_sensor:
- name: "Dryer Active delay"
unique_id: dryer_active_delay
# Change the entity_id below to match your actual smart plug
state: >
{{ states('sensor.smart_plug_power')|float(0) > 5 }}
delay_off: "00:05:00"
device_class: running
icon: mdi:tumble-dryer
4 - Dumb Combo Washing machine & Dryer (smart plug)
type: custom:button-card
entity: binary_sensor.combo_machine_active_delay
name: Dumb Combo
show_state: false
show_label: true
variables:
ent_helper_run: binary_sensor.combo_machine_active_delay
ent_helper_dry_mode: binary_sensor.combo_machine_drying_detector
ent_switch: switch.smart_plug
ent_power: sensor.smart_plug_power
sensor_door: binary_sensor.combo_door_contact
thresh_high: 800
thresh_active: 5
size_icon: 45px
size_shape: 65px
size_card_height: 95px
font_primary: 15px
font_secondary: 13px
font_badge: 11px
styles:
card:
- "--config-icon-size": "[[[ return variables.size_icon ]]]"
- "--config-shape-size": "[[[ return variables.size_shape ]]]"
- "--config-card-height": "[[[ return variables.size_card_height ]]]"
- "--config-font-primary": "[[[ return variables.font_primary ]]]"
- "--config-font-secondary": "[[[ return variables.font_secondary ]]]"
- "--config-font-badge": "[[[ return variables.font_badge ]]]"
- height: var(--config-card-height) !important
- padding: 0px !important
- overflow: hidden
- position: relative
- transition: all 0.5s ease
grid:
- padding: 12px 16px
- height: 100%
- box-sizing: border-box
- grid-template-areas: "\"i n\" \"i l\""
- grid-template-columns: var(--config-shape-size) 1fr
- grid-template-rows: auto auto
- align-content: center
- gap: 0px 12px
- position: relative
icon:
- width: var(--config-icon-size)
- height: var(--config-icon-size)
- color: white
- z-index: 1
- animation: var(--appliance-anim-shake) !important
- transform-origin: 50% 50%
img_cell:
- width: var(--config-shape-size)
- height: var(--config-shape-size)
- border-radius: 50%
- border: 1px solid rgba(255, 255, 255, 0.1) !important
- background: rgba(255, 255, 255, 0.05) !important
- position: relative
- overflow: hidden !important
- justify-self: start
name:
- justify-self: start
- font-size: var(--config-font-primary)
- font-weight: 500
- align-self: end
- margin-bottom: 2px
- position: relative
label:
- justify-self: start
- font-size: var(--config-font-secondary)
- opacity: 0.7
- align-self: start
- margin-top: 2px
- position: relative
custom_fields:
badge1:
- position: absolute
- top: 10px
- right: 10px
- background: rgba(var(--appliance-color), 0.15)
- color: rgb(var(--appliance-color))
- border: 1px solid rgba(var(--appliance-color), 0.3)
- padding: 2px 10px
- border-radius: 12px
- font-size: var(--config-font-badge)
- font-weight: 600
- text-transform: uppercase
- letter-spacing: 0.5px
- white-space: nowrap
- z-index: 1
- transition: all 0.5s ease
badge2:
- position: absolute
- top: 38px
- right: 10px
- padding: 4px 8px
- font-size: 10px
- letter-spacing: 0.5px
- white-space: nowrap
- opacity: 0.9
- text-transform: uppercase
- font-weight: 500
- z-index: 1
- transition: all 0.5s ease
bar:
- position: absolute
- bottom: 0
- left: 0
- height: 3.5px
- width: var(--appliance-level)
- background: rgb(var(--appliance-color))
- box-shadow: 0 0 10px rgb(var(--appliance-color))
- transition: width 0.5s ease
tap_action:
action: more-info
label: |
[[[
let ent = variables.ent_helper_run;
let state = states[ent] ? states[ent].state : 'unknown';
if (state === 'on') return 'Running';
if (state === 'off') return 'Not Running';
return state;
]]]
icon: mdi:washing-machine
custom_fields:
badge1: " "
badge2: " "
bar: " "
extra_styles: |
[[[
let ent_switch = variables.ent_switch;
let ent_power = variables.ent_power;
let ent_helper_run = variables.ent_helper_run;
let ent_dry = variables.ent_helper_dry_mode;
let thresh_high = variables.thresh_high;
let thresh_active = variables.thresh_active;
let switch_state = states[ent_switch] ? states[ent_switch].state : 'unknown';
let power_state = states[ent_power];
let is_power_valid = power_state && !['unknown', 'unavailable', 'none'].includes(power_state.state.toLowerCase());
let power = is_power_valid ? Math.round(parseFloat(power_state.state)) : 0;
let power_text = is_power_valid ? ` • ${power}W` : '';
let run_obj = states[ent_helper_run];
let dry_obj = states[ent_dry];
let has_run_helper = run_obj && !['unknown', 'unavailable', 'none'].includes(run_obj.state);
let is_running = has_run_helper ? run_obj.state === 'on' : (power > thresh_active);
let is_drying_mode = dry_obj && dry_obj.state === 'on';
let time_str = '';
let seconds = 0;
if (is_running && switch_state === 'on') {
if (has_run_helper && run_obj.last_changed) {
let start_time = new Date(run_obj.last_changed);
let now = new Date();
seconds = Math.max(0, Math.floor((now - start_time) / 1000));
let hours = Math.floor(seconds / 3600);
let mins = Math.floor((seconds % 3600) / 60);
time_str = seconds > 60 ? `${hours}h ${mins.toString().padStart(2, '0')}m` : 'Started';
} else {
time_str = 'Running';
}
}
let status_text = 'Idle';
let color = '158, 158, 158';
let anim_type = 'none';
let icon_shake = 'none';
let wave_anim = 'none';
let overlay_img = 'none';
let level = 0;
let badge1 = '';
if (switch_state === 'off') {
status_text = 'Plug Off';
color = '244, 67, 54';
badge1 = status_text;
} else if (['unavailable', 'unknown'].includes(switch_state)) {
status_text = 'Offline';
color = '158, 158, 158';
badge1 = status_text;
} else if (is_running) {
if (is_drying_mode) {
status_text = 'Drying';
color = '255, 152, 0';
anim_type = 'steam-rise 2s ease-in-out infinite';
overlay_img = 'linear-gradient(0deg, transparent, rgba(255,255,255,0.4), transparent)';
icon_shake = 'none';
} else {
status_text = 'Washing';
color = '33, 150, 243';
anim_type = 'bubbles 2s linear infinite';
overlay_img = 'radial-gradient(2px 2px at 20% 80%, white, transparent), radial-gradient(2px 2px at 50% 70%, white, transparent)';
if (power > thresh_high) {
status_text = 'Spinning';
color = '0, 170, 170';
icon_shake = 'washer-spin-smooth 0.8s linear infinite';
} else {
icon_shake = 'shake 2s ease-in-out infinite';
}
}
wave_anim = 'wave 4s linear infinite';
let mins_elapsed = Math.floor(seconds / 60);
level = Math.min(80, 10 + (Math.floor(mins_elapsed / 10) * 10));
badge1 = `${status_text}${power_text}`;
} else {
status_text = 'Idle';
color = '158, 158, 158';
badge1 = `${status_text}${power_text}`;
}
let ent_door = variables.sensor_door;
let door_state = (ent_door && states[ent_door]) ? states[ent_door].state.toLowerCase() : null;
let corner_color = (switch_state === 'off') ? '158, 158, 158' : color;
let corner_display = 'none';
if (ent_door && door_state && door_state !== 'unknown' && door_state !== 'unavailable') {
corner_display = 'block';
let is_open = (door_state === 'on' || door_state === 'open');
if (is_open) {
corner_color = '244, 67, 54';
}
}
let b2_bg = `rgba(${color}, 0.15)`;
let b2_border = `1px solid rgba(128,128,128, 0.2)`;
let b2_br = `2px solid rgb(${color})`;
return `
#card {
--appliance-color: ${color};
--appliance-level: ${level}%;
--appliance-anim-overlay: ${anim_type};
--appliance-anim-shake: ${icon_shake};
--appliance-anim-wave: ${wave_anim};
--appliance-overlay-bg: ${overlay_img};
--door-corner-color: rgb(${corner_color});
--door-corner-display: ${corner_display};
}
#card::after {
content: '';
display: var(--door-corner-display);
position: absolute;
top: -0.5px;
left: -0.5px;
opacity: 0.7;
width: 15px;
height: 15px;
border-top: 5px solid var(--door-corner-color);
border-left: 5px solid var(--door-corner-color);
border-top-left-radius: var(--ha-card-border-radius, 12px);
pointer-events: none;
transition: border-color 0.5s ease;
}
#badge1::before { content: "${badge1}"; }
#badge2 {
display: ${time_str ? 'block' : 'none'};
background: ${b2_bg};
color: var(--primary-text-color, #fff);
border-top: ${b2_border}; border-bottom: ${b2_border}; border-right: ${b2_border};
border-left: ${b2_br}; border-radius: 6px !important;
}
#badge2::before { content: "${time_str}"; }
#img-cell::before {
content: ''; position: absolute; left: -50%; width: 200%; height: 200%;
top: calc(100% - var(--appliance-level)); background: rgba(var(--appliance-color), 0.6) !important;
border-radius: 40%; animation: var(--appliance-anim-wave) !important;
transition: top 0.5s ease; z-index: 1;
display: ${wave_anim === 'none' ? 'none' : 'block'};
}
#img-cell::after {
content: ''; position: absolute; inset: 0; background-image: var(--appliance-overlay-bg);
background-size: 100% 100%; animation: var(--appliance-anim-overlay) !important; z-index: 1;
display: ${anim_type === 'none' ? 'none' : 'block'};
}
@keyframes wave { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes shake { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(5deg) translateY(-1px); } 75% { transform: rotate(-5deg) translateY(1px); } }
@keyframes washer-spin-smooth {
0% { transform: rotate(0deg) translate(0,0); }
25% { transform: rotate(90deg) translate(0.5px, 0.5px); }
50% { transform: rotate(180deg) translate(0,0); }
75% { transform: rotate(270deg) translate(-0.5px, -0.5px); }
100% { transform: rotate(360deg) translate(0,0); }
}
@keyframes bubbles { 0% { transform: translateY(10px); opacity: 0; } 50% { opacity: 1; } 100% { transform: translateY(-20px); opacity: 0; } }
@keyframes steam-rise { 0% { opacity: 0; transform: translateY(10px) scale(0.9); } 50% { opacity: 0.6; } 100% { opacity: 0; transform: translateY(-20px) scale(1.1); } }
`;
]]]
Dumb Combo Washing machine & Dryer (Helper/Template)
- binary_sensor:
# 1. Main "Combo Machine Running" Detector
- name: "Combo Machine Active Delay"
unique_id: combo_machine_active_delay
device_class: running
icon: mdi:washing-machine
state: >
{{ states('sensor.smart_plug_power')|float(0) > 5 }}
delay_off: "00:05:00" # Wait 5 min before saying it's off
# 2. "Drying Mode" Detector
# Logic If we see HIGH power for a sustained period, we assume Drying.
# Note: Washing heaters cycle on/off quickly. Dryers stay on longer.
- name: "Combo Machine Drying Detector"
unique_id: combo_machine_drying_detector
state: >
{{ states('sensor.smart_plug_power')|float(0) > 800 }}
# MUST be high for 15 mins to count as drying
delay_on: "00:15:00"
# Keeps drying active during cool down
delay_off: "00:05:00"
5 - Fridge (smart plug)
type: custom:button-card
entity: sensor.smart_plug_power
name: Dumb Fridge
show_state: false
show_label: true
variables:
ent_switch: switch.smart_plug
ent_power: sensor.smart_plug_power
sensor_door: binary_sensor.fridge_door_contact
sensor_temp_fridge: sensor.fridge_temp
sensor_temp_freezer: sensor.fridge_temp_freezer
thresh_cooling: 1
thresh_super: 180
thresh_defrost: 300
max_power_w: 500
max_temp_fridge: 8
max_temp_freezer: -10
show_bar: "off"
size_icon: 45px
size_shape: 65px
size_card_height: 95px
font_primary: 15px
font_secondary: 12px
font_badge: 10px
styles:
card:
- "--config-icon-size": "[[[ return variables.size_icon ]]]"
- "--config-shape-size": "[[[ return variables.size_shape ]]]"
- "--config-card-height": "[[[ return variables.size_card_height ]]]"
- "--config-font-primary": "[[[ return variables.font_primary ]]]"
- "--config-font-secondary": "[[[ return variables.font_secondary ]]]"
- "--config-font-badge": "[[[ return variables.font_badge ]]]"
- height: var(--config-card-height) !important
- padding: 0px !important
- overflow: hidden
- position: relative
- transition: box-shadow 1s ease, background 1s ease
grid:
- padding: 12px 16px
- height: 100%
- box-sizing: border-box
- grid-template-areas: "\"i n\" \"i l\""
- grid-template-columns: var(--config-shape-size) 1fr
- grid-template-rows: auto auto
- align-content: center
- gap: 0px 12px
- position: relative
- z-index: 2
icon:
- width: var(--config-icon-size)
- height: var(--config-icon-size)
- color: rgb(var(--appliance-color))
- z-index: 3
- animation: var(--appliance-icon-anim) !important
- transform-origin: 50% 60%
img_cell:
- width: var(--config-shape-size)
- height: var(--config-shape-size)
- border-radius: 50%
- border: 1px solid rgba(var(--appliance-color), 0.3) !important
- background: rgba(var(--appliance-color), 0.1) !important
- position: relative
- overflow: visible !important
- justify-self: start
- z-index: 1
name:
- justify-self: start
- font-size: var(--config-font-primary)
- font-weight: 500
- align-self: end
- margin-bottom: 2px
- position: relative
- z-index: 2
label:
- justify-self: start
- font-size: var(--config-font-secondary)
- opacity: 0.7
- align-self: start
- margin-top: 2px
- position: relative
- z-index: 2
custom_fields:
bg1:
- position: absolute
- inset: 0
- pointer-events: none
- z-index: 0
- display: var(--appliance-bg-d1)
- background-image: var(--appliance-bg-img1)
- background-size: var(--appliance-bg-sz1)
- background-repeat: var(--appliance-bg-rep1)
- animation: var(--appliance-bg-anim1)
- opacity: var(--appliance-bg-op1)
bg2:
- position: absolute
- inset: 0
- pointer-events: none
- z-index: 0
- display: var(--appliance-bg-d2)
- background-image: var(--appliance-bg-img2)
- background-size: var(--appliance-bg-sz2)
- background-repeat: repeat
- animation: var(--appliance-bg-anim2)
- opacity: var(--appliance-bg-op2)
badge1:
- position: absolute
- top: 10px
- right: 10px
- padding: 3px 8px
- font-size: var(--config-font-badge)
- font-weight: 600
- text-transform: uppercase
- letter-spacing: 0.5px
- white-space: nowrap
- z-index: 5
- transition: all 0.5s ease
badge2:
- position: absolute
- top: 38px
- right: 10px
- padding: 4px 8px
- font-size: 10px
- letter-spacing: 0.5px
- white-space: nowrap
- opacity: 0.9
- text-transform: uppercase
- font-weight: 500
- z-index: 5
- transition: all 0.5s ease
bar:
- position: absolute
- bottom: 0
- left: 0
- height: 3.5px
- width: var(--appliance-level)
- background: rgb(var(--appliance-color))
- box-shadow: 0 0 10px rgb(var(--appliance-color))
- transition: width 0.5s ease
- display: var(--bar-display)
- z-index: 5
tap_action:
action: more-info
label: |
[[[ return entity ? entity.state : 'Entity Setup Required'; ]]]
icon: |
[[[
let door = states[variables.sensor_door];
return (door && (door.state === 'on' || door.state === 'open')) ? 'mdi:fridge-alert' : 'mdi:fridge';
]]]
custom_fields:
bg1: " "
bg2: " "
badge1: " "
badge2: " "
bar: " "
extra_styles: |
[[[
let ent_switch = variables.ent_switch;
let ent_power = variables.ent_power;
let ent_door = variables.sensor_door;
let ent_temp_f = variables.sensor_temp_fridge;
let ent_temp_z = variables.sensor_temp_freezer;
let thresh_cool = variables.thresh_cooling;
let thresh_super = variables.thresh_super;
let thresh_defrost = variables.thresh_defrost;
let max_power = variables.max_power_w;
let max_temp_f = variables.max_temp_fridge;
let max_temp_z = variables.max_temp_freezer;
let show_bar = variables.show_bar;
let switch_state = states[ent_switch] ? states[ent_switch].state : 'unknown';
let power_state = states[ent_power];
let is_power_valid = power_state && !['unknown', 'unavailable', 'none'].includes(power_state.state.toLowerCase());
let power = is_power_valid ? Math.round(parseFloat(power_state.state)) : 0;
let power_text = is_power_valid ? ` • ${power}W` : '';
let door_state = (ent_door && states[ent_door]) ? states[ent_door].state.toLowerCase() : 'unknown';
let door_open = door_state === 'on' || door_state === 'open';
let raw_temp_f = states[ent_temp_f] ? states[ent_temp_f].state : '';
let raw_temp_z = states[ent_temp_z] ? states[ent_temp_z].state : '';
let bad_states = ['unknown', 'unavailable', ''];
let t_f = !bad_states.includes(raw_temp_f) ? raw_temp_f + '°' : '';
let t_z = !bad_states.includes(raw_temp_z) ? raw_temp_z + '°' : '';
let val_f = parseFloat(raw_temp_f); if (isNaN(val_f)) val_f = -999;
let val_z = parseFloat(raw_temp_z); if (isNaN(val_z)) val_z = -999;
let alert_f = (val_f > max_temp_f) && (val_f !== -999);
let alert_z = (val_z > max_temp_z) && (val_z !== -999);
let status_text = 'Idle';
let color = '158, 158, 158';
let card_shadow = 'var(--ha-card-box-shadow, none)';
let bg_d1 = 'none'; let bg_img1 = 'none'; let bg_sz1 = 'none'; let bg_anim1 = 'none'; let bg_op1 = '1'; let bg_rep1 = 'repeat';
let bg_d2 = 'none'; let bg_img2 = 'none'; let bg_sz2 = 'none'; let bg_anim2 = 'none'; let bg_op2 = '1';
let icon_anim = 'none'; let anim_frost = 'none'; let anim_drip = 'none';
let frost_shadow = 'none'; let overlay_img = 'none';
let snow_img_1 = `radial-gradient(circle at 20px 30px, white 2px, transparent 3px), radial-gradient(circle at 50px 160px, white 2px, transparent 3px), radial-gradient(circle at 90px 40px, white 2px, transparent 3px), radial-gradient(circle at 130px 80px, white 2px, transparent 3px), radial-gradient(circle at 160px 120px, white 2px, transparent 3px), radial-gradient(circle at 240px 300px, white 2px, transparent 3px), radial-gradient(circle at 280px 100px, white 2px, transparent 3px)`;
let snow_img_2 = `radial-gradient(circle at 10px 10px, rgba(255,255,255,0.7) 1px, transparent 2px), radial-gradient(circle at 30px 90px, rgba(255,255,255,0.7) 1px, transparent 2px), radial-gradient(circle at 80px 50px, rgba(255,255,255,0.7) 1px, transparent 2px), radial-gradient(circle at 110px 190px, rgba(255,255,255,0.7) 1px, transparent 2px), radial-gradient(circle at 150px 100px, rgba(255,255,255,0.7) 1px, transparent 2px), radial-gradient(circle at 220px 250px, rgba(255,255,255,0.7) 1px, transparent 2px)`;
let drop_img = `radial-gradient(ellipse at center, rgba(255,152,0,0.5) 0%, transparent 60%), radial-gradient(ellipse at center, rgba(255,152,0,0.4) 0%, transparent 60%), radial-gradient(ellipse at center, rgba(255,152,0,0.5) 0%, transparent 60%), radial-gradient(ellipse at center, rgba(255,152,0,0.4) 0%, transparent 60%)`;
if (switch_state === 'off') {
status_text = 'Plug Off';
color = '244, 67, 54';
} else if (['unavailable', 'unknown'].includes(switch_state)) {
status_text = 'Offline';
} else {
if (power > thresh_defrost) {
status_text = 'Defrost';
color = '255, 152, 0';
card_shadow = 'inset 0 0 40px rgba(255, 152, 0, 0.08)';
bg_d1 = 'block'; bg_img1 = drop_img; bg_sz1 = '100% 100%'; bg_anim1 = 'accumulate-drip-card 4s ease-in infinite'; bg_op1 = '1'; bg_rep1 = 'no-repeat';
anim_drip = 'accumulate-drip-icon 3s ease-in infinite';
overlay_img = `radial-gradient(ellipse at center, rgba(255,193,7,0.8) 0%, transparent 60%), radial-gradient(ellipse at center, rgba(255,193,7,0.6) 0%, transparent 60%)`;
} else if (power > thresh_super) {
status_text = 'Super Cool';
color = '0, 188, 212';
card_shadow = 'inset 0 0 60px rgba(0, 188, 212, 0.15)';
bg_d1 = 'block'; bg_img1 = snow_img_1; bg_sz1 = '300px 400px'; bg_anim1 = 'snow-fall-1 8s linear infinite'; bg_op1 = '0.15'; bg_rep1 = 'repeat';
bg_d2 = 'block'; bg_img2 = snow_img_2; bg_sz2 = '300px 300px'; bg_anim2 = 'snow-fall-2 4s linear infinite'; bg_op2 = '0.1';
icon_anim = 'compressor-rumble 0.1s linear infinite';
anim_frost = 'pulse-frost 1.5s ease-in-out infinite';
frost_shadow = 'inset 0 0 25px 5px rgba(0, 188, 212, 0.6)';
} else if (power > thresh_cool) {
status_text = 'Cooling';
color = '129, 212, 250';
card_shadow = 'inset 0 0 40px rgba(129, 212, 250, 0.1)';
bg_d1 = 'block'; bg_img1 = snow_img_1; bg_sz1 = '300px 400px'; bg_anim1 = 'snow-fall-1 16s linear infinite'; bg_op1 = '0.08'; bg_rep1 = 'repeat';
bg_d2 = 'block'; bg_img2 = snow_img_2; bg_sz2 = '300px 300px'; bg_anim2 = 'snow-fall-2 8s linear infinite'; bg_op2 = '0.05';
icon_anim = 'compressor-rumble 0.3s linear infinite';
anim_frost = 'pulse-frost 3s ease-in-out infinite';
frost_shadow = 'inset 0 0 15px 2px rgba(129, 212, 250, 0.5)';
}
}
if (door_open) {
color = '244, 67, 54';
card_shadow = 'inset 0 0 50px rgba(244, 67, 54, 0.15)';
icon_anim = 'shake-alert 0.5s ease-in-out infinite';
frost_shadow = 'none';
anim_frost = 'none';
anim_drip = 'none';
overlay_img = 'none';
bg_d1 = 'none';
bg_d2 = 'none';
}
let badge1 = door_open ? '⚠️ DOOR OPEN' : `${status_text}${power_text}`;
let b1_c = door_open ? '244, 67, 54' : color;
let b1_bg = `rgba(${b1_c}, 0.15)`;
let b1_border = `1px solid rgba(${b1_c}, 0.3)`;
let b1_bl = `2px solid rgb(${b1_c})`;
let corner_color = color;
let corner_display = 'none';
if (ent_door && door_state && !['unknown', 'unavailable', ''].includes(door_state)) {
corner_display = 'block';
if (door_open) corner_color = '244, 67, 54';
}
let badge2 = '';
let b2_bg = 'transparent'; let b2_bl = 'none'; let b2_br = 'none'; let b2_border = 'none';
let c_cool = '33, 150, 243';
let c_alert = '244, 67, 54';
if (t_f || t_z) {
let f_str = alert_f ? `⚠️ ${t_f}` : t_f;
let z_str = alert_z ? `⚠️ ${t_z}` : t_z;
badge2 = [f_str, z_str].filter(Boolean).join(' | ');
let c_f = alert_f ? c_alert : c_cool;
let c_z = alert_z ? c_alert : c_cool;
b2_border = `1px solid rgba(128,128,128, 0.2)`;
if (t_f && t_z) {
b2_bg = `linear-gradient(90deg, rgba(${c_f}, 0.15) 0%, rgba(${c_z}, 0.15) 100%)`;
b2_bl = `2px solid rgb(${c_f})`;
b2_br = `2px solid rgb(${c_z})`;
} else if (t_f) {
b2_bg = `rgba(${c_f}, 0.15)`;
b2_bl = `2px solid rgb(${c_f})`;
b2_br = b2_border;
} else if (t_z) {
b2_bg = `rgba(${c_z}, 0.15)`;
b2_bl = b2_border;
b2_br = `2px solid rgb(${c_z})`;
}
}
let bar_disp = show_bar === 'on' ? 'block' : 'none';
let progress = Math.min(100, Math.max(0, Math.floor((power / max_power) * 100)));
return `
#card {
--appliance-color: ${color};
--appliance-level: ${progress}%;
--appliance-bg-d1: ${bg_d1}; --appliance-bg-img1: ${bg_img1}; --appliance-bg-sz1: ${bg_sz1}; --appliance-bg-anim1: ${bg_anim1}; --appliance-bg-op1: ${bg_op1}; --appliance-bg-rep1: ${bg_rep1};
--appliance-bg-d2: ${bg_d2}; --appliance-bg-img2: ${bg_img2}; --appliance-bg-sz2: ${bg_sz2}; --appliance-bg-anim2: ${bg_anim2}; --appliance-bg-op2: ${bg_op2};
--appliance-icon-anim: ${icon_anim};
--appliance-anim-frost: ${anim_frost};
--appliance-anim-drip: ${anim_drip};
--appliance-frost-shadow: ${frost_shadow};
--appliance-overlay-bg: ${overlay_img};
--door-corner-color: rgb(${corner_color});
--door-corner-display: ${corner_display};
--bar-display: ${bar_disp};
box-shadow: ${card_shadow} !important;
}
#card::after {
content: '';
display: var(--door-corner-display);
position: absolute;
top: -0.5px;
left: -0.5px;
opacity: 0.7;
width: 15px;
height: 15px;
border-top: 5px solid var(--door-corner-color);
border-left: 5px solid var(--door-corner-color);
border-top-left-radius: var(--ha-card-border-radius, 12px);
pointer-events: none;
transition: border-color 0.5s ease;
z-index: 1;
}
#badge1 {
background: ${b1_bg};
color: rgb(${b1_c});
border-top: ${b1_border};
border-bottom: ${b1_border};
border-right: ${b1_border};
border-left: ${b1_bl};
border-radius: 6px !important;
}
#badge1::before { content: "${badge1}"; }
#badge2 {
display: ${badge2 ? 'block' : 'none'};
background: ${b2_bg};
color: var(--primary-text-color, #fff);
border-top: ${b2_border};
border-bottom: ${b2_border};
border-left: ${b2_bl};
border-right: ${b2_br};
border-radius: 6px !important;
}
#badge2::before { content: "${badge2}"; }
#img-cell::before {
content: ''; position: absolute; inset: 0; box-shadow: var(--appliance-frost-shadow);
animation: var(--appliance-anim-frost) !important; z-index: 1; border-radius: 50%; pointer-events: none;
}
#img-cell::after {
content: ''; position: absolute; inset: 0; background-image: var(--appliance-overlay-bg);
background-repeat: no-repeat; animation: var(--appliance-anim-drip) !important; z-index: 1;
border-radius: 50%; pointer-events: none;
}
@keyframes snow-fall-1 { from { background-position: 0 0; } to { background-position: 0 400px; } }
@keyframes snow-fall-2 { from { background-position: 0 0; } to { background-position: 0 300px; } }
@keyframes accumulate-drip-card {
0% { background-position: 15% 10%, 45% 20%, 75% 15%, 85% 30%; background-size: 0px 0px, 0px 0px, 0px 0px, 0px 0px; opacity: 0; }
40% { background-position: 15% 30%, 45% 40%, 75% 35%, 85% 50%; background-size: 15px 25px, 20px 30px, 12px 20px, 25px 35px; opacity: 0.4; }
80% { background-position: 15% 80%, 45% 85%, 75% 75%, 85% 90%; background-size: 15px 35px, 20px 40px, 12px 30px, 25px 45px; opacity: 0.2; }
100% { background-position: 15% 100%, 45% 105%, 75% 95%, 85% 110%; background-size: 10px 40px, 15px 45px, 8px 35px, 20px 50px; opacity: 0; }
}
@keyframes pulse-frost { 0%, 100% { opacity: 0.4; transform: scale(0.95); } 50% { opacity: 0.8; transform: scale(1); } }
@keyframes accumulate-drip-icon {
0% { background-position: 30% 10%, 70% 20%; background-size: 0px 0px, 0px 0px; opacity: 0; }
40% { background-position: 30% 30%, 70% 40%; background-size: 8px 12px, 12px 16px; opacity: 1; }
80% { background-position: 30% 80%, 70% 90%; background-size: 8px 18px, 12px 22px; opacity: 0.8; }
100% { background-position: 30% 100%, 70% 110%; background-size: 6px 20px, 10px 25px; opacity: 0; }
}
@keyframes compressor-rumble {
0% { transform: translate(0, 0); } 25% { transform: translate(-1px, 0.5px); } 50% { transform: translate(1px, -0.5px); } 75% { transform: translate(-0.5px, 0.5px); } 100% { transform: translate(0, 0); }
}
@keyframes shake-alert {
0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(5deg) translateY(-1px); } 75% { transform: rotate(-5deg) translateY(1px); }
}
`;
]]]