[![Revolut.Me][revolut_me_shield]][revolut_me] [![PayPal.Me][paypal_me_shield]][paypal_me] [![ko_fi][ko_fi_shield]][ko_fi_me] [![buymecoffee][buy_me_coffee_shield]][buy_me_coffee_me] [![patreon][patreon_shield]][patreon_me]
# Home Assistant Animated Appliances Cards V2 >* Dishwasher > >* Washing Machine > >* Dryer > >* Combo Washer & Dryer > >* Fridge This [YouTube Video](https://youtu.be/3Njo1-jht5w) explains how to do it. 40-high `Loading image... please wait`
> [!NOTE] > If you are using the **Sections** view type, you may need to set `rows` to around `1.5` for the card, > otherwise the card may appear compressed. > (USE THIS ONLY IF YOU HAVE ISSUES). > > ```yaml > grid_options: > rows: 1.6 > ```
# Cards (Smart)
1 - Smart Dishwasher ```yaml 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 ```yaml 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 ```yaml 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 ```yaml 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 ```yaml 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) ```yaml 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) ```yaml - 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) ```yaml 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) ```yaml - 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) ```yaml 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) ```yaml - 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) ```yaml 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) ```yaml - 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) ```yaml 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); } } `; ]]] ```
--- [paypal_me_shield]: https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white [paypal_me]: https://paypal.me/anasboxsupport [revolut_me_shield]: https://img.shields.io/badge/revolut-FFFFFF?style=for-the-badge&logo=revolut&logoColor=black [revolut_me]: https://revolut.me/anas4e [ko_fi_shield]: https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white [ko_fi_me]: https://ko-fi.com/anasbox [buy_me_coffee_shield]: https://img.shields.io/badge/Buy%20Me%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black [buy_me_coffee_me]: https://www.buymeacoffee.com/anasbox [patreon_shield]: https://img.shields.io/badge/patreon-404040?style=for-the-badge&logo=patreon&logoColor=white [patreon_me]: https://patreon.com/AnasBox