mirror of
https://github.com/Anashost/HA-Animated-cards.git
synced 2026-05-30 14:49:42 +00:00
3397 lines
127 KiB
Markdown
3397 lines
127 KiB
Markdown
<!-- anashost_support_badges_start -->
|
|
[![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]
|
|
<!-- anashost_support_badges_end -->
|
|
<!--
|
|
```diff
|
|
- text in red
|
|
+ text in green
|
|
! text in orange
|
|
# text in gray
|
|
@@ text in purple (and bold)@@
|
|
```
|
|
-->
|
|
|
|
<div align="left">
|
|
<a href="https://github.com/Anashost/HA-Animated-cards">
|
|
<img src="https://img.shields.io/badge/◀_BACK_TO_HOME_Page-2196F3?style=for-the-badge&logoColor=white" height="60" />
|
|
</a>
|
|
</div>
|
|
|
|
# Home Assistant Animated Appliances Cards V2
|
|
|
|
>* Dishwasher
|
|
>
|
|
>* Washing Machine
|
|
>
|
|
>* Dryer
|
|
>
|
|
>* Combo Washer & Dryer
|
|
>
|
|
>* Fridge
|
|
|
|
This [YouTube Video](https://youtu.be/3Njo1-jht5w) explains how to do it.
|
|
|
|
<img width="1280" height="720" alt="40-high" src="https://github.com/user-attachments/assets/266e4098-7268-4c91-9036-ce5a375aad8a" />
|
|
|
|
|
|
`Loading image... please wait`
|
|
|
|
<hr>
|
|
|
|
> [!NOTE]
|
|
> If you are using the **Sections** view type, you may need to set `rows` to around `1.5` for the card,
|
|
> otherwise the card may appear compressed.
|
|
> (USE THIS ONLY IF YOU HAVE ISSUES).
|
|
>
|
|
> ```yaml
|
|
> grid_options:
|
|
> rows: 1.6
|
|
> ```
|
|
|
|
<hr>
|
|
|
|
# Cards (Smart)
|
|
|
|
<details>
|
|
<summary><strong>1 - Smart Dishwasher</summary>
|
|
|
|
```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); } }
|
|
`;
|
|
]]]
|
|
|
|
```
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>2 - Smart Washing Machine</summary>
|
|
|
|
```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); }
|
|
}
|
|
`;
|
|
]]]
|
|
|
|
```
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>3 - Smart Dryer</summary>
|
|
|
|
```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); } }
|
|
`;
|
|
]]]
|
|
|
|
```
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>4 - Smart Combo Washing machine & Dryer</summary>
|
|
|
|
```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); } }
|
|
`;
|
|
]]]
|
|
|
|
```
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>5 - Smart Fridge</summary>
|
|
|
|
```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); }
|
|
}
|
|
`;
|
|
]]]
|
|
|
|
```
|
|
</details>
|
|
|
|
---
|
|
|
|
# Cards (Dumb)
|
|
|
|
<details>
|
|
<summary><strong>1 - Dumb Dishwasher (smart plug)</summary>
|
|
|
|
```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); } }
|
|
`;
|
|
]]]
|
|
|
|
```
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>Dumb Dishwasher (Helper/Template)</summary>
|
|
|
|
```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
|
|
```
|
|
</details>
|
|
|
|
---
|
|
|
|
<details>
|
|
<summary><strong>2 - Dumb Washing Machine (smart plug)</summary>
|
|
|
|
```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); }
|
|
}
|
|
`;
|
|
]]]
|
|
|
|
```
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>Dumb Washing Machine (Helper/Template)</summary>
|
|
|
|
```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
|
|
```
|
|
</details>
|
|
|
|
---
|
|
|
|
<details>
|
|
<summary><strong>3 - Dumb Dryer (smart plug)</summary>
|
|
|
|
```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); } }
|
|
`;
|
|
]]]
|
|
|
|
```
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>Dumb Dryer (Helper/Template)</summary>
|
|
|
|
```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
|
|
```
|
|
</details>
|
|
|
|
---
|
|
|
|
<details>
|
|
<summary><strong>4 - Dumb Combo Washing machine & Dryer (smart plug)</summary>
|
|
|
|
```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); } }
|
|
`;
|
|
]]]
|
|
|
|
```
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>Dumb Combo Washing machine & Dryer (Helper/Template)</summary>
|
|
|
|
```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"
|
|
```
|
|
</details>
|
|
|
|
---
|
|
|
|
<details>
|
|
<summary><strong>5 - Fridge (smart plug)</summary>
|
|
|
|
```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); }
|
|
}
|
|
`;
|
|
]]]
|
|
|
|
```
|
|
</details>
|
|
|
|
---
|
|
|
|
|
|
[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
|