From 36fd8642dc74e9935f8863c8ccacf664976c4914 Mon Sep 17 00:00:00 2001 From: AnasHost <41167157+Anashost@users.noreply.github.com> Date: Fri, 1 May 2026 16:08:14 +0200 Subject: [PATCH] Add files via upload --- appliances_v2.md | 3396 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3396 insertions(+) create mode 100644 appliances_v2.md diff --git a/appliances_v2.md b/appliances_v2.md new file mode 100644 index 0000000..2275259 --- /dev/null +++ b/appliances_v2.md @@ -0,0 +1,3396 @@ + +[![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/6NM60DEdScA) 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.5 +> ``` + +
+ +# 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