Was doing custom card for Haier/Candy washing machine and since there wasnt any decided to share it if someone would need it.
All is based on Haier hOn integration made/forked by gvigroux which is aviable in HACS/github
Card looks like that:
(red text on image are only descriptions for purpose of explaining sections - they aren’t in actual card)
Main steps are:
- creating additional sensor
- creating card
1. Creating sensor
To card work properly, in first step need to create sensor that will be needed to progress bar work properly, and will calculate percentage remaining time instead only showing remaining time in minutes, which progress bar wont be able to understand.
Sensor will be using provided by hOn integration sensors such as:
sensor.[DeviceName]_remaining_time
sensor.[DeviceName]_start_time
sensor.[DeviceName]_remaining_time
Before creating sensor, I recommend installing Studio Code Server which is file editor and its aviable in Home Assistant Settings>Addodns>Shop in bottom right corner. I will expail why later.
Code for sensor is:
- sensor:
- name: "[SensorName]"
unique_id: [DeviceUnigue_ID]
unit_of_measurement: "%"
state: >
{% if is_state('sensor.[DeviceName]_remaining_time', 'unavailable') or
is_state('sensor.[DeviceName]_remaining_time', 'unknown') or
is_state('sensor.[DeviceName]_start_time', 'unavailable') or
is_state('sensor.[DeviceName]_start_time', 'unknown') %}
0
{% else %}
{% set start_time = as_datetime(states('sensor.[DeviceName]_start_time')) %}
{% set end_time = as_datetime(states('sensor.[DeviceName]_end_time')) %}
{% set now = now() %}
{% set total_duration = (end_time - start_time).total_seconds() %}
{% set elapsed_time = (now - start_time).total_seconds() %}
{% set progress = elapsed_time / total_duration * 100 %}
{{ [0, [100, progress]|min]|max|round(1) }}
{% endif %}
icon: mdi:washing-machine
availability: >
{{ not is_state('sensor.[DeviceName]_remaining_time', 'unavailable') and
not is_state('sensor.[DeviceName]_start_time', 'unavailable') and
not is_state('sensor.[DeviceName]_end_time', 'unavailable') }}
To create sensor use Studio Code Server and edit templates.yaml file.
If you dont have that file just create it in primary directory (in te same directory as configuration.yaml)
During creating sensor, you need to change few things.
First is that need to generate Unique_Id for that sensor. Delate [DeviceUnigue_ID] and later right click and from context menu choose “Generate UUID at cursor”. Generating UUID is way easier/faster in Studio Code Server just because it have that option in context menu so you dont need to generate it, lets say manually. So convenience is kinda only reason why ealier i recommended instaling Studio Code Server.
Second is that need to change all [DeviceName] from code to your device name in your home assistant.
Last is to rename sensor name from [SensorName] to your desired name, for example WMpercentgesensor or whatever name you want, but keep in mind that name of that sensor need to be in quotes “”
To save changes made in templetes.yaml, press ctrl+s. Its Studio Code Server key shourtcut for Save edited files.
2. Creating Card
Just simply copy/paste code to your dashboard.
Here is code for card:
type: custom:button-card
entity: sensor.[DeviceName]_mode
name: [DeviceName]
show_icon: true
icon: mdi:washing-machine
show_label: true
state:
- value: "0"
label: Turned Off
styles:
grid:
- grid-template-areas: "\"i n\" \"program program\" \"phase phase\" \"l l\" \"bar bar\""
- grid-template-columns: auto 1fr
name:
- justify-self: start
- align-self: start
- font-size: 26px
- color: var(--black)
- padding-left: "-8px"
- padding-top: 14px
- opacity: "0.7"
label:
- justify-self: start
- font-size: 40px
- font-weight: 400
- color: var(--black)
- padding: 10px 0px 20px 20px
custom_fields:
program:
- display: none
bar:
- display: none
phase:
- display: none
time_container:
- display: none
- value: "7"
label: Washing End
styles:
grid:
- grid-template-areas: >-
"i n" "program program" "phase phase" "l l" "time_container
time_container" "bar bar"
- grid-template-columns: auto 1fr
name:
- justify-self: start
- align-self: start
- font-size: 26px
- color: var(--black)
- padding-left: "-8px"
- padding-top: 14px
- opacity: "0.7"
label:
- justify-self: start
- font-size: 40px
- font-weight: 400
- color: var(--black)
- padding: 10px 0px 20px 20px
custom_fields:
program:
- display: none
bar:
- display: none
phase:
- display: none
time_container:
- display: none
styles:
card:
- padding: 0
- height: 180px
- border-radius: 20px
- background: null
grid:
- grid-template-areas: >-
"i n" "phase phase" "program program" "l l" "time_container
time_container" "bar bar"
- grid-template-columns: auto 1fr
name:
- justify-self: start
- align-self: start
- font-size: 26px
- color: var(--black)
- padding-left: 0px
- padding-top: 14px
- opacity: "0.7"
label:
- justify-self: start
- font-size: 40px
- font-weight: 400
- color: var(--black)
- padding: 10px 0px 20px 20px
img_cell:
- justify-self: start
- align-self: start
- margin: 10px 0 0 10px
- background: rgba(var(--highlight_active))
- padding: 14px
- border-radius: 50%
- width: 20px
- height: 10px
icon:
- width: 27px
- height: 27px
- color: var(--black)
custom_fields:
program:
- justify-self: start
- font-size: 25px
- font-weight: 300
- color: var(--black)
- padding: 0 5px 28px 24px
phase:
- justify-self: start
- font-size: 20px
- font-weight: 300
- color: var(--black)
- padding: 10px 5px 10px 23px
- opacity: "0.8"
- margin-top: "-5px"
time_container:
- width: 90%
- justify-self: center
- font-size: 14px
- font-weight: 300
- color: var(--black)
- margin: "-15px 0px 5px 0px"
- display: flex
- justify-content: space-between
bar:
- justify-self: center
- width: 90%
- border-radius: 6px
- background: "#D4C9BE"
- height: 12px
- margin: 0px 0px 20px 0px
- position: relative
custom_fields:
program: |
[[[
// Get remaining time from sensor
const remainTimeSensor = states['sensor.[DeviceName]_remaining_time'];
const remainTime = (remainTimeSensor && remainTimeSensor.state !== 'unavailable' && remainTimeSensor.state !== 'unknown') ?
remainTimeSensor.state : '-';
// Get mode from sensor to determine machine status
const modeSensor = states['sensor.[DeviceName]_mode'];
const mode = (modeSensor && modeSensor.state !== 'unavailable' && modeSensor.state !== 'unknown') ?
modeSensor.state : '0';
// Format remaining time from minutes to HH:MM with Polish text
let formattedTime = '-';
if (!isNaN(parseInt(remainTime))) {
const hours = Math.floor(remainTime / 60);
const minutes = remainTime % 60;
formattedTime = (hours > 0 ? hours + ' hour. ' : '') + minutes + ' min.';
}
// Only show time if machine is not in standby
if (entity.state !== 'standby' && mode !== '0') {
return '<div style="text-align: center; font-size: 25px;">End In: ' + formattedTime + '</div>';
} else {
return '';
}
]]]
phase: |
[[[
// Get program phase from sensor
const phaseSensor = states['sensor.[DeviceName]_program_phase'];
const phaseState = (phaseSensor && phaseSensor.state !== 'unavailable' && phaseSensor.state !== 'unknown') ?
phaseSensor.state : '';
// Get mode to determine if we should show the phase
const modeSensor = states['sensor.[DeviceName]_mode'];
const mode = (modeSensor && modeSensor.state !== 'unavailable' && modeSensor.state !== 'unknown') ?
modeSensor.state : '0';
// Map phase state to display text
let phaseText = '';
if (phaseState === '0') {
phaseText = 'Ready';
} else if (phaseState === '2') {
phaseText = 'Washing';
} else if (phaseState === '4') {
phaseText = 'Rinse';
} else if (phaseState === '11') {
phaseText = 'Spin';
} else if (phaseState) {
// For any other states, just display the state number with a generic label
phaseText = 'Phase ' + phaseState;
}
// Only show phase if not in standby and mode is valid
if (entity.state !== 'standby' && mode !== '0' && phaseText) {
return phaseText;
} else {
return '';
}
]]]
time_container: |
[[[
// Get mode to determine if we should show the time information
const modeSensor = states['sensor.[DeviceName]_mode'];
const mode = (modeSensor && modeSensor.state !== 'unavailable' && modeSensor.state !== 'unknown') ?
modeSensor.state : '0';
// Only show time info if not in standby and mode is not 0
if (entity.state === 'standby' || mode === '0' || mode === '7') {
return '';
}
// Get remaining time from sensor
const remainTimeSensor = states['sensor.[DeviceName]_remaining_time'];
const remainTime = (remainTimeSensor && remainTimeSensor.state !== 'unavailable' && remainTimeSensor.state !== 'unknown') ?
remainTimeSensor.state : '0';
// Get progress percentage from sensor
const progressSensor = states['[SensorName]'];
const progress = (progressSensor && progressSensor.state !== 'unavailable' && progressSensor.state !== 'unknown') ?
parseFloat(progressSensor.state) : 0;
// Calculate start time based on current time
const now = new Date();
// Calculate total minutes based on remaining time and progress
const totalMinutes = progress > 0 ? Math.round(remainTime / ((100 - progress) / 100)) : 0;
const elapsedMinutes = totalMinutes - remainTime;
// Calculate start time by subtracting elapsed minutes from current time
const startTime = new Date(now.getTime() - elapsedMinutes * 60000);
const startHours = startTime.getHours().toString().padStart(2, '0');
const startMinutes = startTime.getMinutes().toString().padStart(2, '0');
// Calculate end time by adding remaining minutes to current time
const endTime = new Date(now.getTime() + remainTime * 60000);
const endHours = endTime.getHours().toString().padStart(2, '0');
const endMinutes = endTime.getMinutes().toString().padStart(2, '0');
// Format start and end times
const formattedStartTime = startHours + ':' + startMinutes;
const formattedEndTime = endHours + ':' + endMinutes;
return `<div style="display: flex; justify-content: space-between; width: 100%;">
<div>${formattedStartTime}</div>
<div>${formattedEndTime}</div>
</div>`;
]]]
bar: |
[[[
// Get mode to determine if we should show the bar
const modeSensor = states['sensor.[DeviceName]_mode'];
const mode = (modeSensor && modeSensor.state !== 'unavailable' && modeSensor.state !== 'unknown') ?
modeSensor.state : '0';
// Only show bar if not in standby and mode is not 0
if (entity.state === 'standby' || mode === '0' || mode === '7') {
return '';
}
// Get progress percentage from sensor
const progressSensor = states['[SensorName]'];
const progress = (progressSensor && progressSensor.state !== 'unavailable' && progressSensor.state !== 'unknown') ?
progressSensor.state : '0';
// Return the bar with correct width percentage - without remaining time text
return `<div style="position: relative; height: 12px;">
<div style="background: #A58D84; height: 12px; width: ${progress}%; border-radius: 6px 0 0 6px;"></div>
</div>`;
]]]
To adjust card to your device, you need to change all [DeviceName] from orginal hOn sensors to your device name, for example sensor.YOURNAME_remaining_time.
Just to be clear, i dont mean renaming entities in integration, only renaming in card code.
Next is that need to change [SensorName] in card code to name you decide to name sensor when was creating it in first step.
Another thing is that since hOn integration washing phases states are based on numeric values and integration itself translate them to phase names, there can be errors when displaying phases names because, im guessing here since i tested only on my washing machine and totaly dont sure about it, different models can have different state code/number for particular phases. For example, in my washing machine i counted five defferent state value codes/numbers only for Washing phase.
If you will have that problem, to fix it you need to change in card code phase values to your device code/number.
For example my phases are as below:
// Map phase state to display text
let phaseText = '';
if (phaseState === '0') {
phaseText = 'Ready';
} else if (phaseState === '2') {
phaseText = 'Washing';
} else if (phaseState === '4') {
phaseText = 'Rinse';
} else if (phaseState === '11') {
phaseText = 'Spin';
} else if (phaseState) {
// For any other states, just display the state number with a generic label
phaseText = 'Phase ' + phaseState;
}
In my device phase values are: 0=Ready, 2=Washing, 4=Rinse, 11=Spin
In your device thise values can be different for particular phases.
To check what value your device have for particular phase, just run washing program and go to Home Assistant develoter tools and in states tab filter for phase sensor as on image below and note what your device values are for particular phases. Values will be changing with program phase changes.
Last thing is that you can do cosmetic changes to adjust card to your launguage.
Haier hOn integration made/forked by gvigroux is made in english and dont have translations, so some parts of code are only made to change default english names to my native language.
If you want adjust card to your language, just simply change all english names/labels in code such as Spin, Washing, Rinse, and so on, to names in your language. For purpose of sharing all labels in code are in english, so it will be easier to find them in card code.