Je viens de publier un nouvel article sur notre portail. ![]()
Venez le découvrir dès maintenant !
Merci pour vos commentaires et retours. ![]()
Bonne lecture !
Je viens de publier un nouvel article sur notre portail. ![]()
Venez le découvrir dès maintenant !
Merci pour vos commentaires et retours. ![]()
Bonne lecture !
Pour récupérer les tokens et autre bindkey il est également possible d'utiliser cette intégration. A la base c'est fait pour gérer diverses passerelles Xiaomi (Zigbee et BT) en local, mais si on branche l'intégration sur Mi Home on obtient en retour les infos de chaque objets connectés dans les deux régions, la même chose que Tokens Extractor en GUI.
Salut,
Oui aussi, il y a plusieurs façons, en plus j'utilise cette intégration
. Mais j'ai trouvé que c'était plus simple un exe à lancer sur un PC.
Je l'utilisais, mais pas d'oscillation verticale et le P70, n'est pas complètement supporté. Puis plus maintenu. Alors, je suis parti sur une nouvelle carte maison., je m'en sers pour mon P18 aussi.
Salut Warc0zes,
Je trouve ta carte très attrayante. Pourrais tu nous partager le code s'il te plait ?
Salut,
le code est dans l'article, comme il y a des cartes grid, il faut juste supprimer des boutons, pour avoir le visuel au-dessus.
Mais voici le code.
button_card_templates:
p70_small_compact:
show_icon: true
show_name: true
show_state: false
show_label: true
tap_action:
haptic: light
styles:
card:
- min-height: 48px
- padding: 5px 2px
- border-radius: 11px
- box-shadow: none
- border: 1px solid rgba(127, 127, 127, 0.16)
- background: rgba(127, 127, 127, 0.06)
grid:
- grid-template-areas: "\"i n\" \"i l\""
- grid-template-columns: 25px 1fr
- grid-template-rows: min-content min-content
- column-gap: 3px
icon:
- width: 24px
- color: var(--secondary-text-color)
name:
- justify-self: start
- font-size: 14px
- line-height: 15px
- font-weight: 650
- color: var(--primary-text-color)
label:
- justify-self: start
- margin-top: 6px
- font-size: 12px
- line-height: 13px
- color: var(--secondary-text-color)
p70_toggle_compact:
template: p70_small_compact
variables:
active: false
service_on: null
service_off: null
tap_action:
action: call-service
service: >
[[[ return variables.active ? variables.service_off :
variables.service_on; ]]]
target:
entity_id: "[[[ return entity.entity_id; ]]]"
haptic: light
state:
- operator: template
value: "[[[ return Boolean(variables.active); ]]]"
styles:
card:
- background: rgba(68, 115, 158, 0.15)
- border: 1px solid '#01be9e'
icon:
- color: "#44739E"
name:
- color: "#44739E"
label:
- color: "#44739E"
p70_move_compact:
show_icon: true
show_name: false
show_state: false
variables:
direction: left
tap_action:
action: call-service
service: xiaomi_miio_fan.fan_turn
target:
entity_id: "[[[ return entity.entity_id; ]]]"
data:
direction: "[[[ return variables.direction; ]]]"
haptic: light
hold_action:
action: call-service
service: xiaomi_miio_fan.fan_turn
target:
entity_id: "[[[ return entity.entity_id; ]]]"
data:
direction: "[[[ return variables.direction; ]]]"
repeat: 350
haptic: selection
styles:
card:
- min-height: 34px
- padding: 0
- border-radius: 10px
- box-shadow: none
- border: 1px solid rgba(127, 127, 127, 0.16)
- background: rgba(127, 127, 127, 0.06)
icon:
- width: 24px
- color: "#44739E"
type: custom:button-card
entity: fan.mi_smart_standing_fan_2
name: Mi Smart Standing Fan 2
show_icon: false
show_state: false
show_label: false
show_ripple: false
disable_kbd: true
tap_action:
action: none
hold_action:
action: more-info
double_tap_action:
action: none
extra_styles: |
@keyframes p70-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes p70-swing-h {
0%, 100% { transform: perspective(400px) rotateY(-25deg); }
50% { transform: perspective(400px) rotateY(25deg); }
}
@keyframes p70-swing-v {
0%, 100% { transform: translateY(-5px); }
50% { transform: translateY(5px); }
}
.p70-status {
display: flex;
align-items: center;
gap: 5px;
font-size: 14px;
line-height: 12px;
font-weight: 500;
color: #44739E;
}
.p70-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--disabled-text-color);
}
.p70-dot.on {
background: #01be9e;
box-shadow: 0 0 7px rgba(1, 190, 158, .85);
}
.p70-visual {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 142px;
}
.p70-h.active {
animation: p70-swing-h 4s ease-in-out infinite;
transform-origin: 50% 88%;
}
.p70-v.active {
animation: p70-swing-v 3.2s ease-in-out infinite;
}
.p70-product-stage {
position: relative;
width: 126px;
height: 126px;
display: grid;
place-items: center;
isolation: isolate;
}
.p70-product-stage::before {
content: '';
position: absolute;
z-index: 0;
width: 78px;
height: 78px;
left: 24px;
top: 15px;
border-radius: 50%;
background: radial-gradient(circle, rgba(127,127,127,.15) 0 46%, transparent 72%);
opacity: .42;
transition: opacity .25s ease, box-shadow .25s ease;
}
.p70-product-stage.on::before {
opacity: 1;
box-shadow: 0 0 24px rgba(1,190,158,.22);
background: radial-gradient(circle, rgba(1,190,158,.22) 0 34%, rgba(1,190,158,.08) 52%, transparent 73%);
}
.p70-rotor {
position: absolute;
z-index: 1;
width: 112px;
height: 112px;
left: 7px;
top: 4.5px;
color: rgba(105, 115, 122, .64);
opacity: .58;
filter: drop-shadow(0 0 3px rgba(0,0,0,.12));
}
.p70-product-stage.on .p70-rotor {
color: white;
opacity: .42;
filter: drop-shadow(0 0 5px rgba(1,190,158,.38));
}
.p70-rotor.spinning {
animation: p70-spin var(--p70-duration) linear infinite;
transform-origin: 50% 50%;
}
.p70-product {
position: absolute;
z-index: 2;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none;
user-select: none;
opacity: .72;
filter: grayscale(.18) drop-shadow(0 7px 9px rgba(0,0,0,.15));
transition: opacity .25s ease, filter .25s ease;
}
.p70-product-stage.on .p70-product {
opacity: 1;
filter: drop-shadow(0 7px 10px rgba(0,0,0,.17)) drop-shadow(0 0 6px rgba(1,190,158,.11));
}
.p70-percent {
position: absolute;
z-index: 3;
right: -15px;
bottom: 15px;
min-width: 29px;
height: 18px;
padding: 0 5px;
box-sizing: border-box;
border-radius: 999px;
display: grid;
place-items: center;
background: color-mix(in srgb, var(--card-background-color) 88%, transparent);
border: 1px solid rgba(127,127,127,.25);
box-shadow: 0 2px 8px rgba(0,0,0,.10);
backdrop-filter: blur(4px);
font-size: 12px;
color: var(--secondary-text-color);
}
.p70-product-stage.on .p70-percent {
border-color: #44739E;
color: #44739E;
}
.p70-meta {
margin-top: -1px;
display: flex;
gap: 3px;
flex-wrap: nowrap;
justify-content: center;
}
.p70-chip {
padding: 2px 5px;
border-radius: 999px;
font-size: 12px;
line-height: 10px;
color: #44739E;
background: rgba(127,127,127,.08);
border: 1px solid rgba(127,127,127,.12);
}
/* Curseur 1–100 % intégré : aucun module JS externe */
.p70-speed-native {
--p70-accent: #44739E;
box-sizing: border-box;
width: 100%;
padding: 4px 5px;
border-radius: 12px;
border: 1px solid rgba(127, 127, 127, 0.16);
background: rgba(127, 127, 127, 0.07);
box-shadow: none;
-webkit-tap-highlight-color: transparent;
}
.p70-speed-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 0;
}
.p70-speed-caption {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
min-height: 18px;
color: var(--secondary-text-color);
font-size: 15px;
line-height: 18px;
font-weight: 600;
}
.p70-speed-caption ha-icon {
display: block;
width: 24px;
height: 24px;
flex: 0 0 24px;
color: var(--secondary-text-color);
transform: translateY(0);
}
.p70-speed-caption span {
display: flex;
align-items: center;
min-height: 18px;
line-height: 18px;
white-space: nowrap;
}
.p70-speed-value {
flex: 0 0 40px;
min-width: 40px;
padding: 2px;
border-radius: 999px;
box-sizing: border-box;
text-align: center;
color: var(--secondary-text-color);
background: rgba(127, 127, 127, 0.10);
border: 1px solid rgba(127, 127, 127, 0.18);
font-size: 13px;
line-height: 16px;
font-weight: 700;
}
.p70-speed-native.on .p70-speed-value,
.p70-speed-native.on .p70-speed-caption ha-icon {
color: var(--p70-accent);
}
.p70-speed-native.on .p70-speed-value {
border-color: var(--p70-accent);
background: color-mix(in srgb, var(--p70-accent) 12%, transparent);
}
.p70-speed-row {
display: grid;
grid-template-columns: min-content 1fr min-content;
align-items: center;
gap: 8px;
}
.p70-speed-bound {
color: var(--secondary-text-color);
font-size: 12px;
line-height: 12px;
font-weight: 600;
opacity: .82;
}
.p70-speed-range {
--fill: 0%;
width: 100%;
height: 22px;
margin: 0;
padding: 0;
border: 0;
outline: none;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
background: transparent;
touch-action: pan-y;
}
.p70-speed-range::-webkit-slider-runnable-track {
height: 5px;
border-radius: 999px;
background: linear-gradient(
to right,
var(--p70-accent) 0%,
var(--p70-accent) var(--fill),
rgba(127, 127, 127, 0.20) var(--fill),
rgba(127, 127, 127, 0.20) 100%
);
}
.p70-speed-range::-webkit-slider-thumb {
width: 14px;
height: 14px;
margin-top: -4.5px;
border: 2px solid var(--card-background-color, #fff);
border-radius: 50%;
appearance: none;
-webkit-appearance: none;
background: var(--p70-accent);
box-shadow: 0 1px 5px rgba(0, 0, 0, .24);
}
.p70-speed-range::-moz-range-track {
height: 5px;
border-radius: 999px;
background: rgba(127, 127, 127, 0.20);
}
.p70-speed-range::-moz-range-progress {
height: 5px;
border-radius: 999px;
background: var(--p70-accent);
}
.p70-speed-range::-moz-range-thumb {
width: 12px;
height: 12px;
border: 2px solid var(--card-background-color, #fff);
border-radius: 50%;
background: var(--p70-accent);
box-shadow: 0 1px 5px rgba(0, 0, 0, .24);
}
.p70-speed-range:disabled {
cursor: not-allowed;
opacity: .5;
}
styles:
card:
- padding: 8px 8px 2px 8px
- border-radius: 20px
- overflow: hidden
grid:
- grid-template-areas: >-
"n power" "status power" "visual visual" "speeds speeds" "directions
directions" "modes modes" "cycle cycle"
- grid-template-columns: 1fr 38px
- grid-template-rows: >-
min-content min-content 142px min-content min-content min-content
min-content
- column-gap: 6px
- row-gap: 6px
name:
- justify-self: start
- align-self: end
- font-size: 18px
- line-height: 17px
- font-weight: 700
custom_fields:
power:
- align-self: center
- justify-self: end
status:
- justify-self: start
- align-self: start
visual:
- width: 100%
speeds:
- width: 100%
modes:
- width: 100%
- "--grid-card-gap": 6px
directions:
- width: 100%
- "--grid-card-gap": 4px
custom_fields:
power:
card:
type: custom:button-card
entity: fan.mi_smart_standing_fan_2
icon: mdi:power
show_name: false
show_state: false
tap_action:
action: toggle
haptic: medium
styles:
card:
- width: 36px
- height: 36px
- border-radius: 50%
- padding: 0
- box-shadow: none
- border: 1px solid rgba(127, 127, 127, 0.18)
- background: rgba(127, 127, 127, 0.07)
icon:
- width: 19px
- color: var(--secondary-text-color)
state:
- value: "on"
styles:
card:
- background: rgba(68, 115, 158, 0.16)
- border: 1px solid
icon:
- color: "#44739E"
status: |
[[[
const on = entity.state === 'on';
const pct = Number(entity.attributes.percentage ?? entity.attributes.raw_speed ?? 0);
const mode = entity.attributes.mode === 'nature' ? 'Naturel' : 'Direct';
return `<div class="p70-status">
<span class="p70-dot ${on ? 'on' : ''}"></span>
<span>${on ? `${pct}% · ${mode}` : 'Éteint'}</span>
</div>`;
]]]
visual: |
[[[
const on = entity.state === 'on';
const pct = Number(entity.attributes.percentage ?? entity.attributes.raw_speed ?? 0);
const horizontal = Boolean(entity.attributes.oscillating ?? entity.attributes.oscillate ?? false);
const vertical = Boolean(entity.attributes.vertical_oscillate ?? false);
const angleH = entity.attributes.angle ?? '—';
const angleV = entity.attributes.vertical_angle ?? '—';
const preset = entity.attributes.preset_mode ?? '—';
const duration = Math.max(0.55, 4.6 - pct * 0.037).toFixed(2);
const imageUrl = '/local/images/fan_p18.png';
return `<div class="p70-visual">
<div class="p70-h ${on && horizontal ? 'active' : ''}">
<div class="p70-v ${on && vertical ? 'active' : ''}">
<div class="p70-product-stage ${on ? 'on' : ''}">
<ha-icon class="p70-rotor ${on ? 'spinning' : ''}" style="--p70-duration:${duration}s" icon="mdi:fan"></ha-icon>
<img class="p70-product" src="${imageUrl}" alt="Mi Smart Standing Fan 2" loading="eager">
<div class="p70-percent">${on ? pct : 0}%</div>
</div>
</div>
</div>
<div class="p70-meta">
<span class="p70-chip">${preset}</span>
<span class="p70-chip">H ${angleH}°</span>
</div>
</div>`;
]]]
speeds: |
[[[
const fan = entity;
if (!fan) {
return `<div class="p70-speed-native">
<div class="p70-speed-header">
<div class="p70-speed-caption">
<ha-icon icon="mdi:fan"></ha-icon>
<span>Vitesse</span>
</div>
<span class="p70-speed-value">Indisponible</span>
</div>
</div>`;
}
const min = 1;
const max = 100;
const raw = Number(fan.attributes.percentage ?? fan.attributes.raw_speed ?? min);
const pct = Math.max(min, Math.min(max, Number.isFinite(raw) ? Math.round(raw) : min));
const fill = ((pct - min) / (max - min)) * 100;
const on = fan.state === 'on';
const unavailable = fan.state === 'unavailable' || fan.state === 'unknown';
const valueText = unavailable ? 'Indisponible' : (on ? `${pct} %` : 'Éteint');
const entityId = 'fan.mi_smart_standing_fan_2';
return `<div class="p70-speed-native ${on ? 'on' : ''}">
<div class="p70-speed-header">
<div class="p70-speed-caption">
<ha-icon icon="mdi:fan"></ha-icon>
<span>Vitesse</span>
</div>
<span class="p70-speed-value">${valueText}</span>
</div>
<div class="p70-speed-row">
<span class="p70-speed-bound">1 %</span>
<input
class="p70-speed-range"
type="range"
min="1"
max="100"
step="1"
value="${pct}"
style="--fill:${fill}%;"
aria-label="Vitesse du ventilateur"
${unavailable ? 'disabled' : ''}
onpointerdown="event.stopPropagation()"
onpointerup="event.stopPropagation()"
ontouchstart="event.stopPropagation()"
ontouchend="event.stopPropagation()"
onclick="event.stopPropagation()"
ondblclick="event.stopPropagation()"
onkeydown="event.stopPropagation()"
oninput="event.stopPropagation();const v=Math.max(1,Math.min(100,Math.round(Number(this.value))));this.style.setProperty('--fill',(((v-1)/99)*100)+'%');const box=this.closest('.p70-speed-native');if(box){box.classList.add('on');const value=box.querySelector('.p70-speed-value');if(value)value.textContent=v+' %';}"
onchange="event.stopPropagation();const v=Math.max(1,Math.min(100,Math.round(Number(this.value))));const root=this.getRootNode();const card=root&&root.host;const app=document.querySelector('home-assistant');const ha=(card&&card.hass)||(app&&app.hass);if(ha){ha.callService('fan','set_percentage',{entity_id:'${entityId}',percentage:v});}"
>
<span class="p70-speed-bound">100 %</span>
</div>
</div>`;
]]]
directions:
card:
type: grid
columns: 2
square: false
cards:
- type: custom:button-card
template: p70_move_compact
entity: fan.mi_smart_standing_fan_2
icon: mdi:chevron-left
show_icon: true
show_name: false
show_state: false
tap_action:
action: call-service
service: fan.set_direction
target:
entity_id: "[[[ return entity.entity_id; ]]]"
data:
direction: left
haptic: light
hold_action:
action: call-service
service: fan.set_direction
target:
entity_id: "[[[ return entity.entity_id; ]]]"
data:
direction: left
repeat: 350
haptic: selection
styles:
card:
- min-height: 34px
- padding: 0
- border-radius: 10px
- box-shadow: none
- border: 1px solid rgba(127, 127, 127, 0.16)
- background: rgba(127, 127, 127, 0.06)
icon:
- width: 24px
- color: "#44739E"
- type: custom:button-card
template: p70_move_compact
entity: fan.mi_smart_standing_fan_2
icon: mdi:chevron-right
show_icon: true
show_name: false
show_state: false
tap_action:
action: call-service
service: fan.set_direction
target:
entity_id: "[[[ return entity.entity_id; ]]]"
data:
direction: right
haptic: light
hold_action:
action: call-service
service: fan.set_direction
target:
entity_id: "[[[ return entity.entity_id; ]]]"
data:
direction: right
repeat: 350
haptic: selection
styles:
card:
- min-height: 34px
- padding: 0
- border-radius: 10px
- box-shadow: none
- border: 1px solid rgba(127, 127, 127, 0.16)
- background: rgba(127, 127, 127, 0.06)
icon:
- width: 24px
- color: "#44739E"
modes:
card:
type: grid
columns: 3
square: false
cards:
- type: custom:button-card
template: p70_toggle_compact
entity: fan.mi_smart_standing_fan_2
variables:
active: "[[[ return entity.attributes.mode === \"nature\"; ]]]"
service_on: xiaomi_miio_fan.fan_set_natural_mode_on
service_off: xiaomi_miio_fan.fan_set_natural_mode_off
icon: mdi:leaf
name: Nature
label: >-
[[[ return entity.attributes.mode === "nature" ? "Activé" :
"Désactivé"; ]]]
- type: custom:button-card
template: p70_small_compact
entity: fan.mi_smart_standing_fan_2
icon: mdi:swap-horizontal-bold
name: Horizontal
label: >
[[[ return Boolean(entity.attributes.oscillating ??
entity.attributes.oscillate ?? false) ? 'Activée' : 'Arrêtée'; ]]]
tap_action:
action: call-service
service: fan.oscillate
target:
entity_id: fan.mi_smart_standing_fan_2
data:
oscillating: >
[[[ return !Boolean(entity.attributes.oscillating ??
entity.attributes.oscillate ?? false); ]]]
haptic: light
state:
- operator: template
value: >-
[[[ return Boolean(entity.attributes.oscillating ??
entity.attributes.oscillate ?? false); ]]]
styles:
card:
- background: rgba(68, 115, 158, 0.15)
icon:
- color: "#44739E"
name:
- color: "#44739E"
label:
- color: "#44739E"
- type: custom:button-card
template: p70_small_compact
entity: fan.mi_smart_standing_fan_2
icon: mdi:angle-acute
name: Angle H
label: "[[[ return `${entity.attributes.angle ?? \"—\"}°`; ]]]"
tap_action:
action: call-service
service: xiaomi_miio_fan.fan_set_oscillation_angle
target:
entity_id: fan.mi_smart_standing_fan_2
data:
angle: |
[[[
const values = [30, 60, 90, 120, 140];
const current = Number(entity.attributes.angle ?? 0);
return values.find(v => v > current) ?? values[0];
]]]
haptic: light
- type: custom:button-card
template: p70_small_compact
entity: fan.mi_smart_standing_fan_2
icon: mdi:led-on
name: Voyant
label: "[[[ return entity.attributes.led ? \"Allumé\" : \"Éteint\"; ]]]"
tap_action:
action: call-service
service: xiaomi_miio_fan.fan_set_led_brightness
target:
entity_id: fan.mi_smart_standing_fan_2
data:
brightness: "[[[ return entity.attributes.led ? 2 : 0; ]]]"
haptic: light
state:
- operator: template
value: "[[[ return Boolean(entity.attributes.led); ]]]"
styles:
card:
- background: rgba(68, 115, 158, 0.15)
icon:
- color: "#44739E"
name:
- color: "#44739E"
label:
- color: "#44739E"
- type: custom:button-card
template: p70_toggle_compact
entity: fan.mi_smart_standing_fan_2
variables:
active: "[[[ return Boolean(entity.attributes.buzzer); ]]]"
service_on: xiaomi_miio_fan.fan_set_buzzer_on
service_off: xiaomi_miio_fan.fan_set_buzzer_off
icon: mdi:volume-high
name: Bip
label: "[[[ return entity.attributes.buzzer ? \"Activé\" : \"Muet\"; ]]]"
- type: custom:button-card
template: p70_small_compact
entity: fan.mi_smart_standing_fan_2
icon: mdi:timer-outline
name: Minuterie
label: |
[[[
const minutes = Number(entity.attributes.delay_off_countdown ?? 0);
return minutes > 0 ? `${minutes} min` : 'Arrêtée';
]]]
tap_action:
action: call-service
service: xiaomi_miio_fan.fan_set_delay_off
target:
entity_id: fan.mi_smart_standing_fan_2
data:
delay_off_countdown: |
[[[
const values = [0, 60, 120, 240, 480];
const current = Number(entity.attributes.delay_off_countdown ?? 0);
return values.find(v => v > current) ?? values[0];
]]]
haptic: light
l'image :

Merci tout bien pour le code. Après, il est vrai que j'ai survolé l'article...![]()